@comprehend/telemetry-node 0.1.4 → 0.2.0
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/.claude/settings.local.json +2 -2
- package/.idea/telemetry-node.iml +0 -1
- package/README.md +73 -27
- package/dist/ComprehendDevSpanProcessor.d.ts +9 -6
- package/dist/ComprehendDevSpanProcessor.js +145 -87
- package/dist/ComprehendDevSpanProcessor.test.js +270 -449
- package/dist/ComprehendMetricsExporter.d.ts +18 -0
- package/dist/ComprehendMetricsExporter.js +178 -0
- package/dist/ComprehendMetricsExporter.test.d.ts +1 -0
- package/dist/ComprehendMetricsExporter.test.js +266 -0
- package/dist/ComprehendSDK.d.ts +18 -0
- package/dist/ComprehendSDK.js +56 -0
- package/dist/ComprehendSDK.test.d.ts +1 -0
- package/dist/ComprehendSDK.test.js +126 -0
- package/dist/WebSocketConnection.d.ts +23 -3
- package/dist/WebSocketConnection.js +106 -12
- package/dist/WebSocketConnection.test.js +236 -169
- package/dist/index.d.ts +3 -1
- package/dist/index.js +5 -1
- package/dist/sql-analyzer.js +2 -11
- package/dist/sql-analyzer.test.js +0 -12
- package/dist/util.d.ts +2 -0
- package/dist/util.js +7 -0
- package/dist/wire-protocol.d.ts +168 -28
- package/package.json +3 -1
- package/src/ComprehendDevSpanProcessor.test.ts +311 -507
- package/src/ComprehendDevSpanProcessor.ts +169 -105
- package/src/ComprehendMetricsExporter.test.ts +334 -0
- package/src/ComprehendMetricsExporter.ts +225 -0
- package/src/ComprehendSDK.test.ts +160 -0
- package/src/ComprehendSDK.ts +63 -0
- package/src/WebSocketConnection.test.ts +286 -205
- package/src/WebSocketConnection.ts +135 -13
- package/src/index.ts +3 -2
- package/src/util.ts +6 -0
- package/src/wire-protocol.ts +204 -29
- package/src/sql-analyzer.test.ts +0 -599
- package/src/sql-analyzer.ts +0 -439
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { SpanKind, SpanStatusCode } from '@opentelemetry/api';
|
|
2
2
|
import { ReadableSpan } from '@opentelemetry/sdk-trace-base';
|
|
3
|
-
import { ComprehendDevSpanProcessor } from './ComprehendDevSpanProcessor';
|
|
3
|
+
import { ComprehendDevSpanProcessor, matchSpanRule } from './ComprehendDevSpanProcessor';
|
|
4
4
|
import { WebSocketConnection } from './WebSocketConnection';
|
|
5
5
|
import {
|
|
6
6
|
NewObservedServiceMessage,
|
|
@@ -9,60 +9,48 @@ import {
|
|
|
9
9
|
NewObservedHttpServiceMessage,
|
|
10
10
|
NewObservedHttpRequestMessage,
|
|
11
11
|
NewObservedDatabaseConnectionMessage,
|
|
12
|
-
NewObservedDatabaseQueryMessage,
|
|
13
12
|
ObservationMessage,
|
|
14
13
|
HttpServerObservation,
|
|
15
14
|
HttpClientObservation,
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
DatabaseQueryMessage,
|
|
16
|
+
TraceSpansMessage,
|
|
17
|
+
CustomObservation,
|
|
18
|
+
ObservationInputMessage,
|
|
19
|
+
SpanMatcherRule,
|
|
18
20
|
} from './wire-protocol';
|
|
19
21
|
|
|
20
22
|
// Mock the WebSocketConnection
|
|
21
23
|
jest.mock('./WebSocketConnection');
|
|
22
24
|
|
|
23
|
-
// Mock the SQL analyzer
|
|
24
|
-
jest.mock('./sql-analyzer', () => ({
|
|
25
|
-
analyzeSQL: jest.fn()
|
|
26
|
-
}));
|
|
27
|
-
|
|
28
|
-
import { analyzeSQL } from './sql-analyzer';
|
|
29
|
-
|
|
30
|
-
const mockedAnalyzeSQL = analyzeSQL as jest.MockedFunction<typeof analyzeSQL>;
|
|
31
25
|
const MockedWebSocketConnection = WebSocketConnection as jest.MockedClass<typeof WebSocketConnection>;
|
|
32
26
|
|
|
33
27
|
describe('ComprehendDevSpanProcessor', () => {
|
|
34
28
|
let processor: ComprehendDevSpanProcessor;
|
|
35
29
|
let mockConnection: jest.Mocked<WebSocketConnection>;
|
|
36
30
|
let sentMessages: any[];
|
|
31
|
+
let seqCounter: number;
|
|
37
32
|
|
|
38
33
|
beforeEach(() => {
|
|
39
|
-
// Reset mocks
|
|
40
34
|
MockedWebSocketConnection.mockClear();
|
|
41
|
-
mockedAnalyzeSQL.mockClear();
|
|
42
35
|
sentMessages = [];
|
|
36
|
+
seqCounter = 1;
|
|
43
37
|
|
|
44
|
-
// Create mock connection instance
|
|
45
38
|
mockConnection = {
|
|
46
39
|
sendMessage: jest.fn((message: any) => {
|
|
47
40
|
sentMessages.push(message);
|
|
48
41
|
}),
|
|
42
|
+
setProcessContext: jest.fn(),
|
|
43
|
+
nextSeq: jest.fn(() => seqCounter++),
|
|
49
44
|
close: jest.fn()
|
|
50
45
|
} as any;
|
|
51
46
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
// Create processor instance
|
|
55
|
-
processor = new ComprehendDevSpanProcessor({
|
|
56
|
-
organization: 'test-org',
|
|
57
|
-
token: 'test-token'
|
|
58
|
-
});
|
|
47
|
+
processor = new ComprehendDevSpanProcessor(mockConnection);
|
|
59
48
|
});
|
|
60
49
|
|
|
61
50
|
afterEach(() => {
|
|
62
51
|
processor.shutdown();
|
|
63
52
|
});
|
|
64
53
|
|
|
65
|
-
// Helper function to create a mock span
|
|
66
54
|
function createMockSpan(options: {
|
|
67
55
|
name?: string;
|
|
68
56
|
kind?: SpanKind;
|
|
@@ -72,6 +60,9 @@ describe('ComprehendDevSpanProcessor', () => {
|
|
|
72
60
|
events?: Array<{ name: string; attributes?: Record<string, any> }>;
|
|
73
61
|
startTime?: [number, number];
|
|
74
62
|
duration?: [number, number];
|
|
63
|
+
traceId?: string;
|
|
64
|
+
spanId?: string;
|
|
65
|
+
parentSpanId?: string;
|
|
75
66
|
} = {}): ReadableSpan {
|
|
76
67
|
const {
|
|
77
68
|
name = 'test-span',
|
|
@@ -81,7 +72,10 @@ describe('ComprehendDevSpanProcessor', () => {
|
|
|
81
72
|
status = { code: SpanStatusCode.OK },
|
|
82
73
|
events = [],
|
|
83
74
|
startTime = [1700000000, 123456789],
|
|
84
|
-
duration = [0, 100000000]
|
|
75
|
+
duration = [0, 100000000],
|
|
76
|
+
traceId = 'abc123trace',
|
|
77
|
+
spanId = 'def456span',
|
|
78
|
+
parentSpanId,
|
|
85
79
|
} = options;
|
|
86
80
|
|
|
87
81
|
return {
|
|
@@ -98,8 +92,10 @@ describe('ComprehendDevSpanProcessor', () => {
|
|
|
98
92
|
time: startTime
|
|
99
93
|
})),
|
|
100
94
|
startTime,
|
|
101
|
-
duration
|
|
102
|
-
|
|
95
|
+
duration,
|
|
96
|
+
spanContext: () => ({ traceId, spanId, traceFlags: 1 }),
|
|
97
|
+
parentSpanContext: parentSpanId ? { spanId: parentSpanId, traceId: '', traceFlags: 0 } : undefined,
|
|
98
|
+
} as any;
|
|
103
99
|
}
|
|
104
100
|
|
|
105
101
|
describe('Service Discovery', () => {
|
|
@@ -114,176 +110,110 @@ describe('ComprehendDevSpanProcessor', () => {
|
|
|
114
110
|
|
|
115
111
|
processor.onEnd(span);
|
|
116
112
|
|
|
117
|
-
|
|
118
|
-
expect(
|
|
113
|
+
const serviceMsg = sentMessages.find(m => m.event === 'new-entity' && m.type === 'service');
|
|
114
|
+
expect(serviceMsg).toMatchObject({
|
|
119
115
|
event: 'new-entity',
|
|
120
116
|
type: 'service',
|
|
121
117
|
name: 'my-api',
|
|
122
118
|
namespace: 'production',
|
|
123
119
|
environment: 'prod'
|
|
124
|
-
}
|
|
125
|
-
expect(
|
|
120
|
+
});
|
|
121
|
+
expect(serviceMsg.hash).toBeDefined();
|
|
126
122
|
});
|
|
127
123
|
|
|
128
124
|
it('should not duplicate service registrations', () => {
|
|
129
|
-
const span1 = createMockSpan({
|
|
130
|
-
|
|
131
|
-
});
|
|
132
|
-
const span2 = createMockSpan({
|
|
133
|
-
resourceAttributes: { 'service.name': 'my-api' }
|
|
134
|
-
});
|
|
125
|
+
const span1 = createMockSpan({ resourceAttributes: { 'service.name': 'my-api' } });
|
|
126
|
+
const span2 = createMockSpan({ resourceAttributes: { 'service.name': 'my-api' } });
|
|
135
127
|
|
|
136
128
|
processor.onEnd(span1);
|
|
137
129
|
processor.onEnd(span2);
|
|
138
130
|
|
|
139
|
-
// Should only send one service registration
|
|
140
131
|
const serviceMessages = sentMessages.filter(m => m.event === 'new-entity' && m.type === 'service');
|
|
141
132
|
expect(serviceMessages).toHaveLength(1);
|
|
142
133
|
});
|
|
143
134
|
|
|
144
135
|
it('should handle service without namespace and environment', () => {
|
|
145
|
-
const span = createMockSpan({
|
|
146
|
-
resourceAttributes: { 'service.name': 'simple-service' }
|
|
147
|
-
});
|
|
136
|
+
const span = createMockSpan({ resourceAttributes: { 'service.name': 'simple-service' } });
|
|
148
137
|
|
|
149
138
|
processor.onEnd(span);
|
|
150
139
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
});
|
|
156
|
-
expect(sentMessages[0]).not.toHaveProperty('namespace');
|
|
157
|
-
expect(sentMessages[0]).not.toHaveProperty('environment');
|
|
140
|
+
const serviceMsg = sentMessages.find(m => m.type === 'service');
|
|
141
|
+
expect(serviceMsg).toMatchObject({ name: 'simple-service' });
|
|
142
|
+
expect(serviceMsg).not.toHaveProperty('namespace');
|
|
143
|
+
expect(serviceMsg).not.toHaveProperty('environment');
|
|
158
144
|
});
|
|
159
145
|
|
|
160
146
|
it('should ignore spans without service.name', () => {
|
|
161
|
-
const span = createMockSpan({
|
|
162
|
-
resourceAttributes: {}
|
|
163
|
-
});
|
|
147
|
+
const span = createMockSpan({ resourceAttributes: {} });
|
|
164
148
|
|
|
165
149
|
processor.onEnd(span);
|
|
166
150
|
|
|
167
151
|
expect(sentMessages).toHaveLength(0);
|
|
168
152
|
});
|
|
169
|
-
});
|
|
170
153
|
|
|
171
|
-
|
|
172
|
-
it('should process HTTP server spans and create route entities', () => {
|
|
154
|
+
it('should set process context on first service discovery', () => {
|
|
173
155
|
const span = createMockSpan({
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
'
|
|
177
|
-
'http.method': 'GET',
|
|
178
|
-
'http.target': '/api/users/123',
|
|
179
|
-
'http.status_code': 200
|
|
156
|
+
resourceAttributes: {
|
|
157
|
+
'service.name': 'my-api',
|
|
158
|
+
'deployment.environment': 'prod',
|
|
180
159
|
}
|
|
181
160
|
});
|
|
182
161
|
|
|
183
162
|
processor.onEnd(span);
|
|
184
163
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
expect(
|
|
190
|
-
event: 'new-entity',
|
|
191
|
-
type: 'http-route',
|
|
192
|
-
method: 'GET',
|
|
193
|
-
route: '/api/users/{id}'
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
const observationMessage = sentMessages.find(m => m.event === 'observations') as ObservationMessage;
|
|
197
|
-
expect(observationMessage.observations).toHaveLength(1);
|
|
198
|
-
|
|
199
|
-
const observation = observationMessage.observations[0] as HttpServerObservation;
|
|
200
|
-
expect(observation).toMatchObject({
|
|
201
|
-
type: 'http-server',
|
|
202
|
-
path: '/api/users/123',
|
|
203
|
-
status: 200
|
|
204
|
-
});
|
|
164
|
+
expect(mockConnection.setProcessContext).toHaveBeenCalledTimes(1);
|
|
165
|
+
const [hash, resources] = mockConnection.setProcessContext.mock.calls[0];
|
|
166
|
+
expect(hash).toBeDefined();
|
|
167
|
+
expect(resources['service.name']).toBe('my-api');
|
|
168
|
+
expect(resources['deployment.environment']).toBe('prod');
|
|
205
169
|
});
|
|
206
170
|
|
|
207
|
-
it('should
|
|
208
|
-
const
|
|
209
|
-
|
|
210
|
-
attributes: {
|
|
211
|
-
'http.route': '/api/test',
|
|
212
|
-
'http.method': 'POST',
|
|
213
|
-
'http.url': 'https://example.com/api/test?param=value',
|
|
214
|
-
'http.status_code': 201
|
|
215
|
-
}
|
|
216
|
-
});
|
|
171
|
+
it('should only set process context once', () => {
|
|
172
|
+
const span1 = createMockSpan({ resourceAttributes: { 'service.name': 'svc1' } });
|
|
173
|
+
const span2 = createMockSpan({ resourceAttributes: { 'service.name': 'svc2' } });
|
|
217
174
|
|
|
218
|
-
processor.onEnd(
|
|
175
|
+
processor.onEnd(span1);
|
|
176
|
+
processor.onEnd(span2);
|
|
219
177
|
|
|
220
|
-
|
|
221
|
-
const observation = observationMessage.observations[0] as HttpServerObservation;
|
|
222
|
-
expect(observation.path).toBe('/api/test');
|
|
178
|
+
expect(mockConnection.setProcessContext).toHaveBeenCalledTimes(1);
|
|
223
179
|
});
|
|
180
|
+
});
|
|
224
181
|
|
|
225
|
-
|
|
182
|
+
describe('HTTP Route Processing', () => {
|
|
183
|
+
it('should process HTTP server spans with V2 observations (spanId, traceId)', () => {
|
|
226
184
|
const span = createMockSpan({
|
|
227
185
|
kind: SpanKind.SERVER,
|
|
228
186
|
attributes: {
|
|
229
|
-
'http.route': '/api/
|
|
187
|
+
'http.route': '/api/users/{id}',
|
|
230
188
|
'http.method': 'GET',
|
|
231
|
-
'http.target': '
|
|
232
|
-
'http.status_code':
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
processor.onEnd(span);
|
|
237
|
-
|
|
238
|
-
const observationMessage = sentMessages.find(m => m.event === 'observations') as ObservationMessage;
|
|
239
|
-
const observation = observationMessage.observations[0] as HttpServerObservation;
|
|
240
|
-
// When used with placeholder base, 'invalid-url' becomes '/invalid-url'
|
|
241
|
-
expect(observation.path).toBe('/invalid-url');
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
it('should include optional HTTP attributes when present', () => {
|
|
245
|
-
const span = createMockSpan({
|
|
246
|
-
kind: SpanKind.SERVER,
|
|
247
|
-
attributes: {
|
|
248
|
-
'http.route': '/api/upload',
|
|
249
|
-
'http.method': 'POST',
|
|
250
|
-
'http.target': '/api/upload',
|
|
251
|
-
'http.status_code': 200,
|
|
252
|
-
'http.flavor': '1.1',
|
|
253
|
-
'http.user_agent': 'Mozilla/5.0',
|
|
254
|
-
'http.request_content_length': 1024,
|
|
255
|
-
'http.response_content_length': 256
|
|
256
|
-
}
|
|
189
|
+
'http.target': '/api/users/123',
|
|
190
|
+
'http.status_code': 200
|
|
191
|
+
},
|
|
192
|
+
traceId: 'trace-abc',
|
|
193
|
+
spanId: 'span-123',
|
|
257
194
|
});
|
|
258
195
|
|
|
259
196
|
processor.onEnd(span);
|
|
260
197
|
|
|
261
|
-
const observationMessage = sentMessages.find(m => m.event === 'observations')
|
|
198
|
+
const observationMessage = sentMessages.find(m => m.event === 'observations');
|
|
262
199
|
const observation = observationMessage.observations[0] as HttpServerObservation;
|
|
263
200
|
expect(observation).toMatchObject({
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
201
|
+
type: 'http-server',
|
|
202
|
+
path: '/api/users/123',
|
|
203
|
+
status: 200,
|
|
204
|
+
spanId: 'span-123',
|
|
205
|
+
traceId: 'trace-abc',
|
|
268
206
|
});
|
|
269
207
|
});
|
|
270
208
|
|
|
271
209
|
it('should not duplicate route registrations', () => {
|
|
272
210
|
const span1 = createMockSpan({
|
|
273
211
|
kind: SpanKind.SERVER,
|
|
274
|
-
attributes: {
|
|
275
|
-
'http.route': '/api/users',
|
|
276
|
-
'http.method': 'GET',
|
|
277
|
-
'http.status_code': 200
|
|
278
|
-
}
|
|
212
|
+
attributes: { 'http.route': '/api/users', 'http.method': 'GET', 'http.status_code': 200 }
|
|
279
213
|
});
|
|
280
214
|
const span2 = createMockSpan({
|
|
281
215
|
kind: SpanKind.SERVER,
|
|
282
|
-
attributes: {
|
|
283
|
-
'http.route': '/api/users',
|
|
284
|
-
'http.method': 'GET',
|
|
285
|
-
'http.status_code': 200
|
|
286
|
-
}
|
|
216
|
+
attributes: { 'http.route': '/api/users', 'http.method': 'GET', 'http.status_code': 200 }
|
|
287
217
|
});
|
|
288
218
|
|
|
289
219
|
processor.onEnd(span1);
|
|
@@ -295,112 +225,59 @@ describe('ComprehendDevSpanProcessor', () => {
|
|
|
295
225
|
});
|
|
296
226
|
|
|
297
227
|
describe('HTTP Client Processing', () => {
|
|
298
|
-
it('should process HTTP client spans
|
|
228
|
+
it('should process HTTP client spans with V2 observations', () => {
|
|
299
229
|
const span = createMockSpan({
|
|
300
230
|
kind: SpanKind.CLIENT,
|
|
301
231
|
attributes: {
|
|
302
232
|
'http.url': 'https://api.external.com/v1/data',
|
|
303
233
|
'http.method': 'GET',
|
|
304
234
|
'http.status_code': 200
|
|
305
|
-
}
|
|
235
|
+
},
|
|
236
|
+
traceId: 'trace-xyz',
|
|
237
|
+
spanId: 'span-456',
|
|
306
238
|
});
|
|
307
239
|
|
|
308
240
|
processor.onEnd(span);
|
|
309
241
|
|
|
310
|
-
|
|
311
|
-
expect(sentMessages).toHaveLength(4);
|
|
312
|
-
|
|
313
|
-
const httpServiceMessage = sentMessages.find(m => m.type === 'http-service') as NewObservedHttpServiceMessage;
|
|
314
|
-
expect(httpServiceMessage).toMatchObject({
|
|
315
|
-
event: 'new-entity',
|
|
316
|
-
type: 'http-service',
|
|
317
|
-
protocol: 'https',
|
|
318
|
-
host: 'api.external.com',
|
|
319
|
-
port: 443
|
|
320
|
-
});
|
|
321
|
-
|
|
322
|
-
const httpRequestMessage = sentMessages.find(m => m.type === 'http-request') as NewObservedHttpRequestMessage;
|
|
323
|
-
expect(httpRequestMessage).toMatchObject({
|
|
324
|
-
event: 'new-interaction',
|
|
325
|
-
type: 'http-request'
|
|
326
|
-
});
|
|
327
|
-
|
|
328
|
-
const observationMessage = sentMessages.find(m => m.event === 'observations') as ObservationMessage;
|
|
242
|
+
const observationMessage = sentMessages.find(m => m.event === 'observations');
|
|
329
243
|
const observation = observationMessage.observations[0] as HttpClientObservation;
|
|
330
244
|
expect(observation).toMatchObject({
|
|
331
245
|
type: 'http-client',
|
|
332
246
|
path: '/v1/data',
|
|
333
247
|
method: 'GET',
|
|
334
|
-
status: 200
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
it('should handle HTTP on port 80 and HTTPS on port 443 correctly', () => {
|
|
339
|
-
const httpSpan = createMockSpan({
|
|
340
|
-
kind: SpanKind.CLIENT,
|
|
341
|
-
attributes: {
|
|
342
|
-
'http.url': 'http://api.example.com/test',
|
|
343
|
-
'http.method': 'GET'
|
|
344
|
-
}
|
|
345
|
-
});
|
|
346
|
-
|
|
347
|
-
processor.onEnd(httpSpan);
|
|
348
|
-
|
|
349
|
-
const httpServiceMessage = sentMessages.find(m => m.type === 'http-service') as NewObservedHttpServiceMessage;
|
|
350
|
-
expect(httpServiceMessage).toMatchObject({
|
|
351
|
-
protocol: 'http',
|
|
352
|
-
host: 'api.example.com',
|
|
353
|
-
port: 80
|
|
354
|
-
});
|
|
355
|
-
});
|
|
356
|
-
|
|
357
|
-
it('should handle custom ports', () => {
|
|
358
|
-
const span = createMockSpan({
|
|
359
|
-
kind: SpanKind.CLIENT,
|
|
360
|
-
attributes: {
|
|
361
|
-
'http.url': 'https://api.example.com:8443/test',
|
|
362
|
-
'http.method': 'POST'
|
|
363
|
-
}
|
|
364
|
-
});
|
|
365
|
-
|
|
366
|
-
processor.onEnd(span);
|
|
367
|
-
|
|
368
|
-
const httpServiceMessage = sentMessages.find(m => m.type === 'http-service') as NewObservedHttpServiceMessage;
|
|
369
|
-
expect(httpServiceMessage).toMatchObject({
|
|
370
|
-
protocol: 'https',
|
|
371
|
-
host: 'api.example.com',
|
|
372
|
-
port: 8443
|
|
248
|
+
status: 200,
|
|
249
|
+
spanId: 'span-456',
|
|
250
|
+
traceId: 'trace-xyz',
|
|
373
251
|
});
|
|
374
252
|
});
|
|
253
|
+
});
|
|
375
254
|
|
|
376
|
-
|
|
255
|
+
describe('Database Processing', () => {
|
|
256
|
+
it('should send DatabaseQueryMessage for SQL queries (server-side analysis)', () => {
|
|
377
257
|
const span = createMockSpan({
|
|
378
|
-
kind: SpanKind.CLIENT,
|
|
379
258
|
attributes: {
|
|
380
|
-
'
|
|
381
|
-
|
|
259
|
+
'db.system': 'postgresql',
|
|
260
|
+
'db.statement': 'SELECT * FROM users WHERE id = $1',
|
|
261
|
+
'db.name': 'myapp',
|
|
262
|
+
'net.peer.name': 'localhost',
|
|
263
|
+
'db.response.returned_rows': 1
|
|
382
264
|
}
|
|
383
265
|
});
|
|
384
266
|
|
|
385
267
|
processor.onEnd(span);
|
|
386
268
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
describe('Database Processing', () => {
|
|
394
|
-
beforeEach(() => {
|
|
395
|
-
// Setup SQL analyzer mock
|
|
396
|
-
mockedAnalyzeSQL.mockReturnValue({
|
|
397
|
-
tableOperations: { users: ['SELECT'] },
|
|
398
|
-
normalizedQuery: 'SELECT * FROM users',
|
|
399
|
-
presentableQuery: 'SELECT * FROM users'
|
|
269
|
+
const dbQueryMsg = sentMessages.find(m => m.event === 'db-query') as DatabaseQueryMessage;
|
|
270
|
+
expect(dbQueryMsg).toMatchObject({
|
|
271
|
+
event: 'db-query',
|
|
272
|
+
query: 'SELECT * FROM users WHERE id = $1',
|
|
273
|
+
returnedRows: 1,
|
|
400
274
|
});
|
|
275
|
+
expect(dbQueryMsg.from).toBeDefined();
|
|
276
|
+
expect(dbQueryMsg.to).toBeDefined();
|
|
277
|
+
expect(dbQueryMsg.seq).toBeDefined();
|
|
401
278
|
});
|
|
402
279
|
|
|
403
|
-
it('should
|
|
280
|
+
it('should create database entity and connection interaction', () => {
|
|
404
281
|
const span = createMockSpan({
|
|
405
282
|
attributes: {
|
|
406
283
|
'db.system': 'postgresql',
|
|
@@ -412,7 +289,7 @@ describe('ComprehendDevSpanProcessor', () => {
|
|
|
412
289
|
|
|
413
290
|
processor.onEnd(span);
|
|
414
291
|
|
|
415
|
-
const databaseMessage = sentMessages.find(m => m.type === 'database')
|
|
292
|
+
const databaseMessage = sentMessages.find(m => m.type === 'database');
|
|
416
293
|
expect(databaseMessage).toMatchObject({
|
|
417
294
|
event: 'new-entity',
|
|
418
295
|
type: 'database',
|
|
@@ -421,133 +298,185 @@ describe('ComprehendDevSpanProcessor', () => {
|
|
|
421
298
|
host: 'db.example.com',
|
|
422
299
|
port: 5432
|
|
423
300
|
});
|
|
424
|
-
});
|
|
425
|
-
|
|
426
|
-
it('should process database connection strings', () => {
|
|
427
|
-
const span = createMockSpan({
|
|
428
|
-
attributes: {
|
|
429
|
-
'db.system': 'postgresql',
|
|
430
|
-
'db.connection_string': 'postgresql://user:password@localhost:5432/testdb'
|
|
431
|
-
}
|
|
432
|
-
});
|
|
433
|
-
|
|
434
|
-
processor.onEnd(span);
|
|
435
|
-
|
|
436
|
-
const databaseMessage = sentMessages.find(m => m.type === 'database') as NewObservedDatabaseMessage;
|
|
437
|
-
expect(databaseMessage).toMatchObject({
|
|
438
|
-
system: 'postgresql',
|
|
439
|
-
host: 'localhost',
|
|
440
|
-
port: 5432,
|
|
441
|
-
name: 'testdb'
|
|
442
|
-
});
|
|
443
301
|
|
|
444
|
-
const connectionMessage = sentMessages.find(m => m.type === 'db-connection')
|
|
302
|
+
const connectionMessage = sentMessages.find(m => m.type === 'db-connection');
|
|
445
303
|
expect(connectionMessage).toMatchObject({
|
|
446
304
|
event: 'new-interaction',
|
|
447
305
|
type: 'db-connection',
|
|
448
|
-
user: 'user'
|
|
449
306
|
});
|
|
450
307
|
});
|
|
451
308
|
|
|
452
|
-
it('should
|
|
309
|
+
it('should not send db-query for non-SQL database systems', () => {
|
|
453
310
|
const span = createMockSpan({
|
|
454
311
|
attributes: {
|
|
455
|
-
'db.system': '
|
|
456
|
-
'db.statement': '
|
|
457
|
-
'db.name': 'myapp'
|
|
458
|
-
'net.peer.name': 'localhost',
|
|
459
|
-
'db.response.returned_rows': 1
|
|
312
|
+
'db.system': 'mongodb',
|
|
313
|
+
'db.statement': 'db.users.find({})',
|
|
314
|
+
'db.name': 'myapp'
|
|
460
315
|
}
|
|
461
316
|
});
|
|
462
317
|
|
|
463
318
|
processor.onEnd(span);
|
|
464
319
|
|
|
465
|
-
|
|
320
|
+
const dbQueryMessages = sentMessages.filter(m => m.event === 'db-query');
|
|
321
|
+
expect(dbQueryMessages).toHaveLength(0);
|
|
322
|
+
});
|
|
323
|
+
});
|
|
466
324
|
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
325
|
+
describe('Trace Span Reporting', () => {
|
|
326
|
+
it('should send TraceSpansMessage for every span', () => {
|
|
327
|
+
const span = createMockSpan({
|
|
328
|
+
name: 'GET /api/users',
|
|
329
|
+
kind: SpanKind.SERVER,
|
|
330
|
+
attributes: {
|
|
331
|
+
'http.route': '/api/users',
|
|
332
|
+
'http.method': 'GET',
|
|
333
|
+
'http.status_code': 200,
|
|
334
|
+
},
|
|
335
|
+
traceId: 'trace-001',
|
|
336
|
+
spanId: 'span-001',
|
|
337
|
+
parentSpanId: 'span-000',
|
|
473
338
|
});
|
|
474
339
|
|
|
475
|
-
|
|
476
|
-
const observation = observationMessage.observations[0] as DatabaseQueryObservation;
|
|
477
|
-
expect(observation).toMatchObject({
|
|
478
|
-
type: 'db-query',
|
|
479
|
-
returnedRows: 1
|
|
480
|
-
});
|
|
481
|
-
});
|
|
340
|
+
processor.onEnd(span);
|
|
482
341
|
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
presentableQuery: 'INSERT INTO orders SELECT * FROM users UPDATE products'
|
|
342
|
+
const traceSpanMsg = sentMessages.find(m => m.event === 'tracespans') as TraceSpansMessage;
|
|
343
|
+
expect(traceSpanMsg).toBeDefined();
|
|
344
|
+
expect(traceSpanMsg.data).toHaveLength(1);
|
|
345
|
+
expect(traceSpanMsg.data[0]).toMatchObject({
|
|
346
|
+
trace: 'trace-001',
|
|
347
|
+
span: 'span-001',
|
|
348
|
+
parent: 'span-000',
|
|
349
|
+
name: 'GET /api/users',
|
|
492
350
|
});
|
|
351
|
+
});
|
|
493
352
|
|
|
353
|
+
it('should use empty string for parent when no parentSpanId', () => {
|
|
494
354
|
const span = createMockSpan({
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
'db.name': 'ecommerce'
|
|
499
|
-
}
|
|
355
|
+
traceId: 'trace-root',
|
|
356
|
+
spanId: 'span-root',
|
|
357
|
+
// no parentSpanId
|
|
500
358
|
});
|
|
501
359
|
|
|
502
360
|
processor.onEnd(span);
|
|
503
361
|
|
|
504
|
-
const
|
|
505
|
-
expect(
|
|
506
|
-
selects: ['users'],
|
|
507
|
-
inserts: ['orders'],
|
|
508
|
-
updates: ['products']
|
|
509
|
-
});
|
|
362
|
+
const traceSpanMsg = sentMessages.find(m => m.event === 'tracespans') as TraceSpansMessage;
|
|
363
|
+
expect(traceSpanMsg.data[0].parent).toBe('');
|
|
510
364
|
});
|
|
511
365
|
|
|
512
|
-
it('should
|
|
366
|
+
it('should send trace span even for internal spans with no http/db attributes', () => {
|
|
513
367
|
const span = createMockSpan({
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
'db.statement': 'db.users.find({_id: ObjectId("...")}))',
|
|
517
|
-
'db.name': 'myapp'
|
|
518
|
-
}
|
|
368
|
+
name: 'internal-operation',
|
|
369
|
+
kind: SpanKind.INTERNAL,
|
|
519
370
|
});
|
|
520
371
|
|
|
521
372
|
processor.onEnd(span);
|
|
522
373
|
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
// Should still create database and connection, but no query interaction
|
|
526
|
-
const queryMessages = sentMessages.filter(m => m.type === 'db-query');
|
|
527
|
-
expect(queryMessages).toHaveLength(0);
|
|
374
|
+
const traceSpanMsg = sentMessages.find(m => m.event === 'tracespans');
|
|
375
|
+
expect(traceSpanMsg).toBeDefined();
|
|
528
376
|
});
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
describe('Custom Span Observation Matching', () => {
|
|
380
|
+
it('should match spans by type rule', () => {
|
|
381
|
+
processor.updateCustomMetrics([{
|
|
382
|
+
type: 'span',
|
|
383
|
+
rule: { kind: 'type', value: 'server' },
|
|
384
|
+
subject: 'custom-server-obs',
|
|
385
|
+
}]);
|
|
529
386
|
|
|
530
|
-
it('should handle alternative database connection string formats', () => {
|
|
531
387
|
const span = createMockSpan({
|
|
388
|
+
kind: SpanKind.SERVER,
|
|
532
389
|
attributes: {
|
|
533
|
-
'
|
|
534
|
-
'
|
|
535
|
-
|
|
390
|
+
'http.route': '/test',
|
|
391
|
+
'http.method': 'GET',
|
|
392
|
+
'http.status_code': 200,
|
|
393
|
+
},
|
|
536
394
|
});
|
|
537
395
|
|
|
538
396
|
processor.onEnd(span);
|
|
539
397
|
|
|
540
|
-
const
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
name: 'TestDB'
|
|
545
|
-
});
|
|
398
|
+
const customObs = sentMessages
|
|
399
|
+
.filter(m => m.event === 'observations')
|
|
400
|
+
.flatMap((m: ObservationMessage) => m.observations)
|
|
401
|
+
.find((o: any) => o.type === 'custom') as CustomObservation;
|
|
546
402
|
|
|
547
|
-
|
|
548
|
-
expect(
|
|
549
|
-
|
|
403
|
+
expect(customObs).toBeDefined();
|
|
404
|
+
expect(customObs.subject).toBe('custom-server-obs');
|
|
405
|
+
expect(customObs.id).toBe('custom-server-obs');
|
|
406
|
+
expect(customObs.attributes).toBeDefined();
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
it('should match spans by attribute-present rule', () => {
|
|
410
|
+
processor.updateCustomMetrics([{
|
|
411
|
+
type: 'span',
|
|
412
|
+
rule: { kind: 'attribute-present', key: 'custom.attr' },
|
|
413
|
+
subject: 'has-custom-attr',
|
|
414
|
+
}]);
|
|
415
|
+
|
|
416
|
+
const spanWith = createMockSpan({ attributes: { 'custom.attr': 'value' } });
|
|
417
|
+
const spanWithout = createMockSpan({ attributes: {} });
|
|
418
|
+
|
|
419
|
+
processor.onEnd(spanWith);
|
|
420
|
+
processor.onEnd(spanWithout);
|
|
421
|
+
|
|
422
|
+
const customObs = sentMessages
|
|
423
|
+
.filter(m => m.event === 'observations')
|
|
424
|
+
.flatMap((m: ObservationMessage) => m.observations)
|
|
425
|
+
.filter((o: any) => o.type === 'custom');
|
|
426
|
+
|
|
427
|
+
expect(customObs).toHaveLength(1);
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
it('should match spans by compound all rule', () => {
|
|
431
|
+
processor.updateCustomMetrics([{
|
|
432
|
+
type: 'span',
|
|
433
|
+
rule: {
|
|
434
|
+
kind: 'all',
|
|
435
|
+
rules: [
|
|
436
|
+
{ kind: 'type', value: 'client' },
|
|
437
|
+
{ kind: 'attribute-equals', key: 'rpc.system', value: 'grpc' },
|
|
438
|
+
]
|
|
439
|
+
},
|
|
440
|
+
subject: 'grpc-client',
|
|
441
|
+
}]);
|
|
442
|
+
|
|
443
|
+
const matchingSpan = createMockSpan({
|
|
444
|
+
kind: SpanKind.CLIENT,
|
|
445
|
+
attributes: { 'rpc.system': 'grpc', 'http.url': 'http://x' },
|
|
550
446
|
});
|
|
447
|
+
const nonMatchingSpan = createMockSpan({
|
|
448
|
+
kind: SpanKind.CLIENT,
|
|
449
|
+
attributes: { 'rpc.system': 'thrift' },
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
processor.onEnd(matchingSpan);
|
|
453
|
+
processor.onEnd(nonMatchingSpan);
|
|
454
|
+
|
|
455
|
+
const customObs = sentMessages
|
|
456
|
+
.filter(m => m.event === 'observations')
|
|
457
|
+
.flatMap((m: ObservationMessage) => m.observations)
|
|
458
|
+
.filter((o: any) => o.type === 'custom');
|
|
459
|
+
|
|
460
|
+
expect(customObs).toHaveLength(1);
|
|
461
|
+
expect(customObs[0].subject).toBe('grpc-client');
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
it('should not create custom observations when no specs match', () => {
|
|
465
|
+
processor.updateCustomMetrics([{
|
|
466
|
+
type: 'span',
|
|
467
|
+
rule: { kind: 'type', value: 'internal' },
|
|
468
|
+
subject: 'internal-only',
|
|
469
|
+
}]);
|
|
470
|
+
|
|
471
|
+
const span = createMockSpan({ kind: SpanKind.SERVER });
|
|
472
|
+
processor.onEnd(span);
|
|
473
|
+
|
|
474
|
+
const customObs = sentMessages
|
|
475
|
+
.filter(m => m.event === 'observations')
|
|
476
|
+
.flatMap((m: ObservationMessage) => m.observations)
|
|
477
|
+
.filter((o: any) => o.type === 'custom');
|
|
478
|
+
|
|
479
|
+
expect(customObs).toHaveLength(0);
|
|
551
480
|
});
|
|
552
481
|
});
|
|
553
482
|
|
|
@@ -573,126 +502,41 @@ describe('ComprehendDevSpanProcessor', () => {
|
|
|
573
502
|
|
|
574
503
|
processor.onEnd(span);
|
|
575
504
|
|
|
576
|
-
const observationMessage = sentMessages.find(m => m.event === 'observations')
|
|
577
|
-
const observation = observationMessage.observations[0]
|
|
505
|
+
const observationMessage = sentMessages.find(m => m.event === 'observations');
|
|
506
|
+
const observation = observationMessage.observations[0];
|
|
578
507
|
expect(observation).toMatchObject({
|
|
579
508
|
errorMessage: 'Database connection failed',
|
|
580
509
|
errorType: 'ConnectionError',
|
|
581
510
|
stack: 'Error at line 42\n at method()'
|
|
582
511
|
});
|
|
583
512
|
});
|
|
584
|
-
|
|
585
|
-
it('should fallback to span attributes for error information', () => {
|
|
586
|
-
const span = createMockSpan({
|
|
587
|
-
kind: SpanKind.CLIENT,
|
|
588
|
-
attributes: {
|
|
589
|
-
'http.url': 'https://api.example.com/fail',
|
|
590
|
-
'http.method': 'POST',
|
|
591
|
-
'http.status_code': 404,
|
|
592
|
-
'exception.message': 'Not found',
|
|
593
|
-
'exception.type': 'HttpError'
|
|
594
|
-
},
|
|
595
|
-
status: { code: SpanStatusCode.ERROR }
|
|
596
|
-
});
|
|
597
|
-
|
|
598
|
-
processor.onEnd(span);
|
|
599
|
-
|
|
600
|
-
const observationMessage = sentMessages.find(m => m.event === 'observations') as ObservationMessage;
|
|
601
|
-
const observation = observationMessage.observations[0] as HttpClientObservation;
|
|
602
|
-
expect(observation).toMatchObject({
|
|
603
|
-
errorMessage: 'Not found',
|
|
604
|
-
errorType: 'HttpError'
|
|
605
|
-
});
|
|
606
|
-
});
|
|
607
|
-
|
|
608
|
-
it('should handle spans with error status but no explicit error attributes', () => {
|
|
609
|
-
const span = createMockSpan({
|
|
610
|
-
kind: SpanKind.SERVER,
|
|
611
|
-
attributes: {
|
|
612
|
-
'http.route': '/api/timeout',
|
|
613
|
-
'http.method': 'GET',
|
|
614
|
-
'http.status_code': 500
|
|
615
|
-
},
|
|
616
|
-
status: { code: SpanStatusCode.ERROR, message: 'Request timeout' }
|
|
617
|
-
});
|
|
618
|
-
|
|
619
|
-
processor.onEnd(span);
|
|
620
|
-
|
|
621
|
-
const observationMessage = sentMessages.find(m => m.event === 'observations') as ObservationMessage;
|
|
622
|
-
const observation = observationMessage.observations[0] as HttpServerObservation;
|
|
623
|
-
expect(observation.errorMessage).toBe('Request timeout');
|
|
624
|
-
});
|
|
625
513
|
});
|
|
626
514
|
|
|
627
|
-
describe('Sequence Numbers
|
|
628
|
-
it('should
|
|
515
|
+
describe('Sequence Numbers', () => {
|
|
516
|
+
it('should use connection.nextSeq() for observation messages', () => {
|
|
629
517
|
const span1 = createMockSpan({
|
|
630
518
|
kind: SpanKind.SERVER,
|
|
631
|
-
attributes: {
|
|
632
|
-
'http.route': '/api/test1',
|
|
633
|
-
'http.method': 'GET',
|
|
634
|
-
'http.status_code': 200
|
|
635
|
-
}
|
|
519
|
+
attributes: { 'http.route': '/api/test1', 'http.method': 'GET', 'http.status_code': 200 }
|
|
636
520
|
});
|
|
637
521
|
const span2 = createMockSpan({
|
|
638
522
|
kind: SpanKind.SERVER,
|
|
639
|
-
attributes: {
|
|
640
|
-
'http.route': '/api/test2',
|
|
641
|
-
'http.method': 'GET',
|
|
642
|
-
'http.status_code': 200
|
|
643
|
-
}
|
|
523
|
+
attributes: { 'http.route': '/api/test2', 'http.method': 'GET', 'http.status_code': 200 }
|
|
644
524
|
});
|
|
645
525
|
|
|
646
526
|
processor.onEnd(span1);
|
|
647
527
|
processor.onEnd(span2);
|
|
648
528
|
|
|
649
|
-
|
|
650
|
-
expect(
|
|
651
|
-
|
|
652
|
-
|
|
529
|
+
// nextSeq is called for observations AND tracespans
|
|
530
|
+
expect(mockConnection.nextSeq).toHaveBeenCalled();
|
|
531
|
+
|
|
532
|
+
const observationMessages = sentMessages.filter(m => m.event === 'observations');
|
|
533
|
+
// Each should have a unique seq
|
|
534
|
+
const seqs = observationMessages.map((m: any) => m.seq);
|
|
535
|
+
expect(new Set(seqs).size).toBe(seqs.length);
|
|
653
536
|
});
|
|
654
537
|
});
|
|
655
538
|
|
|
656
539
|
describe('Processor Lifecycle', () => {
|
|
657
|
-
it('should construct with debug logging', () => {
|
|
658
|
-
const mockLogger = jest.fn();
|
|
659
|
-
const debugProcessor = new ComprehendDevSpanProcessor({
|
|
660
|
-
organization: 'test-org',
|
|
661
|
-
token: 'test-token',
|
|
662
|
-
debug: mockLogger
|
|
663
|
-
});
|
|
664
|
-
|
|
665
|
-
expect(MockedWebSocketConnection).toHaveBeenCalledWith('test-org', 'test-token', mockLogger);
|
|
666
|
-
debugProcessor.shutdown();
|
|
667
|
-
});
|
|
668
|
-
|
|
669
|
-
it('should construct with debug enabled as boolean', () => {
|
|
670
|
-
const debugProcessor = new ComprehendDevSpanProcessor({
|
|
671
|
-
organization: 'test-org',
|
|
672
|
-
token: 'test-token',
|
|
673
|
-
debug: true
|
|
674
|
-
});
|
|
675
|
-
|
|
676
|
-
expect(MockedWebSocketConnection).toHaveBeenCalledWith('test-org', 'test-token', console.log);
|
|
677
|
-
debugProcessor.shutdown();
|
|
678
|
-
});
|
|
679
|
-
|
|
680
|
-
it('should construct with debug disabled', () => {
|
|
681
|
-
const noDebugProcessor = new ComprehendDevSpanProcessor({
|
|
682
|
-
organization: 'test-org',
|
|
683
|
-
token: 'test-token',
|
|
684
|
-
debug: false
|
|
685
|
-
});
|
|
686
|
-
|
|
687
|
-
expect(MockedWebSocketConnection).toHaveBeenCalledWith('test-org', 'test-token', undefined);
|
|
688
|
-
noDebugProcessor.shutdown();
|
|
689
|
-
});
|
|
690
|
-
|
|
691
|
-
it('should call connection.close() on shutdown', async () => {
|
|
692
|
-
await processor.shutdown();
|
|
693
|
-
expect(mockConnection.close).toHaveBeenCalled();
|
|
694
|
-
});
|
|
695
|
-
|
|
696
540
|
it('should handle forceFlush without error', async () => {
|
|
697
541
|
await expect(processor.forceFlush()).resolves.toBeUndefined();
|
|
698
542
|
});
|
|
@@ -701,122 +545,82 @@ describe('ComprehendDevSpanProcessor', () => {
|
|
|
701
545
|
expect(() => processor.onStart({} as any, {} as any)).not.toThrow();
|
|
702
546
|
});
|
|
703
547
|
});
|
|
548
|
+
});
|
|
704
549
|
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
'http.user_agent': 'test-client/1.0',
|
|
715
|
-
'http.request_content_length': 0,
|
|
716
|
-
'http.response_content_length': 456
|
|
717
|
-
},
|
|
718
|
-
resourceAttributes: {
|
|
719
|
-
'service.name': 'user-api',
|
|
720
|
-
'service.namespace': 'backend',
|
|
721
|
-
'deployment.environment': 'staging'
|
|
722
|
-
},
|
|
723
|
-
startTime: [1700000000, 500000000],
|
|
724
|
-
duration: [0, 50000000] // 50ms
|
|
725
|
-
});
|
|
726
|
-
|
|
727
|
-
processor.onEnd(span);
|
|
728
|
-
|
|
729
|
-
// Verify all expected messages
|
|
730
|
-
expect(sentMessages).toHaveLength(3);
|
|
731
|
-
|
|
732
|
-
const serviceMessage = sentMessages[0] as NewObservedServiceMessage;
|
|
733
|
-
expect(serviceMessage).toMatchObject({
|
|
734
|
-
event: 'new-entity',
|
|
735
|
-
type: 'service',
|
|
736
|
-
name: 'user-api',
|
|
737
|
-
namespace: 'backend',
|
|
738
|
-
environment: 'staging'
|
|
739
|
-
});
|
|
740
|
-
|
|
741
|
-
const routeMessage = sentMessages[1] as NewObservedHttpRouteMessage;
|
|
742
|
-
expect(routeMessage).toMatchObject({
|
|
743
|
-
event: 'new-entity',
|
|
744
|
-
type: 'http-route',
|
|
745
|
-
parent: serviceMessage.hash,
|
|
746
|
-
method: 'GET',
|
|
747
|
-
route: '/api/users/{id}'
|
|
748
|
-
});
|
|
749
|
-
|
|
750
|
-
const observationMessage = sentMessages[2] as ObservationMessage;
|
|
751
|
-
expect(observationMessage.seq).toBe(1);
|
|
752
|
-
expect(observationMessage.observations).toHaveLength(1);
|
|
753
|
-
|
|
754
|
-
const observation = observationMessage.observations[0] as HttpServerObservation;
|
|
755
|
-
expect(observation).toMatchObject({
|
|
756
|
-
type: 'http-server',
|
|
757
|
-
subject: routeMessage.hash,
|
|
758
|
-
timestamp: [1700000000, 500000000],
|
|
759
|
-
path: '/api/users/123',
|
|
760
|
-
status: 200,
|
|
761
|
-
duration: [0, 50000000],
|
|
762
|
-
userAgent: 'test-client/1.0',
|
|
763
|
-
requestBytes: 0,
|
|
764
|
-
responseBytes: 456
|
|
765
|
-
});
|
|
766
|
-
});
|
|
550
|
+
describe('matchSpanRule', () => {
|
|
551
|
+
function createSpan(kind: SpanKind, attributes: Record<string, any> = {}): ReadableSpan {
|
|
552
|
+
return {
|
|
553
|
+
kind,
|
|
554
|
+
attributes,
|
|
555
|
+
name: 'test',
|
|
556
|
+
spanContext: () => ({ traceId: '', spanId: '', traceFlags: 0 }),
|
|
557
|
+
} as any;
|
|
558
|
+
}
|
|
767
559
|
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
});
|
|
560
|
+
it('should match type rule', () => {
|
|
561
|
+
expect(matchSpanRule(createSpan(SpanKind.SERVER), { kind: 'type', value: 'server' })).toBe(true);
|
|
562
|
+
expect(matchSpanRule(createSpan(SpanKind.CLIENT), { kind: 'type', value: 'server' })).toBe(false);
|
|
563
|
+
expect(matchSpanRule(createSpan(SpanKind.INTERNAL), { kind: 'type', value: 'internal' })).toBe(true);
|
|
564
|
+
});
|
|
774
565
|
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
'db.statement': 'INSERT INTO orders SELECT FROM users WHERE active = true',
|
|
780
|
-
'db.response.returned_rows': 5
|
|
781
|
-
},
|
|
782
|
-
startTime: [1700000000, 0],
|
|
783
|
-
duration: [0, 25000000] // 25ms
|
|
784
|
-
});
|
|
566
|
+
it('should match attribute-present rule', () => {
|
|
567
|
+
expect(matchSpanRule(createSpan(SpanKind.INTERNAL, { 'foo': 'bar' }), { kind: 'attribute-present', key: 'foo' })).toBe(true);
|
|
568
|
+
expect(matchSpanRule(createSpan(SpanKind.INTERNAL, {}), { kind: 'attribute-present', key: 'foo' })).toBe(false);
|
|
569
|
+
});
|
|
785
570
|
|
|
786
|
-
|
|
571
|
+
it('should match attribute-absent rule', () => {
|
|
572
|
+
expect(matchSpanRule(createSpan(SpanKind.INTERNAL, {}), { kind: 'attribute-absent', key: 'foo' })).toBe(true);
|
|
573
|
+
expect(matchSpanRule(createSpan(SpanKind.INTERNAL, { 'foo': 'bar' }), { kind: 'attribute-absent', key: 'foo' })).toBe(false);
|
|
574
|
+
});
|
|
787
575
|
|
|
788
|
-
|
|
789
|
-
|
|
576
|
+
it('should match attribute-equals rule', () => {
|
|
577
|
+
expect(matchSpanRule(createSpan(SpanKind.INTERNAL, { 'k': 'v' }), { kind: 'attribute-equals', key: 'k', value: 'v' })).toBe(true);
|
|
578
|
+
expect(matchSpanRule(createSpan(SpanKind.INTERNAL, { 'k': 'other' }), { kind: 'attribute-equals', key: 'k', value: 'v' })).toBe(false);
|
|
579
|
+
});
|
|
790
580
|
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
port: 5432,
|
|
796
|
-
name: 'ecommerce'
|
|
797
|
-
});
|
|
581
|
+
it('should match attribute-not-equals rule', () => {
|
|
582
|
+
expect(matchSpanRule(createSpan(SpanKind.INTERNAL, { 'k': 'other' }), { kind: 'attribute-not-equals', key: 'k', value: 'v' })).toBe(true);
|
|
583
|
+
expect(matchSpanRule(createSpan(SpanKind.INTERNAL, { 'k': 'v' }), { kind: 'attribute-not-equals', key: 'k', value: 'v' })).toBe(false);
|
|
584
|
+
});
|
|
798
585
|
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
586
|
+
it('should match all rule', () => {
|
|
587
|
+
const rule: SpanMatcherRule = {
|
|
588
|
+
kind: 'all',
|
|
589
|
+
rules: [
|
|
590
|
+
{ kind: 'type', value: 'server' },
|
|
591
|
+
{ kind: 'attribute-present', key: 'http.route' },
|
|
592
|
+
]
|
|
593
|
+
};
|
|
594
|
+
expect(matchSpanRule(createSpan(SpanKind.SERVER, { 'http.route': '/api' }), rule)).toBe(true);
|
|
595
|
+
expect(matchSpanRule(createSpan(SpanKind.SERVER, {}), rule)).toBe(false);
|
|
596
|
+
expect(matchSpanRule(createSpan(SpanKind.CLIENT, { 'http.route': '/api' }), rule)).toBe(false);
|
|
597
|
+
});
|
|
803
598
|
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
599
|
+
it('should match any rule', () => {
|
|
600
|
+
const rule: SpanMatcherRule = {
|
|
601
|
+
kind: 'any',
|
|
602
|
+
rules: [
|
|
603
|
+
{ kind: 'type', value: 'server' },
|
|
604
|
+
{ kind: 'type', value: 'client' },
|
|
605
|
+
]
|
|
606
|
+
};
|
|
607
|
+
expect(matchSpanRule(createSpan(SpanKind.SERVER), rule)).toBe(true);
|
|
608
|
+
expect(matchSpanRule(createSpan(SpanKind.CLIENT), rule)).toBe(true);
|
|
609
|
+
expect(matchSpanRule(createSpan(SpanKind.INTERNAL), rule)).toBe(false);
|
|
610
|
+
});
|
|
810
611
|
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
612
|
+
it('should handle nested rules', () => {
|
|
613
|
+
const rule: SpanMatcherRule = {
|
|
614
|
+
kind: 'all',
|
|
615
|
+
rules: [
|
|
616
|
+
{ kind: 'any', rules: [
|
|
617
|
+
{ kind: 'type', value: 'server' },
|
|
618
|
+
{ kind: 'type', value: 'client' },
|
|
619
|
+
]},
|
|
620
|
+
{ kind: 'attribute-equals', key: 'rpc.system', value: 'grpc' },
|
|
621
|
+
]
|
|
622
|
+
};
|
|
623
|
+
expect(matchSpanRule(createSpan(SpanKind.SERVER, { 'rpc.system': 'grpc' }), rule)).toBe(true);
|
|
624
|
+
expect(matchSpanRule(createSpan(SpanKind.INTERNAL, { 'rpc.system': 'grpc' }), rule)).toBe(false);
|
|
821
625
|
});
|
|
822
|
-
});
|
|
626
|
+
});
|