Logging
Learn how to use Pino for structured logging in your application.
The starter kit uses Pino, a high-performance structured logging library for Node.js. Pino is one of the fastest logging libraries available and provides excellent performance with minimal overhead.
Basic Usage
Import and use the default logger:
import { logger } from '@/lib/logger';
// Simple message
logger.info('Payment processed successfully');
// With structured data
logger.info(
{ userId, amount, transactionId },
'Payment processed successfully'
);
// Error logging
logger.error({ error, userId }, 'Failed to process payment');Log Levels
Pino supports six log levels, from most to least verbose:
trace- Very detailed debugging informationdebug- Debug informationinfo- General informational messages (default)warn- Warning messageserror- Error messagesfatal- Critical errors
Setting Log Level
Control the verbosity of logs via the NEXT_PUBLIC_LOG_LEVEL environment variable:
NEXT_PUBLIC_LOG_LEVEL="debug" # trace, debug, info, warn, error, fatalThe default log level is info. In production, you typically want info or warn to reduce noise.
Grouped Loggers
Create loggers for specific modules or features using the LoggerFactory:
import { LoggerFactory } from '@/lib/logger/factory';
const logger = LoggerFactory.getLogger('Billing');
logger.info({ userId, amount }, 'Processing payment');
// Output: [INFO] Billing: Processing paymentPredefined groups include:
Billing- Payment and subscription related logsAuth- Authentication related logsWebhook- Webhook processing logsDatabase- Database operation logsAPI- API request logsOrganization- Organization management logsUser- User management logsEmail- Email sending logsStorage- File storage logs
You can also create custom groups:
import { LoggerFactory } from '@/lib/logger/factory';
const logger = LoggerFactory.getLogger('Analytics');
logger.info({ event: 'page_view', page: '/dashboard' }, 'Page viewed');Request Context
The logger can merge request context from the AsyncLocalStorage helpers in
lib/logger/context.ts. The repositories do not install a global request
wrapper, so ordinary logger calls include only the fields you pass unless your
code runs inside runWithRequestContext or runWithAdditionalContext.
The context type supports:
requestId- Unique request identifieruserId- Current user IDuserEmail- Current user emailuserRole- Current user roleorganizationId- Active organization IDuserAgent- User agent stringip- Client IP addressendpoint- API endpointmethod- HTTP methodtrpcProcedure- tRPC procedure nametrpcType- tRPC call type (query/mutation)webhookType- Webhook event typesessionId- Session ID
Initialize the context at a request boundary before relying on automatic enrichment:
import { headers } from 'next/headers';
import { logger } from '@/lib/logger';
import { runWithRequestContext } from '@/lib/logger/server';
export async function GET() {
const requestHeaders = await headers();
return runWithRequestContext(
{
requestId: requestHeaders.get('x-request-id') ?? undefined,
userAgent: requestHeaders.get('user-agent') ?? undefined,
endpoint: '/api/users',
method: 'GET'
},
async () => {
logger.info({ action: 'list_users' }, 'Fetching user list');
return Response.json({ users: [] });
}
);
}The shipped tRPC middleware does not use this storage wrapper. On failed tRPC procedures it explicitly logs the procedure, duration, request metadata and any available user or organization fields. Successful procedure calls are not logged automatically.
Context can contain personal data
User email, IP address and user-agent values are supported context fields. Add only what you need, define a retention policy and never log session cookies, authorization headers, passwords, API keys or payment details.
Development vs Production
In development, logs are formatted with colors and readable output:
[INFO] Billing: Payment processed successfully (userId=123 amount=29.99 transactionId=txn_abc)In production, logs are output as JSON for easy parsing by log aggregation services:
{
"level": 30,
"time": 1234567890,
"group": "Billing",
"msg": "Payment processed successfully",
"userId": "123",
"amount": 29.99,
"transactionId": "txn_abc"
}Examples
Server Actions
'use server';
import { logger } from '@/lib/logger';
export async function createUser(email: string, name: string) {
try {
logger.info({ email, name }, 'Creating new user');
// ... create user logic ...
logger.info({ userId: user.id, email }, 'User created successfully');
return { success: true, user };
} catch (error) {
logger.error({ error, email }, 'Failed to create user');
throw error;
}
}API Routes
import { logger } from '@/lib/logger';
export async function POST(request: Request) {
const body = await request.json();
logger.info(
{
webhookType: body.type,
eventId: body.id
},
'Received Stripe webhook'
);
// ... process webhook ...
logger.info({ webhookType: body.type }, 'Webhook processed successfully');
return new Response('OK', { status: 200 });
}tRPC Procedures
import { createTRPCRouter, protectedProcedure } from '@/trpc/init';
import { z } from 'zod';
import { logger } from '@/lib/logger';
export const userRouter = createTRPCRouter({
update: protectedProcedure
.input(z.object({ name: z.string() }))
.mutation(async ({ ctx, input }) => {
logger.info({ userId: ctx.user.id, name: input.name }, 'Updating user');
// ... update logic ...
logger.info({ userId: ctx.user.id }, 'User updated successfully');
return user;
})
});Error Handling
import { logger } from '@/lib/logger';
export function handleError(error: unknown, context?: Record<string, unknown>) {
if (error instanceof Error) {
logger.error(
{
error: error.message,
stack: error.stack,
...context
},
'Error occurred'
);
} else {
logger.error({ error, ...context }, 'Unknown error occurred');
}
}Best Practices
-
Use appropriate log levels: Use
infofor normal operations,warnfor recoverable issues, anderrorfor failures. -
Include structured data: Always include relevant context in your logs:
// ✅ Good logger.info({ userId, orderId, amount }, 'Order processed'); // ❌ Less useful logger.info('Order processed'); -
Use grouped loggers: Create loggers for different modules to make logs easier to filter and search.
-
Don't log sensitive data: Avoid logging passwords, tokens, or other sensitive information.
-
Initialize request context deliberately: Wrap the request before expecting automatic enrichment, or pass identifiers explicitly at the log call.
-
Log at appropriate times: Log important state changes, errors, and significant events, but avoid excessive logging that can impact performance.
Integration with Log Aggregation Services
The JSON output in production is compatible with popular log aggregation services:
- Vercel Logs: Automatically captured in Vercel deployments
- Datadog: Can parse JSON logs
- LogRocket: Supports structured logging
- CloudWatch: AWS CloudWatch Logs
- Google Cloud Logging: Supports JSON logs
Next Steps
- Learn about Sentry for error tracking
- Check out Vercel Analytics for traffic monitoring
- Explore Speed Insights for performance monitoring