@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.
- package/README.md +112 -27
- package/dist/ComprehendDevSpanProcessor.d.ts +10 -6
- package/dist/ComprehendDevSpanProcessor.js +154 -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/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 +178 -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,33 +74,45 @@ 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[] = [];
|
|
81
|
+
|
|
82
|
+
constructor(connection: WebSocketConnection) {
|
|
83
|
+
this.connection = connection;
|
|
84
|
+
}
|
|
80
85
|
|
|
81
|
-
|
|
82
|
-
this.
|
|
83
|
-
|
|
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 {
|
|
87
93
|
}
|
|
88
94
|
|
|
89
95
|
onEnd(span: ReadableSpan): void {
|
|
96
|
+
const process = () => this.processSpan(span);
|
|
97
|
+
if (span.resource.asyncAttributesPending) {
|
|
98
|
+
span.resource.waitForAsyncAttributes?.().then(process);
|
|
99
|
+
} else {
|
|
100
|
+
process();
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
private processSpan(span: ReadableSpan): void {
|
|
90
105
|
const currentService = this.discoverService(span);
|
|
91
106
|
if (!currentService)
|
|
92
107
|
return;
|
|
93
108
|
|
|
94
109
|
const attrs = span.attributes;
|
|
95
|
-
if (span.kind === 1) {
|
|
96
|
-
// Server span, see if it's something to ingest.
|
|
110
|
+
if (span.kind === 1) { // Server span
|
|
97
111
|
if (attrs['http.route'] && attrs['http.method']) {
|
|
98
112
|
this.processHTTPRoute(currentService, attrs['http.route'] as string, attrs['http.method'] as string, span);
|
|
99
113
|
}
|
|
100
114
|
}
|
|
101
|
-
else if (span.kind === 2) {
|
|
102
|
-
// Client span, see if it's something to ingest.
|
|
115
|
+
else if (span.kind === 2) { // Client span
|
|
103
116
|
if (attrs['http.url']) {
|
|
104
117
|
this.processHttpRequest(currentService, attrs['http.url'] as string, span);
|
|
105
118
|
}
|
|
@@ -107,10 +120,15 @@ export class ComprehendDevSpanProcessor implements SpanProcessor {
|
|
|
107
120
|
if (attrs['db.system']) {
|
|
108
121
|
this.processDatabaseOperation(currentService, span);
|
|
109
122
|
}
|
|
123
|
+
|
|
124
|
+
// Report trace span for every span
|
|
125
|
+
this.reportTraceSpan(span);
|
|
126
|
+
|
|
127
|
+
// Check custom span observation matching
|
|
128
|
+
this.checkCustomSpanMatching(span);
|
|
110
129
|
}
|
|
111
130
|
|
|
112
131
|
private discoverService(span: ReadableSpan): ObservedService | undefined {
|
|
113
|
-
// Look for an existing matching entry.
|
|
114
132
|
const resAttrs = span.resource.attributes;
|
|
115
133
|
const name = resAttrs['service.name'] as string | undefined;
|
|
116
134
|
if (!name)
|
|
@@ -125,7 +143,6 @@ export class ComprehendDevSpanProcessor implements SpanProcessor {
|
|
|
125
143
|
if (existing)
|
|
126
144
|
return existing;
|
|
127
145
|
|
|
128
|
-
// New; hash it and add it to the observed services.
|
|
129
146
|
const idString = `service:${name}:${namespace ?? ''}:${environment ?? ''}`;
|
|
130
147
|
const hash = hashIdString(idString);
|
|
131
148
|
const newService: ObservedService = {
|
|
@@ -134,7 +151,6 @@ export class ComprehendDevSpanProcessor implements SpanProcessor {
|
|
|
134
151
|
};
|
|
135
152
|
this.observedServices.push(newService);
|
|
136
153
|
|
|
137
|
-
// Ingest its existence.
|
|
138
154
|
const message: NewObservedServiceMessage = {
|
|
139
155
|
event: "new-entity",
|
|
140
156
|
type: "service",
|
|
@@ -144,22 +160,27 @@ export class ComprehendDevSpanProcessor implements SpanProcessor {
|
|
|
144
160
|
...(environment ? { environment } : {})
|
|
145
161
|
};
|
|
146
162
|
this.ingestMessage(message);
|
|
163
|
+
|
|
164
|
+
// Set process context on first service discovery
|
|
165
|
+
if (!this.processContextSet) {
|
|
166
|
+
this.processContextSet = true;
|
|
167
|
+
const resources = flattenResourceAttributes(resAttrs);
|
|
168
|
+
this.connection.setProcessContext(hash, resources);
|
|
169
|
+
}
|
|
170
|
+
|
|
147
171
|
return newService;
|
|
148
172
|
}
|
|
149
173
|
|
|
150
174
|
private processHTTPRoute(service: ObservedService, route: string, method: string, span: ReadableSpan): void {
|
|
151
|
-
// Check if this route+method already exists under the service
|
|
152
175
|
let observedRoute = service.httpRoutes.find(r =>
|
|
153
176
|
r.route === route && r.method === method
|
|
154
177
|
);
|
|
155
178
|
if (!observedRoute) {
|
|
156
|
-
// It's new; hash it and add it to our collection.
|
|
157
179
|
const idString = `http-route:${service.hash}:${method}:${route}`;
|
|
158
180
|
const hash = hashIdString(idString);
|
|
159
181
|
observedRoute = { method, route, hash };
|
|
160
182
|
service.httpRoutes.push(observedRoute);
|
|
161
183
|
|
|
162
|
-
// Emit observation message
|
|
163
184
|
const message: NewObservedHttpRouteMessage = {
|
|
164
185
|
event: "new-entity",
|
|
165
186
|
type: "http-route",
|
|
@@ -170,14 +191,13 @@ export class ComprehendDevSpanProcessor implements SpanProcessor {
|
|
|
170
191
|
this.ingestMessage(message);
|
|
171
192
|
}
|
|
172
193
|
|
|
173
|
-
// Extract the request path, making sure we
|
|
194
|
+
// Extract the request path, making sure we strip any query string.
|
|
174
195
|
const attrs = span.attributes;
|
|
175
196
|
let path: string;
|
|
176
197
|
if (attrs['http.target']) {
|
|
177
198
|
try {
|
|
178
|
-
// This might be just a path like "/search?q=foo"
|
|
179
199
|
const rawTarget = attrs['http.target'] as string;
|
|
180
|
-
const fakeUrl = new URL(rawTarget, 'http://placeholder');
|
|
200
|
+
const fakeUrl = new URL(rawTarget, 'http://placeholder');
|
|
181
201
|
path = fakeUrl.pathname;
|
|
182
202
|
} catch {
|
|
183
203
|
path = '/';
|
|
@@ -196,7 +216,6 @@ export class ComprehendDevSpanProcessor implements SpanProcessor {
|
|
|
196
216
|
path = '/';
|
|
197
217
|
}
|
|
198
218
|
|
|
199
|
-
// Build and ingest observation.
|
|
200
219
|
const status = attrs['http.status_code'] as number | undefined ?? 0;
|
|
201
220
|
const duration = span.duration;
|
|
202
221
|
const httpVersion = attrs['http.flavor'] as string | undefined;
|
|
@@ -204,9 +223,12 @@ export class ComprehendDevSpanProcessor implements SpanProcessor {
|
|
|
204
223
|
const requestBytes = attrs['http.request_content_length'] as number | undefined;
|
|
205
224
|
const responseBytes = attrs['http.response_content_length'] as number | undefined;
|
|
206
225
|
const { message: errorMessage, type: errorType, stack: stack } = extractErrorInfo(span);
|
|
226
|
+
const { traceId, spanId } = getSpanContext(span);
|
|
207
227
|
const observation: HttpServerObservation = {
|
|
208
228
|
type: 'http-server',
|
|
209
229
|
subject: observedRoute.hash,
|
|
230
|
+
spanId,
|
|
231
|
+
traceId,
|
|
210
232
|
timestamp: span.startTime,
|
|
211
233
|
path,
|
|
212
234
|
status,
|
|
@@ -221,13 +243,12 @@ export class ComprehendDevSpanProcessor implements SpanProcessor {
|
|
|
221
243
|
};
|
|
222
244
|
this.ingestMessage({
|
|
223
245
|
event: "observations",
|
|
224
|
-
seq: this.
|
|
246
|
+
seq: this.connection.nextSeq(),
|
|
225
247
|
observations: [observation]
|
|
226
248
|
});
|
|
227
249
|
}
|
|
228
250
|
|
|
229
251
|
private processDatabaseOperation(currentService: ObservedService, span: ReadableSpan): void {
|
|
230
|
-
// Parse the connection string.
|
|
231
252
|
const attrs = span.attributes;
|
|
232
253
|
const system = attrs['db.system'] as string;
|
|
233
254
|
const rawConnection = attrs['db.connection_string'] as string | undefined;
|
|
@@ -241,11 +262,9 @@ export class ComprehendDevSpanProcessor implements SpanProcessor {
|
|
|
241
262
|
name: attrs['db.name'] as string | undefined,
|
|
242
263
|
};
|
|
243
264
|
|
|
244
|
-
// See if we already have an entry for this database.
|
|
245
265
|
const hash = hashIdString(`database:${system}:${parsed.host ?? ''}:${parsed.port ?? ''}:${parsed.name ?? ''}`);
|
|
246
266
|
let observedDatabase = this.observedDatabases.find(db => db.hash === hash);
|
|
247
267
|
|
|
248
|
-
// If we see it for the first time, add and ingest it.
|
|
249
268
|
if (!observedDatabase) {
|
|
250
269
|
observedDatabase = {
|
|
251
270
|
system,
|
|
@@ -256,7 +275,6 @@ export class ComprehendDevSpanProcessor implements SpanProcessor {
|
|
|
256
275
|
};
|
|
257
276
|
this.observedDatabases.push(observedDatabase);
|
|
258
277
|
|
|
259
|
-
// The existence of the database.
|
|
260
278
|
const message: NewObservedDatabaseMessage = {
|
|
261
279
|
event: "new-entity",
|
|
262
280
|
type: "database",
|
|
@@ -271,7 +289,6 @@ export class ComprehendDevSpanProcessor implements SpanProcessor {
|
|
|
271
289
|
this.ingestMessage(message);
|
|
272
290
|
}
|
|
273
291
|
|
|
274
|
-
// The connection to the database should have an interaction.
|
|
275
292
|
const interactions = this.getInteractions(currentService.hash, observedDatabase.hash);
|
|
276
293
|
let connectionInteraction = interactions.dbConnections.find(c => c.connection === parsed.scrubbed && c.user === parsed.user);
|
|
277
294
|
if (!connectionInteraction) {
|
|
@@ -293,69 +310,40 @@ export class ComprehendDevSpanProcessor implements SpanProcessor {
|
|
|
293
310
|
this.ingestMessage(message);
|
|
294
311
|
}
|
|
295
312
|
|
|
296
|
-
//
|
|
313
|
+
// For SQL databases, send raw query to server for analysis
|
|
297
314
|
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
315
|
const duration = span.duration;
|
|
328
316
|
const returnedRows = (attrs['db.response.returned_rows'] as number | undefined)
|
|
329
317
|
?? (attrs['db.sql.rows'] as number | undefined);
|
|
330
318
|
const { message: errorMessage, type: errorType, stack } = extractErrorInfo(span);
|
|
331
|
-
const
|
|
332
|
-
|
|
333
|
-
|
|
319
|
+
const { traceId, spanId } = span.spanContext();
|
|
320
|
+
const dbQueryMessage: DatabaseQueryMessage = {
|
|
321
|
+
event: "db-query",
|
|
322
|
+
seq: this.connection.nextSeq(),
|
|
323
|
+
query: attrs['db.statement'] as string,
|
|
324
|
+
from: currentService.hash,
|
|
325
|
+
to: observedDatabase.hash,
|
|
334
326
|
timestamp: span.startTime,
|
|
335
327
|
duration,
|
|
328
|
+
traceId,
|
|
329
|
+
spanId,
|
|
336
330
|
...(errorMessage ? { errorMessage } : {}),
|
|
337
|
-
...(errorType ? {
|
|
331
|
+
...(errorType ? { errorType } : {}),
|
|
338
332
|
...(stack ? { stack } : {}),
|
|
339
333
|
...(returnedRows !== undefined ? { returnedRows } : {})
|
|
340
334
|
};
|
|
341
|
-
this.ingestMessage(
|
|
342
|
-
event: "observations",
|
|
343
|
-
seq: this.observationsSeq++,
|
|
344
|
-
observations: [observation]
|
|
345
|
-
});
|
|
335
|
+
this.ingestMessage(dbQueryMessage);
|
|
346
336
|
}
|
|
347
337
|
}
|
|
348
338
|
|
|
349
339
|
private processHttpRequest(currentService: ObservedService, url: string, span: ReadableSpan): void {
|
|
350
|
-
// Build identity based upon protocol, host, and port.
|
|
351
340
|
const parsed = new URL(url);
|
|
352
|
-
const protocol = parsed.protocol.replace(':', '');
|
|
341
|
+
const protocol = parsed.protocol.replace(':', '');
|
|
353
342
|
const host = parsed.hostname;
|
|
354
343
|
const port = parsed.port ? parseInt(parsed.port) : (protocol === 'https' ? 443 : 80);
|
|
355
344
|
const idString = `http-service:${protocol}:${host}:${port}`;
|
|
356
345
|
const hash = hashIdString(idString);
|
|
357
346
|
|
|
358
|
-
// Ingest it if it's not already observed.
|
|
359
347
|
let observedHttpService = this.observedHttpServices.find(s =>
|
|
360
348
|
s.protocol === protocol && s.host === host && s.port === port
|
|
361
349
|
);
|
|
@@ -363,7 +351,6 @@ export class ComprehendDevSpanProcessor implements SpanProcessor {
|
|
|
363
351
|
observedHttpService = { protocol, host, port, hash };
|
|
364
352
|
this.observedHttpServices.push(observedHttpService);
|
|
365
353
|
|
|
366
|
-
// The existence of the service.
|
|
367
354
|
const message: NewObservedHttpServiceMessage = {
|
|
368
355
|
event: "new-entity",
|
|
369
356
|
type: "http-service",
|
|
@@ -375,7 +362,6 @@ export class ComprehendDevSpanProcessor implements SpanProcessor {
|
|
|
375
362
|
this.ingestMessage(message);
|
|
376
363
|
}
|
|
377
364
|
|
|
378
|
-
// Ingest the interaction if first observed.
|
|
379
365
|
const interactions = this.getInteractions(currentService.hash, observedHttpService.hash);
|
|
380
366
|
if (!interactions.httpRequest) {
|
|
381
367
|
const idString = `http-request:${currentService.hash}:${observedHttpService.hash}`;
|
|
@@ -391,11 +377,10 @@ export class ComprehendDevSpanProcessor implements SpanProcessor {
|
|
|
391
377
|
this.ingestMessage(message);
|
|
392
378
|
}
|
|
393
379
|
|
|
394
|
-
// Build and ingest observation.
|
|
395
380
|
const attrs = span.attributes;
|
|
396
381
|
const path = parsed.pathname || '/';
|
|
397
382
|
const method = span.attributes['http.method'] as string;
|
|
398
|
-
if (!method)
|
|
383
|
+
if (!method)
|
|
399
384
|
return;
|
|
400
385
|
const status = attrs['http.status_code'] as number | undefined;
|
|
401
386
|
const duration = span.duration;
|
|
@@ -403,9 +388,12 @@ export class ComprehendDevSpanProcessor implements SpanProcessor {
|
|
|
403
388
|
const requestBytes = attrs['http.request_content_length'] as number | undefined;
|
|
404
389
|
const responseBytes = attrs['http.response_content_length'] as number | undefined;
|
|
405
390
|
const { message: errorMessage, type: errorType, stack } = extractErrorInfo(span);
|
|
391
|
+
const { traceId, spanId } = getSpanContext(span);
|
|
406
392
|
const observation: HttpClientObservation = {
|
|
407
393
|
type: "http-client",
|
|
408
394
|
subject: interactions.httpRequest.hash,
|
|
395
|
+
spanId,
|
|
396
|
+
traceId,
|
|
409
397
|
timestamp: span.startTime,
|
|
410
398
|
path,
|
|
411
399
|
method,
|
|
@@ -415,16 +403,65 @@ export class ComprehendDevSpanProcessor implements SpanProcessor {
|
|
|
415
403
|
...(requestBytes !== undefined ? { requestBytes } : {}),
|
|
416
404
|
...(responseBytes !== undefined ? { responseBytes } : {}),
|
|
417
405
|
...(errorMessage ? { errorMessage } : {}),
|
|
418
|
-
...(errorType ? {
|
|
406
|
+
...(errorType ? { errorType } : {}),
|
|
419
407
|
...(stack ? { stack } : {})
|
|
420
408
|
};
|
|
421
409
|
this.ingestMessage({
|
|
422
410
|
event: "observations",
|
|
423
|
-
seq: this.
|
|
411
|
+
seq: this.connection.nextSeq(),
|
|
424
412
|
observations: [observation]
|
|
425
413
|
});
|
|
426
414
|
}
|
|
427
415
|
|
|
416
|
+
private reportTraceSpan(span: ReadableSpan): void {
|
|
417
|
+
const { traceId, spanId } = getSpanContext(span);
|
|
418
|
+
const parent = getParentSpanId(span);
|
|
419
|
+
const traceSpanMessage: TraceSpansMessage = {
|
|
420
|
+
event: "tracespans",
|
|
421
|
+
seq: this.connection.nextSeq(),
|
|
422
|
+
data: [{
|
|
423
|
+
trace: traceId,
|
|
424
|
+
span: spanId,
|
|
425
|
+
parent,
|
|
426
|
+
name: span.name,
|
|
427
|
+
timestamp: span.startTime,
|
|
428
|
+
}]
|
|
429
|
+
};
|
|
430
|
+
this.ingestMessage(traceSpanMessage);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
private checkCustomSpanMatching(span: ReadableSpan): void {
|
|
434
|
+
for (const spec of this.spanObservationSpecs) {
|
|
435
|
+
if (matchSpanRule(span, spec.rule)) {
|
|
436
|
+
const { traceId, spanId } = getSpanContext(span);
|
|
437
|
+
const { message: errorMessage, type: errorType, stack } = extractErrorInfo(span);
|
|
438
|
+
const collectedAttrs: Record<string, AttributeType> = {};
|
|
439
|
+
for (const [key, value] of Object.entries(span.attributes)) {
|
|
440
|
+
if (value !== undefined) {
|
|
441
|
+
collectedAttrs[key] = value as AttributeType;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
const observation: CustomObservation = {
|
|
445
|
+
type: "custom",
|
|
446
|
+
subject: spec.subject,
|
|
447
|
+
id: spec.subject,
|
|
448
|
+
spanId,
|
|
449
|
+
traceId,
|
|
450
|
+
timestamp: span.startTime,
|
|
451
|
+
attributes: collectedAttrs,
|
|
452
|
+
...(errorMessage ? { errorMessage } : {}),
|
|
453
|
+
...(errorType ? { errorType } : {}),
|
|
454
|
+
...(stack ? { stack } : {}),
|
|
455
|
+
};
|
|
456
|
+
this.ingestMessage({
|
|
457
|
+
event: "observations",
|
|
458
|
+
seq: this.connection.nextSeq(),
|
|
459
|
+
observations: [observation]
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
428
465
|
private getInteractions(from: string, to: string) {
|
|
429
466
|
let fromMap = this.observedInteractions.get(from);
|
|
430
467
|
if (!fromMap) {
|
|
@@ -433,7 +470,7 @@ export class ComprehendDevSpanProcessor implements SpanProcessor {
|
|
|
433
470
|
}
|
|
434
471
|
let interactions = fromMap.get(to);
|
|
435
472
|
if (!interactions) {
|
|
436
|
-
interactions = { httpRequest: undefined, dbConnections: []
|
|
473
|
+
interactions = { httpRequest: undefined, dbConnections: [] };
|
|
437
474
|
fromMap.set(to, interactions);
|
|
438
475
|
}
|
|
439
476
|
return interactions;
|
|
@@ -447,7 +484,6 @@ export class ComprehendDevSpanProcessor implements SpanProcessor {
|
|
|
447
484
|
}
|
|
448
485
|
|
|
449
486
|
async shutdown(): Promise<void> {
|
|
450
|
-
this.connection.close()
|
|
451
487
|
}
|
|
452
488
|
}
|
|
453
489
|
|
|
@@ -457,8 +493,36 @@ function hashIdString(idString: string) {
|
|
|
457
493
|
.join('');
|
|
458
494
|
}
|
|
459
495
|
|
|
460
|
-
|
|
461
|
-
|
|
496
|
+
function getSpanContext(span: ReadableSpan): { traceId: string, spanId: string } {
|
|
497
|
+
const ctx = span.spanContext?.();
|
|
498
|
+
return {
|
|
499
|
+
traceId: ctx?.traceId ?? '',
|
|
500
|
+
spanId: ctx?.spanId ?? '',
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
function getParentSpanId(span: ReadableSpan): string {
|
|
505
|
+
return span.parentSpanContext?.spanId ?? '';
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function flattenResourceAttributes(attrs: Record<string, any>): Record<string, AttributeType> {
|
|
509
|
+
const result: Record<string, AttributeType> = {};
|
|
510
|
+
function flatten(obj: any, prefix: string) {
|
|
511
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
512
|
+
const fullKey = prefix ? `${prefix}.${key}` : key;
|
|
513
|
+
if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
|
|
514
|
+
flatten(value, fullKey);
|
|
515
|
+
} else {
|
|
516
|
+
result[fullKey] = String(value);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
flatten(attrs, '');
|
|
521
|
+
return result;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/** Try to extract data from the database connection string. Many are URL-based; falls back to
|
|
525
|
+
* semicolon-separated key=value parsing for the rest (e.g. MSSQL style). */
|
|
462
526
|
function parseDatabaseConnectionStringRaw(conn: string): {
|
|
463
527
|
scrubbed: string;
|
|
464
528
|
user?: string;
|
|
@@ -467,7 +531,6 @@ function parseDatabaseConnectionStringRaw(conn: string): {
|
|
|
467
531
|
name?: string;
|
|
468
532
|
} {
|
|
469
533
|
try {
|
|
470
|
-
// Try URL-style parsing first; scrub the user details if present.
|
|
471
534
|
const url = new URL(conn);
|
|
472
535
|
const user = url.username || undefined;
|
|
473
536
|
const host = url.hostname || undefined;
|
|
@@ -483,7 +546,6 @@ function parseDatabaseConnectionStringRaw(conn: string): {
|
|
|
483
546
|
name: dbName
|
|
484
547
|
};
|
|
485
548
|
} catch {
|
|
486
|
-
// Not a URL-style string; try semi-structured parsing
|
|
487
549
|
const parts = conn.split(';');
|
|
488
550
|
const kv: Record<string, string> = {};
|
|
489
551
|
|
|
@@ -497,7 +559,6 @@ function parseDatabaseConnectionStringRaw(conn: string): {
|
|
|
497
559
|
const port = kv['port'];
|
|
498
560
|
const name = kv['database'] || kv['initial catalog'];
|
|
499
561
|
|
|
500
|
-
// Reconstruct a scrubbed connection string without credentials
|
|
501
562
|
const scrubbed = Object.entries(kv)
|
|
502
563
|
.filter(([key]) => !['user id', 'uid', 'password', 'pwd'].includes(key))
|
|
503
564
|
.map(([k, v]) => `${k}=${v}`)
|
|
@@ -513,8 +574,8 @@ function parseDatabaseConnectionStringRaw(conn: string): {
|
|
|
513
574
|
}
|
|
514
575
|
}
|
|
515
576
|
|
|
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. */
|
|
577
|
+
/** Try pretty hard to get error info out of a span, if it has any. Handles it being there as an
|
|
578
|
+
* event, directly on the span with error semantics, and some other more ad-hoc cases. */
|
|
518
579
|
function extractErrorInfo(span: ReadableSpan): {
|
|
519
580
|
message?: string;
|
|
520
581
|
type?: string;
|
|
@@ -522,7 +583,6 @@ function extractErrorInfo(span: ReadableSpan): {
|
|
|
522
583
|
} {
|
|
523
584
|
const attrs = span.attributes;
|
|
524
585
|
|
|
525
|
-
// Try to extract from a structured 'exception' event, as it should have more detail
|
|
526
586
|
const exceptionEvent = span.events.find(e => e.name === 'exception');
|
|
527
587
|
if (exceptionEvent?.attributes) {
|
|
528
588
|
const message = exceptionEvent.attributes['exception.message'] as string | undefined;
|
|
@@ -533,7 +593,6 @@ function extractErrorInfo(span: ReadableSpan): {
|
|
|
533
593
|
}
|
|
534
594
|
}
|
|
535
595
|
|
|
536
|
-
// Fallback to attributes directly on the span.
|
|
537
596
|
const isError = span.status.code === 2;
|
|
538
597
|
const message =
|
|
539
598
|
(attrs['exception.message'] as string | undefined) ??
|
|
@@ -553,13 +612,27 @@ function extractErrorInfo(span: ReadableSpan): {
|
|
|
553
612
|
};
|
|
554
613
|
}
|
|
555
614
|
|
|
556
|
-
function
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
615
|
+
export function matchSpanRule(span: ReadableSpan, rule: SpanMatcherRule): boolean {
|
|
616
|
+
switch (rule.kind) {
|
|
617
|
+
case 'type': {
|
|
618
|
+
const kindMap: Record<string, SpanKind> = {
|
|
619
|
+
'client': SpanKind.CLIENT,
|
|
620
|
+
'server': SpanKind.SERVER,
|
|
621
|
+
'internal': SpanKind.INTERNAL,
|
|
622
|
+
};
|
|
623
|
+
return span.kind === kindMap[rule.value];
|
|
624
|
+
}
|
|
625
|
+
case 'attribute-present':
|
|
626
|
+
return span.attributes[rule.key] !== undefined;
|
|
627
|
+
case 'attribute-absent':
|
|
628
|
+
return span.attributes[rule.key] === undefined;
|
|
629
|
+
case 'attribute-equals':
|
|
630
|
+
return String(span.attributes[rule.key]) === rule.value;
|
|
631
|
+
case 'attribute-not-equals':
|
|
632
|
+
return String(span.attributes[rule.key]) !== rule.value;
|
|
633
|
+
case 'all':
|
|
634
|
+
return rule.rules.every(r => matchSpanRule(span, r));
|
|
635
|
+
case 'any':
|
|
636
|
+
return rule.rules.some(r => matchSpanRule(span, r));
|
|
637
|
+
}
|
|
565
638
|
}
|