From b534cadaf5c90cd3c9207cc2ef05024efa35a798 Mon Sep 17 00:00:00 2001 From: lifegpc Date: Wed, 1 Jan 2025 10:55:34 +0800 Subject: [PATCH] feat: Add log query functionality and enhance logging system --- db.ts | 3 +- routes/api/log.ts | 43 ++++++++++ utils/logger.ts | 194 +++++++++++++++++++++++++++++++++++++--------- 3 files changed, 204 insertions(+), 36 deletions(-) create mode 100644 routes/api/log.ts diff --git a/db.ts b/db.ts index 770dd8d..533bacd 100644 --- a/db.ts +++ b/db.ts @@ -136,7 +136,8 @@ export enum UserPermission { DeleteGallery = 1 << 2, ManageTasks = 1 << 3, ShareGallery = 1 << 4, - All = ~(~0 << 5), + QueryLog = 1 << 5, + All = ~(~0 << 6), } export type User = { id: number | bigint; diff --git a/routes/api/log.ts b/routes/api/log.ts new file mode 100644 index 0000000..aadb92e --- /dev/null +++ b/routes/api/log.ts @@ -0,0 +1,43 @@ +import { Handlers } from "$fresh/server.ts"; +import { return_data, return_error } from "../../server/utils.ts"; +import { User, UserPermission } from "../../db.ts"; +import { parse_int } from "../../server/parse_form.ts"; +import { base_logger, LogLevel } from "../../utils/logger.ts"; + +export const handler: Handlers = { + async GET(req, ctx) { + const user = ctx.state.user; + if ( + user && !user.is_admin && + !(Number(user.permissions) & UserPermission.QueryLog) + ) { + return return_error(403, "Permission denied."); + } + const u = new URL(req.url); + const params = u.searchParams; + const page = await parse_int(params.get("page"), null); + const limit = await parse_int(params.get("limit"), 50); + const offset = await parse_int(params.get("offset"), 0); + const type = params.get("type"); + const min_level = await parse_int( + params.get("min_level"), + LogLevel.Log, + ); + const allowed_level = params.get("allowed_level")?.split(",").map((x) => + parseInt(x) + ).filter((x) => !isNaN(x)); + const datas = page === null + ? base_logger.list(offset, limit, type, min_level, allowed_level) + : base_logger.list_page( + page, + limit, + type, + min_level, + allowed_level, + ); + const count = page === null + ? undefined + : base_logger.count(type, min_level, allowed_level); + return return_data({ datas, count }); + }, +}; diff --git a/utils/logger.ts b/utils/logger.ts index 3b1636a..637ee06 100644 --- a/utils/logger.ts +++ b/utils/logger.ts @@ -1,7 +1,7 @@ import { join } from "@std/path"; import { format as format_ver, parse as parse_ver } from "@std/semver"; import { parse_bool, stackTrace } from "../utils.ts"; -import { Db, SqliteMaster } from "./db_interface.ts"; +import { Db, QueryParameterSet, SqliteMaster } from "./db_interface.ts"; const ALL_TABLES = [ "version", @@ -21,12 +21,32 @@ const LOG_TABLE = `CREATE TABLE log ( stack TEXT );`; -export const TRACE_LEVEL = 1; -export const DEBUG_LEVEL = 2; -export const LOG_LEVEL = 3; -export const INFO_LEVEL = 4; -export const WARN_LEVEL = 5; -export const ERROR_LEVEL = 6; +export type LogEntry = { + id: number | bigint; + time: Date; + message: string; + level: LogLevel; + type: string; + stack?: string; +}; + +export type LogEntryRaw = { + id: number | bigint; + time: number | bigint; + message: string; + level: number | bigint; + type: string; + stack: string | null; +}; + +export const enum LogLevel { + Trace = 1, + Debug = 2, + Log = 3, + Info = 4, + Warn = 5, + Error = 6, +} export function format_message( message: unknown[], @@ -115,10 +135,10 @@ class BaseLogger { this.#fallback(type, level, ...messages); if (!this.db) return; const message = format_message(messages); - const stack = - (level >= TRACE_LEVEL && level < DEBUG_LEVEL) || level >= WARN_LEVEL - ? stackTrace(2) - : undefined; + const stack = (level >= LogLevel.Trace && level < LogLevel.Debug) || + level >= LogLevel.Warn + ? stackTrace(2) + : undefined; this.db.query( "INSERT INTO log (time, message, level, type, stack) VALUES (?, ?, ?, ?, ?);", [ @@ -134,40 +154,80 @@ class BaseLogger { this.db?.close(); this.db = undefined; } + #convert(d: LogEntryRaw[]): LogEntry[] { + return d.map((x) => { + return { + id: x.id, + time: new Date(Number(x.time)), + message: x.message, + level: Number(x.level), + type: x.type, + stack: x.stack === null ? undefined : x.stack, + }; + }); + } + count(type?: string | null, min_level?: number, allowed_level?: number[]) { + if (!this.db) return 0; + const where = []; + const args: QueryParameterSet = []; + if (type) { + where.push("type = ?"); + args.push(type); + } + if (min_level) { + where.push("level >= ?"); + args.push(min_level); + } + if (allowed_level) { + where.push( + "level IN (" + allowed_level.map(() => "?").join(",") + ")", + ); + args.push(...allowed_level); + } + const where_str = where.length ? " WHERE " + where.join(" AND ") : ""; + const cur = this.db.query<[number | bigint]>( + `SELECT COUNT(*) FROM log${where_str};`, + args, + ); + for (const i of cur) { + return i[0]; + } + return 0; + } debug(type: string, ...messages: unknown[]) { - this.add(type, DEBUG_LEVEL, ...messages); + this.add(type, LogLevel.Debug, ...messages); } error(type: string, ...messages: unknown[]) { - this.add(type, ERROR_LEVEL, ...messages); + this.add(type, LogLevel.Error, ...messages); } #fallback(type: string, level: number, ...messages: unknown[]) { if (type === "default") { - if (level >= ERROR_LEVEL) { + if (level >= LogLevel.Error) { console.error(...messages, "\n" + stackTrace(3)); - } else if (level >= WARN_LEVEL) { + } else if (level >= LogLevel.Warn) { console.warn(...messages, "\n" + stackTrace(3)); - } else if (level >= INFO_LEVEL) { + } else if (level >= LogLevel.Info) { console.info(...messages); - } else if (level >= LOG_LEVEL) { + } else if (level >= LogLevel.Log) { console.log(...messages); - } else if (level >= DEBUG_LEVEL) { + } else if (level >= LogLevel.Debug) { console.debug(...messages); - } else if (level >= TRACE_LEVEL) { + } else if (level >= LogLevel.Trace) { console.log("Trace:", ...messages, "\n" + stackTrace(3)); } return; } - if (level >= ERROR_LEVEL) { + if (level >= LogLevel.Error) { console.error(type + ":", ...messages, "\n" + stackTrace(3)); - } else if (level >= WARN_LEVEL) { + } else if (level >= LogLevel.Warn) { console.warn(type + ":", ...messages, "\n" + stackTrace(3)); - } else if (level >= INFO_LEVEL) { + } else if (level >= LogLevel.Info) { console.info(type + ":", ...messages); - } else if (level >= LOG_LEVEL) { + } else if (level >= LogLevel.Log) { console.log(type + ":", ...messages); - } else if (level >= DEBUG_LEVEL) { + } else if (level >= LogLevel.Debug) { console.debug(type + ":", ...messages); - } else if (level >= TRACE_LEVEL) { + } else if (level >= LogLevel.Trace) { console.log( "Trace:", type + ":", @@ -180,16 +240,80 @@ class BaseLogger { return new Logger(this, type); } info(type: string, ...messages: unknown[]) { - this.add(type, INFO_LEVEL, ...messages); + this.add(type, LogLevel.Info, ...messages); + } + list( + offset: number = 0, + limit: number = 50, + type?: string | null, + min_level?: number, + allowed_level?: number[], + ) { + if (!this.db) return []; + const where = []; + const args: QueryParameterSet = []; + if (type) { + where.push("type = ?"); + args.push(type); + } + if (min_level) { + where.push("level >= ?"); + args.push(min_level); + } + if (allowed_level) { + where.push( + "level IN (" + allowed_level.map(() => "?").join(",") + ")", + ); + args.push(...allowed_level); + } + args.push(limit, offset); + const where_str = where.length ? " WHERE " + where.join(" AND ") : ""; + const cur = this.db.queryEntries( + `SELECT * FROM log${where_str} ORDER BY id DESC LIMIT ? OFFSET ?;`, + args, + ); + return this.#convert(cur); + } + list_page( + page: number = 0, + page_size: number = 50, + type?: string | null, + min_level?: number, + allowed_level?: number[], + ) { + if (!this.db) return []; + const where = []; + const args: QueryParameterSet = []; + if (type) { + where.push("type = ?"); + args.push(type); + } + if (min_level) { + where.push("level >= ?"); + args.push(min_level); + } + if (allowed_level) { + where.push( + "level IN (" + allowed_level.map(() => "?").join(",") + ")", + ); + args.push(...allowed_level); + } + args.push(page_size, (page - 1) * page_size); + const where_str = where.length ? " WHERE " + where.join(" AND ") : ""; + const cur = this.db.queryEntries( + `SELECT * FROM log${where_str} ORDER BY id DESC LIMIT ? OFFSET ?;`, + args, + ); + return this.#convert(cur); } log(type: string, ...messages: unknown[]) { - this.add(type, LOG_LEVEL, ...messages); + this.add(type, LogLevel.Log, ...messages); } trace(type: string, ...messages: unknown[]) { - this.add(type, TRACE_LEVEL, ...messages); + this.add(type, LogLevel.Trace, ...messages); } warn(type: string, ...messages: unknown[]) { - this.add(type, WARN_LEVEL, ...messages); + this.add(type, LogLevel.Warn, ...messages); } } @@ -201,22 +325,22 @@ class Logger { this.#type = type; } debug(...messages: unknown[]) { - this.#base.add(this.#type, DEBUG_LEVEL, ...messages); + this.#base.add(this.#type, LogLevel.Debug, ...messages); } error(...messages: unknown[]) { - this.#base.add(this.#type, ERROR_LEVEL, ...messages); + this.#base.add(this.#type, LogLevel.Error, ...messages); } info(...messages: unknown[]) { - this.#base.add(this.#type, INFO_LEVEL, ...messages); + this.#base.add(this.#type, LogLevel.Info, ...messages); } log(...messages: unknown[]) { - this.#base.add(this.#type, LOG_LEVEL, ...messages); + this.#base.add(this.#type, LogLevel.Log, ...messages); } trace(...messages: unknown[]) { - this.#base.add(this.#type, TRACE_LEVEL, ...messages); + this.#base.add(this.#type, LogLevel.Trace, ...messages); } warn(...messages: unknown[]) { - this.#base.add(this.#type, WARN_LEVEL, ...messages); + this.#base.add(this.#type, LogLevel.Warn, ...messages); } }