diff --git a/README.md b/README.md index fb6c2e8..74b3965 100644 --- a/README.md +++ b/README.md @@ -129,6 +129,7 @@ instrumentator = Instrumentator( env_var_name="ENABLE_METRICS", inprogress_name="inprogress", inprogress_labels=True, + custom_labels={"service": "example-ms"} ) ``` @@ -168,6 +169,7 @@ instrumentator.add( should_include_status=True, metric_namespace="a", metric_subsystem="b", + custom_labels={"service": "example-ms"} ) ).add( metrics.response_size( @@ -176,6 +178,7 @@ instrumentator.add( should_include_status=True, metric_namespace="namespace", metric_subsystem="subsystem", + custom_labels={"service": "example-ms"} ) ) ``` diff --git a/src/prometheus_fastapi_instrumentator/instrumentation.py b/src/prometheus_fastapi_instrumentator/instrumentation.py index 13cdadc..2443cc5 100644 --- a/src/prometheus_fastapi_instrumentator/instrumentation.py +++ b/src/prometheus_fastapi_instrumentator/instrumentation.py @@ -34,6 +34,7 @@ def __init__( should_respect_env_var: bool = False, should_instrument_requests_inprogress: bool = False, should_exclude_streaming_duration: bool = False, + custom_labels: dict = {}, excluded_handlers: List[str] = [], body_handlers: List[str] = [], round_latency_decimals: int = 4, @@ -119,6 +120,8 @@ def __init__( self.should_instrument_requests_inprogress = should_instrument_requests_inprogress self.should_exclude_streaming_duration = should_exclude_streaming_duration + self.custom_labels = custom_labels + self.round_latency_decimals = round_latency_decimals self.env_var_name = env_var_name self.inprogress_name = inprogress_name @@ -225,6 +228,7 @@ def instrument( should_only_respect_2xx_for_highr=should_only_respect_2xx_for_highr, latency_highr_buckets=latency_highr_buckets, latency_lowr_buckets=latency_lowr_buckets, + custom_labels=self.custom_labels, registry=self.registry, ) return self diff --git a/src/prometheus_fastapi_instrumentator/metrics.py b/src/prometheus_fastapi_instrumentator/metrics.py index b404f96..e147690 100644 --- a/src/prometheus_fastapi_instrumentator/metrics.py +++ b/src/prometheus_fastapi_instrumentator/metrics.py @@ -27,6 +27,7 @@ def __init__( modified_handler: str, modified_status: str, modified_duration: float, + custom_labels: dict, modified_duration_without_streaming: float = 0.0, ): """Creates Info object that is used for instrumentation functions. @@ -53,6 +54,8 @@ def __init__( self.modified_handler = modified_handler self.modified_status = modified_status self.modified_duration = modified_duration + for key, value in custom_labels.items(): + setattr(self, key, value) self.modified_duration_without_streaming = modified_duration_without_streaming @@ -118,6 +121,7 @@ def latency( should_include_handler: bool = True, should_include_method: bool = True, should_include_status: bool = True, + custom_labels: dict = {}, should_exclude_streaming_duration: bool = False, buckets: Sequence[Union[float, str]] = Histogram.DEFAULT_BUCKETS, registry: CollectorRegistry = REGISTRY, @@ -162,7 +166,9 @@ def latency( label_names, info_attribute_names = _build_label_attribute_names( should_include_handler, should_include_method, should_include_status ) - + for key in custom_labels: + label_names.append(key) + info_attribute_names.append(key) # Starlette will call app.build_middleware_stack() with every new middleware # added, which will call all this again, which will make the registry # complain about duplicated metrics. @@ -224,6 +230,7 @@ def request_size( should_include_handler: bool = True, should_include_method: bool = True, should_include_status: bool = True, + custom_labels: dict = {}, registry: CollectorRegistry = REGISTRY, ) -> Optional[Callable[[Info], None]]: """Record the content length of incoming requests. @@ -253,7 +260,9 @@ def request_size( label_names, info_attribute_names = _build_label_attribute_names( should_include_handler, should_include_method, should_include_status ) - + for key in custom_labels: + label_names.append(key) + info_attribute_names.append(key) # Starlette will call app.build_middleware_stack() with every new middleware # added, which will call all this again, which will make the registry # complain about duplicated metrics. @@ -287,7 +296,6 @@ def instrumentation(info: Info) -> None: getattr(info, attribute_name) for attribute_name in info_attribute_names ] - METRIC.labels(*label_values).observe(int(content_length)) else: METRIC.observe(int(content_length)) @@ -308,6 +316,7 @@ def response_size( should_include_handler: bool = True, should_include_method: bool = True, should_include_status: bool = True, + custom_labels: dict = {}, registry: CollectorRegistry = REGISTRY, ) -> Optional[Callable[[Info], None]]: """Record the content length of outgoing responses. @@ -343,7 +352,9 @@ def response_size( label_names, info_attribute_names = _build_label_attribute_names( should_include_handler, should_include_method, should_include_status ) - + for key in custom_labels: + label_names.append(key) + info_attribute_names.append(key) # Starlette will call app.build_middleware_stack() with every new middleware # added, which will call all this again, which will make the registry # complain about duplicated metrics. @@ -402,6 +413,7 @@ def combined_size( should_include_handler: bool = True, should_include_method: bool = True, should_include_status: bool = True, + custom_labels: dict = {}, registry: CollectorRegistry = REGISTRY, ) -> Optional[Callable[[Info], None]]: """Record the combined content length of requests and responses. @@ -437,7 +449,9 @@ def combined_size( label_names, info_attribute_names = _build_label_attribute_names( should_include_handler, should_include_method, should_include_status ) - + for key in custom_labels: + label_names.append(key) + info_attribute_names.append(key) # Starlette will call app.build_middleware_stack() with every new middleware # added, which will call all this again, which will make the registry # complain about duplicated metrics. @@ -500,6 +514,7 @@ def requests( should_include_handler: bool = True, should_include_method: bool = True, should_include_status: bool = True, + custom_labels: dict = {}, registry: CollectorRegistry = REGISTRY, ) -> Optional[Callable[[Info], None]]: """Record the number of requests. @@ -533,7 +548,9 @@ def requests( label_names, info_attribute_names = _build_label_attribute_names( should_include_handler, should_include_method, should_include_status ) - + for key in custom_labels: + label_names.append(key) + info_attribute_names.append(key) # Starlette will call app.build_middleware_stack() with every new middleware # added, which will call all this again, which will make the registry # complain about duplicated metrics. @@ -579,6 +596,21 @@ def instrumentation(info: Info) -> None: return None +def _map_label_name_value(label_name): + atrribute_names = [] + mapping = { + "handler": "modified_handler", + "status": "modified_status", + "duration": "modified_duration", + } + for item in label_name: + if item in mapping: + atrribute_names.append(mapping[item]) + else: + atrribute_names.append(item) + return atrribute_names + + def default( metric_namespace: str = "", metric_subsystem: str = "", @@ -608,6 +640,7 @@ def default( 60, ), latency_lowr_buckets: Sequence[Union[float, str]] = (0.1, 0.5, 1), + custom_labels: dict = {}, registry: CollectorRegistry = REGISTRY, ) -> Optional[Callable[[Info], None]]: """Contains multiple metrics to cover multiple things. @@ -668,20 +701,22 @@ def default( # The Python Prometheus client currently doesn't seem to have a way to # verify if adding a metric will cause errors or not, so the only way to # handle it seems to be with this try block. + additional_label_names = tuple([key for key in custom_labels]) try: + total_label_names = ( + "method", + "status", + "handler", + ) + additional_label_names TOTAL = Counter( name="http_requests_total", documentation="Total number of requests by method, status and handler.", - labelnames=( - "method", - "status", - "handler", - ), + labelnames=total_label_names, namespace=metric_namespace, subsystem=metric_subsystem, registry=registry, ) - + in_size_names = ("handler",) + additional_label_names IN_SIZE = Summary( name="http_request_size_bytes", documentation=( @@ -689,12 +724,12 @@ def default( "Only value of header is respected. Otherwise ignored. " "No percentile calculated. " ), - labelnames=("handler",), + labelnames=in_size_names, namespace=metric_namespace, subsystem=metric_subsystem, registry=registry, ) - + out_size_names = ("handler",) + additional_label_names OUT_SIZE = Summary( name="http_response_size_bytes", documentation=( @@ -702,7 +737,7 @@ def default( "Only value of header is respected. Otherwise ignored. " "No percentile calculated. " ), - labelnames=("handler",), + labelnames=out_size_names, namespace=metric_namespace, subsystem=metric_subsystem, registry=registry, @@ -719,7 +754,10 @@ def default( subsystem=metric_subsystem, registry=registry, ) - + latency_lower_names = ( + "method", + "handler", + ) + additional_label_names LATENCY_LOWR = Histogram( name="http_request_duration_seconds", documentation=( @@ -727,10 +765,7 @@ def default( "Made to be only used if aggregation by handler is important. " ), buckets=latency_lowr_buckets, - labelnames=( - "method", - "handler", - ), + labelnames=latency_lower_names, namespace=metric_namespace, subsystem=metric_subsystem, registry=registry, @@ -743,27 +778,38 @@ def instrumentation(info: Info) -> None: else: duration = info.modified_duration - TOTAL.labels(info.method, info.modified_status, info.modified_handler).inc() - - IN_SIZE.labels(info.modified_handler).observe( + label_values = [ + getattr(info, attribute_name) + for attribute_name in _map_label_name_value(total_label_names) + ] + TOTAL.labels(*label_values).inc() + label_values = [ + getattr(info, attribute_name) + for attribute_name in _map_label_name_value(in_size_names) + ] + IN_SIZE.labels(*label_values).observe( int(info.request.headers.get("Content-Length", 0)) ) - + label_values = [ + getattr(info, attribute_name) + for attribute_name in _map_label_name_value(out_size_names) + ] if info.response and hasattr(info.response, "headers"): - OUT_SIZE.labels(info.modified_handler).observe( + OUT_SIZE.labels(*label_values).observe( int(info.response.headers.get("Content-Length", 0)) ) else: - OUT_SIZE.labels(info.modified_handler).observe(0) + OUT_SIZE.labels(*label_values).observe(0) if not should_only_respect_2xx_for_highr or info.modified_status.startswith( "2" ): LATENCY_HIGHR.observe(duration) - - LATENCY_LOWR.labels( - handler=info.modified_handler, method=info.method - ).observe(duration) + label_values = [ + getattr(info, attribute_name) + for attribute_name in _map_label_name_value(latency_lower_names) + ] + LATENCY_LOWR.labels(*label_values).observe(duration) return instrumentation diff --git a/src/prometheus_fastapi_instrumentator/middleware.py b/src/prometheus_fastapi_instrumentator/middleware.py index c1ae3d4..69d2a45 100644 --- a/src/prometheus_fastapi_instrumentator/middleware.py +++ b/src/prometheus_fastapi_instrumentator/middleware.py @@ -63,6 +63,7 @@ def __init__( 60, ), latency_lowr_buckets: Sequence[Union[float, str]] = (0.1, 0.5, 1), + custom_labels: dict = {}, registry: CollectorRegistry = REGISTRY, ) -> None: self.app = app @@ -79,6 +80,7 @@ def __init__( self.inprogress_name = inprogress_name self.inprogress_labels = inprogress_labels self.registry = registry + self.custom_labels = custom_labels self.excluded_handlers = [re.compile(path) for path in excluded_handlers] self.body_handlers = [re.compile(path) for path in body_handlers] @@ -93,6 +95,7 @@ def __init__( should_exclude_streaming_duration=should_exclude_streaming_duration, latency_highr_buckets=latency_highr_buckets, latency_lowr_buckets=latency_lowr_buckets, + custom_labels=custom_labels, registry=self.registry, ) if default_instrumentation: @@ -211,6 +214,7 @@ async def send_wrapper(message: Message) -> None: modified_handler=handler, modified_status=status, modified_duration=duration, + custom_labels=self.custom_labels, modified_duration_without_streaming=duration_without_streaming, )