Skip to main content

Logging

Mastra's logging system captures function execution, input data, and output responses in a structured format.

When deploying to the Mastra platform, logs are shown in the dashboard. In self-hosted or custom environments, logs can be directed to files or external services depending on the configured transports.

Configuring logs with PinoLogger
Direct link to configuring-logs-with-pinologger

When initializing a new Mastra project using the CLI, PinoLogger is included by default.

src/mastra/index.ts
import { Mastra } from '@mastra/core/mastra'
import { PinoLogger } from '@mastra/loggers'

export const mastra = new Mastra({
logger: new PinoLogger({
name: 'Mastra',
level: 'info',
}),
})

Visit PinoLogger for all available configuration options.

Logging to observability storage
Direct link to Logging to observability storage

When observability is configured, by default all logger calls are automatically forwarded to your observability storage. Every debug, info, warn, error, and trackException call from your application and from Mastra's internal components is stored alongside your traces.

No code changes are required. Loggers with adapter support (PinoLogger and ConsoleLogger) write each record to their own destinations and derive the exported log from that same record. Custom loggers without adapter support are wrapped so they write to both the original logger and the observability system simultaneously.

Trace-correlated log output
Direct link to Trace-correlated log output

When a log call happens inside a traced operation (an agent run, workflow step, or tool call), Mastra injects trace_id and span_id into the logger's native output. The fields use W3C trace context format and match the trace shown in Studio, so you can correlate a stdout log line with its trace directly.

To get machine-parseable JSON on stdout, configure PinoLogger with prettyPrint: false, pretty printing is enabled by default and produces human-readable text instead of JSON:

src/mastra/index.ts
export const mastra = new Mastra({
logger: new PinoLogger({ name: 'Mastra', level: 'info', prettyPrint: false }),
})

The record then looks like this:

{
"level": 30,
"time": 1755522000000,
"name": "Mastra",
"trace_id": "0af7651916cd43dd8448eb211c80319c",
"span_id": "b7ad6b7169203331",
"msg": "tool executed"
}

Outside a traced operation, the fields are omitted. ConsoleLogger appends the same fields as an object argument to its console output instead. With PinoLogger, the fields reach every destination: stdout, files, and custom transports. If you supply your own pino mixin, its fields are preserved, but trace_id and span_id win on conflicts.

Correlation and export can be controlled independently with the loggerOptions configuration:

src/mastra/index.ts
import { Mastra } from '@mastra/core/mastra'
import { PinoLogger } from '@mastra/loggers'

export const mastra = new Mastra({
logger: new PinoLogger({ name: 'Mastra', level: 'info' }),
loggerOptions: {
correlation: true, // inject trace_id/span_id into native log output (default: true)
export: false, // keep trace-correlated output without storing logs in observability (default: true)
},
})

Custom loggers
Direct link to Custom loggers

Custom IMastraLogger implementations keep working: Mastra uses a deprecated dual-write fallback that forwards log calls to observability without adding trace_id or span_id to the logger's native output. To get trace-correlated output, implement the __attachObservability() adapter hook from @mastra/core/logger:

import { MastraLogger, buildLogRecordData } from '@mastra/core/logger'
import type { LoggerAdapterContext } from '@mastra/core/logger'

class MyLogger extends MastraLogger {
#adapterContext?: LoggerAdapterContext

__attachObservability(ctx: LoggerAdapterContext): void {
this.#adapterContext = ctx
}

#log(level: 'debug' | 'info' | 'warn' | 'error', message: string, args: unknown[]): void {
const ctx = this.#adapterContext
// Inject trace fields into your native record when correlation is enabled
const traceFields = ctx?.options.correlation ? ctx.resolveTraceFields() : undefined
console[level === 'debug' ? 'info' : level](
message,
...args,
...(traceFields ? [traceFields] : []),
)
// Export the same record to observability when export is enabled
if (ctx?.options.export) {
ctx.getLogSink()?.[level](message, buildLogRecordData(args))
}
}

debug(message: string, ...args: unknown[]): void {
this.#log('debug', message, args)
}

info(message: string, ...args: unknown[]): void {
this.#log('info', message, args)
}

warn(message: string, ...args: unknown[]): void {
this.#log('warn', message, args)
}

error(message: string, ...args: unknown[]): void {
this.#log('error', message, args)
}
}

Configuring observability log level
Direct link to Configuring observability log level

You can control which log levels reach observability storage independently from your console logger. Add a logging option to your observability instance configuration:

src/mastra/index.ts
import { Mastra } from '@mastra/core/mastra'
import { PinoLogger } from '@mastra/loggers'
import { Observability, MastraStorageExporter } from '@mastra/observability'

export const mastra = new Mastra({
logger: new PinoLogger({ name: 'Mastra', level: 'debug' }),
observability: new Observability({
configs: {
default: {
serviceName: 'my-app',
exporters: [new MastraStorageExporter()],
logging: {
enabled: true, // set to false to disable log forwarding
level: 'info', // minimum level: 'debug' | 'info' | 'warn' | 'error' | 'fatal'
},
},
},
}),
})

In this example, the console logger outputs all levels starting from debug, but only info and above are written to observability storage. This keeps your storage clean while still having verbose console output during development.

OptionTypeDefaultDescription
enabledbooleantrueSet to false to disable all log forwarding to observability storage.
levelLogLevel'debug'Minimum severity level. Logs below this level are discarded.

Querying logs
Direct link to Querying logs

Logs written to observability storage are queryable through the Mastra client SDK:

import { MastraClient } from '@mastra/client-js'

const client = new MastraClient()

const logs = await client.listLogsVNext({
filters: { level: 'error' },
pagination: { page: 1, perPage: 50 },
orderBy: { field: 'timestamp', direction: 'desc' },
})

When using a persistent storage backend like DuckDB or ClickHouse, logs survive restarts and are available for historical analysis.

Customizing logs
Direct link to Customizing logs

Mastra provides access to a logger instance via the mastra.getLogger() method, available inside both workflow steps and tools. The logger supports standard severity levels: debug, info, warn, and error.

Logging from workflow steps
Direct link to Logging from workflow steps

Within a workflow step, access the logger via the mastra parameter inside the execute function. You can log messages relevant to the step's execution.

src/mastra/workflows/test-workflow.ts
import { createWorkflow, createStep } from "@mastra/core/workflows";
import { z } from "zod";

const step1 = createStep({
execute: async ({ mastra }) => {
const logger = mastra.getLogger();
logger.info("workflow info log");

return {
output: ""
};
}
});

export const testWorkflow = createWorkflow({...})
.then(step1)
.commit();

Logging from tools
Direct link to Logging from tools

Similarly, tools have access to the logger instance via the mastra parameter. Use this to log tool-specific activity during execution.

src/mastra/tools/test-tool.ts
import { createTool } from '@mastra/core/tools'
import { z } from 'zod'

export const testTool = createTool({
execute: async (inputData, context) => {
const logger = context?.mastra.getLogger()
logger?.info('tool info log')

return {
output: '',
}
},
})

Logging with additional data
Direct link to Logging with additional data

Logger methods accept an optional second argument for additional data. Pass a structured object to make logs filterable in observability storage.

src/mastra/workflows/test-workflow.ts
import { createWorkflow, createStep } from "@mastra/core/workflows";
import { z } from "zod";

const step1 = createStep({
execute: async ({ mastra }) => {
const testAgent = mastra.getAgent("testAgent");
const logger = mastra.getLogger();

logger.info("workflow info log", { agent: testAgent });

return {
output: ""
};
}
});

export const testWorkflow = createWorkflow({...})
.then(step1)
.commit();