Coverage for ivatar/opentelemetry_config.py: 69%
162 statements
« prev ^ index » next coverage.py v7.14.2, created at 2026-06-21 23:07 +0000
« prev ^ index » next coverage.py v7.14.2, created at 2026-06-21 23: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
22from django.conf import settings
23from django.core.exceptions import ImproperlyConfigured
25# Note: Memcached instrumentation not available in OpenTelemetry Python
27logger = logging.getLogger("ivatar")
30class OpenTelemetryConfig:
31 """
32 OpenTelemetry configuration manager for ivatar.
34 Handles setup of tracing, metrics, and instrumentation for the Django application.
35 """
37 def __init__(self):
38 self.enabled = True # Always enable OpenTelemetry instrumentation
39 self.export_enabled = self._is_export_enabled()
40 self.service_name = self._get_service_name()
41 self.environment = self._get_environment()
42 self.resource = self._create_resource()
44 def _is_export_enabled(self) -> bool:
45 """Check if OpenTelemetry data export is enabled via environment variable."""
46 return os.environ.get("OTEL_EXPORT_ENABLED", "false").lower() in (
47 "true",
48 "1",
49 "yes",
50 )
52 def _get_service_name(self) -> str:
53 """Get service name from environment or default."""
54 return os.environ.get("OTEL_SERVICE_NAME", "ivatar")
56 def _get_environment(self) -> str:
57 """Get environment name (production, development, etc.)."""
58 return os.environ.get("OTEL_ENVIRONMENT", "development")
60 def _create_resource(self) -> Resource:
61 """Create OpenTelemetry resource with service information."""
62 # Get IVATAR_VERSION from environment or settings, handling case where
63 # Django settings might not be configured yet
64 ivatar_version = os.environ.get("IVATAR_VERSION")
65 if not ivatar_version:
66 # Try to access settings, but handle case where Django isn't configured
67 try:
68 ivatar_version = getattr(settings, "IVATAR_VERSION", "2.0")
69 except ImproperlyConfigured:
70 # Django settings not configured yet, use default
71 ivatar_version = "2.0"
73 return Resource.create(
74 {
75 "service.name": self.service_name,
76 "service.version": ivatar_version,
77 "service.namespace": "libravatar",
78 "deployment.environment": self.environment,
79 "service.instance.id": os.environ.get("HOSTNAME", "unknown"),
80 }
81 )
83 def setup_tracing(self) -> None:
84 """Set up OpenTelemetry tracing."""
85 try:
86 # Only set up tracing if export is enabled
87 if not self.export_enabled:
88 logger.info("OpenTelemetry tracing disabled (export disabled)")
89 return
91 # Set up tracer provider
92 trace.set_tracer_provider(TracerProvider(resource=self.resource))
93 tracer_provider = trace.get_tracer_provider()
95 # Configure OTLP exporter if endpoint is provided
96 otlp_endpoint = os.environ.get("OTEL_EXPORTER_OTLP_ENDPOINT")
97 if otlp_endpoint:
98 otlp_exporter = OTLPSpanExporter(endpoint=otlp_endpoint)
99 span_processor = BatchSpanProcessor(otlp_exporter)
100 tracer_provider.add_span_processor(span_processor)
101 logger.info(
102 f"OpenTelemetry tracing configured with OTLP endpoint: {otlp_endpoint}"
103 )
104 else:
105 logger.info("OpenTelemetry tracing configured without OTLP endpoint")
107 except Exception as e:
108 logger.error(f"Failed to setup OpenTelemetry tracing: {e}")
109 # Don't disable OpenTelemetry entirely - metrics and instrumentation can still work
111 def setup_metrics(self) -> None:
112 """Set up OpenTelemetry metrics."""
113 try:
114 # Configure metric readers based on environment
115 metric_readers = []
117 # Configure OTLP exporter if export is enabled and endpoint is provided
118 if self.export_enabled:
119 otlp_endpoint = os.environ.get("OTEL_EXPORTER_OTLP_ENDPOINT")
120 if otlp_endpoint:
121 otlp_exporter = OTLPMetricExporter(endpoint=otlp_endpoint)
122 metric_reader = PeriodicExportingMetricReader(otlp_exporter)
123 metric_readers.append(metric_reader)
124 logger.info(
125 f"OpenTelemetry metrics configured with OTLP endpoint: {otlp_endpoint}"
126 )
128 # For development/local testing, also configure Prometheus HTTP server
129 # In production, metrics are scraped by external Prometheus server
130 prometheus_endpoint = os.environ.get("OTEL_PROMETHEUS_ENDPOINT")
131 if prometheus_endpoint:
132 prometheus_reader = PrometheusMetricReader()
133 metric_readers.append(prometheus_reader)
135 # Set up meter provider with readers
136 meter_provider = MeterProvider(
137 resource=self.resource, metric_readers=metric_readers
138 )
140 # Only set meter provider if it's not already set
141 try:
142 metrics.set_meter_provider(meter_provider)
143 except Exception as e:
144 if "Overriding of current MeterProvider is not allowed" in str(e):
145 logger.warning("MeterProvider already set, using existing provider")
146 # Get the existing meter provider and add our readers
147 existing_provider = metrics.get_meter_provider()
148 if hasattr(existing_provider, "add_metric_reader"):
149 for reader in metric_readers:
150 existing_provider.add_metric_reader(reader)
151 else:
152 raise
154 # Start Prometheus HTTP server for local development (if configured)
155 if prometheus_endpoint:
156 self._start_prometheus_server(prometheus_reader, prometheus_endpoint)
157 logger.info(
158 f"OpenTelemetry metrics configured with Prometheus endpoint: {prometheus_endpoint}"
159 )
161 if not metric_readers:
162 logger.warning(
163 "No metric readers configured - metrics will not be exported"
164 )
166 except Exception as e:
167 logger.error(f"Failed to setup OpenTelemetry metrics: {e}")
168 # Don't disable OpenTelemetry entirely - tracing and instrumentation can still work
170 def _start_prometheus_server(
171 self, prometheus_reader: PrometheusMetricReader, endpoint: str
172 ) -> None:
173 """Start Prometheus HTTP server for metrics endpoint."""
174 try:
175 from prometheus_client import start_http_server, REGISTRY
177 # Parse endpoint to get host and port
178 if ":" in endpoint:
179 host, port = endpoint.split(":", 1)
180 port = int(port)
181 else:
182 host = "0.0.0.0"
183 port = int(endpoint)
185 # Register the PrometheusMetricReader collector with prometheus_client
186 REGISTRY.register(prometheus_reader._collector)
188 # Start HTTP server
189 start_http_server(port, addr=host)
191 logger.info(f"Prometheus metrics server started on {host}:{port}")
193 except OSError as e:
194 if e.errno == 98: # Address already in use
195 logger.warning(
196 f"Prometheus metrics server already running on {endpoint}"
197 )
198 else:
199 logger.error(f"Failed to start Prometheus metrics server: {e}")
200 # Don't disable OpenTelemetry entirely - metrics can still be exported via OTLP
201 except Exception as e:
202 logger.error(f"Failed to start Prometheus metrics server: {e}")
203 # Don't disable OpenTelemetry entirely - metrics can still be exported via OTLP
205 def setup_instrumentation(self) -> None:
206 """Set up OpenTelemetry instrumentation for various libraries."""
207 # Only set up instrumentation if export is enabled
208 if not self.export_enabled:
209 logger.info("OpenTelemetry instrumentation disabled (export disabled)")
210 return
212 try:
213 # Django instrumentation - TEMPORARILY DISABLED TO TEST HEADER ISSUE
214 # DjangoInstrumentor().instrument()
215 # logger.info("Django instrumentation enabled")
217 # Database instrumentation
218 try:
219 from opentelemetry.instrumentation.psycopg2 import Psycopg2Instrumentor
221 Psycopg2Instrumentor().instrument()
222 except ImportError:
223 logger.warning("Psycopg2 instrumentation not available")
225 try:
226 from opentelemetry.instrumentation.pymysql import PyMySQLInstrumentor
228 PyMySQLInstrumentor().instrument()
229 except ImportError:
230 logger.warning("PyMySQL instrumentation not available")
232 logger.info("Database instrumentation enabled")
234 # HTTP client instrumentation
235 try:
236 from opentelemetry.instrumentation.requests import RequestsInstrumentor
238 RequestsInstrumentor().instrument()
239 except ImportError:
240 logger.warning("Requests instrumentation not available")
242 try:
243 from opentelemetry.instrumentation.urllib3 import URLLib3Instrumentor
245 URLLib3Instrumentor().instrument()
246 except ImportError:
247 logger.warning("URLLib3 instrumentation not available")
249 logger.info("HTTP client instrumentation enabled")
251 # Note: Memcached instrumentation not available in OpenTelemetry Python
252 # Cache operations will be traced through Django instrumentation
254 except Exception as e:
255 logger.error(f"Failed to setup OpenTelemetry instrumentation: {e}")
256 # Don't disable OpenTelemetry entirely - tracing and metrics can still work
258 def get_tracer(self, name: str) -> trace.Tracer:
259 """Get a tracer instance."""
260 return trace.get_tracer(name)
262 def get_meter(self, name: str) -> metrics.Meter:
263 """Get a meter instance."""
264 return metrics.get_meter(name)
267# Global OpenTelemetry configuration instance (lazy-loaded)
268_ot_config = None
269_ot_initialized = False
272def get_ot_config():
273 """Get the global OpenTelemetry configuration instance."""
274 global _ot_config
275 if _ot_config is None:
276 _ot_config = OpenTelemetryConfig()
277 return _ot_config
280def setup_opentelemetry() -> None:
281 """
282 Set up OpenTelemetry for the ivatar application.
284 This function should be called during Django application startup.
285 """
286 global _ot_initialized
288 if _ot_initialized:
289 logger.debug("OpenTelemetry already initialized, skipping setup")
290 return
292 logger.info("Setting up OpenTelemetry...")
294 ot_config = get_ot_config()
295 ot_config.setup_tracing()
296 ot_config.setup_metrics()
297 ot_config.setup_instrumentation()
299 if ot_config.enabled:
300 if ot_config.export_enabled:
301 logger.info("OpenTelemetry setup completed successfully (export enabled)")
302 else:
303 logger.info("OpenTelemetry setup completed successfully (export disabled)")
304 _ot_initialized = True
305 else:
306 logger.info("OpenTelemetry setup failed")
309def get_tracer(name: str) -> trace.Tracer:
310 """Get a tracer instance for the given name."""
311 return get_ot_config().get_tracer(name)
314def get_meter(name: str) -> metrics.Meter:
315 """Get a meter instance for the given name."""
316 return get_ot_config().get_meter(name)
319def is_enabled() -> bool:
320 """Check if OpenTelemetry is enabled (always True now)."""
321 return True
324def is_export_enabled() -> bool:
325 """Check if OpenTelemetry data export is enabled."""
326 return get_ot_config().export_enabled