Coverage for agentos/observability/tracing.py: 0%
80 statements
« prev ^ index » next coverage.py v7.14.3, created at 2026-07-06 21:19 +0800
« prev ^ index » next coverage.py v7.14.3, created at 2026-07-06 21:19 +0800
1"""
2OpenTelemetry distributed tracing for AgentOS.
4Minimal setup — wraps standard OpenTelemetry SDK to trace:
5- HTTP requests (inbound via FastAPI middleware, outbound via httpx instrumentor)
6- Agent pipeline phases (PRE_LLM, POST_LLM, PRE_TOOL, POST_TOOL, PRE_EXEC, POST_EXEC)
7- Database queries (SQLAlchemy instrumentor)
9Usage:
10 from agentos.observability.tracing import setup_tracing, get_tracer
12 setup_tracing(service_name="agentos", otlp_endpoint="http://localhost:4317")
13 tracer = get_tracer(__name__)
15 with tracer.start_as_current_span("agent.run") as span:
16 span.set_attribute("agent.id", "agent-1")
17 # ... do work ...
19With env vars:
20 AGENTOS_OTLP_ENDPOINT=http://jaeger:4317
21 AGENTOS_TRACE_ENABLED=true
22"""
24from __future__ import annotations
26import logging
27import os
28from collections.abc import Callable
29from contextlib import contextmanager
30from functools import wraps
31from typing import Any
33logger = logging.getLogger(__name__)
35_tracer_provider: Any = None
36_TRACE_ENABLED: bool = os.environ.get("AGENTOS_TRACE_ENABLED", "").lower() == "true"
38try:
39 from opentelemetry import trace
40 from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
41 from opentelemetry.sdk.resources import SERVICE_NAME, Resource
42 from opentelemetry.sdk.trace import TracerProvider
43 from opentelemetry.sdk.trace.export import BatchSpanProcessor
45 _OTEL_AVAILABLE = True
46except ImportError:
47 _OTEL_AVAILABLE = False
48 logger.debug("opentelemetry not installed — tracing disabled")
51def setup_tracing(
52 service_name: str = "agentos",
53 otlp_endpoint: str | None = None,
54 sample_rate: float = 1.0,
55) -> None:
56 """Initialize OpenTelemetry tracing.
58 Args:
59 service_name: Logical service name for spans.
60 otlp_endpoint: OTLP gRPC collector endpoint (default: AGENTOS_OTLP_ENDPOINT env).
61 sample_rate: Trace sampling rate (1.0 = all).
62 """
63 global _tracer_provider, _TRACE_ENABLED
65 if not _OTEL_AVAILABLE:
66 logger.warning("OpenTelemetry SDK not available — tracing disabled")
67 return
69 endpoint = otlp_endpoint or os.environ.get("AGENTOS_OTLP_ENDPOINT", "")
70 if not endpoint:
71 logger.debug("No OTLP endpoint configured — tracing disabled")
72 return
74 resource = Resource(
75 attributes={
76 SERVICE_NAME: service_name,
77 "deployment.environment": os.environ.get("AGENTOS_ENV", "production"),
78 }
79 )
81 exporter = OTLPSpanExporter(endpoint=endpoint, insecure=True)
82 processor = BatchSpanProcessor(exporter)
84 _tracer_provider = TracerProvider(resource=resource)
85 _tracer_provider.add_span_processor(processor)
86 trace.set_tracer_provider(_tracer_provider)
88 _TRACE_ENABLED = True
89 logger.info(f"Tracing enabled → {endpoint}")
92def shutdown_tracing() -> None:
93 """Flush and shutdown the tracer provider."""
94 if _tracer_provider:
95 _tracer_provider.shutdown()
98def get_tracer(name: str = "agentos") -> Any:
99 """Get a tracer instance (falls back to no-op if OTel not configured)."""
100 if _OTEL_AVAILABLE and _TRACE_ENABLED:
101 return trace.get_tracer(name)
102 # No-op fallback
103 return _NoOpTracer()
106class _NoOpTracer:
107 """Drop-in replacement when tracing is disabled."""
109 @contextmanager
110 def start_as_current_span(self, name: str, **kwargs):
111 yield _NoOpSpan()
113 def start_span(self, name: str, **kwargs):
114 return _NoOpSpan()
117class _NoOpSpan:
118 def set_attribute(self, key: str, value: Any) -> None:
119 pass
121 def set_status(self, status: Any) -> None:
122 pass
124 def add_event(self, name: str, attributes: dict = None) -> None:
125 pass
127 def end(self) -> None:
128 pass
130 def __enter__(self):
131 return self
133 def __exit__(self, *args):
134 pass
137def trace_function(name: str | None = None, attributes: dict = None):
138 """Decorator to trace a function as a span."""
140 def decorator(fn: Callable):
141 span_name = name or f"{fn.__module__}.{fn.__qualname__}"
143 @wraps(fn)
144 def wrapper(*args, **kwargs):
145 tracer = get_tracer(fn.__module__)
146 with tracer.start_as_current_span(span_name) as span:
147 if attributes:
148 for k, v in attributes.items():
149 span.set_attribute(k, v)
150 return fn(*args, **kwargs)
152 return wrapper
154 return decorator
157def get_current_span() -> Any:
158 """Get the current active span (no-op safe)."""
159 if _OTEL_AVAILABLE and _TRACE_ENABLED:
160 return trace.get_current_span()
161 return _NoOpSpan()
164__all__ = [
165 "setup_tracing",
166 "shutdown_tracing",
167 "get_tracer",
168 "trace_function",
169 "get_current_span",
170]