zeddemore-logger 2.0.2 → 2.1.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.
- package/lib/ZeddemoreBase.js +18 -0
- package/lib/ZeddemoreLogger.js +262 -25
- package/lib/enums.js +7 -1
- package/lib/http.js +26 -0
- package/lib/logging.js +8 -2
- package/lib/response.js +19 -1
- package/lib/transports/WinstonEmailTransport.js +127 -0
- package/package.json +6 -3
package/lib/ZeddemoreBase.js
CHANGED
|
@@ -172,6 +172,24 @@ class ZeddemoreBase {
|
|
|
172
172
|
if (_.isFunction(fn)) this.#userGetFn = fn;
|
|
173
173
|
}
|
|
174
174
|
|
|
175
|
+
get values() {
|
|
176
|
+
return {
|
|
177
|
+
contentLengthDigits: this.contentLengthDigits,
|
|
178
|
+
defaultLabel: this.defaultLabel,
|
|
179
|
+
displayCaller: this.displayCaller,
|
|
180
|
+
displayIpAddress: this.displayIpAddress,
|
|
181
|
+
colorize: this.colorize,
|
|
182
|
+
customCallerSettings: this.customCallerSettings,
|
|
183
|
+
defaultUserLogLevel: this.defaultUserLogLevel,
|
|
184
|
+
forceLogLevel: this.forceLogLevel,
|
|
185
|
+
logLevel: this.logLevel,
|
|
186
|
+
requestIdAttribute: this.requestIdAttribute,
|
|
187
|
+
responseTimeDigits: this.responseTimeDigits,
|
|
188
|
+
timestampFormat: this.timestampFormat,
|
|
189
|
+
userGetFn: this.userGetFn,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
175
193
|
get winstonTimestampFormat() {
|
|
176
194
|
return this.timestampFormat;
|
|
177
195
|
}
|
package/lib/ZeddemoreLogger.js
CHANGED
|
@@ -1,29 +1,32 @@
|
|
|
1
|
-
// Copyright (c) 2025 by Beardon Services, Inc.
|
|
1
|
+
// Copyright (c) 2025-2026 by Beardon Services, Inc.
|
|
2
2
|
|
|
3
3
|
const _ = require('lodash');
|
|
4
4
|
const chalk = require('chalk');
|
|
5
|
+
const { hostname } = require('node:os');
|
|
5
6
|
const morgan = require('morgan');
|
|
6
7
|
const sizeof = require('object-sizeof');
|
|
7
8
|
const winston = require('winston');
|
|
8
9
|
|
|
9
|
-
const { areHeadersSent, isResponseSuccessful } = require('./response');
|
|
10
|
+
const { areHeadersSent, isResponseFailure, isResponseSuccessful } = require('./response');
|
|
10
11
|
const { chalkHttpStatuses, chalkHttpVerbs, chalkLogLevel, chalkPackage, chalkTarget } = require('./color');
|
|
11
12
|
const { convertBytes } = require('./math');
|
|
12
13
|
const { convertMilliseconds } = require('./date');
|
|
13
14
|
const defaults = require('./defaults');
|
|
14
15
|
const enums = require('./enums');
|
|
16
|
+
const { extractHttpStatusCode, extractHttpVerb } = require('./http');
|
|
15
17
|
const { extractToken, getRequestResponseTime, isRequestOfMethod, parseRequest } = require('./request');
|
|
16
18
|
const { generateUuid } = require('./uuid');
|
|
17
|
-
const { isMorganFormat, logLevelToWinstonLogLevelName, monkeyPatchConsole } = require('./logging');
|
|
19
|
+
const { isMorganFormat, isValidLogLevelId, isWinstonTransportId, logLevelToWinstonLogLevelName, monkeyPatchConsole } = require('./logging');
|
|
18
20
|
require('./typedef');
|
|
21
|
+
const WinstonEmailTransport = require('./transports/WinstonEmailTransport');
|
|
19
22
|
const ZeddemoreBase = require('./ZeddemoreBase');
|
|
20
23
|
const ZeddemoreLoggerController = require('./ZeddemoreLoggerController');
|
|
21
24
|
const ZeddemoreProgressMultibar = require('./ZeddemoreProgressMultibar');
|
|
22
25
|
|
|
23
26
|
const { createLogger: createWinstonLogger, format, Logform: Format, transports } = winston;
|
|
24
27
|
const { colorize, combine, label, metadata, printf, timestamp } = format;
|
|
25
|
-
const {
|
|
26
|
-
winstonLogLevelNames: wlln, winstonLogLevels: wll } = enums;
|
|
28
|
+
const { httpMethods: http, httpRequestHeaders: rqh, httpResponseHeaders: rsh, morganFormats: mf, morganFormatTokens: mft,
|
|
29
|
+
winstonLogLevelNames: wlln, winstonLogLevels: wll, winstonTransportIds: wti } = enums;
|
|
27
30
|
|
|
28
31
|
class ZeddemoreLogger extends ZeddemoreBase {
|
|
29
32
|
|
|
@@ -34,6 +37,18 @@ class ZeddemoreLogger extends ZeddemoreBase {
|
|
|
34
37
|
#apiRoutes = [ ];
|
|
35
38
|
#apiVersion = null;
|
|
36
39
|
#decodeUrls = true;
|
|
40
|
+
#doConsoleLog = true;
|
|
41
|
+
#doEmailLog = false;
|
|
42
|
+
#emailLogDebug = false;
|
|
43
|
+
#emailLogFrom = null;
|
|
44
|
+
#emailLogHost = null;
|
|
45
|
+
#emailLogLevel = wll.ERROR;
|
|
46
|
+
#emailLogPassword = null;
|
|
47
|
+
#emailLogPort = null;
|
|
48
|
+
#emailLogSubject = 'Zeddemore Log ({{ level }}) - {{ message }}';
|
|
49
|
+
#emailLogTo = null;
|
|
50
|
+
#emailLogUseHtml = false;
|
|
51
|
+
#emailLogUsername = null;
|
|
37
52
|
#httpRequestLogging = true;
|
|
38
53
|
/** @type {ZeddemoreLoggerController} */
|
|
39
54
|
#logger = null;
|
|
@@ -41,6 +56,7 @@ class ZeddemoreLogger extends ZeddemoreBase {
|
|
|
41
56
|
#routeSequelizeLogging = false;
|
|
42
57
|
/** @type {SequelizeModel} */
|
|
43
58
|
#routeSequelizeModel = null;
|
|
59
|
+
#serverUrl = null;
|
|
44
60
|
#suppressNonMutatingRequestApiCallLogging = false;
|
|
45
61
|
#suppressSuccessfulRequestApiCallLogging = false;
|
|
46
62
|
|
|
@@ -55,6 +71,18 @@ class ZeddemoreLogger extends ZeddemoreBase {
|
|
|
55
71
|
this.apiRoutes = options.apiRoutes;
|
|
56
72
|
this.apiVersion = options.apiVersion;
|
|
57
73
|
this.decodeUrls = options.decodeUrls;
|
|
74
|
+
this.doConsoleLog = options.doConsoleLog;
|
|
75
|
+
this.doEmailLog = options.doEmailLog;
|
|
76
|
+
this.emailLogDebug = options.emailLogDebug;
|
|
77
|
+
this.emailLogFrom = options.emailLogFrom;
|
|
78
|
+
this.emailLogHost = options.emailLogHost;
|
|
79
|
+
this.emailLogLevel = options.emailLogLevel;
|
|
80
|
+
this.emailLogPassword = options.emailLogPassword;
|
|
81
|
+
this.emailLogPort = options.emailLogPort;
|
|
82
|
+
this.emailLogSubject = options.emailLogSubject;
|
|
83
|
+
this.emailLogTo = options.emailLogTo;
|
|
84
|
+
this.emailLogUseHtml = options.emailLogUseHtml;
|
|
85
|
+
this.emailLogUsername = options.emailLogUsername;
|
|
58
86
|
this.httpRequestLogging = options.httpRequestLogging;
|
|
59
87
|
if (_.isBoolean(options.disableMorganLogging )) this.httpRequestLogging = !options.disableMorganLogging; // backwards compatibility
|
|
60
88
|
this.morganFormat = options.morganFormat;
|
|
@@ -62,6 +90,7 @@ class ZeddemoreLogger extends ZeddemoreBase {
|
|
|
62
90
|
if (_.isBoolean(options.disableRouteLogging)) this.routeSequelizeLogging = !options.disableRouteLogging; // backwards compatibility
|
|
63
91
|
this.routeSequelizeModel = options.routeSequelizeModel;
|
|
64
92
|
if (this.#isSequelizeModel(options.routeModel)) this.routeSequelizeModel = options.routeModel; // backwards compatibility
|
|
93
|
+
this.serverUrl = options.serverUrl;
|
|
65
94
|
this.suppressNonMutatingRequestApiCallLogging = options.suppressNonMutatingRequestApiCallLogging;
|
|
66
95
|
this.suppressSuccessfulRequestApiCallLogging = options.suppressSuccessfulRequestApiCallLogging;
|
|
67
96
|
}
|
|
@@ -106,6 +135,10 @@ class ZeddemoreLogger extends ZeddemoreBase {
|
|
|
106
135
|
if (_.isString(version) && !!version.length) this.#apiVersion = version;
|
|
107
136
|
}
|
|
108
137
|
|
|
138
|
+
get canEmailLog() {
|
|
139
|
+
return this.doEmailLog && !!this.emailLogFrom && !!this.emailLogHost && !!this.emailLogPassword && !!this.emailLogPort && !!this.emailLogUsername && !!this.emailLogTo;
|
|
140
|
+
}
|
|
141
|
+
|
|
109
142
|
get canSequelizeLogApiCall() {
|
|
110
143
|
return this.apiCallSequelizeLogging && !!this.apiCallSequelizeModel;
|
|
111
144
|
}
|
|
@@ -122,6 +155,102 @@ class ZeddemoreLogger extends ZeddemoreBase {
|
|
|
122
155
|
if (_.isBoolean(doDecode)) this.#decodeUrls = doDecode;
|
|
123
156
|
}
|
|
124
157
|
|
|
158
|
+
get doConsoleLog() {
|
|
159
|
+
return this.#doConsoleLog;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
set doConsoleLog(doLog) {
|
|
163
|
+
if (_.isBoolean(doLog)) this.#doConsoleLog = doLog;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
get doEmailLog() {
|
|
167
|
+
return this.#doEmailLog;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
set doEmailLog(doLog) {
|
|
171
|
+
if (_.isBoolean(doLog)) this.#doEmailLog = doLog;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
get emailLogDebug() {
|
|
175
|
+
return this.#emailLogDebug;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
set emailLogDebug(debug) {
|
|
179
|
+
if (_.isBoolean(debug)) this.#emailLogDebug = debug;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
get emailLogFrom() {
|
|
183
|
+
return this.#emailLogFrom || this.#buildEmailLogFrom();
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
set emailLogFrom(from) {
|
|
187
|
+
if (_.isString(from) && !!from.length) this.#emailLogFrom = from;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
get emailLogHost() {
|
|
191
|
+
return this.#emailLogHost;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
set emailLogHost(host) {
|
|
195
|
+
if (_.isString(host) && !!host.length) this.#emailLogHost = host;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
get emailLogLevel() {
|
|
199
|
+
return this.#emailLogLevel;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
set emailLogLevel(level) {
|
|
203
|
+
if (isValidLogLevelId(level)) this.#emailLogLevel = level;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
get emailLogPassword() {
|
|
207
|
+
return this.#emailLogPassword;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
set emailLogPassword(password) {
|
|
211
|
+
if (_.isString(password) && !!password.length) this.#emailLogPassword = password;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
get emailLogPort() {
|
|
215
|
+
return this.#emailLogPort;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
set emailLogPort(port) {
|
|
219
|
+
if (_.isInteger(port)) this.#emailLogPort = port;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
get emailLogSubject() {
|
|
223
|
+
return this.#emailLogSubject;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
set emailLogSubject(subject) {
|
|
227
|
+
if (_.isString(subject) && !!subject.length) this.#emailLogSubject = subject;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
get emailLogTo() {
|
|
231
|
+
return this.#emailLogTo;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
set emailLogTo(to) {
|
|
235
|
+
if (_.isString(to) && !!to.length) this.#emailLogTo = to;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
get emailLogUseHtml() {
|
|
239
|
+
return this.#emailLogUseHtml;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
set emailLogUseHtml(useHtml) {
|
|
243
|
+
if (_.isBoolean(useHtml)) this.#emailLogUseHtml = useHtml;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
get emailLogUsername() {
|
|
247
|
+
return this.#emailLogUsername;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
set emailLogUsername(username) {
|
|
251
|
+
if (_.isString(username) && !!username.length) this.#emailLogUsername = username;
|
|
252
|
+
}
|
|
253
|
+
|
|
125
254
|
get httpRequestLogging() {
|
|
126
255
|
return this.#httpRequestLogging;
|
|
127
256
|
}
|
|
@@ -159,6 +288,30 @@ class ZeddemoreLogger extends ZeddemoreBase {
|
|
|
159
288
|
if (this.#isSequelizeModel(model)) this.#routeSequelizeModel = model;
|
|
160
289
|
}
|
|
161
290
|
|
|
291
|
+
get serverEnvironment() {
|
|
292
|
+
return process.env.NODE_ENV || 'development';
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
get serverInstance() {
|
|
296
|
+
return (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'
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
get serverName() {
|
|
300
|
+
return this.defaultLabel;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
get serverPort() {
|
|
304
|
+
return process.env.PORT || 3000;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
get serverUrl() {
|
|
308
|
+
return this.#serverUrl || hostname();
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
set serverUrl(url) {
|
|
312
|
+
if (_.isString(url) && !!url.length) this.#serverUrl = url;
|
|
313
|
+
}
|
|
314
|
+
|
|
162
315
|
get suppressNonMutatingRequestApiCallLogging() {
|
|
163
316
|
return this.#suppressNonMutatingRequestApiCallLogging;
|
|
164
317
|
}
|
|
@@ -175,6 +328,45 @@ class ZeddemoreLogger extends ZeddemoreBase {
|
|
|
175
328
|
if (_.isBoolean(suppress)) this.#suppressSuccessfulRequestApiCallLogging = suppress;
|
|
176
329
|
}
|
|
177
330
|
|
|
331
|
+
get values() {
|
|
332
|
+
return {
|
|
333
|
+
...super.values,
|
|
334
|
+
apiCallSequelizeLogging: this.apiCallSequelizeLogging,
|
|
335
|
+
apiCallSequelizeModel: this.apiCallSequelizeModel,
|
|
336
|
+
apiRouteCategories: this.apiRouteCategories,
|
|
337
|
+
apiRoutes: this.routeSequelizeLogging,
|
|
338
|
+
apiVersion: this.apiVersion,
|
|
339
|
+
decodeUrls: this.decodeUrls,
|
|
340
|
+
doConsoleLog: this.doConsoleLog,
|
|
341
|
+
doEmailLog: this.doEmailLog,
|
|
342
|
+
emailLogDebug: this.emailLogDebug,
|
|
343
|
+
emailLogFrom: this.emailLogFrom,
|
|
344
|
+
emailLogHost: this.emailLogHost,
|
|
345
|
+
emailLogLevel: this.emailLogLevel,
|
|
346
|
+
emailLogPort: this.emailLogPort,
|
|
347
|
+
emailLogSubject: this.emailLogSubject,
|
|
348
|
+
emailLogTo: this.emailLogTo,
|
|
349
|
+
emailLogUseHtml: this.emailLogUseHtml,
|
|
350
|
+
httpRequestLogging: this.httpRequestLogging,
|
|
351
|
+
logger: this.logger,
|
|
352
|
+
morganFormat: this.morganFormat,
|
|
353
|
+
routeSequelizeLogging: this.routeSequelizeLogging,
|
|
354
|
+
routeSequelizeModel: this.routeSequelizeModel,
|
|
355
|
+
serverUrl: this.serverUrl,
|
|
356
|
+
suppressNonMutatingRequestApiCallLogging: this.suppressNonMutatingRequestApiCallLogging,
|
|
357
|
+
suppressSuccessfulRequestApiCallLogging: this.suppressSuccessfulRequestApiCallLogging,
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
#buildEmailLogFrom() {
|
|
362
|
+
if (_.isString(this.#emailLogFrom) && !!this.#emailLogFrom) return this.#emailLogFrom;
|
|
363
|
+
const localParts = [ 'zeddemore' ]
|
|
364
|
+
if (!!this.serverName) localParts.push(this.serverName);
|
|
365
|
+
if (!!this.serverInstance) localParts.push(this.serverInstance);
|
|
366
|
+
if (!!this.serverEnvironment) localParts.push(this.serverEnvironment);
|
|
367
|
+
return `${ localParts.join('_') }@${ hostname() }`;
|
|
368
|
+
}
|
|
369
|
+
|
|
178
370
|
/**
|
|
179
371
|
* Build log message
|
|
180
372
|
* @param {String|[String]|Object} [message]
|
|
@@ -249,12 +441,14 @@ class ZeddemoreLogger extends ZeddemoreBase {
|
|
|
249
441
|
* Build a Winston format
|
|
250
442
|
* @param {Object} options
|
|
251
443
|
* @param {[Format]} [additionalFormats]
|
|
444
|
+
* @param {String} [transportId]
|
|
252
445
|
* @returns {Format}
|
|
253
446
|
*/
|
|
254
|
-
#buildWinstonFormat = (options, additionalFormats = [ ]) => {
|
|
447
|
+
#buildWinstonFormat = (options, additionalFormats = [ ], transportId = null) => {
|
|
255
448
|
options = options || { };
|
|
256
449
|
const formats = [
|
|
257
450
|
format((info) => {
|
|
451
|
+
if (isWinstonTransportId(transportId) && isWinstonTransportId(info.transport) && (info.transport !== transportId)) return false;
|
|
258
452
|
info.id = info.id || options.id || null;
|
|
259
453
|
info.ip = info.ip || options.ip || null;
|
|
260
454
|
info.user = info.user || options.user || null;
|
|
@@ -287,30 +481,66 @@ class ZeddemoreLogger extends ZeddemoreBase {
|
|
|
287
481
|
/**
|
|
288
482
|
* Build a Winston Console transport
|
|
289
483
|
* @param {number|String} [logLevel]
|
|
484
|
+
* @param {Object} [options]
|
|
485
|
+
* @param {[Format]} [options.formats]
|
|
290
486
|
* @returns {winston.ConsoleTransportInstance}
|
|
291
487
|
*/
|
|
292
|
-
#
|
|
488
|
+
#buildWinstonConsoleTransport = (logLevel = this.logLevel, options) => {
|
|
489
|
+
options = options || { };
|
|
293
490
|
logLevel = logLevel || this.logLevel;
|
|
294
491
|
const level = logLevelToWinstonLogLevelName(logLevel);
|
|
295
|
-
|
|
492
|
+
const format = this.#buildWinstonFormat(options, options.formats, wti.CONSOLE);
|
|
493
|
+
return new transports.Console({ format, id: wti.CONSOLE, level });
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* Build a Winston Email transport
|
|
498
|
+
* @param {number|String} [logLevel]
|
|
499
|
+
* @param {Object} [options]
|
|
500
|
+
* @param {[Format]} [options.formats]
|
|
501
|
+
* @returns {WinstonEmailTransport}
|
|
502
|
+
*/
|
|
503
|
+
#buildWinstonEmailTransport = (logLevel = this.emailLogLevel, options) => {
|
|
504
|
+
options = options || { };
|
|
505
|
+
logLevel = logLevel || this.emailLogLevel;
|
|
506
|
+
const level = logLevelToWinstonLogLevelName(logLevel);
|
|
507
|
+
const format = this.#buildWinstonFormat(options, options.formats, wti.EMAIL);
|
|
508
|
+
const messageOptions = {
|
|
509
|
+
from: this.emailLogFrom,
|
|
510
|
+
subject: this.emailLogSubject,
|
|
511
|
+
to: this.emailLogTo,
|
|
512
|
+
};
|
|
513
|
+
const transportOptions = {
|
|
514
|
+
auth: {
|
|
515
|
+
user: this.emailLogUsername,
|
|
516
|
+
pass: this.emailLogPassword,
|
|
517
|
+
},
|
|
518
|
+
debug: this.emailLogDebug,
|
|
519
|
+
host: this.emailLogHost,
|
|
520
|
+
port: this.emailLogPort,
|
|
521
|
+
};
|
|
522
|
+
const winstonOptions = { level };
|
|
523
|
+
const _options = { format, id: wti.EMAIL, messageOptions, transportOptions, ...winstonOptions };
|
|
524
|
+
return new WinstonEmailTransport(_options);
|
|
296
525
|
}
|
|
297
526
|
|
|
298
527
|
/**
|
|
299
528
|
* Build Winston transports
|
|
300
529
|
* @param {number|String} [logLevel]
|
|
530
|
+
* @param {number|String} [emailLogLevel]
|
|
301
531
|
* @param {[Object]} [additionalTransports]
|
|
302
532
|
* @returns {Object[]}
|
|
303
533
|
*/
|
|
304
|
-
#buildWinstonTransports = (logLevel = this.logLevel, additionalTransports = [ ]) => {
|
|
305
|
-
|
|
534
|
+
#buildWinstonTransports = (logLevel = this.logLevel, emailLogLevel = this.emailLogLevel, additionalTransports = [ ]) => {
|
|
535
|
+
additionalTransports = _.isArray(additionalTransports) ? additionalTransports : [ ];
|
|
536
|
+
if (this.doConsoleLog) additionalTransports = additionalTransports.concat([ this.#buildWinstonConsoleTransport(logLevel) ]);
|
|
537
|
+
if (this.doEmailLog && this.canEmailLog) additionalTransports = additionalTransports.concat([ this.#buildWinstonEmailTransport(emailLogLevel) ]);
|
|
538
|
+
return additionalTransports;
|
|
306
539
|
}
|
|
307
540
|
|
|
308
541
|
#colorizeResponse(message) {
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
if (splitMessage.length < 2) return message;
|
|
312
|
-
const verb = message.split(' ')[ 0 ];
|
|
313
|
-
return chalkHttpVerbs(verb, message);
|
|
542
|
+
const verb = extractHttpVerb(message);
|
|
543
|
+
return verb ? chalkHttpVerbs(verb, message) : message;
|
|
314
544
|
}
|
|
315
545
|
|
|
316
546
|
/**
|
|
@@ -363,16 +593,15 @@ class ZeddemoreLogger extends ZeddemoreBase {
|
|
|
363
593
|
* Create a Winston Logger
|
|
364
594
|
* @param {Object} [options]
|
|
365
595
|
* @param {[Format]} [options.formats]
|
|
366
|
-
* @param {String} [options.level]
|
|
596
|
+
* @param {number|String} [options.level]
|
|
597
|
+
* @param {number|String} [options.emailLevel]
|
|
367
598
|
* @param {[Object]} [options.transports]
|
|
368
599
|
* @returns {winston.Logger}
|
|
369
600
|
*/
|
|
370
601
|
#createWinstonLogger = (options) => {
|
|
371
602
|
options = options || { };
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
transports: this.#buildWinstonTransports(options.level, options.transports),
|
|
375
|
-
});
|
|
603
|
+
const transports = this.#buildWinstonTransports(options.level, options.emailLevel, options.transports);
|
|
604
|
+
return createWinstonLogger({ transports });
|
|
376
605
|
}
|
|
377
606
|
|
|
378
607
|
/**
|
|
@@ -500,11 +729,13 @@ class ZeddemoreLogger extends ZeddemoreBase {
|
|
|
500
729
|
*/
|
|
501
730
|
#logServerInfo = (options, logLevel = wll.INFO) => {
|
|
502
731
|
options = options || { };
|
|
732
|
+
const serverUrl = options.url || this.serverUrl;
|
|
503
733
|
const showLogLevel = _.isBoolean(options.showLogLevel) ? options.showLogLevel : true;
|
|
734
|
+
const showUrl = _.isBoolean(options.showUrl) ? options.showUrl : true;
|
|
504
735
|
const logLevelLabel = chalkLogLevel(this.logLevel, this.logLevelName.toUpperCase());
|
|
505
736
|
const msgParts = [ `Server is ${ chalk.bold(chalk.yellowBright(defaults.liveMessage)) }` ];
|
|
506
737
|
if (showLogLevel) msgParts.push(`with log level ${ logLevelLabel }${ this.forceLogLevel ? ` (${ chalk.italic('forced') })` : '' }`);
|
|
507
|
-
if (
|
|
738
|
+
if (showUrl) msgParts.push(`at ${ chalk.underline(serverUrl) }`);
|
|
508
739
|
this.log(logLevel)(msgParts.join(' '));
|
|
509
740
|
}
|
|
510
741
|
|
|
@@ -519,10 +750,10 @@ class ZeddemoreLogger extends ZeddemoreBase {
|
|
|
519
750
|
* @returns {void}
|
|
520
751
|
*/
|
|
521
752
|
#logServerListening = (options, logLevel = wll.INFO) => {
|
|
522
|
-
const serverEnvironment = options.environment ||
|
|
523
|
-
const serverInstance = options.instance ||
|
|
524
|
-
const serverName = options.name || this.
|
|
525
|
-
const serverPort = options.port ||
|
|
753
|
+
const serverEnvironment = options.environment || this.serverEnvironment;
|
|
754
|
+
const serverInstance = options.instance || this.serverInstance;
|
|
755
|
+
const serverName = options.name || this.serverName;
|
|
756
|
+
const serverPort = options.port || this.serverPort;
|
|
526
757
|
const msgParts = [ ];
|
|
527
758
|
const serverParts = [ serverName ];
|
|
528
759
|
if (serverEnvironment) serverParts.push(serverEnvironment);
|
|
@@ -639,6 +870,12 @@ class ZeddemoreLogger extends ZeddemoreBase {
|
|
|
639
870
|
if (user) logOptions.user = user;
|
|
640
871
|
const response = colorize ? this.#colorizeResponse(message.trim()) : message.trim();
|
|
641
872
|
ZeddemoreLogger.getLogger(logOptions).http(response);
|
|
873
|
+
if (this.doEmailLog) {
|
|
874
|
+
const statusCode = extractHttpStatusCode(message);
|
|
875
|
+
if (isResponseFailure(statusCode, [ 404 ])) {
|
|
876
|
+
ZeddemoreLogger.getLogger(logOptions).log({ level: wlln.ERROR, message: response, transport: wti.EMAIL });
|
|
877
|
+
}
|
|
878
|
+
}
|
|
642
879
|
},
|
|
643
880
|
},
|
|
644
881
|
})(req, res, next);
|
package/lib/enums.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Copyright (c) 2025 by Beardon Services, Inc.
|
|
1
|
+
// Copyright (c) 2025-2026 by Beardon Services, Inc.
|
|
2
2
|
|
|
3
3
|
function doesEnumInclude(set, value) {
|
|
4
4
|
for (const key in set) {
|
|
@@ -178,6 +178,11 @@ const winstonLogLevels = {
|
|
|
178
178
|
SILLY: 6,
|
|
179
179
|
};
|
|
180
180
|
|
|
181
|
+
const winstonTransportIds = {
|
|
182
|
+
CONSOLE: 'console',
|
|
183
|
+
EMAIL: 'email',
|
|
184
|
+
};
|
|
185
|
+
|
|
181
186
|
module.exports = {
|
|
182
187
|
doesEnumInclude,
|
|
183
188
|
ansiColors,
|
|
@@ -196,4 +201,5 @@ module.exports = {
|
|
|
196
201
|
progressBarAlignments,
|
|
197
202
|
winstonLogLevelNames,
|
|
198
203
|
winstonLogLevels,
|
|
204
|
+
winstonTransportIds,
|
|
199
205
|
};
|
package/lib/http.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// Copyright (c) 2026 by Beardon Services, Inc.
|
|
2
|
+
|
|
3
|
+
const _ = require('lodash');
|
|
4
|
+
|
|
5
|
+
function extractHttpStatusCode(message) {
|
|
6
|
+
if (!_.isString(message) || !message.length) return null;
|
|
7
|
+
// Regex breakdown:
|
|
8
|
+
// \b : Word boundary (ensures we don't grab "12345")
|
|
9
|
+
// [1-5] : Starts with 1 (Info), 2 (Success), 3 (Redirect), 4 (Client Error), or 5 (Server Error)
|
|
10
|
+
// [0-9]{2}: Followed by exactly two digits
|
|
11
|
+
// \b : Ending word boundary
|
|
12
|
+
const match = message.match(/\b[1-5][0-9]{2}\b/);
|
|
13
|
+
return match ? +match[ 0 ] : null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function extractHttpVerb(message) {
|
|
17
|
+
if (!_.isString(message) || !message.length) return null;
|
|
18
|
+
const splitMessage = message.split(' ');
|
|
19
|
+
if (splitMessage.length < 2) return null;
|
|
20
|
+
return message.split(' ')[ 0 ];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
module.exports = {
|
|
24
|
+
extractHttpStatusCode,
|
|
25
|
+
extractHttpVerb,
|
|
26
|
+
}
|
package/lib/logging.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Copyright (c) 2025 by Beardon Services, Inc.
|
|
1
|
+
// Copyright (c) 2025-2026 by Beardon Services, Inc.
|
|
2
2
|
|
|
3
3
|
const _ = require('lodash');
|
|
4
4
|
|
|
@@ -13,7 +13,7 @@ function isMorganFormat(format) {
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
function isValidLogLevel(logLevel) {
|
|
16
|
-
if (!logLevel) return false;
|
|
16
|
+
if (!_.isInteger(logLevel) && (!_.isString(logLevel) || !logLevel.length)) return false;
|
|
17
17
|
if (_.isInteger(logLevel)) return isValidLogLevelId(logLevel);
|
|
18
18
|
if (_.isString(logLevel)) return isValidLogLevelName(logLevel);
|
|
19
19
|
return false;
|
|
@@ -29,6 +29,10 @@ function isValidLogLevelName(logLevelName) {
|
|
|
29
29
|
return !!(_.find(logLevelMap, { winstonName: logLevelName }))
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
function isWinstonTransportId(winstonTransportId) {
|
|
33
|
+
return doesEnumInclude(enums.winstonTransportIds, winstonTransportId);
|
|
34
|
+
}
|
|
35
|
+
|
|
32
36
|
/**
|
|
33
37
|
* Convert a log level ID to a Winston log level name
|
|
34
38
|
* @param levelId {number}
|
|
@@ -70,6 +74,8 @@ function monkeyPatchConsole(methodNames = [ defaults.consoleDirExtensionMethodNa
|
|
|
70
74
|
module.exports = {
|
|
71
75
|
isMorganFormat,
|
|
72
76
|
isValidLogLevel,
|
|
77
|
+
isValidLogLevelId,
|
|
78
|
+
isWinstonTransportId,
|
|
73
79
|
logLevelIdToWinstonLogLevelName,
|
|
74
80
|
logLevelToWinstonLogLevelName,
|
|
75
81
|
monkeyPatchConsole,
|
package/lib/response.js
CHANGED
|
@@ -11,6 +11,19 @@ function areHeadersSent(res) {
|
|
|
11
11
|
return typeof (res.headersSent !== 'boolean') ? Boolean(res._header) : res.headersSent;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
function isResponseFailure(statusCode, disregardStatusCodes = [ ]) {
|
|
15
|
+
if (_.isArray(disregardStatusCodes) && disregardStatusCodes.includes(statusCode)) return false;
|
|
16
|
+
return isResponseStatusCodeClientError(statusCode) || isResponseStatusCodeServerError(statusCode);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function isResponseStatusCodeClientError(statusCode) {
|
|
20
|
+
return isResponseStatusCodeInGroup(statusCode, hscg.CLIENT_ERROR);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function isResponseStatusCodeInformational(statusCode) {
|
|
24
|
+
return isResponseStatusCodeInGroup(statusCode, hscg.INFORMATIONAL);
|
|
25
|
+
}
|
|
26
|
+
|
|
14
27
|
function isResponseStatusCodeInGroup(statusCode, group) {
|
|
15
28
|
if (!statusCode || !group) return false;
|
|
16
29
|
const httpStatusCodeGroupMapEntry = _.find(httpStatusCodeGroupMap, { group });
|
|
@@ -22,15 +35,20 @@ function isResponseStatusCodeRedirection(statusCode) {
|
|
|
22
35
|
return isResponseStatusCodeInGroup(statusCode, hscg.REDIRECTION);
|
|
23
36
|
}
|
|
24
37
|
|
|
38
|
+
function isResponseStatusCodeServerError(statusCode) {
|
|
39
|
+
return isResponseStatusCodeInGroup(statusCode, hscg.SERVER_ERROR);
|
|
40
|
+
}
|
|
41
|
+
|
|
25
42
|
function isResponseStatusCodeSuccessful(statusCode) {
|
|
26
43
|
return isResponseStatusCodeInGroup(statusCode, hscg.SUCCESSFUL);
|
|
27
44
|
}
|
|
28
45
|
|
|
29
46
|
function isResponseSuccessful(statusCode) {
|
|
30
|
-
return isResponseStatusCodeSuccessful(statusCode) || isResponseStatusCodeRedirection(statusCode);
|
|
47
|
+
return isResponseStatusCodeSuccessful(statusCode) || isResponseStatusCodeRedirection(statusCode) || isResponseStatusCodeInformational(statusCode);
|
|
31
48
|
}
|
|
32
49
|
|
|
33
50
|
module.exports = {
|
|
34
51
|
areHeadersSent,
|
|
52
|
+
isResponseFailure,
|
|
35
53
|
isResponseSuccessful,
|
|
36
54
|
};
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// Copyright (c) 2026 by Beardon Services, Inc.
|
|
2
|
+
|
|
3
|
+
const _ = require('lodash');
|
|
4
|
+
const ansiHTML = require('ansi-html');
|
|
5
|
+
const { createTransport } = require('nodemailer');
|
|
6
|
+
const { hostname } = require('node:os');
|
|
7
|
+
const { stripVTControlCharacters } = require('node:util');
|
|
8
|
+
const Transport = require('winston-transport');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @class
|
|
12
|
+
* @extends Transport
|
|
13
|
+
*/
|
|
14
|
+
class WinstonEmailTransport extends Transport {
|
|
15
|
+
|
|
16
|
+
_messageOptions = { };
|
|
17
|
+
#useHtml = false;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @callback FilterFunction
|
|
21
|
+
* @param {Object} info
|
|
22
|
+
* @param {string} info.level
|
|
23
|
+
* @param {string} info.message
|
|
24
|
+
* @returns {boolean}
|
|
25
|
+
*
|
|
26
|
+
* @param {Object} [options={}] Options for this instance
|
|
27
|
+
* @param {Object} [options.transportOptions]
|
|
28
|
+
* @param {Object} [options.messageOptions]
|
|
29
|
+
* @param {string} [options.messageOptions.to]
|
|
30
|
+
* @param {string} [options.messageOptions.from]
|
|
31
|
+
* @param {string} [options.messageOptions.subject]
|
|
32
|
+
* @param {FilterFunction} [options.messageOptions.filter]
|
|
33
|
+
* @throws {Error} if options.messageOptions.to is empty or not set
|
|
34
|
+
*/
|
|
35
|
+
constructor(options) {
|
|
36
|
+
options = options || { };
|
|
37
|
+
const { messageOptions = { }, transportOptions = { }, ...winstonOptions } = options;
|
|
38
|
+
super(winstonOptions);
|
|
39
|
+
if (!_.isString(messageOptions.to) || !messageOptions.to.length) {
|
|
40
|
+
throw new Error('WinstonEmailTransport requires \'to\' property');
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* @member {Object} _messageOptions
|
|
44
|
+
* @member {string} _messageOptions.to
|
|
45
|
+
* @member {string} _messageOptions.from
|
|
46
|
+
* @member {string} _messageOptions.subject
|
|
47
|
+
* @member {FilterFunction} _messageOptions.filter
|
|
48
|
+
* @private
|
|
49
|
+
*/
|
|
50
|
+
this._messageOptions = {
|
|
51
|
+
...messageOptions,
|
|
52
|
+
from: messageOptions.from ?? `winston@${ hostname() }`,
|
|
53
|
+
subject: messageOptions.subject ?? 'Winston Log',
|
|
54
|
+
filter: messageOptions.filter ?? (() => true),
|
|
55
|
+
};
|
|
56
|
+
this.name = 'WinstonEmailTransport';
|
|
57
|
+
/**
|
|
58
|
+
* @member {object}
|
|
59
|
+
* @private
|
|
60
|
+
*/
|
|
61
|
+
this._transportOptions = transportOptions;
|
|
62
|
+
this.useHtml = winstonOptions.useHtml;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
get useHtml() {
|
|
66
|
+
return this.#useHtml;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
set useHtml(useHtml) {
|
|
70
|
+
if (_.isBoolean(useHtml)) this.#useHtml = useHtml;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Core logging method exposed to Winston
|
|
75
|
+
*
|
|
76
|
+
* @callback NextFunction
|
|
77
|
+
* @returns {void}
|
|
78
|
+
*
|
|
79
|
+
* @param {Object} info
|
|
80
|
+
* @param {string} info.level
|
|
81
|
+
* @param {string} info.message
|
|
82
|
+
* @param {NextFunction} callback
|
|
83
|
+
*/
|
|
84
|
+
log(info, callback) {
|
|
85
|
+
try {
|
|
86
|
+
if (!this._messageOptions.filter(info)) {
|
|
87
|
+
setImmediate(callback);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const { level, message, ...meta } = info;
|
|
91
|
+
const _level = stripVTControlCharacters(level);
|
|
92
|
+
const _message = stripVTControlCharacters(message);
|
|
93
|
+
/** @type {Object.<string,*>} messageOptions */
|
|
94
|
+
const { filter, ...messageOptions } = { ...this._messageOptions }
|
|
95
|
+
const subject = messageOptions.subject.replace(/{{\s*level\s*}}/g, _level).replace(/{{\s*message\s*}}/g, `${ _message }`.split('\n')[ 0 ]);
|
|
96
|
+
const content = meta[ Symbol.for('message') ];
|
|
97
|
+
const html = ansiHTML(content);
|
|
98
|
+
const text = stripVTControlCharacters(content);
|
|
99
|
+
messageOptions.subject = subject;
|
|
100
|
+
if (this.useHtml) messageOptions.html = html;
|
|
101
|
+
messageOptions.text = text;
|
|
102
|
+
const transporter = createTransport(this._transportOptions);
|
|
103
|
+
transporter.sendMail(
|
|
104
|
+
messageOptions,
|
|
105
|
+
/**
|
|
106
|
+
* @fires WinstonEmailTransport#error
|
|
107
|
+
* @fires WinstonEmailTransport#logged
|
|
108
|
+
* @param {?Error} err
|
|
109
|
+
* @param {Object} info
|
|
110
|
+
*/
|
|
111
|
+
(err, info) => {
|
|
112
|
+
setImmediate(callback);
|
|
113
|
+
if (err) {
|
|
114
|
+
this.emit('error', err);
|
|
115
|
+
} else {
|
|
116
|
+
this.emit('logged', info);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
);
|
|
120
|
+
} catch (e) {
|
|
121
|
+
console.error(e);
|
|
122
|
+
console.error('Email logging failed, moving on');
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
module.exports = WinstonEmailTransport;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zeddemore-logger",
|
|
3
|
-
"version": "2.0
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "Node.js Express logger using Morgan and Winston for thread and caller info tracking",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -20,13 +20,16 @@
|
|
|
20
20
|
"homepage": "https://github.com/beardon/zeddemore-logger#readme",
|
|
21
21
|
"main": "lib/zeddemore-logger.js",
|
|
22
22
|
"dependencies": {
|
|
23
|
+
"ansi-html": "^0.0.9",
|
|
23
24
|
"chalk": "^4.1.2",
|
|
24
25
|
"cli-progress": "^3.12.0",
|
|
25
|
-
"lodash": "^4.
|
|
26
|
+
"lodash": "^4.18.1",
|
|
26
27
|
"luxon": "^3.7.1",
|
|
27
28
|
"morgan": "^1.10.1",
|
|
29
|
+
"nodemailer": "^8.0.4",
|
|
28
30
|
"object-sizeof": "^2.6.5",
|
|
29
31
|
"uuid": "^10.0.0",
|
|
30
|
-
"winston": "^3.17.0"
|
|
32
|
+
"winston": "^3.17.0",
|
|
33
|
+
"winston-transport": "^4.9.0"
|
|
31
34
|
}
|
|
32
35
|
}
|