From 830b33462d24728732ffec7d524dea5441fc5231 Mon Sep 17 00:00:00 2001 From: Max Date: Sun, 13 Aug 2023 02:26:22 +0200 Subject: [PATCH] feat: add logging --- fresh.gen.ts | 140 +++++++++++++++-------------- lib/cache/cache.ts | 4 +- lib/cache/logs.ts | 37 ++++++++ lib/cache/performance.ts | 11 +-- lib/log.ts | 63 +++++++------ lib/string.ts | 24 +++++ routes/admin/log/index.tsx | 81 +++++++++++++++++ routes/admin/performance/index.tsx | 6 +- routes/api/logs.ts | 14 +++ 9 files changed, 266 insertions(+), 114 deletions(-) create mode 100644 lib/cache/logs.ts create mode 100644 routes/admin/log/index.tsx create mode 100644 routes/api/logs.ts diff --git a/fresh.gen.ts b/fresh.gen.ts index de9850e..93dc204 100644 --- a/fresh.gen.ts +++ b/fresh.gen.ts @@ -5,40 +5,42 @@ import * as $0 from "./routes/_404.tsx"; import * as $1 from "./routes/_app.tsx"; import * as $2 from "./routes/_middleware.ts"; -import * as $3 from "./routes/admin/performance/api.ts"; -import * as $4 from "./routes/admin/performance/index.tsx"; -import * as $5 from "./routes/api/articles/[name].ts"; -import * as $6 from "./routes/api/articles/create/index.ts"; -import * as $7 from "./routes/api/articles/index.ts"; -import * as $8 from "./routes/api/auth/callback.ts"; -import * as $9 from "./routes/api/auth/login.ts"; -import * as $10 from "./routes/api/auth/logout.ts"; -import * as $11 from "./routes/api/cache/index.ts"; -import * as $12 from "./routes/api/images/index.ts"; -import * as $13 from "./routes/api/index.ts"; -import * as $14 from "./routes/api/movies/[name].ts"; -import * as $15 from "./routes/api/movies/enhance/[name].ts"; -import * as $16 from "./routes/api/movies/index.ts"; -import * as $17 from "./routes/api/query/index.ts"; -import * as $18 from "./routes/api/query/sync.ts"; -import * as $19 from "./routes/api/recipes/[name].ts"; -import * as $20 from "./routes/api/recipes/index.ts"; -import * as $21 from "./routes/api/resources.ts"; -import * as $22 from "./routes/api/series/[name].ts"; -import * as $23 from "./routes/api/series/enhance/[name].ts"; -import * as $24 from "./routes/api/series/index.ts"; -import * as $25 from "./routes/api/tmdb/[id].ts"; -import * as $26 from "./routes/api/tmdb/credits/[id].ts"; -import * as $27 from "./routes/api/tmdb/query.ts"; -import * as $28 from "./routes/articles/[name].tsx"; -import * as $29 from "./routes/articles/index.tsx"; -import * as $30 from "./routes/index.tsx"; -import * as $31 from "./routes/movies/[name].tsx"; -import * as $32 from "./routes/movies/index.tsx"; -import * as $33 from "./routes/recipes/[name].tsx"; -import * as $34 from "./routes/recipes/index.tsx"; -import * as $35 from "./routes/series/[name].tsx"; -import * as $36 from "./routes/series/index.tsx"; +import * as $3 from "./routes/admin/log/index.tsx"; +import * as $4 from "./routes/admin/performance/api.ts"; +import * as $5 from "./routes/admin/performance/index.tsx"; +import * as $6 from "./routes/api/articles/[name].ts"; +import * as $7 from "./routes/api/articles/create/index.ts"; +import * as $8 from "./routes/api/articles/index.ts"; +import * as $9 from "./routes/api/auth/callback.ts"; +import * as $10 from "./routes/api/auth/login.ts"; +import * as $11 from "./routes/api/auth/logout.ts"; +import * as $12 from "./routes/api/cache/index.ts"; +import * as $13 from "./routes/api/images/index.ts"; +import * as $14 from "./routes/api/index.ts"; +import * as $15 from "./routes/api/logs.ts"; +import * as $16 from "./routes/api/movies/[name].ts"; +import * as $17 from "./routes/api/movies/enhance/[name].ts"; +import * as $18 from "./routes/api/movies/index.ts"; +import * as $19 from "./routes/api/query/index.ts"; +import * as $20 from "./routes/api/query/sync.ts"; +import * as $21 from "./routes/api/recipes/[name].ts"; +import * as $22 from "./routes/api/recipes/index.ts"; +import * as $23 from "./routes/api/resources.ts"; +import * as $24 from "./routes/api/series/[name].ts"; +import * as $25 from "./routes/api/series/enhance/[name].ts"; +import * as $26 from "./routes/api/series/index.ts"; +import * as $27 from "./routes/api/tmdb/[id].ts"; +import * as $28 from "./routes/api/tmdb/credits/[id].ts"; +import * as $29 from "./routes/api/tmdb/query.ts"; +import * as $30 from "./routes/articles/[name].tsx"; +import * as $31 from "./routes/articles/index.tsx"; +import * as $32 from "./routes/index.tsx"; +import * as $33 from "./routes/movies/[name].tsx"; +import * as $34 from "./routes/movies/index.tsx"; +import * as $35 from "./routes/recipes/[name].tsx"; +import * as $36 from "./routes/recipes/index.tsx"; +import * as $37 from "./routes/series/[name].tsx"; +import * as $38 from "./routes/series/index.tsx"; import * as $$0 from "./islands/Counter.tsx"; import * as $$1 from "./islands/IngredientsList.tsx"; import * as $$2 from "./islands/KMenu.tsx"; @@ -56,40 +58,42 @@ const manifest = { "./routes/_404.tsx": $0, "./routes/_app.tsx": $1, "./routes/_middleware.ts": $2, - "./routes/admin/performance/api.ts": $3, - "./routes/admin/performance/index.tsx": $4, - "./routes/api/articles/[name].ts": $5, - "./routes/api/articles/create/index.ts": $6, - "./routes/api/articles/index.ts": $7, - "./routes/api/auth/callback.ts": $8, - "./routes/api/auth/login.ts": $9, - "./routes/api/auth/logout.ts": $10, - "./routes/api/cache/index.ts": $11, - "./routes/api/images/index.ts": $12, - "./routes/api/index.ts": $13, - "./routes/api/movies/[name].ts": $14, - "./routes/api/movies/enhance/[name].ts": $15, - "./routes/api/movies/index.ts": $16, - "./routes/api/query/index.ts": $17, - "./routes/api/query/sync.ts": $18, - "./routes/api/recipes/[name].ts": $19, - "./routes/api/recipes/index.ts": $20, - "./routes/api/resources.ts": $21, - "./routes/api/series/[name].ts": $22, - "./routes/api/series/enhance/[name].ts": $23, - "./routes/api/series/index.ts": $24, - "./routes/api/tmdb/[id].ts": $25, - "./routes/api/tmdb/credits/[id].ts": $26, - "./routes/api/tmdb/query.ts": $27, - "./routes/articles/[name].tsx": $28, - "./routes/articles/index.tsx": $29, - "./routes/index.tsx": $30, - "./routes/movies/[name].tsx": $31, - "./routes/movies/index.tsx": $32, - "./routes/recipes/[name].tsx": $33, - "./routes/recipes/index.tsx": $34, - "./routes/series/[name].tsx": $35, - "./routes/series/index.tsx": $36, + "./routes/admin/log/index.tsx": $3, + "./routes/admin/performance/api.ts": $4, + "./routes/admin/performance/index.tsx": $5, + "./routes/api/articles/[name].ts": $6, + "./routes/api/articles/create/index.ts": $7, + "./routes/api/articles/index.ts": $8, + "./routes/api/auth/callback.ts": $9, + "./routes/api/auth/login.ts": $10, + "./routes/api/auth/logout.ts": $11, + "./routes/api/cache/index.ts": $12, + "./routes/api/images/index.ts": $13, + "./routes/api/index.ts": $14, + "./routes/api/logs.ts": $15, + "./routes/api/movies/[name].ts": $16, + "./routes/api/movies/enhance/[name].ts": $17, + "./routes/api/movies/index.ts": $18, + "./routes/api/query/index.ts": $19, + "./routes/api/query/sync.ts": $20, + "./routes/api/recipes/[name].ts": $21, + "./routes/api/recipes/index.ts": $22, + "./routes/api/resources.ts": $23, + "./routes/api/series/[name].ts": $24, + "./routes/api/series/enhance/[name].ts": $25, + "./routes/api/series/index.ts": $26, + "./routes/api/tmdb/[id].ts": $27, + "./routes/api/tmdb/credits/[id].ts": $28, + "./routes/api/tmdb/query.ts": $29, + "./routes/articles/[name].tsx": $30, + "./routes/articles/index.tsx": $31, + "./routes/index.tsx": $32, + "./routes/movies/[name].tsx": $33, + "./routes/movies/index.tsx": $34, + "./routes/recipes/[name].tsx": $35, + "./routes/recipes/index.tsx": $36, + "./routes/series/[name].tsx": $37, + "./routes/series/index.tsx": $38, }, islands: { "./islands/Counter.tsx": $$0, diff --git a/lib/cache/cache.ts b/lib/cache/cache.ts index 526d975..6bffdf5 100644 --- a/lib/cache/cache.ts +++ b/lib/cache/cache.ts @@ -6,6 +6,7 @@ import { RedisValue, } from "https://deno.land/x/redis@v0.31.0/mod.ts"; import { createLogger } from "@lib/log.ts"; +import { getTimeCacheKey } from "@lib/string.ts"; const REDIS_HOST = Deno.env.get("REDIS_HOST"); const REDIS_PASS = Deno.env.get("REDIS_PASS") || ""; @@ -82,6 +83,7 @@ export function expire(id: string, seconds: number) { type RedisOptions = { expires?: number; + noLog?: boolean; }; export function del(key: string) { @@ -97,7 +99,7 @@ export function set( content: T, options?: RedisOptions, ) { - log.debug("storing ", { id }); + if (options?.noLog !== true) log.debug("storing ", { id }); return cache.set(id, content, { ex: options?.expires || undefined }); } diff --git a/lib/cache/logs.ts b/lib/cache/logs.ts new file mode 100644 index 0000000..e999532 --- /dev/null +++ b/lib/cache/logs.ts @@ -0,0 +1,37 @@ +import { createLogger } from "@lib/log.ts"; +import * as cache from "@lib/cache/cache.ts"; +import { getTimeCacheKey, parseTimeCacheKey } from "@lib/string.ts"; + +const log = createLogger(""); +log.addEventListener("log", (data) => { + cache.set(`log:${getTimeCacheKey()}`, JSON.stringify(data.detail), { + noLog: true, + }); +}); + +export type Log = { + scope: string; + level: number; + date: Date; + args: unknown[]; +}; + +export async function getLogs() { + const d = new Date(); + const year = d.getFullYear(); + const month = d.getMonth().toString().padStart(2, "0"); + const day = d.getDate().toString().padStart(2, "0"); + + const keys = await cache.keys( + `log:${year}:${month}:${day}:*`, + ); + + const logs = await Promise.all( + keys.map(async (key) => { + const date = parseTimeCacheKey(key); + return { ...JSON.parse(await cache.get(key)), date } as Log; + }), + ); + + return logs.sort((a, b) => a.date.getTime() > b.date.getTime() ? -1 : 1); +} diff --git a/lib/cache/performance.ts b/lib/cache/performance.ts index 93f5d36..11cf307 100644 --- a/lib/cache/performance.ts +++ b/lib/cache/performance.ts @@ -1,4 +1,5 @@ import * as cache from "@lib/cache/cache.ts"; +import { getTimeCacheKey } from "@lib/string.ts"; export type PerformancePoint = { path: string; @@ -16,20 +17,14 @@ export type PerformanceRes = { }; export const savePerformance = (url: string, milliseconds: number) => { - const d = new Date(); - const year = d.getFullYear(); - const month = d.getMonth().toString().padStart(2, "0"); - const day = d.getDay().toString().padStart(2, "0"); - const hour = d.getHours().toString().padStart(2, "0"); - const minute = d.getMinutes().toString().padStart(2, "0"); - const seconds = d.getSeconds().toString().padStart(2, "0"); + const cacheKey = getTimeCacheKey(); const u = new URL(url); if (u.pathname.includes("_frsh/")) return; u.searchParams.delete("__frsh_c"); cache.set( - `performance:${year}:${month}:${day}:${hour}:${minute}:${seconds}`, + `performance:${cacheKey}`, JSON.stringify({ path: decodeURIComponent(u.pathname), search: u.search, diff --git a/lib/log.ts b/lib/log.ts index 607530a..effa4d3 100644 --- a/lib/log.ts +++ b/lib/log.ts @@ -1,51 +1,50 @@ +import { EventEmitter } from "https://deno.land/x/evtemitter@v3.0.0/mod.ts"; + enum LOG_LEVEL { - DEBUG, - INFO, - WARN, - ERROR, + DEBUG = 0, + INFO = 1, + WARN = 2, + ERROR = 3, } +const logFuncs = { + [LOG_LEVEL.DEBUG]: console.debug, + [LOG_LEVEL.INFO]: console.info, + [LOG_LEVEL.WARN]: console.warn, + [LOG_LEVEL.ERROR]: console.error, +} as const; + let longestScope = 0; let logLevel = LOG_LEVEL.WARN; +const ee = new EventEmitter<{ + log: { level: LOG_LEVEL; scope: string; args: unknown[] }; +}>(); + export function setLogLevel(level: LOG_LEVEL) { logLevel = level; } -const getPrefix = (scope: string) => `[${scope.padEnd(longestScope, " ")}]`; - type LoggerOptions = { enabled?: boolean; }; +const createLogFunction = + (scope: string, level: LOG_LEVEL) => (...data: unknown[]) => { + ee.emit("log", { level, scope, args: data }); + if (level <= logLevel) return; + logFuncs[level](`[${scope.padEnd(longestScope, " ")}]`, ...data); + }; + export function createLogger(scope: string, _options?: LoggerOptions) { longestScope = Math.max(scope.length, longestScope); - function debug(...data: unknown[]) { - if (logLevel !== LOG_LEVEL.DEBUG) return; - console.debug(getPrefix(scope), ...data); - } - - function info(...data: unknown[]) { - if (logLevel !== LOG_LEVEL.DEBUG && logLevel !== LOG_LEVEL.INFO) return; - console.info(getPrefix(scope), ...data); - } - - function warn(...data: unknown[]) { - if ( - logLevel !== LOG_LEVEL.DEBUG && logLevel !== LOG_LEVEL.INFO && - logLevel !== LOG_LEVEL.WARN - ) return; - console.warn(getPrefix(scope), ...data); - } - - function error(...data: unknown[]) { - console.error(getPrefix(scope), ...data); - } - return { - debug, - info, - error, - warn, + debug: createLogFunction(scope, LOG_LEVEL.DEBUG), + info: createLogFunction(scope, LOG_LEVEL.INFO), + error: createLogFunction(scope, LOG_LEVEL.ERROR), + warn: createLogFunction(scope, LOG_LEVEL.WARN), + addEventListener: + ((type, cb) => + ee.addEventListener(type, cb)) as typeof ee.addEventListener, }; } diff --git a/lib/string.ts b/lib/string.ts index e9af1ad..768b6ea 100644 --- a/lib/string.ts +++ b/lib/string.ts @@ -113,6 +113,30 @@ function componentToHex(c: number) { return hex.length == 1 ? "0" + hex : hex; } +export function getTimeCacheKey() { + const d = new Date(); + const year = d.getFullYear(); + const month = d.getMonth().toString().padStart(2, "0"); + const day = d.getDate().toString().padStart(2, "0"); + const hour = d.getHours().toString().padStart(2, "0"); + const minute = d.getMinutes().toString().padStart(2, "0"); + const seconds = d.getSeconds().toString().padStart(2, "0"); + return `${year}:${month}:${day}:${hour}:${minute}:${seconds}`; +} + +export function parseTimeCacheKey(key: string) { + const [_year, _month, _day, _hour, _minute, _second] = key.split(":") + .slice(1).map((s) => parseInt(s)); + const d = new Date(); + d.setFullYear(_year); + d.setMonth(_month); + d.setDate(_day); + d.setHours(_hour); + d.setMinutes(_minute); + d.setSeconds(_second); + return d; +} + export function rgbToHex(r: number, g: number, b: number) { return "#" + componentToHex(r) + componentToHex(g) + componentToHex(b); } diff --git a/routes/admin/log/index.tsx b/routes/admin/log/index.tsx new file mode 100644 index 0000000..808ea3a --- /dev/null +++ b/routes/admin/log/index.tsx @@ -0,0 +1,81 @@ +import { MainLayout } from "@components/layouts/main.tsx"; +import { Handlers, PageProps } from "$fresh/server.ts"; +import { AccessDeniedError } from "@lib/errors.ts"; +import { getLogs, Log } from "@lib/cache/logs.ts"; +import { formatDate } from "@lib/string.ts"; +import { renderMarkdown } from "@lib/documents.ts"; + +const renderLog = (t: unknown) => + renderMarkdown(`\`\`\`js +${typeof t === "string" ? t : JSON.stringify(t).trim()} +\`\`\``); + +export const handler: Handlers = { + async GET(_, ctx) { + const logs = await getLogs(); + if (!("session" in ctx.state)) { + throw new AccessDeniedError(); + } + return ctx.render({ + logs: logs.map((l) => { + return { + ...l, + html: l.args.map(renderLog).join("\n"), + }; + }), + }); + }, +}; + +function LogLine( + { log }: { + log: Log; + }, +) { + return ( +
+
+ + {log.date.getHours().toString().padStart(2, "0")}:{log.date + .getMinutes().toString().padStart(2, "0")}:{log.date.getSeconds() + .toString().padStart(2, "0")} {formatDate(log.date)} + + + {log.scope} + + + {log.level} + +
+
+
+          {log.html}
+        
+
+
+ ); +} + +export default function Greet( + { data: { logs }, url }: PageProps<{ logs: Log[] }>, +) { + return ( + +

Performance

+ + {logs.map((r) => { + return ( + + ); + })} +
+ ); +} diff --git a/routes/admin/performance/index.tsx b/routes/admin/performance/index.tsx index ae2532f..d399935 100644 --- a/routes/admin/performance/index.tsx +++ b/routes/admin/performance/index.tsx @@ -1,10 +1,6 @@ import { MainLayout } from "@components/layouts/main.tsx"; import { Handlers, PageProps } from "$fresh/server.ts"; -import { - getPerformances, - PerformancePoint, - PerformanceRes, -} from "@lib/cache/performance.ts"; +import { getPerformances, PerformanceRes } from "@lib/cache/performance.ts"; import { AccessDeniedError } from "@lib/errors.ts"; export const handler: Handlers = { diff --git a/routes/api/logs.ts b/routes/api/logs.ts new file mode 100644 index 0000000..f3fdf71 --- /dev/null +++ b/routes/api/logs.ts @@ -0,0 +1,14 @@ +import { Handlers } from "$fresh/server.ts"; +import { createStreamResponse } from "@lib/helpers.ts"; + +const activeResponses: ReturnType[] = []; + +export const handler: Handlers = { + GET() { + const r = createStreamResponse(); + + activeResponses.push(r); + + return r.response; + }, +};