Coverage for ivatar / opentelemetry_config.py: 81%
147 statements
« prev ^ index » next coverage.py v7.12.0, created at 2025-11-22 00:07 +0000
« prev ^ index » next coverage.py v7.12.0, created at 2025-11-22 00:07 +0000
1"""
2OpenTelemetry configuration for ivatar project.
4This module provides OpenTelemetry setup and configuration for the ivatar
5Django application, including tracing, metrics, and logging integration.
6"""
8import os
9import logging
11from opentelemetry import trace, metrics
12from opentelemetry.sdk.trace import TracerProvider
13from opentelemetry.sdk.trace.export import BatchSpanProcessor
14from opentelemetry.sdk.metrics import MeterProvider
15from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader
16from opentelemetry.sdk.resources import Resource
17from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
18from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import OTLPMetricExporter
19from opentelemetry.exporter.prometheus import PrometheusMetricReader
20from opentelemetry.instrumentation.psycopg2 import Psycopg2Instrumentor
21from opentelemetry.instrumentation.pymysql import PyMySQLInstrumentor
22from opentelemetry.instrumentation.requests import RequestsInstrumentor
23from opentelemetry.instrumentation.urllib3 import URLLib3Instrumentor
25from django.conf import settings
26from django.core.exceptions import ImproperlyConfigured
28# Note: Memcached instrumentation not available in OpenTelemetry Python
30logger = logging.getLogger("ivatar")
33class OpenTelemetryConfig:
34 """
35 OpenTelemetry configuration manager for ivatar.
37 Handles setup of tracing, metrics, and instrumentation for the Django application.
38 """
40 def __init__(self):
41 self.enabled = True # Always enable OpenTelemetry instrumentation
42 self.export_enabled = self._is_export_enabled()
43 self.service_name = self._get_service_name()
44 self.environment = self._get_environment()
45 self.resource = self._create_resource()
47 def _is_export_enabled(self) -> bool:
48 """Check if OpenTelemetry data export is enabled via environment variable."""
49 return os.environ.get("OTEL_EXPORT_ENABLED", "false").lower() in (
50 "true",
51 "1",
52 "yes",
53 )
55 def _get_service_name(self) -> str:
56 """Get service name from environment or default."""
57 return os.environ.get("OTEL_SERVICE_NAME", "ivatar")
59 def _get_environment(self) -> str:
60 """Get environment name (production, development, etc.)."""
61 return os.environ.get("OTEL_ENVIRONMENT", "development")
63 def _create_resource(self) -> Resource:
64 """Create OpenTelemetry resource with service information."""
65 # Get IVATAR_VERSION from environment or settings, handling case where
66 # Django settings might not be configured yet
67 ivatar_version = os.environ.get("IVATAR_VERSION")
68 if not ivatar_version:
69 # Try to access settings, but handle case where Django isn't configured
70 try:
71 ivatar_version = getattr(settings, "IVATAR_VERSION", "2.0")
72 except ImproperlyConfigured:
73 # Django settings not configured yet, use default
74 ivatar_version = "2.0"
76 return Resource.create(
77 {
78 "service.name": self.service_name,
79 "service.version": ivatar_version,
80 "service.namespace": "libravatar",
81 "deployment.environment": self.environment,
82 "service.instance.id": os.environ.get("HOSTNAME", "unknown"),
83 }
84 )
86 def setup_tracing(self) -> None:
87 """Set up OpenTelemetry tracing."""
88 try:
89 # Only set up tracing if export is enabled
90 if not self.export_enabled:
91 logger.info("OpenTelemetry tracing disabled (export disabled)")
92 return
94 # Set up tracer provider
95 trace.set_tracer_provider(TracerProvider(resource=self.resource))
96 tracer_provider = trace.get_tracer_provider()
98 # Configure OTLP exporter if endpoint is provided
99 otlp_endpoint = os.environ.get("OTEL_EXPORTER_OTLP_ENDPOINT")
100 if otlp_endpoint:
101 otlp_exporter = OTLPSpanExporter(endpoint=otlp_endpoint)
102 span_processor = BatchSpanProcessor(otlp_exporter)
103 tracer_provider.add_span_processor(span_processor)
104 logger.info(
105 f"OpenTelemetry tracing configured with OTLP endpoint: {otlp_endpoint}"
106 )
107 else:
108 logger.info("OpenTelemetry tracing configured without OTLP endpoint")
110 except Exception as e:
111 logger.error(f"Failed to setup OpenTelemetry tracing: {e}")
112 # Don't disable OpenTelemetry entirely - metrics and instrumentation can still work
114 def setup_metrics(self) -> None:
115 """Set up OpenTelemetry metrics."""
116 try:
117 # Configure metric readers based on environment
118 metric_readers = []
120 # Configure OTLP exporter if export is enabled and endpoint is provided
121 if self.export_enabled:
122 otlp_endpoint = os.environ.get("OTEL_EXPORTER_OTLP_ENDPOINT")
123 if otlp_endpoint:
124 otlp_exporter = OTLPMetricExporter(endpoint=otlp_endpoint)
125 metric_reader = PeriodicExportingMetricReader(otlp_exporter)
126 metric_readers.append(metric_reader)
127 logger.info(
128 f"OpenTelemetry metrics configured with OTLP endpoint: {otlp_endpoint}"
129 )
131 # For development/local testing, also configure Prometheus HTTP server
132 # In production, metrics are scraped by external Prometheus server
133 prometheus_endpoint = os.environ.get("OTEL_PROMETHEUS_ENDPOINT")
134 if prometheus_endpoint:
135 prometheus_reader = PrometheusMetricReader()
136 metric_readers.append(prometheus_reader)
138 # Set up meter provider with readers
139 meter_provider = MeterProvider(
140 resource=self.resource, metric_readers=metric_readers
141 )
143 # Only set meter provider if it's not already set
144 try:
145 metrics.set_meter_provider(meter_provider)
146 except Exception as e:
147 if "Overriding of current MeterProvider is not allowed" in str(e):
148 logger.warning("MeterProvider already set, using existing provider")
149 # Get the existing meter provider and add our readers
150 existing_provider = metrics.get_meter_provider()
151 if hasattr(existing_provider, "add_metric_reader"):
152 for reader in metric_readers:
153 existing_provider.add_metric_reader(reader)
154 else:
155 raise
157 # Start Prometheus HTTP server for local development (if configured)
158 if prometheus_endpoint:
159 self._start_prometheus_server(prometheus_reader, prometheus_endpoint)
160 logger.info(
161 f"OpenTelemetry metrics configured with Prometheus endpoint: {prometheus_endpoint}"
162 )
164 if not metric_readers:
165 logger.warning(
166 "No metric readers configured - metrics will not be exported"
167 )
169 except Exception as e:
170 logger.error(f"Failed to setup OpenTelemetry metrics: {e}")
171 # Don't disable OpenTelemetry entirely - tracing and instrumentation can still work
173 def _start_prometheus_server(
174 self, prometheus_reader: PrometheusMetricReader, endpoint: str
175 ) -> None:
176 """Start Prometheus HTTP server for metrics endpoint."""
177 try:
178 from prometheus_client import start_http_server, REGISTRY
180 # Parse endpoint to get host and port
181 if ":" in endpoint:
182 host, port = endpoint.split(":", 1)
183 port = int(port)
184 else:
185 host = "0.0.0.0"
186 port = int(endpoint)
188 # Register the PrometheusMetricReader collector with prometheus_client
189 REGISTRY.register(prometheus_reader._collector)
191 # Start HTTP server
192 start_http_server(port, addr=host)
194 logger.info(f"Prometheus metrics server started on {host}:{port}")
196 except OSError as e:
197 if e.errno == 98: # Address already in use
198 logger.warning(
199 f"Prometheus metrics server already running on {endpoint}"
200 )
201 else:
202 logger.error(f"Failed to start Prometheus metrics server: {e}")
203 # Don't disable OpenTelemetry entirely - metrics can still be exported via OTLP
204 except Exception as e:
205 logger.error(f"Failed to start Prometheus metrics server: {e}")
206 # Don't disable OpenTelemetry entirely - metrics can still be exported via OTLP
208 def setup_instrumentation(self) -> None:
209 """Set up OpenTelemetry instrumentation for various libraries."""
210 try:
211 # Django instrumentation - TEMPORARILY DISABLED TO TEST HEADER ISSUE
212 # DjangoInstrumentor().instrument()
213 # logger.info("Django instrumentation enabled")
215 # Database instrumentation
216 Psycopg2Instrumentor().instrument()
217 PyMySQLInstrumentor().instrument()
218 logger.info("Database instrumentation enabled")
220 # HTTP client instrumentation
221 RequestsInstrumentor().instrument()
222 URLLib3Instrumentor().instrument()
223 logger.info("HTTP client instrumentation enabled")
225 # Note: Memcached instrumentation not available in OpenTelemetry Python
226 # Cache operations will be traced through Django instrumentation
228 except Exception as e:
229 logger.error(f"Failed to setup OpenTelemetry instrumentation: {e}")
230 # Don't disable OpenTelemetry entirely - tracing and metrics can still work
232 def get_tracer(self, name: str) -> trace.Tracer:
233 """Get a tracer instance."""
234 return trace.get_tracer(name)
236 def get_meter(self, name: str) -> metrics.Meter:
237 """Get a meter instance."""
238 return metrics.get_meter(name)
241# Global OpenTelemetry configuration instance (lazy-loaded)
242_ot_config = None
243_ot_initialized = False
246def get_ot_config():
247 """Get the global OpenTelemetry configuration instance."""
248 global _ot_config
249 if _ot_config is None:
250 _ot_config = OpenTelemetryConfig()
251 return _ot_config
254def setup_opentelemetry() -> None:
255 """
256 Set up OpenTelemetry for the ivatar application.
258 This function should be called during Django application startup.
259 """
260 global _ot_initialized
262 if _ot_initialized:
263 logger.debug("OpenTelemetry already initialized, skipping setup")
264 return
266 logger.info("Setting up OpenTelemetry...")
268 ot_config = get_ot_config()
269 ot_config.setup_tracing()
270 ot_config.setup_metrics()
271 ot_config.setup_instrumentation()
273 if ot_config.enabled:
274 if ot_config.export_enabled:
275 logger.info("OpenTelemetry setup completed successfully (export enabled)")
276 else:
277 logger.info("OpenTelemetry setup completed successfully (export disabled)")
278 _ot_initialized = True
279 else:
280 logger.info("OpenTelemetry setup failed")
283def get_tracer(name: str) -> trace.Tracer:
284 """Get a tracer instance for the given name."""
285 return get_ot_config().get_tracer(name)
288def get_meter(name: str) -> metrics.Meter:
289 """Get a meter instance for the given name."""
290 return get_ot_config().get_meter(name)
293def is_enabled() -> bool:
294 """Check if OpenTelemetry is enabled (always True now)."""
295 return True
298def is_export_enabled() -> bool:
299 """Check if OpenTelemetry data export is enabled."""
300 return get_ot_config().export_enabled