autotel
Version:
Write Once, Observe Anywhere
222 lines (220 loc) • 6.45 kB
JavaScript
import { SpanStatusCode } from '@opentelemetry/api';
// src/pretty-console-exporter.ts
var ExportResultCode = {
SUCCESS: 0,
FAILED: 1
};
var ANSI = {
reset: "\x1B[0m",
bold: "\x1B[1m",
dim: "\x1B[2m",
green: "\x1B[32m",
red: "\x1B[31m",
yellow: "\x1B[33m",
blue: "\x1B[34m",
cyan: "\x1B[36m",
gray: "\x1B[90m"
};
var PrettyConsoleExporter = class {
options;
constructor(options = {}) {
this.options = {
colors: options.colors ?? process.stdout?.isTTY ?? false,
showAttributes: options.showAttributes ?? true,
maxValueLength: options.maxValueLength ?? 50,
showScope: options.showScope ?? true,
hideAttributes: options.hideAttributes ?? [],
showTraceId: options.showTraceId ?? false
};
}
/**
* Export spans with pretty formatting
*/
export(spans, resultCallback) {
if (spans.length === 0) {
resultCallback({ code: ExportResultCode.SUCCESS });
return;
}
try {
const traceGroups = this.groupByTrace(spans);
for (const [traceId, traceSpans] of traceGroups) {
this.printTrace(traceId, traceSpans);
}
resultCallback({ code: ExportResultCode.SUCCESS });
} catch {
resultCallback({ code: ExportResultCode.SUCCESS });
}
}
/**
* Group spans by their trace ID
*/
groupByTrace(spans) {
const groups = /* @__PURE__ */ new Map();
for (const span of spans) {
const traceId = span.spanContext().traceId;
const group = groups.get(traceId) ?? [];
group.push(span);
groups.set(traceId, group);
}
return groups;
}
/**
* Print a single trace with all its spans as a tree
*/
printTrace(traceId, spans) {
const sorted = [...spans].toSorted((a, b) => {
const aTime = hrTimeToMs(a.startTime);
const bTime = hrTimeToMs(b.startTime);
return aTime - bTime;
});
const tree = this.buildSpanTree(sorted);
if (this.options.showTraceId && tree.length > 0) {
console.log(this.color(`trace: ${traceId}`, "gray"));
}
for (const node of tree) {
this.printNode(node, 0, false);
}
console.log("");
}
/**
* Build a tree structure from flat spans using parent-child relationships
*/
buildSpanTree(spans) {
const spanMap = /* @__PURE__ */ new Map();
const roots = [];
for (const span of spans) {
const spanId = span.spanContext().spanId;
spanMap.set(spanId, { span, children: [] });
}
for (const span of spans) {
const spanId = span.spanContext().spanId;
const parentId = span.parentSpanContext?.spanId;
const node = spanMap.get(spanId);
if (parentId && spanMap.has(parentId)) {
spanMap.get(parentId).children.push(node);
} else {
roots.push(node);
}
}
return roots;
}
/**
* Print a span node with indentation and tree characters
*/
printNode(node, depth, isLast) {
const { span } = node;
const prefix = depth === 0 ? "" : " ".repeat(depth - 1) + (isLast ? "\u2514\u2500 " : "\u251C\u2500 ");
const isError = span.status.code === SpanStatusCode.ERROR;
const statusChar = isError ? "\u2717" : "\u2713";
const statusColor = isError ? "red" : "green";
const durationMs = hrTimeToMs(span.duration);
const durationStr = formatDuration(durationMs);
const durationColor = getDurationColor(durationMs);
const scopeName = this.options.showScope ? this.color(` [${this.getScopeName(span)}]`, "gray") : "";
const line = [
prefix,
this.color(statusChar, statusColor),
" ",
span.name.padEnd(Math.max(35 - prefix.length, 10)),
this.color(durationStr.padStart(8), durationColor),
scopeName
].join("");
console.log(line);
if (this.options.showAttributes) {
const attrs = this.formatAttributes(span);
if (attrs) {
const attrIndent = " ".repeat(depth) + " ";
console.log(this.color(`${attrIndent}${attrs}`, "dim"));
}
}
if (isError && span.status.message) {
const errorIndent = " ".repeat(depth) + " ";
console.log(
this.color(`${errorIndent}Error: ${span.status.message}`, "red")
);
}
const childCount = node.children.length;
let index = 0;
for (const child of node.children) {
this.printNode(child, depth + 1, index === childCount - 1);
index++;
}
}
/**
* Get short scope name from instrumentation scope
*/
getScopeName(span) {
const name = span.instrumentationScope?.name ?? "unknown";
const match = name.match(/@opentelemetry\/instrumentation-(.+)/);
if (match?.[1]) return match[1];
const lastPart = name.split("/").at(-1);
return lastPart ?? name;
}
/**
* Format span attributes as a comma-separated string
*/
formatAttributes(span) {
const attrs = span.attributes;
if (!attrs || Object.keys(attrs).length === 0) {
return "";
}
const pairs = [];
for (const [key, value] of Object.entries(attrs)) {
if (this.options.hideAttributes.includes(key)) continue;
if (value === void 0 || value === null) continue;
const strValue = this.truncate(
Array.isArray(value) ? `[${value.join(", ")}]` : String(value),
this.options.maxValueLength
);
pairs.push(`${key}=${strValue}`);
}
return pairs.join(", ");
}
/**
* Truncate string to max length with ellipsis
*/
truncate(str, max) {
if (str.length <= max) return str;
return str.slice(0, max - 3) + "...";
}
/**
* Apply ANSI color if colors are enabled
*/
color(text, color) {
if (!this.options.colors) return text;
return `${ANSI[color]}${text}${ANSI.reset}`;
}
/**
* Shutdown (no-op for console exporter)
*/
shutdown() {
return Promise.resolve();
}
/**
* Force flush (no-op for console exporter)
*/
forceFlush() {
return Promise.resolve();
}
};
function hrTimeToMs(hrTime) {
const [seconds, nanos] = hrTime;
return seconds * 1e3 + nanos / 1e6;
}
function formatDuration(ms) {
if (ms < 1) {
return `${(ms * 1e3).toFixed(0)}\xB5s`;
}
if (ms < 1e3) {
return `${ms.toFixed(0)}ms`;
}
return `${(ms / 1e3).toFixed(2)}s`;
}
function getDurationColor(ms) {
if (ms < 100) return "green";
if (ms < 500) return "yellow";
return "red";
}
export { PrettyConsoleExporter };
//# sourceMappingURL=chunk-6UQRVUN3.js.map
//# sourceMappingURL=chunk-6UQRVUN3.js.map