UNPKG

isoscribe

Version:

An isomorphic logging utility for any JavaScript runtime, providing structured, styled, and extensible logs.

203 lines (202 loc) 7.5 kB
import { exhaustiveMatchGuard } from "ts-jolt/isomorphic"; import { c } from "./Colorize.js"; const LOG_LEVEL_PRIORITY = { trace: 0, debug: 1, info: 2, warn: 3, error: 4, fatal: 5, }; const LOG_ACTION = { trace: c.magenta(`● ${c.underline("trace")}`), debug: c.magenta(`● ${c.underline("debug")}`), info: c.blueBright(`ℹ︎ ${c.underline("info")}`), warn: c.yellowBright(`! ${c.underline("warn")}`), error: c.red(`✕ ${c.underline("error")}`), fatal: c.redBright(`✕ ${c.underline("fatal")}`), success: c.green(`✓ ${c.underline("success")}`), watch: c.cyan(`⦿ ${c.underline("watching")}`), "checkpoint:start": c.cyanBright(`➤ ${c.underline("checkpoint:start")}`), "checkpoint:end": c.cyanBright(`➤ ${c.underline("checkpoint:end")}`), }; export class Isoscribe { _logLevel; _logFormat; _logLevelNameMaxLen; _logPill; _logName; constructor(args) { this._logLevel = args.logLevel ?? "info"; this._logFormat = args.logFormat ?? "string"; this._logName = args.name; this._logLevelNameMaxLen = Math.max(...Object.keys(LOG_LEVEL_PRIORITY).map((l) => `[${l}]`.length)); this._logPill = { text: this._logName, css: this.getLogPillCss(args.pillColor ?? "#55daf0"), }; } /** Set log level dynamically */ set logLevel(level) { this._logLevel = level; } shouldLog(level) { const shouldLog = LOG_LEVEL_PRIORITY[level] >= LOG_LEVEL_PRIORITY[this._logLevel]; return shouldLog; } getLogEnv() { if (typeof window !== "undefined") { return `browser-${this._logFormat}`; } return `server-${this._logFormat}`; } /** Determines text color for contrast */ getLogPillCss(bgColor) { const rgb = Number.parseInt(bgColor.slice(1), 16); const [r, g, b] = [(rgb >> 16) & 0xff, (rgb >> 8) & 0xff, rgb & 0xff]; const luminance = (ch) => ch / 255 <= 0.03928 ? ch / 12.92 : ((ch + 0.055) / 1.055) ** 2.4; const textColor = 0.2126 * luminance(r) + 0.7152 * luminance(g) + 0.0722 * luminance(b) > 0.179 ? "#000" : "#fff"; const styles = { background: bgColor, color: textColor, ["font-weight"]: "bold", padding: "2px 6px", "border-radius": "4px", }; const css = Object.entries(styles) .reduce((accum, [property, value]) => accum.concat(`${property}: ${value}`), []) .join("; "); return css; } /** Assigns a console logger to the log level */ getLogFn(level) { switch (level) { case "error": case "fatal": return console.error; case "warn": return console.warn; case "debug": case "info": case "trace": return console.log; default: return exhaustiveMatchGuard(level); } } /** Gets the formatted name of the logging event */ getLogLevelName(logLevel, options) { const css = "font-weight: bold"; const name = logLevel.toUpperCase(); if (!options?.setWidth) { return { name, css, }; } return { name: `[${name}]`.padEnd(this._logLevelNameMaxLen), css, }; } /** Get's and formats the timestamp of the logging event */ getLogTimestamp(options) { const format = options?.format ?? "iso"; const now = new Date(); switch (format) { case "iso": return now.toISOString(); case "hh:mm:ss AM/PM": return new Intl.DateTimeFormat("en", { timeStyle: "medium" }).format(new Date()); default: exhaustiveMatchGuard(format); } } log({ level, message, action, }, ...data) { // Do nothing if the level shouldn't be logged if (!this.shouldLog(level)) return; const logger = this.getLogFn(level); // Get the logging function const env = this.getLogEnv(); // Get the environment to figure out _what_ to log // Log a message based upon the env switch (env) { case "browser-string": { const logPill = this._logPill; const logLevelName = this.getLogLevelName(level, { setWidth: true }); const logTimestamp = this.getLogTimestamp({ format: "hh:mm:ss AM/PM" }); const logMessage = c.gray(message); const logAction = action ? LOG_ACTION[action] : ""; logger(`${logTimestamp} %c${logPill.text}, %c${logLevelName.name}`, logPill.css, logLevelName.css, logAction, logMessage, ...data); break; } case "server-string": { const logTimestamp = c.gray(this.getLogTimestamp({ format: "hh:mm:ss AM/PM" })); const logFeature = this._logName; const logAction = action ? LOG_ACTION[action] : ""; const logMessage = c.gray(message); const logLevelName = this.getLogLevelName(level, { setWidth: true }); const logStr = `${logTimestamp} ${logLevelName.name} ${logFeature} ${logAction} ${logMessage}`; if (data.length === 0) { return logger(logStr); } logger(logStr, data); break; } case "browser-json": case "server-json": { const logTimestamp = this.getLogTimestamp({ format: "iso" }); const logFeature = this._logName; logger(JSON.stringify({ timestamp: logTimestamp, feature: logFeature, level, message, data, })); break; } default: exhaustiveMatchGuard(env); } } trace(message, ...data) { this.log({ level: "trace", action: "trace", message }, ...data); } debug(message, ...data) { this.log({ level: "debug", action: "debug", message }, ...data); } info(message, ...data) { this.log({ level: "info", action: "info", message }, ...data); } warn(message, ...data) { this.log({ level: "warn", action: "warn", message }, ...data); } error(message, ...data) { this.log({ level: "error", action: "error", message }, ...data); } fatal(error) { this.log({ level: "fatal", action: "fatal", message: error.message }); // Print out the stack trace if (!this.shouldLog("fatal")) return; const logger = this.getLogFn("debug"); logger(` ${c.gray((error.stack ?? "").replace(`Error: ${error.message}\n`, ""))}`); } // vanity methods success(message, ...data) { this.log({ level: "info", action: "success", message }, ...data); } watch(message, ...data) { this.log({ level: "info", action: "watch", message }, ...data); } checkpointStart(message) { this.log({ level: "debug", action: "checkpoint:start", message }); } checkpointEnd() { this.log({ level: "debug", action: "checkpoint:end", message: "-- --" }); } }