Version 0.4.0
All checks were successful
Run linters on applied template / Python 3.13 lint and build (push) Successful in 1m40s

Changes:
- put ObservabilityMiddleware before ExceptionHandlerMiddleware to avoid repetative code
- add application startup and last metrics update metrics along with CPU usage metric and threads count
- move host and port to new uvicorn section at config along with new reload and forwarded_allow_ips
- add request_id and remove trace_id/span_id generation if tracing is disabled
- move logging logic from utils to observability
- pass trace_id/span_id in HEX form
This commit is contained in:
2026-01-03 11:01:43 +03:00
parent b8acb017fd
commit 53f14a8624
26 changed files with 901 additions and 730 deletions

View File

@@ -0,0 +1,64 @@
"""Observability-related utility functions and classes are located here."""
import re
from collections import defaultdict
import fastapi
import structlog
from opentelemetry import trace
class URLsMapper:
"""Helper to change URL from given regex pattern to the given static value.
For example, with map {"GET": {"/api/debug/.*": "/api/debug/*"}} all GET-requests with URL
starting with "/api/debug/" will be placed in path "/api/debug/*" in metrics.
"""
def __init__(self, urls_map: dict[str, dict[str, str]] | None = None):
self._map: dict[str, dict[re.Pattern, str]] = defaultdict(dict)
"""[method -> [pattern -> mapped_to]]"""
if urls_map is not None:
for method, patterns in urls_map.items():
for pattern, value in patterns.items():
self.add(method, pattern, value)
def add(self, method: str, pattern: str, mapped_to: str) -> None:
"""Add entry to the map. If pattern compilation is failed, ValueError is raised."""
regexp = re.compile(pattern)
self._map[method.upper()][regexp] = mapped_to
def add_routes(self, routes: list[fastapi.routing.APIRoute]) -> None:
"""Add full route regexes to the map."""
logger: structlog.stdlib.BoundLogger = structlog.get_logger(__name__)
for route in routes:
if not hasattr(route, "path_regex") or not hasattr(route, "path"):
logger.warning("route has no 'path_regex' or 'path' attribute", route=route)
continue
if "{" not in route.path: # ignore simple routes
continue
route_path = route.path
while "{" in route_path:
lbrace = route_path.index("{")
rbrace = route_path.index("}", lbrace + 1)
route_path = route_path[:lbrace] + "*" + route_path[rbrace + 1 :]
for method in route.methods:
self._map[method.upper()][route.path_regex] = route_path
def map(self, method: str, url: str) -> str:
"""Check every map entry with `re.match` and return matched value. If not found, return original string."""
for regexp, mapped_to in self._map[method.upper()].items():
if regexp.match(url) is not None:
return mapped_to
return url
def get_tracing_headers() -> dict[str, str]:
ctx = trace.get_current_span().get_span_context()
if ctx.trace_id == 0:
return {}
return {
"X-Span-Id": format(ctx.span_id, "016x"),
"X-Trace-Id": format(ctx.trace_id, "032x"),
}