@geekmidas/telescope 9.0.2 → 10.0.0-alpha.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 +14 -4
- package/dist/{http-D5kn3_Mt.d.mts → http-BiNtg85L.d.mts} +3 -3
- package/dist/{http-D5kn3_Mt.d.mts.map → http-BiNtg85L.d.mts.map} +1 -1
- package/dist/instrumentation/http.d.mts +1 -1
- package/dist/instrumentation/index.cjs +2 -2
- package/dist/instrumentation/index.d.cts +2 -2
- package/dist/instrumentation/index.d.mts +3 -3
- package/dist/instrumentation/index.mjs +2 -2
- package/dist/instrumentation/middleware.cjs +1 -1
- package/dist/instrumentation/middleware.d.cts +1 -1
- package/dist/instrumentation/middleware.d.mts +1 -1
- package/dist/instrumentation/middleware.mjs +1 -1
- package/dist/instrumentation/otel.cjs +1 -1
- package/dist/instrumentation/otel.d.cts +1 -1
- package/dist/instrumentation/otel.d.mts +1 -1
- package/dist/instrumentation/otel.mjs +1 -1
- package/dist/logger/console.d.cts.map +1 -1
- package/dist/logger/pino.d.mts +2 -2
- package/dist/{middleware-DqPQpcup.cjs → middleware-Bcq6XXGG.cjs} +2 -2
- package/dist/middleware-Bcq6XXGG.cjs.map +1 -0
- package/dist/{middleware-4-GrQlOT.mjs → middleware-D1XEHvyy.mjs} +2 -2
- package/dist/middleware-D1XEHvyy.mjs.map +1 -0
- package/dist/{middleware-CA84nsNN.d.cts → middleware-PeyS-9Ny.d.cts} +2 -2
- package/dist/{middleware-CA84nsNN.d.cts.map → middleware-PeyS-9Ny.d.cts.map} +1 -1
- package/dist/{middleware-DAceVpfw.d.mts → middleware-oHvo_ZgU.d.mts} +2 -2
- package/dist/{middleware-DAceVpfw.d.mts.map → middleware-oHvo_ZgU.d.mts.map} +1 -1
- package/dist/{otel-40udNP5g.mjs → otel-B9bNoeZ2.mjs} +3 -3
- package/dist/otel-B9bNoeZ2.mjs.map +1 -0
- package/dist/{otel-SBVoVEHr.d.cts → otel-Bk89b1lX.d.cts} +3 -3
- package/dist/{otel-1bJPb2Z7.d.mts.map → otel-Bk89b1lX.d.cts.map} +1 -1
- package/dist/{otel-COrpz_GZ.cjs → otel-BzjOtVxZ.cjs} +3 -3
- package/dist/otel-BzjOtVxZ.cjs.map +1 -0
- package/dist/{otel-1bJPb2Z7.d.mts → otel-lPMsM26e.d.mts} +3 -3
- package/dist/{otel-SBVoVEHr.d.cts.map → otel-lPMsM26e.d.mts.map} +1 -1
- package/dist/storage/kysely.cjs +48 -38
- package/dist/storage/kysely.cjs.map +1 -1
- package/dist/storage/kysely.d.cts +29 -9
- package/dist/storage/kysely.d.cts.map +1 -1
- package/dist/storage/kysely.d.mts +29 -9
- package/dist/storage/kysely.d.mts.map +1 -1
- package/dist/storage/kysely.mjs +48 -38
- package/dist/storage/kysely.mjs.map +1 -1
- package/dist/tracing-PnECEbEu.d.mts.map +1 -1
- package/package.json +6 -3
- package/CHANGELOG.md +0 -72
- package/dist/middleware-4-GrQlOT.mjs.map +0 -1
- package/dist/middleware-DqPQpcup.cjs.map +0 -1
- package/dist/otel-40udNP5g.mjs.map +0 -1
- package/dist/otel-COrpz_GZ.cjs.map +0 -1
- package/src/Telescope.ts +0 -561
- package/src/__tests__/Telescope.spec.ts +0 -438
- package/src/__tests__/pino-transport.spec.ts +0 -115
- package/src/__tests__/redact.spec.ts +0 -336
- package/src/adapters/__tests__/lambda.spec.ts +0 -318
- package/src/adapters/__tests__/middy.spec.ts +0 -741
- package/src/adapters/index.ts +0 -22
- package/src/adapters/lambda.ts +0 -432
- package/src/adapters/middy.ts +0 -402
- package/src/adapters/types.ts +0 -181
- package/src/core.ts +0 -51
- package/src/index.ts +0 -34
- package/src/instrumentation/__tests__/core.spec.ts +0 -248
- package/src/instrumentation/__tests__/hono.spec.ts +0 -348
- package/src/instrumentation/__tests__/middleware.spec.ts +0 -528
- package/src/instrumentation/__tests__/otel.spec.ts +0 -291
- package/src/instrumentation/__tests__/tracing.spec.ts +0 -208
- package/src/instrumentation/core.ts +0 -228
- package/src/instrumentation/hono.ts +0 -270
- package/src/instrumentation/http.ts +0 -306
- package/src/instrumentation/index.ts +0 -90
- package/src/instrumentation/middleware.ts +0 -297
- package/src/instrumentation/otel.ts +0 -271
- package/src/instrumentation/setup.ts +0 -246
- package/src/instrumentation/tracing.ts +0 -145
- package/src/logger/__tests__/console.spec.ts +0 -366
- package/src/logger/__tests__/pino.spec.ts +0 -217
- package/src/logger/console.ts +0 -230
- package/src/logger/pino.ts +0 -201
- package/src/metrics/MetricsAggregator.ts +0 -555
- package/src/metrics/__tests__/MetricsAggregator.spec.ts +0 -298
- package/src/metrics/index.ts +0 -2
- package/src/metrics/storage/InMemoryMetricsStorage.ts +0 -380
- package/src/metrics/storage/MetricsStorage.ts +0 -74
- package/src/metrics/storage/index.ts +0 -3
- package/src/metrics/storage/types.ts +0 -243
- package/src/otlp/__tests__/receiver.spec.ts +0 -367
- package/src/otlp/__tests__/transformer.spec.ts +0 -455
- package/src/otlp/hono.ts +0 -120
- package/src/otlp/index.ts +0 -22
- package/src/otlp/receiver.ts +0 -162
- package/src/otlp/transformer.ts +0 -367
- package/src/otlp/types.ts +0 -364
- package/src/redact.ts +0 -116
- package/src/server/__tests__/hono.spec.ts +0 -814
- package/src/server/hono.ts +0 -442
- package/src/storage/__tests__/kysely.spec.ts +0 -715
- package/src/storage/__tests__/memory.spec.ts +0 -694
- package/src/storage/kysely.ts +0 -599
- package/src/storage/memory.ts +0 -204
- package/src/types.ts +0 -447
- package/src/ui-assets.ts +0 -40
- package/tsconfig.json +0 -9
- package/ui/CHANGELOG.md +0 -7
- package/ui/index.html +0 -12
- package/ui/node_modules/.bin/tsc +0 -21
- package/ui/node_modules/.bin/tsserver +0 -21
- package/ui/node_modules/.bin/vite +0 -21
- package/ui/package.json +0 -24
- package/ui/src/App.tsx +0 -595
- package/ui/src/api.ts +0 -88
- package/ui/src/components/ExceptionDetail.tsx +0 -100
- package/ui/src/components/FilterBar.tsx +0 -162
- package/ui/src/components/LogDetail.tsx +0 -91
- package/ui/src/components/RequestDetail.tsx +0 -143
- package/ui/src/main.tsx +0 -10
- package/ui/src/styles.css +0 -11
- package/ui/src/types.ts +0 -63
- package/ui/src/vite-env.d.ts +0 -1
- package/ui/src/vite-plugin-gkm-config.ts +0 -54
- package/ui/tsconfig.json +0 -20
- package/ui/tsconfig.tsbuildinfo +0 -1
- package/ui/vite.config.ts +0 -13
|
@@ -1,336 +0,0 @@
|
|
|
1
|
-
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
2
|
-
import { createRedactor, DEFAULT_REDACT_PATHS } from '../redact';
|
|
3
|
-
import { InMemoryStorage } from '../storage/memory';
|
|
4
|
-
import { Telescope } from '../Telescope';
|
|
5
|
-
|
|
6
|
-
describe('createRedactor', () => {
|
|
7
|
-
it('should return undefined when options is undefined', () => {
|
|
8
|
-
expect(createRedactor(undefined)).toBeUndefined();
|
|
9
|
-
});
|
|
10
|
-
|
|
11
|
-
it('should return undefined when options is false', () => {
|
|
12
|
-
expect(createRedactor(false)).toBeUndefined();
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
it('should create redactor with default paths when options is true', () => {
|
|
16
|
-
const redactor = createRedactor(true);
|
|
17
|
-
expect(redactor).toBeDefined();
|
|
18
|
-
|
|
19
|
-
const data = { headers: { authorization: 'Bearer secret' } };
|
|
20
|
-
const result = redactor?.(data);
|
|
21
|
-
expect(result.headers.authorization).toBe('[REDACTED]');
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
it('should merge custom paths with defaults when array is provided', () => {
|
|
25
|
-
const redactor = createRedactor(['custom.secret']);
|
|
26
|
-
expect(redactor).toBeDefined();
|
|
27
|
-
|
|
28
|
-
// Should redact default paths
|
|
29
|
-
const data1 = { headers: { authorization: 'Bearer token' } };
|
|
30
|
-
expect(redactor?.(data1).headers.authorization).toBe('[REDACTED]');
|
|
31
|
-
|
|
32
|
-
// Should redact custom paths
|
|
33
|
-
const data2 = { custom: { secret: 'mysecret' } };
|
|
34
|
-
expect(redactor?.(data2).custom.secret).toBe('[REDACTED]');
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
it('should use custom censor value', () => {
|
|
38
|
-
const redactor = createRedactor({ paths: [], censor: '***' });
|
|
39
|
-
expect(redactor).toBeDefined();
|
|
40
|
-
|
|
41
|
-
const data = { headers: { authorization: 'Bearer token' } };
|
|
42
|
-
const result = redactor?.(data);
|
|
43
|
-
expect(result.headers.authorization).toBe('***');
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
it('should not modify unmatched paths', () => {
|
|
47
|
-
const redactor = createRedactor(true);
|
|
48
|
-
|
|
49
|
-
const data = {
|
|
50
|
-
headers: { 'content-type': 'application/json' },
|
|
51
|
-
body: { username: 'john', email: 'john@example.com' },
|
|
52
|
-
};
|
|
53
|
-
const result = redactor?.(data);
|
|
54
|
-
|
|
55
|
-
expect(result.headers['content-type']).toBe('application/json');
|
|
56
|
-
expect(result.body.username).toBe('john');
|
|
57
|
-
expect(result.body.email).toBe('john@example.com');
|
|
58
|
-
});
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
describe('DEFAULT_REDACT_PATHS', () => {
|
|
62
|
-
it('should include common HTTP header paths', () => {
|
|
63
|
-
expect(DEFAULT_REDACT_PATHS).toContain('headers.authorization');
|
|
64
|
-
expect(DEFAULT_REDACT_PATHS).toContain('headers.cookie');
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
it('should include body sensitive field paths', () => {
|
|
68
|
-
expect(DEFAULT_REDACT_PATHS).toContain('body.password');
|
|
69
|
-
expect(DEFAULT_REDACT_PATHS).toContain('body.token');
|
|
70
|
-
expect(DEFAULT_REDACT_PATHS).toContain('body.apiKey');
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
it('should include context paths for log redaction', () => {
|
|
74
|
-
expect(DEFAULT_REDACT_PATHS).toContain('context.password');
|
|
75
|
-
expect(DEFAULT_REDACT_PATHS).toContain('context.token');
|
|
76
|
-
});
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
describe('Telescope with redaction', () => {
|
|
80
|
-
let telescope: Telescope;
|
|
81
|
-
let storage: InMemoryStorage;
|
|
82
|
-
|
|
83
|
-
beforeEach(() => {
|
|
84
|
-
storage = new InMemoryStorage();
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
afterEach(() => {
|
|
88
|
-
telescope?.destroy();
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
describe('recordRequest', () => {
|
|
92
|
-
it('should redact sensitive headers when redact is enabled', async () => {
|
|
93
|
-
telescope = new Telescope({ storage, redact: true });
|
|
94
|
-
|
|
95
|
-
await telescope.recordRequest({
|
|
96
|
-
method: 'GET',
|
|
97
|
-
path: '/api/users',
|
|
98
|
-
url: 'http://localhost/api/users',
|
|
99
|
-
headers: {
|
|
100
|
-
authorization: 'Bearer supersecrettoken',
|
|
101
|
-
cookie: 'session=abc123',
|
|
102
|
-
'content-type': 'application/json',
|
|
103
|
-
},
|
|
104
|
-
query: {},
|
|
105
|
-
status: 200,
|
|
106
|
-
responseHeaders: { 'content-type': 'application/json' },
|
|
107
|
-
duration: 50,
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
const requests = await telescope.getRequests();
|
|
111
|
-
expect(requests).toHaveLength(1);
|
|
112
|
-
|
|
113
|
-
const req = requests[0];
|
|
114
|
-
expect(req.headers.authorization).toBe('[REDACTED]');
|
|
115
|
-
expect(req.headers.cookie).toBe('[REDACTED]');
|
|
116
|
-
expect(req.headers['content-type']).toBe('application/json');
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
it('should redact sensitive body fields', async () => {
|
|
120
|
-
telescope = new Telescope({ storage, redact: true });
|
|
121
|
-
|
|
122
|
-
await telescope.recordRequest({
|
|
123
|
-
method: 'POST',
|
|
124
|
-
path: '/api/login',
|
|
125
|
-
url: 'http://localhost/api/login',
|
|
126
|
-
headers: {},
|
|
127
|
-
query: {},
|
|
128
|
-
body: {
|
|
129
|
-
username: 'john',
|
|
130
|
-
password: 'secret123',
|
|
131
|
-
token: 'refresh-token-value',
|
|
132
|
-
},
|
|
133
|
-
status: 200,
|
|
134
|
-
responseHeaders: {},
|
|
135
|
-
responseBody: {
|
|
136
|
-
accessToken: 'jwt-token',
|
|
137
|
-
user: { id: '123', name: 'John' },
|
|
138
|
-
},
|
|
139
|
-
duration: 100,
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
const requests = await telescope.getRequests();
|
|
143
|
-
const req = requests[0];
|
|
144
|
-
|
|
145
|
-
expect(req.body).toEqual({
|
|
146
|
-
username: 'john',
|
|
147
|
-
password: '[REDACTED]',
|
|
148
|
-
token: '[REDACTED]',
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
expect(req.responseBody).toEqual({
|
|
152
|
-
accessToken: '[REDACTED]',
|
|
153
|
-
user: { id: '123', name: 'John' },
|
|
154
|
-
});
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
it('should redact query parameters', async () => {
|
|
158
|
-
telescope = new Telescope({ storage, redact: true });
|
|
159
|
-
|
|
160
|
-
await telescope.recordRequest({
|
|
161
|
-
method: 'GET',
|
|
162
|
-
path: '/api/data',
|
|
163
|
-
url: 'http://localhost/api/data?token=secret&page=1',
|
|
164
|
-
headers: {},
|
|
165
|
-
query: {
|
|
166
|
-
token: 'secret-token',
|
|
167
|
-
api_key: 'my-api-key',
|
|
168
|
-
page: '1',
|
|
169
|
-
},
|
|
170
|
-
status: 200,
|
|
171
|
-
responseHeaders: {},
|
|
172
|
-
duration: 50,
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
const requests = await telescope.getRequests();
|
|
176
|
-
const req = requests[0];
|
|
177
|
-
|
|
178
|
-
expect(req.query?.token).toBe('[REDACTED]');
|
|
179
|
-
expect(req.query?.api_key).toBe('[REDACTED]');
|
|
180
|
-
expect(req.query?.page).toBe('1');
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
it('should not redact when redact option is not set', async () => {
|
|
184
|
-
telescope = new Telescope({ storage }); // No redact option
|
|
185
|
-
|
|
186
|
-
await telescope.recordRequest({
|
|
187
|
-
method: 'GET',
|
|
188
|
-
path: '/api/users',
|
|
189
|
-
url: 'http://localhost/api/users',
|
|
190
|
-
headers: { authorization: 'Bearer token' },
|
|
191
|
-
query: {},
|
|
192
|
-
status: 200,
|
|
193
|
-
responseHeaders: {},
|
|
194
|
-
duration: 50,
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
const requests = await telescope.getRequests();
|
|
198
|
-
expect(requests[0].headers.authorization).toBe('Bearer token');
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
it('should use custom censor value', async () => {
|
|
202
|
-
telescope = new Telescope({
|
|
203
|
-
storage,
|
|
204
|
-
redact: { paths: [], censor: '***HIDDEN***' },
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
await telescope.recordRequest({
|
|
208
|
-
method: 'GET',
|
|
209
|
-
path: '/api/users',
|
|
210
|
-
url: 'http://localhost/api/users',
|
|
211
|
-
headers: { authorization: 'Bearer token' },
|
|
212
|
-
query: {},
|
|
213
|
-
status: 200,
|
|
214
|
-
responseHeaders: {},
|
|
215
|
-
duration: 50,
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
const requests = await telescope.getRequests();
|
|
219
|
-
expect(requests[0].headers.authorization).toBe('***HIDDEN***');
|
|
220
|
-
});
|
|
221
|
-
});
|
|
222
|
-
|
|
223
|
-
describe('logging', () => {
|
|
224
|
-
it('should redact sensitive context in info()', async () => {
|
|
225
|
-
telescope = new Telescope({ storage, redact: true });
|
|
226
|
-
|
|
227
|
-
await telescope.info('User authenticated', {
|
|
228
|
-
userId: '123',
|
|
229
|
-
password: 'shouldnotshow',
|
|
230
|
-
token: 'sensitive-token',
|
|
231
|
-
});
|
|
232
|
-
|
|
233
|
-
const logs = await telescope.getLogs();
|
|
234
|
-
expect(logs).toHaveLength(1);
|
|
235
|
-
|
|
236
|
-
const log = logs[0];
|
|
237
|
-
expect(log.context?.userId).toBe('123');
|
|
238
|
-
expect(log.context?.password).toBe('[REDACTED]');
|
|
239
|
-
expect(log.context?.token).toBe('[REDACTED]');
|
|
240
|
-
});
|
|
241
|
-
|
|
242
|
-
it('should redact context in debug()', async () => {
|
|
243
|
-
telescope = new Telescope({ storage, redact: true });
|
|
244
|
-
|
|
245
|
-
await telescope.debug('Debug info', { secret: 'mysecret' });
|
|
246
|
-
|
|
247
|
-
const logs = await telescope.getLogs();
|
|
248
|
-
expect(logs[0].context?.secret).toBe('[REDACTED]');
|
|
249
|
-
});
|
|
250
|
-
|
|
251
|
-
it('should redact context in warn()', async () => {
|
|
252
|
-
telescope = new Telescope({ storage, redact: true });
|
|
253
|
-
|
|
254
|
-
await telescope.warn('Warning', { apiKey: 'key123' });
|
|
255
|
-
|
|
256
|
-
const logs = await telescope.getLogs();
|
|
257
|
-
expect(logs[0].context?.apiKey).toBe('[REDACTED]');
|
|
258
|
-
});
|
|
259
|
-
|
|
260
|
-
it('should redact context in error()', async () => {
|
|
261
|
-
telescope = new Telescope({ storage, redact: true });
|
|
262
|
-
|
|
263
|
-
await telescope.error('Error occurred', { password: 'pass' });
|
|
264
|
-
|
|
265
|
-
const logs = await telescope.getLogs();
|
|
266
|
-
expect(logs[0].context?.password).toBe('[REDACTED]');
|
|
267
|
-
});
|
|
268
|
-
|
|
269
|
-
it('should redact batch log entries', async () => {
|
|
270
|
-
telescope = new Telescope({ storage, redact: true });
|
|
271
|
-
|
|
272
|
-
await telescope.log([
|
|
273
|
-
{ level: 'info', message: 'First', context: { token: 'abc' } },
|
|
274
|
-
{ level: 'debug', message: 'Second', context: { password: 'xyz' } },
|
|
275
|
-
]);
|
|
276
|
-
|
|
277
|
-
const logs = await telescope.getLogs();
|
|
278
|
-
expect(logs).toHaveLength(2);
|
|
279
|
-
expect(logs.find((l) => l.message === 'First')?.context?.token).toBe(
|
|
280
|
-
'[REDACTED]',
|
|
281
|
-
);
|
|
282
|
-
expect(logs.find((l) => l.message === 'Second')?.context?.password).toBe(
|
|
283
|
-
'[REDACTED]',
|
|
284
|
-
);
|
|
285
|
-
});
|
|
286
|
-
|
|
287
|
-
it('should not redact log message', async () => {
|
|
288
|
-
telescope = new Telescope({ storage, redact: true });
|
|
289
|
-
|
|
290
|
-
await telescope.info('Password changed for user');
|
|
291
|
-
|
|
292
|
-
const logs = await telescope.getLogs();
|
|
293
|
-
expect(logs[0].message).toBe('Password changed for user');
|
|
294
|
-
});
|
|
295
|
-
});
|
|
296
|
-
|
|
297
|
-
describe('custom paths', () => {
|
|
298
|
-
it('should redact custom paths merged with defaults', async () => {
|
|
299
|
-
telescope = new Telescope({
|
|
300
|
-
storage,
|
|
301
|
-
redact: ['body.customSecret', 'context.privateData'],
|
|
302
|
-
});
|
|
303
|
-
|
|
304
|
-
await telescope.recordRequest({
|
|
305
|
-
method: 'POST',
|
|
306
|
-
path: '/api/data',
|
|
307
|
-
url: 'http://localhost/api/data',
|
|
308
|
-
headers: { authorization: 'Bearer token' },
|
|
309
|
-
query: {},
|
|
310
|
-
body: {
|
|
311
|
-
customSecret: 'my-custom-secret',
|
|
312
|
-
normalField: 'visible',
|
|
313
|
-
},
|
|
314
|
-
status: 200,
|
|
315
|
-
responseHeaders: {},
|
|
316
|
-
duration: 50,
|
|
317
|
-
});
|
|
318
|
-
|
|
319
|
-
await telescope.info('Log with custom field', {
|
|
320
|
-
privateData: 'hidden',
|
|
321
|
-
publicData: 'visible',
|
|
322
|
-
});
|
|
323
|
-
|
|
324
|
-
const requests = await telescope.getRequests();
|
|
325
|
-
expect(requests[0].headers.authorization).toBe('[REDACTED]');
|
|
326
|
-
expect(requests[0].body).toEqual({
|
|
327
|
-
customSecret: '[REDACTED]',
|
|
328
|
-
normalField: 'visible',
|
|
329
|
-
});
|
|
330
|
-
|
|
331
|
-
const logs = await telescope.getLogs();
|
|
332
|
-
expect(logs[0].context?.privateData).toBe('[REDACTED]');
|
|
333
|
-
expect(logs[0].context?.publicData).toBe('visible');
|
|
334
|
-
});
|
|
335
|
-
});
|
|
336
|
-
});
|
|
@@ -1,318 +0,0 @@
|
|
|
1
|
-
import type { Context as LambdaContext } from 'aws-lambda';
|
|
2
|
-
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
-
import { InMemoryStorage } from '../../storage/memory';
|
|
4
|
-
import { Telescope } from '../../Telescope';
|
|
5
|
-
import {
|
|
6
|
-
detectLambdaResources,
|
|
7
|
-
extractLambdaInvocationContext,
|
|
8
|
-
LambdaAdapter,
|
|
9
|
-
wrapLambdaHandler,
|
|
10
|
-
} from '../lambda';
|
|
11
|
-
|
|
12
|
-
describe('Lambda Adapter', () => {
|
|
13
|
-
let telescope: Telescope;
|
|
14
|
-
let storage: InMemoryStorage;
|
|
15
|
-
let mockContext: LambdaContext;
|
|
16
|
-
|
|
17
|
-
beforeEach(() => {
|
|
18
|
-
storage = new InMemoryStorage();
|
|
19
|
-
telescope = new Telescope({ storage });
|
|
20
|
-
|
|
21
|
-
mockContext = {
|
|
22
|
-
callbackWaitsForEmptyEventLoop: true,
|
|
23
|
-
functionName: 'test-function',
|
|
24
|
-
functionVersion: '1',
|
|
25
|
-
invokedFunctionArn: 'arn:aws:lambda:us-east-1:123456789:function:test',
|
|
26
|
-
memoryLimitInMB: '256',
|
|
27
|
-
awsRequestId: 'test-request-id-123',
|
|
28
|
-
logGroupName: '/aws/lambda/test',
|
|
29
|
-
logStreamName: '2024/01/01/[$LATEST]abc123',
|
|
30
|
-
getRemainingTimeInMillis: () => 10000,
|
|
31
|
-
done: vi.fn(),
|
|
32
|
-
fail: vi.fn(),
|
|
33
|
-
succeed: vi.fn(),
|
|
34
|
-
};
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
describe('detectLambdaResources', () => {
|
|
38
|
-
it('should detect Lambda environment variables', () => {
|
|
39
|
-
const originalEnv = { ...process.env };
|
|
40
|
-
|
|
41
|
-
process.env.AWS_REGION = 'us-west-2';
|
|
42
|
-
process.env.AWS_LAMBDA_FUNCTION_NAME = 'my-function';
|
|
43
|
-
process.env.AWS_LAMBDA_FUNCTION_VERSION = '$LATEST';
|
|
44
|
-
process.env.AWS_LAMBDA_LOG_STREAM_NAME = 'log-stream-123';
|
|
45
|
-
process.env.AWS_LAMBDA_FUNCTION_MEMORY_SIZE = '512';
|
|
46
|
-
|
|
47
|
-
const resources = detectLambdaResources();
|
|
48
|
-
|
|
49
|
-
expect(resources['cloud.provider']).toBe('aws');
|
|
50
|
-
expect(resources['cloud.region']).toBe('us-west-2');
|
|
51
|
-
expect(resources['faas.name']).toBe('my-function');
|
|
52
|
-
expect(resources['faas.version']).toBe('$LATEST');
|
|
53
|
-
expect(resources['faas.instance']).toBe('log-stream-123');
|
|
54
|
-
expect(resources['faas.max_memory']).toBe(512);
|
|
55
|
-
|
|
56
|
-
process.env = originalEnv;
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
it('should return defaults when environment variables are missing', () => {
|
|
60
|
-
const originalEnv = { ...process.env };
|
|
61
|
-
|
|
62
|
-
delete process.env.AWS_REGION;
|
|
63
|
-
delete process.env.AWS_LAMBDA_FUNCTION_NAME;
|
|
64
|
-
delete process.env.AWS_LAMBDA_FUNCTION_VERSION;
|
|
65
|
-
delete process.env.AWS_LAMBDA_LOG_STREAM_NAME;
|
|
66
|
-
delete process.env.AWS_LAMBDA_FUNCTION_MEMORY_SIZE;
|
|
67
|
-
|
|
68
|
-
const resources = detectLambdaResources();
|
|
69
|
-
|
|
70
|
-
expect(resources['cloud.provider']).toBe('aws');
|
|
71
|
-
expect(resources['cloud.region']).toBe('unknown');
|
|
72
|
-
expect(resources['faas.name']).toBe('unknown');
|
|
73
|
-
expect(resources['faas.version']).toBe('unknown');
|
|
74
|
-
expect(resources['faas.instance']).toBe('unknown');
|
|
75
|
-
expect(resources['faas.max_memory']).toBe(128); // Default memory
|
|
76
|
-
|
|
77
|
-
process.env = originalEnv;
|
|
78
|
-
});
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
describe('extractLambdaInvocationContext', () => {
|
|
82
|
-
it('should extract invocation context from Lambda context', () => {
|
|
83
|
-
const result = extractLambdaInvocationContext({}, mockContext);
|
|
84
|
-
|
|
85
|
-
expect(result.requestId).toBe('test-request-id-123');
|
|
86
|
-
expect(result.remainingTimeMs).toBe(10000);
|
|
87
|
-
expect(result.memoryLimitMB).toBe(256);
|
|
88
|
-
expect(result.functionName).toBe('test-function');
|
|
89
|
-
expect(result.functionVersion).toBe('1');
|
|
90
|
-
});
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
describe('LambdaAdapter', () => {
|
|
94
|
-
it('should create adapter with default config', () => {
|
|
95
|
-
const adapter = new LambdaAdapter(telescope);
|
|
96
|
-
|
|
97
|
-
expect(adapter.config.environment).toBe('lambda');
|
|
98
|
-
expect(adapter.config.autoFlush).toBe(true);
|
|
99
|
-
expect(adapter.config.detectResource).toBe(true);
|
|
100
|
-
expect(adapter.config.spanProcessor?.strategy).toBe('simple');
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
it('should merge custom config with defaults', () => {
|
|
104
|
-
const adapter = new LambdaAdapter(telescope, {
|
|
105
|
-
autoFlush: false,
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
expect(adapter.config.autoFlush).toBe(false);
|
|
109
|
-
expect(adapter.config.environment).toBe('lambda'); // Default preserved
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
describe('extractRequestContext', () => {
|
|
113
|
-
it('should extract context from API Gateway v1 event', () => {
|
|
114
|
-
const adapter = new LambdaAdapter(telescope);
|
|
115
|
-
const event = {
|
|
116
|
-
httpMethod: 'POST',
|
|
117
|
-
path: '/api/users',
|
|
118
|
-
headers: { 'Content-Type': 'application/json' },
|
|
119
|
-
queryStringParameters: { page: '1' },
|
|
120
|
-
body: JSON.stringify({ name: 'John' }),
|
|
121
|
-
requestContext: {
|
|
122
|
-
requestId: 'req-123',
|
|
123
|
-
identity: { sourceIp: '192.168.1.1' },
|
|
124
|
-
},
|
|
125
|
-
};
|
|
126
|
-
|
|
127
|
-
const context = adapter.extractRequestContext(event);
|
|
128
|
-
|
|
129
|
-
expect(context.method).toBe('POST');
|
|
130
|
-
expect(context.path).toBe('/api/users');
|
|
131
|
-
expect(context.query).toEqual({ page: '1' });
|
|
132
|
-
expect(context.body).toEqual({ name: 'John' });
|
|
133
|
-
expect(context.ip).toBe('192.168.1.1');
|
|
134
|
-
expect(context.id).toBe('req-123');
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
it('should extract context from API Gateway v2 event', () => {
|
|
138
|
-
const adapter = new LambdaAdapter(telescope);
|
|
139
|
-
const event = {
|
|
140
|
-
rawPath: '/api/items',
|
|
141
|
-
headers: { 'content-type': 'application/json' },
|
|
142
|
-
queryStringParameters: { limit: '10' },
|
|
143
|
-
body: JSON.stringify({ data: 'test' }),
|
|
144
|
-
requestContext: {
|
|
145
|
-
requestId: 'req-456',
|
|
146
|
-
http: {
|
|
147
|
-
method: 'GET',
|
|
148
|
-
sourceIp: '10.0.0.1',
|
|
149
|
-
},
|
|
150
|
-
},
|
|
151
|
-
};
|
|
152
|
-
|
|
153
|
-
const context = adapter.extractRequestContext(event);
|
|
154
|
-
|
|
155
|
-
expect(context.method).toBe('GET');
|
|
156
|
-
expect(context.path).toBe('/api/items');
|
|
157
|
-
expect(context.query).toEqual({ limit: '10' });
|
|
158
|
-
expect(context.body).toEqual({ data: 'test' });
|
|
159
|
-
expect(context.ip).toBe('10.0.0.1');
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
it('should extract context from ALB event', () => {
|
|
163
|
-
const adapter = new LambdaAdapter(telescope);
|
|
164
|
-
// ALB events don't have requestContext, so the adapter falls back to generic handling
|
|
165
|
-
const event = {
|
|
166
|
-
httpMethod: 'DELETE',
|
|
167
|
-
path: '/api/resource/123',
|
|
168
|
-
headers: {},
|
|
169
|
-
queryStringParameters: null,
|
|
170
|
-
body: null,
|
|
171
|
-
};
|
|
172
|
-
|
|
173
|
-
const context = adapter.extractRequestContext(event);
|
|
174
|
-
|
|
175
|
-
expect(context.method).toBe('DELETE');
|
|
176
|
-
expect(context.path).toBe('/api/resource/123');
|
|
177
|
-
// ALB without x-forwarded-for will have undefined IP
|
|
178
|
-
expect(context.ip).toBeUndefined();
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
it('should handle generic Lambda invocation', () => {
|
|
182
|
-
const adapter = new LambdaAdapter(telescope);
|
|
183
|
-
const event = { customField: 'value' };
|
|
184
|
-
|
|
185
|
-
const context = adapter.extractRequestContext(event);
|
|
186
|
-
|
|
187
|
-
expect(context.method).toBe('INVOKE');
|
|
188
|
-
expect(context.path).toBe('/');
|
|
189
|
-
expect(context.body).toEqual({ customField: 'value' });
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
it('should handle base64 encoded body', () => {
|
|
193
|
-
const adapter = new LambdaAdapter(telescope);
|
|
194
|
-
const bodyContent = { encoded: true };
|
|
195
|
-
const event = {
|
|
196
|
-
httpMethod: 'POST',
|
|
197
|
-
path: '/upload',
|
|
198
|
-
headers: {},
|
|
199
|
-
body: Buffer.from(JSON.stringify(bodyContent)).toString('base64'),
|
|
200
|
-
isBase64Encoded: true,
|
|
201
|
-
};
|
|
202
|
-
|
|
203
|
-
const context = adapter.extractRequestContext(event);
|
|
204
|
-
|
|
205
|
-
expect(context.body).toEqual({ encoded: true });
|
|
206
|
-
});
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
describe('extractResponseContext', () => {
|
|
210
|
-
it('should extract context from API Gateway response', () => {
|
|
211
|
-
const adapter = new LambdaAdapter(telescope);
|
|
212
|
-
const response = {
|
|
213
|
-
statusCode: 201,
|
|
214
|
-
headers: { 'x-custom': 'header' },
|
|
215
|
-
body: JSON.stringify({ created: true }),
|
|
216
|
-
};
|
|
217
|
-
const startTime = Date.now() - 100;
|
|
218
|
-
|
|
219
|
-
const context = adapter.extractResponseContext(response, startTime);
|
|
220
|
-
|
|
221
|
-
expect(context.status).toBe(201);
|
|
222
|
-
expect(context.headers).toEqual({ 'x-custom': 'header' });
|
|
223
|
-
expect(context.body).toBe('{"created":true}');
|
|
224
|
-
expect(context.duration).toBeGreaterThanOrEqual(100);
|
|
225
|
-
});
|
|
226
|
-
|
|
227
|
-
it('should handle generic response', () => {
|
|
228
|
-
const adapter = new LambdaAdapter(telescope);
|
|
229
|
-
const response = { data: 'raw' };
|
|
230
|
-
const startTime = Date.now() - 50;
|
|
231
|
-
|
|
232
|
-
const context = adapter.extractResponseContext(response, startTime);
|
|
233
|
-
|
|
234
|
-
expect(context.status).toBe(200);
|
|
235
|
-
expect(context.body).toEqual({ data: 'raw' });
|
|
236
|
-
// Use 45ms threshold to account for timing variations
|
|
237
|
-
expect(context.duration).toBeGreaterThanOrEqual(45);
|
|
238
|
-
});
|
|
239
|
-
});
|
|
240
|
-
|
|
241
|
-
describe('onSetup', () => {
|
|
242
|
-
it('should detect resources when detectResource is true', async () => {
|
|
243
|
-
const adapter = new LambdaAdapter(telescope, { detectResource: true });
|
|
244
|
-
|
|
245
|
-
await adapter.onSetup();
|
|
246
|
-
|
|
247
|
-
expect(adapter.getResourceAttributes()).not.toBeNull();
|
|
248
|
-
});
|
|
249
|
-
|
|
250
|
-
it('should not detect resources when detectResource is false', async () => {
|
|
251
|
-
const adapter = new LambdaAdapter(telescope, { detectResource: false });
|
|
252
|
-
|
|
253
|
-
await adapter.onSetup();
|
|
254
|
-
|
|
255
|
-
expect(adapter.getResourceAttributes()).toBeNull();
|
|
256
|
-
});
|
|
257
|
-
});
|
|
258
|
-
});
|
|
259
|
-
|
|
260
|
-
describe('wrapLambdaHandler', () => {
|
|
261
|
-
it('should wrap handler and record successful request', async () => {
|
|
262
|
-
const baseHandler = async () => ({
|
|
263
|
-
statusCode: 200,
|
|
264
|
-
body: JSON.stringify({ ok: true }),
|
|
265
|
-
});
|
|
266
|
-
|
|
267
|
-
const wrappedHandler = wrapLambdaHandler(telescope, baseHandler);
|
|
268
|
-
|
|
269
|
-
const event = {
|
|
270
|
-
httpMethod: 'GET',
|
|
271
|
-
path: '/health',
|
|
272
|
-
headers: {},
|
|
273
|
-
queryStringParameters: null,
|
|
274
|
-
body: null,
|
|
275
|
-
};
|
|
276
|
-
|
|
277
|
-
const result = await wrappedHandler(event, mockContext);
|
|
278
|
-
|
|
279
|
-
expect(result).toEqual({
|
|
280
|
-
statusCode: 200,
|
|
281
|
-
body: JSON.stringify({ ok: true }),
|
|
282
|
-
});
|
|
283
|
-
|
|
284
|
-
const requests = await storage.getRequests();
|
|
285
|
-
expect(requests).toHaveLength(1);
|
|
286
|
-
expect(requests[0].method).toBe('GET');
|
|
287
|
-
expect(requests[0].path).toBe('/health');
|
|
288
|
-
expect(requests[0].status).toBe(200);
|
|
289
|
-
});
|
|
290
|
-
|
|
291
|
-
it('should record exception and rethrow on error', async () => {
|
|
292
|
-
const error = new Error('Handler error');
|
|
293
|
-
const baseHandler = async () => {
|
|
294
|
-
throw error;
|
|
295
|
-
};
|
|
296
|
-
|
|
297
|
-
const wrappedHandler = wrapLambdaHandler(telescope, baseHandler);
|
|
298
|
-
|
|
299
|
-
await expect(wrappedHandler({}, mockContext)).rejects.toThrow(
|
|
300
|
-
'Handler error',
|
|
301
|
-
);
|
|
302
|
-
|
|
303
|
-
const exceptions = await storage.getExceptions();
|
|
304
|
-
expect(exceptions).toHaveLength(1);
|
|
305
|
-
expect(exceptions[0].message).toBe('Handler error');
|
|
306
|
-
});
|
|
307
|
-
|
|
308
|
-
it('should pass event and context to handler', async () => {
|
|
309
|
-
const baseHandler = vi.fn().mockResolvedValue({ statusCode: 200 });
|
|
310
|
-
const wrappedHandler = wrapLambdaHandler(telescope, baseHandler);
|
|
311
|
-
|
|
312
|
-
const event = { test: 'event' };
|
|
313
|
-
await wrappedHandler(event, mockContext);
|
|
314
|
-
|
|
315
|
-
expect(baseHandler).toHaveBeenCalledWith(event, mockContext);
|
|
316
|
-
});
|
|
317
|
-
});
|
|
318
|
-
});
|