mirror of
https://github.com/versia-pub/server.git
synced 2025-12-06 08:28:19 +01:00
refactor: ♻️ Replace logging system with @logtape/logtape
This commit is contained in:
parent
75992dfe62
commit
bc8220c8f9
3
build.ts
3
build.ts
|
|
@ -32,6 +32,9 @@ await $`sed -i 's|import("node_modules/|import("./node_modules/|g' dist/*.js`;
|
|||
await $`sed -i 's|import"node_modules/|import"./node_modules/|g' dist/**/*.js`;
|
||||
// Replace /temp/node_modules with ./node_modules
|
||||
await $`sed -i 's|/temp/node_modules|./node_modules|g' dist/**/*.js`;
|
||||
// Replace 'export { toFilter, getLevelFilter, getConsoleSink };' to remove getConsoleSink
|
||||
// Because Bun duplicates the export and it causes a runtime error
|
||||
await $`sed -i 's|export { toFilter, getLevelFilter, getConsoleSink };|export { toFilter, getLevelFilter };|g' dist/**/*.js`;
|
||||
|
||||
// Copy Drizzle migrations to dist
|
||||
await $`cp -r drizzle dist/drizzle`;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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()}`;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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()}`;
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
|
|
|
|||
75
index.ts
75
index.ts
|
|
@ -1,10 +1,10 @@
|
|||
import { checkConfig } from "@/init";
|
||||
import { dualLogger } from "@/loggers";
|
||||
import { configureLoggers } from "@/loggers";
|
||||
import { connectMeili } from "@/meilisearch";
|
||||
import { errorResponse, response } from "@/response";
|
||||
import { getLogger } from "@logtape/logtape";
|
||||
import { config } from "config-manager";
|
||||
import { Hono } from "hono";
|
||||
import { LogLevel, LogManager, type MultiLogManager } from "log-manager";
|
||||
import { setupDatabase } from "~/drizzle/db";
|
||||
import { agentBans } from "~/middlewares/agent-bans";
|
||||
import { bait } from "~/middlewares/bait";
|
||||
|
|
@ -21,21 +21,16 @@ const timeAtStart = performance.now();
|
|||
|
||||
const isEntry =
|
||||
import.meta.path === Bun.main && !process.argv.includes("--silent");
|
||||
await configureLoggers(isEntry);
|
||||
|
||||
let dualServerLogger: LogManager | MultiLogManager = new LogManager(
|
||||
Bun.file("/dev/null"),
|
||||
);
|
||||
const serverLogger = getLogger("server");
|
||||
|
||||
if (isEntry) {
|
||||
dualServerLogger = dualLogger;
|
||||
}
|
||||
serverLogger.info`Starting Lysand...`;
|
||||
|
||||
await dualServerLogger.log(LogLevel.Info, "Lysand", "Starting Lysand...");
|
||||
|
||||
await setupDatabase(dualServerLogger);
|
||||
await setupDatabase();
|
||||
|
||||
if (config.meilisearch.enabled) {
|
||||
await connectMeili(dualServerLogger);
|
||||
await connectMeili();
|
||||
}
|
||||
|
||||
process.on("SIGINT", () => {
|
||||
|
|
@ -46,7 +41,7 @@ process.on("SIGINT", () => {
|
|||
const postCount = await Note.getCount();
|
||||
|
||||
if (isEntry) {
|
||||
await checkConfig(config, dualServerLogger);
|
||||
await checkConfig(config);
|
||||
}
|
||||
|
||||
const app = new Hono({
|
||||
|
|
@ -79,7 +74,7 @@ app.options("*", () => {
|
|||
|
||||
app.all("*", async (context) => {
|
||||
if (config.frontend.glitch.enabled) {
|
||||
const glitch = await handleGlitchRequest(context.req.raw, dualLogger);
|
||||
const glitch = await handleGlitchRequest(context.req.raw);
|
||||
|
||||
if (glitch) {
|
||||
return glitch;
|
||||
|
|
@ -91,11 +86,7 @@ app.all("*", async (context) => {
|
|||
config.frontend.url,
|
||||
).toString();
|
||||
|
||||
await dualLogger.log(
|
||||
LogLevel.Debug,
|
||||
"Server.Proxy",
|
||||
`Proxying ${replacedUrl}`,
|
||||
);
|
||||
serverLogger.debug`Proxying ${replacedUrl}`;
|
||||
|
||||
const proxy = await fetch(replacedUrl, {
|
||||
headers: {
|
||||
|
|
@ -104,13 +95,9 @@ app.all("*", async (context) => {
|
|||
"Accept-Encoding": "identity",
|
||||
},
|
||||
redirect: "manual",
|
||||
}).catch(async (e) => {
|
||||
await dualLogger.logError(LogLevel.Error, "Server.Proxy", e as Error);
|
||||
await dualLogger.log(
|
||||
LogLevel.Error,
|
||||
"Server.Proxy",
|
||||
`The Frontend is not running or the route is not found: ${replacedUrl}`,
|
||||
);
|
||||
}).catch((e) => {
|
||||
serverLogger.error`${e}`;
|
||||
serverLogger.error`The Frontend is not running or the route is not found: ${replacedUrl}`;
|
||||
return null;
|
||||
});
|
||||
|
||||
|
|
@ -138,25 +125,13 @@ app.all("*", async (context) => {
|
|||
|
||||
createServer(config, app);
|
||||
|
||||
await dualServerLogger.log(
|
||||
LogLevel.Info,
|
||||
"Server",
|
||||
`Lysand started at ${config.http.bind}:${config.http.bind_port} in ${(performance.now() - timeAtStart).toFixed(0)}ms`,
|
||||
);
|
||||
serverLogger.info`Lysand started at ${config.http.bind}:${config.http.bind_port} in ${(performance.now() - timeAtStart).toFixed(0)}ms`;
|
||||
|
||||
await dualServerLogger.log(
|
||||
LogLevel.Info,
|
||||
"Database",
|
||||
`Database is online, now serving ${postCount} posts`,
|
||||
);
|
||||
serverLogger.info`Database is online, now serving ${postCount} posts`;
|
||||
|
||||
if (config.frontend.enabled) {
|
||||
if (!URL.canParse(config.frontend.url)) {
|
||||
await dualServerLogger.log(
|
||||
LogLevel.Error,
|
||||
"Server",
|
||||
`Frontend URL is not a valid URL: ${config.frontend.url}`,
|
||||
);
|
||||
serverLogger.error`Frontend URL is not a valid URL: ${config.frontend.url}`;
|
||||
// Hang until Ctrl+C is pressed
|
||||
await Bun.sleep(Number.POSITIVE_INFINITY);
|
||||
}
|
||||
|
|
@ -167,23 +142,11 @@ if (config.frontend.enabled) {
|
|||
.catch(() => false);
|
||||
|
||||
if (!response) {
|
||||
await dualServerLogger.log(
|
||||
LogLevel.Error,
|
||||
"Server",
|
||||
`Frontend is unreachable at ${config.frontend.url}`,
|
||||
);
|
||||
await dualServerLogger.log(
|
||||
LogLevel.Error,
|
||||
"Server",
|
||||
"Please ensure the frontend is online and reachable",
|
||||
);
|
||||
serverLogger.error`Frontend is unreachable at ${config.frontend.url}`;
|
||||
serverLogger.error`Please ensure the frontend is online and reachable`;
|
||||
}
|
||||
} else {
|
||||
await dualServerLogger.log(
|
||||
LogLevel.Warning,
|
||||
"Server",
|
||||
"Frontend is disabled, skipping check",
|
||||
);
|
||||
serverLogger.warn`Frontend is disabled, skipping check`;
|
||||
}
|
||||
|
||||
export { app };
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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}`,
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
{
|
||||
"name": "log-manager",
|
||||
"version": "0.0.0",
|
||||
"main": "index.ts",
|
||||
"dependencies": {}
|
||||
}
|
||||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
@ -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;
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
38
utils/api.ts
38
utils/api.ts
|
|
@ -1,4 +1,5 @@
|
|||
import { errorResponse } from "@/response";
|
||||
import { getLogger } from "@logtape/logtape";
|
||||
import { extractParams, verifySolution } from "altcha-lib";
|
||||
import chalk from "chalk";
|
||||
import { config } from "config-manager";
|
||||
|
|
@ -27,7 +28,6 @@ import { type AuthData, getFromHeader } from "~/database/entities/user";
|
|||
import { db } from "~/drizzle/db";
|
||||
import { Challenges } from "~/drizzle/schema";
|
||||
import type { User } from "~/packages/database-interface/user";
|
||||
import { LogLevel, LogManager } from "~/packages/log-manager";
|
||||
import type { ApiRouteMetadata, HttpVerb } from "~/types/api";
|
||||
|
||||
export const applyConfig = (routeMeta: ApiRouteMetadata) => {
|
||||
|
|
@ -395,23 +395,27 @@ export const jsonOrForm = () => {
|
|||
});
|
||||
};
|
||||
|
||||
export const debugRequest = async (
|
||||
req: Request,
|
||||
logger = new LogManager(Bun.stdout),
|
||||
) => {
|
||||
export const debugRequest = async (req: Request) => {
|
||||
const body = await req.clone().text();
|
||||
await logger.log(
|
||||
LogLevel.Debug,
|
||||
"RequestDebugger",
|
||||
`\n${chalk.green(req.method)} ${chalk.blue(req.url)}\n${chalk.bold(
|
||||
"Hash",
|
||||
)}: ${chalk.yellow(
|
||||
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"),
|
||||
)}\n${chalk.bold("Headers")}:\n${Array.from(req.headers.entries())
|
||||
.map(
|
||||
([key, value]) =>
|
||||
` - ${chalk.cyan(key)}: ${chalk.white(value)}`,
|
||||
)}`;
|
||||
|
||||
const headers = `${chalk.bold("Headers")}:\n${Array.from(
|
||||
req.headers.entries(),
|
||||
)
|
||||
.join("\n")}\n${chalk.bold("Body")}: ${chalk.gray(body)}`,
|
||||
);
|
||||
.map(([key, value]) => ` - ${chalk.cyan(key)}: ${chalk.white(value)}`)
|
||||
.join("\n")}`;
|
||||
|
||||
const bodyLog = `${chalk.bold("Body")}: ${chalk.gray(body)}`;
|
||||
|
||||
if (config.logging.log_requests_verbose) {
|
||||
logger.debug`${urlAndMethod}\n${hash}\n${headers}\n${bodyLog}`;
|
||||
} else {
|
||||
logger.debug`${urlAndMethod}`;
|
||||
}
|
||||
};
|
||||
|
|
|
|||
106
utils/init.ts
106
utils/init.ts
|
|
@ -1,43 +1,27 @@
|
|||
import { getLogger } from "@logtape/logtape";
|
||||
import chalk from "chalk";
|
||||
import type { Config } from "~/packages/config-manager";
|
||||
import {
|
||||
LogLevel,
|
||||
type LogManager,
|
||||
type MultiLogManager,
|
||||
} from "~/packages/log-manager";
|
||||
|
||||
export const checkConfig = async (
|
||||
config: Config,
|
||||
logger: LogManager | MultiLogManager,
|
||||
) => {
|
||||
await checkOidcConfig(config, logger);
|
||||
export const checkConfig = async (config: Config) => {
|
||||
await checkOidcConfig(config);
|
||||
|
||||
await checkHttpProxyConfig(config, logger);
|
||||
await checkHttpProxyConfig(config);
|
||||
|
||||
await checkChallengeConfig(config, logger);
|
||||
await checkChallengeConfig(config);
|
||||
};
|
||||
|
||||
const checkHttpProxyConfig = async (
|
||||
config: Config,
|
||||
logger: LogManager | MultiLogManager,
|
||||
) => {
|
||||
const checkHttpProxyConfig = async (config: Config) => {
|
||||
const logger = getLogger("server");
|
||||
|
||||
if (config.http.proxy.enabled) {
|
||||
if (!config.http.proxy.address) {
|
||||
await logger.log(
|
||||
LogLevel.Critical,
|
||||
"Server",
|
||||
"The HTTP proxy is enabled, but the proxy address is not set in the config",
|
||||
);
|
||||
logger.fatal`The HTTP proxy is enabled, but the proxy address is not set in the config`;
|
||||
|
||||
// Hang until Ctrl+C is pressed
|
||||
await Bun.sleep(Number.POSITIVE_INFINITY);
|
||||
}
|
||||
|
||||
await logger.log(
|
||||
LogLevel.Info,
|
||||
"Server",
|
||||
`HTTP proxy enabled at ${chalk.gray(config.http.proxy.address)}, testing...`,
|
||||
);
|
||||
logger.info`HTTP proxy enabled at ${chalk.gray(config.http.proxy.address)}, testing...`;
|
||||
|
||||
// Test the proxy
|
||||
const response = await fetch("https://api.ipify.org?format=json", {
|
||||
|
|
@ -46,18 +30,10 @@ const checkHttpProxyConfig = async (
|
|||
|
||||
const ip = (await response.json()).ip;
|
||||
|
||||
await logger.log(
|
||||
LogLevel.Info,
|
||||
"Server",
|
||||
`Your IPv4 address is ${chalk.gray(ip)}`,
|
||||
);
|
||||
logger.info`Your IPv4 address is ${chalk.gray(ip)}`;
|
||||
|
||||
if (!response.ok) {
|
||||
await logger.log(
|
||||
LogLevel.Critical,
|
||||
"Server",
|
||||
"The HTTP proxy is enabled, but the proxy address is not reachable",
|
||||
);
|
||||
logger.fatal`The HTTP proxy is enabled, but the proxy address is not reachable`;
|
||||
|
||||
// Hang until Ctrl+C is pressed
|
||||
await Bun.sleep(Number.POSITIVE_INFINITY);
|
||||
|
|
@ -65,25 +41,15 @@ const checkHttpProxyConfig = async (
|
|||
}
|
||||
};
|
||||
|
||||
const checkChallengeConfig = async (
|
||||
config: Config,
|
||||
logger: LogManager | MultiLogManager,
|
||||
) => {
|
||||
const checkChallengeConfig = async (config: Config) => {
|
||||
const logger = getLogger("server");
|
||||
|
||||
if (
|
||||
config.validation.challenges.enabled &&
|
||||
!config.validation.challenges.key
|
||||
) {
|
||||
await logger.log(
|
||||
LogLevel.Critical,
|
||||
"Server",
|
||||
"Challenges are enabled, but the challenge key is not set in the config",
|
||||
);
|
||||
|
||||
await logger.log(
|
||||
LogLevel.Critical,
|
||||
"Server",
|
||||
"Below is a generated key for you to copy in the config at validation.challenges.key",
|
||||
);
|
||||
logger.fatal`Challenges are enabled, but the challenge key is not set in the config`;
|
||||
logger.fatal`Below is a generated key for you to copy in the config at validation.challenges.key`;
|
||||
|
||||
const key = await crypto.subtle.generateKey(
|
||||
{
|
||||
|
|
@ -98,32 +64,20 @@ const checkChallengeConfig = async (
|
|||
|
||||
const base64 = Buffer.from(exported).toString("base64");
|
||||
|
||||
await logger.log(
|
||||
LogLevel.Critical,
|
||||
"Server",
|
||||
`Generated key: ${chalk.gray(base64)}`,
|
||||
);
|
||||
logger.fatal`Generated key: ${chalk.gray(base64)}`;
|
||||
|
||||
// Hang until Ctrl+C is pressed
|
||||
await Bun.sleep(Number.POSITIVE_INFINITY);
|
||||
}
|
||||
};
|
||||
|
||||
const checkOidcConfig = async (
|
||||
config: Config,
|
||||
logger: LogManager | MultiLogManager,
|
||||
) => {
|
||||
const checkOidcConfig = async (config: Config) => {
|
||||
const logger = getLogger("server");
|
||||
|
||||
if (!config.oidc.jwt_key) {
|
||||
await logger.log(
|
||||
LogLevel.Critical,
|
||||
"Server",
|
||||
"The JWT private key is not set in the config",
|
||||
);
|
||||
await logger.log(
|
||||
LogLevel.Critical,
|
||||
"Server",
|
||||
"Below is a generated key for you to copy in the config at oidc.jwt_key",
|
||||
);
|
||||
logger.fatal`The JWT private key is not set in the config`;
|
||||
logger.fatal`Below is a generated key for you to copy in the config at oidc.jwt_key`;
|
||||
|
||||
// Generate a key for them
|
||||
const keys = await crypto.subtle.generateKey("Ed25519", true, [
|
||||
"sign",
|
||||
|
|
@ -138,11 +92,7 @@ const checkOidcConfig = async (
|
|||
await crypto.subtle.exportKey("spki", keys.publicKey),
|
||||
).toString("base64");
|
||||
|
||||
await logger.log(
|
||||
LogLevel.Critical,
|
||||
"Server",
|
||||
chalk.gray(`${privateKey};${publicKey}`),
|
||||
);
|
||||
logger.fatal`Generated key: ${chalk.gray(`${privateKey};${publicKey}`)}`;
|
||||
|
||||
// Hang until Ctrl+C is pressed
|
||||
await Bun.sleep(Number.POSITIVE_INFINITY);
|
||||
|
|
@ -171,11 +121,7 @@ const checkOidcConfig = async (
|
|||
.catch((e) => e as Error);
|
||||
|
||||
if (privateKey instanceof Error || publicKey instanceof Error) {
|
||||
await logger.log(
|
||||
LogLevel.Critical,
|
||||
"Server",
|
||||
"The JWT key could not be imported! You may generate a new one by removing the old one from the config and restarting the server (this will invalidate all current JWTs).",
|
||||
);
|
||||
logger.fatal`The JWT key could not be imported! You may generate a new one by removing the old one from the config and restarting the server (this will invalidate all current JWTs).`;
|
||||
|
||||
// Hang until Ctrl+C is pressed
|
||||
await Bun.sleep(Number.POSITIVE_INFINITY);
|
||||
|
|
|
|||
197
utils/loggers.ts
197
utils/loggers.ts
|
|
@ -1,20 +1,191 @@
|
|||
import { LogManager, MultiLogManager } from "log-manager";
|
||||
import {
|
||||
appendFileSync,
|
||||
closeSync,
|
||||
existsSync,
|
||||
mkdirSync,
|
||||
openSync,
|
||||
renameSync,
|
||||
statSync,
|
||||
} from "node:fs";
|
||||
import {
|
||||
type LogLevel,
|
||||
type LogRecord,
|
||||
configure,
|
||||
getConsoleSink,
|
||||
getLevelFilter,
|
||||
} from "@logtape/logtape";
|
||||
import chalk from "chalk";
|
||||
import stripAnsi from "strip-ansi";
|
||||
import { config } from "~/packages/config-manager";
|
||||
|
||||
const noColors = process.env.NO_COLORS === "true";
|
||||
const noFancyDates = process.env.NO_FANCY_DATES === "true";
|
||||
// HACK: This is a workaround for the lack of type exports in the Logtape package.
|
||||
type RotatingFileSinkDriver<T> =
|
||||
import("../node_modules/@logtape/logtape/logtape/sink").RotatingFileSinkDriver<T>;
|
||||
const getBaseRotatingFileSink = (
|
||||
await import("../node_modules/@logtape/logtape/logtape/sink")
|
||||
).getRotatingFileSink;
|
||||
|
||||
const requestsLog = Bun.file(config.logging.storage.requests);
|
||||
const isEntry = true;
|
||||
const levelAbbreviations: Record<LogLevel, string> = {
|
||||
debug: "DBG",
|
||||
info: "INF",
|
||||
warning: "WRN",
|
||||
error: "ERR",
|
||||
fatal: "FTL",
|
||||
};
|
||||
|
||||
export const logger = new LogManager(
|
||||
isEntry ? requestsLog : Bun.file("/dev/null"),
|
||||
export function defaultTextFormatter(record: LogRecord): string {
|
||||
const ts = new Date(record.timestamp);
|
||||
let msg = "";
|
||||
for (let i = 0; i < record.message.length; i++) {
|
||||
if (i % 2 === 0) {
|
||||
msg += record.message[i];
|
||||
} else {
|
||||
msg += Bun.inspect(stripAnsi(record.message[i] as string)).match(
|
||||
/"(.*?)"/,
|
||||
)?.[1];
|
||||
}
|
||||
}
|
||||
const category = record.category.join("\xb7");
|
||||
return `${ts.toISOString().replace("T", " ").replace("Z", " +00:00")} [${
|
||||
levelAbbreviations[record.level]
|
||||
}] ${category}: ${msg}\n`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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[];
|
||||
|
||||
/**
|
||||
* 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);
|
||||
|
||||
export const consoleLogger = new LogManager(
|
||||
isEntry ? Bun.stdout : Bun.file("/dev/null"),
|
||||
!noColors,
|
||||
!noFancyDates,
|
||||
);
|
||||
return [
|
||||
`${formattedTime} ${formattedLevel} ${formattedCategory} ${formattedMsg}`,
|
||||
];
|
||||
}
|
||||
|
||||
export const dualLogger = new MultiLogManager([logger, consoleLogger]);
|
||||
export const nodeDriver: RotatingFileSinkDriver<number> = {
|
||||
openSync(path: string) {
|
||||
return openSync(path, "a");
|
||||
},
|
||||
writeSync(fd, chunk) {
|
||||
appendFileSync(fd, chunk, {
|
||||
flush: true,
|
||||
});
|
||||
},
|
||||
flushSync() {
|
||||
// ...
|
||||
},
|
||||
closeSync(fd) {
|
||||
closeSync(fd);
|
||||
},
|
||||
statSync(path) {
|
||||
// If file does not exist, create it
|
||||
if (!existsSync(path)) {
|
||||
// Mkdir all directories in path
|
||||
const dirs = path.split("/");
|
||||
dirs.pop();
|
||||
mkdirSync(dirs.join("/"), { recursive: true });
|
||||
appendFileSync(path, "");
|
||||
}
|
||||
return statSync(path);
|
||||
},
|
||||
renameSync: renameSync,
|
||||
};
|
||||
|
||||
export const configureLoggers = (silent = false) =>
|
||||
configure({
|
||||
sinks: {
|
||||
console: getConsoleSink({
|
||||
formatter: defaultConsoleFormatter,
|
||||
}),
|
||||
file: getBaseRotatingFileSink(config.logging.storage.requests, {
|
||||
maxFiles: 10,
|
||||
maxSize: 10 * 1024 * 1024,
|
||||
formatter: defaultTextFormatter,
|
||||
...nodeDriver,
|
||||
}),
|
||||
},
|
||||
filters: {
|
||||
configFilter: silent
|
||||
? getLevelFilter(config.logging.log_level)
|
||||
: getLevelFilter(null),
|
||||
},
|
||||
loggers: [
|
||||
{
|
||||
category: "server",
|
||||
sinks: ["console", "file"],
|
||||
filters: ["configFilter"],
|
||||
},
|
||||
{
|
||||
category: "federation",
|
||||
sinks: ["console", "file"],
|
||||
filters: ["configFilter"],
|
||||
},
|
||||
{
|
||||
category: ["federation", "inbox"],
|
||||
sinks: ["console", "file"],
|
||||
filters: ["configFilter"],
|
||||
},
|
||||
{
|
||||
category: "database",
|
||||
sinks: ["console", "file"],
|
||||
filters: ["configFilter"],
|
||||
},
|
||||
{
|
||||
category: "webfinger",
|
||||
sinks: ["console", "file"],
|
||||
filters: ["configFilter"],
|
||||
},
|
||||
{
|
||||
category: "meilisearch",
|
||||
sinks: ["console", "file"],
|
||||
filters: ["configFilter"],
|
||||
},
|
||||
{
|
||||
category: ["logtape", "meta"],
|
||||
level: "error",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import { getLogger } from "@logtape/logtape";
|
||||
import { markdownParse } from "~/database/entities/status";
|
||||
import { LogLevel } from "~/packages/log-manager";
|
||||
import { dualLogger } from "./loggers";
|
||||
|
||||
export const renderMarkdownInPath = async (
|
||||
path: string,
|
||||
|
|
@ -15,7 +14,7 @@ export const renderMarkdownInPath = async (
|
|||
content =
|
||||
(await markdownParse(
|
||||
(await extendedDescriptionFile.text().catch(async (e) => {
|
||||
await dualLogger.logError(LogLevel.Error, "Routes", e);
|
||||
await getLogger("server").error`${e}`;
|
||||
return "";
|
||||
})) ||
|
||||
defaultText ||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { getLogger } from "@logtape/logtape";
|
||||
import { config } from "config-manager";
|
||||
import { count } from "drizzle-orm";
|
||||
import { LogLevel, type LogManager, type MultiLogManager } from "log-manager";
|
||||
import { Meilisearch } from "meilisearch";
|
||||
import { db } from "~/drizzle/db";
|
||||
import { Notes, Users } from "~/drizzle/schema";
|
||||
|
|
@ -11,7 +11,8 @@ export const meilisearch = new Meilisearch({
|
|||
apiKey: config.meilisearch.api_key,
|
||||
});
|
||||
|
||||
export const connectMeili = async (logger: MultiLogManager | LogManager) => {
|
||||
export const connectMeili = async () => {
|
||||
const logger = getLogger("meilisearch");
|
||||
if (!config.meilisearch.enabled) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -33,17 +34,9 @@ export const connectMeili = async (logger: MultiLogManager | LogManager) => {
|
|||
.index(MeiliIndexType.Statuses)
|
||||
.updateSearchableAttributes(["content"]);
|
||||
|
||||
await logger.log(
|
||||
LogLevel.Info,
|
||||
"Meilisearch",
|
||||
"Connected to Meilisearch",
|
||||
);
|
||||
logger.info`Connected to Meilisearch`;
|
||||
} else {
|
||||
await logger.log(
|
||||
LogLevel.Critical,
|
||||
"Meilisearch",
|
||||
"Error while connecting to Meilisearch",
|
||||
);
|
||||
logger.fatal`Error while connecting to Meilisearch`;
|
||||
// Hang until Ctrl+C is pressed
|
||||
await Bun.sleep(Number.POSITIVE_INFINITY);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue