@equipe-tech/observability 0.2.0 → 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.
Files changed (177) hide show
  1. package/README.md +11 -9
  2. package/dist/BrowserEvents.d.ts +89 -12
  3. package/dist/BrowserEvents.js +67 -13
  4. package/dist/Correlation.d.ts +51 -0
  5. package/dist/Correlation.js +95 -0
  6. package/dist/Metrics.d.ts +19 -3
  7. package/dist/Metrics.js +16 -0
  8. package/dist/MetricsRuntime.d.ts +45 -3
  9. package/dist/MetricsRuntime.js +478 -120
  10. package/dist/PolicyOtlpLogger.d.ts +18 -0
  11. package/dist/PolicyOtlpLogger.js +142 -0
  12. package/dist/RedactionPolicy.d.ts +2 -9
  13. package/dist/RedactionPolicy.js +2 -246
  14. package/dist/ResourceIdentity.d.ts +47 -0
  15. package/dist/ResourceIdentity.js +82 -0
  16. package/dist/Telemetry.d.ts +18 -3
  17. package/dist/Telemetry.js +43 -25
  18. package/dist/TelemetryConfig.d.ts +11 -4
  19. package/dist/TelemetryConfig.js +38 -10
  20. package/dist/audit/AuditCommitDocument.d.ts +41 -0
  21. package/dist/audit/AuditCommitDocument.js +58 -0
  22. package/dist/audit/AuditDigest.d.ts +11 -0
  23. package/dist/audit/AuditDigest.js +25 -0
  24. package/dist/audit/AuditOutbox.d.ts +75 -0
  25. package/dist/audit/AuditOutbox.js +104 -0
  26. package/dist/audit/AuditPublisher.d.ts +49 -0
  27. package/dist/audit/AuditPublisher.js +60 -0
  28. package/dist/audit/AuditRecord.d.ts +128 -0
  29. package/dist/audit/AuditRecord.js +172 -0
  30. package/dist/audit/CommittedAuditRecordInternal.d.ts +11 -0
  31. package/dist/audit/CommittedAuditRecordInternal.js +13 -0
  32. package/dist/audit/InvalidAuditRecord.d.ts +12 -0
  33. package/dist/audit/InvalidAuditRecord.js +16 -0
  34. package/dist/audit/index.d.ts +7 -0
  35. package/dist/audit/index.js +7 -0
  36. package/dist/browser/BrowserClient.d.ts +82 -3
  37. package/dist/browser/BrowserClient.js +322 -28
  38. package/dist/browser/BrowserEventLimits.d.ts +8 -0
  39. package/dist/browser/BrowserEventLimits.js +8 -0
  40. package/dist/browser/BrowserMetricRecorder.d.ts +17 -0
  41. package/dist/browser/BrowserMetricRecorder.js +35 -0
  42. package/dist/browser/BrowserTelemetry.d.ts +40 -0
  43. package/dist/browser/BrowserTelemetry.js +105 -0
  44. package/dist/browser/ClientPolicy.d.ts +3 -0
  45. package/dist/browser/ClientPolicy.js +52 -0
  46. package/dist/browser/client.d.ts +3 -2
  47. package/dist/browser/client.js +2 -1
  48. package/dist/browser/index.d.ts +4 -37
  49. package/dist/browser/index.js +3 -95
  50. package/dist/contract/ContractCompatibility.d.ts +31 -0
  51. package/dist/contract/ContractCompatibility.js +274 -0
  52. package/dist/contract/ContractIndex.d.ts +54 -0
  53. package/dist/contract/ContractIndex.js +178 -0
  54. package/dist/contract/ContractSurface.d.ts +111 -0
  55. package/dist/contract/ContractSurface.js +116 -0
  56. package/dist/contract/EventName.d.ts +6 -0
  57. package/dist/contract/EventName.js +55 -0
  58. package/dist/contract/EventProducer.d.ts +123 -0
  59. package/dist/contract/EventProducer.js +209 -0
  60. package/dist/contract/MetricContractError.d.ts +19 -0
  61. package/dist/contract/MetricContractError.js +19 -0
  62. package/dist/contract/MetricDefinition.d.ts +61 -0
  63. package/dist/contract/MetricDefinition.js +91 -0
  64. package/dist/contract/MetricProducer.d.ts +42 -0
  65. package/dist/contract/MetricProducer.js +123 -0
  66. package/dist/contract/OrganizationEvents.d.ts +165 -0
  67. package/dist/contract/OrganizationEvents.js +97 -0
  68. package/dist/contract/TelemetryContract.d.ts +95 -0
  69. package/dist/contract/TelemetryContract.js +451 -0
  70. package/dist/contract/TelemetryContractError.d.ts +44 -0
  71. package/dist/contract/TelemetryContractError.js +64 -0
  72. package/dist/contract/TelemetryEvent.d.ts +59 -0
  73. package/dist/contract/TelemetryEvent.js +54 -0
  74. package/dist/contract/index.d.ts +11 -0
  75. package/dist/contract/index.js +11 -0
  76. package/dist/effect/WideEvent.d.ts +3 -0
  77. package/dist/effect/WideEvent.js +20 -0
  78. package/dist/effect/WideEventSink.d.ts +3 -0
  79. package/dist/effect/WideEventSink.js +79 -0
  80. package/dist/effect/index.d.ts +2 -0
  81. package/dist/effect/index.js +2 -0
  82. package/dist/index.d.ts +17 -2
  83. package/dist/index.js +11 -2
  84. package/dist/metrics/InstrumentGrammar.d.ts +5 -0
  85. package/dist/metrics/InstrumentGrammar.js +12 -0
  86. package/dist/metrics/MetricScalar.d.ts +3 -0
  87. package/dist/metrics/MetricScalar.js +5 -0
  88. package/dist/node/AuditDigest.d.ts +3 -0
  89. package/dist/node/AuditDigest.js +6 -0
  90. package/dist/node/BrowserEventIngest.d.ts +7 -2
  91. package/dist/node/BrowserEventIngest.js +111 -19
  92. package/dist/node/LocalCollector.d.ts +20 -0
  93. package/dist/node/LocalCollector.js +188 -0
  94. package/dist/node/Observability.d.ts +49 -0
  95. package/dist/node/Observability.js +190 -0
  96. package/dist/node/Runtime.d.ts +4 -1
  97. package/dist/node/index.d.ts +4 -0
  98. package/dist/node/index.js +3 -0
  99. package/dist/policy/BrowserFieldPolicy.d.ts +7 -0
  100. package/dist/policy/BrowserFieldPolicy.js +298 -0
  101. package/dist/policy/DataPolicy.d.ts +19 -0
  102. package/dist/policy/DataPolicy.js +254 -0
  103. package/dist/policy/DataPolicyError.d.ts +22 -0
  104. package/dist/policy/DataPolicyError.js +25 -0
  105. package/dist/policy/Deduplication.d.ts +13 -0
  106. package/dist/policy/Deduplication.js +44 -0
  107. package/dist/policy/DefectEnvelope.d.ts +25 -0
  108. package/dist/policy/DefectEnvelope.js +52 -0
  109. package/dist/policy/MetricLabelPolicy.d.ts +5 -0
  110. package/dist/policy/MetricLabelPolicy.js +65 -0
  111. package/dist/policy/PolicyTransform.d.ts +16 -0
  112. package/dist/policy/PolicyTransform.js +136 -0
  113. package/dist/policy/PolicyVocabulary.d.ts +10 -0
  114. package/dist/policy/PolicyVocabulary.js +84 -0
  115. package/dist/policy/ResourceAttributePolicy.d.ts +9 -0
  116. package/dist/policy/ResourceAttributePolicy.js +33 -0
  117. package/dist/policy/entrypoint.d.ts +2 -0
  118. package/dist/policy/entrypoint.js +2 -0
  119. package/dist/policy/index.d.ts +17 -0
  120. package/dist/policy/index.js +9 -0
  121. package/dist/profile/DataPolicy.d.ts +2 -0
  122. package/dist/profile/DataPolicy.js +1 -0
  123. package/dist/profile/EnvironmentPolicy.d.ts +22 -0
  124. package/dist/profile/EnvironmentPolicy.js +41 -0
  125. package/dist/profile/LifecycleRegistry.d.ts +24 -0
  126. package/dist/profile/LifecycleRegistry.js +240 -0
  127. package/dist/profile/ObservabilityAdapter.d.ts +115 -0
  128. package/dist/profile/ObservabilityAdapter.js +50 -0
  129. package/dist/profile/ObservabilityConfig.d.ts +68 -0
  130. package/dist/profile/ObservabilityConfig.js +125 -0
  131. package/dist/profile/ObservabilityConfigError.d.ts +23 -0
  132. package/dist/profile/ObservabilityConfigError.js +39 -0
  133. package/dist/profile/ObservabilityProfile.d.ts +49 -0
  134. package/dist/profile/ObservabilityProfile.js +121 -0
  135. package/dist/profile/ReactWebProfile.d.ts +8 -0
  136. package/dist/profile/ReactWebProfile.js +7 -0
  137. package/dist/profile/index.d.ts +5 -0
  138. package/dist/profile/index.js +5 -0
  139. package/dist/testing/conformance/ConformanceAssertions.d.ts +5 -0
  140. package/dist/testing/conformance/ConformanceAssertions.js +31 -0
  141. package/dist/testing/conformance/ConformanceFailure.d.ts +35 -0
  142. package/dist/testing/conformance/ConformanceFailure.js +56 -0
  143. package/dist/testing/conformance/ConformanceModel.d.ts +97 -0
  144. package/dist/testing/conformance/ConformanceModel.js +8 -0
  145. package/dist/testing/conformance/ConformanceRegistry.d.ts +13 -0
  146. package/dist/testing/conformance/ConformanceRegistry.js +190 -0
  147. package/dist/testing/conformance/ConformanceRunner.d.ts +5 -0
  148. package/dist/testing/conformance/ConformanceRunner.js +198 -0
  149. package/dist/testing/conformance/TelemetryEvidence.d.ts +91 -0
  150. package/dist/testing/conformance/TelemetryEvidence.js +370 -0
  151. package/dist/testing/conformance/index.d.ts +10 -0
  152. package/dist/testing/conformance/index.js +6 -0
  153. package/dist/testing/contract.d.ts +81 -0
  154. package/dist/testing/contract.js +93 -0
  155. package/dist/testing/deployedCanary.d.ts +1 -0
  156. package/dist/testing/deployedCanary.js +1 -0
  157. package/dist/testing/index.d.ts +40 -3
  158. package/dist/testing/index.js +122 -5
  159. package/dist/trace/HttpServerOtlpTracer.d.ts +42 -0
  160. package/dist/trace/HttpServerOtlpTracer.js +301 -0
  161. package/package.json +16 -29
  162. package/dist/WideEvent.d.ts +0 -5
  163. package/dist/WideEvent.js +0 -6
  164. package/dist/nestjs/BrowserEventsController.d.ts +0 -21
  165. package/dist/nestjs/BrowserEventsController.js +0 -52
  166. package/dist/nestjs/HttpRoutePolicy.d.ts +0 -23
  167. package/dist/nestjs/HttpRoutePolicy.js +0 -179
  168. package/dist/nestjs/HttpServerOtlpTracer.d.ts +0 -15
  169. package/dist/nestjs/HttpServerOtlpTracer.js +0 -154
  170. package/dist/nestjs/RequestWideEventTraceCorrelation.d.ts +0 -15
  171. package/dist/nestjs/RequestWideEventTraceCorrelation.js +0 -13
  172. package/dist/nestjs/TelemetryInterceptor.d.ts +0 -31
  173. package/dist/nestjs/TelemetryInterceptor.js +0 -268
  174. package/dist/nestjs/TelemetryModule.d.ts +0 -53
  175. package/dist/nestjs/TelemetryModule.js +0 -428
  176. package/dist/nestjs/index.d.ts +0 -5
  177. package/dist/nestjs/index.js +0 -4
@@ -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
- 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
- };
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 = 16;
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 metricError = (code, operation, message, instrumentName, retryable, cause) => {
57
- const options = {
58
- code,
59
- operation,
60
- message,
61
- retryable,
62
- cause,
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
- 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);
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
- serviceName: options.serviceName,
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 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) => {
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
- numberIsInvalid ||
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}=${attributeIdentity(attribute.value)}`)
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 !== "unit" && key !== "time_unit") {
232
- values.push({ key, value: { stringValue: value } });
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
- 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);
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
- addCounter(leaseId, entry, value, attributes) {
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
- 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);
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
- else {
437
- current.value += value;
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.identity);
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, identity) {
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
- { 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
- ],
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 attributes = directAttributesToOtlp(snapshot.attributes);
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
- 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
- });
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
- 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
- });
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
- this.commitSeries(entry, identity);
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 new DisabledMetrics();
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
- return new ActiveMetrics(lease, options.flushTimeoutMilliseconds);
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 lease = acquireRuntime(parsed, {
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(parsed.metricsEndpoint, client),
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
- return lease.state.registry;
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.effect(Metric.MetricRegistry, makeMetricsRuntime(config, options)).pipe(Layer.provideMerge(OtlpExporter.layerFlusher));
1644
+ export const layerMetricsRuntime = (config, options) => Layer.effectContext(makeMetricsRuntime(config, options)).pipe(Layer.provideMerge(OtlpExporter.layerFlusher));