优化日志系统:网络请求降噪并按接口聚合,补齐双时间说明与实例标识,发布0.3.2

- 逐条网络日志(请求完成 / 接口返回异常 / 响应刷新CSRF已同步)从 info/warn 降为 debug:
  实测一次 6 账号的模拟抢购里 233 条日志有 174 条(74%)属于这类;真实抢购会产生几千条,
  把界面仅 800 条的内存日志冲掉,反而看不到关键信息。HTTP/解析/传输层错误仍为 error 级
- 新增按接口聚合的网络统计,抢购任务结束时输出一行,替代逐条刷屏:
  "接口统计: 锁定兑换商品 ×341 失败341 平均=95ms 最慢=312ms 最后错误=活动火爆,请稍后再试"
- 日志文件开头新增表头:实例名、数据目录、双时间格式说明(本地时间 + D 斗鱼估算时间)、
  并说明"校准完成前偏移为 0,只显示本地时间"。双时间一直存在却没有任何地方解释过,
  多实例日志复制或汇总后也分不清来源
- 界面日志面板:显示日志级别并按级别着色(此前级别只存在于 CSS class,复制出去就丢);
  新增"复制"按钮,按与日志文件一致的单行格式导出(此前直接复制会变成 5 行一条、
  1204 行里其实只有 233 条日志)
- 设置页"日志显示等级"补充说明:Info 起只显示关键日志,需要逐请求细节时切到 Debug
- 新增测试:TestRequestStatsAggregatesByLabel、TestLogFileHeaderDocumentsFormat、
  TestRunNowLogsAggregateInsteadOfPerRequest(断言必须有聚合行、且不得出现逐条请求日志)
This commit is contained in:
yml2213
2026-10-03 19:33:14 +08:00
parent 43270e04a6
commit dce9ce2a5a
12 changed files with 328 additions and 11 deletions
+25 -2
View File
@@ -1,10 +1,24 @@
<script setup lang="ts">
import { nextTick, ref, watch } from 'vue'
import { ClipboardSetText } from '@wails/runtime/runtime'
import { useLogs } from '@/composables/useLogs'
const { logs, clearLogs } = useLogs()
const { logs, clearLogs, exportLogsText } = useLogs()
const expanded = ref(false)
const logBodyRef = ref<HTMLElement | null>(null)
const copyState = ref('复制')
const copyLogs = async () => {
if (logs.value.length === 0) return
try {
// 复制成与后端日志文件一致的单行格式,避免面板的多 span 变成 5 行一条、丢失级别。
const ok = await ClipboardSetText(exportLogsText())
copyState.value = ok ? '已复制' : '复制失败'
} catch {
copyState.value = '复制失败'
}
setTimeout(() => { copyState.value = '复制' }, 1500)
}
watch(logs, async () => {
if (!expanded.value) return
@@ -21,7 +35,10 @@ watch(logs, async () => {
<span>日志</span>
<span v-if="logs.length" class="log-count">{{ logs.length }}</span>
</button>
<button v-if="expanded" class="btn btn-compact" @click="clearLogs">清空</button>
<div v-if="expanded" class="log-actions">
<button class="btn btn-compact" @click="copyLogs">{{ copyState }}</button>
<button class="btn btn-compact" @click="clearLogs">清空</button>
</div>
</div>
<div v-if="expanded" ref="logBodyRef" class="log-body">
<div v-if="logs.length === 0" class="log-i">[系统] 就绪,等待操作...</div>
@@ -31,6 +48,7 @@ watch(logs, async () => {
<span v-if="log.time" class="log-time">[{{ log.time }}]</span>
<span v-if="log.douyuTime" class="log-douyu-time">D:{{ log.douyuTime }}</span>
<span v-if="log.category" class="log-cat">{{ log.category }}</span>
<span class="log-level">[{{ log.level || 'info' }}]</span>
<span v-if="log.account" class="log-account">{{ log.account }}</span>
<span class="log-msg">{{ log.message }}</span>
</template>
@@ -51,6 +69,11 @@ watch(logs, async () => {
.log-line { display: flex; align-items: flex-start; gap: 4px; padding: 2px 0; word-break: break-all; }
.log-prefix, .log-time { color: var(--muted); flex-shrink: 0; font-size: 10px; }
.log-douyu-time { color: var(--accent); flex-shrink: 0; font-size: 10px; font-weight: 600; }
.log-actions { display: inline-flex; gap: 6px; }
.log-level { color: var(--muted-2); flex-shrink: 0; font-size: 10px; }
.log-line.log-w .log-level { color: var(--warning); }
.log-line.log-e .log-level { color: var(--danger); }
.log-line.log-s .log-level { color: var(--success); }
.log-cat, .log-account { display: inline-block; padding: 0 4px; border-radius: 3px; font-size: 10px; line-height: 1.4; flex-shrink: 0; }
.log-cat { border: 1px solid var(--line); color: var(--muted); background: var(--surface); }
.log-account { max-width: 96px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; border: 1px solid var(--accent-border); color: var(--accent); background: var(--accent-soft); }
@@ -130,6 +130,10 @@ defineExpose({ open, close })
<option value="warn">Warn</option>
<option value="error">Error</option>
</select>
<div class="form-hint">
Info 起只显示关键日志;需要看每个请求的耗时与错误码时切到 Debug
(抢购任务结束时仍会输出各类接口的<strong>聚合统计</strong>)
</div>
</div>
</div>
</div>
+23
View File
@@ -13,7 +13,9 @@ export interface LogEntry {
export interface LogMessage {
time: string
rawTime?: string
douyuTime?: string
level?: string
type: 's' | 'e' | 'i' | 'w' | 'd'
message: string
category?: string
@@ -57,6 +59,21 @@ const cleanServerLogMessage = (message: string): string => {
.replace(/^\[账号:[^\]]+\]\s*/i, '')
}
// formatLogLine 把一条日志还原成与后端日志文件一致的单行格式。
// 面板用多个 span 渲染,直接复制会变成 5 行一条、还丢掉级别,
// 所以"复制"走这里:时间(D斗鱼时间) [类别][级别][账号] 正文。
const formatLogLine = (log: LogMessage): string => {
if (log.source !== 'server') {
return `[${log.time}] ${log.message}`
}
const time = log.rawTime || log.time || ''
const douyu = log.douyuTime ? `(D${log.douyuTime})` : ''
const category = log.category ? `[${log.category}]` : ''
const level = `[${log.level || 'info'}]`
const account = log.account ? `[账号:${log.account}]` : ''
return `${time}${douyu} ${category}${level}${account} ${log.message}`.replace(/\s+/g, ' ').trim()
}
// 后端结果按“账号_第N包”返回,账号表按账号名展示,这里做一次归并。
const normalizeResultStatuses = (data: Record<string, string>): Record<string, AccountStatus> => {
const next: Record<string, AccountStatus> = {}
@@ -111,7 +128,9 @@ export function useLogs() {
logs.value.push({
time: formatServerLogTime(item.time || ''),
rawTime: item.time || '',
douyuTime: dyTime || undefined,
level: item.level,
type,
message: cleanMsg,
category: item.category,
@@ -162,9 +181,13 @@ export function useLogs() {
clearLocalResults()
}
// exportLogsText 导出全部日志为单行文本,供"复制"按钮使用。
const exportLogsText = (): string => logs.value.map(formatLogLine).join('\n')
return {
logs,
results,
exportLogsText,
addLocalLog,
fetchServerLogs,
checkResults,