Skip to content

Commit 0716c41

Browse files
committed
Add OpenTelemetry telemetry support via audit event to span mapping
- Introduce OTelAuditSink that converts AuditLogger events into OTel spans (agent.run root span, tool.call child spans with attributes) - Add TracingHTTPTransport to instrument LLM adapter HTTP requests - Support OTLP HTTP/protobuf export and console debugging via TelemetryConfig - Zero-overhead when opentelemetry packages are not installed (HAS_OTEL = False) - Add --telemetry-otlp-endpoint, --telemetry-service-name, --telemetry-console flags to agent run CLI
1 parent 38a424a commit 0716c41

5 files changed

Lines changed: 663 additions & 0 deletions

File tree

pyproject.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@ graphqlite = [
1717
oauth = [
1818
"cryptography>=3.4",
1919
]
20+
telemetry = [
21+
"opentelemetry-api>=1.20",
22+
"opentelemetry-sdk>=1.20",
23+
"opentelemetry-exporter-otlp-proto-http>=1.20",
24+
]
2025
dev = [
2126
"graphqlite>=0.4.4",
2227
"pysqlite3>=0.6.0",

teaagent/__init__.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,14 @@
8888
StatelessMCPResponse,
8989
handle_stateless_tool_request,
9090
)
91+
from teaagent.telemetry import (
92+
HAS_OTEL,
93+
OTelAuditSink,
94+
TelemetryConfig,
95+
TelemetryNotAvailable,
96+
TracingHTTPTransport,
97+
configure_telemetry,
98+
)
9199
from teaagent.tools import ToolAnnotations, ToolRegistry
92100
from teaagent.trace import TraceRecorder
93101
from teaagent.ultrawork import UltraworkStore, WorkerRecord
@@ -151,6 +159,12 @@
151159
'UnsafeCodeError',
152160
'WorkerRecord',
153161
'WorkspaceToolConfig',
162+
'HAS_OTEL',
163+
'OTelAuditSink',
164+
'TelemetryConfig',
165+
'TelemetryNotAvailable',
166+
'TracingHTTPTransport',
167+
'configure_telemetry',
154168
'agentic_retrieve',
155169
'assess_managed_agent_readiness',
156170
'assess_provider_portability',

teaagent/cli.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,22 @@ def build_parser() -> argparse.ArgumentParser:
168168
default=0.0,
169169
help='Emit a heartbeat audit event every N seconds while running. 0 disables.',
170170
)
171+
agent_run.add_argument(
172+
'--telemetry-otlp-endpoint',
173+
default=None,
174+
metavar='URL',
175+
help='Export OpenTelemetry traces to this OTLP HTTP endpoint (e.g. http://localhost:4318/v1/traces).',
176+
)
177+
agent_run.add_argument(
178+
'--telemetry-service-name',
179+
default='teaagent',
180+
help='OTel service.name resource attribute. Default: teaagent.',
181+
)
182+
agent_run.add_argument(
183+
'--telemetry-console',
184+
action='store_true',
185+
help='Also print OpenTelemetry spans to stderr (debug).',
186+
)
171187
agent_run.set_defaults(func=agent_run_task)
172188

173189
agent_preflight = agent_subparsers.add_parser(
@@ -638,6 +654,33 @@ def _execute_agent_task(
638654
adapter = create_llm_adapter(args.provider, model=selected_model)
639655
store = RunStore(args.root)
640656
audit = store.audit_logger()
657+
658+
# --- OpenTelemetry wiring ---
659+
_telemetry_sink = None
660+
if getattr(args, 'telemetry_otlp_endpoint', None) or getattr(args, 'telemetry_console', False):
661+
try:
662+
from teaagent.telemetry import (
663+
TelemetryConfig,
664+
TracingHTTPTransport,
665+
configure_telemetry,
666+
)
667+
668+
cfg = TelemetryConfig(
669+
service_name=getattr(args, 'telemetry_service_name', 'teaagent'),
670+
otlp_endpoint=getattr(args, 'telemetry_otlp_endpoint', None),
671+
console=getattr(args, 'telemetry_console', False),
672+
)
673+
_telemetry_sink, tracer = configure_telemetry(cfg)
674+
audit.add_sink(_telemetry_sink.handle_event)
675+
adapter = create_llm_adapter(
676+
args.provider,
677+
model=selected_model,
678+
transport=TracingHTTPTransport(adapter.transport, tracer), # type: ignore[attr-defined]
679+
)
680+
except Exception as exc:
681+
print(f'Telemetry setup failed: {exc}', file=sys.stderr)
682+
# --- end telemetry wiring ---
683+
641684
approval_handler = cli_approval_handler if args.hitl_approval else None
642685
result = run_chat_agent(
643686
task=task,
@@ -660,6 +703,11 @@ def _execute_agent_task(
660703
initial_observations=initial_observations,
661704
)
662705
store.logger_for_result(result, audit)
706+
# Shut down OTel to flush any pending spans.
707+
if _telemetry_sink is not None:
708+
from contextlib import suppress
709+
with suppress(Exception):
710+
_telemetry_sink.force_flush()
663711
payload = run_result_payload(result, routing=routing.to_dict() if routing else None)
664712
if resumed_from:
665713
payload['resumed_from'] = resumed_from

0 commit comments

Comments
 (0)