Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
4176614
docs(mastra): add design spec for Openlayer Mastra observability expo…
viniciusdsmello Aug 27, 2026
ba34fa5
docs(mastra): resolve spec review findings
viniciusdsmello Aug 27, 2026
d3826c9
docs(mastra): add implementation plan for the Openlayer Mastra exporter
viniciusdsmello Aug 27, 2026
2d0f0a1
feat(mastra): add GenAI semconv v1.38 message coercion
viniciusdsmello Aug 27, 2026
f7494d9
feat(mastra): add span attribute rewriter for Openlayer OTLP ingest
viniciusdsmello Aug 27, 2026
d95bc53
fix(mastra): guard tool spans by span type, add coverage for output.m…
viniciusdsmello Aug 27, 2026
38d9edb
feat(mastra): add Openlayer OTLP trace exporter and optional peer deps
viniciusdsmello Aug 27, 2026
e5e798a
feat(mastra): add OpenlayerExporter with env and explicit configuration
viniciusdsmello Aug 27, 2026
0ce083f
docs(mastra): add runnable example and integration documentation
viniciusdsmello Aug 27, 2026
b26246c
fix(mastra): nest the workflow's agent step under its own trace
viniciusdsmello Aug 27, 2026
45882bc
fix(mastra): use the named createStepFromAgent export
viniciusdsmello Aug 27, 2026
82a6489
test(mastra): add live end-to-end test against a real pipeline
viniciusdsmello Aug 27, 2026
3da18cb
fix(mastra): normalize provider slug so Mastra OpenAI Responses calls…
viniciusdsmello Aug 27, 2026
7d0e340
fix(mastra): assert on content, not just shape, in the live test
viniciusdsmello Aug 27, 2026
cbe478c
fix(mastra): parse the output envelope instead of checking it non-empty
viniciusdsmello Aug 27, 2026
8a45740
fix(mastra): final review fix wave — identity coercion, test rigor, d…
viniciusdsmello Aug 27, 2026
e80b92c
docs(mastra): correct the public import path in spec and plan docs
viniciusdsmello Aug 27, 2026
b0ac4f9
fix(mastra): make the live test skip cleanly under plain jest
viniciusdsmello Aug 27, 2026
e1e14b9
chore(mastra): drop the superpowers spec and plan from the branch
viniciusdsmello Aug 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 123 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -446,6 +446,129 @@ const client = new Openlayer({
});
```

## Mastra

Send Mastra agent, workflow, model, and tool traces to Openlayer.

### Installation

```sh
npm install openlayer @mastra/core @mastra/observability @mastra/otel-exporter @opentelemetry/exporter-trace-otlp-proto
```

`@mastra/observability` is required to configure any custom exporter, Openlayer included, but is
not a peer dependency of `openlayer` itself — it's a Mastra requirement, not an Openlayer one.
`@mastra/core`, `@mastra/otel-exporter`, and `@opentelemetry/exporter-trace-otlp-proto` are all
declared as **optional** peer dependencies of `openlayer`, so none of the three is pulled in for
consumers who do not use Mastra.

### Configuration

Set `OPENLAYER_API_KEY` and `OPENLAYER_INFERENCE_PIPELINE_ID`, then add the exporter. A third,
optional variable, `OPENLAYER_OTEL_ENDPOINT`, overrides the OTLP endpoint the exporter posts
to — it defaults to `https://api.openlayer.com/v1/otel/v1/traces` when unset:

```ts
import { Mastra } from '@mastra/core';
import { Observability } from '@mastra/observability';
import { OpenlayerExporter } from 'openlayer/lib/integrations/mastra';

export const mastra = new Mastra({
observability: new Observability({
configs: {
openlayer: {
serviceName: 'my-service',
exporters: [new OpenlayerExporter()],
},
},
}),
});
```

Every value can also be passed explicitly, which takes precedence over the environment:

```ts
new OpenlayerExporter({
apiKey: process.env.OPENLAYER_API_KEY,
inferencePipelineId: process.env.OPENLAYER_INFERENCE_PIPELINE_ID,
projectName: 'my-service',
endpoint: 'https://api.openlayer.com/v1/otel/v1/traces',
headers: { 'x-custom-header': 'value' },
batchSize: 512,
timeout: 30000,
logLevel: 'debug',
});
```

If credentials are missing the exporter disables itself and logs the reason — it never throws.

### Session and user attribution

Metadata named `sessionId` (or `threadId`) and `userId` is lifted onto the trace, so rows are
grouped by session and user in Openlayer:

```ts
await agent.generate('What is the weather in Lisbon?', {
tracingOptions: { metadata: { sessionId: 'session-123', userId: 'user-456' } },
});
```

Any other metadata is preserved on the step as-is.

### Composing with other exporters

Mastra takes a list, so Openlayer sits alongside anything else:

```ts
exporters: [new OpenlayerExporter(), new ArizeExporter()],
```

### Filtering spans

There are two layers, and they do different jobs:

- **`excludeSpanTypes`** on the Mastra config drops spans before _any_ exporter sees them. Use
this to filter for every exporter at once.
- **`dropSpanTypes`** on `OpenlayerExporter` changes only what Openlayer receives. It defaults
to `[SpanType.MODEL_CHUNK]`, because Mastra emits one span per streaming chunk and an
unfiltered streamed reply would become hundreds of steps. Pass `[]` to export everything.

**`dropSpanTypes` never reparents children.** It is a public knob, and dropping a span type
that has descendants — `WORKFLOW_STEP`, for example — silently loses that entire subtree, not
just the dropped span itself. `MODEL_CHUNK`, the default, is safe from this precisely because
chunk spans are leaves with nothing under them to lose.

`SpanType.MODEL_STEP` is the case that was actually measured: it was a large share of a
trace's steps (4 of 7 in a single one-tool-call turn) and was considered for the default drop
list, but a live run confirmed that dropping it silently lost the nested tool-call step rather
than hoisting it to the surviving `MODEL_GENERATION` ancestor. It is deliberately **not** in
the default list for that reason, and the same caution applies to any span type you add to
`dropSpanTypes` yourself: check what it parents before dropping it.

### Troubleshooting

**Nothing arrives at all.** The exporter disabled itself because credentials were missing. Look
for `[OpenlayerExporter] Missing required configuration` in the logs at startup.

**Rows arrive with empty output.** Something stripped the `mastra.*.input` / `.output` span
attributes before the exporter ran — check any `customSpanFormatter` or span output processor
in your observability config. Openlayer builds a row's input and output from the root span, and
the exporter recovers them from those attributes.

**Hundreds of steps in one trace.** `dropSpanTypes` was overridden and `MODEL_CHUNK` is no
longer filtered. Restore the default or add `SpanType.MODEL_CHUNK` back.

**OpenInference attributes are not read.** Openlayer's OTLP ingest maps the GenAI semantic
conventions; OpenInference `input.value` / `output.value` produce empty rows. This exporter
targets gen_ai deliberately — no configuration will change that.

**Running `mastraExporter.live.test.ts` live needs `--experimental-vm-modules`.** The suite
itself loads and skips cleanly under a plain `npx jest` run with no credentials — `@ai-sdk/openai`
is ESM-only, but the live test imports it lazily inside the test body, which `it.skip` never
executes. The flag is only required to actually run the live assertions once credentials are
set: `NODE_OPTIONS=--experimental-vm-modules npx jest tests/integrations/mastraExporter.live.test.ts`
— see that file's header comment for why.

## Frequently Asked Questions

## Semantic versioning
Expand Down
98 changes: 98 additions & 0 deletions examples/mastra-tracing.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/**
* Mastra → Openlayer tracing example.
*
* Exercises both root span types — an agent run and a workflow run — plus a
* tool call, session/user metadata, and an explicit shutdown so the batch is
* flushed before the process exits.
*
* Run with:
* OPENLAYER_API_KEY=... OPENLAYER_INFERENCE_PIPELINE_ID=... OPENAI_API_KEY=... \
* npx tsx mastra-tracing.ts
*/
import { openai } from '@ai-sdk/openai';
import { Agent } from '@mastra/core/agent';
import { Mastra } from '@mastra/core';
import { createTool } from '@mastra/core/tools';
import { createStepFromAgent, createWorkflow } from '@mastra/core/workflows';
import { Observability } from '@mastra/observability';
import { OpenlayerExporter } from 'openlayer/lib/integrations/mastra';
import { z } from 'zod';

const getWeather = createTool({
id: 'get_weather',
description: 'Get the current weather for a city.',
inputSchema: z.object({ city: z.string() }),
outputSchema: z.object({ tempC: z.number(), sky: z.string() }),
execute: async ({ city }) => {
// A real tool would call a weather API here.
return { tempC: 24, sky: `sunny in ${city}` };
},
});

const weatherAgent = new Agent({
id: 'weatherAgent',
name: 'WeatherAgent',
instructions: 'You are a concise weather assistant. Always use the get_weather tool.',
model: openai('gpt-4o-mini'),
tools: { getWeather },
});

// createStepFromAgent wraps the agent as a step that runs *inside* the
// workflow's own trace, instead of a hand-written step that calls
// `agent.generate()` and would start an unrelated, sibling trace. The
// factory has a fixed contract, not an inferred one: its input type is
// always `{ prompt: string }` and its output always carries `text` — which is
// why the `.map()` step below exists (to produce that exact `{ prompt }`
// shape from the workflow's own `{ city }` input) and why the workflow's
// `outputSchema` declares `text`.
const weatherAgentStep = createStepFromAgent(weatherAgent);

const weatherWorkflow = createWorkflow({
id: 'weatherWorkflow',
inputSchema: z.object({ city: z.string() }),
outputSchema: z.object({ text: z.string() }),
})
.map(async ({ inputData }) => ({ prompt: `What is the weather in ${inputData.city}?` }))
.then(weatherAgentStep)
.commit();

export const mastra = new Mastra({
agents: { weatherAgent },
workflows: { weatherWorkflow },
observability: new Observability({
configs: {
openlayer: {
serviceName: 'mastra-openlayer-example',
// Zero-config: reads OPENLAYER_API_KEY and OPENLAYER_INFERENCE_PIPELINE_ID.
exporters: [new OpenlayerExporter()],
},
},
}),
});

async function main(): Promise<void> {
// 1. A bare agent run — the root span is AGENT_RUN.
const agentResult = await mastra.getAgent('weatherAgent').generate('What is the weather in Lisbon?', {
// Lifted by the exporter to session.id / user.id, which Openlayer reads.
tracingOptions: { metadata: { sessionId: 'demo-session-1', userId: 'demo-user-1' } },
});
console.log('agent:', agentResult.text);

// 2. A workflow run — the root span is WORKFLOW_RUN.
const run = await mastra.getWorkflow('weatherWorkflow').createRun();
const workflowResult = await run.start({ inputData: { city: 'Madrid' } });
if (workflowResult.status === 'success') {
console.log('workflow:', JSON.stringify(workflowResult.result));
} else {
console.log('workflow ended with status:', workflowResult.status);
}

// 3. Flush before exit, or the last batch is lost.
await mastra.observability.shutdown();
console.log('Traces flushed to Openlayer.');
}

main().catch((error) => {
console.error(error);
process.exit(1);
});
8 changes: 7 additions & 1 deletion examples/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,19 @@
"author": "",
"license": "ISC",
"dependencies": {
"@ai-sdk/openai": "^4.0.50",
"@google/genai": "^2.13.0",
"@langchain/core": "^0.3.80",
"@langchain/openai": "^0.6.17",
"@mastra/core": "^1.63.0",
"@mastra/observability": "^1.17.3",
"@mastra/otel-exporter": "^1.3.11",
"@opentelemetry/exporter-trace-otlp-proto": "^0.221.0",
"form-data": "^4.0.4",
"langchain": "^0.3.37",
"openai": "^4.104.0",
"openlayer": "^0.22.2"
"openlayer": "file:..",
"zod": "^4.4.3"
},
"devDependencies": {
"tsx": "^4.21.0",
Expand Down
Loading
Loading