@crimsonsunset/jsg-logger 1.7.14 → 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/README.md CHANGED
@@ -26,6 +26,7 @@ A sophisticated, fully generic logging system that automatically detects its env
26
26
  - 📁 **File-Level Overrides** - Per-file and pattern-based control
27
27
  - ⏰ **Timestamp Modes** - Absolute, readable, relative, or disabled
28
28
  - 🎛️ **Display Toggles** - Control every aspect of log output
29
+ - 🔒 **Automatic Redaction** - Protect sensitive data in logs (passwords, API keys, tokens)
29
30
  - 🎯 **Smart Level Resolution** - Hierarchical level determination
30
31
 
31
32
  ## 🚀 Quick Start
@@ -290,6 +291,121 @@ logger.controls.setDisplayOption('jsonPayload', false);
290
291
  logger.controls.toggleDisplayOption('level');
291
292
  ```
292
293
 
294
+ ## 🔒 **Redaction (Sensitive Data Protection)**
295
+
296
+ Automatically redact sensitive keys in logged objects to prevent accidental exposure of passwords, API keys, tokens, and other sensitive data.
297
+
298
+ ### **Basic Configuration**
299
+
300
+ ```javascript
301
+ const logger = JSGLogger.getInstanceSync({
302
+ redact: {
303
+ paths: ['password', 'token', '*key', '*secret'],
304
+ censor: '[REDACTED]'
305
+ }
306
+ });
307
+
308
+ logger.api.info('User login', {
309
+ username: 'john',
310
+ password: 'secret123', // → [REDACTED]
311
+ apiKey: 'abc123xyz', // → [REDACTED] (matches *key)
312
+ googleApiKey: 'key123', // → [REDACTED] (matches *key)
313
+ email: 'john@example.com' // → visible
314
+ });
315
+ ```
316
+
317
+ ### **Pattern Matching**
318
+
319
+ Redaction supports two pattern types:
320
+
321
+ - **Exact match**: `'password'`, `'token'` - matches keys exactly
322
+ - **Wildcard suffix**: `'*key'`, `'*secret'` - matches any key ending with the suffix (case-insensitive)
323
+
324
+ ```javascript
325
+ // Wildcard patterns match:
326
+ '*key' → apiKey, googleApiKey, secretKey, publicKey, API_KEY, api_key
327
+ '*secret' → secret, secretKey, mySecret, SECRET
328
+ '*apiKey' → apiKey, googleApiKey, customApiKey
329
+ '*api_key' → api_key, GOOGLE_API_KEY
330
+ ```
331
+
332
+ ### **Nested Objects & Arrays**
333
+
334
+ Redaction works recursively through nested objects and arrays:
335
+
336
+ ```javascript
337
+ logger.info('Config loaded', {
338
+ user: {
339
+ name: 'John',
340
+ password: 'secret', // → [REDACTED]
341
+ apiKey: 'key123' // → [REDACTED]
342
+ },
343
+ services: [
344
+ { name: 'API', token: 'abc' }, // → token: [REDACTED]
345
+ { name: 'DB', token: 'xyz' } // → token: [REDACTED]
346
+ ]
347
+ });
348
+ ```
349
+
350
+ ### **Custom Censor Text**
351
+
352
+ ```javascript
353
+ const logger = JSGLogger.getInstanceSync({
354
+ redact: {
355
+ paths: ['password'],
356
+ censor: '***HIDDEN***' // Custom replacement text
357
+ }
358
+ });
359
+ ```
360
+
361
+ ### **File-Specific Redaction**
362
+
363
+ Override redaction per file or pattern:
364
+
365
+ ```json
366
+ {
367
+ "redact": {
368
+ "paths": ["password", "*key"],
369
+ "censor": "[REDACTED]"
370
+ },
371
+ "fileOverrides": {
372
+ "src/auth/*.js": {
373
+ "redact": {
374
+ "paths": ["password", "token", "*key", "*secret"],
375
+ "censor": "***"
376
+ }
377
+ }
378
+ }
379
+ }
380
+ ```
381
+
382
+ ### **Default Configuration**
383
+
384
+ Default redaction patterns (if not configured):
385
+ - `password`
386
+ - `token`
387
+ - `*key` (matches any key ending in "key")
388
+ - `*secret` (matches any key ending in "secret")
389
+ - `*apiKey`
390
+ - `*api_key`
391
+
392
+ Default censor: `[REDACTED]`
393
+
394
+ ### **Disable Redaction**
395
+
396
+ Set empty paths array to disable:
397
+
398
+ ```javascript
399
+ const logger = JSGLogger.getInstanceSync({
400
+ redact: {
401
+ paths: [], // No redaction
402
+ censor: '[REDACTED]'
403
+ }
404
+ });
405
+ ```
406
+
407
+ **Note**: Redaction applies to all formatters (browser, CLI, and server) automatically.
408
+
293
409
  ## 🏗️ Architecture
294
410
 
295
411
  ```
@@ -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 {
@@ -533,6 +537,32 @@ export class ConfigManager {
533
537
  return baseDisplay;
534
538
  }
535
539
 
540
+ /**
541
+ * Get redact configuration with file override support
542
+ * @param {string} filePath - Optional file path for override checking
543
+ * @returns {Object} Redact configuration with paths array and censor string
544
+ */
545
+ getRedactConfig(filePath = null) {
546
+ const baseRedact = this.config.redact || {
547
+ paths: [],
548
+ censor: '[REDACTED]'
549
+ };
550
+
551
+ // Check for file-specific redact overrides
552
+ const checkFile = filePath || this.currentFile;
553
+ if (checkFile) {
554
+ const fileOverride = this.getFileOverride(checkFile);
555
+ if (fileOverride && fileOverride.redact) {
556
+ return {
557
+ ...baseRedact,
558
+ ...fileOverride.redact
559
+ };
560
+ }
561
+ }
562
+
563
+ return baseRedact;
564
+ }
565
+
536
566
  /**
537
567
  * Get project name
538
568
  * @returns {string} Project name
@@ -18,6 +18,10 @@
18
18
  "jsonPayload": true,
19
19
  "stackTrace": true
20
20
  },
21
+ "redact": {
22
+ "paths": ["password", "token", "*key", "*secret", "*apiKey", "*api_key"],
23
+ "censor": "[REDACTED]"
24
+ },
21
25
  "levels": {
22
26
  "10": { "name": "TRACE", "emoji": "🔍", "color": "#6C7B7F" },
23
27
  "20": { "name": "DEBUG", "emoji": "🐛", "color": "#74B9FF" },
@@ -16,6 +16,10 @@
16
16
  "jsonPayload": true,
17
17
  "stackTrace": true
18
18
  },
19
+ "redact": {
20
+ "paths": ["password", "token", "*key", "*secret", "*apiKey", "*api_key"],
21
+ "censor": "[REDACTED]"
22
+ },
19
23
  "components": {
20
24
  "core": {
21
25
  "emoji": "🎯",
@@ -116,6 +120,10 @@
116
120
  "display": {
117
121
  "jsonPayload": true,
118
122
  "stackTrace": true
123
+ },
124
+ "redact": {
125
+ "paths": ["password", "token", "*key", "*secret"],
126
+ "censor": "***"
119
127
  }
120
128
  },
121
129
  "src/database/*.js": {
@@ -5,6 +5,7 @@
5
5
  */
6
6
 
7
7
  import {configManager} from '../config/config-manager.js';
8
+ import {redactValue} from '../utils/redaction.js';
8
9
 
9
10
  /**
10
11
  * Create browser console formatter for a specific component
@@ -28,6 +29,7 @@ export const createBrowserFormatter = (componentName, logStore = null) => {
28
29
  const component = configManager.getComponentConfig(componentName, filePath);
29
30
  const level = configManager.getLevelConfig(logData.level);
30
31
  const displayConfig = configManager.getDisplayConfig(filePath);
32
+ const redactConfig = configManager.getRedactConfig(filePath);
31
33
 
32
34
  // Check if this log should be displayed based on effective level
33
35
  const effectiveLevel = configManager.getEffectiveLevel(componentName, filePath);
@@ -84,7 +86,8 @@ export const createBrowserFormatter = (componentName, logStore = null) => {
84
86
  if (displayConfig.jsonPayload) {
85
87
  const contextData = extractContextData(logData);
86
88
  if (Object.keys(contextData).length > 0) {
87
- displayContextData(contextData);
89
+ const redactedData = redactValue(contextData, redactConfig);
90
+ displayContextData(redactedData);
88
91
  }
89
92
  }
90
93
 
@@ -158,6 +161,7 @@ function extractContextData(logData) {
158
161
  return contextData;
159
162
  }
160
163
 
164
+
161
165
  /**
162
166
  * Display context data with tree-like structure
163
167
  * @param {Object} contextData - Context data to display
@@ -5,6 +5,7 @@
5
5
 
6
6
  import { LEVEL_SCHEME } from '../config/component-schemes.js';
7
7
  import { configManager } from '../config/config-manager.js';
8
+ import { redactValue } from '../utils/redaction.js';
8
9
 
9
10
  /**
10
11
  * Format a value for display in context tree
@@ -27,6 +28,7 @@ const formatValue = (value) => {
27
28
  return String(value);
28
29
  };
29
30
 
31
+
30
32
  /**
31
33
  * Create CLI formatter with context data support
32
34
  * @returns {Object} Stream-like object for Pino
@@ -62,10 +64,14 @@ export const createCLIFormatter = () => {
62
64
  const contextKeys = Object.keys(log).filter(key => !internalFields.includes(key));
63
65
 
64
66
  if (contextKeys.length > 0) {
67
+ // Get redact config and apply redaction
68
+ const redactConfig = configManager.getRedactConfig();
69
+ const redactedLog = redactValue(log, redactConfig);
70
+
65
71
  contextKeys.forEach((key, index) => {
66
72
  const isLast = index === contextKeys.length - 1;
67
73
  const prefix = isLast ? ' └─' : ' ├─';
68
- const value = formatValue(log[key]);
74
+ const value = formatValue(redactedLog[key]);
69
75
  console.log(`${prefix} ${key}: ${value}`);
70
76
  });
71
77
  }
@@ -3,6 +3,8 @@
3
3
  * Structured JSON output for production logging and log aggregation
4
4
  */
5
5
 
6
+ import {configManager} from '../config/config-manager.js';
7
+
6
8
  /**
7
9
  * Create server formatter (structured JSON)
8
10
  * @returns {null} No custom formatter - uses Pino's default JSON output
@@ -18,6 +20,8 @@ export const createServerFormatter = () => {
18
20
  * @returns {Object} Pino configuration for server environments
19
21
  */
20
22
  export const getServerConfig = () => {
23
+ const redactConfig = configManager.getRedactConfig();
24
+
21
25
  return {
22
26
  level: 'info', // More conservative logging in production
23
27
  formatters: {
@@ -33,10 +37,10 @@ export const getServerConfig = () => {
33
37
  };
34
38
  }
35
39
  },
36
- // Redact sensitive information in production
40
+ // Redact sensitive information in production (configurable)
37
41
  redact: {
38
- paths: ['password', 'token', 'key', 'secret'],
39
- censor: '[REDACTED]'
42
+ paths: redactConfig.paths.length > 0 ? redactConfig.paths : ['password', 'token', 'key', 'secret'],
43
+ censor: redactConfig.censor || '[REDACTED]'
40
44
  }
41
45
  };
42
46
  };
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,27 +303,20 @@ 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 = {};
309
312
 
310
313
  // Create loggers for all available components using default config
311
314
  const components = configManager.getAvailableComponents();
312
-
313
- // Debug: Log components being created during initialization
314
- // Note: Using console.log because this.loggers is empty at this point
315
- if (components.length > 0) {
316
- console.log(`[JSG-LOGGER] Creating ${components.length} loggers during initSync:`, components);
317
- }
318
315
 
319
316
  components.forEach(componentName => {
320
- // Use original createLogger to bypass utility method caching
321
317
  const createFn = this._createLoggerOriginal || this.createLogger.bind(this);
322
318
  this.loggers[componentName] = createFn(componentName);
323
319
  });
324
-
325
- // Create legacy compatibility aliases
326
- // Removed: camelCase aliases no longer added to this.loggers
327
320
  // Use logger.components.camelCase() or logger.getComponent('kebab-case') instead
328
321
  // this.createAliases();
329
322
 
@@ -341,7 +334,7 @@ class JSGLogger {
341
334
  try {
342
335
  callback(currentComponents);
343
336
  } catch (error) {
344
- console.error('Component subscriber error:', error);
337
+ metaError('Component subscriber error:', error);
345
338
  }
346
339
  });
347
340
  }
@@ -438,28 +431,37 @@ class JSGLogger {
438
431
  */
439
432
  _wrapPinoLogger(pinoLogger) {
440
433
  const levels = ['trace', 'debug', 'info', 'warn', 'error', 'fatal'];
434
+ const levelNums = {trace: 10, debug: 20, info: 30, warn: 40, error: 50, fatal: 60};
441
435
  const wrapped = {};
436
+ const self = this;
442
437
 
443
438
  levels.forEach(level => {
444
439
  wrapped[level] = (first, ...args) => {
440
+ let message = '';
441
+ let data;
442
+
445
443
  // Handle different argument patterns
446
444
  if (typeof first === 'string') {
447
- // Pattern: logger.info('message', {context})
448
- const message = first;
445
+ message = first;
449
446
  if (args.length === 1 && typeof args[0] === 'object' && args[0] !== null) {
450
- // Has context object
447
+ data = args[0];
451
448
  pinoLogger[level](args[0], message);
452
449
  } else {
453
- // Just message
454
450
  pinoLogger[level](message);
455
451
  }
456
452
  } else if (typeof first === 'object' && first !== null) {
457
- // Pattern: logger.info({context}, 'message') - already pino format
453
+ data = first;
454
+ message = (args.length > 0 && typeof args[0] === 'string') ? args[0] : '';
458
455
  pinoLogger[level](first, ...args);
459
456
  } else {
460
- // Fallback: just pass through
461
457
  pinoLogger[level](first, ...args);
462
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
+ }
463
465
  };
464
466
  });
465
467
 
@@ -566,7 +568,7 @@ class JSGLogger {
566
568
  try {
567
569
  callback(currentComponents);
568
570
  } catch (error) {
569
- console.error('Component subscriber error (initial call):', error);
571
+ this.loggers.core?.error('Component subscriber error (initial call):', error);
570
572
  }
571
573
  // Return unsubscribe function
572
574
  return () => {
@@ -745,6 +747,7 @@ class JSGLogger {
745
747
  const formatter = createBrowserFormatter(componentName, this.logStore);
746
748
  const levels = ['trace', 'debug', 'info', 'warn', 'error', 'fatal'];
747
749
  const levelMap = {trace: 10, debug: 20, info: 30, warn: 40, error: 50, fatal: 60};
750
+ const self = this;
748
751
 
749
752
  const logger = {};
750
753
 
@@ -757,7 +760,6 @@ class JSGLogger {
757
760
  const minLevel = levelMap[effectiveLevel] || 30;
758
761
  if (logLevel < minLevel) return;
759
762
 
760
- // Create log data object
761
763
  let logData = {
762
764
  level: logLevel,
763
765
  time: Date.now(),
@@ -765,24 +767,35 @@ class JSGLogger {
765
767
  v: 1
766
768
  };
767
769
 
770
+ let message = '';
771
+ let data;
772
+
768
773
  // Handle different argument patterns
769
774
  if (typeof first === 'string') {
775
+ message = first;
770
776
  logData.msg = first;
771
- // Add additional args as context
772
777
  if (args.length === 1 && typeof args[0] === 'object') {
778
+ data = args[0];
773
779
  Object.assign(logData, args[0]);
774
780
  } else if (args.length > 0) {
775
781
  logData.args = args;
776
782
  }
777
783
  } else if (typeof first === 'object') {
784
+ data = first;
778
785
  Object.assign(logData, first);
779
786
  if (args.length > 0 && typeof args[0] === 'string') {
787
+ message = args[0];
780
788
  logData.msg = args[0];
781
789
  }
782
790
  }
783
791
 
784
- // Use our beautiful formatter
785
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
+ }
786
799
  };
787
800
  });
788
801
 
@@ -899,7 +912,7 @@ class JSGLogger {
899
912
  try {
900
913
  callback(currentComponents);
901
914
  } catch (error) {
902
- console.error('Component subscriber error:', error);
915
+ this.loggers.core?.error('Component subscriber error:', error);
903
916
  }
904
917
  });
905
918
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crimsonsunset/jsg-logger",
3
- "version": "1.7.14",
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",
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Redaction utility functions for sensitive data protection
3
+ * Used by browser, CLI, and server formatters
4
+ */
5
+
6
+ /**
7
+ * Check if a key should be redacted based on patterns
8
+ * @param {string} key - Key to check
9
+ * @param {string[]} paths - Array of patterns (exact match or wildcard like *key)
10
+ * @returns {boolean} Whether key should be redacted
11
+ */
12
+ export function shouldRedactKey(key, paths) {
13
+ return paths.some(pattern => {
14
+ if (pattern.startsWith('*')) {
15
+ const suffix = pattern.slice(1).toLowerCase();
16
+ return key.toLowerCase().endsWith(suffix);
17
+ }
18
+ return key.toLowerCase() === pattern.toLowerCase();
19
+ });
20
+ }
21
+
22
+ /**
23
+ * Redact sensitive values from an object based on key patterns
24
+ * @param {*} value - Value to redact (object, array, or primitive)
25
+ * @param {Object} redactConfig - Redaction configuration with paths and censor
26
+ * @returns {*} Redacted value
27
+ */
28
+ export function redactValue(value, redactConfig) {
29
+ if (!redactConfig || !redactConfig.paths || redactConfig.paths.length === 0) {
30
+ return value;
31
+ }
32
+
33
+ if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
34
+ const redacted = {};
35
+ for (const [key, val] of Object.entries(value)) {
36
+ if (shouldRedactKey(key, redactConfig.paths)) {
37
+ redacted[key] = redactConfig.censor || '[REDACTED]';
38
+ } else {
39
+ redacted[key] = redactValue(val, redactConfig);
40
+ }
41
+ }
42
+ return redacted;
43
+ }
44
+
45
+ if (Array.isArray(value)) {
46
+ return value.map(item => redactValue(item, redactConfig));
47
+ }
48
+
49
+ return value;
50
+ }
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
+ }