UNPKG

autotel

Version:
347 lines (344 loc) 11.4 kB
'use strict'; 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