Skip to main content

agentic_otel 0.3.1 copy "agentic_otel: ^0.3.1" to clipboard
agentic_otel: ^0.3.1 copied to clipboard

OpenTelemetry export for the agentic framework: agent, model and tool spans sent over OTLP to any backend, with the GenAI semantic conventions.

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.dropped says 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.

0
likes
160
points
407
downloads

Documentation

Documentation
API reference

Publisher

unverified uploader

Weekly Downloads

OpenTelemetry export for the agentic framework: agent, model and tool spans sent over OTLP to any backend, with the GenAI semantic conventions.

Repository (GitHub)
View/report issues
Contributing

Topics

#ai #agents #opentelemetry #observability #agentic

License

MIT (license)

Dependencies

agentic_core, http, meta

More

Packages that depend on agentic_otel