@crimsonsunset/jsg-logger 1.7.15 → 1.8.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/config/config-manager.js +8 -4
- package/index.d.ts +48 -0
- package/index.js +57 -21
- package/package.json +1 -1
- package/utils/redaction.js +3 -0
- package/utils/transport-dispatcher.js +86 -0
package/config/config-manager.js
CHANGED
|
@@ -198,8 +198,6 @@ export class ConfigManager {
|
|
|
198
198
|
|
|
199
199
|
// Handle environment-specific configurations
|
|
200
200
|
if (config.environments) {
|
|
201
|
-
// For now, just log that environment configs exist
|
|
202
|
-
// TODO: Implement environment-based config selection
|
|
203
201
|
metaLog(`[JSG-LOGGER] Found environment configs for: ${Object.keys(config.environments).join(', ')}`);
|
|
204
202
|
}
|
|
205
203
|
|
|
@@ -207,6 +205,11 @@ export class ConfigManager {
|
|
|
207
205
|
if (config.components) {
|
|
208
206
|
normalized.components = this._normalizeComponents(config.components);
|
|
209
207
|
}
|
|
208
|
+
|
|
209
|
+
// Transports are live object instances — pass through as-is, no normalization
|
|
210
|
+
if (config.transports) {
|
|
211
|
+
normalized.transports = config.transports;
|
|
212
|
+
}
|
|
210
213
|
|
|
211
214
|
return normalized;
|
|
212
215
|
}
|
|
@@ -339,10 +342,11 @@ export class ConfigManager {
|
|
|
339
342
|
|
|
340
343
|
for (const key in override) {
|
|
341
344
|
if (override.hasOwnProperty(key)) {
|
|
342
|
-
// Special case: 'components' should be replaced, not merged
|
|
343
|
-
// This allows users to define their own components without getting defaults
|
|
344
345
|
if (key === 'components' && typeof override[key] === 'object') {
|
|
345
346
|
merged[key] = override[key];
|
|
347
|
+
} else if (key === 'transports') {
|
|
348
|
+
// Transports are live object instances — always replace, never deep-merge
|
|
349
|
+
merged[key] = override[key];
|
|
346
350
|
} else if (typeof override[key] === 'object' && !Array.isArray(override[key])) {
|
|
347
351
|
merged[key] = this.mergeConfigs(merged[key] || {}, override[key]);
|
|
348
352
|
} else {
|
package/index.d.ts
CHANGED
|
@@ -2,6 +2,52 @@
|
|
|
2
2
|
* TypeScript definitions for @crimsonsunset/jsg-logger
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
// Transport system
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
|
|
9
|
+
export type LogLevel = 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'fatal';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Structured log entry passed to transports.
|
|
13
|
+
* Built automatically by the logger — consumers never create these directly.
|
|
14
|
+
*/
|
|
15
|
+
export interface LogEntry {
|
|
16
|
+
level: LogLevel;
|
|
17
|
+
levelNum: number;
|
|
18
|
+
message: string;
|
|
19
|
+
component: string;
|
|
20
|
+
data?: Record<string, unknown>;
|
|
21
|
+
timestamp: number;
|
|
22
|
+
/** true when levelNum >= 50 (error / fatal) */
|
|
23
|
+
isError: boolean;
|
|
24
|
+
/** Extracted Error instance from data (data itself, data.err, or data.error) */
|
|
25
|
+
error?: Error;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Thin interface that any external log service must implement.
|
|
30
|
+
* The library calls `send()` for every log that passes the transport's level gate.
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* ```ts
|
|
34
|
+
* class DatadogTransport implements LogTransport {
|
|
35
|
+
* level = 'warn' as const;
|
|
36
|
+
* send(entry: LogEntry) { datadogClient.log(entry.message, entry.data); }
|
|
37
|
+
* }
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
export interface LogTransport {
|
|
41
|
+
/** Minimum level this transport cares about. Logs below this are skipped. */
|
|
42
|
+
level?: LogLevel;
|
|
43
|
+
/** Called for each qualifying log entry. May return a Promise (fire-and-forget). */
|
|
44
|
+
send(entry: LogEntry): void | Promise<void>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
// Logger core
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
|
|
5
51
|
export interface LoggerInstance {
|
|
6
52
|
info: (message: string, data?: any) => void;
|
|
7
53
|
debug: (message: string, data?: any) => void;
|
|
@@ -56,6 +102,8 @@ export interface JSGLoggerConfig {
|
|
|
56
102
|
devtools?: {
|
|
57
103
|
enabled?: boolean;
|
|
58
104
|
};
|
|
105
|
+
/** External log service transports. Each receives qualifying LogEntry objects. */
|
|
106
|
+
transports?: LogTransport[];
|
|
59
107
|
[key: string]: any;
|
|
60
108
|
}
|
|
61
109
|
|
package/index.js
CHANGED
|
@@ -13,6 +13,7 @@ import {createCLIFormatter} from './formatters/cli-formatter.js';
|
|
|
13
13
|
import {createServerFormatter, getServerConfig} from './formatters/server-formatter.js';
|
|
14
14
|
import {LogStore} from './stores/log-store.js';
|
|
15
15
|
import {metaLog, metaWarn, metaError} from './utils/meta-logger.js';
|
|
16
|
+
import {buildLogEntry, dispatchToTransports} from './utils/transport-dispatcher.js';
|
|
16
17
|
import packageJson from './package.json' with {type: 'json'};
|
|
17
18
|
|
|
18
19
|
// Check default config for devtools at module load time
|
|
@@ -54,10 +55,11 @@ class JSGLogger {
|
|
|
54
55
|
constructor() {
|
|
55
56
|
this.loggers = {};
|
|
56
57
|
this.logStore = new LogStore();
|
|
57
|
-
this.environment = null;
|
|
58
|
+
this.environment = null;
|
|
58
59
|
this.initialized = false;
|
|
59
|
-
this.components = {};
|
|
60
|
-
this.componentSubscribers = [];
|
|
60
|
+
this.components = {};
|
|
61
|
+
this.componentSubscribers = [];
|
|
62
|
+
this.transports = [];
|
|
61
63
|
}
|
|
62
64
|
|
|
63
65
|
/**
|
|
@@ -117,6 +119,11 @@ class JSGLogger {
|
|
|
117
119
|
window.__JSG_Logger_Enhanced__ = JSGLogger._enhancedLoggers;
|
|
118
120
|
}
|
|
119
121
|
|
|
122
|
+
// Server equivalent: persist to globalThis so cross-bundle module instances can find it
|
|
123
|
+
if (!isBrowser()) {
|
|
124
|
+
globalThis.__JSG_Logger_Enhanced__ = JSGLogger._enhancedLoggers;
|
|
125
|
+
}
|
|
126
|
+
|
|
120
127
|
return JSGLogger._enhancedLoggers;
|
|
121
128
|
}
|
|
122
129
|
|
|
@@ -141,6 +148,13 @@ class JSGLogger {
|
|
|
141
148
|
};
|
|
142
149
|
}
|
|
143
150
|
|
|
151
|
+
// Server equivalent: check globalThis for cross-module-instance singleton
|
|
152
|
+
// Same problem as browser cross-bundle, but for Node.js bundled server chunks
|
|
153
|
+
if (!isBrowser() && globalThis.__JSG_Logger_Enhanced__) {
|
|
154
|
+
JSGLogger._enhancedLoggers = globalThis.__JSG_Logger_Enhanced__;
|
|
155
|
+
return JSGLogger._enhancedLoggers;
|
|
156
|
+
}
|
|
157
|
+
|
|
144
158
|
// No options and no global instance - first time initialization
|
|
145
159
|
if (!JSGLogger._instance) {
|
|
146
160
|
JSGLogger._instance = new JSGLogger();
|
|
@@ -151,6 +165,11 @@ class JSGLogger {
|
|
|
151
165
|
window.JSG_Logger = JSGLogger._enhancedLoggers.controls;
|
|
152
166
|
window.__JSG_Logger_Enhanced__ = JSGLogger._enhancedLoggers;
|
|
153
167
|
}
|
|
168
|
+
|
|
169
|
+
// Server equivalent: persist to globalThis for cross-bundle access
|
|
170
|
+
if (!isBrowser()) {
|
|
171
|
+
globalThis.__JSG_Logger_Enhanced__ = JSGLogger._enhancedLoggers;
|
|
172
|
+
}
|
|
154
173
|
}
|
|
155
174
|
|
|
156
175
|
return JSGLogger._enhancedLoggers;
|
|
@@ -192,6 +211,9 @@ class JSGLogger {
|
|
|
192
211
|
// NOW determine environment (after config is loaded and forceEnvironment is applied)
|
|
193
212
|
this.environment = getEnvironment();
|
|
194
213
|
|
|
214
|
+
// Pick up transports from config (live objects — not deep-merged)
|
|
215
|
+
this.transports = configManager.config.transports ?? [];
|
|
216
|
+
|
|
195
217
|
// Create loggers for all available components
|
|
196
218
|
const components = configManager.getAvailableComponents();
|
|
197
219
|
|
|
@@ -199,11 +221,6 @@ class JSGLogger {
|
|
|
199
221
|
this.loggers[componentName] = this.createLogger(componentName);
|
|
200
222
|
});
|
|
201
223
|
|
|
202
|
-
// Create legacy compatibility aliases
|
|
203
|
-
// Removed: camelCase aliases no longer added to this.loggers
|
|
204
|
-
// Use logger.components.camelCase() or logger.getComponent('kebab-case') instead
|
|
205
|
-
// this.createAliases();
|
|
206
|
-
|
|
207
224
|
// Add utility methods
|
|
208
225
|
this.addUtilityMethods();
|
|
209
226
|
|
|
@@ -303,6 +320,9 @@ class JSGLogger {
|
|
|
303
320
|
// NOW determine environment (after config is loaded and forceEnvironment is applied)
|
|
304
321
|
this.environment = getEnvironment();
|
|
305
322
|
|
|
323
|
+
// Pick up transports from config (live objects — not deep-merged)
|
|
324
|
+
this.transports = configManager.config.transports ?? [];
|
|
325
|
+
|
|
306
326
|
// Clear existing loggers for clean reinitialization
|
|
307
327
|
this.loggers = {};
|
|
308
328
|
this.components = {};
|
|
@@ -311,13 +331,9 @@ class JSGLogger {
|
|
|
311
331
|
const components = configManager.getAvailableComponents();
|
|
312
332
|
|
|
313
333
|
components.forEach(componentName => {
|
|
314
|
-
// Use original createLogger to bypass utility method caching
|
|
315
334
|
const createFn = this._createLoggerOriginal || this.createLogger.bind(this);
|
|
316
335
|
this.loggers[componentName] = createFn(componentName);
|
|
317
336
|
});
|
|
318
|
-
|
|
319
|
-
// Create legacy compatibility aliases
|
|
320
|
-
// Removed: camelCase aliases no longer added to this.loggers
|
|
321
337
|
// Use logger.components.camelCase() or logger.getComponent('kebab-case') instead
|
|
322
338
|
// this.createAliases();
|
|
323
339
|
|
|
@@ -432,28 +448,37 @@ class JSGLogger {
|
|
|
432
448
|
*/
|
|
433
449
|
_wrapPinoLogger(pinoLogger) {
|
|
434
450
|
const levels = ['trace', 'debug', 'info', 'warn', 'error', 'fatal'];
|
|
451
|
+
const levelNums = {trace: 10, debug: 20, info: 30, warn: 40, error: 50, fatal: 60};
|
|
435
452
|
const wrapped = {};
|
|
453
|
+
const self = this;
|
|
436
454
|
|
|
437
455
|
levels.forEach(level => {
|
|
438
456
|
wrapped[level] = (first, ...args) => {
|
|
457
|
+
let message = '';
|
|
458
|
+
let data;
|
|
459
|
+
|
|
439
460
|
// Handle different argument patterns
|
|
440
461
|
if (typeof first === 'string') {
|
|
441
|
-
|
|
442
|
-
const message = first;
|
|
462
|
+
message = first;
|
|
443
463
|
if (args.length === 1 && typeof args[0] === 'object' && args[0] !== null) {
|
|
444
|
-
|
|
464
|
+
data = args[0];
|
|
445
465
|
pinoLogger[level](args[0], message);
|
|
446
466
|
} else {
|
|
447
|
-
// Just message
|
|
448
467
|
pinoLogger[level](message);
|
|
449
468
|
}
|
|
450
469
|
} else if (typeof first === 'object' && first !== null) {
|
|
451
|
-
|
|
470
|
+
data = first;
|
|
471
|
+
message = (args.length > 0 && typeof args[0] === 'string') ? args[0] : '';
|
|
452
472
|
pinoLogger[level](first, ...args);
|
|
453
473
|
} else {
|
|
454
|
-
// Fallback: just pass through
|
|
455
474
|
pinoLogger[level](first, ...args);
|
|
456
475
|
}
|
|
476
|
+
|
|
477
|
+
// Dispatch to transports
|
|
478
|
+
if (self.transports && self.transports.length > 0) {
|
|
479
|
+
const entry = buildLogEntry(level, levelNums[level], pinoLogger._componentName, message, data);
|
|
480
|
+
dispatchToTransports(entry, self.transports);
|
|
481
|
+
}
|
|
457
482
|
};
|
|
458
483
|
});
|
|
459
484
|
|
|
@@ -739,6 +764,7 @@ class JSGLogger {
|
|
|
739
764
|
const formatter = createBrowserFormatter(componentName, this.logStore);
|
|
740
765
|
const levels = ['trace', 'debug', 'info', 'warn', 'error', 'fatal'];
|
|
741
766
|
const levelMap = {trace: 10, debug: 20, info: 30, warn: 40, error: 50, fatal: 60};
|
|
767
|
+
const self = this;
|
|
742
768
|
|
|
743
769
|
const logger = {};
|
|
744
770
|
|
|
@@ -751,7 +777,6 @@ class JSGLogger {
|
|
|
751
777
|
const minLevel = levelMap[effectiveLevel] || 30;
|
|
752
778
|
if (logLevel < minLevel) return;
|
|
753
779
|
|
|
754
|
-
// Create log data object
|
|
755
780
|
let logData = {
|
|
756
781
|
level: logLevel,
|
|
757
782
|
time: Date.now(),
|
|
@@ -759,24 +784,35 @@ class JSGLogger {
|
|
|
759
784
|
v: 1
|
|
760
785
|
};
|
|
761
786
|
|
|
787
|
+
let message = '';
|
|
788
|
+
let data;
|
|
789
|
+
|
|
762
790
|
// Handle different argument patterns
|
|
763
791
|
if (typeof first === 'string') {
|
|
792
|
+
message = first;
|
|
764
793
|
logData.msg = first;
|
|
765
|
-
// Add additional args as context
|
|
766
794
|
if (args.length === 1 && typeof args[0] === 'object') {
|
|
795
|
+
data = args[0];
|
|
767
796
|
Object.assign(logData, args[0]);
|
|
768
797
|
} else if (args.length > 0) {
|
|
769
798
|
logData.args = args;
|
|
770
799
|
}
|
|
771
800
|
} else if (typeof first === 'object') {
|
|
801
|
+
data = first;
|
|
772
802
|
Object.assign(logData, first);
|
|
773
803
|
if (args.length > 0 && typeof args[0] === 'string') {
|
|
804
|
+
message = args[0];
|
|
774
805
|
logData.msg = args[0];
|
|
775
806
|
}
|
|
776
807
|
}
|
|
777
808
|
|
|
778
|
-
// Use our beautiful formatter
|
|
779
809
|
formatter.write(JSON.stringify(logData));
|
|
810
|
+
|
|
811
|
+
// Dispatch to transports
|
|
812
|
+
if (self.transports && self.transports.length > 0) {
|
|
813
|
+
const entry = buildLogEntry(level, logLevel, componentName, message, data);
|
|
814
|
+
dispatchToTransports(entry, self.transports);
|
|
815
|
+
}
|
|
780
816
|
};
|
|
781
817
|
});
|
|
782
818
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crimsonsunset/jsg-logger",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Multi-environment logger with smart detection, file-level overrides, and beautiful console formatting. Test it live: https://logger.joesangiorgio.com/",
|
|
6
6
|
"main": "index.js",
|
package/utils/redaction.js
CHANGED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transport Dispatcher for JSG Logger
|
|
3
|
+
* Builds log entries and dispatches them to registered transports.
|
|
4
|
+
* Transports are consumer-provided objects implementing the LogTransport interface.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { metaError } from './meta-logger.js';
|
|
8
|
+
|
|
9
|
+
const LEVEL_NAMES = {
|
|
10
|
+
10: 'trace',
|
|
11
|
+
20: 'debug',
|
|
12
|
+
30: 'info',
|
|
13
|
+
40: 'warn',
|
|
14
|
+
50: 'error',
|
|
15
|
+
60: 'fatal',
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const LEVEL_NUMS = {
|
|
19
|
+
trace: 10,
|
|
20
|
+
debug: 20,
|
|
21
|
+
info: 30,
|
|
22
|
+
warn: 40,
|
|
23
|
+
error: 50,
|
|
24
|
+
fatal: 60,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Extract an Error instance from log data if one is present.
|
|
29
|
+
* Checks the value itself, then common keys: `err`, `error`.
|
|
30
|
+
* @param {*} data - The data argument passed to the log method
|
|
31
|
+
* @returns {Error|undefined}
|
|
32
|
+
*/
|
|
33
|
+
function extractError(data) {
|
|
34
|
+
if (data instanceof Error) return data;
|
|
35
|
+
if (data && typeof data === 'object') {
|
|
36
|
+
if (data.err instanceof Error) return data.err;
|
|
37
|
+
if (data.error instanceof Error) return data.error;
|
|
38
|
+
}
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Build a structured LogEntry from raw log call arguments.
|
|
44
|
+
* @param {string} level - Level name ('info', 'warn', 'error', etc.)
|
|
45
|
+
* @param {number} levelNum - Numeric level (10–60)
|
|
46
|
+
* @param {string} component - Component name
|
|
47
|
+
* @param {string} message - Log message
|
|
48
|
+
* @param {Record<string, unknown>} [data] - Optional context/data object
|
|
49
|
+
* @returns {Object} LogEntry
|
|
50
|
+
*/
|
|
51
|
+
export function buildLogEntry(level, levelNum, component, message, data) {
|
|
52
|
+
return {
|
|
53
|
+
level,
|
|
54
|
+
levelNum,
|
|
55
|
+
message: message ?? '',
|
|
56
|
+
component,
|
|
57
|
+
data,
|
|
58
|
+
timestamp: Date.now(),
|
|
59
|
+
isError: levelNum >= 50,
|
|
60
|
+
error: extractError(data),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Dispatch a LogEntry to all registered transports.
|
|
66
|
+
* Each transport is called independently — a failing transport never
|
|
67
|
+
* blocks or crashes the others (or the app).
|
|
68
|
+
* @param {Object} entry - LogEntry object
|
|
69
|
+
* @param {Array} transports - Array of LogTransport instances
|
|
70
|
+
*/
|
|
71
|
+
export function dispatchToTransports(entry, transports) {
|
|
72
|
+
if (!transports || transports.length === 0) return;
|
|
73
|
+
|
|
74
|
+
const entryLevelNum = entry.levelNum;
|
|
75
|
+
|
|
76
|
+
for (const transport of transports) {
|
|
77
|
+
try {
|
|
78
|
+
const minLevel = transport.level ? (LEVEL_NUMS[transport.level] ?? 0) : 0;
|
|
79
|
+
if (entryLevelNum < minLevel) continue;
|
|
80
|
+
|
|
81
|
+
transport.send(entry);
|
|
82
|
+
} catch (err) {
|
|
83
|
+
metaError('[JSG-LOGGER] Transport dispatch error:', err);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|