@comprehend/telemetry-node 0.1.4 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,33 +1,95 @@
1
1
  import WebSocket from 'ws';
2
+ import { randomUUID } from 'crypto';
2
3
  import {
4
+ AttributeType,
5
+ CustomMetricSpecification,
3
6
  InitMessage,
4
7
  NewObservedEntityMessage,
5
8
  NewObservedInteractionMessage,
6
9
  ObservationInputMessage,
7
- ObservationMessage,
8
10
  ObservationOutputMessage,
11
+ StartProcessContextMessage,
12
+ InitAck,
9
13
  } from './wire-protocol';
14
+ import { hrTimeNow } from './util';
10
15
 
11
- const INGESTION_ENDPOINT = 'wss://ingestion.comprehend.dev';
16
+ const INGESTION_ENDPOINT = process.env.COMPREHEND_INGESTION_ENDPOINT || 'wss://ingestion.comprehend.dev';
17
+
18
+ /** A queue of unacknowledged messages keyed by sequence number, with an associated ack type. */
19
+ class SeqQueue<T extends { seq: number }> {
20
+ readonly ackType: string;
21
+ private readonly pending = new Map<number, T>();
22
+
23
+ constructor(ackType: string) {
24
+ this.ackType = ackType;
25
+ }
26
+
27
+ add(message: T): void {
28
+ this.pending.set(message.seq, message);
29
+ }
30
+
31
+ ack(seq: number): void {
32
+ this.pending.delete(seq);
33
+ }
34
+
35
+ values(): IterableIterator<T> {
36
+ return this.pending.values();
37
+ }
38
+ }
12
39
 
13
40
  export class WebSocketConnection {
14
41
  private readonly organization: string;
15
42
  private readonly token: string;
16
43
  private readonly logger?: (message: string) => void;
44
+ private readonly onAuthorized?: (ack: InitAck) => void;
45
+ private readonly onCustomMetricChange?: (specs: CustomMetricSpecification[]) => void;
17
46
  private readonly unacknowledgedObserved = new Map<string, NewObservedEntityMessage | NewObservedInteractionMessage>();
18
- private readonly unacknowledgedObservations = new Map<number, ObservationMessage>();
47
+
48
+ private readonly seqQueues: Record<string, SeqQueue<any>>;
49
+ private readonly observationsQueue = new SeqQueue<any>('ack-observations');
50
+ private readonly timeseriesQueue = new SeqQueue<any>('ack-timeseries');
51
+ private readonly cumulativeQueue = new SeqQueue<any>('ack-cumulative');
52
+ private readonly traceSpansQueue = new SeqQueue<any>('ack-tracespans');
53
+ private readonly dbQueryQueue = new SeqQueue<any>('ack-db-query');
54
+ private readonly contextQueue = new SeqQueue<StartProcessContextMessage>('ack-context');
55
+
19
56
  private socket: WebSocket | null = null;
20
57
  private reconnectDelay = 1000;
21
58
  private shouldReconnect = true;
22
59
  private authorized = false;
23
60
 
24
- constructor(organization: string, token: string, logger?: (message: string) => void) {
25
- this.organization = organization;
26
- this.token = token;
27
- this.logger = logger;
61
+ private processContext: { serviceEntityHash: string, resources: Record<string, AttributeType> } | null = null;
62
+ private ingestionId: string = randomUUID();
63
+
64
+ private _seq = 1;
65
+
66
+ constructor(options: {
67
+ organization: string,
68
+ token: string,
69
+ logger?: (message: string) => void,
70
+ onAuthorized?: (ack: InitAck) => void,
71
+ onCustomMetricChange?: (specs: CustomMetricSpecification[]) => void,
72
+ }) {
73
+ this.organization = options.organization;
74
+ this.token = options.token;
75
+ this.logger = options.logger;
76
+ this.onAuthorized = options.onAuthorized;
77
+ this.onCustomMetricChange = options.onCustomMetricChange;
78
+
79
+ // Build a lookup from ack type to queue for dispatch in onMessage
80
+ this.seqQueues = {};
81
+ for (const q of [this.observationsQueue, this.timeseriesQueue, this.cumulativeQueue,
82
+ this.traceSpansQueue, this.dbQueryQueue, this.contextQueue]) {
83
+ this.seqQueues[q.ackType] = q;
84
+ }
85
+
28
86
  this.connect();
29
87
  }
30
88
 
89
+ public nextSeq(): number {
90
+ return this._seq++;
91
+ }
92
+
31
93
  private log(message: string) {
32
94
  if (this.logger) {
33
95
  this.logger(message);
@@ -47,9 +109,10 @@ export class WebSocketConnection {
47
109
 
48
110
  private onOpen(): void {
49
111
  this.log('WebSocket connected. Sending init/auth message.');
112
+ this.ingestionId = randomUUID();
50
113
  const init: InitMessage = {
51
114
  event: 'init',
52
- protocolVersion: 1,
115
+ protocolVersion: 2,
53
116
  token: this.token,
54
117
  };
55
118
  this.sendRaw(init);
@@ -63,18 +126,42 @@ export class WebSocketConnection {
63
126
  this.authorized = true;
64
127
  this.log('Authorization acknowledged by server.');
65
128
 
129
+ if (this.onAuthorized) {
130
+ this.onAuthorized(msg);
131
+ }
132
+
133
+ // Send context first if we have one
134
+ if (this.processContext) {
135
+ this.sendContextStart();
136
+ }
137
+
138
+ // Replay entities and interactions
66
139
  for (const message of this.unacknowledgedObserved.values()) {
67
140
  this.sendRaw(message);
68
141
  }
69
- for (const message of this.unacknowledgedObservations.values()) {
70
- this.sendRaw(message);
142
+
143
+ // Replay all seq-based queues
144
+ for (const q of Object.values(this.seqQueues)) {
145
+ if (q === this.contextQueue) continue; // already sent above
146
+ for (const message of q.values()) {
147
+ this.sendRaw(message);
148
+ }
71
149
  }
72
150
  }
73
151
  else if (msg.type === 'ack-observed') {
74
152
  this.unacknowledgedObserved.delete(msg.hash);
75
153
  }
76
- else if (msg.type === 'ack-observations') {
77
- this.unacknowledgedObservations.delete(msg.seq);
154
+ else if (msg.type === 'custom-metric-change') {
155
+ if (this.onCustomMetricChange) {
156
+ this.onCustomMetricChange(msg.customMetrics);
157
+ }
158
+ }
159
+ else if ('seq' in msg) {
160
+ // Dispatch seq-based acks to the appropriate queue
161
+ const queue = this.seqQueues[msg.type];
162
+ if (queue) {
163
+ queue.ack(msg.seq);
164
+ }
78
165
  }
79
166
  } catch (e) {
80
167
  this.log('Error parsing message from server: ' + (e instanceof Error ? e.message : String(e)));
@@ -99,12 +186,47 @@ export class WebSocketConnection {
99
186
  }
100
187
  }
101
188
 
189
+ private sendContextStart(): void {
190
+ if (!this.processContext) return;
191
+ const seq = this.nextSeq();
192
+ const contextMsg: StartProcessContextMessage = {
193
+ event: 'context-start',
194
+ seq,
195
+ timestamp: hrTimeNow(),
196
+ ingestionId: this.ingestionId,
197
+ type: 'process',
198
+ serviceEntityHash: this.processContext.serviceEntityHash,
199
+ resources: this.processContext.resources,
200
+ };
201
+ this.contextQueue.add(contextMsg);
202
+ this.sendRaw(contextMsg);
203
+ }
204
+
205
+ public setProcessContext(serviceEntityHash: string, resources: Record<string, AttributeType>): void {
206
+ this.processContext = { serviceEntityHash, resources };
207
+ if (this.authorized) {
208
+ this.sendContextStart();
209
+ }
210
+ }
211
+
102
212
  public sendMessage(message: ObservationInputMessage): void {
103
213
  if (message.event === 'new-entity' || message.event === 'new-interaction') {
104
214
  this.unacknowledgedObserved.set(message.hash, message);
105
215
  }
106
216
  else if (message.event === 'observations') {
107
- this.unacknowledgedObservations.set(message.seq, message);
217
+ this.observationsQueue.add(message);
218
+ }
219
+ else if (message.event === 'timeseries') {
220
+ this.timeseriesQueue.add(message);
221
+ }
222
+ else if (message.event === 'cumulative') {
223
+ this.cumulativeQueue.add(message);
224
+ }
225
+ else if (message.event === 'tracespans') {
226
+ this.traceSpansQueue.add(message);
227
+ }
228
+ else if (message.event === 'db-query') {
229
+ this.dbQueryQueue.add(message);
108
230
  }
109
231
 
110
232
  if (this.authorized) {
package/src/index.ts CHANGED
@@ -1,2 +1,3 @@
1
- export {ComprehendDevSpanProcessor} from "./ComprehendDevSpanProcessor";
2
-
1
+ export { ComprehendSDK } from './ComprehendSDK';
2
+ export { ComprehendDevSpanProcessor } from './ComprehendDevSpanProcessor';
3
+ export { ComprehendMetricsExporter } from './ComprehendMetricsExporter';
package/src/util.ts ADDED
@@ -0,0 +1,6 @@
1
+ import { HrTime } from './wire-protocol';
2
+
3
+ export function hrTimeNow(): HrTime {
4
+ const now = Date.now();
5
+ return [Math.floor(now / 1000), (now % 1000) * 1_000_000];
6
+ }
@@ -1,20 +1,61 @@
1
- import {HrTime} from "@opentelemetry/api";
1
+ export type HrTime = [number, number]; // [seconds, nanoseconds]
2
2
 
3
- export type ObservationInputMessage = InitMessage | NewObservedEntityMessage | NewObservedInteractionMessage | ObservationMessage;
4
- export type ObservationOutputMessage = InitAck | ObservedAck | ObservationsAck;
3
+ export type ObservationInputMessage = InitMessage | StartProcessContextMessage | EndContextMessage
4
+ | NewObservedEntityMessage | NewObservedInteractionMessage
5
+ | ObservationMessage | TimeSeriesMetricsMessage | CumulativeMetricsMessage
6
+ | TraceSpansMessage | DatabaseQueryMessage;
7
+ export type ObservationOutputMessage = InitAck | ContextAck | CustomMetricChange | ObservedAck | ObservationsAck
8
+ | TimeSeriesMetricsAck | CumulativeMetricsAck | TraceSpansAck
9
+ | DatabaseQueryAck;
5
10
 
6
11
 
12
+ // Initialization
13
+
7
14
  export interface InitMessage {
8
15
  event: "init";
9
- protocolVersion: 1;
16
+ protocolVersion: 2;
10
17
  token: string;
11
18
  }
12
19
 
20
+ export interface InitAck {
21
+ type: "ack-authorized";
22
+ customMetrics: CustomMetricSpecification[];
23
+ }
24
+
25
+
26
+ // Contexts
27
+
28
+ export interface StartProcessContextMessage {
29
+ event: "context-start";
30
+ seq: number;
31
+ timestamp: HrTime;
32
+ ingestionId: string;
33
+ type: "process";
34
+ serviceEntityHash: string;
35
+ resources: {
36
+ [key: string]: AttributeType
37
+ };
38
+ }
39
+
40
+ export interface EndContextMessage {
41
+ event: "context-end";
42
+ seq: number;
43
+ timestamp: HrTime;
44
+ ingestionId: string;
45
+ }
46
+
47
+ export interface ContextAck {
48
+ type: "ack-context";
49
+ seq: number;
50
+ }
51
+
52
+
53
+ // Observed entities
13
54
 
14
55
  export interface NewObservedEntityMessage {
15
56
  event: "new-entity";
16
- type: string;
17
57
  hash: string;
58
+ type: string;
18
59
  }
19
60
 
20
61
  export interface NewObservedServiceMessage extends NewObservedEntityMessage {
@@ -46,13 +87,20 @@ export interface NewObservedHttpServiceMessage extends NewObservedEntityMessage
46
87
  port: number;
47
88
  }
48
89
 
90
+ export interface ObservedAck {
91
+ type: "ack-observed";
92
+ hash: string;
93
+ }
94
+
95
+
96
+ // Observed interactions
49
97
 
50
98
  export interface NewObservedInteractionMessage {
51
99
  event: "new-interaction";
52
- type: string;
53
100
  hash: string;
54
101
  from: string;
55
102
  to: string;
103
+ type: string;
56
104
  }
57
105
 
58
106
  export interface NewObservedHttpRequestMessage extends NewObservedInteractionMessage {
@@ -65,15 +113,8 @@ export interface NewObservedDatabaseConnectionMessage extends NewObservedInterac
65
113
  user?: string;
66
114
  }
67
115
 
68
- export interface NewObservedDatabaseQueryMessage extends NewObservedInteractionMessage {
69
- type: "db-query";
70
- query: string;
71
- selects?: string[];
72
- inserts?: string[];
73
- updates?: string[];
74
- deletes?: string[];
75
- }
76
116
 
117
+ // Observations
77
118
 
78
119
  export interface ObservationMessage {
79
120
  event: "observations";
@@ -81,16 +122,20 @@ export interface ObservationMessage {
81
122
  observations: Array<Observation>;
82
123
  }
83
124
 
84
- export interface Observation {
85
- type: string;
86
- subject: string; // Hash of the entity or interaction the observation relates to
125
+ export type Observation = HttpClientObservation | HttpServerObservation | CustomObservation;
126
+
127
+ interface BaseObservation {
128
+ subject: string;
129
+ spanId: string;
130
+ traceId: string;
87
131
  timestamp: HrTime;
88
132
  errorMessage?: string;
89
133
  errorType?: string;
90
134
  stack?: string;
135
+ type: string;
91
136
  }
92
137
 
93
- export interface HttpClientObservation extends Observation {
138
+ export interface HttpClientObservation extends BaseObservation {
94
139
  type: "http-client";
95
140
  path: string;
96
141
  method: string;
@@ -101,7 +146,7 @@ export interface HttpClientObservation extends Observation {
101
146
  responseBytes?: number;
102
147
  }
103
148
 
104
- export interface HttpServerObservation extends Observation {
149
+ export interface HttpServerObservation extends BaseObservation {
105
150
  type: "http-server";
106
151
  path: string;
107
152
  status: number;
@@ -112,23 +157,153 @@ export interface HttpServerObservation extends Observation {
112
157
  userAgent?: string;
113
158
  }
114
159
 
115
- export interface DatabaseQueryObservation extends Observation {
116
- type: "db-query";
160
+ export type AttributeType = string | number | boolean;
161
+
162
+ export interface CustomObservation extends BaseObservation {
163
+ type: "custom";
164
+ id: string;
165
+ attributes: {
166
+ [key: string]: AttributeType
167
+ };
168
+ }
169
+
170
+ export interface ObservationsAck {
171
+ type: "ack-observations";
172
+ seq: number;
173
+ }
174
+
175
+
176
+ // Time series metrics
177
+
178
+ export interface TimeSeriesMetricsMessage {
179
+ event: "timeseries";
180
+ seq: number;
181
+ data: TimeSeriesDataPoint[];
182
+ }
183
+
184
+ export interface TimeSeriesDataPoint {
185
+ subject: string;
186
+ type: string;
187
+ timestamp: HrTime;
188
+ value: number;
189
+ unit: string;
190
+ attributes: {
191
+ [key: string]: AttributeType
192
+ };
193
+ }
194
+
195
+ export interface TimeSeriesMetricsAck {
196
+ type: "ack-timeseries";
197
+ seq: number;
198
+ }
199
+
200
+
201
+ // Cumulative metrics
202
+
203
+ export interface CumulativeMetricsMessage {
204
+ event: "cumulative";
205
+ seq: number;
206
+ data: CumulativeDataPoint[];
207
+ }
208
+
209
+ export interface CumulativeDataPoint {
210
+ subject: string;
211
+ type: string;
212
+ timestamp: HrTime;
213
+ value: number;
214
+ unit: string;
215
+ attributes: {
216
+ [key: string]: AttributeType
217
+ };
218
+ }
219
+
220
+ export interface CumulativeMetricsAck {
221
+ type: "ack-cumulative";
222
+ seq: number;
223
+ }
224
+
225
+
226
+ // Trace spans
227
+
228
+ export interface TraceSpansMessage {
229
+ event: "tracespans";
230
+ seq: number;
231
+ data: TraceSpan[];
232
+ }
233
+
234
+ export interface TraceSpan {
235
+ trace: string;
236
+ span: string;
237
+ parent: string;
238
+ name: string;
239
+ timestamp: HrTime;
240
+ }
241
+
242
+ export interface TraceSpansAck {
243
+ type: "ack-tracespans";
244
+ seq: number;
245
+ }
246
+
247
+
248
+ // Database query
249
+
250
+ export interface DatabaseQueryMessage {
251
+ event: "db-query";
252
+ seq: number;
253
+ query: string;
254
+ from: string;
255
+ to: string;
256
+ timestamp: HrTime;
117
257
  duration: HrTime;
258
+ traceId?: string;
259
+ spanId?: string;
260
+ errorMessage?: string;
261
+ errorType?: string;
262
+ stack?: string;
118
263
  returnedRows?: number;
119
264
  }
120
265
 
266
+ export interface DatabaseQueryAck {
267
+ type: "ack-db-query";
268
+ seq: number;
269
+ }
121
270
 
122
- export interface InitAck {
123
- type: "ack-authorized";
271
+
272
+ // Custom metrics
273
+
274
+ export interface CustomMetricChange {
275
+ type: "custom-metric-change";
276
+ customMetrics: CustomMetricSpecification[];
124
277
  }
125
278
 
126
- export interface ObservedAck {
127
- type: "ack-observed";
128
- hash: string;
279
+ export type CustomMetricSpecification = CustomCumulativeMetricSpecification | CustomTimeSeriesMetricSpecification |
280
+ CustomSpanObservationSpecification;
281
+
282
+ export interface CustomCumulativeMetricSpecification {
283
+ type: "cumulative";
284
+ id: string;
285
+ attributes: string[];
286
+ subject: string;
129
287
  }
130
288
 
131
- export interface ObservationsAck {
132
- type: "ack-observations";
133
- seq: number;
289
+ export interface CustomTimeSeriesMetricSpecification {
290
+ type: "timeseries";
291
+ id: string;
292
+ attributes: string[];
293
+ subject: string;
134
294
  }
295
+
296
+ export interface CustomSpanObservationSpecification {
297
+ type: "span";
298
+ rule: SpanMatcherRule;
299
+ subject: string;
300
+ }
301
+
302
+ export type SpanMatcherRule =
303
+ | { kind: "type", value: "client" | "server" | "internal" }
304
+ | { kind: "attribute-present", key: string }
305
+ | { kind: "attribute-absent", key: string }
306
+ | { kind: "attribute-equals", key: string, value: string }
307
+ | { kind: "attribute-not-equals", key: string, value: string }
308
+ | { kind: "all", rules: SpanMatcherRule[] }
309
+ | { kind: "any", rules: SpanMatcherRule[] };