@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.
@@ -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; // Will be set after config loads
58
+ this.environment = null;
58
59
  this.initialized = false;
59
- this.components = {}; // Auto-discovery getters
60
- this.componentSubscribers = []; // Subscribers for component change events
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
- // Pattern: logger.info('message', {context})
442
- const message = first;
462
+ message = first;
443
463
  if (args.length === 1 && typeof args[0] === 'object' && args[0] !== null) {
444
- // Has context object
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
- // Pattern: logger.info({context}, 'message') - already pino format
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.7.15",
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",
@@ -49,3 +49,6 @@ export function redactValue(value, redactConfig) {
49
49
  return value;
50
50
  }
51
51
 
52
+
53
+
54
+
@@ -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
+ }