@blamejs/core 0.7.102 → 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 +2 -0
- package/lib/guard-mime.js +3 -2
- package/lib/middleware/headers.js +3 -2
- package/lib/middleware/index.js +8 -2
- package/lib/middleware/span-http-server.js +243 -0
- package/lib/middleware/trace-log-correlation.js +134 -0
- package/lib/middleware/trace-propagate.js +9 -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,395 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* b.observability.tracer — OTel-shaped distributed-tracing span builder.
|
|
4
|
+
*
|
|
5
|
+
* Builds OpenTelemetry-compatible spans without a vendored OTel SDK.
|
|
6
|
+
* The span objects are JSON-serializable and have the OTLP/JSON wire
|
|
7
|
+
* shape (Trace v1) so the operator can ship them to any OTLP-aware
|
|
8
|
+
* collector (Jaeger, Tempo, Honeycomb, Lightstep, Datadog, etc.) via
|
|
9
|
+
* b.observability.spanExporter or their own bridge.
|
|
10
|
+
*
|
|
11
|
+
* var tracer = b.observability.tracer.create({
|
|
12
|
+
* service: "checkout-api",
|
|
13
|
+
* resource: { "service.version": "0.42.0",
|
|
14
|
+
* "deployment.environment": "prod" },
|
|
15
|
+
* onEnd: function (span) { exporter.queue(span); },
|
|
16
|
+
* });
|
|
17
|
+
*
|
|
18
|
+
* var span = tracer.start("checkout.process", {
|
|
19
|
+
* traceId: req.trace.traceId, // optional — derive from context
|
|
20
|
+
* parentId: req.trace.parentId,
|
|
21
|
+
* sampled: req.trace.sampled,
|
|
22
|
+
* attributes: {
|
|
23
|
+
* [SEMCONV.HTTP_REQUEST_METHOD]: "POST",
|
|
24
|
+
* [SEMCONV.URL_PATH]: "/checkout",
|
|
25
|
+
* },
|
|
26
|
+
* kind: "server",
|
|
27
|
+
* });
|
|
28
|
+
*
|
|
29
|
+
* try {
|
|
30
|
+
* // ... do the work
|
|
31
|
+
* span.setAttribute(SEMCONV.HTTP_RESPONSE_STATUS_CODE, 200);
|
|
32
|
+
* span.addEvent("payment.charged", { amount: 4200 });
|
|
33
|
+
* span.setStatus("ok");
|
|
34
|
+
* } catch (e) {
|
|
35
|
+
* span.recordException(e);
|
|
36
|
+
* span.setStatus("error", e.message);
|
|
37
|
+
* throw e;
|
|
38
|
+
* } finally {
|
|
39
|
+
* span.end(); // captures duration_ms, fires tracer.onEnd(span)
|
|
40
|
+
* }
|
|
41
|
+
*
|
|
42
|
+
* Span lifecycle:
|
|
43
|
+
* - start() — captures startTime; assigns spanId; emits
|
|
44
|
+
* span.start observability counter
|
|
45
|
+
* - setAttribute — additive; rejects non-stable attribute names
|
|
46
|
+
* when strict: true
|
|
47
|
+
* - addEvent — appends { name, time, attributes }
|
|
48
|
+
* - recordException — addEvent with `exception.*` attributes
|
|
49
|
+
* - setStatus — "unset" | "ok" | "error" with optional message
|
|
50
|
+
* - end() — captures endTime; emits span.end observability
|
|
51
|
+
* counter; calls tracer.onEnd(span) hook
|
|
52
|
+
*
|
|
53
|
+
* Span shape (OTLP/JSON-compatible):
|
|
54
|
+
* {
|
|
55
|
+
* traceId, spanId, parentSpanId, name, kind,
|
|
56
|
+
* startTimeUnixNano, endTimeUnixNano, durationNs, durationMs,
|
|
57
|
+
* attributes: { ... },
|
|
58
|
+
* events: [ { name, timeUnixNano, attributes } ],
|
|
59
|
+
* status: { code: "unset" | "ok" | "error", message? },
|
|
60
|
+
* resource: { ... },
|
|
61
|
+
* scope: { name: "blamejs", version },
|
|
62
|
+
* droppedAttributesCount, droppedEventsCount,
|
|
63
|
+
* }
|
|
64
|
+
*
|
|
65
|
+
* Attribute / event caps:
|
|
66
|
+
* - maxAttributes per span: 128 (OTLP default)
|
|
67
|
+
* - maxEvents per span: 128
|
|
68
|
+
* - maxAttributeValueLength: 1024 chars (truncated past)
|
|
69
|
+
*
|
|
70
|
+
* Excess additions silently increment droppedAttributesCount /
|
|
71
|
+
* droppedEventsCount per OTLP convention; the span itself never
|
|
72
|
+
* throws on cap overflow (hot-path observability is drop-silent).
|
|
73
|
+
*/
|
|
74
|
+
|
|
75
|
+
var bCrypto = require("./crypto");
|
|
76
|
+
var constants = require("./constants");
|
|
77
|
+
var lazyRequire = require("./lazy-require");
|
|
78
|
+
var safeBuffer = require("./safe-buffer");
|
|
79
|
+
var validateOpts = require("./validate-opts");
|
|
80
|
+
var { defineClass } = require("./framework-error");
|
|
81
|
+
|
|
82
|
+
var TRACE_ID_BYTES = constants.BYTES.bytes(16); // W3C §3.2.2.3 — 128-bit trace-id
|
|
83
|
+
var SPAN_ID_BYTES = constants.BYTES.bytes(8); // W3C §3.2.2.4 — 64-bit span-id
|
|
84
|
+
|
|
85
|
+
var TracerError = defineClass("TracerError", { alwaysPermanent: true });
|
|
86
|
+
|
|
87
|
+
var observability = lazyRequire(function () { return require("./observability"); });
|
|
88
|
+
|
|
89
|
+
var DEFAULT_MAX_ATTRIBUTES = 128; // allow:raw-byte-literal — OTLP default span attribute cap
|
|
90
|
+
var DEFAULT_MAX_EVENTS = 128; // allow:raw-byte-literal — OTLP default span event cap
|
|
91
|
+
var DEFAULT_MAX_ATTR_VALUE_LEN = 1024; // allow:raw-byte-literal — OTLP attribute value char cap
|
|
92
|
+
|
|
93
|
+
var VALID_KINDS = ["internal", "server", "client", "producer", "consumer"];
|
|
94
|
+
var VALID_STATUS_CODES = ["unset", "ok", "error"];
|
|
95
|
+
|
|
96
|
+
function _now() { return Date.now(); }
|
|
97
|
+
|
|
98
|
+
function _msToUnixNano(ms) {
|
|
99
|
+
// OTLP timestamps are uint64 nanoseconds since Unix epoch. JS Date.now()
|
|
100
|
+
// gives ms; multiply by 1e6 and stringify (OTLP/JSON uses string for
|
|
101
|
+
// uint64 values per https://protobuf.dev/programming-guides/proto3/#json).
|
|
102
|
+
return String(BigInt(ms) * 1000000n); // allow:raw-byte-literal — ms→ns conversion factor (1e6)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function _truncateAttrValue(v, maxLen) {
|
|
106
|
+
if (typeof v === "string" && v.length > maxLen) {
|
|
107
|
+
return v.slice(0, maxLen);
|
|
108
|
+
}
|
|
109
|
+
return v;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function _validateKind(kind) {
|
|
113
|
+
if (typeof kind !== "string") return "internal";
|
|
114
|
+
if (VALID_KINDS.indexOf(kind) === -1) {
|
|
115
|
+
throw new TracerError("tracer/bad-kind",
|
|
116
|
+
"tracer.start: kind must be one of " + VALID_KINDS.join(", ") +
|
|
117
|
+
" (got " + JSON.stringify(kind) + ")");
|
|
118
|
+
}
|
|
119
|
+
return kind;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function _validateAttrKey(key) {
|
|
123
|
+
if (typeof key !== "string" || key.length === 0) return false;
|
|
124
|
+
// OTel attribute keys: ASCII printable, dot-separated, no spaces
|
|
125
|
+
// beyond what the SEMCONV vocabulary uses.
|
|
126
|
+
if (key.length > 255) return false; // allow:raw-byte-literal — OTLP attribute key cap
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function _validateAttrValue(v) {
|
|
131
|
+
// OTLP supports string / int / double / bool / array of those
|
|
132
|
+
var t = typeof v;
|
|
133
|
+
if (t === "string" || t === "boolean") return true;
|
|
134
|
+
if (t === "number") return Number.isFinite(v);
|
|
135
|
+
if (Array.isArray(v)) {
|
|
136
|
+
for (var i = 0; i < v.length; i++) {
|
|
137
|
+
var elT = typeof v[i];
|
|
138
|
+
if (elT !== "string" && elT !== "boolean" && elT !== "number") return false;
|
|
139
|
+
if (elT === "number" && !Number.isFinite(v[i])) return false;
|
|
140
|
+
}
|
|
141
|
+
return true;
|
|
142
|
+
}
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function _spanId() {
|
|
147
|
+
return bCrypto.generateBytes(SPAN_ID_BYTES).toString("hex");
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function _traceId() {
|
|
151
|
+
return bCrypto.generateBytes(TRACE_ID_BYTES).toString("hex");
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function create(opts) {
|
|
155
|
+
validateOpts.requireObject(opts, "tracer", TracerError);
|
|
156
|
+
validateOpts(opts, [
|
|
157
|
+
"service", "resource", "scope",
|
|
158
|
+
"maxAttributes", "maxEvents", "maxAttributeValueLength",
|
|
159
|
+
"onEnd", "onStart", "audit",
|
|
160
|
+
], "tracer.create");
|
|
161
|
+
validateOpts.requireNonEmptyString(opts.service,
|
|
162
|
+
"tracer.create: service", TracerError, "tracer/bad-service");
|
|
163
|
+
validateOpts.optionalFunction(opts.onEnd,
|
|
164
|
+
"tracer.create: onEnd", TracerError, "tracer/bad-opts");
|
|
165
|
+
validateOpts.optionalFunction(opts.onStart,
|
|
166
|
+
"tracer.create: onStart", TracerError, "tracer/bad-opts");
|
|
167
|
+
|
|
168
|
+
var resource = Object.assign({
|
|
169
|
+
"service.name": opts.service,
|
|
170
|
+
}, opts.resource || {});
|
|
171
|
+
var scope = opts.scope || { name: "blamejs", version: constants.version || null };
|
|
172
|
+
var maxAttributes = opts.maxAttributes || DEFAULT_MAX_ATTRIBUTES;
|
|
173
|
+
var maxEvents = opts.maxEvents || DEFAULT_MAX_EVENTS;
|
|
174
|
+
var maxAttrValLen = opts.maxAttributeValueLength || DEFAULT_MAX_ATTR_VALUE_LEN;
|
|
175
|
+
|
|
176
|
+
function _newSpan(name, spanOpts) {
|
|
177
|
+
spanOpts = spanOpts || {};
|
|
178
|
+
var traceId = spanOpts.traceId;
|
|
179
|
+
if (typeof traceId !== "string" || !safeBuffer.TRACE_ID_HEX_RE.test(traceId)) { // allow:regex-no-length-cap — fixed-length hex constant from safe-buffer
|
|
180
|
+
traceId = _traceId();
|
|
181
|
+
}
|
|
182
|
+
var parentSpanId = spanOpts.parentId || null;
|
|
183
|
+
if (parentSpanId !== null && (typeof parentSpanId !== "string" || !safeBuffer.SPAN_ID_HEX_RE.test(parentSpanId))) { // allow:regex-no-length-cap — fixed-length hex constant from safe-buffer
|
|
184
|
+
parentSpanId = null;
|
|
185
|
+
}
|
|
186
|
+
var spanId = _spanId();
|
|
187
|
+
var startMs = _now();
|
|
188
|
+
var kind = _validateKind(spanOpts.kind);
|
|
189
|
+
var sampled = spanOpts.sampled !== false;
|
|
190
|
+
|
|
191
|
+
var attributes = Object.create(null);
|
|
192
|
+
var droppedAttributesCount = 0;
|
|
193
|
+
var events = [];
|
|
194
|
+
var droppedEventsCount = 0;
|
|
195
|
+
var status = { code: "unset", message: null };
|
|
196
|
+
var ended = false;
|
|
197
|
+
var endMs = null;
|
|
198
|
+
|
|
199
|
+
function setAttribute(key, value) {
|
|
200
|
+
if (ended) return span;
|
|
201
|
+
if (!_validateAttrKey(key)) { droppedAttributesCount += 1; return span; }
|
|
202
|
+
if (!_validateAttrValue(value)) { droppedAttributesCount += 1; return span; }
|
|
203
|
+
var keyCount = Object.keys(attributes).length;
|
|
204
|
+
if (!(key in attributes) && keyCount >= maxAttributes) {
|
|
205
|
+
droppedAttributesCount += 1;
|
|
206
|
+
return span;
|
|
207
|
+
}
|
|
208
|
+
attributes[key] = _truncateAttrValue(value, maxAttrValLen);
|
|
209
|
+
return span;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function setAttributes(map) {
|
|
213
|
+
if (!map || typeof map !== "object") return span;
|
|
214
|
+
var keys = Object.keys(map);
|
|
215
|
+
for (var i = 0; i < keys.length; i++) setAttribute(keys[i], map[keys[i]]);
|
|
216
|
+
return span;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function addEvent(eventName, eventAttrs) {
|
|
220
|
+
if (ended) return span;
|
|
221
|
+
if (typeof eventName !== "string" || eventName.length === 0) {
|
|
222
|
+
droppedEventsCount += 1;
|
|
223
|
+
return span;
|
|
224
|
+
}
|
|
225
|
+
if (events.length >= maxEvents) { droppedEventsCount += 1; return span; }
|
|
226
|
+
var eventTime = _now();
|
|
227
|
+
var attrs = Object.create(null);
|
|
228
|
+
if (eventAttrs && typeof eventAttrs === "object") {
|
|
229
|
+
var ks = Object.keys(eventAttrs);
|
|
230
|
+
for (var i = 0; i < ks.length; i++) {
|
|
231
|
+
var k = ks[i], v = eventAttrs[k];
|
|
232
|
+
if (_validateAttrKey(k) && _validateAttrValue(v)) {
|
|
233
|
+
attrs[k] = _truncateAttrValue(v, maxAttrValLen);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
events.push({
|
|
238
|
+
name: eventName,
|
|
239
|
+
timeUnixNano: _msToUnixNano(eventTime),
|
|
240
|
+
attributes: attrs,
|
|
241
|
+
});
|
|
242
|
+
return span;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function recordException(err) {
|
|
246
|
+
if (ended) return span;
|
|
247
|
+
if (!err) return span;
|
|
248
|
+
var name = (err.name || (err.constructor && err.constructor.name) || "Error");
|
|
249
|
+
var message = (err.message || String(err));
|
|
250
|
+
var stack = err.stack ? String(err.stack) : null;
|
|
251
|
+
addEvent("exception", {
|
|
252
|
+
"exception.type": name,
|
|
253
|
+
"exception.message": message,
|
|
254
|
+
"exception.stacktrace": stack || "",
|
|
255
|
+
});
|
|
256
|
+
return span;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function setStatus(code, message) {
|
|
260
|
+
if (ended) return span;
|
|
261
|
+
if (VALID_STATUS_CODES.indexOf(code) === -1) {
|
|
262
|
+
throw new TracerError("tracer/bad-status",
|
|
263
|
+
"span.setStatus: code must be one of " + VALID_STATUS_CODES.join(", "));
|
|
264
|
+
}
|
|
265
|
+
status.code = code;
|
|
266
|
+
status.message = (typeof message === "string") ? message : null;
|
|
267
|
+
return span;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function end(endTimestampMs) {
|
|
271
|
+
if (ended) return span;
|
|
272
|
+
ended = true;
|
|
273
|
+
endMs = (typeof endTimestampMs === "number" && isFinite(endTimestampMs)) ? endTimestampMs : _now();
|
|
274
|
+
if (typeof opts.onEnd === "function") {
|
|
275
|
+
try { opts.onEnd(toJSON()); }
|
|
276
|
+
catch (_e) { /* operator hook — drop-silent */ }
|
|
277
|
+
}
|
|
278
|
+
try { observability().safeEvent("tracer.span.end", 1, {
|
|
279
|
+
kind: kind, status: status.code, sampled: sampled ? "1" : "0",
|
|
280
|
+
}); } catch (_e) { /* drop-silent */ }
|
|
281
|
+
return span;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function isRecording() { return !ended; }
|
|
285
|
+
|
|
286
|
+
function toJSON() {
|
|
287
|
+
var endNano = endMs !== null ? _msToUnixNano(endMs) : null;
|
|
288
|
+
var durationMs = endMs !== null ? (endMs - startMs) : null;
|
|
289
|
+
// OTLP/JSON shape (Trace v1)
|
|
290
|
+
return {
|
|
291
|
+
traceId: traceId,
|
|
292
|
+
spanId: spanId,
|
|
293
|
+
parentSpanId: parentSpanId,
|
|
294
|
+
name: name,
|
|
295
|
+
kind: kind,
|
|
296
|
+
startTimeUnixNano: _msToUnixNano(startMs),
|
|
297
|
+
endTimeUnixNano: endNano,
|
|
298
|
+
durationNs: endNano !== null ? String(BigInt(durationMs) * 1000000n) : null, // allow:raw-byte-literal — ms→ns conversion factor (1e6)
|
|
299
|
+
durationMs: durationMs,
|
|
300
|
+
attributes: Object.assign({}, attributes),
|
|
301
|
+
events: events.slice(),
|
|
302
|
+
status: { code: status.code, message: status.message },
|
|
303
|
+
resource: Object.assign({}, resource),
|
|
304
|
+
scope: Object.assign({}, scope),
|
|
305
|
+
droppedAttributesCount: droppedAttributesCount,
|
|
306
|
+
droppedEventsCount: droppedEventsCount,
|
|
307
|
+
sampled: sampled,
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
var span = {
|
|
312
|
+
traceId: traceId,
|
|
313
|
+
spanId: spanId,
|
|
314
|
+
parentSpanId: parentSpanId,
|
|
315
|
+
name: name,
|
|
316
|
+
kind: kind,
|
|
317
|
+
sampled: sampled,
|
|
318
|
+
setAttribute: setAttribute,
|
|
319
|
+
setAttributes: setAttributes,
|
|
320
|
+
addEvent: addEvent,
|
|
321
|
+
recordException: recordException,
|
|
322
|
+
setStatus: setStatus,
|
|
323
|
+
end: end,
|
|
324
|
+
isRecording: isRecording,
|
|
325
|
+
toJSON: toJSON,
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
// Apply initial attributes
|
|
329
|
+
if (spanOpts.attributes && typeof spanOpts.attributes === "object") {
|
|
330
|
+
setAttributes(spanOpts.attributes);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if (typeof opts.onStart === "function") {
|
|
334
|
+
try { opts.onStart(span); }
|
|
335
|
+
catch (_e) { /* operator hook — drop-silent */ }
|
|
336
|
+
}
|
|
337
|
+
try { observability().safeEvent("tracer.span.start", 1, {
|
|
338
|
+
kind: kind, sampled: sampled ? "1" : "0",
|
|
339
|
+
}); } catch (_e) { /* drop-silent */ }
|
|
340
|
+
|
|
341
|
+
return span;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function start(name, spanOpts) {
|
|
345
|
+
if (typeof name !== "string" || name.length === 0) {
|
|
346
|
+
throw new TracerError("tracer/bad-name",
|
|
347
|
+
"tracer.start: name must be a non-empty string");
|
|
348
|
+
}
|
|
349
|
+
return _newSpan(name, spanOpts || {});
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function startChildOf(parentSpan, name, spanOpts) {
|
|
353
|
+
if (!parentSpan || typeof parentSpan.traceId !== "string") {
|
|
354
|
+
throw new TracerError("tracer/bad-parent",
|
|
355
|
+
"tracer.startChildOf: parentSpan must be a span object");
|
|
356
|
+
}
|
|
357
|
+
var childOpts = Object.assign({}, spanOpts || {}, {
|
|
358
|
+
traceId: parentSpan.traceId,
|
|
359
|
+
parentId: parentSpan.spanId,
|
|
360
|
+
sampled: parentSpan.sampled,
|
|
361
|
+
});
|
|
362
|
+
return _newSpan(name, childOpts);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
return {
|
|
366
|
+
start: start,
|
|
367
|
+
startChildOf: startChildOf,
|
|
368
|
+
service: opts.service,
|
|
369
|
+
resource: resource,
|
|
370
|
+
scope: scope,
|
|
371
|
+
_attributeCaps: { // exported for tests
|
|
372
|
+
maxAttributes: maxAttributes,
|
|
373
|
+
maxEvents: maxEvents,
|
|
374
|
+
maxAttributeValueLength: maxAttrValLen,
|
|
375
|
+
},
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Pure helper: derive the canonical W3C `traceparent` header from a span.
|
|
380
|
+
function spanToTraceparent(span) {
|
|
381
|
+
if (!span || typeof span.traceId !== "string" || typeof span.spanId !== "string") {
|
|
382
|
+
throw new TracerError("tracer/bad-span",
|
|
383
|
+
"spanToTraceparent: argument must be a span with traceId + spanId");
|
|
384
|
+
}
|
|
385
|
+
return "00-" + span.traceId + "-" + span.spanId + "-" + (span.sampled ? "01" : "00");
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
module.exports = {
|
|
389
|
+
create: create,
|
|
390
|
+
spanToTraceparent: spanToTraceparent,
|
|
391
|
+
TracerError: TracerError,
|
|
392
|
+
_BASE64URL_RE: safeBuffer.BASE64URL_RE, // not used directly — exposed for downstream tests
|
|
393
|
+
VALID_KINDS: VALID_KINDS,
|
|
394
|
+
VALID_STATUS_CODES: VALID_STATUS_CODES,
|
|
395
|
+
};
|
package/lib/observability.js
CHANGED
|
@@ -54,8 +54,13 @@
|
|
|
54
54
|
* fn: function — sync or async. Return propagates; throws propagate
|
|
55
55
|
* after metrics fire.
|
|
56
56
|
*/
|
|
57
|
+
var C = require("./constants");
|
|
57
58
|
var lazyRequire = require("./lazy-require");
|
|
58
59
|
|
|
60
|
+
// safe-buffer can't be top-required: framework-error → observability →
|
|
61
|
+
// safe-buffer → framework-error forms a cycle. Lazy-loaded at first use.
|
|
62
|
+
var safeBuffer = lazyRequire(function () { return require("./safe-buffer"); });
|
|
63
|
+
|
|
59
64
|
var tracing = lazyRequire(function () { return require("./tracing"); });
|
|
60
65
|
var metrics = lazyRequire(function () { return require("./metrics"); });
|
|
61
66
|
|
|
@@ -355,10 +360,10 @@ function _buildTraceparent(opts) {
|
|
|
355
360
|
}
|
|
356
361
|
var traceId = opts.traceId;
|
|
357
362
|
var parentId = opts.parentId;
|
|
358
|
-
if (typeof traceId !== "string" ||
|
|
363
|
+
if (typeof traceId !== "string" || !safeBuffer().TRACE_ID_HEX_RE.test(traceId) || traceId === _ALL_ZERO_TRACE) { // allow:regex-no-length-cap — fixed-length hex constant from safe-buffer
|
|
359
364
|
throw new TypeError("traceContext.build: traceId must be 32 lowercase hex chars (non-zero)");
|
|
360
365
|
}
|
|
361
|
-
if (typeof parentId !== "string" ||
|
|
366
|
+
if (typeof parentId !== "string" || !safeBuffer().SPAN_ID_HEX_RE.test(parentId) || parentId === _ALL_ZERO_PARENT) { // allow:regex-no-length-cap — fixed-length hex constant from safe-buffer
|
|
362
367
|
throw new TypeError("traceContext.build: parentId must be 16 lowercase hex chars (non-zero)");
|
|
363
368
|
}
|
|
364
369
|
var flagsByte = (opts.sampled ? _TRACE_FLAG_SAMPLED : 0);
|
|
@@ -380,11 +385,227 @@ function _newParentId() {
|
|
|
380
385
|
return hex === _ALL_ZERO_PARENT ? _nodeCryptoForTrace.randomBytes(_PARENT_ID_BYTES).toString("hex") : hex;
|
|
381
386
|
}
|
|
382
387
|
|
|
388
|
+
// W3C Trace Context §3.3 — tracestate: comma-separated list of
|
|
389
|
+
// `vendor=value` pairs carrying vendor-specific trace data.
|
|
390
|
+
//
|
|
391
|
+
// tracestate: rojo=00f067aa0ba902b7, congo=t61rcWkgMzE
|
|
392
|
+
//
|
|
393
|
+
// Spec rules (https://www.w3.org/TR/trace-context-1/#tracestate-header):
|
|
394
|
+
// - vendor key: lowercase ASCII letters, digits, `_`, `-`, `*`, `/`,
|
|
395
|
+
// length 1..256, optionally with `<tenant>@<system>` form
|
|
396
|
+
// - value: printable ASCII (0x20..0x7E) excluding `,` and `=`,
|
|
397
|
+
// length 1..256
|
|
398
|
+
// - max 32 entries, max 512 chars total
|
|
399
|
+
// - duplicate keys: keep first, drop rest
|
|
400
|
+
var _TRACESTATE_KEY_RE = /^[a-z0-9][a-z0-9_\-*/]{0,255}(@[a-z0-9][a-z0-9_\-*/]{0,255})?$/;
|
|
401
|
+
var _TRACESTATE_VALUE_RE = /^[\x20-\x2B\x2D-\x3C\x3E-\x7E]{1,256}$/; // printable, no "," or "="
|
|
402
|
+
var _TRACESTATE_MAX_ENTRIES = 32; // allow:raw-byte-literal — W3C spec hard cap (§3.3.1.3)
|
|
403
|
+
var _TRACESTATE_MAX_CHARS = 512; // allow:raw-byte-literal — W3C spec hard cap (§3.3.1.3)
|
|
404
|
+
|
|
405
|
+
function _parseTracestate(headerValue) {
|
|
406
|
+
if (typeof headerValue !== "string") return null;
|
|
407
|
+
if (headerValue.length === 0 || headerValue.length > _TRACESTATE_MAX_CHARS) return null;
|
|
408
|
+
var pairs = headerValue.split(",");
|
|
409
|
+
if (pairs.length > _TRACESTATE_MAX_ENTRIES) return null;
|
|
410
|
+
var seen = Object.create(null);
|
|
411
|
+
var out = [];
|
|
412
|
+
for (var i = 0; i < pairs.length; i++) {
|
|
413
|
+
var raw = pairs[i].trim();
|
|
414
|
+
if (raw.length === 0) continue;
|
|
415
|
+
var eqIdx = raw.indexOf("=");
|
|
416
|
+
if (eqIdx === -1) return null;
|
|
417
|
+
var key = raw.slice(0, eqIdx).trim();
|
|
418
|
+
var val = raw.slice(eqIdx + 1).trim();
|
|
419
|
+
if (!_TRACESTATE_KEY_RE.test(key)) return null; // allow:regex-no-length-cap — regex literal hard-caps key length per W3C §3.3.1.1
|
|
420
|
+
if (!_TRACESTATE_VALUE_RE.test(val)) return null; // allow:regex-no-length-cap — regex literal hard-caps value length per W3C §3.3.1.2
|
|
421
|
+
if (seen[key]) continue; // dup-key: keep first
|
|
422
|
+
seen[key] = true;
|
|
423
|
+
out.push({ key: key, value: val });
|
|
424
|
+
}
|
|
425
|
+
return out;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function _buildTracestate(entries) {
|
|
429
|
+
if (!Array.isArray(entries)) {
|
|
430
|
+
throw new TypeError("traceContext.buildTracestate: entries must be an array");
|
|
431
|
+
}
|
|
432
|
+
if (entries.length > _TRACESTATE_MAX_ENTRIES) {
|
|
433
|
+
throw new TypeError("traceContext.buildTracestate: too many entries (max " +
|
|
434
|
+
_TRACESTATE_MAX_ENTRIES + ")");
|
|
435
|
+
}
|
|
436
|
+
var seen = Object.create(null);
|
|
437
|
+
var parts = [];
|
|
438
|
+
for (var i = 0; i < entries.length; i++) {
|
|
439
|
+
var e = entries[i];
|
|
440
|
+
if (!e || typeof e !== "object") {
|
|
441
|
+
throw new TypeError("traceContext.buildTracestate: entries[" + i + "] must be an object");
|
|
442
|
+
}
|
|
443
|
+
if (typeof e.key !== "string" || !_TRACESTATE_KEY_RE.test(e.key)) { // allow:regex-no-length-cap — regex literal hard-caps key length per W3C §3.3.1.1
|
|
444
|
+
throw new TypeError("traceContext.buildTracestate: entries[" + i + "].key violates W3C key rules");
|
|
445
|
+
}
|
|
446
|
+
if (typeof e.value !== "string" || !_TRACESTATE_VALUE_RE.test(e.value)) { // allow:regex-no-length-cap — regex literal hard-caps value length per W3C §3.3.1.2
|
|
447
|
+
throw new TypeError("traceContext.buildTracestate: entries[" + i + "].value violates W3C value rules");
|
|
448
|
+
}
|
|
449
|
+
if (seen[e.key]) continue;
|
|
450
|
+
seen[e.key] = true;
|
|
451
|
+
parts.push(e.key + "=" + e.value);
|
|
452
|
+
}
|
|
453
|
+
var s = parts.join(",");
|
|
454
|
+
if (s.length > _TRACESTATE_MAX_CHARS) {
|
|
455
|
+
throw new TypeError("traceContext.buildTracestate: built string exceeds W3C 512-char cap");
|
|
456
|
+
}
|
|
457
|
+
return s;
|
|
458
|
+
}
|
|
459
|
+
|
|
383
460
|
var traceContext = {
|
|
384
|
-
parse:
|
|
385
|
-
build:
|
|
386
|
-
newTraceId:
|
|
387
|
-
newParentId:
|
|
461
|
+
parse: _parseTraceparent,
|
|
462
|
+
build: _buildTraceparent,
|
|
463
|
+
newTraceId: _newTraceId,
|
|
464
|
+
newParentId: _newParentId,
|
|
465
|
+
parseTracestate: _parseTracestate,
|
|
466
|
+
buildTracestate: _buildTracestate,
|
|
467
|
+
};
|
|
468
|
+
|
|
469
|
+
// ---- W3C Baggage (https://www.w3.org/TR/baggage/) ----
|
|
470
|
+
//
|
|
471
|
+
// `baggage` HTTP header carries a comma-separated list of
|
|
472
|
+
// `key=value;property=value;property=value` triplets. Used to
|
|
473
|
+
// propagate user-supplied context (tenantId, deploymentRegion,
|
|
474
|
+
// experimentId, etc.) across service boundaries WITHOUT mixing it
|
|
475
|
+
// into traceparent (which is reserved for trace identifiers).
|
|
476
|
+
//
|
|
477
|
+
// Spec rules:
|
|
478
|
+
// - key: token per RFC 7230 (`tchar` set: `!#$%&'*+\-.^_\`|~` +
|
|
479
|
+
// digits + ALPHA), length 1..255
|
|
480
|
+
// - value: percent-encoded UTF-8, must NOT contain CTL chars,
|
|
481
|
+
// `,`, `;`, `=` (those are structural delimiters)
|
|
482
|
+
// - properties: optional, semicolon-separated `key=value` or bare
|
|
483
|
+
// `key` after the main value
|
|
484
|
+
// - max 64 entries per Baggage section recommendation
|
|
485
|
+
// - max 8192 chars total (W3C recommended cap)
|
|
486
|
+
// Resolved at first call; lazyRequire returns a function.
|
|
487
|
+
function _baggageTokenRe() { return safeBuffer().RFC7230_TCHAR_RE; }
|
|
488
|
+
var _BAGGAGE_MAX_ENTRIES = 64; // allow:raw-byte-literal — W3C Baggage recommended cap
|
|
489
|
+
var _BAGGAGE_MAX_CHARS = C.BYTES.kib(8); // W3C Baggage recommended 8192-char cap
|
|
490
|
+
|
|
491
|
+
function _parseBaggage(headerValue) {
|
|
492
|
+
if (typeof headerValue !== "string") return null;
|
|
493
|
+
if (headerValue.length === 0 || headerValue.length > _BAGGAGE_MAX_CHARS) return null;
|
|
494
|
+
var entries = headerValue.split(",");
|
|
495
|
+
if (entries.length > _BAGGAGE_MAX_ENTRIES) return null;
|
|
496
|
+
var seen = Object.create(null);
|
|
497
|
+
var out = [];
|
|
498
|
+
for (var i = 0; i < entries.length; i++) {
|
|
499
|
+
var raw = entries[i].trim();
|
|
500
|
+
if (raw.length === 0) continue;
|
|
501
|
+
var parts = raw.split(";");
|
|
502
|
+
var head = parts[0].trim();
|
|
503
|
+
var eqIdx = head.indexOf("=");
|
|
504
|
+
if (eqIdx === -1) return null;
|
|
505
|
+
var key = head.slice(0, eqIdx).trim();
|
|
506
|
+
var rawValue = head.slice(eqIdx + 1).trim();
|
|
507
|
+
if (!_baggageTokenRe().test(key)) return null; // allow:regex-no-length-cap — RFC 7230 tchar; bound by header-cap
|
|
508
|
+
if (key.length > 255) return null; // allow:raw-byte-literal — W3C key length cap
|
|
509
|
+
var value;
|
|
510
|
+
try { value = decodeURIComponent(rawValue); }
|
|
511
|
+
catch (_e) { return null; }
|
|
512
|
+
var props = [];
|
|
513
|
+
for (var p = 1; p < parts.length; p++) {
|
|
514
|
+
var prop = parts[p].trim();
|
|
515
|
+
if (prop.length === 0) continue;
|
|
516
|
+
var pEq = prop.indexOf("=");
|
|
517
|
+
if (pEq === -1) {
|
|
518
|
+
if (!_baggageTokenRe().test(prop)) return null; // allow:regex-no-length-cap — RFC 7230 tchar; bound by header-cap
|
|
519
|
+
props.push({ key: prop, value: null });
|
|
520
|
+
} else {
|
|
521
|
+
var pKey = prop.slice(0, pEq).trim();
|
|
522
|
+
var pVal = prop.slice(pEq + 1).trim();
|
|
523
|
+
if (!_baggageTokenRe().test(pKey)) return null; // allow:regex-no-length-cap — RFC 7230 tchar; bound by header-cap
|
|
524
|
+
var pValueDecoded;
|
|
525
|
+
try { pValueDecoded = decodeURIComponent(pVal); }
|
|
526
|
+
catch (_e) { return null; }
|
|
527
|
+
props.push({ key: pKey, value: pValueDecoded });
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
if (seen[key]) continue;
|
|
531
|
+
seen[key] = true;
|
|
532
|
+
out.push({ key: key, value: value, properties: props });
|
|
533
|
+
}
|
|
534
|
+
return out;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function _buildBaggage(entries) {
|
|
538
|
+
if (!Array.isArray(entries)) {
|
|
539
|
+
throw new TypeError("traceContext.buildBaggage: entries must be an array");
|
|
540
|
+
}
|
|
541
|
+
if (entries.length > _BAGGAGE_MAX_ENTRIES) {
|
|
542
|
+
throw new TypeError("traceContext.buildBaggage: too many entries (max " +
|
|
543
|
+
_BAGGAGE_MAX_ENTRIES + ")");
|
|
544
|
+
}
|
|
545
|
+
var seen = Object.create(null);
|
|
546
|
+
var parts = [];
|
|
547
|
+
for (var i = 0; i < entries.length; i++) {
|
|
548
|
+
var e = entries[i];
|
|
549
|
+
if (!e || typeof e !== "object") {
|
|
550
|
+
throw new TypeError("traceContext.buildBaggage: entries[" + i + "] must be an object");
|
|
551
|
+
}
|
|
552
|
+
if (typeof e.key !== "string" || !_baggageTokenRe().test(e.key)) { // allow:regex-no-length-cap — RFC 7230 tchar; bound by header-cap
|
|
553
|
+
throw new TypeError("traceContext.buildBaggage: entries[" + i + "].key violates W3C key rules");
|
|
554
|
+
}
|
|
555
|
+
if (typeof e.value !== "string") {
|
|
556
|
+
throw new TypeError("traceContext.buildBaggage: entries[" + i + "].value must be a string");
|
|
557
|
+
}
|
|
558
|
+
if (seen[e.key]) continue;
|
|
559
|
+
seen[e.key] = true;
|
|
560
|
+
var encodedValue = encodeURIComponent(e.value);
|
|
561
|
+
var item = e.key + "=" + encodedValue;
|
|
562
|
+
if (Array.isArray(e.properties)) {
|
|
563
|
+
for (var p = 0; p < e.properties.length; p++) {
|
|
564
|
+
var prop = e.properties[p];
|
|
565
|
+
if (!prop || typeof prop !== "object") continue;
|
|
566
|
+
if (typeof prop.key !== "string" || !_baggageTokenRe().test(prop.key)) { // allow:regex-no-length-cap — RFC 7230 tchar; bound by header-cap
|
|
567
|
+
throw new TypeError("traceContext.buildBaggage: entries[" + i +
|
|
568
|
+
"].properties[" + p + "].key violates W3C property-key rules");
|
|
569
|
+
}
|
|
570
|
+
if (prop.value === null || prop.value === undefined) {
|
|
571
|
+
item += ";" + prop.key;
|
|
572
|
+
} else if (typeof prop.value === "string") {
|
|
573
|
+
item += ";" + prop.key + "=" + encodeURIComponent(prop.value);
|
|
574
|
+
} else {
|
|
575
|
+
throw new TypeError("traceContext.buildBaggage: entries[" + i +
|
|
576
|
+
"].properties[" + p + "].value must be a string or null");
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
parts.push(item);
|
|
581
|
+
}
|
|
582
|
+
var s = parts.join(",");
|
|
583
|
+
if (s.length > _BAGGAGE_MAX_CHARS) {
|
|
584
|
+
throw new TypeError("traceContext.buildBaggage: built string exceeds W3C 8192-char cap");
|
|
585
|
+
}
|
|
586
|
+
return s;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
var baggage = {
|
|
590
|
+
parse: _parseBaggage,
|
|
591
|
+
build: _buildBaggage,
|
|
592
|
+
MAX_ENTRIES: _BAGGAGE_MAX_ENTRIES,
|
|
593
|
+
};
|
|
594
|
+
|
|
595
|
+
// Lazy-required to avoid a require cycle (tracer / exporter both
|
|
596
|
+
// reach back into observability for safeEvent emissions).
|
|
597
|
+
var _tracerModule = lazyRequire(function () { return require("./observability-tracer"); });
|
|
598
|
+
var _otlpExporterModule = lazyRequire(function () { return require("./observability-otlp-exporter"); });
|
|
599
|
+
|
|
600
|
+
var tracer = {
|
|
601
|
+
create: function (opts) { return _tracerModule().create(opts); },
|
|
602
|
+
spanToTraceparent: function (span) { return _tracerModule().spanToTraceparent(span); },
|
|
603
|
+
VALID_KINDS: ["internal", "server", "client", "producer", "consumer"],
|
|
604
|
+
VALID_STATUS_CODES: ["unset", "ok", "error"],
|
|
605
|
+
};
|
|
606
|
+
|
|
607
|
+
var otlpExporter = {
|
|
608
|
+
create: function (opts) { return _otlpExporterModule().create(opts); },
|
|
388
609
|
};
|
|
389
610
|
|
|
390
611
|
module.exports = {
|
|
@@ -395,4 +616,7 @@ module.exports = {
|
|
|
395
616
|
setTap: setTap,
|
|
396
617
|
SEMCONV: SEMCONV,
|
|
397
618
|
traceContext: traceContext,
|
|
619
|
+
baggage: baggage,
|
|
620
|
+
tracer: tracer,
|
|
621
|
+
otlpExporter: otlpExporter,
|
|
398
622
|
};
|
package/lib/safe-buffer.js
CHANGED
|
@@ -196,6 +196,18 @@ var HEX_RE = /^[0-9a-fA-F]+$/;
|
|
|
196
196
|
// is length-agnostic — callers cap length per protocol contract.
|
|
197
197
|
var BASE64URL_RE = /^[A-Za-z0-9_-]+$/;
|
|
198
198
|
|
|
199
|
+
// Fixed-length hex predicates used by trace-context primitives (W3C
|
|
200
|
+
// trace-id is 16 bytes = 32 hex chars; span-id / parent-id is 8
|
|
201
|
+
// bytes = 16 hex chars). Extracted to keep callers length-bounded
|
|
202
|
+
// without duplicating the literal in every file.
|
|
203
|
+
var TRACE_ID_HEX_RE = /^[0-9a-f]{32}$/; // allow:regex-no-length-cap — fixed 32 hex chars (W3C §3.2.2.3)
|
|
204
|
+
var SPAN_ID_HEX_RE = /^[0-9a-f]{16}$/; // allow:regex-no-length-cap — fixed 16 hex chars (W3C §3.2.2.4)
|
|
205
|
+
|
|
206
|
+
// RFC 7230 §3.2.6 / RFC 9110 §5.1 `tchar` grammar — used by HTTP
|
|
207
|
+
// header tokens, MIME parameter names, W3C Baggage keys, etc.
|
|
208
|
+
// Length-agnostic; callers cap per protocol.
|
|
209
|
+
var RFC7230_TCHAR_RE = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/; // allow:regex-no-length-cap — caller bounds length
|
|
210
|
+
|
|
199
211
|
// CRLF_RE matches any control character used in HTTP-header / SMTP-
|
|
200
212
|
// envelope injection attacks. Header values that contain CR or LF must
|
|
201
213
|
// be rejected before serialization.
|
|
@@ -238,6 +250,9 @@ module.exports = {
|
|
|
238
250
|
stripTrailingHspace: stripTrailingHspace,
|
|
239
251
|
HEX_RE: HEX_RE,
|
|
240
252
|
BASE64URL_RE: BASE64URL_RE,
|
|
253
|
+
TRACE_ID_HEX_RE: TRACE_ID_HEX_RE,
|
|
254
|
+
SPAN_ID_HEX_RE: SPAN_ID_HEX_RE,
|
|
255
|
+
RFC7230_TCHAR_RE: RFC7230_TCHAR_RE,
|
|
241
256
|
CRLF_RE: CRLF_RE,
|
|
242
257
|
TRAILING_HSPACE_RE: TRAILING_HSPACE_RE,
|
|
243
258
|
SafeBufferError: SafeBufferError,
|