Version 0.4.0
All checks were successful
Run linters on applied template / Python 3.13 lint and build (push) Successful in 1m40s
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:
64
{{project_slug}}/observability/utils.py
Normal file
64
{{project_slug}}/observability/utils.py
Normal 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"),
|
||||
}
|
||||
Reference in New Issue
Block a user