@tallyrow/safesignal 1.0.1 → 1.2.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 +97 -5
- 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 +541 -0
- package/dist/transport-otlp.cjs.map +1 -0
- package/dist/transport-otlp.d.cts +62 -0
- package/dist/transport-otlp.d.ts +62 -0
- package/dist/transport-otlp.mjs +539 -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 +12 -3
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { j as Transport } from './types-BiRyHi1e.cjs';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `createOtlpTransport` — factory for the `./transport-otlp` subpath.
|
|
5
|
+
*
|
|
6
|
+
* Composes the subpath primitives into a `Transport` that delivers
|
|
7
|
+
* fully-processed `LogEvent`s to an OTLP logs backend as OTLP/HTTP+JSON,
|
|
8
|
+
* batched, fire-and-forget (no retry), fail-closed.
|
|
9
|
+
*
|
|
10
|
+
* Delivery policy (research D6/D7, contracts TO-2..TO-8):
|
|
11
|
+
*
|
|
12
|
+
* send(event)
|
|
13
|
+
* ├── if shutdownComplete: no-op
|
|
14
|
+
* ├── lazily install the pagehide best-effort flush (first send)
|
|
15
|
+
* ├── if serialized record > maxRecordBytes → oversized_event drop
|
|
16
|
+
* ├── if buffered >= maxBufferedEvents → buffer_overflow drop
|
|
17
|
+
* └── batcher.push(event) // flush on size / age
|
|
18
|
+
*
|
|
19
|
+
* flush(batch) // batcher callback
|
|
20
|
+
* ├── serialize(batch) (fail-closed: serialize_failed → drop)
|
|
21
|
+
* └── deliver(endpoint, headers, body) // fetch keepalive, never throws
|
|
22
|
+
* ├── delivered ─────────────────── done
|
|
23
|
+
* ├── unavailable ───────────────── delivery_unavailable notice
|
|
24
|
+
* ├── send_failed ───────────────── send_failed notice (+cause)
|
|
25
|
+
* └── partial_rejection ─────────── partial_rejection notice
|
|
26
|
+
*
|
|
27
|
+
* Every notice is rate-limited to one per failure class per instance per
|
|
28
|
+
* session and NEVER carries a configured header/secret value (FR-009).
|
|
29
|
+
* `send`/`flush`/`shutdown` NEVER throw or reject to the caller; only
|
|
30
|
+
* construction-time validation throws, at the consumer's call site.
|
|
31
|
+
*
|
|
32
|
+
* Boundary discipline (TO-7): the only `src/` import is a type-only import
|
|
33
|
+
* from `'../api/types.js'`. No `@opentelemetry/*` and no
|
|
34
|
+
* `../internal/telemetry/otel/` import — the payload is hand-serialized.
|
|
35
|
+
*
|
|
36
|
+
* Specs: `specs/007-transport-otlp/contracts/*`, `data-model.md`.
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
interface OtlpTransportOptions {
|
|
40
|
+
/** Full OTLP logs endpoint URL (e.g. `https://otlp.example.com/v1/logs`). */
|
|
41
|
+
endpoint: string;
|
|
42
|
+
/** Static request headers (e.g. auth). Sent only on the wire. */
|
|
43
|
+
headers?: Record<string, string>;
|
|
44
|
+
/** Batch flush triggers. */
|
|
45
|
+
batching?: {
|
|
46
|
+
maxBatchSize: number;
|
|
47
|
+
maxBatchAgeMs?: number;
|
|
48
|
+
};
|
|
49
|
+
/** Hard cap on buffered events; over-cap events are dropped. Default 1000. */
|
|
50
|
+
maxBufferedEvents?: number;
|
|
51
|
+
/** Per-record size guard in bytes; larger records are dropped. Default 64 KiB. */
|
|
52
|
+
maxRecordBytes?: number;
|
|
53
|
+
/** Stable diagnostic identifier (`Transport.name`). Default `'otlp'`. */
|
|
54
|
+
name?: string;
|
|
55
|
+
/** Permit `http://` localhost/127.0.0.1/[::1] only. Default false. */
|
|
56
|
+
allowInsecureLoopback?: boolean;
|
|
57
|
+
/** Receives rate-limited diagnostic notices. Never carries header values. */
|
|
58
|
+
onInternalError?: (err: Error) => void;
|
|
59
|
+
}
|
|
60
|
+
declare function createOtlpTransport(options: OtlpTransportOptions): Transport;
|
|
61
|
+
|
|
62
|
+
export { type OtlpTransportOptions, createOtlpTransport };
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { j as Transport } from './types-BiRyHi1e.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `createOtlpTransport` — factory for the `./transport-otlp` subpath.
|
|
5
|
+
*
|
|
6
|
+
* Composes the subpath primitives into a `Transport` that delivers
|
|
7
|
+
* fully-processed `LogEvent`s to an OTLP logs backend as OTLP/HTTP+JSON,
|
|
8
|
+
* batched, fire-and-forget (no retry), fail-closed.
|
|
9
|
+
*
|
|
10
|
+
* Delivery policy (research D6/D7, contracts TO-2..TO-8):
|
|
11
|
+
*
|
|
12
|
+
* send(event)
|
|
13
|
+
* ├── if shutdownComplete: no-op
|
|
14
|
+
* ├── lazily install the pagehide best-effort flush (first send)
|
|
15
|
+
* ├── if serialized record > maxRecordBytes → oversized_event drop
|
|
16
|
+
* ├── if buffered >= maxBufferedEvents → buffer_overflow drop
|
|
17
|
+
* └── batcher.push(event) // flush on size / age
|
|
18
|
+
*
|
|
19
|
+
* flush(batch) // batcher callback
|
|
20
|
+
* ├── serialize(batch) (fail-closed: serialize_failed → drop)
|
|
21
|
+
* └── deliver(endpoint, headers, body) // fetch keepalive, never throws
|
|
22
|
+
* ├── delivered ─────────────────── done
|
|
23
|
+
* ├── unavailable ───────────────── delivery_unavailable notice
|
|
24
|
+
* ├── send_failed ───────────────── send_failed notice (+cause)
|
|
25
|
+
* └── partial_rejection ─────────── partial_rejection notice
|
|
26
|
+
*
|
|
27
|
+
* Every notice is rate-limited to one per failure class per instance per
|
|
28
|
+
* session and NEVER carries a configured header/secret value (FR-009).
|
|
29
|
+
* `send`/`flush`/`shutdown` NEVER throw or reject to the caller; only
|
|
30
|
+
* construction-time validation throws, at the consumer's call site.
|
|
31
|
+
*
|
|
32
|
+
* Boundary discipline (TO-7): the only `src/` import is a type-only import
|
|
33
|
+
* from `'../api/types.js'`. No `@opentelemetry/*` and no
|
|
34
|
+
* `../internal/telemetry/otel/` import — the payload is hand-serialized.
|
|
35
|
+
*
|
|
36
|
+
* Specs: `specs/007-transport-otlp/contracts/*`, `data-model.md`.
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
interface OtlpTransportOptions {
|
|
40
|
+
/** Full OTLP logs endpoint URL (e.g. `https://otlp.example.com/v1/logs`). */
|
|
41
|
+
endpoint: string;
|
|
42
|
+
/** Static request headers (e.g. auth). Sent only on the wire. */
|
|
43
|
+
headers?: Record<string, string>;
|
|
44
|
+
/** Batch flush triggers. */
|
|
45
|
+
batching?: {
|
|
46
|
+
maxBatchSize: number;
|
|
47
|
+
maxBatchAgeMs?: number;
|
|
48
|
+
};
|
|
49
|
+
/** Hard cap on buffered events; over-cap events are dropped. Default 1000. */
|
|
50
|
+
maxBufferedEvents?: number;
|
|
51
|
+
/** Per-record size guard in bytes; larger records are dropped. Default 64 KiB. */
|
|
52
|
+
maxRecordBytes?: number;
|
|
53
|
+
/** Stable diagnostic identifier (`Transport.name`). Default `'otlp'`. */
|
|
54
|
+
name?: string;
|
|
55
|
+
/** Permit `http://` localhost/127.0.0.1/[::1] only. Default false. */
|
|
56
|
+
allowInsecureLoopback?: boolean;
|
|
57
|
+
/** Receives rate-limited diagnostic notices. Never carries header values. */
|
|
58
|
+
onInternalError?: (err: Error) => void;
|
|
59
|
+
}
|
|
60
|
+
declare function createOtlpTransport(options: OtlpTransportOptions): Transport;
|
|
61
|
+
|
|
62
|
+
export { type OtlpTransportOptions, createOtlpTransport };
|
|
@@ -0,0 +1,539 @@
|
|
|
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/otlp-transport.ts
|
|
315
|
+
var DEFAULTS = {
|
|
316
|
+
maxBatchSize: 20,
|
|
317
|
+
maxBatchAgeMs: 5e3,
|
|
318
|
+
maxBufferedEvents: 1e3,
|
|
319
|
+
maxRecordBytes: 65536,
|
|
320
|
+
name: "otlp"
|
|
321
|
+
};
|
|
322
|
+
function validateOptions(options) {
|
|
323
|
+
if (typeof options !== "object" || options === null) {
|
|
324
|
+
throw new TypeError("otlp transport: options must be a non-null object");
|
|
325
|
+
}
|
|
326
|
+
const { headers, batching, maxBufferedEvents, maxRecordBytes } = options;
|
|
327
|
+
if (headers !== void 0) {
|
|
328
|
+
if (typeof headers !== "object" || headers === null) {
|
|
329
|
+
throw new TypeError("otlp transport: headers must be an object");
|
|
330
|
+
}
|
|
331
|
+
for (const key of Object.keys(headers)) {
|
|
332
|
+
if (typeof headers[key] !== "string") {
|
|
333
|
+
throw new TypeError(
|
|
334
|
+
`otlp transport: header '${key}' must be a string value`
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
const maxBatchSize = batching?.maxBatchSize ?? DEFAULTS.maxBatchSize;
|
|
340
|
+
if (!Number.isInteger(maxBatchSize) || maxBatchSize < 1) {
|
|
341
|
+
throw new RangeError(
|
|
342
|
+
"otlp transport: batching.maxBatchSize must be an integer >= 1"
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
const cap = maxBufferedEvents ?? DEFAULTS.maxBufferedEvents;
|
|
346
|
+
if (!Number.isInteger(cap) || cap < maxBatchSize) {
|
|
347
|
+
throw new RangeError(
|
|
348
|
+
"otlp transport: maxBufferedEvents must be an integer >= maxBatchSize"
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
const recBytes = maxRecordBytes ?? DEFAULTS.maxRecordBytes;
|
|
352
|
+
if (!Number.isInteger(recBytes) || recBytes < 1) {
|
|
353
|
+
throw new RangeError(
|
|
354
|
+
"otlp transport: maxRecordBytes must be an integer >= 1"
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
function createOtlpTransport(options) {
|
|
359
|
+
validateOptions(options);
|
|
360
|
+
const allowInsecureLoopback = options.allowInsecureLoopback ?? false;
|
|
361
|
+
validateEndpoint(options.endpoint, allowInsecureLoopback);
|
|
362
|
+
const name = options.name ?? DEFAULTS.name;
|
|
363
|
+
const maxBatchSize = options.batching?.maxBatchSize ?? DEFAULTS.maxBatchSize;
|
|
364
|
+
const maxBatchAgeMs = options.batching?.maxBatchAgeMs ?? DEFAULTS.maxBatchAgeMs;
|
|
365
|
+
const state = {
|
|
366
|
+
endpoint: options.endpoint,
|
|
367
|
+
// Copy + freeze the headers so later consumer mutation cannot change
|
|
368
|
+
// what we send, and so nothing outside delivery can read them.
|
|
369
|
+
headers: Object.freeze({ ...options.headers ?? {} }),
|
|
370
|
+
name,
|
|
371
|
+
onInternalError: options.onInternalError ?? (() => void 0),
|
|
372
|
+
maxBufferedEvents: options.maxBufferedEvents ?? DEFAULTS.maxBufferedEvents,
|
|
373
|
+
maxRecordBytes: options.maxRecordBytes ?? DEFAULTS.maxRecordBytes,
|
|
374
|
+
notified: freshNotifiedLedger(),
|
|
375
|
+
// Placeholder; real batcher assigned below once the flush closure exists.
|
|
376
|
+
batcher: void 0,
|
|
377
|
+
pagehideInstalled: false,
|
|
378
|
+
pagehideUninstall: null,
|
|
379
|
+
shutdownComplete: false,
|
|
380
|
+
inFlight: /* @__PURE__ */ new Set(),
|
|
381
|
+
pending: 0
|
|
382
|
+
};
|
|
383
|
+
state.batcher = createBatcher({
|
|
384
|
+
maxBatchSize,
|
|
385
|
+
maxBatchAgeMs,
|
|
386
|
+
flush: (events) => {
|
|
387
|
+
void flushBatch(state, events);
|
|
388
|
+
}
|
|
389
|
+
});
|
|
390
|
+
return {
|
|
391
|
+
name,
|
|
392
|
+
send(event) {
|
|
393
|
+
if (state.shutdownComplete) return;
|
|
394
|
+
ensurePagehide(state);
|
|
395
|
+
try {
|
|
396
|
+
const record = toLogRecord(event, Date.now());
|
|
397
|
+
if (byteLength(JSON.stringify(record)) > state.maxRecordBytes) {
|
|
398
|
+
notifyOnce(
|
|
399
|
+
state,
|
|
400
|
+
"oversized_event",
|
|
401
|
+
`dropped an event whose serialized record exceeds ${state.maxRecordBytes} bytes`
|
|
402
|
+
);
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
} catch (cause) {
|
|
406
|
+
notifyOnce(
|
|
407
|
+
state,
|
|
408
|
+
"serialize_failed",
|
|
409
|
+
"dropped an event that failed to serialize",
|
|
410
|
+
cause
|
|
411
|
+
);
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
if (state.pending >= state.maxBufferedEvents) {
|
|
415
|
+
notifyOnce(
|
|
416
|
+
state,
|
|
417
|
+
"buffer_overflow",
|
|
418
|
+
`${state.maxBufferedEvents} events undelivered; dropping event`
|
|
419
|
+
);
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
state.pending += 1;
|
|
423
|
+
state.batcher.push(event);
|
|
424
|
+
},
|
|
425
|
+
async flush() {
|
|
426
|
+
state.batcher.flush();
|
|
427
|
+
await settleInFlight(state);
|
|
428
|
+
},
|
|
429
|
+
async shutdown() {
|
|
430
|
+
if (state.shutdownComplete) {
|
|
431
|
+
await settleInFlight(state);
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
state.shutdownComplete = true;
|
|
435
|
+
try {
|
|
436
|
+
state.batcher.flush();
|
|
437
|
+
await settleInFlight(state);
|
|
438
|
+
} catch (cause) {
|
|
439
|
+
notifyOnce(state, "shutdown_failed", "shutdown flush failed", cause);
|
|
440
|
+
} finally {
|
|
441
|
+
teardownPagehide(state);
|
|
442
|
+
state.batcher.shutdown();
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
async function flushBatch(state, events) {
|
|
448
|
+
const count = events.length;
|
|
449
|
+
let body;
|
|
450
|
+
try {
|
|
451
|
+
body = encode(serializeBatch(events, Date.now()));
|
|
452
|
+
} catch (cause) {
|
|
453
|
+
state.pending = Math.max(0, state.pending - count);
|
|
454
|
+
notifyOnce(
|
|
455
|
+
state,
|
|
456
|
+
"serialize_failed",
|
|
457
|
+
"dropped a batch that failed to serialize",
|
|
458
|
+
cause
|
|
459
|
+
);
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
const promise = (async () => {
|
|
463
|
+
const result = await deliver(
|
|
464
|
+
state.endpoint,
|
|
465
|
+
state.headers,
|
|
466
|
+
body
|
|
467
|
+
);
|
|
468
|
+
mapResult(state, result);
|
|
469
|
+
})().catch(() => {
|
|
470
|
+
});
|
|
471
|
+
state.inFlight.add(promise);
|
|
472
|
+
void promise.finally(() => {
|
|
473
|
+
state.inFlight.delete(promise);
|
|
474
|
+
state.pending = Math.max(0, state.pending - count);
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
function mapResult(state, result) {
|
|
478
|
+
switch (result.kind) {
|
|
479
|
+
case "delivered":
|
|
480
|
+
return;
|
|
481
|
+
case "unavailable":
|
|
482
|
+
notifyOnce(
|
|
483
|
+
state,
|
|
484
|
+
"delivery_unavailable",
|
|
485
|
+
"fetch is unavailable; dropping batch"
|
|
486
|
+
);
|
|
487
|
+
return;
|
|
488
|
+
case "send_failed":
|
|
489
|
+
notifyOnce(
|
|
490
|
+
state,
|
|
491
|
+
"send_failed",
|
|
492
|
+
`delivery failed (${result.detail})`,
|
|
493
|
+
result.cause
|
|
494
|
+
);
|
|
495
|
+
return;
|
|
496
|
+
case "partial_rejection":
|
|
497
|
+
notifyOnce(
|
|
498
|
+
state,
|
|
499
|
+
"partial_rejection",
|
|
500
|
+
`backend rejected ${result.rejected} record(s)`
|
|
501
|
+
);
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
async function settleInFlight(state) {
|
|
506
|
+
await Promise.all([...state.inFlight]);
|
|
507
|
+
}
|
|
508
|
+
function ensurePagehide(state) {
|
|
509
|
+
if (state.pagehideInstalled) return;
|
|
510
|
+
const target = globalThis;
|
|
511
|
+
state.pagehideInstalled = true;
|
|
512
|
+
if (typeof target.addEventListener !== "function") {
|
|
513
|
+
state.pagehideUninstall = null;
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
const handler = () => {
|
|
517
|
+
state.batcher.flush();
|
|
518
|
+
};
|
|
519
|
+
target.addEventListener("pagehide", handler);
|
|
520
|
+
state.pagehideUninstall = () => {
|
|
521
|
+
if (typeof target.removeEventListener === "function") {
|
|
522
|
+
target.removeEventListener("pagehide", handler);
|
|
523
|
+
}
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
function teardownPagehide(state) {
|
|
527
|
+
if (state.pagehideUninstall !== null) {
|
|
528
|
+
state.pagehideUninstall();
|
|
529
|
+
state.pagehideUninstall = null;
|
|
530
|
+
}
|
|
531
|
+
state.pagehideInstalled = false;
|
|
532
|
+
}
|
|
533
|
+
function byteLength(s) {
|
|
534
|
+
return new TextEncoder().encode(s).length;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
export { createOtlpTransport };
|
|
538
|
+
//# sourceMappingURL=transport-otlp.mjs.map
|
|
539
|
+
//# sourceMappingURL=transport-otlp.mjs.map
|