Mastra
Reshape Mastra's OpenTelemetry bridge spans for Unhandled's AI views with @unhandled/instrumentation-mastra, including model metadata and evaluations.
@unhandled/instrumentation-mastra makes Mastra agents readable in the AI agents views. Mastra's OpenTelemetry bridge already emits spans; this package adds a span processor that maps Mastra's attribute names to the ones Unhandled reads, copies model metadata onto agent spans and normalizes evaluation scores. It creates no spans and no bridge of its own, but it ships a helper that configures the bridge for you.
Peers: @mastra/core, @mastra/otel-bridge and @mastra/observability, all 1.0.0 or later.
Install
npm install @unhandled/instrumentation-mastraRegister with the AdonisJS agent
The integration registers the span processor ahead of the export pipeline:
import env from '#start/env'
import { defineConfig } from '@unhandled/agent-adonisjs'
import { mastra } from '@unhandled/instrumentation-mastra/integration'
export default defineConfig({
dsn: env.get('UNHANDLED_DSN'),
integrations: [mastra({ recordInputs: true, recordOutputs: false })],
})Register standalone
Add the processor to your tracer provider before the processor that exports spans, because it rewrites attributes when spans end:
import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node'
import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-base'
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-proto'
import { MastraSpanProcessor } from '@unhandled/instrumentation-mastra'
const provider = new NodeTracerProvider({
spanProcessors: [
new MastraSpanProcessor({ maxBufferedTraces: 1000 }),
new BatchSpanProcessor(new OTLPTraceExporter()),
],
})
provider.register()See any OpenTelemetry SDK for the exporter's endpoint and headers.
Configure Mastra
Hand Mastra an observability config with the bridge. createMastraObservabilityConfig() builds one:
import { Observability, SamplingStrategyType } from '@mastra/observability'
import { createMastraObservabilityConfig } from '@unhandled/instrumentation-mastra'
export const observability = new Observability({
configs: {
default: createMastraObservabilityConfig({
serviceName: 'support-agent',
sampling: { type: SamplingStrategyType.ALWAYS },
}),
},
})The helper adds a new OtelBridge() and sets excludeSpanTypes to MODEL_STEP and MODEL_CHUNK (exported as DEFAULT_EXCLUDED_SPAN_TYPES), which keeps per-step and per-chunk spans out of your traces. Pass excludeSpanTypes to replace that list, or bridge to supply your own bridge, for example one with a custom tracer provider. Every other option is forwarded as is.
Options
| Option | Where | Default | Effect |
|---|---|---|---|
recordInputs | integration, processor | false | true keeps input content: messages, system instructions, tool arguments. |
recordOutputs | integration, processor | false | true keeps output content and response previews. |
enabled | integration | true | false skips the integration. |
maxBufferedTraces | processor | 500 | Traces tracked for metadata propagation. The oldest is dropped first. |
maxBufferedSpansPerTrace | processor | 256 | Spans tracked per trace before the oldest entry is dropped. |
These flags are independent of the Vercel AI integration's flags.
What the processor changes
Mastra already writes most semconv names. The processor renames the rest, keeping a semconv value that is already set:
| Mastra attribute | Renamed to |
|---|---|
gen_ai.usage.cached_input_tokens | gen_ai.usage.cache_read.input_tokens |
gen_ai.usage.cache_creation.input_tokens | gen_ai.usage.cache_write.input_tokens |
gen_ai.usage.reasoning_tokens | gen_ai.usage.reasoning.output_tokens |
gen_ai.agent.name is also copied to gen_ai.function_id when that is not set.
Model metadata (request and response model, provider and, with recordOutputs, the response preview) is copied from each chat span to its nearest invoke_agent ancestor when the agent span lacks it, so an agent run shows its model. Usage stays on the chat span, so tokens and cost are counted once. Children are buffered per trace until the agent span ends, within the limits above. When several model calls run under one agent, the first one to end supplies each value.
Prompts and responses are dropped unless you opt in, because they often hold personal data. With recordInputs or recordOutputs off, the processor also removes Mastra's own mastra.*.input and mastra.*.output attributes.
Evaluations
Span events named gen_ai.evaluation.result are recognized, and a numeric string in gen_ai.evaluation.score.value is exported as a number. To read them yourself, extractEvaluations(span) returns one object per event:
| Field | Attribute |
|---|---|
name | gen_ai.evaluation.name |
scoreValue | gen_ai.evaluation.score.value |
scoreLabel | gen_ai.evaluation.score.label |
explanation | gen_ai.evaluation.explanation |
scorerId | gen_ai.evaluation.scorer.id |
scorerVersion | gen_ai.evaluation.scorer.version |
targetTraceId | gen_ai.evaluation.target.trace_id |