agentic_otel
OpenTelemetry export for the agentic framework. Agent, model and tool spans, sent over OTLP to any backend that speaks it.
dependencies:
agentic_otel: ^0.3.0
Why
The framework's tracing is OpenTelemetry-shaped, and until something exports it, it stays inside the process. That is fine while you are looking at a debug panel on your own device, and useless the moment a user reports that "it was slow yesterday".
import 'package:agentic_core/agentic_core.dart';
import 'package:agentic_otel/agentic_otel.dart';
final exporter = BatchSpanExporter(
OtlpHttpSpanExporter(
endpoint: Uri.parse('https://collector.example.com/v1/traces'),
headers: {'authorization': 'Bearer $token'},
resource: const OtlpResource(
serviceName: 'pocket_agent',
serviceVersion: '1.2.0',
deploymentEnvironment: 'production',
),
onError: (error, stack) => log.warning('trace export failed: $error'),
),
);
final context = AgenticContext.root(
tracer: Tracer(exporter: exporter, sampler: (name) => true),
);
Every agent run, model call and tool execution beneath that context now arrives
at your collector. Anything that reads OTLP works: Langfuse, Arize Phoenix,
Grafana Tempo, Honeycomb, Datadog, Google Cloud Trace, or a local
otel-collector in Docker.
Batching, because this runs on a phone
BatchSpanExporter buffers spans and sends one request instead of one per span.
On a device that is the difference between one TLS handshake and thirty, on the
same radio the model call is using.
BatchSpanExporter(
inner,
maxBatch: 128, // send when this many are waiting
maxQueue: 2048, // then drop the oldest
flushEvery: const Duration(seconds: 5), // and send a partial batch anyway
);
Two behaviours worth knowing:
- The oldest spans are dropped when the queue is full, not the newest. In a
backlog of stale telemetry the recent spans are the ones worth keeping.
exporter.droppedsays how many went, so the gap is visible. - A rejected batch is dropped rather than retried forever. Retrying into a
collector that is down turns one problem into an unbounded queue; the failure
has already been reported through
onError.
On Flutter, flush when the app is backgrounded — that is when the last spans of a run would otherwise be lost:
// In a WidgetsBindingObserver:
if (state == AppLifecycleState.paused) unawaited(exporter.flush());
dispose() flushes what is left.
Failures never reach your application
Telemetry that breaks a run is worse than no telemetry. A rejected export, a
timeout, a collector that is not running — all are reported through onError
and dropped. Nothing is thrown into the code that produced the span.
Naming spans so a backend understands them
A tracing backend renders an LLM span specially — model, tokens, cost — only
when it recognises the attribute names. GenAiAttributes holds the
OpenTelemetry GenAI convention keys, and three helpers build the usual sets:
await context.step('chat', (child, span) async {
span.setAttributes(genAiChatAttributes(
provider: 'anthropic',
requestModel: AnthropicModels.sonnet,
inputTokens: response.usage.promptTokens,
outputTokens: response.usage.completionTokens,
finishReason: response.finishReason.name,
));
return response;
});
genAiToolAttributes(toolName: 'issue_refund', callId: call.id);
genAiAgentAttributes(agentName: 'support', conversationId: session.id);
The conventions are still marked Development upstream, so they move. They live in one file here for that reason.
What this package does not do
It does not send prompts or completions. Message content is the most sensitive thing the framework touches, and a backend that shows it to everyone with a dashboard login is a decision an application makes deliberately. The attribute keys are there; attaching content is a line you write, after deciding what redaction applies.
It does not export metrics or logs. Spans carry the timings, the token counts and the failures, which is what a trace view needs.
It does not start a trace for you. Tracer does that. This package is the
exporter, so an application that already has its own transport can use
encodeSpans and post the JSON itself.
Joining a trace that started elsewhere
agentic_core reads and writes W3C trace-context headers, and the HTTP
transports send them, so a Flutter app calling your backend calling an MCP
server is one trace rather than three:
final incoming = TraceContext.fromHeaders(request.headers);
final context = AgenticContext.root(traceContext: incoming, tracer: tracer);
Licence
MIT.
Libraries
- agentic_otel
- OpenTelemetry export for the agentic framework.