from __future__ import annotations import logging import os from collections.abc import MutableMapping from dataclasses import dataclass from typing import Any from fastapi import FastAPI from opentelemetry import metrics, trace from opentelemetry._logs import set_logger_provider from opentelemetry.exporter.otlp.proto.grpc._log_exporter import OTLPLogExporter from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import OTLPMetricExporter from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter from opentelemetry.instrumentation.botocore import BotocoreInstrumentor from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor from opentelemetry.instrumentation.httpx import HTTPXClientInstrumentor from opentelemetry.instrumentation.redis import RedisInstrumentor from opentelemetry.instrumentation.sqlalchemy import SQLAlchemyInstrumentor from opentelemetry.propagate import extract, inject, set_global_textmap from opentelemetry.sdk._logs import LoggerProvider, LoggingHandler 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.sdk.trace.sampling import ALWAYS_ON from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator @dataclass(slots=True) class TelemetryRuntime: tracer_provider: TracerProvider meter_provider: MeterProvider logger_provider: LoggerProvider logging_handler: LoggingHandler def shutdown(self) -> None: self.logger_provider.shutdown() 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", "api-backend")) 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) logger_provider = LoggerProvider(resource=resource) logger_provider.add_log_record_processor( BatchLogRecordProcessor( OTLPLogExporter(endpoint=endpoint, insecure=insecure, timeout=3), max_queue_size=2048, schedule_delay_millis=5000, max_export_batch_size=512, export_timeout_millis=3000, ) ) set_logger_provider(logger_provider) logging_handler = LoggingHandler(level=logging.NOTSET, logger_provider=logger_provider) HTTPXClientInstrumentor().instrument() SQLAlchemyInstrumentor().instrument(enable_commenter=False) RedisInstrumentor().instrument() BotocoreInstrumentor().instrument() _runtime = TelemetryRuntime( tracer_provider, meter_provider, logger_provider, logging_handler, ) return _runtime def instrument_fastapi(app: FastAPI) -> None: FastAPIInstrumentor.instrument_app( app, excluded_urls="/health/live,/health/ready,/nginx-health/live", ) def add_trace_context( _logger: Any, _method_name: str, event_dict: MutableMapping[str, Any], ) -> MutableMapping[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 def current_trace_id() -> str | None: context = trace.get_current_span().get_span_context() return format(context.trace_id, "032x") if context.is_valid else None def current_traceparent() -> str | None: carrier: dict[str, str] = {} inject(carrier) return carrier.get("traceparent") def origin_links(traceparent: str | None) -> list[trace.Link]: if not traceparent: return [] context = extract({"traceparent": traceparent}) span_context = trace.get_current_span(context).get_span_context() return [trace.Link(span_context)] if span_context.is_valid else []