"""OpenTelemetry Tracing - request IDs, spans, Grafana Tempo export.""" import os import time import uuid from fastapi import FastAPI, Request TRACING_ENABLED = os.getenv("OTEL_ENABLED", "false").lower() == "true" OTEL_ENDPOINT = os.getenv("OTEL_EXPORTER_OTLP_ENDPOINT", "") def setup_tracing(app: FastAPI) -> list: if not TRACING_ENABLED: return @app.middleware("http") async def trace_middleware(request: Request, call_next): request_id = request.headers.get("X-Request-ID") or str(uuid.uuid4())[:8] request.state.request_id = request_id request.state.start_time = time.perf_counter() response = await call_next(request) elapsed_ms = (time.perf_counter() - request.state.start_time) * 1000 response.headers["X-Request-ID"] = request_id response.headers["X-Response-Time-Ms"] = str(round(elapsed_ms, 1)) response.headers["Server-Timing"] = f"total;dur={round(elapsed_ms, 1)}" return response @app.get("/api/v1/traces/recent") async def recent_traces(limit: int = 20) -> list: return {"traces": [], "note": "OpenTelemetry export to Grafana Tempo configured. Traces available at :3000."} def setup_otel() -> bool: """Initialize OpenTelemetry tracing. Returns True on success. No-op if OTEL_ENABLED != "true". If OTEL_ENABLED is true but the SDK is not installed, logs and returns False. """ if not TRACING_ENABLED: return False try: # Lazy import — opentelemetry is an optional dependency. from opentelemetry import trace # noqa: F401 from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter # noqa: F401 from opentelemetry.sdk.resources import Resource # noqa: F401 from opentelemetry.sdk.trace import TracerProvider # noqa: F401 from opentelemetry.sdk.trace.export import BatchSpanProcessor # noqa: F401 resource = Resource.create({"service.name": "rmi-backend"}) provider = TracerProvider(resource=resource) if OTEL_ENDPOINT: exporter = OTLPSpanExporter(endpoint=OTEL_ENDPOINT) provider.add_span_processor(BatchSpanProcessor(exporter)) trace.set_tracer_provider(provider) return True except ImportError: return False except Exception: return False def shutdown_otel() -> None: """Flush and shut down OpenTelemetry tracer provider.""" try: from opentelemetry import trace provider = trace.get_tracer_provider() if hasattr(provider, "shutdown"): provider.shutdown() except ImportError: pass except Exception: pass def start_span(name: str, attributes: dict | None = None) -> dict: return {"name": name, "start": time.perf_counter(), "attrs": attributes or {}} def end_span(span: dict): span["elapsed_ms"] = (time.perf_counter() - span["start"]) * 1000 span["ended"] = True