from __future__ import annotations import logging from dataclasses import dataclass from typing import Any from fastapi import FastAPI from opentelemetry import metrics, trace from opentelemetry.baggage.propagation import W3CBaggagePropagator from opentelemetry.exporter.otlp.proto.http.metric_exporter import OTLPMetricExporter from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor from opentelemetry.instrumentation.httpx import HTTPXClientInstrumentor from opentelemetry.instrumentation.logging import LoggingInstrumentor from opentelemetry.instrumentation.sqlalchemy import SQLAlchemyInstrumentor from opentelemetry.propagate import set_global_textmap from opentelemetry.propagators.composite import CompositePropagator from opentelemetry.sdk.metrics import MeterProvider from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchSpanProcessor from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator try: from opentelemetry.instrumentation.system_metrics import SystemMetricsInstrumentor except ImportError: # pragma: no cover - defensive fallback for minimal envs SystemMetricsInstrumentor = None # type: ignore[assignment] from app.core.config import Settings LOGGER = logging.getLogger(__name__) @dataclass class TelemetryProviders: tracer_provider: TracerProvider meter_provider: MeterProvider def configure_otel(settings: Settings) -> TelemetryProviders: set_global_textmap( CompositePropagator([TraceContextTextMapPropagator(), W3CBaggagePropagator()]) ) resource = Resource.create( { "service.name": settings.otel_service_name, "service.namespace": settings.otel_service_namespace, "deployment.environment": settings.app_env, } ) trace_exporter = OTLPSpanExporter( endpoint=f"{settings.otel_collector_endpoint}/v1/traces", timeout=settings.otel_export_timeout_ms / 1000, ) tracer_provider = TracerProvider(resource=resource) tracer_provider.add_span_processor(BatchSpanProcessor(trace_exporter)) trace.set_tracer_provider(tracer_provider) metric_reader = PeriodicExportingMetricReader( exporter=OTLPMetricExporter( endpoint=f"{settings.otel_collector_endpoint}/v1/metrics", timeout=settings.otel_export_timeout_ms / 1000, ), export_interval_millis=10000, ) meter_provider = MeterProvider(resource=resource, metric_readers=[metric_reader]) metrics.set_meter_provider(meter_provider) LoggingInstrumentor().instrument(set_logging_format=True) if SystemMetricsInstrumentor is not None: SystemMetricsInstrumentor().instrument() else: LOGGER.warning( "System metrics instrumentor not available, runtime host metrics disabled." ) LOGGER.info("OpenTelemetry providers configured") return TelemetryProviders( tracer_provider=tracer_provider, meter_provider=meter_provider ) def instrument_fastapi(app: FastAPI) -> None: FastAPIInstrumentor.instrument_app(app) def instrument_sqlalchemy_engines(engines: dict[str, Any]) -> None: for engine in engines.values(): SQLAlchemyInstrumentor().instrument(engine=engine) def instrument_httpx_clients() -> None: HTTPXClientInstrumentor().instrument() def shutdown_otel(providers: TelemetryProviders) -> None: HTTPXClientInstrumentor().uninstrument() if SystemMetricsInstrumentor is not None: SystemMetricsInstrumentor().uninstrument() LoggingInstrumentor().uninstrument() providers.meter_provider.shutdown() providers.tracer_provider.shutdown()