isoscribe
Version:
An isomorphic logging utility for any JavaScript runtime, providing structured, styled, and extensible logs.
203 lines (202 loc) • 7.5 kB
JavaScript
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: "-- --" });
}
}