"""OpenTelemetry metrics setup and recording helpers.""" from typing import Sequence from opentelemetry import metrics from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import OTLPMetricExporter from opentelemetry.sdk.metrics import MeterProvider from opentelemetry.sdk.metrics.export import ( MetricReader, PeriodicExportingMetricReader, ) from opentelemetry.sdk.resources import Resource _METER_PROVIDER: MeterProvider | None = None _METRICS: "ObservabilityMetrics" | None = None _METER_NAME = "g2s.observability" class ObservabilityMetrics: def __init__( self, meter_name: str, *, meter_provider: MeterProvider | None = None, ) -> None: if meter_provider is None: meter = metrics.get_meter(meter_name) else: meter = meter_provider.get_meter(meter_name) self._request_count = meter.create_counter( name="http.server.request.count", unit="1", description="Total number of HTTP requests.", ) self._error_count = meter.create_counter( name="http.server.error.count", unit="1", description="Total number of HTTP server errors.", ) self._request_latency = meter.create_histogram( name="http.server.request.latency", unit="ms", description="HTTP request latency.", ) self._provider_availability = meter.create_histogram( name="delivery.provider.availability", unit="1", description="Provider availability sample (1=available, 0=unavailable).", ) def record_http_request( self, *, method: str, route: str, status_code: int, duration_ms: float, is_error: bool, ) -> None: attributes = { "http.method": method, "http.route": route, "http.status_code": status_code, } self._request_count.add(1, attributes) self._request_latency.record(duration_ms, attributes) if is_error: self._error_count.add(1, attributes) def record_provider_availability( self, *, provider: str, is_available: bool, ) -> None: self._provider_availability.record( 1.0 if is_available else 0.0, {"provider": provider}, ) def setup_metrics( *, service_name: str, otlp_endpoint: str, meter_provider: MeterProvider | None = None, metric_readers: Sequence[MetricReader] | None = None, ) -> MeterProvider: global _METER_PROVIDER, _METRICS if meter_provider is None and _METER_PROVIDER is not None: return _METER_PROVIDER if meter_provider is None: readers = list(metric_readers or ()) if otlp_endpoint: metric_exporter = OTLPMetricExporter( endpoint=otlp_endpoint, insecure=otlp_endpoint.startswith("http://"), ) readers.append(PeriodicExportingMetricReader(metric_exporter)) meter_provider = MeterProvider( resource=Resource.create({"service.name": service_name}), metric_readers=readers, ) _METER_PROVIDER = meter_provider _METRICS = ObservabilityMetrics( _METER_NAME, meter_provider=meter_provider, ) return meter_provider def get_metrics() -> ObservabilityMetrics: global _METRICS if _METRICS is None: _METRICS = ObservabilityMetrics(_METER_NAME) return _METRICS def reset_metrics_for_tests() -> None: global _METER_PROVIDER, _METRICS if _METER_PROVIDER is not None: _METER_PROVIDER.shutdown() _METER_PROVIDER = None _METRICS = None