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

1""" 

2OpenTelemetry configuration for ivatar project. 

3 

4This module provides OpenTelemetry setup and configuration for the ivatar 

5Django application, including tracing, metrics, and logging integration. 

6""" 

7 

8import os 

9import logging 

10 

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 

24 

25from django.conf import settings 

26from django.core.exceptions import ImproperlyConfigured 

27 

28# Note: Memcached instrumentation not available in OpenTelemetry Python 

29 

30logger = logging.getLogger("ivatar") 

31 

32 

33class OpenTelemetryConfig: 

34 """ 

35 OpenTelemetry configuration manager for ivatar. 

36 

37 Handles setup of tracing, metrics, and instrumentation for the Django application. 

38 """ 

39 

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() 

46 

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 ) 

54 

55 def _get_service_name(self) -> str: 

56 """Get service name from environment or default.""" 

57 return os.environ.get("OTEL_SERVICE_NAME", "ivatar") 

58 

59 def _get_environment(self) -> str: 

60 """Get environment name (production, development, etc.).""" 

61 return os.environ.get("OTEL_ENVIRONMENT", "development") 

62 

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" 

75 

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 ) 

85 

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 

93 

94 # Set up tracer provider 

95 trace.set_tracer_provider(TracerProvider(resource=self.resource)) 

96 tracer_provider = trace.get_tracer_provider() 

97 

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") 

109 

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 

113 

114 def setup_metrics(self) -> None: 

115 """Set up OpenTelemetry metrics.""" 

116 try: 

117 # Configure metric readers based on environment 

118 metric_readers = [] 

119 

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 ) 

130 

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) 

137 

138 # Set up meter provider with readers 

139 meter_provider = MeterProvider( 

140 resource=self.resource, metric_readers=metric_readers 

141 ) 

142 

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 

156 

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 ) 

163 

164 if not metric_readers: 

165 logger.warning( 

166 "No metric readers configured - metrics will not be exported" 

167 ) 

168 

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 

172 

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 

179 

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) 

187 

188 # Register the PrometheusMetricReader collector with prometheus_client 

189 REGISTRY.register(prometheus_reader._collector) 

190 

191 # Start HTTP server 

192 start_http_server(port, addr=host) 

193 

194 logger.info(f"Prometheus metrics server started on {host}:{port}") 

195 

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 

207 

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") 

214 

215 # Database instrumentation 

216 Psycopg2Instrumentor().instrument() 

217 PyMySQLInstrumentor().instrument() 

218 logger.info("Database instrumentation enabled") 

219 

220 # HTTP client instrumentation 

221 RequestsInstrumentor().instrument() 

222 URLLib3Instrumentor().instrument() 

223 logger.info("HTTP client instrumentation enabled") 

224 

225 # Note: Memcached instrumentation not available in OpenTelemetry Python 

226 # Cache operations will be traced through Django instrumentation 

227 

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 

231 

232 def get_tracer(self, name: str) -> trace.Tracer: 

233 """Get a tracer instance.""" 

234 return trace.get_tracer(name) 

235 

236 def get_meter(self, name: str) -> metrics.Meter: 

237 """Get a meter instance.""" 

238 return metrics.get_meter(name) 

239 

240 

241# Global OpenTelemetry configuration instance (lazy-loaded) 

242_ot_config = None 

243_ot_initialized = False 

244 

245 

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 

252 

253 

254def setup_opentelemetry() -> None: 

255 """ 

256 Set up OpenTelemetry for the ivatar application. 

257 

258 This function should be called during Django application startup. 

259 """ 

260 global _ot_initialized 

261 

262 if _ot_initialized: 

263 logger.debug("OpenTelemetry already initialized, skipping setup") 

264 return 

265 

266 logger.info("Setting up OpenTelemetry...") 

267 

268 ot_config = get_ot_config() 

269 ot_config.setup_tracing() 

270 ot_config.setup_metrics() 

271 ot_config.setup_instrumentation() 

272 

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") 

281 

282 

283def get_tracer(name: str) -> trace.Tracer: 

284 """Get a tracer instance for the given name.""" 

285 return get_ot_config().get_tracer(name) 

286 

287 

288def get_meter(name: str) -> metrics.Meter: 

289 """Get a meter instance for the given name.""" 

290 return get_ot_config().get_meter(name) 

291 

292 

293def is_enabled() -> bool: 

294 """Check if OpenTelemetry is enabled (always True now).""" 

295 return True 

296 

297 

298def is_export_enabled() -> bool: 

299 """Check if OpenTelemetry data export is enabled.""" 

300 return get_ot_config().export_enabled