Integrationsobservability
Better Stack tracing
Send agent runs to a Better Stack OpenTelemetry source with @mayurajs/observability-betterstack: setup, options, limits and errors.
- Package
- @mayurajs/observability-betterstack
- Install
- npm install mayura @mayurajs/observability-betterstack
- Vendor
- Better Stack ↗
Better Stack collects logs, metrics and traces into sources you query and chart.
@mayurajs/observability-betterstack presets Mayura's OTLP trace exporter
(mayura/exporter-otlp) for a Better Stack OpenTelemetry source:
its ingesting host and its token. It sends OTLP/HTTP JSON with fetch: no Better Stack SDK, no dependencies besides
mayura, and it runs on every runtime Mayura runs on.
Install
npm install mayura @mayurajs/observability-betterstackCredentials
Create an OpenTelemetry source in Better Stack (Telemetry, Sources, Connect
source). Its settings show the two values the exporter needs: the source token and the ingesting host, such as
s1234.eu-nbg-2.betterstackdata.com. The package reads nothing from the environment: pass them in yourself, for
example from BETTERSTACK_SOURCE_TOKEN and BETTERSTACK_INGESTING_HOST.
Use it
Follow a run with an observer, then turn its events into spans with agentRunTraceSpans and give them to the
exporter's sink:
import { createObserver } from 'mayura/observability';
import { agentRunTraceSpans } from 'mayura/exporter-otlp';
import { betterStackTraceExporter } from '@mayurajs/observability-betterstack';
const betterStack = betterStackTraceExporter({
sourceToken: process.env.BETTERSTACK_SOURCE_TOKEN ?? '',
ingestingHost: process.env.BETTERSTACK_INGESTING_HOST ?? '',
serviceName: 'support-agent',
});
// Keep enough events per run to pair every call's start and completion.
const observer = createObserver({ maxRecentEventsPerRun: 256 });
const handle = runtime.submit(agent, { input: { question: 'Where is order 42?' } });
const observation = observer.observe(handle);
await handle.result();
await observation.done();
const spans = agentRunTraceSpans(observer.inspect(handle.id)?.recent ?? []);
if (spans.length > 0) await betterStack.sink(spans, { signal: AbortSignal.timeout(10_000) });
console.log(betterStack.inspect().metrics.recordsAccepted);The exporter is not a tool, so a run needs no permission string for it: it runs in your code after the run settles.
For durable workflows, give a second exporter's sink to
createWorkflowTraceExport; each export needs its own instance, because an exporter sends one request at a time:
import { createWorkflowTraceExport } from 'mayura/workflows';
import { betterStackTraceExporter } from '@mayurajs/observability-betterstack';
const workflowTraces = createWorkflowTraceExport({
source: host.runtime,
store,
scope,
exportId: 'betterstack',
definitions,
sink: betterStackTraceExporter({ sourceToken, ingestingHost, serviceName: 'orders' }).sink,
});See Trace durable workflow runs for the worker unit and nesting an agent's spans under the step that ran it.
Options
| Option | Default | Meaning |
|---|---|---|
sourceToken |
required | The source's token: 8 to 256 letters, digits, _ or -. Copied once; never shown by inspect() or in errors. |
ingestingHost |
required | The source's ingesting host: a host name only, without a scheme, path or port. |
serviceName |
required | The service.name resource attribute: a stable identifier. |
serviceVersion |
none | The service.version resource attribute. |
resourceAttributes |
none | More resource attributes, under the catalog's rules. |
spanAttributes |
none | A function adding your own attributes to each span, under the same rules. |
timeoutMs |
5,000 | Per request. |
maxBatchSize |
256 | Spans per sink call (at most 256). |
maxRequestBytes |
1 MiB | The largest request body (at most 16 MiB). |
maxResponseBytes |
64 KiB | The largest reply read (at most 1 MiB). |
fetch |
globalThis.fetch |
Your own transport, for tests or a proxy. |
Endpoint and authentication
The endpoint is https://<ingestingHost>/v1/traces, always HTTPS. Every request carries
Authorization: Bearer <sourceToken>.
What is sent
Each span is metadata from the run's events: names, ids, timing, statuses and integers from a fixed attribute catalog:
agent:<agent id>for the run, withgen_ai.operation.name: invoke_agent,gen_ai.agent.id,gen_ai.agent.nameand the run's status and budget attributes.model.callfor each model call, withgen_ai.provider.name,gen_ai.request.model,gen_ai.usage.input_tokensandgen_ai.usage.output_tokens(when the provider reports them), and the call'smayura.cost.micros.tool:<tool id>for each tool call, withgen_ai.tool.name,gen_ai.tool.call.idandmayura.tool.status.
In Better Stack you query and chart spans by these attributes: model, provider, token counts, cost and statuses. No prompt, message, tool input, tool output or error text is sent: the catalog has no field that could carry them.
Delivery and limits
- One
sinkcall is one HTTP request of at mostmaxBatchSizespans. Split a larger run into several calls. - The exporter sends one request at a time; a second call while one is in flight is refused with
CONFLICT. - It never retries and follows no redirects. A failed batch is dropped and counted.
inspect()returns{ state, inFlight, metrics }. The metrics countbatchesAttempted,recordsAttempted,recordsAccepted,recordsDropped,partialResponses,failedRequests,timedOutRequests,cancelledRequests,requestBytesandresponseBytes. Headers and the token are never shown.close()aborts the request in flight and refuses later calls.
With your OpenTelemetry SDK
If your application already runs an OpenTelemetry SDK that exports to Better Stack, you can send Mayura's spans through
it with createOpenTelemetryTraceBridge instead of this
package. The spans carry the same attributes either way.
Errors
Failures reach you as a MayuraError with a code. None of them repeats your token or Better Stack's reply text.
code |
When |
|---|---|
INVALID_CONFIG |
When you call betterStackTraceExporter(): a missing or malformed token, or an ingesting host that is not a bare host name (a scheme, a path, user info or an IP address). Nothing is sent. |
INVALID_INPUT |
An empty batch, more than maxBatchSize spans, a span outside the attribute catalog, or a request over maxRequestBytes. The batch is dropped before it is sent. |
CONFLICT |
The exporter is closed, or another sink call is in flight. |
TIMEOUT |
Better Stack did not answer within timeoutMs. |
CANCELLED |
Your signal aborted, or close() was called. |
TOOL_FAILED |
Better Stack answered anything but HTTP 200 (for example 401 for a wrong token), or a reply that is not valid OTLP. |
A reply that reports some spans rejected (OTLP partial success) resolves normally and counts them in recordsDropped
and partialResponses.
How it is tested
- Unit tests run the exporter against a fake
fetchthat checks the endpoint on the ingesting host, the bearer token and that the body carries the GenAI attributes. - They also check refused tokens and hosts, and a rejected request counted as dropped, and that no error or
inspect()result contains the token. scripts/live/observability-betterstack.mjssends one agent run's spans to a Better Stack source: Better Stack accepted them on October 6, 2026.
Related
- Observability: observers, the OTLP exporters and trace projections.
- Datadog APM and LLM Observability and Sentry tracing: other monitoring backends.
- Durable workflows: runs whose traces
createWorkflowTraceExportsends.