@crimsonsunset/jsg-logger 1.0.8
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/CHANGELOG.md +55 -0
- package/CONTRIBUTING.md +58 -0
- package/LICENSE +15 -0
- package/README.md +428 -0
- package/config/component-schemes.js +26 -0
- package/config/config-manager.js +378 -0
- package/config/default-config.json +92 -0
- package/docs/next-session.md +136 -0
- package/docs/roadmap.md +404 -0
- package/examples/advanced-config.json +151 -0
- package/formatters/browser-formatter.js +227 -0
- package/formatters/cli-formatter.js +43 -0
- package/formatters/server-formatter.js +42 -0
- package/index.js +413 -0
- package/package.json +74 -0
- package/stores/log-store.js +148 -0
- package/utils/environment-detector.js +38 -0
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser Console Formatter for CACP Logger
|
|
3
|
+
* Beautiful console output with emoji, colors, and context trees
|
|
4
|
+
* Supports file overrides, display toggles, and smart configuration
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {configManager} from '../config/config-manager.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Create browser console formatter for a specific component
|
|
11
|
+
* @param {string} componentName - Component identifier
|
|
12
|
+
* @param {Object} logStore - Optional log store for capturing logs
|
|
13
|
+
* @returns {Object} Stream-like object for Pino
|
|
14
|
+
*/
|
|
15
|
+
export const createBrowserFormatter = (componentName, logStore = null) => {
|
|
16
|
+
return {
|
|
17
|
+
write: (data) => {
|
|
18
|
+
try {
|
|
19
|
+
const logData = typeof data === 'string' ? JSON.parse(data) : data;
|
|
20
|
+
|
|
21
|
+
// Get file context from stack trace if available
|
|
22
|
+
const filePath = getFilePathFromStack();
|
|
23
|
+
if (filePath) {
|
|
24
|
+
configManager.setCurrentFile(filePath);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Get component config with file override support
|
|
28
|
+
const component = configManager.getComponentConfig(componentName, filePath);
|
|
29
|
+
const level = configManager.getLevelConfig(logData.level);
|
|
30
|
+
const displayConfig = configManager.getDisplayConfig(filePath);
|
|
31
|
+
|
|
32
|
+
// Check if this log should be displayed based on effective level
|
|
33
|
+
const effectiveLevel = configManager.getEffectiveLevel(componentName, filePath);
|
|
34
|
+
if (!shouldDisplay(logData.level, effectiveLevel)) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Build the log parts
|
|
39
|
+
const logParts = [];
|
|
40
|
+
const logStyles = [];
|
|
41
|
+
|
|
42
|
+
// 1. Timestamp
|
|
43
|
+
if (displayConfig.timestamp) {
|
|
44
|
+
const timestamp = configManager.formatTimestamp(logData.time);
|
|
45
|
+
if (timestamp) {
|
|
46
|
+
logParts.push(`%c${timestamp}`);
|
|
47
|
+
logStyles.push('color: #888; font-family: monospace;');
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// 2. Level emoji
|
|
52
|
+
if (displayConfig.emoji) {
|
|
53
|
+
const emoji = component.emoji || level.emoji;
|
|
54
|
+
logParts.push(`%c${emoji}`);
|
|
55
|
+
logStyles.push(`color: ${level.color}; font-size: 14px;`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// 3. Component name
|
|
59
|
+
if (displayConfig.component) {
|
|
60
|
+
const componentName = component.name.toUpperCase().replace(/([a-z])([A-Z])/g, '$1-$2');
|
|
61
|
+
logParts.push(`%c[${componentName}]`);
|
|
62
|
+
logStyles.push(`color: ${component.color}; font-weight: bold;`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// 4. Level name (optional)
|
|
66
|
+
if (displayConfig.level) {
|
|
67
|
+
logParts.push(`%c${level.name}`);
|
|
68
|
+
logStyles.push(`color: ${level.color}; font-weight: normal;`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// 5. Message
|
|
72
|
+
if (displayConfig.message) {
|
|
73
|
+
const message = logData.msg || '';
|
|
74
|
+
logParts.push(`%c${message}`);
|
|
75
|
+
logStyles.push('color: inherit;');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Combine and log the main message
|
|
79
|
+
if (logParts.length > 0) {
|
|
80
|
+
console.log(logParts.join(' '), ...logStyles);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// 6. JSON payload context (if enabled)
|
|
84
|
+
if (displayConfig.jsonPayload) {
|
|
85
|
+
const contextData = extractContextData(logData);
|
|
86
|
+
if (Object.keys(contextData).length > 0) {
|
|
87
|
+
displayContextData(contextData);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// 7. Stack trace (if enabled and present)
|
|
92
|
+
if (displayConfig.stackTrace && logData.err && logData.err.stack) {
|
|
93
|
+
console.error(' ╰─ Error Stack:');
|
|
94
|
+
console.error(logData.err.stack);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Store log if logStore provided
|
|
98
|
+
if (logStore) {
|
|
99
|
+
logStore.add({
|
|
100
|
+
...logData,
|
|
101
|
+
filePath,
|
|
102
|
+
effectiveLevel,
|
|
103
|
+
component: componentName,
|
|
104
|
+
displayConfig
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
} catch (error) {
|
|
109
|
+
// Fallback for malformed data
|
|
110
|
+
console.log(data);
|
|
111
|
+
if (logStore) {
|
|
112
|
+
logStore.add({
|
|
113
|
+
level: 50,
|
|
114
|
+
time: Date.now(),
|
|
115
|
+
msg: 'Logger formatting error',
|
|
116
|
+
error: error.message,
|
|
117
|
+
data
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Check if a log should be displayed based on level hierarchy
|
|
127
|
+
* @param {number} logLevel - Log level number
|
|
128
|
+
* @param {string} effectiveLevel - Effective level string
|
|
129
|
+
* @returns {boolean} Whether log should be displayed
|
|
130
|
+
* @private
|
|
131
|
+
*/
|
|
132
|
+
function shouldDisplay(logLevel, effectiveLevel) {
|
|
133
|
+
const levelMap = {
|
|
134
|
+
'trace': 10, 'debug': 20, 'info': 30,
|
|
135
|
+
'warn': 40, 'error': 50, 'fatal': 60
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const minLevel = levelMap[effectiveLevel] || 30;
|
|
139
|
+
return logLevel >= minLevel;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Extract context data from log data (excluding standard fields)
|
|
144
|
+
* @param {Object} logData - Log data object
|
|
145
|
+
* @returns {Object} Context data
|
|
146
|
+
* @private
|
|
147
|
+
*/
|
|
148
|
+
function extractContextData(logData) {
|
|
149
|
+
const excludeKeys = new Set(['level', 'time', 'msg', 'name', 'pid', 'hostname', 'v']);
|
|
150
|
+
const contextData = {};
|
|
151
|
+
|
|
152
|
+
Object.keys(logData).forEach(key => {
|
|
153
|
+
if (!excludeKeys.has(key)) {
|
|
154
|
+
contextData[key] = logData[key];
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
return contextData;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Display context data with tree-like structure
|
|
163
|
+
* @param {Object} contextData - Context data to display
|
|
164
|
+
* @private
|
|
165
|
+
*/
|
|
166
|
+
function displayContextData(contextData) {
|
|
167
|
+
Object.entries(contextData).forEach(([key, value]) => {
|
|
168
|
+
if (typeof value === 'object' && value !== null) {
|
|
169
|
+
console.log(` ├─ %c${key}:`, 'color: #00C896; font-weight: bold;', value);
|
|
170
|
+
} else {
|
|
171
|
+
console.log(` ├─ %c${key}: %c${value}`, 'color: #00C896; font-weight: bold;', 'color: inherit;');
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Extract file path from error stack trace
|
|
178
|
+
* @returns {string|null} File path or null
|
|
179
|
+
* @private
|
|
180
|
+
*/
|
|
181
|
+
function getFilePathFromStack() {
|
|
182
|
+
try {
|
|
183
|
+
const stack = new Error().stack;
|
|
184
|
+
if (!stack) return null;
|
|
185
|
+
|
|
186
|
+
const lines = stack.split('\n');
|
|
187
|
+
|
|
188
|
+
// Look for the first line that doesn't contain logger internals
|
|
189
|
+
for (let i = 1; i < lines.length; i++) {
|
|
190
|
+
const line = lines[i];
|
|
191
|
+
|
|
192
|
+
// Skip logger internal files
|
|
193
|
+
if (line.includes('logger') || line.includes('pino') || line.includes('browser-formatter')) {
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Extract file path from stack trace
|
|
198
|
+
const match = line.match(/at .+?\((.+?):\d+:\d+\)/);
|
|
199
|
+
if (match) {
|
|
200
|
+
let filePath = match[1];
|
|
201
|
+
|
|
202
|
+
// Clean up the file path
|
|
203
|
+
if (filePath.startsWith('file://')) {
|
|
204
|
+
filePath = filePath.replace('file://', '');
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Extract relative path from absolute path
|
|
208
|
+
const srcIndex = filePath.lastIndexOf('/src/');
|
|
209
|
+
if (srcIndex !== -1) {
|
|
210
|
+
return 'src/' + filePath.substring(srcIndex + 5);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Return the last part of the path
|
|
214
|
+
const parts = filePath.split('/');
|
|
215
|
+
if (parts.length >= 2) {
|
|
216
|
+
return parts.slice(-2).join('/');
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return parts[parts.length - 1];
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return null;
|
|
224
|
+
} catch (error) {
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI/Terminal Formatter for CACP Logger
|
|
3
|
+
* Uses pino-colada for beautiful terminal output with fallbacks
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { COMPONENT_SCHEME, LEVEL_SCHEME } from '../config/component-schemes.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Create CLI formatter using pino-colada or pino-pretty
|
|
10
|
+
* @returns {Object} Stream-like object for Pino
|
|
11
|
+
*/
|
|
12
|
+
export const createCLIFormatter = () => {
|
|
13
|
+
try {
|
|
14
|
+
// Try to use pino-colada if available
|
|
15
|
+
const pinoColada = require('pino-colada');
|
|
16
|
+
return pinoColada();
|
|
17
|
+
} catch (error) {
|
|
18
|
+
// Fallback to pino-pretty if pino-colada not available
|
|
19
|
+
try {
|
|
20
|
+
const pinoPretty = require('pino-pretty');
|
|
21
|
+
return pinoPretty({
|
|
22
|
+
colorize: true,
|
|
23
|
+
translateTime: 'HH:MM:ss.l',
|
|
24
|
+
messageFormat: (log, messageKey, levelLabel) => {
|
|
25
|
+
const component = COMPONENT_SCHEME[log.name] || COMPONENT_SCHEME['cacp'];
|
|
26
|
+
const componentName = component.name.toUpperCase().replace(/([a-z])([A-Z])/g, '$1-$2');
|
|
27
|
+
const level = LEVEL_SCHEME[log.level] || LEVEL_SCHEME[30];
|
|
28
|
+
return `${level.emoji} [${componentName}] ${log[messageKey]}`;
|
|
29
|
+
},
|
|
30
|
+
customPrettifiers: {
|
|
31
|
+
level: () => '', // Hide level since we show it in messageFormat
|
|
32
|
+
time: (timestamp) => timestamp,
|
|
33
|
+
name: () => '' // Hide name since we show it in messageFormat
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
} catch (error) {
|
|
37
|
+
// Ultimate fallback - basic JSON
|
|
38
|
+
return {
|
|
39
|
+
write: (data) => console.log(data)
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server/Production Formatter for CACP Logger
|
|
3
|
+
* Structured JSON output for production logging and log aggregation
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Create server formatter (structured JSON)
|
|
8
|
+
* @returns {null} No custom formatter - uses Pino's default JSON output
|
|
9
|
+
*/
|
|
10
|
+
export const createServerFormatter = () => {
|
|
11
|
+
// Server environments use default Pino JSON output
|
|
12
|
+
// This provides structured logs suitable for log aggregation systems
|
|
13
|
+
return null;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Server-specific configuration
|
|
18
|
+
* @returns {Object} Pino configuration for server environments
|
|
19
|
+
*/
|
|
20
|
+
export const getServerConfig = () => {
|
|
21
|
+
return {
|
|
22
|
+
level: 'info', // More conservative logging in production
|
|
23
|
+
formatters: {
|
|
24
|
+
// Ensure consistent timestamp format
|
|
25
|
+
time: () => `,"time":"${new Date().toISOString()}"`,
|
|
26
|
+
|
|
27
|
+
// Add environment context
|
|
28
|
+
bindings: (bindings) => {
|
|
29
|
+
return {
|
|
30
|
+
pid: bindings.pid,
|
|
31
|
+
hostname: bindings.hostname,
|
|
32
|
+
environment: 'server'
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
// Redact sensitive information in production
|
|
37
|
+
redact: {
|
|
38
|
+
paths: ['password', 'token', 'key', 'secret'],
|
|
39
|
+
censor: '[REDACTED]'
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
};
|