JCC Express

Digging Deeper

Logging

Introduction

JCC Express MVC ships with a Laravel-style logging API built on top of winston.

Core capabilities:

  • PSR-3 / RFC 5424 levels (emergency, alert, critical, error, warning, notice, info, debug)
  • Multiple channels (single, daily, console, stderr, stack)
  • Channel stacks — write one message to several channels at once
  • Per-logger context (withContext) attached to every entry
  • Static facade usage via Log (exported from jcc-express-mvc/Core)
  • A global logger() helper for quick debug logging

Configuration and bootstrapping

Logging is configured in app/Config/logging.ts and merged in app/Config/index.ts. At runtime, LogServiceProvider reads app.config.logging and calls Log.configure(...). If no config is present it falls back to defaults derived from the LOG_CHANNEL, LOG_LEVEL and LOG_DAILY_DAYS environment variables.

TypeScript
import path from "path";
import appRootPath from "app-root-path";
import { config } from "jcc-express-mvc";
import type { LoggingConfig, LogLevel } from "jcc-express-mvc/Core";

const logDir = path.join(appRootPath.path, "storage", "logs");
const level = config.get("LOG_LEVEL", "debug") as LogLevel;

export const logging: LoggingConfig = {
  // Channel used by Log.info(...) and the global logger() helper.
  default: config.get("LOG_CHANNEL", "stack"),

  channels: {
    // Fans the message out to several channels at once.
    stack: { driver: "stack", channels: ["single", "console"] },

    // A single growing file: storage/logs/app.log
    single: { driver: "single", path: path.join(logDir, "app.log"), level },

    // Date-stamped files (storage/logs/jcc-YYYY-MM-DD.log), pruned after `days`.
    daily: {
      driver: "daily",
      path: path.join(logDir, "jcc.log"),
      level,
      days: Number(config.get("LOG_DAILY_DAYS", "14")),
    },

    console: { driver: "console", level },
    stderr: { driver: "stderr", level },
  },
};

Relevant .env keys:

dotenv
LOG_CHANNEL=stack   # default channel name
LOG_LEVEL=debug     # minimum severity written by file/console channels
LOG_DAILY_DAYS=14   # how many daily log files to keep

You can also reconfigure logging at runtime (useful in tests):

TypeScript
import { Log } from "jcc-express-mvc/Core";

Log.configure({
  default: "daily",
  channels: {
    daily: { driver: "daily", path: "storage/logs/jcc.log", days: 7, level: "info" },
    console: { driver: "console", level: "debug" },
    stack: { driver: "stack", channels: ["daily", "console"] },
  },
});

Channel drivers

DriverDescription
singleAppends every entry to one file (path, default storage/logs/app.log).
dailyWrites to <name>-YYYY-MM-DD.log next to path and prunes files older than days (default 14).
consoleWrites to stdout.
stderrLike console, but all levels go to stderr.
stackForwards to a list of other channels (channels: [...]).

Each non-stack channel accepts an optional level — entries below that severity are dropped for that channel.

The daily driver is a lightweight built-in transport (no extra dependency). If you need rotation by size or compression, install winston-daily-rotate-file and configure a custom channel in your logging config.


Writing log messages

TypeScript
import { Log } from "jcc-express-mvc/Core";

Log.emergency("System is unusable");
Log.alert("Action required immediately");
Log.critical("Critical condition");
Log.error("Something failed");
Log.warning("Disk space getting low");
Log.notice("Unusual but harmless");
Log.info("User registered");
Log.debug("Cache miss for key user:1");

Each method accepts an optional context object, which is appended to the line as JSON:

TypeScript
Log.info("Order placed", { orderId: 42, total: 19.99 });
// [2026-05-11 12:00:00] INFO: Order placed {"orderId":42,"total":19.99}

Passing an Error logs its message and stack trace:

TypeScript
try {
  await processPayment();
} catch (err) {
  Log.error(err as Error);
}

Log.log(level, message, context?) (and its alias Log.write) let you pass the level dynamically.


Channels and stacks

TypeScript
// Write to a specific channel
Log.channel("daily").warning("disk getting full");

// Build an ad-hoc stack on the fly
Log.stack(["single", "stderr"]).critical("payment gateway down");

Log.channel() with no argument returns the default channel — the same target used by Log.info(...).


Contextual logging

withContext returns a logger that merges the given data into every subsequent entry — handy for request IDs, user IDs, job names, etc.

TypeScript
const log = Log.withContext({ requestId: req.id, userId: req.user?.id });

log.info("handling request");
log.info("request finished", { status: 200 });

You can also bind context on a specific channel:

TypeScript
Log.channel("daily").withContext({ job: "ProcessEmails" }).info("started");

The logger() helper

A global logger() helper is available everywhere for quick logging:

TypeScript
logger("cache warmed", { keys: 12 });

Log files

By default, file channels write under storage/logs/:

  • singlestorage/logs/app.log
  • dailystorage/logs/jcc-YYYY-MM-DD.log

The directory is created automatically on first write.


Summary

  • Configure channels in app/Config/logging.ts; LogServiceProvider applies it via Log.configure(...).
  • Use the Log facade (jcc-express-mvc/Core) with PSR-3 level methods.
  • Use Log.channel(name) / Log.stack([...]) to target specific outputs.
  • Use Log.withContext({...}) to attach structured data to every entry.
  • Tune LOG_CHANNEL, LOG_LEVEL and LOG_DAILY_DAYS in .env.