openvibers/openvibe.shared trace.js

trace.js

main 4.8 KB · 91 lines · javascript

1'use strict';
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 */
24const crypto = require('crypto');
25const { AsyncLocalStorage } = require('async_hooks');
26
27const als = new AsyncLocalStorage();
28const TRACE_RE = /^00-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})$/;
29const OPENVIBE_HOST = /(^|\.)(openvibe\.(network|live|tools|media|community|chat|games|tips|vip|codes|wiki|blog|news|reviews|deals|coupons|trade|host)|openre\.stream)$/i;
30const LOOPBACK_HOST = /^(127\.\d+\.\d+\.\d+|localhost|\[?::1\]?)$/i;
31
32function 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
40function current() { return als.getStore() || null; }
41function run(ctx, fn) { return als.run(ctx && ctx.traceId ? { traceId: ctx.traceId, requestId: ctx.requestId || null } : fromHeaders({}), fn); }
42
43function 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
50function 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)? */
54function 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
61const WRAPPED = Symbol.for('openvibe.trace.fetch');
62function 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. */
80function 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
90module.exports = { install, current, run, outboundHeaders, insideNetwork, _wrapFetch: wrapFetch };
91