@comprehend/telemetry-node 0.1.4 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +2 -2
- package/.idea/telemetry-node.iml +0 -1
- package/README.md +73 -27
- package/dist/ComprehendDevSpanProcessor.d.ts +9 -6
- package/dist/ComprehendDevSpanProcessor.js +145 -87
- package/dist/ComprehendDevSpanProcessor.test.js +270 -449
- package/dist/ComprehendMetricsExporter.d.ts +18 -0
- package/dist/ComprehendMetricsExporter.js +178 -0
- package/dist/ComprehendMetricsExporter.test.d.ts +1 -0
- package/dist/ComprehendMetricsExporter.test.js +266 -0
- package/dist/ComprehendSDK.d.ts +18 -0
- package/dist/ComprehendSDK.js +56 -0
- package/dist/ComprehendSDK.test.d.ts +1 -0
- package/dist/ComprehendSDK.test.js +126 -0
- package/dist/WebSocketConnection.d.ts +23 -3
- package/dist/WebSocketConnection.js +106 -12
- package/dist/WebSocketConnection.test.js +236 -169
- package/dist/index.d.ts +3 -1
- package/dist/index.js +5 -1
- package/dist/sql-analyzer.js +2 -11
- package/dist/sql-analyzer.test.js +0 -12
- package/dist/util.d.ts +2 -0
- package/dist/util.js +7 -0
- package/dist/wire-protocol.d.ts +168 -28
- package/package.json +3 -1
- package/src/ComprehendDevSpanProcessor.test.ts +311 -507
- package/src/ComprehendDevSpanProcessor.ts +169 -105
- package/src/ComprehendMetricsExporter.test.ts +334 -0
- package/src/ComprehendMetricsExporter.ts +225 -0
- package/src/ComprehendSDK.test.ts +160 -0
- package/src/ComprehendSDK.ts +63 -0
- package/src/WebSocketConnection.test.ts +286 -205
- package/src/WebSocketConnection.ts +135 -13
- package/src/index.ts +3 -2
- package/src/util.ts +6 -0
- package/src/wire-protocol.ts +204 -29
- package/src/sql-analyzer.test.ts +0 -599
- package/src/sql-analyzer.ts +0 -439
|
@@ -1,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
|
-
|
|
6
|
+
AttributeType,
|
|
7
|
+
CustomObservation,
|
|
8
|
+
CustomSpanObservationSpecification,
|
|
9
|
+
DatabaseQueryMessage,
|
|
7
10
|
HttpClientObservation,
|
|
8
|
-
HttpServerObservation,
|
|
9
|
-
|
|
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;
|
|
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
|
|
79
|
+
private processContextSet = false;
|
|
80
|
+
private spanObservationSpecs: CustomSpanObservationSpecification[] = [];
|
|
80
81
|
|
|
81
|
-
constructor(
|
|
82
|
-
this.connection =
|
|
83
|
-
|
|
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
|
|
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');
|
|
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.
|
|
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
|
-
//
|
|
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
|
|
332
|
-
|
|
333
|
-
|
|
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 ? {
|
|
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(':', '');
|
|
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)
|
|
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 ? {
|
|
397
|
+
...(errorType ? { errorType } : {}),
|
|
419
398
|
...(stack ? { stack } : {})
|
|
420
399
|
};
|
|
421
400
|
this.ingestMessage({
|
|
422
401
|
event: "observations",
|
|
423
|
-
seq: this.
|
|
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: []
|
|
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
|
-
|
|
461
|
-
|
|
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
|
|
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
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
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
|
}
|