@crimsonsunset/jsg-logger 1.7.15 → 1.8.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/config/config-manager.js +8 -4
- package/index.d.ts +48 -0
- package/index.js +40 -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
|
/**
|
|
@@ -192,6 +194,9 @@ class JSGLogger {
|
|
|
192
194
|
// NOW determine environment (after config is loaded and forceEnvironment is applied)
|
|
193
195
|
this.environment = getEnvironment();
|
|
194
196
|
|
|
197
|
+
// Pick up transports from config (live objects — not deep-merged)
|
|
198
|
+
this.transports = configManager.config.transports ?? [];
|
|
199
|
+
|
|
195
200
|
// Create loggers for all available components
|
|
196
201
|
const components = configManager.getAvailableComponents();
|
|
197
202
|
|
|
@@ -199,11 +204,6 @@ class JSGLogger {
|
|
|
199
204
|
this.loggers[componentName] = this.createLogger(componentName);
|
|
200
205
|
});
|
|
201
206
|
|
|
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
207
|
// Add utility methods
|
|
208
208
|
this.addUtilityMethods();
|
|
209
209
|
|
|
@@ -303,6 +303,9 @@ class JSGLogger {
|
|
|
303
303
|
// NOW determine environment (after config is loaded and forceEnvironment is applied)
|
|
304
304
|
this.environment = getEnvironment();
|
|
305
305
|
|
|
306
|
+
// Pick up transports from config (live objects — not deep-merged)
|
|
307
|
+
this.transports = configManager.config.transports ?? [];
|
|
308
|
+
|
|
306
309
|
// Clear existing loggers for clean reinitialization
|
|
307
310
|
this.loggers = {};
|
|
308
311
|
this.components = {};
|
|
@@ -311,13 +314,9 @@ class JSGLogger {
|
|
|
311
314
|
const components = configManager.getAvailableComponents();
|
|
312
315
|
|
|
313
316
|
components.forEach(componentName => {
|
|
314
|
-
// Use original createLogger to bypass utility method caching
|
|
315
317
|
const createFn = this._createLoggerOriginal || this.createLogger.bind(this);
|
|
316
318
|
this.loggers[componentName] = createFn(componentName);
|
|
317
319
|
});
|
|
318
|
-
|
|
319
|
-
// Create legacy compatibility aliases
|
|
320
|
-
// Removed: camelCase aliases no longer added to this.loggers
|
|
321
320
|
// Use logger.components.camelCase() or logger.getComponent('kebab-case') instead
|
|
322
321
|
// this.createAliases();
|
|
323
322
|
|
|
@@ -432,28 +431,37 @@ class JSGLogger {
|
|
|
432
431
|
*/
|
|
433
432
|
_wrapPinoLogger(pinoLogger) {
|
|
434
433
|
const levels = ['trace', 'debug', 'info', 'warn', 'error', 'fatal'];
|
|
434
|
+
const levelNums = {trace: 10, debug: 20, info: 30, warn: 40, error: 50, fatal: 60};
|
|
435
435
|
const wrapped = {};
|
|
436
|
+
const self = this;
|
|
436
437
|
|
|
437
438
|
levels.forEach(level => {
|
|
438
439
|
wrapped[level] = (first, ...args) => {
|
|
440
|
+
let message = '';
|
|
441
|
+
let data;
|
|
442
|
+
|
|
439
443
|
// Handle different argument patterns
|
|
440
444
|
if (typeof first === 'string') {
|
|
441
|
-
|
|
442
|
-
const message = first;
|
|
445
|
+
message = first;
|
|
443
446
|
if (args.length === 1 && typeof args[0] === 'object' && args[0] !== null) {
|
|
444
|
-
|
|
447
|
+
data = args[0];
|
|
445
448
|
pinoLogger[level](args[0], message);
|
|
446
449
|
} else {
|
|
447
|
-
// Just message
|
|
448
450
|
pinoLogger[level](message);
|
|
449
451
|
}
|
|
450
452
|
} else if (typeof first === 'object' && first !== null) {
|
|
451
|
-
|
|
453
|
+
data = first;
|
|
454
|
+
message = (args.length > 0 && typeof args[0] === 'string') ? args[0] : '';
|
|
452
455
|
pinoLogger[level](first, ...args);
|
|
453
456
|
} else {
|
|
454
|
-
// Fallback: just pass through
|
|
455
457
|
pinoLogger[level](first, ...args);
|
|
456
458
|
}
|
|
459
|
+
|
|
460
|
+
// Dispatch to transports
|
|
461
|
+
if (self.transports && self.transports.length > 0) {
|
|
462
|
+
const entry = buildLogEntry(level, levelNums[level], pinoLogger._componentName, message, data);
|
|
463
|
+
dispatchToTransports(entry, self.transports);
|
|
464
|
+
}
|
|
457
465
|
};
|
|
458
466
|
});
|
|
459
467
|
|
|
@@ -739,6 +747,7 @@ class JSGLogger {
|
|
|
739
747
|
const formatter = createBrowserFormatter(componentName, this.logStore);
|
|
740
748
|
const levels = ['trace', 'debug', 'info', 'warn', 'error', 'fatal'];
|
|
741
749
|
const levelMap = {trace: 10, debug: 20, info: 30, warn: 40, error: 50, fatal: 60};
|
|
750
|
+
const self = this;
|
|
742
751
|
|
|
743
752
|
const logger = {};
|
|
744
753
|
|
|
@@ -751,7 +760,6 @@ class JSGLogger {
|
|
|
751
760
|
const minLevel = levelMap[effectiveLevel] || 30;
|
|
752
761
|
if (logLevel < minLevel) return;
|
|
753
762
|
|
|
754
|
-
// Create log data object
|
|
755
763
|
let logData = {
|
|
756
764
|
level: logLevel,
|
|
757
765
|
time: Date.now(),
|
|
@@ -759,24 +767,35 @@ class JSGLogger {
|
|
|
759
767
|
v: 1
|
|
760
768
|
};
|
|
761
769
|
|
|
770
|
+
let message = '';
|
|
771
|
+
let data;
|
|
772
|
+
|
|
762
773
|
// Handle different argument patterns
|
|
763
774
|
if (typeof first === 'string') {
|
|
775
|
+
message = first;
|
|
764
776
|
logData.msg = first;
|
|
765
|
-
// Add additional args as context
|
|
766
777
|
if (args.length === 1 && typeof args[0] === 'object') {
|
|
778
|
+
data = args[0];
|
|
767
779
|
Object.assign(logData, args[0]);
|
|
768
780
|
} else if (args.length > 0) {
|
|
769
781
|
logData.args = args;
|
|
770
782
|
}
|
|
771
783
|
} else if (typeof first === 'object') {
|
|
784
|
+
data = first;
|
|
772
785
|
Object.assign(logData, first);
|
|
773
786
|
if (args.length > 0 && typeof args[0] === 'string') {
|
|
787
|
+
message = args[0];
|
|
774
788
|
logData.msg = args[0];
|
|
775
789
|
}
|
|
776
790
|
}
|
|
777
791
|
|
|
778
|
-
// Use our beautiful formatter
|
|
779
792
|
formatter.write(JSON.stringify(logData));
|
|
793
|
+
|
|
794
|
+
// Dispatch to transports
|
|
795
|
+
if (self.transports && self.transports.length > 0) {
|
|
796
|
+
const entry = buildLogEntry(level, logLevel, componentName, message, data);
|
|
797
|
+
dispatchToTransports(entry, self.transports);
|
|
798
|
+
}
|
|
780
799
|
};
|
|
781
800
|
});
|
|
782
801
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crimsonsunset/jsg-logger",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.0",
|
|
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
|
+
}
|