@anvia/otel

OpenTelemetry tracing for Anvia agents.

@anvia/otel sends agent lifecycle events to any OpenTelemetry-compatible backend (Jaeger, Zipkin, Grafana Tempo, Datadog, etc.). Use it when you want full control over your tracing pipeline.

Install

pnpm add @anvia/otel

@opentelemetry/api is a transitive dependency. You need to configure an OpenTelemetry exporter separately.

Quick Start

import { AgentBuilder } from "@anvia/core";
import { OpenAIClient } from "@anvia/openai";
import { otel } from "@anvia/otel";

const tracing = otel.create({
  tracerName: "my-app",
  serviceName: "support-service",
});

const client = new OpenAIClient({ apiKey });
const agent = new AgentBuilder("support", client.completionModel())
  .instructions("Answer support questions.")
  .observe(tracing)
  .build();

const response = await agent
  .prompt("How do I reset my password?")
  .withTrace({
    name: "support-question",
    userId: "user_123",
    sessionId: "session_456",
  })
  .send();

Configuration

type OtelTracingOptions = {
  tracer?: Tracer;       // pre-configured OpenTelemetry Tracer
  tracerName?: string;   // tracer name (default: "@anvia/otel")
  tracerVersion?: string; // tracer version
  serviceName?: string;  // service.name resource attribute
};
// Minimal
const tracing = otel.create();

// With service name
const tracing = otel.create({
  tracerName: "my-app",
  serviceName: "support-service",
});

// Pre-configured tracer
import { trace } from "@opentelemetry/api";
const tracing = otel.create({
  tracer: trace.getTracer("my-app"),
});

Setting Up an Exporter

@anvia/otel creates spans but does not configure an exporter. You need to set up the OpenTelemetry SDK exporter in your application:

import { NodeSDK } from "@opentelemetry/sdk-node";
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";

const sdk = new NodeSDK({
  traceExporter: new OTLPTraceExporter({
    url: "http://localhost:4318/v1/traces",
  }),
});

sdk.start();

Or use environment variables:

OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318
OTEL_SERVICE_NAME=my-app

Span Structure

Each agent run produces this span tree:

agent.{name}                          (root span)
  ├── model.turn.0                    (generation span)
  ├── tool.{tool_name}                (tool span)
  │     ├── {child_agent}.run         (child agent span, if applicable)
  │     │     ├── {child}.model.turn.0
  │     │     └── {child}.{tool}
  │     └── {child}.{tool_name}       (child tool span)
  └── model.turn.1                    (final generation)

Span Attributes

The observer sets these attributes on spans:

Run spans (agent.*)

  • anvia.agent.name, anvia.agent.description, anvia.agent.instructions
  • anvia.run.max_turns, anvia.run.prompt, anvia.run.history
  • anvia.run.output, anvia.run.messages
  • anvia.trace.name, anvia.trace.user_id, anvia.trace.session_id, anvia.trace.tags
  • anvia.usage.input_tokens, anvia.usage.output_tokens, anvia.usage.total_tokens

Generation spans (model.turn.*)

  • anvia.generation.turn, anvia.generation.model
  • anvia.generation.input, anvia.generation.output, anvia.generation.output_text
  • anvia.generation.tool_count, anvia.generation.has_output_schema
  • anvia.generation.temperature, anvia.generation.max_tokens, anvia.generation.tool_choice
  • anvia.generation.first_delta_ms
  • anvia.usage.* (per-generation usage)

Tool spans (tool.*)

  • anvia.tool.name, anvia.tool.turn, anvia.tool.args, anvia.tool.result
  • anvia.tool.call, anvia.tool.call_id, anvia.tool.internal_call_id
  • anvia.tool.skipped

Parent Trace Linking

If a traceId is provided via .withTrace(...), the observer creates a remote parent context so the agent run span links to an existing trace. This is useful when the agent is called from within a larger distributed trace.

Child Agent Tracing

When an agent uses another agent as a tool, the observer creates nested spans under the tool span. Child agent events (generations, tool calls) are automatically captured as sub-spans.

Error Handling

Observer failures are swallowed by default so tracing does not break application behavior. Use strict mode in tests:

const agent = new AgentBuilder("support", model)
  .observe(tracing, { failOnObserverError: true })
  .build();

Langfuse vs OTEL

Aspect@anvia/langfuse@anvia/otel
BackendLangfuse onlyAny OTEL-compatible backend
SetupOne-line langfuse.create(...)Requires OTEL SDK + exporter config
ScoringBuilt-in tracing.score(...)Use your backend's scoring API
Eval reportingBuilt-in createLangfuseEvalReporterCustom reporter needed
UILangfuse dashboardYour chosen backend's UI

Use @anvia/langfuse for a batteries-included experience. Use @anvia/otel when you already have an OpenTelemetry pipeline or need vendor flexibility.