@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 +116 -0
- package/config/config-manager.js +34 -4
- 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.d.ts +48 -0
- package/index.js +43 -30
- package/package.json +1 -1
- package/utils/redaction.js +54 -0
- package/utils/transport-dispatcher.js +86 -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
|
@@ -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
|
-
|
|
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.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,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
|
-
|
|
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
|
-
|
|
448
|
-
const message = first;
|
|
445
|
+
message = first;
|
|
449
446
|
if (args.length === 1 && typeof args[0] === 'object' && args[0] !== null) {
|
|
450
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
+
}
|