"""OpenTelemetry metrics bootstrap and application instruments.""" 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 PeriodicExportingMetricReader from opentelemetry.sdk.resources import Resource from app.config import ObservabilityConfig _METER_PROVIDER: MeterProvider | None = None _CACHE_METRICS: "CacheMetrics | None" = None class CacheMetrics: """Metrics emitted by the Redis-backed price cache.""" def __init__(self) -> None: meter = metrics.get_meter("app.repositories.cache") self._requests = meter.create_counter( "cache_requests_total", unit="{request}", description="Number of cache operations by operation and outcome.", ) self._errors = meter.create_counter( "cache_errors_total", unit="{error}", description="Number of failed cache operations by error type.", ) self._hit_ratio = meter.create_histogram( "cache_hit_ratio", unit="1", description="Cache lookup result: 1 for a hit and 0 for a miss.", ) def record_request(self, *, operation: str, outcome: str) -> None: self._requests.add(1, {"operation": operation, "outcome": outcome}) def record_error(self, *, operation: str, error_type: str) -> None: self._errors.add(1, {"operation": operation, "error.type": error_type}) def record_hit(self, *, hit: bool) -> None: self._hit_ratio.record(1.0 if hit else 0.0) def configure_metrics(observability: ObservabilityConfig) -> None: """Configure the process-wide OTLP metrics exporter once.""" global _METER_PROVIDER if not observability.enabled or _METER_PROVIDER is not None: return meter_provider = _build_meter_provider(observability) metrics.set_meter_provider(meter_provider) _METER_PROVIDER = meter_provider def get_cache_metrics() -> CacheMetrics: """Return the process-wide cache instruments.""" global _CACHE_METRICS if _CACHE_METRICS is None: _CACHE_METRICS = CacheMetrics() return _CACHE_METRICS def _build_meter_provider(observability: ObservabilityConfig) -> MeterProvider: exporter = OTLPMetricExporter( endpoint=observability.otlp_endpoint, insecure=observability.otlp_insecure, ) reader = PeriodicExportingMetricReader(exporter) resource = Resource.create({"service.name": observability.service_name}) return MeterProvider(resource=resource, metric_readers=[reader])