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._log_exporter import OTLPLogExporter 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.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._logs import LoggerProvider from opentelemetry.sdk._logs.export import BatchLogRecordProcessor 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 from opentelemetry._logs import set_logger_provider try: from opentelemetry.instrumentation.system_metrics import SystemMetricsInstrumentor except ImportError: SystemMetricsInstrumentor = None # type: ignore[assignment] from app.core.config import Settings LOGGER = logging.getLogger(__name__) @dataclass class TelemetryProviders: tracer_provider: TracerProvider meter_provider: MeterProvider logger_provider: LoggerProvider 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, } ) tracer_provider = TracerProvider(resource=resource) tracer_provider.add_span_processor( BatchSpanProcessor( OTLPSpanExporter( endpoint=f"{settings.otel_collector_endpoint}/v1/traces", timeout=settings.otel_export_timeout_ms / 1000, ) ) ) trace.set_tracer_provider(tracer_provider) meter_provider = MeterProvider( resource=resource, metric_readers=[ PeriodicExportingMetricReader( exporter=OTLPMetricExporter( endpoint=f"{settings.otel_collector_endpoint}/v1/metrics", timeout=settings.otel_export_timeout_ms / 1000, ), export_interval_millis=10_000, ) ], ) metrics.set_meter_provider(meter_provider) logger_provider = LoggerProvider(resource=resource) logger_provider.add_log_record_processor( BatchLogRecordProcessor( OTLPLogExporter( endpoint=f"{settings.otel_collector_endpoint}/v1/logs", timeout=settings.otel_export_timeout_ms / 1000, ) ) ) set_logger_provider(logger_provider) LoggingInstrumentor().instrument(set_logging_format=True) if SystemMetricsInstrumentor is not None: SystemMetricsInstrumentor().instrument() else: LOGGER.warning("SystemMetricsInstrumentor not available — skipping.") LOGGER.info("OTel providers configured", extra={"service.name": settings.otel_service_name}) return TelemetryProviders( tracer_provider=tracer_provider, meter_provider=meter_provider, logger_provider=logger_provider, ) def instrument_fastapi(app: FastAPI) -> None: FastAPIInstrumentor.instrument_app(app) def instrument_sqlalchemy(engines: dict[str, Any]) -> None: for engine in engines.values(): SQLAlchemyInstrumentor().instrument(engine=engine) def shutdown_otel(providers: TelemetryProviders) -> None: if SystemMetricsInstrumentor is not None: SystemMetricsInstrumentor().uninstrument() LoggingInstrumentor().uninstrument() providers.meter_provider.shutdown() providers.tracer_provider.shutdown() providers.logger_provider.shutdown()