@comprehend/telemetry-node 0.1.3 → 0.1.4

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.
@@ -0,0 +1,674 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const api_1 = require("@opentelemetry/api");
4
+ const ComprehendDevSpanProcessor_1 = require("./ComprehendDevSpanProcessor");
5
+ const WebSocketConnection_1 = require("./WebSocketConnection");
6
+ // Mock the WebSocketConnection
7
+ jest.mock('./WebSocketConnection');
8
+ // Mock the SQL analyzer
9
+ jest.mock('./sql-analyzer', () => ({
10
+ analyzeSQL: jest.fn()
11
+ }));
12
+ const sql_analyzer_1 = require("./sql-analyzer");
13
+ const mockedAnalyzeSQL = sql_analyzer_1.analyzeSQL;
14
+ const MockedWebSocketConnection = WebSocketConnection_1.WebSocketConnection;
15
+ describe('ComprehendDevSpanProcessor', () => {
16
+ let processor;
17
+ let mockConnection;
18
+ let sentMessages;
19
+ beforeEach(() => {
20
+ // Reset mocks
21
+ MockedWebSocketConnection.mockClear();
22
+ mockedAnalyzeSQL.mockClear();
23
+ sentMessages = [];
24
+ // Create mock connection instance
25
+ mockConnection = {
26
+ sendMessage: jest.fn((message) => {
27
+ sentMessages.push(message);
28
+ }),
29
+ close: jest.fn()
30
+ };
31
+ MockedWebSocketConnection.mockImplementation(() => mockConnection);
32
+ // Create processor instance
33
+ processor = new ComprehendDevSpanProcessor_1.ComprehendDevSpanProcessor({
34
+ organization: 'test-org',
35
+ token: 'test-token'
36
+ });
37
+ });
38
+ afterEach(() => {
39
+ processor.shutdown();
40
+ });
41
+ // Helper function to create a mock span
42
+ function createMockSpan(options = {}) {
43
+ const { name = 'test-span', kind = api_1.SpanKind.INTERNAL, attributes = {}, resourceAttributes = { 'service.name': 'test-service' }, status = { code: api_1.SpanStatusCode.OK }, events = [], startTime = [1700000000, 123456789], duration = [0, 100000000] // 100ms
44
+ } = options;
45
+ return {
46
+ name,
47
+ kind,
48
+ attributes,
49
+ resource: {
50
+ attributes: resourceAttributes
51
+ },
52
+ status,
53
+ events: events.map(e => ({
54
+ name: e.name,
55
+ attributes: e.attributes || {},
56
+ time: startTime
57
+ })),
58
+ startTime,
59
+ duration
60
+ };
61
+ }
62
+ describe('Service Discovery', () => {
63
+ it('should register a new service and send NewObservedServiceMessage', () => {
64
+ const span = createMockSpan({
65
+ resourceAttributes: {
66
+ 'service.name': 'my-api',
67
+ 'service.namespace': 'production',
68
+ 'deployment.environment': 'prod'
69
+ }
70
+ });
71
+ processor.onEnd(span);
72
+ expect(sentMessages).toHaveLength(1);
73
+ expect(sentMessages[0]).toMatchObject({
74
+ event: 'new-entity',
75
+ type: 'service',
76
+ name: 'my-api',
77
+ namespace: 'production',
78
+ environment: 'prod'
79
+ });
80
+ expect(sentMessages[0].hash).toBeDefined();
81
+ });
82
+ it('should not duplicate service registrations', () => {
83
+ const span1 = createMockSpan({
84
+ resourceAttributes: { 'service.name': 'my-api' }
85
+ });
86
+ const span2 = createMockSpan({
87
+ resourceAttributes: { 'service.name': 'my-api' }
88
+ });
89
+ processor.onEnd(span1);
90
+ processor.onEnd(span2);
91
+ // Should only send one service registration
92
+ const serviceMessages = sentMessages.filter(m => m.event === 'new-entity' && m.type === 'service');
93
+ expect(serviceMessages).toHaveLength(1);
94
+ });
95
+ it('should handle service without namespace and environment', () => {
96
+ const span = createMockSpan({
97
+ resourceAttributes: { 'service.name': 'simple-service' }
98
+ });
99
+ processor.onEnd(span);
100
+ expect(sentMessages[0]).toMatchObject({
101
+ event: 'new-entity',
102
+ type: 'service',
103
+ name: 'simple-service'
104
+ });
105
+ expect(sentMessages[0]).not.toHaveProperty('namespace');
106
+ expect(sentMessages[0]).not.toHaveProperty('environment');
107
+ });
108
+ it('should ignore spans without service.name', () => {
109
+ const span = createMockSpan({
110
+ resourceAttributes: {}
111
+ });
112
+ processor.onEnd(span);
113
+ expect(sentMessages).toHaveLength(0);
114
+ });
115
+ });
116
+ describe('HTTP Route Processing', () => {
117
+ it('should process HTTP server spans and create route entities', () => {
118
+ const span = createMockSpan({
119
+ kind: api_1.SpanKind.SERVER,
120
+ attributes: {
121
+ 'http.route': '/api/users/{id}',
122
+ 'http.method': 'GET',
123
+ 'http.target': '/api/users/123',
124
+ 'http.status_code': 200
125
+ }
126
+ });
127
+ processor.onEnd(span);
128
+ // Should have service + route messages
129
+ expect(sentMessages).toHaveLength(3); // service, route, observation
130
+ const routeMessage = sentMessages.find(m => m.type === 'http-route');
131
+ expect(routeMessage).toMatchObject({
132
+ event: 'new-entity',
133
+ type: 'http-route',
134
+ method: 'GET',
135
+ route: '/api/users/{id}'
136
+ });
137
+ const observationMessage = sentMessages.find(m => m.event === 'observations');
138
+ expect(observationMessage.observations).toHaveLength(1);
139
+ const observation = observationMessage.observations[0];
140
+ expect(observation).toMatchObject({
141
+ type: 'http-server',
142
+ path: '/api/users/123',
143
+ status: 200
144
+ });
145
+ });
146
+ it('should extract path from http.url when http.target is not available', () => {
147
+ const span = createMockSpan({
148
+ kind: api_1.SpanKind.SERVER,
149
+ attributes: {
150
+ 'http.route': '/api/test',
151
+ 'http.method': 'POST',
152
+ 'http.url': 'https://example.com/api/test?param=value',
153
+ 'http.status_code': 201
154
+ }
155
+ });
156
+ processor.onEnd(span);
157
+ const observationMessage = sentMessages.find(m => m.event === 'observations');
158
+ const observation = observationMessage.observations[0];
159
+ expect(observation.path).toBe('/api/test');
160
+ });
161
+ it('should handle relative URLs in http.target', () => {
162
+ const span = createMockSpan({
163
+ kind: api_1.SpanKind.SERVER,
164
+ attributes: {
165
+ 'http.route': '/api/test',
166
+ 'http.method': 'GET',
167
+ 'http.target': 'invalid-url',
168
+ 'http.status_code': 400
169
+ }
170
+ });
171
+ processor.onEnd(span);
172
+ const observationMessage = sentMessages.find(m => m.event === 'observations');
173
+ const observation = observationMessage.observations[0];
174
+ // When used with placeholder base, 'invalid-url' becomes '/invalid-url'
175
+ expect(observation.path).toBe('/invalid-url');
176
+ });
177
+ it('should include optional HTTP attributes when present', () => {
178
+ const span = createMockSpan({
179
+ kind: api_1.SpanKind.SERVER,
180
+ attributes: {
181
+ 'http.route': '/api/upload',
182
+ 'http.method': 'POST',
183
+ 'http.target': '/api/upload',
184
+ 'http.status_code': 200,
185
+ 'http.flavor': '1.1',
186
+ 'http.user_agent': 'Mozilla/5.0',
187
+ 'http.request_content_length': 1024,
188
+ 'http.response_content_length': 256
189
+ }
190
+ });
191
+ processor.onEnd(span);
192
+ const observationMessage = sentMessages.find(m => m.event === 'observations');
193
+ const observation = observationMessage.observations[0];
194
+ expect(observation).toMatchObject({
195
+ httpVersion: '1.1',
196
+ userAgent: 'Mozilla/5.0',
197
+ requestBytes: 1024,
198
+ responseBytes: 256
199
+ });
200
+ });
201
+ it('should not duplicate route registrations', () => {
202
+ const span1 = createMockSpan({
203
+ kind: api_1.SpanKind.SERVER,
204
+ attributes: {
205
+ 'http.route': '/api/users',
206
+ 'http.method': 'GET',
207
+ 'http.status_code': 200
208
+ }
209
+ });
210
+ const span2 = createMockSpan({
211
+ kind: api_1.SpanKind.SERVER,
212
+ attributes: {
213
+ 'http.route': '/api/users',
214
+ 'http.method': 'GET',
215
+ 'http.status_code': 200
216
+ }
217
+ });
218
+ processor.onEnd(span1);
219
+ processor.onEnd(span2);
220
+ const routeMessages = sentMessages.filter(m => m.type === 'http-route');
221
+ expect(routeMessages).toHaveLength(1);
222
+ });
223
+ });
224
+ describe('HTTP Client Processing', () => {
225
+ it('should process HTTP client spans and create service entities and interactions', () => {
226
+ const span = createMockSpan({
227
+ kind: api_1.SpanKind.CLIENT,
228
+ attributes: {
229
+ 'http.url': 'https://api.external.com/v1/data',
230
+ 'http.method': 'GET',
231
+ 'http.status_code': 200
232
+ }
233
+ });
234
+ processor.onEnd(span);
235
+ // Should have service + http-service + http-request + observation
236
+ expect(sentMessages).toHaveLength(4);
237
+ const httpServiceMessage = sentMessages.find(m => m.type === 'http-service');
238
+ expect(httpServiceMessage).toMatchObject({
239
+ event: 'new-entity',
240
+ type: 'http-service',
241
+ protocol: 'https',
242
+ host: 'api.external.com',
243
+ port: 443
244
+ });
245
+ const httpRequestMessage = sentMessages.find(m => m.type === 'http-request');
246
+ expect(httpRequestMessage).toMatchObject({
247
+ event: 'new-interaction',
248
+ type: 'http-request'
249
+ });
250
+ const observationMessage = sentMessages.find(m => m.event === 'observations');
251
+ const observation = observationMessage.observations[0];
252
+ expect(observation).toMatchObject({
253
+ type: 'http-client',
254
+ path: '/v1/data',
255
+ method: 'GET',
256
+ status: 200
257
+ });
258
+ });
259
+ it('should handle HTTP on port 80 and HTTPS on port 443 correctly', () => {
260
+ const httpSpan = createMockSpan({
261
+ kind: api_1.SpanKind.CLIENT,
262
+ attributes: {
263
+ 'http.url': 'http://api.example.com/test',
264
+ 'http.method': 'GET'
265
+ }
266
+ });
267
+ processor.onEnd(httpSpan);
268
+ const httpServiceMessage = sentMessages.find(m => m.type === 'http-service');
269
+ expect(httpServiceMessage).toMatchObject({
270
+ protocol: 'http',
271
+ host: 'api.example.com',
272
+ port: 80
273
+ });
274
+ });
275
+ it('should handle custom ports', () => {
276
+ const span = createMockSpan({
277
+ kind: api_1.SpanKind.CLIENT,
278
+ attributes: {
279
+ 'http.url': 'https://api.example.com:8443/test',
280
+ 'http.method': 'POST'
281
+ }
282
+ });
283
+ processor.onEnd(span);
284
+ const httpServiceMessage = sentMessages.find(m => m.type === 'http-service');
285
+ expect(httpServiceMessage).toMatchObject({
286
+ protocol: 'https',
287
+ host: 'api.example.com',
288
+ port: 8443
289
+ });
290
+ });
291
+ it('should not process client spans without http.method', () => {
292
+ const span = createMockSpan({
293
+ kind: api_1.SpanKind.CLIENT,
294
+ attributes: {
295
+ 'http.url': 'https://api.example.com/test'
296
+ // Missing http.method
297
+ }
298
+ });
299
+ processor.onEnd(span);
300
+ // Should still create service and interaction, but no observation
301
+ const observationMessages = sentMessages.filter(m => m.event === 'observations');
302
+ expect(observationMessages).toHaveLength(0);
303
+ });
304
+ });
305
+ describe('Database Processing', () => {
306
+ beforeEach(() => {
307
+ // Setup SQL analyzer mock
308
+ mockedAnalyzeSQL.mockReturnValue({
309
+ tableOperations: { users: ['SELECT'] },
310
+ normalizedQuery: 'SELECT * FROM users',
311
+ presentableQuery: 'SELECT * FROM users'
312
+ });
313
+ });
314
+ it('should process database spans and create database entities', () => {
315
+ const span = createMockSpan({
316
+ attributes: {
317
+ 'db.system': 'postgresql',
318
+ 'db.name': 'myapp',
319
+ 'net.peer.name': 'db.example.com',
320
+ 'net.peer.port': 5432
321
+ }
322
+ });
323
+ processor.onEnd(span);
324
+ const databaseMessage = sentMessages.find(m => m.type === 'database');
325
+ expect(databaseMessage).toMatchObject({
326
+ event: 'new-entity',
327
+ type: 'database',
328
+ system: 'postgresql',
329
+ name: 'myapp',
330
+ host: 'db.example.com',
331
+ port: 5432
332
+ });
333
+ });
334
+ it('should process database connection strings', () => {
335
+ const span = createMockSpan({
336
+ attributes: {
337
+ 'db.system': 'postgresql',
338
+ 'db.connection_string': 'postgresql://user:password@localhost:5432/testdb'
339
+ }
340
+ });
341
+ processor.onEnd(span);
342
+ const databaseMessage = sentMessages.find(m => m.type === 'database');
343
+ expect(databaseMessage).toMatchObject({
344
+ system: 'postgresql',
345
+ host: 'localhost',
346
+ port: 5432,
347
+ name: 'testdb'
348
+ });
349
+ const connectionMessage = sentMessages.find(m => m.type === 'db-connection');
350
+ expect(connectionMessage).toMatchObject({
351
+ event: 'new-interaction',
352
+ type: 'db-connection',
353
+ user: 'user'
354
+ });
355
+ });
356
+ it('should process SQL queries and create query interactions', () => {
357
+ const span = createMockSpan({
358
+ attributes: {
359
+ 'db.system': 'postgresql',
360
+ 'db.statement': 'SELECT * FROM users WHERE id = $1',
361
+ 'db.name': 'myapp',
362
+ 'net.peer.name': 'localhost',
363
+ 'db.response.returned_rows': 1
364
+ }
365
+ });
366
+ processor.onEnd(span);
367
+ expect(mockedAnalyzeSQL).toHaveBeenCalledWith('SELECT * FROM users WHERE id = $1');
368
+ const queryMessage = sentMessages.find(m => m.type === 'db-query');
369
+ expect(queryMessage).toMatchObject({
370
+ event: 'new-interaction',
371
+ type: 'db-query',
372
+ query: 'SELECT * FROM users',
373
+ selects: ['users']
374
+ });
375
+ const observationMessage = sentMessages.find(m => m.event === 'observations');
376
+ const observation = observationMessage.observations[0];
377
+ expect(observation).toMatchObject({
378
+ type: 'db-query',
379
+ returnedRows: 1
380
+ });
381
+ });
382
+ it('should handle multiple table operations in SQL', () => {
383
+ mockedAnalyzeSQL.mockReturnValue({
384
+ tableOperations: {
385
+ users: ['SELECT'],
386
+ orders: ['INSERT'],
387
+ products: ['UPDATE']
388
+ },
389
+ normalizedQuery: 'INSERT INTO orders SELECT * FROM users UPDATE products',
390
+ presentableQuery: 'INSERT INTO orders SELECT * FROM users UPDATE products'
391
+ });
392
+ const span = createMockSpan({
393
+ attributes: {
394
+ 'db.system': 'mysql',
395
+ 'db.statement': 'INSERT INTO orders SELECT * FROM users; UPDATE products SET stock = 0',
396
+ 'db.name': 'ecommerce'
397
+ }
398
+ });
399
+ processor.onEnd(span);
400
+ const queryMessage = sentMessages.find(m => m.type === 'db-query');
401
+ expect(queryMessage).toMatchObject({
402
+ selects: ['users'],
403
+ inserts: ['orders'],
404
+ updates: ['products']
405
+ });
406
+ });
407
+ it('should not process SQL for non-SQL database systems', () => {
408
+ const span = createMockSpan({
409
+ attributes: {
410
+ 'db.system': 'mongodb',
411
+ 'db.statement': 'db.users.find({_id: ObjectId("...")}))',
412
+ 'db.name': 'myapp'
413
+ }
414
+ });
415
+ processor.onEnd(span);
416
+ expect(mockedAnalyzeSQL).not.toHaveBeenCalled();
417
+ // Should still create database and connection, but no query interaction
418
+ const queryMessages = sentMessages.filter(m => m.type === 'db-query');
419
+ expect(queryMessages).toHaveLength(0);
420
+ });
421
+ it('should handle alternative database connection string formats', () => {
422
+ const span = createMockSpan({
423
+ attributes: {
424
+ 'db.system': 'mssql',
425
+ 'db.connection_string': 'Server=localhost;Database=TestDB;User Id=sa;Password=secret;'
426
+ }
427
+ });
428
+ processor.onEnd(span);
429
+ const databaseMessage = sentMessages.find(m => m.type === 'database');
430
+ expect(databaseMessage).toMatchObject({
431
+ system: 'mssql',
432
+ host: 'localhost',
433
+ name: 'TestDB'
434
+ });
435
+ const connectionMessage = sentMessages.find(m => m.type === 'db-connection');
436
+ expect(connectionMessage).toMatchObject({
437
+ user: 'sa'
438
+ });
439
+ });
440
+ });
441
+ describe('Error Handling', () => {
442
+ it('should extract error information from exception events', () => {
443
+ const span = createMockSpan({
444
+ kind: api_1.SpanKind.SERVER,
445
+ attributes: {
446
+ 'http.route': '/api/error',
447
+ 'http.method': 'GET',
448
+ 'http.status_code': 500
449
+ },
450
+ events: [{
451
+ name: 'exception',
452
+ attributes: {
453
+ 'exception.message': 'Database connection failed',
454
+ 'exception.type': 'ConnectionError',
455
+ 'exception.stacktrace': 'Error at line 42\n at method()'
456
+ }
457
+ }],
458
+ status: { code: api_1.SpanStatusCode.ERROR, message: 'Internal server error' }
459
+ });
460
+ processor.onEnd(span);
461
+ const observationMessage = sentMessages.find(m => m.event === 'observations');
462
+ const observation = observationMessage.observations[0];
463
+ expect(observation).toMatchObject({
464
+ errorMessage: 'Database connection failed',
465
+ errorType: 'ConnectionError',
466
+ stack: 'Error at line 42\n at method()'
467
+ });
468
+ });
469
+ it('should fallback to span attributes for error information', () => {
470
+ const span = createMockSpan({
471
+ kind: api_1.SpanKind.CLIENT,
472
+ attributes: {
473
+ 'http.url': 'https://api.example.com/fail',
474
+ 'http.method': 'POST',
475
+ 'http.status_code': 404,
476
+ 'exception.message': 'Not found',
477
+ 'exception.type': 'HttpError'
478
+ },
479
+ status: { code: api_1.SpanStatusCode.ERROR }
480
+ });
481
+ processor.onEnd(span);
482
+ const observationMessage = sentMessages.find(m => m.event === 'observations');
483
+ const observation = observationMessage.observations[0];
484
+ expect(observation).toMatchObject({
485
+ errorMessage: 'Not found',
486
+ errorType: 'HttpError'
487
+ });
488
+ });
489
+ it('should handle spans with error status but no explicit error attributes', () => {
490
+ const span = createMockSpan({
491
+ kind: api_1.SpanKind.SERVER,
492
+ attributes: {
493
+ 'http.route': '/api/timeout',
494
+ 'http.method': 'GET',
495
+ 'http.status_code': 500
496
+ },
497
+ status: { code: api_1.SpanStatusCode.ERROR, message: 'Request timeout' }
498
+ });
499
+ processor.onEnd(span);
500
+ const observationMessage = sentMessages.find(m => m.event === 'observations');
501
+ const observation = observationMessage.observations[0];
502
+ expect(observation.errorMessage).toBe('Request timeout');
503
+ });
504
+ });
505
+ describe('Sequence Numbers and Batching', () => {
506
+ it('should increment sequence numbers for observation messages', () => {
507
+ const span1 = createMockSpan({
508
+ kind: api_1.SpanKind.SERVER,
509
+ attributes: {
510
+ 'http.route': '/api/test1',
511
+ 'http.method': 'GET',
512
+ 'http.status_code': 200
513
+ }
514
+ });
515
+ const span2 = createMockSpan({
516
+ kind: api_1.SpanKind.SERVER,
517
+ attributes: {
518
+ 'http.route': '/api/test2',
519
+ 'http.method': 'GET',
520
+ 'http.status_code': 200
521
+ }
522
+ });
523
+ processor.onEnd(span1);
524
+ processor.onEnd(span2);
525
+ const observationMessages = sentMessages.filter(m => m.event === 'observations');
526
+ expect(observationMessages).toHaveLength(2);
527
+ expect(observationMessages[0].seq).toBe(1);
528
+ expect(observationMessages[1].seq).toBe(2);
529
+ });
530
+ });
531
+ describe('Processor Lifecycle', () => {
532
+ it('should construct with debug logging', () => {
533
+ const mockLogger = jest.fn();
534
+ const debugProcessor = new ComprehendDevSpanProcessor_1.ComprehendDevSpanProcessor({
535
+ organization: 'test-org',
536
+ token: 'test-token',
537
+ debug: mockLogger
538
+ });
539
+ expect(MockedWebSocketConnection).toHaveBeenCalledWith('test-org', 'test-token', mockLogger);
540
+ debugProcessor.shutdown();
541
+ });
542
+ it('should construct with debug enabled as boolean', () => {
543
+ const debugProcessor = new ComprehendDevSpanProcessor_1.ComprehendDevSpanProcessor({
544
+ organization: 'test-org',
545
+ token: 'test-token',
546
+ debug: true
547
+ });
548
+ expect(MockedWebSocketConnection).toHaveBeenCalledWith('test-org', 'test-token', console.log);
549
+ debugProcessor.shutdown();
550
+ });
551
+ it('should construct with debug disabled', () => {
552
+ const noDebugProcessor = new ComprehendDevSpanProcessor_1.ComprehendDevSpanProcessor({
553
+ organization: 'test-org',
554
+ token: 'test-token',
555
+ debug: false
556
+ });
557
+ expect(MockedWebSocketConnection).toHaveBeenCalledWith('test-org', 'test-token', undefined);
558
+ noDebugProcessor.shutdown();
559
+ });
560
+ it('should call connection.close() on shutdown', async () => {
561
+ await processor.shutdown();
562
+ expect(mockConnection.close).toHaveBeenCalled();
563
+ });
564
+ it('should handle forceFlush without error', async () => {
565
+ await expect(processor.forceFlush()).resolves.toBeUndefined();
566
+ });
567
+ it('onStart should be a no-op', () => {
568
+ expect(() => processor.onStart({}, {})).not.toThrow();
569
+ });
570
+ });
571
+ describe('Integration Tests', () => {
572
+ it('should handle a complete HTTP server request flow', () => {
573
+ const span = createMockSpan({
574
+ kind: api_1.SpanKind.SERVER,
575
+ attributes: {
576
+ 'http.route': '/api/users/{id}',
577
+ 'http.method': 'GET',
578
+ 'http.target': '/api/users/123?include=profile',
579
+ 'http.status_code': 200,
580
+ 'http.user_agent': 'test-client/1.0',
581
+ 'http.request_content_length': 0,
582
+ 'http.response_content_length': 456
583
+ },
584
+ resourceAttributes: {
585
+ 'service.name': 'user-api',
586
+ 'service.namespace': 'backend',
587
+ 'deployment.environment': 'staging'
588
+ },
589
+ startTime: [1700000000, 500000000],
590
+ duration: [0, 50000000] // 50ms
591
+ });
592
+ processor.onEnd(span);
593
+ // Verify all expected messages
594
+ expect(sentMessages).toHaveLength(3);
595
+ const serviceMessage = sentMessages[0];
596
+ expect(serviceMessage).toMatchObject({
597
+ event: 'new-entity',
598
+ type: 'service',
599
+ name: 'user-api',
600
+ namespace: 'backend',
601
+ environment: 'staging'
602
+ });
603
+ const routeMessage = sentMessages[1];
604
+ expect(routeMessage).toMatchObject({
605
+ event: 'new-entity',
606
+ type: 'http-route',
607
+ parent: serviceMessage.hash,
608
+ method: 'GET',
609
+ route: '/api/users/{id}'
610
+ });
611
+ const observationMessage = sentMessages[2];
612
+ expect(observationMessage.seq).toBe(1);
613
+ expect(observationMessage.observations).toHaveLength(1);
614
+ const observation = observationMessage.observations[0];
615
+ expect(observation).toMatchObject({
616
+ type: 'http-server',
617
+ subject: routeMessage.hash,
618
+ timestamp: [1700000000, 500000000],
619
+ path: '/api/users/123',
620
+ status: 200,
621
+ duration: [0, 50000000],
622
+ userAgent: 'test-client/1.0',
623
+ requestBytes: 0,
624
+ responseBytes: 456
625
+ });
626
+ });
627
+ it('should handle a complete database operation flow', () => {
628
+ mockedAnalyzeSQL.mockReturnValue({
629
+ tableOperations: { users: ['SELECT'], orders: ['INSERT'] },
630
+ normalizedQuery: 'INSERT INTO orders SELECT FROM users WHERE active = true',
631
+ presentableQuery: 'INSERT INTO orders SELECT FROM users WHERE active = true'
632
+ });
633
+ const span = createMockSpan({
634
+ attributes: {
635
+ 'db.system': 'postgresql',
636
+ 'db.connection_string': 'postgresql://app_user:secret@db.prod.com:5432/ecommerce',
637
+ 'db.statement': 'INSERT INTO orders SELECT FROM users WHERE active = true',
638
+ 'db.response.returned_rows': 5
639
+ },
640
+ startTime: [1700000000, 0],
641
+ duration: [0, 25000000] // 25ms
642
+ });
643
+ processor.onEnd(span);
644
+ // Verify all expected messages: service, database, connection, query, observation
645
+ expect(sentMessages).toHaveLength(5);
646
+ const databaseMessage = sentMessages.find(m => m.type === 'database');
647
+ expect(databaseMessage).toMatchObject({
648
+ system: 'postgresql',
649
+ host: 'db.prod.com',
650
+ port: 5432,
651
+ name: 'ecommerce'
652
+ });
653
+ const connectionMessage = sentMessages.find(m => m.type === 'db-connection');
654
+ expect(connectionMessage).toMatchObject({
655
+ user: 'app_user'
656
+ });
657
+ const queryMessage = sentMessages.find(m => m.type === 'db-query');
658
+ expect(queryMessage).toMatchObject({
659
+ query: 'INSERT INTO orders SELECT FROM users WHERE active = true',
660
+ selects: ['users'],
661
+ inserts: ['orders']
662
+ });
663
+ const observationMessage = sentMessages.find(m => m.event === 'observations');
664
+ const observation = observationMessage.observations[0];
665
+ expect(observation).toMatchObject({
666
+ type: 'db-query',
667
+ subject: queryMessage.hash,
668
+ timestamp: [1700000000, 0],
669
+ duration: [0, 25000000],
670
+ returnedRows: 5
671
+ });
672
+ });
673
+ });
674
+ });
@@ -0,0 +1 @@
1
+ export {};