MasterController

Logging

Category loggers, structured templates, scopes, and request correlation for your application code.

master.createLogger(category) gives your code an ASP.NET Core ILogger<T> equivalent: levels per category from configuration, message templates with structured properties, scopes that flow through await, an X-Request-Id on every request, and pluggable providers. Controllers get this.loggerfor free. (The framework’s own diagnostics keep using the internal error logger; this API is for your code.)

Loggers and levels#

javascript
import master from 'mastercontroller';

const log = master.createLogger('Orders.Service');          // ILoggerFactory.CreateLogger("Orders.Service")

log.info('order {id} placed for {amount}', { id: order.id, amount: order.total });   // message template + properties
log.warn('retrying payment', { attempt: 2 });
log.error('import failed', err);                              // error serialized: { name, message, stack, code }
log.error('import failed', { file }, err);
log.debug('payload', { password: '…' });                      // sensitive keys are redacted

if (log.isEnabled('trace')) log.trace('expensive {dump}', { dump: inspect(state) });
const child = log.child('Retry');                             // category 'Orders.Service.Retry'

Levels: trace · debug · info · warn · error · critical (fatal is an alias). Also log(level, …) and isEnabled(level). Templates are rendered into message while the properties stay on the entry for sinks. Property keys such as password, secret, token, authorization, cookie, apiKey are redacted (master.logging.redact).

Minimum levels from configuration#

Same keys as ASP.NET in config/appsettings.json — the longest matching category prefix wins, Logging:MinimumLevel is honoured too, and levels are re-applied when configuration reloads.

config/appsettings.json
{
  "Logging": {
    "LogLevel": { "Default": "info", "Orders": "debug", "Request": "warn" },
    "Console": { "Json": false }
  }
}

Scopes#

logger.beginScope(state) returns a disposer; await logger.scope(state, fn) is the async-safe form. State is carried across awaits with AsyncLocalStorage and nested scopes merge.

javascript
// BeginScope — state flows through every await inside the callback
await log.scope({ orderId: order.id }, async () => {
  log.info('charging card');          // carries orderId
  await gateway.charge(order);
  log.info('charged');                // still carries orderId
});

// disposer form
const end = log.beginScope({ tenant: 'acme' });
try { log.info('inside'); } finally { end(); }

Requests: correlation and this.logger#

Every request runs inside a scope { requestId, method, path }. An incoming X-Request-Id is honoured, otherwise a UUID is generated; either way it is echoed on the response (HttpContext.TraceIdentifier). One access-log line per request is written under the Request category at info.

ordersController.js
export default class OrdersController {
  async create() {
    this.logger.info('creating order');     // category = 'OrdersController'
    // scope: { requestId, method, path } is already attached to every line in this request
  }
}

Providers#

The console provider is registered by default — readable lines in development, JSON lines when NODE_ENV=production or Logging:Console:Json=true. Add your own sinks with master.logging.addProvider(fn | { write, minimumLevel }); a throwing provider never breaks a request.

javascript
import master, { consoleProvider, memoryProvider } from 'mastercontroller';

// a sink receives plain entries: { timestamp, level, category, message, properties?, error?, scope? }
master.logging.addProvider((entry) => shipToDatadog(entry));
master.logging.addProvider({ name: 'alerts', minimumLevel: 'critical', write: pageSomeone });

master.logging.addProvider(consoleProvider({ json: true }));   // JSON lines (default when NODE_ENV=production)
const mem = memoryProvider();                                  // tests: mem.entries, mem.clear()
master.logging.addProvider(mem);

master.logging.enrich({ service: 'api', version: process.env.APP_VERSION });   // added to every entry
master.logging.setMinimumLevel('warn', 'Request');                              // programmatic override
master.requestLogging = 'scopeOnly';   // keep correlation, drop the per-request access line; false disables both
one JSON line
{"timestamp":"2026-08-22T10:15:04.231Z","level":"info","category":"Orders.Service","message":"order 812 placed for 49.9","properties":{"id":812,"amount":49.9},"scope":{"requestId":"2f1c…","method":"POST","path":"/orders","orderId":812},"service":"api"}
Named exports
import { LoggerFactory, consoleProvider, memoryProvider } from 'mastercontroller' LoggerFactory if you want a standalone factory outside the app instance; memoryProvider() for asserting on log output in tests.