@objectstack/core 0.6.1 → 0.7.1
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 +7 -0
- package/ENHANCED_FEATURES.md +380 -0
- package/README.md +299 -12
- package/dist/contracts/data-engine.d.ts +39 -22
- package/dist/contracts/data-engine.d.ts.map +1 -1
- package/dist/contracts/logger.d.ts +63 -0
- package/dist/contracts/logger.d.ts.map +1 -0
- package/dist/contracts/logger.js +1 -0
- package/dist/enhanced-kernel.d.ts +103 -0
- package/dist/enhanced-kernel.d.ts.map +1 -0
- package/dist/enhanced-kernel.js +403 -0
- package/dist/enhanced-kernel.test.d.ts +2 -0
- package/dist/enhanced-kernel.test.d.ts.map +1 -0
- package/dist/enhanced-kernel.test.js +412 -0
- package/dist/index.d.ts +11 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -2
- package/dist/kernel-base.d.ts +84 -0
- package/dist/kernel-base.d.ts.map +1 -0
- package/dist/kernel-base.js +219 -0
- package/dist/kernel.d.ts +11 -18
- package/dist/kernel.d.ts.map +1 -1
- package/dist/kernel.js +43 -114
- package/dist/kernel.test.d.ts +2 -0
- package/dist/kernel.test.d.ts.map +1 -0
- package/dist/kernel.test.js +161 -0
- package/dist/logger.d.ts +70 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +268 -0
- package/dist/logger.test.d.ts +2 -0
- package/dist/logger.test.d.ts.map +1 -0
- package/dist/logger.test.js +92 -0
- package/dist/plugin-loader.d.ts +148 -0
- package/dist/plugin-loader.d.ts.map +1 -0
- package/dist/plugin-loader.js +287 -0
- package/dist/plugin-loader.test.d.ts +2 -0
- package/dist/plugin-loader.test.d.ts.map +1 -0
- package/dist/plugin-loader.test.js +339 -0
- package/dist/types.d.ts +2 -1
- package/dist/types.d.ts.map +1 -1
- package/examples/enhanced-kernel-example.ts +309 -0
- package/package.json +19 -4
- package/src/contracts/data-engine.ts +46 -24
- package/src/contracts/logger.ts +70 -0
- package/src/enhanced-kernel.test.ts +535 -0
- package/src/enhanced-kernel.ts +496 -0
- package/src/index.ts +23 -2
- package/src/kernel-base.ts +256 -0
- package/src/kernel.test.ts +200 -0
- package/src/kernel.ts +55 -129
- package/src/logger.test.ts +116 -0
- package/src/logger.ts +306 -0
- package/src/plugin-loader.test.ts +412 -0
- package/src/plugin-loader.ts +435 -0
- package/src/types.ts +2 -1
- package/vitest.config.ts +8 -0
package/src/kernel.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import { Plugin
|
|
1
|
+
import { Plugin } from './types.js';
|
|
2
|
+
import { createLogger, ObjectLogger } from './logger.js';
|
|
3
|
+
import type { LoggerConfig } from '@objectstack/spec/system';
|
|
4
|
+
import { ObjectKernelBase } from './kernel-base.js';
|
|
2
5
|
|
|
3
6
|
/**
|
|
4
7
|
* ObjectKernel - MiniKernel Architecture
|
|
@@ -8,63 +11,28 @@ import { Plugin, PluginContext } from './types.js';
|
|
|
8
11
|
* - Provides dependency injection via service registry
|
|
9
12
|
* - Implements event/hook system for inter-plugin communication
|
|
10
13
|
* - Handles dependency resolution (topological sort)
|
|
14
|
+
* - Provides configurable logging for server and browser
|
|
11
15
|
*
|
|
12
16
|
* Core philosophy:
|
|
13
17
|
* - Business logic is completely separated into plugins
|
|
14
18
|
* - Kernel only manages lifecycle, DI, and hooks
|
|
15
19
|
* - Plugins are loaded as equal building blocks
|
|
16
20
|
*/
|
|
17
|
-
export class ObjectKernel {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
*/
|
|
26
|
-
private context: PluginContext = {
|
|
27
|
-
registerService: (name, service) => {
|
|
28
|
-
if (this.services.has(name)) {
|
|
29
|
-
throw new Error(`[Kernel] Service '${name}' already registered`);
|
|
30
|
-
}
|
|
31
|
-
this.services.set(name, service);
|
|
32
|
-
this.context.logger.log(`[Kernel] Service '${name}' registered`);
|
|
33
|
-
},
|
|
34
|
-
getService: <T>(name: string) => {
|
|
35
|
-
const service = this.services.get(name);
|
|
36
|
-
if (!service) {
|
|
37
|
-
throw new Error(`[Kernel] Service '${name}' not found`);
|
|
38
|
-
}
|
|
39
|
-
return service as T;
|
|
40
|
-
},
|
|
41
|
-
hook: (name, handler) => {
|
|
42
|
-
if (!this.hooks.has(name)) {
|
|
43
|
-
this.hooks.set(name, []);
|
|
44
|
-
}
|
|
45
|
-
this.hooks.get(name)!.push(handler);
|
|
46
|
-
},
|
|
47
|
-
trigger: async (name, ...args) => {
|
|
48
|
-
const handlers = this.hooks.get(name) || [];
|
|
49
|
-
for (const handler of handlers) {
|
|
50
|
-
await handler(...args);
|
|
51
|
-
}
|
|
52
|
-
},
|
|
53
|
-
getServices: () => {
|
|
54
|
-
return new Map(this.services);
|
|
55
|
-
},
|
|
56
|
-
logger: console,
|
|
57
|
-
getKernel: () => this,
|
|
58
|
-
};
|
|
21
|
+
export class ObjectKernel extends ObjectKernelBase {
|
|
22
|
+
constructor(config?: { logger?: Partial<LoggerConfig> }) {
|
|
23
|
+
const logger = createLogger(config?.logger);
|
|
24
|
+
super(logger);
|
|
25
|
+
|
|
26
|
+
// Initialize context after logger is created
|
|
27
|
+
this.context = this.createContext();
|
|
28
|
+
}
|
|
59
29
|
|
|
60
30
|
/**
|
|
61
31
|
* Register a plugin
|
|
62
32
|
* @param plugin - Plugin instance
|
|
63
33
|
*/
|
|
64
|
-
use(plugin: Plugin) {
|
|
65
|
-
|
|
66
|
-
throw new Error('[Kernel] Cannot register plugins after bootstrap has started');
|
|
67
|
-
}
|
|
34
|
+
use(plugin: Plugin): this {
|
|
35
|
+
this.validateIdle();
|
|
68
36
|
|
|
69
37
|
const pluginName = plugin.name;
|
|
70
38
|
if (this.plugins.has(pluginName)) {
|
|
@@ -75,51 +43,6 @@ export class ObjectKernel {
|
|
|
75
43
|
return this;
|
|
76
44
|
}
|
|
77
45
|
|
|
78
|
-
/**
|
|
79
|
-
* Resolve plugin dependencies using topological sort
|
|
80
|
-
* @returns Ordered list of plugins
|
|
81
|
-
*/
|
|
82
|
-
private resolveDependencies(): Plugin[] {
|
|
83
|
-
const resolved: Plugin[] = [];
|
|
84
|
-
const visited = new Set<string>();
|
|
85
|
-
const visiting = new Set<string>();
|
|
86
|
-
|
|
87
|
-
const visit = (pluginName: string) => {
|
|
88
|
-
if (visited.has(pluginName)) return;
|
|
89
|
-
|
|
90
|
-
if (visiting.has(pluginName)) {
|
|
91
|
-
throw new Error(`[Kernel] Circular dependency detected: ${pluginName}`);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
const plugin = this.plugins.get(pluginName);
|
|
95
|
-
if (!plugin) {
|
|
96
|
-
throw new Error(`[Kernel] Plugin '${pluginName}' not found`);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
visiting.add(pluginName);
|
|
100
|
-
|
|
101
|
-
// Visit dependencies first
|
|
102
|
-
const deps = plugin.dependencies || [];
|
|
103
|
-
for (const dep of deps) {
|
|
104
|
-
if (!this.plugins.has(dep)) {
|
|
105
|
-
throw new Error(`[Kernel] Dependency '${dep}' not found for plugin '${pluginName}'`);
|
|
106
|
-
}
|
|
107
|
-
visit(dep);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
visiting.delete(pluginName);
|
|
111
|
-
visited.add(pluginName);
|
|
112
|
-
resolved.push(plugin);
|
|
113
|
-
};
|
|
114
|
-
|
|
115
|
-
// Visit all plugins
|
|
116
|
-
for (const pluginName of this.plugins.keys()) {
|
|
117
|
-
visit(pluginName);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
return resolved;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
46
|
/**
|
|
124
47
|
* Bootstrap the kernel
|
|
125
48
|
* 1. Resolve dependencies (topological sort)
|
|
@@ -127,62 +50,72 @@ export class ObjectKernel {
|
|
|
127
50
|
* 3. Start phase - plugins execute business logic
|
|
128
51
|
* 4. Trigger 'kernel:ready' hook
|
|
129
52
|
*/
|
|
130
|
-
async bootstrap() {
|
|
131
|
-
|
|
132
|
-
throw new Error('[Kernel] Kernel already bootstrapped');
|
|
133
|
-
}
|
|
53
|
+
async bootstrap(): Promise<void> {
|
|
54
|
+
this.validateState('idle');
|
|
134
55
|
|
|
135
56
|
this.state = 'initializing';
|
|
136
|
-
this.
|
|
57
|
+
this.logger.info('Bootstrap started');
|
|
137
58
|
|
|
138
59
|
// Resolve dependencies
|
|
139
60
|
const orderedPlugins = this.resolveDependencies();
|
|
140
61
|
|
|
141
62
|
// Phase 1: Init - Plugins register services
|
|
142
|
-
this.
|
|
63
|
+
this.logger.info('Phase 1: Init plugins');
|
|
143
64
|
for (const plugin of orderedPlugins) {
|
|
144
|
-
this.
|
|
145
|
-
await plugin.init(this.context);
|
|
65
|
+
await this.runPluginInit(plugin);
|
|
146
66
|
}
|
|
147
67
|
|
|
148
68
|
// Phase 2: Start - Plugins execute business logic
|
|
149
|
-
this.
|
|
69
|
+
this.logger.info('Phase 2: Start plugins');
|
|
150
70
|
this.state = 'running';
|
|
71
|
+
|
|
151
72
|
for (const plugin of orderedPlugins) {
|
|
152
|
-
|
|
153
|
-
this.context.logger.log(`[Kernel] Start: ${plugin.name}`);
|
|
154
|
-
await plugin.start(this.context);
|
|
155
|
-
}
|
|
73
|
+
await this.runPluginStart(plugin);
|
|
156
74
|
}
|
|
157
75
|
|
|
158
|
-
//
|
|
159
|
-
this.
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
76
|
+
// Trigger ready hook
|
|
77
|
+
await this.triggerHook('kernel:ready');
|
|
78
|
+
this.logger.info('✅ Bootstrap complete', {
|
|
79
|
+
pluginCount: this.plugins.size
|
|
80
|
+
});
|
|
163
81
|
}
|
|
164
82
|
|
|
165
83
|
/**
|
|
166
84
|
* Shutdown the kernel
|
|
167
85
|
* Calls destroy on all plugins in reverse order
|
|
168
86
|
*/
|
|
169
|
-
async shutdown() {
|
|
170
|
-
|
|
171
|
-
|
|
87
|
+
async shutdown(): Promise<void> {
|
|
88
|
+
await this.destroy();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Graceful shutdown - destroy all plugins in reverse order
|
|
93
|
+
*/
|
|
94
|
+
async destroy(): Promise<void> {
|
|
95
|
+
if (this.state === 'stopped') {
|
|
96
|
+
this.logger.warn('Kernel already stopped');
|
|
97
|
+
return;
|
|
172
98
|
}
|
|
173
99
|
|
|
174
|
-
this.
|
|
175
|
-
this.
|
|
100
|
+
this.state = 'stopping';
|
|
101
|
+
this.logger.info('Shutdown started');
|
|
176
102
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
103
|
+
// Trigger shutdown hook
|
|
104
|
+
await this.triggerHook('kernel:shutdown');
|
|
105
|
+
|
|
106
|
+
// Destroy plugins in reverse order
|
|
107
|
+
const orderedPlugins = this.resolveDependencies();
|
|
108
|
+
for (const plugin of orderedPlugins.reverse()) {
|
|
109
|
+
await this.runPluginDestroy(plugin);
|
|
183
110
|
}
|
|
184
111
|
|
|
185
|
-
this.
|
|
112
|
+
this.state = 'stopped';
|
|
113
|
+
this.logger.info('✅ Shutdown complete');
|
|
114
|
+
|
|
115
|
+
// Cleanup logger resources
|
|
116
|
+
if (this.logger && typeof (this.logger as ObjectLogger).destroy === 'function') {
|
|
117
|
+
await (this.logger as ObjectLogger).destroy();
|
|
118
|
+
}
|
|
186
119
|
}
|
|
187
120
|
|
|
188
121
|
/**
|
|
@@ -199,11 +132,4 @@ export class ObjectKernel {
|
|
|
199
132
|
isRunning(): boolean {
|
|
200
133
|
return this.state === 'running';
|
|
201
134
|
}
|
|
202
|
-
|
|
203
|
-
/**
|
|
204
|
-
* Get kernel state
|
|
205
|
-
*/
|
|
206
|
-
getState(): string {
|
|
207
|
-
return this.state;
|
|
208
|
-
}
|
|
209
135
|
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { createLogger, ObjectLogger } from './logger';
|
|
3
|
+
|
|
4
|
+
describe('ObjectLogger', () => {
|
|
5
|
+
let logger: ObjectLogger;
|
|
6
|
+
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
logger = createLogger();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
afterEach(async () => {
|
|
12
|
+
await logger.destroy();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
describe('Basic Logging', () => {
|
|
16
|
+
it('should create a logger with default config', () => {
|
|
17
|
+
expect(logger).toBeDefined();
|
|
18
|
+
expect(logger.info).toBeDefined();
|
|
19
|
+
expect(logger.debug).toBeDefined();
|
|
20
|
+
expect(logger.warn).toBeDefined();
|
|
21
|
+
expect(logger.error).toBeDefined();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('should log info messages', () => {
|
|
25
|
+
expect(() => logger.info('Test message')).not.toThrow();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('should log debug messages', () => {
|
|
29
|
+
expect(() => logger.debug('Debug message')).not.toThrow();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('should log warn messages', () => {
|
|
33
|
+
expect(() => logger.warn('Warning message')).not.toThrow();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('should log error messages', () => {
|
|
37
|
+
const error = new Error('Test error');
|
|
38
|
+
expect(() => logger.error('Error occurred', error)).not.toThrow();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('should log with metadata', () => {
|
|
42
|
+
expect(() => logger.info('Message with metadata', { userId: '123', action: 'login' })).not.toThrow();
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe('Configuration', () => {
|
|
47
|
+
it('should respect log level configuration', async () => {
|
|
48
|
+
const warnLogger = createLogger({ level: 'warn' });
|
|
49
|
+
|
|
50
|
+
// These should not throw but might not output anything
|
|
51
|
+
expect(() => warnLogger.debug('Debug message')).not.toThrow();
|
|
52
|
+
expect(() => warnLogger.info('Info message')).not.toThrow();
|
|
53
|
+
expect(() => warnLogger.warn('Warning message')).not.toThrow();
|
|
54
|
+
|
|
55
|
+
await warnLogger.destroy();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('should support different formats', async () => {
|
|
59
|
+
const jsonLogger = createLogger({ format: 'json' });
|
|
60
|
+
const textLogger = createLogger({ format: 'text' });
|
|
61
|
+
const prettyLogger = createLogger({ format: 'pretty' });
|
|
62
|
+
|
|
63
|
+
expect(() => jsonLogger.info('JSON format')).not.toThrow();
|
|
64
|
+
expect(() => textLogger.info('Text format')).not.toThrow();
|
|
65
|
+
expect(() => prettyLogger.info('Pretty format')).not.toThrow();
|
|
66
|
+
|
|
67
|
+
await jsonLogger.destroy();
|
|
68
|
+
await textLogger.destroy();
|
|
69
|
+
await prettyLogger.destroy();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('should redact sensitive keys', async () => {
|
|
73
|
+
const logger = createLogger({ redact: ['password', 'apiKey'] });
|
|
74
|
+
|
|
75
|
+
// This should work without exposing the password
|
|
76
|
+
expect(() => logger.info('User login', {
|
|
77
|
+
username: 'john',
|
|
78
|
+
password: 'secret123',
|
|
79
|
+
apiKey: 'key-12345'
|
|
80
|
+
})).not.toThrow();
|
|
81
|
+
|
|
82
|
+
await logger.destroy();
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe('Child Loggers', () => {
|
|
87
|
+
it('should create child logger with context', () => {
|
|
88
|
+
const childLogger = logger.child({ service: 'api', requestId: '123' });
|
|
89
|
+
|
|
90
|
+
expect(childLogger).toBeDefined();
|
|
91
|
+
expect(() => childLogger.info('Child log message')).not.toThrow();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('should support trace context', () => {
|
|
95
|
+
const tracedLogger = logger.withTrace('trace-123', 'span-456');
|
|
96
|
+
|
|
97
|
+
expect(tracedLogger).toBeDefined();
|
|
98
|
+
expect(() => tracedLogger.info('Traced message')).not.toThrow();
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
describe('Environment Detection', () => {
|
|
103
|
+
it('should detect Node.js environment', async () => {
|
|
104
|
+
// This test runs in Node.js, so logger should detect it
|
|
105
|
+
const nodeLogger = createLogger({ format: 'json' });
|
|
106
|
+
expect(() => nodeLogger.info('Node environment')).not.toThrow();
|
|
107
|
+
await nodeLogger.destroy();
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
describe('Compatibility', () => {
|
|
112
|
+
it('should support console.log compatibility', () => {
|
|
113
|
+
expect(() => logger.log('Compatible log')).not.toThrow();
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
});
|
package/src/logger.ts
ADDED
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
import type { LoggerConfig, LogLevel } from '@objectstack/spec/system';
|
|
2
|
+
import type { Logger } from '@objectstack/spec/contracts';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Universal Logger Implementation
|
|
6
|
+
*
|
|
7
|
+
* A configurable logger that works in both browser and Node.js environments.
|
|
8
|
+
* - Node.js: Uses Pino for high-performance structured logging
|
|
9
|
+
* - Browser: Simple console-based implementation
|
|
10
|
+
*
|
|
11
|
+
* Features:
|
|
12
|
+
* - Structured logging with multiple formats (json, text, pretty)
|
|
13
|
+
* - Log level filtering
|
|
14
|
+
* - Sensitive data redaction
|
|
15
|
+
* - File logging with rotation (Node.js only via Pino)
|
|
16
|
+
* - Browser console integration
|
|
17
|
+
* - Distributed tracing support (traceId, spanId)
|
|
18
|
+
*/
|
|
19
|
+
export class ObjectLogger implements Logger {
|
|
20
|
+
private config: Required<Omit<LoggerConfig, 'file' | 'rotation' | 'name'>> & { file?: string; rotation?: { maxSize: string; maxFiles: number }; name?: string };
|
|
21
|
+
private isNode: boolean;
|
|
22
|
+
private pinoLogger?: any; // Pino logger instance for Node.js
|
|
23
|
+
private pinoInstance?: any; // Base Pino instance for creating child loggers
|
|
24
|
+
|
|
25
|
+
constructor(config: Partial<LoggerConfig> = {}) {
|
|
26
|
+
// Detect runtime environment
|
|
27
|
+
this.isNode = typeof process !== 'undefined' && process.versions?.node !== undefined;
|
|
28
|
+
|
|
29
|
+
// Set defaults
|
|
30
|
+
this.config = {
|
|
31
|
+
name: config.name,
|
|
32
|
+
level: config.level ?? 'info',
|
|
33
|
+
format: config.format ?? (this.isNode ? 'json' : 'pretty'),
|
|
34
|
+
redact: config.redact ?? ['password', 'token', 'secret', 'key'],
|
|
35
|
+
sourceLocation: config.sourceLocation ?? false,
|
|
36
|
+
file: config.file,
|
|
37
|
+
rotation: config.rotation ?? {
|
|
38
|
+
maxSize: '10m',
|
|
39
|
+
maxFiles: 5
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// Initialize Pino logger for Node.js
|
|
44
|
+
if (this.isNode) {
|
|
45
|
+
this.initPinoLogger();
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Initialize Pino logger for Node.js
|
|
51
|
+
*/
|
|
52
|
+
private initPinoLogger() {
|
|
53
|
+
if (!this.isNode) return;
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
// Dynamic import for Pino (Node.js only)
|
|
57
|
+
const pino = require('pino');
|
|
58
|
+
|
|
59
|
+
// Build Pino options
|
|
60
|
+
const pinoOptions: any = {
|
|
61
|
+
level: this.config.level,
|
|
62
|
+
redact: {
|
|
63
|
+
paths: this.config.redact,
|
|
64
|
+
censor: '***REDACTED***'
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
// Add name if provided
|
|
69
|
+
if (this.config.name) {
|
|
70
|
+
pinoOptions.name = this.config.name;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Transport configuration for pretty printing or file output
|
|
74
|
+
const targets: any[] = [];
|
|
75
|
+
|
|
76
|
+
// Console transport
|
|
77
|
+
if (this.config.format === 'pretty') {
|
|
78
|
+
targets.push({
|
|
79
|
+
target: 'pino-pretty',
|
|
80
|
+
options: {
|
|
81
|
+
colorize: true,
|
|
82
|
+
translateTime: 'SYS:standard',
|
|
83
|
+
ignore: 'pid,hostname'
|
|
84
|
+
},
|
|
85
|
+
level: this.config.level
|
|
86
|
+
});
|
|
87
|
+
} else if (this.config.format === 'json') {
|
|
88
|
+
// JSON to stdout
|
|
89
|
+
targets.push({
|
|
90
|
+
target: 'pino/file',
|
|
91
|
+
options: { destination: 1 }, // stdout
|
|
92
|
+
level: this.config.level
|
|
93
|
+
});
|
|
94
|
+
} else {
|
|
95
|
+
// text format (simple)
|
|
96
|
+
targets.push({
|
|
97
|
+
target: 'pino/file',
|
|
98
|
+
options: { destination: 1 },
|
|
99
|
+
level: this.config.level
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// File transport (if configured)
|
|
104
|
+
if (this.config.file) {
|
|
105
|
+
targets.push({
|
|
106
|
+
target: 'pino/file',
|
|
107
|
+
options: {
|
|
108
|
+
destination: this.config.file,
|
|
109
|
+
mkdir: true
|
|
110
|
+
},
|
|
111
|
+
level: this.config.level
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Create transport
|
|
116
|
+
if (targets.length > 0) {
|
|
117
|
+
pinoOptions.transport = targets.length === 1 ? targets[0] : { targets };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Create Pino logger
|
|
121
|
+
this.pinoInstance = pino(pinoOptions);
|
|
122
|
+
this.pinoLogger = this.pinoInstance;
|
|
123
|
+
|
|
124
|
+
} catch (error) {
|
|
125
|
+
// Fallback to console if Pino is not available
|
|
126
|
+
console.warn('[Logger] Pino not available, falling back to console:', error);
|
|
127
|
+
this.pinoLogger = null;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Redact sensitive keys from context object (for browser)
|
|
133
|
+
*/
|
|
134
|
+
private redactSensitive(obj: any): any {
|
|
135
|
+
if (!obj || typeof obj !== 'object') return obj;
|
|
136
|
+
|
|
137
|
+
const redacted = Array.isArray(obj) ? [...obj] : { ...obj };
|
|
138
|
+
|
|
139
|
+
for (const key in redacted) {
|
|
140
|
+
const lowerKey = key.toLowerCase();
|
|
141
|
+
const shouldRedact = this.config.redact.some((pattern: string) =>
|
|
142
|
+
lowerKey.includes(pattern.toLowerCase())
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
if (shouldRedact) {
|
|
146
|
+
redacted[key] = '***REDACTED***';
|
|
147
|
+
} else if (typeof redacted[key] === 'object' && redacted[key] !== null) {
|
|
148
|
+
redacted[key] = this.redactSensitive(redacted[key]);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return redacted;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Format log entry for browser
|
|
157
|
+
*/
|
|
158
|
+
private formatBrowserLog(level: LogLevel, message: string, context?: Record<string, any>): string {
|
|
159
|
+
if (this.config.format === 'json') {
|
|
160
|
+
return JSON.stringify({
|
|
161
|
+
timestamp: new Date().toISOString(),
|
|
162
|
+
level,
|
|
163
|
+
message,
|
|
164
|
+
...context
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (this.config.format === 'text') {
|
|
169
|
+
const parts = [new Date().toISOString(), level.toUpperCase(), message];
|
|
170
|
+
if (context && Object.keys(context).length > 0) {
|
|
171
|
+
parts.push(JSON.stringify(context));
|
|
172
|
+
}
|
|
173
|
+
return parts.join(' | ');
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Pretty format
|
|
177
|
+
const levelColors: Record<LogLevel, string> = {
|
|
178
|
+
debug: '\x1b[36m', // Cyan
|
|
179
|
+
info: '\x1b[32m', // Green
|
|
180
|
+
warn: '\x1b[33m', // Yellow
|
|
181
|
+
error: '\x1b[31m', // Red
|
|
182
|
+
fatal: '\x1b[35m' // Magenta
|
|
183
|
+
};
|
|
184
|
+
const reset = '\x1b[0m';
|
|
185
|
+
const color = levelColors[level] || '';
|
|
186
|
+
|
|
187
|
+
let output = `${color}[${level.toUpperCase()}]${reset} ${message}`;
|
|
188
|
+
|
|
189
|
+
if (context && Object.keys(context).length > 0) {
|
|
190
|
+
output += ` ${JSON.stringify(context, null, 2)}`;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return output;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Log using browser console
|
|
198
|
+
*/
|
|
199
|
+
private logBrowser(level: LogLevel, message: string, context?: Record<string, any>, error?: Error) {
|
|
200
|
+
const redactedContext = context ? this.redactSensitive(context) : undefined;
|
|
201
|
+
const mergedContext = error ? { ...redactedContext, error: { message: error.message, stack: error.stack } } : redactedContext;
|
|
202
|
+
|
|
203
|
+
const formatted = this.formatBrowserLog(level, message, mergedContext);
|
|
204
|
+
|
|
205
|
+
const consoleMethod = level === 'debug' ? 'debug' :
|
|
206
|
+
level === 'info' ? 'log' :
|
|
207
|
+
level === 'warn' ? 'warn' :
|
|
208
|
+
level === 'error' || level === 'fatal' ? 'error' :
|
|
209
|
+
'log';
|
|
210
|
+
|
|
211
|
+
console[consoleMethod](formatted);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Public logging methods
|
|
216
|
+
*/
|
|
217
|
+
debug(message: string, meta?: Record<string, any>): void {
|
|
218
|
+
if (this.isNode && this.pinoLogger) {
|
|
219
|
+
this.pinoLogger.debug(meta || {}, message);
|
|
220
|
+
} else {
|
|
221
|
+
this.logBrowser('debug', message, meta);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
info(message: string, meta?: Record<string, any>): void {
|
|
226
|
+
if (this.isNode && this.pinoLogger) {
|
|
227
|
+
this.pinoLogger.info(meta || {}, message);
|
|
228
|
+
} else {
|
|
229
|
+
this.logBrowser('info', message, meta);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
warn(message: string, meta?: Record<string, any>): void {
|
|
234
|
+
if (this.isNode && this.pinoLogger) {
|
|
235
|
+
this.pinoLogger.warn(meta || {}, message);
|
|
236
|
+
} else {
|
|
237
|
+
this.logBrowser('warn', message, meta);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
error(message: string, error?: Error, meta?: Record<string, any>): void {
|
|
242
|
+
if (this.isNode && this.pinoLogger) {
|
|
243
|
+
const errorContext = error ? { err: error, ...meta } : meta || {};
|
|
244
|
+
this.pinoLogger.error(errorContext, message);
|
|
245
|
+
} else {
|
|
246
|
+
this.logBrowser('error', message, meta, error);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
fatal(message: string, error?: Error, meta?: Record<string, any>): void {
|
|
251
|
+
if (this.isNode && this.pinoLogger) {
|
|
252
|
+
const errorContext = error ? { err: error, ...meta } : meta || {};
|
|
253
|
+
this.pinoLogger.fatal(errorContext, message);
|
|
254
|
+
} else {
|
|
255
|
+
this.logBrowser('fatal', message, meta, error);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Create a child logger with additional context
|
|
261
|
+
* Note: Child loggers share the parent's Pino instance
|
|
262
|
+
*/
|
|
263
|
+
child(context: Record<string, any>): ObjectLogger {
|
|
264
|
+
const childLogger = new ObjectLogger(this.config);
|
|
265
|
+
|
|
266
|
+
// For Node.js with Pino, create a Pino child logger
|
|
267
|
+
if (this.isNode && this.pinoInstance) {
|
|
268
|
+
childLogger.pinoLogger = this.pinoInstance.child(context);
|
|
269
|
+
childLogger.pinoInstance = this.pinoInstance;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return childLogger;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Set trace context for distributed tracing
|
|
277
|
+
*/
|
|
278
|
+
withTrace(traceId: string, spanId?: string): ObjectLogger {
|
|
279
|
+
return this.child({ traceId, spanId });
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Cleanup resources
|
|
284
|
+
*/
|
|
285
|
+
async destroy(): Promise<void> {
|
|
286
|
+
if (this.pinoLogger && this.pinoLogger.flush) {
|
|
287
|
+
await new Promise<void>((resolve) => {
|
|
288
|
+
this.pinoLogger.flush(() => resolve());
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Compatibility method for console.log usage
|
|
295
|
+
*/
|
|
296
|
+
log(message: string, ...args: any[]): void {
|
|
297
|
+
this.info(message, args.length > 0 ? { args } : undefined);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Create a logger instance
|
|
303
|
+
*/
|
|
304
|
+
export function createLogger(config?: Partial<LoggerConfig>): ObjectLogger {
|
|
305
|
+
return new ObjectLogger(config);
|
|
306
|
+
}
|