Get the FREE Ultimate OpenClaw Setup Guide →
npx machina-cli add skill JanSzewczyk/claude-plugins/structured-logging --openclaw
Files (1)
SKILL.md
8.5 KB

Structured Logging Skill

Structured logging patterns with Pino for Next.js applications.

Reference Files:

Project Configuration

The logger is configured in lib/logger.ts:

import pino from "pino";

const logger = pino({
  level: process.env.LOG_LEVEL || "info",
  transport:
    process.env.NODE_ENV === "development"
      ? {
          target: "pino-pretty",
          options: {
            colorize: true,
            translateTime: "SYS:standard",
            ignore: "pid,hostname",
          },
        }
      : undefined,
  formatters: {
    level: (label) => ({ level: label.toUpperCase() }),
  },
  timestamp: pino.stdTimeFunctions.isoTime,
});

export function createLogger(context: Record<string, unknown>) {
  return logger.child(context);
}

export default logger;

Quick Start

Basic Logging

import logger from "~/lib/logger";

// Simple message
logger.info("Server started");

// With context object
logger.info({ port: 3000 }, "Server started");

// Error logging
logger.error({ error, userId }, "Failed to process request");

Module-Specific Logger

import { createLogger } from "~/lib/logger";

const logger = createLogger({ module: "user-service" });

// All logs include { module: "user-service" }
logger.info({ userId: "123" }, "User created");
// Output: { module: "user-service", userId: "123", msg: "User created" }

Log Levels

LevelWhen to UseExample
fatalApp crash, unrecoverableDatabase connection lost
errorOperation failedUser creation failed
warnUnexpected but recoverableRate limit approaching
infoNormal operationsUser logged in
debugDevelopment detailsRequest payload
traceFine-grained debuggingFunction entry/exit

Usage

logger.fatal({ error }, "Database connection lost, shutting down");
logger.error({ userId, errorCode: error.code }, "Failed to update user");
logger.warn({ requestCount, limit }, "Rate limit 80% reached");
logger.info({ userId }, "User logged in successfully");
logger.debug({ payload }, "Processing request");
logger.trace({ functionName: "processData" }, "Entering function");

Key Patterns

Always Log Context Objects First

// ✅ Good - context object first, then message
logger.info({ userId, action: "login" }, "User authenticated");

// ❌ Bad - no context
logger.info("User authenticated");

// ❌ Bad - string interpolation
logger.info(`User ${userId} authenticated`);

Error Logging

import { categorizeDbError, DbError } from "~/lib/firebase/errors";

try {
  await updateUser(userId, data);
} catch (error) {
  const dbError = categorizeDbError(error, "User");

  logger.error(
    {
      userId,
      errorCode: dbError.code,
      isRetryable: dbError.isRetryable,
      operation: "updateUser",
    },
    "Failed to update user",
  );

  return [dbError, null];
}

Database Operations

const logger = createLogger({ module: "user-db" });

export async function getUserById(id: string) {
  logger.debug({ userId: id }, "Fetching user");

  try {
    const user = await db.collection("users").doc(id).get();

    if (!user.exists) {
      logger.warn({ userId: id }, "User not found");
      return [DbError.notFound("User"), null];
    }

    logger.info({ userId: id }, "User fetched successfully");
    return [null, transformUser(user)];
  } catch (error) {
    const dbError = categorizeDbError(error, "User");
    logger.error(
      {
        userId: id,
        errorCode: dbError.code,
        isRetryable: dbError.isRetryable,
      },
      "Database error fetching user",
    );
    return [dbError, null];
  }
}

Server Actions

const logger = createLogger({ module: "user-actions" });

export async function updateProfile(data: ProfileData): ActionResponse {
  const { userId } = await auth();

  if (!userId) {
    logger.warn({ action: "updateProfile" }, "Unauthorized access attempt");
    return { success: false, error: "Unauthorized" };
  }

  logger.info({ userId, action: "updateProfile" }, "Starting profile update");

  const [error] = await updateUserProfile(userId, data);

  if (error) {
    logger.error(
      {
        userId,
        errorCode: error.code,
        action: "updateProfile",
      },
      "Profile update failed",
    );
    return { success: false, error: error.message };
  }

  logger.info(
    { userId, action: "updateProfile" },
    "Profile updated successfully",
  );
  return { success: true, data: null };
}

Environment Configuration

# .env.local
LOG_LEVEL=debug  # Development: see all logs

# .env.production
LOG_LEVEL=info   # Production: info and above

File Locations

PurposeLocation
Logger setuplib/logger.ts
Feature loggersCreate in feature modules
Log level configdata/env/server.ts

Sensitive Data Protection

Never log secrets, credentials, or personally identifiable information (PII).

What NOT to Log

// ❌ NEVER log these
logger.info({ password, token, apiKey }, "User login");
logger.info({ creditCard: card.number }, "Payment processed");
logger.info({ ssn, dateOfBirth, fullAddress }, "User profile loaded");
logger.debug({ cookie: req.headers.cookie }, "Request received");
logger.info({ authorization: req.headers.authorization }, "API call");

Safe Logging Patterns

// ✅ Log identifiers, not values
logger.info({ userId, email: maskEmail(email) }, "User login");
logger.info({ cardLast4: card.number.slice(-4) }, "Payment processed");
logger.debug({ hasAuthHeader: !!req.headers.authorization }, "API call");

// ✅ Log metadata, not content
logger.info({ bodySize: JSON.stringify(body).length }, "Request received");
logger.info({ fieldCount: Object.keys(formData).length }, "Form submitted");

Sensitive Fields Checklist

CategoryFields to NEVER log
Authpassword, token, apiKey, secret, refreshToken, sessionId, cookie, authorization header
PIISSN, date of birth, full address, phone number (log last 4 digits max)
Financialcredit card number, bank account, CVV, routing number
Healthmedical records, diagnoses, insurance IDs

Masking Helper

export function maskEmail(email: string): string {
  const [local, domain] = email.split("@");
  return `${local[0]}***@${domain}`;
}

export function maskId(id: string): string {
  return id.length > 8 ? `${id.slice(0, 4)}...${id.slice(-4)}` : "***";
}

Framework-Level Protection

Consider adding a Pino redaction config to automatically strip sensitive fields:

const logger = pino({
  level: process.env.LOG_LEVEL || "info",
  redact: {
    paths: [
      "password",
      "token",
      "apiKey",
      "*.password",
      "*.token",
      "*.apiKey",
      "authorization",
      "cookie",
      "creditCard",
      "ssn",
    ],
    censor: "[REDACTED]",
  },
});

Related Skills

  • firebase-firestore - Database logging patterns
  • server-actions - Action logging patterns
  • t3-env-validation - LOG_LEVEL configuration

Source

git clone https://github.com/JanSzewczyk/claude-plugins/blob/main/plugins/nextjs-react/skills/structured-logging/SKILL.mdView on GitHub

Overview

This skill teaches structured logging using Pino in Next.js applications. It covers log levels, context enrichment, and creating child loggers to keep logs consistent and searchable, along with production best practices for observability.

How This Skill Works

Configure Pino in lib/logger.ts with a dynamic level and a development transport for readability. Use createLogger(context) to produce child loggers that automatically include context in every log entry, and always pass a context object before the message to keep logs structured and searchable in production.

When to Use It

  • Add logging to a server action to trace flow and outcomes
  • Decide which log level to use for events and errors
  • Create a child logger with a shared context (e.g., module or request) for consistent logs
  • Log errors with stack traces and relevant context to aid debugging
  • Operate in production with structured JSON logs and appropriate transports

Quick Start

  1. Step 1: Configure the logger in lib/logger.ts with pino and development transport (pino-pretty) for readability
  2. Step 2: Use logger.info, logger.error, etc., and always pass a context object (e.g., { module: 'service' }) before the message
  3. Step 3: Create module-specific loggers via createLogger({ module: 'your-service' }) or logger.child(context) and reference them in code

Best Practices

  • Always log a context object first, before the message
  • Prefer structured object payloads over string interpolation in logs
  • Use appropriate levels: fatal, error, warn, info, debug, trace
  • Leverage child loggers to attach module, request, or operation context
  • In production, rely on JSON logs and transport configurations; enable pretty printing only in development

Example Use Cases

  • Server started with an info log: logger.info('Server started')
  • Module-specific logging: const logger = createLogger({ module: 'user-service' }); logger.info({ userId: '123' }, 'User created')
  • Error logging with context: logger.error({ userId, errorCode: error.code }, 'Failed to process request')
  • Create a child logger with context: const logger = createLogger({ module: 'order-service', requestId: 'abc-123' });
  • Comprehensive level usage: logger.fatal(...); logger.error(...); logger.warn(...); logger.info(...); logger.debug(...); logger.trace(...)

Frequently Asked Questions

Add this skill to your agents

Related Skills

Sponsor this space

Reach thousands of developers