/** * @fileoverview Correlation / trace IDs for failure tracing across HTTP and NATS. * @module debug/trace */ import { createDebugger } from './logger.js'; import { traceStorage, generateTraceId, getTraceId, getTraceContext, } from './trace-context.js'; export { generateTraceId, getTraceId, getTraceContext }; const log = createDebugger('trace'); /** * Run `fn` within a trace context. Nested calls inherit the same `traceId` * unless `traceId` is explicitly overridden. * * @template T * @param {object|(() => T|Promise)} options - Options or the callback itself. * @param {string} [options.traceId] - Existing id (e.g. from `X-Trace-Id` header). * @param {string} [options.span] - Human label for this span (logged when debug on). * @param {() => T|Promise} [fn] - Work to execute inside the context when options is an object. * @returns {T|Promise} Return value of `fn`. * * @example * await withTrace({ span: 'timestamp.wait', traceId: req.headers['x-trace-id'] }, async () => { * // getTraceId() is stable here and in awaited children * }); */ export function withTrace(options, fn) { const opts = typeof options === 'function' ? {} : options ?? {}; const callback = typeof options === 'function' ? options : fn; if (typeof callback !== 'function') { throw new TypeError('withTrace requires a function to execute'); } const parent = traceStorage.getStore(); const traceId = opts.traceId || parent?.traceId || generateTraceId(); const span = opts.span || parent?.span || 'root'; const ctx = { traceId, span }; return traceStorage.run(ctx, () => { log.debug('span enter', { span }); try { const result = callback(); if (result && typeof result.then === 'function') { return result.then( (value) => { log.debug('span exit', { span, ok: true }); return value; }, (err) => { log.debug('span exit', { span, ok: false, error: err?.message }); throw err; }, ); } log.debug('span exit', { span, ok: true }); return result; } catch (err) { log.debug('span exit', { span, ok: false, error: err?.message }); throw err; } }); } /** * Express middleware that establishes a trace context per request. * * Reads `X-Trace-Id` when present; otherwise generates a new id. * Sets `req.traceId` and response header `X-Trace-Id`. * * @param {import('express').Request} req * @param {import('express').Response} res * @param {import('express').NextFunction} next * @returns {void} */ export function traceMiddleware(req, res, next) { const incoming = req.headers['x-trace-id']; const traceId = typeof incoming === 'string' && incoming.trim() ? incoming.trim() : generateTraceId(); req.traceId = traceId; res.setHeader('X-Trace-Id', traceId); withTrace({ traceId, span: `${req.method} ${req.path}` }, () => { next(); }); }