145 lines
4.2 KiB
Python
145 lines
4.2 KiB
Python
"""OpenTelemetry tracer integration with no-op fallback."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from dataclasses import dataclass
|
|
from typing import ContextManager
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
@dataclass
|
|
class TelemetryConfig:
|
|
"""Telemetry configuration."""
|
|
|
|
enabled: bool = False
|
|
otlp_endpoint: str = ""
|
|
service_name: str = "agentkit"
|
|
sample_rate: float = 1.0
|
|
|
|
|
|
class NoOpSpan:
|
|
"""No-op span when telemetry is disabled."""
|
|
|
|
def __enter__(self):
|
|
return self
|
|
|
|
def __exit__(self, *args):
|
|
pass
|
|
|
|
def set_attribute(self, key: str, value: object) -> None:
|
|
pass
|
|
|
|
def add_event(self, name: str, attributes: dict[str, object] | None = None) -> None:
|
|
pass
|
|
|
|
def record_exception(self, exception: Exception) -> None:
|
|
pass
|
|
|
|
def is_recording(self) -> bool:
|
|
return False
|
|
|
|
|
|
class NoOpTracer:
|
|
"""No-op tracer when telemetry is disabled."""
|
|
|
|
def start_span(
|
|
self, name: str, attributes: dict[str, object] | None = None
|
|
) -> ContextManager[NoOpSpan]:
|
|
return NoOpSpan()
|
|
|
|
def start_as_current_span(
|
|
self, name: str, attributes: dict[str, object] | None = None
|
|
) -> ContextManager[NoOpSpan]:
|
|
return NoOpSpan()
|
|
|
|
|
|
class OTelSpan:
|
|
"""Wrapper around OpenTelemetry Span."""
|
|
|
|
def __init__(self, span: object):
|
|
self._span = span
|
|
|
|
def __enter__(self):
|
|
self._span.__enter__()
|
|
return self
|
|
|
|
def __exit__(self, *args):
|
|
self._span.__exit__(*args)
|
|
|
|
def set_attribute(self, key: str, value: object) -> None:
|
|
self._span.set_attribute(key, value)
|
|
|
|
def add_event(self, name: str, attributes: dict[str, object] | None = None) -> None:
|
|
self._span.add_event(name, attributes or {})
|
|
|
|
def record_exception(self, exception: Exception) -> None:
|
|
self._span.record_exception(exception)
|
|
|
|
def is_recording(self) -> bool:
|
|
return self._span.is_recording()
|
|
|
|
|
|
class OTelTracer:
|
|
"""Wrapper around OpenTelemetry Tracer."""
|
|
|
|
def __init__(self, tracer: object):
|
|
self._tracer = tracer
|
|
|
|
def start_span(self, name: str, attributes: dict[str, object] | None = None) -> OTelSpan:
|
|
span = self._tracer.start_span(name, attributes=attributes)
|
|
return OTelSpan(span)
|
|
|
|
def start_as_current_span(
|
|
self, name: str, attributes: dict[str, object] | None = None
|
|
) -> OTelSpan:
|
|
span = self._tracer.start_as_current_span(name, attributes=attributes)
|
|
return OTelSpan(span)
|
|
|
|
|
|
# Global tracer instance
|
|
_tracer: NoOpTracer | OTelTracer = NoOpTracer()
|
|
|
|
|
|
def get_tracer() -> NoOpTracer | OTelTracer:
|
|
"""Get the global tracer instance."""
|
|
return _tracer
|
|
|
|
|
|
def init_telemetry(config: TelemetryConfig) -> None:
|
|
"""Initialize telemetry with the given configuration.
|
|
|
|
U1: OTel deps now required (pyproject.toml). ImportError fallback removed —
|
|
a missing opentelemetry-* package is now a hard install error, not a silent
|
|
monitoring blind spot. Runtime errors (network, exporter connect) still
|
|
fall back to NoOpTracer so app startup never crashes on transient OTLP issues.
|
|
"""
|
|
global _tracer
|
|
|
|
if not config.enabled:
|
|
_tracer = NoOpTracer()
|
|
logger.info("Telemetry disabled, using no-op tracer")
|
|
return
|
|
|
|
from opentelemetry import trace
|
|
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
|
|
|
|
try:
|
|
resource = Resource.create({"service.name": config.service_name})
|
|
provider = TracerProvider(resource=resource)
|
|
|
|
if config.otlp_endpoint:
|
|
exporter = OTLPSpanExporter(endpoint=config.otlp_endpoint)
|
|
provider.add_span_processor(BatchSpanProcessor(exporter))
|
|
|
|
trace.set_tracer_provider(provider)
|
|
_tracer = OTelTracer(trace.get_tracer(config.service_name))
|
|
logger.info(f"Telemetry initialized with endpoint: {config.otlp_endpoint}")
|
|
except Exception as e:
|
|
logger.warning(f"Failed to initialize telemetry: {e}, using no-op tracer")
|
|
_tracer = NoOpTracer()
|