admin/logs: show logs file size

Massimo Melina committed Apr 11, 2025 at 12:25 UTC 541b40080d9570695dec4cec8180e8b23a81dfc7
3 files changed +30 -4
admin/src/LogsPage.ts
+15 -3
@@ -3,10 +3,11 @@
3 import { createElement as h, Fragment, ReactNode, useEffect, useMemo, useState } from 'react'
4 import httpCodes from './httpCodes'
5 import { Box, Tab, Tabs } from '@mui/material'
6 -import { API_URL, apiCall, useApi, useApiList } from './api'
6 +import { PageProps } from './App'
7 +import { API_URL, apiCall, useApi, useApiEx, useApiList } from './api'
8 import { DataTable, DataTableColumn, DataTableProps } from './DataTable'
9 import {
9 - CFG, Dict, formatBytes, HTTP_UNAUTHORIZED, newDialog, prefix, shortenAgent, splitAt, tryJson, md, typedKeys,
10 + CFG, Dict, formatBytes, HTTP_UNAUTHORIZED, newDialog, prefix, shortenAgent, splitAt, tryJson, md, typedKeys, with_,
11 _dbg, mapFilter, safeDecodeURIComponent, stringAfter, onlyTruthy, formatTimestamp, formatSpeed, copyTextToClipboard
12 } from '@hfs/shared'
13 import {
@@ -33,12 +34,23 @@ const logLabels = {
34
35 let reloadIps: any
36
36 -export default function LogsPage() {
37 +export default function LogsPage({ setTitleSide }: PageProps) {
38 const [tab, setTab] = useState(0)
39 const files = typedKeys(logLabels)
40 const shorterLabels = !useBreakpoint('sm') && { error_log: "Not", console: h(Terminal), disconnections: h(LinkOff) }
41 const file = files[tab]
42 const fileAvailable = file.endsWith('log')
43 +
44 + const logInfo = useApiEx('get_log_info')
45 + setTitleSide(useMemo(() => fileAvailable && (logInfo.element || with_(logInfo.data, data =>
46 + h(Box, { fontSize: 'smaller' },
47 + `Current: ${formatBytes(_.sum(Object.values(data.current)))}`,
48 + h('br'),
49 + with_(Object.values(data.rotated).flat(), rotatedAsArray =>
50 + `Archived: ${formatBytes(_.sumBy(rotatedAsArray, 'size'))} / ${rotatedAsArray.length} files`)
51 + )
52 + )), [logInfo.element, logInfo.data, fileAvailable]))
53 +
54 return h(Fragment, {},
55 h(Flex, { gap: 0 },
56 h(Tabs, { value: tab, onChange(ev,i){ setTab(i) } },
src/api.log.ts
+7 -1
@@ -4,13 +4,19 @@ import { consoleLog } from './consoleLog'
4 import { HTTP_BAD_REQUEST, HTTP_NOT_ACCEPTABLE, HTTP_NOT_FOUND, wait } from './cross'
5 import { apiAssertTypes } from './misc'
6 import events from './events'
7 -import { loggers } from './log'
7 +import { getRotatedFiles, loggers } from './log'
8 +import { stat } from 'fs/promises'
9 import { SendListReadable } from './SendList'
10 import { forceDownload, serveFile } from './serveFile'
11 import { ips } from './ips'
12 import { disconnectionsLog } from './connections'
13
14 export default {
15 + async get_log_info() {
16 + const current = Object.fromEntries(await Promise.all(loggers.map(async x => [x.name, await stat(x.path).then(s => s.size)])))
17 + return { current, rotated: await getRotatedFiles() }
18 + },
19 +
20 async get_log_file({ file = 'log', range = '' }, ctx) { // this is limited to logs on file, and serves the file instead of a list of records
21 const log = _.find(loggers, { name: file })
22 if (!log)
src/log.ts
+8
@@ -15,6 +15,7 @@ import events from './events'
15 import { getConnection } from './connections'
16 import { app } from './index'
17 import { logGui } from './serveGuiFiles'
18 +import glob from 'fast-glob'
19
20 class Logger {
21 stream?: Writable
@@ -172,6 +173,13 @@ function doubleDigit(n: number) {
173 return n > 9 ? n : '0'+n
174 }
175
176 +export async function getRotatedFiles() {
177 + return Object.fromEntries(await Promise.all(loggers.map(async x => {
178 + const mask = strinsert(x.path, x.path.length - extname(x.path).length, '-2*') // including 2, initial digit of the year, will only take rotated files and not "-error"
179 + return [x.name, (await glob(mask, { stats: true })).map(x => ({ path: x.path, size: x.stats?.size }))]
180 + })))
181 +}
182 +
183 // dump console.error to file
184 let debugLogFile = createWriteStream('debug.log', { flags: 'a' })
185 debugLogFile.once('open', () => {