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.
@@ -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
  }
@@ -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 { morganFormats: mf, morganFormatTokens: mft, httpMethods: http, httpRequestHeaders: rqh, httpResponseHeaders: rsh,
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
- #buildWinstonTransport = (logLevel = this.logLevel) => {
488
+ #buildWinstonConsoleTransport = (logLevel = this.logLevel, options) => {
489
+ options = options || { };
293
490
  logLevel = logLevel || this.logLevel;
294
491
  const level = logLevelToWinstonLogLevelName(logLevel);
295
- return new transports.Console({ level });
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
- return additionalTransports.concat([ this.#buildWinstonTransport(logLevel) ]);
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
- 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);
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
- return createWinstonLogger({
373
- format: this.#buildWinstonFormat(options, options.formats),
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 (options.url) msgParts.push(`at ${ chalk.underline(options.url) }`);
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 || 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;
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.2",
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.17.23",
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
  }