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`;
// 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`;

BIN
bun.lockb

Binary file not shown.

View file

@ -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<void> {
await super.init();
await setupDatabase(consoleLogger, false);
await setupDatabase(false);
}
}

View file

@ -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

View file

@ -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;

View file

@ -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()}`;
}
}
};

View file

@ -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()}`;
}
};

View file

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

View file

@ -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 };

View file

@ -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) => {

View file

@ -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}`,

View file

@ -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();

View file

@ -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",

View file

@ -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),

View file

@ -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<typeof Notes, StatusWithRelations> {
*/
static async fromLysand(note: LysandNote, author: User): Promise<Note> {
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<typeof Notes, StatusWithRelations> {
const resolvedAttachment = await Attachment.fromLysand(
attachment,
).catch((e) => {
dualLogger.logError(
LogLevel.Error,
"Federation.StatusResolver",
e,
);
logger.error`${e}`;
return null;
});

View file

@ -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<Response | null> => {
const url = new URL(req.url);
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 { 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;
});

View file

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

View file

@ -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",

View file

@ -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

View file

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

View file

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

View file

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

View file

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

View file

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