bedrock
Version:
A core foundation for rich Web applications.
198 lines (180 loc) • 5.71 kB
JavaScript
/*!
* Copyright (c) 2012-2022 Digital Bazaar, Inc. All rights reserved.
*/
import * as fileLogger from './fileLogger.js';
import * as formatters from './formatters.js';
import cluster from 'cluster';
import {config} from '../config.js';
import crypto from 'crypto';
import {Mail as WinstonMail} from 'winston-mail';
import util from 'util';
import winston from 'winston';
import {WorkerTransport} from './WorkerTransport.js';
const randomBytes = util.promisify(crypto.randomBytes);
// create custom logging levels
const levels = {
silly: 6,
verbose: 5,
debug: 4,
info: 3,
warning: 2,
error: 1,
critical: 0
};
const colors = {
silly: 'cyan',
verbose: 'blue',
debug: 'blue',
info: 'green',
warning: 'yellow',
error: 'red',
critical: 'red'
};
// create the container for the primary and all of the workers
export const container = new winston.Container();
// override get to use a wrapper so loggers that are retrieved via `.get()`
// prior to logger initialization will receive the updated configuration
// when logging after initialization
container._add = container.add;
container.add = function(id) {
const existing = container.has(id);
let logger = container._add.apply(container, arguments);
if(!existing) {
const wrapper = Object.create(logger);
wrapper.log = function(level, msg, ...meta) {
// get default meta to ensure it is always applied
const {defaultMeta} = this;
// use `Object.getPrototypeOf` because the prototype may change
// while the logging subsystem initializes
const log = Object.getPrototypeOf(wrapper).log;
if(arguments.length === 1) {
return log.apply(wrapper, [level]);
}
meta[0] = {...defaultMeta, ...meta[0]};
return log.apply(wrapper, [level, msg, ...meta]);
};
const createChild = wrapper.child;
wrapper.child = function(childMeta) {
// simple string name is shortcut for {module: name}
if(typeof childMeta === 'string') {
childMeta = {module: childMeta};
}
// create child logger from wrapper (merging child meta into parent meta)
return createChild.apply(wrapper, [childMeta]);
};
logger = wrapper;
container.loggers.set(id, wrapper);
}
return logger;
};
if(cluster.isMaster) {
// reserved transports
container.transports = {
access: null,
app: null,
console: null,
email: null,
error: null,
};
}
/**
* Initializes the logging system.
*/
container.init = async () => {
const workerId = await _generateWorkerId();
if(cluster.isMaster) {
// create shared transports
const transports = container.transports;
transports.console = new winston.transports.Console({
...config.loggers.console,
format: formatters.fromConfig(config.loggers.console.bedrock),
});
transports.email = new WinstonMail(config.loggers.email);
if(config.loggers.enableFileTransport) {
await fileLogger.init({transports});
}
// create primary loggers
for(const cat in config.loggers.categories) {
const transportNames = config.loggers.categories[cat];
const options = {levels, transports: []};
transportNames.forEach(name => {
// only use transports that have been initialized
if(transports[name]) {
options.transports.push(transports[name]);
}
});
const logger = winston.createLogger(options);
const _existingLogger = container.get(cat);
if(_existingLogger) {
_existingLogger.__proto__ = logger;
} else {
const wrapper = {};
wrapper.__proto__ = logger;
container.loggers.set(cat, wrapper);
}
}
// set the colors to use on the console
winston.addColors(colors);
/**
* Attaches a message handler to the given worker. This should be
* called by the primary process to handle worker log messages.
*
* @param {object} worker - The worker to attach the message handler to.
*/
container.attach = function(worker) {
// set up message handler for primary process
worker.on('message', function(msg) {
if(typeof msg === 'object' && msg.type === 'bedrock.logger') {
container.get(msg.category).log(msg.info);
}
});
};
return;
}
// create worker loggers
const lowestLevel = Object.keys(levels)[0];
for(const cat in config.loggers.categories) {
const logger = winston.createLogger({
levels,
transports: [
new WorkerTransport({level: lowestLevel, category: cat, workerId})
]
});
const _existingLogger = container.get(cat);
if(_existingLogger) {
_existingLogger.__proto__ = logger;
} else {
const wrapper = {};
wrapper.__proto__ = logger;
container.loggers.set(cat, wrapper);
}
}
// set the colors to use on the console
winston.addColors(colors);
};
/**
* Adds a new transport. This must be called prior to `container.init` and
* is a noop if the current process is not the primary process.
*
* @param {string} name - The name of the transport to add; if a name has
* already been taken, an error will be thrown.
* @param {object} transport - The transport to add.
*/
container.addTransport = function(name, transport) {
if(!cluster.isMaster) {
return;
}
if(name in container.transports) {
throw new Error(
'Cannot add logger transport; the transport name "' + name +
'" is already used.');
}
if(!('name' in transport)) {
transport.name = name;
}
container.transports[name] = transport;
};
async function _generateWorkerId() {
const buffer = await randomBytes(8);
return buffer.toString('hex');
}