Skip to content

Commit

Permalink
add control
Browse files Browse the repository at this point in the history
  • Loading branch information
arily committed Nov 23, 2024
1 parent 086d7b9 commit 9d5c06a
Show file tree
Hide file tree
Showing 6 changed files with 159 additions and 59 deletions.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,10 @@
"directory-tree": "^3.5.2",
"dotenv-cli": "^7.4.2",
"drizzle-orm": "^0.30.10",
"fs-reverse": "^0.0.3",
"glob": "10.3.10",
"highlight.js": "^11.10.0",
"i": "^0.3.7",
"image-type": "^5.2.0",
"lodash-es": "^4.17.21",
"lowlight": "^3.1.0",
Expand Down
51 changes: 49 additions & 2 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 11 additions & 0 deletions src/def/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,17 @@ import type {
ScoreRankingSystem,
} from './common'

export enum LogLevel {
unknown = -1,
error = 0,
warn = 1,
info = 2,
http = 3,
verbose = 4,
debug = 5,
silly = 6,
}

export enum Lang {
enGB = 'en-GB',
zhCN = 'zh-CN',
Expand Down
53 changes: 49 additions & 4 deletions src/pages/admin/logs.vue
Original file line number Diff line number Diff line change
@@ -1,13 +1,26 @@
<script setup lang="ts" async>
import { LogLevel } from '~/def'
enum Direction {
asc = 'asc',
desc = 'desc',
}
definePageMeta({
middleware: 'admin',
})
const app = useNuxtApp()
const last = ref(50)
const { data: logs } = await app.$client.admin.log.last.useQuery(last)
const { t, locale } = useI18n()
const direction = ref<Direction>(Direction.desc)
const q = reactive({
last: 50,
loglevel: LogLevel.warn,
})
const { data: logs, refresh } = await app.$client.admin.log.last.useQuery(q)
watch(q, () => refresh())
useHead({
title: () => t(localeKey.title.logs.__path__),
titleTemplate: title => `${title} - ${t(localeKey.server.name.__path__)}`,
Expand All @@ -28,6 +41,38 @@ async function truncate() {
truncate
</button>
</div>
<div class="grid grid-cols-12 py-4 gap-4">
<div class="col-span-12 md:col-span-6 lg:col-span-4">
<label for="loglevel" class="label">Log Level: {{ LogLevel[q.loglevel] }}</label>
<input id="loglevel" v-model.number="q.loglevel" type="range" min="0" max="6" class="range range-sm" step="1">
<div class="flex w-full justify-between px-2 text-xs">
<span>|</span>
<span>|</span>
<span>|</span>
<span>|</span>
<span>|</span>
<span>|</span>
<span>|</span>
</div>
</div>
<div class="col-span-6 md:col-span-4 lg:col-span-2 form-control">
<label for="last" class="label">Last</label>
<input id="last" v-model.number="q.last" class="input input-sm">
</div>
<div class="col-span-6 md:col-span-4 lg:col-span-2 form-control">
<label for="direction" class="label">Direction</label>
<select id="direction" v-model="direction" class="select select-sm">
<option v-for="d in Object.values(Direction)" :key="d" :value="d">
{{ d }}
</option>
</select>
</div>
</div>
<div class="py-4">
<button class="btn" @click="() => refresh()">
fetch
</button>
</div>
<div class="overflow-x-auto rounded border border-base-300">
<table class="table table-sm table-zebra table-pin-rows">
<thead>
Expand All @@ -40,9 +85,9 @@ async function truncate() {
</tr>
</thead>
<tbody>
<tr v-for="log in logs" :key="`log-${log.timestamp}`">
<tr v-for="log in direction === Direction.asc ? logs : logs?.toReversed()" :key="`log-${log.timestamp}`">
<td class="whitespace-pre">
{{ log.timestamp.toLocaleString(locale) }}
{{ new Date(log.timestamp).toLocaleString(locale) }}
</td>
<td class="whitespace-pre">
{{ log.level }}
Expand Down
88 changes: 40 additions & 48 deletions src/server/backend/$base/server/log.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { resolve } from 'node:path'
import { access, rename, writeFile } from 'node:fs/promises'
import fs from 'node:fs'
import fsR from 'fs-reverse'
import winston from 'winston'
import { type Id } from '..'
import { Monitored } from './@extends'
import { Logger, disposeAll, observe } from '$base/logger'
import { type UserCompact } from '~/def/user'
import { LogLevel } from '~/def'

export class LogProvider implements Monitored {
[Monitored.status]: Monitored[typeof Monitored.status] = [Monitored.Status.Up]
Expand All @@ -15,39 +17,26 @@ export class LogProvider implements Monitored {
LogProvider.setupFileTransports()
}

async get(last: number) {
async get(opt: Partial<{ last: number; loglevel: LogLevel }>) {
const {
last = 100,
loglevel = LogLevel.warn,
} = opt
try {
await access(LogProvider.combined, fs.constants.R_OK)
return LogProvider.readLastNLinesFromFile(LogProvider.combined, last)
.then(lines => lines.map((line) => {
if (!line.trim()) {
return undefined
}
try {
const v = JSON.parse(line)
v.timestamp = new Date(v.timestamp)
return v
}
catch (e) {
return {
level: 'Worst',
label: 'Unknown cause, recorded bad log',
backend: 'Log',
message: line,
timestamp: new Date(0),
}
}
}).filter(TSFilter)) as Promise<{
[x: string]: unknown
level: string
label: string
backend?: string
timestamp: Date
message?: string
fix?: string
}[]>
return await LogProvider.readLastNLinesFromFile(LogProvider.combined, last, loglevel)
.then(lines => lines.filter(TSFilter)) as {
[x: string]: unknown
level: string
label: string
backend?: string
timestamp: Date
message?: string
fix?: string
}[]
}
catch (e) {
console.error(e)
this[Monitored.status] = [Monitored.Status.Degraded]
throw e
}
Expand Down Expand Up @@ -90,27 +79,30 @@ export class LogProvider implements Monitored {
observe(new winston.transports.File({ ...LogProvider.sharedBaseCfg, filename: LogProvider.combined }))
}

static async readLastNLinesFromFile(filePath: string, n: number): Promise<string[]> {
const fileStream = fs.createReadStream(filePath, { encoding: 'utf8', highWaterMark: 1024 * 1024 })
const buffer: string[] = []
const lastNLines: string[] = []
static async readLastNLinesFromFile(filePath: string, n: number, level: LogLevel): Promise<Record<any, any>[]> {
const fileStream = fsR(filePath, { encoding: 'utf8', highWaterMark: 1024 * 1024, matcher: '\n' }) as fs.ReadStream

for await (const chunk of fileStream) {
const lines = chunk.split(/\r?\n/)
const tail = lines.pop()
if (tail) {
buffer.unshift(tail)
}
lastNLines.push(...lines.reverse())
if (lastNLines.length >= n) {
break
}
}
const matched = await new Promise<Array<Record<any, any> & { level: LogLevel }>>((resolve) => {
const matched: Array<Record<any, any> & { level: LogLevel }> = []
fileStream.on('data', (line) => {
try {
const json = JSON.parse(line as string)
if (LogLevel[json.level as keyof typeof LogLevel] <= level) {
matched.push(json)
}
if (matched.length >= n) {
resolve(matched)
}
}
catch {}
})
fileStream.on('end', () => {
resolve(matched)
})
})

if (lastNLines.length < n) {
lastNLines.push(...buffer)
}
fileStream.destroy()

return lastNLines.slice(0, n)
return matched
}
}
13 changes: 8 additions & 5 deletions src/server/trpc/routers/admin/log.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
import { number } from 'zod'
import { nativeEnum, number, object } from 'zod'
import { LogLevel } from '~/def'
import { logs } from '~/server/singleton/service'
import { staffProcedure as pAdmin } from '~/server/trpc/middleware/role'
import { router as _router } from '~/server/trpc/trpc'

export const router = _router({
last: pAdmin.input(number()).query(async ({ input }) => {
return (await logs.get(input)).reverse()
}),
last: pAdmin
.input(object({ last: number(), loglevel: nativeEnum(LogLevel) }))
.query(async ({ input }) => {
return (await logs.get(input)).reverse()
}),
truncate: pAdmin.mutation(async ({ ctx }) => {
await logs.truncate(ctx.user)
return await logs.get(2)
return await logs.get({ last: 2 })
}),
})

0 comments on commit 9d5c06a

Please sign in to comment.