// lib/trace.js - Traçage hiérarchique pour SourceFinder const { AsyncLocalStorage } = require('node:async_hooks'); const { randomUUID } = require('node:crypto'); const als = new AsyncLocalStorage(); // Logger sera injecté pour éviter références circulaires let loggerFn = console.log; // Fallback function setLogger(fn) { loggerFn = fn; } function now() { return performance.now(); } function dur(ms) { if (ms < 1e3) return `${ms.toFixed(1)}ms`; const s = ms / 1e3; return s < 60 ? `${s.toFixed(2)}s` : `${(s/60).toFixed(2)}m`; } class Span { constructor({ name, parent = null, attrs = {} }) { this.id = randomUUID(); this.name = name; this.parent = parent; this.children = []; this.attrs = attrs; this.start = now(); this.end = null; this.status = 'ok'; this.error = null; } pathNames() { const names = []; let cur = this; while (cur) { names.unshift(cur.name); cur = cur.parent; } return names.join(' > '); } finish() { this.end = now(); } duration() { return (this.end ?? now()) - this.start; } } class Tracer { constructor() { this.rootSpans = []; } current() { return als.getStore(); } async startSpan(name, attrs = {}) { const parent = this.current(); const span = new Span({ name, parent, attrs }); if (parent) parent.children.push(span); else this.rootSpans.push(span); // Formater les paramètres pour affichage const paramsStr = this.formatParams(attrs); await loggerFn(`▶ ${name}${paramsStr}`, 'TRACE'); return span; } async run(name, fn, attrs = {}) { const parent = this.current(); const span = await this.startSpan(name, attrs); return await als.run(span, async () => { try { const res = await fn(); span.finish(); const paramsStr = this.formatParams(span.attrs); await loggerFn(`✔ ${name}${paramsStr} (${dur(span.duration())})`, 'TRACE'); return res; } catch (err) { span.status = 'error'; span.error = { message: err?.message, stack: err?.stack }; span.finish(); const paramsStr = this.formatParams(span.attrs); await loggerFn(`✖ ${name}${paramsStr} FAILED (${dur(span.duration())})`, 'ERROR'); await loggerFn(`Stack trace: ${span.error.message}`, 'ERROR'); if (span.error.stack) { const stackLines = span.error.stack.split('\n').slice(1, 6); // Première 5 lignes du stack for (const line of stackLines) { await loggerFn(` ${line.trim()}`, 'ERROR'); } } throw err; } }); } async event(msg, extra = {}) { const span = this.current(); const data = { trace: true, evt: 'span.event', ...extra }; if (span) { data.span = span.id; data.path = span.pathNames(); data.since_ms = +( (now() - span.start).toFixed(1) ); } await loggerFn(`• ${msg}`, 'TRACE'); } async annotate(fields = {}) { const span = this.current(); if (span) Object.assign(span.attrs, fields); await loggerFn('… annotate', 'TRACE'); } formatParams(attrs = {}) { const params = Object.entries(attrs) .filter(([key, value]) => value !== undefined && value !== null) .map(([key, value]) => { // Tronquer les valeurs trop longues const strValue = String(value); const truncated = strValue.length > 50 ? strValue.substring(0, 47) + '...' : strValue; return `${key}=${truncated}`; }); return params.length > 0 ? `(${params.join(', ')})` : ''; } printSummary() { const lines = []; const draw = (node, depth = 0) => { const pad = ' '.repeat(depth); const icon = node.status === 'error' ? '✖' : '✔'; lines.push(`${pad}${icon} ${node.name} (${dur(node.duration())})`); if (Object.keys(node.attrs ?? {}).length) { lines.push(`${pad} attrs: ${JSON.stringify(node.attrs)}`); } for (const ch of node.children) draw(ch, depth + 1); if (node.status === 'error' && node.error?.message) { lines.push(`${pad} error: ${node.error.message}`); if (node.error.stack) { const stackLines = String(node.error.stack || '').split('\n').slice(1, 4).map(s => s.trim()); if (stackLines.length) { lines.push(`${pad} stack:`); stackLines.forEach(line => { if (line) lines.push(`${pad} ${line}`); }); } } } }; for (const r of this.rootSpans) draw(r, 0); const summary = lines.join('\n'); loggerFn(`\n—— TRACE SUMMARY ——\n${summary}\n—— END TRACE ——`, 'INFO'); return summary; } } const tracer = new Tracer(); function setupTracer(moduleName = 'Default') { return { run: (name, fn, params = {}) => tracer.run(name, fn, params), event: (msg, extra = {}) => tracer.event(msg, extra), annotate: (fields = {}) => tracer.annotate(fields) }; } module.exports = { Span, Tracer, tracer, setupTracer, setLogger };