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

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

View file

@ -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`; await $`sed -i 's|import"node_modules/|import"./node_modules/|g' dist/**/*.js`;
// Replace /temp/node_modules with ./node_modules // Replace /temp/node_modules with ./node_modules
await $`sed -i 's|/temp/node_modules|./node_modules|g' dist/**/*.js`; 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 // Copy Drizzle migrations to dist
await $`cp -r drizzle dist/drizzle`; await $`cp -r drizzle dist/drizzle`;

BIN
bun.lockb

Binary file not shown.

View file

@ -1,4 +1,3 @@
import { consoleLogger } from "@/loggers";
import { Command } from "@oclif/core"; import { Command } from "@oclif/core";
import { setupDatabase } from "~/drizzle/db"; import { setupDatabase } from "~/drizzle/db";
@ -6,6 +5,6 @@ export abstract class BaseCommand<_T extends typeof Command> extends Command {
protected async init(): Promise<void> { protected async init(): Promise<void> {
await super.init(); await super.init();
await setupDatabase(consoleLogger, false); await setupDatabase(false);
} }
} }

View file

@ -375,7 +375,7 @@ bio = []
log_requests = false log_requests = false
# Log request and their contents (warning: this is a lot of data) # Log request and their contents (warning: this is a lot of data)
log_requests_verbose = false log_requests_verbose = false
# Available levels: debug, info, warning, error, critical # Available levels: debug, info, warning, error, fatal
log_level = "debug" log_level = "debug"
# For GDPR compliance, you can disable logging of IPs # For GDPR compliance, you can disable logging of IPs
log_ip = false log_ip = false

View file

@ -1,9 +1,9 @@
import { debugRequest } from "@/api"; import { debugRequest } from "@/api";
import { getLogger } from "@logtape/logtape";
import { SignatureConstructor } from "@lysand-org/federation"; import { SignatureConstructor } from "@lysand-org/federation";
import type { Entity, Undo } from "@lysand-org/federation/types"; import type { Entity, Undo } from "@lysand-org/federation/types";
import { config } from "config-manager"; import { config } from "config-manager";
import type { User } from "~/packages/database-interface/user"; import type { User } from "~/packages/database-interface/user";
import { LogLevel, LogManager } from "~/packages/log-manager";
export const localObjectUri = (id: string) => export const localObjectUri = (id: string) =>
new URL(`/objects/${id}`, config.http.base_url).toString(); new URL(`/objects/${id}`, config.http.base_url).toString();
@ -48,19 +48,13 @@ export const objectToInboxRequest = async (
// Debug request // Debug request
await debugRequest(signed); await debugRequest(signed);
const logger = getLogger("federation");
// Log public key // Log public key
new LogManager(Bun.stdout).log( logger.debug`Sender public key: ${author.data.publicKey}`;
LogLevel.Debug,
"Inbox.Signature",
`Sender public key: ${author.data.publicKey}`,
);
// Log signed string // Log signed string
new LogManager(Bun.stdout).log( logger.debug`Signed string:\n${signedString}`;
LogLevel.Debug,
"Inbox.Signature",
`Signed string:\n${signedString}`,
);
} }
return signed; return signed;

View file

@ -1,7 +1,7 @@
import { mentionValidator } from "@/api"; import { mentionValidator } from "@/api";
import { dualLogger } from "@/loggers";
import { sanitizeHtml, sanitizeHtmlInline } from "@/sanitization"; import { sanitizeHtml, sanitizeHtmlInline } from "@/sanitization";
import markdownItTaskLists from "@hackmd/markdown-it-task-lists"; import markdownItTaskLists from "@hackmd/markdown-it-task-lists";
import { getLogger } from "@logtape/logtape";
import type { ContentFormat } from "@lysand-org/federation/types"; import type { ContentFormat } from "@lysand-org/federation/types";
import { config } from "config-manager"; import { config } from "config-manager";
import { import {
@ -35,7 +35,6 @@ import {
} from "~/drizzle/schema"; } from "~/drizzle/schema";
import type { Note } from "~/packages/database-interface/note"; import type { Note } from "~/packages/database-interface/note";
import { User } from "~/packages/database-interface/user"; import { User } from "~/packages/database-interface/user";
import { LogLevel } from "~/packages/log-manager";
import type { Application } from "./application"; import type { Application } from "./application";
import type { EmojiWithInstance } from "./emoji"; import type { EmojiWithInstance } from "./emoji";
import { objectToInboxRequest } from "./federation"; import { objectToInboxRequest } from "./federation";
@ -453,16 +452,10 @@ export const federateNote = async (note: Note) => {
}); });
if (!response.ok) { if (!response.ok) {
dualLogger.log( const logger = getLogger("federation");
LogLevel.Debug,
"Federation.Status", logger.debug`${await response.text()}`;
await response.text(), logger.error`Failed to federate status ${note.data.id} to ${user.getUri()}`;
);
dualLogger.log(
LogLevel.Error,
"Federation.Status",
`Failed to federate status ${note.data.id} to ${user.getUri()}`,
);
} }
} }
}; };

View file

@ -1,4 +1,4 @@
import { dualLogger } from "@/loggers"; import { getLogger } from "@logtape/logtape";
import type { import type {
Follow, Follow,
FollowAccept, FollowAccept,
@ -17,7 +17,6 @@ import {
Users, Users,
} from "~/drizzle/schema"; } from "~/drizzle/schema";
import { User } from "~/packages/database-interface/user"; import { User } from "~/packages/database-interface/user";
import { LogLevel } from "~/packages/log-manager";
import type { Application } from "./application"; import type { Application } from "./application";
import type { EmojiWithInstance } from "./emoji"; import type { EmojiWithInstance } from "./emoji";
import { objectToInboxRequest } from "./federation"; import { objectToInboxRequest } from "./federation";
@ -180,19 +179,10 @@ export const followRequestUser = async (
}); });
if (!response.ok) { if (!response.ok) {
dualLogger.log( const logger = getLogger("federation");
LogLevel.Debug,
"Federation.FollowRequest",
await response.text(),
);
dualLogger.log( logger.debug`${await response.text()}`;
LogLevel.Error, logger.error`Failed to federate follow request from ${follower.id} to ${followee.getUri()}`;
"Federation.FollowRequest",
`Failed to federate follow request from ${
follower.id
} to ${followee.getUri()}`,
);
await db await db
.update(Relationships) .update(Relationships)
@ -237,19 +227,10 @@ export const sendFollowAccept = async (follower: User, followee: User) => {
}); });
if (!response.ok) { if (!response.ok) {
dualLogger.log( const logger = getLogger("federation");
LogLevel.Debug,
"Federation.FollowAccept",
await response.text(),
);
dualLogger.log( logger.debug`${await response.text()}`;
LogLevel.Error, logger.error`Failed to federate follow accept from ${followee.id} to ${follower.getUri()}`;
"Federation.FollowAccept",
`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) { if (!response.ok) {
dualLogger.log( const logger = getLogger("federation");
LogLevel.Debug,
"Federation.FollowReject",
await response.text(),
);
dualLogger.log( logger.debug`${await response.text()}`;
LogLevel.Error, logger.error`Failed to federate follow reject from ${followee.id} to ${follower.getUri()}`;
"Federation.FollowReject",
`Failed to federate follow reject from ${
followee.id
} to ${follower.getUri()}`,
);
} }
}; };

View file

@ -1,6 +1,6 @@
import { getLogger } from "@logtape/logtape";
import { drizzle } from "drizzle-orm/node-postgres"; import { drizzle } from "drizzle-orm/node-postgres";
import { migrate } from "drizzle-orm/postgres-js/migrator"; import { migrate } from "drizzle-orm/postgres-js/migrator";
import { LogLevel, LogManager, type MultiLogManager } from "log-manager";
import { Client } from "pg"; import { Client } from "pg";
import { config } from "~/packages/config-manager"; import { config } from "~/packages/config-manager";
import * as schema from "./schema"; import * as schema from "./schema";
@ -13,10 +13,9 @@ export const client = new Client({
database: config.database.database, database: config.database.database,
}); });
export const setupDatabase = async ( export const setupDatabase = async (info = true) => {
logger: LogManager | MultiLogManager = new LogManager(Bun.stdout), const logger = getLogger("database");
info = true,
) => {
try { try {
await client.connect(); await client.connect();
} catch (e) { } catch (e) {
@ -27,39 +26,29 @@ export const setupDatabase = async (
return; return;
} }
await logger.logError(LogLevel.Critical, "Database", e as Error); logger.fatal`${e}`;
logger.fatal`Failed to connect to database. Please check your configuration.`;
await logger.log(
LogLevel.Critical,
"Database",
"Failed to connect to database. Please check your configuration.",
);
// Hang until Ctrl+C is pressed // Hang until Ctrl+C is pressed
await Bun.sleep(Number.POSITIVE_INFINITY); await Bun.sleep(Number.POSITIVE_INFINITY);
} }
// Migrate the database // Migrate the database
info && info && logger.info`Migrating database...`;
(await logger.log(LogLevel.Info, "Database", "Migrating database..."));
try { try {
await migrate(db, { await migrate(db, {
migrationsFolder: "./drizzle/migrations", migrationsFolder: "./drizzle/migrations",
}); });
} catch (e) { } catch (e) {
await logger.logError(LogLevel.Critical, "Database", e as Error); logger.fatal`${e}`;
await logger.log( logger.fatal`Failed to migrate database. Please check your configuration.`;
LogLevel.Critical,
"Database",
"Failed to migrate database. Please check your configuration.",
);
// Hang until Ctrl+C is pressed // Hang until Ctrl+C is pressed
await Bun.sleep(Number.POSITIVE_INFINITY); 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 }); export const db = drizzle(client, { schema });

View file

@ -1,10 +1,10 @@
import { checkConfig } from "@/init"; import { checkConfig } from "@/init";
import { dualLogger } from "@/loggers"; import { configureLoggers } from "@/loggers";
import { connectMeili } from "@/meilisearch"; import { connectMeili } from "@/meilisearch";
import { errorResponse, response } from "@/response"; import { errorResponse, response } from "@/response";
import { getLogger } from "@logtape/logtape";
import { config } from "config-manager"; import { config } from "config-manager";
import { Hono } from "hono"; import { Hono } from "hono";
import { LogLevel, LogManager, type MultiLogManager } from "log-manager";
import { setupDatabase } from "~/drizzle/db"; import { setupDatabase } from "~/drizzle/db";
import { agentBans } from "~/middlewares/agent-bans"; import { agentBans } from "~/middlewares/agent-bans";
import { bait } from "~/middlewares/bait"; import { bait } from "~/middlewares/bait";
@ -21,21 +21,16 @@ const timeAtStart = performance.now();
const isEntry = const isEntry =
import.meta.path === Bun.main && !process.argv.includes("--silent"); import.meta.path === Bun.main && !process.argv.includes("--silent");
await configureLoggers(isEntry);
let dualServerLogger: LogManager | MultiLogManager = new LogManager( const serverLogger = getLogger("server");
Bun.file("/dev/null"),
);
if (isEntry) { serverLogger.info`Starting Lysand...`;
dualServerLogger = dualLogger;
}
await dualServerLogger.log(LogLevel.Info, "Lysand", "Starting Lysand..."); await setupDatabase();
await setupDatabase(dualServerLogger);
if (config.meilisearch.enabled) { if (config.meilisearch.enabled) {
await connectMeili(dualServerLogger); await connectMeili();
} }
process.on("SIGINT", () => { process.on("SIGINT", () => {
@ -46,7 +41,7 @@ process.on("SIGINT", () => {
const postCount = await Note.getCount(); const postCount = await Note.getCount();
if (isEntry) { if (isEntry) {
await checkConfig(config, dualServerLogger); await checkConfig(config);
} }
const app = new Hono({ const app = new Hono({
@ -79,7 +74,7 @@ app.options("*", () => {
app.all("*", async (context) => { app.all("*", async (context) => {
if (config.frontend.glitch.enabled) { if (config.frontend.glitch.enabled) {
const glitch = await handleGlitchRequest(context.req.raw, dualLogger); const glitch = await handleGlitchRequest(context.req.raw);
if (glitch) { if (glitch) {
return glitch; return glitch;
@ -91,11 +86,7 @@ app.all("*", async (context) => {
config.frontend.url, config.frontend.url,
).toString(); ).toString();
await dualLogger.log( serverLogger.debug`Proxying ${replacedUrl}`;
LogLevel.Debug,
"Server.Proxy",
`Proxying ${replacedUrl}`,
);
const proxy = await fetch(replacedUrl, { const proxy = await fetch(replacedUrl, {
headers: { headers: {
@ -104,13 +95,9 @@ app.all("*", async (context) => {
"Accept-Encoding": "identity", "Accept-Encoding": "identity",
}, },
redirect: "manual", redirect: "manual",
}).catch(async (e) => { }).catch((e) => {
await dualLogger.logError(LogLevel.Error, "Server.Proxy", e as Error); serverLogger.error`${e}`;
await dualLogger.log( serverLogger.error`The Frontend is not running or the route is not found: ${replacedUrl}`;
LogLevel.Error,
"Server.Proxy",
`The Frontend is not running or the route is not found: ${replacedUrl}`,
);
return null; return null;
}); });
@ -138,25 +125,13 @@ app.all("*", async (context) => {
createServer(config, app); createServer(config, app);
await dualServerLogger.log( serverLogger.info`Lysand started at ${config.http.bind}:${config.http.bind_port} in ${(performance.now() - timeAtStart).toFixed(0)}ms`;
LogLevel.Info,
"Server",
`Lysand started at ${config.http.bind}:${config.http.bind_port} in ${(performance.now() - timeAtStart).toFixed(0)}ms`,
);
await dualServerLogger.log( serverLogger.info`Database is online, now serving ${postCount} posts`;
LogLevel.Info,
"Database",
`Database is online, now serving ${postCount} posts`,
);
if (config.frontend.enabled) { if (config.frontend.enabled) {
if (!URL.canParse(config.frontend.url)) { if (!URL.canParse(config.frontend.url)) {
await dualServerLogger.log( serverLogger.error`Frontend URL is not a valid URL: ${config.frontend.url}`;
LogLevel.Error,
"Server",
`Frontend URL is not a valid URL: ${config.frontend.url}`,
);
// Hang until Ctrl+C is pressed // Hang until Ctrl+C is pressed
await Bun.sleep(Number.POSITIVE_INFINITY); await Bun.sleep(Number.POSITIVE_INFINITY);
} }
@ -167,23 +142,11 @@ if (config.frontend.enabled) {
.catch(() => false); .catch(() => false);
if (!response) { if (!response) {
await dualServerLogger.log( serverLogger.error`Frontend is unreachable at ${config.frontend.url}`;
LogLevel.Error, serverLogger.error`Please ensure the frontend is online and reachable`;
"Server",
`Frontend is unreachable at ${config.frontend.url}`,
);
await dualServerLogger.log(
LogLevel.Error,
"Server",
"Please ensure the frontend is online and reachable",
);
} }
} else { } else {
await dualServerLogger.log( serverLogger.warn`Frontend is disabled, skipping check`;
LogLevel.Warning,
"Server",
"Frontend is disabled, skipping check",
);
} }
export { app }; export { app };

View file

@ -1,10 +1,9 @@
import { logger } from "@/loggers";
import { response } from "@/response"; import { response } from "@/response";
import { getLogger } from "@logtape/logtape";
import type { SocketAddress } from "bun"; import type { SocketAddress } from "bun";
import { createMiddleware } from "hono/factory"; import { createMiddleware } from "hono/factory";
import { matches } from "ip-matching"; import { matches } from "ip-matching";
import { config } from "~/packages/config-manager"; import { config } from "~/packages/config-manager";
import { LogLevel } from "~/packages/log-manager";
const baitFile = async () => { const baitFile = async () => {
const file = Bun.file(config.http.bait.send_file || "./beemovie.txt"); const file = Bun.file(config.http.bait.send_file || "./beemovie.txt");
@ -13,11 +12,9 @@ const baitFile = async () => {
return file; return file;
} }
await logger.log( const logger = getLogger("server");
LogLevel.Error,
"Server.Bait", logger.error`Bait file not found: ${config.http.bait.send_file}`;
`Bait file not found: ${config.http.bait.send_file}`,
);
}; };
export const bait = createMiddleware(async (context, next) => { export const bait = createMiddleware(async (context, next) => {

View file

@ -1,10 +1,9 @@
import { logger } from "@/loggers";
import { errorResponse } from "@/response"; import { errorResponse } from "@/response";
import { getLogger } from "@logtape/logtape";
import type { SocketAddress } from "bun"; import type { SocketAddress } from "bun";
import { createMiddleware } from "hono/factory"; import { createMiddleware } from "hono/factory";
import { matches } from "ip-matching"; import { matches } from "ip-matching";
import { config } from "~/packages/config-manager"; import { config } from "~/packages/config-manager";
import { LogLevel } from "~/packages/log-manager";
export const ipBans = createMiddleware(async (context, next) => { export const ipBans = createMiddleware(async (context, next) => {
// Check for banned IPs // Check for banned IPs
@ -22,12 +21,10 @@ export const ipBans = createMiddleware(async (context, next) => {
return errorResponse("Forbidden", 403); return errorResponse("Forbidden", 403);
} }
} catch (e) { } catch (e) {
logger.log( const logger = getLogger("server");
LogLevel.Error,
"Server.IPCheck", logger.error`Error while parsing banned IP "${ip}" `;
`Error while parsing banned IP "${ip}" `, logger.error`${e}`;
);
logger.logError(LogLevel.Error, "Server.IPCheck", e as Error);
return errorResponse( return errorResponse(
`A server error occured: ${(e as Error).message}`, `A server error occured: ${(e as Error).message}`,

View file

@ -1,17 +1,10 @@
import { dualLogger } from "@/loggers"; import { debugRequest } from "@/api";
import type { SocketAddress } from "bun";
import { createMiddleware } from "hono/factory"; import { createMiddleware } from "hono/factory";
import { config } from "~/packages/config-manager"; import { config } from "~/packages/config-manager";
export const logger = createMiddleware(async (context, next) => { export const logger = createMiddleware(async (context, next) => {
const requestIp = context.env?.ip as SocketAddress | undefined | null;
if (config.logging.log_requests) { if (config.logging.log_requests) {
await dualLogger.logRequest( await debugRequest(context.req.raw);
context.req.raw,
config.logging.log_ip ? requestIp?.address : undefined,
config.logging.log_requests_verbose,
);
} }
await next(); await next();

View file

@ -100,6 +100,7 @@
"@inquirer/confirm": "^3.1.10", "@inquirer/confirm": "^3.1.10",
"@inquirer/input": "^2.1.10", "@inquirer/input": "^2.1.10",
"@json2csv/plainjs": "^7.0.6", "@json2csv/plainjs": "^7.0.6",
"@logtape/logtape": "npm:@jsr/logtape__logtape",
"@lysand-org/federation": "^2.0.0", "@lysand-org/federation": "^2.0.0",
"@oclif/core": "^4.0.6", "@oclif/core": "^4.0.6",
"@tufjs/canonical-json": "^2.0.0", "@tufjs/canonical-json": "^2.0.0",
@ -121,7 +122,6 @@
"linkify-html": "^4.1.3", "linkify-html": "^4.1.3",
"linkify-string": "^4.1.3", "linkify-string": "^4.1.3",
"linkifyjs": "^4.1.3", "linkifyjs": "^4.1.3",
"log-manager": "workspace:*",
"magic-regexp": "^0.8.0", "magic-regexp": "^0.8.0",
"markdown-it": "^14.1.0", "markdown-it": "^14.1.0",
"markdown-it-anchor": "^9.0.1", "markdown-it-anchor": "^9.0.1",
@ -137,6 +137,7 @@
"sharp": "^0.33.4", "sharp": "^0.33.4",
"string-comparison": "^1.3.0", "string-comparison": "^1.3.0",
"stringify-entities": "^4.0.4", "stringify-entities": "^4.0.4",
"strip-ansi": "^7.1.0",
"table": "^6.8.2", "table": "^6.8.2",
"unzipit": "^1.4.3", "unzipit": "^1.4.3",
"uqr": "^0.1.2", "uqr": "^0.1.2",

View file

@ -546,7 +546,7 @@ export const configValidator = z.object({
log_requests: z.boolean().default(false), log_requests: z.boolean().default(false),
log_requests_verbose: z.boolean().default(false), log_requests_verbose: z.boolean().default(false),
log_level: z log_level: z
.enum(["debug", "info", "warning", "error", "critical"]) .enum(["debug", "info", "warning", "error", "fatal"])
.default("info"), .default("info"),
log_ip: z.boolean().default(false), log_ip: z.boolean().default(false),
log_filters: z.boolean().default(true), log_filters: z.boolean().default(true),

View file

@ -1,7 +1,7 @@
import { idValidator } from "@/api"; import { idValidator } from "@/api";
import { dualLogger } from "@/loggers";
import { proxyUrl } from "@/response"; import { proxyUrl } from "@/response";
import { sanitizedHtmlStrip } from "@/sanitization"; import { sanitizedHtmlStrip } from "@/sanitization";
import { getLogger } from "@logtape/logtape";
import { EntityValidator } from "@lysand-org/federation"; import { EntityValidator } from "@lysand-org/federation";
import type { import type {
ContentFormat, ContentFormat,
@ -19,7 +19,6 @@ import {
sql, sql,
} from "drizzle-orm"; } from "drizzle-orm";
import { htmlToText } from "html-to-text"; import { htmlToText } from "html-to-text";
import { LogLevel } from "log-manager";
import { createRegExp, exactly, global } from "magic-regexp"; import { createRegExp, exactly, global } from "magic-regexp";
import { import {
type Application, type Application,
@ -622,16 +621,13 @@ export class Note extends BaseInterface<typeof Notes, StatusWithRelations> {
*/ */
static async fromLysand(note: LysandNote, author: User): Promise<Note> { static async fromLysand(note: LysandNote, author: User): Promise<Note> {
const emojis: Emoji[] = []; const emojis: Emoji[] = [];
const logger = getLogger("federation");
for (const emoji of note.extensions?.["org.lysand:custom_emojis"] for (const emoji of note.extensions?.["org.lysand:custom_emojis"]
?.emojis ?? []) { ?.emojis ?? []) {
const resolvedEmoji = await Emoji.fetchFromRemote(emoji).catch( const resolvedEmoji = await Emoji.fetchFromRemote(emoji).catch(
(e) => { (e) => {
dualLogger.logError( logger.error`${e}`;
LogLevel.Error,
"Federation.StatusResolver",
e,
);
return null; return null;
}, },
); );
@ -647,11 +643,7 @@ export class Note extends BaseInterface<typeof Notes, StatusWithRelations> {
const resolvedAttachment = await Attachment.fromLysand( const resolvedAttachment = await Attachment.fromLysand(
attachment, attachment,
).catch((e) => { ).catch((e) => {
dualLogger.logError( logger.error`${e}`;
LogLevel.Error,
"Federation.StatusResolver",
e,
);
return null; return null;
}); });

View file

@ -4,7 +4,6 @@ import type { BunFile } from "bun";
import { config } from "config-manager"; import { config } from "config-manager";
import { retrieveUserFromToken } from "~/database/entities/user"; import { retrieveUserFromToken } from "~/database/entities/user";
import type { User } from "~/packages/database-interface/user"; import type { User } from "~/packages/database-interface/user";
import type { LogManager, MultiLogManager } from "~/packages/log-manager";
import { languages } from "./glitch-languages"; import { languages } from "./glitch-languages";
const handleManifestRequest = () => { const handleManifestRequest = () => {
@ -327,7 +326,6 @@ const htmlTransforms = async (
export const handleGlitchRequest = async ( export const handleGlitchRequest = async (
req: Request, req: Request,
_logger: LogManager | MultiLogManager,
): Promise<Response | null> => { ): Promise<Response | null> => {
const url = new URL(req.url); const url = new URL(req.url);
let path = url.pathname; let path = url.pathname;

View file

@ -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<string> {
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<string> {
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<string> {
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<void> {
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);
}
}

View file

@ -1,6 +0,0 @@
{
"name": "log-manager",
"version": "0.0.0",
"main": "index.ts",
"dependencies": {}
}

View file

@ -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<typeof fs.appendFile>;
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);
});
});

View file

@ -1,7 +1,7 @@
import { applyConfig, auth, handleZodError } from "@/api"; import { applyConfig, auth, handleZodError } from "@/api";
import { dualLogger } from "@/loggers";
import { errorResponse, jsonResponse } from "@/response"; import { errorResponse, jsonResponse } from "@/response";
import { zValidator } from "@hono/zod-validator"; import { zValidator } from "@hono/zod-validator";
import { getLogger } from "@logtape/logtape";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import type { Hono } from "hono"; import type { Hono } from "hono";
import { import {
@ -19,7 +19,6 @@ import { z } from "zod";
import { resolveWebFinger } from "~/database/entities/user"; import { resolveWebFinger } from "~/database/entities/user";
import { RolePermissions, Users } from "~/drizzle/schema"; import { RolePermissions, Users } from "~/drizzle/schema";
import { User } from "~/packages/database-interface/user"; import { User } from "~/packages/database-interface/user";
import { LogLevel } from "~/packages/log-manager";
export const meta = applyConfig({ export const meta = applyConfig({
allowedMethods: ["GET"], allowedMethods: ["GET"],
@ -83,11 +82,7 @@ export default (app: Hono) =>
username, username,
domain, domain,
).catch((e) => { ).catch((e) => {
dualLogger.logError( getLogger("webfinger").error`${e}`;
LogLevel.Error,
"WebFinger.Resolve",
e as Error,
);
return null; return null;
}); });

View file

@ -1,8 +1,8 @@
import { applyConfig, auth, handleZodError, userAddressValidator } from "@/api"; import { applyConfig, auth, handleZodError, userAddressValidator } from "@/api";
import { dualLogger } from "@/loggers";
import { MeiliIndexType, meilisearch } from "@/meilisearch"; import { MeiliIndexType, meilisearch } from "@/meilisearch";
import { errorResponse, jsonResponse } from "@/response"; import { errorResponse, jsonResponse } from "@/response";
import { zValidator } from "@hono/zod-validator"; import { zValidator } from "@hono/zod-validator";
import { getLogger } from "@logtape/logtape";
import { and, eq, inArray, sql } from "drizzle-orm"; import { and, eq, inArray, sql } from "drizzle-orm";
import type { Hono } from "hono"; import type { Hono } from "hono";
import { z } from "zod"; import { z } from "zod";
@ -12,7 +12,6 @@ import { Instances, Notes, RolePermissions, Users } from "~/drizzle/schema";
import { config } from "~/packages/config-manager"; import { config } from "~/packages/config-manager";
import { Note } from "~/packages/database-interface/note"; import { Note } from "~/packages/database-interface/note";
import { User } from "~/packages/database-interface/user"; import { User } from "~/packages/database-interface/user";
import { LogLevel } from "~/packages/log-manager";
export const meta = applyConfig({ export const meta = applyConfig({
allowedMethods: ["GET"], allowedMethods: ["GET"],
@ -119,11 +118,7 @@ export default (app: Hono) =>
username, username,
domain, domain,
).catch((e) => { ).catch((e) => {
dualLogger.logError( getLogger("webfinger").error`${e}`;
LogLevel.Error,
"WebFinger.Resolve",
e,
);
return null; return null;
}); });

View file

@ -1,7 +1,7 @@
import { applyConfig, debugRequest, handleZodError } from "@/api"; import { applyConfig, debugRequest, handleZodError } from "@/api";
import { dualLogger } from "@/loggers";
import { errorResponse, jsonResponse, response } from "@/response"; import { errorResponse, jsonResponse, response } from "@/response";
import { zValidator } from "@hono/zod-validator"; import { zValidator } from "@hono/zod-validator";
import { getLogger } from "@logtape/logtape";
import { import {
EntityValidator, EntityValidator,
RequestParserHandler, RequestParserHandler,
@ -23,7 +23,6 @@ import { Notes, Notifications, Relationships } from "~/drizzle/schema";
import { config } from "~/packages/config-manager"; import { config } from "~/packages/config-manager";
import { Note } from "~/packages/database-interface/note"; import { Note } from "~/packages/database-interface/note";
import { User } from "~/packages/database-interface/user"; import { User } from "~/packages/database-interface/user";
import { LogLevel, LogManager } from "~/packages/log-manager";
export const meta = applyConfig({ export const meta = applyConfig({
allowedMethods: ["POST"], allowedMethods: ["POST"],
@ -61,6 +60,7 @@ export default (app: Hono) =>
const { uuid } = context.req.valid("param"); const { uuid } = context.req.valid("param");
const { signature, date, authorization, origin } = const { signature, date, authorization, origin } =
context.req.valid("header"); context.req.valid("header");
const logger = getLogger(["federation", "inbox"]);
// Check if Origin is defederated // Check if Origin is defederated
if ( if (
@ -146,11 +146,7 @@ export default (app: Hono) =>
if (config.debug.federation) { if (config.debug.federation) {
// Log public key // Log public key
new LogManager(Bun.stdout).log( logger.debug`Sender public key: ${sender.data.publicKey}`;
LogLevel.Debug,
"Inbox.Signature",
`Sender public key: ${sender.data.publicKey}`,
);
} }
const validator = await SignatureValidator.fromStringKey( const validator = await SignatureValidator.fromStringKey(
@ -179,11 +175,7 @@ export default (app: Hono) =>
}), }),
) )
.catch((e) => { .catch((e) => {
new LogManager(Bun.stdout).logError( logger.error`${e}`;
LogLevel.Error,
"Inbox.Signature",
e as Error,
);
return false; return false;
}); });
@ -208,11 +200,7 @@ export default (app: Hono) =>
note, note,
account, account,
).catch((e) => { ).catch((e) => {
dualLogger.logError( logger.error`${e}`;
LogLevel.Error,
"Inbox.NoteResolve",
e as Error,
);
return null; return null;
}); });
@ -436,7 +424,7 @@ export default (app: Hono) =>
if (isValidationError(e)) { if (isValidationError(e)) {
return errorResponse((e as ValidationError).message, 400); return errorResponse((e as ValidationError).message, 400);
} }
dualLogger.logError(LogLevel.Error, "Inbox", e as Error); logger.error`${e}`;
return jsonResponse( return jsonResponse(
{ {
error: "Failed to process request", error: "Failed to process request",

View file

@ -1,5 +1,4 @@
import { generateChallenge } from "@/challenges"; import { generateChallenge } from "@/challenges";
import { consoleLogger } from "@/loggers";
import { randomString } from "@/math"; import { randomString } from "@/math";
import { solveChallenge } from "altcha-lib"; import { solveChallenge } from "altcha-lib";
import { asc, inArray, like } from "drizzle-orm"; import { asc, inArray, like } from "drizzle-orm";
@ -11,7 +10,7 @@ import { app } from "~/index";
import { Note } from "~/packages/database-interface/note"; import { Note } from "~/packages/database-interface/note";
import { User } from "~/packages/database-interface/user"; 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 * This allows us to send a test request to the server even when it isnt running

View file

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

View file

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

View file

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

View file

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

View file

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