From 8de03a0dd49abccd3ea5ed37bbd538cf47c77c72 Mon Sep 17 00:00:00 2001 From: openmindev <147775420+openminddev@users.noreply.github.com> Date: Thu, 28 May 2026 10:59:07 -0700 Subject: [PATCH] Add Prometheus metrics for HTTPX requests Add a new prometheus metrics module and hook it into HTTPX event handlers. Created src/om1_utils/prometheus/__init__.py which defines Histograms and Gauges for request duration, upstream total, upstream TTFB, and proxy total (including `_last_seconds` gauges). Updated src/om1_utils/httpx/__init__.py to import these metrics, handle missing request start_time (log a warning and skip metrics), compute elapsed time, and record observe/set metrics by parsing relevant response headers (convert ms to seconds and ignore invalid values). --- src/om1_utils/httpx/__init__.py | 69 +++++++++++++++++++++++++++- src/om1_utils/prometheus/__init__.py | 49 ++++++++++++++++++++ 2 files changed, 117 insertions(+), 1 deletion(-) create mode 100644 src/om1_utils/prometheus/__init__.py diff --git a/src/om1_utils/httpx/__init__.py b/src/om1_utils/httpx/__init__.py index b24a343..5480802 100644 --- a/src/om1_utils/httpx/__init__.py +++ b/src/om1_utils/httpx/__init__.py @@ -3,6 +3,17 @@ import httpx +from ..prometheus import ( + om1_http_proxy_total_last_seconds, + om1_http_proxy_total_seconds, + om1_http_request_duration_last_seconds, + om1_http_request_duration_seconds, + om1_http_upstream_total_last_seconds, + om1_http_upstream_total_seconds, + om1_http_upstream_ttfb_last_seconds, + om1_http_upstream_ttfb_seconds, +) + def get_httpx_event_hooks() -> dict[str, list]: """ @@ -34,7 +45,14 @@ def log_response(response: httpx.Response): response : httpx.Response The HTTP response object to log. """ - start_time = response.request.extensions.get("start_time", 0) + start_time = response.request.extensions.get("start_time", None) + if start_time is None: + logging.warning( + f"HTTP {response.request.method} {response.request.url} - " + "No start_time recorded, skipping metrics" + ) + return + elapsed = (time.perf_counter() - start_time) * 1000 http_version = response.http_version proxy_parse_total_time = response.headers.get("x-proxy-parse-ms", "?") @@ -52,6 +70,55 @@ def log_response(response: httpx.Response): f"Proxy Total Time: {proxy_total_time} ms" ) + method = response.request.method + status_code = str(response.status_code) + host = str(response.request.url.host) + path = str(response.request.url.path) + elapsed_s = elapsed / 1000.0 + + om1_http_request_duration_seconds.labels( + host=host, path=path, method=method, status_code=status_code + ).observe(elapsed_s) + om1_http_request_duration_last_seconds.labels( + host=host, path=path, method=method, status_code=status_code + ).set(elapsed_s) + + if upstream_total_time != "?": + try: + val = float(upstream_total_time) / 1000.0 + om1_http_upstream_total_seconds.labels( + host=host, path=path, method=method, status_code=status_code + ).observe(val) + om1_http_upstream_total_last_seconds.labels( + host=host, path=path, method=method, status_code=status_code + ).set(val) + except ValueError: + pass + + if upstream_ttfb_time != "?": + try: + val = float(upstream_ttfb_time) / 1000.0 + om1_http_upstream_ttfb_seconds.labels( + host=host, path=path, method=method, status_code=status_code + ).observe(val) + om1_http_upstream_ttfb_last_seconds.labels( + host=host, path=path, method=method, status_code=status_code + ).set(val) + except ValueError: + pass + + if proxy_total_time != "?": + try: + val = float(proxy_total_time) / 1000.0 + om1_http_proxy_total_seconds.labels( + host=host, path=path, method=method, status_code=status_code + ).observe(val) + om1_http_proxy_total_last_seconds.labels( + host=host, path=path, method=method, status_code=status_code + ).set(val) + except ValueError: + pass + return { "request": [log_request], "response": [log_response], diff --git a/src/om1_utils/prometheus/__init__.py b/src/om1_utils/prometheus/__init__.py new file mode 100644 index 0000000..81372c1 --- /dev/null +++ b/src/om1_utils/prometheus/__init__.py @@ -0,0 +1,49 @@ +from prometheus_client import Gauge, Histogram + +om1_http_request_duration_seconds = Histogram( + "om1_http_request_duration_seconds", + "Total HTTP request duration (client-side) in seconds", + ["host", "path", "method", "status_code"], +) + +om1_http_upstream_total_seconds = Histogram( + "om1_http_upstream_total_seconds", + "Upstream total time in seconds (from x-upstream-total-ms header)", + ["host", "path", "method", "status_code"], +) + +om1_http_upstream_ttfb_seconds = Histogram( + "om1_http_upstream_ttfb_seconds", + "Upstream TTFB in seconds (from x-upstream-ttfb-ms header)", + ["host", "path", "method", "status_code"], +) + +om1_http_proxy_total_seconds = Histogram( + "om1_http_proxy_total_seconds", + "Proxy total time in seconds (from x-proxy-total-ms header)", + ["host", "path", "method", "status_code"], +) + +om1_http_request_duration_last_seconds = Gauge( + "om1_http_request_duration_last_seconds", + "Most recent HTTP request duration (client-side) in seconds", + ["host", "path", "method", "status_code"], +) + +om1_http_upstream_total_last_seconds = Gauge( + "om1_http_upstream_total_last_seconds", + "Most recent upstream total time in seconds", + ["host", "path", "method", "status_code"], +) + +om1_http_upstream_ttfb_last_seconds = Gauge( + "om1_http_upstream_ttfb_last_seconds", + "Most recent upstream TTFB in seconds", + ["host", "path", "method", "status_code"], +) + +om1_http_proxy_total_last_seconds = Gauge( + "om1_http_proxy_total_last_seconds", + "Most recent proxy total time in seconds", + ["host", "path", "method", "status_code"], +)