""" OpenTelemetry Metrics Integration for SigNoz Exports metrics to SigNoz via OpenTelemetry Collector using gRPC protocol """ import os import structlog from typing import Optional from opentelemetry import metrics from opentelemetry.sdk.metrics import MeterProvider from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader from opentelemetry.sdk.resources import Resource # Import both gRPC and HTTP exporters try: from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import OTLPMetricExporter as GrpcMetricExporter GRPC_AVAILABLE = True except ImportError: GRPC_AVAILABLE = False GrpcMetricExporter = None try: from opentelemetry.exporter.otlp.proto.http.metric_exporter import OTLPMetricExporter as HttpMetricExporter HTTP_AVAILABLE = True except ImportError: HTTP_AVAILABLE = False HttpMetricExporter = None from .otel_config import OTelConfig logger = structlog.get_logger() def setup_otel_metrics( service_name: str, service_version: str = "1.0.0", otel_endpoint: Optional[str] = None, export_interval_millis: int = 60000, # Export every 60 seconds protocol: Optional[str] = None # "grpc" or "http", defaults to grpc ) -> Optional[MeterProvider]: """ Setup OpenTelemetry metrics to export to SigNoz. Supports both gRPC (recommended, port 4317) and HTTP (port 4318) protocols. Default protocol is gRPC for better performance. Args: service_name: Name of the service (e.g., "auth-service") service_version: Version of the service otel_endpoint: Optional override for OTLP endpoint export_interval_millis: How often to push metrics in milliseconds (default 60s) protocol: Protocol to use ("grpc" or "http"). Defaults to "grpc" Returns: MeterProvider instance if successful, None otherwise Example: from shared.monitoring.metrics_exporter import setup_otel_metrics # Setup with gRPC (default) meter_provider = setup_otel_metrics("auth-service", "1.0.0") # Or with HTTP meter_provider = setup_otel_metrics("auth-service", "1.0.0", protocol="http") # Create meters for your metrics meter = meter_provider.get_meter(__name__) request_counter = meter.create_counter( "http.server.requests", description="Total HTTP requests", unit="1" ) # Record metrics request_counter.add(1, {"method": "GET", "status": "200"}) """ # Check if metrics export is enabled if not OTelConfig.is_enabled("metrics"): logger.info( "OpenTelemetry metrics export disabled", service=service_name, reason="ENABLE_OTEL_METRICS not set to 'true'" ) return None # Determine protocol to use if protocol is None: protocol = OTelConfig.get_protocol("metrics") # Validate protocol is available if protocol == "grpc" and not GRPC_AVAILABLE: logger.warning( "gRPC exporter not available, falling back to HTTP", service=service_name ) protocol = "http" elif protocol == "http" and not HTTP_AVAILABLE: logger.warning( "HTTP exporter not available, falling back to gRPC", service=service_name ) protocol = "grpc" if protocol not in ["grpc", "http"]: logger.error( "Invalid protocol specified", service=service_name, protocol=protocol ) return None try: # Get endpoints from centralized config endpoints = OTelConfig.get_endpoints() # Determine which endpoint to use if otel_endpoint: # User provided override if protocol == "grpc": endpoint = OTelConfig._clean_grpc_endpoint(otel_endpoint) else: endpoint = OTelConfig._ensure_http_endpoint(otel_endpoint, "/v1/metrics") else: # Use config-determined endpoint if protocol == "grpc": endpoint = endpoints.metrics_grpc else: endpoint = endpoints.metrics_http # Get resource attributes resource_attrs = OTelConfig.get_resource_attributes(service_name, service_version) resource = Resource(attributes=resource_attrs) # Configure OTLP exporter based on protocol if protocol == "grpc": otlp_exporter = GrpcMetricExporter( endpoint=endpoint, insecure=True, # Use secure=False in production with proper TLS timeout=10 ) else: # http otlp_exporter = HttpMetricExporter( endpoint=endpoint, timeout=10 ) # Create periodic metric reader metric_reader = PeriodicExportingMetricReader( exporter=otlp_exporter, export_interval_millis=export_interval_millis ) # Configure meter provider meter_provider = MeterProvider( resource=resource, metric_readers=[metric_reader] ) # Set global meter provider metrics.set_meter_provider(meter_provider) logger.info( "OpenTelemetry metrics export configured successfully", service=service_name, endpoint=endpoint, protocol=protocol, export_interval_seconds=export_interval_millis / 1000 ) return meter_provider except Exception as e: logger.error( "Failed to setup OpenTelemetry metrics export", service=service_name, error=str(e), protocol=protocol ) return None class OTelMetricsCollector: """ Wrapper for OpenTelemetry metrics that provides a similar interface to the Prometheus MetricsCollector. This allows services to emit metrics that go to both Prometheus and SigNoz. """ def __init__(self, service_name: str, meter_provider: MeterProvider): self.service_name = service_name self.meter_provider = meter_provider self.meter = meter_provider.get_meter(__name__) # Store created instruments self._counters = {} self._histograms = {} self._gauges = {} def create_counter(self, name: str, description: str = "", unit: str = "1"): """Create or get an OpenTelemetry Counter""" if name not in self._counters: self._counters[name] = self.meter.create_counter( name=f"{self.service_name.replace('-', '_')}_{name}", description=description, unit=unit ) return self._counters[name] def create_histogram(self, name: str, description: str = "", unit: str = "1"): """Create or get an OpenTelemetry Histogram""" if name not in self._histograms: self._histograms[name] = self.meter.create_histogram( name=f"{self.service_name.replace('-', '_')}_{name}", description=description, unit=unit ) return self._histograms[name] def create_gauge(self, name: str, description: str = "", unit: str = "1"): """ Create or get an OpenTelemetry observable gauge. Note: Gauges in OTEL require a callback function. """ if name not in self._gauges: # Store gauge reference for callback registration self._gauges[name] = { "name": f"{self.service_name.replace('-', '_')}_{name}", "description": description, "unit": unit, "value": 0, "attributes": {} } return self._gauges[name] def increment_counter(self, name: str, value: int = 1, attributes: dict = None): """Increment a counter with optional attributes""" if name in self._counters: if attributes is None: attributes = {"service": self.service_name} elif "service" not in attributes: attributes["service"] = self.service_name self._counters[name].add(value, attributes) def observe_histogram(self, name: str, value: float, attributes: dict = None): """Record a histogram observation with optional attributes""" if name in self._histograms: if attributes is None: attributes = {"service": self.service_name} elif "service" not in attributes: attributes["service"] = self.service_name self._histograms[name].record(value, attributes) def set_gauge(self, name: str, value: float, attributes: dict = None): """Set a gauge value (stores for next callback)""" if name in self._gauges: if attributes is None: attributes = {"service": self.service_name} elif "service" not in attributes: attributes["service"] = self.service_name self._gauges[name]["value"] = value self._gauges[name]["attributes"] = attributes def create_dual_metrics_collector(service_name: str, service_version: str = "1.0.0"): """ Create a metrics collector that exports to both Prometheus and SigNoz. This function sets up both collection strategies: 1. Prometheus client library (for /metrics endpoint scraping) 2. OpenTelemetry metrics (for OTLP push to SigNoz) Returns a tuple: (prometheus_collector, otel_collector) Both collectors can be used independently or together. Example: from shared.monitoring.metrics_exporter import create_dual_metrics_collector prom_collector, otel_collector = create_dual_metrics_collector("auth-service") # Prometheus counter prom_collector.register_counter("requests_total", "Total requests") prom_collector.increment_counter("requests_total", labels={"status": "200"}) # OpenTelemetry counter (pushed to SigNoz) counter = otel_collector.create_counter("requests_total", "Total requests") counter.add(1, {"status": "200"}) """ from shared.monitoring.metrics import MetricsCollector # Create Prometheus collector prom_collector = MetricsCollector(service_name) # Create OpenTelemetry collector meter_provider = setup_otel_metrics(service_name, service_version) otel_collector = None if meter_provider: otel_collector = OTelMetricsCollector(service_name, meter_provider) return prom_collector, otel_collector