@noego/telemetry 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +130 -0
- package/dist/browser/index.cjs +1346 -0
- package/dist/browser/index.cjs.map +1 -0
- package/dist/browser/index.d.cts +5 -0
- package/dist/browser/index.d.ts +5 -0
- package/dist/browser/index.js +1318 -0
- package/dist/browser/index.js.map +1 -0
- package/dist/browser_telemetry_client-BzBPQm4m.d.cts +29 -0
- package/dist/browser_telemetry_client-Ch_VzwEa.d.ts +29 -0
- package/dist/cli/telemetry_bin.cjs +5531 -0
- package/dist/cli/telemetry_bin.cjs.map +1 -0
- package/dist/cli/telemetry_bin.d.cts +1 -0
- package/dist/cli/telemetry_bin.d.ts +1 -0
- package/dist/cli/telemetry_bin.js +5517 -0
- package/dist/cli/telemetry_bin.js.map +1 -0
- package/dist/cli/telemetry_cli.cjs +5523 -0
- package/dist/cli/telemetry_cli.cjs.map +1 -0
- package/dist/cli/telemetry_cli.d.cts +92 -0
- package/dist/cli/telemetry_cli.d.ts +92 -0
- package/dist/cli/telemetry_cli.js +5497 -0
- package/dist/cli/telemetry_cli.js.map +1 -0
- package/dist/client_defaults-B5tC-TgG.d.ts +8 -0
- package/dist/client_defaults-C9LnSX4j.d.cts +8 -0
- package/dist/core/index.cjs +2354 -0
- package/dist/core/index.cjs.map +1 -0
- package/dist/core/index.d.cts +168 -0
- package/dist/core/index.d.ts +168 -0
- package/dist/core/index.js +2318 -0
- package/dist/core/index.js.map +1 -0
- package/dist/electron/index.cjs +1379 -0
- package/dist/electron/index.cjs.map +1 -0
- package/dist/electron/index.d.cts +31 -0
- package/dist/electron/index.d.ts +31 -0
- package/dist/electron/index.js +1353 -0
- package/dist/electron/index.js.map +1 -0
- package/dist/envelope.types-rOLpWXRC.d.cts +42 -0
- package/dist/envelope.types-rOLpWXRC.d.ts +42 -0
- package/dist/in_memory_batch_transport-DKW9EZnU.d.cts +16 -0
- package/dist/in_memory_batch_transport-oOwNxXnA.d.ts +16 -0
- package/dist/index.cjs +3325 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +66 -0
- package/dist/index.d.ts +66 -0
- package/dist/index.js +3285 -0
- package/dist/index.js.map +1 -0
- package/dist/maintenance_coordinator.types-B-bZUNvw.d.cts +127 -0
- package/dist/maintenance_coordinator.types-CBA5y74b.d.ts +127 -0
- package/dist/metric_definition_contract-BR6a_GzF.d.cts +12 -0
- package/dist/metric_definition_contract-l4i4SaAp.d.ts +12 -0
- package/dist/node/index.cjs +7157 -0
- package/dist/node/index.cjs.map +1 -0
- package/dist/node/index.d.cts +74 -0
- package/dist/node/index.d.ts +74 -0
- package/dist/node/index.js +7122 -0
- package/dist/node/index.js.map +1 -0
- package/dist/node/producer/index.cjs +1322 -0
- package/dist/node/producer/index.cjs.map +1 -0
- package/dist/node/producer/index.d.cts +21 -0
- package/dist/node/producer/index.d.ts +21 -0
- package/dist/node/producer/index.js +1294 -0
- package/dist/node/producer/index.js.map +1 -0
- package/dist/query.types-Bntwh6KF.d.cts +186 -0
- package/dist/query.types-DySoqYop.d.ts +186 -0
- package/dist/result.types-171Huujb.d.cts +9 -0
- package/dist/result.types-171Huujb.d.ts +9 -0
- package/dist/sqlite/index.cjs +3409 -0
- package/dist/sqlite/index.cjs.map +1 -0
- package/dist/sqlite/index.d.cts +77 -0
- package/dist/sqlite/index.d.ts +77 -0
- package/dist/sqlite/index.js +3382 -0
- package/dist/sqlite/index.js.map +1 -0
- package/dist/storage.types-BTCGVxOM.d.ts +76 -0
- package/dist/storage.types-DjuUMinz.d.cts +76 -0
- package/dist/telemetry_client-BXw8WH_F.d.ts +141 -0
- package/dist/telemetry_client-CYaPYfOl.d.cts +141 -0
- package/dist/telemetry_configuration_parser-C7Et_gwG.d.ts +39 -0
- package/dist/telemetry_configuration_parser-R-niZQRZ.d.cts +39 -0
- package/dist/testing/index.cjs +215 -0
- package/dist/testing/index.cjs.map +1 -0
- package/dist/testing/index.d.cts +32 -0
- package/dist/testing/index.d.ts +32 -0
- package/dist/testing/index.js +187 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/trace-adapter/index.cjs +1034 -0
- package/dist/trace-adapter/index.cjs.map +1 -0
- package/dist/trace-adapter/index.d.cts +72 -0
- package/dist/trace-adapter/index.d.ts +72 -0
- package/dist/trace-adapter/index.js +1007 -0
- package/dist/trace-adapter/index.js.map +1 -0
- package/dist/transport.types-BAl_TRtU.d.cts +99 -0
- package/dist/transport.types-DDnZoKWi.d.ts +99 -0
- package/migrations/1783695629569_raw_events.down.sql +1 -0
- package/migrations/1783695629569_raw_events.up.sql +30 -0
- package/migrations/1783695630053_batch_acknowledgements.down.sql +1 -0
- package/migrations/1783695630053_batch_acknowledgements.up.sql +13 -0
- package/migrations/1783695630960_ingest_watermarks.down.sql +1 -0
- package/migrations/1783695630960_ingest_watermarks.up.sql +9 -0
- package/migrations/1783695631522_metric_definitions.down.sql +1 -0
- package/migrations/1783695631522_metric_definitions.up.sql +10 -0
- package/migrations/1783695631966_metric_samples.down.sql +1 -0
- package/migrations/1783695631966_metric_samples.up.sql +15 -0
- package/migrations/1783695632417_metric_rollups.down.sql +1 -0
- package/migrations/1783695632417_metric_rollups.up.sql +22 -0
- package/migrations/1783695632860_rollup_completions.down.sql +1 -0
- package/migrations/1783695632860_rollup_completions.up.sql +17 -0
- package/migrations/1783695633302_extraction_failures.down.sql +1 -0
- package/migrations/1783695633302_extraction_failures.up.sql +10 -0
- package/migrations/1783695633748_maintenance_runs.down.sql +1 -0
- package/migrations/1783695633748_maintenance_runs.up.sql +12 -0
- package/migrations/1783695634221_reader_leases.down.sql +1 -0
- package/migrations/1783695634221_reader_leases.up.sql +9 -0
- package/migrations/1783702703104_committed_drop_counters.down.sql +1 -0
- package/migrations/1783702703104_committed_drop_counters.up.sql +11 -0
- package/migrations/1783705542684_rollup_completion_state.down.sql +2 -0
- package/migrations/1783705542684_rollup_completion_state.up.sql +6 -0
- package/migrations/1784309799539_metric_samples_event_index.down.sql +1 -0
- package/migrations/1784309799539_metric_samples_event_index.up.sql +2 -0
- package/native/noego_telemetry_native.node +0 -0
- package/package.json +137 -0
- package/proper.json +8 -0
|
@@ -0,0 +1,1318 @@
|
|
|
1
|
+
/* @noego/telemetry */
|
|
2
|
+
|
|
3
|
+
// src/core/helper/canonical_json.ts
|
|
4
|
+
var UTF8_ENCODER = new TextEncoder();
|
|
5
|
+
function compareCanonicalKeys(left, right) {
|
|
6
|
+
if (left === right) return 0;
|
|
7
|
+
if (/^[\x00-\x7f]*$/.test(left) && /^[\x00-\x7f]*$/.test(right)) {
|
|
8
|
+
return left < right ? -1 : 1;
|
|
9
|
+
}
|
|
10
|
+
const leftBytes = UTF8_ENCODER.encode(left);
|
|
11
|
+
const rightBytes = UTF8_ENCODER.encode(right);
|
|
12
|
+
const length = Math.min(leftBytes.length, rightBytes.length);
|
|
13
|
+
for (let index = 0; index < length; index += 1) {
|
|
14
|
+
const difference = (leftBytes[index] ?? 0) - (rightBytes[index] ?? 0);
|
|
15
|
+
if (difference !== 0) return difference;
|
|
16
|
+
}
|
|
17
|
+
return leftBytes.length - rightBytes.length;
|
|
18
|
+
}
|
|
19
|
+
function canonicalJson(value) {
|
|
20
|
+
if (value === null || typeof value === "string" || typeof value === "boolean") {
|
|
21
|
+
return JSON.stringify(value);
|
|
22
|
+
}
|
|
23
|
+
if (typeof value === "number") {
|
|
24
|
+
if (!Number.isFinite(value)) {
|
|
25
|
+
throw new Error("Canonical telemetry JSON cannot encode a non-finite number");
|
|
26
|
+
}
|
|
27
|
+
return JSON.stringify(value);
|
|
28
|
+
}
|
|
29
|
+
if (Array.isArray(value)) {
|
|
30
|
+
const items = value.map((item) => canonicalJson(item));
|
|
31
|
+
return `[${items.join(",")}]`;
|
|
32
|
+
}
|
|
33
|
+
if (typeof value !== "object") {
|
|
34
|
+
throw new Error(`Canonical telemetry JSON cannot encode ${typeof value}`);
|
|
35
|
+
}
|
|
36
|
+
const entries = Object.entries(value).sort(([left], [right]) => compareCanonicalKeys(left, right));
|
|
37
|
+
const properties = entries.map(([key, item]) => `${JSON.stringify(key)}:${canonicalJson(item)}`);
|
|
38
|
+
return `{${properties.join(",")}}`;
|
|
39
|
+
}
|
|
40
|
+
function telemetryUtf8Length(value) {
|
|
41
|
+
return new TextEncoder().encode(value).byteLength;
|
|
42
|
+
}
|
|
43
|
+
function telemetryChecksum(value) {
|
|
44
|
+
const bytes = new TextEncoder().encode(canonicalJson(value));
|
|
45
|
+
let hash = 0xcbf29ce484222325n;
|
|
46
|
+
const prime = 0x100000001b3n;
|
|
47
|
+
for (const byte of bytes) {
|
|
48
|
+
hash ^= BigInt(byte);
|
|
49
|
+
hash = BigInt.asUintN(64, hash * prime);
|
|
50
|
+
}
|
|
51
|
+
return hash.toString(16).padStart(16, "0");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// src/core/bounded_metadata_snapshotter.ts
|
|
55
|
+
var SENTINEL_CIRCULAR = "[telemetry:circular]";
|
|
56
|
+
var SENTINEL_TRUNCATED = "[telemetry:truncated]";
|
|
57
|
+
var BoundedMetadataSnapshotter = class {
|
|
58
|
+
constructor(limits) {
|
|
59
|
+
this.limits = limits;
|
|
60
|
+
this.validateLimits();
|
|
61
|
+
}
|
|
62
|
+
limits;
|
|
63
|
+
snapshot(input) {
|
|
64
|
+
const state = {
|
|
65
|
+
bytes: 0,
|
|
66
|
+
properties: 0,
|
|
67
|
+
truncatedValues: 0,
|
|
68
|
+
redactedValues: 0,
|
|
69
|
+
serializationFailures: 0,
|
|
70
|
+
seen: /* @__PURE__ */ new WeakSet()
|
|
71
|
+
};
|
|
72
|
+
let normalized;
|
|
73
|
+
try {
|
|
74
|
+
normalized = this.normalize(input, "$", 0, state);
|
|
75
|
+
} catch {
|
|
76
|
+
state.serializationFailures += 1;
|
|
77
|
+
normalized = { __telemetry: "[telemetry:snapshot-failed]" };
|
|
78
|
+
}
|
|
79
|
+
const root = this.ensureObject(normalized);
|
|
80
|
+
const canonical = canonicalJson(root);
|
|
81
|
+
const metadata = telemetryUtf8Length(canonical) <= this.limits.maxBytes ? root : Object.freeze({ __telemetry: SENTINEL_TRUNCATED });
|
|
82
|
+
if (metadata !== root) {
|
|
83
|
+
state.truncatedValues += 1;
|
|
84
|
+
}
|
|
85
|
+
const stats = Object.freeze({
|
|
86
|
+
estimatedBytes: Math.min(telemetryUtf8Length(canonical), this.limits.maxBytes),
|
|
87
|
+
truncatedValues: state.truncatedValues,
|
|
88
|
+
redactedValues: state.redactedValues,
|
|
89
|
+
serializationFailures: state.serializationFailures
|
|
90
|
+
});
|
|
91
|
+
return Object.freeze({ metadata, stats });
|
|
92
|
+
}
|
|
93
|
+
normalize(input, path, depth, state) {
|
|
94
|
+
const replacement = this.redactionFor(path);
|
|
95
|
+
if (replacement !== null) {
|
|
96
|
+
state.redactedValues += 1;
|
|
97
|
+
return replacement;
|
|
98
|
+
}
|
|
99
|
+
if (depth > this.limits.maxDepth || state.bytes >= this.limits.maxBytes) {
|
|
100
|
+
state.truncatedValues += 1;
|
|
101
|
+
return SENTINEL_TRUNCATED;
|
|
102
|
+
}
|
|
103
|
+
if (input === null || typeof input === "boolean") {
|
|
104
|
+
return input;
|
|
105
|
+
}
|
|
106
|
+
if (typeof input === "string") {
|
|
107
|
+
return this.normalizeString(input, state);
|
|
108
|
+
}
|
|
109
|
+
if (typeof input === "number") {
|
|
110
|
+
return Number.isFinite(input) ? input : `[telemetry:number:${String(input)}]`;
|
|
111
|
+
}
|
|
112
|
+
if (typeof input === "bigint") {
|
|
113
|
+
return `[telemetry:bigint:${input.toString()}]`;
|
|
114
|
+
}
|
|
115
|
+
if (typeof input === "undefined") {
|
|
116
|
+
return "[telemetry:undefined]";
|
|
117
|
+
}
|
|
118
|
+
if (typeof input === "symbol") {
|
|
119
|
+
return `[telemetry:symbol:${input.description ?? ""}]`;
|
|
120
|
+
}
|
|
121
|
+
if (typeof input === "function") {
|
|
122
|
+
return "[telemetry:function]";
|
|
123
|
+
}
|
|
124
|
+
if (input instanceof Error) {
|
|
125
|
+
return this.normalizeError(input, path, depth, state);
|
|
126
|
+
}
|
|
127
|
+
if (typeof input !== "object") {
|
|
128
|
+
return `[telemetry:unsupported:${typeof input}]`;
|
|
129
|
+
}
|
|
130
|
+
if (state.seen.has(input)) {
|
|
131
|
+
return SENTINEL_CIRCULAR;
|
|
132
|
+
}
|
|
133
|
+
state.seen.add(input);
|
|
134
|
+
try {
|
|
135
|
+
return Array.isArray(input) ? this.normalizeArray(input, path, depth, state) : this.normalizeObject(input, path, depth, state);
|
|
136
|
+
} finally {
|
|
137
|
+
state.seen.delete(input);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
normalizeString(input, state) {
|
|
141
|
+
const stringLimit = Math.min(this.limits.maxStringLength, Math.max(0, this.limits.maxBytes - state.bytes));
|
|
142
|
+
const truncated = input.length > stringLimit;
|
|
143
|
+
const normalized = truncated ? input.slice(0, stringLimit) : input;
|
|
144
|
+
state.bytes += telemetryUtf8Length(normalized);
|
|
145
|
+
if (truncated) {
|
|
146
|
+
state.truncatedValues += 1;
|
|
147
|
+
}
|
|
148
|
+
return normalized;
|
|
149
|
+
}
|
|
150
|
+
normalizeArray(input, path, depth, state) {
|
|
151
|
+
const length = Math.min(input.length, this.limits.maxArrayLength);
|
|
152
|
+
const values = [];
|
|
153
|
+
for (let index = 0; index < length; index += 1) {
|
|
154
|
+
let item;
|
|
155
|
+
try {
|
|
156
|
+
item = input[index];
|
|
157
|
+
} catch {
|
|
158
|
+
state.serializationFailures += 1;
|
|
159
|
+
item = "[telemetry:property-read-failed]";
|
|
160
|
+
}
|
|
161
|
+
values.push(this.normalize(item, `${path}[${index}]`, depth + 1, state));
|
|
162
|
+
}
|
|
163
|
+
if (input.length > length) {
|
|
164
|
+
values.push(SENTINEL_TRUNCATED);
|
|
165
|
+
state.truncatedValues += 1;
|
|
166
|
+
}
|
|
167
|
+
return Object.freeze(values);
|
|
168
|
+
}
|
|
169
|
+
normalizeObject(input, path, depth, state) {
|
|
170
|
+
let keys;
|
|
171
|
+
try {
|
|
172
|
+
keys = Object.keys(input).sort((left, right) => left.localeCompare(right));
|
|
173
|
+
} catch {
|
|
174
|
+
state.serializationFailures += 1;
|
|
175
|
+
return Object.freeze({ __telemetry: "[telemetry:keys-failed]" });
|
|
176
|
+
}
|
|
177
|
+
const output = {};
|
|
178
|
+
for (const key of keys) {
|
|
179
|
+
if (state.properties >= this.limits.maxProperties) {
|
|
180
|
+
output.__telemetry = SENTINEL_TRUNCATED;
|
|
181
|
+
state.truncatedValues += 1;
|
|
182
|
+
break;
|
|
183
|
+
}
|
|
184
|
+
state.properties += 1;
|
|
185
|
+
state.bytes += telemetryUtf8Length(key);
|
|
186
|
+
const childPath = path === "$" ? key : `${path}.${key}`;
|
|
187
|
+
const redaction = this.redactionFor(childPath);
|
|
188
|
+
if (redaction !== null) {
|
|
189
|
+
output[key] = redaction;
|
|
190
|
+
state.redactedValues += 1;
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
let property;
|
|
194
|
+
try {
|
|
195
|
+
property = Reflect.get(input, key);
|
|
196
|
+
} catch {
|
|
197
|
+
state.serializationFailures += 1;
|
|
198
|
+
property = "[telemetry:property-read-failed]";
|
|
199
|
+
}
|
|
200
|
+
output[key] = this.normalize(property, childPath, depth + 1, state);
|
|
201
|
+
}
|
|
202
|
+
return Object.freeze(output);
|
|
203
|
+
}
|
|
204
|
+
normalizeError(error, path, depth, state) {
|
|
205
|
+
const name = this.readErrorField(error, "name", state);
|
|
206
|
+
const message = this.readErrorField(error, "message", state);
|
|
207
|
+
const stack = this.readErrorField(error, "stack", state).slice(0, this.limits.maxErrorStackLength);
|
|
208
|
+
return Object.freeze({
|
|
209
|
+
name: this.normalize(name, `${path}.name`, depth + 1, state),
|
|
210
|
+
message: this.normalize(message, `${path}.message`, depth + 1, state),
|
|
211
|
+
stack: this.normalize(stack, `${path}.stack`, depth + 1, state)
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
readErrorField(error, field, state) {
|
|
215
|
+
try {
|
|
216
|
+
return typeof error[field] === "string" ? error[field] : "";
|
|
217
|
+
} catch {
|
|
218
|
+
state.serializationFailures += 1;
|
|
219
|
+
return "[telemetry:error-field-read-failed]";
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
ensureObject(value) {
|
|
223
|
+
return this.isJsonObject(value) ? value : Object.freeze({ value });
|
|
224
|
+
}
|
|
225
|
+
isJsonObject(value) {
|
|
226
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
227
|
+
}
|
|
228
|
+
redactionFor(path) {
|
|
229
|
+
const matchingRule = this.limits.redactions.find((rule) => rule.path === path);
|
|
230
|
+
return matchingRule?.replacement ?? null;
|
|
231
|
+
}
|
|
232
|
+
validateLimits() {
|
|
233
|
+
const limits = [
|
|
234
|
+
this.limits.maxBytes,
|
|
235
|
+
this.limits.maxDepth,
|
|
236
|
+
this.limits.maxProperties,
|
|
237
|
+
this.limits.maxArrayLength,
|
|
238
|
+
this.limits.maxStringLength,
|
|
239
|
+
this.limits.maxErrorStackLength
|
|
240
|
+
];
|
|
241
|
+
const hasInvalidLimit = limits.some((limit) => !Number.isInteger(limit) || limit <= 0);
|
|
242
|
+
if (hasInvalidLimit) {
|
|
243
|
+
throw new Error("Metadata snapshot limits must be positive integers");
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
// src/core/helper/client_defaults.ts
|
|
249
|
+
var DEFAULT_TELEMETRY_ADMISSION_POLICY = Object.freeze({
|
|
250
|
+
maxQueuedEvents: 2e3,
|
|
251
|
+
maxQueuedBytes: 2e6,
|
|
252
|
+
maxBatchEvents: 128,
|
|
253
|
+
maxBatchBytes: 256e3,
|
|
254
|
+
maxInFlightBatches: 1,
|
|
255
|
+
retryLimit: 3,
|
|
256
|
+
retryBaseDelayMs: 25,
|
|
257
|
+
sampleRates: Object.freeze({
|
|
258
|
+
low: 1,
|
|
259
|
+
normal: 1,
|
|
260
|
+
high: 1
|
|
261
|
+
})
|
|
262
|
+
});
|
|
263
|
+
var DEFAULT_METADATA_SNAPSHOT_LIMITS = Object.freeze({
|
|
264
|
+
maxBytes: 32768,
|
|
265
|
+
maxDepth: 8,
|
|
266
|
+
maxProperties: 256,
|
|
267
|
+
maxArrayLength: 128,
|
|
268
|
+
maxStringLength: 4096,
|
|
269
|
+
maxErrorStackLength: 8192,
|
|
270
|
+
redactions: Object.freeze([])
|
|
271
|
+
});
|
|
272
|
+
var DEFAULT_TELEMETRY_BATCH_VALIDATION_LIMITS = Object.freeze({
|
|
273
|
+
maxBatchEvents: 1024,
|
|
274
|
+
maxBatchBytes: 1e6,
|
|
275
|
+
maxEventBytes: 256e3,
|
|
276
|
+
maxMetadataBytes: 131072,
|
|
277
|
+
maxMetadataDepth: 16,
|
|
278
|
+
maxMetadataProperties: 1024,
|
|
279
|
+
maxMetadataArrayLength: 1024,
|
|
280
|
+
maxMetadataStringBytes: 65536,
|
|
281
|
+
maxDropCounters: 15,
|
|
282
|
+
maxIdentityBytes: 1024
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
// src/core/telemetry_batch_contract.ts
|
|
286
|
+
var BATCH_KEYS = Object.freeze([
|
|
287
|
+
"schemaVersion",
|
|
288
|
+
"producerId",
|
|
289
|
+
"sequenceStart",
|
|
290
|
+
"sequenceEnd",
|
|
291
|
+
"idempotencyKey",
|
|
292
|
+
"checksum",
|
|
293
|
+
"envelopes",
|
|
294
|
+
"dropCounters"
|
|
295
|
+
]);
|
|
296
|
+
var ENVELOPE_KEYS = Object.freeze([
|
|
297
|
+
"schemaVersion",
|
|
298
|
+
"eventId",
|
|
299
|
+
"producerId",
|
|
300
|
+
"sequence",
|
|
301
|
+
"occurredAtMs",
|
|
302
|
+
"monotonicNs",
|
|
303
|
+
"source",
|
|
304
|
+
"processKind",
|
|
305
|
+
"level",
|
|
306
|
+
"label",
|
|
307
|
+
"conversationId",
|
|
308
|
+
"correlationId",
|
|
309
|
+
"metadata"
|
|
310
|
+
]);
|
|
311
|
+
var DROP_COUNTER_KEYS = Object.freeze(["reason", "priority", "count"]);
|
|
312
|
+
var DROP_REASONS = /* @__PURE__ */ new Set(["capacity", "sampled", "stopped", "invalid", "transport"]);
|
|
313
|
+
var DROP_PRIORITIES = /* @__PURE__ */ new Set(["low", "normal", "high"]);
|
|
314
|
+
var TelemetryBatchContract = class _TelemetryBatchContract {
|
|
315
|
+
static schemaVersion = 1;
|
|
316
|
+
static checksumAlgorithm = "fnv1a64-canonical-json-v1";
|
|
317
|
+
limits;
|
|
318
|
+
constructor(limits = {}) {
|
|
319
|
+
this.limits = Object.freeze({
|
|
320
|
+
...DEFAULT_TELEMETRY_BATCH_VALIDATION_LIMITS,
|
|
321
|
+
...limits
|
|
322
|
+
});
|
|
323
|
+
this.validateLimits();
|
|
324
|
+
}
|
|
325
|
+
create(envelopes, dropCounters) {
|
|
326
|
+
try {
|
|
327
|
+
return this.createChecked(envelopes, dropCounters);
|
|
328
|
+
} catch {
|
|
329
|
+
return { ok: false, error: "Telemetry batch creation failed safe validation" };
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
validate(batch) {
|
|
333
|
+
try {
|
|
334
|
+
return this.validateChecked(batch);
|
|
335
|
+
} catch {
|
|
336
|
+
return { ok: false, error: "Telemetry batch validation failed safely" };
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
createChecked(envelopes, dropCounters) {
|
|
340
|
+
const payloadError = this.validatePayload(envelopes, dropCounters);
|
|
341
|
+
if (payloadError !== null) {
|
|
342
|
+
return { ok: false, error: payloadError };
|
|
343
|
+
}
|
|
344
|
+
const payloadBytesError = this.payloadBytesError(envelopes, dropCounters);
|
|
345
|
+
if (payloadBytesError !== null) {
|
|
346
|
+
return { ok: false, error: payloadBytesError };
|
|
347
|
+
}
|
|
348
|
+
const first = envelopes[0];
|
|
349
|
+
const last = envelopes[envelopes.length - 1];
|
|
350
|
+
if (!first || !last) {
|
|
351
|
+
return { ok: false, error: "A telemetry batch must contain a first and last envelope" };
|
|
352
|
+
}
|
|
353
|
+
const producerId = first.producerId;
|
|
354
|
+
const sequenceStart = first.sequence;
|
|
355
|
+
const sequenceEnd = last.sequence;
|
|
356
|
+
const checksumPayload = this.checksumPayload(
|
|
357
|
+
producerId,
|
|
358
|
+
sequenceStart,
|
|
359
|
+
sequenceEnd,
|
|
360
|
+
envelopes,
|
|
361
|
+
dropCounters
|
|
362
|
+
);
|
|
363
|
+
const checksum = telemetryChecksum(checksumPayload);
|
|
364
|
+
const idempotencyKey = `tb1:${producerId}:${sequenceStart}-${sequenceEnd}:${checksum}`;
|
|
365
|
+
const batch = Object.freeze({
|
|
366
|
+
schemaVersion: _TelemetryBatchContract.schemaVersion,
|
|
367
|
+
producerId,
|
|
368
|
+
sequenceStart,
|
|
369
|
+
sequenceEnd,
|
|
370
|
+
idempotencyKey,
|
|
371
|
+
checksum,
|
|
372
|
+
envelopes: Object.freeze([...envelopes]),
|
|
373
|
+
dropCounters: Object.freeze([...dropCounters])
|
|
374
|
+
});
|
|
375
|
+
const framingError = this.validateFraming(batch);
|
|
376
|
+
if (framingError !== null) {
|
|
377
|
+
return { ok: false, error: framingError };
|
|
378
|
+
}
|
|
379
|
+
const byteError = this.serializedByteError(batch, this.limits.maxBatchBytes, "batch");
|
|
380
|
+
return byteError === null ? { ok: true, value: this.detachBatch(batch) } : { ok: false, error: byteError };
|
|
381
|
+
}
|
|
382
|
+
validateChecked(batch) {
|
|
383
|
+
if (!this.isRecord(batch)) {
|
|
384
|
+
return { ok: false, error: "Telemetry batch must be a plain object" };
|
|
385
|
+
}
|
|
386
|
+
const schemaVersion = batch.schemaVersion;
|
|
387
|
+
if (!Number.isSafeInteger(schemaVersion)) {
|
|
388
|
+
return { ok: false, error: "Telemetry batch schema version must be a safe integer" };
|
|
389
|
+
}
|
|
390
|
+
if (schemaVersion !== _TelemetryBatchContract.schemaVersion) {
|
|
391
|
+
return { ok: false, error: `Unsupported telemetry schema version ${String(schemaVersion)}` };
|
|
392
|
+
}
|
|
393
|
+
if (!this.hasExactKeys(batch, BATCH_KEYS)) {
|
|
394
|
+
return { ok: false, error: "Telemetry batch fields do not match the schema" };
|
|
395
|
+
}
|
|
396
|
+
const framingError = this.validateFraming(batch);
|
|
397
|
+
if (framingError !== null) {
|
|
398
|
+
return { ok: false, error: framingError };
|
|
399
|
+
}
|
|
400
|
+
const recreated = this.create(
|
|
401
|
+
batch.envelopes,
|
|
402
|
+
batch.dropCounters
|
|
403
|
+
);
|
|
404
|
+
if (!recreated.ok) {
|
|
405
|
+
return recreated;
|
|
406
|
+
}
|
|
407
|
+
const identityError = this.compareFraming(batch, recreated.value);
|
|
408
|
+
return identityError === null ? { ok: true, value: recreated.value } : { ok: false, error: identityError };
|
|
409
|
+
}
|
|
410
|
+
validatePayload(envelopes, dropCounters) {
|
|
411
|
+
if (!Array.isArray(envelopes) || envelopes.length === 0) {
|
|
412
|
+
return "A telemetry batch must contain at least one envelope";
|
|
413
|
+
}
|
|
414
|
+
if (envelopes.length > this.limits.maxBatchEvents) {
|
|
415
|
+
return `Telemetry batch exceeds ${this.limits.maxBatchEvents} envelopes`;
|
|
416
|
+
}
|
|
417
|
+
const envelopeError = this.validateEnvelopes(envelopes);
|
|
418
|
+
return envelopeError ?? this.validateDropCounters(dropCounters);
|
|
419
|
+
}
|
|
420
|
+
validateEnvelopes(envelopes) {
|
|
421
|
+
let previousSequence = -1;
|
|
422
|
+
let producerId = null;
|
|
423
|
+
for (const envelope of envelopes) {
|
|
424
|
+
const envelopeError = this.validateEnvelope(envelope, producerId, previousSequence);
|
|
425
|
+
if (envelopeError !== null) {
|
|
426
|
+
return envelopeError;
|
|
427
|
+
}
|
|
428
|
+
const typedEnvelope = envelope;
|
|
429
|
+
producerId = typedEnvelope.producerId;
|
|
430
|
+
previousSequence = typedEnvelope.sequence;
|
|
431
|
+
}
|
|
432
|
+
return null;
|
|
433
|
+
}
|
|
434
|
+
validateEnvelope(envelope, expectedProducerId, previousSequence) {
|
|
435
|
+
if (!this.isRecord(envelope) || !this.hasExactKeys(envelope, ENVELOPE_KEYS)) {
|
|
436
|
+
return "Telemetry envelope fields do not match the schema";
|
|
437
|
+
}
|
|
438
|
+
const identityError = this.validateEnvelopeIdentity(envelope, expectedProducerId, previousSequence);
|
|
439
|
+
if (identityError !== null) {
|
|
440
|
+
return identityError;
|
|
441
|
+
}
|
|
442
|
+
const valueError = this.validateEnvelopeValues(envelope);
|
|
443
|
+
if (valueError !== null) {
|
|
444
|
+
return valueError;
|
|
445
|
+
}
|
|
446
|
+
const metadataError = this.validateMetadata(envelope.metadata);
|
|
447
|
+
return metadataError ?? this.serializedByteError(envelope, this.limits.maxEventBytes, "event");
|
|
448
|
+
}
|
|
449
|
+
validateEnvelopeIdentity(envelope, expectedProducerId, previousSequence) {
|
|
450
|
+
if (envelope.schemaVersion !== _TelemetryBatchContract.schemaVersion) {
|
|
451
|
+
return "Telemetry envelope schema version does not match its batch";
|
|
452
|
+
}
|
|
453
|
+
if (!this.isIdentity(envelope.producerId)) {
|
|
454
|
+
return "Telemetry envelope producer identity is invalid";
|
|
455
|
+
}
|
|
456
|
+
if (!this.isSequence(envelope.sequence) || envelope.sequence <= previousSequence) {
|
|
457
|
+
return "Telemetry envelope sequences must be nonnegative safe integers in increasing order";
|
|
458
|
+
}
|
|
459
|
+
if (expectedProducerId !== null && envelope.producerId !== expectedProducerId) {
|
|
460
|
+
return "A telemetry batch cannot mix producer identities";
|
|
461
|
+
}
|
|
462
|
+
const eventId = `${envelope.producerId}:${envelope.sequence}`;
|
|
463
|
+
if (envelope.eventId !== eventId || telemetryUtf8Length(eventId) > this.limits.maxIdentityBytes) {
|
|
464
|
+
return "Telemetry envelope event identity is invalid";
|
|
465
|
+
}
|
|
466
|
+
return null;
|
|
467
|
+
}
|
|
468
|
+
validateEnvelopeValues(envelope) {
|
|
469
|
+
if (!Number.isSafeInteger(envelope.occurredAtMs) || Number(envelope.occurredAtMs) < 0) {
|
|
470
|
+
return "Telemetry envelope occurredAtMs must be a nonnegative safe integer";
|
|
471
|
+
}
|
|
472
|
+
if (!this.isMonotonicTimestamp(envelope.monotonicNs)) {
|
|
473
|
+
return "Telemetry envelope monotonicNs is invalid";
|
|
474
|
+
}
|
|
475
|
+
if (!this.isIdentity(envelope.source) || !this.isIdentity(envelope.processKind)) {
|
|
476
|
+
return "Telemetry envelope source and process kind are invalid";
|
|
477
|
+
}
|
|
478
|
+
if (!this.isIdentity(envelope.level)) {
|
|
479
|
+
return "Telemetry envelope level is invalid";
|
|
480
|
+
}
|
|
481
|
+
if (!this.isLabel(envelope.label)) {
|
|
482
|
+
return "Telemetry envelope label is invalid";
|
|
483
|
+
}
|
|
484
|
+
if (!this.isConversationId(envelope.conversationId)) {
|
|
485
|
+
return "Telemetry envelope conversation identity is invalid";
|
|
486
|
+
}
|
|
487
|
+
if (!this.isOptionalBoundedString(envelope.correlationId)) {
|
|
488
|
+
return "Telemetry envelope correlation identity is invalid";
|
|
489
|
+
}
|
|
490
|
+
return null;
|
|
491
|
+
}
|
|
492
|
+
validateMetadata(metadata) {
|
|
493
|
+
if (!this.isRecord(metadata)) {
|
|
494
|
+
return "Telemetry envelope metadata must be a plain JSON object";
|
|
495
|
+
}
|
|
496
|
+
const state = { properties: 0, bytes: 0, active: /* @__PURE__ */ new WeakSet() };
|
|
497
|
+
const jsonError = this.validateJsonValue(metadata, 0, state);
|
|
498
|
+
return jsonError ?? this.serializedByteError(metadata, this.limits.maxMetadataBytes, "metadata");
|
|
499
|
+
}
|
|
500
|
+
validateJsonValue(value, depth, state) {
|
|
501
|
+
if (value === null || typeof value === "boolean") {
|
|
502
|
+
state.bytes += value === null ? 4 : 5;
|
|
503
|
+
return null;
|
|
504
|
+
}
|
|
505
|
+
if (typeof value === "number") {
|
|
506
|
+
if (!Number.isFinite(value)) {
|
|
507
|
+
return "Telemetry metadata contains a non-finite number";
|
|
508
|
+
}
|
|
509
|
+
state.bytes += String(value).length;
|
|
510
|
+
return this.metadataBudgetError(state);
|
|
511
|
+
}
|
|
512
|
+
if (typeof value === "string") {
|
|
513
|
+
if (!this.isWithinUtf8Limit(value, this.limits.maxMetadataStringBytes)) {
|
|
514
|
+
return "Telemetry metadata string exceeds the receiver limit";
|
|
515
|
+
}
|
|
516
|
+
state.bytes += telemetryUtf8Length(value);
|
|
517
|
+
return this.metadataBudgetError(state);
|
|
518
|
+
}
|
|
519
|
+
if (typeof value !== "object") {
|
|
520
|
+
return "Telemetry metadata contains a non-JSON value";
|
|
521
|
+
}
|
|
522
|
+
if (depth > this.limits.maxMetadataDepth) {
|
|
523
|
+
return "Telemetry metadata exceeds the receiver depth limit";
|
|
524
|
+
}
|
|
525
|
+
return Array.isArray(value) ? this.validateJsonArray(value, depth, state) : this.validateJsonObject(value, depth, state);
|
|
526
|
+
}
|
|
527
|
+
validateJsonArray(value, depth, state) {
|
|
528
|
+
if (value.length > this.limits.maxMetadataArrayLength) {
|
|
529
|
+
return "Telemetry metadata array exceeds the receiver length limit";
|
|
530
|
+
}
|
|
531
|
+
const keys = Object.keys(value);
|
|
532
|
+
if (keys.length !== value.length || Object.getOwnPropertySymbols(value).length > 0) {
|
|
533
|
+
return "Telemetry metadata arrays must be dense JSON arrays";
|
|
534
|
+
}
|
|
535
|
+
return this.validateJsonChildren(value, value, depth, state);
|
|
536
|
+
}
|
|
537
|
+
validateJsonObject(value, depth, state) {
|
|
538
|
+
if (!this.isRecord(value)) {
|
|
539
|
+
return "Telemetry metadata contains a non-plain object";
|
|
540
|
+
}
|
|
541
|
+
const keys = Object.keys(value);
|
|
542
|
+
if (Object.getOwnPropertySymbols(value).length > 0) {
|
|
543
|
+
return "Telemetry metadata objects cannot contain symbol keys";
|
|
544
|
+
}
|
|
545
|
+
state.properties += keys.length;
|
|
546
|
+
if (state.properties > this.limits.maxMetadataProperties) {
|
|
547
|
+
return "Telemetry metadata exceeds the receiver property limit";
|
|
548
|
+
}
|
|
549
|
+
if (keys.some((key) => !this.isWithinUtf8Limit(key, this.limits.maxMetadataStringBytes))) {
|
|
550
|
+
return "Telemetry metadata property name exceeds the receiver string limit";
|
|
551
|
+
}
|
|
552
|
+
for (const key of keys) {
|
|
553
|
+
state.bytes += telemetryUtf8Length(key);
|
|
554
|
+
}
|
|
555
|
+
const budgetError = this.metadataBudgetError(state);
|
|
556
|
+
if (budgetError !== null) {
|
|
557
|
+
return budgetError;
|
|
558
|
+
}
|
|
559
|
+
return this.validateJsonChildren(value, keys.map((key) => Reflect.get(value, key)), depth, state);
|
|
560
|
+
}
|
|
561
|
+
validateJsonChildren(owner, values, depth, state) {
|
|
562
|
+
if (state.active.has(owner)) {
|
|
563
|
+
return "Telemetry metadata cannot contain a cycle";
|
|
564
|
+
}
|
|
565
|
+
state.active.add(owner);
|
|
566
|
+
try {
|
|
567
|
+
for (const value of values) {
|
|
568
|
+
const error = this.validateJsonValue(value, depth + 1, state);
|
|
569
|
+
if (error !== null) {
|
|
570
|
+
return error;
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
return null;
|
|
574
|
+
} finally {
|
|
575
|
+
state.active.delete(owner);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
validateDropCounters(dropCounters) {
|
|
579
|
+
if (!Array.isArray(dropCounters) || dropCounters.length > this.limits.maxDropCounters) {
|
|
580
|
+
return "Telemetry drop counters exceed the fixed receiver limit";
|
|
581
|
+
}
|
|
582
|
+
const identities = /* @__PURE__ */ new Set();
|
|
583
|
+
let total = 0;
|
|
584
|
+
for (const counter of dropCounters) {
|
|
585
|
+
const error = this.validateDropCounter(counter, identities, total);
|
|
586
|
+
if (error !== null) {
|
|
587
|
+
return error;
|
|
588
|
+
}
|
|
589
|
+
total += counter.count;
|
|
590
|
+
}
|
|
591
|
+
return null;
|
|
592
|
+
}
|
|
593
|
+
validateDropCounter(counter, identities, currentTotal) {
|
|
594
|
+
if (!this.isRecord(counter) || !this.hasExactKeys(counter, DROP_COUNTER_KEYS)) {
|
|
595
|
+
return "Telemetry drop counter fields do not match the schema";
|
|
596
|
+
}
|
|
597
|
+
if (!DROP_REASONS.has(String(counter.reason)) || !DROP_PRIORITIES.has(String(counter.priority))) {
|
|
598
|
+
return "Telemetry drop counter identity is invalid";
|
|
599
|
+
}
|
|
600
|
+
if (!Number.isSafeInteger(counter.count) || Number(counter.count) <= 0) {
|
|
601
|
+
return "Telemetry drop counter count must be a positive safe integer";
|
|
602
|
+
}
|
|
603
|
+
if (currentTotal > Number.MAX_SAFE_INTEGER - Number(counter.count)) {
|
|
604
|
+
return "Telemetry drop counter total exceeds the safe integer range";
|
|
605
|
+
}
|
|
606
|
+
const identity = `${counter.reason}:${counter.priority}`;
|
|
607
|
+
if (identities.has(identity)) {
|
|
608
|
+
return "Telemetry drop counter identities must be unique within a batch";
|
|
609
|
+
}
|
|
610
|
+
identities.add(identity);
|
|
611
|
+
return null;
|
|
612
|
+
}
|
|
613
|
+
validateFraming(batch) {
|
|
614
|
+
if (!this.isIdentity(batch.producerId)) {
|
|
615
|
+
return "Telemetry batch producer identity is invalid";
|
|
616
|
+
}
|
|
617
|
+
if (!this.isSequence(batch.sequenceStart) || !this.isSequence(batch.sequenceEnd)) {
|
|
618
|
+
return "Telemetry batch sequence range must use nonnegative safe integers";
|
|
619
|
+
}
|
|
620
|
+
if (Number(batch.sequenceEnd) < Number(batch.sequenceStart)) {
|
|
621
|
+
return "Telemetry batch sequence range is reversed";
|
|
622
|
+
}
|
|
623
|
+
if (typeof batch.checksum !== "string" || !/^[a-f0-9]{16}$/.test(batch.checksum)) {
|
|
624
|
+
return "Telemetry batch checksum is invalid";
|
|
625
|
+
}
|
|
626
|
+
if (typeof batch.idempotencyKey !== "string" || !this.isWithinUtf8Limit(batch.idempotencyKey, this.limits.maxIdentityBytes)) {
|
|
627
|
+
return "Telemetry batch idempotency identity is invalid";
|
|
628
|
+
}
|
|
629
|
+
return null;
|
|
630
|
+
}
|
|
631
|
+
compareFraming(batch, recreated) {
|
|
632
|
+
const matches = recreated.checksum === batch.checksum && recreated.idempotencyKey === batch.idempotencyKey && recreated.sequenceStart === batch.sequenceStart && recreated.sequenceEnd === batch.sequenceEnd && recreated.producerId === batch.producerId;
|
|
633
|
+
return matches ? null : "Telemetry batch framing or checksum is invalid";
|
|
634
|
+
}
|
|
635
|
+
checksumPayload(producerId, sequenceStart, sequenceEnd, envelopes, dropCounters) {
|
|
636
|
+
return {
|
|
637
|
+
schemaVersion: _TelemetryBatchContract.schemaVersion,
|
|
638
|
+
producerId,
|
|
639
|
+
sequenceStart,
|
|
640
|
+
sequenceEnd,
|
|
641
|
+
envelopes,
|
|
642
|
+
dropCounters
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
serializedByteError(value, maximum, kind) {
|
|
646
|
+
const bytes = telemetryUtf8Length(canonicalJson(value));
|
|
647
|
+
return bytes <= maximum ? null : `Telemetry ${kind} exceeds ${maximum} bytes`;
|
|
648
|
+
}
|
|
649
|
+
payloadBytesError(envelopes, dropCounters) {
|
|
650
|
+
let bytes = 0;
|
|
651
|
+
for (const envelope of envelopes) {
|
|
652
|
+
bytes += telemetryUtf8Length(canonicalJson(envelope));
|
|
653
|
+
if (bytes > this.limits.maxBatchBytes) {
|
|
654
|
+
return `Telemetry batch exceeds ${this.limits.maxBatchBytes} bytes`;
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
bytes += telemetryUtf8Length(canonicalJson(dropCounters));
|
|
658
|
+
return bytes > this.limits.maxBatchBytes ? `Telemetry batch exceeds ${this.limits.maxBatchBytes} bytes` : null;
|
|
659
|
+
}
|
|
660
|
+
metadataBudgetError(state) {
|
|
661
|
+
return state.bytes > this.limits.maxMetadataBytes ? `Telemetry metadata exceeds ${this.limits.maxMetadataBytes} bytes` : null;
|
|
662
|
+
}
|
|
663
|
+
detachBatch(batch) {
|
|
664
|
+
const detached = JSON.parse(canonicalJson(batch));
|
|
665
|
+
return this.deepFreeze(detached);
|
|
666
|
+
}
|
|
667
|
+
deepFreeze(value) {
|
|
668
|
+
if (typeof value !== "object" || value === null || Object.isFrozen(value)) {
|
|
669
|
+
return value;
|
|
670
|
+
}
|
|
671
|
+
for (const child of Object.values(value)) {
|
|
672
|
+
this.deepFreeze(child);
|
|
673
|
+
}
|
|
674
|
+
return Object.freeze(value);
|
|
675
|
+
}
|
|
676
|
+
isRecord(value) {
|
|
677
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
678
|
+
return false;
|
|
679
|
+
}
|
|
680
|
+
const prototype = Object.getPrototypeOf(value);
|
|
681
|
+
return prototype === null || prototype.constructor?.name === "Object";
|
|
682
|
+
}
|
|
683
|
+
hasExactKeys(value, expected) {
|
|
684
|
+
const keys = Object.keys(value);
|
|
685
|
+
return Object.getOwnPropertySymbols(value).length === 0 && keys.length === expected.length && expected.every((key) => Object.prototype.hasOwnProperty.call(value, key));
|
|
686
|
+
}
|
|
687
|
+
isSequence(value) {
|
|
688
|
+
return Number.isSafeInteger(value) && Number(value) >= 0;
|
|
689
|
+
}
|
|
690
|
+
isIdentity(value) {
|
|
691
|
+
return typeof value === "string" && value.length <= this.limits.maxIdentityBytes && value.trim().length > 0 && this.isWithinUtf8Limit(value, this.limits.maxIdentityBytes) && !/[\u0000-\u001f\u007f]/.test(value);
|
|
692
|
+
}
|
|
693
|
+
isLabel(value) {
|
|
694
|
+
return typeof value === "string" && value.length <= Math.min(256, this.limits.maxIdentityBytes) && /^[A-Za-z0-9][A-Za-z0-9_.:-]*$/.test(value) && this.isWithinUtf8Limit(value, Math.min(256, this.limits.maxIdentityBytes));
|
|
695
|
+
}
|
|
696
|
+
isMonotonicTimestamp(value) {
|
|
697
|
+
return value === null || typeof value === "string" && value.length <= this.limits.maxIdentityBytes && /^\d+$/.test(value) && this.isWithinUtf8Limit(value, this.limits.maxIdentityBytes);
|
|
698
|
+
}
|
|
699
|
+
isConversationId(value) {
|
|
700
|
+
return value === null || typeof value === "number" && Number.isSafeInteger(value) || typeof value === "string" && this.isWithinUtf8Limit(value, this.limits.maxIdentityBytes) && !/[\u0000-\u001f\u007f]/.test(value);
|
|
701
|
+
}
|
|
702
|
+
isOptionalBoundedString(value) {
|
|
703
|
+
return value === null || typeof value === "string" && this.isWithinUtf8Limit(value, this.limits.maxIdentityBytes) && !/[\u0000-\u001f\u007f]/.test(value);
|
|
704
|
+
}
|
|
705
|
+
isWithinUtf8Limit(value, maximum) {
|
|
706
|
+
return value.length <= maximum && telemetryUtf8Length(value) <= maximum;
|
|
707
|
+
}
|
|
708
|
+
validateLimits() {
|
|
709
|
+
const values = Object.values(this.limits);
|
|
710
|
+
if (values.some((value) => !Number.isSafeInteger(value) || value <= 0)) {
|
|
711
|
+
throw new Error("Telemetry batch validation limits must be positive safe integers");
|
|
712
|
+
}
|
|
713
|
+
if (this.limits.maxMetadataBytes > this.limits.maxEventBytes || this.limits.maxEventBytes > this.limits.maxBatchBytes) {
|
|
714
|
+
throw new Error("Telemetry receiver byte limits must satisfy metadata <= event <= batch");
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
};
|
|
718
|
+
|
|
719
|
+
// src/core/telemetry_client.ts
|
|
720
|
+
var TelemetryClient = class {
|
|
721
|
+
constructor(options) {
|
|
722
|
+
this.options = options;
|
|
723
|
+
this.validateOptions();
|
|
724
|
+
this.snapshotter = new BoundedMetadataSnapshotter(options.metadata);
|
|
725
|
+
}
|
|
726
|
+
options;
|
|
727
|
+
snapshotter;
|
|
728
|
+
batchContract = new TelemetryBatchContract();
|
|
729
|
+
queue = [];
|
|
730
|
+
dropCounts = /* @__PURE__ */ new Map();
|
|
731
|
+
state = "accepting";
|
|
732
|
+
nextSequence = 0;
|
|
733
|
+
queuedBytes = 0;
|
|
734
|
+
inFlightBatches = 0;
|
|
735
|
+
drainScheduled = false;
|
|
736
|
+
activeDrain = null;
|
|
737
|
+
lastAcknowledgement = null;
|
|
738
|
+
trace(level, label, metadata = {}) {
|
|
739
|
+
return this.emit(level, label, metadata);
|
|
740
|
+
}
|
|
741
|
+
debug(label, metadata = {}) {
|
|
742
|
+
return this.emit("debug", label, metadata);
|
|
743
|
+
}
|
|
744
|
+
info(label, metadata = {}) {
|
|
745
|
+
return this.emit("info", label, metadata);
|
|
746
|
+
}
|
|
747
|
+
warn(label, metadata = {}) {
|
|
748
|
+
return this.emit("warn", label, metadata);
|
|
749
|
+
}
|
|
750
|
+
error(label, metadata = {}) {
|
|
751
|
+
return this.emit("error", label, metadata);
|
|
752
|
+
}
|
|
753
|
+
fatal(label, metadata = {}) {
|
|
754
|
+
return this.emit("fatal", label, metadata);
|
|
755
|
+
}
|
|
756
|
+
emit(level, label, metadata) {
|
|
757
|
+
return this.admit({
|
|
758
|
+
source: this.options.source,
|
|
759
|
+
processKind: this.options.processKind,
|
|
760
|
+
level,
|
|
761
|
+
label,
|
|
762
|
+
occurredAtMs: this.options.now(),
|
|
763
|
+
monotonicNs: this.options.monotonicNowNs(),
|
|
764
|
+
conversationId: null,
|
|
765
|
+
correlationId: null,
|
|
766
|
+
metadata
|
|
767
|
+
});
|
|
768
|
+
}
|
|
769
|
+
admit(input) {
|
|
770
|
+
const sequence = this.nextSequence;
|
|
771
|
+
this.nextSequence += 1;
|
|
772
|
+
if (this.state !== "accepting") {
|
|
773
|
+
this.incrementDrop("stopped", this.priorityFor(input.level));
|
|
774
|
+
return { kind: "dropped", producerId: this.options.producerId, sequence, reason: "client-stopped" };
|
|
775
|
+
}
|
|
776
|
+
const hasInvalidIdentity = !this.isValidLabel(input.label) || !input.source.trim() || !input.processKind.trim() || !Number.isFinite(input.occurredAtMs);
|
|
777
|
+
if (hasInvalidIdentity) {
|
|
778
|
+
this.incrementDrop("invalid", this.priorityFor(input.level));
|
|
779
|
+
return { kind: "dropped", producerId: this.options.producerId, sequence, reason: "invalid-label" };
|
|
780
|
+
}
|
|
781
|
+
const priority = this.priorityFor(input.level);
|
|
782
|
+
if (!this.isSampled(priority)) {
|
|
783
|
+
this.incrementDrop("sampled", priority);
|
|
784
|
+
return { kind: "dropped", producerId: this.options.producerId, sequence, reason: "sampled" };
|
|
785
|
+
}
|
|
786
|
+
const snapshot = this.snapshotter.snapshot(input.metadata);
|
|
787
|
+
const extractedCorrelation = this.resolveCorrelation(snapshot.metadata);
|
|
788
|
+
const correlation = {
|
|
789
|
+
conversationId: input.conversationId ?? extractedCorrelation.conversationId,
|
|
790
|
+
correlationId: input.correlationId ?? extractedCorrelation.correlationId
|
|
791
|
+
};
|
|
792
|
+
const envelope = this.createEnvelope(sequence, input, snapshot.metadata, correlation);
|
|
793
|
+
const bytes = telemetryUtf8Length(canonicalJson(envelope));
|
|
794
|
+
const single = this.batchContract.create([envelope], []);
|
|
795
|
+
if (!single.ok) {
|
|
796
|
+
this.incrementDrop("invalid", priority);
|
|
797
|
+
return { kind: "dropped", producerId: this.options.producerId, sequence, reason: "invalid-envelope" };
|
|
798
|
+
}
|
|
799
|
+
const framedBytes = telemetryUtf8Length(canonicalJson(single.value));
|
|
800
|
+
const queueWouldOverflow = this.queue.length >= this.options.admission.maxQueuedEvents || this.queuedBytes + bytes > this.options.admission.maxQueuedBytes || framedBytes > this.options.admission.maxBatchBytes;
|
|
801
|
+
if (queueWouldOverflow) {
|
|
802
|
+
this.incrementDrop("capacity", priority);
|
|
803
|
+
return { kind: "dropped", producerId: this.options.producerId, sequence, reason: "capacity" };
|
|
804
|
+
}
|
|
805
|
+
this.queue.push(Object.freeze({ envelope, bytes }));
|
|
806
|
+
this.queuedBytes += bytes;
|
|
807
|
+
this.scheduleDrain();
|
|
808
|
+
return {
|
|
809
|
+
kind: "admitted",
|
|
810
|
+
producerId: this.options.producerId,
|
|
811
|
+
sequence,
|
|
812
|
+
eventId: envelope.eventId
|
|
813
|
+
};
|
|
814
|
+
}
|
|
815
|
+
status() {
|
|
816
|
+
return Object.freeze({
|
|
817
|
+
state: this.state,
|
|
818
|
+
producerId: this.options.producerId,
|
|
819
|
+
nextSequence: this.nextSequence,
|
|
820
|
+
queuedEvents: this.queue.length,
|
|
821
|
+
queuedBytes: this.queuedBytes,
|
|
822
|
+
inFlightBatches: this.inFlightBatches,
|
|
823
|
+
dropCounters: this.dropCounterSnapshot(),
|
|
824
|
+
lastAcknowledgement: this.lastAcknowledgement
|
|
825
|
+
});
|
|
826
|
+
}
|
|
827
|
+
async flush(deadlineMs) {
|
|
828
|
+
this.scheduleDrain();
|
|
829
|
+
while ((this.queue.length > 0 || this.activeDrain !== null) && this.options.now() < deadlineMs) {
|
|
830
|
+
const currentDrain = this.activeDrain;
|
|
831
|
+
if (currentDrain) {
|
|
832
|
+
await Promise.race([currentDrain, this.options.delay(1)]);
|
|
833
|
+
} else {
|
|
834
|
+
await this.options.delay(1);
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
await this.settleExternalShutdown(
|
|
838
|
+
() => this.options.transport.flush(deadlineMs),
|
|
839
|
+
deadlineMs
|
|
840
|
+
);
|
|
841
|
+
}
|
|
842
|
+
async stop(deadlineMs) {
|
|
843
|
+
if (this.state === "stopped") {
|
|
844
|
+
return;
|
|
845
|
+
}
|
|
846
|
+
this.state = "draining";
|
|
847
|
+
await this.flush(deadlineMs);
|
|
848
|
+
const abandoned = this.queue.splice(0, this.queue.length);
|
|
849
|
+
if (abandoned.length > 0) {
|
|
850
|
+
this.incrementDropBy("transport", "normal", abandoned.length);
|
|
851
|
+
this.queuedBytes = 0;
|
|
852
|
+
}
|
|
853
|
+
await this.settleExternalShutdown(
|
|
854
|
+
() => this.options.transport.close(deadlineMs),
|
|
855
|
+
deadlineMs
|
|
856
|
+
);
|
|
857
|
+
this.state = "stopped";
|
|
858
|
+
}
|
|
859
|
+
createEnvelope(sequence, input, metadata, correlation) {
|
|
860
|
+
return Object.freeze({
|
|
861
|
+
schemaVersion: TelemetryBatchContract.schemaVersion,
|
|
862
|
+
eventId: `${this.options.producerId}:${sequence}`,
|
|
863
|
+
producerId: this.options.producerId,
|
|
864
|
+
sequence,
|
|
865
|
+
occurredAtMs: input.occurredAtMs,
|
|
866
|
+
monotonicNs: input.monotonicNs,
|
|
867
|
+
source: input.source,
|
|
868
|
+
processKind: input.processKind,
|
|
869
|
+
level: input.level.trim().toLowerCase() || "info",
|
|
870
|
+
label: input.label,
|
|
871
|
+
conversationId: correlation.conversationId,
|
|
872
|
+
correlationId: correlation.correlationId,
|
|
873
|
+
metadata
|
|
874
|
+
});
|
|
875
|
+
}
|
|
876
|
+
resolveCorrelation(metadata) {
|
|
877
|
+
const conversationValue = metadata.conversationId ?? metadata.threadId ?? null;
|
|
878
|
+
const conversationId = typeof conversationValue === "string" || typeof conversationValue === "number" ? conversationValue : null;
|
|
879
|
+
const correlationValue = metadata.correlationId;
|
|
880
|
+
const correlationId = typeof correlationValue === "string" ? correlationValue : null;
|
|
881
|
+
return { conversationId, correlationId };
|
|
882
|
+
}
|
|
883
|
+
scheduleDrain() {
|
|
884
|
+
if (this.drainScheduled || this.activeDrain !== null || this.queue.length === 0) {
|
|
885
|
+
return;
|
|
886
|
+
}
|
|
887
|
+
this.drainScheduled = true;
|
|
888
|
+
try {
|
|
889
|
+
this.options.schedule(() => {
|
|
890
|
+
this.drainScheduled = false;
|
|
891
|
+
const drain = this.drainLoop();
|
|
892
|
+
this.activeDrain = drain;
|
|
893
|
+
const settle = () => {
|
|
894
|
+
this.activeDrain = null;
|
|
895
|
+
if (this.queue.length > 0) {
|
|
896
|
+
this.scheduleDrain();
|
|
897
|
+
}
|
|
898
|
+
};
|
|
899
|
+
void drain.then(settle, settle);
|
|
900
|
+
});
|
|
901
|
+
} catch {
|
|
902
|
+
this.drainScheduled = false;
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
async drainLoop() {
|
|
906
|
+
while (this.queue.length > 0) {
|
|
907
|
+
const dropSnapshot = this.dropCounterSnapshot();
|
|
908
|
+
const prepared = this.prepareBatch(dropSnapshot);
|
|
909
|
+
if (prepared === null) {
|
|
910
|
+
continue;
|
|
911
|
+
}
|
|
912
|
+
this.inFlightBatches += 1;
|
|
913
|
+
try {
|
|
914
|
+
const acknowledgement = await this.deliverWithRetry(prepared.batch);
|
|
915
|
+
this.lastAcknowledgement = acknowledgement;
|
|
916
|
+
if (acknowledgement.kind === "committed") {
|
|
917
|
+
this.subtractDropSnapshot(prepared.dropCounters);
|
|
918
|
+
} else {
|
|
919
|
+
this.incrementDropBy("transport", "normal", prepared.eventCount);
|
|
920
|
+
}
|
|
921
|
+
} finally {
|
|
922
|
+
this.inFlightBatches -= 1;
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
prepareBatch(dropCounters) {
|
|
927
|
+
const entries = this.peekBatchEntries();
|
|
928
|
+
const withCounters = this.findFittingBatch(entries, dropCounters);
|
|
929
|
+
const prepared = withCounters ?? this.findFittingBatch(entries, []);
|
|
930
|
+
if (prepared === null) {
|
|
931
|
+
const invalid = this.queue.shift();
|
|
932
|
+
if (invalid !== void 0) {
|
|
933
|
+
this.queuedBytes -= invalid.bytes;
|
|
934
|
+
this.incrementDrop("invalid", this.priorityFor(invalid.envelope.level));
|
|
935
|
+
}
|
|
936
|
+
return null;
|
|
937
|
+
}
|
|
938
|
+
for (let index = 0; index < prepared.eventCount; index += 1) {
|
|
939
|
+
const removed = this.queue.shift();
|
|
940
|
+
if (removed !== void 0) {
|
|
941
|
+
this.queuedBytes -= removed.bytes;
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
return prepared;
|
|
945
|
+
}
|
|
946
|
+
peekBatchEntries() {
|
|
947
|
+
const entries = [];
|
|
948
|
+
let batchBytes = 0;
|
|
949
|
+
while (entries.length < this.options.admission.maxBatchEvents) {
|
|
950
|
+
const next = this.queue[entries.length];
|
|
951
|
+
if (!next) {
|
|
952
|
+
break;
|
|
953
|
+
}
|
|
954
|
+
const exceedsBatchBytes = entries.length > 0 && batchBytes + next.bytes > this.options.admission.maxBatchBytes;
|
|
955
|
+
if (exceedsBatchBytes) {
|
|
956
|
+
break;
|
|
957
|
+
}
|
|
958
|
+
batchBytes += next.bytes;
|
|
959
|
+
entries.push(next);
|
|
960
|
+
}
|
|
961
|
+
return entries;
|
|
962
|
+
}
|
|
963
|
+
findFittingBatch(entries, dropCounters) {
|
|
964
|
+
for (let eventCount = entries.length; eventCount > 0; eventCount -= 1) {
|
|
965
|
+
const created = this.batchContract.create(
|
|
966
|
+
entries.slice(0, eventCount).map((entry) => entry.envelope),
|
|
967
|
+
dropCounters
|
|
968
|
+
);
|
|
969
|
+
if (created.ok && telemetryUtf8Length(canonicalJson(created.value)) <= this.options.admission.maxBatchBytes) {
|
|
970
|
+
return Object.freeze({ batch: created.value, dropCounters, eventCount });
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
return null;
|
|
974
|
+
}
|
|
975
|
+
async deliverWithRetry(batch) {
|
|
976
|
+
let attempt = 0;
|
|
977
|
+
while (attempt <= this.options.admission.retryLimit) {
|
|
978
|
+
let acknowledgement;
|
|
979
|
+
try {
|
|
980
|
+
const received = await this.options.transport.send(batch);
|
|
981
|
+
acknowledgement = this.normalizeAcknowledgement(batch, received, attempt);
|
|
982
|
+
} catch (cause) {
|
|
983
|
+
acknowledgement = {
|
|
984
|
+
kind: "retryable",
|
|
985
|
+
idempotencyKey: batch.idempotencyKey,
|
|
986
|
+
reason: this.errorMessage(cause),
|
|
987
|
+
retryAfterMs: this.retryDelay(attempt),
|
|
988
|
+
targetStates: []
|
|
989
|
+
};
|
|
990
|
+
}
|
|
991
|
+
if (acknowledgement.kind !== "retryable" || attempt === this.options.admission.retryLimit) {
|
|
992
|
+
return acknowledgement;
|
|
993
|
+
}
|
|
994
|
+
const requestedDelay = Math.max(acknowledgement.retryAfterMs, this.retryDelay(attempt));
|
|
995
|
+
await this.options.delay(requestedDelay);
|
|
996
|
+
attempt += 1;
|
|
997
|
+
}
|
|
998
|
+
return {
|
|
999
|
+
kind: "retryable",
|
|
1000
|
+
idempotencyKey: batch.idempotencyKey,
|
|
1001
|
+
reason: "retry-limit-exhausted",
|
|
1002
|
+
retryAfterMs: 0,
|
|
1003
|
+
targetStates: []
|
|
1004
|
+
};
|
|
1005
|
+
}
|
|
1006
|
+
retryDelay(attempt) {
|
|
1007
|
+
return Math.min(
|
|
1008
|
+
Number.MAX_SAFE_INTEGER,
|
|
1009
|
+
this.options.admission.retryBaseDelayMs * 2 ** attempt
|
|
1010
|
+
);
|
|
1011
|
+
}
|
|
1012
|
+
normalizeAcknowledgement(batch, value, attempt) {
|
|
1013
|
+
try {
|
|
1014
|
+
const error = this.acknowledgementError(batch, value);
|
|
1015
|
+
return error === null ? this.detachAcknowledgement(value) : this.invalidAcknowledgement(batch, error, attempt);
|
|
1016
|
+
} catch {
|
|
1017
|
+
return this.invalidAcknowledgement(batch, "acknowledgement could not be inspected", attempt);
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
acknowledgementError(batch, value) {
|
|
1021
|
+
if (!this.isRecord(value) || value.idempotencyKey !== batch.idempotencyKey) {
|
|
1022
|
+
return "acknowledgement identity is missing or mismatched";
|
|
1023
|
+
}
|
|
1024
|
+
const targetsError = this.targetStatesError(value.targetStates);
|
|
1025
|
+
if (targetsError !== null) {
|
|
1026
|
+
return targetsError;
|
|
1027
|
+
}
|
|
1028
|
+
switch (value.kind) {
|
|
1029
|
+
case "committed":
|
|
1030
|
+
return this.committedAcknowledgementError(batch, value);
|
|
1031
|
+
case "retryable":
|
|
1032
|
+
return this.retryableAcknowledgementError(value);
|
|
1033
|
+
case "rejected":
|
|
1034
|
+
return this.rejectedAcknowledgementError(value);
|
|
1035
|
+
case "conflict":
|
|
1036
|
+
return this.conflictAcknowledgementError(value);
|
|
1037
|
+
default:
|
|
1038
|
+
return "acknowledgement kind is invalid";
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
committedAcknowledgementError(batch, value) {
|
|
1042
|
+
const valid = this.hasExactKeys(value, [
|
|
1043
|
+
"kind",
|
|
1044
|
+
"idempotencyKey",
|
|
1045
|
+
"committedAtMs",
|
|
1046
|
+
"acceptedEvents",
|
|
1047
|
+
"duplicate",
|
|
1048
|
+
"targetStates"
|
|
1049
|
+
]) && Number.isSafeInteger(value.committedAtMs) && Number(value.committedAtMs) >= 0 && value.acceptedEvents === batch.envelopes.length && typeof value.duplicate === "boolean";
|
|
1050
|
+
return valid ? null : "committed acknowledgement fields are invalid";
|
|
1051
|
+
}
|
|
1052
|
+
retryableAcknowledgementError(value) {
|
|
1053
|
+
const valid = this.hasExactKeys(value, [
|
|
1054
|
+
"kind",
|
|
1055
|
+
"idempotencyKey",
|
|
1056
|
+
"reason",
|
|
1057
|
+
"retryAfterMs",
|
|
1058
|
+
"targetStates"
|
|
1059
|
+
]) && this.isBoundedReason(value.reason) && Number.isFinite(value.retryAfterMs) && Number(value.retryAfterMs) >= 0 && Number(value.retryAfterMs) <= Number.MAX_SAFE_INTEGER;
|
|
1060
|
+
return valid ? null : "retryable acknowledgement fields are invalid";
|
|
1061
|
+
}
|
|
1062
|
+
rejectedAcknowledgementError(value) {
|
|
1063
|
+
const codes = ["invalid-batch", "unsupported-version", "no-required-terminal", "closed"];
|
|
1064
|
+
const valid = this.hasExactKeys(value, [
|
|
1065
|
+
"kind",
|
|
1066
|
+
"idempotencyKey",
|
|
1067
|
+
"code",
|
|
1068
|
+
"reason",
|
|
1069
|
+
"targetStates"
|
|
1070
|
+
]) && codes.includes(String(value.code)) && this.isBoundedReason(value.reason);
|
|
1071
|
+
return valid ? null : "rejected acknowledgement fields are invalid";
|
|
1072
|
+
}
|
|
1073
|
+
conflictAcknowledgementError(value) {
|
|
1074
|
+
const valid = this.hasExactKeys(value, ["kind", "idempotencyKey", "reason", "targetStates"]) && this.isBoundedReason(value.reason);
|
|
1075
|
+
return valid ? null : "conflict acknowledgement fields are invalid";
|
|
1076
|
+
}
|
|
1077
|
+
targetStatesError(value) {
|
|
1078
|
+
if (!Array.isArray(value) || value.length > 1024) {
|
|
1079
|
+
return "acknowledgement target states are invalid";
|
|
1080
|
+
}
|
|
1081
|
+
const targetIds = /* @__PURE__ */ new Set();
|
|
1082
|
+
for (const target of value) {
|
|
1083
|
+
if (!this.isValidTargetState(target) || targetIds.has(target.targetId)) {
|
|
1084
|
+
return "acknowledgement target states are invalid";
|
|
1085
|
+
}
|
|
1086
|
+
targetIds.add(target.targetId);
|
|
1087
|
+
}
|
|
1088
|
+
return null;
|
|
1089
|
+
}
|
|
1090
|
+
isValidTargetState(value) {
|
|
1091
|
+
if (!this.isRecord(value) || !this.hasExactKeys(value, ["targetId", "delivery", "state", "attempts", "reason"])) {
|
|
1092
|
+
return false;
|
|
1093
|
+
}
|
|
1094
|
+
const deliveries = ["required", "best-effort"];
|
|
1095
|
+
const states = ["pending", "committed", "failed", "abandoned"];
|
|
1096
|
+
return typeof value.targetId === "string" && value.targetId.length > 0 && telemetryUtf8Length(value.targetId) <= 256 && deliveries.includes(String(value.delivery)) && states.includes(String(value.state)) && Number.isSafeInteger(value.attempts) && Number(value.attempts) >= 0 && (value.reason === null || this.isBoundedReason(value.reason));
|
|
1097
|
+
}
|
|
1098
|
+
invalidAcknowledgement(batch, error, attempt) {
|
|
1099
|
+
return {
|
|
1100
|
+
kind: "retryable",
|
|
1101
|
+
idempotencyKey: batch.idempotencyKey,
|
|
1102
|
+
reason: `Invalid telemetry acknowledgement: ${error}`,
|
|
1103
|
+
retryAfterMs: this.retryDelay(attempt),
|
|
1104
|
+
targetStates: []
|
|
1105
|
+
};
|
|
1106
|
+
}
|
|
1107
|
+
detachAcknowledgement(value) {
|
|
1108
|
+
const detached = JSON.parse(canonicalJson(value));
|
|
1109
|
+
const targetStates = Object.freeze(detached.targetStates.map((target) => Object.freeze({ ...target })));
|
|
1110
|
+
return Object.freeze({ ...detached, targetStates });
|
|
1111
|
+
}
|
|
1112
|
+
isRecord(value) {
|
|
1113
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1114
|
+
}
|
|
1115
|
+
hasExactKeys(value, expected) {
|
|
1116
|
+
const keys = Object.keys(value);
|
|
1117
|
+
return Object.getOwnPropertySymbols(value).length === 0 && keys.length === expected.length && expected.every((key) => Object.prototype.hasOwnProperty.call(value, key));
|
|
1118
|
+
}
|
|
1119
|
+
isBoundedReason(value) {
|
|
1120
|
+
return typeof value === "string" && telemetryUtf8Length(value) <= 512;
|
|
1121
|
+
}
|
|
1122
|
+
dropCounterSnapshot() {
|
|
1123
|
+
const counters = [...this.dropCounts.entries()].filter(([, count]) => count > 0).sort(([left], [right]) => left.localeCompare(right)).map(([key, count]) => {
|
|
1124
|
+
const [reason, priority] = key.split(":");
|
|
1125
|
+
return Object.freeze({
|
|
1126
|
+
reason,
|
|
1127
|
+
priority,
|
|
1128
|
+
count
|
|
1129
|
+
});
|
|
1130
|
+
});
|
|
1131
|
+
return Object.freeze(counters);
|
|
1132
|
+
}
|
|
1133
|
+
subtractDropSnapshot(snapshot) {
|
|
1134
|
+
for (const counter of snapshot) {
|
|
1135
|
+
const key = `${counter.reason}:${counter.priority}`;
|
|
1136
|
+
const current = this.dropCounts.get(key) ?? 0;
|
|
1137
|
+
this.dropCounts.set(key, Math.max(0, current - counter.count));
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
incrementDrop(reason, priority) {
|
|
1141
|
+
this.incrementDropBy(reason, priority, 1);
|
|
1142
|
+
}
|
|
1143
|
+
incrementDropBy(reason, priority, count) {
|
|
1144
|
+
const key = `${reason}:${priority}`;
|
|
1145
|
+
const current = this.dropCounts.get(key) ?? 0;
|
|
1146
|
+
this.dropCounts.set(key, Math.min(Number.MAX_SAFE_INTEGER, current + count));
|
|
1147
|
+
}
|
|
1148
|
+
priorityFor(level) {
|
|
1149
|
+
const normalized = level.trim().toLowerCase();
|
|
1150
|
+
if (normalized === "error" || normalized === "fatal") {
|
|
1151
|
+
return "high";
|
|
1152
|
+
}
|
|
1153
|
+
if (normalized === "trace" || normalized === "debug") {
|
|
1154
|
+
return "low";
|
|
1155
|
+
}
|
|
1156
|
+
return "normal";
|
|
1157
|
+
}
|
|
1158
|
+
isSampled(priority) {
|
|
1159
|
+
const sampleRate = this.options.admission.sampleRates[priority];
|
|
1160
|
+
return sampleRate >= 1 || this.options.random() < sampleRate;
|
|
1161
|
+
}
|
|
1162
|
+
isValidLabel(label) {
|
|
1163
|
+
return /^[A-Za-z0-9][A-Za-z0-9_.:-]*$/.test(label) && label.length <= 256;
|
|
1164
|
+
}
|
|
1165
|
+
errorMessage(cause) {
|
|
1166
|
+
if (cause instanceof Error) {
|
|
1167
|
+
return cause.message.slice(0, 512);
|
|
1168
|
+
}
|
|
1169
|
+
return String(cause).slice(0, 512);
|
|
1170
|
+
}
|
|
1171
|
+
async settleExternalShutdown(work, deadlineMs) {
|
|
1172
|
+
const operation = Promise.resolve().then(work);
|
|
1173
|
+
const remainingMs = deadlineMs - this.options.now();
|
|
1174
|
+
if (!Number.isFinite(remainingMs) || remainingMs <= 0) {
|
|
1175
|
+
void operation.catch(() => void 0);
|
|
1176
|
+
return;
|
|
1177
|
+
}
|
|
1178
|
+
await new Promise((resolve, reject) => {
|
|
1179
|
+
let settled = false;
|
|
1180
|
+
const finish = (failed, error) => {
|
|
1181
|
+
if (settled) {
|
|
1182
|
+
return;
|
|
1183
|
+
}
|
|
1184
|
+
settled = true;
|
|
1185
|
+
clearTimeout(timer);
|
|
1186
|
+
if (failed) {
|
|
1187
|
+
reject(error);
|
|
1188
|
+
} else {
|
|
1189
|
+
resolve();
|
|
1190
|
+
}
|
|
1191
|
+
};
|
|
1192
|
+
const timer = setTimeout(
|
|
1193
|
+
() => finish(false),
|
|
1194
|
+
Math.min(remainingMs, 2147483647)
|
|
1195
|
+
);
|
|
1196
|
+
void operation.then(
|
|
1197
|
+
() => finish(false),
|
|
1198
|
+
(error) => finish(true, error)
|
|
1199
|
+
);
|
|
1200
|
+
});
|
|
1201
|
+
}
|
|
1202
|
+
validateOptions() {
|
|
1203
|
+
this.validateIdentityOptions();
|
|
1204
|
+
this.validateOrderedDeliveryOptions();
|
|
1205
|
+
this.validateCapacityOptions();
|
|
1206
|
+
this.validateBatchEventOptions();
|
|
1207
|
+
this.validateBatchByteOptions();
|
|
1208
|
+
this.validateMetadataOptions();
|
|
1209
|
+
this.validateRetryOptions();
|
|
1210
|
+
this.validateSampleRateOptions();
|
|
1211
|
+
}
|
|
1212
|
+
validateIdentityOptions() {
|
|
1213
|
+
if (!this.options.source.trim() || !this.options.processKind.trim() || !this.options.producerId.trim()) {
|
|
1214
|
+
throw new Error("Telemetry client source, process kind, and producer id are required");
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
validateOrderedDeliveryOptions() {
|
|
1218
|
+
if (this.options.admission.maxInFlightBatches !== 1) {
|
|
1219
|
+
throw new Error("Ordered telemetry clients currently require maxInFlightBatches to equal 1");
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
validateCapacityOptions() {
|
|
1223
|
+
const capacities = [
|
|
1224
|
+
this.options.admission.maxQueuedEvents,
|
|
1225
|
+
this.options.admission.maxQueuedBytes,
|
|
1226
|
+
this.options.admission.maxBatchEvents,
|
|
1227
|
+
this.options.admission.maxBatchBytes,
|
|
1228
|
+
this.options.admission.maxInFlightBatches,
|
|
1229
|
+
this.options.transport.capabilities.maxBatchBytes,
|
|
1230
|
+
this.options.transport.capabilities.boundedCapacity
|
|
1231
|
+
];
|
|
1232
|
+
if (capacities.some((capacity) => !Number.isSafeInteger(capacity) || capacity <= 0)) {
|
|
1233
|
+
throw new Error("Telemetry client and transport capacities must be positive safe integers");
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
validateBatchEventOptions() {
|
|
1237
|
+
if (this.options.admission.maxBatchEvents > this.options.admission.maxQueuedEvents || this.options.admission.maxBatchEvents > this.batchContract.limits.maxBatchEvents) {
|
|
1238
|
+
throw new Error("Telemetry maxBatchEvents must not exceed queue or receiver capacity");
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
validateBatchByteOptions() {
|
|
1242
|
+
if (this.options.admission.maxBatchBytes > this.options.admission.maxQueuedBytes || this.options.admission.maxBatchBytes > this.options.transport.capabilities.maxBatchBytes || this.options.admission.maxBatchBytes > this.batchContract.limits.maxBatchBytes) {
|
|
1243
|
+
throw new Error("Telemetry maxBatchBytes must not exceed queue, transport, or receiver capacity");
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
validateMetadataOptions() {
|
|
1247
|
+
if (this.options.metadata.maxBytes > this.options.admission.maxBatchBytes) {
|
|
1248
|
+
throw new Error("Telemetry metadata bytes must not exceed maxBatchBytes");
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
validateRetryOptions() {
|
|
1252
|
+
if (!Number.isSafeInteger(this.options.admission.retryLimit) || this.options.admission.retryLimit < 0 || !Number.isFinite(this.options.admission.retryBaseDelayMs) || this.options.admission.retryBaseDelayMs < 0) {
|
|
1253
|
+
throw new Error("Telemetry retry limit and delay must be nonnegative finite values");
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
validateSampleRateOptions() {
|
|
1257
|
+
const rates = Object.values(this.options.admission.sampleRates);
|
|
1258
|
+
if (rates.some((rate) => !Number.isFinite(rate) || rate < 0 || rate > 1)) {
|
|
1259
|
+
throw new Error("Telemetry sample rates must be between zero and one");
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
};
|
|
1263
|
+
|
|
1264
|
+
// src/platform/browser/browser_telemetry_client.ts
|
|
1265
|
+
var BrowserTelemetryClient = class {
|
|
1266
|
+
telemetry;
|
|
1267
|
+
lifecycle = null;
|
|
1268
|
+
pageHideListener = () => {
|
|
1269
|
+
void this.telemetry.flush(Date.now() + 100);
|
|
1270
|
+
};
|
|
1271
|
+
constructor(options) {
|
|
1272
|
+
const producerId = options.producerId ?? this.createProducerId();
|
|
1273
|
+
this.telemetry = new TelemetryClient({
|
|
1274
|
+
source: options.source,
|
|
1275
|
+
processKind: options.processKind,
|
|
1276
|
+
transport: options.transport,
|
|
1277
|
+
admission: options.admission,
|
|
1278
|
+
metadata: options.metadata,
|
|
1279
|
+
producerId,
|
|
1280
|
+
now: () => Date.now(),
|
|
1281
|
+
monotonicNowNs: () => this.monotonicNowNs(),
|
|
1282
|
+
schedule: (work) => queueMicrotask(work),
|
|
1283
|
+
delay: (durationMs) => new Promise((resolve) => setTimeout(resolve, durationMs)),
|
|
1284
|
+
random: () => Math.random()
|
|
1285
|
+
});
|
|
1286
|
+
}
|
|
1287
|
+
attachLifecycle(target) {
|
|
1288
|
+
this.detachLifecycle();
|
|
1289
|
+
this.lifecycle = target;
|
|
1290
|
+
target.addEventListener("pagehide", this.pageHideListener);
|
|
1291
|
+
}
|
|
1292
|
+
detachLifecycle() {
|
|
1293
|
+
this.lifecycle?.removeEventListener("pagehide", this.pageHideListener);
|
|
1294
|
+
this.lifecycle = null;
|
|
1295
|
+
}
|
|
1296
|
+
async stop(deadlineMs) {
|
|
1297
|
+
this.detachLifecycle();
|
|
1298
|
+
await this.telemetry.stop(deadlineMs);
|
|
1299
|
+
}
|
|
1300
|
+
createProducerId() {
|
|
1301
|
+
const cryptoApi = globalThis.crypto;
|
|
1302
|
+
if (cryptoApi && typeof cryptoApi.randomUUID === "function") {
|
|
1303
|
+
return cryptoApi.randomUUID();
|
|
1304
|
+
}
|
|
1305
|
+
const randomPart = Math.random().toString(36).slice(2);
|
|
1306
|
+
return `browser-${Date.now().toString(36)}-${randomPart}`;
|
|
1307
|
+
}
|
|
1308
|
+
monotonicNowNs() {
|
|
1309
|
+
const performanceApi = globalThis.performance;
|
|
1310
|
+
return performanceApi ? Math.floor(performanceApi.now() * 1e6).toString() : null;
|
|
1311
|
+
}
|
|
1312
|
+
};
|
|
1313
|
+
export {
|
|
1314
|
+
BrowserTelemetryClient,
|
|
1315
|
+
DEFAULT_METADATA_SNAPSHOT_LIMITS,
|
|
1316
|
+
DEFAULT_TELEMETRY_ADMISSION_POLICY
|
|
1317
|
+
};
|
|
1318
|
+
//# sourceMappingURL=index.js.map
|