kozukata-toa/src/lib/logger/logger.ts

196 lines
4.6 KiB
TypeScript

import { Config } from "lib/setup/config";
import SafeJSONStringify from "safe-json-stringify";
import { EscapeStringRegexp } from "utils/misc";
import winston, { format, transports } from "winston";
import "winston-daily-rotate-file";
const level = process.env.LOG_LEVEL ?? Config.LOGGER_CONFIG.LOG_LEVEL;
const formatExcessProperties = (meta: Record<string, unknown>, limit = false) => {
let i = 0;
for (const [key, val] of Object.entries(meta)) {
// this is probably fine
// eslint-disable-next-line cadence/no-instanceof
if (val instanceof Error) {
meta[key] = { message: val.message, stack: val.stack };
}
i++;
}
if (!i) {
return "";
}
const content = SafeJSONStringify(meta);
return ` ${limit ? StrCap(content) : content}`;
};
const formatExcessPropertiesNoStack = (
meta: Record<string, unknown>,
omitKeys: Array<string> = [],
limit = false
) => {
const realMeta: Record<string, unknown> = {};
for (const [key, val] of Object.entries(meta)) {
if (omitKeys.includes(key)) {
continue;
}
// this is probably fine
// eslint-disable-next-line cadence/no-instanceof
if (val instanceof Error) {
realMeta[key] = { message: val.message };
} else if (!key.startsWith("__") && !key.startsWith("!")) {
realMeta[key] = val;
}
}
if (Object.keys(realMeta).length === 0) {
return "";
}
const content = SafeJSONStringify(realMeta);
return ` ${limit ? StrCap(content) : content}`;
};
function StrCap(string: string) {
if (string.length > 300) {
return `${string.slice(0, 297)}...`;
}
return string;
}
const printf = format.printf(
({ level, message, context = "toa-root", timestamp, ...meta }) =>
`${timestamp} [${
Array.isArray(context) ? context.join(" | ") : context
}] ${level}: ${message}${formatExcessProperties(meta)}`
);
const consolePrintf = format.printf(
({ level, message, context = "toa-root", timestamp, hideFromConsole, ...meta }) =>
`${timestamp} [${
Array.isArray(context) ? context.join(" | ") : context
}] ${level}: ${message}${formatExcessPropertiesNoStack(
meta,
hideFromConsole as Array<string>,
true
)}`
);
winston.addColors({
crit: ["bgRed", "black"],
error: ["red"],
warn: ["yellow"],
info: ["blue"],
verbose: ["cyan"],
debug: ["white"],
});
const baseFormatter = format.combine(
format.timestamp({
format: "YYYY-MM-DD HH:mm:ss",
})
);
const defaultFormatter = format.combine(baseFormatter, format.errors({ stack: false }), printf);
const consoleFormatter = format.combine(
baseFormatter,
format.errors({ stack: false }),
consolePrintf,
format.colorize({
all: true,
})
);
const tports: Array<winston.transport> = [];
if (Config.LOGGER_CONFIG.CONSOLE || process.env.FORCE_CONSOLE_LOG) {
tports.push(
new transports.Console({
format: consoleFormatter,
})
);
}
if (Config.LOGGER_CONFIG.FOLDER) {
tports.push(
new transports.DailyRotateFile({
filename: `${Config.LOGGER_CONFIG.FOLDER}/toa-%DATE%.log`,
datePattern: "YYYY-MM-DD-HH",
zippedArchive: true,
maxSize: "20m",
maxFiles: "14d",
createSymlink: true,
symlinkName: "toa.log",
format: defaultFormatter,
})
);
}
export const rootLogger = winston.createLogger({
level,
format: defaultFormatter,
transports: tports,
defaultMeta: {
__ServerName: Config.NAME,
},
});
if (tports.length === 0) {
// eslint-disable-next-line no-console
console.warn(
"You have no transports set. Absolutely no logs will be saved. This is a terrible idea!"
);
}
export default function CreateLogCtx(filename: string, lg = rootLogger): winston.Logger {
const replacedFilename = filename.replace(
new RegExp(`^${EscapeStringRegexp(process.cwd())}[\\\\/]((js|src)[\\\\/])?`, "u"),
""
);
const logger = lg.child({
context: [replacedFilename],
});
// @hack, defaultMeta isn't reactive -- won't be updated unless we do this.
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
logger.defaultMeta = { ...(logger.defaultMeta ?? {}), context: [replacedFilename] };
return logger;
}
export function AppendLogCtx(context: string, lg: winston.Logger): winston.Logger {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
const newContext = [...lg.defaultMeta.context, context];
return lg.child({ context: newContext });
}
export function ChangeRootLogLevel(
level: "crit" | "debug" | "error" | "info" | "severe" | "verbose" | "warn"
) {
rootLogger.info(`Changing log level to ${level}.`);
for (const tp of tports) {
tp.level = level;
}
}
export function GetLogLevel() {
return (
tports.map((e) => e.level).find((e) => typeof e === "string") ??
Config.LOGGER_CONFIG.LOG_LEVEL
);
}
export const Transports = tports;