@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.
@@ -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
  /**
@@ -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
- // Pattern: logger.info('message', {context})
442
- const message = first;
445
+ message = first;
443
446
  if (args.length === 1 && typeof args[0] === 'object' && args[0] !== null) {
444
- // Has context object
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
- // Pattern: logger.info({context}, 'message') - already pino format
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.7.15",
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",
@@ -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
+ }