zeddemore-logger 1.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.
package/README.md ADDED
@@ -0,0 +1,3 @@
1
+ # zeddemore-logger
2
+
3
+ Node.js logger using Morgan and Winston.
package/lib/color.js ADDED
@@ -0,0 +1,107 @@
1
+ // Copyright (c) 2025 by Beardon Services, Inc.
2
+
3
+ const _ = require('lodash');
4
+ const chalk = require('chalk');
5
+
6
+ const colorMappings = require('./mappings/color_mappings');
7
+
8
+ function chalkDatabaseOperation(operation, string = null) {
9
+ if (!string) string = operation;
10
+ return chalkViaColorMap(operation, string, colorMappings.databaseOperationColors, true);
11
+ }
12
+
13
+ function chalkHttpStatus(statusCode) {
14
+ const colorMap = _.find(colorMappings.httpStatusColors, (httpStatusColor) => httpStatusColor.range.includes(statusCode));
15
+ if (!colorMap) return statusCode;
16
+ let chalkedMatch = statusCode;
17
+ if (colorMap.ansi) chalkedMatch = `\x1B[${ colorMap.ansi }m${ statusCode }\x1B[39m`;
18
+ return chalkedMatch;
19
+ }
20
+
21
+ function chalkHttpVerb(verb, string = null) {
22
+ if (!string) string = verb;
23
+ return chalkViaColorMap(verb, string, colorMappings.httpVerbColors, false);
24
+ }
25
+
26
+ function chalkViaColorMap(match, string, colorMap, useGlobal = true) {
27
+ function fixPattern(pattern) {
28
+ return pattern.replace(/([.?*+^$[\]\\(){}|-])/g, '\\$1');
29
+ }
30
+ if (!match || !_.isString(match) || !string || !_.isString(string) || !_.isObject(colorMap)) return string;
31
+ const cleanMatch = match.replace(/[^A-Z]/g, '');
32
+ const mapping = _.find(colorMap, { match: cleanMatch });
33
+ if (!mapping) return string;
34
+ const flags = useGlobal ? 'g' : '';
35
+ const re = new RegExp(fixPattern(match), flags);
36
+ if (!string.match(re)) return string;
37
+ let chalkedMatch = cleanMatch;
38
+ if (mapping.fore) chalkedMatch = chalk.hex(mapping.fore)(chalkedMatch);
39
+ if (mapping.back) chalkedMatch = chalk.bgHex(mapping.back)(chalkedMatch);
40
+ return string.replace(re, chalkedMatch);
41
+ }
42
+
43
+ // adapted from https://stackoverflow.com/a/44134328
44
+ function hslToHex(hue, saturation, lightness) {
45
+ lightness /= 100;
46
+ const a = saturation * Math.min(lightness, 1 - lightness) / 100;
47
+ const convert = (n) => {
48
+ const k = (n + hue / 30) % 12;
49
+ const color = lightness - a * Math.max(Math.min(k - 3, 9 - k, 1), -1);
50
+ return Math.round(255 * color).toString(16).padStart(2, '0'); // convert to Hex and prefix "0" if needed
51
+ };
52
+ return `#${ convert(0) }${ convert(8) }${ convert(4) }`;
53
+ }
54
+
55
+ // adapted from https://gist.github.com/0x263b/2bdd90886c2036a1ad5bcf06d6e6fb37
56
+ function stringToIdempotentHexColor(str, useHsl = true) {
57
+ if (useHsl) {
58
+ const { h, s, l } = stringToIdempotentHslValues(str);
59
+ return hslToHex(h, s, l);
60
+ }
61
+ let hash = 0;
62
+ if (str.length === 0) return hash;
63
+ for (let i = 0; i < str.length; i++) {
64
+ hash = str.charCodeAt(i) + ((hash << 5) - hash);
65
+ hash = hash & hash;
66
+ }
67
+ let hexColor = '#';
68
+ for (let i = 0; i < 3; i++) {
69
+ let value = (hash >> (i * 8)) & 255;
70
+ hexColor += (`00${ value.toString(16) }`).substring(-2);
71
+ }
72
+ return hexColor;
73
+ }
74
+
75
+ // adapted from https://gist.github.com/0x263b/2bdd90886c2036a1ad5bcf06d6e6fb37
76
+ function stringToIdempotentHslValues(str, hslOptions) {
77
+ function range(hash, min, max) {
78
+ const diff = max - min;
79
+ const x = ((hash % diff) + diff) % diff;
80
+ return x + min;
81
+ }
82
+
83
+ hslOptions = hslOptions || { };
84
+ const hueRange = hslOptions.hue || [ 0, 360 ];
85
+ const saturationRange = hslOptions.saturation || [ 75, 100 ];
86
+ const lightnessRange = hslOptions.lightness || [ 40, 60 ];
87
+
88
+ let hash = 0;
89
+ if (!str || str.length === 0) return hash;
90
+ for (let i = 0; i < str.length; i++) {
91
+ hash = str.charCodeAt(i) + ((hash << 5) - hash);
92
+ hash = hash & hash;
93
+ }
94
+
95
+ const hue = range(hash, hueRange[ 0 ], hueRange[ 1 ]);
96
+ const saturation = range(hash, saturationRange[ 0 ], saturationRange[ 1 ]);
97
+ const lightness = range(hash, lightnessRange[ 0 ], lightnessRange[ 1 ]);
98
+
99
+ return { h: hue, s: saturation, l: lightness };
100
+ }
101
+
102
+ module.exports = {
103
+ chalkDatabaseOperation,
104
+ chalkHttpStatus,
105
+ chalkHttpVerb,
106
+ stringToIdempotentHexColor,
107
+ };
package/lib/date.js ADDED
@@ -0,0 +1,51 @@
1
+ // Copyright (c) 2025 by Beardon Services, Inc.
2
+
3
+ function convertMilliseconds(milliseconds, options) {
4
+ options = options || { };
5
+ const digits = options.digits || 3;
6
+ const spaced = options.hasOwnProperty('spaced') ? options.spaced : false;
7
+ let value = milliseconds || 0;
8
+ let unit = 'ms';
9
+ if (milliseconds) {
10
+ const seconds = milliseconds / 1000;
11
+ if (seconds >= 1) {
12
+ value = seconds;
13
+ unit = 's';
14
+ const minutes = seconds / 60;
15
+ if (minutes >= 1) {
16
+ value = minutes;
17
+ unit = 'm';
18
+ const hours = minutes / 60;
19
+ if (hours >= 1) {
20
+ value = hours;
21
+ unit = 'h';
22
+ const days = hours / 24;
23
+ if (days >= 1) {
24
+ value = days;
25
+ unit = 'd';
26
+ const weeks = days / 7;
27
+ if (weeks >= 1) {
28
+ value = weeks;
29
+ unit = 'w';
30
+ const months = days / 30;
31
+ if (months >= 1) {
32
+ value = months;
33
+ unit = 'M';
34
+ const years = days / 365;
35
+ if (years >= 1) {
36
+ value = years;
37
+ unit = 'y';
38
+ }
39
+ }
40
+ }
41
+ }
42
+ }
43
+ }
44
+ }
45
+ }
46
+ return `${ value.toFixed(digits) }${ spaced ? ' ' : '' }${ unit }`;
47
+ }
48
+
49
+ module.exports = {
50
+ convertMilliseconds,
51
+ };
package/lib/enums.js ADDED
@@ -0,0 +1,96 @@
1
+ // Copyright (c) 2025 by Beardon Services, Inc.
2
+
3
+ const apiLogLevelCaptions = {
4
+ DEFAULT: 'Default',
5
+ ERROR: 'Error',
6
+ WARNING: 'Warning',
7
+ INFO: 'Info',
8
+ HTTP: 'HTTP',
9
+ VERBOSE: 'Verbose',
10
+ DEBUG: 'Debug',
11
+ SILLY: 'Silly',
12
+ };
13
+
14
+ const apiLogLevels = {
15
+ DEFAULT: -1,
16
+ ERROR: 0,
17
+ WARNING: 1,
18
+ INFO: 2,
19
+ HTTP: 3,
20
+ VERBOSE: 4,
21
+ DEBUG: 5,
22
+ SILLY: 6,
23
+ };
24
+
25
+ const databaseOperations = {
26
+ DELETE: 'DELETE',
27
+ INSERT: 'INSERT',
28
+ SELECT: 'SELECT',
29
+ TRUNCATE: 'TRUNCATE',
30
+ UPDATE: 'UPDATE',
31
+ };
32
+
33
+ const httpMethods = {
34
+ GET: 'GET',
35
+ HEAD: 'HEAD',
36
+ POST: 'POST',
37
+ PUT: 'PUT',
38
+ DELETE: 'DELETE',
39
+ CONNECT: 'CONNECT',
40
+ OPTIONS: 'OPTIONS',
41
+ TRACE: 'TRACE',
42
+ PATCH: 'PATCH',
43
+ };
44
+
45
+ const httpRequestHeaders = {
46
+ ALLOW: 'Allow',
47
+ AUTHORIZATION: 'Authorization',
48
+ BEARER_PREFIX: 'Bearer',
49
+ REQUEST_ID: 'X-Request-Id',
50
+ };
51
+
52
+ const httpResponseHeaders = {
53
+ CONTENT_LENGTH: 'Content-Length',
54
+ };
55
+
56
+ const morganFormats = {
57
+ COMBINED: 'combined',
58
+ COMMON: 'common',
59
+ DEV: 'dev',
60
+ DEV_ENHANCED: ':method :url :status-colored :response-time-format :content-length-format',
61
+ SHORT: 'short',
62
+ TINY: 'tiny',
63
+ };
64
+
65
+ const winstonLogLevelNames = {
66
+ ERROR: 'error',
67
+ WARNING: 'warn',
68
+ INFO: 'info',
69
+ HTTP: 'http',
70
+ VERBOSE: 'verbose',
71
+ DEBUG: 'debug',
72
+ SILLY: 'silly',
73
+ };
74
+
75
+ // using NPM logging levels
76
+ const winstonLogLevels = {
77
+ ERROR: 0,
78
+ WARNING: 1,
79
+ INFO: 2,
80
+ HTTP: 3,
81
+ VERBOSE: 4,
82
+ DEBUG: 5,
83
+ SILLY: 6,
84
+ };
85
+
86
+ module.exports = {
87
+ apiLogLevelCaptions,
88
+ apiLogLevels,
89
+ databaseOperations,
90
+ httpMethods,
91
+ httpRequestHeaders,
92
+ httpResponseHeaders,
93
+ morganFormats,
94
+ winstonLogLevelNames,
95
+ winstonLogLevels,
96
+ };
package/lib/http.js ADDED
@@ -0,0 +1,47 @@
1
+ // Copyright (c) 2025 by Beardon Services, Inc.
2
+
3
+ function areHeadersSent(res) {
4
+ return typeof (res.headersSent !== 'boolean') ? Boolean(res._header) : res.headersSent;
5
+ }
6
+
7
+ function getRequestResponseTime(req, res) {
8
+ if (!req._startAt || !res._startAt) return null;
9
+ return (res._startAt[ 0 ] - req._startAt[ 0 ]) * 1e3 + (res._startAt[ 1 ] - req._startAt[ 1 ]) * 1e-6;
10
+ }
11
+
12
+ function parseRequest(req) {
13
+ if (!req) return { };
14
+ const body = req.body || { };
15
+ return {
16
+ acceptLanguage: parseRequestAcceptLanguage(req),
17
+ client: body.client || null,
18
+ clientVersion: body.applicationVersion || null,
19
+ ipAddress: parseRequestIpAddress(req),
20
+ sessionAge: body.uptime || 0,
21
+ sessionId: body.sessionId || null,
22
+ userAgent: parseRequestUserAgent(req),
23
+ };
24
+ }
25
+
26
+ function parseRequestAcceptLanguage(req) {
27
+ const body = req.body || { };
28
+ return body.language || req.header('Accept-Language');
29
+ }
30
+
31
+ function parseRequestIpAddress(req) {
32
+ const body = req.body || { };
33
+ const LOCALHOST_IPS = [ '127.0.0.1', '::1' ];
34
+ const ipAddress = body.ipAddress || req.get('X-Forwarded-For') || req.socket.remoteAddress;
35
+ return LOCALHOST_IPS.includes(ipAddress) ? 'localhost' : ipAddress;
36
+ }
37
+
38
+ function parseRequestUserAgent(req) {
39
+ const body = req.body || { };
40
+ return body.agent || body.osVersion || req.get('User-Agent');
41
+ }
42
+
43
+ module.exports = {
44
+ areHeadersSent,
45
+ getRequestResponseTime,
46
+ parseRequest,
47
+ };
@@ -0,0 +1,40 @@
1
+ // Copyright (c) 2025 by Beardon Services, Inc.
2
+
3
+ const _ = require('lodash');
4
+ const enums = require('../enums');
5
+
6
+ const { databaseOperations: dbo, httpMethods: http } = enums;
7
+
8
+ const databaseOperationColors = [
9
+ { match: dbo.DELETE, fore: '#F22613', back: null },
10
+ { match: dbo.INSERT, fore: '#00B16A', back: null },
11
+ { match: dbo.SELECT, fore: '#1E90FF', back: null },
12
+ { match: dbo.TRUNCATE, fore: '#750505', back: null },
13
+ { match: dbo.UPDATE, fore: '#F9690E', back: null },
14
+ ];
15
+
16
+ const httpStatusColors = [
17
+ { range: _.range(500, 600), ansi: 31 },
18
+ { range: _.range(400, 500), ansi: 33 },
19
+ { range: _.range(300, 400), ansi: 36 },
20
+ { range: _.range(200, 300), ansi: 32 },
21
+ { range: _.range(100, 200), ansi: 0 },
22
+ ];
23
+
24
+ const httpVerbColors = [
25
+ { match: http.GET, fore: '#000000', back: '#67D193' },
26
+ { match: http.HEAD, fore: '#000000', back: '#68D696' },
27
+ { match: http.POST, fore: '#000000', back: '#F4DA7A' },
28
+ { match: http.PUT, fore: '#000000', back: '#74AEF6' },
29
+ { match: http.DELETE, fore: '#000000', back: '#EF968A' },
30
+ { match: http.CONNECT, fore: '#000000', back: '#FFFFFF' },
31
+ { match: http.OPTIONS, fore: '#000000', back: '#E55AA8' },
32
+ { match: http.TRACE, fore: '#000000', back: '#FFFFFF' },
33
+ { match: http.PATCH, fore: '#000000', back: '#C0A8E1' },
34
+ ];
35
+
36
+ module.exports = {
37
+ databaseOperationColors,
38
+ httpStatusColors,
39
+ httpVerbColors,
40
+ };
@@ -0,0 +1,20 @@
1
+ // Copyright (c) 2025 by Beardon Services, Inc.
2
+
3
+ const enums = require('../enums');
4
+
5
+ const { apiLogLevels: all, apiLogLevelCaptions: allc, winstonLogLevelNames: wlln, winstonLogLevels: wll } = enums;
6
+
7
+ const logLevelMap = [
8
+ { apiLevel: all.DEFAULT, winstonLevel: null, winstonName: null, caption: allc.DEFAULT },
9
+ { apiLevel: all.ERROR, winstonLevel: wll.ERROR, winstonName: wlln.ERROR, caption: allc.ERROR },
10
+ { apiLevel: all.WARNING, winstonLevel: wll.WARNING, winstonName: wlln.WARNING, caption: allc.WARNING },
11
+ { apiLevel: all.INFO, winstonLevel: wll.INFO, winstonName: wlln.INFO, caption: allc.INFO },
12
+ { apiLevel: all.HTTP, winstonLevel: wll.HTTP, winstonName: wlln.HTTP, caption: allc.HTTP },
13
+ { apiLevel: all.VERBOSE, winstonLevel: wll.VERBOSE, winstonName: wlln.VERBOSE, caption: allc.VERBOSE },
14
+ { apiLevel: all.DEBUG, winstonLevel: wll.DEBUG, winstonName: wlln.DEBUG, caption: allc.DEBUG },
15
+ { apiLevel: all.SILLY, winstonLevel: wll.SILLY, winstonName: wlln.SILLY, caption: allc.SILLY },
16
+ ];
17
+
18
+ module.exports = {
19
+ logLevelMap,
20
+ };
package/lib/math.js ADDED
@@ -0,0 +1,29 @@
1
+ // Copyright (c) 2025 by Beardon Services, Inc.
2
+
3
+ const _ = require('lodash');
4
+
5
+ function convertBytes(bytes, options = { }) {
6
+ options = options || { };
7
+ if (!bytes) return null;
8
+ const useBinaryUnits = options.useBinaryUnits || false;
9
+ const decimals = options.decimals || 2;
10
+ const spaced = options.hasOwnProperty('spaced') ? options.spaced : false;
11
+ const base = useBinaryUnits ? 1024 : 1000;
12
+ const units = useBinaryUnits
13
+ ? [ 'B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB' ]
14
+ : [ 'B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB' ];
15
+ const i = Math.floor(Math.log(bytes) / Math.log(base));
16
+ const size = (bytes / Math.pow(base, i)).toFixed(decimals);
17
+ return `${ size }${ spaced ? ' ' : '' }${ units[ i ] }`;
18
+ }
19
+
20
+ function round(value, decimals = 2) {
21
+ if (!_.isNumber(value)) return value;
22
+ const rounder = Math.pow(10, decimals);
23
+ return Math.round((value + Number.EPSILON) * rounder) / rounder;
24
+ }
25
+
26
+ module.exports = {
27
+ convertBytes,
28
+ round,
29
+ };
@@ -0,0 +1,280 @@
1
+ // Copyright (c) 2025 by Beardon Services, Inc.
2
+
3
+ const _ = require('lodash');
4
+ const chalk = require('chalk');
5
+ const { createLogger: createWinstonLogger, format, transports } = require('winston');
6
+ const morgan = require('morgan');
7
+ const { v4: uuid } = require('uuid');
8
+
9
+ const { areHeadersSent, getRequestResponseTime, parseRequest } = require('./http');
10
+ const { chalkHttpStatus, chalkHttpVerb, stringToIdempotentHexColor } = require('./color');
11
+ const { convertBytes } = require('./math');
12
+ const { convertMilliseconds } = require('./date');
13
+ const enums = require('./enums');
14
+ const { logLevelMap } = require('./mappings/log_mappings');
15
+
16
+ const { colorize, combine, label, metadata, printf, timestamp } = format;
17
+ const { morganFormats: mf, httpRequestHeaders: rqh, httpResponseHeaders: rsh, winstonLogLevels: wll, winstonLogLevelNames: wlln } = enums;
18
+
19
+ const COLOR_CONSOLE_WHITE_HEX = '#ECECEC';
20
+ const DEFAULT_REQUEST_ID_ATTRIBUTE = 'requestId';
21
+ const DEFAULT_USER_LOG_LEVEL = -1;
22
+ const WINSTON_CONSOLE_TRANSPORT_NAME = 'console';
23
+
24
+ class ZeddemoreLogger {
25
+
26
+ constructor(options) {
27
+ options = options || { };
28
+ this.additionalWriteFn = options.additionalWriteFn || null;
29
+ this.defaultLabel = options.defaultLabel || 'zeddemore-logger';
30
+ this.disableMorganLogging = options.hasOwnProperty('disableMorganLogging') ? options.disableMorganLogging : false;
31
+ this.displayCaller = options.hasOwnProperty('displayCaller') ? options.displayUsername : true;
32
+ this.displayIpAddress = options.hasOwnProperty('displayIpAddress') ? options.displayIpAddress : true;
33
+ this.displayRequestId = options.hasOwnProperty('displayRequestId') ? options.displayRequestId : true;
34
+ this.customCallerSettings = options.customCallerSettings || [ ];
35
+ this.defaultUserLogLevel = options.defaultUserLogLevel || DEFAULT_USER_LOG_LEVEL;
36
+ this.forceLogLevel = options.hasOwnProperty('forceLogLevel') ? options.forceLogLevel : false;
37
+ this.logLevel = options.hasOwnProperty('logLevel') ? options.logLevel : wll.INFO;
38
+ this.morganFormat = options.morganFormat || mf.DEV_ENHANCED;
39
+ this.timestampFormat = options.timestampFormat || 'YYYY-MM-DD HH:mm:ss';
40
+ this.requestIdAttribute = options.requestIdAttribute || DEFAULT_REQUEST_ID_ATTRIBUTE;
41
+ this.userGetFn = options.userGetFn || this.#userGetFn;
42
+ }
43
+
44
+ #buildWinstonFormat = (options, additionalFormats = [ ]) =>{
45
+ options = options || { };
46
+ const colorizedTemplate = this.#colorizedTemplate.bind(this);
47
+ const formats = [
48
+ format((info) => {
49
+ info.id = info.id || options.id || null;
50
+ info.ip = info.ip || options.ip || null;
51
+ info.user = info.user || options.user || null;
52
+ return info;
53
+ })(),
54
+ colorize(),
55
+ label({ label: options.label || this.defaultLabel }),
56
+ timestamp({ format: this.timestampFormat }),
57
+ metadata(),
58
+ printf(this.#colorizedTemplate),
59
+ ];
60
+ return combine(...additionalFormats.concat(formats));
61
+ }
62
+
63
+ #buildWinstonTransport = (logLevel = this.logLevel) => {
64
+ logLevel = logLevel || this.logLevel;
65
+ const level = ZeddemoreLogger.winstonLogLevelIdToName(logLevel);
66
+ return new transports.Console({ level, name: WINSTON_CONSOLE_TRANSPORT_NAME });
67
+ }
68
+
69
+ #buildWinstonTransports = (logLevel = this.logLevel, additionalTransports = [ ]) => {
70
+ return additionalTransports.concat([ this.#buildWinstonTransport(logLevel) ]);
71
+ }
72
+
73
+ #callerToHexColor = (caller) => {
74
+ if (!caller) return COLOR_CONSOLE_WHITE_HEX;
75
+ const customCallerSetting = _.find(this.customCallerSettings, { caller });
76
+ if (customCallerSetting && customCallerSetting.color && customCallerSetting.color.fore) return customCallerSetting.color.fore;
77
+ return stringToIdempotentHexColor(caller, true);
78
+ }
79
+
80
+ #colorizeResponse(message) {
81
+ const verb = message.split(' ')[ 0 ];
82
+ return chalkHttpVerb(verb, message);
83
+ }
84
+
85
+ #colorizedTemplate = (info) => {
86
+ const id = info.metadata.id ? info.metadata.id.substring(0, 8) : null;
87
+ const subId = info.metadata.subId || null;
88
+ const caller = info.metadata.caller || info.metadata.user || null;
89
+ const ipAddress = info.metadata.ip || null;
90
+ const prefix = info.metadata.prefix || null;
91
+ const isError = (info.level.indexOf(wlln.ERROR) > -1);
92
+ const leaveUncompressed = isError || info.metadata.raw;
93
+ const idHexColor = stringToIdempotentHexColor(id, true);
94
+ const callerColorSource = caller || ipAddress;
95
+ const callerHexColor = this.#callerToHexColor(callerColorSource);
96
+ const parts = [ ];
97
+ parts.push(info.metadata.timestamp);
98
+ parts.push(`[${ info.metadata.target || info.metadata.label }]`);
99
+ parts.push(`${ info.level }:`);
100
+ const fullId = '🧵' + id + (subId ? `[${ subId }]` : '');
101
+ if (id && this.displayRequestId) parts.push(chalk.bold.hex(idHexColor)(fullId));
102
+ const callerParts = [ ];
103
+ if (caller && this.displayCaller) {
104
+ const customCallerSettings = _.find(this.customCallerSettings, { caller });
105
+ const logPrefix = customCallerSettings ? customCallerSettings.logPrefix : null;
106
+ callerParts.push(logPrefix ? logPrefix + caller : caller);
107
+ }
108
+ if (ipAddress && this.displayIpAddress) callerParts.push(ipAddress);
109
+ const fullCaller = callerParts.join('@');
110
+ if (fullCaller) parts.push(`<${ chalk.underline.hex(callerHexColor).bold(fullCaller) }>`);
111
+ if (prefix) parts.push(`[${ prefix }]`);
112
+ let message = Array.isArray(info.message) ? info.message.join('\n') : info.message;
113
+ message = _.isObject(message) ? JSON.stringify(message) : message;
114
+ parts.push(leaveUncompressed ? message : message.replace(/(\n?\s+)/g, ' '));
115
+ if (info.err) parts.push(info.err);
116
+ return isError ? chalk.red(parts.join(' ')) : parts.join(' ');
117
+ }
118
+
119
+ /**
120
+ * Create a Winston Logger
121
+ * @param options
122
+ * @returns {Logger}
123
+ */
124
+ #createLogger = (options = { }) => {
125
+ options = options || { };
126
+ return createWinstonLogger({
127
+ format: this.#buildWinstonFormat(options, options.formats),
128
+ transports: this.#buildWinstonTransports(options.level, options.transports),
129
+ });
130
+ }
131
+
132
+ #getRequestId = (req) => {
133
+ if (!req) return null;
134
+ return req[ this.requestIdAttribute ];
135
+ }
136
+
137
+ #initializeMorgan() {
138
+ function buildContentLengthFormatToken(res) {
139
+ const contentLength = convertBytes(res.get('content-length'), { digits: 2 });
140
+ return contentLength ? ` - ${ contentLength }` : '';
141
+ }
142
+ function buildResponseTimeFormatToken(req, res) {
143
+ const responseTime = getRequestResponseTime(req, res);
144
+ return responseTime ? convertMilliseconds(responseTime, { digits: 0 }) : '-';
145
+ }
146
+ function buildStatusColoredToken(res) {
147
+ const statusCode = areHeadersSent(res) ? res.statusCode : null;
148
+ if (!statusCode) return '-';
149
+ return chalkHttpStatus(statusCode);
150
+ }
151
+ morgan.token('content-length-format', (req, res) => { return buildContentLengthFormatToken(res); });
152
+ morgan.token('response-time-format', (req, res) => { return buildResponseTimeFormatToken(req, res); });
153
+ morgan.token('status-colored', (req, res) => { return buildStatusColoredToken(res); });
154
+ }
155
+
156
+ #createRequestLogger = (requestId = null, ipAddress = null) => {
157
+ requestId = requestId || this.#generateUuid();
158
+ const loggerOptions = { id: requestId };
159
+ if (ipAddress) loggerOptions.ip = ipAddress;
160
+ return this.#createLogger(loggerOptions);
161
+ }
162
+
163
+ #createUserLogger = (requestId = null, username = null, logLevel = this.defaultUserLogLevel, ipAddress = null) => {
164
+ requestId = requestId || this.#generateUuid();
165
+ logLevel = logLevel || this.defaultUserLogLevel;
166
+ const loggerOptions = { id: requestId, level: logLevel, user: username };
167
+ if (ipAddress) loggerOptions.ip = ipAddress;
168
+ return this.#createLogger(loggerOptions);
169
+ }
170
+
171
+ #generateUuid() {
172
+ return uuid(null, null, null);
173
+ }
174
+
175
+ static log(options) {
176
+ options = options || { };
177
+ const logger = options.logger || (options.user ? options.user.logger : null) || ((app && app.locals) ? app.locals.logger : null);
178
+ if (logger) return logger;
179
+ const zeddemoreLogger = new ZeddemoreLogger();
180
+ return zeddemoreLogger.#createLogger(options);
181
+ }
182
+
183
+ morganMiddleware = (options) => {
184
+ options = options || { };
185
+ const additionalWriteFn = (_.isFunction(options.additionalWriteFn) ? options.additionalWriteFn : null) || this.additionalWriteFn;
186
+ const disableMorganLogging = options.hasOwnProperty('disableMorganLogging') ? options.disableMorganLogging : this.disableMorganLogging;
187
+ const morganFormat = options.morganFormat || this.morganFormat;
188
+ const userGetFn = (_.isFunction(options.userGetFn) ? options.userGetFn : null) || this.userGetFn;
189
+ return (req, res, next) => {
190
+ this.#initializeMorgan();
191
+ return morgan(morganFormat, {
192
+ skip: () => disableMorganLogging,
193
+ stream: {
194
+ write: (message) => {
195
+ if (additionalWriteFn) additionalWriteFn(req, res);
196
+ const logOptions = { };
197
+ const user = userGetFn ? userGetFn(req, res) : null;
198
+ if (user) logOptions.user = user;
199
+ ZeddemoreLogger.log(logOptions).http(this.#colorizeResponse(message.trim()));
200
+ },
201
+ },
202
+ })(req, res, next);
203
+ }
204
+ }
205
+
206
+ requestLoggerMiddleware = (req, res, next) => {
207
+ const requestValues = parseRequest(req);
208
+ const requestId = uuid();//this.#generateUuid();
209
+ req[ this.requestIdAttribute ] = requestId;
210
+ res.header(rqh.REQUEST_ID, requestId);
211
+ const ipAddress = requestValues.ipAddress;
212
+ res.locals.user = res.locals.user || { };
213
+ res.locals.user.logger = this.#createRequestLogger(requestId, ipAddress);
214
+ return next();
215
+ }
216
+
217
+ #userGetFn(req, res) {
218
+ return (res.locals ? res.locals.user : null) || (req.locals ? req.locals.user : null) || req.user || null;
219
+ }
220
+
221
+ userLoggerMiddleware = (req, res, next) => {
222
+ const requestId = this.#getRequestId(req);
223
+ const user = res.locals.user || { };
224
+ if (!requestId) return next();
225
+ const username = user.login || null;
226
+ let ipAddress = user.ipAddress || null;
227
+ if (!ipAddress) {
228
+ const requestValues = parseRequest(req);
229
+ ipAddress = requestValues.ipAddress;
230
+ }
231
+ let userLogLevel = !_.isNil(user.logLevel) ? user.logLevel : this.defaultUserLogLevel;
232
+ if (userLogLevel < 0) userLogLevel = this.logLevel;
233
+ const level = this.forceLogLevel ? this.logLevel : userLogLevel;
234
+ res.locals.user = res.locals.user || user;
235
+ res.locals.user.logger = this.#createUserLogger(requestId, username, level, ipAddress);
236
+ return next();
237
+ }
238
+
239
+ static winstonLogLevelIdToName(levelId) {
240
+ const logLevelEntry = _.find(logLevelMap, { winstonLevel: levelId });
241
+ return logLevelEntry ? logLevelEntry.winstonName : wlln.HTTP;
242
+ }
243
+
244
+ static writeAxiosLog(axiosResponse, logOptions, options) {
245
+ function massageLogLevel(logLevel) {
246
+ if (!logLevel) return wlln.VERBOSE;
247
+ if (!_.isInteger(logLevel)) return logLevel;
248
+ return ZeddemoreLogger.winstonLogLevelIdToName(logLevel);
249
+ }
250
+ if (!axiosResponse || !axiosResponse.request) return;
251
+ logOptions = logOptions || { };
252
+ options = options || { };
253
+ const axiosConfig = logOptions.axios || { };
254
+ const configData = logOptions.data ? JSON.stringify(axiosConfig.data) : null;
255
+ const logLevel = massageLogLevel(logOptions.logLevel);
256
+ const logger = options.logger || ZeddemoreLogger.log(options);
257
+ if (!logger) return;
258
+ const _method = axiosResponse.request.method || null;
259
+ const method = _method ? chalkHttpVerb(_method) : null;
260
+ const path = axiosResponse.request.path || '';
261
+ const prefix = logOptions.prefix ? `[${ logOptions.prefix }]` : null;
262
+ const responseData = logOptions.response ? JSON.stringify(axiosResponse.data) : null;
263
+ const status = chalkHttpStatus(axiosResponse.status);
264
+ const _duration = axiosResponse.duration;
265
+ const duration = _duration ? convertMilliseconds(_duration) : '-';
266
+ const _contentLength = axiosResponse.headers ? axiosResponse.headers[ rsh.CONTENT_LENGTH ] : null;
267
+ const contentLength = _contentLength ? `- ${ convertBytes(_contentLength) }` : null;
268
+ const parts = [ prefix, method, path, configData, status, responseData, duration, contentLength ];
269
+ const message = _.compact(parts).join(' ');
270
+ logger[ logLevel ](message);
271
+ }
272
+
273
+ }
274
+
275
+ module.exports = {
276
+ log: ZeddemoreLogger.log,
277
+ ZeddemoreLogger,
278
+ winstonLogLevelIdToName: ZeddemoreLogger.winstonLogLevelIdToName,
279
+ writeAxiosLog: ZeddemoreLogger.writeAxiosLog,
280
+ };
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "zeddemore-logger",
3
+ "version": "1.0.0",
4
+ "description": "Node.js Express logger using Morgan and Winston for thread and caller info tracking",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/beardon/zeddemore-logger.git"
8
+ },
9
+ "keywords": [
10
+ "express",
11
+ "logger",
12
+ "morgan",
13
+ "winston"
14
+ ],
15
+ "author": "Aaron Bean <aaron.bean@beardon.com> (https://beardon.com)",
16
+ "license": "UNLICENSED",
17
+ "bugs": {
18
+ "url": "https://github.com/beardon/zeddemore-logger/issues"
19
+ },
20
+ "homepage": "https://github.com/beardon/zeddemore-logger#readme",
21
+ "main": "lib/zeddemore-logger.js",
22
+ "dependencies": {
23
+ "chalk": "^4.1.2",
24
+ "lodash": "^4.17.21",
25
+ "morgan": "^1.10.0",
26
+ "uuid": "^10.0.0",
27
+ "winston": "^3.17.0"
28
+ }
29
+ }