@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.
Files changed (103) hide show
  1. package/README.md +521 -0
  2. package/dist/Telescope-B3Wd82yk.cjs +602 -0
  3. package/dist/Telescope-B3Wd82yk.cjs.map +1 -0
  4. package/dist/Telescope-C5dyDYYB.d.cts +133 -0
  5. package/dist/Telescope-D-uoZB6b.mjs +596 -0
  6. package/dist/Telescope-D-uoZB6b.mjs.map +1 -0
  7. package/dist/Telescope-DyIWgh9-.d.mts +133 -0
  8. package/dist/Telescope.cjs +3 -0
  9. package/dist/Telescope.d.cts +3 -0
  10. package/dist/Telescope.d.mts +3 -0
  11. package/dist/Telescope.mjs +3 -0
  12. package/dist/chunk-CUT6urMc.cjs +30 -0
  13. package/dist/index.cjs +5 -0
  14. package/dist/index.d.cts +4 -0
  15. package/dist/index.d.mts +4 -0
  16. package/dist/index.mjs +4 -0
  17. package/dist/logger/console.cjs +161 -0
  18. package/dist/logger/console.cjs.map +1 -0
  19. package/dist/logger/console.d.cts +109 -0
  20. package/dist/logger/console.d.mts +109 -0
  21. package/dist/logger/console.mjs +159 -0
  22. package/dist/logger/console.mjs.map +1 -0
  23. package/dist/logger/pino.cjs +118 -0
  24. package/dist/logger/pino.cjs.map +1 -0
  25. package/dist/logger/pino.d.cts +89 -0
  26. package/dist/logger/pino.d.mts +89 -0
  27. package/dist/logger/pino.mjs +116 -0
  28. package/dist/logger/pino.mjs.map +1 -0
  29. package/dist/memory-9-B9WACq.cjs +110 -0
  30. package/dist/memory-9-B9WACq.cjs.map +1 -0
  31. package/dist/memory-Cm0eevCS.d.mts +38 -0
  32. package/dist/memory-DiP1a-pp.d.cts +38 -0
  33. package/dist/memory-SdN5vtG9.mjs +104 -0
  34. package/dist/memory-SdN5vtG9.mjs.map +1 -0
  35. package/dist/server/hono.cjs +180 -0
  36. package/dist/server/hono.cjs.map +1 -0
  37. package/dist/server/hono.d.cts +26 -0
  38. package/dist/server/hono.d.mts +26 -0
  39. package/dist/server/hono.mjs +176 -0
  40. package/dist/server/hono.mjs.map +1 -0
  41. package/dist/storage/kysely.cjs +336 -0
  42. package/dist/storage/kysely.cjs.map +1 -0
  43. package/dist/storage/kysely.d.cts +161 -0
  44. package/dist/storage/kysely.d.mts +161 -0
  45. package/dist/storage/kysely.mjs +334 -0
  46. package/dist/storage/kysely.mjs.map +1 -0
  47. package/dist/storage/memory.cjs +3 -0
  48. package/dist/storage/memory.d.cts +3 -0
  49. package/dist/storage/memory.d.mts +3 -0
  50. package/dist/storage/memory.mjs +3 -0
  51. package/dist/types-BGDhFv4R.d.cts +170 -0
  52. package/dist/types-CZbzz8kx.d.mts +170 -0
  53. package/dist/types.cjs +0 -0
  54. package/dist/types.d.cts +2 -0
  55. package/dist/types.d.mts +2 -0
  56. package/dist/types.mjs +0 -0
  57. package/dist/ui-assets-D6-8TAr_.mjs +30 -0
  58. package/dist/ui-assets-D6-8TAr_.mjs.map +1 -0
  59. package/dist/ui-assets-ulevVble.cjs +48 -0
  60. package/dist/ui-assets-ulevVble.cjs.map +1 -0
  61. package/dist/ui-assets.cjs +5 -0
  62. package/dist/ui-assets.d.cts +12 -0
  63. package/dist/ui-assets.d.mts +12 -0
  64. package/dist/ui-assets.mjs +3 -0
  65. package/package.json +83 -0
  66. package/scripts/embed-ui.ts +90 -0
  67. package/src/Telescope.ts +714 -0
  68. package/src/__tests__/Telescope.spec.ts +356 -0
  69. package/src/index.ts +23 -0
  70. package/src/logger/__tests__/console.spec.ts +266 -0
  71. package/src/logger/__tests__/pino.spec.ts +217 -0
  72. package/src/logger/console.ts +230 -0
  73. package/src/logger/pino.ts +191 -0
  74. package/src/server/__tests__/hono.spec.ts +340 -0
  75. package/src/server/hono.ts +247 -0
  76. package/src/storage/__tests__/kysely.spec.ts +715 -0
  77. package/src/storage/__tests__/memory.spec.ts +411 -0
  78. package/src/storage/kysely.ts +572 -0
  79. package/src/storage/memory.ts +168 -0
  80. package/src/types.ts +188 -0
  81. package/src/ui-assets.ts +40 -0
  82. package/ui/index.html +12 -0
  83. package/ui/node_modules/.bin/browserslist +21 -0
  84. package/ui/node_modules/.bin/jiti +21 -0
  85. package/ui/node_modules/.bin/terser +21 -0
  86. package/ui/node_modules/.bin/tsc +21 -0
  87. package/ui/node_modules/.bin/tsserver +21 -0
  88. package/ui/node_modules/.bin/tsx +21 -0
  89. package/ui/node_modules/.bin/vite +21 -0
  90. package/ui/package.json +24 -0
  91. package/ui/src/App.tsx +342 -0
  92. package/ui/src/api.ts +75 -0
  93. package/ui/src/components/ExceptionDetail.tsx +100 -0
  94. package/ui/src/components/LogDetail.tsx +91 -0
  95. package/ui/src/components/RequestDetail.tsx +143 -0
  96. package/ui/src/main.tsx +10 -0
  97. package/ui/src/styles.css +10 -0
  98. package/ui/src/types.ts +63 -0
  99. package/ui/src/vite-env.d.ts +1 -0
  100. package/ui/src/vite-plugin-gkm-config.ts +54 -0
  101. package/ui/tsconfig.json +20 -0
  102. package/ui/tsconfig.tsbuildinfo +14 -0
  103. package/ui/vite.config.ts +13 -0
@@ -0,0 +1,356 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import { Telescope } from '../Telescope';
3
+ import { InMemoryStorage } from '../storage/memory';
4
+
5
+ describe('Telescope', () => {
6
+ let telescope: Telescope;
7
+ let storage: InMemoryStorage;
8
+
9
+ beforeEach(() => {
10
+ storage = new InMemoryStorage();
11
+ telescope = new Telescope({ storage });
12
+ });
13
+
14
+ afterEach(() => {
15
+ telescope.destroy();
16
+ });
17
+
18
+ describe('constructor', () => {
19
+ it('should create with default options', () => {
20
+ expect(telescope.enabled).toBe(true);
21
+ expect(telescope.recordBody).toBe(true);
22
+ });
23
+
24
+ it('should respect enabled option', () => {
25
+ const disabled = new Telescope({ storage, enabled: false });
26
+ expect(disabled.enabled).toBe(false);
27
+ disabled.destroy();
28
+ });
29
+
30
+ it('should respect recordBody option', () => {
31
+ const noBody = new Telescope({ storage, recordBody: false });
32
+ expect(noBody.recordBody).toBe(false);
33
+ noBody.destroy();
34
+ });
35
+ });
36
+
37
+ describe('recordRequest', () => {
38
+ it('should record a request and return an ID', async () => {
39
+ const requestId = await telescope.recordRequest({
40
+ method: 'GET',
41
+ path: '/api/users',
42
+ url: 'http://localhost:3000/api/users',
43
+ headers: { 'content-type': 'application/json' },
44
+ query: { page: '1' },
45
+ status: 200,
46
+ responseHeaders: { 'content-type': 'application/json' },
47
+ responseBody: { users: [] },
48
+ duration: 50,
49
+ });
50
+
51
+ expect(requestId).toBeDefined();
52
+ expect(typeof requestId).toBe('string');
53
+
54
+ const request = await telescope.getRequest(requestId);
55
+ expect(request).not.toBeNull();
56
+ expect(request?.method).toBe('GET');
57
+ expect(request?.path).toBe('/api/users');
58
+ expect(request?.status).toBe(200);
59
+ });
60
+
61
+ it('should not record when disabled', async () => {
62
+ const disabled = new Telescope({ storage, enabled: false });
63
+
64
+ const requestId = await disabled.recordRequest({
65
+ method: 'GET',
66
+ path: '/api/test',
67
+ url: 'http://localhost/api/test',
68
+ headers: {},
69
+ query: {},
70
+ status: 200,
71
+ responseHeaders: {},
72
+ duration: 10,
73
+ });
74
+
75
+ expect(requestId).toBe('');
76
+
77
+ const requests = await storage.getRequests();
78
+ expect(requests).toHaveLength(0);
79
+
80
+ disabled.destroy();
81
+ });
82
+ });
83
+
84
+ describe('logging', () => {
85
+ it('should record a log entry with info()', async () => {
86
+ await telescope.info('User logged in', { userId: '123' });
87
+
88
+ const logs = await telescope.getLogs();
89
+ expect(logs).toHaveLength(1);
90
+ expect(logs[0].level).toBe('info');
91
+ expect(logs[0].message).toBe('User logged in');
92
+ expect(logs[0].context).toEqual({ userId: '123' });
93
+ });
94
+
95
+ it('should record different log levels', async () => {
96
+ await telescope.debug('Debug message');
97
+ await telescope.info('Info message');
98
+ await telescope.warn('Warning message');
99
+ await telescope.error('Error message');
100
+
101
+ const logs = await telescope.getLogs();
102
+ expect(logs).toHaveLength(4);
103
+
104
+ const levels = logs.map((l) => l.level);
105
+ expect(levels).toContain('debug');
106
+ expect(levels).toContain('info');
107
+ expect(levels).toContain('warn');
108
+ expect(levels).toContain('error');
109
+ });
110
+
111
+ it('should associate log with request ID', async () => {
112
+ const requestId = await telescope.recordRequest({
113
+ method: 'POST',
114
+ path: '/api/login',
115
+ url: 'http://localhost/api/login',
116
+ headers: {},
117
+ query: {},
118
+ status: 200,
119
+ responseHeaders: {},
120
+ duration: 100,
121
+ });
122
+
123
+ await telescope.info('Login successful', {}, requestId);
124
+
125
+ const logs = await telescope.getLogs();
126
+ expect(logs[0].requestId).toBe(requestId);
127
+ });
128
+
129
+ it('should not record when disabled', async () => {
130
+ const disabled = new Telescope({ storage, enabled: false });
131
+
132
+ await disabled.info('Test message');
133
+
134
+ const logs = await storage.getLogs();
135
+ expect(logs).toHaveLength(0);
136
+
137
+ disabled.destroy();
138
+ });
139
+
140
+ it('should batch log entries with log()', async () => {
141
+ await telescope.log([
142
+ { level: 'info', message: 'First' },
143
+ { level: 'debug', message: 'Second', context: { step: 2 } },
144
+ { level: 'warn', message: 'Third' },
145
+ ]);
146
+
147
+ const logs = await telescope.getLogs();
148
+ expect(logs).toHaveLength(3);
149
+ });
150
+ });
151
+
152
+ describe('exception', () => {
153
+ it('should record an exception with stack trace', async () => {
154
+ const error = new Error('Something went wrong');
155
+
156
+ await telescope.exception(error);
157
+
158
+ const exceptions = await telescope.getExceptions();
159
+ expect(exceptions).toHaveLength(1);
160
+ expect(exceptions[0].name).toBe('Error');
161
+ expect(exceptions[0].message).toBe('Something went wrong');
162
+ expect(exceptions[0].stack.length).toBeGreaterThan(0);
163
+ });
164
+
165
+ it('should associate exception with request ID', async () => {
166
+ const requestId = await telescope.recordRequest({
167
+ method: 'GET',
168
+ path: '/api/error',
169
+ url: 'http://localhost/api/error',
170
+ headers: {},
171
+ query: {},
172
+ status: 500,
173
+ responseHeaders: {},
174
+ duration: 10,
175
+ });
176
+
177
+ await telescope.exception(new Error('Request failed'), requestId);
178
+
179
+ const exceptions = await telescope.getExceptions();
180
+ expect(exceptions[0].requestId).toBe(requestId);
181
+ });
182
+ });
183
+
184
+ describe('shouldIgnore', () => {
185
+ it('should ignore paths matching patterns', () => {
186
+ const withPatterns = new Telescope({
187
+ storage,
188
+ ignorePatterns: ['/health', '/metrics', '/__telescope/*'],
189
+ });
190
+
191
+ expect(withPatterns.shouldIgnore('/health')).toBe(true);
192
+ expect(withPatterns.shouldIgnore('/metrics')).toBe(true);
193
+ expect(withPatterns.shouldIgnore('/__telescope/api/requests')).toBe(true);
194
+ expect(withPatterns.shouldIgnore('/api/users')).toBe(false);
195
+
196
+ withPatterns.destroy();
197
+ });
198
+
199
+ it('should return false when no patterns configured', () => {
200
+ expect(telescope.shouldIgnore('/any/path')).toBe(false);
201
+ });
202
+ });
203
+
204
+ describe('getRequests', () => {
205
+ it('should return requests with query options', async () => {
206
+ for (let i = 0; i < 5; i++) {
207
+ await telescope.recordRequest({
208
+ method: 'GET',
209
+ path: `/api/item/${i}`,
210
+ url: `http://localhost/api/item/${i}`,
211
+ headers: {},
212
+ query: {},
213
+ status: 200,
214
+ responseHeaders: {},
215
+ duration: 10,
216
+ });
217
+ }
218
+
219
+ const requests = await telescope.getRequests({ limit: 2 });
220
+ expect(requests).toHaveLength(2);
221
+ });
222
+ });
223
+
224
+ describe('getStats', () => {
225
+ it('should return aggregated statistics', async () => {
226
+ await telescope.recordRequest({
227
+ method: 'GET',
228
+ path: '/api/test',
229
+ url: 'http://localhost/api/test',
230
+ headers: {},
231
+ query: {},
232
+ status: 200,
233
+ responseHeaders: {},
234
+ duration: 10,
235
+ });
236
+
237
+ await telescope.info('Test log');
238
+ await telescope.exception(new Error('Test error'));
239
+
240
+ const stats = await telescope.getStats();
241
+
242
+ expect(stats.requests).toBe(1);
243
+ expect(stats.logs).toBe(1);
244
+ expect(stats.exceptions).toBe(1);
245
+ });
246
+ });
247
+
248
+ describe('prune', () => {
249
+ it('should remove old entries', async () => {
250
+ // Add an old entry directly to storage
251
+ await storage.saveRequest({
252
+ id: 'old-req',
253
+ method: 'GET',
254
+ path: '/old',
255
+ url: 'http://localhost/old',
256
+ headers: {},
257
+ query: {},
258
+ status: 200,
259
+ responseHeaders: {},
260
+ duration: 10,
261
+ timestamp: new Date('2020-01-01'),
262
+ });
263
+
264
+ // Add a new entry
265
+ await telescope.recordRequest({
266
+ method: 'GET',
267
+ path: '/new',
268
+ url: 'http://localhost/new',
269
+ headers: {},
270
+ query: {},
271
+ status: 200,
272
+ responseHeaders: {},
273
+ duration: 10,
274
+ });
275
+
276
+ const deleted = await telescope.prune(new Date('2023-01-01'));
277
+
278
+ expect(deleted).toBe(1);
279
+
280
+ const requests = await telescope.getRequests();
281
+ expect(requests).toHaveLength(1);
282
+ expect(requests[0].path).toBe('/new');
283
+ });
284
+ });
285
+
286
+ describe('auto-prune', () => {
287
+ it('should configure pruneAfterHours option', () => {
288
+ const autoPrune = new Telescope({
289
+ storage,
290
+ pruneAfterHours: 1,
291
+ });
292
+
293
+ // Verify the telescope was created with pruneAfterHours
294
+ // The actual pruning is tested via the prune() method
295
+ expect(autoPrune).toBeDefined();
296
+
297
+ autoPrune.destroy();
298
+ });
299
+ });
300
+
301
+ describe('WebSocket clients', () => {
302
+ it('should manage WebSocket client connections', () => {
303
+ const mockWs = {
304
+ send: vi.fn(),
305
+ readyState: 1, // OPEN
306
+ } as unknown as WebSocket;
307
+
308
+ telescope.addWsClient(mockWs);
309
+
310
+ // Broadcast should send to client (including the 'connected' event)
311
+ const callCount = mockWs.send.mock.calls.length;
312
+ expect(callCount).toBeGreaterThan(0);
313
+
314
+ telescope.removeWsClient(mockWs);
315
+
316
+ // After removal, broadcast should not send
317
+ const newMock = vi.fn();
318
+ mockWs.send = newMock;
319
+ telescope.broadcast({
320
+ type: 'request',
321
+ payload: {},
322
+ timestamp: Date.now(),
323
+ });
324
+
325
+ expect(newMock).not.toHaveBeenCalled();
326
+ });
327
+
328
+ it('should broadcast to all connected clients', () => {
329
+ const ws1 = { send: vi.fn() } as unknown as WebSocket;
330
+ const ws2 = { send: vi.fn() } as unknown as WebSocket;
331
+
332
+ telescope.addWsClient(ws1);
333
+ telescope.addWsClient(ws2);
334
+
335
+ telescope.broadcast({
336
+ type: 'log',
337
+ payload: { test: true },
338
+ timestamp: Date.now(),
339
+ });
340
+
341
+ // Both should receive the broadcast
342
+ expect(ws1.send).toHaveBeenCalled();
343
+ expect(ws2.send).toHaveBeenCalled();
344
+ });
345
+ });
346
+
347
+ describe('getDashboardHtml', () => {
348
+ it('should return HTML string', () => {
349
+ const html = telescope.getDashboardHtml();
350
+
351
+ expect(typeof html).toBe('string');
352
+ expect(html).toContain('<!DOCTYPE html>');
353
+ expect(html).toContain('Telescope');
354
+ });
355
+ });
356
+ });
package/src/index.ts ADDED
@@ -0,0 +1,23 @@
1
+ // Core
2
+ export { Telescope } from './Telescope';
3
+
4
+ // Storage
5
+ export { InMemoryStorage } from './storage/memory';
6
+ export type { InMemoryStorageOptions } from './storage/memory';
7
+
8
+ // Types
9
+ export type {
10
+ ExceptionEntry,
11
+ LogEntry,
12
+ NormalizedTelescopeOptions,
13
+ QueryOptions,
14
+ RequestContext,
15
+ RequestEntry,
16
+ SourceContext,
17
+ StackFrame,
18
+ TelescopeEvent,
19
+ TelescopeEventType,
20
+ TelescopeOptions,
21
+ TelescopeStats,
22
+ TelescopeStorage,
23
+ } from './types';
@@ -0,0 +1,266 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import { Telescope } from '../../Telescope';
3
+ import { InMemoryStorage } from '../../storage/memory';
4
+ import { TelescopeLogger, createTelescopeLogger } from '../console';
5
+ import type { Logger } from '../console';
6
+
7
+ describe('TelescopeLogger', () => {
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
+ describe('without underlying logger', () => {
21
+ it('should log to Telescope only', async () => {
22
+ const logger = new TelescopeLogger({ telescope });
23
+
24
+ logger.info({ userId: '123' }, 'User logged in');
25
+
26
+ // Wait for async log
27
+ await new Promise((r) => setTimeout(r, 10));
28
+
29
+ const logs = await telescope.getLogs();
30
+ expect(logs).toHaveLength(1);
31
+ expect(logs[0].level).toBe('info');
32
+ expect(logs[0].message).toBe('User logged in');
33
+ expect(logs[0].context).toEqual({ userId: '123' });
34
+ });
35
+
36
+ it('should handle string-only logging', async () => {
37
+ const logger = new TelescopeLogger({ telescope });
38
+
39
+ logger.debug('Simple debug message');
40
+
41
+ await new Promise((r) => setTimeout(r, 10));
42
+
43
+ const logs = await telescope.getLogs();
44
+ expect(logs[0].message).toBe('Simple debug message');
45
+ expect(logs[0].level).toBe('debug');
46
+ });
47
+ });
48
+
49
+ describe('with underlying logger', () => {
50
+ it('should forward logs to both Telescope and underlying logger', async () => {
51
+ const mockLogger: Logger = {
52
+ debug: vi.fn(),
53
+ info: vi.fn(),
54
+ warn: vi.fn(),
55
+ error: vi.fn(),
56
+ fatal: vi.fn(),
57
+ trace: vi.fn(),
58
+ child: vi.fn(() => mockLogger),
59
+ };
60
+
61
+ const logger = new TelescopeLogger({ telescope, logger: mockLogger });
62
+
63
+ logger.info({ action: 'test' }, 'Test message');
64
+
65
+ expect(mockLogger.info).toHaveBeenCalledWith(
66
+ { action: 'test' },
67
+ 'Test message',
68
+ );
69
+
70
+ await new Promise((r) => setTimeout(r, 10));
71
+
72
+ const logs = await telescope.getLogs();
73
+ expect(logs).toHaveLength(1);
74
+ });
75
+
76
+ it('should forward string-only logs to underlying logger', async () => {
77
+ const mockLogger: Logger = {
78
+ debug: vi.fn(),
79
+ info: vi.fn(),
80
+ warn: vi.fn(),
81
+ error: vi.fn(),
82
+ fatal: vi.fn(),
83
+ trace: vi.fn(),
84
+ child: vi.fn(() => mockLogger),
85
+ };
86
+
87
+ const logger = new TelescopeLogger({ telescope, logger: mockLogger });
88
+
89
+ logger.warn('Warning message');
90
+
91
+ expect(mockLogger.warn).toHaveBeenCalledWith('Warning message');
92
+ });
93
+ });
94
+
95
+ describe('log levels', () => {
96
+ it('should support all log levels', async () => {
97
+ const logger = new TelescopeLogger({ telescope });
98
+
99
+ logger.debug({}, 'Debug');
100
+ logger.info({}, 'Info');
101
+ logger.warn({}, 'Warn');
102
+ logger.error({}, 'Error');
103
+
104
+ await new Promise((r) => setTimeout(r, 10));
105
+
106
+ const logs = await telescope.getLogs();
107
+ expect(logs).toHaveLength(4);
108
+
109
+ const levels = logs.map((l) => l.level);
110
+ expect(levels).toContain('debug');
111
+ expect(levels).toContain('info');
112
+ expect(levels).toContain('warn');
113
+ expect(levels).toContain('error');
114
+ });
115
+
116
+ it('should map fatal to error level for Telescope', async () => {
117
+ const logger = new TelescopeLogger({ telescope });
118
+
119
+ logger.fatal({}, 'Fatal error');
120
+
121
+ await new Promise((r) => setTimeout(r, 10));
122
+
123
+ const logs = await telescope.getLogs();
124
+ expect(logs[0].level).toBe('error');
125
+ expect(logs[0].context).toEqual({ level: 'fatal' });
126
+ });
127
+
128
+ it('should map trace to debug level for Telescope', async () => {
129
+ const logger = new TelescopeLogger({ telescope });
130
+
131
+ logger.trace({}, 'Trace message');
132
+
133
+ await new Promise((r) => setTimeout(r, 10));
134
+
135
+ const logs = await telescope.getLogs();
136
+ expect(logs[0].level).toBe('debug');
137
+ expect(logs[0].context).toEqual({ level: 'trace' });
138
+ });
139
+ });
140
+
141
+ describe('child loggers', () => {
142
+ it('should create child logger with inherited context', async () => {
143
+ const logger = new TelescopeLogger({
144
+ telescope,
145
+ context: { app: 'myApp' },
146
+ });
147
+
148
+ const childLogger = logger.child({ module: 'auth' });
149
+ childLogger.info({}, 'Child log');
150
+
151
+ await new Promise((r) => setTimeout(r, 10));
152
+
153
+ const logs = await telescope.getLogs();
154
+ expect(logs[0].context).toEqual({ app: 'myApp', module: 'auth' });
155
+ });
156
+
157
+ it('should create child of underlying logger too', async () => {
158
+ const childMock: Logger = {
159
+ debug: vi.fn(),
160
+ info: vi.fn(),
161
+ warn: vi.fn(),
162
+ error: vi.fn(),
163
+ fatal: vi.fn(),
164
+ trace: vi.fn(),
165
+ child: vi.fn(() => childMock),
166
+ };
167
+
168
+ const mockLogger: Logger = {
169
+ debug: vi.fn(),
170
+ info: vi.fn(),
171
+ warn: vi.fn(),
172
+ error: vi.fn(),
173
+ fatal: vi.fn(),
174
+ trace: vi.fn(),
175
+ child: vi.fn(() => childMock),
176
+ };
177
+
178
+ const logger = new TelescopeLogger({ telescope, logger: mockLogger });
179
+ const child = logger.child({ module: 'test' });
180
+
181
+ expect(mockLogger.child).toHaveBeenCalledWith({ module: 'test' });
182
+
183
+ child.info({}, 'Test');
184
+ expect(childMock.info).toHaveBeenCalled();
185
+ });
186
+ });
187
+
188
+ describe('withRequestId', () => {
189
+ it('should bind logs to a request ID', async () => {
190
+ const logger = new TelescopeLogger({ telescope });
191
+ const requestLogger = logger.withRequestId('req-123');
192
+
193
+ requestLogger.info({}, 'Request log');
194
+
195
+ await new Promise((r) => setTimeout(r, 10));
196
+
197
+ const logs = await telescope.getLogs();
198
+ expect(logs[0].requestId).toBe('req-123');
199
+ });
200
+
201
+ it('should preserve context when binding request ID', async () => {
202
+ const logger = new TelescopeLogger({
203
+ telescope,
204
+ context: { app: 'test' },
205
+ });
206
+ const requestLogger = logger.withRequestId('req-456');
207
+
208
+ requestLogger.info({ action: 'test' }, 'Log');
209
+
210
+ await new Promise((r) => setTimeout(r, 10));
211
+
212
+ const logs = await telescope.getLogs();
213
+ expect(logs[0].context).toEqual({ app: 'test', action: 'test' });
214
+ expect(logs[0].requestId).toBe('req-456');
215
+ });
216
+ });
217
+
218
+ describe('createTelescopeLogger factory', () => {
219
+ it('should create logger without underlying logger', async () => {
220
+ const logger = createTelescopeLogger(telescope);
221
+
222
+ logger.info({}, 'Factory test');
223
+
224
+ await new Promise((r) => setTimeout(r, 10));
225
+
226
+ const logs = await telescope.getLogs();
227
+ expect(logs).toHaveLength(1);
228
+ });
229
+
230
+ it('should create logger with underlying logger', async () => {
231
+ const mockLogger: Logger = {
232
+ debug: vi.fn(),
233
+ info: vi.fn(),
234
+ warn: vi.fn(),
235
+ error: vi.fn(),
236
+ fatal: vi.fn(),
237
+ trace: vi.fn(),
238
+ child: vi.fn(() => mockLogger),
239
+ };
240
+
241
+ const logger = createTelescopeLogger(telescope, mockLogger);
242
+
243
+ logger.error({ err: 'test' }, 'Error message');
244
+
245
+ expect(mockLogger.error).toHaveBeenCalled();
246
+
247
+ await new Promise((r) => setTimeout(r, 10));
248
+
249
+ const logs = await telescope.getLogs();
250
+ expect(logs).toHaveLength(1);
251
+ });
252
+
253
+ it('should create logger with initial context', async () => {
254
+ const logger = createTelescopeLogger(telescope, undefined, {
255
+ service: 'api',
256
+ });
257
+
258
+ logger.info({}, 'With context');
259
+
260
+ await new Promise((r) => setTimeout(r, 10));
261
+
262
+ const logs = await telescope.getLogs();
263
+ expect(logs[0].context).toEqual({ service: 'api' });
264
+ });
265
+ });
266
+ });