Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Feature to add custom labels with metrics #287

Draft
wants to merge 3 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ instrumentator = Instrumentator(
env_var_name="ENABLE_METRICS",
inprogress_name="inprogress",
inprogress_labels=True,
custom_labels={"service": "example-ms"}
)
```

Expand Down Expand Up @@ -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(
Expand All @@ -176,6 +178,7 @@ instrumentator.add(
should_include_status=True,
metric_namespace="namespace",
metric_subsystem="subsystem",
custom_labels={"service": "example-ms"}
)
)
```
Expand Down
4 changes: 4 additions & 0 deletions src/prometheus_fastapi_instrumentator/instrumentation.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
106 changes: 76 additions & 30 deletions src/prometheus_fastapi_instrumentator/metrics.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ def __init__(
modified_handler: str,
modified_status: str,
modified_duration: float,
custom_labels: dict,
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I prefer to not have this attribute in the Info class. It should only contain attributes that are relevant for the respective request / response pair.

It is sufficient if all metrics closures included with this package get it as an additional parameter.

modified_duration_without_streaming: float = 0.0,
):
"""Creates Info object that is used for instrumentation functions.
Expand All @@ -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


Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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))
Expand All @@ -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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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 = "",
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -668,41 +701,43 @@ 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=(
"Content length of incoming requests by handler. "
"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=(
"Content length of outgoing responses by handler. "
"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,
Expand All @@ -719,18 +754,18 @@ def default(
subsystem=metric_subsystem,
registry=registry,
)

latency_lower_names = (
"method",
"handler",
) + additional_label_names
LATENCY_LOWR = Histogram(
name="http_request_duration_seconds",
documentation=(
"Latency with only few buckets by handler. "
"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,
Expand All @@ -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

Expand Down
4 changes: 4 additions & 0 deletions src/prometheus_fastapi_instrumentator/middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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]
Expand All @@ -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:
Expand Down Expand Up @@ -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,
)

Expand Down
Loading