UNPKG

11.4 kBJavaScriptView Raw
1/*!
2 * Copyright (c) 2012-2019 Digital Bazaar, Inc. All rights reserved.
3 */
4'use strict';
5
6const brUtil = require('./util');
7const cc = brUtil.config.main.computer();
8const cluster = require('cluster');
9const config = require('./config');
10const cycle = require('cycle');
11const fs = require('fs').promises;
12const path = require('path');
13const uidNumber = require('uid-number');
14const util = require('util');
15const winston = require('winston');
16const WinstonMail = require('winston-mail').Mail;
17
18// config filenames
19// configured here instead of config.js due to util dependency issues
20cc({
21 'loggers.app.filename': () => path.join(config.paths.log, 'app.log'),
22 'loggers.access.filename': () => path.join(config.paths.log, 'access.log'),
23 'loggers.error.filename': () => path.join(config.paths.log, 'error.log')
24});
25
26// create custom logging levels
27const levels = {
28 silly: 0,
29 verbose: 1,
30 debug: 2,
31 info: 3,
32 warning: 4,
33 error: 5,
34 critical: 6
35};
36const colors = {
37 silly: 'cyan',
38 verbose: 'blue',
39 debug: 'blue',
40 info: 'green',
41 warning: 'yellow',
42 error: 'red',
43 critical: 'red'
44};
45
46// create the container for the master and all of the workers
47const container = new winston.Container();
48// override get to use a wrapper so loggers that are retrieved via `.get()`
49// prior to logger initialization will receive the updated configuration
50// when logging after initialization
51container._get = container.get;
52container._add = container.add;
53container.get = container.add = function(id) {
54 const existing = container.loggers[id];
55 let logger = container._get.apply(container, arguments);
56 if(!existing) {
57 const wrapper = Object.create(logger);
58 wrapper.log = function(level, msg, meta) {
59 // merge per-logger and per-log meta and call parent logger
60 meta = Object.assign({}, this.meta, meta);
61 return Object.getPrototypeOf(wrapper).log.apply(
62 wrapper, [level, msg, meta]);
63 };
64 wrapper.child = function(childMeta) {
65 // simple string name is shortcut for {module: name}
66 if(typeof childMeta === 'string') {
67 childMeta = {module: childMeta};
68 }
69 // create child logger with merged meta data from parent
70 const child = Object.create(this);
71 child.meta = Object.assign({}, this.meta, childMeta);
72 child.setLevels(levels);
73 return child;
74 };
75 logger = container.loggers[id] = wrapper;
76 }
77 return logger;
78};
79module.exports = container;
80
81if(cluster.isMaster) {
82 // reserved transports
83 container.transports = {
84 console: true,
85 app: true,
86 access: true,
87 error: true,
88 email: true
89 };
90}
91
92async function chown(filename) {
93 if(config.core.running.userId) {
94 let uid = config.core.running.userId;
95 if(typeof uid !== 'number') {
96 if(process.platform === 'win32') {
97 // on Windows, just get the current UID
98 uid = process.getuid();
99 } else {
100 try {
101 let gid;
102 /* eslint-disable-next-line no-unused-vars */
103 [uid, gid] = await new Promise((resolve, reject) => {
104 uidNumber(uid, (err, uid, gid) => {
105 if(err) {
106 reject(err);
107 return;
108 }
109 resolve([uid, gid]);
110 });
111 });
112 } catch(e) {
113 throw new brUtil.BedrockError(
114 `Unable to convert user "${uid}" to a numeric user id. ` +
115 'Try using a uid number instead.',
116 'Error', {cause: e});
117 }
118 }
119 }
120 if(process.getgid) {
121 await fs.chown(filename, uid, process.getgid());
122 }
123 }
124}
125
126// class to handle pretty printing module name
127class ModuleConsoleTransport extends winston.transports.Console {
128 constructor(config) {
129 super(config);
130 this.modulePrefix = config.bedrock.modulePrefix;
131 this.onlyModules = config.bedrock.onlyModules;
132 this.excludeModules = config.bedrock.excludeModules;
133 }
134 log(level, msg, meta, callback) {
135 // only output messages from modules specified by --log-only
136 if(this.onlyModules && this.modulePrefix &&
137 'module' in meta && !this.onlyModules.includes(meta.module)) {
138 return;
139 }
140 // do not output messages from modules specified by --log-exclude
141 if(this.excludeModules && this.modulePrefix &&
142 'module' in meta && this.excludeModules.includes(meta.module)) {
143 return;
144 }
145 if(this.modulePrefix && 'module' in meta) {
146 // add pretty module prefix
147 msg = '[' + meta.module + '] ' + msg;
148 // copy to avoid changing shared original
149 meta = Object.assign({}, meta);
150 // remove module so not re-printed as details
151 delete meta.module;
152 }
153 super.log(level, msg, meta, callback);
154 }
155}
156
157// class to handle pretty printing module name
158class ModuleFileTransport extends winston.transports.File {
159 constructor(config) {
160 super(config);
161 this.modulePrefix = config.bedrock.modulePrefix;
162 }
163 log(level, msg, meta, callback) {
164 if(this.modulePrefix && 'module' in meta) {
165 // add pretty module prefix
166 msg = '[' + meta.module + '] ' + msg;
167 // copy to avoid changing shared original
168 meta = Object.assign({}, meta);
169 // remove module so not re-printed as details
170 delete meta.module;
171 }
172 super.log(level, msg, meta, callback);
173 }
174}
175
176/**
177 * Initializes the logging system.
178 */
179container.init = async () => {
180 if(cluster.isMaster) {
181 // create shared transports
182 const transports = container.transports;
183 transports.console = new ModuleConsoleTransport(config.loggers.console);
184 transports.app = new ModuleFileTransport(config.loggers.app);
185 transports.access = new ModuleFileTransport(config.loggers.access);
186 transports.error = new ModuleFileTransport(config.loggers.error);
187 transports.email = new WinstonMail(config.loggers.email);
188
189 // set unique names for file transports
190 transports.app.name = 'app';
191 transports.access.name = 'access';
192 transports.error.name = 'error';
193
194 // ensure all files are created and have ownership set to the
195 // application process user
196 const fileLoggers = Object.keys(config.loggers).filter(function(name) {
197 const logger = config.loggers[name];
198 return (brUtil.isObject(logger) && 'filename' in logger);
199 }).map(function(name) {
200 return config.loggers[name];
201 });
202 // TODO: run in parallel
203 for(const fileLogger of fileLoggers) {
204 const dirname = path.dirname(fileLogger.filename);
205 // make directory and chown if requested
206 await fs.mkdir(dirname, {recursive: true});
207 if('bedrock' in fileLogger && fileLogger.bedrock.enableChownDir) {
208 await chown(dirname);
209 }
210 // check file can be opened
211 const f = await fs.open(fileLogger.filename, 'a');
212 await f.close();
213 // always chown log file
214 await chown(fileLogger.filename);
215 }
216
217 // create master loggers
218 for(const cat in config.loggers.categories) {
219 const transportNames = config.loggers.categories[cat];
220 const options = {transports: []};
221 transportNames.forEach(function(name) {
222 options.transports.push(transports[name]);
223 });
224 const logger = new winston.Logger(options);
225 logger.setLevels(levels);
226 if(container.loggers[cat]) {
227 container.loggers[cat].__proto__ = logger;
228 } else {
229 const wrapper = {};
230 wrapper.__proto__ = logger;
231 container.loggers[cat] = wrapper;
232 }
233 }
234
235 // set the colors to use on the console
236 winston.addColors(colors);
237
238 /**
239 * Attaches a message handler to the given worker. This should be
240 * called by the master process to handle worker log messages.
241 *
242 * @param worker the worker to attach the message handler to.
243 */
244 container.attach = function(worker) {
245 // set up message handler for master process
246 worker.on('message', function(msg) {
247 if(typeof msg === 'object' && msg.type === 'bedrock.logger') {
248 container.get(msg.category).log(msg.level, msg.msg, msg.meta);
249 }
250 });
251 };
252
253 return;
254 }
255
256 // workers:
257
258 // define transport that transmits log message to master logger
259 const WorkerTransport = function(options) {
260 winston.Transport.call(this, options);
261 this.category = options.category;
262 };
263 util.inherits(WorkerTransport, winston.Transport);
264 WorkerTransport.prototype.name = 'worker';
265 WorkerTransport.prototype.log = function(level, msg, meta, callback) {
266 if(this.silent) {
267 return callback(null, true);
268 }
269
270 // FIXME: rework this error handling and callers
271 // Add support for BedrockError and its toObject() feature.
272
273 // pull out any meta values that are pre-formatted
274 meta = meta || {};
275 let preformatted = null;
276 const metaIsObject = brUtil.isObject(meta);
277 let module = null;
278 if(metaIsObject) {
279 if('preformatted' in meta) {
280 preformatted = meta.preformatted;
281 delete meta.preformatted;
282 }
283 if('module' in meta) {
284 module = meta.module;
285 delete meta.module;
286 }
287 }
288 // stringify and include the worker PID in the meta information
289 let json;
290 try {
291 json = JSON.stringify(meta, null, 2);
292 } catch(e) {
293 json = JSON.stringify(cycle.decycle(meta), null, 2);
294 }
295 let error;
296 if(meta instanceof Error) {
297 error = ('stack' in meta) ? meta.stack : meta;
298 meta = {error, workerPid: process.pid};
299 } else if(metaIsObject && 'error' in meta) {
300 error = ('stack' in meta.error) ? meta.error.stack : meta.error;
301 meta = {error, workerPid: process.pid};
302 } else {
303 meta = {workerPid: process.pid};
304 }
305
306 // only add details if they exist
307 if(json !== '{}') {
308 meta.details = json;
309 }
310
311 // only add pre-formatted entries if they exist
312 if(preformatted) {
313 meta.preformatted = preformatted;
314 }
315
316 // only add module if it was specified
317 if(module) {
318 meta.module = module;
319 }
320
321 // send logger message to master
322 process.send({
323 type: 'bedrock.logger',
324 level,
325 msg,
326 meta,
327 category: this.category
328 });
329 this.emit('logged');
330 callback(null, true);
331 };
332
333 // create worker loggers
334 const lowestLevel = Object.keys(levels)[0];
335 for(const cat in config.loggers.categories) {
336 const logger = new winston.Logger({
337 transports: [new WorkerTransport({level: lowestLevel, category: cat})]
338 });
339 logger.setLevels(levels);
340 if(container.loggers[cat]) {
341 container.loggers[cat].__proto__ = logger;
342 } else {
343 const wrapper = {};
344 wrapper.__proto__ = logger;
345 container.loggers[cat] = wrapper;
346 }
347 }
348
349 // set the colors to use on the console
350 winston.addColors(colors);
351};
352
353/**
354 * Adds a new transport. This must be called prior to `container.init` and
355 * is a noop if the current process is not the master process.
356 *
357 * @param name the name of the transport to add; if a name has already been
358 * taken, an error will be thrown.
359 * @param transport the transport to add.
360 */
361container.addTransport = function(name, transport) {
362 if(!cluster.isMaster) {
363 return;
364 }
365 if(name in container.transports) {
366 throw new Error(
367 'Cannot add logger transport; the transport name "' + name +
368 '" is already used.');
369 }
370 if(!('name' in transport)) {
371 transport.name = name;
372 }
373 container.transports[name] = transport;
374};