from __future__ import annotations import os from dataclasses import dataclass from typing import Any from fastapi import FastAPI from opentelemetry import metrics, trace from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import OTLPMetricExporter from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor from opentelemetry.instrumentation.httpx import HTTPXClientInstrumentor from opentelemetry.instrumentation.sqlalchemy import SQLAlchemyInstrumentor from opentelemetry.propagate import set_global_textmap 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.sdk.trace.sampling import ALWAYS_ON from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator @dataclass(slots=True) class TelemetryRuntime: tracer_provider: TracerProvider meter_provider: MeterProvider def shutdown(self) -> None: self.meter_provider.shutdown() self.tracer_provider.shutdown() _runtime: TelemetryRuntime | None = None def _resource(service_name: str) -> Resource: return Resource.create( { "service.name": service_name, "service.namespace": "han-chat", "service.version": os.getenv("RELEASE_VERSION", "unknown"), "deployment.environment": os.getenv("APP_ENV", "production-like"), } ) def init_telemetry(service_name: str | None = None) -> TelemetryRuntime | None: global _runtime if _runtime is not None: return _runtime endpoint = os.getenv("OTEL_EXPORTER_OTLP_ENDPOINT", "").strip() if not endpoint: return None resource = _resource(service_name or os.getenv("OTEL_SERVICE_NAME", "sms-service")) insecure = endpoint.startswith("http://") set_global_textmap(TraceContextTextMapPropagator()) tracer_provider = TracerProvider(resource=resource, sampler=ALWAYS_ON) tracer_provider.add_span_processor( BatchSpanProcessor( OTLPSpanExporter(endpoint=endpoint, insecure=insecure, timeout=3), max_queue_size=2048, schedule_delay_millis=5000, max_export_batch_size=512, export_timeout_millis=3000, ) ) trace.set_tracer_provider(tracer_provider) metric_reader = PeriodicExportingMetricReader( OTLPMetricExporter(endpoint=endpoint, insecure=insecure, timeout=3), export_interval_millis=30000, export_timeout_millis=3000, ) meter_provider = MeterProvider(resource=resource, metric_readers=[metric_reader]) metrics.set_meter_provider(meter_provider) HTTPXClientInstrumentor().instrument() SQLAlchemyInstrumentor().instrument(enable_commenter=False) _runtime = TelemetryRuntime(tracer_provider, meter_provider) return _runtime def instrument_fastapi(app: FastAPI) -> None: FastAPIInstrumentor.instrument_app( app, excluded_urls="/health/live,/health/ready", ) def add_trace_context( _logger: Any, _method_name: str, event_dict: dict[str, Any], ) -> dict[str, Any]: context = trace.get_current_span().get_span_context() if context.is_valid: event_dict["trace_id"] = format(context.trace_id, "032x") event_dict["span_id"] = format(context.span_id, "016x") return event_dict