Add OpenTelemetry integration

This commit is contained in:
Oliver Falk
2025-10-16 15:22:54 +02:00
parent 2c5e7ab90f
commit a9021fc0f7
17 changed files with 2269 additions and 62 deletions

View File

@@ -0,0 +1,233 @@
# -*- coding: utf-8 -*-
"""
OpenTelemetry configuration for ivatar project.
This module provides OpenTelemetry setup and configuration for the ivatar
Django application, including tracing, metrics, and logging integration.
"""
import os
import logging
from opentelemetry import trace, metrics
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.sdk.metrics import MeterProvider
from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader
from opentelemetry.sdk.resources import Resource
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import OTLPMetricExporter
from opentelemetry.exporter.prometheus import PrometheusMetricReader
from opentelemetry.instrumentation.django import DjangoInstrumentor
from opentelemetry.instrumentation.psycopg2 import Psycopg2Instrumentor
from opentelemetry.instrumentation.pymysql import PyMySQLInstrumentor
from opentelemetry.instrumentation.requests import RequestsInstrumentor
from opentelemetry.instrumentation.urllib3 import URLLib3Instrumentor
# Note: Memcached instrumentation not available in OpenTelemetry Python
logger = logging.getLogger("ivatar")
class OpenTelemetryConfig:
"""
OpenTelemetry configuration manager for ivatar.
Handles setup of tracing, metrics, and instrumentation for the Django application.
"""
def __init__(self):
self.enabled = self._is_enabled()
self.service_name = self._get_service_name()
self.environment = self._get_environment()
self.resource = self._create_resource()
def _is_enabled(self) -> bool:
"""Check if OpenTelemetry is enabled via environment variable and Django settings."""
# First check Django settings (for F/LOSS deployments)
try:
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
try:
if getattr(settings, "ENABLE_OPENTELEMETRY", False):
return True
except ImproperlyConfigured:
# Django settings not configured yet, fall back to environment variable
pass
except ImportError:
# Django not available yet, fall back to environment variable
pass
# Then check OpenTelemetry-specific environment variable
return os.environ.get("OTEL_ENABLED", "false").lower() in ("true", "1", "yes")
def _get_service_name(self) -> str:
"""Get service name from environment or default."""
return os.environ.get("OTEL_SERVICE_NAME", "ivatar")
def _get_environment(self) -> str:
"""Get environment name (production, development, etc.)."""
return os.environ.get("OTEL_ENVIRONMENT", "development")
def _create_resource(self) -> Resource:
"""Create OpenTelemetry resource with service information."""
return Resource.create(
{
"service.name": self.service_name,
"service.version": os.environ.get("IVATAR_VERSION", "1.8.0"),
"service.namespace": "libravatar",
"deployment.environment": self.environment,
"service.instance.id": os.environ.get("HOSTNAME", "unknown"),
}
)
def setup_tracing(self) -> None:
"""Set up OpenTelemetry tracing."""
if not self.enabled:
logger.info("OpenTelemetry tracing disabled")
return
try:
# Set up tracer provider
trace.set_tracer_provider(TracerProvider(resource=self.resource))
tracer_provider = trace.get_tracer_provider()
# Configure OTLP exporter if endpoint is provided
otlp_endpoint = os.environ.get("OTEL_EXPORTER_OTLP_ENDPOINT")
if otlp_endpoint:
otlp_exporter = OTLPSpanExporter(endpoint=otlp_endpoint)
span_processor = BatchSpanProcessor(otlp_exporter)
tracer_provider.add_span_processor(span_processor)
logger.info(
f"OpenTelemetry tracing configured with OTLP endpoint: {otlp_endpoint}"
)
else:
logger.info("OpenTelemetry tracing configured without OTLP exporter")
except Exception as e:
logger.error(f"Failed to setup OpenTelemetry tracing: {e}")
self.enabled = False
def setup_metrics(self) -> None:
"""Set up OpenTelemetry metrics."""
if not self.enabled:
logger.info("OpenTelemetry metrics disabled")
return
try:
# Configure metric readers
metric_readers = []
# Configure Prometheus exporter for metrics
prometheus_endpoint = os.environ.get(
"OTEL_PROMETHEUS_ENDPOINT", "0.0.0.0:9464"
)
prometheus_reader = PrometheusMetricReader()
metric_readers.append(prometheus_reader)
# Configure OTLP exporter if endpoint is provided
otlp_endpoint = os.environ.get("OTEL_EXPORTER_OTLP_ENDPOINT")
if otlp_endpoint:
otlp_exporter = OTLPMetricExporter(endpoint=otlp_endpoint)
metric_reader = PeriodicExportingMetricReader(otlp_exporter)
metric_readers.append(metric_reader)
logger.info(
f"OpenTelemetry metrics configured with OTLP endpoint: {otlp_endpoint}"
)
# Set up meter provider with readers
meter_provider = MeterProvider(
resource=self.resource, metric_readers=metric_readers
)
metrics.set_meter_provider(meter_provider)
logger.info(
f"OpenTelemetry metrics configured with Prometheus endpoint: {prometheus_endpoint}"
)
except Exception as e:
logger.error(f"Failed to setup OpenTelemetry metrics: {e}")
self.enabled = False
def setup_instrumentation(self) -> None:
"""Set up OpenTelemetry instrumentation for various libraries."""
if not self.enabled:
logger.info("OpenTelemetry instrumentation disabled")
return
try:
# Django instrumentation
DjangoInstrumentor().instrument()
logger.info("Django instrumentation enabled")
# Database instrumentation
Psycopg2Instrumentor().instrument()
PyMySQLInstrumentor().instrument()
logger.info("Database instrumentation enabled")
# HTTP client instrumentation
RequestsInstrumentor().instrument()
URLLib3Instrumentor().instrument()
logger.info("HTTP client instrumentation enabled")
# Note: Memcached instrumentation not available in OpenTelemetry Python
# Cache operations will be traced through Django instrumentation
except Exception as e:
logger.error(f"Failed to setup OpenTelemetry instrumentation: {e}")
self.enabled = False
def get_tracer(self, name: str) -> trace.Tracer:
"""Get a tracer instance."""
return trace.get_tracer(name)
def get_meter(self, name: str) -> metrics.Meter:
"""Get a meter instance."""
return metrics.get_meter(name)
# Global OpenTelemetry configuration instance (lazy-loaded)
_ot_config = None
def get_ot_config():
"""Get the global OpenTelemetry configuration instance."""
global _ot_config
if _ot_config is None:
_ot_config = OpenTelemetryConfig()
return _ot_config
def setup_opentelemetry() -> None:
"""
Set up OpenTelemetry for the ivatar application.
This function should be called during Django application startup.
"""
logger.info("Setting up OpenTelemetry...")
ot_config = get_ot_config()
ot_config.setup_tracing()
ot_config.setup_metrics()
ot_config.setup_instrumentation()
if ot_config.enabled:
logger.info("OpenTelemetry setup completed successfully")
else:
logger.info("OpenTelemetry setup skipped (disabled)")
def get_tracer(name: str) -> trace.Tracer:
"""Get a tracer instance for the given name."""
return get_ot_config().get_tracer(name)
def get_meter(name: str) -> metrics.Meter:
"""Get a meter instance for the given name."""
return get_ot_config().get_meter(name)
def is_enabled() -> bool:
"""Check if OpenTelemetry is enabled."""
return get_ot_config().enabled