@comprehend/telemetry-node 0.1.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.
@@ -0,0 +1,8 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="ProjectModuleManager">
4
+ <modules>
5
+ <module fileurl="file://$PROJECT_DIR$/.idea/telemetry-node.iml" filepath="$PROJECT_DIR$/.idea/telemetry-node.iml" />
6
+ </modules>
7
+ </component>
8
+ </project>
@@ -0,0 +1,13 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <module type="WEB_MODULE" version="4">
3
+ <component name="NewModuleRootManager">
4
+ <content url="file://$MODULE_DIR$">
5
+ <excludeFolder url="file://$MODULE_DIR$/.tmp" />
6
+ <excludeFolder url="file://$MODULE_DIR$/dist" />
7
+ <excludeFolder url="file://$MODULE_DIR$/temp" />
8
+ <excludeFolder url="file://$MODULE_DIR$/tmp" />
9
+ </content>
10
+ <orderEntry type="inheritedJdk" />
11
+ <orderEntry type="sourceFolder" forTests="false" />
12
+ </component>
13
+ </module>
package/.idea/vcs.xml ADDED
@@ -0,0 +1,6 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="VcsDirectoryMappings">
4
+ <mapping directory="$PROJECT_DIR$/.." vcs="Git" />
5
+ </component>
6
+ </project>
@@ -0,0 +1,25 @@
1
+ import { Context } from "@opentelemetry/api";
2
+ import { ReadableSpan, Span, SpanProcessor } from "@opentelemetry/sdk-trace-base";
3
+ export declare class ComprehendDevSpanProcessor implements SpanProcessor {
4
+ private readonly connection;
5
+ private observedServices;
6
+ private observedDatabases;
7
+ private observedHttpServices;
8
+ private observedInteractions;
9
+ private observationsSeq;
10
+ constructor(options: {
11
+ organization: string;
12
+ token: string;
13
+ debug?: boolean | ((message: string) => void);
14
+ });
15
+ onStart(span: Span, parentContext: Context): void;
16
+ onEnd(span: ReadableSpan): void;
17
+ private discoverService;
18
+ private processHTTPRoute;
19
+ private processDatabaseOperation;
20
+ private processHttpRequest;
21
+ private getInteractions;
22
+ private ingestMessage;
23
+ forceFlush(): Promise<void>;
24
+ shutdown(): Promise<void>;
25
+ }
@@ -0,0 +1,447 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ComprehendDevSpanProcessor = void 0;
4
+ const sha2_1 = require("@noble/hashes/sha2");
5
+ const utils_1 = require("@noble/hashes/utils");
6
+ const sql_analyzer_1 = require("./sql-analyzer");
7
+ const WebSocketConnection_1 = require("./WebSocketConnection");
8
+ const sqlDbSystems = new Set([
9
+ 'mysql', 'postgresql', 'mssql', 'oracle', 'db2', 'sqlite', 'hsqldb', 'h2',
10
+ 'informix', 'cockroachdb', 'redshift', 'tidb', 'trino', 'greenplum'
11
+ ]);
12
+ class ComprehendDevSpanProcessor {
13
+ constructor(options) {
14
+ this.observedServices = [];
15
+ this.observedDatabases = [];
16
+ this.observedHttpServices = [];
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);
20
+ }
21
+ onStart(span, parentContext) {
22
+ }
23
+ onEnd(span) {
24
+ const currentService = this.discoverService(span);
25
+ if (!currentService)
26
+ return;
27
+ const attrs = span.attributes;
28
+ if (span.kind === 1) {
29
+ // Server span, see if it's something to ingest.
30
+ if (attrs['http.route'] && attrs['http.method']) {
31
+ this.processHTTPRoute(currentService, attrs['http.route'], attrs['http.method'], span);
32
+ }
33
+ }
34
+ else if (span.kind === 2) {
35
+ // Client span, see if it's something to ingest.
36
+ if (attrs['http.url']) {
37
+ this.processHttpRequest(currentService, attrs['http.url'], span);
38
+ }
39
+ }
40
+ if (attrs['db.system']) {
41
+ this.processDatabaseOperation(currentService, span);
42
+ }
43
+ }
44
+ discoverService(span) {
45
+ // Look for an existing matching entry.
46
+ const resAttrs = span.resource.attributes;
47
+ const name = resAttrs['service.name'];
48
+ if (!name)
49
+ return;
50
+ const namespace = resAttrs['service.namespace'];
51
+ const environment = resAttrs['deployment.environment'];
52
+ const existing = this.observedServices.find(s => s.name === name &&
53
+ s.namespace === namespace &&
54
+ s.environment === environment);
55
+ if (existing)
56
+ return existing;
57
+ // New; hash it and add it to the observed services.
58
+ const idString = `service:${name}:${namespace ?? ''}:${environment ?? ''}`;
59
+ const hash = hashIdString(idString);
60
+ const newService = {
61
+ name, namespace, environment, hash,
62
+ httpRoutes: []
63
+ };
64
+ this.observedServices.push(newService);
65
+ // Ingest its existence.
66
+ const message = {
67
+ event: "new-entity",
68
+ type: "service",
69
+ hash,
70
+ name,
71
+ ...(namespace ? { namespace } : {}),
72
+ ...(environment ? { environment } : {})
73
+ };
74
+ this.ingestMessage(message);
75
+ }
76
+ processHTTPRoute(service, route, method, span) {
77
+ // Check if this route+method already exists under the service
78
+ let observedRoute = service.httpRoutes.find(r => r.route === route && r.method === method);
79
+ if (!observedRoute) {
80
+ // It's new; hash it and add it to our collection.
81
+ const idString = `http-route:${service.hash}:${method}:${route}`;
82
+ const hash = hashIdString(idString);
83
+ observedRoute = { method, route, hash };
84
+ service.httpRoutes.push(observedRoute);
85
+ // Emit observation message
86
+ const message = {
87
+ event: "new-entity",
88
+ type: "http-route",
89
+ hash,
90
+ parent: service.hash,
91
+ method, route
92
+ };
93
+ this.ingestMessage(message);
94
+ }
95
+ // Extract the request path, making sure we only get that, without any query string.
96
+ const attrs = span.attributes;
97
+ let path;
98
+ if (attrs['http.target']) {
99
+ try {
100
+ // This might be just a path like "/search?q=foo"
101
+ const rawTarget = attrs['http.target'];
102
+ const fakeUrl = new URL(rawTarget, 'http://placeholder'); // placeholder base
103
+ path = fakeUrl.pathname;
104
+ }
105
+ catch {
106
+ path = '/';
107
+ }
108
+ }
109
+ else if (attrs['http.url']) {
110
+ try {
111
+ const rawUrl = attrs['http.url'];
112
+ const parsed = new URL(rawUrl);
113
+ path = parsed.pathname;
114
+ }
115
+ catch {
116
+ path = '/';
117
+ }
118
+ }
119
+ else {
120
+ path = '/';
121
+ }
122
+ // Build and ingest observation.
123
+ const status = attrs['http.status_code'] ?? 0;
124
+ const duration = span.duration;
125
+ const httpVersion = attrs['http.flavor'];
126
+ const userAgent = attrs['http.user_agent'];
127
+ const requestBytes = attrs['http.request_content_length'];
128
+ const responseBytes = attrs['http.response_content_length'];
129
+ const { message: errorMessage, type: errorType, stack: stack } = extractErrorInfo(span);
130
+ const observation = {
131
+ type: 'http-server',
132
+ subject: observedRoute.hash,
133
+ timestamp: span.startTime,
134
+ path,
135
+ status,
136
+ duration,
137
+ ...(httpVersion ? { httpVersion } : {}),
138
+ ...(userAgent ? { userAgent } : {}),
139
+ ...(requestBytes !== undefined ? { requestBytes } : {}),
140
+ ...(responseBytes !== undefined ? { responseBytes } : {}),
141
+ ...(errorMessage ? { errorMessage } : {}),
142
+ ...(errorType ? { errorType } : {}),
143
+ ...(stack ? { stack } : {})
144
+ };
145
+ this.ingestMessage({
146
+ event: "observations",
147
+ seq: this.observationsSeq++,
148
+ observations: [observation]
149
+ });
150
+ }
151
+ processDatabaseOperation(currentService, span) {
152
+ // Parse the connection string.
153
+ const attrs = span.attributes;
154
+ const system = attrs['db.system'];
155
+ const rawConnection = attrs['db.connection_string'];
156
+ const parsed = rawConnection
157
+ ? parseDatabaseConnectionStringRaw(rawConnection)
158
+ : {
159
+ scrubbed: '',
160
+ user: undefined,
161
+ host: (attrs['net.peer.name'] ?? attrs['net.peer.ip']),
162
+ port: attrs['net.peer.port']?.toString(),
163
+ name: attrs['db.name'],
164
+ };
165
+ // See if we already have an entry for this database.
166
+ const hash = hashIdString(`database:${system}:${parsed.host ?? ''}:${parsed.port ?? ''}:${parsed.name ?? ''}`);
167
+ let observedDatabase = this.observedDatabases.find(db => db.hash === hash);
168
+ // If we see it for the first time, add and ingest it.
169
+ if (!observedDatabase) {
170
+ observedDatabase = {
171
+ system,
172
+ host: parsed.host,
173
+ port: parsed.port ? parseInt(parsed.port) : undefined,
174
+ name: parsed.name,
175
+ hash
176
+ };
177
+ this.observedDatabases.push(observedDatabase);
178
+ // The existence of the database.
179
+ const message = {
180
+ event: "new-entity",
181
+ type: "database",
182
+ hash,
183
+ system,
184
+ ...(parsed.name ? { name: parsed.name } : {}),
185
+ ...(parsed.scrubbed ? { connection: parsed.scrubbed } : {}),
186
+ ...(parsed.host ? { host: parsed.host } : {}),
187
+ ...(parsed.port ? { port: parseInt(parsed.port) } : {}),
188
+ ...(parsed.user ? { user: parsed.user } : {})
189
+ };
190
+ this.ingestMessage(message);
191
+ }
192
+ // The connection to the database should have an interaction.
193
+ const interactions = this.getInteractions(currentService.hash, observedDatabase.hash);
194
+ let connectionInteraction = interactions.dbConnections.find(c => c.connection === parsed.scrubbed && c.user === parsed.user);
195
+ if (!connectionInteraction) {
196
+ connectionInteraction = {
197
+ hash: hashIdString(`db-connection:${currentService.hash}:${observedDatabase.hash}:${parsed.scrubbed ?? ''}:${parsed.user ?? ''}`),
198
+ connection: parsed.scrubbed,
199
+ user: parsed.user
200
+ };
201
+ interactions.dbConnections.push(connectionInteraction);
202
+ const message = {
203
+ event: "new-interaction",
204
+ type: "db-connection",
205
+ hash: connectionInteraction.hash,
206
+ from: currentService.hash,
207
+ to: observedDatabase.hash,
208
+ ...(connectionInteraction.connection ? { connection: connectionInteraction.connection } : {}),
209
+ ...(connectionInteraction.user ? { user: connectionInteraction.user } : {})
210
+ };
211
+ this.ingestMessage(message);
212
+ }
213
+ // The query of the database from the service (only for SQL for now).
214
+ if (sqlDbSystems.has(system) && attrs['db.statement']) {
215
+ // The interaction, based upon the normalized query.
216
+ let queryInfo = (0, sql_analyzer_1.analyzeSQL)(attrs['db.statement']);
217
+ let queryInteraction = interactions.dbQueries.find(q => q.query === queryInfo.normalizedQuery);
218
+ if (!queryInteraction) {
219
+ queryInteraction = {
220
+ hash: hashIdString(`db-query:${currentService.hash}:${observedDatabase.hash}:${queryInfo.normalizedQuery}`),
221
+ query: queryInfo.normalizedQuery
222
+ };
223
+ interactions.dbQueries.push(queryInteraction);
224
+ const selects = getTablesWithOperation(queryInfo.tableOperations, 'SELECT');
225
+ const inserts = getTablesWithOperation(queryInfo.tableOperations, 'INSERT');
226
+ const updates = getTablesWithOperation(queryInfo.tableOperations, 'UPDATE');
227
+ const deletes = getTablesWithOperation(queryInfo.tableOperations, 'DELETE');
228
+ const message = {
229
+ event: "new-interaction",
230
+ type: "db-query",
231
+ hash: queryInteraction.hash,
232
+ from: currentService.hash,
233
+ to: observedDatabase.hash,
234
+ query: queryInfo.presentableQuery,
235
+ ...(selects ? { selects } : {}),
236
+ ...(inserts ? { inserts } : {}),
237
+ ...(updates ? { updates } : {}),
238
+ ...(deletes ? { deletes } : {}),
239
+ };
240
+ this.ingestMessage(message);
241
+ }
242
+ // Build and ingest observation.
243
+ const duration = span.duration;
244
+ const returnedRows = attrs['db.response.returned_rows']
245
+ ?? attrs['db.sql.rows'];
246
+ const { message: errorMessage, type: errorType, stack } = extractErrorInfo(span);
247
+ const observation = {
248
+ type: "db-query",
249
+ subject: queryInteraction.hash,
250
+ timestamp: span.startTime,
251
+ duration,
252
+ ...(errorMessage ? { errorMessage } : {}),
253
+ ...(errorType ? { errorType } : {}),
254
+ ...(stack ? { stack } : {}),
255
+ ...(returnedRows !== undefined ? { returnedRows } : {})
256
+ };
257
+ this.ingestMessage({
258
+ event: "observations",
259
+ seq: this.observationsSeq++,
260
+ observations: [observation]
261
+ });
262
+ }
263
+ }
264
+ processHttpRequest(currentService, url, span) {
265
+ // Build identity based upon protocol, host, and port.
266
+ const parsed = new URL(url);
267
+ const protocol = parsed.protocol.replace(':', ''); // Remove trailing colon
268
+ const host = parsed.hostname;
269
+ const port = parsed.port ? parseInt(parsed.port) : (protocol === 'https' ? 443 : 80);
270
+ const idString = `http-service:${protocol}:${host}:${port}`;
271
+ const hash = hashIdString(idString);
272
+ // Ingest it if it's not already observed.
273
+ let observedHttpService = this.observedHttpServices.find(s => s.protocol === protocol && s.host === host && s.port === port);
274
+ if (!observedHttpService) {
275
+ observedHttpService = { protocol, host, port, hash };
276
+ this.observedHttpServices.push(observedHttpService);
277
+ // The existence of the service.
278
+ const message = {
279
+ event: "new-entity",
280
+ type: "http-service",
281
+ hash,
282
+ protocol,
283
+ host,
284
+ port
285
+ };
286
+ this.ingestMessage(message);
287
+ }
288
+ // Ingest the interaction if first observed.
289
+ const interactions = this.getInteractions(currentService.hash, observedHttpService.hash);
290
+ if (!interactions.httpRequest) {
291
+ const idString = `http-request:${currentService.hash}:${observedHttpService.hash}`;
292
+ const hash = hashIdString(idString);
293
+ interactions.httpRequest = { hash };
294
+ const message = {
295
+ event: "new-interaction",
296
+ type: "http-request",
297
+ hash,
298
+ from: currentService.hash,
299
+ to: observedHttpService.hash
300
+ };
301
+ this.ingestMessage(message);
302
+ }
303
+ // Build and ingest observation.
304
+ const attrs = span.attributes;
305
+ const path = parsed.pathname || '/';
306
+ const method = span.attributes['http.method'];
307
+ if (!method) // Really should always be there
308
+ return;
309
+ const status = attrs['http.status_code'];
310
+ const duration = span.duration;
311
+ const httpVersion = span.attributes['http.flavor'];
312
+ const requestBytes = attrs['http.request_content_length'];
313
+ const responseBytes = attrs['http.response_content_length'];
314
+ const { message: errorMessage, type: errorType, stack } = extractErrorInfo(span);
315
+ const observation = {
316
+ type: "http-client",
317
+ subject: interactions.httpRequest.hash,
318
+ timestamp: span.startTime,
319
+ path,
320
+ method,
321
+ ...(status !== undefined ? { status } : {}),
322
+ duration,
323
+ ...(httpVersion !== undefined ? { httpVersion } : {}),
324
+ ...(requestBytes !== undefined ? { requestBytes } : {}),
325
+ ...(responseBytes !== undefined ? { responseBytes } : {}),
326
+ ...(errorMessage ? { errorMessage } : {}),
327
+ ...(errorType ? { errorType } : {}),
328
+ ...(stack ? { stack } : {})
329
+ };
330
+ this.ingestMessage({
331
+ event: "observations",
332
+ seq: this.observationsSeq++,
333
+ observations: [observation]
334
+ });
335
+ }
336
+ getInteractions(from, to) {
337
+ let fromMap = this.observedInteractions.get(from);
338
+ if (!fromMap) {
339
+ fromMap = new Map();
340
+ this.observedInteractions.set(from, fromMap);
341
+ }
342
+ let interactions = fromMap.get(to);
343
+ if (!interactions) {
344
+ interactions = { httpRequest: undefined, dbConnections: [], dbQueries: [] };
345
+ fromMap.set(to, interactions);
346
+ }
347
+ return interactions;
348
+ }
349
+ ingestMessage(message) {
350
+ this.connection.sendMessage(message);
351
+ }
352
+ async forceFlush() {
353
+ }
354
+ async shutdown() {
355
+ this.connection.close();
356
+ }
357
+ }
358
+ exports.ComprehendDevSpanProcessor = ComprehendDevSpanProcessor;
359
+ function hashIdString(idString) {
360
+ return Array.from((0, sha2_1.sha256)((0, utils_1.utf8ToBytes)(idString)))
361
+ .map(b => b.toString(16).padStart(2, '0'))
362
+ .join('');
363
+ }
364
+ /** Try to extract data from the database connection string. Many are URL based, fall back on
365
+ * some kind of comma-seperated style for the rest. */
366
+ function parseDatabaseConnectionStringRaw(conn) {
367
+ try {
368
+ // Try URL-style parsing first; scrub the user details if present.
369
+ const url = new URL(conn);
370
+ const user = url.username || undefined;
371
+ const host = url.hostname || undefined;
372
+ const port = url.port || undefined;
373
+ const dbName = url.pathname?.replace(/^\//, '') || undefined;
374
+ url.username = '';
375
+ url.password = '';
376
+ return {
377
+ scrubbed: url.toString(),
378
+ user,
379
+ host,
380
+ port,
381
+ name: dbName
382
+ };
383
+ }
384
+ catch {
385
+ // Not a URL-style string; try semi-structured parsing
386
+ const parts = conn.split(';');
387
+ const kv = {};
388
+ for (const part of parts) {
389
+ const [k, v] = part.split('=');
390
+ if (k && v)
391
+ kv[k.trim().toLowerCase()] = v.trim();
392
+ }
393
+ const user = kv['user id'] || kv['uid'];
394
+ const host = kv['server'] || kv['data source'] || kv['address'];
395
+ const port = kv['port'];
396
+ const name = kv['database'] || kv['initial catalog'];
397
+ // Reconstruct a scrubbed connection string without credentials
398
+ const scrubbed = Object.entries(kv)
399
+ .filter(([key]) => !['user id', 'uid', 'password', 'pwd'].includes(key))
400
+ .map(([k, v]) => `${k}=${v}`)
401
+ .join(';');
402
+ return {
403
+ scrubbed,
404
+ user,
405
+ host,
406
+ port,
407
+ name
408
+ };
409
+ }
410
+ }
411
+ /** Try pretty hard to get error info out of a span, if it has any. Handles it being there as an event,
412
+ * directly on the span with error semantics, and some other more ad-hoc cases. */
413
+ function extractErrorInfo(span) {
414
+ const attrs = span.attributes;
415
+ // Try to extract from a structured 'exception' event, as it should have more detail
416
+ const exceptionEvent = span.events.find(e => e.name === 'exception');
417
+ if (exceptionEvent?.attributes) {
418
+ const message = exceptionEvent.attributes['exception.message'];
419
+ const type = exceptionEvent.attributes['exception.type'];
420
+ const stack = exceptionEvent.attributes['exception.stacktrace'];
421
+ if (message || type || stack) {
422
+ return { message, type, stack };
423
+ }
424
+ }
425
+ // Fallback to attributes directly on the span.
426
+ const isError = span.status.code === 2;
427
+ const message = attrs['exception.message'] ??
428
+ attrs['http.error_message'] ??
429
+ attrs['db.response.status_code'] ??
430
+ (isError ? attrs['otel.status_description'] : undefined);
431
+ const type = attrs['exception.type'] ??
432
+ attrs['error.type'] ??
433
+ attrs['http.error_name'];
434
+ const stack = attrs['exception.stacktrace'];
435
+ return {
436
+ message,
437
+ type,
438
+ stack
439
+ };
440
+ }
441
+ function getTablesWithOperation(tableOps, operation) {
442
+ const op = operation.toUpperCase();
443
+ const result = Object.entries(tableOps)
444
+ .filter(([_, ops]) => ops.includes(op))
445
+ .map(([table]) => table);
446
+ return result.length > 0 ? result : undefined;
447
+ }
@@ -0,0 +1,22 @@
1
+ import { ObservationInputMessage } from './wire-protocol';
2
+ export declare class WebSocketConnection {
3
+ private readonly organization;
4
+ private readonly token;
5
+ private readonly logger?;
6
+ private readonly unacknowledgedObserved;
7
+ private readonly unacknowledgedObservations;
8
+ private socket;
9
+ private reconnectDelay;
10
+ private shouldReconnect;
11
+ private authorized;
12
+ constructor(organization: string, token: string, logger?: (message: string) => void);
13
+ private log;
14
+ private connect;
15
+ private onOpen;
16
+ private onMessage;
17
+ private onClose;
18
+ private onError;
19
+ private sendRaw;
20
+ sendMessage(message: ObservationInputMessage): void;
21
+ close(): void;
22
+ }
@@ -0,0 +1,102 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.WebSocketConnection = void 0;
7
+ const ws_1 = __importDefault(require("ws"));
8
+ const INGESTION_ENDPOINT = 'wss://ingestion.comprehend.dev';
9
+ class WebSocketConnection {
10
+ constructor(organization, token, logger) {
11
+ this.unacknowledgedObserved = new Map();
12
+ this.unacknowledgedObservations = new Map();
13
+ this.socket = null;
14
+ this.reconnectDelay = 1000;
15
+ this.shouldReconnect = true;
16
+ this.authorized = false;
17
+ this.organization = organization;
18
+ this.token = token;
19
+ this.logger = logger;
20
+ this.connect();
21
+ }
22
+ log(message) {
23
+ if (this.logger) {
24
+ this.logger(message);
25
+ }
26
+ }
27
+ connect() {
28
+ const webSocketUrl = `${INGESTION_ENDPOINT}/${this.organization}/observations`;
29
+ this.log(`Attempting to connect to ${webSocketUrl}...`);
30
+ this.socket = new ws_1.default(webSocketUrl);
31
+ this.socket.on('open', () => this.onOpen());
32
+ this.socket.on('message', (data) => this.onMessage(data));
33
+ this.socket.on('close', (code, reason) => this.onClose(code, reason.toString()));
34
+ this.socket.on('error', (err) => this.onError(err));
35
+ }
36
+ onOpen() {
37
+ this.log('WebSocket connected. Sending init/auth message.');
38
+ const init = {
39
+ event: 'init',
40
+ protocolVersion: 1,
41
+ token: this.token,
42
+ };
43
+ this.sendRaw(init);
44
+ }
45
+ onMessage(data) {
46
+ try {
47
+ const msg = JSON.parse(data.toString());
48
+ if (msg.type === 'ack-authorized') {
49
+ this.authorized = true;
50
+ this.log('Authorization acknowledged by server.');
51
+ for (const message of this.unacknowledgedObserved.values()) {
52
+ this.sendRaw(message);
53
+ }
54
+ for (const message of this.unacknowledgedObservations.values()) {
55
+ this.sendRaw(message);
56
+ }
57
+ }
58
+ else if (msg.type === 'ack-observed') {
59
+ this.unacknowledgedObserved.delete(msg.hash);
60
+ }
61
+ else if (msg.type === 'ack-observations') {
62
+ this.unacknowledgedObservations.delete(msg.seq);
63
+ }
64
+ }
65
+ catch (e) {
66
+ this.log('Error parsing message from server: ' + (e instanceof Error ? e.message : String(e)));
67
+ }
68
+ }
69
+ onClose(code, reason) {
70
+ this.log(`WebSocket disconnected. Code: ${code}, Reason: ${reason}`);
71
+ this.authorized = false;
72
+ if (this.shouldReconnect) {
73
+ setTimeout(() => this.connect(), this.reconnectDelay);
74
+ }
75
+ }
76
+ onError(error) {
77
+ this.log(`WebSocket encountered an error: ${error.message}`);
78
+ }
79
+ sendRaw(message) {
80
+ if (this.socket && this.socket.readyState === ws_1.default.OPEN) {
81
+ this.socket.send(JSON.stringify(message));
82
+ }
83
+ }
84
+ sendMessage(message) {
85
+ if (message.event === 'new-entity' || message.event === 'new-interaction') {
86
+ this.unacknowledgedObserved.set(message.hash, message);
87
+ }
88
+ else if (message.event === 'observations') {
89
+ this.unacknowledgedObservations.set(message.seq, message);
90
+ }
91
+ if (this.authorized) {
92
+ this.sendRaw(message);
93
+ }
94
+ }
95
+ close() {
96
+ this.shouldReconnect = false;
97
+ if (this.socket) {
98
+ this.socket.close();
99
+ }
100
+ }
101
+ }
102
+ exports.WebSocketConnection = WebSocketConnection;
@@ -0,0 +1 @@
1
+ export { ComprehendDevSpanProcessor } from "./ComprehendDevSpanProcessor";
package/dist/index.js ADDED
@@ -0,0 +1,5 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ComprehendDevSpanProcessor = void 0;
4
+ var ComprehendDevSpanProcessor_1 = require("./ComprehendDevSpanProcessor");
5
+ Object.defineProperty(exports, "ComprehendDevSpanProcessor", { enumerable: true, get: function () { return ComprehendDevSpanProcessor_1.ComprehendDevSpanProcessor; } });
@@ -0,0 +1,22 @@
1
+ type Token = {
2
+ type: 'keyword' | 'identifier' | 'id-quote' | 'string' | 'comment' | 'punct' | 'operator' | 'whitespace' | 'unknown';
3
+ value: string;
4
+ };
5
+ export interface SQLAnalysisResult {
6
+ tableOperations: Record<string, string[]>;
7
+ normalizedQuery: string;
8
+ presentableQuery: string;
9
+ }
10
+ /** Performs a rough tokenization of the SQL, extracts the tables involved and the operations on them, and
11
+ * produces two versions of the query:
12
+ * - A normalized version for hashing purposes that does not account for whitespace, comments, and collapses
13
+ * IN clauses that might cause a cardinality explosion.
14
+ * - A presentable version that only does the IN clause collapsing */
15
+ export declare function analyzeSQL(sql: string): SQLAnalysisResult;
16
+ export declare function analyzeSQLTokens(tokens: Token[]): {
17
+ tableOperations: {
18
+ [k: string]: string[];
19
+ };
20
+ normalizedQuery: string;
21
+ };
22
+ export {};