1 |
|
2 |
|
3 |
|
4 | 'use strict';
|
5 |
|
6 | const brUtil = require('./util');
|
7 | const cc = brUtil.config.main.computer();
|
8 | const cluster = require('cluster');
|
9 | const config = require('./config');
|
10 | const cycle = require('cycle');
|
11 | const fs = require('fs').promises;
|
12 | const path = require('path');
|
13 | const uidNumber = require('uid-number');
|
14 | const util = require('util');
|
15 | const winston = require('winston');
|
16 | const WinstonMail = require('winston-mail').Mail;
|
17 |
|
18 |
|
19 |
|
20 | cc({
|
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 |
|
27 | const levels = {
|
28 | silly: 0,
|
29 | verbose: 1,
|
30 | debug: 2,
|
31 | info: 3,
|
32 | warning: 4,
|
33 | error: 5,
|
34 | critical: 6
|
35 | };
|
36 | const 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 |
|
47 | const container = new winston.Container();
|
48 |
|
49 |
|
50 |
|
51 | container._get = container.get;
|
52 | container._add = container.add;
|
53 | container.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 |
|
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 |
|
66 | if(typeof childMeta === 'string') {
|
67 | childMeta = {module: childMeta};
|
68 | }
|
69 |
|
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 | };
|
79 | module.exports = container;
|
80 |
|
81 | if(cluster.isMaster) {
|
82 |
|
83 | container.transports = {
|
84 | console: true,
|
85 | app: true,
|
86 | access: true,
|
87 | error: true,
|
88 | email: true
|
89 | };
|
90 | }
|
91 |
|
92 | async 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 |
|
98 | uid = process.getuid();
|
99 | } else {
|
100 | try {
|
101 | let gid;
|
102 |
|
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 |
|
127 | class 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 |
|
136 | if(this.onlyModules && this.modulePrefix &&
|
137 | 'module' in meta && !this.onlyModules.includes(meta.module)) {
|
138 | return;
|
139 | }
|
140 |
|
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 |
|
147 | msg = '[' + meta.module + '] ' + msg;
|
148 |
|
149 | meta = Object.assign({}, meta);
|
150 |
|
151 | delete meta.module;
|
152 | }
|
153 | super.log(level, msg, meta, callback);
|
154 | }
|
155 | }
|
156 |
|
157 |
|
158 | class 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 |
|
166 | msg = '[' + meta.module + '] ' + msg;
|
167 |
|
168 | meta = Object.assign({}, meta);
|
169 |
|
170 | delete meta.module;
|
171 | }
|
172 | super.log(level, msg, meta, callback);
|
173 | }
|
174 | }
|
175 |
|
176 |
|
177 |
|
178 |
|
179 | container.init = async () => {
|
180 | if(cluster.isMaster) {
|
181 |
|
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 |
|
190 | transports.app.name = 'app';
|
191 | transports.access.name = 'access';
|
192 | transports.error.name = 'error';
|
193 |
|
194 |
|
195 |
|
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 |
|
203 | for(const fileLogger of fileLoggers) {
|
204 | const dirname = path.dirname(fileLogger.filename);
|
205 |
|
206 | await fs.mkdir(dirname, {recursive: true});
|
207 | if('bedrock' in fileLogger && fileLogger.bedrock.enableChownDir) {
|
208 | await chown(dirname);
|
209 | }
|
210 |
|
211 | const f = await fs.open(fileLogger.filename, 'a');
|
212 | await f.close();
|
213 |
|
214 | await chown(fileLogger.filename);
|
215 | }
|
216 |
|
217 |
|
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 |
|
236 | winston.addColors(colors);
|
237 |
|
238 | |
239 |
|
240 |
|
241 |
|
242 |
|
243 |
|
244 | container.attach = function(worker) {
|
245 |
|
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 |
|
257 |
|
258 |
|
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 |
|
271 |
|
272 |
|
273 |
|
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 |
|
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 |
|
307 | if(json !== '{}') {
|
308 | meta.details = json;
|
309 | }
|
310 |
|
311 |
|
312 | if(preformatted) {
|
313 | meta.preformatted = preformatted;
|
314 | }
|
315 |
|
316 |
|
317 | if(module) {
|
318 | meta.module = module;
|
319 | }
|
320 |
|
321 |
|
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 |
|
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 |
|
350 | winston.addColors(colors);
|
351 | };
|
352 |
|
353 |
|
354 |
|
355 |
|
356 |
|
357 |
|
358 |
|
359 |
|
360 |
|
361 | container.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 | };
|