fischer-agentkit/src/agentkit/telemetry/tracer.py

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