@blamejs/core 0.7.4 → 0.7.18
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 +423 -395
- package/README.md +150 -149
- package/bin/blamejs.js +0 -0
- package/index.js +308 -284
- package/lib/api-key.js +660 -663
- package/lib/api-snapshot.js +338 -338
- package/lib/app-shutdown.js +385 -385
- package/lib/app.js +365 -365
- package/lib/archive.js +250 -250
- package/lib/atomic-file.js +544 -544
- package/lib/audit-chain.js +177 -177
- package/lib/audit-sign.js +344 -344
- package/lib/audit-tools.js +677 -677
- package/lib/audit.js +766 -766
- package/lib/auth/jwt.js +311 -311
- package/lib/auth/lockout.js +436 -436
- package/lib/auth/oauth.js +721 -721
- package/lib/auth/passkey.js +181 -181
- package/lib/auth/password.js +594 -594
- package/lib/backup/bundle.js +217 -217
- package/lib/backup/crypto.js +176 -176
- package/lib/backup/index.js +515 -515
- package/lib/backup/manifest.js +282 -282
- package/lib/break-glass.js +1338 -1338
- package/lib/bundler.js +441 -441
- package/lib/cache-redis.js +256 -256
- package/lib/cache.js +1206 -1206
- package/lib/canonical-json.js +115 -115
- package/lib/chain-writer.js +234 -234
- package/lib/cli-helpers.js +206 -206
- package/lib/cli.js +2334 -2334
- package/lib/cluster-provider-db.js +317 -317
- package/lib/cluster-storage.js +226 -226
- package/lib/cluster.js +703 -703
- package/lib/codepoint-class.js +196 -0
- package/lib/config-drift.js +301 -301
- package/lib/consent.js +222 -222
- package/lib/constants.js +191 -191
- package/lib/cookies.js +315 -315
- package/lib/credential-hash.js +322 -322
- package/lib/crypto.js +266 -266
- package/lib/csv.js +275 -286
- package/lib/db-declare-row-policy.js +267 -267
- package/lib/db-declare-view.js +420 -421
- package/lib/db-query.js +406 -406
- package/lib/db-schema.js +319 -319
- package/lib/db.js +1288 -1288
- package/lib/deprecate.js +222 -222
- package/lib/dev.js +335 -335
- package/lib/dual-control.js +473 -473
- package/lib/error-page.js +420 -420
- package/lib/external-db-migrate.js +441 -441
- package/lib/external-db.js +1061 -1061
- package/lib/file-type.js +273 -273
- package/lib/file-upload.js +213 -10
- package/lib/forms.js +422 -422
- package/lib/framework-error.js +293 -215
- package/lib/framework-schema.js +717 -717
- package/lib/gate-contract.js +971 -0
- package/lib/guard-all.js +405 -0
- package/lib/guard-archive.js +739 -0
- package/lib/guard-csv.js +816 -0
- package/lib/guard-email.js +744 -0
- package/lib/guard-filename.js +724 -0
- package/lib/guard-html.js +976 -0
- package/lib/guard-json.js +729 -0
- package/lib/guard-markdown.js +586 -0
- package/lib/guard-svg.js +976 -0
- package/lib/guard-xml.js +405 -0
- package/lib/guard-yaml.js +529 -0
- package/lib/handlers.js +350 -350
- package/lib/http-client-cookie-jar.js +508 -508
- package/lib/http-client.js +1195 -1195
- package/lib/i18n.js +878 -878
- package/lib/jobs.js +185 -185
- package/lib/log-stream-cloudwatch.js +369 -369
- package/lib/log-stream-local.js +146 -146
- package/lib/log-stream-otlp-grpc.js +410 -410
- package/lib/log-stream-otlp.js +286 -286
- package/lib/log-stream-syslog.js +302 -302
- package/lib/log-stream-webhook.js +199 -199
- package/lib/log-stream.js +330 -330
- package/lib/log.js +500 -500
- package/lib/mail-bounce.js +528 -528
- package/lib/mail-dkim.js +369 -362
- package/lib/mail.js +981 -962
- package/lib/metrics.js +683 -683
- package/lib/middleware/api-encrypt.js +936 -936
- package/lib/middleware/attach-user.js +157 -157
- package/lib/middleware/body-parser.js +1170 -1091
- package/lib/middleware/bot-guard.js +178 -178
- package/lib/middleware/compression.js +452 -452
- package/lib/middleware/cors.js +314 -314
- package/lib/middleware/csp-nonce.js +348 -348
- package/lib/middleware/csrf-protect.js +316 -316
- package/lib/middleware/db-role-for.js +264 -264
- package/lib/middleware/health.js +392 -392
- package/lib/middleware/index.js +79 -79
- package/lib/middleware/rate-limit.js +358 -358
- package/lib/middleware/request-id.js +61 -61
- package/lib/middleware/request-log.js +168 -168
- package/lib/middleware/require-auth.js +104 -104
- package/lib/middleware/security-headers.js +116 -116
- package/lib/middleware/sse.js +166 -166
- package/lib/migrations.js +383 -383
- package/lib/mtls-ca.js +518 -518
- package/lib/mtls-engine-default.js +481 -481
- package/lib/network-dns.js +632 -632
- package/lib/network-heartbeat.js +290 -290
- package/lib/network-nts.js +574 -574
- package/lib/network-proxy.js +265 -265
- package/lib/network-tls.js +328 -328
- package/lib/network.js +233 -233
- package/lib/notify.js +612 -612
- package/lib/ntp-check.js +229 -229
- package/lib/numeric-bounds.js +111 -91
- package/lib/object-store/azure-blob-bucket-ops.js +349 -349
- package/lib/object-store/azure-blob.js +488 -488
- package/lib/object-store/gcs-bucket-ops.js +351 -351
- package/lib/object-store/gcs.js +519 -519
- package/lib/object-store/http-put.js +153 -153
- package/lib/object-store/index.js +197 -197
- package/lib/object-store/sigv4-bucket-ops.js +1092 -1092
- package/lib/object-store/sigv4.js +903 -903
- package/lib/observability.js +151 -151
- package/lib/otel-export.js +269 -269
- package/lib/pagination.js +464 -464
- package/lib/parsers/index.js +80 -80
- package/lib/parsers/safe-env.js +642 -642
- package/lib/parsers/safe-ini.js +292 -292
- package/lib/parsers/safe-toml.js +784 -784
- package/lib/parsers/safe-xml.js +390 -390
- package/lib/parsers/safe-yaml.js +1015 -1015
- package/lib/permissions.js +708 -708
- package/lib/pqc-agent.js +87 -87
- package/lib/pqc-gate.js +279 -279
- package/lib/protobuf-encoder.js +190 -190
- package/lib/protocol-dispatcher.js +161 -161
- package/lib/pubsub-redis.js +167 -167
- package/lib/pubsub.js +429 -429
- package/lib/queue-local.js +476 -476
- package/lib/queue-redis.js +745 -745
- package/lib/queue-sqs.js +319 -319
- package/lib/queue.js +695 -695
- package/lib/redis-client.js +519 -519
- package/lib/request-helpers.js +340 -340
- package/lib/restore-bundle.js +237 -237
- package/lib/restore-rollback.js +259 -259
- package/lib/restore.js +409 -409
- package/lib/retry.js +376 -376
- package/lib/router.js +748 -748
- package/lib/safe-async.js +735 -735
- package/lib/safe-buffer.js +237 -237
- package/lib/safe-json.js +541 -541
- package/lib/safe-schema.js +1266 -1266
- package/lib/safe-url.js +159 -159
- package/lib/scheduler.js +706 -706
- package/lib/security-assert.js +373 -373
- package/lib/seeders.js +618 -618
- package/lib/session.js +478 -478
- package/lib/slug.js +269 -269
- package/lib/ssrf-guard.js +401 -401
- package/lib/static.js +184 -4
- package/lib/storage.js +471 -471
- package/lib/subject.js +281 -281
- package/lib/template.js +791 -791
- package/lib/testing.js +798 -798
- package/lib/time.js +310 -310
- package/lib/totp.js +302 -302
- package/lib/tracing.js +494 -494
- package/lib/uuid.js +132 -132
- package/lib/validate-opts.js +340 -319
- package/lib/vault/index.js +308 -308
- package/lib/vault/rotate.js +784 -784
- package/lib/vault/wrap.js +296 -296
- package/lib/vendor/noble-ciphers.cjs +9 -9
- package/lib/webhook.js +595 -595
- package/lib/websocket.js +1048 -1048
- package/package.json +77 -77
- package/sbom.cyclonedx.json +7 -7
package/lib/tracing.js
CHANGED
|
@@ -1,494 +1,494 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* tracing — OpenTelemetry seam without an OTel runtime dependency.
|
|
4
|
-
*
|
|
5
|
-
* The framework doesn't bundle the OTel SDK — operators install
|
|
6
|
-
* `@opentelemetry/api` (and an exporter) themselves when they want
|
|
7
|
-
* tracing. This module:
|
|
8
|
-
*
|
|
9
|
-
* - Detects if @opentelemetry/api is installed (try/catch require,
|
|
10
|
-
* cached). When it's there, every framework span call routes into
|
|
11
|
-
* OTel's real tracer and shows up in the operator's exporter
|
|
12
|
-
* (Jaeger, Zipkin, OTLP, console, whatever they wired).
|
|
13
|
-
* - When OTel ISN'T installed, every call is a pass-through. The
|
|
14
|
-
* wrapped function still executes, return values still propagate,
|
|
15
|
-
* thrown errors still escape — but no span is created and no
|
|
16
|
-
* overhead is paid beyond one cached lookup.
|
|
17
|
-
*
|
|
18
|
-
* Public API:
|
|
19
|
-
*
|
|
20
|
-
* var t = b.tracing.create({
|
|
21
|
-
* instrumentationName: "blamejs",
|
|
22
|
-
* instrumentationVersion: "1.0.0",
|
|
23
|
-
* });
|
|
24
|
-
*
|
|
25
|
-
* // Wrap async work in a span. Returns whatever fn returns.
|
|
26
|
-
* var result = await t.span("my-op", async function (span) {
|
|
27
|
-
* span.setAttribute("user_id", "abc");
|
|
28
|
-
* span.addEvent("cache-miss");
|
|
29
|
-
* return await doWork();
|
|
30
|
-
* }, { kind: "internal", attributes: { route: "/users" } });
|
|
31
|
-
*
|
|
32
|
-
* // Sync variant.
|
|
33
|
-
* var x = t.spanSync("compute", function (span) { ... return v; });
|
|
34
|
-
*
|
|
35
|
-
* // Read / write the current active span.
|
|
36
|
-
* t.currentSpan(); // null when no active span
|
|
37
|
-
* t.setAttributes({ user_id: "abc" }); // sets on current
|
|
38
|
-
* t.recordException(err); // records on current
|
|
39
|
-
*
|
|
40
|
-
* // HTTP propagation. contextHeaders() returns headers to add to
|
|
41
|
-
* // outbound requests (W3C `traceparent`); extractContext(headers)
|
|
42
|
-
* // pulls a parent context from inbound headers.
|
|
43
|
-
* var headers = t.contextHeaders();
|
|
44
|
-
* var parentCtx = t.extractContext(req.headers);
|
|
45
|
-
*
|
|
46
|
-
* // Auto-span request middleware — wraps each handler in a span
|
|
47
|
-
* // named after method + route pattern.
|
|
48
|
-
* router.use(t.requestMiddleware());
|
|
49
|
-
*
|
|
50
|
-
* // Framework-internal hot-path tap — wraps fn in a span named
|
|
51
|
-
* // `name` if a registry is active; pass-through otherwise. Like
|
|
52
|
-
* // metrics.tap() but for tracing instead of counting.
|
|
53
|
-
* b.tracing.tap("audit.record", attributes, fn);
|
|
54
|
-
*
|
|
55
|
-
* Even WITHOUT @opentelemetry/api installed:
|
|
56
|
-
* - contextHeaders() / extractContext() still parse and emit the
|
|
57
|
-
* W3C traceparent format. So a framework process without OTel
|
|
58
|
-
* can still propagate trace IDs through logs and HTTP for
|
|
59
|
-
* correlation, even without span telemetry. Operators get the
|
|
60
|
-
* "trace ID per request" plumbing as a free baseline.
|
|
61
|
-
* - currentSpan() returns a minimal pass-through "span" object so
|
|
62
|
-
* operator code can call setAttribute / addEvent / recordException
|
|
63
|
-
* unconditionally — they're no-ops without OTel.
|
|
64
|
-
*
|
|
65
|
-
* Why no @otel runtime dep:
|
|
66
|
-
* - The framework keeps zero npm runtime deps. Apps that want tracing
|
|
67
|
-
* install OTel themselves; apps that don't pay nothing.
|
|
68
|
-
* - The OTel API is unstable enough that pinning a vendored version
|
|
69
|
-
* would create more churn than it saves.
|
|
70
|
-
*
|
|
71
|
-
* Out of scope (with structural reasons):
|
|
72
|
-
* - Vendoring the SDK: see above.
|
|
73
|
-
* - Sampling decisions: OTel handles this when wired; without OTel
|
|
74
|
-
* there's nothing to sample.
|
|
75
|
-
* - Exporter integration: belongs to the OTel SDK, not the framework.
|
|
76
|
-
* - Custom propagators: framework ships W3C traceparent only. OTel
|
|
77
|
-
* adds others (b3, jaeger) when wired by the operator.
|
|
78
|
-
* - Async-context propagation across setTimeout / setImmediate:
|
|
79
|
-
* OTel's NodeSDK auto-instrumentation handles this when installed;
|
|
80
|
-
* without OTel, framework code uses fn-passing rather than
|
|
81
|
-
* async-context, which is fine for the surfaces we instrument.
|
|
82
|
-
*/
|
|
83
|
-
|
|
84
|
-
var C = require("./constants");
|
|
85
|
-
var crypto = require("./crypto");
|
|
86
|
-
var validateOpts = require("./validate-opts");
|
|
87
|
-
var { defineClass } = require("./framework-error");
|
|
88
|
-
var { resolveRoute, captureResponseStatus } = require("./request-helpers");
|
|
89
|
-
|
|
90
|
-
// W3C trace-context fixed widths (RFC TraceContext §3.2.2.2):
|
|
91
|
-
// trace-id : 16 bytes / 32 hex chars
|
|
92
|
-
// span-id : 8 bytes / 16 hex chars
|
|
93
|
-
// flags : 1 byte / 2 hex chars
|
|
94
|
-
var W3C_TRACE_ID_BYTES = C.BYTES.bytes(16);
|
|
95
|
-
var W3C_SPAN_ID_BYTES = C.BYTES.bytes(8);
|
|
96
|
-
var HEX_RADIX = 0x10;
|
|
97
|
-
|
|
98
|
-
var TracingError = defineClass("TracingError", { alwaysPermanent: true });
|
|
99
|
-
|
|
100
|
-
// ---- OTel API detection (cached) ----
|
|
101
|
-
//
|
|
102
|
-
// Tri-state: undefined=not yet checked, null=not available, object=available.
|
|
103
|
-
// We cache the lookup so the per-call cost is one === comparison after
|
|
104
|
-
// the first call.
|
|
105
|
-
|
|
106
|
-
var _otel = undefined;
|
|
107
|
-
|
|
108
|
-
function _getOtel() {
|
|
109
|
-
if (_otel !== undefined) return _otel;
|
|
110
|
-
try { _otel = require("@opentelemetry/api"); }
|
|
111
|
-
catch (_e) { _otel = null; }
|
|
112
|
-
return _otel;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
// Test seam — letting tests force the OTel resolution. Pass `null` to
|
|
116
|
-
// simulate "not installed" or an object to simulate a vendored OTel API.
|
|
117
|
-
function _setOtelForTest(value) { _otel = value === undefined ? undefined : value; }
|
|
118
|
-
|
|
119
|
-
// ---- W3C traceparent parsing ----
|
|
120
|
-
//
|
|
121
|
-
// Format: 00-<32 hex trace-id>-<16 hex span-id>-<2 hex flags>
|
|
122
|
-
// Example: 00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01
|
|
123
|
-
//
|
|
124
|
-
// We accept only version 00 (the only released W3C version). Future
|
|
125
|
-
// versions are technically forward-compatible but would extend the
|
|
126
|
-
// suffix; we ignore unrecognized versions to be safe.
|
|
127
|
-
|
|
128
|
-
var TRACEPARENT_RE = /^00-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})$/;
|
|
129
|
-
// Special-case all-zeros: invalid per spec but malicious clients send them.
|
|
130
|
-
// (32 = W3C_TRACE_ID_BYTES * 2 hex chars; 16 = W3C_SPAN_ID_BYTES * 2.)
|
|
131
|
-
var ZERO_TRACE_ID = "0".repeat(W3C_TRACE_ID_BYTES * 2);
|
|
132
|
-
var ZERO_SPAN_ID = "0".repeat(W3C_SPAN_ID_BYTES * 2);
|
|
133
|
-
|
|
134
|
-
function _parseTraceparent(value) {
|
|
135
|
-
if (typeof value !== "string") return null;
|
|
136
|
-
var m = value.match(TRACEPARENT_RE);
|
|
137
|
-
if (!m) return null;
|
|
138
|
-
if (m[1] === ZERO_TRACE_ID || m[2] === ZERO_SPAN_ID) return null;
|
|
139
|
-
return { traceId: m[1], spanId: m[2], flags: m[3] };
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
function _formatTraceparent(traceId, spanId, flags) {
|
|
143
|
-
return "00-" + traceId + "-" + spanId + "-" + (flags || "01");
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
function _newTraceId() {
|
|
147
|
-
return crypto.generateToken(W3C_TRACE_ID_BYTES);
|
|
148
|
-
}
|
|
149
|
-
function _newSpanId() {
|
|
150
|
-
return crypto.generateToken(W3C_SPAN_ID_BYTES);
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// ---- Pass-through span (used when OTel isn't installed) ----
|
|
154
|
-
//
|
|
155
|
-
// Operator code does span.setAttribute() / span.addEvent() /
|
|
156
|
-
// span.recordException() unconditionally. When OTel is absent these
|
|
157
|
-
// calls go to a minimal stub that no-ops but keeps the API shape
|
|
158
|
-
// compatible. Trace ID is still tracked so log correlation works.
|
|
159
|
-
|
|
160
|
-
function _passthroughSpan(traceId, spanId, parentSpanId) {
|
|
161
|
-
return {
|
|
162
|
-
spanContext: function () {
|
|
163
|
-
return { traceId: traceId, spanId: spanId, traceFlags: 1, isRemote: false };
|
|
164
|
-
},
|
|
165
|
-
setAttribute: function () { return this; },
|
|
166
|
-
setAttributes: function () { return this; },
|
|
167
|
-
addEvent: function () { return this; },
|
|
168
|
-
recordException: function () { return this; },
|
|
169
|
-
setStatus: function () { return this; },
|
|
170
|
-
updateName: function () { return this; },
|
|
171
|
-
end: function () { },
|
|
172
|
-
// Internal — used by the registry to thread parent context.
|
|
173
|
-
_isPassthrough: true,
|
|
174
|
-
_parentSpanId: parentSpanId,
|
|
175
|
-
};
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
// ---- Pass-through tracer (used when OTel isn't installed) ----
|
|
179
|
-
//
|
|
180
|
-
// Implements just enough of the OTel Tracer interface to drive the
|
|
181
|
-
// framework's span() / spanSync() helpers. Real OTel takes over when
|
|
182
|
-
// installed; the wrapper code below doesn't change.
|
|
183
|
-
|
|
184
|
-
function _passthroughTracer() {
|
|
185
|
-
// Active-span stack. Single-threaded JS lets us use a process-level
|
|
186
|
-
// stack rather than async-context. Pure synchronous span lifetimes
|
|
187
|
-
// are correct; async work that spans suspension points won't have
|
|
188
|
-
// currentSpan() right inside continuations — but the operator's fn
|
|
189
|
-
// still receives the span as its first arg, so attribute/event
|
|
190
|
-
// setting via that reference always works.
|
|
191
|
-
var stack = [];
|
|
192
|
-
return {
|
|
193
|
-
startSpan: function (_name, _opts) {
|
|
194
|
-
var traceId = stack.length > 0 ? stack[stack.length - 1].spanContext().traceId : _newTraceId();
|
|
195
|
-
var span = _passthroughSpan(traceId, _newSpanId(),
|
|
196
|
-
stack.length > 0 ? stack[stack.length - 1].spanContext().spanId : null);
|
|
197
|
-
return span;
|
|
198
|
-
},
|
|
199
|
-
_push: function (span) { stack.push(span); },
|
|
200
|
-
_pop: function () { return stack.pop(); },
|
|
201
|
-
_peek: function () { return stack.length > 0 ? stack[stack.length - 1] : null; },
|
|
202
|
-
_isPassthrough: true,
|
|
203
|
-
};
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
// ---- Registry factory ----
|
|
207
|
-
|
|
208
|
-
function create(opts) {
|
|
209
|
-
opts = opts || {};
|
|
210
|
-
validateOpts(opts, [
|
|
211
|
-
"instrumentationName", "instrumentationVersion",
|
|
212
|
-
], "b.tracing");
|
|
213
|
-
var instrumentationName = opts.instrumentationName || "blamejs";
|
|
214
|
-
var instrumentationVersion = opts.instrumentationVersion || "0.0.0";
|
|
215
|
-
|
|
216
|
-
// Resolve the tracer once. If @opentelemetry/api is installed, this
|
|
217
|
-
// is a real Tracer; otherwise a passthrough. Cached per-registry so
|
|
218
|
-
// the passthrough stack persists across span() and currentSpan() calls
|
|
219
|
-
// (otherwise each call would see a fresh empty stack).
|
|
220
|
-
var _cachedPassthrough = null;
|
|
221
|
-
function _tracer() {
|
|
222
|
-
var otel = _getOtel();
|
|
223
|
-
if (otel) return otel.trace.getTracer(instrumentationName, instrumentationVersion);
|
|
224
|
-
if (!_cachedPassthrough) _cachedPassthrough = _passthroughTracer();
|
|
225
|
-
return _cachedPassthrough;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
function _isReal() { return _getOtel() !== null; }
|
|
229
|
-
|
|
230
|
-
// Wrap fn in a span. The span is automatically end()'d on return /
|
|
231
|
-
// throw / promise rejection. exception is recorded on throw and
|
|
232
|
-
// span status set to ERROR.
|
|
233
|
-
function span(name, fn, sopts) {
|
|
234
|
-
if (typeof name !== "string" || name.length === 0) {
|
|
235
|
-
throw new TracingError("tracing/bad-name",
|
|
236
|
-
"span name must be a non-empty string");
|
|
237
|
-
}
|
|
238
|
-
if (typeof fn !== "function") {
|
|
239
|
-
throw new TracingError("tracing/bad-fn",
|
|
240
|
-
"span body must be a function");
|
|
241
|
-
}
|
|
242
|
-
sopts = sopts || {};
|
|
243
|
-
var tracer = _tracer();
|
|
244
|
-
var otel = _getOtel();
|
|
245
|
-
|
|
246
|
-
// Real OTel path — use context.with so async-context propagation
|
|
247
|
-
// works automatically once the SDK is wired.
|
|
248
|
-
if (otel) {
|
|
249
|
-
var spanInst = tracer.startSpan(name, {
|
|
250
|
-
kind: _kindFromString(otel, sopts.kind),
|
|
251
|
-
attributes: sopts.attributes,
|
|
252
|
-
});
|
|
253
|
-
// Run fn inside the span's context so child spans nest.
|
|
254
|
-
var ctx = otel.trace.setSpan(otel.context.active(), spanInst);
|
|
255
|
-
return otel.context.with(ctx, function () {
|
|
256
|
-
try {
|
|
257
|
-
var ret = fn(spanInst);
|
|
258
|
-
if (ret && typeof ret.then === "function") {
|
|
259
|
-
return ret.then(
|
|
260
|
-
function (v) { spanInst.end(); return v; },
|
|
261
|
-
function (e) {
|
|
262
|
-
spanInst.recordException(e);
|
|
263
|
-
spanInst.setStatus({ code: 2, message: (e && e.message) || String(e) });
|
|
264
|
-
spanInst.end();
|
|
265
|
-
throw e;
|
|
266
|
-
}
|
|
267
|
-
);
|
|
268
|
-
}
|
|
269
|
-
spanInst.end();
|
|
270
|
-
return ret;
|
|
271
|
-
} catch (e) {
|
|
272
|
-
spanInst.recordException(e);
|
|
273
|
-
spanInst.setStatus({ code: 2, message: (e && e.message) || String(e) });
|
|
274
|
-
spanInst.end();
|
|
275
|
-
throw e;
|
|
276
|
-
}
|
|
277
|
-
});
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
// Pass-through path — still wrap so operator code that calls
|
|
281
|
-
// span.setAttribute / addEvent / recordException works, just
|
|
282
|
-
// without telemetry output.
|
|
283
|
-
var pSpan = tracer.startSpan(name, sopts);
|
|
284
|
-
if (sopts.attributes) pSpan.setAttributes(sopts.attributes);
|
|
285
|
-
tracer._push(pSpan);
|
|
286
|
-
var done = false;
|
|
287
|
-
function _finish() { if (!done) { done = true; tracer._pop(); pSpan.end(); } }
|
|
288
|
-
try {
|
|
289
|
-
var res = fn(pSpan);
|
|
290
|
-
if (res && typeof res.then === "function") {
|
|
291
|
-
return res.then(
|
|
292
|
-
function (v) { _finish(); return v; },
|
|
293
|
-
function (e) { _finish(); throw e; }
|
|
294
|
-
);
|
|
295
|
-
}
|
|
296
|
-
_finish();
|
|
297
|
-
return res;
|
|
298
|
-
} catch (e) {
|
|
299
|
-
_finish();
|
|
300
|
-
throw e;
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
function spanSync(name, fn, sopts) {
|
|
305
|
-
if (typeof fn !== "function") {
|
|
306
|
-
throw new TracingError("tracing/bad-fn", "spanSync body must be a function");
|
|
307
|
-
}
|
|
308
|
-
// Same shape as span(); fn must return a non-Promise. We don't
|
|
309
|
-
// enforce that, but we don't await either — async fn used here
|
|
310
|
-
// ends the span before resolution.
|
|
311
|
-
return span(name, fn, sopts);
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
function currentSpan() {
|
|
315
|
-
var otel = _getOtel();
|
|
316
|
-
if (otel) {
|
|
317
|
-
var s = otel.trace.getActiveSpan();
|
|
318
|
-
return s || null;
|
|
319
|
-
}
|
|
320
|
-
var t = _tracer();
|
|
321
|
-
return t._peek();
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
function setAttributes(attrs) {
|
|
325
|
-
var s = currentSpan();
|
|
326
|
-
if (s) s.setAttributes(attrs);
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
function recordException(err) {
|
|
330
|
-
var s = currentSpan();
|
|
331
|
-
if (s) {
|
|
332
|
-
s.recordException(err);
|
|
333
|
-
s.setStatus({ code: 2, message: (err && err.message) || String(err) });
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
// ---- W3C propagation (works with or without OTel) ----
|
|
338
|
-
|
|
339
|
-
function contextHeaders() {
|
|
340
|
-
var s = currentSpan();
|
|
341
|
-
if (!s) return {};
|
|
342
|
-
var sc = s.spanContext ? s.spanContext() : null;
|
|
343
|
-
if (!sc || !sc.traceId || sc.traceId === ZERO_TRACE_ID) return {};
|
|
344
|
-
return {
|
|
345
|
-
traceparent: _formatTraceparent(sc.traceId, sc.spanId,
|
|
346
|
-
(sc.traceFlags === undefined ? 1 : sc.traceFlags).toString(HEX_RADIX).padStart(2, "0")),
|
|
347
|
-
};
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
// Parse traceparent from incoming headers. With OTel installed we
|
|
351
|
-
// use its propagation API for correctness across propagator types
|
|
352
|
-
// an operator might have configured; without OTel we fall back to
|
|
353
|
-
// our own parser which always understands W3C.
|
|
354
|
-
function extractContext(headers) {
|
|
355
|
-
if (!headers || typeof headers !== "object") return null;
|
|
356
|
-
var raw = headers.traceparent || headers["Traceparent"] || headers["TRACEPARENT"];
|
|
357
|
-
var parsed = _parseTraceparent(raw);
|
|
358
|
-
if (!parsed) return null;
|
|
359
|
-
return parsed;
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
// ---- request middleware ----
|
|
363
|
-
|
|
364
|
-
function requestMiddleware() {
|
|
365
|
-
return function tracingMiddleware(req, res, next) {
|
|
366
|
-
// Span starts BEFORE the router populates req.routePattern, so
|
|
367
|
-
// initial name+http.route come from the URL fallback. We promote
|
|
368
|
-
// both to the template form at res.end if the matcher set one.
|
|
369
|
-
var initialRoute = resolveRoute(req);
|
|
370
|
-
var spanName = "HTTP " + (req.method || "GET") + " " + initialRoute;
|
|
371
|
-
var attrs = {
|
|
372
|
-
"http.method": req.method || "GET",
|
|
373
|
-
"http.route": initialRoute,
|
|
374
|
-
"http.url": req.url || "",
|
|
375
|
-
};
|
|
376
|
-
var parent = extractContext(req.headers);
|
|
377
|
-
if (parent) {
|
|
378
|
-
attrs["traceparent.parent"] = parent.traceId + "-" + parent.spanId;
|
|
379
|
-
}
|
|
380
|
-
span(spanName, function (s) {
|
|
381
|
-
req.span = s;
|
|
382
|
-
captureResponseStatus(res, function (status) {
|
|
383
|
-
try {
|
|
384
|
-
s.setAttribute("http.status_code", status);
|
|
385
|
-
// Promote to route template if the router resolved one.
|
|
386
|
-
var finalRoute = resolveRoute(req);
|
|
387
|
-
if (finalRoute !== initialRoute) {
|
|
388
|
-
s.setAttribute("http.route", finalRoute);
|
|
389
|
-
if (typeof s.updateName === "function") {
|
|
390
|
-
s.updateName("HTTP " + (req.method || "GET") + " " + finalRoute);
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
} catch (_e) { /* span attr write must not break the response */ }
|
|
394
|
-
});
|
|
395
|
-
return next();
|
|
396
|
-
}, { attributes: attrs }).catch(function () {
|
|
397
|
-
// Span error already recorded by span() wrapper; the next()
|
|
398
|
-
// chain handles request-level error handling separately.
|
|
399
|
-
});
|
|
400
|
-
};
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
// ---- framework auto-tap ----
|
|
404
|
-
//
|
|
405
|
-
// Like metrics.tap, the tracing tap routes framework hot-path calls
|
|
406
|
-
// into spans when this registry is the active one.
|
|
407
|
-
|
|
408
|
-
var _activeOnTap = null;
|
|
409
|
-
|
|
410
|
-
function tap(name, attributes, fn) {
|
|
411
|
-
if (typeof attributes === "function") {
|
|
412
|
-
fn = attributes; attributes = null;
|
|
413
|
-
}
|
|
414
|
-
if (typeof fn !== "function") {
|
|
415
|
-
throw new TracingError("tracing/bad-fn", "tap fn must be a function");
|
|
416
|
-
}
|
|
417
|
-
if (_activeOnTap !== this && registry._isActive !== true) {
|
|
418
|
-
// No active registry — execute fn directly. Caller never knows.
|
|
419
|
-
return fn(currentSpan());
|
|
420
|
-
}
|
|
421
|
-
return span(name, fn, { attributes: attributes });
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
// OTel SpanKind enum values (from @opentelemetry/api). When OTel
|
|
425
|
-
// isn't installed, kind is just an attribute string.
|
|
426
|
-
function _kindFromString(otel, kindStr) {
|
|
427
|
-
if (!otel || !otel.SpanKind) return undefined;
|
|
428
|
-
if (kindStr === "server") return otel.SpanKind.SERVER;
|
|
429
|
-
if (kindStr === "client") return otel.SpanKind.CLIENT;
|
|
430
|
-
if (kindStr === "producer") return otel.SpanKind.PRODUCER;
|
|
431
|
-
if (kindStr === "consumer") return otel.SpanKind.CONSUMER;
|
|
432
|
-
return otel.SpanKind.INTERNAL;
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
var registry = {
|
|
436
|
-
span: span,
|
|
437
|
-
spanSync: spanSync,
|
|
438
|
-
currentSpan: currentSpan,
|
|
439
|
-
setAttributes: setAttributes,
|
|
440
|
-
recordException: recordException,
|
|
441
|
-
contextHeaders: contextHeaders,
|
|
442
|
-
extractContext: extractContext,
|
|
443
|
-
requestMiddleware: requestMiddleware,
|
|
444
|
-
tap: tap,
|
|
445
|
-
isReal: _isReal,
|
|
446
|
-
_isActive: true,
|
|
447
|
-
deactivate: function () {
|
|
448
|
-
registry._isActive = false;
|
|
449
|
-
if (_globalRegistry === registry) _globalRegistry = null;
|
|
450
|
-
},
|
|
451
|
-
};
|
|
452
|
-
_globalRegistry = registry;
|
|
453
|
-
return registry;
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
// ---- Global tap stub for framework modules ----
|
|
457
|
-
//
|
|
458
|
-
// Same pattern as metrics.tap: framework modules call b.tracing.tap()
|
|
459
|
-
// at hot paths. Without an active registry the call is a pass-through
|
|
460
|
-
// that just executes fn. With one, it routes into span().
|
|
461
|
-
|
|
462
|
-
var _globalRegistry = null;
|
|
463
|
-
|
|
464
|
-
function tap(name, attributes, fn) {
|
|
465
|
-
if (typeof attributes === "function") {
|
|
466
|
-
fn = attributes; attributes = null;
|
|
467
|
-
}
|
|
468
|
-
if (typeof fn !== "function") {
|
|
469
|
-
throw new TracingError("tracing/bad-fn", "tap fn must be a function");
|
|
470
|
-
}
|
|
471
|
-
if (_globalRegistry === null || _globalRegistry._isActive !== true) {
|
|
472
|
-
return fn(null);
|
|
473
|
-
}
|
|
474
|
-
return _globalRegistry.span(name, fn, { attributes: attributes });
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
function _resetForTest() {
|
|
478
|
-
_globalRegistry = null;
|
|
479
|
-
_setOtelForTest(undefined);
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
module.exports = {
|
|
483
|
-
create: create,
|
|
484
|
-
tap: tap,
|
|
485
|
-
TracingError: TracingError,
|
|
486
|
-
_setOtelForTest: _setOtelForTest,
|
|
487
|
-
_resetForTest: _resetForTest,
|
|
488
|
-
// Internal helpers exposed for tests
|
|
489
|
-
_parseTraceparent: _parseTraceparent,
|
|
490
|
-
_formatTraceparent: _formatTraceparent,
|
|
491
|
-
_newTraceId: _newTraceId,
|
|
492
|
-
_newSpanId: _newSpanId,
|
|
493
|
-
TRACEPARENT_RE: TRACEPARENT_RE,
|
|
494
|
-
};
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* tracing — OpenTelemetry seam without an OTel runtime dependency.
|
|
4
|
+
*
|
|
5
|
+
* The framework doesn't bundle the OTel SDK — operators install
|
|
6
|
+
* `@opentelemetry/api` (and an exporter) themselves when they want
|
|
7
|
+
* tracing. This module:
|
|
8
|
+
*
|
|
9
|
+
* - Detects if @opentelemetry/api is installed (try/catch require,
|
|
10
|
+
* cached). When it's there, every framework span call routes into
|
|
11
|
+
* OTel's real tracer and shows up in the operator's exporter
|
|
12
|
+
* (Jaeger, Zipkin, OTLP, console, whatever they wired).
|
|
13
|
+
* - When OTel ISN'T installed, every call is a pass-through. The
|
|
14
|
+
* wrapped function still executes, return values still propagate,
|
|
15
|
+
* thrown errors still escape — but no span is created and no
|
|
16
|
+
* overhead is paid beyond one cached lookup.
|
|
17
|
+
*
|
|
18
|
+
* Public API:
|
|
19
|
+
*
|
|
20
|
+
* var t = b.tracing.create({
|
|
21
|
+
* instrumentationName: "blamejs",
|
|
22
|
+
* instrumentationVersion: "1.0.0",
|
|
23
|
+
* });
|
|
24
|
+
*
|
|
25
|
+
* // Wrap async work in a span. Returns whatever fn returns.
|
|
26
|
+
* var result = await t.span("my-op", async function (span) {
|
|
27
|
+
* span.setAttribute("user_id", "abc");
|
|
28
|
+
* span.addEvent("cache-miss");
|
|
29
|
+
* return await doWork();
|
|
30
|
+
* }, { kind: "internal", attributes: { route: "/users" } });
|
|
31
|
+
*
|
|
32
|
+
* // Sync variant.
|
|
33
|
+
* var x = t.spanSync("compute", function (span) { ... return v; });
|
|
34
|
+
*
|
|
35
|
+
* // Read / write the current active span.
|
|
36
|
+
* t.currentSpan(); // null when no active span
|
|
37
|
+
* t.setAttributes({ user_id: "abc" }); // sets on current
|
|
38
|
+
* t.recordException(err); // records on current
|
|
39
|
+
*
|
|
40
|
+
* // HTTP propagation. contextHeaders() returns headers to add to
|
|
41
|
+
* // outbound requests (W3C `traceparent`); extractContext(headers)
|
|
42
|
+
* // pulls a parent context from inbound headers.
|
|
43
|
+
* var headers = t.contextHeaders();
|
|
44
|
+
* var parentCtx = t.extractContext(req.headers);
|
|
45
|
+
*
|
|
46
|
+
* // Auto-span request middleware — wraps each handler in a span
|
|
47
|
+
* // named after method + route pattern.
|
|
48
|
+
* router.use(t.requestMiddleware());
|
|
49
|
+
*
|
|
50
|
+
* // Framework-internal hot-path tap — wraps fn in a span named
|
|
51
|
+
* // `name` if a registry is active; pass-through otherwise. Like
|
|
52
|
+
* // metrics.tap() but for tracing instead of counting.
|
|
53
|
+
* b.tracing.tap("audit.record", attributes, fn);
|
|
54
|
+
*
|
|
55
|
+
* Even WITHOUT @opentelemetry/api installed:
|
|
56
|
+
* - contextHeaders() / extractContext() still parse and emit the
|
|
57
|
+
* W3C traceparent format. So a framework process without OTel
|
|
58
|
+
* can still propagate trace IDs through logs and HTTP for
|
|
59
|
+
* correlation, even without span telemetry. Operators get the
|
|
60
|
+
* "trace ID per request" plumbing as a free baseline.
|
|
61
|
+
* - currentSpan() returns a minimal pass-through "span" object so
|
|
62
|
+
* operator code can call setAttribute / addEvent / recordException
|
|
63
|
+
* unconditionally — they're no-ops without OTel.
|
|
64
|
+
*
|
|
65
|
+
* Why no @otel runtime dep:
|
|
66
|
+
* - The framework keeps zero npm runtime deps. Apps that want tracing
|
|
67
|
+
* install OTel themselves; apps that don't pay nothing.
|
|
68
|
+
* - The OTel API is unstable enough that pinning a vendored version
|
|
69
|
+
* would create more churn than it saves.
|
|
70
|
+
*
|
|
71
|
+
* Out of scope (with structural reasons):
|
|
72
|
+
* - Vendoring the SDK: see above.
|
|
73
|
+
* - Sampling decisions: OTel handles this when wired; without OTel
|
|
74
|
+
* there's nothing to sample.
|
|
75
|
+
* - Exporter integration: belongs to the OTel SDK, not the framework.
|
|
76
|
+
* - Custom propagators: framework ships W3C traceparent only. OTel
|
|
77
|
+
* adds others (b3, jaeger) when wired by the operator.
|
|
78
|
+
* - Async-context propagation across setTimeout / setImmediate:
|
|
79
|
+
* OTel's NodeSDK auto-instrumentation handles this when installed;
|
|
80
|
+
* without OTel, framework code uses fn-passing rather than
|
|
81
|
+
* async-context, which is fine for the surfaces we instrument.
|
|
82
|
+
*/
|
|
83
|
+
|
|
84
|
+
var C = require("./constants");
|
|
85
|
+
var crypto = require("./crypto");
|
|
86
|
+
var validateOpts = require("./validate-opts");
|
|
87
|
+
var { defineClass } = require("./framework-error");
|
|
88
|
+
var { resolveRoute, captureResponseStatus } = require("./request-helpers");
|
|
89
|
+
|
|
90
|
+
// W3C trace-context fixed widths (RFC TraceContext §3.2.2.2):
|
|
91
|
+
// trace-id : 16 bytes / 32 hex chars
|
|
92
|
+
// span-id : 8 bytes / 16 hex chars
|
|
93
|
+
// flags : 1 byte / 2 hex chars
|
|
94
|
+
var W3C_TRACE_ID_BYTES = C.BYTES.bytes(16);
|
|
95
|
+
var W3C_SPAN_ID_BYTES = C.BYTES.bytes(8);
|
|
96
|
+
var HEX_RADIX = 0x10;
|
|
97
|
+
|
|
98
|
+
var TracingError = defineClass("TracingError", { alwaysPermanent: true });
|
|
99
|
+
|
|
100
|
+
// ---- OTel API detection (cached) ----
|
|
101
|
+
//
|
|
102
|
+
// Tri-state: undefined=not yet checked, null=not available, object=available.
|
|
103
|
+
// We cache the lookup so the per-call cost is one === comparison after
|
|
104
|
+
// the first call.
|
|
105
|
+
|
|
106
|
+
var _otel = undefined;
|
|
107
|
+
|
|
108
|
+
function _getOtel() {
|
|
109
|
+
if (_otel !== undefined) return _otel;
|
|
110
|
+
try { _otel = require("@opentelemetry/api"); }
|
|
111
|
+
catch (_e) { _otel = null; }
|
|
112
|
+
return _otel;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Test seam — letting tests force the OTel resolution. Pass `null` to
|
|
116
|
+
// simulate "not installed" or an object to simulate a vendored OTel API.
|
|
117
|
+
function _setOtelForTest(value) { _otel = value === undefined ? undefined : value; }
|
|
118
|
+
|
|
119
|
+
// ---- W3C traceparent parsing ----
|
|
120
|
+
//
|
|
121
|
+
// Format: 00-<32 hex trace-id>-<16 hex span-id>-<2 hex flags>
|
|
122
|
+
// Example: 00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01
|
|
123
|
+
//
|
|
124
|
+
// We accept only version 00 (the only released W3C version). Future
|
|
125
|
+
// versions are technically forward-compatible but would extend the
|
|
126
|
+
// suffix; we ignore unrecognized versions to be safe.
|
|
127
|
+
|
|
128
|
+
var TRACEPARENT_RE = /^00-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})$/;
|
|
129
|
+
// Special-case all-zeros: invalid per spec but malicious clients send them.
|
|
130
|
+
// (32 = W3C_TRACE_ID_BYTES * 2 hex chars; 16 = W3C_SPAN_ID_BYTES * 2.)
|
|
131
|
+
var ZERO_TRACE_ID = "0".repeat(W3C_TRACE_ID_BYTES * 2);
|
|
132
|
+
var ZERO_SPAN_ID = "0".repeat(W3C_SPAN_ID_BYTES * 2);
|
|
133
|
+
|
|
134
|
+
function _parseTraceparent(value) {
|
|
135
|
+
if (typeof value !== "string") return null;
|
|
136
|
+
var m = value.match(TRACEPARENT_RE);
|
|
137
|
+
if (!m) return null;
|
|
138
|
+
if (m[1] === ZERO_TRACE_ID || m[2] === ZERO_SPAN_ID) return null;
|
|
139
|
+
return { traceId: m[1], spanId: m[2], flags: m[3] };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function _formatTraceparent(traceId, spanId, flags) {
|
|
143
|
+
return "00-" + traceId + "-" + spanId + "-" + (flags || "01");
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function _newTraceId() {
|
|
147
|
+
return crypto.generateToken(W3C_TRACE_ID_BYTES);
|
|
148
|
+
}
|
|
149
|
+
function _newSpanId() {
|
|
150
|
+
return crypto.generateToken(W3C_SPAN_ID_BYTES);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ---- Pass-through span (used when OTel isn't installed) ----
|
|
154
|
+
//
|
|
155
|
+
// Operator code does span.setAttribute() / span.addEvent() /
|
|
156
|
+
// span.recordException() unconditionally. When OTel is absent these
|
|
157
|
+
// calls go to a minimal stub that no-ops but keeps the API shape
|
|
158
|
+
// compatible. Trace ID is still tracked so log correlation works.
|
|
159
|
+
|
|
160
|
+
function _passthroughSpan(traceId, spanId, parentSpanId) {
|
|
161
|
+
return {
|
|
162
|
+
spanContext: function () {
|
|
163
|
+
return { traceId: traceId, spanId: spanId, traceFlags: 1, isRemote: false };
|
|
164
|
+
},
|
|
165
|
+
setAttribute: function () { return this; },
|
|
166
|
+
setAttributes: function () { return this; },
|
|
167
|
+
addEvent: function () { return this; },
|
|
168
|
+
recordException: function () { return this; },
|
|
169
|
+
setStatus: function () { return this; },
|
|
170
|
+
updateName: function () { return this; },
|
|
171
|
+
end: function () { },
|
|
172
|
+
// Internal — used by the registry to thread parent context.
|
|
173
|
+
_isPassthrough: true,
|
|
174
|
+
_parentSpanId: parentSpanId,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// ---- Pass-through tracer (used when OTel isn't installed) ----
|
|
179
|
+
//
|
|
180
|
+
// Implements just enough of the OTel Tracer interface to drive the
|
|
181
|
+
// framework's span() / spanSync() helpers. Real OTel takes over when
|
|
182
|
+
// installed; the wrapper code below doesn't change.
|
|
183
|
+
|
|
184
|
+
function _passthroughTracer() {
|
|
185
|
+
// Active-span stack. Single-threaded JS lets us use a process-level
|
|
186
|
+
// stack rather than async-context. Pure synchronous span lifetimes
|
|
187
|
+
// are correct; async work that spans suspension points won't have
|
|
188
|
+
// currentSpan() right inside continuations — but the operator's fn
|
|
189
|
+
// still receives the span as its first arg, so attribute/event
|
|
190
|
+
// setting via that reference always works.
|
|
191
|
+
var stack = [];
|
|
192
|
+
return {
|
|
193
|
+
startSpan: function (_name, _opts) {
|
|
194
|
+
var traceId = stack.length > 0 ? stack[stack.length - 1].spanContext().traceId : _newTraceId();
|
|
195
|
+
var span = _passthroughSpan(traceId, _newSpanId(),
|
|
196
|
+
stack.length > 0 ? stack[stack.length - 1].spanContext().spanId : null);
|
|
197
|
+
return span;
|
|
198
|
+
},
|
|
199
|
+
_push: function (span) { stack.push(span); },
|
|
200
|
+
_pop: function () { return stack.pop(); },
|
|
201
|
+
_peek: function () { return stack.length > 0 ? stack[stack.length - 1] : null; },
|
|
202
|
+
_isPassthrough: true,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// ---- Registry factory ----
|
|
207
|
+
|
|
208
|
+
function create(opts) {
|
|
209
|
+
opts = opts || {};
|
|
210
|
+
validateOpts(opts, [
|
|
211
|
+
"instrumentationName", "instrumentationVersion",
|
|
212
|
+
], "b.tracing");
|
|
213
|
+
var instrumentationName = opts.instrumentationName || "blamejs";
|
|
214
|
+
var instrumentationVersion = opts.instrumentationVersion || "0.0.0";
|
|
215
|
+
|
|
216
|
+
// Resolve the tracer once. If @opentelemetry/api is installed, this
|
|
217
|
+
// is a real Tracer; otherwise a passthrough. Cached per-registry so
|
|
218
|
+
// the passthrough stack persists across span() and currentSpan() calls
|
|
219
|
+
// (otherwise each call would see a fresh empty stack).
|
|
220
|
+
var _cachedPassthrough = null;
|
|
221
|
+
function _tracer() {
|
|
222
|
+
var otel = _getOtel();
|
|
223
|
+
if (otel) return otel.trace.getTracer(instrumentationName, instrumentationVersion);
|
|
224
|
+
if (!_cachedPassthrough) _cachedPassthrough = _passthroughTracer();
|
|
225
|
+
return _cachedPassthrough;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function _isReal() { return _getOtel() !== null; }
|
|
229
|
+
|
|
230
|
+
// Wrap fn in a span. The span is automatically end()'d on return /
|
|
231
|
+
// throw / promise rejection. exception is recorded on throw and
|
|
232
|
+
// span status set to ERROR.
|
|
233
|
+
function span(name, fn, sopts) {
|
|
234
|
+
if (typeof name !== "string" || name.length === 0) {
|
|
235
|
+
throw new TracingError("tracing/bad-name",
|
|
236
|
+
"span name must be a non-empty string");
|
|
237
|
+
}
|
|
238
|
+
if (typeof fn !== "function") {
|
|
239
|
+
throw new TracingError("tracing/bad-fn",
|
|
240
|
+
"span body must be a function");
|
|
241
|
+
}
|
|
242
|
+
sopts = sopts || {};
|
|
243
|
+
var tracer = _tracer();
|
|
244
|
+
var otel = _getOtel();
|
|
245
|
+
|
|
246
|
+
// Real OTel path — use context.with so async-context propagation
|
|
247
|
+
// works automatically once the SDK is wired.
|
|
248
|
+
if (otel) {
|
|
249
|
+
var spanInst = tracer.startSpan(name, {
|
|
250
|
+
kind: _kindFromString(otel, sopts.kind),
|
|
251
|
+
attributes: sopts.attributes,
|
|
252
|
+
});
|
|
253
|
+
// Run fn inside the span's context so child spans nest.
|
|
254
|
+
var ctx = otel.trace.setSpan(otel.context.active(), spanInst);
|
|
255
|
+
return otel.context.with(ctx, function () {
|
|
256
|
+
try {
|
|
257
|
+
var ret = fn(spanInst);
|
|
258
|
+
if (ret && typeof ret.then === "function") {
|
|
259
|
+
return ret.then(
|
|
260
|
+
function (v) { spanInst.end(); return v; },
|
|
261
|
+
function (e) {
|
|
262
|
+
spanInst.recordException(e);
|
|
263
|
+
spanInst.setStatus({ code: 2, message: (e && e.message) || String(e) });
|
|
264
|
+
spanInst.end();
|
|
265
|
+
throw e;
|
|
266
|
+
}
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
spanInst.end();
|
|
270
|
+
return ret;
|
|
271
|
+
} catch (e) {
|
|
272
|
+
spanInst.recordException(e);
|
|
273
|
+
spanInst.setStatus({ code: 2, message: (e && e.message) || String(e) });
|
|
274
|
+
spanInst.end();
|
|
275
|
+
throw e;
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Pass-through path — still wrap so operator code that calls
|
|
281
|
+
// span.setAttribute / addEvent / recordException works, just
|
|
282
|
+
// without telemetry output.
|
|
283
|
+
var pSpan = tracer.startSpan(name, sopts);
|
|
284
|
+
if (sopts.attributes) pSpan.setAttributes(sopts.attributes);
|
|
285
|
+
tracer._push(pSpan);
|
|
286
|
+
var done = false;
|
|
287
|
+
function _finish() { if (!done) { done = true; tracer._pop(); pSpan.end(); } }
|
|
288
|
+
try {
|
|
289
|
+
var res = fn(pSpan);
|
|
290
|
+
if (res && typeof res.then === "function") {
|
|
291
|
+
return res.then(
|
|
292
|
+
function (v) { _finish(); return v; },
|
|
293
|
+
function (e) { _finish(); throw e; }
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
_finish();
|
|
297
|
+
return res;
|
|
298
|
+
} catch (e) {
|
|
299
|
+
_finish();
|
|
300
|
+
throw e;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function spanSync(name, fn, sopts) {
|
|
305
|
+
if (typeof fn !== "function") {
|
|
306
|
+
throw new TracingError("tracing/bad-fn", "spanSync body must be a function");
|
|
307
|
+
}
|
|
308
|
+
// Same shape as span(); fn must return a non-Promise. We don't
|
|
309
|
+
// enforce that, but we don't await either — async fn used here
|
|
310
|
+
// ends the span before resolution.
|
|
311
|
+
return span(name, fn, sopts);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function currentSpan() {
|
|
315
|
+
var otel = _getOtel();
|
|
316
|
+
if (otel) {
|
|
317
|
+
var s = otel.trace.getActiveSpan();
|
|
318
|
+
return s || null;
|
|
319
|
+
}
|
|
320
|
+
var t = _tracer();
|
|
321
|
+
return t._peek();
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function setAttributes(attrs) {
|
|
325
|
+
var s = currentSpan();
|
|
326
|
+
if (s) s.setAttributes(attrs);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function recordException(err) {
|
|
330
|
+
var s = currentSpan();
|
|
331
|
+
if (s) {
|
|
332
|
+
s.recordException(err);
|
|
333
|
+
s.setStatus({ code: 2, message: (err && err.message) || String(err) });
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// ---- W3C propagation (works with or without OTel) ----
|
|
338
|
+
|
|
339
|
+
function contextHeaders() {
|
|
340
|
+
var s = currentSpan();
|
|
341
|
+
if (!s) return {};
|
|
342
|
+
var sc = s.spanContext ? s.spanContext() : null;
|
|
343
|
+
if (!sc || !sc.traceId || sc.traceId === ZERO_TRACE_ID) return {};
|
|
344
|
+
return {
|
|
345
|
+
traceparent: _formatTraceparent(sc.traceId, sc.spanId,
|
|
346
|
+
(sc.traceFlags === undefined ? 1 : sc.traceFlags).toString(HEX_RADIX).padStart(2, "0")),
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Parse traceparent from incoming headers. With OTel installed we
|
|
351
|
+
// use its propagation API for correctness across propagator types
|
|
352
|
+
// an operator might have configured; without OTel we fall back to
|
|
353
|
+
// our own parser which always understands W3C.
|
|
354
|
+
function extractContext(headers) {
|
|
355
|
+
if (!headers || typeof headers !== "object") return null;
|
|
356
|
+
var raw = headers.traceparent || headers["Traceparent"] || headers["TRACEPARENT"];
|
|
357
|
+
var parsed = _parseTraceparent(raw);
|
|
358
|
+
if (!parsed) return null;
|
|
359
|
+
return parsed;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// ---- request middleware ----
|
|
363
|
+
|
|
364
|
+
function requestMiddleware() {
|
|
365
|
+
return function tracingMiddleware(req, res, next) {
|
|
366
|
+
// Span starts BEFORE the router populates req.routePattern, so
|
|
367
|
+
// initial name+http.route come from the URL fallback. We promote
|
|
368
|
+
// both to the template form at res.end if the matcher set one.
|
|
369
|
+
var initialRoute = resolveRoute(req);
|
|
370
|
+
var spanName = "HTTP " + (req.method || "GET") + " " + initialRoute;
|
|
371
|
+
var attrs = {
|
|
372
|
+
"http.method": req.method || "GET",
|
|
373
|
+
"http.route": initialRoute,
|
|
374
|
+
"http.url": req.url || "",
|
|
375
|
+
};
|
|
376
|
+
var parent = extractContext(req.headers);
|
|
377
|
+
if (parent) {
|
|
378
|
+
attrs["traceparent.parent"] = parent.traceId + "-" + parent.spanId;
|
|
379
|
+
}
|
|
380
|
+
span(spanName, function (s) {
|
|
381
|
+
req.span = s;
|
|
382
|
+
captureResponseStatus(res, function (status) {
|
|
383
|
+
try {
|
|
384
|
+
s.setAttribute("http.status_code", status);
|
|
385
|
+
// Promote to route template if the router resolved one.
|
|
386
|
+
var finalRoute = resolveRoute(req);
|
|
387
|
+
if (finalRoute !== initialRoute) {
|
|
388
|
+
s.setAttribute("http.route", finalRoute);
|
|
389
|
+
if (typeof s.updateName === "function") {
|
|
390
|
+
s.updateName("HTTP " + (req.method || "GET") + " " + finalRoute);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
} catch (_e) { /* span attr write must not break the response */ }
|
|
394
|
+
});
|
|
395
|
+
return next();
|
|
396
|
+
}, { attributes: attrs }).catch(function () {
|
|
397
|
+
// Span error already recorded by span() wrapper; the next()
|
|
398
|
+
// chain handles request-level error handling separately.
|
|
399
|
+
});
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// ---- framework auto-tap ----
|
|
404
|
+
//
|
|
405
|
+
// Like metrics.tap, the tracing tap routes framework hot-path calls
|
|
406
|
+
// into spans when this registry is the active one.
|
|
407
|
+
|
|
408
|
+
var _activeOnTap = null;
|
|
409
|
+
|
|
410
|
+
function tap(name, attributes, fn) {
|
|
411
|
+
if (typeof attributes === "function") {
|
|
412
|
+
fn = attributes; attributes = null;
|
|
413
|
+
}
|
|
414
|
+
if (typeof fn !== "function") {
|
|
415
|
+
throw new TracingError("tracing/bad-fn", "tap fn must be a function");
|
|
416
|
+
}
|
|
417
|
+
if (_activeOnTap !== this && registry._isActive !== true) {
|
|
418
|
+
// No active registry — execute fn directly. Caller never knows.
|
|
419
|
+
return fn(currentSpan());
|
|
420
|
+
}
|
|
421
|
+
return span(name, fn, { attributes: attributes });
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// OTel SpanKind enum values (from @opentelemetry/api). When OTel
|
|
425
|
+
// isn't installed, kind is just an attribute string.
|
|
426
|
+
function _kindFromString(otel, kindStr) {
|
|
427
|
+
if (!otel || !otel.SpanKind) return undefined;
|
|
428
|
+
if (kindStr === "server") return otel.SpanKind.SERVER;
|
|
429
|
+
if (kindStr === "client") return otel.SpanKind.CLIENT;
|
|
430
|
+
if (kindStr === "producer") return otel.SpanKind.PRODUCER;
|
|
431
|
+
if (kindStr === "consumer") return otel.SpanKind.CONSUMER;
|
|
432
|
+
return otel.SpanKind.INTERNAL;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
var registry = {
|
|
436
|
+
span: span,
|
|
437
|
+
spanSync: spanSync,
|
|
438
|
+
currentSpan: currentSpan,
|
|
439
|
+
setAttributes: setAttributes,
|
|
440
|
+
recordException: recordException,
|
|
441
|
+
contextHeaders: contextHeaders,
|
|
442
|
+
extractContext: extractContext,
|
|
443
|
+
requestMiddleware: requestMiddleware,
|
|
444
|
+
tap: tap,
|
|
445
|
+
isReal: _isReal,
|
|
446
|
+
_isActive: true,
|
|
447
|
+
deactivate: function () {
|
|
448
|
+
registry._isActive = false;
|
|
449
|
+
if (_globalRegistry === registry) _globalRegistry = null;
|
|
450
|
+
},
|
|
451
|
+
};
|
|
452
|
+
_globalRegistry = registry;
|
|
453
|
+
return registry;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// ---- Global tap stub for framework modules ----
|
|
457
|
+
//
|
|
458
|
+
// Same pattern as metrics.tap: framework modules call b.tracing.tap()
|
|
459
|
+
// at hot paths. Without an active registry the call is a pass-through
|
|
460
|
+
// that just executes fn. With one, it routes into span().
|
|
461
|
+
|
|
462
|
+
var _globalRegistry = null;
|
|
463
|
+
|
|
464
|
+
function tap(name, attributes, fn) {
|
|
465
|
+
if (typeof attributes === "function") {
|
|
466
|
+
fn = attributes; attributes = null;
|
|
467
|
+
}
|
|
468
|
+
if (typeof fn !== "function") {
|
|
469
|
+
throw new TracingError("tracing/bad-fn", "tap fn must be a function");
|
|
470
|
+
}
|
|
471
|
+
if (_globalRegistry === null || _globalRegistry._isActive !== true) {
|
|
472
|
+
return fn(null);
|
|
473
|
+
}
|
|
474
|
+
return _globalRegistry.span(name, fn, { attributes: attributes });
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function _resetForTest() {
|
|
478
|
+
_globalRegistry = null;
|
|
479
|
+
_setOtelForTest(undefined);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
module.exports = {
|
|
483
|
+
create: create,
|
|
484
|
+
tap: tap,
|
|
485
|
+
TracingError: TracingError,
|
|
486
|
+
_setOtelForTest: _setOtelForTest,
|
|
487
|
+
_resetForTest: _resetForTest,
|
|
488
|
+
// Internal helpers exposed for tests
|
|
489
|
+
_parseTraceparent: _parseTraceparent,
|
|
490
|
+
_formatTraceparent: _formatTraceparent,
|
|
491
|
+
_newTraceId: _newTraceId,
|
|
492
|
+
_newSpanId: _newSpanId,
|
|
493
|
+
TRACEPARENT_RE: TRACEPARENT_RE,
|
|
494
|
+
};
|