@blamejs/core 0.7.101 → 0.7.103
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/CHANGELOG.md +4 -0
- package/lib/guard-mime.js +3 -2
- package/lib/middleware/headers.js +3 -2
- package/lib/middleware/index.js +9 -0
- package/lib/middleware/span-http-server.js +243 -0
- package/lib/middleware/trace-log-correlation.js +134 -0
- package/lib/middleware/trace-propagate.js +116 -0
- package/lib/observability-otlp-exporter.js +351 -0
- package/lib/observability-tracer.js +395 -0
- package/lib/observability.js +230 -6
- package/lib/safe-buffer.js +15 -0
- package/package.json +1 -1
- package/sbom.cyclonedx.json +6 -6
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* b.observability.otlpExporter — OTLP/HTTP JSON span exporter.
|
|
4
|
+
*
|
|
5
|
+
* Buffers spans produced by b.observability.tracer and ships them to
|
|
6
|
+
* an OTLP-compatible HTTP collector. Implements the OpenTelemetry
|
|
7
|
+
* Protocol (OTLP) §3 trace export wire shape over HTTP/JSON
|
|
8
|
+
* (https://opentelemetry.io/docs/specs/otlp/#otlphttp).
|
|
9
|
+
*
|
|
10
|
+
* var tracer = b.observability.tracer.create({ service: "api" });
|
|
11
|
+
* var exporter = b.observability.otlpExporter.create({
|
|
12
|
+
* endpoint: "https://collector.example.com/v1/traces",
|
|
13
|
+
* headers: { "Authorization": "Bearer ..." },
|
|
14
|
+
* batchSize: 200,
|
|
15
|
+
* flushIntervalMs: C.TIME.seconds(5),
|
|
16
|
+
* maxQueueSize: 4096,
|
|
17
|
+
* });
|
|
18
|
+
* tracer.start("...", { onEnd: exporter.queue });
|
|
19
|
+
* ...
|
|
20
|
+
* await exporter.shutdown();
|
|
21
|
+
*
|
|
22
|
+
* Failure modes:
|
|
23
|
+
* - HTTP 5xx / network failure → exponential-backoff retry, up to
|
|
24
|
+
* maxAttempts (default 3); spans dropped after that
|
|
25
|
+
* - Queue overflow → drops oldest unexported spans, increments the
|
|
26
|
+
* drop counter
|
|
27
|
+
* - Endpoint unreachable at boot → exporter is still constructed;
|
|
28
|
+
* queue retries on every flush tick
|
|
29
|
+
*
|
|
30
|
+
* Wire shape: OTLP/JSON resourceSpans envelope per §3.4.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
var C = require("./constants");
|
|
34
|
+
var lazyRequire = require("./lazy-require");
|
|
35
|
+
var safeAsync = require("./safe-async");
|
|
36
|
+
var safeBuffer = require("./safe-buffer");
|
|
37
|
+
var validateOpts = require("./validate-opts");
|
|
38
|
+
var safeUrl = require("./safe-url");
|
|
39
|
+
var { defineClass } = require("./framework-error");
|
|
40
|
+
|
|
41
|
+
var OtlpExporterError = defineClass("OtlpExporterError", { alwaysPermanent: true });
|
|
42
|
+
|
|
43
|
+
var observability = lazyRequire(function () { return require("./observability"); });
|
|
44
|
+
|
|
45
|
+
var DEFAULT_BATCH_SIZE = 200; // allow:raw-byte-literal — OTLP recommended batch
|
|
46
|
+
var DEFAULT_MAX_QUEUE_SIZE = 4096; // allow:raw-byte-literal — operator-side queue cap
|
|
47
|
+
var DEFAULT_FLUSH_INTERVAL_MS = C.TIME.seconds(5);
|
|
48
|
+
var DEFAULT_MAX_ATTEMPTS = 3; // allow:raw-byte-literal — retry attempt count
|
|
49
|
+
var DEFAULT_BACKOFF_INITIAL_MS = C.TIME.seconds(1);
|
|
50
|
+
var DEFAULT_BACKOFF_MAX_MS = C.TIME.seconds(30);
|
|
51
|
+
var DEFAULT_TIMEOUT_MS = C.TIME.seconds(30);
|
|
52
|
+
|
|
53
|
+
// OTLP severity numbers per §3.5 (logs); not used for traces but
|
|
54
|
+
// retained as a reference for future log-export support.
|
|
55
|
+
var STATUS_CODE_TO_OTLP = Object.freeze({
|
|
56
|
+
unset: 0, // allow:raw-byte-literal — OTLP STATUS_CODE_UNSET enum
|
|
57
|
+
ok: 1, // allow:raw-byte-literal — OTLP STATUS_CODE_OK enum
|
|
58
|
+
error: 2, // allow:raw-byte-literal — OTLP STATUS_CODE_ERROR enum
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
var KIND_TO_OTLP = Object.freeze({
|
|
62
|
+
internal: 1, // allow:raw-byte-literal — OTLP SPAN_KIND_INTERNAL
|
|
63
|
+
server: 2, // allow:raw-byte-literal — OTLP SPAN_KIND_SERVER
|
|
64
|
+
client: 3, // allow:raw-byte-literal — OTLP SPAN_KIND_CLIENT
|
|
65
|
+
producer: 4, // allow:raw-byte-literal — OTLP SPAN_KIND_PRODUCER
|
|
66
|
+
consumer: 5, // allow:raw-byte-literal — OTLP SPAN_KIND_CONSUMER
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
function _attrToOtlp(attrs) {
|
|
70
|
+
// OTLP attribute shape: [{ key, value: { stringValue | intValue |
|
|
71
|
+
// doubleValue | boolValue | arrayValue: { values: [...] } } }, ...]
|
|
72
|
+
var out = [];
|
|
73
|
+
if (!attrs || typeof attrs !== "object") return out;
|
|
74
|
+
var keys = Object.keys(attrs);
|
|
75
|
+
for (var i = 0; i < keys.length; i++) {
|
|
76
|
+
var k = keys[i];
|
|
77
|
+
var v = attrs[k];
|
|
78
|
+
out.push({ key: k, value: _valueToOtlp(v) });
|
|
79
|
+
}
|
|
80
|
+
return out;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function _valueToOtlp(v) {
|
|
84
|
+
var t = typeof v;
|
|
85
|
+
if (t === "string") return { stringValue: v };
|
|
86
|
+
if (t === "boolean") return { boolValue: v };
|
|
87
|
+
if (t === "number") {
|
|
88
|
+
if (Number.isInteger(v)) return { intValue: String(v) };
|
|
89
|
+
return { doubleValue: v };
|
|
90
|
+
}
|
|
91
|
+
if (Array.isArray(v)) {
|
|
92
|
+
return {
|
|
93
|
+
arrayValue: {
|
|
94
|
+
values: v.map(function (el) { return _valueToOtlp(el); }),
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
return { stringValue: String(v) };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function _spanToOtlp(span) {
|
|
102
|
+
return {
|
|
103
|
+
traceId: span.traceId,
|
|
104
|
+
spanId: span.spanId,
|
|
105
|
+
parentSpanId: span.parentSpanId || "",
|
|
106
|
+
name: span.name,
|
|
107
|
+
kind: KIND_TO_OTLP[span.kind] || KIND_TO_OTLP.internal,
|
|
108
|
+
startTimeUnixNano: span.startTimeUnixNano,
|
|
109
|
+
endTimeUnixNano: span.endTimeUnixNano || span.startTimeUnixNano,
|
|
110
|
+
attributes: _attrToOtlp(span.attributes),
|
|
111
|
+
droppedAttributesCount: span.droppedAttributesCount || 0,
|
|
112
|
+
events: (span.events || []).map(function (e) {
|
|
113
|
+
return {
|
|
114
|
+
name: e.name,
|
|
115
|
+
timeUnixNano: e.timeUnixNano,
|
|
116
|
+
attributes: _attrToOtlp(e.attributes),
|
|
117
|
+
droppedAttributesCount: 0,
|
|
118
|
+
};
|
|
119
|
+
}),
|
|
120
|
+
droppedEventsCount: span.droppedEventsCount || 0,
|
|
121
|
+
status: {
|
|
122
|
+
code: STATUS_CODE_TO_OTLP[span.status && span.status.code] || 0,
|
|
123
|
+
message: (span.status && span.status.message) || "",
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function _bundleSpans(spans) {
|
|
129
|
+
// Group spans by resource → OTLP resourceSpans envelope. Spans that
|
|
130
|
+
// share the same resource attributes get bundled together.
|
|
131
|
+
if (spans.length === 0) return { resourceSpans: [] };
|
|
132
|
+
var byResource = new Map();
|
|
133
|
+
for (var i = 0; i < spans.length; i++) {
|
|
134
|
+
var s = spans[i];
|
|
135
|
+
var resKey = JSON.stringify(s.resource || {});
|
|
136
|
+
var bucket = byResource.get(resKey);
|
|
137
|
+
if (!bucket) {
|
|
138
|
+
bucket = {
|
|
139
|
+
resource: s.resource || {},
|
|
140
|
+
scope: s.scope || { name: "blamejs", version: null },
|
|
141
|
+
spans: [],
|
|
142
|
+
};
|
|
143
|
+
byResource.set(resKey, bucket);
|
|
144
|
+
}
|
|
145
|
+
bucket.spans.push(s);
|
|
146
|
+
}
|
|
147
|
+
var resourceSpans = [];
|
|
148
|
+
for (var entry of byResource) {
|
|
149
|
+
var b = entry[1];
|
|
150
|
+
resourceSpans.push({
|
|
151
|
+
resource: { attributes: _attrToOtlp(b.resource) },
|
|
152
|
+
scopeSpans: [{
|
|
153
|
+
scope: {
|
|
154
|
+
name: b.scope.name,
|
|
155
|
+
version: b.scope.version || "",
|
|
156
|
+
},
|
|
157
|
+
spans: b.spans.map(_spanToOtlp),
|
|
158
|
+
}],
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
return { resourceSpans: resourceSpans };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function create(opts) {
|
|
165
|
+
validateOpts.requireObject(opts, "otlpExporter", OtlpExporterError);
|
|
166
|
+
validateOpts(opts, [
|
|
167
|
+
"endpoint", "headers", "batchSize", "maxQueueSize",
|
|
168
|
+
"flushIntervalMs", "timeoutMs", "maxAttempts",
|
|
169
|
+
"backoffInitialMs", "backoffMaxMs",
|
|
170
|
+
"fetchImpl", "audit", "allowedProtocols",
|
|
171
|
+
], "otlpExporter.create");
|
|
172
|
+
validateOpts.requireNonEmptyString(opts.endpoint,
|
|
173
|
+
"otlpExporter.create: endpoint", OtlpExporterError, "otlp/bad-endpoint");
|
|
174
|
+
// Validate that endpoint is an http(s) URL via the framework's safe-url.
|
|
175
|
+
// Operators using cleartext (e.g. localhost dev collector) opt in to
|
|
176
|
+
// ALLOW_HTTP_ALL; production deployments leave the default which
|
|
177
|
+
// requires HTTPS for outbound telemetry.
|
|
178
|
+
var allowedProtocols = opts.allowedProtocols || safeUrl.ALLOW_HTTPS_ONLY;
|
|
179
|
+
try { safeUrl.parse(opts.endpoint, { allowedProtocols: allowedProtocols }); }
|
|
180
|
+
catch (e) {
|
|
181
|
+
throw new OtlpExporterError("otlp/bad-endpoint",
|
|
182
|
+
"otlpExporter.create: endpoint must be a valid URL: " + e.message);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
validateOpts.optionalPositiveFinite(opts.batchSize,
|
|
186
|
+
"otlpExporter.create: batchSize", OtlpExporterError, "otlp/bad-opts");
|
|
187
|
+
validateOpts.optionalPositiveFinite(opts.maxQueueSize,
|
|
188
|
+
"otlpExporter.create: maxQueueSize", OtlpExporterError, "otlp/bad-opts");
|
|
189
|
+
if (opts.flushIntervalMs !== undefined && opts.flushIntervalMs !== 0) {
|
|
190
|
+
validateOpts.optionalPositiveFinite(opts.flushIntervalMs,
|
|
191
|
+
"otlpExporter.create: flushIntervalMs", OtlpExporterError, "otlp/bad-opts");
|
|
192
|
+
}
|
|
193
|
+
validateOpts.optionalPositiveFinite(opts.timeoutMs,
|
|
194
|
+
"otlpExporter.create: timeoutMs", OtlpExporterError, "otlp/bad-opts");
|
|
195
|
+
validateOpts.optionalPositiveFinite(opts.maxAttempts,
|
|
196
|
+
"otlpExporter.create: maxAttempts", OtlpExporterError, "otlp/bad-opts");
|
|
197
|
+
|
|
198
|
+
var endpoint = opts.endpoint;
|
|
199
|
+
var headers = Object.assign({
|
|
200
|
+
"Content-Type": "application/json",
|
|
201
|
+
}, opts.headers || {});
|
|
202
|
+
var batchSize = opts.batchSize || DEFAULT_BATCH_SIZE;
|
|
203
|
+
var maxQueue = opts.maxQueueSize || DEFAULT_MAX_QUEUE_SIZE;
|
|
204
|
+
var flushIntervalMs = opts.flushIntervalMs || DEFAULT_FLUSH_INTERVAL_MS;
|
|
205
|
+
var timeoutMs = opts.timeoutMs || DEFAULT_TIMEOUT_MS;
|
|
206
|
+
var maxAttempts = opts.maxAttempts || DEFAULT_MAX_ATTEMPTS;
|
|
207
|
+
var backoffInitial = opts.backoffInitialMs || DEFAULT_BACKOFF_INITIAL_MS;
|
|
208
|
+
var backoffMax = opts.backoffMaxMs || DEFAULT_BACKOFF_MAX_MS;
|
|
209
|
+
var fetchImpl = opts.fetchImpl || ((typeof globalThis.fetch === "function") ? globalThis.fetch.bind(globalThis) : null);
|
|
210
|
+
if (typeof fetchImpl !== "function") {
|
|
211
|
+
throw new OtlpExporterError("otlp/no-fetch",
|
|
212
|
+
"otlpExporter.create: fetchImpl required (globalThis.fetch unavailable)");
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
var queue = [];
|
|
216
|
+
var droppedQueueOverflow = 0;
|
|
217
|
+
var droppedExportFailed = 0;
|
|
218
|
+
var inFlight = false;
|
|
219
|
+
var stopping = false;
|
|
220
|
+
|
|
221
|
+
function _emitMetric(verb, n, labels) {
|
|
222
|
+
try { observability().safeEvent("otlp.exporter." + verb, n || 1, labels || {}); }
|
|
223
|
+
catch (_e) { /* drop-silent */ }
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function queue_(span) {
|
|
227
|
+
if (stopping) { droppedExportFailed += 1; return; }
|
|
228
|
+
if (!span || typeof span !== "object") return;
|
|
229
|
+
if (queue.length >= maxQueue) {
|
|
230
|
+
// Drop oldest, append newest — keeps the most-recent telemetry.
|
|
231
|
+
queue.shift();
|
|
232
|
+
droppedQueueOverflow += 1;
|
|
233
|
+
_emitMetric("queue_overflow", 1, {});
|
|
234
|
+
}
|
|
235
|
+
queue.push(span);
|
|
236
|
+
if (queue.length >= batchSize) {
|
|
237
|
+
// Best-effort flush; don't block the caller.
|
|
238
|
+
flush().catch(function () { /* drop-silent */ });
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function _backoffMs(attempt) {
|
|
243
|
+
var ms = backoffInitial * Math.pow(2, Math.max(0, attempt - 1)); // allow:raw-byte-literal — exponential factor
|
|
244
|
+
return Math.min(ms, backoffMax);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function _sleep(ms) {
|
|
248
|
+
return safeAsync.sleep(ms);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
async function _post(payload, attempt) {
|
|
252
|
+
attempt = attempt || 1;
|
|
253
|
+
var ac = (typeof AbortController === "function") ? new AbortController() : null;
|
|
254
|
+
var t = ac ? setTimeout(function () { ac.abort(); }, timeoutMs) : null;
|
|
255
|
+
try {
|
|
256
|
+
var res = await fetchImpl(endpoint, {
|
|
257
|
+
method: "POST",
|
|
258
|
+
headers: headers,
|
|
259
|
+
body: JSON.stringify(payload),
|
|
260
|
+
signal: ac ? ac.signal : undefined,
|
|
261
|
+
});
|
|
262
|
+
if (res && res.ok) return { ok: true, status: res.status };
|
|
263
|
+
var status = res && res.status;
|
|
264
|
+
// 5xx + 408/429 → retryable; everything else permanent
|
|
265
|
+
var retryable = (status >= 500 && status < 600) || status === 408 || status === 429; // allow:raw-byte-literal — HTTP status ranges
|
|
266
|
+
if (retryable && attempt < maxAttempts) {
|
|
267
|
+
await _sleep(_backoffMs(attempt));
|
|
268
|
+
return await _post(payload, attempt + 1);
|
|
269
|
+
}
|
|
270
|
+
return { ok: false, status: status, retryable: retryable };
|
|
271
|
+
} catch (e) {
|
|
272
|
+
// Network error / abort
|
|
273
|
+
if (attempt < maxAttempts) {
|
|
274
|
+
await _sleep(_backoffMs(attempt));
|
|
275
|
+
return await _post(payload, attempt + 1);
|
|
276
|
+
}
|
|
277
|
+
return { ok: false, error: (e && e.message) || String(e), retryable: true };
|
|
278
|
+
} finally {
|
|
279
|
+
if (t) clearTimeout(t);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
async function flush() {
|
|
284
|
+
if (inFlight) return { sent: 0, skipped: true };
|
|
285
|
+
if (queue.length === 0) return { sent: 0 };
|
|
286
|
+
inFlight = true;
|
|
287
|
+
try {
|
|
288
|
+
var batch = queue.splice(0, batchSize);
|
|
289
|
+
var payload = _bundleSpans(batch);
|
|
290
|
+
var result = await _post(payload, 1);
|
|
291
|
+
if (result.ok) {
|
|
292
|
+
_emitMetric("export_ok", batch.length, { http_status: String(result.status) });
|
|
293
|
+
return { sent: batch.length };
|
|
294
|
+
}
|
|
295
|
+
droppedExportFailed += batch.length;
|
|
296
|
+
_emitMetric("export_failed", batch.length, {
|
|
297
|
+
http_status: String(result.status || "network"),
|
|
298
|
+
});
|
|
299
|
+
return { sent: 0, dropped: batch.length };
|
|
300
|
+
} finally {
|
|
301
|
+
inFlight = false;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Periodic flush worker
|
|
306
|
+
var ticker = null;
|
|
307
|
+
if (flushIntervalMs > 0) {
|
|
308
|
+
ticker = safeAsync.repeating(function () {
|
|
309
|
+
flush().catch(function () { /* drop-silent */ });
|
|
310
|
+
}, flushIntervalMs, { name: "otlp-exporter-flush" });
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
async function shutdown() {
|
|
314
|
+
stopping = true;
|
|
315
|
+
if (ticker) { ticker.stop(); ticker = null; }
|
|
316
|
+
// Drain remaining spans, best-effort
|
|
317
|
+
while (queue.length > 0) {
|
|
318
|
+
var r = await flush();
|
|
319
|
+
if (!r || r.sent === 0) break;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function stats() {
|
|
324
|
+
return {
|
|
325
|
+
queueLength: queue.length,
|
|
326
|
+
droppedQueueOverflow: droppedQueueOverflow,
|
|
327
|
+
droppedExportFailed: droppedExportFailed,
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return {
|
|
332
|
+
queue: queue_,
|
|
333
|
+
flush: flush,
|
|
334
|
+
shutdown: shutdown,
|
|
335
|
+
stats: stats,
|
|
336
|
+
// Internal hook for tests
|
|
337
|
+
_bundleForTest: _bundleSpans,
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
module.exports = {
|
|
342
|
+
create: create,
|
|
343
|
+
STATUS_CODE_TO_OTLP: STATUS_CODE_TO_OTLP,
|
|
344
|
+
KIND_TO_OTLP: KIND_TO_OTLP,
|
|
345
|
+
OtlpExporterError: OtlpExporterError,
|
|
346
|
+
// Exported for tests
|
|
347
|
+
_spanToOtlp: _spanToOtlp,
|
|
348
|
+
_bundleSpans: _bundleSpans,
|
|
349
|
+
_attrToOtlp: _attrToOtlp,
|
|
350
|
+
_BASE64URL_RE_REF: safeBuffer.BASE64URL_RE, // not used; reserved for OTLP/protobuf shape upgrade
|
|
351
|
+
};
|