Coverage for ivatar/opentelemetry_config.py: 81%
139 statements
« prev ^ index » next coverage.py v7.11.0, created at 2025-10-24 23:06 +0000
« prev ^ index » next coverage.py v7.11.0, created at 2025-10-24 23:06 +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
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 return Resource.create(
63 {
64 "service.name": self.service_name,
65 "service.version": os.environ.get("IVATAR_VERSION", "1.8.0"),
66 "service.namespace": "libravatar",
67 "deployment.environment": self.environment,
68 "service.instance.id": os.environ.get("HOSTNAME", "unknown"),
69 }
70 )
72 def setup_tracing(self) -> None:
73 """Set up OpenTelemetry tracing."""
74 try:
75 # Only set up tracing if export is enabled
76 if not self.export_enabled:
77 logger.info("OpenTelemetry tracing disabled (export disabled)")
78 return
80 # Set up tracer provider
81 trace.set_tracer_provider(TracerProvider(resource=self.resource))
82 tracer_provider = trace.get_tracer_provider()
84 # Configure OTLP exporter if endpoint is provided
85 otlp_endpoint = os.environ.get("OTEL_EXPORTER_OTLP_ENDPOINT")
86 if otlp_endpoint:
87 otlp_exporter = OTLPSpanExporter(endpoint=otlp_endpoint)
88 span_processor = BatchSpanProcessor(otlp_exporter)
89 tracer_provider.add_span_processor(span_processor)
90 logger.info(
91 f"OpenTelemetry tracing configured with OTLP endpoint: {otlp_endpoint}"
92 )
93 else:
94 logger.info("OpenTelemetry tracing configured without OTLP endpoint")
96 except Exception as e:
97 logger.error(f"Failed to setup OpenTelemetry tracing: {e}")
98 # Don't disable OpenTelemetry entirely - metrics and instrumentation can still work
100 def setup_metrics(self) -> None:
101 """Set up OpenTelemetry metrics."""
102 try:
103 # Configure metric readers based on environment
104 metric_readers = []
106 # Configure OTLP exporter if export is enabled and endpoint is provided
107 if self.export_enabled:
108 otlp_endpoint = os.environ.get("OTEL_EXPORTER_OTLP_ENDPOINT")
109 if otlp_endpoint:
110 otlp_exporter = OTLPMetricExporter(endpoint=otlp_endpoint)
111 metric_reader = PeriodicExportingMetricReader(otlp_exporter)
112 metric_readers.append(metric_reader)
113 logger.info(
114 f"OpenTelemetry metrics configured with OTLP endpoint: {otlp_endpoint}"
115 )
117 # For development/local testing, also configure Prometheus HTTP server
118 # In production, metrics are scraped by external Prometheus server
119 prometheus_endpoint = os.environ.get("OTEL_PROMETHEUS_ENDPOINT")
120 if prometheus_endpoint:
121 prometheus_reader = PrometheusMetricReader()
122 metric_readers.append(prometheus_reader)
124 # Set up meter provider with readers
125 meter_provider = MeterProvider(
126 resource=self.resource, metric_readers=metric_readers
127 )
129 # Only set meter provider if it's not already set
130 try:
131 metrics.set_meter_provider(meter_provider)
132 except Exception as e:
133 if "Overriding of current MeterProvider is not allowed" in str(e):
134 logger.warning("MeterProvider already set, using existing provider")
135 # Get the existing meter provider and add our readers
136 existing_provider = metrics.get_meter_provider()
137 if hasattr(existing_provider, "add_metric_reader"):
138 for reader in metric_readers:
139 existing_provider.add_metric_reader(reader)
140 else:
141 raise
143 # Start Prometheus HTTP server for local development (if configured)
144 if prometheus_endpoint:
145 self._start_prometheus_server(prometheus_reader, prometheus_endpoint)
146 logger.info(
147 f"OpenTelemetry metrics configured with Prometheus endpoint: {prometheus_endpoint}"
148 )
150 if not metric_readers:
151 logger.warning(
152 "No metric readers configured - metrics will not be exported"
153 )
155 except Exception as e:
156 logger.error(f"Failed to setup OpenTelemetry metrics: {e}")
157 # Don't disable OpenTelemetry entirely - tracing and instrumentation can still work
159 def _start_prometheus_server(
160 self, prometheus_reader: PrometheusMetricReader, endpoint: str
161 ) -> None:
162 """Start Prometheus HTTP server for metrics endpoint."""
163 try:
164 from prometheus_client import start_http_server, REGISTRY
166 # Parse endpoint to get host and port
167 if ":" in endpoint:
168 host, port = endpoint.split(":", 1)
169 port = int(port)
170 else:
171 host = "0.0.0.0"
172 port = int(endpoint)
174 # Register the PrometheusMetricReader collector with prometheus_client
175 REGISTRY.register(prometheus_reader._collector)
177 # Start HTTP server
178 start_http_server(port, addr=host)
180 logger.info(f"Prometheus metrics server started on {host}:{port}")
182 except OSError as e:
183 if e.errno == 98: # Address already in use
184 logger.warning(
185 f"Prometheus metrics server already running on {endpoint}"
186 )
187 else:
188 logger.error(f"Failed to start Prometheus metrics server: {e}")
189 # Don't disable OpenTelemetry entirely - metrics can still be exported via OTLP
190 except Exception as e:
191 logger.error(f"Failed to start Prometheus metrics server: {e}")
192 # Don't disable OpenTelemetry entirely - metrics can still be exported via OTLP
194 def setup_instrumentation(self) -> None:
195 """Set up OpenTelemetry instrumentation for various libraries."""
196 try:
197 # Django instrumentation - TEMPORARILY DISABLED TO TEST HEADER ISSUE
198 # DjangoInstrumentor().instrument()
199 # logger.info("Django instrumentation enabled")
201 # Database instrumentation
202 Psycopg2Instrumentor().instrument()
203 PyMySQLInstrumentor().instrument()
204 logger.info("Database instrumentation enabled")
206 # HTTP client instrumentation
207 RequestsInstrumentor().instrument()
208 URLLib3Instrumentor().instrument()
209 logger.info("HTTP client instrumentation enabled")
211 # Note: Memcached instrumentation not available in OpenTelemetry Python
212 # Cache operations will be traced through Django instrumentation
214 except Exception as e:
215 logger.error(f"Failed to setup OpenTelemetry instrumentation: {e}")
216 # Don't disable OpenTelemetry entirely - tracing and metrics can still work
218 def get_tracer(self, name: str) -> trace.Tracer:
219 """Get a tracer instance."""
220 return trace.get_tracer(name)
222 def get_meter(self, name: str) -> metrics.Meter:
223 """Get a meter instance."""
224 return metrics.get_meter(name)
227# Global OpenTelemetry configuration instance (lazy-loaded)
228_ot_config = None
229_ot_initialized = False
232def get_ot_config():
233 """Get the global OpenTelemetry configuration instance."""
234 global _ot_config
235 if _ot_config is None:
236 _ot_config = OpenTelemetryConfig()
237 return _ot_config
240def setup_opentelemetry() -> None:
241 """
242 Set up OpenTelemetry for the ivatar application.
244 This function should be called during Django application startup.
245 """
246 global _ot_initialized
248 if _ot_initialized:
249 logger.debug("OpenTelemetry already initialized, skipping setup")
250 return
252 logger.info("Setting up OpenTelemetry...")
254 ot_config = get_ot_config()
255 ot_config.setup_tracing()
256 ot_config.setup_metrics()
257 ot_config.setup_instrumentation()
259 if ot_config.enabled:
260 if ot_config.export_enabled:
261 logger.info("OpenTelemetry setup completed successfully (export enabled)")
262 else:
263 logger.info("OpenTelemetry setup completed successfully (export disabled)")
264 _ot_initialized = True
265 else:
266 logger.info("OpenTelemetry setup failed")
269def get_tracer(name: str) -> trace.Tracer:
270 """Get a tracer instance for the given name."""
271 return get_ot_config().get_tracer(name)
274def get_meter(name: str) -> metrics.Meter:
275 """Get a meter instance for the given name."""
276 return get_ot_config().get_meter(name)
279def is_enabled() -> bool:
280 """Check if OpenTelemetry is enabled (always True now)."""
281 return True
284def is_export_enabled() -> bool:
285 """Check if OpenTelemetry data export is enabled."""
286 return get_ot_config().export_enabled