General
Observability

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:

lib/actions/billing.ts
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 information
  • debug - Debug information
  • info - General informational messages (default)
  • warn - Warning messages
  • error - Error messages
  • fatal - Critical errors

Setting Log Level

Control the verbosity of logs via the NEXT_PUBLIC_LOG_LEVEL environment variable:

.env.local
NEXT_PUBLIC_LOG_LEVEL="debug" # trace, debug, info, warn, error, fatal

The 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:

lib/billing/payment-processor.ts
import { LoggerFactory } from '@/lib/logger/factory';

const logger = LoggerFactory.getLogger('Billing');

logger.info({ userId, amount }, 'Processing payment');
// Output: [INFO] Billing: Processing payment

Predefined groups include:

  • Billing - Payment and subscription related logs
  • Auth - Authentication related logs
  • Webhook - Webhook processing logs
  • Database - Database operation logs
  • API - API request logs
  • Organization - Organization management logs
  • User - User management logs
  • Email - Email sending logs
  • Storage - File storage logs

You can also create custom groups:

lib/features/analytics.ts
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 identifier
  • userId - Current user ID
  • userEmail - Current user email
  • userRole - Current user role
  • organizationId - Active organization ID
  • userAgent - User agent string
  • ip - Client IP address
  • endpoint - API endpoint
  • method - HTTP method
  • trpcProcedure - tRPC procedure name
  • trpcType - tRPC call type (query/mutation)
  • webhookType - Webhook event type
  • sessionId - Session ID

The context is automatically merged with your log data:

app/api/users/route.ts
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

app/actions/create-user.ts
'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

app/api/webhooks/stripe/route.ts
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

trpc/routers/user.ts
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

lib/utils/error-handler.ts
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

  1. Use appropriate log levels: Use info for normal operations, warn for recoverable issues, and error for failures.

  2. Include structured data: Always include relevant context in your logs:

    // ✅ Good
    logger.info({ userId, orderId, amount }, 'Order processed');
    
    // ❌ Less useful
    logger.info('Order processed');
  3. Use grouped loggers: Create loggers for different modules to make logs easier to filter and search.

  4. Don't log sensitive data: Avoid logging passwords, tokens, or other sensitive information.

  5. Use request context: The logger automatically includes request context, so you don't need to manually pass common fields like userId or requestId.

  6. 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