Logging
We use a centralized, request-scoped logging pattern where loggers are created by middleware and passed through the request/function chain.
API Route Logging (Primary Pattern)
Use middleware wrappers that automatically create loggers with request context:
typescriptimport { withError, withAuth, withEmailAccount, withEmailProvider } from "@/utils/middleware"; // Basic route with error handling and logging export const POST = withError("my-route", async (request) => { const logger = request.logger; logger.info("Processing request"); // ... }); // Authenticated route - logger includes userId export const GET = withAuth("my-route", async (request) => { request.logger.info("User action"); // Already has userId context // ... }); // Email account route - logger includes emailAccountId, email export const POST = withEmailAccount("my-route", async (request) => { request.logger.info("Email action"); // Has userId, emailAccountId, email // ... }); // Email provider route - same as email account, plus provides emailProvider export const GET = withEmailProvider("my-route", async (request) => { request.logger.info("Provider action"); const emails = await request.emailProvider.getMessages(); // ... });
The middleware automatically adds:
requestId- Unique ID for request tracingurl- Request URLuserId- For authenticated routesemailAccountId,email- For email account routes
Enriching Logger Context
Add additional context within your route handler:
typescriptexport const POST = withEmailAccount("digest", async (request) => { let logger = request.logger; const body = await request.json(); logger = logger.with({ messageId: body.messageId }); logger.info("Processing message"); // ... });
Helper Function Logging
Helper functions called from routes should receive the logger as a parameter instead of creating their own:
typescriptimport type { Logger } from "@/utils/logger"; export async function processEmail( emailId: string, logger: Logger, ) { logger = logger.with({ emailId }); logger.info("Processing email"); // ... }
Then call from your route:
typescriptexport const POST = withEmailAccount("process", async (request) => { await processEmail(body.emailId, request.logger); });
Server Action Logging
Server actions using actionClient receive the logger through context, similar to route middleware:
typescriptimport { actionClient } from "@/utils/actions/safe-action"; export const createRuleAction = actionClient .metadata({ name: "createRule" }) .inputSchema(createRuleBody) .action( async ({ ctx: { emailAccountId, logger, provider }, parsedInput: { name, actions }, }) => { logger.info("Creating rule", { name }); // ... }, );
The actionClient context provides:
logger- Scoped logger with request contextemailAccountId- Current email accountprovider- Email provider type
When to Use createScopedLogger
Use createScopedLogger only for code that doesn't run within a middleware chain (route or action):
typescriptimport { createScopedLogger } from "@/utils/logger"; // Standalone scripts const logger = createScopedLogger("script/migrate"); // Tests const logger = createScopedLogger("test");
Don't use .with() for a global/file-level logger. Only use within a specific function.
Field Names Must Be Fixed
Axiom turns every nested key of a logged object into a permanent dataset field, and each dataset has a plan-dependent field limit. Once it is full, events that add a new field name are silently dropped.
Never log an object whose keys come from data (rule names, label names, sender addresses, IDs, header names). Log an array instead, since arrays are stored as one field:
typescript// Bad: one new field per rule name logger.info("Scores", { scores: { Newsletter: 0.6, Receipts: 0.4 } }); // Good: one field logger.info("Scores", { scores: [{ rule: "Newsletter", score: 0.6 }, { rule: "Receipts", score: 0.4 }], });


