UNPKG

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