@comprehend/telemetry-node 0.1.3 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +2 -1
- package/.idea/telemetry-node.iml +0 -1
- package/DEVELOPMENT.md +69 -0
- package/README.md +173 -0
- package/dist/ComprehendDevSpanProcessor.d.ts +9 -6
- package/dist/ComprehendDevSpanProcessor.js +146 -87
- package/dist/ComprehendDevSpanProcessor.test.d.ts +1 -0
- package/dist/ComprehendDevSpanProcessor.test.js +495 -0
- 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.d.ts +1 -0
- package/dist/WebSocketConnection.test.js +473 -0
- 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/jest.config.js +1 -0
- package/package.json +4 -2
- package/src/ComprehendDevSpanProcessor.test.ts +626 -0
- package/src/ComprehendDevSpanProcessor.ts +170 -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 +616 -0
- 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,21 +151,27 @@ export class ComprehendDevSpanProcessor implements SpanProcessor {
|
|
|
144
151
|
...(environment ? { environment } : {})
|
|
145
152
|
};
|
|
146
153
|
this.ingestMessage(message);
|
|
154
|
+
|
|
155
|
+
// Set process context on first service discovery
|
|
156
|
+
if (!this.processContextSet) {
|
|
157
|
+
this.processContextSet = true;
|
|
158
|
+
const resources = flattenResourceAttributes(resAttrs);
|
|
159
|
+
this.connection.setProcessContext(hash, resources);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return newService;
|
|
147
163
|
}
|
|
148
164
|
|
|
149
165
|
private processHTTPRoute(service: ObservedService, route: string, method: string, span: ReadableSpan): void {
|
|
150
|
-
// Check if this route+method already exists under the service
|
|
151
166
|
let observedRoute = service.httpRoutes.find(r =>
|
|
152
167
|
r.route === route && r.method === method
|
|
153
168
|
);
|
|
154
169
|
if (!observedRoute) {
|
|
155
|
-
// It's new; hash it and add it to our collection.
|
|
156
170
|
const idString = `http-route:${service.hash}:${method}:${route}`;
|
|
157
171
|
const hash = hashIdString(idString);
|
|
158
172
|
observedRoute = { method, route, hash };
|
|
159
173
|
service.httpRoutes.push(observedRoute);
|
|
160
174
|
|
|
161
|
-
// Emit observation message
|
|
162
175
|
const message: NewObservedHttpRouteMessage = {
|
|
163
176
|
event: "new-entity",
|
|
164
177
|
type: "http-route",
|
|
@@ -169,14 +182,13 @@ export class ComprehendDevSpanProcessor implements SpanProcessor {
|
|
|
169
182
|
this.ingestMessage(message);
|
|
170
183
|
}
|
|
171
184
|
|
|
172
|
-
// Extract the request path, making sure we
|
|
185
|
+
// Extract the request path, making sure we strip any query string.
|
|
173
186
|
const attrs = span.attributes;
|
|
174
187
|
let path: string;
|
|
175
188
|
if (attrs['http.target']) {
|
|
176
189
|
try {
|
|
177
|
-
// This might be just a path like "/search?q=foo"
|
|
178
190
|
const rawTarget = attrs['http.target'] as string;
|
|
179
|
-
const fakeUrl = new URL(rawTarget, 'http://placeholder');
|
|
191
|
+
const fakeUrl = new URL(rawTarget, 'http://placeholder');
|
|
180
192
|
path = fakeUrl.pathname;
|
|
181
193
|
} catch {
|
|
182
194
|
path = '/';
|
|
@@ -195,7 +207,6 @@ export class ComprehendDevSpanProcessor implements SpanProcessor {
|
|
|
195
207
|
path = '/';
|
|
196
208
|
}
|
|
197
209
|
|
|
198
|
-
// Build and ingest observation.
|
|
199
210
|
const status = attrs['http.status_code'] as number | undefined ?? 0;
|
|
200
211
|
const duration = span.duration;
|
|
201
212
|
const httpVersion = attrs['http.flavor'] as string | undefined;
|
|
@@ -203,9 +214,12 @@ export class ComprehendDevSpanProcessor implements SpanProcessor {
|
|
|
203
214
|
const requestBytes = attrs['http.request_content_length'] as number | undefined;
|
|
204
215
|
const responseBytes = attrs['http.response_content_length'] as number | undefined;
|
|
205
216
|
const { message: errorMessage, type: errorType, stack: stack } = extractErrorInfo(span);
|
|
217
|
+
const { traceId, spanId } = getSpanContext(span);
|
|
206
218
|
const observation: HttpServerObservation = {
|
|
207
219
|
type: 'http-server',
|
|
208
220
|
subject: observedRoute.hash,
|
|
221
|
+
spanId,
|
|
222
|
+
traceId,
|
|
209
223
|
timestamp: span.startTime,
|
|
210
224
|
path,
|
|
211
225
|
status,
|
|
@@ -220,13 +234,12 @@ export class ComprehendDevSpanProcessor implements SpanProcessor {
|
|
|
220
234
|
};
|
|
221
235
|
this.ingestMessage({
|
|
222
236
|
event: "observations",
|
|
223
|
-
seq: this.
|
|
237
|
+
seq: this.connection.nextSeq(),
|
|
224
238
|
observations: [observation]
|
|
225
239
|
});
|
|
226
240
|
}
|
|
227
241
|
|
|
228
242
|
private processDatabaseOperation(currentService: ObservedService, span: ReadableSpan): void {
|
|
229
|
-
// Parse the connection string.
|
|
230
243
|
const attrs = span.attributes;
|
|
231
244
|
const system = attrs['db.system'] as string;
|
|
232
245
|
const rawConnection = attrs['db.connection_string'] as string | undefined;
|
|
@@ -240,11 +253,9 @@ export class ComprehendDevSpanProcessor implements SpanProcessor {
|
|
|
240
253
|
name: attrs['db.name'] as string | undefined,
|
|
241
254
|
};
|
|
242
255
|
|
|
243
|
-
// See if we already have an entry for this database.
|
|
244
256
|
const hash = hashIdString(`database:${system}:${parsed.host ?? ''}:${parsed.port ?? ''}:${parsed.name ?? ''}`);
|
|
245
257
|
let observedDatabase = this.observedDatabases.find(db => db.hash === hash);
|
|
246
258
|
|
|
247
|
-
// If we see it for the first time, add and ingest it.
|
|
248
259
|
if (!observedDatabase) {
|
|
249
260
|
observedDatabase = {
|
|
250
261
|
system,
|
|
@@ -255,7 +266,6 @@ export class ComprehendDevSpanProcessor implements SpanProcessor {
|
|
|
255
266
|
};
|
|
256
267
|
this.observedDatabases.push(observedDatabase);
|
|
257
268
|
|
|
258
|
-
// The existence of the database.
|
|
259
269
|
const message: NewObservedDatabaseMessage = {
|
|
260
270
|
event: "new-entity",
|
|
261
271
|
type: "database",
|
|
@@ -270,7 +280,6 @@ export class ComprehendDevSpanProcessor implements SpanProcessor {
|
|
|
270
280
|
this.ingestMessage(message);
|
|
271
281
|
}
|
|
272
282
|
|
|
273
|
-
// The connection to the database should have an interaction.
|
|
274
283
|
const interactions = this.getInteractions(currentService.hash, observedDatabase.hash);
|
|
275
284
|
let connectionInteraction = interactions.dbConnections.find(c => c.connection === parsed.scrubbed && c.user === parsed.user);
|
|
276
285
|
if (!connectionInteraction) {
|
|
@@ -292,69 +301,40 @@ export class ComprehendDevSpanProcessor implements SpanProcessor {
|
|
|
292
301
|
this.ingestMessage(message);
|
|
293
302
|
}
|
|
294
303
|
|
|
295
|
-
//
|
|
304
|
+
// For SQL databases, send raw query to server for analysis
|
|
296
305
|
if (sqlDbSystems.has(system) && attrs['db.statement']) {
|
|
297
|
-
// The interaction, based upon the normalized query.
|
|
298
|
-
let queryInfo = analyzeSQL(attrs['db.statement'] as string);
|
|
299
|
-
let queryInteraction = interactions.dbQueries.find(q => q.query === queryInfo.normalizedQuery);
|
|
300
|
-
if (!queryInteraction) {
|
|
301
|
-
queryInteraction = {
|
|
302
|
-
hash: hashIdString(`db-query:${currentService.hash}:${observedDatabase.hash}:${queryInfo.normalizedQuery}`),
|
|
303
|
-
query: queryInfo.normalizedQuery
|
|
304
|
-
};
|
|
305
|
-
interactions.dbQueries.push(queryInteraction);
|
|
306
|
-
const selects = getTablesWithOperation(queryInfo.tableOperations, 'SELECT');
|
|
307
|
-
const inserts = getTablesWithOperation(queryInfo.tableOperations, 'INSERT');
|
|
308
|
-
const updates = getTablesWithOperation(queryInfo.tableOperations, 'UPDATE');
|
|
309
|
-
const deletes = getTablesWithOperation(queryInfo.tableOperations, 'DELETE');
|
|
310
|
-
const message: NewObservedDatabaseQueryMessage = {
|
|
311
|
-
event: "new-interaction",
|
|
312
|
-
type: "db-query",
|
|
313
|
-
hash: queryInteraction.hash,
|
|
314
|
-
from: currentService.hash,
|
|
315
|
-
to: observedDatabase.hash,
|
|
316
|
-
query: queryInfo.presentableQuery,
|
|
317
|
-
...(selects ? { selects } : {}),
|
|
318
|
-
...(inserts ? { inserts } : {}),
|
|
319
|
-
...(updates ? { updates } : {}),
|
|
320
|
-
...(deletes ? { deletes } : {}),
|
|
321
|
-
};
|
|
322
|
-
this.ingestMessage(message);
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
// Build and ingest observation.
|
|
326
306
|
const duration = span.duration;
|
|
327
307
|
const returnedRows = (attrs['db.response.returned_rows'] as number | undefined)
|
|
328
308
|
?? (attrs['db.sql.rows'] as number | undefined);
|
|
329
309
|
const { message: errorMessage, type: errorType, stack } = extractErrorInfo(span);
|
|
330
|
-
const
|
|
331
|
-
|
|
332
|
-
|
|
310
|
+
const { traceId, spanId } = span.spanContext();
|
|
311
|
+
const dbQueryMessage: DatabaseQueryMessage = {
|
|
312
|
+
event: "db-query",
|
|
313
|
+
seq: this.connection.nextSeq(),
|
|
314
|
+
query: attrs['db.statement'] as string,
|
|
315
|
+
from: currentService.hash,
|
|
316
|
+
to: observedDatabase.hash,
|
|
333
317
|
timestamp: span.startTime,
|
|
334
318
|
duration,
|
|
319
|
+
traceId,
|
|
320
|
+
spanId,
|
|
335
321
|
...(errorMessage ? { errorMessage } : {}),
|
|
336
|
-
...(errorType ? {
|
|
322
|
+
...(errorType ? { errorType } : {}),
|
|
337
323
|
...(stack ? { stack } : {}),
|
|
338
324
|
...(returnedRows !== undefined ? { returnedRows } : {})
|
|
339
325
|
};
|
|
340
|
-
this.ingestMessage(
|
|
341
|
-
event: "observations",
|
|
342
|
-
seq: this.observationsSeq++,
|
|
343
|
-
observations: [observation]
|
|
344
|
-
});
|
|
326
|
+
this.ingestMessage(dbQueryMessage);
|
|
345
327
|
}
|
|
346
328
|
}
|
|
347
329
|
|
|
348
330
|
private processHttpRequest(currentService: ObservedService, url: string, span: ReadableSpan): void {
|
|
349
|
-
// Build identity based upon protocol, host, and port.
|
|
350
331
|
const parsed = new URL(url);
|
|
351
|
-
const protocol = parsed.protocol.replace(':', '');
|
|
332
|
+
const protocol = parsed.protocol.replace(':', '');
|
|
352
333
|
const host = parsed.hostname;
|
|
353
334
|
const port = parsed.port ? parseInt(parsed.port) : (protocol === 'https' ? 443 : 80);
|
|
354
335
|
const idString = `http-service:${protocol}:${host}:${port}`;
|
|
355
336
|
const hash = hashIdString(idString);
|
|
356
337
|
|
|
357
|
-
// Ingest it if it's not already observed.
|
|
358
338
|
let observedHttpService = this.observedHttpServices.find(s =>
|
|
359
339
|
s.protocol === protocol && s.host === host && s.port === port
|
|
360
340
|
);
|
|
@@ -362,7 +342,6 @@ export class ComprehendDevSpanProcessor implements SpanProcessor {
|
|
|
362
342
|
observedHttpService = { protocol, host, port, hash };
|
|
363
343
|
this.observedHttpServices.push(observedHttpService);
|
|
364
344
|
|
|
365
|
-
// The existence of the service.
|
|
366
345
|
const message: NewObservedHttpServiceMessage = {
|
|
367
346
|
event: "new-entity",
|
|
368
347
|
type: "http-service",
|
|
@@ -374,7 +353,6 @@ export class ComprehendDevSpanProcessor implements SpanProcessor {
|
|
|
374
353
|
this.ingestMessage(message);
|
|
375
354
|
}
|
|
376
355
|
|
|
377
|
-
// Ingest the interaction if first observed.
|
|
378
356
|
const interactions = this.getInteractions(currentService.hash, observedHttpService.hash);
|
|
379
357
|
if (!interactions.httpRequest) {
|
|
380
358
|
const idString = `http-request:${currentService.hash}:${observedHttpService.hash}`;
|
|
@@ -390,11 +368,10 @@ export class ComprehendDevSpanProcessor implements SpanProcessor {
|
|
|
390
368
|
this.ingestMessage(message);
|
|
391
369
|
}
|
|
392
370
|
|
|
393
|
-
// Build and ingest observation.
|
|
394
371
|
const attrs = span.attributes;
|
|
395
372
|
const path = parsed.pathname || '/';
|
|
396
373
|
const method = span.attributes['http.method'] as string;
|
|
397
|
-
if (!method)
|
|
374
|
+
if (!method)
|
|
398
375
|
return;
|
|
399
376
|
const status = attrs['http.status_code'] as number | undefined;
|
|
400
377
|
const duration = span.duration;
|
|
@@ -402,9 +379,12 @@ export class ComprehendDevSpanProcessor implements SpanProcessor {
|
|
|
402
379
|
const requestBytes = attrs['http.request_content_length'] as number | undefined;
|
|
403
380
|
const responseBytes = attrs['http.response_content_length'] as number | undefined;
|
|
404
381
|
const { message: errorMessage, type: errorType, stack } = extractErrorInfo(span);
|
|
382
|
+
const { traceId, spanId } = getSpanContext(span);
|
|
405
383
|
const observation: HttpClientObservation = {
|
|
406
384
|
type: "http-client",
|
|
407
385
|
subject: interactions.httpRequest.hash,
|
|
386
|
+
spanId,
|
|
387
|
+
traceId,
|
|
408
388
|
timestamp: span.startTime,
|
|
409
389
|
path,
|
|
410
390
|
method,
|
|
@@ -414,16 +394,65 @@ export class ComprehendDevSpanProcessor implements SpanProcessor {
|
|
|
414
394
|
...(requestBytes !== undefined ? { requestBytes } : {}),
|
|
415
395
|
...(responseBytes !== undefined ? { responseBytes } : {}),
|
|
416
396
|
...(errorMessage ? { errorMessage } : {}),
|
|
417
|
-
...(errorType ? {
|
|
397
|
+
...(errorType ? { errorType } : {}),
|
|
418
398
|
...(stack ? { stack } : {})
|
|
419
399
|
};
|
|
420
400
|
this.ingestMessage({
|
|
421
401
|
event: "observations",
|
|
422
|
-
seq: this.
|
|
402
|
+
seq: this.connection.nextSeq(),
|
|
423
403
|
observations: [observation]
|
|
424
404
|
});
|
|
425
405
|
}
|
|
426
406
|
|
|
407
|
+
private reportTraceSpan(span: ReadableSpan): void {
|
|
408
|
+
const { traceId, spanId } = getSpanContext(span);
|
|
409
|
+
const parent = getParentSpanId(span);
|
|
410
|
+
const traceSpanMessage: TraceSpansMessage = {
|
|
411
|
+
event: "tracespans",
|
|
412
|
+
seq: this.connection.nextSeq(),
|
|
413
|
+
data: [{
|
|
414
|
+
trace: traceId,
|
|
415
|
+
span: spanId,
|
|
416
|
+
parent,
|
|
417
|
+
name: span.name,
|
|
418
|
+
timestamp: span.startTime,
|
|
419
|
+
}]
|
|
420
|
+
};
|
|
421
|
+
this.ingestMessage(traceSpanMessage);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
private checkCustomSpanMatching(span: ReadableSpan): void {
|
|
425
|
+
for (const spec of this.spanObservationSpecs) {
|
|
426
|
+
if (matchSpanRule(span, spec.rule)) {
|
|
427
|
+
const { traceId, spanId } = getSpanContext(span);
|
|
428
|
+
const { message: errorMessage, type: errorType, stack } = extractErrorInfo(span);
|
|
429
|
+
const collectedAttrs: Record<string, AttributeType> = {};
|
|
430
|
+
for (const [key, value] of Object.entries(span.attributes)) {
|
|
431
|
+
if (value !== undefined) {
|
|
432
|
+
collectedAttrs[key] = value as AttributeType;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
const observation: CustomObservation = {
|
|
436
|
+
type: "custom",
|
|
437
|
+
subject: spec.subject,
|
|
438
|
+
id: spec.subject,
|
|
439
|
+
spanId,
|
|
440
|
+
traceId,
|
|
441
|
+
timestamp: span.startTime,
|
|
442
|
+
attributes: collectedAttrs,
|
|
443
|
+
...(errorMessage ? { errorMessage } : {}),
|
|
444
|
+
...(errorType ? { errorType } : {}),
|
|
445
|
+
...(stack ? { stack } : {}),
|
|
446
|
+
};
|
|
447
|
+
this.ingestMessage({
|
|
448
|
+
event: "observations",
|
|
449
|
+
seq: this.connection.nextSeq(),
|
|
450
|
+
observations: [observation]
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
427
456
|
private getInteractions(from: string, to: string) {
|
|
428
457
|
let fromMap = this.observedInteractions.get(from);
|
|
429
458
|
if (!fromMap) {
|
|
@@ -432,7 +461,7 @@ export class ComprehendDevSpanProcessor implements SpanProcessor {
|
|
|
432
461
|
}
|
|
433
462
|
let interactions = fromMap.get(to);
|
|
434
463
|
if (!interactions) {
|
|
435
|
-
interactions = { httpRequest: undefined, dbConnections: []
|
|
464
|
+
interactions = { httpRequest: undefined, dbConnections: [] };
|
|
436
465
|
fromMap.set(to, interactions);
|
|
437
466
|
}
|
|
438
467
|
return interactions;
|
|
@@ -446,7 +475,6 @@ export class ComprehendDevSpanProcessor implements SpanProcessor {
|
|
|
446
475
|
}
|
|
447
476
|
|
|
448
477
|
async shutdown(): Promise<void> {
|
|
449
|
-
this.connection.close()
|
|
450
478
|
}
|
|
451
479
|
}
|
|
452
480
|
|
|
@@ -456,8 +484,36 @@ function hashIdString(idString: string) {
|
|
|
456
484
|
.join('');
|
|
457
485
|
}
|
|
458
486
|
|
|
459
|
-
|
|
460
|
-
|
|
487
|
+
function getSpanContext(span: ReadableSpan): { traceId: string, spanId: string } {
|
|
488
|
+
const ctx = span.spanContext?.();
|
|
489
|
+
return {
|
|
490
|
+
traceId: ctx?.traceId ?? '',
|
|
491
|
+
spanId: ctx?.spanId ?? '',
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
function getParentSpanId(span: ReadableSpan): string {
|
|
496
|
+
return span.parentSpanContext?.spanId ?? '';
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function flattenResourceAttributes(attrs: Record<string, any>): Record<string, AttributeType> {
|
|
500
|
+
const result: Record<string, AttributeType> = {};
|
|
501
|
+
function flatten(obj: any, prefix: string) {
|
|
502
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
503
|
+
const fullKey = prefix ? `${prefix}.${key}` : key;
|
|
504
|
+
if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
|
|
505
|
+
flatten(value, fullKey);
|
|
506
|
+
} else {
|
|
507
|
+
result[fullKey] = String(value);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
flatten(attrs, '');
|
|
512
|
+
return result;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/** Try to extract data from the database connection string. Many are URL-based; falls back to
|
|
516
|
+
* semicolon-separated key=value parsing for the rest (e.g. MSSQL style). */
|
|
461
517
|
function parseDatabaseConnectionStringRaw(conn: string): {
|
|
462
518
|
scrubbed: string;
|
|
463
519
|
user?: string;
|
|
@@ -466,7 +522,6 @@ function parseDatabaseConnectionStringRaw(conn: string): {
|
|
|
466
522
|
name?: string;
|
|
467
523
|
} {
|
|
468
524
|
try {
|
|
469
|
-
// Try URL-style parsing first; scrub the user details if present.
|
|
470
525
|
const url = new URL(conn);
|
|
471
526
|
const user = url.username || undefined;
|
|
472
527
|
const host = url.hostname || undefined;
|
|
@@ -482,7 +537,6 @@ function parseDatabaseConnectionStringRaw(conn: string): {
|
|
|
482
537
|
name: dbName
|
|
483
538
|
};
|
|
484
539
|
} catch {
|
|
485
|
-
// Not a URL-style string; try semi-structured parsing
|
|
486
540
|
const parts = conn.split(';');
|
|
487
541
|
const kv: Record<string, string> = {};
|
|
488
542
|
|
|
@@ -496,7 +550,6 @@ function parseDatabaseConnectionStringRaw(conn: string): {
|
|
|
496
550
|
const port = kv['port'];
|
|
497
551
|
const name = kv['database'] || kv['initial catalog'];
|
|
498
552
|
|
|
499
|
-
// Reconstruct a scrubbed connection string without credentials
|
|
500
553
|
const scrubbed = Object.entries(kv)
|
|
501
554
|
.filter(([key]) => !['user id', 'uid', 'password', 'pwd'].includes(key))
|
|
502
555
|
.map(([k, v]) => `${k}=${v}`)
|
|
@@ -512,8 +565,8 @@ function parseDatabaseConnectionStringRaw(conn: string): {
|
|
|
512
565
|
}
|
|
513
566
|
}
|
|
514
567
|
|
|
515
|
-
/** Try pretty hard to get error info out of a span, if it has any. Handles it being there as an
|
|
516
|
-
* directly on the span with error semantics, and some other more ad-hoc cases. */
|
|
568
|
+
/** Try pretty hard to get error info out of a span, if it has any. Handles it being there as an
|
|
569
|
+
* event, directly on the span with error semantics, and some other more ad-hoc cases. */
|
|
517
570
|
function extractErrorInfo(span: ReadableSpan): {
|
|
518
571
|
message?: string;
|
|
519
572
|
type?: string;
|
|
@@ -521,7 +574,6 @@ function extractErrorInfo(span: ReadableSpan): {
|
|
|
521
574
|
} {
|
|
522
575
|
const attrs = span.attributes;
|
|
523
576
|
|
|
524
|
-
// Try to extract from a structured 'exception' event, as it should have more detail
|
|
525
577
|
const exceptionEvent = span.events.find(e => e.name === 'exception');
|
|
526
578
|
if (exceptionEvent?.attributes) {
|
|
527
579
|
const message = exceptionEvent.attributes['exception.message'] as string | undefined;
|
|
@@ -532,7 +584,6 @@ function extractErrorInfo(span: ReadableSpan): {
|
|
|
532
584
|
}
|
|
533
585
|
}
|
|
534
586
|
|
|
535
|
-
// Fallback to attributes directly on the span.
|
|
536
587
|
const isError = span.status.code === 2;
|
|
537
588
|
const message =
|
|
538
589
|
(attrs['exception.message'] as string | undefined) ??
|
|
@@ -552,13 +603,27 @@ function extractErrorInfo(span: ReadableSpan): {
|
|
|
552
603
|
};
|
|
553
604
|
}
|
|
554
605
|
|
|
555
|
-
function
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
606
|
+
export function matchSpanRule(span: ReadableSpan, rule: SpanMatcherRule): boolean {
|
|
607
|
+
switch (rule.kind) {
|
|
608
|
+
case 'type': {
|
|
609
|
+
const kindMap: Record<string, SpanKind> = {
|
|
610
|
+
'client': SpanKind.CLIENT,
|
|
611
|
+
'server': SpanKind.SERVER,
|
|
612
|
+
'internal': SpanKind.INTERNAL,
|
|
613
|
+
};
|
|
614
|
+
return span.kind === kindMap[rule.value];
|
|
615
|
+
}
|
|
616
|
+
case 'attribute-present':
|
|
617
|
+
return span.attributes[rule.key] !== undefined;
|
|
618
|
+
case 'attribute-absent':
|
|
619
|
+
return span.attributes[rule.key] === undefined;
|
|
620
|
+
case 'attribute-equals':
|
|
621
|
+
return String(span.attributes[rule.key]) === rule.value;
|
|
622
|
+
case 'attribute-not-equals':
|
|
623
|
+
return String(span.attributes[rule.key]) !== rule.value;
|
|
624
|
+
case 'all':
|
|
625
|
+
return rule.rules.every(r => matchSpanRule(span, r));
|
|
626
|
+
case 'any':
|
|
627
|
+
return rule.rules.some(r => matchSpanRule(span, r));
|
|
628
|
+
}
|
|
564
629
|
}
|