"""
OpenTelemetry setup (safe, optional, idempotent).
This module configures tracing (OTLP exporter) and auto-instrumentation for:
- Flask (when a Flask app instance is provided)
- requests (HTTP client)
- PyMongo
Design goals:
- Fail-open: if OpenTelemetry packages are not installed or misconfigured,
setup_telemetry silently returns without raising.
- Idempotent: multiple calls are safe; only the first successful init takes effect.
- Non-invasive: avoid changing existing structlog configuration; we rely on
observability._add_otel_ids to inject trace_id/span_id when tracing is active.
"""
from __future__ import annotations
from typing import Any
import os
_OTEL_INITIALIZED: bool = False
def _str2bool(value: str | None) -> bool:
if value is None:
return False
return value.strip().lower() in {"1", "true", "yes", "y", "on"}
[תיעוד]
def setup_telemetry(
*,
service_name: str = "codebot-service",
service_version: str | None = None,
environment: str | None = None,
flask_app: Any | None = None,
) -> None:
"""Initialize OpenTelemetry tracing and basic instrumentation.
Parameters
----------
service_name: str
Logical service name for resource attributes.
service_version: Optional[str]
Version string reported with resource.
environment: Optional[str]
Environment hint (e.g., production/staging/development).
flask_app: Optional[Flask]
Flask app instance for explicit instrumentation; if None, Flask
instrumentation is skipped.
"""
global _OTEL_INITIALIZED
if _OTEL_INITIALIZED:
return
# Import inside the function to keep the dependency optional
try:
from opentelemetry import trace, metrics
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.sdk.resources import Resource
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import (
OTLPSpanExporter,
)
# Metrics are optional; configure provider if exporter(s) are available
try:
from opentelemetry.sdk.metrics import MeterProvider
from opentelemetry.sdk.metrics.export import (
PeriodicExportingMetricReader,
)
from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import (
OTLPMetricExporter,
)
_METRICS_AVAILABLE = True
except Exception:
_METRICS_AVAILABLE = False
# Instrumentations (all optional)
try:
from opentelemetry.instrumentation.requests import (
RequestsInstrumentor,
)
except Exception:
RequestsInstrumentor = None
try:
from opentelemetry.instrumentation.flask import (
FlaskInstrumentor,
)
except Exception:
FlaskInstrumentor = None
try:
from opentelemetry.instrumentation.pymongo import (
PymongoInstrumentor,
)
except Exception:
PymongoInstrumentor = None
# ----- Resource -----
resource_attrs = {
"service.name": service_name,
}
if service_version:
resource_attrs["service.version"] = service_version
env_val = (environment or os.getenv("ENVIRONMENT") or os.getenv("ENV") or "production").strip()
resource_attrs["deployment.environment"] = env_val
resource_attrs["deployment.type"] = os.getenv("DEPLOYMENT_TYPE", "render")
resource = Resource.create(resource_attrs)
# ----- Tracing -----
# אין להשתמש ב-localhost כברירת מחדל בעננים (Render וכד')
# אם לא הוגדר OTEL_EXPORTER_OTLP_ENDPOINT – דלג בשקט על אתחול OTel
endpoint = os.getenv("OTEL_EXPORTER_OTLP_ENDPOINT") or ""
insecure = _str2bool(os.getenv("OTEL_EXPORTER_INSECURE", "false"))
if endpoint.strip():
span_exporter = OTLPSpanExporter(endpoint=endpoint, insecure=insecure)
tracer_provider = TracerProvider(resource=resource)
tracer_provider.add_span_processor(BatchSpanProcessor(span_exporter))
trace.set_tracer_provider(tracer_provider)
# ----- Metrics (best-effort) -----
# Guard metrics initialization behind explicit feature flags.
#
# - OTLP metrics exporter requires a valid endpoint to avoid noisy retries to localhost:4317.
# - Prometheus exporter does NOT require an OTLP endpoint (scraped via /metrics).
if _METRICS_AVAILABLE:
try:
enable_metrics = _str2bool(os.getenv("ENABLE_METRICS", "false"))
except Exception:
enable_metrics = False
try:
enable_prometheus = _str2bool(os.getenv("ENABLE_PROMETHEUS_METRICS", "false")) or _str2bool(
os.getenv("ENABLE_PROMETHEUS_OTEL_METRICS", "false")
)
except Exception:
enable_prometheus = False
metric_readers = []
# OTLP exporter (push)
if enable_metrics and endpoint.strip():
try:
metric_exporter = OTLPMetricExporter(endpoint=endpoint, insecure=insecure)
metric_reader = PeriodicExportingMetricReader(
metric_exporter, export_interval_millis=60000
)
metric_readers.append(metric_reader)
except Exception:
# Metrics are optional – ignore exporter/transport errors
pass
# Prometheus exporter (pull/scrape)
if enable_prometheus:
try:
from opentelemetry.exporter.prometheus import PrometheusMetricReader # type: ignore
metric_readers.append(PrometheusMetricReader())
except Exception:
# Prometheus exporter is optional and may not be installed
pass
if metric_readers:
try:
meter_provider = MeterProvider(resource=resource, metric_readers=metric_readers)
metrics.set_meter_provider(meter_provider)
except Exception:
pass
# ----- Auto-instrumentation -----
try:
if RequestsInstrumentor is not None:
RequestsInstrumentor().instrument()
except Exception:
pass
try:
if FlaskInstrumentor is not None and flask_app is not None:
# Prefer explicit instrumentation of the passed app instance
FlaskInstrumentor().instrument_app(flask_app)
except Exception:
pass
try:
if PymongoInstrumentor is not None:
PymongoInstrumentor().instrument()
except Exception:
pass
_OTEL_INITIALIZED = True
except Exception:
# Entire telemetry stack is optional – fail-open
return
__all__ = ["setup_telemetry"]