zeddemore-logger 1.2.4 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,814 @@
1
+ // Copyright (c) 2025 by Beardon Services, Inc.
2
+
3
+ const _ = require('lodash');
4
+ const chalk = require('chalk');
5
+ const morgan = require('morgan');
6
+ const sizeof = require('object-sizeof');
7
+ const winston = require('winston');
8
+
9
+ const { areHeadersSent, isResponseSuccessful } = require('./response');
10
+ const { chalkHttpStatuses, chalkHttpVerbs, chalkLogLevel, chalkPackage, chalkTarget } = require('./color');
11
+ const { convertBytes } = require('./math');
12
+ const { convertMilliseconds } = require('./date');
13
+ const defaults = require('./defaults');
14
+ const enums = require('./enums');
15
+ const { extractToken, getRequestResponseTime, isRequestOfMethod, parseRequest } = require('./request');
16
+ const { generateUuid } = require('./uuid');
17
+ const { isMorganFormat, logLevelToWinstonLogLevelName, monkeyPatchConsole } = require('./logging');
18
+ require('./typedef');
19
+ const ZeddemoreBase = require('./ZeddemoreBase');
20
+ const ZeddemoreLoggerController = require('./ZeddemoreLoggerController');
21
+ const ZeddemoreProgressMultibar = require('./ZeddemoreProgressMultibar');
22
+
23
+ const { createLogger: createWinstonLogger, format, Logform: Format, transports } = winston;
24
+ const { colorize, combine, label, metadata, printf, timestamp } = format;
25
+ const { morganFormats: mf, morganFormatTokens: mft, httpMethods: http, httpRequestHeaders: rqh, httpResponseHeaders: rsh,
26
+ winstonLogLevelNames: wlln, winstonLogLevels: wll } = enums;
27
+
28
+ class ZeddemoreLogger extends ZeddemoreBase {
29
+
30
+ #apiCallSequelizeLogging = false;
31
+ /** @type {SequelizeModel} */
32
+ #apiCallSequelizeModel = null;
33
+ #apiRouteCategories = [ ];
34
+ #apiRoutes = [ ];
35
+ #apiVersion = null;
36
+ #decodeUrls = true;
37
+ #httpRequestLogging = true;
38
+ /** @type {ZeddemoreLoggerController} */
39
+ #logger = null;
40
+ #morganFormat = mf.ZEDDEMORE;
41
+ #routeSequelizeLogging = false;
42
+ /** @type {SequelizeModel} */
43
+ #routeSequelizeModel = null;
44
+ #suppressNonMutatingRequestApiCallLogging = false;
45
+ #suppressSuccessfulRequestApiCallLogging = false;
46
+
47
+ constructor(options) {
48
+ options = options || { };
49
+ super(options);
50
+ this.apiCallSequelizeLogging = options.apiCallSequelizeLogging;
51
+ if (_.isBoolean(options.disableApiCallLogging)) this.apiCallSequelizeLogging = !options.disableApiCallLogging; // backwards compatibility
52
+ this.apiCallSequelizeModel = options.apiCallSequelizeModel;
53
+ if (this.#isSequelizeModel(options.apiCallModel)) this.apiCallSequelizeModel = options.apiCallModel; // backwards compatibility
54
+ this.apiRouteCategories = options.apiRouteCategories;
55
+ this.apiRoutes = options.apiRoutes;
56
+ this.apiVersion = options.apiVersion;
57
+ this.decodeUrls = options.decodeUrls;
58
+ this.httpRequestLogging = options.httpRequestLogging;
59
+ if (_.isBoolean(options.disableMorganLogging )) this.httpRequestLogging = !options.disableMorganLogging; // backwards compatibility
60
+ this.morganFormat = options.morganFormat;
61
+ this.routeSequelizeLogging = options.routeSequelizeLogging;
62
+ if (_.isBoolean(options.disableRouteLogging)) this.routeSequelizeLogging = !options.disableRouteLogging; // backwards compatibility
63
+ this.routeSequelizeModel = options.routeSequelizeModel;
64
+ if (this.#isSequelizeModel(options.routeModel)) this.routeSequelizeModel = options.routeModel; // backwards compatibility
65
+ this.suppressNonMutatingRequestApiCallLogging = options.suppressNonMutatingRequestApiCallLogging;
66
+ this.suppressSuccessfulRequestApiCallLogging = options.suppressSuccessfulRequestApiCallLogging;
67
+ }
68
+
69
+ get apiCallSequelizeLogging() {
70
+ return this.#apiCallSequelizeLogging;
71
+ }
72
+
73
+ set apiCallSequelizeLogging(enabled) {
74
+ if (_.isBoolean(enabled)) this.#apiCallSequelizeLogging = enabled;
75
+ }
76
+
77
+ get apiCallSequelizeModel() {
78
+ return this.#apiCallSequelizeModel;
79
+ }
80
+
81
+ set apiCallSequelizeModel(model) {
82
+ if (this.#isSequelizeModel(model)) this.#apiCallSequelizeModel = model;
83
+ }
84
+
85
+ get apiRouteCategories() {
86
+ return this.#apiRouteCategories;
87
+ }
88
+
89
+ set apiRouteCategories(categories) {
90
+ if (_.isArray(categories)) this.#apiRouteCategories = categories;
91
+ }
92
+
93
+ get apiRoutes() {
94
+ return this.#apiRoutes;
95
+ }
96
+
97
+ set apiRoutes(routes) {
98
+ if (_.isArray(routes)) this.#apiRoutes = routes;
99
+ }
100
+
101
+ get apiVersion() {
102
+ return this.#apiVersion;
103
+ }
104
+
105
+ set apiVersion(version) {
106
+ if (_.isString(version) && !!version.length) this.#apiVersion = version;
107
+ }
108
+
109
+ get canSequelizeLogApiCall() {
110
+ return this.apiCallSequelizeLogging && !!this.apiCallSequelizeModel;
111
+ }
112
+
113
+ get canSequelizeLogRoute() {
114
+ return this.routeSequelizeLogging && !!this.routeSequelizeModel;
115
+ }
116
+
117
+ get decodeUrls() {
118
+ return this.#decodeUrls;
119
+ }
120
+
121
+ set decodeUrls(doDecode) {
122
+ if (_.isBoolean(doDecode)) this.#decodeUrls = doDecode;
123
+ }
124
+
125
+ get httpRequestLogging() {
126
+ return this.#httpRequestLogging;
127
+ }
128
+
129
+ set httpRequestLogging(enabled) {
130
+ if (_.isBoolean(enabled)) this.#httpRequestLogging = enabled;
131
+ }
132
+
133
+ get logger() {
134
+ if (!(this.#logger instanceof ZeddemoreLoggerController)) this.#logger = this.createLogger();
135
+ return this.#logger;
136
+ }
137
+
138
+ get morganFormat() {
139
+ return this.#morganFormat;
140
+ }
141
+
142
+ set morganFormat(format) {
143
+ if (isMorganFormat(format)) this.#morganFormat = format;
144
+ }
145
+
146
+ get routeSequelizeLogging() {
147
+ return this.#routeSequelizeLogging;
148
+ }
149
+
150
+ set routeSequelizeLogging(enabled) {
151
+ if (_.isBoolean(enabled)) this.#routeSequelizeLogging = enabled;
152
+ }
153
+
154
+ get routeSequelizeModel() {
155
+ return this.#routeSequelizeModel;
156
+ }
157
+
158
+ set routeSequelizeModel(model) {
159
+ if (this.#isSequelizeModel(model)) this.#routeSequelizeModel = model;
160
+ }
161
+
162
+ get suppressNonMutatingRequestApiCallLogging() {
163
+ return this.#suppressNonMutatingRequestApiCallLogging;
164
+ }
165
+
166
+ set suppressNonMutatingRequestApiCallLogging(suppress) {
167
+ if (_.isBoolean(suppress)) this.#suppressNonMutatingRequestApiCallLogging = suppress;
168
+ }
169
+
170
+ get suppressSuccessfulRequestApiCallLogging() {
171
+ return this.#suppressSuccessfulRequestApiCallLogging;
172
+ }
173
+
174
+ set suppressSuccessfulRequestApiCallLogging(suppress) {
175
+ if (_.isBoolean(suppress)) this.#suppressSuccessfulRequestApiCallLogging = suppress;
176
+ }
177
+
178
+ /**
179
+ * Build log message
180
+ * @param {String|[String]|Object} [message]
181
+ * @param {String} [level]
182
+ * @param {String} [error]
183
+ * @param {Metadata} [metadata]
184
+ * @param {Boolean} [colorize]
185
+ * @returns {string}
186
+ */
187
+ #buildLogMessage = (message, level, error, metadata, colorize = this.colorize) => {
188
+ metadata = this._structureMetadata(metadata);
189
+ const isError = !!level && (level.indexOf(wlln.ERROR) > -1);
190
+ const leaveUncompressed = isError || !!metadata.raw;
191
+ const parts = [ this._buildLogMessagePrefix(level, metadata, colorize) ];
192
+ let msg = Array.isArray(message) ? message.join('\n') : message;
193
+ msg = _.isObject(message) ? JSON.stringify(msg) : msg;
194
+ parts.push(leaveUncompressed ? msg : msg.replace(/(\n?\s+)/g, ' '));
195
+ if (!!error) parts.push(error);
196
+ const logMessage = parts.join(' ').trim();
197
+ return (isError && colorize) ? chalk.red(logMessage) : logMessage;
198
+ }
199
+
200
+ /**
201
+ * Build a Morgan format
202
+ * @param {[String]} morganTokens
203
+ * @returns {string}
204
+ */
205
+ #buildMorganFormat = (morganTokens = [ ]) => {
206
+ function addArgument(token, argument) {
207
+ if (hasArgument(token)) return token;
208
+ const openBracketIndex = token.indexOf('[');
209
+ const _token = (openBracketIndex > -1) ? token.substring(0, openBracketIndex) : token;
210
+ return `${ _token }[${ argument }]`;
211
+ }
212
+ function hasArgument(token) {
213
+ const openBracketIndex = token.indexOf('[');
214
+ const closeBracketIndex = token.indexOf(']');
215
+ return ((openBracketIndex > -1) && (closeBracketIndex > -1) && (closeBracketIndex > (openBracketIndex + 1)));
216
+ }
217
+ if (!morganTokens || !_.isArray(morganTokens) || !morganTokens.length) return mf.DEV;
218
+ const _morganTokens = _.map(morganTokens, (token) => {
219
+ if (!_.isString(token) || !token.length) return null;
220
+ let _token = token.trim();
221
+ if (_token[ 0 ] !== ':') _token = `:${ _token }`;
222
+ switch (_token) {
223
+ case mft.CONTENT_LENGTH_FORMAT: return hasArgument(_token) ? _token : addArgument(_token, this.contentLengthDigits);
224
+ case mft.RESPONSE_TIME:
225
+ case mft.RESPONSE_TIME_FORMAT: return hasArgument(_token) ? _token : addArgument(_token, this.responseTimeDigits);
226
+ default: return _token;
227
+ }
228
+ });
229
+ return (_.compact(_morganTokens)).join(' ');
230
+ }
231
+
232
+ /**
233
+ * Build a ZeddemoreProgressBars info
234
+ * @param {Object} options
235
+ * @returns {Object}
236
+ */
237
+ #buildProgressInfo = (options) => {
238
+ options = options || { };
239
+ const label = options.label || this.defaultLabel;
240
+ return {
241
+ ...options,
242
+ colorize: this.colorize,
243
+ label,
244
+ timestampFormat: this.luxonTimestampFormat,
245
+ };
246
+ }
247
+
248
+ /**
249
+ * Build a Winston format
250
+ * @param {Object} options
251
+ * @param {[Format]} [additionalFormats]
252
+ * @returns {Format}
253
+ */
254
+ #buildWinstonFormat = (options, additionalFormats = [ ]) => {
255
+ options = options || { };
256
+ const formats = [
257
+ format((info) => {
258
+ info.id = info.id || options.id || null;
259
+ info.ip = info.ip || options.ip || null;
260
+ info.user = info.user || options.user || null;
261
+ return info;
262
+ })(),
263
+ colorize(),
264
+ label({ label: options.label || this.defaultLabel }),
265
+ timestamp({ format: this.winstonTimestampFormat }),
266
+ metadata(),
267
+ printf(this.#buildWinstonTemplate),
268
+ ];
269
+ return combine(...additionalFormats.concat(formats));
270
+ }
271
+
272
+ /**
273
+ * Build Winston log template
274
+ * @param {Object} info
275
+ * @param {String} [info.err]
276
+ * @param {String} [info.level]
277
+ * @param {String|[String]|Object} [info.message]
278
+ * @param {Object} [info.metadata]
279
+ * @param {Boolean} [colorize]
280
+ * @returns {string}
281
+ */
282
+ #buildWinstonTemplate = (info, colorize = this.colorize) => {
283
+ info = info || { };
284
+ return this.#buildLogMessage(info.message, info.level, info.err, info.metadata, colorize);
285
+ }
286
+
287
+ /**
288
+ * Build a Winston Console transport
289
+ * @param {number|String} [logLevel]
290
+ * @returns {winston.ConsoleTransportInstance}
291
+ */
292
+ #buildWinstonTransport = (logLevel = this.logLevel) => {
293
+ logLevel = logLevel || this.logLevel;
294
+ const level = logLevelToWinstonLogLevelName(logLevel);
295
+ return new transports.Console({ level });
296
+ }
297
+
298
+ /**
299
+ * Build Winston transports
300
+ * @param {number|String} [logLevel]
301
+ * @param {[Object]} [additionalTransports]
302
+ * @returns {Object[]}
303
+ */
304
+ #buildWinstonTransports = (logLevel = this.logLevel, additionalTransports = [ ]) => {
305
+ return additionalTransports.concat([ this.#buildWinstonTransport(logLevel) ]);
306
+ }
307
+
308
+ #colorizeResponse(message) {
309
+ if (!_.isString(message) || !message.length) return message;
310
+ const splitMessage = message.split(' ');
311
+ if (splitMessage.length < 2) return message;
312
+ const verb = message.split(' ')[ 0 ];
313
+ return chalkHttpVerbs(verb, message);
314
+ }
315
+
316
+ /**
317
+ * Create a ZeddemoreLoggerController
318
+ * @param {Object} [options]
319
+ * @param {[Format]} [options.formats]
320
+ * @param {String} [options.level]
321
+ * @param {[Object]} [options.transports]
322
+ * @returns {ZeddemoreLoggerController}
323
+ */
324
+ createLogger = (options) => {
325
+ options = options || { };
326
+ const winstonLogger = this.#createWinstonLogger(options);
327
+ const multiBar = this.#createZeddemoreProgressMultibar(options);
328
+ this.#logger = new ZeddemoreLoggerController({ logger: winstonLogger, progress: multiBar });
329
+ return this.#logger;
330
+ }
331
+
332
+ /**
333
+ * Create a Winston logger with request information
334
+ * @param {String} [requestId]
335
+ * @param {String} [ipAddress]
336
+ * @returns {ZeddemoreLoggerController}
337
+ */
338
+ createRequestLogger = (requestId = null, ipAddress = null) => {
339
+ requestId = requestId || generateUuid();
340
+ /** @type {Object} */
341
+ const loggerOptions = { id: requestId };
342
+ if (ipAddress) loggerOptions.ip = ipAddress;
343
+ return this.createLogger(loggerOptions);
344
+ }
345
+
346
+ /**
347
+ * Create a Winston logger with user information
348
+ * @param {String} [requestId]
349
+ * @param {String} [username]
350
+ * @param {number|String} [logLevel]
351
+ * @param {String} [ipAddress]
352
+ * @returns {ZeddemoreLoggerController}
353
+ */
354
+ createUserLogger = (requestId = null, username = null, logLevel = this.defaultUserLogLevel, ipAddress = null) => {
355
+ requestId = requestId || generateUuid();
356
+ logLevel = logLevel || this.defaultUserLogLevel;
357
+ const loggerOptions = { id: requestId, level: logLevel, user: username };
358
+ if (ipAddress) loggerOptions.ip = ipAddress;
359
+ return this.createLogger(loggerOptions);
360
+ }
361
+
362
+ /**
363
+ * Create a Winston Logger
364
+ * @param {Object} [options]
365
+ * @param {[Format]} [options.formats]
366
+ * @param {String} [options.level]
367
+ * @param {[Object]} [options.transports]
368
+ * @returns {winston.Logger}
369
+ */
370
+ #createWinstonLogger = (options) => {
371
+ options = options || { };
372
+ return createWinstonLogger({
373
+ format: this.#buildWinstonFormat(options, options.formats),
374
+ transports: this.#buildWinstonTransports(options.level, options.transports),
375
+ });
376
+ }
377
+
378
+ /**
379
+ * Create a progress MultiBar
380
+ * @param {Object} [options]
381
+ * @returns {ZeddemoreProgressMultibar}
382
+ */
383
+ #createZeddemoreProgressMultibar = (options) => {
384
+ return new ZeddemoreProgressMultibar(this.#buildProgressInfo(options));
385
+ }
386
+
387
+ /**
388
+ * Enable console.dir extension
389
+ * @deprecated
390
+ */
391
+ enableConsoleDirp(...args) {
392
+ return this.enableConsoleDirExtension(...args);
393
+ }
394
+
395
+ /**
396
+ * Enable console.dir extension
397
+ * @param {[string]} [methodNames]
398
+ * @param {Boolean} [colors]
399
+ * @param {number|null} [depth]
400
+ * @param {Boolean} [showHidden]
401
+ * @returns {void}
402
+ */
403
+ enableConsoleDirExtension(methodNames = [ defaults.consoleDirExtensionMethodName ], colors = defaults.consoleDirExtensionColors, depth = defaults.consoleDirExtensionDepth, showHidden = defaults.consoleDirExtensionShowHidden) {
404
+ monkeyPatchConsole(methodNames, colors, depth, showHidden);
405
+ }
406
+
407
+ /**
408
+ * Get a Zeddemore Logger wrapper instance
409
+ * @param {Object} options
410
+ * @param {[Format]} [options.formats]
411
+ * @param {String} [options.level]
412
+ * @param {ZeddemoreLoggerController} [options.logger]
413
+ * @param {[Object]} [options.transports]
414
+ * @param {Object} [options.user]
415
+ * @returns {ZeddemoreLoggerController}
416
+ */
417
+ static getLogger(options) {
418
+ options = options || { };
419
+ /** @type {app} app */
420
+ const logger = options.logger || (options.user ? options.user.logger : null) || ((app && app.locals) ? app.locals.logger : null);
421
+ if (!!logger) return logger;
422
+ const zeddemoreLogger = new ZeddemoreLogger(options);
423
+ return zeddemoreLogger.createLogger(options);
424
+ }
425
+
426
+ /**
427
+ * Get a Winston Logger instance
428
+ * @param {Object} options
429
+ * @param {ZeddemoreProgressBars} [options.progress]
430
+ * @param {Object} [options.user]
431
+ * @returns {ZeddemoreProgressBars}
432
+ */
433
+ static getProgress(options) {
434
+ options = options || { };
435
+ /** @type {app} */
436
+ const progress = options.progress || (options.user ? options.user.progress : null) || ((app && app.locals) ? app.locals.progress : null);
437
+ if (!!progress) return progress;
438
+ const zeddemoreLogger = new ZeddemoreLogger();
439
+ return zeddemoreLogger.#createZeddemoreProgressMultibar(options);
440
+ }
441
+
442
+ /**
443
+ * Initialize Morgan logging
444
+ * @param {Boolean} [colorize]
445
+ */
446
+ #initializeMorgan(colorize = this.colorize) {
447
+ function buildContentLengthFormatToken(res, decimals = defaults.contentLengthDigits) {
448
+ const contentLength = convertBytes(res.get('content-length'), { decimals });
449
+ return contentLength ? ` - ${ contentLength }` : '';
450
+ }
451
+ /**
452
+ * @param {Object} req
453
+ * @param {String} [req.originalUrl]
454
+ * @param {String} [req.url]
455
+ * @returns {string}
456
+ */
457
+ function buildDecodedUrl(req) {
458
+ const url = req.originalUrl || req.url;
459
+ return decodeURI(url);
460
+ }
461
+ function buildResponseTimeFormatToken(req, res, digits = defaults.responseTimeDigits) {
462
+ const responseTime = getRequestResponseTime(req, res);
463
+ return responseTime ? convertMilliseconds(responseTime, { digits }) : '-';
464
+ }
465
+ function buildStatusColoredToken(res) {
466
+ const statusCode = areHeadersSent(res) ? res.statusCode : null;
467
+ if (!statusCode) return '-';
468
+ return chalkHttpStatuses(statusCode);
469
+ }
470
+ morgan.token(mft.CONTENT_LENGTH_FORMAT, (req, res, decimals) => { return buildContentLengthFormatToken(res, decimals); });
471
+ morgan.token(mft.DECODED_URL, (req, res) => { return buildDecodedUrl(req); });
472
+ morgan.token(mft.RESPONSE_TIME_FORMAT, (req, res, digits) => { return buildResponseTimeFormatToken(req, res, digits); });
473
+ morgan.token(mft.STATUS_COLORED, (req, res) => { return buildStatusColoredToken(res); });
474
+ const morganTokens = colorize ? defaults.morganFormatTokensColorized : defaults.morganFormatTokens;
475
+ morgan.format(mf.ZEDDEMORE, this.#buildMorganFormat(morganTokens));
476
+ }
477
+
478
+ #isSequelizeModel(model) {
479
+ return _.isObject(model) && Object.hasOwn(model, 'create') && Object.hasOwn(model, 'findOne');
480
+ }
481
+
482
+ /**
483
+ * Log a message for the specified log level
484
+ * @param {number|String} logLevel
485
+ * @returns {Function}
486
+ */
487
+ log = (logLevel) => {
488
+ logLevel = logLevel || this.logLevel;
489
+ const logLevelName = logLevelToWinstonLogLevelName(logLevel);
490
+ return this.logger[ logLevelName ];
491
+ }
492
+
493
+ /**
494
+ * Log server information
495
+ * @param {Object} options
496
+ * @param {Boolean} [options.showLogLevel=true]
497
+ * @param {String} [options.url]
498
+ * @param {number|String} [logLevel=wll.INFO]
499
+ * @returns {void}
500
+ */
501
+ #logServerInfo = (options, logLevel = wll.INFO) => {
502
+ options = options || { };
503
+ const showLogLevel = _.isBoolean(options.showLogLevel) ? options.showLogLevel : true;
504
+ const logLevelLabel = chalkLogLevel(this.logLevel, this.logLevelName.toUpperCase());
505
+ const msgParts = [ `Server is ${ chalk.bold(chalk.yellowBright(defaults.liveMessage)) }` ];
506
+ if (showLogLevel) msgParts.push(`with log level ${ logLevelLabel }${ this.forceLogLevel ? ` (${ chalk.italic('forced') })` : '' }`);
507
+ if (options.url) msgParts.push(`at ${ chalk.underline(options.url) }`);
508
+ this.log(logLevel)(msgParts.join(' '));
509
+ }
510
+
511
+ /**
512
+ * Log server listening information
513
+ * @param {Object} options
514
+ * @param {String} [options.environment]
515
+ * @param {String} [options.instance]
516
+ * @param {String} [options.name]
517
+ * @param {number} [options.port]
518
+ * @param {number|String} [logLevel=wll.INFO]
519
+ * @returns {void}
520
+ */
521
+ #logServerListening = (options, logLevel = wll.INFO) => {
522
+ const serverEnvironment = options.environment || process.env.NODE_ENV || 'development';
523
+ const serverInstance = options.instance || (process.env.NODE_APP_INSTANCE && (process.env.NODE_APP_INSTANCE.length > 1)) ? process.env.NODE_APP_INSTANCE : null; // handles goofy PM2 NODE_APP_INSTANCE of '0'
524
+ const serverName = options.name || this.defaultLabel;
525
+ const serverPort = options.port || process.env.PORT || 3000;
526
+ const msgParts = [ ];
527
+ const serverParts = [ serverName ];
528
+ if (serverEnvironment) serverParts.push(serverEnvironment);
529
+ if (serverInstance) serverParts.push(serverInstance);
530
+ msgParts.push(`[${ serverParts.join(':') }]`);
531
+ msgParts.push(`Node.js Express server listening on port ${ chalk.bold(serverPort) }`);
532
+ this.log(logLevel)(msgParts.join(' '));
533
+ }
534
+
535
+ /**
536
+ * Log server packages information
537
+ * @param {[Object]} packages
538
+ * @param {number|String} [logLevel=wll.INFO]
539
+ * @returns {void}
540
+ */
541
+ #logServerPackagesInfo = (packages, logLevel = wll.INFO) => {
542
+ function buildVersionLabel(_package) {
543
+ if (!_package || !_package.name || !_package.version) return null;
544
+ const packageName = _package.style ? chalkTarget(_package.name, _package.style) : chalkPackage(_package.name);
545
+ return `${ packageName }@${ _package.version }`;
546
+ }
547
+ const versionParts = _.compact(_.sortBy(packages, (_package) => _package.name.toUpperCase()).map(buildVersionLabel));
548
+ this.log(logLevel)(`Packages: ${ versionParts.join(' | ') }`);
549
+ }
550
+
551
+ /**
552
+ * Log server routes added
553
+ * @param {Object} options
554
+ * @param {[String]} [options.categories]
555
+ * @param {[String]} [options.routes]
556
+ * @param {Boolean} [options.showCategories=true]
557
+ * @param {number|String} [logLevel=wll.INFO]
558
+ * @returns {void}
559
+ */
560
+ #logServerRoutesAdded = (options, logLevel = wll.INFO) => {
561
+ options = options || { };
562
+ const showCategories = _.isBoolean(options.showCategories) ? options.showCategories : true;
563
+ const routes = options.routes || this.apiRoutes || [ ];
564
+ const routesLabel = (routes.length === 1) ? 'route' : 'routes';
565
+ const addedLabel = chalk.green('added');
566
+ const msgParts = [ `${ routes.length } ${ routesLabel } ${ addedLabel }` ];
567
+ if (showCategories) {
568
+ const categories = options.categories || this.apiRouteCategories || [];
569
+ msgParts.push(`in: ${ categories.sort().join('/') }`);
570
+ }
571
+ this.log(logLevel)(msgParts.join(', '));
572
+ }
573
+
574
+ /**
575
+ * Log server startup information
576
+ * @param {Object} options
577
+ * @param {[String]} [options.categories]
578
+ * @param {String} [options.environment]
579
+ * @param {String} [options.instance]
580
+ * @param {String} [options.name]
581
+ * @param {number} [options.port]
582
+ * @param {[Object]} [options.packages]
583
+ * @param {[String]} [options.routes]
584
+ * @param {Boolean} [options.showCategories=true]
585
+ * @param {Boolean} [options.showInfo=true]
586
+ * @param {Boolean} [options.showListening=true]
587
+ * @param {Boolean} [options.showLogLevel=true]
588
+ * @param {Boolean} [options.showPackages=true]
589
+ * @param {Boolean} [options.showRoutes=true]
590
+ * @param {number|String} [logLevel=wll.INFO]
591
+ * @returns {void}
592
+ */
593
+ logServerStartup = (options, logLevel = wll.INFO) => {
594
+ options = options || { };
595
+ const showListening = _.isBoolean(options.showListening) ? options.showListening : true;
596
+ const showPackages = _.isBoolean(options.showPackages) ? options.showPackages : true;
597
+ const showRoutes = _.isBoolean(options.showRoutes) ? options.showRoutes : true;
598
+ const showInfo = _.isBoolean(options.showInfo) ? options.showInfo : true;
599
+ if (showListening) this.#logServerListening(options, logLevel);
600
+ if (showPackages && options.packages) this.#logServerPackagesInfo(options.packages, logLevel);
601
+ if (showRoutes) this.#logServerRoutesAdded(options, logLevel);
602
+ if (showInfo) this.#logServerInfo(options, logLevel);
603
+ }
604
+
605
+ #matchPathToRoute = (path) => {
606
+ if (!path) return null;
607
+ let matchedRoute = null;
608
+ for (const route of this.apiRoutes) {
609
+ if (path.match(route.regexp)) {
610
+ matchedRoute = route.path;
611
+ break;
612
+ }
613
+ }
614
+ return matchedRoute;
615
+ }
616
+
617
+ /**
618
+ * Create Morgan middleware for logging HTTP requests
619
+ * @param {Object} options
620
+ * @param {Boolean} [options.colorize]
621
+ * @param {Boolean} [options.disableMorganLogging]
622
+ * @param {String} [options.morganFormat]
623
+ * @returns {function(*, *, *): void}
624
+ */
625
+ morganMiddleware = (options) => {
626
+ options = options || { };
627
+ const colorize = _.isBoolean(options.colorize) ? options.colorize : this.colorize;
628
+ const disableMorganLogging = _.isBoolean(options.disableMorganLogging) ? options.disableMorganLogging : !this.httpRequestLogging;
629
+ const morganFormat = options.morganFormat || this.#buildMorganFormat(defaults.morganFormatTokens);
630
+ return (req, res, next) => {
631
+ this.#initializeMorgan(colorize);
632
+ return morgan(morganFormat, {
633
+ skip: () => disableMorganLogging,
634
+ stream: {
635
+ write: (message) => {
636
+ if (this.canSequelizeLogApiCall) this.#writeApiCallToDatabase(req, res, options);
637
+ const logOptions = { };
638
+ const user = this.userGetFn(req, res);
639
+ if (user) logOptions.user = user;
640
+ const response = colorize ? this.#colorizeResponse(message.trim()) : message.trim();
641
+ ZeddemoreLogger.getLogger(logOptions).http(response);
642
+ },
643
+ },
644
+ })(req, res, next);
645
+ }
646
+ }
647
+
648
+ requestLoggerMiddleware = (req, res, next) => {
649
+ const requestValues = parseRequest(req);
650
+ const requestId = generateUuid();
651
+ req[ this.requestIdAttribute ] = requestId;
652
+ res.header(rqh.REQUEST_ID, requestId);
653
+ const ipAddress = requestValues.ipAddress;
654
+ res.locals = res.locals || { };
655
+ res.locals.user = res.locals.user || { };
656
+ res.locals.user.logger = this.createRequestLogger(requestId, ipAddress);
657
+ return next();
658
+ }
659
+
660
+ routeLoggingMiddleware = (req, res, next) => {
661
+ if (!this.canSequelizeLogRoute) return next();
662
+ try {
663
+ const routeValues = {
664
+ route: req.path,
665
+ method: req.method,
666
+ lastCalled: new Date().toISOString(),
667
+ };
668
+ this.#writeRouteToDatabase(routeValues);
669
+ } catch (e) {
670
+ } finally {
671
+ next();
672
+ }
673
+ }
674
+
675
+ userLoggerMiddleware = (req, res, next) => {
676
+ const requestId = this._getRequestId(req);
677
+ /** @type {user} */
678
+ const user = this.userGetFn(req, res) || { };
679
+ if (!requestId) return next();
680
+ const username = user.username || user.login || null;
681
+ let ipAddress = user.ipAddress || null;
682
+ if (!ipAddress) {
683
+ const requestValues = parseRequest(req);
684
+ ipAddress = requestValues.ipAddress;
685
+ }
686
+ let userLogLevel = !_.isNil(user.logLevel) ? user.logLevel : this.defaultUserLogLevel;
687
+ if (userLogLevel < 0) userLogLevel = this.logLevel;
688
+ const level = this.forceLogLevel ? this.logLevel : userLogLevel;
689
+ res.locals = res.locals || { };
690
+ res.locals.user = res.locals.user || user;
691
+ res.locals.user.logger = this.createUserLogger(requestId, username, level, ipAddress);
692
+ return next();
693
+ }
694
+
695
+ #writeApiCallToDatabase = async (req, res, options) => {
696
+ options = options || { };
697
+ if (!this.canSequelizeLogApiCall || !req) return;
698
+ const user = this.userGetFn(req, res) || { };
699
+ const statusCode = options.statusCode || (res ? res.statusCode : null);
700
+ if (isResponseSuccessful(statusCode) && this.suppressSuccessfulRequestApiCallLogging) return;
701
+ if (!isRequestOfMethod(req, [ http.DELETE, http.PUT ]) && this.suppressNonMutatingRequestApiCallLogging) return;
702
+ const matchedRoute = this.#matchPathToRoute(req.path);
703
+ let metadataDefaults = Object.assign({ }, user);
704
+ delete(metadataDefaults.id);
705
+ delete(metadataDefaults.logger);
706
+ const _metadata = _.defaults({ }, (options.metadata || { }), metadataDefaults);
707
+ if (matchedRoute) {
708
+ try {
709
+ const appCallValues = {
710
+ apiVersion: this.apiVersion,
711
+ client: user ? user.clientId : null,
712
+ clientVersion: req.header(rqh.APP_VERSION),
713
+ httpStatusCode: statusCode,
714
+ ipAddress: req.header(rqh.FORWARDED_FOR) || req.socket.remoteAddress,
715
+ metadata: _metadata,
716
+ method: req.method,
717
+ route: matchedRoute,
718
+ token: extractToken(req),
719
+ userAgent: req.header(rqh.USER_AGENT),
720
+ userId: user ? user.id : null,
721
+ };
722
+ await this.apiCallSequelizeModel.create(appCallValues);
723
+ } catch (e) {
724
+ console.error(e);
725
+ ZeddemoreLogger.getLogger({ user }).error('Api call logging failed, moving on');
726
+ }
727
+ }
728
+ }
729
+
730
+ #writeRouteToDatabase = async (routeValues) => {
731
+ if (!this.canSequelizeLogRoute || !routeValues) return;
732
+ const matchedRoute = this.#matchPathToRoute(routeValues.route);
733
+ if (matchedRoute) {
734
+ try {
735
+ const route = await this.routeSequelizeModel.findOne({ where: { route: matchedRoute }, logging: false });
736
+ if (route) {
737
+ route.count++;
738
+ route.lastCalledAt = new Date();
739
+ route.save();
740
+ } else {
741
+ this.routeSequelizeModel.create({
742
+ count: 1,
743
+ lastCalledAt: new Date(),
744
+ method: routeValues.method,
745
+ route: matchedRoute,
746
+ });
747
+ }
748
+ } catch (e) {
749
+ console.error(e);
750
+ console.error('Route logging failed, moving on');
751
+ }
752
+ }
753
+ }
754
+
755
+ static writeAxiosErrorLog(axiosError, logOptions, options) {
756
+ logOptions = logOptions || { };
757
+ options = options || { };
758
+ if (!axiosError || !axiosError.response) return;
759
+ const _response = axiosError.response;
760
+ if (!_response.request) return;
761
+ const _request = _response.request;
762
+ const logger = options.logger || ZeddemoreLogger.getLogger(options);
763
+ if (!logger) return;
764
+ const _method = _request.method || null;
765
+ const method = _method ? chalkHttpVerbs(_method) : null;
766
+ const _config = _response.config || null;
767
+ const _headers = _response.headers || null;
768
+ const _data = _response.data || null;
769
+ const path = _config ? _config.url : '';
770
+ const prefix = logOptions.prefix ? `[${ logOptions.prefix }]` : null;
771
+ const status = chalkHttpStatuses(_response.status);
772
+ const _duration = axiosError.duration || _response.duration;
773
+ const duration = _duration ? convertMilliseconds(_duration, logger.responseTimeDigits) : '-';
774
+ let _contentLength = _headers ? _headers[ rsh.CONTENT_LENGTH ] : null;
775
+ if (!_contentLength) _contentLength = _data ? sizeof(_data) : null;
776
+ const contentLength = _contentLength ? `- ${ convertBytes(_contentLength, logger.contentLengthDigits) }` : null;
777
+ const data = _data ? JSON.stringify(_data) : null;
778
+ const parts = [ prefix, method, path, status, duration, contentLength, data ];
779
+ const message = _.compact(parts).join(' ');
780
+ logger.error(message);
781
+ }
782
+
783
+ static writeAxiosResponseLog(axiosResponse, logOptions, options) {
784
+ logOptions = logOptions || { };
785
+ options = options || { };
786
+ if (!axiosResponse || !axiosResponse.request) return;
787
+ const _request = axiosResponse.request;
788
+ const axiosConfig = logOptions.axios || { };
789
+ const configData = (logOptions.data && _.isObject(axiosConfig.data)) ? JSON.stringify(axiosConfig.data) : null;
790
+ const logLevel = logLevelToWinstonLogLevelName(logOptions.logLevel);
791
+ const logger = options.logger || ZeddemoreLogger.getLogger(options);
792
+ if (!logger) return;
793
+ const _method = _request.method || null;
794
+ const method = _method ? chalkHttpVerbs(_method) : null;
795
+ const _headers = axiosResponse.headers || null;
796
+ const _data = axiosResponse.data || null;
797
+ const path = _request.path || '';
798
+ const prefix = logOptions.prefix ? `[${ logOptions.prefix }]` : null;
799
+ const responseData = (logOptions.response && _.isObject(_data)) ? JSON.stringify(_data) : null;
800
+ const status = chalkHttpStatuses(axiosResponse.status);
801
+ const _duration = axiosResponse.duration;
802
+ const duration = _duration ? convertMilliseconds(_duration, logger.responseTimeDigits) : '-';
803
+ let _contentLength = _headers ? _headers[ rsh.CONTENT_LENGTH ] : null;
804
+ if (!_contentLength) _contentLength = _data ? sizeof(_data) : null;
805
+ const contentLengthDigits = logOptions.contentLengthDigits || logger.contentLengthDigits || defaults.contentLengthDigits;
806
+ const contentLength = _contentLength ? `- ${ convertBytes(_contentLength, contentLengthDigits) }` : null;
807
+ const parts = [ prefix, method, path, configData, status, responseData, duration, contentLength ];
808
+ const message = _.compact(parts).join(' ');
809
+ logger[ logLevel ](message);
810
+ }
811
+
812
+ }
813
+
814
+ module.exports = ZeddemoreLogger;