openvibers/openvibe.shared trace.js
trace.js
| 1 | ; |
| 2 | /** |
| 3 | * openvibe-shared/trace — carry the W3C trace of the request being served onto the calls a service |
| 4 | * makes while serving it (roadmap Track O): one trace across Live → Chat → VIP → Billing, without |
| 5 | * threading a context object through every function. |
| 6 | * |
| 7 | * const trace = require('openvibe-shared/trace'); |
| 8 | * app.use(contracts.http.middleware()); // accepts or starts traceparent, sets req.ov |
| 9 | * trace.install(app); // right after it: remembers req.ov for the request, |
| 10 | * // and outbound fetch() carries it |
| 11 | * |
| 12 | * install(app) adds a middleware that runs the rest of the request inside an AsyncLocalStorage |
| 13 | * holding { traceId, requestId } (from req.ov, or parsed from the incoming traceparent), and wraps |
| 14 | * globalThis.fetch once per process: a request to a loopback address or an OpenVibe host |
| 15 | * (*.openvibe.network, openvibe.live, …, TRACE_HOSTS) that has no traceparent of its own gets |
| 16 | * `traceparent: 00-<traceId>-<new span id>-01` and `X-OpenVibe-Request-Id`. Calls to anyone else |
| 17 | * (payment providers, AI providers, user-chosen URLs) are left exactly as they were, so a trace id |
| 18 | * never leaves the network. Outside a request (timers, workers) nothing is added. |
| 19 | * |
| 20 | * trace.current() { traceId, requestId } or null |
| 21 | * trace.outboundHeaders() the headers install() would add ({} outside a request) |
| 22 | * trace.run(ctx, fn) run fn inside a context (jobs that want one) |
| 23 | */ |
| 24 | const crypto = require('crypto'); |
| 25 | const { AsyncLocalStorage } = require('async_hooks'); |
| 26 | |
| 27 | const als = new AsyncLocalStorage(); |
| 28 | const TRACE_RE = /^00-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})$/; |
| 29 | const OPENVIBE_HOST = /(^|\.)(openvibe\.(network|live|tools|media|community|chat|games|tips|vip|codes|wiki|blog|news|reviews|deals|coupons|trade|host)|openre\.stream)$/i; |
| 30 | const LOOPBACK_HOST = /^(127\.\d+\.\d+\.\d+|localhost|\[?::1\]?)$/i; |
| 31 | |
| 32 | function fromHeaders(headers = {}) { |
| 33 | const get = (k) => (typeof headers.get === 'function' ? headers.get(k) : headers[k] || headers[k.toLowerCase()]); |
| 34 | const m = TRACE_RE.exec(String(get('traceparent') || '').trim().toLowerCase()); |
| 35 | const traceId = m && !/^0+$/.test(m[1]) ? m[1] : crypto.randomBytes(16).toString('hex'); |
| 36 | const rid = String(get('x-openvibe-request-id') || '').slice(0, 128); |
| 37 | return { traceId, requestId: /^[A-Za-z0-9._:-]{1,128}$/.test(rid) ? rid : `req_${crypto.randomBytes(12).toString('hex')}` }; |
| 38 | } |
| 39 | |
| 40 | function current() { return als.getStore() || null; } |
| 41 | function run(ctx, fn) { return als.run(ctx && ctx.traceId ? { traceId: ctx.traceId, requestId: ctx.requestId || null } : fromHeaders({}), fn); } |
| 42 | |
| 43 | function outboundHeaders(ctx = current()) { |
| 44 | if (!ctx || !ctx.traceId) return {}; |
| 45 | const out = { traceparent: `00-${ctx.traceId}-${crypto.randomBytes(8).toString('hex')}-01` }; |
| 46 | if (ctx.requestId) out['X-OpenVibe-Request-Id'] = ctx.requestId; |
| 47 | return out; |
| 48 | } |
| 49 | |
| 50 | function extraHosts() { |
| 51 | return String(process.env.TRACE_HOSTS || '').split(',').map((h) => h.trim().toLowerCase()).filter(Boolean); |
| 52 | } |
| 53 | /** Does a call to this URL stay inside the network (loopback or an OpenVibe host)? */ |
| 54 | function insideNetwork(url) { |
| 55 | let host; |
| 56 | try { host = new URL(url).hostname.toLowerCase(); } catch { return false; } |
| 57 | host = host.replace(/^\[|\]$/g, ''); |
| 58 | return LOOPBACK_HOST.test(host) || OPENVIBE_HOST.test(host) || extraHosts().includes(host); |
| 59 | } |
| 60 | |
| 61 | const WRAPPED = Symbol.for('openvibe.trace.fetch'); |
| 62 | function wrapFetch() { |
| 63 | const original = globalThis.fetch; |
| 64 | if (typeof original !== 'function' || original[WRAPPED]) return; |
| 65 | const traced = function tracedFetch(input, init) { |
| 66 | const ctx = current(); |
| 67 | if (!ctx) return original(input, init); |
| 68 | const url = typeof input === 'string' ? input : (input && (input.url || input.href)) || ''; |
| 69 | if (!insideNetwork(url)) return original(input, init); |
| 70 | const headers = new Headers((init && init.headers) || (input && typeof input === 'object' && input.headers) || undefined); |
| 71 | if (headers.has('traceparent')) return original(input, init); |
| 72 | for (const [k, v] of Object.entries(outboundHeaders(ctx))) if (!headers.has(k)) headers.set(k, v); |
| 73 | return original(input, { ...(init || {}), headers }); |
| 74 | }; |
| 75 | traced[WRAPPED] = true; |
| 76 | globalThis.fetch = traced; |
| 77 | } |
| 78 | |
| 79 | /** Express: the request's trace for everything it calls, and fetch() that carries it. */ |
| 80 | function install(app) { |
| 81 | wrapFetch(); |
| 82 | const mw = function openvibeTrace(req, _res, next) { |
| 83 | const ctx = req.ov && req.ov.traceId ? { traceId: req.ov.traceId, requestId: req.ov.requestId || null } : fromHeaders(req.headers || {}); |
| 84 | als.run(ctx, next); |
| 85 | }; |
| 86 | if (app && typeof app.use === 'function') app.use(mw); |
| 87 | return mw; |
| 88 | } |
| 89 | |
| 90 | module.exports = { install, current, run, outboundHeaders, insideNetwork, _wrapFetch: wrapFetch }; |
| 91 |