@equipe-tech/observability 0.1.0 → 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/LICENSE +202 -0
- package/README.md +27 -0
- package/dist/Metrics.d.ts +74 -0
- package/dist/Metrics.js +20 -0
- package/dist/MetricsRuntime.d.ts +11 -0
- package/dist/MetricsRuntime.js +1286 -0
- package/dist/RedactionPolicy.d.ts +9 -0
- package/dist/RedactionPolicy.js +246 -0
- package/dist/Telemetry.d.ts +8 -4
- package/dist/Telemetry.js +25 -10
- package/dist/TelemetryConfig.d.ts +1 -0
- package/dist/TelemetryConfig.js +1 -1
- package/dist/browser/BrowserClient.d.ts +74 -0
- package/dist/browser/BrowserClient.js +189 -0
- package/dist/browser/client.d.ts +2 -0
- package/dist/browser/client.js +1 -0
- package/dist/browser/index.d.ts +4 -2
- package/dist/browser/index.js +40 -50
- package/dist/nestjs/BrowserEventsController.d.ts +1 -1
- package/dist/nestjs/BrowserEventsController.js +9 -1
- package/dist/nestjs/HttpRoutePolicy.d.ts +23 -0
- package/dist/nestjs/HttpRoutePolicy.js +179 -0
- package/dist/nestjs/HttpServerOtlpTracer.d.ts +15 -0
- package/dist/nestjs/HttpServerOtlpTracer.js +154 -0
- package/dist/nestjs/RequestWideEventTraceCorrelation.d.ts +15 -0
- package/dist/nestjs/RequestWideEventTraceCorrelation.js +13 -0
- package/dist/nestjs/TelemetryInterceptor.d.ts +23 -4
- package/dist/nestjs/TelemetryInterceptor.js +205 -39
- package/dist/nestjs/TelemetryModule.d.ts +53 -0
- package/dist/nestjs/TelemetryModule.js +428 -0
- package/dist/nestjs/index.d.ts +4 -1
- package/dist/nestjs/index.js +3 -1
- package/dist/node/BrowserEventIngest.d.ts +2 -2
- package/dist/node/BrowserEventIngest.js +3 -3
- package/dist/testing/index.d.ts +34 -2
- package/dist/testing/index.js +92 -10
- package/package.json +23 -1
|
@@ -0,0 +1,1286 @@
|
|
|
1
|
+
import { Context, Effect, Layer, Metric, Predicate, Schema } from "effect";
|
|
2
|
+
import { HttpBody, HttpClient, HttpClientRequest } from "effect/unstable/http";
|
|
3
|
+
import { OtlpExporter } from "effect/unstable/observability";
|
|
4
|
+
import { MetricsError } from "./Metrics.js";
|
|
5
|
+
const instrumentNamePattern = /^[A-Za-z][A-Za-z0-9_.\-/]{0,254}$/;
|
|
6
|
+
const unitPattern = /^(?:1|%|[A-Za-z][A-Za-z0-9]*(?:[./*^][A-Za-z0-9]+)*)$/;
|
|
7
|
+
const containsControlCharacter = (value) => {
|
|
8
|
+
for (const character of value) {
|
|
9
|
+
const codePoint = character.codePointAt(0);
|
|
10
|
+
if (codePoint !== undefined && (codePoint <= 31 || codePoint === 127)) {
|
|
11
|
+
return true;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
return false;
|
|
15
|
+
};
|
|
16
|
+
const maximumInstruments = 100;
|
|
17
|
+
const maximumInstrumentSeries = 1_000;
|
|
18
|
+
const maximumRuntimeSeries = 10_000;
|
|
19
|
+
const maximumAttributes = 16;
|
|
20
|
+
const maximumCallbacks = 16;
|
|
21
|
+
const maximumObservations = 100;
|
|
22
|
+
const MetricAttributeInput = Schema.Struct({
|
|
23
|
+
key: Schema.String,
|
|
24
|
+
value: Schema.Union([Schema.String, Schema.Number, Schema.Boolean]),
|
|
25
|
+
});
|
|
26
|
+
const MetricAttributesInput = Schema.Array(MetricAttributeInput);
|
|
27
|
+
const InstrumentDefinitionInput = Schema.Struct({
|
|
28
|
+
name: Schema.String,
|
|
29
|
+
description: Schema.String,
|
|
30
|
+
unit: Schema.String,
|
|
31
|
+
});
|
|
32
|
+
const HistogramDefinitionInput = Schema.Struct({
|
|
33
|
+
name: Schema.String,
|
|
34
|
+
description: Schema.String,
|
|
35
|
+
unit: Schema.String,
|
|
36
|
+
boundaries: Schema.Array(Schema.Number),
|
|
37
|
+
});
|
|
38
|
+
const GaugeObservationsInput = Schema.Array(Schema.Struct({
|
|
39
|
+
value: Schema.Number,
|
|
40
|
+
attributes: MetricAttributesInput.pipe(Schema.optionalKey),
|
|
41
|
+
}));
|
|
42
|
+
const MetricsOptionsInput = Schema.Struct({
|
|
43
|
+
enabled: Schema.Boolean.pipe(Schema.optionalKey),
|
|
44
|
+
serviceName: Schema.String,
|
|
45
|
+
serviceVersion: Schema.String,
|
|
46
|
+
environment: Schema.String,
|
|
47
|
+
otlpEndpoint: Schema.String,
|
|
48
|
+
exportIntervalMilliseconds: Schema.Number.pipe(Schema.optionalKey),
|
|
49
|
+
flushTimeoutMilliseconds: Schema.Number.pipe(Schema.optionalKey),
|
|
50
|
+
});
|
|
51
|
+
const decodeMetricAttributes = Schema.decodeUnknownSync(MetricAttributesInput);
|
|
52
|
+
const decodeInstrumentDefinition = Schema.decodeUnknownSync(InstrumentDefinitionInput);
|
|
53
|
+
const decodeHistogramDefinition = Schema.decodeUnknownSync(HistogramDefinitionInput);
|
|
54
|
+
const decodeGaugeObservations = Schema.decodeUnknownSync(GaugeObservationsInput);
|
|
55
|
+
const decodeMetricsOptions = Schema.decodeUnknownSync(MetricsOptionsInput);
|
|
56
|
+
const metricError = (code, operation, message, instrumentName, retryable, cause) => {
|
|
57
|
+
const options = {
|
|
58
|
+
code,
|
|
59
|
+
operation,
|
|
60
|
+
message,
|
|
61
|
+
retryable,
|
|
62
|
+
cause,
|
|
63
|
+
};
|
|
64
|
+
if (instrumentName === undefined) {
|
|
65
|
+
return new MetricsError(options);
|
|
66
|
+
}
|
|
67
|
+
return new MetricsError({ ...options, instrumentName });
|
|
68
|
+
};
|
|
69
|
+
const parseOptions = (input) => {
|
|
70
|
+
let options;
|
|
71
|
+
try {
|
|
72
|
+
options = decodeMetricsOptions(input);
|
|
73
|
+
}
|
|
74
|
+
catch (cause) {
|
|
75
|
+
throw metricError("INVALID_CONFIGURATION", "createMetrics", "Metrics configuration is invalid. Provide valid service metadata and an HTTP OTLP endpoint.", undefined, false, cause);
|
|
76
|
+
}
|
|
77
|
+
if (options.serviceName.length === 0 ||
|
|
78
|
+
options.serviceVersion.length === 0 ||
|
|
79
|
+
options.environment.length === 0) {
|
|
80
|
+
throw metricError("INVALID_CONFIGURATION", "createMetrics", "Metrics configuration is invalid. Service name, version, and environment must be nonempty.", undefined, false);
|
|
81
|
+
}
|
|
82
|
+
let endpoint;
|
|
83
|
+
try {
|
|
84
|
+
endpoint = new URL(options.otlpEndpoint);
|
|
85
|
+
}
|
|
86
|
+
catch (cause) {
|
|
87
|
+
throw metricError("INVALID_CONFIGURATION", "createMetrics", "Metrics configuration is invalid. Set otlpEndpoint to an HTTP or HTTPS URL without credentials.", undefined, false, cause);
|
|
88
|
+
}
|
|
89
|
+
if ((endpoint.protocol !== "http:" && endpoint.protocol !== "https:") ||
|
|
90
|
+
endpoint.username.length > 0 ||
|
|
91
|
+
endpoint.password.length > 0) {
|
|
92
|
+
throw metricError("INVALID_CONFIGURATION", "createMetrics", "Metrics configuration is invalid. Set otlpEndpoint to an HTTP or HTTPS URL without credentials.", undefined, false);
|
|
93
|
+
}
|
|
94
|
+
const exportIntervalMilliseconds = options.exportIntervalMilliseconds ?? 10_000;
|
|
95
|
+
const flushTimeoutMilliseconds = options.flushTimeoutMilliseconds ?? 3_000;
|
|
96
|
+
if (!Number.isSafeInteger(exportIntervalMilliseconds) ||
|
|
97
|
+
exportIntervalMilliseconds < 1 ||
|
|
98
|
+
!Number.isSafeInteger(flushTimeoutMilliseconds) ||
|
|
99
|
+
flushTimeoutMilliseconds < 1) {
|
|
100
|
+
throw metricError("INVALID_CONFIGURATION", "createMetrics", "Metrics configuration is invalid. Export and flush intervals must be positive safe integer milliseconds.", undefined, false);
|
|
101
|
+
}
|
|
102
|
+
endpoint.pathname = `${endpoint.pathname.replace(/\/$/, "")}/v1/metrics`;
|
|
103
|
+
endpoint.search = "";
|
|
104
|
+
endpoint.hash = "";
|
|
105
|
+
const enabled = options.enabled ?? true;
|
|
106
|
+
const poolKey = JSON.stringify([
|
|
107
|
+
endpoint.toString(),
|
|
108
|
+
options.serviceName,
|
|
109
|
+
options.serviceVersion,
|
|
110
|
+
options.environment,
|
|
111
|
+
exportIntervalMilliseconds,
|
|
112
|
+
flushTimeoutMilliseconds,
|
|
113
|
+
]);
|
|
114
|
+
return {
|
|
115
|
+
enabled,
|
|
116
|
+
serviceName: options.serviceName,
|
|
117
|
+
serviceVersion: options.serviceVersion,
|
|
118
|
+
environment: options.environment,
|
|
119
|
+
metricsEndpoint: endpoint.toString(),
|
|
120
|
+
exportIntervalMilliseconds,
|
|
121
|
+
flushTimeoutMilliseconds,
|
|
122
|
+
poolKey,
|
|
123
|
+
};
|
|
124
|
+
};
|
|
125
|
+
const parseDefinition = (input, operation) => {
|
|
126
|
+
let definition;
|
|
127
|
+
try {
|
|
128
|
+
definition = decodeInstrumentDefinition(input);
|
|
129
|
+
}
|
|
130
|
+
catch (cause) {
|
|
131
|
+
throw metricError("INVALID_INSTRUMENT", operation, "Metric instrument definition is invalid. Provide a valid name, description, and unit.", undefined, false, cause);
|
|
132
|
+
}
|
|
133
|
+
if (!instrumentNamePattern.test(definition.name) ||
|
|
134
|
+
definition.description.length < 1 ||
|
|
135
|
+
definition.description.length > 1_024 ||
|
|
136
|
+
containsControlCharacter(definition.description) ||
|
|
137
|
+
definition.unit.length > 63 ||
|
|
138
|
+
!unitPattern.test(definition.unit)) {
|
|
139
|
+
throw metricError("INVALID_INSTRUMENT", operation, `Metric instrument "${definition.name}" is invalid. Check its name, description, and unit.`, definition.name, false);
|
|
140
|
+
}
|
|
141
|
+
return definition;
|
|
142
|
+
};
|
|
143
|
+
const parseHistogramDefinition = (input) => {
|
|
144
|
+
let definition;
|
|
145
|
+
try {
|
|
146
|
+
definition = decodeHistogramDefinition(input);
|
|
147
|
+
}
|
|
148
|
+
catch (cause) {
|
|
149
|
+
throw metricError("INVALID_INSTRUMENT", "histogram", "Histogram definition is invalid. Provide a valid definition and finite boundaries.", undefined, false, cause);
|
|
150
|
+
}
|
|
151
|
+
const common = parseDefinition(definition, "histogram");
|
|
152
|
+
if (definition.boundaries.length < 1 || definition.boundaries.length > 50) {
|
|
153
|
+
throw metricError("INVALID_INSTRUMENT", "histogram", `Histogram "${definition.name}" must have between 1 and 50 boundaries.`, definition.name, false);
|
|
154
|
+
}
|
|
155
|
+
let previous = Number.NEGATIVE_INFINITY;
|
|
156
|
+
for (const boundary of definition.boundaries) {
|
|
157
|
+
if (!Number.isFinite(boundary) || boundary <= previous) {
|
|
158
|
+
throw metricError("INVALID_INSTRUMENT", "histogram", `Histogram "${definition.name}" boundaries must be finite and strictly increasing.`, definition.name, false);
|
|
159
|
+
}
|
|
160
|
+
previous = boundary;
|
|
161
|
+
}
|
|
162
|
+
return { ...common, boundaries: [...definition.boundaries] };
|
|
163
|
+
};
|
|
164
|
+
const attributeIdentity = (value) => {
|
|
165
|
+
if (Predicate.isString(value)) {
|
|
166
|
+
return `s:${JSON.stringify(value)}`;
|
|
167
|
+
}
|
|
168
|
+
if (Predicate.isBoolean(value)) {
|
|
169
|
+
return value ? "b:1" : "b:0";
|
|
170
|
+
}
|
|
171
|
+
return `n:${Object.is(value, -0) ? 0 : value}`;
|
|
172
|
+
};
|
|
173
|
+
const parseAttributes = (input, operation, instrumentName) => {
|
|
174
|
+
let attributes;
|
|
175
|
+
try {
|
|
176
|
+
attributes = decodeMetricAttributes(input ?? []);
|
|
177
|
+
}
|
|
178
|
+
catch (cause) {
|
|
179
|
+
throw metricError("INVALID_MEASUREMENT", operation, `Metric "${instrumentName}" attributes are invalid. Use bounded scalar attributes.`, instrumentName, false, cause);
|
|
180
|
+
}
|
|
181
|
+
if (attributes.length > maximumAttributes) {
|
|
182
|
+
throw metricError("LIMIT_EXCEEDED", operation, `Metric "${instrumentName}" exceeds the ${maximumAttributes}-attribute limit.`, instrumentName, false);
|
|
183
|
+
}
|
|
184
|
+
const keys = new Set();
|
|
185
|
+
const normalized = [];
|
|
186
|
+
for (const attribute of attributes) {
|
|
187
|
+
const numberIsInvalid = Predicate.isNumber(attribute.value) && !Number.isFinite(attribute.value);
|
|
188
|
+
const stringIsInvalid = Predicate.isString(attribute.value) &&
|
|
189
|
+
(attribute.value.length > 256 || containsControlCharacter(attribute.value));
|
|
190
|
+
if (attribute.key.length > 128 ||
|
|
191
|
+
!instrumentNamePattern.test(attribute.key) ||
|
|
192
|
+
attribute.key === "unit" ||
|
|
193
|
+
attribute.key === "time_unit" ||
|
|
194
|
+
keys.has(attribute.key) ||
|
|
195
|
+
numberIsInvalid ||
|
|
196
|
+
stringIsInvalid) {
|
|
197
|
+
throw metricError("INVALID_MEASUREMENT", operation, `Metric "${instrumentName}" attributes are invalid. Use unique bounded keys and finite scalar values.`, instrumentName, false);
|
|
198
|
+
}
|
|
199
|
+
keys.add(attribute.key);
|
|
200
|
+
normalized.push({
|
|
201
|
+
key: attribute.key,
|
|
202
|
+
value: Predicate.isNumber(attribute.value) && Object.is(attribute.value, -0) ? 0 : attribute.value,
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
normalized.sort((left, right) => left.key.localeCompare(right.key));
|
|
206
|
+
return {
|
|
207
|
+
identity: normalized
|
|
208
|
+
.map((attribute) => `${attribute.key}=${attributeIdentity(attribute.value)}`)
|
|
209
|
+
.join("|"),
|
|
210
|
+
values: normalized,
|
|
211
|
+
};
|
|
212
|
+
};
|
|
213
|
+
const attributesToOtlp = (attributes) => attributes.map((attribute) => {
|
|
214
|
+
if (Predicate.isString(attribute.value)) {
|
|
215
|
+
return { key: attribute.key, value: { stringValue: attribute.value } };
|
|
216
|
+
}
|
|
217
|
+
if (Predicate.isBoolean(attribute.value)) {
|
|
218
|
+
return { key: attribute.key, value: { boolValue: attribute.value } };
|
|
219
|
+
}
|
|
220
|
+
if (Number.isInteger(attribute.value)) {
|
|
221
|
+
return { key: attribute.key, value: { intValue: attribute.value } };
|
|
222
|
+
}
|
|
223
|
+
return { key: attribute.key, value: { doubleValue: attribute.value } };
|
|
224
|
+
});
|
|
225
|
+
const directAttributesToOtlp = (attributes) => {
|
|
226
|
+
if (attributes === undefined) {
|
|
227
|
+
return [];
|
|
228
|
+
}
|
|
229
|
+
const values = [];
|
|
230
|
+
for (const [key, value] of Object.entries(attributes)) {
|
|
231
|
+
if (key !== "unit" && key !== "time_unit") {
|
|
232
|
+
values.push({ key, value: { stringValue: value } });
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return values;
|
|
236
|
+
};
|
|
237
|
+
const sameDefinition = (entry, kind, definition, boundaries) => {
|
|
238
|
+
if (entry.kind !== kind ||
|
|
239
|
+
entry.definition.name !== definition.name ||
|
|
240
|
+
entry.definition.description !== definition.description ||
|
|
241
|
+
entry.definition.unit !== definition.unit) {
|
|
242
|
+
return false;
|
|
243
|
+
}
|
|
244
|
+
if (entry.kind !== "histogram") {
|
|
245
|
+
return boundaries === undefined;
|
|
246
|
+
}
|
|
247
|
+
if (boundaries === undefined || entry.definition.boundaries.length !== boundaries.length) {
|
|
248
|
+
return false;
|
|
249
|
+
}
|
|
250
|
+
return entry.definition.boundaries.every((boundary, index) => boundary === boundaries[index]);
|
|
251
|
+
};
|
|
252
|
+
const histogramBucketIndex = (boundaries, value) => {
|
|
253
|
+
for (let index = 0; index < boundaries.length; index++) {
|
|
254
|
+
const boundary = boundaries[index];
|
|
255
|
+
if (boundary !== undefined && value <= boundary) {
|
|
256
|
+
return index;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
return boundaries.length;
|
|
260
|
+
};
|
|
261
|
+
const nanosNow = () => String(BigInt(Date.now()) * 1000000n);
|
|
262
|
+
class MetricsRuntimeState {
|
|
263
|
+
registry = new Map();
|
|
264
|
+
directContext = Context.make(Metric.MetricRegistry, this.registry);
|
|
265
|
+
catalog = new Map();
|
|
266
|
+
runtimeLifetimeSeries = new Set();
|
|
267
|
+
lifetimeSeriesByInstrument = new Map();
|
|
268
|
+
lifetimeInstrumentNames = new Set();
|
|
269
|
+
leases = new Set();
|
|
270
|
+
transports = new Map();
|
|
271
|
+
options;
|
|
272
|
+
removeFromPool;
|
|
273
|
+
startTimeUnixNano = nanosNow();
|
|
274
|
+
tail = Promise.resolve();
|
|
275
|
+
timer;
|
|
276
|
+
nextLeaseId = 1;
|
|
277
|
+
nextCallbackId = 1;
|
|
278
|
+
referenceCount = 0;
|
|
279
|
+
periodicFailureActive = false;
|
|
280
|
+
constructor(options, removeFromPool) {
|
|
281
|
+
this.options = options;
|
|
282
|
+
this.removeFromPool = removeFromPool;
|
|
283
|
+
this.timer = setInterval(() => {
|
|
284
|
+
this.scheduleExport(this.options.flushTimeoutMilliseconds).then((result) => this.recordPeriodicOutcome(result.gaugeFailures.length > 0), () => this.recordPeriodicOutcome(true));
|
|
285
|
+
}, options.exportIntervalMilliseconds);
|
|
286
|
+
this.timer.unref?.();
|
|
287
|
+
}
|
|
288
|
+
acquire(transport) {
|
|
289
|
+
const leaseId = this.nextLeaseId++;
|
|
290
|
+
this.leases.add(leaseId);
|
|
291
|
+
this.transports.set(leaseId, transport);
|
|
292
|
+
this.referenceCount++;
|
|
293
|
+
return { leaseId, state: this };
|
|
294
|
+
}
|
|
295
|
+
nextGaugeCallbackId() {
|
|
296
|
+
return this.nextCallbackId++;
|
|
297
|
+
}
|
|
298
|
+
flush(timeoutMilliseconds) {
|
|
299
|
+
return this.scheduleExport(timeoutMilliseconds);
|
|
300
|
+
}
|
|
301
|
+
async closeLease(leaseId, timeoutMilliseconds) {
|
|
302
|
+
let result;
|
|
303
|
+
let failure;
|
|
304
|
+
try {
|
|
305
|
+
result = await this.scheduleExport(timeoutMilliseconds);
|
|
306
|
+
}
|
|
307
|
+
catch (cause) {
|
|
308
|
+
result = { gaugeFailures: [] };
|
|
309
|
+
failure =
|
|
310
|
+
cause instanceof MetricsError
|
|
311
|
+
? cause
|
|
312
|
+
: metricError("EXPORT_FAILED", "close", "The final metrics export failed. The runtime lease was still released.", undefined, true, cause);
|
|
313
|
+
}
|
|
314
|
+
this.removeLeaseState(leaseId);
|
|
315
|
+
this.leases.delete(leaseId);
|
|
316
|
+
this.transports.delete(leaseId);
|
|
317
|
+
this.referenceCount--;
|
|
318
|
+
if (this.referenceCount === 0) {
|
|
319
|
+
if (this.timer !== undefined) {
|
|
320
|
+
clearInterval(this.timer);
|
|
321
|
+
this.timer = undefined;
|
|
322
|
+
}
|
|
323
|
+
this.removeFromPool();
|
|
324
|
+
}
|
|
325
|
+
if (failure !== undefined) {
|
|
326
|
+
throw failure;
|
|
327
|
+
}
|
|
328
|
+
return result;
|
|
329
|
+
}
|
|
330
|
+
registerCounter(leaseId, definition) {
|
|
331
|
+
const existing = this.catalog.get(definition.name);
|
|
332
|
+
if (existing !== undefined) {
|
|
333
|
+
if (existing.kind !== "counter" ||
|
|
334
|
+
!sameDefinition(existing, "counter", definition, undefined)) {
|
|
335
|
+
throw this.instrumentConflict("counter", definition.name);
|
|
336
|
+
}
|
|
337
|
+
existing.leases.add(leaseId);
|
|
338
|
+
return existing;
|
|
339
|
+
}
|
|
340
|
+
this.assertInstrumentCapacity(definition.name);
|
|
341
|
+
const entry = {
|
|
342
|
+
kind: "counter",
|
|
343
|
+
definition,
|
|
344
|
+
leases: new Set([leaseId]),
|
|
345
|
+
residualSeries: new Map(),
|
|
346
|
+
seriesByLease: new Map(),
|
|
347
|
+
lifetimeSeries: this.lifetimeSeriesFor(definition.name),
|
|
348
|
+
};
|
|
349
|
+
this.catalog.set(definition.name, entry);
|
|
350
|
+
return entry;
|
|
351
|
+
}
|
|
352
|
+
registerHistogram(leaseId, definition) {
|
|
353
|
+
const existing = this.catalog.get(definition.name);
|
|
354
|
+
if (existing !== undefined) {
|
|
355
|
+
if (existing.kind !== "histogram" ||
|
|
356
|
+
!sameDefinition(existing, "histogram", definition, definition.boundaries)) {
|
|
357
|
+
throw this.instrumentConflict("histogram", definition.name);
|
|
358
|
+
}
|
|
359
|
+
existing.leases.add(leaseId);
|
|
360
|
+
return existing;
|
|
361
|
+
}
|
|
362
|
+
this.assertInstrumentCapacity(definition.name);
|
|
363
|
+
const entry = {
|
|
364
|
+
kind: "histogram",
|
|
365
|
+
definition,
|
|
366
|
+
leases: new Set([leaseId]),
|
|
367
|
+
residualSeries: new Map(),
|
|
368
|
+
seriesByLease: new Map(),
|
|
369
|
+
lifetimeSeries: this.lifetimeSeriesFor(definition.name),
|
|
370
|
+
};
|
|
371
|
+
this.catalog.set(definition.name, entry);
|
|
372
|
+
return entry;
|
|
373
|
+
}
|
|
374
|
+
registerGauge(leaseId, definition, callback) {
|
|
375
|
+
const existing = this.catalog.get(definition.name);
|
|
376
|
+
let entry;
|
|
377
|
+
if (existing !== undefined) {
|
|
378
|
+
if (!sameDefinition(existing, "gauge", definition, undefined) || existing.kind !== "gauge") {
|
|
379
|
+
throw this.instrumentConflict("observableGauge", definition.name);
|
|
380
|
+
}
|
|
381
|
+
entry = existing;
|
|
382
|
+
}
|
|
383
|
+
else {
|
|
384
|
+
this.assertInstrumentCapacity(definition.name);
|
|
385
|
+
entry = {
|
|
386
|
+
kind: "gauge",
|
|
387
|
+
definition,
|
|
388
|
+
leases: new Set(),
|
|
389
|
+
callbacks: new Map(),
|
|
390
|
+
lifetimeSeries: this.lifetimeSeriesFor(definition.name),
|
|
391
|
+
};
|
|
392
|
+
this.catalog.set(definition.name, entry);
|
|
393
|
+
}
|
|
394
|
+
if (entry.callbacks.size >= maximumCallbacks) {
|
|
395
|
+
throw metricError("LIMIT_EXCEEDED", "observableGauge", `Observable gauge "${definition.name}" exceeds the ${maximumCallbacks}-callback limit.`, definition.name, false);
|
|
396
|
+
}
|
|
397
|
+
const callbackId = this.nextGaugeCallbackId();
|
|
398
|
+
entry.leases.add(leaseId);
|
|
399
|
+
entry.callbacks.set(callbackId, { id: callbackId, leaseId, callback });
|
|
400
|
+
return { entry, callbackId };
|
|
401
|
+
}
|
|
402
|
+
unregisterGauge(name, callbackId) {
|
|
403
|
+
const entry = this.catalog.get(name);
|
|
404
|
+
if (entry === undefined || entry.kind !== "gauge") {
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
const registration = entry.callbacks.get(callbackId);
|
|
408
|
+
if (registration === undefined) {
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
entry.callbacks.delete(callbackId);
|
|
412
|
+
const leaseStillRegistered = Array.from(entry.callbacks.values()).some((candidate) => candidate.leaseId === registration.leaseId);
|
|
413
|
+
if (!leaseStillRegistered) {
|
|
414
|
+
entry.leases.delete(registration.leaseId);
|
|
415
|
+
}
|
|
416
|
+
if (entry.callbacks.size === 0) {
|
|
417
|
+
this.catalog.delete(name);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
addCounter(leaseId, entry, value, attributes) {
|
|
421
|
+
this.assertLease(leaseId, "add", entry.definition.name);
|
|
422
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
423
|
+
throw metricError("INVALID_MEASUREMENT", "add", `Counter "${entry.definition.name}" accepts only finite values greater than or equal to zero.`, entry.definition.name, false);
|
|
424
|
+
}
|
|
425
|
+
this.prepareSeries(entry, attributes, "add");
|
|
426
|
+
let leaseSeries = entry.seriesByLease.get(leaseId);
|
|
427
|
+
if (leaseSeries === undefined) {
|
|
428
|
+
leaseSeries = new Map();
|
|
429
|
+
entry.seriesByLease.set(leaseId, leaseSeries);
|
|
430
|
+
}
|
|
431
|
+
const current = leaseSeries.get(attributes.identity);
|
|
432
|
+
if (current === undefined) {
|
|
433
|
+
leaseSeries.set(attributes.identity, { attributes, value });
|
|
434
|
+
this.commitSeries(entry, attributes.identity);
|
|
435
|
+
}
|
|
436
|
+
else {
|
|
437
|
+
current.value += value;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
recordHistogram(leaseId, entry, value, attributes) {
|
|
441
|
+
this.assertLease(leaseId, "record", entry.definition.name);
|
|
442
|
+
if (!Number.isFinite(value)) {
|
|
443
|
+
throw metricError("INVALID_MEASUREMENT", "record", `Histogram "${entry.definition.name}" accepts only finite values.`, entry.definition.name, false);
|
|
444
|
+
}
|
|
445
|
+
this.prepareSeries(entry, attributes, "record");
|
|
446
|
+
let leaseSeries = entry.seriesByLease.get(leaseId);
|
|
447
|
+
if (leaseSeries === undefined) {
|
|
448
|
+
leaseSeries = new Map();
|
|
449
|
+
entry.seriesByLease.set(leaseId, leaseSeries);
|
|
450
|
+
}
|
|
451
|
+
const current = leaseSeries.get(attributes.identity);
|
|
452
|
+
if (current === undefined) {
|
|
453
|
+
const bucketCounts = Array.from({ length: entry.definition.boundaries.length + 1 }, () => 0);
|
|
454
|
+
const bucketIndex = histogramBucketIndex(entry.definition.boundaries, value);
|
|
455
|
+
bucketCounts[bucketIndex] = 1;
|
|
456
|
+
leaseSeries.set(attributes.identity, {
|
|
457
|
+
attributes,
|
|
458
|
+
count: 1,
|
|
459
|
+
sum: value,
|
|
460
|
+
min: value,
|
|
461
|
+
max: value,
|
|
462
|
+
bucketCounts,
|
|
463
|
+
});
|
|
464
|
+
this.commitSeries(entry, attributes.identity);
|
|
465
|
+
}
|
|
466
|
+
else {
|
|
467
|
+
current.count++;
|
|
468
|
+
current.sum += value;
|
|
469
|
+
current.min = Math.min(current.min, value);
|
|
470
|
+
current.max = Math.max(current.max, value);
|
|
471
|
+
const bucketIndex = histogramBucketIndex(entry.definition.boundaries, value);
|
|
472
|
+
const count = current.bucketCounts[bucketIndex];
|
|
473
|
+
current.bucketCounts[bucketIndex] = (count ?? 0) + 1;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
lifetimeSeriesFor(name) {
|
|
477
|
+
let series = this.lifetimeSeriesByInstrument.get(name);
|
|
478
|
+
if (series === undefined) {
|
|
479
|
+
series = new Set();
|
|
480
|
+
this.lifetimeSeriesByInstrument.set(name, series);
|
|
481
|
+
}
|
|
482
|
+
return series;
|
|
483
|
+
}
|
|
484
|
+
recordPeriodicOutcome(failed) {
|
|
485
|
+
if (failed === this.periodicFailureActive) {
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
this.periodicFailureActive = failed;
|
|
489
|
+
console.warn(failed
|
|
490
|
+
? "OBS_METRICS_PERIODIC_EXPORT_FAILED: The periodic metrics export entered a failed state."
|
|
491
|
+
: "OBS_METRICS_PERIODIC_EXPORT_RECOVERED: The periodic metrics export recovered.");
|
|
492
|
+
}
|
|
493
|
+
assertInstrumentCapacity(name) {
|
|
494
|
+
if (this.lifetimeInstrumentNames.has(name)) {
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
if (this.lifetimeInstrumentNames.size >= maximumInstruments) {
|
|
498
|
+
throw metricError("LIMIT_EXCEEDED", "registerInstrument", `Metric runtime exceeds the ${maximumInstruments}-instrument lifetime limit while registering "${name}".`, name, false);
|
|
499
|
+
}
|
|
500
|
+
this.lifetimeInstrumentNames.add(name);
|
|
501
|
+
}
|
|
502
|
+
instrumentConflict(operation, name) {
|
|
503
|
+
return metricError("INSTRUMENT_CONFLICT", operation, `Metric instrument "${name}" conflicts with an existing definition. Reuse the exact kind, unit, description, and boundaries.`, name, false);
|
|
504
|
+
}
|
|
505
|
+
assertLease(leaseId, operation, name) {
|
|
506
|
+
if (!this.leases.has(leaseId)) {
|
|
507
|
+
throw metricError("CLOSED", operation, `Metrics lifecycle is closed. Create a new lifecycle before using "${name}".`, name, false);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
prepareSeries(entry, attributes, operation) {
|
|
511
|
+
const runtimeIdentity = `${entry.definition.name}:${attributes.identity}`;
|
|
512
|
+
if (!entry.lifetimeSeries.has(attributes.identity) &&
|
|
513
|
+
entry.lifetimeSeries.size >= maximumInstrumentSeries) {
|
|
514
|
+
throw metricError("LIMIT_EXCEEDED", operation, `Metric "${entry.definition.name}" exceeds the ${maximumInstrumentSeries}-series lifetime limit.`, entry.definition.name, false);
|
|
515
|
+
}
|
|
516
|
+
if (!this.runtimeLifetimeSeries.has(runtimeIdentity) &&
|
|
517
|
+
this.runtimeLifetimeSeries.size >= maximumRuntimeSeries) {
|
|
518
|
+
throw metricError("LIMIT_EXCEEDED", operation, `Metric runtime exceeds the ${maximumRuntimeSeries}-series lifetime limit.`, entry.definition.name, false);
|
|
519
|
+
}
|
|
520
|
+
return runtimeIdentity;
|
|
521
|
+
}
|
|
522
|
+
commitSeries(entry, identity) {
|
|
523
|
+
entry.lifetimeSeries.add(identity);
|
|
524
|
+
this.runtimeLifetimeSeries.add(`${entry.definition.name}:${identity}`);
|
|
525
|
+
}
|
|
526
|
+
transportForExport() {
|
|
527
|
+
let selected;
|
|
528
|
+
for (const transport of this.transports.values()) {
|
|
529
|
+
if (selected === undefined || transport.kind === "layer") {
|
|
530
|
+
selected = transport;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
if (selected === undefined) {
|
|
534
|
+
throw metricError("EXPORT_FAILED", "flush", "Metrics export has no active transport. Acquire a runtime lease before flushing.", undefined, false);
|
|
535
|
+
}
|
|
536
|
+
return selected.send;
|
|
537
|
+
}
|
|
538
|
+
scheduleExport(timeoutMilliseconds) {
|
|
539
|
+
const controller = new AbortController();
|
|
540
|
+
let timedOut = false;
|
|
541
|
+
const operation = this.tail.then(async () => {
|
|
542
|
+
if (controller.signal.aborted) {
|
|
543
|
+
throw metricError("FLUSH_TIMED_OUT", "flush", `Metrics flush exceeded ${timeoutMilliseconds} milliseconds. Retry the flush before closing.`, undefined, true);
|
|
544
|
+
}
|
|
545
|
+
const collection = this.collectPayload();
|
|
546
|
+
const transport = this.transportForExport();
|
|
547
|
+
try {
|
|
548
|
+
await transport(collection.payload, controller.signal);
|
|
549
|
+
}
|
|
550
|
+
catch (cause) {
|
|
551
|
+
if (timedOut || controller.signal.aborted) {
|
|
552
|
+
throw metricError("FLUSH_TIMED_OUT", "flush", `Metrics flush exceeded ${timeoutMilliseconds} milliseconds. Retry the flush before closing.`, undefined, true, cause);
|
|
553
|
+
}
|
|
554
|
+
throw metricError("EXPORT_FAILED", "flush", "Metrics export failed. Verify the OTLP endpoint and retry the flush.", undefined, true, cause);
|
|
555
|
+
}
|
|
556
|
+
return collection.result;
|
|
557
|
+
});
|
|
558
|
+
this.tail = operation.then(() => undefined, () => undefined);
|
|
559
|
+
let timer;
|
|
560
|
+
const timeout = new Promise((_resolve, reject) => {
|
|
561
|
+
timer = setTimeout(() => {
|
|
562
|
+
timedOut = true;
|
|
563
|
+
controller.abort();
|
|
564
|
+
reject(metricError("FLUSH_TIMED_OUT", "flush", `Metrics flush exceeded ${timeoutMilliseconds} milliseconds. Retry the flush before closing.`, undefined, true));
|
|
565
|
+
}, timeoutMilliseconds);
|
|
566
|
+
});
|
|
567
|
+
return Promise.race([operation, timeout]).finally(() => {
|
|
568
|
+
if (timer !== undefined) {
|
|
569
|
+
clearTimeout(timer);
|
|
570
|
+
}
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
collectPayload() {
|
|
574
|
+
const timeUnixNano = nanosNow();
|
|
575
|
+
const metrics = [];
|
|
576
|
+
const names = new Map();
|
|
577
|
+
for (const snapshot of Metric.snapshotUnsafe(this.directContext)) {
|
|
578
|
+
this.appendDirectMetric(metrics, names, snapshot, timeUnixNano);
|
|
579
|
+
}
|
|
580
|
+
const gaugeFailures = [];
|
|
581
|
+
for (const entry of this.catalog.values()) {
|
|
582
|
+
const existing = names.get(entry.definition.name);
|
|
583
|
+
const definitionIdentity = `${entry.kind}:${entry.definition.unit}:${entry.definition.description}`;
|
|
584
|
+
if (existing !== undefined && existing.definition !== definitionIdentity) {
|
|
585
|
+
throw metricError("EXPORT_FAILED", "flush", `Metric "${entry.definition.name}" conflicts with a direct Effect metric. Rename one instrument before retrying.`, entry.definition.name, false);
|
|
586
|
+
}
|
|
587
|
+
names.set(entry.definition.name, { kind: entry.kind, definition: definitionIdentity });
|
|
588
|
+
if (entry.kind === "counter") {
|
|
589
|
+
metrics.push(this.collectCounter(entry, timeUnixNano));
|
|
590
|
+
}
|
|
591
|
+
else if (entry.kind === "histogram") {
|
|
592
|
+
metrics.push(this.collectHistogram(entry, timeUnixNano));
|
|
593
|
+
}
|
|
594
|
+
else {
|
|
595
|
+
const gauge = this.collectGauge(entry, timeUnixNano);
|
|
596
|
+
gaugeFailures.push(...gauge.failures);
|
|
597
|
+
if (gauge.metric !== undefined) {
|
|
598
|
+
metrics.push(gauge.metric);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
return {
|
|
603
|
+
payload: {
|
|
604
|
+
resourceMetrics: [
|
|
605
|
+
{
|
|
606
|
+
resource: {
|
|
607
|
+
attributes: [
|
|
608
|
+
{ key: "service.name", value: { stringValue: this.options.serviceName } },
|
|
609
|
+
{ key: "service.version", value: { stringValue: this.options.serviceVersion } },
|
|
610
|
+
{
|
|
611
|
+
key: "deployment.environment.name",
|
|
612
|
+
value: { stringValue: this.options.environment },
|
|
613
|
+
},
|
|
614
|
+
],
|
|
615
|
+
droppedAttributesCount: 0,
|
|
616
|
+
},
|
|
617
|
+
scopeMetrics: [
|
|
618
|
+
{
|
|
619
|
+
scope: { name: this.options.serviceName },
|
|
620
|
+
metrics,
|
|
621
|
+
},
|
|
622
|
+
],
|
|
623
|
+
},
|
|
624
|
+
],
|
|
625
|
+
},
|
|
626
|
+
result: { gaugeFailures },
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
appendDirectMetric(metrics, names, snapshot, timeUnixNano) {
|
|
630
|
+
const unit = snapshot.attributes?.unit ?? snapshot.attributes?.time_unit ?? "1";
|
|
631
|
+
const description = snapshot.description ?? "";
|
|
632
|
+
const definitionIdentity = `${snapshot.type}:${unit}:${description}`;
|
|
633
|
+
const existing = names.get(snapshot.id);
|
|
634
|
+
if (existing !== undefined && existing.definition !== definitionIdentity) {
|
|
635
|
+
throw metricError("EXPORT_FAILED", "flush", `Direct Effect metric "${snapshot.id}" has incompatible definitions. Rename or align the definitions before retrying.`, snapshot.id, false);
|
|
636
|
+
}
|
|
637
|
+
const attributes = directAttributesToOtlp(snapshot.attributes);
|
|
638
|
+
const previous = metrics.find((metric) => metric.name === snapshot.id);
|
|
639
|
+
names.set(snapshot.id, { kind: snapshot.type, definition: definitionIdentity });
|
|
640
|
+
if (snapshot.type === "Counter") {
|
|
641
|
+
const point = this.numberPoint(snapshot.state.count, attributes, timeUnixNano);
|
|
642
|
+
if (previous?.sum !== undefined) {
|
|
643
|
+
previous.sum.dataPoints.push(point);
|
|
644
|
+
}
|
|
645
|
+
else {
|
|
646
|
+
metrics.push({
|
|
647
|
+
name: snapshot.id,
|
|
648
|
+
description,
|
|
649
|
+
unit,
|
|
650
|
+
sum: {
|
|
651
|
+
aggregationTemporality: 2,
|
|
652
|
+
isMonotonic: snapshot.state.incremental,
|
|
653
|
+
dataPoints: [point],
|
|
654
|
+
},
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
else if (snapshot.type === "Gauge") {
|
|
659
|
+
const point = this.numberPoint(snapshot.state.value, attributes, timeUnixNano);
|
|
660
|
+
if (previous?.gauge !== undefined) {
|
|
661
|
+
previous.gauge.dataPoints.push(point);
|
|
662
|
+
}
|
|
663
|
+
else {
|
|
664
|
+
metrics.push({ name: snapshot.id, description, unit, gauge: { dataPoints: [point] } });
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
else if (snapshot.type === "Histogram") {
|
|
668
|
+
const buckets = [];
|
|
669
|
+
const bounds = [];
|
|
670
|
+
let previousCount = 0;
|
|
671
|
+
for (let index = 0; index < snapshot.state.buckets.length; index++) {
|
|
672
|
+
const bucket = snapshot.state.buckets[index];
|
|
673
|
+
if (bucket !== undefined) {
|
|
674
|
+
if (index < snapshot.state.buckets.length - 1) {
|
|
675
|
+
bounds.push(bucket[0]);
|
|
676
|
+
}
|
|
677
|
+
buckets.push(bucket[1] - previousCount);
|
|
678
|
+
previousCount = bucket[1];
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
const point = {
|
|
682
|
+
attributes,
|
|
683
|
+
startTimeUnixNano: this.startTimeUnixNano,
|
|
684
|
+
timeUnixNano,
|
|
685
|
+
count: snapshot.state.count,
|
|
686
|
+
sum: snapshot.state.sum,
|
|
687
|
+
min: snapshot.state.min,
|
|
688
|
+
max: snapshot.state.max,
|
|
689
|
+
explicitBounds: bounds,
|
|
690
|
+
bucketCounts: buckets,
|
|
691
|
+
};
|
|
692
|
+
if (previous?.histogram !== undefined) {
|
|
693
|
+
previous.histogram.dataPoints.push(point);
|
|
694
|
+
}
|
|
695
|
+
else {
|
|
696
|
+
metrics.push({
|
|
697
|
+
name: snapshot.id,
|
|
698
|
+
description,
|
|
699
|
+
unit,
|
|
700
|
+
histogram: { aggregationTemporality: 2, dataPoints: [point] },
|
|
701
|
+
});
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
else if (snapshot.type === "Frequency") {
|
|
705
|
+
const dataPoints = [];
|
|
706
|
+
for (const [key, value] of snapshot.state.occurrences) {
|
|
707
|
+
dataPoints.push({
|
|
708
|
+
...this.numberPoint(value, attributes, timeUnixNano),
|
|
709
|
+
attributes: [...attributes, { key: "key", value: { stringValue: key } }],
|
|
710
|
+
});
|
|
711
|
+
}
|
|
712
|
+
if (previous?.sum !== undefined) {
|
|
713
|
+
previous.sum.dataPoints.push(...dataPoints);
|
|
714
|
+
}
|
|
715
|
+
else {
|
|
716
|
+
metrics.push({
|
|
717
|
+
name: snapshot.id,
|
|
718
|
+
description,
|
|
719
|
+
unit,
|
|
720
|
+
sum: { aggregationTemporality: 2, isMonotonic: true, dataPoints },
|
|
721
|
+
});
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
else {
|
|
725
|
+
const derivedNames = [
|
|
726
|
+
`${snapshot.id}_quantiles`,
|
|
727
|
+
`${snapshot.id}_count`,
|
|
728
|
+
`${snapshot.id}_sum`,
|
|
729
|
+
];
|
|
730
|
+
for (const derivedName of derivedNames) {
|
|
731
|
+
const derivedDefinition = `${definitionIdentity}:${derivedName}`;
|
|
732
|
+
const derivedExisting = names.get(derivedName);
|
|
733
|
+
if (derivedExisting !== undefined && derivedExisting.definition !== derivedDefinition) {
|
|
734
|
+
throw metricError("EXPORT_FAILED", "flush", `Direct Effect summary "${snapshot.id}" conflicts with metric "${derivedName}". Rename one instrument before retrying.`, snapshot.id, false);
|
|
735
|
+
}
|
|
736
|
+
names.set(derivedName, { kind: "Summary", definition: derivedDefinition });
|
|
737
|
+
}
|
|
738
|
+
const dataPoints = [];
|
|
739
|
+
dataPoints.push({
|
|
740
|
+
...this.numberPoint(snapshot.state.min, attributes, timeUnixNano),
|
|
741
|
+
attributes: [...attributes, { key: "quantile", value: { stringValue: "min" } }],
|
|
742
|
+
});
|
|
743
|
+
for (const [quantile, value] of snapshot.state.quantiles) {
|
|
744
|
+
dataPoints.push({
|
|
745
|
+
...this.numberPoint(value ?? 0, attributes, timeUnixNano),
|
|
746
|
+
attributes: [
|
|
747
|
+
...attributes,
|
|
748
|
+
{ key: "quantile", value: { stringValue: quantile.toString() } },
|
|
749
|
+
],
|
|
750
|
+
});
|
|
751
|
+
}
|
|
752
|
+
dataPoints.push({
|
|
753
|
+
...this.numberPoint(snapshot.state.max, attributes, timeUnixNano),
|
|
754
|
+
attributes: [...attributes, { key: "quantile", value: { stringValue: "max" } }],
|
|
755
|
+
});
|
|
756
|
+
const countPoint = this.numberPoint(snapshot.state.count, attributes, timeUnixNano);
|
|
757
|
+
const sumPoint = this.numberPoint(snapshot.state.sum, attributes, timeUnixNano);
|
|
758
|
+
const existingQuantiles = metrics.find((metric) => metric.name === `${snapshot.id}_quantiles`);
|
|
759
|
+
const existingCount = metrics.find((metric) => metric.name === `${snapshot.id}_count`);
|
|
760
|
+
const existingSum = metrics.find((metric) => metric.name === `${snapshot.id}_sum`);
|
|
761
|
+
if (existingQuantiles?.sum !== undefined &&
|
|
762
|
+
existingCount?.sum !== undefined &&
|
|
763
|
+
existingSum?.sum !== undefined) {
|
|
764
|
+
existingQuantiles.sum.dataPoints.push(...dataPoints);
|
|
765
|
+
existingCount.sum.dataPoints.push(countPoint);
|
|
766
|
+
existingSum.sum.dataPoints.push(sumPoint);
|
|
767
|
+
}
|
|
768
|
+
else {
|
|
769
|
+
metrics.push({
|
|
770
|
+
name: `${snapshot.id}_quantiles`,
|
|
771
|
+
description,
|
|
772
|
+
unit,
|
|
773
|
+
sum: { aggregationTemporality: 2, isMonotonic: false, dataPoints },
|
|
774
|
+
}, {
|
|
775
|
+
name: `${snapshot.id}_count`,
|
|
776
|
+
description,
|
|
777
|
+
unit: "1",
|
|
778
|
+
sum: {
|
|
779
|
+
aggregationTemporality: 2,
|
|
780
|
+
isMonotonic: true,
|
|
781
|
+
dataPoints: [countPoint],
|
|
782
|
+
},
|
|
783
|
+
}, {
|
|
784
|
+
name: `${snapshot.id}_sum`,
|
|
785
|
+
description,
|
|
786
|
+
unit: "1",
|
|
787
|
+
sum: {
|
|
788
|
+
aggregationTemporality: 2,
|
|
789
|
+
isMonotonic: true,
|
|
790
|
+
dataPoints: [sumPoint],
|
|
791
|
+
},
|
|
792
|
+
});
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
numberPoint(value, attributes, timeUnixNano) {
|
|
797
|
+
const common = {
|
|
798
|
+
attributes,
|
|
799
|
+
startTimeUnixNano: this.startTimeUnixNano,
|
|
800
|
+
timeUnixNano,
|
|
801
|
+
};
|
|
802
|
+
if (Predicate.isBigInt(value)) {
|
|
803
|
+
return { ...common, asInt: Number(value) };
|
|
804
|
+
}
|
|
805
|
+
return { ...common, asDouble: value };
|
|
806
|
+
}
|
|
807
|
+
collectCounter(entry, timeUnixNano) {
|
|
808
|
+
const combined = new Map();
|
|
809
|
+
for (const [identity, series] of entry.residualSeries) {
|
|
810
|
+
combined.set(identity, { attributes: series.attributes, value: series.value });
|
|
811
|
+
}
|
|
812
|
+
for (const leaseSeries of entry.seriesByLease.values()) {
|
|
813
|
+
for (const [identity, series] of leaseSeries) {
|
|
814
|
+
const current = combined.get(identity);
|
|
815
|
+
if (current === undefined) {
|
|
816
|
+
combined.set(identity, { attributes: series.attributes, value: series.value });
|
|
817
|
+
}
|
|
818
|
+
else {
|
|
819
|
+
current.value += series.value;
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
return {
|
|
824
|
+
name: entry.definition.name,
|
|
825
|
+
description: entry.definition.description,
|
|
826
|
+
unit: entry.definition.unit,
|
|
827
|
+
sum: {
|
|
828
|
+
aggregationTemporality: 2,
|
|
829
|
+
isMonotonic: true,
|
|
830
|
+
dataPoints: Array.from(combined.values()).map((series) => ({
|
|
831
|
+
attributes: attributesToOtlp(series.attributes.values),
|
|
832
|
+
startTimeUnixNano: this.startTimeUnixNano,
|
|
833
|
+
timeUnixNano,
|
|
834
|
+
asDouble: series.value,
|
|
835
|
+
})),
|
|
836
|
+
},
|
|
837
|
+
};
|
|
838
|
+
}
|
|
839
|
+
collectHistogram(entry, timeUnixNano) {
|
|
840
|
+
const combined = new Map();
|
|
841
|
+
for (const [identity, series] of entry.residualSeries) {
|
|
842
|
+
combined.set(identity, {
|
|
843
|
+
attributes: series.attributes,
|
|
844
|
+
count: series.count,
|
|
845
|
+
sum: series.sum,
|
|
846
|
+
min: series.min,
|
|
847
|
+
max: series.max,
|
|
848
|
+
bucketCounts: [...series.bucketCounts],
|
|
849
|
+
});
|
|
850
|
+
}
|
|
851
|
+
for (const leaseSeries of entry.seriesByLease.values()) {
|
|
852
|
+
for (const [identity, series] of leaseSeries) {
|
|
853
|
+
const current = combined.get(identity);
|
|
854
|
+
if (current === undefined) {
|
|
855
|
+
combined.set(identity, {
|
|
856
|
+
attributes: series.attributes,
|
|
857
|
+
count: series.count,
|
|
858
|
+
sum: series.sum,
|
|
859
|
+
min: series.min,
|
|
860
|
+
max: series.max,
|
|
861
|
+
bucketCounts: [...series.bucketCounts],
|
|
862
|
+
});
|
|
863
|
+
}
|
|
864
|
+
else {
|
|
865
|
+
current.count += series.count;
|
|
866
|
+
current.sum += series.sum;
|
|
867
|
+
current.min = Math.min(current.min, series.min);
|
|
868
|
+
current.max = Math.max(current.max, series.max);
|
|
869
|
+
for (let index = 0; index < current.bucketCounts.length; index++) {
|
|
870
|
+
current.bucketCounts[index] =
|
|
871
|
+
(current.bucketCounts[index] ?? 0) + (series.bucketCounts[index] ?? 0);
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
return {
|
|
877
|
+
name: entry.definition.name,
|
|
878
|
+
description: entry.definition.description,
|
|
879
|
+
unit: entry.definition.unit,
|
|
880
|
+
histogram: {
|
|
881
|
+
aggregationTemporality: 2,
|
|
882
|
+
dataPoints: Array.from(combined.values()).map((series) => ({
|
|
883
|
+
attributes: attributesToOtlp(series.attributes.values),
|
|
884
|
+
startTimeUnixNano: this.startTimeUnixNano,
|
|
885
|
+
timeUnixNano,
|
|
886
|
+
count: series.count,
|
|
887
|
+
sum: series.sum,
|
|
888
|
+
min: series.min,
|
|
889
|
+
max: series.max,
|
|
890
|
+
explicitBounds: entry.definition.boundaries,
|
|
891
|
+
bucketCounts: series.bucketCounts,
|
|
892
|
+
})),
|
|
893
|
+
},
|
|
894
|
+
};
|
|
895
|
+
}
|
|
896
|
+
collectGauge(entry, timeUnixNano) {
|
|
897
|
+
const observations = [];
|
|
898
|
+
const failures = [];
|
|
899
|
+
const proposedIdentities = new Set();
|
|
900
|
+
for (const registration of entry.callbacks.values()) {
|
|
901
|
+
let callbackResult;
|
|
902
|
+
try {
|
|
903
|
+
callbackResult = registration.callback();
|
|
904
|
+
}
|
|
905
|
+
catch {
|
|
906
|
+
failures.push({
|
|
907
|
+
instrumentName: entry.definition.name,
|
|
908
|
+
code: "CALLBACK_FAILED",
|
|
909
|
+
message: `Observable gauge "${entry.definition.name}" callback failed and was omitted from this export.`,
|
|
910
|
+
});
|
|
911
|
+
continue;
|
|
912
|
+
}
|
|
913
|
+
let callbackObservations;
|
|
914
|
+
try {
|
|
915
|
+
callbackObservations = decodeGaugeObservations(callbackResult);
|
|
916
|
+
}
|
|
917
|
+
catch {
|
|
918
|
+
failures.push({
|
|
919
|
+
instrumentName: entry.definition.name,
|
|
920
|
+
code: "INVALID_OBSERVATION",
|
|
921
|
+
message: `Observable gauge "${entry.definition.name}" returned an invalid synchronous observation batch.`,
|
|
922
|
+
});
|
|
923
|
+
continue;
|
|
924
|
+
}
|
|
925
|
+
if (callbackObservations.length > maximumObservations) {
|
|
926
|
+
failures.push({
|
|
927
|
+
instrumentName: entry.definition.name,
|
|
928
|
+
code: "SERIES_LIMIT_EXCEEDED",
|
|
929
|
+
message: `Observable gauge "${entry.definition.name}" exceeds the ${maximumObservations}-observation collection limit.`,
|
|
930
|
+
});
|
|
931
|
+
continue;
|
|
932
|
+
}
|
|
933
|
+
const callbackBatch = [];
|
|
934
|
+
const callbackIdentities = new Set();
|
|
935
|
+
let callbackFailed = false;
|
|
936
|
+
for (const observation of callbackObservations) {
|
|
937
|
+
if (!Number.isFinite(observation.value)) {
|
|
938
|
+
failures.push({
|
|
939
|
+
instrumentName: entry.definition.name,
|
|
940
|
+
code: "INVALID_OBSERVATION",
|
|
941
|
+
message: `Observable gauge "${entry.definition.name}" produced a non-finite observation.`,
|
|
942
|
+
});
|
|
943
|
+
callbackFailed = true;
|
|
944
|
+
break;
|
|
945
|
+
}
|
|
946
|
+
try {
|
|
947
|
+
const attributes = parseAttributes(observation.attributes, "collectObservableGauge", entry.definition.name);
|
|
948
|
+
if (proposedIdentities.has(attributes.identity) ||
|
|
949
|
+
callbackIdentities.has(attributes.identity)) {
|
|
950
|
+
failures.push({
|
|
951
|
+
instrumentName: entry.definition.name,
|
|
952
|
+
code: "INVALID_OBSERVATION",
|
|
953
|
+
message: `Observable gauge "${entry.definition.name}" produced duplicate attribute sets.`,
|
|
954
|
+
});
|
|
955
|
+
callbackFailed = true;
|
|
956
|
+
break;
|
|
957
|
+
}
|
|
958
|
+
callbackIdentities.add(attributes.identity);
|
|
959
|
+
callbackBatch.push({ value: observation.value, attributes });
|
|
960
|
+
}
|
|
961
|
+
catch (cause) {
|
|
962
|
+
const code = cause instanceof MetricsError && cause.code === "LIMIT_EXCEEDED"
|
|
963
|
+
? "ATTRIBUTE_LIMIT_EXCEEDED"
|
|
964
|
+
: "INVALID_OBSERVATION";
|
|
965
|
+
failures.push({
|
|
966
|
+
instrumentName: entry.definition.name,
|
|
967
|
+
code,
|
|
968
|
+
message: `Observable gauge "${entry.definition.name}" produced invalid bounded attributes.`,
|
|
969
|
+
});
|
|
970
|
+
callbackFailed = true;
|
|
971
|
+
break;
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
if (!callbackFailed) {
|
|
975
|
+
for (const observation of callbackBatch) {
|
|
976
|
+
proposedIdentities.add(observation.attributes.identity);
|
|
977
|
+
observations.push(observation);
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
if (failures.length > 0) {
|
|
982
|
+
return { metric: undefined, failures };
|
|
983
|
+
}
|
|
984
|
+
const newIdentities = Array.from(proposedIdentities).filter((identity) => !entry.lifetimeSeries.has(identity));
|
|
985
|
+
const newRuntimeIdentities = newIdentities.filter((identity) => !this.runtimeLifetimeSeries.has(`${entry.definition.name}:${identity}`));
|
|
986
|
+
if (entry.lifetimeSeries.size + newIdentities.length > maximumInstrumentSeries ||
|
|
987
|
+
this.runtimeLifetimeSeries.size + newRuntimeIdentities.length > maximumRuntimeSeries) {
|
|
988
|
+
return {
|
|
989
|
+
metric: undefined,
|
|
990
|
+
failures: [
|
|
991
|
+
{
|
|
992
|
+
instrumentName: entry.definition.name,
|
|
993
|
+
code: "SERIES_LIMIT_EXCEEDED",
|
|
994
|
+
message: `Observable gauge "${entry.definition.name}" exceeds a lifetime series limit.`,
|
|
995
|
+
},
|
|
996
|
+
],
|
|
997
|
+
};
|
|
998
|
+
}
|
|
999
|
+
for (const identity of newIdentities) {
|
|
1000
|
+
this.commitSeries(entry, identity);
|
|
1001
|
+
}
|
|
1002
|
+
return {
|
|
1003
|
+
metric: {
|
|
1004
|
+
name: entry.definition.name,
|
|
1005
|
+
description: entry.definition.description,
|
|
1006
|
+
unit: entry.definition.unit,
|
|
1007
|
+
gauge: {
|
|
1008
|
+
dataPoints: observations.map((observation) => ({
|
|
1009
|
+
attributes: attributesToOtlp(observation.attributes.values),
|
|
1010
|
+
startTimeUnixNano: this.startTimeUnixNano,
|
|
1011
|
+
timeUnixNano,
|
|
1012
|
+
asDouble: observation.value,
|
|
1013
|
+
})),
|
|
1014
|
+
},
|
|
1015
|
+
},
|
|
1016
|
+
failures: [],
|
|
1017
|
+
};
|
|
1018
|
+
}
|
|
1019
|
+
foldCounterLease(entry, leaseId) {
|
|
1020
|
+
const leaseSeries = entry.seriesByLease.get(leaseId);
|
|
1021
|
+
if (leaseSeries === undefined) {
|
|
1022
|
+
return;
|
|
1023
|
+
}
|
|
1024
|
+
for (const [identity, series] of leaseSeries) {
|
|
1025
|
+
const residual = entry.residualSeries.get(identity);
|
|
1026
|
+
if (residual === undefined) {
|
|
1027
|
+
entry.residualSeries.set(identity, {
|
|
1028
|
+
attributes: series.attributes,
|
|
1029
|
+
value: series.value,
|
|
1030
|
+
});
|
|
1031
|
+
}
|
|
1032
|
+
else {
|
|
1033
|
+
residual.value += series.value;
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
entry.seriesByLease.delete(leaseId);
|
|
1037
|
+
}
|
|
1038
|
+
foldHistogramLease(entry, leaseId) {
|
|
1039
|
+
const leaseSeries = entry.seriesByLease.get(leaseId);
|
|
1040
|
+
if (leaseSeries === undefined) {
|
|
1041
|
+
return;
|
|
1042
|
+
}
|
|
1043
|
+
for (const [identity, series] of leaseSeries) {
|
|
1044
|
+
const residual = entry.residualSeries.get(identity);
|
|
1045
|
+
if (residual === undefined) {
|
|
1046
|
+
entry.residualSeries.set(identity, {
|
|
1047
|
+
attributes: series.attributes,
|
|
1048
|
+
count: series.count,
|
|
1049
|
+
sum: series.sum,
|
|
1050
|
+
min: series.min,
|
|
1051
|
+
max: series.max,
|
|
1052
|
+
bucketCounts: [...series.bucketCounts],
|
|
1053
|
+
});
|
|
1054
|
+
}
|
|
1055
|
+
else {
|
|
1056
|
+
residual.count += series.count;
|
|
1057
|
+
residual.sum += series.sum;
|
|
1058
|
+
residual.min = Math.min(residual.min, series.min);
|
|
1059
|
+
residual.max = Math.max(residual.max, series.max);
|
|
1060
|
+
for (let index = 0; index < residual.bucketCounts.length; index++) {
|
|
1061
|
+
residual.bucketCounts[index] =
|
|
1062
|
+
(residual.bucketCounts[index] ?? 0) + (series.bucketCounts[index] ?? 0);
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
entry.seriesByLease.delete(leaseId);
|
|
1067
|
+
}
|
|
1068
|
+
removeLeaseState(leaseId) {
|
|
1069
|
+
for (const [name, entry] of this.catalog) {
|
|
1070
|
+
entry.leases.delete(leaseId);
|
|
1071
|
+
if (entry.kind === "counter") {
|
|
1072
|
+
this.foldCounterLease(entry, leaseId);
|
|
1073
|
+
if (entry.leases.size === 0 && entry.residualSeries.size === 0) {
|
|
1074
|
+
this.catalog.delete(name);
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
else if (entry.kind === "histogram") {
|
|
1078
|
+
this.foldHistogramLease(entry, leaseId);
|
|
1079
|
+
if (entry.leases.size === 0 && entry.residualSeries.size === 0) {
|
|
1080
|
+
this.catalog.delete(name);
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
else {
|
|
1084
|
+
for (const [callbackId, registration] of entry.callbacks) {
|
|
1085
|
+
if (registration.leaseId === leaseId) {
|
|
1086
|
+
entry.callbacks.delete(callbackId);
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
if (entry.leases.size === 0) {
|
|
1090
|
+
this.catalog.delete(name);
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
const runtimePool = new Map();
|
|
1097
|
+
const fetchTransport = (endpoint) => async (payload, signal) => {
|
|
1098
|
+
const response = await fetch(endpoint, {
|
|
1099
|
+
method: "POST",
|
|
1100
|
+
headers: { "content-type": "application/json" },
|
|
1101
|
+
body: JSON.stringify(payload),
|
|
1102
|
+
signal,
|
|
1103
|
+
});
|
|
1104
|
+
if (!response.ok) {
|
|
1105
|
+
throw metricError("EXPORT_FAILED", "export", `OTLP metrics endpoint returned HTTP ${response.status}. Verify the endpoint and retry.`, undefined, true);
|
|
1106
|
+
}
|
|
1107
|
+
};
|
|
1108
|
+
const acquireRuntime = (options, transport) => {
|
|
1109
|
+
let state = runtimePool.get(options.poolKey);
|
|
1110
|
+
if (state === undefined) {
|
|
1111
|
+
state = new MetricsRuntimeState(options, () => {
|
|
1112
|
+
if (runtimePool.get(options.poolKey) === state) {
|
|
1113
|
+
runtimePool.delete(options.poolKey);
|
|
1114
|
+
}
|
|
1115
|
+
});
|
|
1116
|
+
runtimePool.set(options.poolKey, state);
|
|
1117
|
+
}
|
|
1118
|
+
return state.acquire(transport);
|
|
1119
|
+
};
|
|
1120
|
+
class ActiveMetrics {
|
|
1121
|
+
lease;
|
|
1122
|
+
timeoutMilliseconds;
|
|
1123
|
+
closed = false;
|
|
1124
|
+
closePromise;
|
|
1125
|
+
constructor(lease, timeoutMilliseconds) {
|
|
1126
|
+
this.lease = lease;
|
|
1127
|
+
this.timeoutMilliseconds = timeoutMilliseconds;
|
|
1128
|
+
}
|
|
1129
|
+
counter(definitionInput) {
|
|
1130
|
+
this.assertOpen("counter");
|
|
1131
|
+
const definition = parseDefinition(definitionInput, "counter");
|
|
1132
|
+
const entry = this.lease.state.registerCounter(this.lease.leaseId, definition);
|
|
1133
|
+
return {
|
|
1134
|
+
add: (value, attributes) => {
|
|
1135
|
+
this.assertOpen("add", definition.name);
|
|
1136
|
+
const parsedAttributes = parseAttributes(attributes, "add", definition.name);
|
|
1137
|
+
this.lease.state.addCounter(this.lease.leaseId, entry, value, parsedAttributes);
|
|
1138
|
+
},
|
|
1139
|
+
};
|
|
1140
|
+
}
|
|
1141
|
+
histogram(definitionInput) {
|
|
1142
|
+
this.assertOpen("histogram");
|
|
1143
|
+
const definition = parseHistogramDefinition(definitionInput);
|
|
1144
|
+
const entry = this.lease.state.registerHistogram(this.lease.leaseId, definition);
|
|
1145
|
+
return {
|
|
1146
|
+
record: (value, attributes) => {
|
|
1147
|
+
this.assertOpen("record", definition.name);
|
|
1148
|
+
const parsedAttributes = parseAttributes(attributes, "record", definition.name);
|
|
1149
|
+
this.lease.state.recordHistogram(this.lease.leaseId, entry, value, parsedAttributes);
|
|
1150
|
+
},
|
|
1151
|
+
};
|
|
1152
|
+
}
|
|
1153
|
+
observableGauge(definitionInput, callback) {
|
|
1154
|
+
this.assertOpen("observableGauge");
|
|
1155
|
+
const definition = parseDefinition(definitionInput, "observableGauge");
|
|
1156
|
+
if (!Predicate.isFunction(callback)) {
|
|
1157
|
+
throw metricError("INVALID_INSTRUMENT", "observableGauge", `Observable gauge "${definition.name}" requires a synchronous callback.`, definition.name, false);
|
|
1158
|
+
}
|
|
1159
|
+
const registered = this.lease.state.registerGauge(this.lease.leaseId, definition, callback);
|
|
1160
|
+
let unregistered = false;
|
|
1161
|
+
const unregister = () => {
|
|
1162
|
+
if (unregistered) {
|
|
1163
|
+
return;
|
|
1164
|
+
}
|
|
1165
|
+
unregistered = true;
|
|
1166
|
+
this.lease.state.unregisterGauge(definition.name, registered.callbackId);
|
|
1167
|
+
};
|
|
1168
|
+
return { unregister, [Symbol.dispose]: unregister };
|
|
1169
|
+
}
|
|
1170
|
+
flush() {
|
|
1171
|
+
this.assertOpen("flush");
|
|
1172
|
+
return this.lease.state.flush(this.timeoutMilliseconds);
|
|
1173
|
+
}
|
|
1174
|
+
close() {
|
|
1175
|
+
if (this.closePromise !== undefined) {
|
|
1176
|
+
return this.closePromise;
|
|
1177
|
+
}
|
|
1178
|
+
this.closed = true;
|
|
1179
|
+
this.closePromise = this.lease.state.closeLease(this.lease.leaseId, this.timeoutMilliseconds);
|
|
1180
|
+
return this.closePromise;
|
|
1181
|
+
}
|
|
1182
|
+
assertOpen(operation, instrumentName) {
|
|
1183
|
+
if (this.closed) {
|
|
1184
|
+
throw metricError("CLOSED", operation, "Metrics lifecycle is closed. Create a new lifecycle before recording or flushing metrics.", instrumentName, false);
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
class DisabledMetrics {
|
|
1189
|
+
closed = false;
|
|
1190
|
+
closePromise;
|
|
1191
|
+
counter(definitionInput) {
|
|
1192
|
+
this.assertOpen("counter");
|
|
1193
|
+
const definition = parseDefinition(definitionInput, "counter");
|
|
1194
|
+
return {
|
|
1195
|
+
add: (value, attributes) => {
|
|
1196
|
+
this.assertOpen("add", definition.name);
|
|
1197
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
1198
|
+
throw metricError("INVALID_MEASUREMENT", "add", `Counter "${definition.name}" accepts only finite values greater than or equal to zero.`, definition.name, false);
|
|
1199
|
+
}
|
|
1200
|
+
parseAttributes(attributes, "add", definition.name);
|
|
1201
|
+
},
|
|
1202
|
+
};
|
|
1203
|
+
}
|
|
1204
|
+
histogram(definitionInput) {
|
|
1205
|
+
this.assertOpen("histogram");
|
|
1206
|
+
const definition = parseHistogramDefinition(definitionInput);
|
|
1207
|
+
return {
|
|
1208
|
+
record: (value, attributes) => {
|
|
1209
|
+
this.assertOpen("record", definition.name);
|
|
1210
|
+
if (!Number.isFinite(value)) {
|
|
1211
|
+
throw metricError("INVALID_MEASUREMENT", "record", `Histogram "${definition.name}" accepts only finite values.`, definition.name, false);
|
|
1212
|
+
}
|
|
1213
|
+
parseAttributes(attributes, "record", definition.name);
|
|
1214
|
+
},
|
|
1215
|
+
};
|
|
1216
|
+
}
|
|
1217
|
+
observableGauge(definitionInput, callback) {
|
|
1218
|
+
this.assertOpen("observableGauge");
|
|
1219
|
+
const definition = parseDefinition(definitionInput, "observableGauge");
|
|
1220
|
+
if (!Predicate.isFunction(callback)) {
|
|
1221
|
+
throw metricError("INVALID_INSTRUMENT", "observableGauge", `Observable gauge "${definition.name}" requires a synchronous callback.`, definition.name, false);
|
|
1222
|
+
}
|
|
1223
|
+
const unregister = () => undefined;
|
|
1224
|
+
return { unregister, [Symbol.dispose]: unregister };
|
|
1225
|
+
}
|
|
1226
|
+
flush() {
|
|
1227
|
+
this.assertOpen("flush");
|
|
1228
|
+
return Promise.resolve({ gaugeFailures: [] });
|
|
1229
|
+
}
|
|
1230
|
+
close() {
|
|
1231
|
+
if (this.closePromise !== undefined) {
|
|
1232
|
+
return this.closePromise;
|
|
1233
|
+
}
|
|
1234
|
+
this.closed = true;
|
|
1235
|
+
this.closePromise = Promise.resolve({ gaugeFailures: [] });
|
|
1236
|
+
return this.closePromise;
|
|
1237
|
+
}
|
|
1238
|
+
assertOpen(operation, instrumentName) {
|
|
1239
|
+
if (this.closed) {
|
|
1240
|
+
throw metricError("CLOSED", operation, "Metrics lifecycle is closed. Create a new lifecycle before recording or flushing metrics.", instrumentName, false);
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
export const createStandaloneMetrics = async (optionsInput) => {
|
|
1245
|
+
const options = parseOptions(optionsInput);
|
|
1246
|
+
if (!options.enabled) {
|
|
1247
|
+
return new DisabledMetrics();
|
|
1248
|
+
}
|
|
1249
|
+
const lease = acquireRuntime(options, {
|
|
1250
|
+
kind: "fetch",
|
|
1251
|
+
send: fetchTransport(options.metricsEndpoint),
|
|
1252
|
+
});
|
|
1253
|
+
return new ActiveMetrics(lease, options.flushTimeoutMilliseconds);
|
|
1254
|
+
};
|
|
1255
|
+
const makeEffectTransport = (endpoint, client) => {
|
|
1256
|
+
const request = HttpClientRequest.post(endpoint, {
|
|
1257
|
+
headers: { "content-type": "application/json" },
|
|
1258
|
+
});
|
|
1259
|
+
return async (payload, signal) => {
|
|
1260
|
+
const program = client
|
|
1261
|
+
.execute(HttpClientRequest.setBody(request, HttpBody.jsonUnsafe(payload)))
|
|
1262
|
+
.pipe(Effect.flatMap((response) => response.status >= 200 && response.status < 300
|
|
1263
|
+
? Effect.void
|
|
1264
|
+
: Effect.fail(metricError("EXPORT_FAILED", "export", `OTLP metrics endpoint returned HTTP ${response.status}. Verify the endpoint and retry.`, undefined, true))), Effect.scoped);
|
|
1265
|
+
await Effect.runPromise(program, { signal });
|
|
1266
|
+
};
|
|
1267
|
+
};
|
|
1268
|
+
const makeMetricsRuntime = Effect.fn("makeMetricsRuntime")(function* (config, options) {
|
|
1269
|
+
const client = yield* HttpClient.HttpClient;
|
|
1270
|
+
const flusher = yield* OtlpExporter.Flusher;
|
|
1271
|
+
const parsed = parseOptions({
|
|
1272
|
+
serviceName: config.serviceName,
|
|
1273
|
+
serviceVersion: config.serviceVersion,
|
|
1274
|
+
environment: config.environment,
|
|
1275
|
+
otlpEndpoint: config.otlpEndpoint.toString(),
|
|
1276
|
+
flushTimeoutMilliseconds: options.shutdownTimeoutMilliseconds,
|
|
1277
|
+
});
|
|
1278
|
+
const lease = acquireRuntime(parsed, {
|
|
1279
|
+
kind: "layer",
|
|
1280
|
+
send: makeEffectTransport(parsed.metricsEndpoint, client),
|
|
1281
|
+
});
|
|
1282
|
+
yield* flusher.register(Effect.tryPromise(() => lease.state.flush(parsed.flushTimeoutMilliseconds)).pipe(Effect.catch(() => Effect.void), Effect.asVoid));
|
|
1283
|
+
yield* Effect.addFinalizer(() => Effect.tryPromise(() => lease.state.closeLease(lease.leaseId, parsed.flushTimeoutMilliseconds)).pipe(Effect.catch(() => Effect.void), Effect.asVoid));
|
|
1284
|
+
return lease.state.registry;
|
|
1285
|
+
});
|
|
1286
|
+
export const layerMetricsRuntime = (config, options) => Layer.effect(Metric.MetricRegistry, makeMetricsRuntime(config, options)).pipe(Layer.provideMerge(OtlpExporter.layerFlusher));
|