verae-middleware/src/debug/trace.js

95 lines
2.9 KiB
JavaScript

/**
* @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<T>)} 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<T>} [fn] - Work to execute inside the context when options is an object.
* @returns {T|Promise<T>} 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();
});
}