@comprehend/telemetry-node 0.1.3 → 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.
Files changed (42) hide show
  1. package/.claude/settings.local.json +2 -1
  2. package/.idea/telemetry-node.iml +0 -1
  3. package/DEVELOPMENT.md +69 -0
  4. package/README.md +173 -0
  5. package/dist/ComprehendDevSpanProcessor.d.ts +9 -6
  6. package/dist/ComprehendDevSpanProcessor.js +146 -87
  7. package/dist/ComprehendDevSpanProcessor.test.d.ts +1 -0
  8. package/dist/ComprehendDevSpanProcessor.test.js +495 -0
  9. package/dist/ComprehendMetricsExporter.d.ts +18 -0
  10. package/dist/ComprehendMetricsExporter.js +178 -0
  11. package/dist/ComprehendMetricsExporter.test.d.ts +1 -0
  12. package/dist/ComprehendMetricsExporter.test.js +266 -0
  13. package/dist/ComprehendSDK.d.ts +18 -0
  14. package/dist/ComprehendSDK.js +56 -0
  15. package/dist/ComprehendSDK.test.d.ts +1 -0
  16. package/dist/ComprehendSDK.test.js +126 -0
  17. package/dist/WebSocketConnection.d.ts +23 -3
  18. package/dist/WebSocketConnection.js +106 -12
  19. package/dist/WebSocketConnection.test.d.ts +1 -0
  20. package/dist/WebSocketConnection.test.js +473 -0
  21. package/dist/index.d.ts +3 -1
  22. package/dist/index.js +5 -1
  23. package/dist/sql-analyzer.js +2 -11
  24. package/dist/sql-analyzer.test.js +0 -12
  25. package/dist/util.d.ts +2 -0
  26. package/dist/util.js +7 -0
  27. package/dist/wire-protocol.d.ts +168 -28
  28. package/jest.config.js +1 -0
  29. package/package.json +4 -2
  30. package/src/ComprehendDevSpanProcessor.test.ts +626 -0
  31. package/src/ComprehendDevSpanProcessor.ts +170 -105
  32. package/src/ComprehendMetricsExporter.test.ts +334 -0
  33. package/src/ComprehendMetricsExporter.ts +225 -0
  34. package/src/ComprehendSDK.test.ts +160 -0
  35. package/src/ComprehendSDK.ts +63 -0
  36. package/src/WebSocketConnection.test.ts +616 -0
  37. package/src/WebSocketConnection.ts +135 -13
  38. package/src/index.ts +3 -2
  39. package/src/util.ts +6 -0
  40. package/src/wire-protocol.ts +204 -29
  41. package/src/sql-analyzer.test.ts +0 -599
  42. package/src/sql-analyzer.ts +0 -439
@@ -1,19 +1,25 @@
1
1
  import {sha256} from "@noble/hashes/sha2";
2
2
  import {utf8ToBytes} from "@noble/hashes/utils";
3
- import {Context} from "@opentelemetry/api";
3
+ import {Context, SpanKind} from "@opentelemetry/api";
4
4
  import {ReadableSpan, Span, SpanProcessor} from "@opentelemetry/sdk-trace-base";
5
5
  import {
6
- DatabaseQueryObservation,
6
+ AttributeType,
7
+ CustomObservation,
8
+ CustomSpanObservationSpecification,
9
+ DatabaseQueryMessage,
7
10
  HttpClientObservation,
8
- HttpServerObservation, NewObservedDatabaseConnectionMessage,
9
- NewObservedDatabaseMessage, NewObservedDatabaseQueryMessage,
11
+ HttpServerObservation,
12
+ NewObservedDatabaseConnectionMessage,
13
+ NewObservedDatabaseMessage,
10
14
  NewObservedHttpRequestMessage,
11
15
  NewObservedHttpRouteMessage,
12
16
  NewObservedHttpServiceMessage,
13
17
  NewObservedServiceMessage,
14
- ObservationInputMessage
18
+ ObservationInputMessage,
19
+ SpanMatcherRule,
20
+ TraceSpansMessage,
21
+ CustomMetricSpecification,
15
22
  } from "./wire-protocol";
16
- import {analyzeSQL} from "./sql-analyzer";
17
23
  import {WebSocketConnection} from "./WebSocketConnection";
18
24
 
19
25
  interface ObservedService {
@@ -32,7 +38,7 @@ interface ObservedHTTPRoute {
32
38
 
33
39
  interface ObservedDatabase {
34
40
  system: string;
35
- connection?: string; // Connection string, hopefully scrubbed of user info
41
+ connection?: string;
36
42
  host?: string;
37
43
  port?: number;
38
44
  name?: string;
@@ -56,11 +62,6 @@ interface ObservedDatabaseConnection {
56
62
  user?: string;
57
63
  }
58
64
 
59
- interface ObservedDatabaseQuery {
60
- hash: string;
61
- query: string;
62
- }
63
-
64
65
  const sqlDbSystems = new Set([
65
66
  'mysql', 'postgresql', 'mssql', 'oracle', 'db2', 'sqlite', 'hsqldb', 'h2',
66
67
  'informix', 'cockroachdb', 'redshift', 'tidb', 'trino', 'greenplum'
@@ -73,14 +74,19 @@ export class ComprehendDevSpanProcessor implements SpanProcessor {
73
74
  private observedHttpServices: ObservedHttpService[] = [];
74
75
  private observedInteractions: Map<string, Map<string, {
75
76
  dbConnections: ObservedDatabaseConnection[],
76
- dbQueries: ObservedDatabaseQuery[],
77
77
  httpRequest?: ObservedHttpRequest
78
78
  }>> = new Map();
79
- private observationsSeq = 1;
79
+ private processContextSet = false;
80
+ private spanObservationSpecs: CustomSpanObservationSpecification[] = [];
80
81
 
81
- constructor(options: { organization: string, token: string, debug?: boolean | ((message: string) => void) }) {
82
- this.connection = new WebSocketConnection(options.organization, options.token,
83
- options.debug === true ? console.log : options.debug === false ? undefined : options.debug);
82
+ constructor(connection: WebSocketConnection) {
83
+ this.connection = connection;
84
+ }
85
+
86
+ updateCustomMetrics(specs: CustomMetricSpecification[]): void {
87
+ this.spanObservationSpecs = specs.filter(
88
+ (s): s is CustomSpanObservationSpecification => s.type === 'span'
89
+ );
84
90
  }
85
91
 
86
92
  onStart(span: Span, parentContext: Context): void {
@@ -92,14 +98,12 @@ export class ComprehendDevSpanProcessor implements SpanProcessor {
92
98
  return;
93
99
 
94
100
  const attrs = span.attributes;
95
- if (span.kind === 1) {
96
- // Server span, see if it's something to ingest.
101
+ if (span.kind === 1) { // Server span
97
102
  if (attrs['http.route'] && attrs['http.method']) {
98
103
  this.processHTTPRoute(currentService, attrs['http.route'] as string, attrs['http.method'] as string, span);
99
104
  }
100
105
  }
101
- else if (span.kind === 2) {
102
- // Client span, see if it's something to ingest.
106
+ else if (span.kind === 2) { // Client span
103
107
  if (attrs['http.url']) {
104
108
  this.processHttpRequest(currentService, attrs['http.url'] as string, span);
105
109
  }
@@ -107,10 +111,15 @@ export class ComprehendDevSpanProcessor implements SpanProcessor {
107
111
  if (attrs['db.system']) {
108
112
  this.processDatabaseOperation(currentService, span);
109
113
  }
114
+
115
+ // Report trace span for every span
116
+ this.reportTraceSpan(span);
117
+
118
+ // Check custom span observation matching
119
+ this.checkCustomSpanMatching(span);
110
120
  }
111
121
 
112
122
  private discoverService(span: ReadableSpan): ObservedService | undefined {
113
- // Look for an existing matching entry.
114
123
  const resAttrs = span.resource.attributes;
115
124
  const name = resAttrs['service.name'] as string | undefined;
116
125
  if (!name)
@@ -125,7 +134,6 @@ export class ComprehendDevSpanProcessor implements SpanProcessor {
125
134
  if (existing)
126
135
  return existing;
127
136
 
128
- // New; hash it and add it to the observed services.
129
137
  const idString = `service:${name}:${namespace ?? ''}:${environment ?? ''}`;
130
138
  const hash = hashIdString(idString);
131
139
  const newService: ObservedService = {
@@ -134,7 +142,6 @@ export class ComprehendDevSpanProcessor implements SpanProcessor {
134
142
  };
135
143
  this.observedServices.push(newService);
136
144
 
137
- // Ingest its existence.
138
145
  const message: NewObservedServiceMessage = {
139
146
  event: "new-entity",
140
147
  type: "service",
@@ -144,21 +151,27 @@ export class ComprehendDevSpanProcessor implements SpanProcessor {
144
151
  ...(environment ? { environment } : {})
145
152
  };
146
153
  this.ingestMessage(message);
154
+
155
+ // Set process context on first service discovery
156
+ if (!this.processContextSet) {
157
+ this.processContextSet = true;
158
+ const resources = flattenResourceAttributes(resAttrs);
159
+ this.connection.setProcessContext(hash, resources);
160
+ }
161
+
162
+ return newService;
147
163
  }
148
164
 
149
165
  private processHTTPRoute(service: ObservedService, route: string, method: string, span: ReadableSpan): void {
150
- // Check if this route+method already exists under the service
151
166
  let observedRoute = service.httpRoutes.find(r =>
152
167
  r.route === route && r.method === method
153
168
  );
154
169
  if (!observedRoute) {
155
- // It's new; hash it and add it to our collection.
156
170
  const idString = `http-route:${service.hash}:${method}:${route}`;
157
171
  const hash = hashIdString(idString);
158
172
  observedRoute = { method, route, hash };
159
173
  service.httpRoutes.push(observedRoute);
160
174
 
161
- // Emit observation message
162
175
  const message: NewObservedHttpRouteMessage = {
163
176
  event: "new-entity",
164
177
  type: "http-route",
@@ -169,14 +182,13 @@ export class ComprehendDevSpanProcessor implements SpanProcessor {
169
182
  this.ingestMessage(message);
170
183
  }
171
184
 
172
- // Extract the request path, making sure we only get that, without any query string.
185
+ // Extract the request path, making sure we strip any query string.
173
186
  const attrs = span.attributes;
174
187
  let path: string;
175
188
  if (attrs['http.target']) {
176
189
  try {
177
- // This might be just a path like "/search?q=foo"
178
190
  const rawTarget = attrs['http.target'] as string;
179
- const fakeUrl = new URL(rawTarget, 'http://placeholder'); // placeholder base
191
+ const fakeUrl = new URL(rawTarget, 'http://placeholder');
180
192
  path = fakeUrl.pathname;
181
193
  } catch {
182
194
  path = '/';
@@ -195,7 +207,6 @@ export class ComprehendDevSpanProcessor implements SpanProcessor {
195
207
  path = '/';
196
208
  }
197
209
 
198
- // Build and ingest observation.
199
210
  const status = attrs['http.status_code'] as number | undefined ?? 0;
200
211
  const duration = span.duration;
201
212
  const httpVersion = attrs['http.flavor'] as string | undefined;
@@ -203,9 +214,12 @@ export class ComprehendDevSpanProcessor implements SpanProcessor {
203
214
  const requestBytes = attrs['http.request_content_length'] as number | undefined;
204
215
  const responseBytes = attrs['http.response_content_length'] as number | undefined;
205
216
  const { message: errorMessage, type: errorType, stack: stack } = extractErrorInfo(span);
217
+ const { traceId, spanId } = getSpanContext(span);
206
218
  const observation: HttpServerObservation = {
207
219
  type: 'http-server',
208
220
  subject: observedRoute.hash,
221
+ spanId,
222
+ traceId,
209
223
  timestamp: span.startTime,
210
224
  path,
211
225
  status,
@@ -220,13 +234,12 @@ export class ComprehendDevSpanProcessor implements SpanProcessor {
220
234
  };
221
235
  this.ingestMessage({
222
236
  event: "observations",
223
- seq: this.observationsSeq++,
237
+ seq: this.connection.nextSeq(),
224
238
  observations: [observation]
225
239
  });
226
240
  }
227
241
 
228
242
  private processDatabaseOperation(currentService: ObservedService, span: ReadableSpan): void {
229
- // Parse the connection string.
230
243
  const attrs = span.attributes;
231
244
  const system = attrs['db.system'] as string;
232
245
  const rawConnection = attrs['db.connection_string'] as string | undefined;
@@ -240,11 +253,9 @@ export class ComprehendDevSpanProcessor implements SpanProcessor {
240
253
  name: attrs['db.name'] as string | undefined,
241
254
  };
242
255
 
243
- // See if we already have an entry for this database.
244
256
  const hash = hashIdString(`database:${system}:${parsed.host ?? ''}:${parsed.port ?? ''}:${parsed.name ?? ''}`);
245
257
  let observedDatabase = this.observedDatabases.find(db => db.hash === hash);
246
258
 
247
- // If we see it for the first time, add and ingest it.
248
259
  if (!observedDatabase) {
249
260
  observedDatabase = {
250
261
  system,
@@ -255,7 +266,6 @@ export class ComprehendDevSpanProcessor implements SpanProcessor {
255
266
  };
256
267
  this.observedDatabases.push(observedDatabase);
257
268
 
258
- // The existence of the database.
259
269
  const message: NewObservedDatabaseMessage = {
260
270
  event: "new-entity",
261
271
  type: "database",
@@ -270,7 +280,6 @@ export class ComprehendDevSpanProcessor implements SpanProcessor {
270
280
  this.ingestMessage(message);
271
281
  }
272
282
 
273
- // The connection to the database should have an interaction.
274
283
  const interactions = this.getInteractions(currentService.hash, observedDatabase.hash);
275
284
  let connectionInteraction = interactions.dbConnections.find(c => c.connection === parsed.scrubbed && c.user === parsed.user);
276
285
  if (!connectionInteraction) {
@@ -292,69 +301,40 @@ export class ComprehendDevSpanProcessor implements SpanProcessor {
292
301
  this.ingestMessage(message);
293
302
  }
294
303
 
295
- // The query of the database from the service (only for SQL for now).
304
+ // For SQL databases, send raw query to server for analysis
296
305
  if (sqlDbSystems.has(system) && attrs['db.statement']) {
297
- // The interaction, based upon the normalized query.
298
- let queryInfo = analyzeSQL(attrs['db.statement'] as string);
299
- let queryInteraction = interactions.dbQueries.find(q => q.query === queryInfo.normalizedQuery);
300
- if (!queryInteraction) {
301
- queryInteraction = {
302
- hash: hashIdString(`db-query:${currentService.hash}:${observedDatabase.hash}:${queryInfo.normalizedQuery}`),
303
- query: queryInfo.normalizedQuery
304
- };
305
- interactions.dbQueries.push(queryInteraction);
306
- const selects = getTablesWithOperation(queryInfo.tableOperations, 'SELECT');
307
- const inserts = getTablesWithOperation(queryInfo.tableOperations, 'INSERT');
308
- const updates = getTablesWithOperation(queryInfo.tableOperations, 'UPDATE');
309
- const deletes = getTablesWithOperation(queryInfo.tableOperations, 'DELETE');
310
- const message: NewObservedDatabaseQueryMessage = {
311
- event: "new-interaction",
312
- type: "db-query",
313
- hash: queryInteraction.hash,
314
- from: currentService.hash,
315
- to: observedDatabase.hash,
316
- query: queryInfo.presentableQuery,
317
- ...(selects ? { selects } : {}),
318
- ...(inserts ? { inserts } : {}),
319
- ...(updates ? { updates } : {}),
320
- ...(deletes ? { deletes } : {}),
321
- };
322
- this.ingestMessage(message);
323
- }
324
-
325
- // Build and ingest observation.
326
306
  const duration = span.duration;
327
307
  const returnedRows = (attrs['db.response.returned_rows'] as number | undefined)
328
308
  ?? (attrs['db.sql.rows'] as number | undefined);
329
309
  const { message: errorMessage, type: errorType, stack } = extractErrorInfo(span);
330
- const observation: DatabaseQueryObservation = {
331
- type: "db-query",
332
- subject: queryInteraction.hash,
310
+ const { traceId, spanId } = span.spanContext();
311
+ const dbQueryMessage: DatabaseQueryMessage = {
312
+ event: "db-query",
313
+ seq: this.connection.nextSeq(),
314
+ query: attrs['db.statement'] as string,
315
+ from: currentService.hash,
316
+ to: observedDatabase.hash,
333
317
  timestamp: span.startTime,
334
318
  duration,
319
+ traceId,
320
+ spanId,
335
321
  ...(errorMessage ? { errorMessage } : {}),
336
- ...(errorType ? { errorType } : {}),
322
+ ...(errorType ? { errorType } : {}),
337
323
  ...(stack ? { stack } : {}),
338
324
  ...(returnedRows !== undefined ? { returnedRows } : {})
339
325
  };
340
- this.ingestMessage({
341
- event: "observations",
342
- seq: this.observationsSeq++,
343
- observations: [observation]
344
- });
326
+ this.ingestMessage(dbQueryMessage);
345
327
  }
346
328
  }
347
329
 
348
330
  private processHttpRequest(currentService: ObservedService, url: string, span: ReadableSpan): void {
349
- // Build identity based upon protocol, host, and port.
350
331
  const parsed = new URL(url);
351
- const protocol = parsed.protocol.replace(':', ''); // Remove trailing colon
332
+ const protocol = parsed.protocol.replace(':', '');
352
333
  const host = parsed.hostname;
353
334
  const port = parsed.port ? parseInt(parsed.port) : (protocol === 'https' ? 443 : 80);
354
335
  const idString = `http-service:${protocol}:${host}:${port}`;
355
336
  const hash = hashIdString(idString);
356
337
 
357
- // Ingest it if it's not already observed.
358
338
  let observedHttpService = this.observedHttpServices.find(s =>
359
339
  s.protocol === protocol && s.host === host && s.port === port
360
340
  );
@@ -362,7 +342,6 @@ export class ComprehendDevSpanProcessor implements SpanProcessor {
362
342
  observedHttpService = { protocol, host, port, hash };
363
343
  this.observedHttpServices.push(observedHttpService);
364
344
 
365
- // The existence of the service.
366
345
  const message: NewObservedHttpServiceMessage = {
367
346
  event: "new-entity",
368
347
  type: "http-service",
@@ -374,7 +353,6 @@ export class ComprehendDevSpanProcessor implements SpanProcessor {
374
353
  this.ingestMessage(message);
375
354
  }
376
355
 
377
- // Ingest the interaction if first observed.
378
356
  const interactions = this.getInteractions(currentService.hash, observedHttpService.hash);
379
357
  if (!interactions.httpRequest) {
380
358
  const idString = `http-request:${currentService.hash}:${observedHttpService.hash}`;
@@ -390,11 +368,10 @@ export class ComprehendDevSpanProcessor implements SpanProcessor {
390
368
  this.ingestMessage(message);
391
369
  }
392
370
 
393
- // Build and ingest observation.
394
371
  const attrs = span.attributes;
395
372
  const path = parsed.pathname || '/';
396
373
  const method = span.attributes['http.method'] as string;
397
- if (!method) // Really should always be there
374
+ if (!method)
398
375
  return;
399
376
  const status = attrs['http.status_code'] as number | undefined;
400
377
  const duration = span.duration;
@@ -402,9 +379,12 @@ export class ComprehendDevSpanProcessor implements SpanProcessor {
402
379
  const requestBytes = attrs['http.request_content_length'] as number | undefined;
403
380
  const responseBytes = attrs['http.response_content_length'] as number | undefined;
404
381
  const { message: errorMessage, type: errorType, stack } = extractErrorInfo(span);
382
+ const { traceId, spanId } = getSpanContext(span);
405
383
  const observation: HttpClientObservation = {
406
384
  type: "http-client",
407
385
  subject: interactions.httpRequest.hash,
386
+ spanId,
387
+ traceId,
408
388
  timestamp: span.startTime,
409
389
  path,
410
390
  method,
@@ -414,16 +394,65 @@ export class ComprehendDevSpanProcessor implements SpanProcessor {
414
394
  ...(requestBytes !== undefined ? { requestBytes } : {}),
415
395
  ...(responseBytes !== undefined ? { responseBytes } : {}),
416
396
  ...(errorMessage ? { errorMessage } : {}),
417
- ...(errorType ? { errorType } : {}),
397
+ ...(errorType ? { errorType } : {}),
418
398
  ...(stack ? { stack } : {})
419
399
  };
420
400
  this.ingestMessage({
421
401
  event: "observations",
422
- seq: this.observationsSeq++,
402
+ seq: this.connection.nextSeq(),
423
403
  observations: [observation]
424
404
  });
425
405
  }
426
406
 
407
+ private reportTraceSpan(span: ReadableSpan): void {
408
+ const { traceId, spanId } = getSpanContext(span);
409
+ const parent = getParentSpanId(span);
410
+ const traceSpanMessage: TraceSpansMessage = {
411
+ event: "tracespans",
412
+ seq: this.connection.nextSeq(),
413
+ data: [{
414
+ trace: traceId,
415
+ span: spanId,
416
+ parent,
417
+ name: span.name,
418
+ timestamp: span.startTime,
419
+ }]
420
+ };
421
+ this.ingestMessage(traceSpanMessage);
422
+ }
423
+
424
+ private checkCustomSpanMatching(span: ReadableSpan): void {
425
+ for (const spec of this.spanObservationSpecs) {
426
+ if (matchSpanRule(span, spec.rule)) {
427
+ const { traceId, spanId } = getSpanContext(span);
428
+ const { message: errorMessage, type: errorType, stack } = extractErrorInfo(span);
429
+ const collectedAttrs: Record<string, AttributeType> = {};
430
+ for (const [key, value] of Object.entries(span.attributes)) {
431
+ if (value !== undefined) {
432
+ collectedAttrs[key] = value as AttributeType;
433
+ }
434
+ }
435
+ const observation: CustomObservation = {
436
+ type: "custom",
437
+ subject: spec.subject,
438
+ id: spec.subject,
439
+ spanId,
440
+ traceId,
441
+ timestamp: span.startTime,
442
+ attributes: collectedAttrs,
443
+ ...(errorMessage ? { errorMessage } : {}),
444
+ ...(errorType ? { errorType } : {}),
445
+ ...(stack ? { stack } : {}),
446
+ };
447
+ this.ingestMessage({
448
+ event: "observations",
449
+ seq: this.connection.nextSeq(),
450
+ observations: [observation]
451
+ });
452
+ }
453
+ }
454
+ }
455
+
427
456
  private getInteractions(from: string, to: string) {
428
457
  let fromMap = this.observedInteractions.get(from);
429
458
  if (!fromMap) {
@@ -432,7 +461,7 @@ export class ComprehendDevSpanProcessor implements SpanProcessor {
432
461
  }
433
462
  let interactions = fromMap.get(to);
434
463
  if (!interactions) {
435
- interactions = { httpRequest: undefined, dbConnections: [], dbQueries: [] };
464
+ interactions = { httpRequest: undefined, dbConnections: [] };
436
465
  fromMap.set(to, interactions);
437
466
  }
438
467
  return interactions;
@@ -446,7 +475,6 @@ export class ComprehendDevSpanProcessor implements SpanProcessor {
446
475
  }
447
476
 
448
477
  async shutdown(): Promise<void> {
449
- this.connection.close()
450
478
  }
451
479
  }
452
480
 
@@ -456,8 +484,36 @@ function hashIdString(idString: string) {
456
484
  .join('');
457
485
  }
458
486
 
459
- /** Try to extract data from the database connection string. Many are URL based, fall back on
460
- * some kind of comma-seperated style for the rest. */
487
+ function getSpanContext(span: ReadableSpan): { traceId: string, spanId: string } {
488
+ const ctx = span.spanContext?.();
489
+ return {
490
+ traceId: ctx?.traceId ?? '',
491
+ spanId: ctx?.spanId ?? '',
492
+ };
493
+ }
494
+
495
+ function getParentSpanId(span: ReadableSpan): string {
496
+ return span.parentSpanContext?.spanId ?? '';
497
+ }
498
+
499
+ function flattenResourceAttributes(attrs: Record<string, any>): Record<string, AttributeType> {
500
+ const result: Record<string, AttributeType> = {};
501
+ function flatten(obj: any, prefix: string) {
502
+ for (const [key, value] of Object.entries(obj)) {
503
+ const fullKey = prefix ? `${prefix}.${key}` : key;
504
+ if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
505
+ flatten(value, fullKey);
506
+ } else {
507
+ result[fullKey] = String(value);
508
+ }
509
+ }
510
+ }
511
+ flatten(attrs, '');
512
+ return result;
513
+ }
514
+
515
+ /** Try to extract data from the database connection string. Many are URL-based; falls back to
516
+ * semicolon-separated key=value parsing for the rest (e.g. MSSQL style). */
461
517
  function parseDatabaseConnectionStringRaw(conn: string): {
462
518
  scrubbed: string;
463
519
  user?: string;
@@ -466,7 +522,6 @@ function parseDatabaseConnectionStringRaw(conn: string): {
466
522
  name?: string;
467
523
  } {
468
524
  try {
469
- // Try URL-style parsing first; scrub the user details if present.
470
525
  const url = new URL(conn);
471
526
  const user = url.username || undefined;
472
527
  const host = url.hostname || undefined;
@@ -482,7 +537,6 @@ function parseDatabaseConnectionStringRaw(conn: string): {
482
537
  name: dbName
483
538
  };
484
539
  } catch {
485
- // Not a URL-style string; try semi-structured parsing
486
540
  const parts = conn.split(';');
487
541
  const kv: Record<string, string> = {};
488
542
 
@@ -496,7 +550,6 @@ function parseDatabaseConnectionStringRaw(conn: string): {
496
550
  const port = kv['port'];
497
551
  const name = kv['database'] || kv['initial catalog'];
498
552
 
499
- // Reconstruct a scrubbed connection string without credentials
500
553
  const scrubbed = Object.entries(kv)
501
554
  .filter(([key]) => !['user id', 'uid', 'password', 'pwd'].includes(key))
502
555
  .map(([k, v]) => `${k}=${v}`)
@@ -512,8 +565,8 @@ function parseDatabaseConnectionStringRaw(conn: string): {
512
565
  }
513
566
  }
514
567
 
515
- /** Try pretty hard to get error info out of a span, if it has any. Handles it being there as an event,
516
- * directly on the span with error semantics, and some other more ad-hoc cases. */
568
+ /** Try pretty hard to get error info out of a span, if it has any. Handles it being there as an
569
+ * event, directly on the span with error semantics, and some other more ad-hoc cases. */
517
570
  function extractErrorInfo(span: ReadableSpan): {
518
571
  message?: string;
519
572
  type?: string;
@@ -521,7 +574,6 @@ function extractErrorInfo(span: ReadableSpan): {
521
574
  } {
522
575
  const attrs = span.attributes;
523
576
 
524
- // Try to extract from a structured 'exception' event, as it should have more detail
525
577
  const exceptionEvent = span.events.find(e => e.name === 'exception');
526
578
  if (exceptionEvent?.attributes) {
527
579
  const message = exceptionEvent.attributes['exception.message'] as string | undefined;
@@ -532,7 +584,6 @@ function extractErrorInfo(span: ReadableSpan): {
532
584
  }
533
585
  }
534
586
 
535
- // Fallback to attributes directly on the span.
536
587
  const isError = span.status.code === 2;
537
588
  const message =
538
589
  (attrs['exception.message'] as string | undefined) ??
@@ -552,13 +603,27 @@ function extractErrorInfo(span: ReadableSpan): {
552
603
  };
553
604
  }
554
605
 
555
- function getTablesWithOperation(
556
- tableOps: Record<string, string[]>,
557
- operation: string
558
- ): string[] | undefined {
559
- const op = operation.toUpperCase();
560
- const result = Object.entries(tableOps)
561
- .filter(([_, ops]) => ops.includes(op))
562
- .map(([table]) => table);
563
- return result.length > 0 ? result : undefined;
606
+ export function matchSpanRule(span: ReadableSpan, rule: SpanMatcherRule): boolean {
607
+ switch (rule.kind) {
608
+ case 'type': {
609
+ const kindMap: Record<string, SpanKind> = {
610
+ 'client': SpanKind.CLIENT,
611
+ 'server': SpanKind.SERVER,
612
+ 'internal': SpanKind.INTERNAL,
613
+ };
614
+ return span.kind === kindMap[rule.value];
615
+ }
616
+ case 'attribute-present':
617
+ return span.attributes[rule.key] !== undefined;
618
+ case 'attribute-absent':
619
+ return span.attributes[rule.key] === undefined;
620
+ case 'attribute-equals':
621
+ return String(span.attributes[rule.key]) === rule.value;
622
+ case 'attribute-not-equals':
623
+ return String(span.attributes[rule.key]) !== rule.value;
624
+ case 'all':
625
+ return rule.rules.every(r => matchSpanRule(span, r));
626
+ case 'any':
627
+ return rule.rules.some(r => matchSpanRule(span, r));
628
+ }
564
629
  }