mirror of
https://github.com/versia-pub/server.git
synced 2026-03-13 05:49:16 +01:00
refactor: ♻️ Replace logging system with @logtape/logtape
This commit is contained in:
parent
75992dfe62
commit
bc8220c8f9
28 changed files with 324 additions and 858 deletions
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue