Pino Logging & Sentry Error Monitoring
NEXTY.DEV ships a production-grade logging and error monitoring solution built on Pino + Sentry.
Architecture Overview
Repository
│
└── getLogger("module-name")
│
├── Pino (structured logging)
│ ├── Development → colorized output (pino-pretty)
│ ├── Production → JSON to stdout (collected by cloud platform)
│ └── VPS + LOG_DIR → file rotation (pino-roll)
│
└── Sentry (error reporting)
├── warn → add Breadcrumb
├── error → captureException
└── fatal → captureException (level: fatal)Each tool has a clear responsibility:
- Pino handles all log output — high-performance, structured JSON
- Sentry handles error aggregation, alerting, stack traces, and user impact analysis
Environment Variables
All variables are optional. When not configured, all Sentry calls are silently ignored, and Pino defaults to info level output to stdout.
Sentry
NEXT_PUBLIC_SENTRY_DSN
How to get it:
- Log in to sentry.io and open your project (or create a new Next.js project)
- Left sidebar → Settings → Projects → select your project
- Left sidebar → Client Keys (DSN)
- Copy the DSN value — it looks like
https://[email protected]/xxx
NEXT_PUBLIC_SENTRY_DSN=https://[email protected]/789

SENTRY_AUTH_TOKEN (optional, for source map uploads)
How to get it:
- Log in to sentry.io → left sidebar Settings → Organization Tokens
- Click Create New Token
- Copy the generated token (it starts with
sntrys_)
SENTRY_AUTH_TOKEN=sntrys_eyJ0eXBlIjoiYXV0aF90b2tlbiJ9...
SENTRY_ORG and SENTRY_PROJECT (used with AUTH_TOKEN)
How to get them:
SENTRY_ORG: Settings → General Settings → Organization Slug (also the org name in your sentry.io URL, e.g.my-companyfromhttps://my-company.sentry.io)SENTRY_PROJECT: Settings → Projects → click your project → Project Slug
SENTRY_ORG=my-company
SENTRY_PROJECT=my-nextjs-appSENTRY_DEBUG (optional)
By default, Sentry does not report events in development (to avoid polluting your data). Set this to true to enable real event reporting in development — useful when debugging the Sentry integration itself.
SENTRY_DEBUG=truePino
LOG_LEVEL
Controls which log levels are emitted. Anything below this level is suppressed. Accepted values: trace | debug | info | warn | error | fatal.
LOG_LEVEL=info # recommended for production
LOG_LEVEL=debug # use temporarily when troubleshootingLOG_DIR (optional, VPS only)
When set, enables pino-roll file rotation and writes logs to this directory. Leave it unset on Vercel and Cloudflare Workers — those platforms collect stdout automatically.
LOG_DIR=./logs # relative to the project root
LOG_DIR=/var/log/myapp # or an absolute pathUsage
Basic Usage
import { getLogger } from "@/lib/logger";
const logger = getLogger("payment-service");
// trace / debug: for development debugging, suppressed in production by default
logger.trace({ sql: "SELECT ..." }, "Executing query");
logger.debug({ params }, "Request received");
// info: normal business events
logger.info({ userId, planId }, "User subscribed");
// warn: recoverable issues (also adds a Sentry Breadcrumb)
logger.warn({ retryCount: 3, url }, "Webhook delivery failed, retrying");
// error: errors (automatically reported to Sentry)
logger.error(new Error("Stripe API timeout"), "Payment failed");
// fatal: critical failures (automatically reported to Sentry at fatal level)
logger.fatal(new Error("DB connection lost"), "Service unavailable");Errors with Context
// Pass an Error object directly
logger.error(new Error("Invalid signature"), "Webhook verification failed");
// Pass an object — when it contains an `err` field, it's automatically extracted and sent to Sentry
logger.error({ err: error, webhookId, payload }, "Webhook processing error");
// Manual capture: equivalent to error(), but more explicit
logger.captureError(error, { userId, action: "checkout" });Accessing the Underlying Pino Logger
When you need to create a child logger or use advanced Pino features:
const logger = getLogger("api");
const childLogger = logger.pino.child({ requestId: "req-123" });
childLogger.info("Request started");In a Server Action
// app/actions/payment.ts
import { getLogger } from "@/lib/logger";
const logger = getLogger("payment-action");
export async function createCheckout(planId: string) {
try {
const session = await stripe.checkout.sessions.create({ ... });
logger.info({ planId, sessionId: session.id }, "Checkout session created");
return { url: session.url };
} catch (error) {
logger.error(error instanceof Error ? error : new Error(String(error)), "Checkout failed");
throw error;
}
}In a Route Handler
// app/api/webhooks/stripe/route.ts
import { getLogger } from "@/lib/logger";
const logger = getLogger("stripe-webhook");
export async function POST(request: Request) {
try {
// ...
logger.info({ event: event.type }, "Webhook processed");
return Response.json({ ok: true });
} catch (error) {
logger.error(error instanceof Error ? error : new Error(String(error)), "Webhook error");
return Response.json({ error: "Internal error" }, { status: 500 });
}
}Log Levels
| Level | When to use | Sentry behavior |
|---|---|---|
trace | SQL queries, detailed execution paths | None |
debug | Request parameters, intermediate state | None |
info | User actions, order creation, login | None |
warn | Retries, degraded mode, rate limiting | Adds Breadcrumb |
error | API failures, database errors, payment failures | Reports exception |
fatal | Service unavailable, initialization failure | Reports exception (fatal level) |
The default production level is info, so trace and debug logs are suppressed. To enable them temporarily, set LOG_LEVEL=debug.
Deployment Guide
Vercel
No additional configuration needed. Stdout from Next.js functions is automatically forwarded to Vercel's logging system.
View logs: Vercel dashboard → project → Logs → Functions
NEXT_PUBLIC_SENTRY_DSN=https://[email protected]/xxx
LOG_LEVEL=info
# Leave LOG_DIR unsetAttention: Vercel function logs are only retained for 1 hour on the free plan. For production, pair this with Sentry as your primary error tracking tool.
Cloudflare Workers
Workers run on the Edge runtime with no filesystem. The logger automatically detects the Edge environment and falls back to pure JSON stdout mode (skipping pino-roll and pino-pretty).
View logs: Cloudflare dashboard → Workers & Pages → project → Logs
NEXT_PUBLIC_SENTRY_DSN=https://[email protected]/xxx
LOG_LEVEL=info
# LOG_DIR must be left unset — Cloudflare Workers has no filesystemReal-time logs: During development, use wrangler tail to stream logs directly in your terminal.
VPS — Coolify
Coolify runs your app in a Docker container. Stdout is captured automatically by Docker and is viewable in the dashboard.
View stdout logs: Coolify dashboard → app → Logs tab (live streaming)
NEXT_PUBLIC_SENTRY_DSN=https://[email protected]/xxx
LOG_LEVEL=info
# Leave LOG_DIR unset — use stdout, viewable directly in the Coolify dashboardTo persist logs to a file:
- Coolify dashboard → app → Storages → Add Storage
- Fill in:
- Name:
logs - Source Path (Host): path on the host machine, e.g.
/var/lib/coolify/myapp/logs - Destination Path (Container): path inside the container, e.g.
/app/logs
- Name:
- Set the environment variable:
LOG_DIR=/app/logs- After redeploying, log files will be written to
/var/lib/coolify/myapp/logs/on the host. You can follow them over SSH:
tail -f /var/lib/coolify/myapp/logs/payment-service.2025-03-08.1.logVPS — Dokploy
Dokploy also runs on Docker. Stdout logs are viewable directly in the dashboard.
View stdout logs: Dokploy dashboard → app → Logs tab
NEXT_PUBLIC_SENTRY_DSN=https://[email protected]/xxx
LOG_LEVEL=info
# Leave LOG_DIR unset — use stdoutTo persist logs to a file:
- Dokploy dashboard → app → Advanced → Mounts → Add Mount
- Select Volume Mount and fill in:
- Host Path: path on the host machine, e.g.
/var/dokploy/myapp/logs - Container Path: path inside the container, e.g.
/app/logs
- Host Path: path on the host machine, e.g.
- Set the environment variable:
LOG_DIR=/app/logs- After redeploying, follow the logs over SSH:
tail -f /var/dokploy/myapp/logs/payment-service.2025-03-08.1.logVPS — Node.js / PM2
Option A: PM2 (recommended — no LOG_DIR needed)
PM2 automatically collects stdout to a file with no code changes required:
pm2 start npm --name "myapp" -- start
pm2 logs myapp # stream logs in real time
# Log file location: ~/.pm2/logs/myapp-out.logOption B: LOG_DIR file rotation
For setups without PM2:
LOG_DIR=./logs
# Log files: logs/payment-service.2025-03-08.1.log (rotated daily, split at 10 MB)Pair this with a cron job to clean up old logs:
# Keep the last 30 days
0 2 * * * find /path/to/app/logs -name "*.log" -mtime +30 -deleteBest Practices
Use structured fields — avoid string interpolation
// Recommended: fields can be indexed and filtered by your logging platform
logger.info({ userId, amount, currency }, "Payment completed");
// Avoid: not searchable
logger.info({}, `User ${userId} paid ${amount} ${currency}`);Keep sensitive data out of logs
// Never log passwords, tokens, or full card numbers
logger.info({ password, stripeKey }, "User data");
// Only log the non-sensitive identifiers you actually need
logger.info({ userId, last4: card.last4 }, "Payment method added");Use a separate logger per module
const logger = getLogger("stripe-webhook"); // payment webhooks
const logger = getLogger("ai-chat"); // AI features
const logger = getLogger("auth"); // authenticationDo not use in client components
The logger depends on the Node.js/Edge runtime and must only be used server-side:
// Server Component, Server Action, Route Handler, middleware
import { getLogger } from "@/lib/logger";
// Do NOT use inside "use client" componentsClient-side errors are captured automatically by the Sentry SDK configured in sentry.client.config.ts — no manual logger calls needed.
Related Files
lib/
logger/
index.ts # Logger core implementation
sentry.client.config.ts # Client-side Sentry config (includes Session Replay)
sentry.server.config.ts # Server-side Sentry config
sentry.edge.config.ts # Edge runtime Sentry config
instrumentation.ts # Next.js initialization hook
next.config.mjs # serverExternalPackages + withSentryConfig