import globals from "./globals"; import { isStackFrames } from "./stack"; /** * ConsoleWriter is a writer that converts the JSON input * into a human readable format. */ export function consoleWriter(obj: unknown): string { if ( !objIsLog(obj) || !(globals.MessageFieldName in obj) || !(globals.TimestampFieldName in obj) || !(globals.LevelFieldName in obj) ) { return JSON.stringify(obj) + "\n"; } return writeParts(obj) + writeFields(obj) + "\n"; } type logEntry = Record; function objIsLog(obj: unknown): obj is logEntry { return typeof obj === "object" && obj !== null; } /** * Write parts takes the standard fields of a log entry and writes the beginning of the human * readable log line. */ function writeParts(obj: logEntry) { const parts: string[] = []; if (globals.TimestampFieldName in obj) { parts.push(formatTime(obj[globals.TimestampFieldName])); } if (globals.LevelFieldName in obj) { parts.push(formatLevel(obj[globals.LevelFieldName])); } if (globals.CallerFieldName in obj) { parts.push(formatCaller(obj[globals.CallerFieldName])); } if (globals.MessageFieldName in obj) { parts.push(formatMessage(obj[globals.MessageFieldName])); } return parts.filter((part) => part.length > 0).join(" "); } function formatTime(timestamp: unknown): string { let time: Date; if (typeof timestamp === "string") { time = new Date(timestamp); } else if (typeof timestamp === "number") { time = new Date(timestamp); } else if (timestamp instanceof Date) { time = timestamp; } else { return colorize("", color.darkGray); } let ampm = "AM"; let hour = time.getHours(); if (hour > 12) { hour -= 12; ampm = "PM"; } const minute = time.getMinutes(); const t = `${hour < 10 ? " " : ""}${hour}:${ minute < 10 ? "0" : "" }${minute}${ampm}`; return colorize(t, color.darkGray); } function formatLevel(level: unknown): string { if (typeof level !== "string") { return colorize("???", color.bold); } switch (level) { case globals.LevelTraceValue: return colorize("TRC", color.magenta); case globals.LevelDebugValue: return colorize("DBG", color.yellow); case globals.LevelInfoValue: return colorize("INF", color.green); case globals.LevelWarnValue: return colorize("WRN", color.red); case globals.LevelErrorValue: return colorize(colorize("ERR", color.red), color.bold); case globals.LevelFatalValue: return colorize(colorize("FTL", color.red), color.bold); default: if (level.length > 0) { return level.substring(0, 3).toUpperCase().padEnd(3, " "); } else { return colorize("???", color.bold); } } } function formatCaller(caller: unknown): string { if (typeof caller !== "string") { return colorize("", color.darkGray); } if (caller.length === 0) { return colorize("", color.darkGray); } let callerStr: string = caller; const cwd = process.cwd() + "/"; if (callerStr.startsWith(cwd)) { callerStr = callerStr.substring(cwd.length); } return colorize(callerStr, color.bold) + colorize(" >", color.cyan); } function formatMessage(msg: unknown): string { if (typeof msg !== "string") { return colorize("", color.bold); } return msg; } function writeFields(obj: logEntry): string { let fields: string[] = []; for (const key in obj) { if ( key === globals.TimestampFieldName || key === globals.LevelFieldName || key === globals.MessageFieldName || // FIXME: Implement Caller key === globals.CallerFieldName || key === globals.StackFieldName ) { continue; } fields.push(key); } // Sort the fields alphabetically, but put the error field first if it exists fields = fields.sort((a, b) => { if (a === b) { return 0; } else if (a === globals.ErrorFieldName) { return -1; } else if (b === globals.ErrorFieldName) { return 1; } return a < b ? -1 : 1; }); if (fields.length === 0) { return ""; } const parts: string[] = []; for (const field of fields) { const value = obj[field]; if (value === undefined || value === null) { continue; } let valueFormat: (str: string) => string; if (field === globals.ErrorFieldName) { valueFormat = errorValueFormat; } else { valueFormat = fieldValueFormat; } const fmtedName = colorize(field + "=", color.cyan); let fmtedValue: string; if (typeof value === "string") { if (needsQuote(value)) { fmtedValue = valueFormat(JSON.stringify(value)); } else { fmtedValue = valueFormat(value); } } else if (typeof value === "number") { fmtedValue = valueFormat(value.toString()); } else { try { fmtedValue = valueFormat(JSON.stringify(value)); } catch (e: unknown) { fmtedValue = colorize(`[error: ${e}]`, color.red); } } parts.push(fmtedName + fmtedValue); } // Append the stack const stack = []; if (globals.StackFieldName in obj) { const frames = obj[globals.StackFieldName]; if (isStackFrames(frames) && frames.length > 0) { stack.push("\n Stack Trace:"); for (let i = 0; i < frames.length; i++) { const frame = frames[i]; const line = ` at ${frame.file}:${frame.line}:${frame.column}`; stack.push(line); if (i >= 6) { stack.push( ` ... remaining ${frames.length - i} frames omitted...`, ); break; } } } } return " " + parts.join(" ") + stack.join("\n"); } function fieldValueFormat(v: string): string { return v; } function errorValueFormat(v: string): string { return colorize(v, color.red); } function needsQuote(v: string): boolean { return ( v.includes(" ") || v.includes("\t") || v.includes("\n") || v.includes("\r") || v.includes("\\") || v.includes('"') ); } const color = { black: 30, red: 31, green: 32, yellow: 33, blue: 34, magenta: 35, cyan: 36, white: 37, bold: 1, darkGray: 90, }; function colorize(str: string, color: number): string { return `\x1b[${color}m${str}\x1b[0m`; }