@tallyrow/safesignal 1.0.1 → 1.3.0
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 +127 -7
- package/dist/index.cjs +81 -12
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +26 -3
- package/dist/index.d.ts +26 -3
- package/dist/index.mjs +81 -13
- package/dist/index.mjs.map +1 -1
- package/dist/testing.cjs.map +1 -1
- package/dist/testing.d.cts +1 -1
- package/dist/testing.d.ts +1 -1
- package/dist/testing.mjs.map +1 -1
- package/dist/transport-beacon.cjs +65 -50
- package/dist/transport-beacon.cjs.map +1 -1
- package/dist/transport-beacon.d.cts +1 -1
- package/dist/transport-beacon.d.ts +1 -1
- package/dist/transport-beacon.mjs +65 -50
- package/dist/transport-beacon.mjs.map +1 -1
- package/dist/transport-otlp.cjs +621 -0
- package/dist/transport-otlp.cjs.map +1 -0
- package/dist/transport-otlp.d.cts +71 -0
- package/dist/transport-otlp.d.ts +71 -0
- package/dist/transport-otlp.mjs +619 -0
- package/dist/transport-otlp.mjs.map +1 -0
- package/dist/{types-D-xVvmvX.d.cts → types-BiRyHi1e.d.cts} +20 -1
- package/dist/{types-D-xVvmvX.d.ts → types-BiRyHi1e.d.ts} +20 -1
- package/package.json +17 -4
|
@@ -0,0 +1,619 @@
|
|
|
1
|
+
// src/transport-otlp/batcher.ts
|
|
2
|
+
function createBatcher(opts) {
|
|
3
|
+
const buffer = [];
|
|
4
|
+
let maxAgeTimer = null;
|
|
5
|
+
let flushCallback = opts.flush;
|
|
6
|
+
const clearTimer = () => {
|
|
7
|
+
if (maxAgeTimer !== null) {
|
|
8
|
+
clearTimeout(maxAgeTimer);
|
|
9
|
+
maxAgeTimer = null;
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
const armTimer = () => {
|
|
13
|
+
if (opts.maxBatchAgeMs === void 0) return;
|
|
14
|
+
maxAgeTimer = setTimeout(() => {
|
|
15
|
+
maxAgeTimer = null;
|
|
16
|
+
doFlush();
|
|
17
|
+
}, opts.maxBatchAgeMs);
|
|
18
|
+
};
|
|
19
|
+
const doFlush = () => {
|
|
20
|
+
if (buffer.length === 0) return;
|
|
21
|
+
if (flushCallback === null) {
|
|
22
|
+
buffer.length = 0;
|
|
23
|
+
clearTimer();
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
const events = buffer.slice();
|
|
27
|
+
buffer.length = 0;
|
|
28
|
+
clearTimer();
|
|
29
|
+
try {
|
|
30
|
+
flushCallback(events);
|
|
31
|
+
} catch {
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
return {
|
|
35
|
+
push(event) {
|
|
36
|
+
if (flushCallback === null) return;
|
|
37
|
+
buffer.push(event);
|
|
38
|
+
if (buffer.length === 1 && opts.maxBatchAgeMs !== void 0) {
|
|
39
|
+
armTimer();
|
|
40
|
+
}
|
|
41
|
+
if (buffer.length >= opts.maxBatchSize) {
|
|
42
|
+
doFlush();
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
flush() {
|
|
46
|
+
doFlush();
|
|
47
|
+
},
|
|
48
|
+
shutdown() {
|
|
49
|
+
clearTimer();
|
|
50
|
+
buffer.length = 0;
|
|
51
|
+
flushCallback = null;
|
|
52
|
+
},
|
|
53
|
+
size() {
|
|
54
|
+
return buffer.length;
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// src/transport-otlp/delivery.ts
|
|
60
|
+
async function deliver(endpoint, headers, body) {
|
|
61
|
+
const fetchFn = globalThis.fetch;
|
|
62
|
+
if (typeof fetchFn !== "function") {
|
|
63
|
+
return { kind: "unavailable" };
|
|
64
|
+
}
|
|
65
|
+
let response;
|
|
66
|
+
try {
|
|
67
|
+
response = await fetchFn(endpoint, {
|
|
68
|
+
method: "POST",
|
|
69
|
+
body,
|
|
70
|
+
headers: { "content-type": "application/json", ...headers },
|
|
71
|
+
keepalive: true,
|
|
72
|
+
credentials: "same-origin"
|
|
73
|
+
});
|
|
74
|
+
} catch (cause) {
|
|
75
|
+
return { kind: "send_failed", detail: "fetch rejected", cause };
|
|
76
|
+
}
|
|
77
|
+
if (!response.ok) {
|
|
78
|
+
return { kind: "send_failed", detail: `HTTP ${response.status}` };
|
|
79
|
+
}
|
|
80
|
+
const rejected = await readRejectedCount(response);
|
|
81
|
+
if (rejected > 0) {
|
|
82
|
+
return { kind: "partial_rejection", rejected };
|
|
83
|
+
}
|
|
84
|
+
return { kind: "delivered" };
|
|
85
|
+
}
|
|
86
|
+
async function readRejectedCount(response) {
|
|
87
|
+
try {
|
|
88
|
+
if (typeof response.json !== "function") return 0;
|
|
89
|
+
const parsed = await response.json();
|
|
90
|
+
if (typeof parsed !== "object" || parsed === null) return 0;
|
|
91
|
+
const partial = parsed.partialSuccess;
|
|
92
|
+
if (typeof partial !== "object" || partial === null) return 0;
|
|
93
|
+
const raw = partial.rejectedLogRecords;
|
|
94
|
+
const n = typeof raw === "string" ? Number(raw) : raw;
|
|
95
|
+
return typeof n === "number" && Number.isFinite(n) && n > 0 ? n : 0;
|
|
96
|
+
} catch {
|
|
97
|
+
return 0;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// src/transport-otlp/endpoint-validation.ts
|
|
102
|
+
var LOOPBACK_HOSTS = /* @__PURE__ */ new Set([
|
|
103
|
+
"localhost",
|
|
104
|
+
"127.0.0.1",
|
|
105
|
+
"[::1]"
|
|
106
|
+
]);
|
|
107
|
+
function validateEndpoint(endpoint, allowInsecureLoopback) {
|
|
108
|
+
if (typeof endpoint !== "string") {
|
|
109
|
+
throw new TypeError(
|
|
110
|
+
`otlp transport: endpoint must be a string, got ${typeName(endpoint)}`
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
let parsed;
|
|
114
|
+
try {
|
|
115
|
+
parsed = new URL(endpoint);
|
|
116
|
+
} catch {
|
|
117
|
+
throw new TypeError(`otlp transport: invalid endpoint URL: '${endpoint}'`);
|
|
118
|
+
}
|
|
119
|
+
if (parsed.protocol === "https:") {
|
|
120
|
+
return parsed;
|
|
121
|
+
}
|
|
122
|
+
if (parsed.protocol === "http:") {
|
|
123
|
+
if (!allowInsecureLoopback) {
|
|
124
|
+
throw new Error(
|
|
125
|
+
`otlp transport refuses non-HTTPS endpoint '${endpoint}'`
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
if (!LOOPBACK_HOSTS.has(parsed.hostname)) {
|
|
129
|
+
throw new Error(
|
|
130
|
+
`otlp transport: allowInsecureLoopback permits only localhost / 127.0.0.1 / [::1]; got '${parsed.hostname}' in '${endpoint}'`
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
return parsed;
|
|
134
|
+
}
|
|
135
|
+
throw new Error(
|
|
136
|
+
`otlp transport refuses non-HTTPS endpoint '${endpoint}' (scheme '${parsed.protocol}' is not permitted)`
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
function typeName(value) {
|
|
140
|
+
if (value === null) return "null";
|
|
141
|
+
return typeof value;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// src/transport-otlp/errors.ts
|
|
145
|
+
var OtlpError = class extends Error {
|
|
146
|
+
constructor(code, transportName, message, cause) {
|
|
147
|
+
super(message);
|
|
148
|
+
this.name = "OtlpError";
|
|
149
|
+
this.code = code;
|
|
150
|
+
this.transportName = transportName;
|
|
151
|
+
if (cause !== void 0) {
|
|
152
|
+
Object.defineProperty(this, "cause", {
|
|
153
|
+
value: cause,
|
|
154
|
+
enumerable: true,
|
|
155
|
+
writable: false,
|
|
156
|
+
configurable: false
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
function notifyOnce(ctx, code, message, cause) {
|
|
162
|
+
if (ctx.notified[code]) return;
|
|
163
|
+
ctx.notified[code] = true;
|
|
164
|
+
const err = new OtlpError(
|
|
165
|
+
code,
|
|
166
|
+
ctx.name,
|
|
167
|
+
`otlp transport '${ctx.name}': ${message}`,
|
|
168
|
+
cause
|
|
169
|
+
);
|
|
170
|
+
try {
|
|
171
|
+
ctx.onInternalError(err);
|
|
172
|
+
} catch {
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
function freshNotifiedLedger() {
|
|
176
|
+
return {
|
|
177
|
+
oversized_event: false,
|
|
178
|
+
buffer_overflow: false,
|
|
179
|
+
delivery_unavailable: false,
|
|
180
|
+
send_failed: false,
|
|
181
|
+
partial_rejection: false,
|
|
182
|
+
serialize_failed: false,
|
|
183
|
+
shutdown_failed: false
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// src/transport-otlp/attributes.ts
|
|
188
|
+
function toAnyValue(value) {
|
|
189
|
+
if (value === null) {
|
|
190
|
+
return {};
|
|
191
|
+
}
|
|
192
|
+
switch (typeof value) {
|
|
193
|
+
case "string":
|
|
194
|
+
return { stringValue: value };
|
|
195
|
+
case "boolean":
|
|
196
|
+
return { boolValue: value };
|
|
197
|
+
case "number":
|
|
198
|
+
return Number.isInteger(value) ? { intValue: String(value) } : { doubleValue: value };
|
|
199
|
+
}
|
|
200
|
+
if (Array.isArray(value)) {
|
|
201
|
+
return { arrayValue: { values: value.map(toAnyValue) } };
|
|
202
|
+
}
|
|
203
|
+
return { kvlistValue: { values: toKeyValues(value) } };
|
|
204
|
+
}
|
|
205
|
+
function toKeyValues(record, keyPrefix = "") {
|
|
206
|
+
const out = [];
|
|
207
|
+
for (const key of Object.keys(record)) {
|
|
208
|
+
const value = record[key];
|
|
209
|
+
if (value === void 0) continue;
|
|
210
|
+
out.push({ key: keyPrefix + key, value: toAnyValue(value) });
|
|
211
|
+
}
|
|
212
|
+
return out;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// src/transport-otlp/resource.ts
|
|
216
|
+
function buildResource(context) {
|
|
217
|
+
const attributes = [];
|
|
218
|
+
const push = (key, value) => {
|
|
219
|
+
if (typeof value === "string" && value.length > 0) {
|
|
220
|
+
attributes.push({ key, value: { stringValue: value } });
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
push("service.name", context.application?.name);
|
|
224
|
+
push("service.version", context.application?.version);
|
|
225
|
+
push("deployment.environment", context.environment);
|
|
226
|
+
return { attributes };
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// src/transport-otlp/otlp-serializer.ts
|
|
230
|
+
var SCOPE_NAME = "@tallyrow/safesignal";
|
|
231
|
+
var LEVEL_TO_SEVERITY_NUMBER = {
|
|
232
|
+
debug: 5,
|
|
233
|
+
info: 9,
|
|
234
|
+
warn: 13,
|
|
235
|
+
error: 17
|
|
236
|
+
};
|
|
237
|
+
var LEVEL_TO_SEVERITY_TEXT = {
|
|
238
|
+
debug: "DEBUG",
|
|
239
|
+
info: "INFO",
|
|
240
|
+
warn: "WARN",
|
|
241
|
+
error: "ERROR"
|
|
242
|
+
};
|
|
243
|
+
function toLogRecord(event, fallbackTimeMs) {
|
|
244
|
+
const ms = toEpochMs(event.timestamp, fallbackTimeMs);
|
|
245
|
+
const nano = String(ms * 1e6);
|
|
246
|
+
const attributes = toKeyValues(event.attributes);
|
|
247
|
+
const context = event.context;
|
|
248
|
+
if (context.attributes !== void 0) {
|
|
249
|
+
attributes.push(...toKeyValues(context.attributes, "context."));
|
|
250
|
+
}
|
|
251
|
+
pushModuleIdentity(attributes, context);
|
|
252
|
+
pushException(attributes, event);
|
|
253
|
+
const record = {
|
|
254
|
+
timeUnixNano: nano,
|
|
255
|
+
observedTimeUnixNano: nano,
|
|
256
|
+
severityNumber: LEVEL_TO_SEVERITY_NUMBER[event.level],
|
|
257
|
+
severityText: LEVEL_TO_SEVERITY_TEXT[event.level],
|
|
258
|
+
body: { stringValue: event.message },
|
|
259
|
+
attributes
|
|
260
|
+
};
|
|
261
|
+
const trace = context.trace;
|
|
262
|
+
if (trace !== void 0) {
|
|
263
|
+
record.traceId = trace.traceId;
|
|
264
|
+
record.spanId = trace.spanId;
|
|
265
|
+
if (trace.traceFlags !== void 0) {
|
|
266
|
+
record.flags = trace.traceFlags;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
return record;
|
|
270
|
+
}
|
|
271
|
+
function serializeBatch(batch, fallbackTimeMs) {
|
|
272
|
+
const first = batch[0];
|
|
273
|
+
const resource = buildResource(first ? first.context : {});
|
|
274
|
+
const logRecords = batch.map((e) => toLogRecord(e, fallbackTimeMs));
|
|
275
|
+
return {
|
|
276
|
+
resourceLogs: [
|
|
277
|
+
{
|
|
278
|
+
resource,
|
|
279
|
+
scopeLogs: [{ scope: { name: SCOPE_NAME }, logRecords }]
|
|
280
|
+
}
|
|
281
|
+
]
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
function encode(request) {
|
|
285
|
+
return JSON.stringify(request);
|
|
286
|
+
}
|
|
287
|
+
function toEpochMs(iso, fallbackMs) {
|
|
288
|
+
const parsed = Date.parse(iso);
|
|
289
|
+
return Number.isFinite(parsed) ? parsed : fallbackMs;
|
|
290
|
+
}
|
|
291
|
+
function pushModuleIdentity(out, context) {
|
|
292
|
+
const mod = context.module;
|
|
293
|
+
if (mod === void 0) return;
|
|
294
|
+
if (typeof mod.name === "string" && mod.name.length > 0) {
|
|
295
|
+
out.push({ key: "module.name", value: { stringValue: mod.name } });
|
|
296
|
+
}
|
|
297
|
+
if (typeof mod.version === "string" && mod.version.length > 0) {
|
|
298
|
+
out.push({ key: "module.version", value: { stringValue: mod.version } });
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
function pushException(out, event) {
|
|
302
|
+
const err = event.error;
|
|
303
|
+
if (err === void 0) return;
|
|
304
|
+
out.push({ key: "exception.type", value: { stringValue: err.name } });
|
|
305
|
+
out.push({ key: "exception.message", value: { stringValue: err.message } });
|
|
306
|
+
if (typeof err.stack === "string" && err.stack.length > 0) {
|
|
307
|
+
out.push({
|
|
308
|
+
key: "exception.stacktrace",
|
|
309
|
+
value: { stringValue: err.stack }
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// src/transport-otlp/traceparent-header.ts
|
|
315
|
+
var MAX_TRACESTATE_LEN = 512;
|
|
316
|
+
var TRACE_ID_RE = /^[0-9a-f]{32}$/;
|
|
317
|
+
var SPAN_ID_RE = /^[0-9a-f]{16}$/;
|
|
318
|
+
var ALL_ZERO_TRACE_ID = "0".repeat(32);
|
|
319
|
+
var ALL_ZERO_SPAN_ID = "0".repeat(16);
|
|
320
|
+
var NONE = {
|
|
321
|
+
key: "none",
|
|
322
|
+
traceparent: null,
|
|
323
|
+
traceState: null
|
|
324
|
+
};
|
|
325
|
+
function hasValidIds(trace) {
|
|
326
|
+
return typeof trace.traceId === "string" && TRACE_ID_RE.test(trace.traceId) && trace.traceId !== ALL_ZERO_TRACE_ID && typeof trace.spanId === "string" && SPAN_ID_RE.test(trace.spanId) && trace.spanId !== ALL_ZERO_SPAN_ID;
|
|
327
|
+
}
|
|
328
|
+
function flagsHex(traceFlags) {
|
|
329
|
+
const n = typeof traceFlags === "number" && Number.isInteger(traceFlags) && traceFlags >= 0 && traceFlags <= 255 ? traceFlags : 0;
|
|
330
|
+
return n.toString(16).padStart(2, "0");
|
|
331
|
+
}
|
|
332
|
+
function resolve(event) {
|
|
333
|
+
const trace = event.context.trace;
|
|
334
|
+
if (trace === void 0 || !hasValidIds(trace)) {
|
|
335
|
+
return NONE;
|
|
336
|
+
}
|
|
337
|
+
const traceparent = `00-${trace.traceId}-${trace.spanId}-${flagsHex(
|
|
338
|
+
trace.traceFlags
|
|
339
|
+
)}`;
|
|
340
|
+
const traceState = typeof trace.traceState === "string" && trace.traceState.length > 0 && trace.traceState.length <= MAX_TRACESTATE_LEN ? trace.traceState : null;
|
|
341
|
+
return { key: traceparent, traceparent, traceState };
|
|
342
|
+
}
|
|
343
|
+
function decideBatchTraceparent(events) {
|
|
344
|
+
if (events.length === 0) {
|
|
345
|
+
return { inject: false };
|
|
346
|
+
}
|
|
347
|
+
const first = resolve(events[0]);
|
|
348
|
+
if (first.key === "none") {
|
|
349
|
+
return { inject: false };
|
|
350
|
+
}
|
|
351
|
+
let tracestateUniform = true;
|
|
352
|
+
for (let i = 1; i < events.length; i += 1) {
|
|
353
|
+
const r = resolve(events[i]);
|
|
354
|
+
if (r.key !== first.key) {
|
|
355
|
+
return { inject: false };
|
|
356
|
+
}
|
|
357
|
+
if (r.traceState !== first.traceState) {
|
|
358
|
+
tracestateUniform = false;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
const traceparent = first.traceparent;
|
|
362
|
+
if (tracestateUniform && first.traceState !== null) {
|
|
363
|
+
return { inject: true, traceparent, tracestate: first.traceState };
|
|
364
|
+
}
|
|
365
|
+
return { inject: true, traceparent };
|
|
366
|
+
}
|
|
367
|
+
function buildRequestHeaders(base, events, enabled) {
|
|
368
|
+
if (!enabled) {
|
|
369
|
+
return base;
|
|
370
|
+
}
|
|
371
|
+
const decision = decideBatchTraceparent(events);
|
|
372
|
+
if (!decision.inject) {
|
|
373
|
+
return base;
|
|
374
|
+
}
|
|
375
|
+
const injected = {
|
|
376
|
+
traceparent: decision.traceparent
|
|
377
|
+
};
|
|
378
|
+
if (decision.tracestate !== void 0) {
|
|
379
|
+
injected.tracestate = decision.tracestate;
|
|
380
|
+
}
|
|
381
|
+
return { ...injected, ...base };
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// src/transport-otlp/otlp-transport.ts
|
|
385
|
+
var DEFAULTS = {
|
|
386
|
+
maxBatchSize: 20,
|
|
387
|
+
maxBatchAgeMs: 5e3,
|
|
388
|
+
maxBufferedEvents: 1e3,
|
|
389
|
+
maxRecordBytes: 65536,
|
|
390
|
+
name: "otlp"
|
|
391
|
+
};
|
|
392
|
+
function validateOptions(options) {
|
|
393
|
+
if (typeof options !== "object" || options === null) {
|
|
394
|
+
throw new TypeError("otlp transport: options must be a non-null object");
|
|
395
|
+
}
|
|
396
|
+
const { headers, batching, maxBufferedEvents, maxRecordBytes } = options;
|
|
397
|
+
if (options.injectTraceparent !== void 0 && typeof options.injectTraceparent !== "boolean") {
|
|
398
|
+
throw new TypeError("otlp transport: injectTraceparent must be a boolean");
|
|
399
|
+
}
|
|
400
|
+
if (headers !== void 0) {
|
|
401
|
+
if (typeof headers !== "object" || headers === null) {
|
|
402
|
+
throw new TypeError("otlp transport: headers must be an object");
|
|
403
|
+
}
|
|
404
|
+
for (const key of Object.keys(headers)) {
|
|
405
|
+
if (typeof headers[key] !== "string") {
|
|
406
|
+
throw new TypeError(
|
|
407
|
+
`otlp transport: header '${key}' must be a string value`
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
const maxBatchSize = batching?.maxBatchSize ?? DEFAULTS.maxBatchSize;
|
|
413
|
+
if (!Number.isInteger(maxBatchSize) || maxBatchSize < 1) {
|
|
414
|
+
throw new RangeError(
|
|
415
|
+
"otlp transport: batching.maxBatchSize must be an integer >= 1"
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
const cap = maxBufferedEvents ?? DEFAULTS.maxBufferedEvents;
|
|
419
|
+
if (!Number.isInteger(cap) || cap < maxBatchSize) {
|
|
420
|
+
throw new RangeError(
|
|
421
|
+
"otlp transport: maxBufferedEvents must be an integer >= maxBatchSize"
|
|
422
|
+
);
|
|
423
|
+
}
|
|
424
|
+
const recBytes = maxRecordBytes ?? DEFAULTS.maxRecordBytes;
|
|
425
|
+
if (!Number.isInteger(recBytes) || recBytes < 1) {
|
|
426
|
+
throw new RangeError(
|
|
427
|
+
"otlp transport: maxRecordBytes must be an integer >= 1"
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
function createOtlpTransport(options) {
|
|
432
|
+
validateOptions(options);
|
|
433
|
+
const allowInsecureLoopback = options.allowInsecureLoopback ?? false;
|
|
434
|
+
validateEndpoint(options.endpoint, allowInsecureLoopback);
|
|
435
|
+
const name = options.name ?? DEFAULTS.name;
|
|
436
|
+
const maxBatchSize = options.batching?.maxBatchSize ?? DEFAULTS.maxBatchSize;
|
|
437
|
+
const maxBatchAgeMs = options.batching?.maxBatchAgeMs ?? DEFAULTS.maxBatchAgeMs;
|
|
438
|
+
const state = {
|
|
439
|
+
endpoint: options.endpoint,
|
|
440
|
+
// Copy + freeze the headers so later consumer mutation cannot change
|
|
441
|
+
// what we send, and so nothing outside delivery can read them.
|
|
442
|
+
headers: Object.freeze({ ...options.headers ?? {} }),
|
|
443
|
+
name,
|
|
444
|
+
onInternalError: options.onInternalError ?? (() => void 0),
|
|
445
|
+
maxBufferedEvents: options.maxBufferedEvents ?? DEFAULTS.maxBufferedEvents,
|
|
446
|
+
maxRecordBytes: options.maxRecordBytes ?? DEFAULTS.maxRecordBytes,
|
|
447
|
+
injectTraceparent: options.injectTraceparent ?? false,
|
|
448
|
+
notified: freshNotifiedLedger(),
|
|
449
|
+
// Placeholder; real batcher assigned below once the flush closure exists.
|
|
450
|
+
batcher: void 0,
|
|
451
|
+
pagehideInstalled: false,
|
|
452
|
+
pagehideUninstall: null,
|
|
453
|
+
shutdownComplete: false,
|
|
454
|
+
inFlight: /* @__PURE__ */ new Set(),
|
|
455
|
+
pending: 0
|
|
456
|
+
};
|
|
457
|
+
state.batcher = createBatcher({
|
|
458
|
+
maxBatchSize,
|
|
459
|
+
maxBatchAgeMs,
|
|
460
|
+
flush: (events) => {
|
|
461
|
+
void flushBatch(state, events);
|
|
462
|
+
}
|
|
463
|
+
});
|
|
464
|
+
return {
|
|
465
|
+
name,
|
|
466
|
+
send(event) {
|
|
467
|
+
if (state.shutdownComplete) return;
|
|
468
|
+
ensurePagehide(state);
|
|
469
|
+
try {
|
|
470
|
+
const record = toLogRecord(event, Date.now());
|
|
471
|
+
if (byteLength(JSON.stringify(record)) > state.maxRecordBytes) {
|
|
472
|
+
notifyOnce(
|
|
473
|
+
state,
|
|
474
|
+
"oversized_event",
|
|
475
|
+
`dropped an event whose serialized record exceeds ${state.maxRecordBytes} bytes`
|
|
476
|
+
);
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
} catch (cause) {
|
|
480
|
+
notifyOnce(
|
|
481
|
+
state,
|
|
482
|
+
"serialize_failed",
|
|
483
|
+
"dropped an event that failed to serialize",
|
|
484
|
+
cause
|
|
485
|
+
);
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
if (state.pending >= state.maxBufferedEvents) {
|
|
489
|
+
notifyOnce(
|
|
490
|
+
state,
|
|
491
|
+
"buffer_overflow",
|
|
492
|
+
`${state.maxBufferedEvents} events undelivered; dropping event`
|
|
493
|
+
);
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
state.pending += 1;
|
|
497
|
+
state.batcher.push(event);
|
|
498
|
+
},
|
|
499
|
+
async flush() {
|
|
500
|
+
state.batcher.flush();
|
|
501
|
+
await settleInFlight(state);
|
|
502
|
+
},
|
|
503
|
+
async shutdown() {
|
|
504
|
+
if (state.shutdownComplete) {
|
|
505
|
+
await settleInFlight(state);
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
state.shutdownComplete = true;
|
|
509
|
+
try {
|
|
510
|
+
state.batcher.flush();
|
|
511
|
+
await settleInFlight(state);
|
|
512
|
+
} catch (cause) {
|
|
513
|
+
notifyOnce(state, "shutdown_failed", "shutdown flush failed", cause);
|
|
514
|
+
} finally {
|
|
515
|
+
teardownPagehide(state);
|
|
516
|
+
state.batcher.shutdown();
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
async function flushBatch(state, events) {
|
|
522
|
+
const count = events.length;
|
|
523
|
+
let body;
|
|
524
|
+
try {
|
|
525
|
+
body = encode(serializeBatch(events, Date.now()));
|
|
526
|
+
} catch (cause) {
|
|
527
|
+
state.pending = Math.max(0, state.pending - count);
|
|
528
|
+
notifyOnce(
|
|
529
|
+
state,
|
|
530
|
+
"serialize_failed",
|
|
531
|
+
"dropped a batch that failed to serialize",
|
|
532
|
+
cause
|
|
533
|
+
);
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
let headers = state.headers;
|
|
537
|
+
try {
|
|
538
|
+
headers = buildRequestHeaders(
|
|
539
|
+
state.headers,
|
|
540
|
+
events,
|
|
541
|
+
state.injectTraceparent
|
|
542
|
+
);
|
|
543
|
+
} catch {
|
|
544
|
+
headers = state.headers;
|
|
545
|
+
}
|
|
546
|
+
const promise = (async () => {
|
|
547
|
+
const result = await deliver(state.endpoint, headers, body);
|
|
548
|
+
mapResult(state, result);
|
|
549
|
+
})().catch(() => {
|
|
550
|
+
});
|
|
551
|
+
state.inFlight.add(promise);
|
|
552
|
+
void promise.finally(() => {
|
|
553
|
+
state.inFlight.delete(promise);
|
|
554
|
+
state.pending = Math.max(0, state.pending - count);
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
function mapResult(state, result) {
|
|
558
|
+
switch (result.kind) {
|
|
559
|
+
case "delivered":
|
|
560
|
+
return;
|
|
561
|
+
case "unavailable":
|
|
562
|
+
notifyOnce(
|
|
563
|
+
state,
|
|
564
|
+
"delivery_unavailable",
|
|
565
|
+
"fetch is unavailable; dropping batch"
|
|
566
|
+
);
|
|
567
|
+
return;
|
|
568
|
+
case "send_failed":
|
|
569
|
+
notifyOnce(
|
|
570
|
+
state,
|
|
571
|
+
"send_failed",
|
|
572
|
+
`delivery failed (${result.detail})`,
|
|
573
|
+
result.cause
|
|
574
|
+
);
|
|
575
|
+
return;
|
|
576
|
+
case "partial_rejection":
|
|
577
|
+
notifyOnce(
|
|
578
|
+
state,
|
|
579
|
+
"partial_rejection",
|
|
580
|
+
`backend rejected ${result.rejected} record(s)`
|
|
581
|
+
);
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
async function settleInFlight(state) {
|
|
586
|
+
await Promise.all([...state.inFlight]);
|
|
587
|
+
}
|
|
588
|
+
function ensurePagehide(state) {
|
|
589
|
+
if (state.pagehideInstalled) return;
|
|
590
|
+
const target = globalThis;
|
|
591
|
+
state.pagehideInstalled = true;
|
|
592
|
+
if (typeof target.addEventListener !== "function") {
|
|
593
|
+
state.pagehideUninstall = null;
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
const handler = () => {
|
|
597
|
+
state.batcher.flush();
|
|
598
|
+
};
|
|
599
|
+
target.addEventListener("pagehide", handler);
|
|
600
|
+
state.pagehideUninstall = () => {
|
|
601
|
+
if (typeof target.removeEventListener === "function") {
|
|
602
|
+
target.removeEventListener("pagehide", handler);
|
|
603
|
+
}
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
function teardownPagehide(state) {
|
|
607
|
+
if (state.pagehideUninstall !== null) {
|
|
608
|
+
state.pagehideUninstall();
|
|
609
|
+
state.pagehideUninstall = null;
|
|
610
|
+
}
|
|
611
|
+
state.pagehideInstalled = false;
|
|
612
|
+
}
|
|
613
|
+
function byteLength(s) {
|
|
614
|
+
return new TextEncoder().encode(s).length;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
export { createOtlpTransport };
|
|
618
|
+
//# sourceMappingURL=transport-otlp.mjs.map
|
|
619
|
+
//# sourceMappingURL=transport-otlp.mjs.map
|