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 automatically includes request context when available. This context is set via AsyncLocalStorage and includes:
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
The context is automatically merged with your log data:
import { logger } from '@/lib/logger';
// This log will automatically include requestId, userId, endpoint, method, etc.
logger.info({ action: 'list_users' }, 'Fetching user list');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.
-
Use request context: The logger automatically includes request context, so you don't need to manually pass common fields like
userIdorrequestId. -
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