echo/src/lib/config.ts
creations f313a8a329
All checks were successful
Code quality checks / biome (push) Successful in 9s
try fix for windows, change how logging works allows much more flexibility
2025-06-11 18:24:48 -04:00

315 lines
7.4 KiB
TypeScript

import { readFileSync } from "node:fs";
import { resolve } from "node:path";
import type { LogLevel, LoggerConfig } from "@types";
const logLevelValues = {
trace: 10,
debug: 20,
info: 30,
warn: 40,
error: 50,
fatal: 60,
silent: 70,
} as const;
const defaultLevelColor: Record<LogLevel, keyof typeof ansiColors> = {
trace: "cyan",
debug: "blue",
info: "green",
warn: "yellow",
error: "red",
fatal: "red",
silent: "gray",
};
const ansiColors: Record<string, string> = {
reset: "\x1b[0m",
dim: "\x1b[2m",
bright: "\x1b[1m",
black: "\x1b[30m",
red: "\x1b[31m",
green: "\x1b[32m",
yellow: "\x1b[33m",
blue: "\x1b[34m",
magenta: "\x1b[35m",
cyan: "\x1b[36m",
white: "\x1b[37m",
gray: "\x1b[90m",
bold: "\x1b[1m",
underline: "\x1b[4m",
inverse: "\x1b[7m",
hidden: "\x1b[8m",
strikethrough: "\x1b[9m",
} as const;
const defaultConfig: Required<LoggerConfig> = {
directory: "logs",
level: "info",
disableFile: false,
rotate: true,
maxFiles: null,
fileNameFormat: "yyyy-MM-dd",
console: true,
consoleColor: true,
dateFormat: "yyyy-MM-dd HH:mm:ss.SSS",
timezone: "local",
silent: false,
pattern:
"{color:gray}{pretty-timestamp}{reset} {color:levelColor}[{level-name}]{reset} {color:gray}({reset}{file-name}:{line}:{column}{color:gray}){reset} {data}",
levelColor: {
trace: "cyan",
debug: "blue",
info: "green",
warn: "yellow",
error: "red",
fatal: "red",
},
customColors: {},
customPattern:
"{color:gray}{pretty-timestamp}{reset} {color:tagColor}[{tag}]{reset} {color:contextColor}({context}){reset} {data}",
prettyPrint: true,
};
function isValidLogLevel(level: string): level is LogLevel {
return level in logLevelValues;
}
function isValidColor(color: string): color is keyof typeof ansiColors {
return color in ansiColors;
}
function parseNumericEnv(
value: string | undefined,
min = 0,
): number | undefined {
if (!value) return undefined;
const parsed = Number.parseInt(value, 10);
if (Number.isNaN(parsed) || parsed < min) {
return undefined;
}
return parsed;
}
function parseBooleanEnv(value: string | undefined): boolean | undefined {
if (!value) return undefined;
return value.toLowerCase() === "true";
}
function validateAndSanitizeConfig(
config: LoggerConfig,
source: string,
): LoggerConfig {
const sanitized = { ...config };
const warnings: string[] = [];
if (sanitized.level && !isValidLogLevel(sanitized.level)) {
warnings.push(
`Invalid log level "${sanitized.level}" in ${source}, using default "info"`,
);
sanitized.level = "info";
}
if (sanitized.maxFiles !== undefined && sanitized.maxFiles !== null) {
if (
typeof sanitized.maxFiles !== "number" ||
sanitized.maxFiles < 1 ||
!Number.isInteger(sanitized.maxFiles)
) {
warnings.push(
`Invalid maxFiles value "${sanitized.maxFiles}" in ${source}, setting to null`,
);
sanitized.maxFiles = null;
}
}
if (sanitized.levelColor) {
const validLevelColors: Partial<Record<LogLevel, keyof typeof ansiColors>> =
{};
for (const [level, color] of Object.entries(sanitized.levelColor)) {
if (!isValidLogLevel(level)) {
warnings.push(
`Invalid log level "${level}" in levelColor from ${source}, skipping`,
);
continue;
}
if (!isValidColor(color)) {
warnings.push(
`Invalid color "${color}" for level "${level}" in ${source}, using default`,
);
validLevelColors[level as LogLevel] =
defaultLevelColor[level as LogLevel];
} else {
validLevelColors[level as LogLevel] = color;
}
}
sanitized.levelColor = validLevelColors;
}
if (sanitized.customColors) {
const validCustomColors: Record<string, keyof typeof ansiColors> = {};
for (const [tag, color] of Object.entries(sanitized.customColors)) {
if (!isValidColor(color)) {
warnings.push(
`Invalid color "${color}" for tag "${tag}" in ${source}, skipping`,
);
continue;
}
validCustomColors[tag] = color;
}
sanitized.customColors = validCustomColors;
}
if (warnings.length > 0) {
console.warn(
`[@atums/echo] Configuration warnings:\n ${warnings.join("\n ")}`,
);
}
return sanitized;
}
function loadLoggerConfig(configPath = "logger.json"): LoggerConfig {
try {
const fullPath: string = resolve(process.cwd(), configPath);
const raw: string = readFileSync(fullPath, "utf-8");
const parsed = JSON.parse(raw);
if (typeof parsed !== "object" || parsed === null) {
console.warn(`[@atums/echo] Invalid config file format: ${configPath}`);
return {};
}
return validateAndSanitizeConfig(parsed, `config file "${configPath}"`);
} catch (error) {
if (error instanceof Error && !error.message.includes("ENOENT")) {
console.warn(
`[@atums/echo] Failed to load config file ${configPath}:`,
error.message,
);
}
return {};
}
}
function loadEnvConfig(): LoggerConfig {
const config: LoggerConfig = {};
if (process.env.LOG_LEVEL && isValidLogLevel(process.env.LOG_LEVEL)) {
config.level = process.env.LOG_LEVEL;
}
if (process.env.LOG_DIRECTORY) {
config.directory = process.env.LOG_DIRECTORY;
}
config.disableFile = parseBooleanEnv(process.env.LOG_DISABLE_FILE);
config.rotate = parseBooleanEnv(process.env.LOG_ROTATE);
config.console = parseBooleanEnv(process.env.LOG_CONSOLE);
config.consoleColor = parseBooleanEnv(process.env.LOG_CONSOLE_COLOR);
config.silent = parseBooleanEnv(process.env.LOG_SILENT);
config.prettyPrint = parseBooleanEnv(process.env.LOG_PRETTY_PRINT);
const maxFiles = parseNumericEnv(process.env.LOG_MAX_FILES, 1);
if (maxFiles !== undefined) {
config.maxFiles = maxFiles;
}
if (process.env.LOG_FILE_NAME_FORMAT) {
config.fileNameFormat = process.env.LOG_FILE_NAME_FORMAT;
}
if (process.env.LOG_DATE_FORMAT) {
config.dateFormat = process.env.LOG_DATE_FORMAT;
}
if (process.env.LOG_TIMEZONE) {
config.timezone = process.env.LOG_TIMEZONE;
}
if (process.env.LOG_PATTERN) {
config.pattern = process.env.LOG_PATTERN;
}
if (process.env.LOG_CUSTOM_PATTERN) {
config.customPattern = process.env.LOG_CUSTOM_PATTERN;
}
if (process.env.LOG_LEVEL_COLOR) {
const colors = process.env.LOG_LEVEL_COLOR.split(",");
const levelColor: Partial<Record<LogLevel, keyof typeof ansiColors>> = {};
for (const colorPair of colors) {
const [level, colorName] = colorPair.split(":");
if (
level &&
colorName &&
isValidLogLevel(level) &&
isValidColor(colorName)
) {
levelColor[level] = colorName;
} else {
console.warn(`[@atums/echo] Invalid level color pair: ${colorPair}`);
}
}
if (Object.keys(levelColor).length > 0) {
config.levelColor = levelColor;
}
}
if (process.env.LOG_CUSTOM_COLORS) {
const colors = process.env.LOG_CUSTOM_COLORS.split(",");
const customColors: Record<string, keyof typeof ansiColors> = {};
for (const colorPair of colors) {
const [tag, colorName] = colorPair.split(":");
if (tag && colorName && isValidColor(colorName)) {
customColors[tag] = colorName;
} else {
console.warn(`[@atums/echo] Invalid custom color pair: ${colorPair}`);
}
}
if (Object.keys(customColors).length > 0) {
config.customColors = customColors;
}
}
const sanitizedConfig = validateAndSanitizeConfig(
config,
"environment variables",
);
return Object.fromEntries(
Object.entries(sanitizedConfig).filter(([_, value]) => value !== undefined),
) as LoggerConfig;
}
export {
defaultConfig,
defaultLevelColor,
loadLoggerConfig,
loadEnvConfig,
logLevelValues,
ansiColors,
validateAndSanitizeConfig,
isValidLogLevel,
isValidColor,
};