diff --git a/build.ts b/build.ts index 41772e57..468f91ca 100644 --- a/build.ts +++ b/build.ts @@ -32,6 +32,9 @@ await $`sed -i 's|import("node_modules/|import("./node_modules/|g' dist/*.js`; await $`sed -i 's|import"node_modules/|import"./node_modules/|g' dist/**/*.js`; // Replace /temp/node_modules with ./node_modules await $`sed -i 's|/temp/node_modules|./node_modules|g' dist/**/*.js`; +// Replace 'export { toFilter, getLevelFilter, getConsoleSink };' to remove getConsoleSink +// Because Bun duplicates the export and it causes a runtime error +await $`sed -i 's|export { toFilter, getLevelFilter, getConsoleSink };|export { toFilter, getLevelFilter };|g' dist/**/*.js`; // Copy Drizzle migrations to dist await $`cp -r drizzle dist/drizzle`; diff --git a/bun.lockb b/bun.lockb index b913705c..febdb13a 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/cli/base.ts b/cli/base.ts index 85164f1a..063aa075 100644 --- a/cli/base.ts +++ b/cli/base.ts @@ -1,4 +1,3 @@ -import { consoleLogger } from "@/loggers"; import { Command } from "@oclif/core"; import { setupDatabase } from "~/drizzle/db"; @@ -6,6 +5,6 @@ export abstract class BaseCommand<_T extends typeof Command> extends Command { protected async init(): Promise { await super.init(); - await setupDatabase(consoleLogger, false); + await setupDatabase(false); } } diff --git a/config/config.example.toml b/config/config.example.toml index 5c30e270..fc9854dd 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -375,7 +375,7 @@ bio = [] log_requests = false # Log request and their contents (warning: this is a lot of data) log_requests_verbose = false -# Available levels: debug, info, warning, error, critical +# Available levels: debug, info, warning, error, fatal log_level = "debug" # For GDPR compliance, you can disable logging of IPs log_ip = false diff --git a/database/entities/federation.ts b/database/entities/federation.ts index 12e2c53f..1b300a7d 100644 --- a/database/entities/federation.ts +++ b/database/entities/federation.ts @@ -1,9 +1,9 @@ import { debugRequest } from "@/api"; +import { getLogger } from "@logtape/logtape"; import { SignatureConstructor } from "@lysand-org/federation"; import type { Entity, Undo } from "@lysand-org/federation/types"; import { config } from "config-manager"; import type { User } from "~/packages/database-interface/user"; -import { LogLevel, LogManager } from "~/packages/log-manager"; export const localObjectUri = (id: string) => new URL(`/objects/${id}`, config.http.base_url).toString(); @@ -48,19 +48,13 @@ export const objectToInboxRequest = async ( // Debug request await debugRequest(signed); + const logger = getLogger("federation"); + // Log public key - new LogManager(Bun.stdout).log( - LogLevel.Debug, - "Inbox.Signature", - `Sender public key: ${author.data.publicKey}`, - ); + logger.debug`Sender public key: ${author.data.publicKey}`; // Log signed string - new LogManager(Bun.stdout).log( - LogLevel.Debug, - "Inbox.Signature", - `Signed string:\n${signedString}`, - ); + logger.debug`Signed string:\n${signedString}`; } return signed; diff --git a/database/entities/status.ts b/database/entities/status.ts index cddea61d..7969532e 100644 --- a/database/entities/status.ts +++ b/database/entities/status.ts @@ -1,7 +1,7 @@ import { mentionValidator } from "@/api"; -import { dualLogger } from "@/loggers"; import { sanitizeHtml, sanitizeHtmlInline } from "@/sanitization"; import markdownItTaskLists from "@hackmd/markdown-it-task-lists"; +import { getLogger } from "@logtape/logtape"; import type { ContentFormat } from "@lysand-org/federation/types"; import { config } from "config-manager"; import { @@ -35,7 +35,6 @@ import { } from "~/drizzle/schema"; import type { Note } from "~/packages/database-interface/note"; import { User } from "~/packages/database-interface/user"; -import { LogLevel } from "~/packages/log-manager"; import type { Application } from "./application"; import type { EmojiWithInstance } from "./emoji"; import { objectToInboxRequest } from "./federation"; @@ -453,16 +452,10 @@ export const federateNote = async (note: Note) => { }); if (!response.ok) { - dualLogger.log( - LogLevel.Debug, - "Federation.Status", - await response.text(), - ); - dualLogger.log( - LogLevel.Error, - "Federation.Status", - `Failed to federate status ${note.data.id} to ${user.getUri()}`, - ); + const logger = getLogger("federation"); + + logger.debug`${await response.text()}`; + logger.error`Failed to federate status ${note.data.id} to ${user.getUri()}`; } } }; diff --git a/database/entities/user.ts b/database/entities/user.ts index 79452960..816662c9 100644 --- a/database/entities/user.ts +++ b/database/entities/user.ts @@ -1,4 +1,4 @@ -import { dualLogger } from "@/loggers"; +import { getLogger } from "@logtape/logtape"; import type { Follow, FollowAccept, @@ -17,7 +17,6 @@ import { Users, } from "~/drizzle/schema"; import { User } from "~/packages/database-interface/user"; -import { LogLevel } from "~/packages/log-manager"; import type { Application } from "./application"; import type { EmojiWithInstance } from "./emoji"; import { objectToInboxRequest } from "./federation"; @@ -180,19 +179,10 @@ export const followRequestUser = async ( }); if (!response.ok) { - dualLogger.log( - LogLevel.Debug, - "Federation.FollowRequest", - await response.text(), - ); + const logger = getLogger("federation"); - dualLogger.log( - LogLevel.Error, - "Federation.FollowRequest", - `Failed to federate follow request from ${ - follower.id - } to ${followee.getUri()}`, - ); + logger.debug`${await response.text()}`; + logger.error`Failed to federate follow request from ${follower.id} to ${followee.getUri()}`; await db .update(Relationships) @@ -237,19 +227,10 @@ export const sendFollowAccept = async (follower: User, followee: User) => { }); if (!response.ok) { - dualLogger.log( - LogLevel.Debug, - "Federation.FollowAccept", - await response.text(), - ); + const logger = getLogger("federation"); - dualLogger.log( - LogLevel.Error, - "Federation.FollowAccept", - `Failed to federate follow accept from ${ - followee.id - } to ${follower.getUri()}`, - ); + logger.debug`${await response.text()}`; + logger.error`Failed to federate follow accept from ${followee.id} to ${follower.getUri()}`; } }; @@ -267,19 +248,10 @@ export const sendFollowReject = async (follower: User, followee: User) => { }); if (!response.ok) { - dualLogger.log( - LogLevel.Debug, - "Federation.FollowReject", - await response.text(), - ); + const logger = getLogger("federation"); - dualLogger.log( - LogLevel.Error, - "Federation.FollowReject", - `Failed to federate follow reject from ${ - followee.id - } to ${follower.getUri()}`, - ); + logger.debug`${await response.text()}`; + logger.error`Failed to federate follow reject from ${followee.id} to ${follower.getUri()}`; } }; diff --git a/drizzle/db.ts b/drizzle/db.ts index 0a8ca67f..8f4ae087 100644 --- a/drizzle/db.ts +++ b/drizzle/db.ts @@ -1,6 +1,6 @@ +import { getLogger } from "@logtape/logtape"; import { drizzle } from "drizzle-orm/node-postgres"; import { migrate } from "drizzle-orm/postgres-js/migrator"; -import { LogLevel, LogManager, type MultiLogManager } from "log-manager"; import { Client } from "pg"; import { config } from "~/packages/config-manager"; import * as schema from "./schema"; @@ -13,10 +13,9 @@ export const client = new Client({ database: config.database.database, }); -export const setupDatabase = async ( - logger: LogManager | MultiLogManager = new LogManager(Bun.stdout), - info = true, -) => { +export const setupDatabase = async (info = true) => { + const logger = getLogger("database"); + try { await client.connect(); } catch (e) { @@ -27,39 +26,29 @@ export const setupDatabase = async ( return; } - await logger.logError(LogLevel.Critical, "Database", e as Error); - - await logger.log( - LogLevel.Critical, - "Database", - "Failed to connect to database. Please check your configuration.", - ); + logger.fatal`${e}`; + logger.fatal`Failed to connect to database. Please check your configuration.`; // Hang until Ctrl+C is pressed await Bun.sleep(Number.POSITIVE_INFINITY); } // Migrate the database - info && - (await logger.log(LogLevel.Info, "Database", "Migrating database...")); + info && logger.info`Migrating database...`; try { await migrate(db, { migrationsFolder: "./drizzle/migrations", }); } catch (e) { - await logger.logError(LogLevel.Critical, "Database", e as Error); - await logger.log( - LogLevel.Critical, - "Database", - "Failed to migrate database. Please check your configuration.", - ); + logger.fatal`${e}`; + logger.fatal`Failed to migrate database. Please check your configuration.`; // Hang until Ctrl+C is pressed await Bun.sleep(Number.POSITIVE_INFINITY); } - info && (await logger.log(LogLevel.Info, "Database", "Database migrated")); + info && logger.info`Database migrated`; }; export const db = drizzle(client, { schema }); diff --git a/index.ts b/index.ts index a0987bb9..b7556b1c 100644 --- a/index.ts +++ b/index.ts @@ -1,10 +1,10 @@ import { checkConfig } from "@/init"; -import { dualLogger } from "@/loggers"; +import { configureLoggers } from "@/loggers"; import { connectMeili } from "@/meilisearch"; import { errorResponse, response } from "@/response"; +import { getLogger } from "@logtape/logtape"; import { config } from "config-manager"; import { Hono } from "hono"; -import { LogLevel, LogManager, type MultiLogManager } from "log-manager"; import { setupDatabase } from "~/drizzle/db"; import { agentBans } from "~/middlewares/agent-bans"; import { bait } from "~/middlewares/bait"; @@ -21,21 +21,16 @@ const timeAtStart = performance.now(); const isEntry = import.meta.path === Bun.main && !process.argv.includes("--silent"); +await configureLoggers(isEntry); -let dualServerLogger: LogManager | MultiLogManager = new LogManager( - Bun.file("/dev/null"), -); +const serverLogger = getLogger("server"); -if (isEntry) { - dualServerLogger = dualLogger; -} +serverLogger.info`Starting Lysand...`; -await dualServerLogger.log(LogLevel.Info, "Lysand", "Starting Lysand..."); - -await setupDatabase(dualServerLogger); +await setupDatabase(); if (config.meilisearch.enabled) { - await connectMeili(dualServerLogger); + await connectMeili(); } process.on("SIGINT", () => { @@ -46,7 +41,7 @@ process.on("SIGINT", () => { const postCount = await Note.getCount(); if (isEntry) { - await checkConfig(config, dualServerLogger); + await checkConfig(config); } const app = new Hono({ @@ -79,7 +74,7 @@ app.options("*", () => { app.all("*", async (context) => { if (config.frontend.glitch.enabled) { - const glitch = await handleGlitchRequest(context.req.raw, dualLogger); + const glitch = await handleGlitchRequest(context.req.raw); if (glitch) { return glitch; @@ -91,11 +86,7 @@ app.all("*", async (context) => { config.frontend.url, ).toString(); - await dualLogger.log( - LogLevel.Debug, - "Server.Proxy", - `Proxying ${replacedUrl}`, - ); + serverLogger.debug`Proxying ${replacedUrl}`; const proxy = await fetch(replacedUrl, { headers: { @@ -104,13 +95,9 @@ app.all("*", async (context) => { "Accept-Encoding": "identity", }, redirect: "manual", - }).catch(async (e) => { - await dualLogger.logError(LogLevel.Error, "Server.Proxy", e as Error); - await dualLogger.log( - LogLevel.Error, - "Server.Proxy", - `The Frontend is not running or the route is not found: ${replacedUrl}`, - ); + }).catch((e) => { + serverLogger.error`${e}`; + serverLogger.error`The Frontend is not running or the route is not found: ${replacedUrl}`; return null; }); @@ -138,25 +125,13 @@ app.all("*", async (context) => { createServer(config, app); -await dualServerLogger.log( - LogLevel.Info, - "Server", - `Lysand started at ${config.http.bind}:${config.http.bind_port} in ${(performance.now() - timeAtStart).toFixed(0)}ms`, -); +serverLogger.info`Lysand started at ${config.http.bind}:${config.http.bind_port} in ${(performance.now() - timeAtStart).toFixed(0)}ms`; -await dualServerLogger.log( - LogLevel.Info, - "Database", - `Database is online, now serving ${postCount} posts`, -); +serverLogger.info`Database is online, now serving ${postCount} posts`; if (config.frontend.enabled) { if (!URL.canParse(config.frontend.url)) { - await dualServerLogger.log( - LogLevel.Error, - "Server", - `Frontend URL is not a valid URL: ${config.frontend.url}`, - ); + serverLogger.error`Frontend URL is not a valid URL: ${config.frontend.url}`; // Hang until Ctrl+C is pressed await Bun.sleep(Number.POSITIVE_INFINITY); } @@ -167,23 +142,11 @@ if (config.frontend.enabled) { .catch(() => false); if (!response) { - await dualServerLogger.log( - LogLevel.Error, - "Server", - `Frontend is unreachable at ${config.frontend.url}`, - ); - await dualServerLogger.log( - LogLevel.Error, - "Server", - "Please ensure the frontend is online and reachable", - ); + serverLogger.error`Frontend is unreachable at ${config.frontend.url}`; + serverLogger.error`Please ensure the frontend is online and reachable`; } } else { - await dualServerLogger.log( - LogLevel.Warning, - "Server", - "Frontend is disabled, skipping check", - ); + serverLogger.warn`Frontend is disabled, skipping check`; } export { app }; diff --git a/middlewares/bait.ts b/middlewares/bait.ts index 9c08ea2e..d09c1db4 100644 --- a/middlewares/bait.ts +++ b/middlewares/bait.ts @@ -1,10 +1,9 @@ -import { logger } from "@/loggers"; import { response } from "@/response"; +import { getLogger } from "@logtape/logtape"; import type { SocketAddress } from "bun"; import { createMiddleware } from "hono/factory"; import { matches } from "ip-matching"; import { config } from "~/packages/config-manager"; -import { LogLevel } from "~/packages/log-manager"; const baitFile = async () => { const file = Bun.file(config.http.bait.send_file || "./beemovie.txt"); @@ -13,11 +12,9 @@ const baitFile = async () => { return file; } - await logger.log( - LogLevel.Error, - "Server.Bait", - `Bait file not found: ${config.http.bait.send_file}`, - ); + const logger = getLogger("server"); + + logger.error`Bait file not found: ${config.http.bait.send_file}`; }; export const bait = createMiddleware(async (context, next) => { diff --git a/middlewares/ip-bans.ts b/middlewares/ip-bans.ts index ff875154..6540b80e 100644 --- a/middlewares/ip-bans.ts +++ b/middlewares/ip-bans.ts @@ -1,10 +1,9 @@ -import { logger } from "@/loggers"; import { errorResponse } from "@/response"; +import { getLogger } from "@logtape/logtape"; import type { SocketAddress } from "bun"; import { createMiddleware } from "hono/factory"; import { matches } from "ip-matching"; import { config } from "~/packages/config-manager"; -import { LogLevel } from "~/packages/log-manager"; export const ipBans = createMiddleware(async (context, next) => { // Check for banned IPs @@ -22,12 +21,10 @@ export const ipBans = createMiddleware(async (context, next) => { return errorResponse("Forbidden", 403); } } catch (e) { - logger.log( - LogLevel.Error, - "Server.IPCheck", - `Error while parsing banned IP "${ip}" `, - ); - logger.logError(LogLevel.Error, "Server.IPCheck", e as Error); + const logger = getLogger("server"); + + logger.error`Error while parsing banned IP "${ip}" `; + logger.error`${e}`; return errorResponse( `A server error occured: ${(e as Error).message}`, diff --git a/middlewares/logger.ts b/middlewares/logger.ts index c1503adb..5ee876ca 100644 --- a/middlewares/logger.ts +++ b/middlewares/logger.ts @@ -1,17 +1,10 @@ -import { dualLogger } from "@/loggers"; -import type { SocketAddress } from "bun"; +import { debugRequest } from "@/api"; import { createMiddleware } from "hono/factory"; import { config } from "~/packages/config-manager"; export const logger = createMiddleware(async (context, next) => { - const requestIp = context.env?.ip as SocketAddress | undefined | null; - if (config.logging.log_requests) { - await dualLogger.logRequest( - context.req.raw, - config.logging.log_ip ? requestIp?.address : undefined, - config.logging.log_requests_verbose, - ); + await debugRequest(context.req.raw); } await next(); diff --git a/package.json b/package.json index bfc98f12..97e91973 100644 --- a/package.json +++ b/package.json @@ -100,6 +100,7 @@ "@inquirer/confirm": "^3.1.10", "@inquirer/input": "^2.1.10", "@json2csv/plainjs": "^7.0.6", + "@logtape/logtape": "npm:@jsr/logtape__logtape", "@lysand-org/federation": "^2.0.0", "@oclif/core": "^4.0.6", "@tufjs/canonical-json": "^2.0.0", @@ -121,7 +122,6 @@ "linkify-html": "^4.1.3", "linkify-string": "^4.1.3", "linkifyjs": "^4.1.3", - "log-manager": "workspace:*", "magic-regexp": "^0.8.0", "markdown-it": "^14.1.0", "markdown-it-anchor": "^9.0.1", @@ -137,6 +137,7 @@ "sharp": "^0.33.4", "string-comparison": "^1.3.0", "stringify-entities": "^4.0.4", + "strip-ansi": "^7.1.0", "table": "^6.8.2", "unzipit": "^1.4.3", "uqr": "^0.1.2", diff --git a/packages/config-manager/config.type.ts b/packages/config-manager/config.type.ts index 01b85a29..7bc4867e 100644 --- a/packages/config-manager/config.type.ts +++ b/packages/config-manager/config.type.ts @@ -546,7 +546,7 @@ export const configValidator = z.object({ log_requests: z.boolean().default(false), log_requests_verbose: z.boolean().default(false), log_level: z - .enum(["debug", "info", "warning", "error", "critical"]) + .enum(["debug", "info", "warning", "error", "fatal"]) .default("info"), log_ip: z.boolean().default(false), log_filters: z.boolean().default(true), diff --git a/packages/database-interface/note.ts b/packages/database-interface/note.ts index ce74d44b..f21b49e7 100644 --- a/packages/database-interface/note.ts +++ b/packages/database-interface/note.ts @@ -1,7 +1,7 @@ import { idValidator } from "@/api"; -import { dualLogger } from "@/loggers"; import { proxyUrl } from "@/response"; import { sanitizedHtmlStrip } from "@/sanitization"; +import { getLogger } from "@logtape/logtape"; import { EntityValidator } from "@lysand-org/federation"; import type { ContentFormat, @@ -19,7 +19,6 @@ import { sql, } from "drizzle-orm"; import { htmlToText } from "html-to-text"; -import { LogLevel } from "log-manager"; import { createRegExp, exactly, global } from "magic-regexp"; import { type Application, @@ -622,16 +621,13 @@ export class Note extends BaseInterface { */ static async fromLysand(note: LysandNote, author: User): Promise { const emojis: Emoji[] = []; + const logger = getLogger("federation"); for (const emoji of note.extensions?.["org.lysand:custom_emojis"] ?.emojis ?? []) { const resolvedEmoji = await Emoji.fetchFromRemote(emoji).catch( (e) => { - dualLogger.logError( - LogLevel.Error, - "Federation.StatusResolver", - e, - ); + logger.error`${e}`; return null; }, ); @@ -647,11 +643,7 @@ export class Note extends BaseInterface { const resolvedAttachment = await Attachment.fromLysand( attachment, ).catch((e) => { - dualLogger.logError( - LogLevel.Error, - "Federation.StatusResolver", - e, - ); + logger.error`${e}`; return null; }); diff --git a/packages/glitch-server/main.ts b/packages/glitch-server/main.ts index 15f23982..23cb3433 100644 --- a/packages/glitch-server/main.ts +++ b/packages/glitch-server/main.ts @@ -4,7 +4,6 @@ import type { BunFile } from "bun"; import { config } from "config-manager"; import { retrieveUserFromToken } from "~/database/entities/user"; import type { User } from "~/packages/database-interface/user"; -import type { LogManager, MultiLogManager } from "~/packages/log-manager"; import { languages } from "./glitch-languages"; const handleManifestRequest = () => { @@ -327,7 +326,6 @@ const htmlTransforms = async ( export const handleGlitchRequest = async ( req: Request, - _logger: LogManager | MultiLogManager, ): Promise => { const url = new URL(req.url); let path = url.pathname; diff --git a/packages/log-manager/index.ts b/packages/log-manager/index.ts deleted file mode 100644 index 86e3e8a3..00000000 --- a/packages/log-manager/index.ts +++ /dev/null @@ -1,287 +0,0 @@ -import { appendFile, exists, mkdir } from "node:fs/promises"; -import { dirname } from "node:path"; -import type { BunFile } from "bun"; -import chalk from "chalk"; -import { config } from "config-manager"; - -export enum LogLevel { - Debug = "debug", - Info = "info", - Warning = "warning", - Error = "error", - Critical = "critical", -} - -const logOrder = [ - LogLevel.Debug, - LogLevel.Info, - LogLevel.Warning, - LogLevel.Error, - LogLevel.Critical, -]; - -/** - * Class for handling logging to disk or to stdout - * @param output BunFile of output (can be a normal file or something like Bun.stdout) - */ -export class LogManager { - constructor( - private output: BunFile, - private enableColors = false, - private prettyDates = false, - ) { - /* void this.write( - `--- INIT LogManager at ${new Date().toISOString()} ---`, - ); */ - } - - getLevelColor(level: LogLevel) { - switch (level) { - case LogLevel.Debug: - return chalk.blue; - case LogLevel.Info: - return chalk.green; - case LogLevel.Warning: - return chalk.yellow; - case LogLevel.Error: - return chalk.red; - case LogLevel.Critical: - return chalk.bgRed; - } - } - - getFormattedDate(date: Date = new Date()) { - return this.prettyDates - ? date.toLocaleString(undefined, { - year: "numeric", - month: "2-digit", - day: "2-digit", - hour: "2-digit", - minute: "2-digit", - second: "2-digit", - }) - : date.toISOString(); - } - - /** - * Logs a message to the output - * @param level Importance of the log - * @param entity Emitter of the log - * @param message Message to log - * @param showTimestamp Whether to show the timestamp in the log - */ - async log( - level: LogLevel, - entity: string, - message: string, - showTimestamp = true, - ) { - if ( - logOrder.indexOf(level) < - logOrder.indexOf(config.logging.log_level as LogLevel) - ) { - return; - } - - if (this.enableColors) { - await this.write( - `${ - showTimestamp - ? `${chalk.gray(this.getFormattedDate())} ` - : "" - }[${this.getLevelColor(level)( - level.toUpperCase(), - )}] ${chalk.bold(entity)}: ${message}`, - ); - } else { - await this.write( - `${ - showTimestamp ? `${this.getFormattedDate()} ` : "" - }[${level.toUpperCase()}] ${entity}: ${message}`, - ); - } - } - - private async write(text: string) { - if (this.output === Bun.stdout) { - console.info(text); - } else { - if (!(await exists(this.output.name ?? ""))) { - // Create file if it doesn't exist - try { - await mkdir(dirname(this.output.name ?? ""), { - recursive: true, - }); - this.output = Bun.file(this.output.name ?? ""); - } catch { - // - } - } - await appendFile(this.output.name ?? "", `${text}\n`); - } - } - - /** - * Logs an error to the output, wrapper for log - * @param level Importance of the log - * @param entity Emitter of the log - * @param error Error to log - */ - async logError(level: LogLevel, entity: string, error: Error) { - error.stack && (await this.log(LogLevel.Debug, entity, error.stack)); - await this.log(level, entity, error.message); - } - - /** - * Logs the headers of a request - * @param req Request to log - */ - public logHeaders(req: Request): string { - let string = " [Headers]\n"; - for (const [key, value] of req.headers.entries()) { - string += ` ${key}: ${value}\n`; - } - return string; - } - - /** - * Logs the body of a request - * @param req Request to log - */ - async logBody(req: Request): Promise { - let string = " [Body]\n"; - const contentType = req.headers.get("Content-Type"); - - if (contentType?.includes("application/json")) { - string += await this.logJsonBody(req); - } else if ( - contentType && - (contentType.includes("application/x-www-form-urlencoded") || - contentType.includes("multipart/form-data")) - ) { - string += await this.logFormData(req); - } else { - const text = await req.text(); - string += ` ${text}\n`; - } - return string; - } - - /** - * Logs the JSON body of a request - * @param req Request to log - */ - async logJsonBody(req: Request): Promise { - let string = ""; - try { - const json = await req.clone().json(); - const stringified = JSON.stringify(json, null, 4) - .split("\n") - .map((line) => ` ${line}`) - .join("\n"); - - string += `${stringified}\n`; - } catch { - string += ` [Invalid JSON] (raw: ${await req.clone().text()})\n`; - } - return string; - } - - /** - * Logs the form data of a request - * @param req Request to log - */ - async logFormData(req: Request): Promise { - let string = ""; - const formData = await req.clone().formData(); - for (const [key, value] of formData.entries()) { - if (value.toString().length < 300) { - string += ` ${key}: ${value.toString()}\n`; - } else { - string += ` ${key}: <${value.toString().length} bytes>\n`; - } - } - return string; - } - - /** - * Logs a request to the output - * @param req Request to log - * @param ip IP of the request - * @param logAllDetails Whether to log all details of the request - */ - async logRequest( - req: Request, - ip?: string, - logAllDetails = false, - ): Promise { - let string = ip ? `${ip}: ` : ""; - - string += `${req.method} ${req.url}`; - - if (logAllDetails) { - string += "\n"; - string += await this.logHeaders(req); - string += await this.logBody(req); - } - await this.log(LogLevel.Info, "Request", string); - } -} - -/** - * Outputs to multiple LogManager instances at once - */ -export class MultiLogManager { - constructor(private logManagers: LogManager[]) {} - - /** - * Logs a message to all logManagers - * @param level Importance of the log - * @param entity Emitter of the log - * @param message Message to log - * @param showTimestamp Whether to show the timestamp in the log - */ - async log( - level: LogLevel, - entity: string, - message: string, - showTimestamp = true, - ) { - for (const logManager of this.logManagers) { - await logManager.log(level, entity, message, showTimestamp); - } - } - - /** - * Logs an error to all logManagers - * @param level Importance of the log - * @param entity Emitter of the log - * @param error Error to log - */ - async logError(level: LogLevel, entity: string, error: Error) { - for (const logManager of this.logManagers) { - await logManager.logError(level, entity, error); - } - } - - /** - * Logs a request to all logManagers - * @param req Request to log - * @param ip IP of the request - * @param logAllDetails Whether to log all details of the request - */ - async logRequest(req: Request, ip?: string, logAllDetails = false) { - for (const logManager of this.logManagers) { - await logManager.logRequest(req, ip, logAllDetails); - } - } - - /** - * Create a MultiLogManager from multiple LogManager instances - * @param logManagers LogManager instances to use - * @returns - */ - static fromLogManagers(...logManagers: LogManager[]) { - return new MultiLogManager(logManagers); - } -} diff --git a/packages/log-manager/package.json b/packages/log-manager/package.json deleted file mode 100644 index 2cc02e72..00000000 --- a/packages/log-manager/package.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "log-manager", - "version": "0.0.0", - "main": "index.ts", - "dependencies": {} -} diff --git a/packages/log-manager/tests/log-manager.test.ts b/packages/log-manager/tests/log-manager.test.ts deleted file mode 100644 index a4278378..00000000 --- a/packages/log-manager/tests/log-manager.test.ts +++ /dev/null @@ -1,222 +0,0 @@ -import { - type Mock, - beforeEach, - describe, - expect, - it, - jest, - mock, - test, -} from "bun:test"; -import type fs from "node:fs/promises"; -import type { BunFile } from "bun"; -// FILEPATH: /home/jessew/Dev/lysand/packages/log-manager/log-manager.test.ts -import { LogLevel, LogManager, MultiLogManager } from "../index"; - -describe("LogManager", () => { - let logManager: LogManager; - let mockOutput: BunFile; - let mockAppend: Mock; - - beforeEach(async () => { - mockOutput = Bun.file("test.log"); - mockAppend = jest.fn(); - await mock.module("node:fs/promises", () => ({ - appendFile: mockAppend, - })); - logManager = new LogManager(mockOutput); - }); - - /* it("should initialize and write init log", () => { - new LogManager(mockOutput); - expect(mockAppend).toHaveBeenCalledWith( - mockOutput.name, - expect.stringContaining("--- INIT LogManager at"), - ); - }); - */ - it("should log message with timestamp", async () => { - await logManager.log(LogLevel.Info, "TestEntity", "Test message"); - expect(mockAppend).toHaveBeenCalledWith( - mockOutput.name, - expect.stringContaining("[INFO] TestEntity: Test message"), - ); - }); - - it("should log message without timestamp", async () => { - await logManager.log( - LogLevel.Info, - "TestEntity", - "Test message", - false, - ); - expect(mockAppend).toHaveBeenCalledWith( - mockOutput.name, - "[INFO] TestEntity: Test message\n", - ); - }); - - // biome-ignore lint/suspicious/noSkippedTests: I need to fix this :sob: - test.skip("should write to stdout", async () => { - logManager = new LogManager(Bun.stdout); - await logManager.log(LogLevel.Info, "TestEntity", "Test message"); - - const writeMock = jest.fn(); - - await mock.module("Bun", () => ({ - stdout: Bun.stdout, - write: writeMock, - })); - - expect(writeMock).toHaveBeenCalledWith( - Bun.stdout, - expect.stringContaining("[INFO] TestEntity: Test message"), - ); - }); - - it("should log error message", async () => { - const error = new Error("Test error"); - await logManager.logError(LogLevel.Error, "TestEntity", error); - expect(mockAppend).toHaveBeenCalledWith( - mockOutput.name, - expect.stringContaining("[ERROR] TestEntity: Test error"), - ); - }); - - it("should log basic request details", async () => { - const req = new Request("http://localhost/test", { method: "GET" }); - await logManager.logRequest(req, "127.0.0.1"); - - expect(mockAppend).toHaveBeenCalledWith( - mockOutput.name, - expect.stringContaining("127.0.0.1: GET http://localhost/test"), - ); - }); - - describe("Request logger", () => { - it("should log all request details for JSON content type", async () => { - const req = new Request("http://localhost/test", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ test: "value" }), - }); - await logManager.logRequest(req, "127.0.0.1", true); - - const expectedLog = `127.0.0.1: POST http://localhost/test - [Headers] - content-type: application/json - [Body] - { - "test": "value" - } -`; - - expect(mockAppend).toHaveBeenCalledWith( - mockOutput.name, - expect.stringContaining(expectedLog), - ); - }); - - it("should log all request details for text content type", async () => { - const req = new Request("http://localhost/test", { - method: "POST", - headers: { "Content-Type": "text/plain" }, - body: "Test body", - }); - await logManager.logRequest(req, "127.0.0.1", true); - - const expectedLog = `127.0.0.1: POST http://localhost/test - [Headers] - content-type: text/plain - [Body] - Test body -`; - expect(mockAppend).toHaveBeenCalledWith( - mockOutput.name, - expect.stringContaining(expectedLog), - ); - }); - - it("should log all request details for FormData content-type", async () => { - const formData = new FormData(); - formData.append("test", "value"); - const req = new Request("http://localhost/test", { - method: "POST", - body: formData, - }); - await logManager.logRequest(req, "127.0.0.1", true); - - const expectedLog = `127.0.0.1: POST http://localhost/test - [Headers] - content-type: multipart/form-data; boundary=${ - req.headers.get("Content-Type")?.split("boundary=")[1] ?? "" - } - [Body] - test: value -`; - - expect(mockAppend).toHaveBeenCalledWith( - mockOutput.name, - expect.stringContaining( - expectedLog.replace("----", expect.any(String)), - ), - ); - }); - }); -}); - -describe("MultiLogManager", () => { - let multiLogManager: MultiLogManager; - let mockLogManagers: LogManager[]; - let mockLog: jest.Mock; - let mockLogError: jest.Mock; - let mockLogRequest: jest.Mock; - - beforeEach(() => { - mockLog = jest.fn(); - mockLogError = jest.fn(); - mockLogRequest = jest.fn(); - mockLogManagers = [ - { - log: mockLog, - logError: mockLogError, - logRequest: mockLogRequest, - }, - { - log: mockLog, - logError: mockLogError, - logRequest: mockLogRequest, - }, - ] as unknown as LogManager[]; - multiLogManager = MultiLogManager.fromLogManagers(...mockLogManagers); - }); - - it("should log message to all logManagers", async () => { - await multiLogManager.log(LogLevel.Info, "TestEntity", "Test message"); - expect(mockLog).toHaveBeenCalledTimes(2); - expect(mockLog).toHaveBeenCalledWith( - LogLevel.Info, - "TestEntity", - "Test message", - true, - ); - }); - - it("should log error to all logManagers", async () => { - const error = new Error("Test error"); - await multiLogManager.logError(LogLevel.Error, "TestEntity", error); - expect(mockLogError).toHaveBeenCalledTimes(2); - expect(mockLogError).toHaveBeenCalledWith( - LogLevel.Error, - "TestEntity", - error, - ); - }); - - it("should log request to all logManagers", async () => { - const req = new Request("http://localhost/test", { method: "GET" }); - await multiLogManager.logRequest(req, "127.0.0.1", true); - expect(mockLogRequest).toHaveBeenCalledTimes(2); - expect(mockLogRequest).toHaveBeenCalledWith(req, "127.0.0.1", true); - }); -}); diff --git a/server/api/api/v1/accounts/lookup/index.ts b/server/api/api/v1/accounts/lookup/index.ts index 7246326c..40fdb119 100644 --- a/server/api/api/v1/accounts/lookup/index.ts +++ b/server/api/api/v1/accounts/lookup/index.ts @@ -1,7 +1,7 @@ import { applyConfig, auth, handleZodError } from "@/api"; -import { dualLogger } from "@/loggers"; import { errorResponse, jsonResponse } from "@/response"; import { zValidator } from "@hono/zod-validator"; +import { getLogger } from "@logtape/logtape"; import { eq } from "drizzle-orm"; import type { Hono } from "hono"; import { @@ -19,7 +19,6 @@ import { z } from "zod"; import { resolveWebFinger } from "~/database/entities/user"; import { RolePermissions, Users } from "~/drizzle/schema"; import { User } from "~/packages/database-interface/user"; -import { LogLevel } from "~/packages/log-manager"; export const meta = applyConfig({ allowedMethods: ["GET"], @@ -83,11 +82,7 @@ export default (app: Hono) => username, domain, ).catch((e) => { - dualLogger.logError( - LogLevel.Error, - "WebFinger.Resolve", - e as Error, - ); + getLogger("webfinger").error`${e}`; return null; }); diff --git a/server/api/api/v2/search/index.ts b/server/api/api/v2/search/index.ts index 34210893..dd69514d 100644 --- a/server/api/api/v2/search/index.ts +++ b/server/api/api/v2/search/index.ts @@ -1,8 +1,8 @@ import { applyConfig, auth, handleZodError, userAddressValidator } from "@/api"; -import { dualLogger } from "@/loggers"; import { MeiliIndexType, meilisearch } from "@/meilisearch"; import { errorResponse, jsonResponse } from "@/response"; import { zValidator } from "@hono/zod-validator"; +import { getLogger } from "@logtape/logtape"; import { and, eq, inArray, sql } from "drizzle-orm"; import type { Hono } from "hono"; import { z } from "zod"; @@ -12,7 +12,6 @@ import { Instances, Notes, RolePermissions, Users } from "~/drizzle/schema"; import { config } from "~/packages/config-manager"; import { Note } from "~/packages/database-interface/note"; import { User } from "~/packages/database-interface/user"; -import { LogLevel } from "~/packages/log-manager"; export const meta = applyConfig({ allowedMethods: ["GET"], @@ -119,11 +118,7 @@ export default (app: Hono) => username, domain, ).catch((e) => { - dualLogger.logError( - LogLevel.Error, - "WebFinger.Resolve", - e, - ); + getLogger("webfinger").error`${e}`; return null; }); diff --git a/server/api/users/:uuid/inbox/index.ts b/server/api/users/:uuid/inbox/index.ts index d58cbb75..24d66f0a 100644 --- a/server/api/users/:uuid/inbox/index.ts +++ b/server/api/users/:uuid/inbox/index.ts @@ -1,7 +1,7 @@ import { applyConfig, debugRequest, handleZodError } from "@/api"; -import { dualLogger } from "@/loggers"; import { errorResponse, jsonResponse, response } from "@/response"; import { zValidator } from "@hono/zod-validator"; +import { getLogger } from "@logtape/logtape"; import { EntityValidator, RequestParserHandler, @@ -23,7 +23,6 @@ import { Notes, Notifications, Relationships } from "~/drizzle/schema"; import { config } from "~/packages/config-manager"; import { Note } from "~/packages/database-interface/note"; import { User } from "~/packages/database-interface/user"; -import { LogLevel, LogManager } from "~/packages/log-manager"; export const meta = applyConfig({ allowedMethods: ["POST"], @@ -61,6 +60,7 @@ export default (app: Hono) => const { uuid } = context.req.valid("param"); const { signature, date, authorization, origin } = context.req.valid("header"); + const logger = getLogger(["federation", "inbox"]); // Check if Origin is defederated if ( @@ -146,11 +146,7 @@ export default (app: Hono) => if (config.debug.federation) { // Log public key - new LogManager(Bun.stdout).log( - LogLevel.Debug, - "Inbox.Signature", - `Sender public key: ${sender.data.publicKey}`, - ); + logger.debug`Sender public key: ${sender.data.publicKey}`; } const validator = await SignatureValidator.fromStringKey( @@ -179,11 +175,7 @@ export default (app: Hono) => }), ) .catch((e) => { - new LogManager(Bun.stdout).logError( - LogLevel.Error, - "Inbox.Signature", - e as Error, - ); + logger.error`${e}`; return false; }); @@ -208,11 +200,7 @@ export default (app: Hono) => note, account, ).catch((e) => { - dualLogger.logError( - LogLevel.Error, - "Inbox.NoteResolve", - e as Error, - ); + logger.error`${e}`; return null; }); @@ -436,7 +424,7 @@ export default (app: Hono) => if (isValidationError(e)) { return errorResponse((e as ValidationError).message, 400); } - dualLogger.logError(LogLevel.Error, "Inbox", e as Error); + logger.error`${e}`; return jsonResponse( { error: "Failed to process request", diff --git a/tests/utils.ts b/tests/utils.ts index 4e82a490..60630a0e 100644 --- a/tests/utils.ts +++ b/tests/utils.ts @@ -1,5 +1,4 @@ import { generateChallenge } from "@/challenges"; -import { consoleLogger } from "@/loggers"; import { randomString } from "@/math"; import { solveChallenge } from "altcha-lib"; import { asc, inArray, like } from "drizzle-orm"; @@ -11,7 +10,7 @@ import { app } from "~/index"; import { Note } from "~/packages/database-interface/note"; import { User } from "~/packages/database-interface/user"; -await setupDatabase(consoleLogger); +await setupDatabase(); /** * This allows us to send a test request to the server even when it isnt running diff --git a/utils/api.ts b/utils/api.ts index 14c30ff3..efcd6d3b 100644 --- a/utils/api.ts +++ b/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}`; + } }; diff --git a/utils/init.ts b/utils/init.ts index dcf5a912..b861b5ca 100644 --- a/utils/init.ts +++ b/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); diff --git a/utils/loggers.ts b/utils/loggers.ts index 3e798cac..4b74d82a 100644 --- a/utils/loggers.ts +++ b/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 = + import("../node_modules/@logtape/logtape/logtape/sink").RotatingFileSinkDriver; +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 = { + 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 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 = { + 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", + }, + ], + }); diff --git a/utils/markdown.ts b/utils/markdown.ts index 2047035e..95ebdbc1 100644 --- a/utils/markdown.ts +++ b/utils/markdown.ts @@ -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 || diff --git a/utils/meilisearch.ts b/utils/meilisearch.ts index 3aa1cf52..5a75f205 100644 --- a/utils/meilisearch.ts +++ b/utils/meilisearch.ts @@ -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); }