@crimsonsunset/jsg-logger 1.7.14 → 1.7.15
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 +116 -0
- package/config/config-manager.js +26 -0
- package/config/default-config.json +4 -0
- package/examples/advanced-config.json +8 -0
- package/formatters/browser-formatter.js +5 -1
- package/formatters/cli-formatter.js +7 -1
- package/formatters/server-formatter.js +7 -3
- package/index.js +3 -9
- package/package.json +1 -1
- package/utils/redaction.js +51 -0
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
|
```
|
package/config/config-manager.js
CHANGED
|
@@ -533,6 +533,32 @@ export class ConfigManager {
|
|
|
533
533
|
return baseDisplay;
|
|
534
534
|
}
|
|
535
535
|
|
|
536
|
+
/**
|
|
537
|
+
* Get redact configuration with file override support
|
|
538
|
+
* @param {string} filePath - Optional file path for override checking
|
|
539
|
+
* @returns {Object} Redact configuration with paths array and censor string
|
|
540
|
+
*/
|
|
541
|
+
getRedactConfig(filePath = null) {
|
|
542
|
+
const baseRedact = this.config.redact || {
|
|
543
|
+
paths: [],
|
|
544
|
+
censor: '[REDACTED]'
|
|
545
|
+
};
|
|
546
|
+
|
|
547
|
+
// Check for file-specific redact overrides
|
|
548
|
+
const checkFile = filePath || this.currentFile;
|
|
549
|
+
if (checkFile) {
|
|
550
|
+
const fileOverride = this.getFileOverride(checkFile);
|
|
551
|
+
if (fileOverride && fileOverride.redact) {
|
|
552
|
+
return {
|
|
553
|
+
...baseRedact,
|
|
554
|
+
...fileOverride.redact
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
return baseRedact;
|
|
560
|
+
}
|
|
561
|
+
|
|
536
562
|
/**
|
|
537
563
|
* Get project name
|
|
538
564
|
* @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
|
-
|
|
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(
|
|
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.js
CHANGED
|
@@ -309,12 +309,6 @@ class JSGLogger {
|
|
|
309
309
|
|
|
310
310
|
// Create loggers for all available components using default config
|
|
311
311
|
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
312
|
|
|
319
313
|
components.forEach(componentName => {
|
|
320
314
|
// Use original createLogger to bypass utility method caching
|
|
@@ -341,7 +335,7 @@ class JSGLogger {
|
|
|
341
335
|
try {
|
|
342
336
|
callback(currentComponents);
|
|
343
337
|
} catch (error) {
|
|
344
|
-
|
|
338
|
+
metaError('Component subscriber error:', error);
|
|
345
339
|
}
|
|
346
340
|
});
|
|
347
341
|
}
|
|
@@ -566,7 +560,7 @@ class JSGLogger {
|
|
|
566
560
|
try {
|
|
567
561
|
callback(currentComponents);
|
|
568
562
|
} catch (error) {
|
|
569
|
-
|
|
563
|
+
this.loggers.core?.error('Component subscriber error (initial call):', error);
|
|
570
564
|
}
|
|
571
565
|
// Return unsubscribe function
|
|
572
566
|
return () => {
|
|
@@ -899,7 +893,7 @@ class JSGLogger {
|
|
|
899
893
|
try {
|
|
900
894
|
callback(currentComponents);
|
|
901
895
|
} catch (error) {
|
|
902
|
-
|
|
896
|
+
this.loggers.core?.error('Component subscriber error:', error);
|
|
903
897
|
}
|
|
904
898
|
});
|
|
905
899
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crimsonsunset/jsg-logger",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.15",
|
|
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,51 @@
|
|
|
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
|
+
|