refactor: ♻️ Replace logging system with @logtape/logtape

This commit is contained in:
Jesse Wierzbinski 2024-06-26 13:11:39 -10:00
parent 75992dfe62
commit bc8220c8f9
No known key found for this signature in database
28 changed files with 324 additions and 858 deletions

View file

@ -1,4 +1,5 @@
import { errorResponse } from "@/response";
import { getLogger } from "@logtape/logtape";
import { extractParams, verifySolution } from "altcha-lib";
import chalk from "chalk";
import { config } from "config-manager";
@ -27,7 +28,6 @@ import { type AuthData, getFromHeader } from "~/database/entities/user";
import { db } from "~/drizzle/db";
import { Challenges } from "~/drizzle/schema";
import type { User } from "~/packages/database-interface/user";
import { LogLevel, LogManager } from "~/packages/log-manager";
import type { ApiRouteMetadata, HttpVerb } from "~/types/api";
export const applyConfig = (routeMeta: ApiRouteMetadata) => {
@ -395,23 +395,27 @@ export const jsonOrForm = () => {
});
};
export const debugRequest = async (
req: Request,
logger = new LogManager(Bun.stdout),
) => {
export const debugRequest = async (req: Request) => {
const body = await req.clone().text();
await logger.log(
LogLevel.Debug,
"RequestDebugger",
`\n${chalk.green(req.method)} ${chalk.blue(req.url)}\n${chalk.bold(
"Hash",
)}: ${chalk.yellow(
new Bun.SHA256().update(body).digest("hex"),
)}\n${chalk.bold("Headers")}:\n${Array.from(req.headers.entries())
.map(
([key, value]) =>
` - ${chalk.cyan(key)}: ${chalk.white(value)}`,
)
.join("\n")}\n${chalk.bold("Body")}: ${chalk.gray(body)}`,
);
const logger = getLogger("server");
const urlAndMethod = `${chalk.green(req.method)} ${chalk.blue(req.url)}`;
const hash = `${chalk.bold("Hash")}: ${chalk.yellow(
new Bun.SHA256().update(body).digest("hex"),
)}`;
const headers = `${chalk.bold("Headers")}:\n${Array.from(
req.headers.entries(),
)
.map(([key, value]) => ` - ${chalk.cyan(key)}: ${chalk.white(value)}`)
.join("\n")}`;
const bodyLog = `${chalk.bold("Body")}: ${chalk.gray(body)}`;
if (config.logging.log_requests_verbose) {
logger.debug`${urlAndMethod}\n${hash}\n${headers}\n${bodyLog}`;
} else {
logger.debug`${urlAndMethod}`;
}
};

View file

@ -1,43 +1,27 @@
import { getLogger } from "@logtape/logtape";
import chalk from "chalk";
import type { Config } from "~/packages/config-manager";
import {
LogLevel,
type LogManager,
type MultiLogManager,
} from "~/packages/log-manager";
export const checkConfig = async (
config: Config,
logger: LogManager | MultiLogManager,
) => {
await checkOidcConfig(config, logger);
export const checkConfig = async (config: Config) => {
await checkOidcConfig(config);
await checkHttpProxyConfig(config, logger);
await checkHttpProxyConfig(config);
await checkChallengeConfig(config, logger);
await checkChallengeConfig(config);
};
const checkHttpProxyConfig = async (
config: Config,
logger: LogManager | MultiLogManager,
) => {
const checkHttpProxyConfig = async (config: Config) => {
const logger = getLogger("server");
if (config.http.proxy.enabled) {
if (!config.http.proxy.address) {
await logger.log(
LogLevel.Critical,
"Server",
"The HTTP proxy is enabled, but the proxy address is not set in the config",
);
logger.fatal`The HTTP proxy is enabled, but the proxy address is not set in the config`;
// Hang until Ctrl+C is pressed
await Bun.sleep(Number.POSITIVE_INFINITY);
}
await logger.log(
LogLevel.Info,
"Server",
`HTTP proxy enabled at ${chalk.gray(config.http.proxy.address)}, testing...`,
);
logger.info`HTTP proxy enabled at ${chalk.gray(config.http.proxy.address)}, testing...`;
// Test the proxy
const response = await fetch("https://api.ipify.org?format=json", {
@ -46,18 +30,10 @@ const checkHttpProxyConfig = async (
const ip = (await response.json()).ip;
await logger.log(
LogLevel.Info,
"Server",
`Your IPv4 address is ${chalk.gray(ip)}`,
);
logger.info`Your IPv4 address is ${chalk.gray(ip)}`;
if (!response.ok) {
await logger.log(
LogLevel.Critical,
"Server",
"The HTTP proxy is enabled, but the proxy address is not reachable",
);
logger.fatal`The HTTP proxy is enabled, but the proxy address is not reachable`;
// Hang until Ctrl+C is pressed
await Bun.sleep(Number.POSITIVE_INFINITY);
@ -65,25 +41,15 @@ const checkHttpProxyConfig = async (
}
};
const checkChallengeConfig = async (
config: Config,
logger: LogManager | MultiLogManager,
) => {
const checkChallengeConfig = async (config: Config) => {
const logger = getLogger("server");
if (
config.validation.challenges.enabled &&
!config.validation.challenges.key
) {
await logger.log(
LogLevel.Critical,
"Server",
"Challenges are enabled, but the challenge key is not set in the config",
);
await logger.log(
LogLevel.Critical,
"Server",
"Below is a generated key for you to copy in the config at validation.challenges.key",
);
logger.fatal`Challenges are enabled, but the challenge key is not set in the config`;
logger.fatal`Below is a generated key for you to copy in the config at validation.challenges.key`;
const key = await crypto.subtle.generateKey(
{
@ -98,32 +64,20 @@ const checkChallengeConfig = async (
const base64 = Buffer.from(exported).toString("base64");
await logger.log(
LogLevel.Critical,
"Server",
`Generated key: ${chalk.gray(base64)}`,
);
logger.fatal`Generated key: ${chalk.gray(base64)}`;
// Hang until Ctrl+C is pressed
await Bun.sleep(Number.POSITIVE_INFINITY);
}
};
const checkOidcConfig = async (
config: Config,
logger: LogManager | MultiLogManager,
) => {
const checkOidcConfig = async (config: Config) => {
const logger = getLogger("server");
if (!config.oidc.jwt_key) {
await logger.log(
LogLevel.Critical,
"Server",
"The JWT private key is not set in the config",
);
await logger.log(
LogLevel.Critical,
"Server",
"Below is a generated key for you to copy in the config at oidc.jwt_key",
);
logger.fatal`The JWT private key is not set in the config`;
logger.fatal`Below is a generated key for you to copy in the config at oidc.jwt_key`;
// Generate a key for them
const keys = await crypto.subtle.generateKey("Ed25519", true, [
"sign",
@ -138,11 +92,7 @@ const checkOidcConfig = async (
await crypto.subtle.exportKey("spki", keys.publicKey),
).toString("base64");
await logger.log(
LogLevel.Critical,
"Server",
chalk.gray(`${privateKey};${publicKey}`),
);
logger.fatal`Generated key: ${chalk.gray(`${privateKey};${publicKey}`)}`;
// Hang until Ctrl+C is pressed
await Bun.sleep(Number.POSITIVE_INFINITY);
@ -171,11 +121,7 @@ const checkOidcConfig = async (
.catch((e) => e as Error);
if (privateKey instanceof Error || publicKey instanceof Error) {
await logger.log(
LogLevel.Critical,
"Server",
"The JWT key could not be imported! You may generate a new one by removing the old one from the config and restarting the server (this will invalidate all current JWTs).",
);
logger.fatal`The JWT key could not be imported! You may generate a new one by removing the old one from the config and restarting the server (this will invalidate all current JWTs).`;
// Hang until Ctrl+C is pressed
await Bun.sleep(Number.POSITIVE_INFINITY);

View file

@ -1,20 +1,191 @@
import { LogManager, MultiLogManager } from "log-manager";
import {
appendFileSync,
closeSync,
existsSync,
mkdirSync,
openSync,
renameSync,
statSync,
} from "node:fs";
import {
type LogLevel,
type LogRecord,
configure,
getConsoleSink,
getLevelFilter,
} from "@logtape/logtape";
import chalk from "chalk";
import stripAnsi from "strip-ansi";
import { config } from "~/packages/config-manager";
const noColors = process.env.NO_COLORS === "true";
const noFancyDates = process.env.NO_FANCY_DATES === "true";
// HACK: This is a workaround for the lack of type exports in the Logtape package.
type RotatingFileSinkDriver<T> =
import("../node_modules/@logtape/logtape/logtape/sink").RotatingFileSinkDriver<T>;
const getBaseRotatingFileSink = (
await import("../node_modules/@logtape/logtape/logtape/sink")
).getRotatingFileSink;
const requestsLog = Bun.file(config.logging.storage.requests);
const isEntry = true;
const levelAbbreviations: Record<LogLevel, string> = {
debug: "DBG",
info: "INF",
warning: "WRN",
error: "ERR",
fatal: "FTL",
};
export const logger = new LogManager(
isEntry ? requestsLog : Bun.file("/dev/null"),
);
export function defaultTextFormatter(record: LogRecord): string {
const ts = new Date(record.timestamp);
let msg = "";
for (let i = 0; i < record.message.length; i++) {
if (i % 2 === 0) {
msg += record.message[i];
} else {
msg += Bun.inspect(stripAnsi(record.message[i] as string)).match(
/"(.*?)"/,
)?.[1];
}
}
const category = record.category.join("\xb7");
return `${ts.toISOString().replace("T", " ").replace("Z", " +00:00")} [${
levelAbbreviations[record.level]
}] ${category}: ${msg}\n`;
}
export const consoleLogger = new LogManager(
isEntry ? Bun.stdout : Bun.file("/dev/null"),
!noColors,
!noFancyDates,
);
/**
* A console formatter is a function that accepts a log record and returns
* an array of arguments to pass to {@link console.log}.
*
* @param record The log record to format.
* @returns The formatted log record, as an array of arguments for
* {@link console.log}.
*/
export type ConsoleFormatter = (record: LogRecord) => readonly unknown[];
export const dualLogger = new MultiLogManager([logger, consoleLogger]);
/**
* The styles for the log level in the console.
*/
const logLevelStyles: Record<LogLevel, (text: string) => string> = {
debug: chalk.white.bgGray,
info: chalk.black.bgWhite,
warning: chalk.black.bgYellow,
error: chalk.white.bgRed,
fatal: chalk.white.bgRedBright,
};
/**
* The default console formatter.
*
* @param record The log record to format.
* @returns The formatted log record, as an array of arguments for
* {@link console.log}.
*/
export function defaultConsoleFormatter(record: LogRecord): string[] {
const msg = record.message.join("");
const date = new Date(record.timestamp);
const time = `${date.getUTCHours().toString().padStart(2, "0")}:${date
.getUTCMinutes()
.toString()
.padStart(
2,
"0",
)}:${date.getUTCSeconds().toString().padStart(2, "0")}.${date
.getUTCMilliseconds()
.toString()
.padStart(3, "0")}`;
const formattedTime = chalk.gray(time);
const formattedLevel = logLevelStyles[record.level](
levelAbbreviations[record.level],
);
const formattedCategory = chalk.gray(record.category.join("\xb7"));
const formattedMsg = chalk.reset(msg);
return [
`${formattedTime} ${formattedLevel} ${formattedCategory} ${formattedMsg}`,
];
}
export const nodeDriver: RotatingFileSinkDriver<number> = {
openSync(path: string) {
return openSync(path, "a");
},
writeSync(fd, chunk) {
appendFileSync(fd, chunk, {
flush: true,
});
},
flushSync() {
// ...
},
closeSync(fd) {
closeSync(fd);
},
statSync(path) {
// If file does not exist, create it
if (!existsSync(path)) {
// Mkdir all directories in path
const dirs = path.split("/");
dirs.pop();
mkdirSync(dirs.join("/"), { recursive: true });
appendFileSync(path, "");
}
return statSync(path);
},
renameSync: renameSync,
};
export const configureLoggers = (silent = false) =>
configure({
sinks: {
console: getConsoleSink({
formatter: defaultConsoleFormatter,
}),
file: getBaseRotatingFileSink(config.logging.storage.requests, {
maxFiles: 10,
maxSize: 10 * 1024 * 1024,
formatter: defaultTextFormatter,
...nodeDriver,
}),
},
filters: {
configFilter: silent
? getLevelFilter(config.logging.log_level)
: getLevelFilter(null),
},
loggers: [
{
category: "server",
sinks: ["console", "file"],
filters: ["configFilter"],
},
{
category: "federation",
sinks: ["console", "file"],
filters: ["configFilter"],
},
{
category: ["federation", "inbox"],
sinks: ["console", "file"],
filters: ["configFilter"],
},
{
category: "database",
sinks: ["console", "file"],
filters: ["configFilter"],
},
{
category: "webfinger",
sinks: ["console", "file"],
filters: ["configFilter"],
},
{
category: "meilisearch",
sinks: ["console", "file"],
filters: ["configFilter"],
},
{
category: ["logtape", "meta"],
level: "error",
},
],
});

View file

@ -1,6 +1,5 @@
import { getLogger } from "@logtape/logtape";
import { markdownParse } from "~/database/entities/status";
import { LogLevel } from "~/packages/log-manager";
import { dualLogger } from "./loggers";
export const renderMarkdownInPath = async (
path: string,
@ -15,7 +14,7 @@ export const renderMarkdownInPath = async (
content =
(await markdownParse(
(await extendedDescriptionFile.text().catch(async (e) => {
await dualLogger.logError(LogLevel.Error, "Routes", e);
await getLogger("server").error`${e}`;
return "";
})) ||
defaultText ||

View file

@ -1,6 +1,6 @@
import { getLogger } from "@logtape/logtape";
import { config } from "config-manager";
import { count } from "drizzle-orm";
import { LogLevel, type LogManager, type MultiLogManager } from "log-manager";
import { Meilisearch } from "meilisearch";
import { db } from "~/drizzle/db";
import { Notes, Users } from "~/drizzle/schema";
@ -11,7 +11,8 @@ export const meilisearch = new Meilisearch({
apiKey: config.meilisearch.api_key,
});
export const connectMeili = async (logger: MultiLogManager | LogManager) => {
export const connectMeili = async () => {
const logger = getLogger("meilisearch");
if (!config.meilisearch.enabled) {
return;
}
@ -33,17 +34,9 @@ export const connectMeili = async (logger: MultiLogManager | LogManager) => {
.index(MeiliIndexType.Statuses)
.updateSearchableAttributes(["content"]);
await logger.log(
LogLevel.Info,
"Meilisearch",
"Connected to Meilisearch",
);
logger.info`Connected to Meilisearch`;
} else {
await logger.log(
LogLevel.Critical,
"Meilisearch",
"Error while connecting to Meilisearch",
);
logger.fatal`Error while connecting to Meilisearch`;
// Hang until Ctrl+C is pressed
await Bun.sleep(Number.POSITIVE_INFINITY);
}