Added plan.md with complete architecture for format-agnostic content generation: - Support for Markdown, HTML, Plain Text, JSON formats - New FormatExporter module with neutral data structure - Integration strategy with existing ContentAssembly and ArticleStorage - Bonus features: SEO metadata generation, readability scoring, WordPress Gutenberg format - Implementation roadmap with 4 phases (6h total estimated) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
149 lines
4.7 KiB
JavaScript
149 lines
4.7 KiB
JavaScript
// lib/trace.js
|
|
const { AsyncLocalStorage } = require('node:async_hooks');
|
|
const { randomUUID } = require('node:crypto');
|
|
const { logSh } = require('./ErrorReporting');
|
|
|
|
const als = new AsyncLocalStorage();
|
|
|
|
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 logSh(`▶ ${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 logSh(`✔ ${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 logSh(`✖ ${name}${paramsStr} FAILED (${dur(span.duration())})`, 'ERROR');
|
|
await logSh(`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 logSh(` ${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 logSh(`• ${msg}`, 'TRACE');
|
|
}
|
|
|
|
async annotate(fields = {}) {
|
|
const span = this.current();
|
|
if (span) Object.assign(span.attrs, fields);
|
|
await logSh('… 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');
|
|
logSh(`\n—— TRACE SUMMARY ——\n${summary}\n—— END TRACE ——`, 'INFO');
|
|
return summary;
|
|
}
|
|
}
|
|
|
|
const tracer = new Tracer();
|
|
|
|
module.exports = {
|
|
Span,
|
|
Tracer,
|
|
tracer
|
|
}; |