From bc8220c8f96f1f60f66965101e09a4261f79641e Mon Sep 17 00:00:00 2001 From: Jesse Wierzbinski Date: Wed, 26 Jun 2024 13:11:39 -1000 Subject: [PATCH] refactor: :recycle: Replace logging system with @logtape/logtape --- build.ts | 3 + bun.lockb | Bin 265556 -> 249836 bytes cli/base.ts | 3 +- config/config.example.toml | 2 +- database/entities/federation.ts | 16 +- database/entities/status.ts | 17 +- database/entities/user.ts | 48 +-- drizzle/db.ts | 31 +- index.ts | 75 ++--- middlewares/bait.ts | 11 +- middlewares/ip-bans.ts | 13 +- middlewares/logger.ts | 11 +- package.json | 3 +- packages/config-manager/config.type.ts | 2 +- packages/database-interface/note.ts | 16 +- packages/glitch-server/main.ts | 2 - packages/log-manager/index.ts | 287 ------------------ packages/log-manager/package.json | 6 - .../log-manager/tests/log-manager.test.ts | 222 -------------- server/api/api/v1/accounts/lookup/index.ts | 9 +- server/api/api/v2/search/index.ts | 9 +- server/api/users/:uuid/inbox/index.ts | 24 +- tests/utils.ts | 3 +- utils/api.ts | 42 +-- utils/init.ts | 106 ++----- utils/loggers.ts | 199 +++++++++++- utils/markdown.ts | 5 +- utils/meilisearch.ts | 17 +- 28 files changed, 324 insertions(+), 858 deletions(-) delete mode 100644 packages/log-manager/index.ts delete mode 100644 packages/log-manager/package.json delete mode 100644 packages/log-manager/tests/log-manager.test.ts 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 b913705c862aa58cdf785a83f91110ba73cfca2b..febdb13a05046828358350487e7d07a340e1be9b 100755 GIT binary patch delta 64803 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�HfJdAs>jIbYucu<>F!ijxMa>O<_0YlB>Hk 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 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); }