Logging & Observability
Structured logging for Node, Edge and Workers with request IDs, redaction and per-route examples. Works on Vercel, Cloudflare and Node servers.
Basic Concepts
- Structured logs: Emit JSON objects, not free-form strings. Easier to search, filter and correlate.
- Correlation IDs: Attach a stable
request_id
to every log for a request. Lets you trace a user action across middleware, API handlers, and services. - Levels: Use
debug
for development,info
for normal flow,warn
for anomalies, anderror
for failures. - Redaction: Never log secrets, tokens or cookies. Redact known-sensitive keys by default and extend as needed.
What’s Included
- Cross‑runtime logger API with the same shape in Node and Edge/Workers:
- Node (Next.js server functions, serverless, Node server): Pino for fast JSON logs with dev pretty‑print.
- Edge/Workers (Vercel Edge, Cloudflare Workers, middleware): lightweight JSON console implementation.
- Request ID propagation via middleware, without touching auth cookies.
- Example wiring on the storage presign route.
Files
- Node logger:
src/lib/logger/server.ts
- Edge logger:
src/lib/logger/edge.ts
- Logger types:
src/lib/logger/types.ts
- Middleware (sets response
x-request-id
only):src/middleware.ts
- Example usage (presign upload):
src/app/api/storage/uploads/route.ts
Configuration
- Env var:
LOG_LEVEL
=debug
|info
|warn
|error
(defaults:debug
in dev,info
in prod) - Pretty output (dev): pipe through
pino-pretty
instead of in‑process transports:pnpm dev 2>&1 | pnpx pino-pretty
- or save pretty logs to a file:
pnpm dev 2>&1 | pnpx pino-pretty | tee logs/dev.log
- Redaction (Node): see
redactPaths
insrc/lib/logger/server.ts
— extends to headers and common secret‑like keys. - Redaction (Edge): shallow key‑name match; extend
redactKeys
insrc/lib/logger/edge.ts
if needed.
See Your Logs
- Local dev: logs print to your terminal (Node = one‑line JSON; Edge = one‑line JSON). Use
pino-pretty
piping above for readability. - Vercel (Node + Edge):
- Dashboard: Project → Deployments → Functions → Logs.
- CLI:
vercel logs <deployment-url> --since=1h
.
- Cloudflare Workers (Edge):
- CLI:
wrangler tail
while the worker is running. - Dashboard: Workers & Pages → your Worker → Logs.
- CLI:
No Log File by Default
- We do not write to files in code; logs go to stdout/stderr so platforms can capture them.
- To capture locally:
pnpm dev 2>&1 | tee logs/dev.json
(or pipe throughpino-pretty
as shown above). - Avoid file sinks in serverless (Vercel/CF) because disks are ephemeral.
Usage: Node/Serverless routes
import { logger, requestIdFromHeaders } from '@/lib/logger/server';
export async function POST(req: Request) {
const request_id = requestIdFromHeaders(req.headers);
const log = logger.child({ request_id, route: '/api/example' });
const start = Date.now();
try {
log.info({ event: 'example.start' });
// ... handler logic
log.info({ event: 'example.ok', duration_ms: Date.now() - start });
return new Response('ok');
} catch (e: any) {
log.error({ event: 'example.error', message: e?.message });
return new Response('error', { status: 500 });
}
}
Usage: Edge routes / middleware
import { logger } from '@/lib/logger/edge';
export const runtime = 'edge';
export async function GET() {
logger.info({ event: 'edge.heartbeat' });
return new Response('ok');
}
Helper: withApiLogging
- For Node routes, you can wrap a handler to get standard start/ok/error logs with duration automatically.
import { withApiLogging } from '@/lib/logger/server';
export const POST = withApiLogging(async (req: Request) => {
// your logic
return new Response('ok');
}, { route: '/api/foo', event: 'foo.process' });
Example Already Wired
src/app/api/storage/uploads/route.ts
logsstorage.presign.create
with{ request_id, user_id, file_id, key, size, content_type, bucket }
and emits errors asstorage.presign.create.error
.
Best Practices
- Add
request_id
,user_id
(when available) on a per‑request child logger. - Keep messages short; favor fields over long strings.
- Log at completion with
duration_ms
instead of spamming progress logs. - Avoid logging the full request body; log a hash or selected fields.
Customizing Redaction
- Node (Pino): edit
redactPaths
insrc/lib/logger/server.ts
. Pino will replace matched paths with[REDACTED]
. - Edge: edit
redactKeys
insrc/lib/logger/edge.ts
to include more key name patterns.
Replacing console.*
- For server code under
src/app/api/**
andsrc/services/**
, replaceconsole.log/warn/error
with the logger:import { logger } from '@/lib/logger/server'
- Use
logger.info({ event: '...' })
or a request child logger.
- For client components, keep
console
minimal; prefer an error reporter (e.g., Sentry) instead of verbose logs.
Optional Integrations
- Sentry (
@sentry/nextjs
) for exceptions and traces on both client and server. - Managed log sinks (Axiom, Better Stack, Datadog, New Relic) when you need search/retention/alerts:
- Node: add a Pino transport to forward JSON logs.
- Edge: send one‑shot HTTP ingestion calls or rely on platform collectors.
Troubleshooting
- Seeing unexpected redirects to login? Ensure middleware does not override request headers or cookies. Our middleware only sets a response
x-request-id
header and leaves request headers intact. - Missing logs on Edge? Confirm the route or middleware runs in the
edge
runtime and checkwrangler tail
(CF) or Vercel function logs.
Private File Uploads (S3 / R2)
A step‑by‑step, beginner‑friendly guide to adding user‑private uploads with S3‑compatible storage. Includes concepts, setup, env, API, UI, errors, and S3↔R2 migration.
Email Service (Resend)
Integrate transactional email with Resend. Verify your domain, create API keys, render welcome and payment emails on the server, and send through Resend with environment-based configuration.