Logging
How structured logging works, logSource vs scope, verbosity knobs, and the DB query diagnostic.
Overview
All server-side logging goes through rootLogger from
src/classes/loggers/application/. Every log line carries a logSource field
that maps to an architectural layer, making it easy to follow a layer in
production:
grep '"logSource":"action"' .logs/winston-combined.logTwo log files are written to .logs/ at the project root:
| File | Contents |
|---|---|
winston-combined.log | All log levels |
winston-error.log | Errors only |
In production the files write to /tmp/.logs/ (writable on most platforms).
Console output is also emitted in all environments except test.
The logger automatically goes silent during next build
(NEXT_PHASE=phase-production-build). This prevents startup noise from
polluting build output - the logger returns a no-op instance during that
phase.
logSource vs scope
logSource is the architectural layer:
logSource | Layer |
|---|---|
api | app/api/ route handlers |
action | actions/ server actions |
service | classes/services/<domain>/ |
scope is the optional free-form notation for the specific module or concern:
logger.error({
message: 'Failed to create example item',
scope: 'example-items',
error,
});This keeps logSource closed (architectural layer only) while leaving scope
open for whatever granularity you need.
Usage
Get a logger by calling rootLogger with the appropriate source at the top of
your file. The logger is a winston child logger - call info, warn, error,
or debug with a structured object:
import { LOG_SOURCE, rootLogger } from '@/classes/loggers/application';
const logger = rootLogger(LOG_SOURCE.ACTION);
// Informational - structured fields alongside the message
logger.info({ message: 'Item created', id: data.id });
// Error - include scope and the raw error for stack trace
logger.error({
message: 'Failed to create item',
scope: 'example-items',
error,
});See src/actions/example-item-actions.ts for the reference shape in a server
action.
Where to log
Log at the boundary that owns the outcome, not in the service.
Services are thin wrappers around data calls - they rethrow on failure without logging. The write boundary (a server action or a route handler) is where an exception becomes a user-facing result, so that is the one place it's caught and logged. Logging in the service and re-logging in the action would record the same failure twice under two different sources.
// ✅ Action owns the outcome - log once here
try {
const data = await exampleItemService.create(parsed.data);
logger.info({ message: 'Item created', id: data.id });
return { ok: true, data };
} catch (error) {
logger.error({ message: 'Create failed', scope: 'example-items', error });
return { ok: false, error: 'Something went wrong' };
}
// ❌ Don't log in the service and rethrow - it double-logsVerbosity
Two independent knobs control how much you see:
LOG_LEVEL - the winston level floor for all rootLogger output. Defaults
per environment when unset:
| Environment | Default level |
|---|---|
development | debug |
test | warn |
production | info |
Override for a single run:
LOG_LEVEL=debug pnpm devDB_QUERY_LOG - streams raw Drizzle SQL and params to the console. This is an
opt-in diagnostic for watching queries live during development.
DB_QUERY_LOG=1 pnpm devThe query logger (src/classes/loggers/query/) deliberately uses console.debug
rather than rootLogger, wrapping each query in ___QUERY___ / ___END_QUERY___
delimiters:
console.debug('___QUERY___');
console.debug(query);
console.debug(params);
console.debug('___END_QUERY___');This is the one sanctioned console.* in server code. It's a raw dev
firehose, not a structured log - the delimiters just give the SQL and params a
readable, scannable shape in the terminal. Routing it through rootLogger
would wrap every query in JSON envelope noise and defeat the point. It's safe
because it's off by default and gated behind DB_QUERY_LOG=1, so it never
reaches production output.
Console logging
console.log and friends are fine for short-lived debugging.
console.log(response); // quick value inspect
console.table(items); // tabular view of an array
console.dir(obj, { depth: 3 }); // deep object structureJust make sure to clean these up before committing. Stray console.log calls
end up in production bundle output and server logs, adding noise that makes
real issues harder to spot. If a log line is worth keeping, it belongs in
rootLogger so it's structured, filterable, and respects the verbosity
settings.