|
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|
|
|
|
|
|
|
import json
|
|
import json
|
|
|
import logging
|
|
import logging
|
|
|
|
|
+from uuid import uuid4
|
|
|
from collections import defaultdict
|
|
from collections import defaultdict
|
|
|
from dataclasses import dataclass
|
|
from dataclasses import dataclass
|
|
|
from time import perf_counter
|
|
from time import perf_counter
|
|
@@ -17,6 +18,9 @@ from starlette.types import ASGIApp
|
|
|
|
|
|
|
|
_METRICS_CONTENT_TYPE = "text/plain; version=0.0.4; charset=utf-8"
|
|
_METRICS_CONTENT_TYPE = "text/plain; version=0.0.4; charset=utf-8"
|
|
|
_DURATION_BUCKETS = (0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0)
|
|
_DURATION_BUCKETS = (0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0)
|
|
|
|
|
+TRACE_ID_HEADER = "x-trace-id"
|
|
|
|
|
+SPAN_ID_HEADER = "x-span-id"
|
|
|
|
|
+PARENT_SPAN_ID_HEADER = "x-parent-span-id"
|
|
|
RouteDecorator = Callable[[Callable[..., Awaitable[Response]]], Callable[..., Awaitable[Response]]]
|
|
RouteDecorator = Callable[[Callable[..., Awaitable[Response]]], Callable[..., Awaitable[Response]]]
|
|
|
|
|
|
|
|
|
|
|
|
@@ -125,9 +129,19 @@ class ObservabilityMiddleware(BaseHTTPMiddleware):
|
|
|
|
|
|
|
|
started_at_monotonic = perf_counter()
|
|
started_at_monotonic = perf_counter()
|
|
|
status_code = 500
|
|
status_code = 500
|
|
|
|
|
+ trace_id = _resolve_trace_id(request.headers)
|
|
|
|
|
+ parent_span_id = _header(request.headers, SPAN_ID_HEADER)
|
|
|
|
|
+ span_id = uuid4().hex[:16]
|
|
|
|
|
+ request.state.trace_id = trace_id
|
|
|
|
|
+ request.state.span_id = span_id
|
|
|
|
|
+ request.state.parent_span_id = parent_span_id
|
|
|
try:
|
|
try:
|
|
|
response = await call_next(request)
|
|
response = await call_next(request)
|
|
|
status_code = response.status_code
|
|
status_code = response.status_code
|
|
|
|
|
+ response.headers[TRACE_ID_HEADER] = trace_id
|
|
|
|
|
+ response.headers[SPAN_ID_HEADER] = span_id
|
|
|
|
|
+ if parent_span_id is not None:
|
|
|
|
|
+ response.headers[PARENT_SPAN_ID_HEADER] = parent_span_id
|
|
|
return response
|
|
return response
|
|
|
finally:
|
|
finally:
|
|
|
duration_seconds = perf_counter() - started_at_monotonic
|
|
duration_seconds = perf_counter() - started_at_monotonic
|
|
@@ -158,6 +172,9 @@ class ObservabilityMiddleware(BaseHTTPMiddleware):
|
|
|
"duration_ms": round(duration_seconds * 1000, 3),
|
|
"duration_ms": round(duration_seconds * 1000, 3),
|
|
|
"request_id": _header(headers, "x-request-id"),
|
|
"request_id": _header(headers, "x-request-id"),
|
|
|
"tenant_id": _header(headers, "x-tenant-id"),
|
|
"tenant_id": _header(headers, "x-tenant-id"),
|
|
|
|
|
+ "trace_id": getattr(request.state, "trace_id", None),
|
|
|
|
|
+ "span_id": getattr(request.state, "span_id", None),
|
|
|
|
|
+ "parent_span_id": getattr(request.state, "parent_span_id", None),
|
|
|
}
|
|
}
|
|
|
self._logger.info(json.dumps(payload, ensure_ascii=False, separators=(",", ":")))
|
|
self._logger.info(json.dumps(payload, ensure_ascii=False, separators=(",", ":")))
|
|
|
|
|
|
|
@@ -191,6 +208,18 @@ def _header(headers: Headers, name: str) -> str | None:
|
|
|
return value
|
|
return value
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
+def _resolve_trace_id(headers: Headers) -> str:
|
|
|
|
|
+ trace_id = _header(headers, TRACE_ID_HEADER)
|
|
|
|
|
+ if trace_id is not None:
|
|
|
|
|
+ return trace_id
|
|
|
|
|
+ traceparent = _header(headers, "traceparent")
|
|
|
|
|
+ if traceparent is not None:
|
|
|
|
|
+ parts = traceparent.split("-")
|
|
|
|
|
+ if len(parts) >= 2 and parts[1]:
|
|
|
|
|
+ return parts[1]
|
|
|
|
|
+ return uuid4().hex
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
def _format_bucket(value: float) -> str:
|
|
def _format_bucket(value: float) -> str:
|
|
|
return f"{value:g}"
|
|
return f"{value:g}"
|
|
|
|
|
|