@geekmidas/telescope 0.0.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/README.md +521 -0
- package/dist/Telescope-B3Wd82yk.cjs +602 -0
- package/dist/Telescope-B3Wd82yk.cjs.map +1 -0
- package/dist/Telescope-C5dyDYYB.d.cts +133 -0
- package/dist/Telescope-D-uoZB6b.mjs +596 -0
- package/dist/Telescope-D-uoZB6b.mjs.map +1 -0
- package/dist/Telescope-DyIWgh9-.d.mts +133 -0
- package/dist/Telescope.cjs +3 -0
- package/dist/Telescope.d.cts +3 -0
- package/dist/Telescope.d.mts +3 -0
- package/dist/Telescope.mjs +3 -0
- package/dist/chunk-CUT6urMc.cjs +30 -0
- package/dist/index.cjs +5 -0
- package/dist/index.d.cts +4 -0
- package/dist/index.d.mts +4 -0
- package/dist/index.mjs +4 -0
- package/dist/logger/console.cjs +161 -0
- package/dist/logger/console.cjs.map +1 -0
- package/dist/logger/console.d.cts +109 -0
- package/dist/logger/console.d.mts +109 -0
- package/dist/logger/console.mjs +159 -0
- package/dist/logger/console.mjs.map +1 -0
- package/dist/logger/pino.cjs +118 -0
- package/dist/logger/pino.cjs.map +1 -0
- package/dist/logger/pino.d.cts +89 -0
- package/dist/logger/pino.d.mts +89 -0
- package/dist/logger/pino.mjs +116 -0
- package/dist/logger/pino.mjs.map +1 -0
- package/dist/memory-9-B9WACq.cjs +110 -0
- package/dist/memory-9-B9WACq.cjs.map +1 -0
- package/dist/memory-Cm0eevCS.d.mts +38 -0
- package/dist/memory-DiP1a-pp.d.cts +38 -0
- package/dist/memory-SdN5vtG9.mjs +104 -0
- package/dist/memory-SdN5vtG9.mjs.map +1 -0
- package/dist/server/hono.cjs +180 -0
- package/dist/server/hono.cjs.map +1 -0
- package/dist/server/hono.d.cts +26 -0
- package/dist/server/hono.d.mts +26 -0
- package/dist/server/hono.mjs +176 -0
- package/dist/server/hono.mjs.map +1 -0
- package/dist/storage/kysely.cjs +336 -0
- package/dist/storage/kysely.cjs.map +1 -0
- package/dist/storage/kysely.d.cts +161 -0
- package/dist/storage/kysely.d.mts +161 -0
- package/dist/storage/kysely.mjs +334 -0
- package/dist/storage/kysely.mjs.map +1 -0
- package/dist/storage/memory.cjs +3 -0
- package/dist/storage/memory.d.cts +3 -0
- package/dist/storage/memory.d.mts +3 -0
- package/dist/storage/memory.mjs +3 -0
- package/dist/types-BGDhFv4R.d.cts +170 -0
- package/dist/types-CZbzz8kx.d.mts +170 -0
- package/dist/types.cjs +0 -0
- package/dist/types.d.cts +2 -0
- package/dist/types.d.mts +2 -0
- package/dist/types.mjs +0 -0
- package/dist/ui-assets-D6-8TAr_.mjs +30 -0
- package/dist/ui-assets-D6-8TAr_.mjs.map +1 -0
- package/dist/ui-assets-ulevVble.cjs +48 -0
- package/dist/ui-assets-ulevVble.cjs.map +1 -0
- package/dist/ui-assets.cjs +5 -0
- package/dist/ui-assets.d.cts +12 -0
- package/dist/ui-assets.d.mts +12 -0
- package/dist/ui-assets.mjs +3 -0
- package/package.json +83 -0
- package/scripts/embed-ui.ts +90 -0
- package/src/Telescope.ts +714 -0
- package/src/__tests__/Telescope.spec.ts +356 -0
- package/src/index.ts +23 -0
- package/src/logger/__tests__/console.spec.ts +266 -0
- package/src/logger/__tests__/pino.spec.ts +217 -0
- package/src/logger/console.ts +230 -0
- package/src/logger/pino.ts +191 -0
- package/src/server/__tests__/hono.spec.ts +340 -0
- package/src/server/hono.ts +247 -0
- package/src/storage/__tests__/kysely.spec.ts +715 -0
- package/src/storage/__tests__/memory.spec.ts +411 -0
- package/src/storage/kysely.ts +572 -0
- package/src/storage/memory.ts +168 -0
- package/src/types.ts +188 -0
- package/src/ui-assets.ts +40 -0
- package/ui/index.html +12 -0
- package/ui/node_modules/.bin/browserslist +21 -0
- package/ui/node_modules/.bin/jiti +21 -0
- package/ui/node_modules/.bin/terser +21 -0
- package/ui/node_modules/.bin/tsc +21 -0
- package/ui/node_modules/.bin/tsserver +21 -0
- package/ui/node_modules/.bin/tsx +21 -0
- package/ui/node_modules/.bin/vite +21 -0
- package/ui/package.json +24 -0
- package/ui/src/App.tsx +342 -0
- package/ui/src/api.ts +75 -0
- package/ui/src/components/ExceptionDetail.tsx +100 -0
- package/ui/src/components/LogDetail.tsx +91 -0
- package/ui/src/components/RequestDetail.tsx +143 -0
- package/ui/src/main.tsx +10 -0
- package/ui/src/styles.css +10 -0
- package/ui/src/types.ts +63 -0
- package/ui/src/vite-env.d.ts +1 -0
- package/ui/src/vite-plugin-gkm-config.ts +54 -0
- package/ui/tsconfig.json +20 -0
- package/ui/tsconfig.tsbuildinfo +14 -0
- package/ui/vite.config.ts +13 -0
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import pino from 'pino';
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
3
|
+
import { Telescope } from '../../Telescope';
|
|
4
|
+
import { InMemoryStorage } from '../../storage/memory';
|
|
5
|
+
import { createPinoDestination, createPinoTransport } from '../pino';
|
|
6
|
+
|
|
7
|
+
describe('Pino Transport', () => {
|
|
8
|
+
let telescope: Telescope;
|
|
9
|
+
let storage: InMemoryStorage;
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
storage = new InMemoryStorage();
|
|
13
|
+
telescope = new Telescope({ storage });
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
telescope.destroy();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
// Helper to create transport with fast flush for tests
|
|
21
|
+
const createTestTransport = (
|
|
22
|
+
opts: Parameters<typeof createPinoTransport>[0],
|
|
23
|
+
) => createPinoTransport({ flushIntervalMs: 50, ...opts });
|
|
24
|
+
|
|
25
|
+
describe('createPinoTransport', () => {
|
|
26
|
+
it('should parse and forward JSON log lines', async () => {
|
|
27
|
+
const transport = createTestTransport({ telescope });
|
|
28
|
+
const logger = pino({ level: 'debug' }, transport);
|
|
29
|
+
|
|
30
|
+
logger.info({ userId: '123' }, 'Test message');
|
|
31
|
+
|
|
32
|
+
// Wait for flush interval
|
|
33
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
34
|
+
|
|
35
|
+
const logs = await telescope.getLogs();
|
|
36
|
+
expect(logs).toHaveLength(1);
|
|
37
|
+
expect(logs[0].level).toBe('info');
|
|
38
|
+
expect(logs[0].message).toBe('Test message');
|
|
39
|
+
expect(logs[0].context).toMatchObject({ userId: '123' });
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should map Pino log levels correctly', async () => {
|
|
43
|
+
const transport = createTestTransport({ telescope });
|
|
44
|
+
const logger = pino({ level: 'trace' }, transport);
|
|
45
|
+
|
|
46
|
+
logger.trace('Trace message');
|
|
47
|
+
logger.debug('Debug message');
|
|
48
|
+
logger.info('Info message');
|
|
49
|
+
logger.warn('Warn message');
|
|
50
|
+
logger.error('Error message');
|
|
51
|
+
logger.fatal('Fatal message');
|
|
52
|
+
|
|
53
|
+
await new Promise((r) => setTimeout(r, 150));
|
|
54
|
+
|
|
55
|
+
const logs = await telescope.getLogs();
|
|
56
|
+
expect(logs).toHaveLength(6);
|
|
57
|
+
|
|
58
|
+
const logLevels = logs.map((l) => l.level);
|
|
59
|
+
expect(logLevels.filter((l) => l === 'debug')).toHaveLength(2); // trace + debug
|
|
60
|
+
expect(logLevels).toContain('info');
|
|
61
|
+
expect(logLevels).toContain('warn');
|
|
62
|
+
expect(logLevels.filter((l) => l === 'error')).toHaveLength(2); // error + fatal
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('should extract requestId from log data', async () => {
|
|
66
|
+
const transport = createTestTransport({ telescope });
|
|
67
|
+
const logger = pino({ level: 'debug' }, transport);
|
|
68
|
+
|
|
69
|
+
logger.info({ requestId: 'req-abc123' }, 'Request log');
|
|
70
|
+
|
|
71
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
72
|
+
|
|
73
|
+
const logs = await telescope.getLogs();
|
|
74
|
+
expect(logs[0].requestId).toBe('req-abc123');
|
|
75
|
+
// requestId should not be in context
|
|
76
|
+
expect(logs[0].context).not.toHaveProperty('requestId');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('should use static requestId option', async () => {
|
|
80
|
+
const transport = createTestTransport({
|
|
81
|
+
telescope,
|
|
82
|
+
requestId: 'static-req-id',
|
|
83
|
+
});
|
|
84
|
+
const logger = pino({ level: 'debug' }, transport);
|
|
85
|
+
|
|
86
|
+
logger.info('Log with static ID');
|
|
87
|
+
|
|
88
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
89
|
+
|
|
90
|
+
const logs = await telescope.getLogs();
|
|
91
|
+
expect(logs[0].requestId).toBe('static-req-id');
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('should use requestId function option', async () => {
|
|
95
|
+
const transport = createTestTransport({
|
|
96
|
+
telescope,
|
|
97
|
+
requestId: (data) => data.traceId as string | undefined,
|
|
98
|
+
});
|
|
99
|
+
const logger = pino({ level: 'debug' }, transport);
|
|
100
|
+
|
|
101
|
+
logger.info({ traceId: 'trace-xyz' }, 'Log with extracted ID');
|
|
102
|
+
|
|
103
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
104
|
+
|
|
105
|
+
const logs = await telescope.getLogs();
|
|
106
|
+
expect(logs[0].requestId).toBe('trace-xyz');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('should handle empty message', async () => {
|
|
110
|
+
const transport = createTestTransport({ telescope });
|
|
111
|
+
const logger = pino({ level: 'debug' }, transport);
|
|
112
|
+
|
|
113
|
+
logger.info({ data: 'some data' });
|
|
114
|
+
|
|
115
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
116
|
+
|
|
117
|
+
const logs = await telescope.getLogs();
|
|
118
|
+
expect(logs[0].message).toBe('');
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('should strip Pino metadata from context', async () => {
|
|
122
|
+
const transport = createTestTransport({ telescope });
|
|
123
|
+
const logger = pino({ level: 'debug' }, transport);
|
|
124
|
+
|
|
125
|
+
logger.info({ customField: 'value' }, 'Test');
|
|
126
|
+
|
|
127
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
128
|
+
|
|
129
|
+
const logs = await telescope.getLogs();
|
|
130
|
+
const context = logs[0].context;
|
|
131
|
+
|
|
132
|
+
expect(context).not.toHaveProperty('level');
|
|
133
|
+
expect(context).not.toHaveProperty('msg');
|
|
134
|
+
expect(context).not.toHaveProperty('time');
|
|
135
|
+
expect(context).not.toHaveProperty('pid');
|
|
136
|
+
expect(context).not.toHaveProperty('hostname');
|
|
137
|
+
expect(context).toMatchObject({ customField: 'value' });
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('should return a writable stream', () => {
|
|
141
|
+
const transport = createPinoTransport({ telescope });
|
|
142
|
+
|
|
143
|
+
expect(typeof transport.write).toBe('function');
|
|
144
|
+
expect(typeof transport.end).toBe('function');
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('should batch logs and flush at batchSize', async () => {
|
|
148
|
+
const transport = createPinoTransport({
|
|
149
|
+
telescope,
|
|
150
|
+
batchSize: 3,
|
|
151
|
+
flushIntervalMs: 5000, // Long interval to test batch size trigger
|
|
152
|
+
});
|
|
153
|
+
const logger = pino({ level: 'debug' }, transport);
|
|
154
|
+
|
|
155
|
+
// Write 2 logs - should not flush yet
|
|
156
|
+
logger.info('Log 1');
|
|
157
|
+
logger.info('Log 2');
|
|
158
|
+
|
|
159
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
160
|
+
let logs = await telescope.getLogs();
|
|
161
|
+
expect(logs).toHaveLength(0); // Not flushed yet
|
|
162
|
+
|
|
163
|
+
// Write 3rd log - should trigger flush
|
|
164
|
+
logger.info('Log 3');
|
|
165
|
+
|
|
166
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
167
|
+
logs = await telescope.getLogs();
|
|
168
|
+
expect(logs).toHaveLength(3);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('should flush on close', async () => {
|
|
172
|
+
const transport = createPinoTransport({
|
|
173
|
+
telescope,
|
|
174
|
+
batchSize: 100,
|
|
175
|
+
flushIntervalMs: 10000, // Long interval
|
|
176
|
+
});
|
|
177
|
+
const logger = pino({ level: 'debug' }, transport);
|
|
178
|
+
|
|
179
|
+
logger.info('Will be flushed on close');
|
|
180
|
+
|
|
181
|
+
// End the stream to trigger close
|
|
182
|
+
transport.end();
|
|
183
|
+
|
|
184
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
185
|
+
|
|
186
|
+
const logs = await telescope.getLogs();
|
|
187
|
+
expect(logs).toHaveLength(1);
|
|
188
|
+
expect(logs[0].message).toBe('Will be flushed on close');
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
describe('createPinoDestination', () => {
|
|
193
|
+
it('should be an alias for createPinoTransport', () => {
|
|
194
|
+
const transport = createPinoTransport({ telescope });
|
|
195
|
+
const destination = createPinoDestination({ telescope });
|
|
196
|
+
|
|
197
|
+
expect(typeof transport.write).toBe('function');
|
|
198
|
+
expect(typeof destination.write).toBe('function');
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it('should work the same as createPinoTransport', async () => {
|
|
202
|
+
const destination = createPinoDestination({
|
|
203
|
+
telescope,
|
|
204
|
+
flushIntervalMs: 50,
|
|
205
|
+
});
|
|
206
|
+
const logger = pino({ level: 'debug' }, destination);
|
|
207
|
+
|
|
208
|
+
logger.info('Destination test');
|
|
209
|
+
|
|
210
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
211
|
+
|
|
212
|
+
const logs = await telescope.getLogs();
|
|
213
|
+
expect(logs).toHaveLength(1);
|
|
214
|
+
expect(logs[0].message).toBe('Destination test');
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
});
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import type { Telescope } from '../Telescope';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Logger interface matching @geekmidas/logger
|
|
5
|
+
*/
|
|
6
|
+
export interface Logger {
|
|
7
|
+
debug: LogFn;
|
|
8
|
+
info: LogFn;
|
|
9
|
+
warn: LogFn;
|
|
10
|
+
error: LogFn;
|
|
11
|
+
fatal: LogFn;
|
|
12
|
+
trace: LogFn;
|
|
13
|
+
child: (obj: object) => Logger;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type LogFn = {
|
|
17
|
+
<T extends object>(obj: T, msg?: string, ...args: any[]): void;
|
|
18
|
+
(msg: string): void;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export interface TelescopeLoggerOptions {
|
|
22
|
+
/**
|
|
23
|
+
* The Telescope instance to send logs to
|
|
24
|
+
*/
|
|
25
|
+
telescope: Telescope;
|
|
26
|
+
/**
|
|
27
|
+
* Optional underlying logger to also forward logs to.
|
|
28
|
+
* If not provided, logs will only go to Telescope.
|
|
29
|
+
*/
|
|
30
|
+
logger?: Logger;
|
|
31
|
+
/**
|
|
32
|
+
* Request ID to associate logs with a specific request.
|
|
33
|
+
*/
|
|
34
|
+
requestId?: string;
|
|
35
|
+
/**
|
|
36
|
+
* Initial context data to include in all log messages
|
|
37
|
+
*/
|
|
38
|
+
context?: Record<string, unknown>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
type LogLevel = 'debug' | 'info' | 'warn' | 'error';
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* A logger that sends logs to both Telescope and an optional underlying logger.
|
|
45
|
+
* Implements the Logger interface from @geekmidas/logger.
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* ```typescript
|
|
49
|
+
* import { Telescope, InMemoryStorage } from '@geekmidas/telescope';
|
|
50
|
+
* import { TelescopeLogger } from '@geekmidas/telescope/logger/console';
|
|
51
|
+
* import { ConsoleLogger } from '@geekmidas/logger/console';
|
|
52
|
+
*
|
|
53
|
+
* const telescope = new Telescope({ storage: new InMemoryStorage() });
|
|
54
|
+
*
|
|
55
|
+
* // With underlying logger (logs to both console and Telescope)
|
|
56
|
+
* const logger = new TelescopeLogger({
|
|
57
|
+
* telescope,
|
|
58
|
+
* logger: new ConsoleLogger({ app: 'myApp' }),
|
|
59
|
+
* });
|
|
60
|
+
*
|
|
61
|
+
* // Without underlying logger (logs only to Telescope)
|
|
62
|
+
* const telescopeOnly = new TelescopeLogger({ telescope });
|
|
63
|
+
*
|
|
64
|
+
* // Usage
|
|
65
|
+
* logger.info({ userId: '123' }, 'User logged in');
|
|
66
|
+
* logger.error({ error: 'Something failed' }, 'Operation failed');
|
|
67
|
+
* ```
|
|
68
|
+
*/
|
|
69
|
+
export class TelescopeLogger implements Logger {
|
|
70
|
+
private telescope: Telescope;
|
|
71
|
+
private logger?: Logger;
|
|
72
|
+
private requestId?: string;
|
|
73
|
+
private context: Record<string, unknown>;
|
|
74
|
+
|
|
75
|
+
constructor(options: TelescopeLoggerOptions) {
|
|
76
|
+
this.telescope = options.telescope;
|
|
77
|
+
this.logger = options.logger;
|
|
78
|
+
this.requestId = options.requestId;
|
|
79
|
+
this.context = options.context ?? {};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
private createLogFn(level: LogLevel): LogFn {
|
|
83
|
+
const fn = <T extends object>(
|
|
84
|
+
objOrMsg: T | string,
|
|
85
|
+
msg?: string,
|
|
86
|
+
...args: any[]
|
|
87
|
+
): void => {
|
|
88
|
+
let context: Record<string, unknown>;
|
|
89
|
+
let message: string;
|
|
90
|
+
|
|
91
|
+
if (typeof objOrMsg === 'string') {
|
|
92
|
+
context = { ...this.context };
|
|
93
|
+
message = objOrMsg;
|
|
94
|
+
} else {
|
|
95
|
+
context = { ...this.context, ...objOrMsg };
|
|
96
|
+
message = msg ?? '';
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Forward to underlying logger if present
|
|
100
|
+
if (this.logger) {
|
|
101
|
+
if (typeof objOrMsg === 'string') {
|
|
102
|
+
this.logger[level](objOrMsg);
|
|
103
|
+
} else {
|
|
104
|
+
this.logger[level](objOrMsg as any, msg, ...args);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Send to Telescope (fire and forget)
|
|
109
|
+
this.telescope[level](message, context, this.requestId).catch(() => {});
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
return fn as LogFn;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
debug: LogFn = this.createLogFn('debug');
|
|
116
|
+
info: LogFn = this.createLogFn('info');
|
|
117
|
+
warn: LogFn = this.createLogFn('warn');
|
|
118
|
+
error: LogFn = this.createLogFn('error');
|
|
119
|
+
|
|
120
|
+
// Map fatal and trace to error and debug for Telescope
|
|
121
|
+
fatal: LogFn = ((
|
|
122
|
+
objOrMsg: object | string,
|
|
123
|
+
msg?: string,
|
|
124
|
+
...args: any[]
|
|
125
|
+
): void => {
|
|
126
|
+
// Forward to underlying logger if present
|
|
127
|
+
if (this.logger) {
|
|
128
|
+
if (typeof objOrMsg === 'string') {
|
|
129
|
+
this.logger.fatal(objOrMsg);
|
|
130
|
+
} else {
|
|
131
|
+
this.logger.fatal(objOrMsg as any, msg, ...args);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Send to Telescope as error level
|
|
136
|
+
let context: Record<string, unknown>;
|
|
137
|
+
let message: string;
|
|
138
|
+
|
|
139
|
+
if (typeof objOrMsg === 'string') {
|
|
140
|
+
context = { ...this.context, level: 'fatal' };
|
|
141
|
+
message = objOrMsg;
|
|
142
|
+
} else {
|
|
143
|
+
context = { ...this.context, ...objOrMsg, level: 'fatal' };
|
|
144
|
+
message = msg ?? '';
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
this.telescope.error(message, context, this.requestId).catch(() => {});
|
|
148
|
+
}) as LogFn;
|
|
149
|
+
|
|
150
|
+
trace: LogFn = ((
|
|
151
|
+
objOrMsg: object | string,
|
|
152
|
+
msg?: string,
|
|
153
|
+
...args: any[]
|
|
154
|
+
): void => {
|
|
155
|
+
// Forward to underlying logger if present
|
|
156
|
+
if (this.logger) {
|
|
157
|
+
if (typeof objOrMsg === 'string') {
|
|
158
|
+
this.logger.trace(objOrMsg);
|
|
159
|
+
} else {
|
|
160
|
+
this.logger.trace(objOrMsg as any, msg, ...args);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Send to Telescope as debug level
|
|
165
|
+
let context: Record<string, unknown>;
|
|
166
|
+
let message: string;
|
|
167
|
+
|
|
168
|
+
if (typeof objOrMsg === 'string') {
|
|
169
|
+
context = { ...this.context, level: 'trace' };
|
|
170
|
+
message = objOrMsg;
|
|
171
|
+
} else {
|
|
172
|
+
context = { ...this.context, ...objOrMsg, level: 'trace' };
|
|
173
|
+
message = msg ?? '';
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
this.telescope.debug(message, context, this.requestId).catch(() => {});
|
|
177
|
+
}) as LogFn;
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Creates a child logger with additional context.
|
|
181
|
+
* The child logger inherits all context from the parent.
|
|
182
|
+
*/
|
|
183
|
+
child(obj: object): Logger {
|
|
184
|
+
return new TelescopeLogger({
|
|
185
|
+
telescope: this.telescope,
|
|
186
|
+
logger: this.logger?.child(obj),
|
|
187
|
+
requestId: this.requestId,
|
|
188
|
+
context: { ...this.context, ...obj },
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Create a child logger bound to a specific request ID.
|
|
194
|
+
* Useful for correlating logs with HTTP requests.
|
|
195
|
+
*/
|
|
196
|
+
withRequestId(requestId: string): TelescopeLogger {
|
|
197
|
+
return new TelescopeLogger({
|
|
198
|
+
telescope: this.telescope,
|
|
199
|
+
logger: this.logger,
|
|
200
|
+
requestId,
|
|
201
|
+
context: this.context,
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Create a logger that sends logs to Telescope.
|
|
208
|
+
* Convenience function for creating a TelescopeLogger.
|
|
209
|
+
*
|
|
210
|
+
* @example
|
|
211
|
+
* ```typescript
|
|
212
|
+
* import { createTelescopeLogger } from '@geekmidas/telescope/logger/console';
|
|
213
|
+
* import { ConsoleLogger } from '@geekmidas/logger/console';
|
|
214
|
+
*
|
|
215
|
+
* const telescope = new Telescope({ storage: new InMemoryStorage() });
|
|
216
|
+
* const baseLogger = new ConsoleLogger({ app: 'myApp' });
|
|
217
|
+
*
|
|
218
|
+
* const logger = createTelescopeLogger(telescope, baseLogger);
|
|
219
|
+
* logger.info({ action: 'startup' }, 'Application started');
|
|
220
|
+
* ```
|
|
221
|
+
*/
|
|
222
|
+
export function createTelescopeLogger(
|
|
223
|
+
telescope: Telescope,
|
|
224
|
+
logger?: Logger,
|
|
225
|
+
context?: Record<string, unknown>,
|
|
226
|
+
): TelescopeLogger {
|
|
227
|
+
return new TelescopeLogger({ telescope, logger, context });
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export type { Telescope };
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import build from 'pino-abstract-transport';
|
|
2
|
+
import type { Telescope } from '../Telescope';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Pino log object structure after parsing
|
|
6
|
+
*/
|
|
7
|
+
export interface PinoLogObject {
|
|
8
|
+
level: number;
|
|
9
|
+
time: number;
|
|
10
|
+
pid?: number;
|
|
11
|
+
hostname?: string;
|
|
12
|
+
msg?: string;
|
|
13
|
+
[key: string]: unknown;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface TelescopePinoTransportOptions {
|
|
17
|
+
/**
|
|
18
|
+
* The Telescope instance to send logs to
|
|
19
|
+
*/
|
|
20
|
+
telescope: Telescope;
|
|
21
|
+
/**
|
|
22
|
+
* Request ID to associate logs with a specific request.
|
|
23
|
+
* Can be a static string or a function that extracts the ID from log data.
|
|
24
|
+
*/
|
|
25
|
+
requestId?: string | ((data: PinoLogObject) => string | undefined);
|
|
26
|
+
/**
|
|
27
|
+
* Batch size before flushing to Telescope (default: 100)
|
|
28
|
+
*/
|
|
29
|
+
batchSize?: number;
|
|
30
|
+
/**
|
|
31
|
+
* Flush interval in milliseconds (default: 1000ms)
|
|
32
|
+
*/
|
|
33
|
+
flushIntervalMs?: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
type TelescopeLogLevel = 'debug' | 'info' | 'warn' | 'error';
|
|
37
|
+
|
|
38
|
+
interface LogBatch {
|
|
39
|
+
level: TelescopeLogLevel;
|
|
40
|
+
message: string;
|
|
41
|
+
context: Record<string, unknown>;
|
|
42
|
+
requestId?: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Map Pino numeric levels to Telescope log levels
|
|
47
|
+
*/
|
|
48
|
+
function mapPinoLevel(level: number): TelescopeLogLevel {
|
|
49
|
+
// Pino levels: trace=10, debug=20, info=30, warn=40, error=50, fatal=60
|
|
50
|
+
if (level <= 20) return 'debug'; // trace, debug
|
|
51
|
+
if (level <= 30) return 'info';
|
|
52
|
+
if (level <= 40) return 'warn';
|
|
53
|
+
return 'error'; // error, fatal
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Create a Pino transport that sends logs to Telescope.
|
|
58
|
+
*
|
|
59
|
+
* Uses pino-abstract-transport for proper async iteration and backpressure handling.
|
|
60
|
+
* Logs are batched for performance and flushed either when the batch is full
|
|
61
|
+
* or after the flush interval.
|
|
62
|
+
*
|
|
63
|
+
* @example
|
|
64
|
+
* ```typescript
|
|
65
|
+
* import pino from 'pino';
|
|
66
|
+
* import { Telescope, InMemoryStorage } from '@geekmidas/telescope';
|
|
67
|
+
* import { createPinoTransport } from '@geekmidas/telescope/logger/pino';
|
|
68
|
+
*
|
|
69
|
+
* const telescope = new Telescope({ storage: new InMemoryStorage() });
|
|
70
|
+
*
|
|
71
|
+
* // Use with pino.multistream to log to both stdout and Telescope
|
|
72
|
+
* const logger = pino(
|
|
73
|
+
* { level: 'debug' },
|
|
74
|
+
* pino.multistream([
|
|
75
|
+
* { stream: process.stdout },
|
|
76
|
+
* { stream: createPinoTransport({ telescope }) }
|
|
77
|
+
* ])
|
|
78
|
+
* );
|
|
79
|
+
*
|
|
80
|
+
* logger.info({ userId: '123' }, 'User logged in');
|
|
81
|
+
* ```
|
|
82
|
+
*/
|
|
83
|
+
export function createPinoTransport(options: TelescopePinoTransportOptions) {
|
|
84
|
+
const {
|
|
85
|
+
telescope,
|
|
86
|
+
requestId,
|
|
87
|
+
batchSize = 100,
|
|
88
|
+
flushIntervalMs = 1000,
|
|
89
|
+
} = options;
|
|
90
|
+
|
|
91
|
+
const batch: LogBatch[] = [];
|
|
92
|
+
let flushTimer: ReturnType<typeof setTimeout> | null = null;
|
|
93
|
+
|
|
94
|
+
async function flush(): Promise<void> {
|
|
95
|
+
if (batch.length === 0) return;
|
|
96
|
+
|
|
97
|
+
const toFlush = batch.splice(0, batch.length);
|
|
98
|
+
|
|
99
|
+
// Use batch insert for better performance
|
|
100
|
+
try {
|
|
101
|
+
await telescope.log(toFlush);
|
|
102
|
+
} catch {
|
|
103
|
+
// Silently ignore errors to not break logging
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function scheduleFlush(): void {
|
|
108
|
+
if (flushTimer === null) {
|
|
109
|
+
flushTimer = setTimeout(() => {
|
|
110
|
+
flushTimer = null;
|
|
111
|
+
flush().catch(() => {
|
|
112
|
+
// Silently ignore flush errors
|
|
113
|
+
});
|
|
114
|
+
}, flushIntervalMs);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function clearFlushTimer(): void {
|
|
119
|
+
if (flushTimer !== null) {
|
|
120
|
+
clearTimeout(flushTimer);
|
|
121
|
+
flushTimer = null;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return build(
|
|
126
|
+
async (source) => {
|
|
127
|
+
for await (const obj of source) {
|
|
128
|
+
const data = obj as PinoLogObject;
|
|
129
|
+
const { level, msg, time, pid, hostname, ...context } = data;
|
|
130
|
+
|
|
131
|
+
const telescopeLevel = mapPinoLevel(level);
|
|
132
|
+
|
|
133
|
+
// Extract request ID
|
|
134
|
+
let reqId: string | undefined;
|
|
135
|
+
if (typeof requestId === 'function') {
|
|
136
|
+
reqId = requestId(data);
|
|
137
|
+
} else if (typeof requestId === 'string') {
|
|
138
|
+
reqId = requestId;
|
|
139
|
+
} else if (typeof context.requestId === 'string') {
|
|
140
|
+
reqId = context.requestId;
|
|
141
|
+
delete context.requestId;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
batch.push({
|
|
145
|
+
level: telescopeLevel,
|
|
146
|
+
message: msg || '',
|
|
147
|
+
context,
|
|
148
|
+
requestId: reqId,
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
if (batch.length >= batchSize) {
|
|
152
|
+
clearFlushTimer();
|
|
153
|
+
await flush();
|
|
154
|
+
} else {
|
|
155
|
+
scheduleFlush();
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
async close() {
|
|
161
|
+
clearFlushTimer();
|
|
162
|
+
await flush();
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Create a Pino destination that sends logs to Telescope.
|
|
170
|
+
* Alias for createPinoTransport for API compatibility.
|
|
171
|
+
*
|
|
172
|
+
* @example
|
|
173
|
+
* ```typescript
|
|
174
|
+
* import pino from 'pino';
|
|
175
|
+
* import { createPinoDestination } from '@geekmidas/telescope/logger/pino';
|
|
176
|
+
*
|
|
177
|
+
* const telescope = new Telescope({ storage: new InMemoryStorage() });
|
|
178
|
+
*
|
|
179
|
+
* // Use with pino.multistream to log to both stdout and Telescope
|
|
180
|
+
* const logger = pino(
|
|
181
|
+
* { level: 'debug' },
|
|
182
|
+
* pino.multistream([
|
|
183
|
+
* { stream: process.stdout },
|
|
184
|
+
* { stream: createPinoDestination({ telescope }) }
|
|
185
|
+
* ])
|
|
186
|
+
* );
|
|
187
|
+
* ```
|
|
188
|
+
*/
|
|
189
|
+
export const createPinoDestination = createPinoTransport;
|
|
190
|
+
|
|
191
|
+
export type { Telescope };
|