iii-sdk 0.2.0 → 0.2.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/dist/{iii-7FpY7bi9.d.mts → iii-CAEmlG6f.d.mts} +18 -5
- package/dist/iii-CAEmlG6f.d.mts.map +1 -0
- package/dist/{iii-BhtLRYBs.d.cts → iii-DEbgvZtB.d.cts} +15 -2
- package/dist/iii-DEbgvZtB.d.cts.map +1 -0
- package/dist/index.cjs +9 -7
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +9 -7
- package/dist/index.mjs.map +1 -1
- package/dist/{otel-worker-gauges-Dka1CFDy.mjs → otel-worker-gauges-CHooGOLC.mjs} +144 -12
- package/dist/otel-worker-gauges-CHooGOLC.mjs.map +1 -0
- package/dist/{otel-worker-gauges-8AUNGQgJ.cjs → otel-worker-gauges-CLZyxI1P.cjs} +137 -5
- package/dist/otel-worker-gauges-CLZyxI1P.cjs.map +1 -0
- package/dist/stream-Bzpo5JNV.d.mts.map +1 -1
- package/dist/telemetry.cjs +1 -1
- package/dist/telemetry.d.cts +1 -1
- package/dist/telemetry.d.mts +1 -1
- package/dist/telemetry.mjs +1 -1
- package/package.json +1 -1
- package/tests/api-triggers.test.ts +163 -0
- package/tests/bridge.test.ts +84 -0
- package/tests/exports.test.ts +37 -0
- package/tests/fetch-instrumentation.test.ts +623 -0
- package/tests/fixtures/config-test.yaml +66 -0
- package/tests/healthcheck.test.ts +1 -2
- package/tests/kv-server.test.ts +82 -0
- package/tests/pubsub.test.ts +117 -0
- package/tests/setup.ts +2 -32
- package/tests/state.test.ts +1 -2
- package/tests/stream.test.ts +1 -2
- package/tests/utils.ts +2 -23
- package/dist/iii-7FpY7bi9.d.mts.map +0 -1
- package/dist/iii-BhtLRYBs.d.cts.map +0 -1
- package/dist/otel-worker-gauges-8AUNGQgJ.cjs.map +0 -1
- package/dist/otel-worker-gauges-Dka1CFDy.mjs.map +0 -1
|
@@ -0,0 +1,623 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for global fetch auto-instrumentation behavior.
|
|
3
|
+
*
|
|
4
|
+
* These tests verify that globalThis.fetch is patched by default
|
|
5
|
+
* and can be disabled via fetchInstrumentationEnabled config.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, it, expect, afterEach, vi, beforeEach } from 'vitest'
|
|
9
|
+
import { type Tracer, SpanStatusCode } from '@opentelemetry/api'
|
|
10
|
+
import type { Instrumentation } from '@opentelemetry/instrumentation'
|
|
11
|
+
|
|
12
|
+
// Mock WebSocket to prevent real connections
|
|
13
|
+
vi.mock('ws', () => {
|
|
14
|
+
const MockWebSocket = vi.fn().mockImplementation(() => ({
|
|
15
|
+
on: vi.fn(),
|
|
16
|
+
close: vi.fn(),
|
|
17
|
+
send: vi.fn(),
|
|
18
|
+
readyState: 0,
|
|
19
|
+
}))
|
|
20
|
+
return { WebSocket: MockWebSocket, default: { WebSocket: MockWebSocket } }
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
// Mock NodeTracerProvider to avoid real tracer setup
|
|
24
|
+
vi.mock('@opentelemetry/sdk-trace-node', () => ({
|
|
25
|
+
NodeTracerProvider: vi.fn().mockImplementation(() => ({
|
|
26
|
+
register: vi.fn(),
|
|
27
|
+
shutdown: vi.fn().mockResolvedValue(undefined),
|
|
28
|
+
})),
|
|
29
|
+
}))
|
|
30
|
+
|
|
31
|
+
// Mock MeterProvider
|
|
32
|
+
vi.mock('@opentelemetry/sdk-metrics', () => ({
|
|
33
|
+
MeterProvider: vi.fn().mockImplementation(() => ({
|
|
34
|
+
getMeter: vi.fn().mockReturnValue({ createCounter: vi.fn() }),
|
|
35
|
+
shutdown: vi.fn().mockResolvedValue(undefined),
|
|
36
|
+
})),
|
|
37
|
+
PeriodicExportingMetricReader: vi.fn().mockImplementation(() => ({})),
|
|
38
|
+
}))
|
|
39
|
+
|
|
40
|
+
// Mock LoggerProvider
|
|
41
|
+
vi.mock('@opentelemetry/sdk-logs', () => ({
|
|
42
|
+
LoggerProvider: vi.fn().mockImplementation(() => ({
|
|
43
|
+
getLogger: vi.fn().mockReturnValue({ emit: vi.fn() }),
|
|
44
|
+
addLogRecordProcessor: vi.fn(),
|
|
45
|
+
shutdown: vi.fn().mockResolvedValue(undefined),
|
|
46
|
+
})),
|
|
47
|
+
SimpleLogRecordProcessor: vi.fn().mockImplementation(() => ({})),
|
|
48
|
+
}))
|
|
49
|
+
|
|
50
|
+
// Mock span processor
|
|
51
|
+
vi.mock('@opentelemetry/sdk-trace-base', () => ({
|
|
52
|
+
BatchSpanProcessor: vi.fn().mockImplementation(() => ({})),
|
|
53
|
+
}))
|
|
54
|
+
|
|
55
|
+
// Mock exporters
|
|
56
|
+
vi.mock('../src/telemetry-system/exporters', () => ({
|
|
57
|
+
EngineSpanExporter: vi.fn().mockImplementation(() => ({})),
|
|
58
|
+
EngineMetricsExporter: vi.fn().mockImplementation(() => ({})),
|
|
59
|
+
EngineLogExporter: vi.fn().mockImplementation(() => ({})),
|
|
60
|
+
}))
|
|
61
|
+
|
|
62
|
+
// Mock the shared connection
|
|
63
|
+
vi.mock('../src/telemetry-system/connection', () => ({
|
|
64
|
+
SharedEngineConnection: vi.fn().mockImplementation(() => ({
|
|
65
|
+
send: vi.fn(),
|
|
66
|
+
onConnected: vi.fn(),
|
|
67
|
+
getState: vi.fn().mockReturnValue('disconnected'),
|
|
68
|
+
shutdown: vi.fn().mockResolvedValue(undefined),
|
|
69
|
+
})),
|
|
70
|
+
}))
|
|
71
|
+
|
|
72
|
+
import { initOtel, shutdownOtel, getTracer } from '../src/telemetry-system/index'
|
|
73
|
+
|
|
74
|
+
describe('Fetch instrumentation', () => {
|
|
75
|
+
const originalEnv = process.env
|
|
76
|
+
const nativeFetch = globalThis.fetch
|
|
77
|
+
|
|
78
|
+
beforeEach(() => {
|
|
79
|
+
process.env = { ...originalEnv }
|
|
80
|
+
delete process.env.OTEL_ENABLED
|
|
81
|
+
// Ensure globalThis.fetch is the native version before each test
|
|
82
|
+
globalThis.fetch = nativeFetch
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
afterEach(async () => {
|
|
86
|
+
await shutdownOtel()
|
|
87
|
+
process.env = originalEnv
|
|
88
|
+
// Restore native fetch after shutdown
|
|
89
|
+
globalThis.fetch = nativeFetch
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it('should initialize OTel successfully with fetch instrumentation enabled by default', () => {
|
|
93
|
+
expect(() => initOtel()).not.toThrow()
|
|
94
|
+
expect(getTracer()).not.toBeNull()
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
it('should patch globalThis.fetch when enabled by default', () => {
|
|
98
|
+
initOtel()
|
|
99
|
+
// After initOtel, globalThis.fetch should be patched (different from native)
|
|
100
|
+
expect(globalThis.fetch).not.toBe(nativeFetch)
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it('should NOT patch globalThis.fetch when fetchInstrumentationEnabled is false', () => {
|
|
104
|
+
initOtel({ fetchInstrumentationEnabled: false })
|
|
105
|
+
// globalThis.fetch should remain the native version
|
|
106
|
+
expect(globalThis.fetch).toBe(nativeFetch)
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
it('should restore globalThis.fetch on shutdown', async () => {
|
|
110
|
+
initOtel()
|
|
111
|
+
expect(globalThis.fetch).not.toBe(nativeFetch)
|
|
112
|
+
|
|
113
|
+
await shutdownOtel()
|
|
114
|
+
expect(globalThis.fetch).toBe(nativeFetch)
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
it('should NOT patch globalThis.fetch when OTel is disabled', () => {
|
|
118
|
+
initOtel({ enabled: false })
|
|
119
|
+
expect(globalThis.fetch).toBe(nativeFetch)
|
|
120
|
+
expect(getTracer()).toBeNull()
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
it('should initialize OTel successfully when fetchInstrumentationEnabled is explicitly true', () => {
|
|
124
|
+
expect(() => initOtel({ fetchInstrumentationEnabled: true })).not.toThrow()
|
|
125
|
+
expect(getTracer()).not.toBeNull()
|
|
126
|
+
expect(globalThis.fetch).not.toBe(nativeFetch)
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
it('should accept user instrumentations alongside fetch patch', () => {
|
|
130
|
+
const userInstrumentation = {
|
|
131
|
+
instrumentationName: 'custom-test',
|
|
132
|
+
instrumentationVersion: '1.0.0',
|
|
133
|
+
getConfig: () => ({}),
|
|
134
|
+
setConfig: vi.fn(),
|
|
135
|
+
setTracerProvider: vi.fn(),
|
|
136
|
+
setMeterProvider: vi.fn(),
|
|
137
|
+
enable: vi.fn(),
|
|
138
|
+
disable: vi.fn(),
|
|
139
|
+
}
|
|
140
|
+
expect(() =>
|
|
141
|
+
initOtel({ instrumentations: [userInstrumentation as Instrumentation] }),
|
|
142
|
+
).not.toThrow()
|
|
143
|
+
expect(getTracer()).not.toBeNull()
|
|
144
|
+
// Both fetch patch and user instrumentations should work
|
|
145
|
+
expect(globalThis.fetch).not.toBe(nativeFetch)
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
it('should accept user instrumentations when fetch instrumentation is disabled', () => {
|
|
149
|
+
const userInstrumentation = {
|
|
150
|
+
instrumentationName: 'custom-test',
|
|
151
|
+
instrumentationVersion: '1.0.0',
|
|
152
|
+
getConfig: () => ({}),
|
|
153
|
+
setConfig: vi.fn(),
|
|
154
|
+
setTracerProvider: vi.fn(),
|
|
155
|
+
setMeterProvider: vi.fn(),
|
|
156
|
+
enable: vi.fn(),
|
|
157
|
+
disable: vi.fn(),
|
|
158
|
+
}
|
|
159
|
+
expect(() =>
|
|
160
|
+
initOtel({
|
|
161
|
+
fetchInstrumentationEnabled: false,
|
|
162
|
+
instrumentations: [userInstrumentation as Instrumentation],
|
|
163
|
+
}),
|
|
164
|
+
).not.toThrow()
|
|
165
|
+
expect(getTracer()).not.toBeNull()
|
|
166
|
+
// Fetch should NOT be patched
|
|
167
|
+
expect(globalThis.fetch).toBe(nativeFetch)
|
|
168
|
+
})
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
describe('Fetch span attributes', () => {
|
|
172
|
+
const nativeFetch = globalThis.fetch
|
|
173
|
+
let unpatch: () => void
|
|
174
|
+
|
|
175
|
+
afterEach(() => {
|
|
176
|
+
unpatch?.()
|
|
177
|
+
// restore in case the test replaced fetch manually
|
|
178
|
+
globalThis.fetch = nativeFetch
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
it('sets url.scheme, url.path, network.protocol.name, server.address and span name on success', async () => {
|
|
182
|
+
const spanMock = {
|
|
183
|
+
setAttribute: vi.fn(),
|
|
184
|
+
setStatus: vi.fn(),
|
|
185
|
+
recordException: vi.fn(),
|
|
186
|
+
end: vi.fn(),
|
|
187
|
+
}
|
|
188
|
+
const tracerMock = {
|
|
189
|
+
startActiveSpan: vi
|
|
190
|
+
.fn()
|
|
191
|
+
.mockImplementation(
|
|
192
|
+
(_name: string, _opts: unknown, _ctx: unknown, fn: (span: typeof spanMock) => unknown) =>
|
|
193
|
+
fn(spanMock),
|
|
194
|
+
),
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const { patchGlobalFetch, unpatchGlobalFetch } = await import(
|
|
198
|
+
'../src/telemetry-system/fetch-instrumentation'
|
|
199
|
+
)
|
|
200
|
+
unpatch = unpatchGlobalFetch
|
|
201
|
+
|
|
202
|
+
// Replace native fetch with a fake before patching so the patch wraps the fake
|
|
203
|
+
const fakeFetch = vi.fn().mockResolvedValue(new Response('ok', { status: 200 }))
|
|
204
|
+
globalThis.fetch = fakeFetch
|
|
205
|
+
|
|
206
|
+
patchGlobalFetch(tracerMock as unknown as Tracer)
|
|
207
|
+
|
|
208
|
+
await globalThis.fetch('https://example.com/api/items?q=1')
|
|
209
|
+
|
|
210
|
+
expect(tracerMock.startActiveSpan).toHaveBeenCalledWith(
|
|
211
|
+
'GET /api/items',
|
|
212
|
+
expect.objectContaining({
|
|
213
|
+
attributes: expect.objectContaining({
|
|
214
|
+
'http.request.method': 'GET',
|
|
215
|
+
'url.full': 'https://example.com/api/items?q=1',
|
|
216
|
+
'url.scheme': 'https',
|
|
217
|
+
'url.path': '/api/items',
|
|
218
|
+
'network.protocol.name': 'http',
|
|
219
|
+
'server.address': 'example.com',
|
|
220
|
+
}),
|
|
221
|
+
}),
|
|
222
|
+
expect.anything(),
|
|
223
|
+
expect.any(Function),
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
expect(spanMock.setAttribute).toHaveBeenCalledWith('http.response.status_code', 200)
|
|
227
|
+
expect(spanMock.setStatus).toHaveBeenCalledWith({ code: SpanStatusCode.OK })
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
it('sets url.query when query string is present', async () => {
|
|
231
|
+
const spanMock = {
|
|
232
|
+
setAttribute: vi.fn(),
|
|
233
|
+
setStatus: vi.fn(),
|
|
234
|
+
recordException: vi.fn(),
|
|
235
|
+
end: vi.fn(),
|
|
236
|
+
}
|
|
237
|
+
const tracerMock = {
|
|
238
|
+
startActiveSpan: vi
|
|
239
|
+
.fn()
|
|
240
|
+
.mockImplementation(
|
|
241
|
+
(_name: string, _opts: unknown, _ctx: unknown, fn: (span: typeof spanMock) => unknown) =>
|
|
242
|
+
fn(spanMock),
|
|
243
|
+
),
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const { patchGlobalFetch, unpatchGlobalFetch } = await import(
|
|
247
|
+
'../src/telemetry-system/fetch-instrumentation'
|
|
248
|
+
)
|
|
249
|
+
unpatch = unpatchGlobalFetch
|
|
250
|
+
|
|
251
|
+
const fakeFetch = vi.fn().mockResolvedValue(new Response('ok', { status: 200 }))
|
|
252
|
+
globalThis.fetch = fakeFetch
|
|
253
|
+
|
|
254
|
+
patchGlobalFetch(tracerMock as unknown as Tracer)
|
|
255
|
+
|
|
256
|
+
await globalThis.fetch('https://example.com/api/items?q=1&page=2')
|
|
257
|
+
|
|
258
|
+
expect(tracerMock.startActiveSpan).toHaveBeenCalledWith(
|
|
259
|
+
'GET /api/items',
|
|
260
|
+
expect.objectContaining({
|
|
261
|
+
attributes: expect.objectContaining({
|
|
262
|
+
'url.query': 'q=1&page=2',
|
|
263
|
+
}),
|
|
264
|
+
}),
|
|
265
|
+
expect.anything(),
|
|
266
|
+
expect.any(Function),
|
|
267
|
+
)
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
it('does not set url.query when no query string', async () => {
|
|
271
|
+
const spanMock = {
|
|
272
|
+
setAttribute: vi.fn(),
|
|
273
|
+
setStatus: vi.fn(),
|
|
274
|
+
recordException: vi.fn(),
|
|
275
|
+
end: vi.fn(),
|
|
276
|
+
}
|
|
277
|
+
const tracerMock = {
|
|
278
|
+
startActiveSpan: vi
|
|
279
|
+
.fn()
|
|
280
|
+
.mockImplementation(
|
|
281
|
+
(_name: string, _opts: unknown, _ctx: unknown, fn: (span: typeof spanMock) => unknown) =>
|
|
282
|
+
fn(spanMock),
|
|
283
|
+
),
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const { patchGlobalFetch, unpatchGlobalFetch } = await import(
|
|
287
|
+
'../src/telemetry-system/fetch-instrumentation'
|
|
288
|
+
)
|
|
289
|
+
unpatch = unpatchGlobalFetch
|
|
290
|
+
|
|
291
|
+
const fakeFetch = vi.fn().mockResolvedValue(new Response('ok', { status: 200 }))
|
|
292
|
+
globalThis.fetch = fakeFetch
|
|
293
|
+
|
|
294
|
+
patchGlobalFetch(tracerMock as unknown as Tracer)
|
|
295
|
+
|
|
296
|
+
await globalThis.fetch('https://example.com/api/items')
|
|
297
|
+
|
|
298
|
+
const callArgs = tracerMock.startActiveSpan.mock.calls[0][1] as {
|
|
299
|
+
attributes: Record<string, unknown>
|
|
300
|
+
}
|
|
301
|
+
expect(callArgs.attributes['url.query']).toBeUndefined()
|
|
302
|
+
})
|
|
303
|
+
|
|
304
|
+
it('sets http.request.body.size for string body', async () => {
|
|
305
|
+
const spanMock = {
|
|
306
|
+
setAttribute: vi.fn(),
|
|
307
|
+
setStatus: vi.fn(),
|
|
308
|
+
recordException: vi.fn(),
|
|
309
|
+
end: vi.fn(),
|
|
310
|
+
}
|
|
311
|
+
const tracerMock = {
|
|
312
|
+
startActiveSpan: vi
|
|
313
|
+
.fn()
|
|
314
|
+
.mockImplementation(
|
|
315
|
+
(_name: string, _opts: unknown, _ctx: unknown, fn: (span: typeof spanMock) => unknown) =>
|
|
316
|
+
fn(spanMock),
|
|
317
|
+
),
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const { patchGlobalFetch, unpatchGlobalFetch } = await import(
|
|
321
|
+
'../src/telemetry-system/fetch-instrumentation'
|
|
322
|
+
)
|
|
323
|
+
unpatch = unpatchGlobalFetch
|
|
324
|
+
|
|
325
|
+
const fakeFetch = vi.fn().mockResolvedValue(new Response('ok', { status: 200 }))
|
|
326
|
+
globalThis.fetch = fakeFetch
|
|
327
|
+
|
|
328
|
+
patchGlobalFetch(tracerMock as unknown as Tracer)
|
|
329
|
+
|
|
330
|
+
await globalThis.fetch('https://example.com/api', {
|
|
331
|
+
method: 'POST',
|
|
332
|
+
body: 'hello',
|
|
333
|
+
})
|
|
334
|
+
|
|
335
|
+
// 'hello' is 5 bytes in UTF-8
|
|
336
|
+
expect(spanMock.setAttribute).toHaveBeenCalledWith('http.request.body.size', 5)
|
|
337
|
+
})
|
|
338
|
+
|
|
339
|
+
it('sets http.request.body.size for Uint8Array body', async () => {
|
|
340
|
+
const spanMock = {
|
|
341
|
+
setAttribute: vi.fn(),
|
|
342
|
+
setStatus: vi.fn(),
|
|
343
|
+
recordException: vi.fn(),
|
|
344
|
+
end: vi.fn(),
|
|
345
|
+
}
|
|
346
|
+
const tracerMock = {
|
|
347
|
+
startActiveSpan: vi
|
|
348
|
+
.fn()
|
|
349
|
+
.mockImplementation(
|
|
350
|
+
(_name: string, _opts: unknown, _ctx: unknown, fn: (span: typeof spanMock) => unknown) =>
|
|
351
|
+
fn(spanMock),
|
|
352
|
+
),
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const { patchGlobalFetch, unpatchGlobalFetch } = await import(
|
|
356
|
+
'../src/telemetry-system/fetch-instrumentation'
|
|
357
|
+
)
|
|
358
|
+
unpatch = unpatchGlobalFetch
|
|
359
|
+
|
|
360
|
+
const fakeFetch = vi.fn().mockResolvedValue(new Response('ok', { status: 200 }))
|
|
361
|
+
globalThis.fetch = fakeFetch
|
|
362
|
+
|
|
363
|
+
patchGlobalFetch(tracerMock as unknown as Tracer)
|
|
364
|
+
|
|
365
|
+
await globalThis.fetch('https://example.com/api', {
|
|
366
|
+
method: 'POST',
|
|
367
|
+
body: new Uint8Array([1, 2, 3, 4]),
|
|
368
|
+
})
|
|
369
|
+
|
|
370
|
+
expect(spanMock.setAttribute).toHaveBeenCalledWith('http.request.body.size', 4)
|
|
371
|
+
})
|
|
372
|
+
|
|
373
|
+
it('does not set http.request.body.size when body is absent', async () => {
|
|
374
|
+
const spanMock = {
|
|
375
|
+
setAttribute: vi.fn(),
|
|
376
|
+
setStatus: vi.fn(),
|
|
377
|
+
recordException: vi.fn(),
|
|
378
|
+
end: vi.fn(),
|
|
379
|
+
}
|
|
380
|
+
const tracerMock = {
|
|
381
|
+
startActiveSpan: vi
|
|
382
|
+
.fn()
|
|
383
|
+
.mockImplementation(
|
|
384
|
+
(_name: string, _opts: unknown, _ctx: unknown, fn: (span: typeof spanMock) => unknown) =>
|
|
385
|
+
fn(spanMock),
|
|
386
|
+
),
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const { patchGlobalFetch, unpatchGlobalFetch } = await import(
|
|
390
|
+
'../src/telemetry-system/fetch-instrumentation'
|
|
391
|
+
)
|
|
392
|
+
unpatch = unpatchGlobalFetch
|
|
393
|
+
|
|
394
|
+
const fakeFetch = vi.fn().mockResolvedValue(new Response('ok', { status: 200 }))
|
|
395
|
+
globalThis.fetch = fakeFetch
|
|
396
|
+
|
|
397
|
+
patchGlobalFetch(tracerMock as unknown as Tracer)
|
|
398
|
+
|
|
399
|
+
await globalThis.fetch('https://example.com/api')
|
|
400
|
+
|
|
401
|
+
const calls = spanMock.setAttribute.mock.calls.map(c => c[0])
|
|
402
|
+
expect(calls).not.toContain('http.request.body.size')
|
|
403
|
+
})
|
|
404
|
+
|
|
405
|
+
it('sets http.response.body.size from Content-Length response header', async () => {
|
|
406
|
+
const spanMock = {
|
|
407
|
+
setAttribute: vi.fn(),
|
|
408
|
+
setStatus: vi.fn(),
|
|
409
|
+
recordException: vi.fn(),
|
|
410
|
+
end: vi.fn(),
|
|
411
|
+
}
|
|
412
|
+
const tracerMock = {
|
|
413
|
+
startActiveSpan: vi
|
|
414
|
+
.fn()
|
|
415
|
+
.mockImplementation(
|
|
416
|
+
(_name: string, _opts: unknown, _ctx: unknown, fn: (span: typeof spanMock) => unknown) =>
|
|
417
|
+
fn(spanMock),
|
|
418
|
+
),
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
const { patchGlobalFetch, unpatchGlobalFetch } = await import(
|
|
422
|
+
'../src/telemetry-system/fetch-instrumentation'
|
|
423
|
+
)
|
|
424
|
+
unpatch = unpatchGlobalFetch
|
|
425
|
+
|
|
426
|
+
const fakeFetch = vi.fn().mockResolvedValue(
|
|
427
|
+
new Response('hello world', {
|
|
428
|
+
status: 200,
|
|
429
|
+
headers: { 'content-length': '11' },
|
|
430
|
+
}),
|
|
431
|
+
)
|
|
432
|
+
globalThis.fetch = fakeFetch
|
|
433
|
+
|
|
434
|
+
patchGlobalFetch(tracerMock as unknown as Tracer)
|
|
435
|
+
|
|
436
|
+
await globalThis.fetch('https://example.com/api')
|
|
437
|
+
|
|
438
|
+
expect(spanMock.setAttribute).toHaveBeenCalledWith('http.response.body.size', 11)
|
|
439
|
+
})
|
|
440
|
+
|
|
441
|
+
it('does not set http.response.body.size when Content-Length header absent', async () => {
|
|
442
|
+
const spanMock = {
|
|
443
|
+
setAttribute: vi.fn(),
|
|
444
|
+
setStatus: vi.fn(),
|
|
445
|
+
recordException: vi.fn(),
|
|
446
|
+
end: vi.fn(),
|
|
447
|
+
}
|
|
448
|
+
const tracerMock = {
|
|
449
|
+
startActiveSpan: vi
|
|
450
|
+
.fn()
|
|
451
|
+
.mockImplementation(
|
|
452
|
+
(_name: string, _opts: unknown, _ctx: unknown, fn: (span: typeof spanMock) => unknown) =>
|
|
453
|
+
fn(spanMock),
|
|
454
|
+
),
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
const { patchGlobalFetch, unpatchGlobalFetch } = await import(
|
|
458
|
+
'../src/telemetry-system/fetch-instrumentation'
|
|
459
|
+
)
|
|
460
|
+
unpatch = unpatchGlobalFetch
|
|
461
|
+
|
|
462
|
+
const fakeFetch = vi.fn().mockResolvedValue(new Response('ok', { status: 200 }))
|
|
463
|
+
globalThis.fetch = fakeFetch
|
|
464
|
+
|
|
465
|
+
patchGlobalFetch(tracerMock as unknown as Tracer)
|
|
466
|
+
|
|
467
|
+
await globalThis.fetch('https://example.com/api')
|
|
468
|
+
|
|
469
|
+
const calls = spanMock.setAttribute.mock.calls.map(c => c[0])
|
|
470
|
+
expect(calls).not.toContain('http.response.body.size')
|
|
471
|
+
})
|
|
472
|
+
|
|
473
|
+
it('captures http.request.header.content-type and http.request.header.accept', async () => {
|
|
474
|
+
const spanMock = {
|
|
475
|
+
setAttribute: vi.fn(),
|
|
476
|
+
setStatus: vi.fn(),
|
|
477
|
+
recordException: vi.fn(),
|
|
478
|
+
end: vi.fn(),
|
|
479
|
+
}
|
|
480
|
+
const tracerMock = {
|
|
481
|
+
startActiveSpan: vi
|
|
482
|
+
.fn()
|
|
483
|
+
.mockImplementation(
|
|
484
|
+
(_name: string, _opts: unknown, _ctx: unknown, fn: (span: typeof spanMock) => unknown) =>
|
|
485
|
+
fn(spanMock),
|
|
486
|
+
),
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
const { patchGlobalFetch, unpatchGlobalFetch } = await import(
|
|
490
|
+
'../src/telemetry-system/fetch-instrumentation'
|
|
491
|
+
)
|
|
492
|
+
unpatch = unpatchGlobalFetch
|
|
493
|
+
|
|
494
|
+
const fakeFetch = vi.fn().mockResolvedValue(new Response('ok', { status: 200 }))
|
|
495
|
+
globalThis.fetch = fakeFetch
|
|
496
|
+
|
|
497
|
+
patchGlobalFetch(tracerMock as unknown as Tracer)
|
|
498
|
+
|
|
499
|
+
await globalThis.fetch('https://example.com/api', {
|
|
500
|
+
method: 'POST',
|
|
501
|
+
headers: {
|
|
502
|
+
'content-type': 'application/json',
|
|
503
|
+
accept: 'application/json',
|
|
504
|
+
},
|
|
505
|
+
body: '{}',
|
|
506
|
+
})
|
|
507
|
+
|
|
508
|
+
expect(spanMock.setAttribute).toHaveBeenCalledWith(
|
|
509
|
+
'http.request.header.content-type',
|
|
510
|
+
'application/json',
|
|
511
|
+
)
|
|
512
|
+
expect(spanMock.setAttribute).toHaveBeenCalledWith(
|
|
513
|
+
'http.request.header.accept',
|
|
514
|
+
'application/json',
|
|
515
|
+
)
|
|
516
|
+
})
|
|
517
|
+
|
|
518
|
+
it('captures http.response.header.content-type', async () => {
|
|
519
|
+
const spanMock = {
|
|
520
|
+
setAttribute: vi.fn(),
|
|
521
|
+
setStatus: vi.fn(),
|
|
522
|
+
recordException: vi.fn(),
|
|
523
|
+
end: vi.fn(),
|
|
524
|
+
}
|
|
525
|
+
const tracerMock = {
|
|
526
|
+
startActiveSpan: vi
|
|
527
|
+
.fn()
|
|
528
|
+
.mockImplementation(
|
|
529
|
+
(_name: string, _opts: unknown, _ctx: unknown, fn: (span: typeof spanMock) => unknown) =>
|
|
530
|
+
fn(spanMock),
|
|
531
|
+
),
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
const { patchGlobalFetch, unpatchGlobalFetch } = await import(
|
|
535
|
+
'../src/telemetry-system/fetch-instrumentation'
|
|
536
|
+
)
|
|
537
|
+
unpatch = unpatchGlobalFetch
|
|
538
|
+
|
|
539
|
+
const fakeFetch = vi.fn().mockResolvedValue(
|
|
540
|
+
new Response('{}', {
|
|
541
|
+
status: 200,
|
|
542
|
+
headers: { 'content-type': 'application/json' },
|
|
543
|
+
}),
|
|
544
|
+
)
|
|
545
|
+
globalThis.fetch = fakeFetch
|
|
546
|
+
|
|
547
|
+
patchGlobalFetch(tracerMock as unknown as Tracer)
|
|
548
|
+
|
|
549
|
+
await globalThis.fetch('https://example.com/api')
|
|
550
|
+
|
|
551
|
+
expect(spanMock.setAttribute).toHaveBeenCalledWith(
|
|
552
|
+
'http.response.header.content-type',
|
|
553
|
+
'application/json',
|
|
554
|
+
)
|
|
555
|
+
})
|
|
556
|
+
|
|
557
|
+
it('does not set header attributes when headers are absent', async () => {
|
|
558
|
+
const spanMock = {
|
|
559
|
+
setAttribute: vi.fn(),
|
|
560
|
+
setStatus: vi.fn(),
|
|
561
|
+
recordException: vi.fn(),
|
|
562
|
+
end: vi.fn(),
|
|
563
|
+
}
|
|
564
|
+
const tracerMock = {
|
|
565
|
+
startActiveSpan: vi
|
|
566
|
+
.fn()
|
|
567
|
+
.mockImplementation(
|
|
568
|
+
(_name: string, _opts: unknown, _ctx: unknown, fn: (span: typeof spanMock) => unknown) =>
|
|
569
|
+
fn(spanMock),
|
|
570
|
+
),
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
const { patchGlobalFetch, unpatchGlobalFetch } = await import(
|
|
574
|
+
'../src/telemetry-system/fetch-instrumentation'
|
|
575
|
+
)
|
|
576
|
+
unpatch = unpatchGlobalFetch
|
|
577
|
+
|
|
578
|
+
// Use null body so the Response has no auto-set content-type header
|
|
579
|
+
const fakeFetch = vi.fn().mockResolvedValue(new Response(null, { status: 200 }))
|
|
580
|
+
globalThis.fetch = fakeFetch
|
|
581
|
+
|
|
582
|
+
patchGlobalFetch(tracerMock as unknown as Tracer)
|
|
583
|
+
|
|
584
|
+
await globalThis.fetch('https://example.com/api')
|
|
585
|
+
|
|
586
|
+
const calls = spanMock.setAttribute.mock.calls.map(c => c[0])
|
|
587
|
+
expect(calls).not.toContain('http.request.header.content-type')
|
|
588
|
+
expect(calls).not.toContain('http.request.header.accept')
|
|
589
|
+
expect(calls).not.toContain('http.response.header.content-type')
|
|
590
|
+
})
|
|
591
|
+
|
|
592
|
+
it('sets error.type and ERROR status on 4xx response', async () => {
|
|
593
|
+
const spanMock = {
|
|
594
|
+
setAttribute: vi.fn(),
|
|
595
|
+
setStatus: vi.fn(),
|
|
596
|
+
recordException: vi.fn(),
|
|
597
|
+
end: vi.fn(),
|
|
598
|
+
}
|
|
599
|
+
const tracerMock = {
|
|
600
|
+
startActiveSpan: vi
|
|
601
|
+
.fn()
|
|
602
|
+
.mockImplementation(
|
|
603
|
+
(_name: string, _opts: unknown, _ctx: unknown, fn: (span: typeof spanMock) => unknown) =>
|
|
604
|
+
fn(spanMock),
|
|
605
|
+
),
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
const { patchGlobalFetch, unpatchGlobalFetch } = await import(
|
|
609
|
+
'../src/telemetry-system/fetch-instrumentation'
|
|
610
|
+
)
|
|
611
|
+
unpatch = unpatchGlobalFetch
|
|
612
|
+
|
|
613
|
+
const fakeFetch = vi.fn().mockResolvedValue(new Response('not found', { status: 404 }))
|
|
614
|
+
globalThis.fetch = fakeFetch
|
|
615
|
+
|
|
616
|
+
patchGlobalFetch(tracerMock as unknown as Tracer)
|
|
617
|
+
|
|
618
|
+
await globalThis.fetch('https://example.com/missing')
|
|
619
|
+
|
|
620
|
+
expect(spanMock.setAttribute).toHaveBeenCalledWith('error.type', '404')
|
|
621
|
+
expect(spanMock.setStatus).toHaveBeenCalledWith({ code: SpanStatusCode.ERROR })
|
|
622
|
+
})
|
|
623
|
+
})
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
port: 49199
|
|
2
|
+
modules:
|
|
3
|
+
- class: modules::stream::StreamModule
|
|
4
|
+
config:
|
|
5
|
+
port: ${STREAMS_PORT:3112}
|
|
6
|
+
host: 0.0.0.0
|
|
7
|
+
adapter:
|
|
8
|
+
class: modules::stream::adapters::KvStore
|
|
9
|
+
config:
|
|
10
|
+
store_method: file_based
|
|
11
|
+
file_path: ./data/stream_store
|
|
12
|
+
|
|
13
|
+
- class: modules::state::StateModule
|
|
14
|
+
config:
|
|
15
|
+
adapter:
|
|
16
|
+
class: modules::state::adapters::KvStore
|
|
17
|
+
config:
|
|
18
|
+
store_method: file_based
|
|
19
|
+
file_path: ./data/state_store.db
|
|
20
|
+
|
|
21
|
+
- class: modules::api::RestApiModule
|
|
22
|
+
config:
|
|
23
|
+
host: 0.0.0.0
|
|
24
|
+
port: 3199
|
|
25
|
+
default_timeout: 30000
|
|
26
|
+
|
|
27
|
+
- class: modules::kv_server::KvServer
|
|
28
|
+
config:
|
|
29
|
+
store_method: file_based
|
|
30
|
+
file_path: ./data/kv_store
|
|
31
|
+
save_interval_ms: 1000
|
|
32
|
+
|
|
33
|
+
- class: modules::pubsub::PubSubModule
|
|
34
|
+
config:
|
|
35
|
+
adapter:
|
|
36
|
+
class: modules::pubsub::LocalAdapter
|
|
37
|
+
|
|
38
|
+
- class: modules::observability::OtelModule
|
|
39
|
+
config:
|
|
40
|
+
enabled: ${OTEL_ENABLED:true}
|
|
41
|
+
service_name: ${OTEL_SERVICE_NAME:iii-test}
|
|
42
|
+
service_version: ${SERVICE_VERSION:0.2.0}
|
|
43
|
+
exporter: ${OTEL_EXPORTER_TYPE:memory}
|
|
44
|
+
endpoint: ${OTEL_EXPORTER_OTLP_ENDPOINT:http://localhost:4317}
|
|
45
|
+
sampling_ratio: 1.0
|
|
46
|
+
memory_max_spans: ${OTEL_MEMORY_MAX_SPANS:10000}
|
|
47
|
+
metrics_enabled: true
|
|
48
|
+
metrics_exporter: ${OTEL_METRICS_EXPORTER:memory}
|
|
49
|
+
metrics_retention_seconds: 3600
|
|
50
|
+
metrics_max_count: 10000
|
|
51
|
+
logs_enabled: ${OTEL_LOGS_ENABLED:true}
|
|
52
|
+
logs_exporter: ${OTEL_LOGS_EXPORTER:memory}
|
|
53
|
+
logs_max_count: ${OTEL_LOGS_MAX_COUNT:1000}
|
|
54
|
+
logs_retention_seconds: ${OTEL_LOGS_RETENTION_SECONDS:3600}
|
|
55
|
+
logs_sampling_ratio: ${OTEL_LOGS_SAMPLING_RATIO:1.0}
|
|
56
|
+
logs_console_output: ${OTEL_LOGS_CONSOLE_OUTPUT:false}
|
|
57
|
+
|
|
58
|
+
- class: modules::queue::QueueModule
|
|
59
|
+
config:
|
|
60
|
+
adapter:
|
|
61
|
+
class: modules::queue::BuiltinQueueAdapter
|
|
62
|
+
|
|
63
|
+
- class: modules::cron::CronModule
|
|
64
|
+
config:
|
|
65
|
+
adapter:
|
|
66
|
+
class: modules::cron::KvCronAdapter
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest'
|
|
2
2
|
import type { ApiRequest, ApiResponse } from '../src'
|
|
3
|
-
import { skipIfServerUnavailable } from './setup'
|
|
4
3
|
import { execute, httpRequest, iii } from './utils'
|
|
5
4
|
|
|
6
|
-
describe
|
|
5
|
+
describe('Healthcheck Endpoint', () => {
|
|
7
6
|
it('should register a healthcheck function and trigger', async () => {
|
|
8
7
|
const fn = iii.registerFunction(
|
|
9
8
|
{ id: 'test.healthcheck' },
|