Skip to main content
Anvil traces map directly to OpenTelemetry spans following the GenAI semantic conventions, so you can forward them to any OTLP-compatible backend without a separate SDK or agent. Use otlpHttpExporter to create an onExport callback and pass it to Tracer — every finished trace is serialised to the OTLP JSON format and POSTed to your collector in the background.

How export works

When a Tracer is constructed with an onExport callback, that callback fires once per trace, immediately after the agent run ends and traceHandle.end() is called. The otlpHttpExporter factory returns exactly such a callback: it converts the Trace object into an OTLP resourceSpans payload and sends it via fetch. Export is fire-and-forget — failures are logged to console.error but never propagate to the request, so a collector outage cannot break your API.

Setting up the exporter

import { Tracer, SqliteTraceStore, otlpHttpExporter, traceToOtelSpans } from 'anvil/trace';

const store = await SqliteTraceStore.open('.anvil/traces.db');

const exporter = otlpHttpExporter({
  url: 'https://your-collector/v1/traces',
  headers: {
    'Authorization': 'Bearer YOUR_API_KEY',
  },
  serviceName: 'my-anvil-app',
});

const tracer = new Tracer(store, { onExport: exporter });
Pass tracer to defineAgent as usual. Every completed trace is now forwarded to your collector alongside being stored locally.

OtlpExporterOptions

url
string
required
The OTLP/HTTP endpoint to POST traces to. This is the /v1/traces path on your collector. Common values:
BackendEndpoint
Datadoghttps://http-intake.logs.datadoghq.com/api/v2/otlp
Grafana Tempohttps://<your-stack>.grafana.net/otlp/v1/traces
Langfusehttps://cloud.langfuse.com/api/public/otel/v1/traces
Braintrusthttps://api.braintrustdata.com/otel/v1/traces
Local collectorhttp://localhost:4318/v1/traces
headers
Record<string, string>
HTTP headers added to every export request. Use this to pass API keys, tokens, or dataset identifiers required by your backend.
serviceName
string
default:"anvil"
The service.name resource attribute included in every exported payload. Set this to identify your application in the backend’s UI.

Converting traces manually with traceToOtelSpans

If you need to control the HTTP transport yourself, use traceToOtelSpans directly to obtain the OTLP payload and send it with your own client:
import { traceToOtelSpans } from 'anvil/trace';
import type { Trace } from 'anvil/trace';

function myExporter(trace: Trace): void {
  const resourceSpans = traceToOtelSpans(trace, 'my-service');

  fetch('https://collector.example.com/v1/traces', {
    method: 'POST',
    headers: { 'content-type': 'application/json' },
    body: JSON.stringify({ resourceSpans: [resourceSpans] }),
  }).catch(console.error);
}

const tracer = new Tracer(store, { onExport: myExporter });
traceToOtelSpans(trace, serviceName?) returns an OtlpResourceSpans object ready to be nested under { resourceSpans: [...] } in the OTLP JSON body.

GenAI semantic convention attributes

Anvil maps span fields to the OpenTelemetry GenAI semantic conventions. The following attributes are emitted on model spans:
OTel attributeSource
gen_ai.systemspan.attributes.provider
gen_ai.request.modelspan.attributes.model
gen_ai.response.modelspan.attributes.model
gen_ai.usage.input_tokensspan.attributes.usage.inputTokens
gen_ai.usage.output_tokensspan.attributes.usage.outputTokens
gen_ai.usage.costspan.attributes.costUsd
gen_ai.operation.namespan.kind
Tool spans emit gen_ai.tool.name from span.attributes.name.

Sending to multiple backends

Compose multiple exporters inside a single onExport callback:
import { otlpHttpExporter } from 'anvil/trace';

const datadogExporter = otlpHttpExporter({
  url: 'https://http-intake.logs.datadoghq.com/api/v2/otlp',
  headers: { 'DD-API-KEY': process.env.DD_API_KEY! },
  serviceName: 'anvil-prod',
});

const langfuseExporter = otlpHttpExporter({
  url: 'https://cloud.langfuse.com/api/public/otel/v1/traces',
  headers: {
    'Authorization': `Basic ${Buffer.from(`${process.env.LANGFUSE_PUBLIC_KEY}:${process.env.LANGFUSE_SECRET_KEY}`).toString('base64')}`,
  },
});

const tracer = new Tracer(store, {
  onExport: (trace) => {
    datadogExporter(trace);
    langfuseExporter(trace);
  },
});
Both exporters run concurrently and independently. A failure in one does not affect the other, and neither can throw back into the request path because export errors are caught and logged internally.

OtlpResourceSpans type reference

For advanced use cases, the full OTLP type shapes are exported from anvil/trace:
interface OtlpResourceSpans {
  resource: { attributes: OtlpAttribute[] };
  scopeSpans: Array<{
    scope: { name: string; version: string };
    spans: OtlpSpan[];
  }>;
}

interface OtlpSpan {
  traceId: string;      // 32-char lowercase hex
  spanId: string;       // 16-char lowercase hex
  parentSpanId?: string;
  name: string;
  kind: number;         // 1 = SPAN_KIND_INTERNAL
  startTimeUnixNano: string;
  endTimeUnixNano: string;
  attributes: OtlpAttribute[];
  status: { code: number; message?: string };
}