mirror of
https://github.com/versia-pub/server.git
synced 2026-03-13 05:49:16 +01:00
refactor: ♻️ Replace logging system with @logtape/logtape
This commit is contained in:
parent
75992dfe62
commit
bc8220c8f9
28 changed files with 324 additions and 858 deletions
42
utils/api.ts
42
utils/api.ts
|
|
@ -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}`;
|
||||
}
|
||||
};
|
||||
|
|
|
|||
106
utils/init.ts
106
utils/init.ts
|
|
@ -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);
|
||||
|
|
|
|||
199
utils/loggers.ts
199
utils/loggers.ts
|
|
@ -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",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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 ||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue