@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.
Files changed (38) hide show
  1. package/.claude/settings.local.json +2 -2
  2. package/.idea/telemetry-node.iml +0 -1
  3. package/README.md +73 -27
  4. package/dist/ComprehendDevSpanProcessor.d.ts +9 -6
  5. package/dist/ComprehendDevSpanProcessor.js +145 -87
  6. package/dist/ComprehendDevSpanProcessor.test.js +270 -449
  7. package/dist/ComprehendMetricsExporter.d.ts +18 -0
  8. package/dist/ComprehendMetricsExporter.js +178 -0
  9. package/dist/ComprehendMetricsExporter.test.d.ts +1 -0
  10. package/dist/ComprehendMetricsExporter.test.js +266 -0
  11. package/dist/ComprehendSDK.d.ts +18 -0
  12. package/dist/ComprehendSDK.js +56 -0
  13. package/dist/ComprehendSDK.test.d.ts +1 -0
  14. package/dist/ComprehendSDK.test.js +126 -0
  15. package/dist/WebSocketConnection.d.ts +23 -3
  16. package/dist/WebSocketConnection.js +106 -12
  17. package/dist/WebSocketConnection.test.js +236 -169
  18. package/dist/index.d.ts +3 -1
  19. package/dist/index.js +5 -1
  20. package/dist/sql-analyzer.js +2 -11
  21. package/dist/sql-analyzer.test.js +0 -12
  22. package/dist/util.d.ts +2 -0
  23. package/dist/util.js +7 -0
  24. package/dist/wire-protocol.d.ts +168 -28
  25. package/package.json +3 -1
  26. package/src/ComprehendDevSpanProcessor.test.ts +311 -507
  27. package/src/ComprehendDevSpanProcessor.ts +169 -105
  28. package/src/ComprehendMetricsExporter.test.ts +334 -0
  29. package/src/ComprehendMetricsExporter.ts +225 -0
  30. package/src/ComprehendSDK.test.ts +160 -0
  31. package/src/ComprehendSDK.ts +63 -0
  32. package/src/WebSocketConnection.test.ts +286 -205
  33. package/src/WebSocketConnection.ts +135 -13
  34. package/src/index.ts +3 -2
  35. package/src/util.ts +6 -0
  36. package/src/wire-protocol.ts +204 -29
  37. package/src/sql-analyzer.test.ts +0 -599
  38. 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,22 +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
+
147
162
  return newService;
148
163
  }
149
164
 
150
165
  private processHTTPRoute(service: ObservedService, route: string, method: string, span: ReadableSpan): void {
151
- // Check if this route+method already exists under the service
152
166
  let observedRoute = service.httpRoutes.find(r =>
153
167
  r.route === route && r.method === method
154
168
  );
155
169
  if (!observedRoute) {
156
- // It's new; hash it and add it to our collection.
157
170
  const idString = `http-route:${service.hash}:${method}:${route}`;
158
171
  const hash = hashIdString(idString);
159
172
  observedRoute = { method, route, hash };
160
173
  service.httpRoutes.push(observedRoute);
161
174
 
162
- // Emit observation message
163
175
  const message: NewObservedHttpRouteMessage = {
164
176
  event: "new-entity",
165
177
  type: "http-route",
@@ -170,14 +182,13 @@ export class ComprehendDevSpanProcessor implements SpanProcessor {
170
182
  this.ingestMessage(message);
171
183
  }
172
184
 
173
- // 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.
174
186
  const attrs = span.attributes;
175
187
  let path: string;
176
188
  if (attrs['http.target']) {
177
189
  try {
178
- // This might be just a path like "/search?q=foo"
179
190
  const rawTarget = attrs['http.target'] as string;
180
- const fakeUrl = new URL(rawTarget, 'http://placeholder'); // placeholder base
191
+ const fakeUrl = new URL(rawTarget, 'http://placeholder');
181
192
  path = fakeUrl.pathname;
182
193
  } catch {
183
194
  path = '/';
@@ -196,7 +207,6 @@ export class ComprehendDevSpanProcessor implements SpanProcessor {
196
207
  path = '/';
197
208
  }
198
209
 
199
- // Build and ingest observation.
200
210
  const status = attrs['http.status_code'] as number | undefined ?? 0;
201
211
  const duration = span.duration;
202
212
  const httpVersion = attrs['http.flavor'] as string | undefined;
@@ -204,9 +214,12 @@ export class ComprehendDevSpanProcessor implements SpanProcessor {
204
214
  const requestBytes = attrs['http.request_content_length'] as number | undefined;
205
215
  const responseBytes = attrs['http.response_content_length'] as number | undefined;
206
216
  const { message: errorMessage, type: errorType, stack: stack } = extractErrorInfo(span);
217
+ const { traceId, spanId } = getSpanContext(span);
207
218
  const observation: HttpServerObservation = {
208
219
  type: 'http-server',
209
220
  subject: observedRoute.hash,
221
+ spanId,
222
+ traceId,
210
223
  timestamp: span.startTime,
211
224
  path,
212
225
  status,
@@ -221,13 +234,12 @@ export class ComprehendDevSpanProcessor implements SpanProcessor {
221
234
  };
222
235
  this.ingestMessage({
223
236
  event: "observations",
224
- seq: this.observationsSeq++,
237
+ seq: this.connection.nextSeq(),
225
238
  observations: [observation]
226
239
  });
227
240
  }
228
241
 
229
242
  private processDatabaseOperation(currentService: ObservedService, span: ReadableSpan): void {
230
- // Parse the connection string.
231
243
  const attrs = span.attributes;
232
244
  const system = attrs['db.system'] as string;
233
245
  const rawConnection = attrs['db.connection_string'] as string | undefined;
@@ -241,11 +253,9 @@ export class ComprehendDevSpanProcessor implements SpanProcessor {
241
253
  name: attrs['db.name'] as string | undefined,
242
254
  };
243
255
 
244
- // See if we already have an entry for this database.
245
256
  const hash = hashIdString(`database:${system}:${parsed.host ?? ''}:${parsed.port ?? ''}:${parsed.name ?? ''}`);
246
257
  let observedDatabase = this.observedDatabases.find(db => db.hash === hash);
247
258
 
248
- // If we see it for the first time, add and ingest it.
249
259
  if (!observedDatabase) {
250
260
  observedDatabase = {
251
261
  system,
@@ -256,7 +266,6 @@ export class ComprehendDevSpanProcessor implements SpanProcessor {
256
266
  };
257
267
  this.observedDatabases.push(observedDatabase);
258
268
 
259
- // The existence of the database.
260
269
  const message: NewObservedDatabaseMessage = {
261
270
  event: "new-entity",
262
271
  type: "database",
@@ -271,7 +280,6 @@ export class ComprehendDevSpanProcessor implements SpanProcessor {
271
280
  this.ingestMessage(message);
272
281
  }
273
282
 
274
- // The connection to the database should have an interaction.
275
283
  const interactions = this.getInteractions(currentService.hash, observedDatabase.hash);
276
284
  let connectionInteraction = interactions.dbConnections.find(c => c.connection === parsed.scrubbed && c.user === parsed.user);
277
285
  if (!connectionInteraction) {
@@ -293,69 +301,40 @@ export class ComprehendDevSpanProcessor implements SpanProcessor {
293
301
  this.ingestMessage(message);
294
302
  }
295
303
 
296
- // The query of the database from the service (only for SQL for now).
304
+ // For SQL databases, send raw query to server for analysis
297
305
  if (sqlDbSystems.has(system) && attrs['db.statement']) {
298
- // The interaction, based upon the normalized query.
299
- let queryInfo = analyzeSQL(attrs['db.statement'] as string);
300
- let queryInteraction = interactions.dbQueries.find(q => q.query === queryInfo.normalizedQuery);
301
- if (!queryInteraction) {
302
- queryInteraction = {
303
- hash: hashIdString(`db-query:${currentService.hash}:${observedDatabase.hash}:${queryInfo.normalizedQuery}`),
304
- query: queryInfo.normalizedQuery
305
- };
306
- interactions.dbQueries.push(queryInteraction);
307
- const selects = getTablesWithOperation(queryInfo.tableOperations, 'SELECT');
308
- const inserts = getTablesWithOperation(queryInfo.tableOperations, 'INSERT');
309
- const updates = getTablesWithOperation(queryInfo.tableOperations, 'UPDATE');
310
- const deletes = getTablesWithOperation(queryInfo.tableOperations, 'DELETE');
311
- const message: NewObservedDatabaseQueryMessage = {
312
- event: "new-interaction",
313
- type: "db-query",
314
- hash: queryInteraction.hash,
315
- from: currentService.hash,
316
- to: observedDatabase.hash,
317
- query: queryInfo.presentableQuery,
318
- ...(selects ? { selects } : {}),
319
- ...(inserts ? { inserts } : {}),
320
- ...(updates ? { updates } : {}),
321
- ...(deletes ? { deletes } : {}),
322
- };
323
- this.ingestMessage(message);
324
- }
325
-
326
- // Build and ingest observation.
327
306
  const duration = span.duration;
328
307
  const returnedRows = (attrs['db.response.returned_rows'] as number | undefined)
329
308
  ?? (attrs['db.sql.rows'] as number | undefined);
330
309
  const { message: errorMessage, type: errorType, stack } = extractErrorInfo(span);
331
- const observation: DatabaseQueryObservation = {
332
- type: "db-query",
333
- 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,
334
317
  timestamp: span.startTime,
335
318
  duration,
319
+ traceId,
320
+ spanId,
336
321
  ...(errorMessage ? { errorMessage } : {}),
337
- ...(errorType ? { errorType } : {}),
322
+ ...(errorType ? { errorType } : {}),
338
323
  ...(stack ? { stack } : {}),
339
324
  ...(returnedRows !== undefined ? { returnedRows } : {})
340
325
  };
341
- this.ingestMessage({
342
- event: "observations",
343
- seq: this.observationsSeq++,
344
- observations: [observation]
345
- });
326
+ this.ingestMessage(dbQueryMessage);
346
327
  }
347
328
  }
348
329
 
349
330
  private processHttpRequest(currentService: ObservedService, url: string, span: ReadableSpan): void {
350
- // Build identity based upon protocol, host, and port.
351
331
  const parsed = new URL(url);
352
- const protocol = parsed.protocol.replace(':', ''); // Remove trailing colon
332
+ const protocol = parsed.protocol.replace(':', '');
353
333
  const host = parsed.hostname;
354
334
  const port = parsed.port ? parseInt(parsed.port) : (protocol === 'https' ? 443 : 80);
355
335
  const idString = `http-service:${protocol}:${host}:${port}`;
356
336
  const hash = hashIdString(idString);
357
337
 
358
- // Ingest it if it's not already observed.
359
338
  let observedHttpService = this.observedHttpServices.find(s =>
360
339
  s.protocol === protocol && s.host === host && s.port === port
361
340
  );
@@ -363,7 +342,6 @@ export class ComprehendDevSpanProcessor implements SpanProcessor {
363
342
  observedHttpService = { protocol, host, port, hash };
364
343
  this.observedHttpServices.push(observedHttpService);
365
344
 
366
- // The existence of the service.
367
345
  const message: NewObservedHttpServiceMessage = {
368
346
  event: "new-entity",
369
347
  type: "http-service",
@@ -375,7 +353,6 @@ export class ComprehendDevSpanProcessor implements SpanProcessor {
375
353
  this.ingestMessage(message);
376
354
  }
377
355
 
378
- // Ingest the interaction if first observed.
379
356
  const interactions = this.getInteractions(currentService.hash, observedHttpService.hash);
380
357
  if (!interactions.httpRequest) {
381
358
  const idString = `http-request:${currentService.hash}:${observedHttpService.hash}`;
@@ -391,11 +368,10 @@ export class ComprehendDevSpanProcessor implements SpanProcessor {
391
368
  this.ingestMessage(message);
392
369
  }
393
370
 
394
- // Build and ingest observation.
395
371
  const attrs = span.attributes;
396
372
  const path = parsed.pathname || '/';
397
373
  const method = span.attributes['http.method'] as string;
398
- if (!method) // Really should always be there
374
+ if (!method)
399
375
  return;
400
376
  const status = attrs['http.status_code'] as number | undefined;
401
377
  const duration = span.duration;
@@ -403,9 +379,12 @@ export class ComprehendDevSpanProcessor implements SpanProcessor {
403
379
  const requestBytes = attrs['http.request_content_length'] as number | undefined;
404
380
  const responseBytes = attrs['http.response_content_length'] as number | undefined;
405
381
  const { message: errorMessage, type: errorType, stack } = extractErrorInfo(span);
382
+ const { traceId, spanId } = getSpanContext(span);
406
383
  const observation: HttpClientObservation = {
407
384
  type: "http-client",
408
385
  subject: interactions.httpRequest.hash,
386
+ spanId,
387
+ traceId,
409
388
  timestamp: span.startTime,
410
389
  path,
411
390
  method,
@@ -415,16 +394,65 @@ export class ComprehendDevSpanProcessor implements SpanProcessor {
415
394
  ...(requestBytes !== undefined ? { requestBytes } : {}),
416
395
  ...(responseBytes !== undefined ? { responseBytes } : {}),
417
396
  ...(errorMessage ? { errorMessage } : {}),
418
- ...(errorType ? { errorType } : {}),
397
+ ...(errorType ? { errorType } : {}),
419
398
  ...(stack ? { stack } : {})
420
399
  };
421
400
  this.ingestMessage({
422
401
  event: "observations",
423
- seq: this.observationsSeq++,
402
+ seq: this.connection.nextSeq(),
424
403
  observations: [observation]
425
404
  });
426
405
  }
427
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
+
428
456
  private getInteractions(from: string, to: string) {
429
457
  let fromMap = this.observedInteractions.get(from);
430
458
  if (!fromMap) {
@@ -433,7 +461,7 @@ export class ComprehendDevSpanProcessor implements SpanProcessor {
433
461
  }
434
462
  let interactions = fromMap.get(to);
435
463
  if (!interactions) {
436
- interactions = { httpRequest: undefined, dbConnections: [], dbQueries: [] };
464
+ interactions = { httpRequest: undefined, dbConnections: [] };
437
465
  fromMap.set(to, interactions);
438
466
  }
439
467
  return interactions;
@@ -447,7 +475,6 @@ export class ComprehendDevSpanProcessor implements SpanProcessor {
447
475
  }
448
476
 
449
477
  async shutdown(): Promise<void> {
450
- this.connection.close()
451
478
  }
452
479
  }
453
480
 
@@ -457,8 +484,36 @@ function hashIdString(idString: string) {
457
484
  .join('');
458
485
  }
459
486
 
460
- /** Try to extract data from the database connection string. Many are URL based, fall back on
461
- * 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). */
462
517
  function parseDatabaseConnectionStringRaw(conn: string): {
463
518
  scrubbed: string;
464
519
  user?: string;
@@ -467,7 +522,6 @@ function parseDatabaseConnectionStringRaw(conn: string): {
467
522
  name?: string;
468
523
  } {
469
524
  try {
470
- // Try URL-style parsing first; scrub the user details if present.
471
525
  const url = new URL(conn);
472
526
  const user = url.username || undefined;
473
527
  const host = url.hostname || undefined;
@@ -483,7 +537,6 @@ function parseDatabaseConnectionStringRaw(conn: string): {
483
537
  name: dbName
484
538
  };
485
539
  } catch {
486
- // Not a URL-style string; try semi-structured parsing
487
540
  const parts = conn.split(';');
488
541
  const kv: Record<string, string> = {};
489
542
 
@@ -497,7 +550,6 @@ function parseDatabaseConnectionStringRaw(conn: string): {
497
550
  const port = kv['port'];
498
551
  const name = kv['database'] || kv['initial catalog'];
499
552
 
500
- // Reconstruct a scrubbed connection string without credentials
501
553
  const scrubbed = Object.entries(kv)
502
554
  .filter(([key]) => !['user id', 'uid', 'password', 'pwd'].includes(key))
503
555
  .map(([k, v]) => `${k}=${v}`)
@@ -513,8 +565,8 @@ function parseDatabaseConnectionStringRaw(conn: string): {
513
565
  }
514
566
  }
515
567
 
516
- /** Try pretty hard to get error info out of a span, if it has any. Handles it being there as an event,
517
- * 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. */
518
570
  function extractErrorInfo(span: ReadableSpan): {
519
571
  message?: string;
520
572
  type?: string;
@@ -522,7 +574,6 @@ function extractErrorInfo(span: ReadableSpan): {
522
574
  } {
523
575
  const attrs = span.attributes;
524
576
 
525
- // Try to extract from a structured 'exception' event, as it should have more detail
526
577
  const exceptionEvent = span.events.find(e => e.name === 'exception');
527
578
  if (exceptionEvent?.attributes) {
528
579
  const message = exceptionEvent.attributes['exception.message'] as string | undefined;
@@ -533,7 +584,6 @@ function extractErrorInfo(span: ReadableSpan): {
533
584
  }
534
585
  }
535
586
 
536
- // Fallback to attributes directly on the span.
537
587
  const isError = span.status.code === 2;
538
588
  const message =
539
589
  (attrs['exception.message'] as string | undefined) ??
@@ -553,13 +603,27 @@ function extractErrorInfo(span: ReadableSpan): {
553
603
  };
554
604
  }
555
605
 
556
- function getTablesWithOperation(
557
- tableOps: Record<string, string[]>,
558
- operation: string
559
- ): string[] | undefined {
560
- const op = operation.toUpperCase();
561
- const result = Object.entries(tableOps)
562
- .filter(([_, ops]) => ops.includes(op))
563
- .map(([table]) => table);
564
- 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
+ }
565
629
  }