Mastra
Trace Mastra agents, workflows, and tools with Neatlogs (TypeScript).
Wrap the Mastra entities you build with wrapMastra — it patches Agent, Workflow, Vector, and Memory methods to emit method-level spans (AGENT, WORKFLOW, TOOL, LLM, RETRIEVER, VECTOR_STORE). Initialize Neatlogs once at startup, before constructing your entities.
Prerequisites
npm install neatlogs@latest @mastra/core @ai-sdk/openaiUsage
import { init, wrapMastra, flush, shutdown } from 'neatlogs';
async function main() {
await init({ apiKey: process.env.NEATLOGS_API_KEY, workflowName: 'mastra-demo' });
const { Agent } = await import('@mastra/core/agent');
const { openai } = await import('@ai-sdk/openai');
const agent = wrapMastra(
new Agent({ name: 'assistant', instructions: 'Be concise.', model: openai('gpt-4o') }),
);
const res = await agent.generate('In one sentence, what is Mastra?');
console.log(res.text);
await flush();
await shutdown();
}
main().catch(console.error);Alternative: getMastraObservability(). If you prefer Mastra's native observability hook, pass await getMastraObservability() into new Mastra({ observability }). It's an async helper exported from the main neatlogs package and requires the extra @neatlogs/instrumentation-mastra package. For most apps, wrapMastra is simpler and needs no extra dependency.
Sessions & end-user
Say your Mastra agent calls generate() a handful of times across one back-and-forth and you'd like all of it filed under a single session, tied to whichever of your users is chatting — since wrapMastra already opens the trace root, wrap the turn in identify() and each generate() picks up the session and end-user:
// Same sessionId every turn → one session; endUserId attributes it to your user.
await identify(
{ sessionId: `conv_${conversationId}`, endUserId: userId },
async () => {
await agent.generate(message);
},
);Not every run is a conversation. A standalone workflow — a one-off job, not a back-and-forth — is a single trace with no turns to group. You still attribute it to the customer it ran for, sourcing the id and any metadata from your own user or request object:
A standalone workflow must not carry a sessionId. Omit it entirely — Neatlogs sets sessionId = traceId, so the run is its own single-turn session, fully attributed to the end-user. Reusing one sessionId across unrelated runs wrongly folds them into a single multi-turn conversation.
async function summarize(document, user) {
// Standalone workflow — attribute to the end-user; do NOT pass sessionId.
return identify(
{ endUserId: String(user.id), endUserMetadata: { plan: user.plan } },
async () => {
const res = await agent.generate(`Summarize:\n${document}`);
return res.text;
},
);
}Sessions and End-User Identity walk through the whole model.
