@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
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
import { ExportResultCode } from '@opentelemetry/core';
|
|
2
|
+
import {
|
|
3
|
+
AggregationTemporality,
|
|
4
|
+
DataPointType,
|
|
5
|
+
InstrumentType,
|
|
6
|
+
ResourceMetrics,
|
|
7
|
+
} from '@opentelemetry/sdk-metrics';
|
|
8
|
+
import { ComprehendMetricsExporter } from './ComprehendMetricsExporter';
|
|
9
|
+
import { WebSocketConnection } from './WebSocketConnection';
|
|
10
|
+
import {
|
|
11
|
+
CumulativeMetricsMessage,
|
|
12
|
+
TimeSeriesMetricsMessage,
|
|
13
|
+
CustomMetricSpecification,
|
|
14
|
+
} from './wire-protocol';
|
|
15
|
+
|
|
16
|
+
jest.mock('./WebSocketConnection');
|
|
17
|
+
|
|
18
|
+
describe('ComprehendMetricsExporter', () => {
|
|
19
|
+
let exporter: ComprehendMetricsExporter;
|
|
20
|
+
let mockConnection: jest.Mocked<WebSocketConnection>;
|
|
21
|
+
let sentMessages: any[];
|
|
22
|
+
let seqCounter: number;
|
|
23
|
+
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
sentMessages = [];
|
|
26
|
+
seqCounter = 1;
|
|
27
|
+
|
|
28
|
+
mockConnection = {
|
|
29
|
+
sendMessage: jest.fn((message: any) => {
|
|
30
|
+
sentMessages.push(message);
|
|
31
|
+
}),
|
|
32
|
+
nextSeq: jest.fn(() => seqCounter++),
|
|
33
|
+
close: jest.fn(),
|
|
34
|
+
setProcessContext: jest.fn(),
|
|
35
|
+
} as any;
|
|
36
|
+
|
|
37
|
+
exporter = new ComprehendMetricsExporter(mockConnection);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
function createResourceMetrics(metrics: Array<{
|
|
41
|
+
name: string;
|
|
42
|
+
unit?: string;
|
|
43
|
+
dataPointType: DataPointType;
|
|
44
|
+
dataPoints: Array<{
|
|
45
|
+
startTime?: [number, number];
|
|
46
|
+
endTime: [number, number];
|
|
47
|
+
attributes: Record<string, any>;
|
|
48
|
+
value: any;
|
|
49
|
+
}>;
|
|
50
|
+
isMonotonic?: boolean;
|
|
51
|
+
}>, resourceAttributes: Record<string, any> = { 'service.name': 'test-service' }): ResourceMetrics {
|
|
52
|
+
return ({
|
|
53
|
+
resource: { attributes: resourceAttributes } as any,
|
|
54
|
+
scopeMetrics: [{
|
|
55
|
+
scope: { name: 'test-scope' } as any,
|
|
56
|
+
metrics: metrics.map(m => ({
|
|
57
|
+
descriptor: {
|
|
58
|
+
name: m.name,
|
|
59
|
+
description: '',
|
|
60
|
+
unit: m.unit ?? '',
|
|
61
|
+
valueType: 0,
|
|
62
|
+
},
|
|
63
|
+
dataPointType: m.dataPointType,
|
|
64
|
+
dataPoints: m.dataPoints.map(dp => ({
|
|
65
|
+
startTime: dp.startTime ?? [0, 0],
|
|
66
|
+
endTime: dp.endTime,
|
|
67
|
+
attributes: dp.attributes,
|
|
68
|
+
value: dp.value,
|
|
69
|
+
})),
|
|
70
|
+
...(m.isMonotonic !== undefined ? { isMonotonic: m.isMonotonic } : {}),
|
|
71
|
+
aggregationTemporality: AggregationTemporality.CUMULATIVE,
|
|
72
|
+
})),
|
|
73
|
+
}],
|
|
74
|
+
}) as ResourceMetrics;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
describe('Standard Gauge Metrics', () => {
|
|
78
|
+
it('should forward known gauge metrics as timeseries', () => {
|
|
79
|
+
const rm = createResourceMetrics([{
|
|
80
|
+
name: 'process.runtime.nodejs.memory.heap.used',
|
|
81
|
+
unit: 'By',
|
|
82
|
+
dataPointType: DataPointType.GAUGE,
|
|
83
|
+
dataPoints: [{
|
|
84
|
+
endTime: [1700000000, 0],
|
|
85
|
+
attributes: {},
|
|
86
|
+
value: 52428800,
|
|
87
|
+
}],
|
|
88
|
+
}]);
|
|
89
|
+
|
|
90
|
+
const callback = jest.fn();
|
|
91
|
+
exporter.export(rm, callback);
|
|
92
|
+
|
|
93
|
+
expect(callback).toHaveBeenCalledWith({ code: ExportResultCode.SUCCESS });
|
|
94
|
+
|
|
95
|
+
const tsMsg = sentMessages.find(m => m.event === 'timeseries') as TimeSeriesMetricsMessage;
|
|
96
|
+
expect(tsMsg).toBeDefined();
|
|
97
|
+
expect(tsMsg.data).toHaveLength(1);
|
|
98
|
+
expect(tsMsg.data[0]).toMatchObject({
|
|
99
|
+
type: 'process.runtime.nodejs.memory.heap.used',
|
|
100
|
+
value: 52428800,
|
|
101
|
+
unit: 'By',
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('should forward multiple known gauge metrics', () => {
|
|
106
|
+
const rm = createResourceMetrics([
|
|
107
|
+
{
|
|
108
|
+
name: 'process.runtime.nodejs.memory.heap.total',
|
|
109
|
+
unit: 'By',
|
|
110
|
+
dataPointType: DataPointType.GAUGE,
|
|
111
|
+
dataPoints: [{ endTime: [1700000000, 0], attributes: {}, value: 104857600 }],
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
name: 'nodejs.eventloop.delay.mean',
|
|
115
|
+
unit: 'ms',
|
|
116
|
+
dataPointType: DataPointType.GAUGE,
|
|
117
|
+
dataPoints: [{ endTime: [1700000000, 0], attributes: {}, value: 1.5 }],
|
|
118
|
+
},
|
|
119
|
+
]);
|
|
120
|
+
|
|
121
|
+
const callback = jest.fn();
|
|
122
|
+
exporter.export(rm, callback);
|
|
123
|
+
|
|
124
|
+
const tsMessages = sentMessages.filter(m => m.event === 'timeseries');
|
|
125
|
+
expect(tsMessages).toHaveLength(2);
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
describe('Unknown Metrics', () => {
|
|
130
|
+
it('should ignore unknown metric names', () => {
|
|
131
|
+
const rm = createResourceMetrics([{
|
|
132
|
+
name: 'some.unknown.metric',
|
|
133
|
+
dataPointType: DataPointType.GAUGE,
|
|
134
|
+
dataPoints: [{ endTime: [0, 0], attributes: {}, value: 42 }],
|
|
135
|
+
}]);
|
|
136
|
+
|
|
137
|
+
const callback = jest.fn();
|
|
138
|
+
exporter.export(rm, callback);
|
|
139
|
+
|
|
140
|
+
expect(sentMessages).toHaveLength(0);
|
|
141
|
+
expect(callback).toHaveBeenCalledWith({ code: ExportResultCode.SUCCESS });
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
describe('Histogram Metrics', () => {
|
|
146
|
+
it('should skip histogram data point types', () => {
|
|
147
|
+
const rm = createResourceMetrics([{
|
|
148
|
+
name: 'process.runtime.nodejs.memory.heap.used',
|
|
149
|
+
dataPointType: DataPointType.HISTOGRAM,
|
|
150
|
+
dataPoints: [{ endTime: [0, 0], attributes: {}, value: { buckets: {} } }],
|
|
151
|
+
}]);
|
|
152
|
+
|
|
153
|
+
const callback = jest.fn();
|
|
154
|
+
exporter.export(rm, callback);
|
|
155
|
+
|
|
156
|
+
expect(sentMessages).toHaveLength(0);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('should skip exponential histogram data point types', () => {
|
|
160
|
+
const rm = createResourceMetrics([{
|
|
161
|
+
name: 'process.runtime.nodejs.memory.heap.used',
|
|
162
|
+
dataPointType: DataPointType.EXPONENTIAL_HISTOGRAM,
|
|
163
|
+
dataPoints: [{ endTime: [0, 0], attributes: {}, value: {} }],
|
|
164
|
+
}]);
|
|
165
|
+
|
|
166
|
+
const callback = jest.fn();
|
|
167
|
+
exporter.export(rm, callback);
|
|
168
|
+
|
|
169
|
+
expect(sentMessages).toHaveLength(0);
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
describe('Custom Metrics', () => {
|
|
174
|
+
it('should forward custom cumulative metrics', () => {
|
|
175
|
+
exporter.updateCustomMetrics([
|
|
176
|
+
{ type: 'cumulative', id: 'app.requests.total', attributes: ['method', 'status'], subject: 'custom-sub' },
|
|
177
|
+
]);
|
|
178
|
+
|
|
179
|
+
const rm = createResourceMetrics([{
|
|
180
|
+
name: 'app.requests.total',
|
|
181
|
+
unit: '1',
|
|
182
|
+
dataPointType: DataPointType.SUM,
|
|
183
|
+
dataPoints: [{
|
|
184
|
+
endTime: [1700000000, 0],
|
|
185
|
+
attributes: { method: 'GET', status: '200', ignored: 'yes' },
|
|
186
|
+
value: 150,
|
|
187
|
+
}],
|
|
188
|
+
}]);
|
|
189
|
+
|
|
190
|
+
const callback = jest.fn();
|
|
191
|
+
exporter.export(rm, callback);
|
|
192
|
+
|
|
193
|
+
const cumMsg = sentMessages.find(m => m.event === 'cumulative') as CumulativeMetricsMessage;
|
|
194
|
+
expect(cumMsg).toBeDefined();
|
|
195
|
+
expect(cumMsg.data[0]).toMatchObject({
|
|
196
|
+
subject: 'custom-sub',
|
|
197
|
+
type: 'app.requests.total',
|
|
198
|
+
value: 150,
|
|
199
|
+
});
|
|
200
|
+
// Should only include filtered attributes
|
|
201
|
+
expect(cumMsg.data[0].attributes).toEqual({ method: 'GET', status: '200' });
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it('should forward custom timeseries metrics', () => {
|
|
205
|
+
exporter.updateCustomMetrics([
|
|
206
|
+
{ type: 'timeseries', id: 'app.cpu.usage', attributes: ['core'], subject: 'cpu-sub' },
|
|
207
|
+
]);
|
|
208
|
+
|
|
209
|
+
const rm = createResourceMetrics([{
|
|
210
|
+
name: 'app.cpu.usage',
|
|
211
|
+
unit: '%',
|
|
212
|
+
dataPointType: DataPointType.GAUGE,
|
|
213
|
+
dataPoints: [{
|
|
214
|
+
endTime: [1700000000, 0],
|
|
215
|
+
attributes: { core: '0' },
|
|
216
|
+
value: 45.2,
|
|
217
|
+
}],
|
|
218
|
+
}]);
|
|
219
|
+
|
|
220
|
+
const callback = jest.fn();
|
|
221
|
+
exporter.export(rm, callback);
|
|
222
|
+
|
|
223
|
+
const tsMsg = sentMessages.find(m => m.event === 'timeseries') as TimeSeriesMetricsMessage;
|
|
224
|
+
expect(tsMsg).toBeDefined();
|
|
225
|
+
expect(tsMsg.data[0]).toMatchObject({
|
|
226
|
+
subject: 'cpu-sub',
|
|
227
|
+
type: 'app.cpu.usage',
|
|
228
|
+
value: 45.2,
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it('should update custom metric specs when updateCustomMetrics is called', () => {
|
|
233
|
+
exporter.updateCustomMetrics([
|
|
234
|
+
{ type: 'cumulative', id: 'old.metric', attributes: [], subject: 's1' },
|
|
235
|
+
]);
|
|
236
|
+
|
|
237
|
+
// Replace with new specs
|
|
238
|
+
exporter.updateCustomMetrics([
|
|
239
|
+
{ type: 'timeseries', id: 'new.metric', attributes: [], subject: 's2' },
|
|
240
|
+
]);
|
|
241
|
+
|
|
242
|
+
const rmOld = createResourceMetrics([{
|
|
243
|
+
name: 'old.metric',
|
|
244
|
+
dataPointType: DataPointType.SUM,
|
|
245
|
+
dataPoints: [{ endTime: [0, 0], attributes: {}, value: 1 }],
|
|
246
|
+
}]);
|
|
247
|
+
const rmNew = createResourceMetrics([{
|
|
248
|
+
name: 'new.metric',
|
|
249
|
+
dataPointType: DataPointType.GAUGE,
|
|
250
|
+
dataPoints: [{ endTime: [0, 0], attributes: {}, value: 2 }],
|
|
251
|
+
}]);
|
|
252
|
+
|
|
253
|
+
const callback = jest.fn();
|
|
254
|
+
exporter.export(rmOld, callback);
|
|
255
|
+
expect(sentMessages).toHaveLength(0); // old spec no longer active
|
|
256
|
+
|
|
257
|
+
exporter.export(rmNew, callback);
|
|
258
|
+
expect(sentMessages).toHaveLength(1);
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
describe('Service Subject Hashing', () => {
|
|
263
|
+
it('should derive service subject from resource attributes', () => {
|
|
264
|
+
const rm = createResourceMetrics([{
|
|
265
|
+
name: 'process.runtime.nodejs.memory.heap.used',
|
|
266
|
+
unit: 'By',
|
|
267
|
+
dataPointType: DataPointType.GAUGE,
|
|
268
|
+
dataPoints: [{ endTime: [0, 0], attributes: {}, value: 100 }],
|
|
269
|
+
}], {
|
|
270
|
+
'service.name': 'my-svc',
|
|
271
|
+
'service.namespace': 'ns',
|
|
272
|
+
'deployment.environment': 'prod',
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
const callback = jest.fn();
|
|
276
|
+
exporter.export(rm, callback);
|
|
277
|
+
|
|
278
|
+
const tsMsg = sentMessages.find(m => m.event === 'timeseries');
|
|
279
|
+
expect(tsMsg.data[0].subject).toBeDefined();
|
|
280
|
+
expect(typeof tsMsg.data[0].subject).toBe('string');
|
|
281
|
+
expect(tsMsg.data[0].subject.length).toBe(64); // SHA256 hex
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it('should skip known metrics when no service.name in resource', () => {
|
|
285
|
+
const rm = createResourceMetrics([{
|
|
286
|
+
name: 'process.runtime.nodejs.memory.heap.used',
|
|
287
|
+
dataPointType: DataPointType.GAUGE,
|
|
288
|
+
dataPoints: [{ endTime: [0, 0], attributes: {}, value: 100 }],
|
|
289
|
+
}], {}); // No service.name
|
|
290
|
+
|
|
291
|
+
const callback = jest.fn();
|
|
292
|
+
exporter.export(rm, callback);
|
|
293
|
+
|
|
294
|
+
expect(sentMessages).toHaveLength(0);
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
describe('Aggregation Temporality', () => {
|
|
299
|
+
it('should return DELTA for COUNTER', () => {
|
|
300
|
+
expect(exporter.selectAggregationTemporality(InstrumentType.COUNTER))
|
|
301
|
+
.toBe(AggregationTemporality.DELTA);
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
it('should return DELTA for HISTOGRAM', () => {
|
|
305
|
+
expect(exporter.selectAggregationTemporality(InstrumentType.HISTOGRAM))
|
|
306
|
+
.toBe(AggregationTemporality.DELTA);
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
it('should return CUMULATIVE for OBSERVABLE_GAUGE', () => {
|
|
310
|
+
expect(exporter.selectAggregationTemporality(InstrumentType.OBSERVABLE_GAUGE))
|
|
311
|
+
.toBe(AggregationTemporality.CUMULATIVE);
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
it('should return CUMULATIVE for OBSERVABLE_COUNTER', () => {
|
|
315
|
+
expect(exporter.selectAggregationTemporality(InstrumentType.OBSERVABLE_COUNTER))
|
|
316
|
+
.toBe(AggregationTemporality.CUMULATIVE);
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
it('should return CUMULATIVE for GAUGE', () => {
|
|
320
|
+
expect(exporter.selectAggregationTemporality(InstrumentType.GAUGE))
|
|
321
|
+
.toBe(AggregationTemporality.CUMULATIVE);
|
|
322
|
+
});
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
describe('Lifecycle', () => {
|
|
326
|
+
it('should resolve forceFlush', async () => {
|
|
327
|
+
await expect(exporter.forceFlush()).resolves.toBeUndefined();
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
it('should resolve shutdown', async () => {
|
|
331
|
+
await expect(exporter.shutdown()).resolves.toBeUndefined();
|
|
332
|
+
});
|
|
333
|
+
});
|
|
334
|
+
});
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { ExportResult, ExportResultCode } from '@opentelemetry/core';
|
|
2
|
+
import {
|
|
3
|
+
AggregationTemporality,
|
|
4
|
+
DataPointType,
|
|
5
|
+
InstrumentType,
|
|
6
|
+
PushMetricExporter,
|
|
7
|
+
ResourceMetrics,
|
|
8
|
+
} from '@opentelemetry/sdk-metrics';
|
|
9
|
+
import { sha256 } from '@noble/hashes/sha2';
|
|
10
|
+
import { utf8ToBytes } from '@noble/hashes/utils';
|
|
11
|
+
import {
|
|
12
|
+
AttributeType,
|
|
13
|
+
CumulativeDataPoint,
|
|
14
|
+
CustomCumulativeMetricSpecification,
|
|
15
|
+
CustomMetricSpecification,
|
|
16
|
+
CustomTimeSeriesMetricSpecification,
|
|
17
|
+
HrTime,
|
|
18
|
+
TimeSeriesDataPoint,
|
|
19
|
+
} from './wire-protocol';
|
|
20
|
+
import { WebSocketConnection } from './WebSocketConnection';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Well-known Node.js runtime metric names that we forward as timeseries (gauge) metrics.
|
|
24
|
+
* Sources:
|
|
25
|
+
* - @opentelemetry/instrumentation-runtime-node (process.runtime.nodejs.*, nodejs.*)
|
|
26
|
+
* - OTel semconv: process metrics (process.cpu.utilization, process.uptime)
|
|
27
|
+
* - OTel semconv: V8 JS metrics (v8js.memory.heap.*)
|
|
28
|
+
*/
|
|
29
|
+
const KNOWN_GAUGE_METRICS = new Set([
|
|
30
|
+
// Node.js runtime instrumentation metrics (@opentelemetry/instrumentation-runtime-node)
|
|
31
|
+
'process.runtime.nodejs.memory.heap.total',
|
|
32
|
+
'process.runtime.nodejs.memory.heap.used',
|
|
33
|
+
'process.runtime.nodejs.memory.rss',
|
|
34
|
+
'process.runtime.nodejs.memory.array_buffers',
|
|
35
|
+
'process.runtime.nodejs.memory.external',
|
|
36
|
+
'nodejs.eventloop.delay.min',
|
|
37
|
+
'nodejs.eventloop.delay.max',
|
|
38
|
+
'nodejs.eventloop.delay.mean',
|
|
39
|
+
'nodejs.eventloop.delay.p50',
|
|
40
|
+
'nodejs.eventloop.delay.p99',
|
|
41
|
+
'nodejs.eventloop.utilization',
|
|
42
|
+
'nodejs.active_handles.total',
|
|
43
|
+
// OTel semconv process metrics
|
|
44
|
+
'process.cpu.utilization',
|
|
45
|
+
'process.uptime',
|
|
46
|
+
// OTel semconv V8 JS runtime metrics
|
|
47
|
+
'v8js.memory.heap.limit',
|
|
48
|
+
'v8js.memory.heap.used',
|
|
49
|
+
'v8js.memory.heap.space.available_size',
|
|
50
|
+
'v8js.memory.heap.space.physical_size',
|
|
51
|
+
// Host metrics (@opentelemetry/host-metrics)
|
|
52
|
+
'process.memory.usage',
|
|
53
|
+
]);
|
|
54
|
+
|
|
55
|
+
/** Well-known counter/cumulative metrics. */
|
|
56
|
+
const KNOWN_CUMULATIVE_METRICS = new Set([
|
|
57
|
+
'process.cpu.time',
|
|
58
|
+
'process.memory.virtual',
|
|
59
|
+
]);
|
|
60
|
+
|
|
61
|
+
export class ComprehendMetricsExporter implements PushMetricExporter {
|
|
62
|
+
private readonly connection: WebSocketConnection;
|
|
63
|
+
private customCumulativeSpecs: CustomCumulativeMetricSpecification[] = [];
|
|
64
|
+
private customTimeSeriesSpecs: CustomTimeSeriesMetricSpecification[] = [];
|
|
65
|
+
|
|
66
|
+
constructor(connection: WebSocketConnection) {
|
|
67
|
+
this.connection = connection;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
updateCustomMetrics(specs: CustomMetricSpecification[]): void {
|
|
71
|
+
this.customCumulativeSpecs = specs.filter(
|
|
72
|
+
(s): s is CustomCumulativeMetricSpecification => s.type === 'cumulative'
|
|
73
|
+
);
|
|
74
|
+
this.customTimeSeriesSpecs = specs.filter(
|
|
75
|
+
(s): s is CustomTimeSeriesMetricSpecification => s.type === 'timeseries'
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export(metrics: ResourceMetrics, resultCallback: (result: ExportResult) => void): void {
|
|
80
|
+
try {
|
|
81
|
+
const serviceSubject = this.getServiceSubject(metrics);
|
|
82
|
+
for (const scopeMetrics of metrics.scopeMetrics) {
|
|
83
|
+
for (const metric of scopeMetrics.metrics) {
|
|
84
|
+
const name = metric.descriptor.name;
|
|
85
|
+
const unit = metric.descriptor.unit;
|
|
86
|
+
|
|
87
|
+
// Skip histogram types entirely
|
|
88
|
+
if (metric.dataPointType === DataPointType.HISTOGRAM ||
|
|
89
|
+
metric.dataPointType === DataPointType.EXPONENTIAL_HISTOGRAM) {
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Check standard well-known metrics
|
|
94
|
+
if (KNOWN_GAUGE_METRICS.has(name) && serviceSubject) {
|
|
95
|
+
this.sendTimeSeriesData(serviceSubject, name, unit, metric);
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (KNOWN_CUMULATIVE_METRICS.has(name) && serviceSubject) {
|
|
99
|
+
this.sendCumulativeData(serviceSubject, name, unit, metric);
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Check custom metric specs
|
|
104
|
+
const cumulativeSpec = this.customCumulativeSpecs.find(s => s.id === name);
|
|
105
|
+
if (cumulativeSpec) {
|
|
106
|
+
this.sendCumulativeData(cumulativeSpec.subject, name, unit, metric, cumulativeSpec.attributes);
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const timeSeriesSpec = this.customTimeSeriesSpecs.find(s => s.id === name);
|
|
111
|
+
if (timeSeriesSpec) {
|
|
112
|
+
this.sendTimeSeriesData(timeSeriesSpec.subject, name, unit, metric, timeSeriesSpec.attributes);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
resultCallback({ code: ExportResultCode.SUCCESS });
|
|
117
|
+
} catch {
|
|
118
|
+
resultCallback({ code: ExportResultCode.FAILED });
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
private sendTimeSeriesData(
|
|
123
|
+
subject: string,
|
|
124
|
+
name: string,
|
|
125
|
+
unit: string,
|
|
126
|
+
metric: { dataPointType: DataPointType, dataPoints: any[] },
|
|
127
|
+
filterAttributes?: string[],
|
|
128
|
+
): void {
|
|
129
|
+
const data: TimeSeriesDataPoint[] = [];
|
|
130
|
+
for (const dp of metric.dataPoints) {
|
|
131
|
+
data.push({
|
|
132
|
+
subject,
|
|
133
|
+
type: name,
|
|
134
|
+
timestamp: hrTimeFromOtel(dp.endTime),
|
|
135
|
+
value: dp.value as number,
|
|
136
|
+
unit,
|
|
137
|
+
attributes: extractAttributes(dp.attributes, filterAttributes),
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
if (data.length > 0) {
|
|
141
|
+
this.connection.sendMessage({
|
|
142
|
+
event: 'timeseries',
|
|
143
|
+
seq: this.connection.nextSeq(),
|
|
144
|
+
data,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
private sendCumulativeData(
|
|
150
|
+
subject: string,
|
|
151
|
+
name: string,
|
|
152
|
+
unit: string,
|
|
153
|
+
metric: { dataPointType: DataPointType, dataPoints: any[] },
|
|
154
|
+
filterAttributes?: string[],
|
|
155
|
+
): void {
|
|
156
|
+
const data: CumulativeDataPoint[] = [];
|
|
157
|
+
for (const dp of metric.dataPoints) {
|
|
158
|
+
data.push({
|
|
159
|
+
subject,
|
|
160
|
+
type: name,
|
|
161
|
+
timestamp: hrTimeFromOtel(dp.endTime),
|
|
162
|
+
value: dp.value as number,
|
|
163
|
+
unit,
|
|
164
|
+
attributes: extractAttributes(dp.attributes, filterAttributes),
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
if (data.length > 0) {
|
|
168
|
+
this.connection.sendMessage({
|
|
169
|
+
event: 'cumulative',
|
|
170
|
+
seq: this.connection.nextSeq(),
|
|
171
|
+
data,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
private getServiceSubject(metrics: ResourceMetrics): string | null {
|
|
177
|
+
const attrs = metrics.resource.attributes;
|
|
178
|
+
const name = attrs['service.name'] as string | undefined;
|
|
179
|
+
if (!name) return null;
|
|
180
|
+
const namespace = attrs['service.namespace'] as string | undefined;
|
|
181
|
+
const environment = attrs['deployment.environment'] as string | undefined;
|
|
182
|
+
const idString = `service:${name}:${namespace ?? ''}:${environment ?? ''}`;
|
|
183
|
+
return hashIdString(idString);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
selectAggregationTemporality(instrumentType: InstrumentType): AggregationTemporality {
|
|
187
|
+
switch (instrumentType) {
|
|
188
|
+
case InstrumentType.COUNTER:
|
|
189
|
+
case InstrumentType.HISTOGRAM:
|
|
190
|
+
return AggregationTemporality.DELTA;
|
|
191
|
+
default:
|
|
192
|
+
return AggregationTemporality.CUMULATIVE;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async forceFlush(): Promise<void> {
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async shutdown(): Promise<void> {
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function hashIdString(idString: string) {
|
|
204
|
+
return Array.from(sha256(utf8ToBytes(idString)))
|
|
205
|
+
.map(b => b.toString(16).padStart(2, '0'))
|
|
206
|
+
.join('');
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function extractAttributes(
|
|
210
|
+
attrs: Record<string, any>,
|
|
211
|
+
filterKeys?: string[],
|
|
212
|
+
): Record<string, AttributeType> {
|
|
213
|
+
const result: Record<string, AttributeType> = {};
|
|
214
|
+
for (const [key, value] of Object.entries(attrs)) {
|
|
215
|
+
if (value === undefined) continue;
|
|
216
|
+
if (filterKeys && !filterKeys.includes(key)) continue;
|
|
217
|
+
result[key] = value as AttributeType;
|
|
218
|
+
}
|
|
219
|
+
return result;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** Convert OTel HrTime to our wire protocol HrTime (same format: [seconds, nanoseconds]). */
|
|
223
|
+
function hrTimeFromOtel(time: [number, number]): HrTime {
|
|
224
|
+
return time;
|
|
225
|
+
}
|