Crux
GuidesObservability

Production Telemetry

Send Crux traces to Datadog, Honeycomb, Grafana, and other observability platforms via OpenTelemetry.

Crux devtools is great for development, but production environments need traces in your existing observability stack. @crux/otel emits OpenTelemetry spans for every instrumented event — one integration covers Datadog, Honeycomb, Grafana, New Relic, and any OTel-compatible platform.

Setup

bash pnpm add @crux/otel
bash npm install @crux/otel
bash yarn add @crux/otel
bash bun add @crux/otel
crux.config.ts
import { config } from '@crux/core'
import { withTelemetry } from '@crux/otel'

export default config({
  plugins: [withTelemetry({ serviceName: 'my-app' })],
})

That's it. Spans flow to whatever observability platform you've configured.

Export paths

@crux/otel supports two strategies depending on your runtime environment.

Standard OTel (Node.js servers)

For long-lived servers (Next.js, Express, Fastify), set up the OTel SDK once in your application entrypoint. @crux/otel creates spans via @opentelemetry/api which routes them through your globally registered TracerProvider.

instrumentation.ts
import { NodeSDK } from '@opentelemetry/sdk-node'
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'

const sdk = new NodeSDK({
  traceExporter: new OTLPTraceExporter({
    url: 'https://otel-collector.example.com/v1/traces',
    headers: { 'DD-API-KEY': process.env.DD_API_KEY },
  }),
})
sdk.start()
crux.config.ts
import { withTelemetry } from '@crux/otel'

// No exporter needed — uses the global TracerProvider
config({
  plugins: [withTelemetry({ serviceName: 'my-app' })],
})

@opentelemetry/api is an optional peer dependency. Install it alongside the OTel SDK when using the standard path.

Lightweight (Lambda, Convex, Cloudflare Workers)

For ephemeral runtimes where the full OTel SDK isn't available, pass an exporter option:

URL exporter
withTelemetry({
  serviceName: 'my-app',
  exporter: {
    url: 'https://collector.example.com/v1/traces',
    headers: { 'X-Api-Key': process.env.COLLECTOR_KEY },
  },
})
Callback exporter
withTelemetry({
  serviceName: 'my-app',
  exporter: (spans) => {
    // Send to PostHog, Convex action, or any custom endpoint
    ctx.runAction(internal.telemetry.sendTraces, { spans })
  },
})

The URL exporter uses fire-and-forget HTTP POST (5-second timeout) — failures are silently ignored so telemetry never impacts your application.

What gets traced

Every instrumented Crux event produces a span:

EventSpan NameKey Attributes
generate() / stream()crux.generategen_ai.system, gen_ai.request.model, token usage, cost
Cost trackingcrux.cost.{report|warn|limit}crux.cost, crux.cost.total, crux.cost.source, crux.cost.threshold
Tool executioncrux.tool.{name}crux.tool.name, crux.tool.call_id, model-output type, raw/model-output sizes, estimated savings
flow().run()crux.flowcrux.flow.id, crux.flow.name
flow.step()crux.flow.stepcrux.step.id, crux.step.label
flow.suspend()Event on crux.flow + span endcrux.flow.suspend_point
Resumecrux.flow.resumecrux.flow.id, crux.flow.name
Compositionscrux.composition.{kind}crux.composition.kind, agent count
Memory opscrux.memory.{read|write}crux.memory.type, crux.memory.operation
Corpus synccrux.corpus.synccrux.corpus.id, hashed namespace, sync counts, dry-run flag
Corpus source eventscrux.corpus.sourcecrux.corpus.action, hashed namespace/source ID, chunk count
Compactioncrux.compactcrux.compaction.ratio
Judge scorescrux.judgecrux.judge.metric, crux.judge.score
Delegationcrux.delegatecrux.delegate.id

Tool spans include privacy-safe toModelOutput() metadata only. OTel records the output shape and byte-size difference; it does not record raw tool results, model-facing tool content, queries, files, or metadata.

Generate/stream spans follow the OpenTelemetry GenAI semantic conventions — attributes like gen_ai.system, gen_ai.request.model, gen_ai.usage.input_tokens work with existing dashboards and alerting rules.

Running alongside devtools

Both local devtools and OTel can run simultaneously. Use devtools.serverUrl for a local server or tunnel, and keep production export explicit through @crux/otel or observability config. The plugin system's fan-out semantics ensure every event reaches both configured destinations:

crux.config.ts
config({
  devtools: { serverUrl: process.env.DEVTOOLS_URL }, // local server or tunnel only
  plugins: [withTelemetry({ serviceName: 'my-app' })],
})

Devtools uses the visual UI for development. OTel sends spans to your production dashboards. Same events, deliberately configured destinations.

Options

OptionTypeDefaultDescription
serviceNamestring'@crux/otel'Tracer name / service identifier
recordContentbooleanfalseInclude prompt content in span attributes
attributesRecord<string, string>Custom attributes on every span
exporterUrlExporter | CallbackExporterExport strategy (omit for standard OTel path)

Next steps

On this page