ntlogger 2.8.2 → 2.9.1

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/logger.js CHANGED
@@ -86,15 +86,26 @@ const colorRgb = {
86
86
  };
87
87
  const colorAnsiCode = `\x1b[38;2;${colorRgb.r};${colorRgb.g};${colorRgb.b}m`;
88
88
 
89
+ // Helper function to strip ANSI codes from a string
90
+ function stripAnsiCodes(str) {
91
+ if (typeof str !== 'string') {
92
+ return str;
93
+ }
94
+ // Remove ANSI escape codes: \u001b[ followed by numbers, semicolons, and ending with 'm'
95
+ return str.replace(/\u001b\[[0-9;]*m/g, '');
96
+ }
97
+
89
98
  // Custom console formatter with random color for session ID and set color for log level and message
90
99
  const consoleFormatter = (config) => winston.format.combine(
91
100
  winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
92
101
  winston.format.printf(({ timestamp, level, message, ...meta }) => {
93
102
  const shortSessionId = sessionId.substring(sessionId.length - 6); // Get the last 6 characters of the session ID
94
- const paddedLevel = level.padEnd(8); // Pad the level to ensure consistent spacing
103
+ // Strip ANSI codes and normalize the level string before padding to ensure consistent alignment
104
+ const cleanLevel = stripAnsiCodes(String(level)).trim();
105
+ const paddedLevel = cleanLevel.padEnd(8); // Pad the level to ensure consistent spacing
95
106
 
96
107
  // Retrieve the corresponding ANSI color code for the level from custom settings
97
- const levelColor = customSettings.colors[level] || ''; // Default to no color if the level is unknown
108
+ const levelColor = customSettings.colors[cleanLevel] || customSettings.colors[level] || ''; // Default to no color if the level is unknown
98
109
  const resetCode = '\x1b[0m';
99
110
 
100
111
  // Build location string (optionally include filePath if reportPath is enabled)
@@ -113,7 +124,9 @@ const consoleFormatter = (config) => winston.format.combine(
113
124
  const fileFormatter = (config) => winston.format.combine(
114
125
  winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
115
126
  winston.format.printf(({ timestamp, level, message, ...meta }) => {
116
- const paddedLevel = level.padEnd(8); // Pad the level to ensure consistent spacing
127
+ // Strip ANSI codes and normalize the level string before padding to ensure consistent alignment
128
+ const cleanLevel = stripAnsiCodes(String(level)).trim();
129
+ const paddedLevel = cleanLevel.padEnd(8); // Pad the level to ensure consistent spacing
117
130
 
118
131
  // Build location string (optionally include filePath if reportPath is enabled)
119
132
  let locationStr = meta.location || 'Unknown';
@@ -489,11 +502,39 @@ const createLoggerInstance = (location = "Unknown", config = {}, transports, par
489
502
  const logger = (location = "Unknown", config = {}) => {
490
503
  // Check if a logger for the given location already exists
491
504
  if (loggerInstances.has(location) && !config.skipCache) {
492
- // If it does, grab the existing logger instance from the map
493
- const logger = loggerInstances.get(location);
494
-
495
- logger.internal(`Logger instance retrieved for location ${location}`); // Log the retrieval of the logger instance
496
- return logger;
505
+ const cachedLogger = loggerInstances.get(location);
506
+
507
+ // Check if config has advanced features (sampling, rateLimit, deduplication)
508
+ // If so, we need to recreate the logger to apply these features
509
+ const hasAdvancedFeatures = (
510
+ (config.sampling && Object.keys(config.sampling).length > 0) ||
511
+ (config.rateLimit && Object.keys(config.rateLimit).length > 0) ||
512
+ (config.deduplication && config.deduplication.enabled === true)
513
+ );
514
+
515
+ // If advanced features are requested but logger was cached without them, recreate it
516
+ if (hasAdvancedFeatures) {
517
+ // Check if cached logger has these features by checking if getStats returns non-null values
518
+ const stats = cachedLogger.getStats ? cachedLogger.getStats() : null;
519
+ const cachedHasFeatures = stats && (
520
+ (stats.sampling !== null) ||
521
+ (stats.deduplication !== null)
522
+ );
523
+
524
+ // If cached logger doesn't have the features but config requests them, recreate
525
+ if (!cachedHasFeatures) {
526
+ // Remove from cache and recreate with new config
527
+ loggerInstances.delete(location);
528
+ } else {
529
+ // Cached logger has features, return it
530
+ cachedLogger.internal(`Logger instance retrieved for location ${location}`);
531
+ return cachedLogger;
532
+ }
533
+ } else {
534
+ // No advanced features requested, return cached logger
535
+ cachedLogger.internal(`Logger instance retrieved for location ${location}`);
536
+ return cachedLogger;
537
+ }
497
538
  }
498
539
 
499
540
  let transports = null;
package/package.json CHANGED
@@ -1,16 +1,18 @@
1
1
  {
2
2
  "name": "ntlogger",
3
- "version": "2.8.2",
3
+ "version": "2.9.1",
4
4
  "description": "A Custom Ready-To-Go Logging Wrapper Built on Winston",
5
5
  "keywords": [
6
6
  "custom logger",
7
7
  "discord compatible",
8
+ "fastify logger",
8
9
  "log management",
9
10
  "logger",
10
11
  "logger wrapper",
11
12
  "logging",
12
13
  "logging utility",
13
14
  "mysql compatible",
15
+ "pino transport",
14
16
  "sentry compatible",
15
17
  "syslog compatible",
16
18
  "teams compatible",
@@ -24,26 +26,43 @@
24
26
  "type": "git",
25
27
  "url": "git+https://github.com/NightSquawk/NightTimeLogger.git"
26
28
  },
29
+ "exports": {
30
+ ".": "./index.js",
31
+ "./pino": "./pino.js"
32
+ },
27
33
  "files": [
28
34
  "index.js",
29
35
  "index.d.ts",
36
+ "pino.js",
30
37
  "lib/*.js",
31
- "plugins/**/*"
38
+ "plugins/**/*",
39
+ "transports/"
32
40
  ],
33
41
  "scripts": {
34
42
  "test": "jest"
35
43
  },
36
44
  "dependencies": {
37
- "@sentry/node": "^10.25.0",
38
- "mysql2": "^3.15.3",
39
- "pg": "^8.16.3",
40
- "winston": "^3.18.3",
45
+ "@sentry/node": "^10.34.0",
46
+ "mysql2": "^3.16.1",
47
+ "pg": "^8.17.1",
48
+ "winston": "^3.19.0",
41
49
  "winston-transport": "^4.9.0"
42
50
  },
51
+ "optionalDependencies": {
52
+ "split2": "^4.2.0"
53
+ },
54
+ "peerDependencies": {
55
+ "pino": ">=8.0.0"
56
+ },
57
+ "peerDependenciesMeta": {
58
+ "pino": {
59
+ "optional": true
60
+ }
61
+ },
43
62
  "devDependencies": {
44
- "@sentry/profiling-node": "^10.25.0",
45
- "dotenv": "^17.2.3",
46
- "jest": "^30.2.0"
63
+ "@sentry/profiling-node": "^10.38.0",
64
+ "dotenv": "^17.2.4",
65
+ "jest": "^30.3.0"
47
66
  },
48
67
  "overrides": {
49
68
  "glob": "^11.1.0"
package/pino.js ADDED
@@ -0,0 +1,36 @@
1
+ 'use strict';
2
+
3
+ // Re-export the transport for use with Pino's transport option
4
+ // Usage: transport: { target: 'ntlogger/pino' }
5
+ module.exports = require('./transports/pino');
6
+
7
+ // Helper for creating a pre-configured Pino instance
8
+ module.exports.createLogger = function (opts = {}) {
9
+ const pino = require('pino');
10
+ const path = require('path');
11
+
12
+ const isDev = process.env.NODE_ENV !== 'production';
13
+ const level = opts.level || process.env.LOG_LEVEL || (isDev ? 'debug' : 'info');
14
+
15
+ const loggerOpts = {
16
+ level,
17
+ };
18
+
19
+ // Add module name as a mixin if provided
20
+ if (opts.module) {
21
+ loggerOpts.mixin = () => ({ module: opts.module });
22
+ }
23
+
24
+ // In dev: use NTL formatter. In production: plain JSON.
25
+ if (isDev) {
26
+ loggerOpts.transport = {
27
+ target: path.join(__dirname, 'transports', 'pino.js'),
28
+ options: {
29
+ defaultModule: opts.module || opts.defaultModule,
30
+ colorize: opts.colorize,
31
+ },
32
+ };
33
+ }
34
+
35
+ return pino(loggerOpts);
36
+ };
@@ -0,0 +1,172 @@
1
+ 'use strict';
2
+
3
+ const split = require('split2');
4
+ const colors = require('../lib/colors');
5
+
6
+ const levelColors = colors.console;
7
+
8
+ // Utility ANSI codes not exported by lib/colors.js
9
+ const RESET = '\x1b[0m';
10
+ const DIM = '\x1b[2m';
11
+ const GREY = '\x1b[90m';
12
+ const GREEN = '\x1b[32m';
13
+ const MAGENTA = '\x1b[35m';
14
+ const CYAN = '\x1b[36m';
15
+
16
+ /**
17
+ * Map Pino numeric levels to NTL level names and colors.
18
+ *
19
+ * Pino levels: 10=trace, 20=debug, 30=info, 40=warn, 50=error, 60=fatal
20
+ * NTL levels: trace(5), debug(4), info(3), warn(2), error(1), fatal(0)
21
+ *
22
+ * We only need the display names and colors here, not NTL's numeric ordering.
23
+ */
24
+ const PINO_LEVEL_MAP = {
25
+ 10: { name: 'trace', color: levelColors.trace },
26
+ 20: { name: 'debug', color: levelColors.debug },
27
+ 30: { name: 'info', color: levelColors.info },
28
+ 40: { name: 'warn', color: levelColors.warn },
29
+ 50: { name: 'error', color: levelColors.error },
30
+ 60: { name: 'fatal', color: levelColors.fatal },
31
+ };
32
+
33
+ // Status code colors
34
+ const STATUS_COLORS = {
35
+ 2: GREEN,
36
+ 3: CYAN,
37
+ 4: '\x1b[33m',
38
+ 5: '\x1b[31m',
39
+ };
40
+
41
+ /**
42
+ * Format a timestamp to YYYY-MM-DD HH:mm:ss (NTL style).
43
+ */
44
+ function formatTimestamp(epoch) {
45
+ const d = new Date(epoch);
46
+ const Y = d.getFullYear();
47
+ const M = String(d.getMonth() + 1).padStart(2, '0');
48
+ const D = String(d.getDate()).padStart(2, '0');
49
+ const h = String(d.getHours()).padStart(2, '0');
50
+ const m = String(d.getMinutes()).padStart(2, '0');
51
+ const s = String(d.getSeconds()).padStart(2, '0');
52
+ return `${Y}-${M}-${D} ${h}:${m}:${s}`;
53
+ }
54
+
55
+ /**
56
+ * Pad level name to 8 chars (NTL style: [info ], [error ], [warn ]).
57
+ */
58
+ function padLevel(name) {
59
+ return name.padEnd(8);
60
+ }
61
+
62
+ /**
63
+ * Format a single Pino log object into an NTL-style string.
64
+ *
65
+ * @param {Object} obj - Pino JSON log object
66
+ * @param {Object} opts - Transport options
67
+ * @returns {string} Formatted log line
68
+ */
69
+ function formatLine(obj, opts = {}) {
70
+ const parts = [];
71
+
72
+ // 1. Timestamp
73
+ const ts = formatTimestamp(obj.time);
74
+ parts.push(`${GREY}${ts}${RESET}`);
75
+
76
+ // 2. Level — [info ] padded and colored
77
+ const levelInfo = PINO_LEVEL_MAP[obj.level] || { name: 'unknown', color: '' };
78
+ parts.push(`${levelInfo.color}[${padLevel(levelInfo.name)}]${RESET}`);
79
+
80
+ // 3. Request/Session ID — [ID: xxx]
81
+ const id = obj.reqId || obj.requestId || obj.sessionId || null;
82
+ if (id) {
83
+ parts.push(`${MAGENTA}[ID: ${id}]${RESET}`);
84
+ }
85
+
86
+ // 4. Location/Module label — [ModuleName]
87
+ const location = obj.module || obj.location || obj.name || opts.defaultModule || null;
88
+ if (location) {
89
+ parts.push(`${GREEN}[${location}]${RESET}:`);
90
+ }
91
+
92
+ // 5. Message
93
+ let msg = obj.msg || obj.message || '';
94
+
95
+ // 6. HTTP request completion — format as: METHOD /path — STATUS (TIMEms)
96
+ if (obj.res && obj.responseTime !== undefined) {
97
+ const status = obj.res.statusCode;
98
+ const statusGroup = Math.floor(status / 100);
99
+ const statusColor = STATUS_COLORS[statusGroup] || '';
100
+ const method = (obj.req && obj.req.method) || '';
101
+ const url = (obj.req && obj.req.url) || '';
102
+ const time = typeof obj.responseTime === 'number'
103
+ ? obj.responseTime.toFixed(1)
104
+ : obj.responseTime;
105
+
106
+ if (method && url) {
107
+ msg = `${method} ${url} — ${statusColor}${status}${RESET} ${GREY}(${time}ms)${RESET}`;
108
+ } else if (msg) {
109
+ msg = `${msg} — ${statusColor}${status}${RESET} ${GREY}(${time}ms)${RESET}`;
110
+ } else {
111
+ msg = `${statusColor}${status}${RESET} ${GREY}(${time}ms)${RESET}`;
112
+ }
113
+ }
114
+
115
+ // 7. Error stack traces
116
+ if (obj.err) {
117
+ const stack = obj.err.stack || obj.err.message || '';
118
+ if (stack) {
119
+ msg += `\n${levelInfo.color}${stack}${RESET}`;
120
+ }
121
+ }
122
+
123
+ parts.push(msg);
124
+
125
+ return parts.join(' ');
126
+ }
127
+
128
+ /**
129
+ * NightTimeLogger Pino Transport.
130
+ *
131
+ * Usage with Fastify:
132
+ * const app = Fastify({
133
+ * logger: {
134
+ * transport: {
135
+ * target: 'ntlogger/pino',
136
+ * options: { defaultModule: 'API' }
137
+ * }
138
+ * }
139
+ * });
140
+ *
141
+ * Usage with plain Pino:
142
+ * const pino = require('pino');
143
+ * const logger = pino({
144
+ * transport: {
145
+ * target: 'ntlogger/pino',
146
+ * options: { defaultModule: 'MyApp' }
147
+ * }
148
+ * });
149
+ *
150
+ * Options:
151
+ * - defaultModule: Default location label when obj.module is not set (e.g., 'API')
152
+ *
153
+ * @param {Object} opts - Transport options
154
+ */
155
+ module.exports = function (opts = {}) {
156
+ return split(function (line) {
157
+ let obj;
158
+ try {
159
+ obj = JSON.parse(line);
160
+ } catch {
161
+ process.stdout.write(line + '\n');
162
+ return;
163
+ }
164
+ const formatted = formatLine(obj, opts);
165
+ process.stdout.write(formatted + '\n');
166
+ });
167
+ };
168
+
169
+ // Export internals for testing
170
+ module.exports.formatLine = formatLine;
171
+ module.exports.formatTimestamp = formatTimestamp;
172
+ module.exports.padLevel = padLevel;