autotel
Version:
Write Once, Observe Anywhere
347 lines (344 loc) • 11.4 kB
JavaScript
;
var apiLogs = require('@opentelemetry/api-logs');
// src/pretty-log-formatter.ts
var RESET = "\x1B[0m";
var DIM = "\x1B[2m";
var BOLD = "\x1B[1m";
var RED = "\x1B[31m";
var YELLOW = "\x1B[33m";
var GREEN = "\x1B[32m";
var CYAN = "\x1B[36m";
var GRAY = "\x1B[90m";
var LEVEL_COLORS = {
debug: GRAY,
info: GREEN,
warn: YELLOW,
error: RED
};
var SKIP_PREFIXES = [
"telemetry.",
"otel.",
"process.",
"os.",
"host.",
"service.",
"autotel."
];
var SKIP_KEYS = /* @__PURE__ */ new Set([
"operation",
"traceId",
"spanId",
"correlationId",
"duration_ms",
"duration",
"status_code",
"status_message",
"timestamp",
"http.request.method",
"url.path",
"http.route",
"http.response.status_code"
]);
function useColor() {
if (typeof process !== "undefined") {
if (process.env.NO_COLOR) return false;
if (process.env.FORCE_COLOR) return true;
if (process.stdout?.isTTY) return true;
}
return false;
}
function c(color, text) {
return useColor() ? `${color}${text}${RESET}` : text;
}
function formatDuration(ms) {
if (ms < 1e3) return `${Math.round(ms)}ms`;
if (ms < 6e4) {
const seconds2 = ms / 1e3;
return seconds2 < 10 ? `${seconds2.toFixed(1)}s` : `${Math.round(seconds2)}s`;
}
const minutes = Math.floor(ms / 6e4);
const seconds = Math.round(ms % 6e4 / 1e3);
return seconds > 0 ? `${minutes}m ${seconds}s` : `${minutes}m`;
}
function formatTime(iso) {
try {
const d = new Date(iso);
return d.toLocaleTimeString("en-GB", { hour12: false });
} catch {
return iso.slice(11, 19);
}
}
function formatValue(value) {
if (typeof value === "string") return value;
if (typeof value === "number" || typeof value === "boolean")
return String(value);
if (value == null) return "";
try {
return JSON.stringify(value);
} catch {
return String(value);
}
}
function groupAttributes(event) {
const tree = {};
for (const [key, value] of Object.entries(event)) {
if (SKIP_KEYS.has(key)) continue;
if (SKIP_PREFIXES.some((p) => key.startsWith(p))) continue;
if (value == null || value === "") continue;
const parts = key.split(".");
if (parts.length === 1) {
tree[key] = value;
} else {
let current = tree;
for (let i = 0; i < parts.length - 1; i++) {
const part = parts[i];
if (!(part in current) || typeof current[part] !== "object") {
current[part] = {};
}
current = current[part];
}
current[parts[parts.length - 1]] = value;
}
}
return tree;
}
function renderTree(obj, indent, isLast) {
const lines = [];
const entries = Object.entries(obj);
entries.forEach(([key, value], idx) => {
const last = idx === entries.length - 1;
const connector = last ? "\u2514\u2500" : "\u251C\u2500";
const prefix = indent + connector + " ";
if (value && typeof value === "object" && !Array.isArray(value)) {
const nested = value;
const flatValues = Object.entries(nested).filter(
([, v]) => typeof v !== "object" || v === null
);
const nestedObjs = Object.entries(nested).filter(
([, v]) => typeof v === "object" && v !== null && !Array.isArray(v)
);
if (nestedObjs.length === 0) {
const inline = flatValues.map(([k, v]) => `${c(CYAN, k)}=${formatValue(v)}`).join(" ");
lines.push(`${prefix}${c(BOLD, key)}: ${inline}`);
} else {
lines.push(`${prefix}${c(BOLD, key)}:`);
const nextIndent = indent + (last ? " " : "\u2502 ");
lines.push(...renderTree(nested, nextIndent, [...isLast, last]));
}
} else {
lines.push(`${prefix}${c(CYAN, key)}: ${c(DIM, formatValue(value))}`);
}
});
return lines;
}
function formatPrettyLogLine(ctx) {
const { event, level, message } = ctx;
const timestamp = formatTime(String(event.timestamp ?? ""));
const service = event["service.name"] || event.service || "";
const method = event["http.request.method"] || "";
const path = event["http.route"] || event["url.path"] || "";
const status = event["http.response.status_code"] || event.status_code || "";
const durationMs = Number(event.duration_ms ?? 0);
const duration = formatDuration(durationMs);
const levelColor = LEVEL_COLORS[level] ?? "";
const levelStr = c(levelColor, level.toUpperCase().padEnd(5));
const parts = [c(DIM, timestamp), levelStr];
if (service) parts.push(c(DIM, `[${service}]`));
if (method) parts.push(c(BOLD, String(method)));
if (path) parts.push(String(path));
if (status) {
const statusNum = Number(status);
const statusColor = statusNum >= 500 ? RED : statusNum >= 400 ? YELLOW : GREEN;
parts.push(c(statusColor, String(status)));
}
parts.push(c(DIM, `in ${duration}`));
const header = parts.join(" ");
const tree = groupAttributes(event);
if (Object.keys(tree).length === 0) {
return header;
}
const treeLines = renderTree(tree, " ", []);
return [header, ...treeLines].join("\n");
}
var CanonicalLogLineProcessor = class {
logger;
rootSpansOnly;
minLevel;
messageFormat;
includeResourceAttributes;
attributeRedactor;
shouldEmit;
drain;
onDrainError;
pretty;
getOTelLogger = null;
constructor(options = {}) {
this.logger = options.logger;
this.rootSpansOnly = options.rootSpansOnly ?? false;
this.minLevel = options.minLevel ?? "info";
this.messageFormat = options.messageFormat ?? ((span) => `[${span.name}] Request completed`);
this.includeResourceAttributes = options.includeResourceAttributes ?? true;
this.attributeRedactor = options.attributeRedactor;
this.shouldEmit = options.shouldEmit ?? this.buildKeepPredicate(options.keep);
this.drain = options.drain;
this.onDrainError = options.onDrainError;
this.pretty = options.pretty ?? (typeof process !== "undefined" && process.env.NODE_ENV === "development");
if (!this.logger) {
this.getOTelLogger = () => apiLogs.logs.getLogger("autotel.canonical-log-line");
}
}
buildKeepPredicate(keep) {
if (!keep || keep.length === 0) return void 0;
return (ctx) => {
return keep.some((condition) => {
if (condition.status !== void 0) {
const httpStatus = Number(
ctx.event["http.response.status_code"] ?? 0
);
if (httpStatus >= condition.status) return true;
}
if (condition.durationMs !== void 0 && Number(ctx.event.duration_ms ?? 0) >= condition.durationMs) {
return true;
}
if (condition.path !== void 0) {
const route = String(
ctx.event["http.route"] ?? ctx.event["url.path"] ?? ""
);
if (route.startsWith(condition.path)) return true;
}
return false;
});
};
}
onStart() {
}
onEnd(span) {
if (this.rootSpansOnly && span.parentSpanContext?.spanId && !span.parentSpanContext.isRemote) {
return;
}
const level = this.getLogLevel(span);
if (!this.shouldLog(level)) {
return;
}
const canonicalLogLine = this.buildCanonicalLogLine(span);
const message = this.messageFormat(span);
const eventContext = {
span,
level,
message,
event: canonicalLogLine
};
if (this.shouldEmit && !this.shouldEmit(eventContext)) return;
if (this.pretty) {
console.log(formatPrettyLogLine(eventContext));
}
if (this.logger) {
this.emitViaLogger(level, message, canonicalLogLine);
} else if (this.getOTelLogger) {
const otelLogger = this.getOTelLogger();
this.emitViaOTel(level, message, canonicalLogLine, otelLogger);
}
if (this.drain) {
Promise.resolve(this.drain(eventContext)).catch((error) => {
if (this.onDrainError) {
this.onDrainError(error, eventContext);
return;
}
this.reportInternalWarning("canonicalLogLines.drain failed", error);
});
}
}
buildCanonicalLogLine(span) {
const durationMs = span.duration[0] * 1e3 + span.duration[1] / 1e6;
const timestamp = new Date(
span.startTime[0] * 1e3 + span.startTime[1] / 1e6
).toISOString();
const canonicalLogLine = {};
const attributes = this.redactAttributes(span.attributes);
Object.assign(canonicalLogLine, attributes);
if (this.includeResourceAttributes) {
const resourceAttrs = this.redactAttributes(
span.resource.attributes
);
Object.assign(canonicalLogLine, resourceAttrs);
}
canonicalLogLine.operation = span.name;
canonicalLogLine.traceId = span.spanContext().traceId;
canonicalLogLine.spanId = span.spanContext().spanId;
canonicalLogLine.correlationId = span.spanContext().traceId.slice(0, 16);
canonicalLogLine.duration_ms = Math.round(durationMs * 100) / 100;
canonicalLogLine.duration = formatDuration(durationMs);
canonicalLogLine.status_code = span.status.code;
canonicalLogLine.status_message = span.status.message || void 0;
canonicalLogLine.timestamp = timestamp;
return canonicalLogLine;
}
redactAttributes(attributes) {
if (!this.attributeRedactor) {
return { ...attributes };
}
const redacted = {};
for (const [key, value] of Object.entries(attributes)) {
if (value !== void 0) {
redacted[key] = this.attributeRedactor(key, value);
}
}
return redacted;
}
emitViaLogger(level, message, canonicalLogLine) {
this.logger[level](canonicalLogLine, message);
}
emitViaOTel(level, message, canonicalLogLine, otelLogger) {
const otelAttributes = {};
for (const [key, value] of Object.entries(canonicalLogLine)) {
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
otelAttributes[key] = value;
} else if (value !== null && value !== void 0) {
otelAttributes[key] = String(value);
}
}
otelLogger.emit({
severityNumber: this.getSeverityNumber(level),
severityText: level.toUpperCase(),
body: message,
attributes: otelAttributes
});
}
getLogLevel(span) {
const explicitLevel = span.attributes["autotel.log.level"];
if (explicitLevel === "debug" || explicitLevel === "info" || explicitLevel === "warn" || explicitLevel === "error") {
return explicitLevel;
}
if (span.status.code === 2) return "error";
return "info";
}
shouldLog(level) {
const levels = ["debug", "info", "warn", "error"];
return levels.indexOf(level) >= levels.indexOf(this.minLevel);
}
getSeverityNumber(level) {
const mapping = {
debug: apiLogs.SeverityNumber.DEBUG,
info: apiLogs.SeverityNumber.INFO,
warn: apiLogs.SeverityNumber.WARN,
error: apiLogs.SeverityNumber.ERROR
};
return mapping[level] ?? apiLogs.SeverityNumber.INFO;
}
reportInternalWarning(message, error) {
const err = error instanceof Error ? error.message : String(error ?? "unknown error");
if (this.logger) {
this.logger.warn({ error: err }, `[autotel] ${message}`);
return;
}
console.warn(`[autotel] ${message}: ${err}`);
}
async forceFlush() {
}
async shutdown() {
}
};
exports.CanonicalLogLineProcessor = CanonicalLogLineProcessor;
exports.formatDuration = formatDuration;
//# sourceMappingURL=chunk-6S5RUKU3.cjs.map
//# sourceMappingURL=chunk-6S5RUKU3.cjs.map