EEPNext.js Template
Conventions

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.log

Two log files are written to .logs/ at the project root:

FileContents
winston-combined.logAll log levels
winston-error.logErrors 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:

logSourceLayer
apiapp/api/ route handlers
actionactions/ server actions
serviceclasses/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-logs

Verbosity

Two independent knobs control how much you see:

LOG_LEVEL - the winston level floor for all rootLogger output. Defaults per environment when unset:

EnvironmentDefault level
developmentdebug
testwarn
productioninfo

Override for a single run:

LOG_LEVEL=debug pnpm dev

DB_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 dev

The 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 structure

Just 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.


On this page