Structured Logging
beginnerStructured JSON logging with Pino. Fast, low-overhead logging with log levels and context.
loggingpinoobservabilityjson
Tested on⬢20▲16⚛19TS5.9
$ bunx sinew add monitoring/logging1The Problem
Console.log doesn't scale:
- No log levels for filtering
- No structured data for querying
- Slow in production
- No context propagation
2The Solution
Use Pino for fast, structured JSON logging that works great with log aggregation services.
3Files
lib/logger.ts
lib/logger.tsTypeScript
import pino from "pino";
const isDev = process.env.NODE_ENV === "development";
export const logger = pino({
level: process.env.LOG_LEVEL || (isDev ? "debug" : "info"),
...(isDev && {
transport: {
target: "pino-pretty",
options: {
colorize: true,
ignore: "pid,hostname",
translateTime: "HH:MM:ss",
},
},
}),
base: {
env: process.env.NODE_ENV,
version: process.env.npm_package_version,
},
redact: {
paths: ["password", "token", "authorization", "cookie"],
censor: "[REDACTED]",
},
});
// Create child logger with context
export function createLogger(context: Record<string, unknown>) {
return logger.child(context);
}lib/request-logger.ts
lib/request-logger.tsTypeScript
import { NextRequest } from "next/server";
import { logger } from "./logger";
export function logRequest(req: NextRequest, startTime: number, statusCode: number) {
const duration = Date.now() - startTime;
const requestId = req.headers.get("x-request-id") || "unknown";
logger.info({
type: "request",
requestId,
method: req.method,
path: req.nextUrl.pathname,
statusCode,
duration,
userAgent: req.headers.get("user-agent"),
ip: req.headers.get("x-forwarded-for"),
});
}
export function withRequestLogging<T extends unknown[]>(
handler: (req: NextRequest, ...args: T) => Promise<Response>
) {
return async (req: NextRequest, ...args: T): Promise<Response> => {
const startTime = Date.now();
try {
const response = await handler(req, ...args);
logRequest(req, startTime, response.status);
return response;
} catch (error) {
logger.error({
type: "request_error",
method: req.method,
path: req.nextUrl.pathname,
error: error instanceof Error ? error.message : "Unknown error",
});
throw error;
}
};
}lib/action-logger.ts
lib/action-logger.tsTypeScript
import { logger, createLogger } from "./logger";
type ActionContext = {
action: string;
userId?: string;
[key: string]: unknown;
};
export function createActionLogger(context: ActionContext) {
return createLogger({ type: "action", ...context });
}
// Usage in Server Actions
export function withActionLogging<TArgs extends unknown[], TResult>(
actionName: string,
action: (...args: TArgs) => Promise<TResult>
) {
return async (...args: TArgs): Promise<TResult> => {
const startTime = Date.now();
const log = createActionLogger({ action: actionName });
try {
log.info({ event: "action_start" });
const result = await action(...args);
log.info({ event: "action_success", duration: Date.now() - startTime });
return result;
} catch (error) {
log.error({
event: "action_error",
duration: Date.now() - startTime,
error: error instanceof Error ? error.message : "Unknown error",
});
throw error;
}
};
}app/api/users/route.ts
app/api/users/route.tsTypeScript
import { NextRequest, NextResponse } from "next/server";
import { withRequestLogging } from "@/lib/request-logger";
import { logger } from "@/lib/logger";
export const GET = withRequestLogging(async (req: NextRequest) => {
logger.debug({ event: "fetching_users" });
const users = []; // await db.user.findMany();
logger.info({ event: "users_fetched", count: users.length });
return NextResponse.json(users);
});app/actions/create-user.ts
app/actions/create-user.tsTypeScript
"use server";
import { withActionLogging } from "@/lib/action-logger";
import { logger } from "@/lib/logger";
async function createUserAction(data: { name: string; email: string }) {
logger.debug({ event: "creating_user", email: data.email });
// const user = await db.user.create({ data });
logger.info({ event: "user_created", email: data.email });
return { success: true };
}
export const createUser = withActionLogging("createUser", createUserAction);4Dependencies
$ bun add pino$ bun add -D pino-pretty5Configuration
Log Levels
// Available levels (in order of priority)
logger.fatal("App crashed");
logger.error("Operation failed");
logger.warn("Deprecated feature used");
logger.info("User logged in");
logger.debug("Query executed");
logger.trace("Detailed trace info");TypeScript
Environment Variables
# Set log level
LOG_LEVEL="debug" # development
LOG_LEVEL="info" # production
# Disable pretty printing
NODE_ENV="production"Bash
6Usage
Basic Logging
import { logger } from "@/lib/logger";
// Simple message
logger.info("User logged in");
// With context
logger.info({ userId: "123", action: "login" }, "User logged in");
// Error logging
logger.error({ err: error, userId: "123" }, "Operation failed");TypeScript
Child Loggers
import { createLogger } from "@/lib/logger";
// Create a logger with context
const userLogger = createLogger({ module: "users", userId: "123" });
// All logs include the context
userLogger.info("Profile updated");
// Output: {"module":"users","userId":"123","msg":"Profile updated"}TypeScript
7Troubleshooting
Logs not appearing
- Check
LOG_LEVELenvironment variable - Ensure pino-pretty is installed for development
- Verify logger is imported correctly
Performance issues
- Pino is async by default, which is optimal
- Avoid logging large objects
- Use appropriate log levels in production