Skip to content
Playground

OpenTelemetry Tracing

Last updated 1 day ago · fc28b6a

@reactive-agents/observe bridges the agent event bus to OpenInference-compliant OpenTelemetry spans. Every agent run automatically emits a span hierarchy — workflow → LLM calls → tool calls — that any OTLP-compatible backend can ingest.

Terminal window
npm install @reactive-agents/observe
# or
bun add @reactive-agents/observe

Set OTEL_EXPORTER_OTLP_ENDPOINT and call autoConfigureExporter before running agents:

import { autoConfigureExporter } from "@reactive-agents/observe"
import { OpenInferenceTracerLayer } from "@reactive-agents/observe"
// OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318 already set in env
const handle = autoConfigureExporter({ serviceName: "my-agent" })
// Wire tracer layer into your Effect runtime...
// (see "Effect integration" section below)
await handle.shutdown() // flush before process exit

autoConfigureExporter is a no-op when OTEL_EXPORTER_OTLP_ENDPOINT is not set — safe to ship in all environments.

Each agent run produces a nested span tree:

agent:my-agent-id ← openinference.span.kind = AGENT
llm:anthropic/claude-... ← openinference.span.kind = LLM
tool:web-search ← openinference.span.kind = TOOL
llm:anthropic/claude-...

All child spans share the workflow trace ID, so backends show the full call graph per invocation.

AttributeDescription
openinference.span.kindAGENT
llm.model_nameModel at start
llm.providerProvider name
agent.idAgent identifier
task.idTask correlation ID
agent.iterationsTotal reasoning loop iterations
llm.token_count.totalAggregate tokens across run
agent.successBoolean — false on error
AttributeDescription
openinference.span.kindLLM
llm.model_nameModel name
llm.providerProvider
llm.token_count.promptInput tokens
llm.token_count.completionOutput tokens
llm.token_count.totalTotal tokens
llm.estimated_cost_usdEstimated cost
llm.cachedtrue when served from prompt cache
llm.duration_msRound-trip latency
AttributeDescription
openinference.span.kindTOOL
tool.nameTool identifier
tool.parametersJSON-serialized arguments
tool.outputJSON-serialized result
agent.iterationReasoning loop iteration
tool.duration_msExecution latency
tool.successBoolean — false on error

OpenInferenceTracerLayer is an Effect Layer that subscribes to the EventBus. Provide it alongside your other layers:

import { Effect, Layer } from "effect"
import { EventBusLive } from "@reactive-agents/core"
import { OpenInferenceTracerLayer } from "@reactive-agents/observe"
import { autoConfigureExporter } from "@reactive-agents/observe"
const handle = autoConfigureExporter({ serviceName: "my-agent" })
const AppLayer = Layer.merge(
EventBusLive,
OpenInferenceTracerLayer,
// ... other layers
)
await Effect.runPromise(
myAgentProgram.pipe(Effect.provide(AppLayer))
)
await handle.shutdown()
import { setupOpenInferenceExporter } from "@reactive-agents/observe"
const handle = setupOpenInferenceExporter({
endpoint: "http://my-collector:4318",
serviceName: "production-agent",
headers: {
Authorization: `Bearer ${process.env.BACKEND_TOKEN}`,
},
})

Langfuse ingests OpenTelemetry traces natively over OTLP HTTP. setupLangfuseExporter is a thin preset over setupOpenInferenceExporter — it computes the /api/public/otel endpoint and the HTTP Basic auth header from your project keys, so spans arrive with OpenInference attributes and render as agent/LLM/tool traces in the Langfuse UI.

import { setupLangfuseExporter } from "@reactive-agents/observe"
// Reads LANGFUSE_PUBLIC_KEY / LANGFUSE_SECRET_KEY / LANGFUSE_HOST from env.
const handle = setupLangfuseExporter()
// ... run agents (with OpenInferenceTracerLayer wired in) ...
await handle.shutdown()

Or pass credentials explicitly:

import { setupLangfuseExporter } from "@reactive-agents/observe"
const handle = setupLangfuseExporter({
publicKey: "pk-lf-...",
secretKey: "sk-lf-...",
host: "https://us.cloud.langfuse.com", // regional or self-hosted; defaults to https://cloud.langfuse.com
serviceName: "production-agent",
})

host must match the region your Langfuse project lives in — pick the base URL and set it via host or LANGFUSE_HOST:

  • EU (default) — https://cloud.langfuse.com
  • US — https://us.cloud.langfuse.com
  • Japan — https://jp.cloud.langfuse.com
  • Self-hosted — your own base URL (e.g. http://localhost:3000)

A region mismatch surfaces as 401 Invalid credentials. Confirm that you've configured the correct host. — the keys are fine, the host is wrong.

The preset base64-encodes publicKey:secretKey into Authorization: Basic ... and sets x-langfuse-ingestion-version: 4 (Langfuse v4 real-time ingestion — without it, directly-ingested OTel data can be delayed up to 10 minutes). Both are overridable via headers.

@reactive-agents/observe emits standard OTLP HTTP spans with OpenInference semantic attributes. Works out of the box with:

  • Jaeger — OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318
  • Grafana Tempo — OTEL_EXPORTER_OTLP_ENDPOINT=https://tempo.example.com
  • Langfuse — use the setupLangfuseExporter preset above
  • Arize Phoenix — OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:6006
  • Any OTLP HTTP-compatible collector

@reactive-agents/observe is @stable as of v0.11. The OpenInferenceTracerLayer, setupOpenInferenceExporter, setupLangfuseExporter, and autoConfigureExporter exports are stable. A dedicated Braintrust preset and sampling support are not yet built — any OTLP-compatible backend works today via the standard OTLP endpoint config above.