@comprehend/telemetry-node 0.1.4 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,38 +1,49 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ComprehendDevSpanProcessor = void 0;
4
+ exports.matchSpanRule = matchSpanRule;
4
5
  const sha2_1 = require("@noble/hashes/sha2");
5
6
  const utils_1 = require("@noble/hashes/utils");
6
- const sql_analyzer_1 = require("./sql-analyzer");
7
- const WebSocketConnection_1 = require("./WebSocketConnection");
7
+ const api_1 = require("@opentelemetry/api");
8
8
  const sqlDbSystems = new Set([
9
9
  'mysql', 'postgresql', 'mssql', 'oracle', 'db2', 'sqlite', 'hsqldb', 'h2',
10
10
  'informix', 'cockroachdb', 'redshift', 'tidb', 'trino', 'greenplum'
11
11
  ]);
12
12
  class ComprehendDevSpanProcessor {
13
- constructor(options) {
13
+ constructor(connection) {
14
14
  this.observedServices = [];
15
15
  this.observedDatabases = [];
16
16
  this.observedHttpServices = [];
17
17
  this.observedInteractions = new Map();
18
- this.observationsSeq = 1;
19
- this.connection = new WebSocketConnection_1.WebSocketConnection(options.organization, options.token, options.debug === true ? console.log : options.debug === false ? undefined : options.debug);
18
+ this.processContextSet = false;
19
+ this.spanObservationSpecs = [];
20
+ this.connection = connection;
21
+ }
22
+ updateCustomMetrics(specs) {
23
+ this.spanObservationSpecs = specs.filter((s) => s.type === 'span');
20
24
  }
21
25
  onStart(span, parentContext) {
22
26
  }
23
27
  onEnd(span) {
28
+ const process = () => this.processSpan(span);
29
+ if (span.resource.asyncAttributesPending) {
30
+ span.resource.waitForAsyncAttributes?.().then(process);
31
+ }
32
+ else {
33
+ process();
34
+ }
35
+ }
36
+ processSpan(span) {
24
37
  const currentService = this.discoverService(span);
25
38
  if (!currentService)
26
39
  return;
27
40
  const attrs = span.attributes;
28
- if (span.kind === 1) {
29
- // Server span, see if it's something to ingest.
41
+ if (span.kind === 1) { // Server span
30
42
  if (attrs['http.route'] && attrs['http.method']) {
31
43
  this.processHTTPRoute(currentService, attrs['http.route'], attrs['http.method'], span);
32
44
  }
33
45
  }
34
- else if (span.kind === 2) {
35
- // Client span, see if it's something to ingest.
46
+ else if (span.kind === 2) { // Client span
36
47
  if (attrs['http.url']) {
37
48
  this.processHttpRequest(currentService, attrs['http.url'], span);
38
49
  }
@@ -40,9 +51,12 @@ class ComprehendDevSpanProcessor {
40
51
  if (attrs['db.system']) {
41
52
  this.processDatabaseOperation(currentService, span);
42
53
  }
54
+ // Report trace span for every span
55
+ this.reportTraceSpan(span);
56
+ // Check custom span observation matching
57
+ this.checkCustomSpanMatching(span);
43
58
  }
44
59
  discoverService(span) {
45
- // Look for an existing matching entry.
46
60
  const resAttrs = span.resource.attributes;
47
61
  const name = resAttrs['service.name'];
48
62
  if (!name)
@@ -54,7 +68,6 @@ class ComprehendDevSpanProcessor {
54
68
  s.environment === environment);
55
69
  if (existing)
56
70
  return existing;
57
- // New; hash it and add it to the observed services.
58
71
  const idString = `service:${name}:${namespace ?? ''}:${environment ?? ''}`;
59
72
  const hash = hashIdString(idString);
60
73
  const newService = {
@@ -62,7 +75,6 @@ class ComprehendDevSpanProcessor {
62
75
  httpRoutes: []
63
76
  };
64
77
  this.observedServices.push(newService);
65
- // Ingest its existence.
66
78
  const message = {
67
79
  event: "new-entity",
68
80
  type: "service",
@@ -72,18 +84,21 @@ class ComprehendDevSpanProcessor {
72
84
  ...(environment ? { environment } : {})
73
85
  };
74
86
  this.ingestMessage(message);
87
+ // Set process context on first service discovery
88
+ if (!this.processContextSet) {
89
+ this.processContextSet = true;
90
+ const resources = flattenResourceAttributes(resAttrs);
91
+ this.connection.setProcessContext(hash, resources);
92
+ }
75
93
  return newService;
76
94
  }
77
95
  processHTTPRoute(service, route, method, span) {
78
- // Check if this route+method already exists under the service
79
96
  let observedRoute = service.httpRoutes.find(r => r.route === route && r.method === method);
80
97
  if (!observedRoute) {
81
- // It's new; hash it and add it to our collection.
82
98
  const idString = `http-route:${service.hash}:${method}:${route}`;
83
99
  const hash = hashIdString(idString);
84
100
  observedRoute = { method, route, hash };
85
101
  service.httpRoutes.push(observedRoute);
86
- // Emit observation message
87
102
  const message = {
88
103
  event: "new-entity",
89
104
  type: "http-route",
@@ -93,14 +108,13 @@ class ComprehendDevSpanProcessor {
93
108
  };
94
109
  this.ingestMessage(message);
95
110
  }
96
- // Extract the request path, making sure we only get that, without any query string.
111
+ // Extract the request path, making sure we strip any query string.
97
112
  const attrs = span.attributes;
98
113
  let path;
99
114
  if (attrs['http.target']) {
100
115
  try {
101
- // This might be just a path like "/search?q=foo"
102
116
  const rawTarget = attrs['http.target'];
103
- const fakeUrl = new URL(rawTarget, 'http://placeholder'); // placeholder base
117
+ const fakeUrl = new URL(rawTarget, 'http://placeholder');
104
118
  path = fakeUrl.pathname;
105
119
  }
106
120
  catch {
@@ -120,7 +134,6 @@ class ComprehendDevSpanProcessor {
120
134
  else {
121
135
  path = '/';
122
136
  }
123
- // Build and ingest observation.
124
137
  const status = attrs['http.status_code'] ?? 0;
125
138
  const duration = span.duration;
126
139
  const httpVersion = attrs['http.flavor'];
@@ -128,9 +141,12 @@ class ComprehendDevSpanProcessor {
128
141
  const requestBytes = attrs['http.request_content_length'];
129
142
  const responseBytes = attrs['http.response_content_length'];
130
143
  const { message: errorMessage, type: errorType, stack: stack } = extractErrorInfo(span);
144
+ const { traceId, spanId } = getSpanContext(span);
131
145
  const observation = {
132
146
  type: 'http-server',
133
147
  subject: observedRoute.hash,
148
+ spanId,
149
+ traceId,
134
150
  timestamp: span.startTime,
135
151
  path,
136
152
  status,
@@ -145,12 +161,11 @@ class ComprehendDevSpanProcessor {
145
161
  };
146
162
  this.ingestMessage({
147
163
  event: "observations",
148
- seq: this.observationsSeq++,
164
+ seq: this.connection.nextSeq(),
149
165
  observations: [observation]
150
166
  });
151
167
  }
152
168
  processDatabaseOperation(currentService, span) {
153
- // Parse the connection string.
154
169
  const attrs = span.attributes;
155
170
  const system = attrs['db.system'];
156
171
  const rawConnection = attrs['db.connection_string'];
@@ -163,10 +178,8 @@ class ComprehendDevSpanProcessor {
163
178
  port: attrs['net.peer.port']?.toString(),
164
179
  name: attrs['db.name'],
165
180
  };
166
- // See if we already have an entry for this database.
167
181
  const hash = hashIdString(`database:${system}:${parsed.host ?? ''}:${parsed.port ?? ''}:${parsed.name ?? ''}`);
168
182
  let observedDatabase = this.observedDatabases.find(db => db.hash === hash);
169
- // If we see it for the first time, add and ingest it.
170
183
  if (!observedDatabase) {
171
184
  observedDatabase = {
172
185
  system,
@@ -176,7 +189,6 @@ class ComprehendDevSpanProcessor {
176
189
  hash
177
190
  };
178
191
  this.observedDatabases.push(observedDatabase);
179
- // The existence of the database.
180
192
  const message = {
181
193
  event: "new-entity",
182
194
  type: "database",
@@ -190,7 +202,6 @@ class ComprehendDevSpanProcessor {
190
202
  };
191
203
  this.ingestMessage(message);
192
204
  }
193
- // The connection to the database should have an interaction.
194
205
  const interactions = this.getInteractions(currentService.hash, observedDatabase.hash);
195
206
  let connectionInteraction = interactions.dbConnections.find(c => c.connection === parsed.scrubbed && c.user === parsed.user);
196
207
  if (!connectionInteraction) {
@@ -211,71 +222,42 @@ class ComprehendDevSpanProcessor {
211
222
  };
212
223
  this.ingestMessage(message);
213
224
  }
214
- // The query of the database from the service (only for SQL for now).
225
+ // For SQL databases, send raw query to server for analysis
215
226
  if (sqlDbSystems.has(system) && attrs['db.statement']) {
216
- // The interaction, based upon the normalized query.
217
- let queryInfo = (0, sql_analyzer_1.analyzeSQL)(attrs['db.statement']);
218
- let queryInteraction = interactions.dbQueries.find(q => q.query === queryInfo.normalizedQuery);
219
- if (!queryInteraction) {
220
- queryInteraction = {
221
- hash: hashIdString(`db-query:${currentService.hash}:${observedDatabase.hash}:${queryInfo.normalizedQuery}`),
222
- query: queryInfo.normalizedQuery
223
- };
224
- interactions.dbQueries.push(queryInteraction);
225
- const selects = getTablesWithOperation(queryInfo.tableOperations, 'SELECT');
226
- const inserts = getTablesWithOperation(queryInfo.tableOperations, 'INSERT');
227
- const updates = getTablesWithOperation(queryInfo.tableOperations, 'UPDATE');
228
- const deletes = getTablesWithOperation(queryInfo.tableOperations, 'DELETE');
229
- const message = {
230
- event: "new-interaction",
231
- type: "db-query",
232
- hash: queryInteraction.hash,
233
- from: currentService.hash,
234
- to: observedDatabase.hash,
235
- query: queryInfo.presentableQuery,
236
- ...(selects ? { selects } : {}),
237
- ...(inserts ? { inserts } : {}),
238
- ...(updates ? { updates } : {}),
239
- ...(deletes ? { deletes } : {}),
240
- };
241
- this.ingestMessage(message);
242
- }
243
- // Build and ingest observation.
244
227
  const duration = span.duration;
245
228
  const returnedRows = attrs['db.response.returned_rows']
246
229
  ?? attrs['db.sql.rows'];
247
230
  const { message: errorMessage, type: errorType, stack } = extractErrorInfo(span);
248
- const observation = {
249
- type: "db-query",
250
- subject: queryInteraction.hash,
231
+ const { traceId, spanId } = span.spanContext();
232
+ const dbQueryMessage = {
233
+ event: "db-query",
234
+ seq: this.connection.nextSeq(),
235
+ query: attrs['db.statement'],
236
+ from: currentService.hash,
237
+ to: observedDatabase.hash,
251
238
  timestamp: span.startTime,
252
239
  duration,
240
+ traceId,
241
+ spanId,
253
242
  ...(errorMessage ? { errorMessage } : {}),
254
243
  ...(errorType ? { errorType } : {}),
255
244
  ...(stack ? { stack } : {}),
256
245
  ...(returnedRows !== undefined ? { returnedRows } : {})
257
246
  };
258
- this.ingestMessage({
259
- event: "observations",
260
- seq: this.observationsSeq++,
261
- observations: [observation]
262
- });
247
+ this.ingestMessage(dbQueryMessage);
263
248
  }
264
249
  }
265
250
  processHttpRequest(currentService, url, span) {
266
- // Build identity based upon protocol, host, and port.
267
251
  const parsed = new URL(url);
268
- const protocol = parsed.protocol.replace(':', ''); // Remove trailing colon
252
+ const protocol = parsed.protocol.replace(':', '');
269
253
  const host = parsed.hostname;
270
254
  const port = parsed.port ? parseInt(parsed.port) : (protocol === 'https' ? 443 : 80);
271
255
  const idString = `http-service:${protocol}:${host}:${port}`;
272
256
  const hash = hashIdString(idString);
273
- // Ingest it if it's not already observed.
274
257
  let observedHttpService = this.observedHttpServices.find(s => s.protocol === protocol && s.host === host && s.port === port);
275
258
  if (!observedHttpService) {
276
259
  observedHttpService = { protocol, host, port, hash };
277
260
  this.observedHttpServices.push(observedHttpService);
278
- // The existence of the service.
279
261
  const message = {
280
262
  event: "new-entity",
281
263
  type: "http-service",
@@ -286,7 +268,6 @@ class ComprehendDevSpanProcessor {
286
268
  };
287
269
  this.ingestMessage(message);
288
270
  }
289
- // Ingest the interaction if first observed.
290
271
  const interactions = this.getInteractions(currentService.hash, observedHttpService.hash);
291
272
  if (!interactions.httpRequest) {
292
273
  const idString = `http-request:${currentService.hash}:${observedHttpService.hash}`;
@@ -301,11 +282,10 @@ class ComprehendDevSpanProcessor {
301
282
  };
302
283
  this.ingestMessage(message);
303
284
  }
304
- // Build and ingest observation.
305
285
  const attrs = span.attributes;
306
286
  const path = parsed.pathname || '/';
307
287
  const method = span.attributes['http.method'];
308
- if (!method) // Really should always be there
288
+ if (!method)
309
289
  return;
310
290
  const status = attrs['http.status_code'];
311
291
  const duration = span.duration;
@@ -313,9 +293,12 @@ class ComprehendDevSpanProcessor {
313
293
  const requestBytes = attrs['http.request_content_length'];
314
294
  const responseBytes = attrs['http.response_content_length'];
315
295
  const { message: errorMessage, type: errorType, stack } = extractErrorInfo(span);
296
+ const { traceId, spanId } = getSpanContext(span);
316
297
  const observation = {
317
298
  type: "http-client",
318
299
  subject: interactions.httpRequest.hash,
300
+ spanId,
301
+ traceId,
319
302
  timestamp: span.startTime,
320
303
  path,
321
304
  method,
@@ -330,10 +313,57 @@ class ComprehendDevSpanProcessor {
330
313
  };
331
314
  this.ingestMessage({
332
315
  event: "observations",
333
- seq: this.observationsSeq++,
316
+ seq: this.connection.nextSeq(),
334
317
  observations: [observation]
335
318
  });
336
319
  }
320
+ reportTraceSpan(span) {
321
+ const { traceId, spanId } = getSpanContext(span);
322
+ const parent = getParentSpanId(span);
323
+ const traceSpanMessage = {
324
+ event: "tracespans",
325
+ seq: this.connection.nextSeq(),
326
+ data: [{
327
+ trace: traceId,
328
+ span: spanId,
329
+ parent,
330
+ name: span.name,
331
+ timestamp: span.startTime,
332
+ }]
333
+ };
334
+ this.ingestMessage(traceSpanMessage);
335
+ }
336
+ checkCustomSpanMatching(span) {
337
+ for (const spec of this.spanObservationSpecs) {
338
+ if (matchSpanRule(span, spec.rule)) {
339
+ const { traceId, spanId } = getSpanContext(span);
340
+ const { message: errorMessage, type: errorType, stack } = extractErrorInfo(span);
341
+ const collectedAttrs = {};
342
+ for (const [key, value] of Object.entries(span.attributes)) {
343
+ if (value !== undefined) {
344
+ collectedAttrs[key] = value;
345
+ }
346
+ }
347
+ const observation = {
348
+ type: "custom",
349
+ subject: spec.subject,
350
+ id: spec.subject,
351
+ spanId,
352
+ traceId,
353
+ timestamp: span.startTime,
354
+ attributes: collectedAttrs,
355
+ ...(errorMessage ? { errorMessage } : {}),
356
+ ...(errorType ? { errorType } : {}),
357
+ ...(stack ? { stack } : {}),
358
+ };
359
+ this.ingestMessage({
360
+ event: "observations",
361
+ seq: this.connection.nextSeq(),
362
+ observations: [observation]
363
+ });
364
+ }
365
+ }
366
+ }
337
367
  getInteractions(from, to) {
338
368
  let fromMap = this.observedInteractions.get(from);
339
369
  if (!fromMap) {
@@ -342,7 +372,7 @@ class ComprehendDevSpanProcessor {
342
372
  }
343
373
  let interactions = fromMap.get(to);
344
374
  if (!interactions) {
345
- interactions = { httpRequest: undefined, dbConnections: [], dbQueries: [] };
375
+ interactions = { httpRequest: undefined, dbConnections: [] };
346
376
  fromMap.set(to, interactions);
347
377
  }
348
378
  return interactions;
@@ -353,7 +383,6 @@ class ComprehendDevSpanProcessor {
353
383
  async forceFlush() {
354
384
  }
355
385
  async shutdown() {
356
- this.connection.close();
357
386
  }
358
387
  }
359
388
  exports.ComprehendDevSpanProcessor = ComprehendDevSpanProcessor;
@@ -362,11 +391,36 @@ function hashIdString(idString) {
362
391
  .map(b => b.toString(16).padStart(2, '0'))
363
392
  .join('');
364
393
  }
365
- /** Try to extract data from the database connection string. Many are URL based, fall back on
366
- * some kind of comma-seperated style for the rest. */
394
+ function getSpanContext(span) {
395
+ const ctx = span.spanContext?.();
396
+ return {
397
+ traceId: ctx?.traceId ?? '',
398
+ spanId: ctx?.spanId ?? '',
399
+ };
400
+ }
401
+ function getParentSpanId(span) {
402
+ return span.parentSpanContext?.spanId ?? '';
403
+ }
404
+ function flattenResourceAttributes(attrs) {
405
+ const result = {};
406
+ function flatten(obj, prefix) {
407
+ for (const [key, value] of Object.entries(obj)) {
408
+ const fullKey = prefix ? `${prefix}.${key}` : key;
409
+ if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
410
+ flatten(value, fullKey);
411
+ }
412
+ else {
413
+ result[fullKey] = String(value);
414
+ }
415
+ }
416
+ }
417
+ flatten(attrs, '');
418
+ return result;
419
+ }
420
+ /** Try to extract data from the database connection string. Many are URL-based; falls back to
421
+ * semicolon-separated key=value parsing for the rest (e.g. MSSQL style). */
367
422
  function parseDatabaseConnectionStringRaw(conn) {
368
423
  try {
369
- // Try URL-style parsing first; scrub the user details if present.
370
424
  const url = new URL(conn);
371
425
  const user = url.username || undefined;
372
426
  const host = url.hostname || undefined;
@@ -383,7 +437,6 @@ function parseDatabaseConnectionStringRaw(conn) {
383
437
  };
384
438
  }
385
439
  catch {
386
- // Not a URL-style string; try semi-structured parsing
387
440
  const parts = conn.split(';');
388
441
  const kv = {};
389
442
  for (const part of parts) {
@@ -395,7 +448,6 @@ function parseDatabaseConnectionStringRaw(conn) {
395
448
  const host = kv['server'] || kv['data source'] || kv['address'];
396
449
  const port = kv['port'];
397
450
  const name = kv['database'] || kv['initial catalog'];
398
- // Reconstruct a scrubbed connection string without credentials
399
451
  const scrubbed = Object.entries(kv)
400
452
  .filter(([key]) => !['user id', 'uid', 'password', 'pwd'].includes(key))
401
453
  .map(([k, v]) => `${k}=${v}`)
@@ -409,11 +461,10 @@ function parseDatabaseConnectionStringRaw(conn) {
409
461
  };
410
462
  }
411
463
  }
412
- /** Try pretty hard to get error info out of a span, if it has any. Handles it being there as an event,
413
- * directly on the span with error semantics, and some other more ad-hoc cases. */
464
+ /** Try pretty hard to get error info out of a span, if it has any. Handles it being there as an
465
+ * event, directly on the span with error semantics, and some other more ad-hoc cases. */
414
466
  function extractErrorInfo(span) {
415
467
  const attrs = span.attributes;
416
- // Try to extract from a structured 'exception' event, as it should have more detail
417
468
  const exceptionEvent = span.events.find(e => e.name === 'exception');
418
469
  if (exceptionEvent?.attributes) {
419
470
  const message = exceptionEvent.attributes['exception.message'];
@@ -423,7 +474,6 @@ function extractErrorInfo(span) {
423
474
  return { message, type, stack };
424
475
  }
425
476
  }
426
- // Fallback to attributes directly on the span.
427
477
  const isError = span.status.code === 2;
428
478
  const message = attrs['exception.message'] ??
429
479
  attrs['http.error_message'] ??
@@ -440,10 +490,27 @@ function extractErrorInfo(span) {
440
490
  stack
441
491
  };
442
492
  }
443
- function getTablesWithOperation(tableOps, operation) {
444
- const op = operation.toUpperCase();
445
- const result = Object.entries(tableOps)
446
- .filter(([_, ops]) => ops.includes(op))
447
- .map(([table]) => table);
448
- return result.length > 0 ? result : undefined;
493
+ function matchSpanRule(span, rule) {
494
+ switch (rule.kind) {
495
+ case 'type': {
496
+ const kindMap = {
497
+ 'client': api_1.SpanKind.CLIENT,
498
+ 'server': api_1.SpanKind.SERVER,
499
+ 'internal': api_1.SpanKind.INTERNAL,
500
+ };
501
+ return span.kind === kindMap[rule.value];
502
+ }
503
+ case 'attribute-present':
504
+ return span.attributes[rule.key] !== undefined;
505
+ case 'attribute-absent':
506
+ return span.attributes[rule.key] === undefined;
507
+ case 'attribute-equals':
508
+ return String(span.attributes[rule.key]) === rule.value;
509
+ case 'attribute-not-equals':
510
+ return String(span.attributes[rule.key]) !== rule.value;
511
+ case 'all':
512
+ return rule.rules.every(r => matchSpanRule(span, r));
513
+ case 'any':
514
+ return rule.rules.some(r => matchSpanRule(span, r));
515
+ }
449
516
  }