@equipe-tech/observability 0.2.1 → 0.3.5
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 +11 -9
- package/dist/BrowserEvents.d.ts +89 -12
- package/dist/BrowserEvents.js +67 -13
- package/dist/Correlation.d.ts +51 -0
- package/dist/Correlation.js +95 -0
- package/dist/Metrics.d.ts +19 -3
- package/dist/Metrics.js +16 -0
- package/dist/MetricsRuntime.d.ts +45 -3
- package/dist/MetricsRuntime.js +478 -120
- package/dist/PolicyOtlpLogger.d.ts +18 -0
- package/dist/PolicyOtlpLogger.js +142 -0
- package/dist/RedactionPolicy.d.ts +2 -9
- package/dist/RedactionPolicy.js +2 -246
- package/dist/ResourceIdentity.d.ts +47 -0
- package/dist/ResourceIdentity.js +82 -0
- package/dist/Telemetry.d.ts +18 -3
- package/dist/Telemetry.js +43 -25
- package/dist/TelemetryConfig.d.ts +11 -4
- package/dist/TelemetryConfig.js +38 -10
- package/dist/audit/AuditCommitDocument.d.ts +41 -0
- package/dist/audit/AuditCommitDocument.js +58 -0
- package/dist/audit/AuditDigest.d.ts +11 -0
- package/dist/audit/AuditDigest.js +25 -0
- package/dist/audit/AuditOutbox.d.ts +75 -0
- package/dist/audit/AuditOutbox.js +104 -0
- package/dist/audit/AuditPublisher.d.ts +49 -0
- package/dist/audit/AuditPublisher.js +60 -0
- package/dist/audit/AuditRecord.d.ts +128 -0
- package/dist/audit/AuditRecord.js +172 -0
- package/dist/audit/CommittedAuditRecordInternal.d.ts +11 -0
- package/dist/audit/CommittedAuditRecordInternal.js +13 -0
- package/dist/audit/InvalidAuditRecord.d.ts +12 -0
- package/dist/audit/InvalidAuditRecord.js +16 -0
- package/dist/audit/index.d.ts +7 -0
- package/dist/audit/index.js +7 -0
- package/dist/browser/BrowserClient.d.ts +82 -3
- package/dist/browser/BrowserClient.js +322 -28
- package/dist/browser/BrowserEventLimits.d.ts +8 -0
- package/dist/browser/BrowserEventLimits.js +8 -0
- package/dist/browser/BrowserMetricRecorder.d.ts +17 -0
- package/dist/browser/BrowserMetricRecorder.js +35 -0
- package/dist/browser/BrowserTelemetry.d.ts +40 -0
- package/dist/browser/BrowserTelemetry.js +105 -0
- package/dist/browser/ClientPolicy.d.ts +3 -0
- package/dist/browser/ClientPolicy.js +52 -0
- package/dist/browser/client.d.ts +3 -2
- package/dist/browser/client.js +2 -1
- package/dist/browser/index.d.ts +4 -37
- package/dist/browser/index.js +3 -95
- package/dist/contract/ContractCompatibility.d.ts +31 -0
- package/dist/contract/ContractCompatibility.js +274 -0
- package/dist/contract/ContractIndex.d.ts +54 -0
- package/dist/contract/ContractIndex.js +178 -0
- package/dist/contract/ContractSurface.d.ts +111 -0
- package/dist/contract/ContractSurface.js +116 -0
- package/dist/contract/EventName.d.ts +6 -0
- package/dist/contract/EventName.js +55 -0
- package/dist/contract/EventProducer.d.ts +123 -0
- package/dist/contract/EventProducer.js +209 -0
- package/dist/contract/MetricContractError.d.ts +19 -0
- package/dist/contract/MetricContractError.js +19 -0
- package/dist/contract/MetricDefinition.d.ts +61 -0
- package/dist/contract/MetricDefinition.js +91 -0
- package/dist/contract/MetricProducer.d.ts +42 -0
- package/dist/contract/MetricProducer.js +123 -0
- package/dist/contract/OrganizationEvents.d.ts +165 -0
- package/dist/contract/OrganizationEvents.js +97 -0
- package/dist/contract/TelemetryContract.d.ts +95 -0
- package/dist/contract/TelemetryContract.js +451 -0
- package/dist/contract/TelemetryContractError.d.ts +44 -0
- package/dist/contract/TelemetryContractError.js +64 -0
- package/dist/contract/TelemetryEvent.d.ts +59 -0
- package/dist/contract/TelemetryEvent.js +54 -0
- package/dist/contract/index.d.ts +11 -0
- package/dist/contract/index.js +11 -0
- package/dist/effect/WideEvent.d.ts +3 -0
- package/dist/effect/WideEvent.js +20 -0
- package/dist/effect/WideEventSink.d.ts +3 -0
- package/dist/effect/WideEventSink.js +79 -0
- package/dist/effect/index.d.ts +2 -0
- package/dist/effect/index.js +2 -0
- package/dist/index.d.ts +17 -2
- package/dist/index.js +11 -2
- package/dist/metrics/InstrumentGrammar.d.ts +5 -0
- package/dist/metrics/InstrumentGrammar.js +12 -0
- package/dist/metrics/MetricScalar.d.ts +3 -0
- package/dist/metrics/MetricScalar.js +5 -0
- package/dist/node/AuditDigest.d.ts +3 -0
- package/dist/node/AuditDigest.js +6 -0
- package/dist/node/BrowserEventIngest.d.ts +7 -2
- package/dist/node/BrowserEventIngest.js +111 -19
- package/dist/node/LocalCollector.d.ts +20 -0
- package/dist/node/LocalCollector.js +188 -0
- package/dist/node/Observability.d.ts +49 -0
- package/dist/node/Observability.js +190 -0
- package/dist/node/Runtime.d.ts +4 -1
- package/dist/node/index.d.ts +4 -0
- package/dist/node/index.js +3 -0
- package/dist/policy/BrowserFieldPolicy.d.ts +7 -0
- package/dist/policy/BrowserFieldPolicy.js +298 -0
- package/dist/policy/DataPolicy.d.ts +19 -0
- package/dist/policy/DataPolicy.js +254 -0
- package/dist/policy/DataPolicyError.d.ts +22 -0
- package/dist/policy/DataPolicyError.js +25 -0
- package/dist/policy/Deduplication.d.ts +13 -0
- package/dist/policy/Deduplication.js +44 -0
- package/dist/policy/DefectEnvelope.d.ts +25 -0
- package/dist/policy/DefectEnvelope.js +52 -0
- package/dist/policy/MetricLabelPolicy.d.ts +5 -0
- package/dist/policy/MetricLabelPolicy.js +65 -0
- package/dist/policy/PolicyTransform.d.ts +16 -0
- package/dist/policy/PolicyTransform.js +136 -0
- package/dist/policy/PolicyVocabulary.d.ts +10 -0
- package/dist/policy/PolicyVocabulary.js +84 -0
- package/dist/policy/ResourceAttributePolicy.d.ts +9 -0
- package/dist/policy/ResourceAttributePolicy.js +33 -0
- package/dist/policy/entrypoint.d.ts +2 -0
- package/dist/policy/entrypoint.js +2 -0
- package/dist/policy/index.d.ts +17 -0
- package/dist/policy/index.js +9 -0
- package/dist/profile/DataPolicy.d.ts +2 -0
- package/dist/profile/DataPolicy.js +1 -0
- package/dist/profile/EnvironmentPolicy.d.ts +22 -0
- package/dist/profile/EnvironmentPolicy.js +41 -0
- package/dist/profile/LifecycleRegistry.d.ts +24 -0
- package/dist/profile/LifecycleRegistry.js +240 -0
- package/dist/profile/ObservabilityAdapter.d.ts +115 -0
- package/dist/profile/ObservabilityAdapter.js +50 -0
- package/dist/profile/ObservabilityConfig.d.ts +68 -0
- package/dist/profile/ObservabilityConfig.js +125 -0
- package/dist/profile/ObservabilityConfigError.d.ts +23 -0
- package/dist/profile/ObservabilityConfigError.js +39 -0
- package/dist/profile/ObservabilityProfile.d.ts +49 -0
- package/dist/profile/ObservabilityProfile.js +121 -0
- package/dist/profile/ReactWebProfile.d.ts +8 -0
- package/dist/profile/ReactWebProfile.js +7 -0
- package/dist/profile/index.d.ts +5 -0
- package/dist/profile/index.js +5 -0
- package/dist/testing/conformance/ConformanceAssertions.d.ts +5 -0
- package/dist/testing/conformance/ConformanceAssertions.js +31 -0
- package/dist/testing/conformance/ConformanceFailure.d.ts +35 -0
- package/dist/testing/conformance/ConformanceFailure.js +56 -0
- package/dist/testing/conformance/ConformanceModel.d.ts +97 -0
- package/dist/testing/conformance/ConformanceModel.js +8 -0
- package/dist/testing/conformance/ConformanceRegistry.d.ts +13 -0
- package/dist/testing/conformance/ConformanceRegistry.js +190 -0
- package/dist/testing/conformance/ConformanceRunner.d.ts +5 -0
- package/dist/testing/conformance/ConformanceRunner.js +198 -0
- package/dist/testing/conformance/TelemetryEvidence.d.ts +91 -0
- package/dist/testing/conformance/TelemetryEvidence.js +370 -0
- package/dist/testing/conformance/index.d.ts +10 -0
- package/dist/testing/conformance/index.js +6 -0
- package/dist/testing/contract.d.ts +81 -0
- package/dist/testing/contract.js +93 -0
- package/dist/testing/deployedCanary.d.ts +1 -0
- package/dist/testing/deployedCanary.js +1 -0
- package/dist/testing/index.d.ts +40 -3
- package/dist/testing/index.js +122 -5
- package/dist/trace/HttpServerOtlpTracer.d.ts +42 -0
- package/dist/trace/HttpServerOtlpTracer.js +301 -0
- package/package.json +16 -29
- package/dist/WideEvent.d.ts +0 -5
- package/dist/WideEvent.js +0 -6
- package/dist/nestjs/BrowserEventsController.d.ts +0 -21
- package/dist/nestjs/BrowserEventsController.js +0 -52
- package/dist/nestjs/HttpRoutePolicy.d.ts +0 -23
- package/dist/nestjs/HttpRoutePolicy.js +0 -179
- package/dist/nestjs/HttpServerOtlpTracer.d.ts +0 -15
- package/dist/nestjs/HttpServerOtlpTracer.js +0 -154
- package/dist/nestjs/RequestWideEventTraceCorrelation.d.ts +0 -15
- package/dist/nestjs/RequestWideEventTraceCorrelation.js +0 -13
- package/dist/nestjs/TelemetryInterceptor.d.ts +0 -31
- package/dist/nestjs/TelemetryInterceptor.js +0 -268
- package/dist/nestjs/TelemetryModule.d.ts +0 -53
- package/dist/nestjs/TelemetryModule.js +0 -428
- package/dist/nestjs/index.d.ts +0 -5
- package/dist/nestjs/index.js +0 -4
package/dist/MetricsRuntime.js
CHANGED
|
@@ -1,22 +1,18 @@
|
|
|
1
|
-
import { Context, Effect, Layer, Metric, Predicate, Schema } from "effect";
|
|
1
|
+
import { Context, Effect, Layer, Metric, Option, Predicate, Result, Schema } from "effect";
|
|
2
2
|
import { HttpBody, HttpClient, HttpClientRequest } from "effect/unstable/http";
|
|
3
3
|
import { OtlpExporter } from "effect/unstable/observability";
|
|
4
4
|
import { MetricsError } from "./Metrics.js";
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
return true;
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
|
-
return false;
|
|
15
|
-
};
|
|
5
|
+
import { InvalidMetricMeasurement } from "./contract/MetricContractError.js";
|
|
6
|
+
import { EnvironmentAliasPolicy as EnvironmentAliasPolicySchema, parseResourceIdentity, serviceResourceAttributes, } from "./ResourceIdentity.js";
|
|
7
|
+
import { baseDataPolicy } from "./policy/DataPolicy.js";
|
|
8
|
+
import { metricLabelRejection } from "./policy/MetricLabelPolicy.js";
|
|
9
|
+
import { isFiniteMetricScalar, metricScalarIdentity } from "./metrics/MetricScalar.js";
|
|
10
|
+
import { containsControlCharacter, instrumentNamePattern, maximumMetricAttributeCardinality, maximumMetricAttributes, unitPattern, } from "./metrics/InstrumentGrammar.js";
|
|
16
11
|
const maximumInstruments = 100;
|
|
17
12
|
const maximumInstrumentSeries = 1_000;
|
|
13
|
+
const maximumAttributeCardinality = maximumMetricAttributeCardinality;
|
|
18
14
|
const maximumRuntimeSeries = 10_000;
|
|
19
|
-
const maximumAttributes =
|
|
15
|
+
const maximumAttributes = maximumMetricAttributes;
|
|
20
16
|
const maximumCallbacks = 16;
|
|
21
17
|
const maximumObservations = 100;
|
|
22
18
|
const MetricAttributeInput = Schema.Struct({
|
|
@@ -44,6 +40,7 @@ const MetricsOptionsInput = Schema.Struct({
|
|
|
44
40
|
serviceName: Schema.String,
|
|
45
41
|
serviceVersion: Schema.String,
|
|
46
42
|
environment: Schema.String,
|
|
43
|
+
deploymentEnvironmentAlias: EnvironmentAliasPolicySchema.pipe(Schema.optionalKey),
|
|
47
44
|
otlpEndpoint: Schema.String,
|
|
48
45
|
exportIntervalMilliseconds: Schema.Number.pipe(Schema.optionalKey),
|
|
49
46
|
flushTimeoutMilliseconds: Schema.Number.pipe(Schema.optionalKey),
|
|
@@ -53,20 +50,26 @@ const decodeInstrumentDefinition = Schema.decodeUnknownSync(InstrumentDefinition
|
|
|
53
50
|
const decodeHistogramDefinition = Schema.decodeUnknownSync(HistogramDefinitionInput);
|
|
54
51
|
const decodeGaugeObservations = Schema.decodeUnknownSync(GaugeObservationsInput);
|
|
55
52
|
const decodeMetricsOptions = Schema.decodeUnknownSync(MetricsOptionsInput);
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
53
|
+
const gaugeObservationContracts = new WeakMap();
|
|
54
|
+
export const registerContractGaugeObservation = (observation, contract) => {
|
|
55
|
+
gaugeObservationContracts.set(observation, contract);
|
|
56
|
+
return observation;
|
|
57
|
+
};
|
|
58
|
+
const metricError = (code, operation, message, instrumentName, retryable, cause, attributeKey, policyReason) => {
|
|
59
|
+
const options = { code, operation, message, retryable };
|
|
60
|
+
if (cause !== undefined)
|
|
61
|
+
options.cause = cause;
|
|
62
|
+
if (attributeKey !== undefined)
|
|
63
|
+
options.attributeKey = attributeKey;
|
|
64
|
+
if (policyReason !== undefined)
|
|
65
|
+
options.policyReason = policyReason;
|
|
64
66
|
if (instrumentName === undefined) {
|
|
65
67
|
return new MetricsError(options);
|
|
66
68
|
}
|
|
67
69
|
return new MetricsError({ ...options, instrumentName });
|
|
68
70
|
};
|
|
69
71
|
const parseOptions = (input) => {
|
|
72
|
+
const policy = input.policy ?? baseDataPolicy;
|
|
70
73
|
let options;
|
|
71
74
|
try {
|
|
72
75
|
options = decodeMetricsOptions(input);
|
|
@@ -74,11 +77,25 @@ const parseOptions = (input) => {
|
|
|
74
77
|
catch (cause) {
|
|
75
78
|
throw metricError("INVALID_CONFIGURATION", "createMetrics", "Metrics configuration is invalid. Provide valid service metadata and an HTTP OTLP endpoint.", undefined, false, cause);
|
|
76
79
|
}
|
|
77
|
-
|
|
78
|
-
options.
|
|
79
|
-
options.
|
|
80
|
-
|
|
80
|
+
const parsedIdentity = Effect.runSync(Effect.result(parseResourceIdentity({
|
|
81
|
+
serviceName: options.serviceName,
|
|
82
|
+
serviceVersion: options.serviceVersion,
|
|
83
|
+
environment: options.environment,
|
|
84
|
+
instance: Option.none(),
|
|
85
|
+
})));
|
|
86
|
+
if (Result.isFailure(parsedIdentity)) {
|
|
87
|
+
const failure = parsedIdentity.failure;
|
|
88
|
+
throw new MetricsError({
|
|
89
|
+
code: "INVALID_CONFIGURATION",
|
|
90
|
+
operation: "createMetrics",
|
|
91
|
+
message: failure.message,
|
|
92
|
+
field: failure.field,
|
|
93
|
+
rule: failure.rule,
|
|
94
|
+
retryable: false,
|
|
95
|
+
cause: failure,
|
|
96
|
+
});
|
|
81
97
|
}
|
|
98
|
+
const identity = parsedIdentity.success;
|
|
82
99
|
let endpoint;
|
|
83
100
|
try {
|
|
84
101
|
endpoint = new URL(options.otlpEndpoint);
|
|
@@ -103,23 +120,31 @@ const parseOptions = (input) => {
|
|
|
103
120
|
endpoint.search = "";
|
|
104
121
|
endpoint.hash = "";
|
|
105
122
|
const enabled = options.enabled ?? true;
|
|
123
|
+
const policyKey = JSON.stringify([
|
|
124
|
+
Array.from(policy.attributes.entries()),
|
|
125
|
+
policy.blockedKeys.map((pattern) => [pattern.source, pattern.flags]),
|
|
126
|
+
policy.blockedValuePatterns.map((pattern) => [pattern.source, pattern.flags]),
|
|
127
|
+
]);
|
|
128
|
+
const environmentAlias = options.deploymentEnvironmentAlias ?? "omitted";
|
|
106
129
|
const poolKey = JSON.stringify([
|
|
107
130
|
endpoint.toString(),
|
|
108
131
|
options.serviceName,
|
|
109
132
|
options.serviceVersion,
|
|
110
133
|
options.environment,
|
|
134
|
+
options.deploymentEnvironmentAlias ?? "omitted",
|
|
111
135
|
exportIntervalMilliseconds,
|
|
112
136
|
flushTimeoutMilliseconds,
|
|
137
|
+
policyKey,
|
|
113
138
|
]);
|
|
114
139
|
return {
|
|
115
140
|
enabled,
|
|
116
|
-
|
|
117
|
-
serviceVersion: options.serviceVersion,
|
|
118
|
-
environment: options.environment,
|
|
141
|
+
identity,
|
|
119
142
|
metricsEndpoint: endpoint.toString(),
|
|
120
143
|
exportIntervalMilliseconds,
|
|
121
144
|
flushTimeoutMilliseconds,
|
|
122
145
|
poolKey,
|
|
146
|
+
policy,
|
|
147
|
+
resourceAttributes: new Map(Object.entries(serviceResourceAttributes(identity, environmentAlias))),
|
|
123
148
|
};
|
|
124
149
|
};
|
|
125
150
|
const parseDefinition = (input, operation) => {
|
|
@@ -161,16 +186,7 @@ const parseHistogramDefinition = (input) => {
|
|
|
161
186
|
}
|
|
162
187
|
return { ...common, boundaries: [...definition.boundaries] };
|
|
163
188
|
};
|
|
164
|
-
const
|
|
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) => {
|
|
189
|
+
const parseAttributes = (input, operation, instrumentName, policy) => {
|
|
174
190
|
let attributes;
|
|
175
191
|
try {
|
|
176
192
|
attributes = decodeMetricAttributes(input ?? []);
|
|
@@ -184,18 +200,19 @@ const parseAttributes = (input, operation, instrumentName) => {
|
|
|
184
200
|
const keys = new Set();
|
|
185
201
|
const normalized = [];
|
|
186
202
|
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
203
|
if (attribute.key.length > 128 ||
|
|
191
204
|
!instrumentNamePattern.test(attribute.key) ||
|
|
192
205
|
attribute.key === "unit" ||
|
|
193
206
|
attribute.key === "time_unit" ||
|
|
207
|
+
attribute.key === "service.instance.id" ||
|
|
194
208
|
keys.has(attribute.key) ||
|
|
195
|
-
|
|
196
|
-
stringIsInvalid) {
|
|
209
|
+
!isFiniteMetricScalar(attribute.value)) {
|
|
197
210
|
throw metricError("INVALID_MEASUREMENT", operation, `Metric "${instrumentName}" attributes are invalid. Use unique bounded keys and finite scalar values.`, instrumentName, false);
|
|
198
211
|
}
|
|
212
|
+
const rejection = metricLabelRejection(policy, attribute.key, attribute.value);
|
|
213
|
+
if (rejection !== undefined) {
|
|
214
|
+
throw metricError("POLICY_BLOCKED", operation, `Metric "${instrumentName}" contains a label blocked by the data policy. Remove the label before recording.`, instrumentName, false, undefined, undefined, rejection);
|
|
215
|
+
}
|
|
199
216
|
keys.add(attribute.key);
|
|
200
217
|
normalized.push({
|
|
201
218
|
key: attribute.key,
|
|
@@ -205,7 +222,7 @@ const parseAttributes = (input, operation, instrumentName) => {
|
|
|
205
222
|
normalized.sort((left, right) => left.key.localeCompare(right.key));
|
|
206
223
|
return {
|
|
207
224
|
identity: normalized
|
|
208
|
-
.map((attribute) => `${attribute.key}=${
|
|
225
|
+
.map((attribute) => `${attribute.key}=${metricScalarIdentity(attribute.value)}`)
|
|
209
226
|
.join("|"),
|
|
210
227
|
values: normalized,
|
|
211
228
|
};
|
|
@@ -222,17 +239,31 @@ const attributesToOtlp = (attributes) => attributes.map((attribute) => {
|
|
|
222
239
|
}
|
|
223
240
|
return { key: attribute.key, value: { doubleValue: attribute.value } };
|
|
224
241
|
});
|
|
225
|
-
const directAttributesToOtlp = (attributes) => {
|
|
242
|
+
const directAttributesToOtlp = (attributes, instrumentName, policy) => {
|
|
226
243
|
if (attributes === undefined) {
|
|
227
|
-
return [];
|
|
244
|
+
return { values: [], failures: [] };
|
|
228
245
|
}
|
|
229
246
|
const values = [];
|
|
247
|
+
const failures = [];
|
|
230
248
|
for (const [key, value] of Object.entries(attributes)) {
|
|
231
|
-
if (key
|
|
232
|
-
|
|
249
|
+
if (key === "unit" || key === "time_unit")
|
|
250
|
+
continue;
|
|
251
|
+
if (key === "service.instance.id") {
|
|
252
|
+
throw metricError("EXPORT_FAILED", "flush", `Metric "${instrumentName}" cannot use service.instance.id as a datapoint attribute. Remove the reserved key before retrying.`, instrumentName, false);
|
|
233
253
|
}
|
|
254
|
+
const rejection = metricLabelRejection(policy, key, value);
|
|
255
|
+
if (rejection !== undefined) {
|
|
256
|
+
failures.push({
|
|
257
|
+
instrumentName,
|
|
258
|
+
code: "POLICY_BLOCKED",
|
|
259
|
+
message: `Metric "${instrumentName}" dropped a label blocked by the data policy.`,
|
|
260
|
+
policyReason: rejection,
|
|
261
|
+
});
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
values.push({ key, value: { stringValue: value } });
|
|
234
265
|
}
|
|
235
|
-
return values;
|
|
266
|
+
return { values, failures };
|
|
236
267
|
};
|
|
237
268
|
const sameDefinition = (entry, kind, definition, boundaries) => {
|
|
238
269
|
if (entry.kind !== kind ||
|
|
@@ -261,10 +292,13 @@ const histogramBucketIndex = (boundaries, value) => {
|
|
|
261
292
|
const nanosNow = () => String(BigInt(Date.now()) * 1000000n);
|
|
262
293
|
class MetricsRuntimeState {
|
|
263
294
|
registry = new Map();
|
|
295
|
+
policy;
|
|
264
296
|
directContext = Context.make(Metric.MetricRegistry, this.registry);
|
|
265
297
|
catalog = new Map();
|
|
266
298
|
runtimeLifetimeSeries = new Set();
|
|
267
299
|
lifetimeSeriesByInstrument = new Map();
|
|
300
|
+
lifetimeValuesByInstrumentKey = new Map();
|
|
301
|
+
contractValuesByDefinition = new WeakMap();
|
|
268
302
|
lifetimeInstrumentNames = new Set();
|
|
269
303
|
leases = new Set();
|
|
270
304
|
transports = new Map();
|
|
@@ -279,6 +313,7 @@ class MetricsRuntimeState {
|
|
|
279
313
|
periodicFailureActive = false;
|
|
280
314
|
constructor(options, removeFromPool) {
|
|
281
315
|
this.options = options;
|
|
316
|
+
this.policy = options.policy;
|
|
282
317
|
this.removeFromPool = removeFromPool;
|
|
283
318
|
this.timer = setInterval(() => {
|
|
284
319
|
this.scheduleExport(this.options.flushTimeoutMilliseconds).then((result) => this.recordPeriodicOutcome(result.gaugeFailures.length > 0), () => this.recordPeriodicOutcome(true));
|
|
@@ -298,18 +333,19 @@ class MetricsRuntimeState {
|
|
|
298
333
|
flush(timeoutMilliseconds) {
|
|
299
334
|
return this.scheduleExport(timeoutMilliseconds);
|
|
300
335
|
}
|
|
301
|
-
async closeLease(leaseId, timeoutMilliseconds) {
|
|
302
|
-
let result;
|
|
336
|
+
async closeLease(leaseId, timeoutMilliseconds, exportBeforeClose = true) {
|
|
337
|
+
let result = { gaugeFailures: [] };
|
|
303
338
|
let failure;
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
339
|
+
if (exportBeforeClose) {
|
|
340
|
+
try {
|
|
341
|
+
result = await this.scheduleExport(timeoutMilliseconds);
|
|
342
|
+
}
|
|
343
|
+
catch (cause) {
|
|
344
|
+
failure =
|
|
345
|
+
cause instanceof MetricsError
|
|
346
|
+
? cause
|
|
347
|
+
: metricError("EXPORT_FAILED", "close", "The final metrics export failed. The runtime lease was still released.", undefined, true, cause);
|
|
348
|
+
}
|
|
313
349
|
}
|
|
314
350
|
this.removeLeaseState(leaseId);
|
|
315
351
|
this.leases.delete(leaseId);
|
|
@@ -417,25 +453,137 @@ class MetricsRuntimeState {
|
|
|
417
453
|
this.catalog.delete(name);
|
|
418
454
|
}
|
|
419
455
|
}
|
|
420
|
-
|
|
456
|
+
prepareCounter(leaseId, entry, value, attributes) {
|
|
421
457
|
this.assertLease(leaseId, "add", entry.definition.name);
|
|
422
458
|
if (!Number.isFinite(value) || value < 0) {
|
|
423
459
|
throw metricError("INVALID_MEASUREMENT", "add", `Counter "${entry.definition.name}" accepts only finite values greater than or equal to zero.`, entry.definition.name, false);
|
|
424
460
|
}
|
|
425
461
|
this.prepareSeries(entry, attributes, "add");
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
leaseSeries
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
462
|
+
return () => {
|
|
463
|
+
let leaseSeries = entry.seriesByLease.get(leaseId);
|
|
464
|
+
if (leaseSeries === undefined) {
|
|
465
|
+
leaseSeries = new Map();
|
|
466
|
+
entry.seriesByLease.set(leaseId, leaseSeries);
|
|
467
|
+
}
|
|
468
|
+
const current = leaseSeries.get(attributes.identity);
|
|
469
|
+
if (current === undefined) {
|
|
470
|
+
leaseSeries.set(attributes.identity, { attributes, value });
|
|
471
|
+
this.commitSeries(entry, attributes);
|
|
472
|
+
}
|
|
473
|
+
else {
|
|
474
|
+
current.value += value;
|
|
475
|
+
}
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
reserveCounterBatch(leaseId, measurements) {
|
|
479
|
+
const projectedInstrumentNames = new Set(this.lifetimeInstrumentNames);
|
|
480
|
+
const projectedRuntimeSeries = new Set(this.runtimeLifetimeSeries);
|
|
481
|
+
const projectedSeriesByInstrument = new Map();
|
|
482
|
+
const projectedValuesByInstrumentKey = new Map();
|
|
483
|
+
const entries = new Map();
|
|
484
|
+
const prepared = [];
|
|
485
|
+
for (const measurement of measurements) {
|
|
486
|
+
this.assertLease(leaseId, "add", measurement.definition.name);
|
|
487
|
+
let entry = entries.get(measurement.definition.name);
|
|
488
|
+
if (entry === undefined) {
|
|
489
|
+
const existing = this.catalog.get(measurement.definition.name);
|
|
490
|
+
if (existing !== undefined) {
|
|
491
|
+
if (existing.kind !== "counter" ||
|
|
492
|
+
!sameDefinition(existing, "counter", measurement.definition, undefined)) {
|
|
493
|
+
throw this.instrumentConflict("counter", measurement.definition.name);
|
|
494
|
+
}
|
|
495
|
+
entry = existing;
|
|
496
|
+
}
|
|
497
|
+
else {
|
|
498
|
+
if (!projectedInstrumentNames.has(measurement.definition.name)) {
|
|
499
|
+
if (projectedInstrumentNames.size >= maximumInstruments) {
|
|
500
|
+
throw metricError("LIMIT_EXCEEDED", "registerInstrument", `Metric runtime exceeds the ${maximumInstruments}-instrument lifetime limit while registering "${measurement.definition.name}".`, measurement.definition.name, false);
|
|
501
|
+
}
|
|
502
|
+
projectedInstrumentNames.add(measurement.definition.name);
|
|
503
|
+
}
|
|
504
|
+
entry = {
|
|
505
|
+
kind: "counter",
|
|
506
|
+
definition: measurement.definition,
|
|
507
|
+
leases: new Set(),
|
|
508
|
+
residualSeries: new Map(),
|
|
509
|
+
seriesByLease: new Map(),
|
|
510
|
+
lifetimeSeries: this.lifetimeSeriesByInstrument.get(measurement.definition.name) ?? new Set(),
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
entries.set(measurement.definition.name, entry);
|
|
514
|
+
}
|
|
515
|
+
else if (!sameDefinition(entry, "counter", measurement.definition, undefined)) {
|
|
516
|
+
throw this.instrumentConflict("counter", measurement.definition.name);
|
|
517
|
+
}
|
|
518
|
+
if (!Number.isFinite(measurement.value) || measurement.value < 0) {
|
|
519
|
+
throw metricError("INVALID_MEASUREMENT", "add", `Counter "${entry.definition.name}" accepts only finite values greater than or equal to zero.`, entry.definition.name, false);
|
|
520
|
+
}
|
|
521
|
+
for (const attribute of measurement.attributes.values) {
|
|
522
|
+
const key = `${entry.definition.name}:${attribute.key}`;
|
|
523
|
+
let values = projectedValuesByInstrumentKey.get(key);
|
|
524
|
+
if (values === undefined) {
|
|
525
|
+
values = new Set(this.lifetimeValuesByInstrumentKey.get(key));
|
|
526
|
+
projectedValuesByInstrumentKey.set(key, values);
|
|
527
|
+
}
|
|
528
|
+
const identity = metricScalarIdentity(attribute.value);
|
|
529
|
+
if (!values.has(identity) && values.size >= maximumAttributeCardinality) {
|
|
530
|
+
throw metricError("LIMIT_EXCEEDED", "add", `Metric "${entry.definition.name}" exceeds the ${maximumAttributeCardinality}-value lifetime limit for one label.`, entry.definition.name, false, undefined, attribute.key);
|
|
531
|
+
}
|
|
532
|
+
values.add(identity);
|
|
533
|
+
}
|
|
534
|
+
let series = projectedSeriesByInstrument.get(entry.definition.name);
|
|
535
|
+
if (series === undefined) {
|
|
536
|
+
series = new Set(entry.lifetimeSeries);
|
|
537
|
+
projectedSeriesByInstrument.set(entry.definition.name, series);
|
|
538
|
+
}
|
|
539
|
+
if (!series.has(measurement.attributes.identity)) {
|
|
540
|
+
if (series.size >= maximumInstrumentSeries) {
|
|
541
|
+
throw metricError("LIMIT_EXCEEDED", "add", `Metric "${entry.definition.name}" exceeds the ${maximumInstrumentSeries}-series lifetime limit.`, entry.definition.name, false);
|
|
542
|
+
}
|
|
543
|
+
series.add(measurement.attributes.identity);
|
|
544
|
+
}
|
|
545
|
+
const runtimeIdentity = `${entry.definition.name}:${measurement.attributes.identity}`;
|
|
546
|
+
if (!projectedRuntimeSeries.has(runtimeIdentity)) {
|
|
547
|
+
if (projectedRuntimeSeries.size >= maximumRuntimeSeries) {
|
|
548
|
+
throw metricError("LIMIT_EXCEEDED", "add", `Metric runtime exceeds the ${maximumRuntimeSeries}-series lifetime limit.`, entry.definition.name, false);
|
|
549
|
+
}
|
|
550
|
+
projectedRuntimeSeries.add(runtimeIdentity);
|
|
551
|
+
}
|
|
552
|
+
prepared.push({ entry, value: measurement.value, attributes: measurement.attributes });
|
|
435
553
|
}
|
|
436
|
-
|
|
437
|
-
|
|
554
|
+
for (const [name, entry] of entries) {
|
|
555
|
+
this.lifetimeInstrumentNames.add(name);
|
|
556
|
+
entry.leases.add(leaseId);
|
|
557
|
+
if (!this.lifetimeSeriesByInstrument.has(name)) {
|
|
558
|
+
this.lifetimeSeriesByInstrument.set(name, entry.lifetimeSeries);
|
|
559
|
+
}
|
|
560
|
+
if (!this.catalog.has(name))
|
|
561
|
+
this.catalog.set(name, entry);
|
|
438
562
|
}
|
|
563
|
+
for (const measurement of prepared)
|
|
564
|
+
this.commitSeries(measurement.entry, measurement.attributes);
|
|
565
|
+
return () => {
|
|
566
|
+
for (const measurement of prepared) {
|
|
567
|
+
let leaseSeries = measurement.entry.seriesByLease.get(leaseId);
|
|
568
|
+
if (leaseSeries === undefined) {
|
|
569
|
+
leaseSeries = new Map();
|
|
570
|
+
measurement.entry.seriesByLease.set(leaseId, leaseSeries);
|
|
571
|
+
}
|
|
572
|
+
const current = leaseSeries.get(measurement.attributes.identity);
|
|
573
|
+
if (current === undefined) {
|
|
574
|
+
leaseSeries.set(measurement.attributes.identity, {
|
|
575
|
+
attributes: measurement.attributes,
|
|
576
|
+
value: measurement.value,
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
else {
|
|
580
|
+
current.value += measurement.value;
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
addCounter(leaseId, entry, value, attributes) {
|
|
586
|
+
this.prepareCounter(leaseId, entry, value, attributes)();
|
|
439
587
|
}
|
|
440
588
|
recordHistogram(leaseId, entry, value, attributes) {
|
|
441
589
|
this.assertLease(leaseId, "record", entry.definition.name);
|
|
@@ -461,7 +609,7 @@ class MetricsRuntimeState {
|
|
|
461
609
|
max: value,
|
|
462
610
|
bucketCounts,
|
|
463
611
|
});
|
|
464
|
-
this.commitSeries(entry, attributes
|
|
612
|
+
this.commitSeries(entry, attributes);
|
|
465
613
|
}
|
|
466
614
|
else {
|
|
467
615
|
current.count++;
|
|
@@ -509,6 +657,16 @@ class MetricsRuntimeState {
|
|
|
509
657
|
}
|
|
510
658
|
prepareSeries(entry, attributes, operation) {
|
|
511
659
|
const runtimeIdentity = `${entry.definition.name}:${attributes.identity}`;
|
|
660
|
+
for (const attribute of attributes.values) {
|
|
661
|
+
const key = `${entry.definition.name}:${attribute.key}`;
|
|
662
|
+
const values = this.lifetimeValuesByInstrumentKey.get(key);
|
|
663
|
+
const identity = metricScalarIdentity(attribute.value);
|
|
664
|
+
if (values !== undefined &&
|
|
665
|
+
!values.has(identity) &&
|
|
666
|
+
values.size >= maximumAttributeCardinality) {
|
|
667
|
+
throw metricError("LIMIT_EXCEEDED", operation, `Metric "${entry.definition.name}" exceeds the ${maximumAttributeCardinality}-value lifetime limit for one label.`, entry.definition.name, false, undefined, attribute.key);
|
|
668
|
+
}
|
|
669
|
+
}
|
|
512
670
|
if (!entry.lifetimeSeries.has(attributes.identity) &&
|
|
513
671
|
entry.lifetimeSeries.size >= maximumInstrumentSeries) {
|
|
514
672
|
throw metricError("LIMIT_EXCEEDED", operation, `Metric "${entry.definition.name}" exceeds the ${maximumInstrumentSeries}-series lifetime limit.`, entry.definition.name, false);
|
|
@@ -519,9 +677,18 @@ class MetricsRuntimeState {
|
|
|
519
677
|
}
|
|
520
678
|
return runtimeIdentity;
|
|
521
679
|
}
|
|
522
|
-
commitSeries(entry,
|
|
523
|
-
entry.lifetimeSeries.add(identity);
|
|
524
|
-
this.runtimeLifetimeSeries.add(`${entry.definition.name}:${identity}`);
|
|
680
|
+
commitSeries(entry, attributes) {
|
|
681
|
+
entry.lifetimeSeries.add(attributes.identity);
|
|
682
|
+
this.runtimeLifetimeSeries.add(`${entry.definition.name}:${attributes.identity}`);
|
|
683
|
+
for (const attribute of attributes.values) {
|
|
684
|
+
const key = `${entry.definition.name}:${attribute.key}`;
|
|
685
|
+
let values = this.lifetimeValuesByInstrumentKey.get(key);
|
|
686
|
+
if (values === undefined) {
|
|
687
|
+
values = new Set();
|
|
688
|
+
this.lifetimeValuesByInstrumentKey.set(key, values);
|
|
689
|
+
}
|
|
690
|
+
values.add(metricScalarIdentity(attribute.value));
|
|
691
|
+
}
|
|
525
692
|
}
|
|
526
693
|
transportForExport() {
|
|
527
694
|
let selected;
|
|
@@ -574,10 +741,10 @@ class MetricsRuntimeState {
|
|
|
574
741
|
const timeUnixNano = nanosNow();
|
|
575
742
|
const metrics = [];
|
|
576
743
|
const names = new Map();
|
|
744
|
+
const gaugeFailures = [];
|
|
577
745
|
for (const snapshot of Metric.snapshotUnsafe(this.directContext)) {
|
|
578
|
-
this.appendDirectMetric(metrics, names, snapshot, timeUnixNano);
|
|
746
|
+
this.appendDirectMetric(metrics, names, snapshot, timeUnixNano, gaugeFailures);
|
|
579
747
|
}
|
|
580
|
-
const gaugeFailures = [];
|
|
581
748
|
for (const entry of this.catalog.values()) {
|
|
582
749
|
const existing = names.get(entry.definition.name);
|
|
583
750
|
const definitionIdentity = `${entry.kind}:${entry.definition.unit}:${entry.definition.description}`;
|
|
@@ -604,19 +771,15 @@ class MetricsRuntimeState {
|
|
|
604
771
|
resourceMetrics: [
|
|
605
772
|
{
|
|
606
773
|
resource: {
|
|
607
|
-
attributes: [
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
key: "deployment.environment.name",
|
|
612
|
-
value: { stringValue: this.options.environment },
|
|
613
|
-
},
|
|
614
|
-
],
|
|
774
|
+
attributes: Array.from(this.options.resourceAttributes, ([key, value]) => ({
|
|
775
|
+
key,
|
|
776
|
+
value: { stringValue: value },
|
|
777
|
+
})),
|
|
615
778
|
droppedAttributesCount: 0,
|
|
616
779
|
},
|
|
617
780
|
scopeMetrics: [
|
|
618
781
|
{
|
|
619
|
-
scope: { name: this.options.serviceName },
|
|
782
|
+
scope: { name: this.options.identity.serviceName },
|
|
620
783
|
metrics,
|
|
621
784
|
},
|
|
622
785
|
],
|
|
@@ -626,7 +789,7 @@ class MetricsRuntimeState {
|
|
|
626
789
|
result: { gaugeFailures },
|
|
627
790
|
};
|
|
628
791
|
}
|
|
629
|
-
appendDirectMetric(metrics, names, snapshot, timeUnixNano) {
|
|
792
|
+
appendDirectMetric(metrics, names, snapshot, timeUnixNano, failures) {
|
|
630
793
|
const unit = snapshot.attributes?.unit ?? snapshot.attributes?.time_unit ?? "1";
|
|
631
794
|
const description = snapshot.description ?? "";
|
|
632
795
|
const definitionIdentity = `${snapshot.type}:${unit}:${description}`;
|
|
@@ -634,7 +797,9 @@ class MetricsRuntimeState {
|
|
|
634
797
|
if (existing !== undefined && existing.definition !== definitionIdentity) {
|
|
635
798
|
throw metricError("EXPORT_FAILED", "flush", `Direct Effect metric "${snapshot.id}" has incompatible definitions. Rename or align the definitions before retrying.`, snapshot.id, false);
|
|
636
799
|
}
|
|
637
|
-
const
|
|
800
|
+
const policyAttributes = directAttributesToOtlp(snapshot.attributes, snapshot.id, this.options.policy);
|
|
801
|
+
const attributes = policyAttributes.values;
|
|
802
|
+
failures.push(...policyAttributes.failures);
|
|
638
803
|
const previous = metrics.find((metric) => metric.name === snapshot.id);
|
|
639
804
|
names.set(snapshot.id, { kind: snapshot.type, definition: definitionIdentity });
|
|
640
805
|
if (snapshot.type === "Counter") {
|
|
@@ -896,18 +1061,29 @@ class MetricsRuntimeState {
|
|
|
896
1061
|
collectGauge(entry, timeUnixNano) {
|
|
897
1062
|
const observations = [];
|
|
898
1063
|
const failures = [];
|
|
1064
|
+
const contractTransaction = new MetricContractTransaction();
|
|
899
1065
|
const proposedIdentities = new Set();
|
|
900
1066
|
for (const registration of entry.callbacks.values()) {
|
|
901
1067
|
let callbackResult;
|
|
902
1068
|
try {
|
|
903
1069
|
callbackResult = registration.callback();
|
|
904
1070
|
}
|
|
905
|
-
catch {
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
1071
|
+
catch (cause) {
|
|
1072
|
+
if (cause instanceof InvalidMetricMeasurement) {
|
|
1073
|
+
failures.push({
|
|
1074
|
+
instrumentName: entry.definition.name,
|
|
1075
|
+
code: "CONTRACT_REJECTED",
|
|
1076
|
+
message: `Observable gauge "${entry.definition.name}" callback violated its metric contract and was omitted from this export.`,
|
|
1077
|
+
contractReason: cause.code,
|
|
1078
|
+
});
|
|
1079
|
+
}
|
|
1080
|
+
else {
|
|
1081
|
+
failures.push({
|
|
1082
|
+
instrumentName: entry.definition.name,
|
|
1083
|
+
code: "CALLBACK_FAILED",
|
|
1084
|
+
message: `Observable gauge "${entry.definition.name}" callback failed and was omitted from this export.`,
|
|
1085
|
+
});
|
|
1086
|
+
}
|
|
911
1087
|
continue;
|
|
912
1088
|
}
|
|
913
1089
|
let callbackObservations;
|
|
@@ -933,7 +1109,7 @@ class MetricsRuntimeState {
|
|
|
933
1109
|
const callbackBatch = [];
|
|
934
1110
|
const callbackIdentities = new Set();
|
|
935
1111
|
let callbackFailed = false;
|
|
936
|
-
for (const observation of callbackObservations) {
|
|
1112
|
+
for (const [index, observation] of callbackObservations.entries()) {
|
|
937
1113
|
if (!Number.isFinite(observation.value)) {
|
|
938
1114
|
failures.push({
|
|
939
1115
|
instrumentName: entry.definition.name,
|
|
@@ -944,7 +1120,7 @@ class MetricsRuntimeState {
|
|
|
944
1120
|
break;
|
|
945
1121
|
}
|
|
946
1122
|
try {
|
|
947
|
-
const attributes = parseAttributes(observation.attributes, "collectObservableGauge", entry.definition.name);
|
|
1123
|
+
const attributes = parseAttributes(observation.attributes, "collectObservableGauge", entry.definition.name, this.policy);
|
|
948
1124
|
if (proposedIdentities.has(attributes.identity) ||
|
|
949
1125
|
callbackIdentities.has(attributes.identity)) {
|
|
950
1126
|
failures.push({
|
|
@@ -956,17 +1132,29 @@ class MetricsRuntimeState {
|
|
|
956
1132
|
break;
|
|
957
1133
|
}
|
|
958
1134
|
callbackIdentities.add(attributes.identity);
|
|
1135
|
+
const contract = gaugeObservationContracts.get(callbackResult[index] ?? observation);
|
|
1136
|
+
if (contract !== undefined)
|
|
1137
|
+
contractTransaction.prepare(contract);
|
|
959
1138
|
callbackBatch.push({ value: observation.value, attributes });
|
|
960
1139
|
}
|
|
961
1140
|
catch (cause) {
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
}
|
|
1141
|
+
if (cause instanceof InvalidMetricMeasurement) {
|
|
1142
|
+
failures.push({
|
|
1143
|
+
instrumentName: entry.definition.name,
|
|
1144
|
+
code: "CONTRACT_REJECTED",
|
|
1145
|
+
message: `Observable gauge "${entry.definition.name}" callback violated its metric contract and was omitted from this export.`,
|
|
1146
|
+
contractReason: cause.code,
|
|
1147
|
+
});
|
|
1148
|
+
}
|
|
1149
|
+
else {
|
|
1150
|
+
failures.push({
|
|
1151
|
+
instrumentName: entry.definition.name,
|
|
1152
|
+
code: cause instanceof MetricsError && cause.code === "LIMIT_EXCEEDED"
|
|
1153
|
+
? "ATTRIBUTE_LIMIT_EXCEEDED"
|
|
1154
|
+
: "INVALID_OBSERVATION",
|
|
1155
|
+
message: `Observable gauge "${entry.definition.name}" produced invalid bounded attributes.`,
|
|
1156
|
+
});
|
|
1157
|
+
}
|
|
970
1158
|
callbackFailed = true;
|
|
971
1159
|
break;
|
|
972
1160
|
}
|
|
@@ -997,8 +1185,12 @@ class MetricsRuntimeState {
|
|
|
997
1185
|
};
|
|
998
1186
|
}
|
|
999
1187
|
for (const identity of newIdentities) {
|
|
1000
|
-
|
|
1188
|
+
const observation = observations.find((candidate) => candidate.attributes.identity === identity);
|
|
1189
|
+
if (observation !== undefined) {
|
|
1190
|
+
this.commitSeries(entry, observation.attributes);
|
|
1191
|
+
}
|
|
1001
1192
|
}
|
|
1193
|
+
contractTransaction.commit();
|
|
1002
1194
|
return {
|
|
1003
1195
|
metric: {
|
|
1004
1196
|
name: entry.definition.name,
|
|
@@ -1126,6 +1318,19 @@ class ActiveMetrics {
|
|
|
1126
1318
|
this.lease = lease;
|
|
1127
1319
|
this.timeoutMilliseconds = timeoutMilliseconds;
|
|
1128
1320
|
}
|
|
1321
|
+
reserveCounterBatch(measurements) {
|
|
1322
|
+
this.assertOpen("counter");
|
|
1323
|
+
const normalized = measurements.map((measurement) => {
|
|
1324
|
+
const definition = parseDefinition(measurement.definition, "counter");
|
|
1325
|
+
this.assertOpen("add", definition.name);
|
|
1326
|
+
return {
|
|
1327
|
+
definition,
|
|
1328
|
+
value: measurement.value,
|
|
1329
|
+
attributes: parseAttributes(measurement.attributes, "add", definition.name, this.lease.state.policy),
|
|
1330
|
+
};
|
|
1331
|
+
});
|
|
1332
|
+
return this.lease.state.reserveCounterBatch(this.lease.leaseId, normalized);
|
|
1333
|
+
}
|
|
1129
1334
|
counter(definitionInput) {
|
|
1130
1335
|
this.assertOpen("counter");
|
|
1131
1336
|
const definition = parseDefinition(definitionInput, "counter");
|
|
@@ -1133,7 +1338,7 @@ class ActiveMetrics {
|
|
|
1133
1338
|
return {
|
|
1134
1339
|
add: (value, attributes) => {
|
|
1135
1340
|
this.assertOpen("add", definition.name);
|
|
1136
|
-
const parsedAttributes = parseAttributes(attributes, "add", definition.name);
|
|
1341
|
+
const parsedAttributes = parseAttributes(attributes, "add", definition.name, this.lease.state.policy);
|
|
1137
1342
|
this.lease.state.addCounter(this.lease.leaseId, entry, value, parsedAttributes);
|
|
1138
1343
|
},
|
|
1139
1344
|
};
|
|
@@ -1145,7 +1350,7 @@ class ActiveMetrics {
|
|
|
1145
1350
|
return {
|
|
1146
1351
|
record: (value, attributes) => {
|
|
1147
1352
|
this.assertOpen("record", definition.name);
|
|
1148
|
-
const parsedAttributes = parseAttributes(attributes, "record", definition.name);
|
|
1353
|
+
const parsedAttributes = parseAttributes(attributes, "record", definition.name, this.lease.state.policy);
|
|
1149
1354
|
this.lease.state.recordHistogram(this.lease.leaseId, entry, value, parsedAttributes);
|
|
1150
1355
|
},
|
|
1151
1356
|
};
|
|
@@ -1179,6 +1384,13 @@ class ActiveMetrics {
|
|
|
1179
1384
|
this.closePromise = this.lease.state.closeLease(this.lease.leaseId, this.timeoutMilliseconds);
|
|
1180
1385
|
return this.closePromise;
|
|
1181
1386
|
}
|
|
1387
|
+
releaseWithoutFlush() {
|
|
1388
|
+
if (this.closePromise !== undefined)
|
|
1389
|
+
return this.closePromise;
|
|
1390
|
+
this.closed = true;
|
|
1391
|
+
this.closePromise = this.lease.state.closeLease(this.lease.leaseId, this.timeoutMilliseconds, false);
|
|
1392
|
+
return this.closePromise;
|
|
1393
|
+
}
|
|
1182
1394
|
assertOpen(operation, instrumentName) {
|
|
1183
1395
|
if (this.closed) {
|
|
1184
1396
|
throw metricError("CLOSED", operation, "Metrics lifecycle is closed. Create a new lifecycle before recording or flushing metrics.", instrumentName, false);
|
|
@@ -1186,8 +1398,24 @@ class ActiveMetrics {
|
|
|
1186
1398
|
}
|
|
1187
1399
|
}
|
|
1188
1400
|
class DisabledMetrics {
|
|
1401
|
+
policy;
|
|
1189
1402
|
closed = false;
|
|
1190
1403
|
closePromise;
|
|
1404
|
+
constructor(policy) {
|
|
1405
|
+
this.policy = policy;
|
|
1406
|
+
}
|
|
1407
|
+
reserveCounterBatch(measurements) {
|
|
1408
|
+
for (const measurement of measurements) {
|
|
1409
|
+
this.assertOpen("counter");
|
|
1410
|
+
const definition = parseDefinition(measurement.definition, "counter");
|
|
1411
|
+
this.assertOpen("add", definition.name);
|
|
1412
|
+
if (!Number.isFinite(measurement.value) || measurement.value < 0) {
|
|
1413
|
+
throw metricError("INVALID_MEASUREMENT", "add", `Counter "${definition.name}" accepts only finite values greater than or equal to zero.`, definition.name, false);
|
|
1414
|
+
}
|
|
1415
|
+
parseAttributes(measurement.attributes, "add", definition.name, this.policy);
|
|
1416
|
+
}
|
|
1417
|
+
return () => undefined;
|
|
1418
|
+
}
|
|
1191
1419
|
counter(definitionInput) {
|
|
1192
1420
|
this.assertOpen("counter");
|
|
1193
1421
|
const definition = parseDefinition(definitionInput, "counter");
|
|
@@ -1197,7 +1425,7 @@ class DisabledMetrics {
|
|
|
1197
1425
|
if (!Number.isFinite(value) || value < 0) {
|
|
1198
1426
|
throw metricError("INVALID_MEASUREMENT", "add", `Counter "${definition.name}" accepts only finite values greater than or equal to zero.`, definition.name, false);
|
|
1199
1427
|
}
|
|
1200
|
-
parseAttributes(attributes, "add", definition.name);
|
|
1428
|
+
parseAttributes(attributes, "add", definition.name, this.policy);
|
|
1201
1429
|
},
|
|
1202
1430
|
};
|
|
1203
1431
|
}
|
|
@@ -1210,7 +1438,7 @@ class DisabledMetrics {
|
|
|
1210
1438
|
if (!Number.isFinite(value)) {
|
|
1211
1439
|
throw metricError("INVALID_MEASUREMENT", "record", `Histogram "${definition.name}" accepts only finite values.`, definition.name, false);
|
|
1212
1440
|
}
|
|
1213
|
-
parseAttributes(attributes, "record", definition.name);
|
|
1441
|
+
parseAttributes(attributes, "record", definition.name, this.policy);
|
|
1214
1442
|
},
|
|
1215
1443
|
};
|
|
1216
1444
|
}
|
|
@@ -1241,17 +1469,128 @@ class DisabledMetrics {
|
|
|
1241
1469
|
}
|
|
1242
1470
|
}
|
|
1243
1471
|
}
|
|
1472
|
+
const metricContractStates = new WeakMap();
|
|
1473
|
+
class MetricContractTransaction {
|
|
1474
|
+
valuesByLedger = new Map();
|
|
1475
|
+
prepare(contract) {
|
|
1476
|
+
const state = metricContractStates.get(contract.metrics);
|
|
1477
|
+
if (state === undefined) {
|
|
1478
|
+
throw unsupportedMetricsFacade(contract.metricAlias, contract.metricName);
|
|
1479
|
+
}
|
|
1480
|
+
let stagedByDefinition = this.valuesByLedger.get(state.values);
|
|
1481
|
+
if (stagedByDefinition === undefined) {
|
|
1482
|
+
stagedByDefinition = new Map();
|
|
1483
|
+
this.valuesByLedger.set(state.values, stagedByDefinition);
|
|
1484
|
+
}
|
|
1485
|
+
let stagedValues = stagedByDefinition.get(contract.definitionIdentity);
|
|
1486
|
+
if (stagedValues === undefined) {
|
|
1487
|
+
stagedValues = new Map();
|
|
1488
|
+
stagedByDefinition.set(contract.definitionIdentity, stagedValues);
|
|
1489
|
+
}
|
|
1490
|
+
prepareContractValues(stagedValues, state.values.get(contract.definitionIdentity), contract.metricAlias, contract.metricName, contract.attributes, contract.limits);
|
|
1491
|
+
}
|
|
1492
|
+
commit() {
|
|
1493
|
+
for (const [ledger, stagedByDefinition] of this.valuesByLedger) {
|
|
1494
|
+
for (const [definitionIdentity, stagedValues] of stagedByDefinition) {
|
|
1495
|
+
let committedByAttribute = ledger.get(definitionIdentity);
|
|
1496
|
+
if (committedByAttribute === undefined) {
|
|
1497
|
+
committedByAttribute = new Map();
|
|
1498
|
+
ledger.set(definitionIdentity, committedByAttribute);
|
|
1499
|
+
}
|
|
1500
|
+
for (const [attributeName, values] of stagedValues) {
|
|
1501
|
+
let committedValues = committedByAttribute.get(attributeName);
|
|
1502
|
+
if (committedValues === undefined) {
|
|
1503
|
+
committedValues = new Set();
|
|
1504
|
+
committedByAttribute.set(attributeName, committedValues);
|
|
1505
|
+
}
|
|
1506
|
+
for (const identity of values)
|
|
1507
|
+
committedValues.add(identity);
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
const unsupportedMetricsFacade = (metricAlias, metricName) => new InvalidMetricMeasurement({
|
|
1514
|
+
code: "OBS_METRIC_INVALID_VALUE",
|
|
1515
|
+
operation: "record",
|
|
1516
|
+
message: `Metric alias "${metricAlias}" is bound to an unsupported metrics facade. Use createMetrics or a Node observability handle.`,
|
|
1517
|
+
metricAlias,
|
|
1518
|
+
metricName,
|
|
1519
|
+
});
|
|
1520
|
+
export const reserveMetricCounterBatch = (metrics, measurements) => {
|
|
1521
|
+
const contractTransaction = new MetricContractTransaction();
|
|
1522
|
+
for (const measurement of measurements)
|
|
1523
|
+
contractTransaction.prepare(measurement.admission);
|
|
1524
|
+
if (metrics instanceof ActiveMetrics || metrics instanceof DisabledMetrics) {
|
|
1525
|
+
const commit = metrics.reserveCounterBatch(measurements);
|
|
1526
|
+
contractTransaction.commit();
|
|
1527
|
+
return commit;
|
|
1528
|
+
}
|
|
1529
|
+
const measurement = measurements[0];
|
|
1530
|
+
throw unsupportedMetricsFacade(measurement?.definition.name ?? "counter", measurement?.definition.name ?? "counter");
|
|
1531
|
+
};
|
|
1532
|
+
const prepareContractValues = (stagedValues, committedValues, metricAlias, metricName, attributes, limits) => {
|
|
1533
|
+
for (const limit of limits) {
|
|
1534
|
+
const attribute = attributes.find((candidate) => candidate.key === limit.attributeName);
|
|
1535
|
+
if (attribute === undefined)
|
|
1536
|
+
continue;
|
|
1537
|
+
let values = stagedValues.get(limit.attributeName);
|
|
1538
|
+
if (values === undefined) {
|
|
1539
|
+
values = new Set(committedValues?.get(limit.attributeName));
|
|
1540
|
+
stagedValues.set(limit.attributeName, values);
|
|
1541
|
+
}
|
|
1542
|
+
const identity = metricScalarIdentity(attribute.value);
|
|
1543
|
+
if (!values.has(identity) && values.size >= limit.maximumCardinality) {
|
|
1544
|
+
throw new InvalidMetricMeasurement({
|
|
1545
|
+
code: "OBS_METRIC_CARDINALITY_EXCEEDED",
|
|
1546
|
+
operation: "record",
|
|
1547
|
+
message: `Metric "${metricName}" attribute "${limit.attributeName}" exceeds its declared maximum cardinality of ${limit.maximumCardinality}. Reuse a declared value or revise the contract.`,
|
|
1548
|
+
metricAlias,
|
|
1549
|
+
metricName,
|
|
1550
|
+
attributeName: limit.attributeName,
|
|
1551
|
+
});
|
|
1552
|
+
}
|
|
1553
|
+
values.add(identity);
|
|
1554
|
+
}
|
|
1555
|
+
};
|
|
1556
|
+
export const prepareContractMetricBatch = (admissions) => {
|
|
1557
|
+
const transaction = new MetricContractTransaction();
|
|
1558
|
+
for (const admission of admissions)
|
|
1559
|
+
transaction.prepare(admission);
|
|
1560
|
+
return () => transaction.commit();
|
|
1561
|
+
};
|
|
1562
|
+
export const prepareContractMetricAttributes = (metrics, metricAlias, metricName, definitionIdentity, attributes, limits) => prepareContractMetricBatch([
|
|
1563
|
+
{ metrics, metricAlias, metricName, definitionIdentity, attributes, limits },
|
|
1564
|
+
]);
|
|
1565
|
+
export const releaseMetricsLease = async (metrics) => {
|
|
1566
|
+
if (metrics instanceof ActiveMetrics) {
|
|
1567
|
+
await metrics.releaseWithoutFlush();
|
|
1568
|
+
return;
|
|
1569
|
+
}
|
|
1570
|
+
await metrics.close();
|
|
1571
|
+
};
|
|
1572
|
+
export const createDisabledMetrics = (policy = baseDataPolicy) => {
|
|
1573
|
+
const metrics = new DisabledMetrics(policy);
|
|
1574
|
+
metricContractStates.set(metrics, { values: new WeakMap() });
|
|
1575
|
+
return metrics;
|
|
1576
|
+
};
|
|
1244
1577
|
export const createStandaloneMetrics = async (optionsInput) => {
|
|
1245
1578
|
const options = parseOptions(optionsInput);
|
|
1246
1579
|
if (!options.enabled) {
|
|
1247
|
-
return
|
|
1580
|
+
return createDisabledMetrics(options.policy);
|
|
1248
1581
|
}
|
|
1249
1582
|
const lease = acquireRuntime(options, {
|
|
1250
1583
|
kind: "fetch",
|
|
1251
1584
|
send: fetchTransport(options.metricsEndpoint),
|
|
1252
1585
|
});
|
|
1253
|
-
|
|
1586
|
+
const metrics = new ActiveMetrics(lease, options.flushTimeoutMilliseconds);
|
|
1587
|
+
metricContractStates.set(metrics, {
|
|
1588
|
+
values: lease.state.contractValuesByDefinition,
|
|
1589
|
+
});
|
|
1590
|
+
return metrics;
|
|
1254
1591
|
};
|
|
1592
|
+
export class LayerMetricsRuntime extends Context.Service()("@equipe-tech/observability/internal/LayerMetricsRuntime") {
|
|
1593
|
+
}
|
|
1255
1594
|
const makeEffectTransport = (endpoint, client) => {
|
|
1256
1595
|
const request = HttpClientRequest.post(endpoint, {
|
|
1257
1596
|
headers: { "content-type": "application/json" },
|
|
@@ -1269,18 +1608,37 @@ const makeMetricsRuntime = Effect.fn("makeMetricsRuntime")(function* (config, op
|
|
|
1269
1608
|
const client = yield* HttpClient.HttpClient;
|
|
1270
1609
|
const flusher = yield* OtlpExporter.Flusher;
|
|
1271
1610
|
const parsed = parseOptions({
|
|
1272
|
-
serviceName: config.serviceName,
|
|
1273
|
-
serviceVersion: config.serviceVersion,
|
|
1274
|
-
environment: config.environment,
|
|
1611
|
+
serviceName: config.identity.serviceName,
|
|
1612
|
+
serviceVersion: config.identity.serviceVersion,
|
|
1613
|
+
environment: config.identity.environment,
|
|
1614
|
+
deploymentEnvironmentAlias: config.environmentAlias,
|
|
1275
1615
|
otlpEndpoint: config.otlpEndpoint.toString(),
|
|
1276
1616
|
flushTimeoutMilliseconds: options.shutdownTimeoutMilliseconds,
|
|
1617
|
+
policy: options.policy,
|
|
1277
1618
|
});
|
|
1278
|
-
const
|
|
1619
|
+
const resourceAttributes = new Map(options.resourceAttributes);
|
|
1620
|
+
resourceAttributes.delete("service.instance.id");
|
|
1621
|
+
const parsedResourceKey = JSON.stringify(Array.from(parsed.resourceAttributes));
|
|
1622
|
+
const resourceKey = JSON.stringify(Array.from(resourceAttributes));
|
|
1623
|
+
const normalized = {
|
|
1624
|
+
...parsed,
|
|
1625
|
+
poolKey: resourceKey === parsedResourceKey
|
|
1626
|
+
? parsed.poolKey
|
|
1627
|
+
: JSON.stringify([parsed.poolKey, resourceKey]),
|
|
1628
|
+
resourceAttributes,
|
|
1629
|
+
};
|
|
1630
|
+
const transport = {
|
|
1279
1631
|
kind: "layer",
|
|
1280
|
-
send: makeEffectTransport(
|
|
1281
|
-
}
|
|
1632
|
+
send: makeEffectTransport(normalized.metricsEndpoint, client),
|
|
1633
|
+
};
|
|
1634
|
+
const lease = acquireRuntime(normalized, transport);
|
|
1282
1635
|
yield* flusher.register(Effect.tryPromise(() => lease.state.flush(parsed.flushTimeoutMilliseconds)).pipe(Effect.catch(() => Effect.void), Effect.asVoid));
|
|
1283
1636
|
yield* Effect.addFinalizer(() => Effect.tryPromise(() => lease.state.closeLease(lease.leaseId, parsed.flushTimeoutMilliseconds)).pipe(Effect.catch(() => Effect.void), Effect.asVoid));
|
|
1284
|
-
|
|
1637
|
+
const acquireMetrics = () => {
|
|
1638
|
+
const metrics = new ActiveMetrics(lease.state.acquire(transport), parsed.flushTimeoutMilliseconds);
|
|
1639
|
+
metricContractStates.set(metrics, { values: lease.state.contractValuesByDefinition });
|
|
1640
|
+
return metrics;
|
|
1641
|
+
};
|
|
1642
|
+
return Context.make(Metric.MetricRegistry, lease.state.registry).pipe(Context.add(LayerMetricsRuntime, LayerMetricsRuntime.of({ acquireMetrics })));
|
|
1285
1643
|
});
|
|
1286
|
-
export const layerMetricsRuntime = (config, options) => Layer.
|
|
1644
|
+
export const layerMetricsRuntime = (config, options) => Layer.effectContext(makeMetricsRuntime(config, options)).pipe(Layer.provideMerge(OtlpExporter.layerFlusher));
|