优化结构

This commit is contained in:
Yvan 2026-08-21 16:09:27 +08:00
parent a1fea67e79
commit e22301f8f6
4 changed files with 162 additions and 5 deletions

View File

@ -88,7 +88,7 @@ admin:
router_prefix: ""
jwt:
# Production deployments must override this value with a private secret.
signing_key: 812e9411-08e7-47bb-85a0-02a2274b68be
signing_key: 593e26b1-54dc-4465-9245-498ae08a957a
expires_time: 604800s
buffer_time: 86400s
issuer: kra

View File

@ -23,7 +23,6 @@ func DefaultIgnoredAPIs(staticPath string) []IgnoredAPI {
{Method: "POST", Path: "/base/captcha"},
{Method: "POST", Path: "/init/initdb"},
{Method: "POST", Path: "/init/checkdb"},
{Method: "GET", Path: "/info/getInfoDataSource"},
{Method: "GET", Path: "/info/getInfoPublic"},
}
}

View File

@ -20,3 +20,11 @@ func TestDefaultIgnoredAPIsIncludeSwagger(t *testing.T) {
}
}
}
func TestDefaultIgnoredAPIsDoNotHideAnnouncementDataSource(t *testing.T) {
for _, api := range DefaultIgnoredAPIs("uploads/file") {
if api.Method == "GET" && api.Path == "/info/getInfoDataSource" {
t.Fatal("announcement data-source endpoint must remain protected by the normal API policy")
}
}
}

View File

@ -137,6 +137,10 @@
<el-switch v-model="wrapLines" aria-label="自动换行" />
自动换行
</label>
<label v-if="viewMode === 'structured'" class="flex shrink-0 items-center gap-2 text-xs text-muted-foreground">
<el-switch v-model="showSqlLogs" aria-label="显示 SQL 日志" />
SQL
</label>
<div class="log-view-mode" role="group" aria-label="日志显示模式">
<button type="button" :class="{ active: viewMode === 'structured' }" @click="viewMode = 'structured'">结构化</button>
<button type="button" :class="{ active: viewMode === 'raw' }" @click="viewMode = 'raw'">原始</button>
@ -208,10 +212,19 @@
<span v-if="line.json.caller" class="log-caller">{{ line.json.caller }}</span>
</div>
<div class="log-message">{{ line.json.msg || '未命名日志' }}</div>
<div v-if="isSqlLog(line.json)" class="log-sql">{{ line.json.sql }}</div>
<details v-if="isSqlLog(line.json)" class="log-detail log-sql-detail">
<summary>
<span>SQL</span>
<code>{{ sqlSummary(line.json.sql) }}</code>
</summary>
<pre class="log-sql">{{ line.json.sql }}</pre>
</details>
<div v-if="isHttpLog(line.json)" class="log-metrics">
<span v-if="line.json.http_method || line.json.http_path"><b>{{ line.json.http_method || '-' }}</b> {{ line.json.http_path || '-' }}</span>
<span v-if="line.json.http_status != null" :class="httpStatusTone(line.json.http_status)">HTTP {{ line.json.http_status }}</span>
<span v-if="responseSummary(line.json)" :class="businessCodeTone(responseSummary(line.json).code)">
业务 {{ responseSummary(line.json).code }} · {{ responseSummary(line.json).msg || '无消息' }}
</span>
<span v-if="line.json.latency_ms != null">耗时 {{ line.json.latency_ms }} ms</span>
<span v-if="line.json.bytes_out != null">响应 {{ formatFileSize(Number(line.json.bytes_out)) }}</span>
</div>
@ -220,6 +233,15 @@
<b>{{ field.label }}</b><span :title="field.value">{{ field.value }}</span>
</span>
</div>
<details v-if="detailFields(line).length" class="log-detail">
<summary>查看请求与响应详情{{ detailFields(line).length }}</summary>
<div class="log-detail-content">
<section v-for="field in detailFields(line)" :key="field.key">
<b>{{ field.label }}</b>
<pre>{{ field.value }}</pre>
</section>
</div>
</details>
</div>
<pre v-else class="log-fallback" :class="wrapLines ? 'whitespace-pre-wrap break-all' : 'whitespace-pre'">{{ line.text }}</pre>
</article>
@ -286,6 +308,7 @@ const limitedByBytes = ref(false)
const windowLimited = ref(false)
const searchText = ref('')
const wrapLines = ref(false)
const showSqlLogs = ref(false)
const viewMode = ref('structured')
const loadingDates = ref(false)
const loadingFiles = ref(false)
@ -340,8 +363,10 @@ const allLines = computed(() => {
})
const displayLines = computed(() => {
const keyword = searchText.value.trim().toLowerCase()
if (!keyword) return allLines.value
return allLines.value.filter(line => line.text.toLowerCase().includes(keyword))
return allLines.value.filter(line => {
if (viewMode.value === 'structured' && !showSqlLogs.value && isSqlLog(line.json)) return false
return !keyword || line.text.toLowerCase().includes(keyword)
})
})
const emptyContentText = computed(() => {
if (!activePath.value) return '请先选择日志日期和文件'
@ -396,6 +421,8 @@ function parseLogLine(text) {
}
function logEntryTone(log) {
const response = responseSummary(log)
if (response && Number(response.code) !== 0) return 'entry-error'
const level = log?.level
if (/^(error|dpanic|panic|fatal)$/i.test(level || '')) return 'entry-error'
if (/^warn$/i.test(level || '')) return 'entry-warn'
@ -425,6 +452,31 @@ function isHttpLog(log) {
return Boolean(log && (log.mod === 'http' || log.http_method || log.http_status != null))
}
function parseEmbeddedJSON(value) {
if (value && typeof value === 'object') return value
if (typeof value !== 'string' || !value.trim()) return null
try {
return JSON.parse(value)
} catch {
return null
}
}
function responseSummary(log) {
const response = parseEmbeddedJSON(log?.resp_data)
if (!response || typeof response.code !== 'number') return null
return { code: response.code, msg: String(response.msg || '') }
}
function businessCodeTone(code) {
return Number(code) === 0 ? 'business-success' : 'business-error'
}
function sqlSummary(sql) {
const compact = String(sql || '').replace(/\s+/g, ' ').trim()
return compact.length > 140 ? `${compact.slice(0, 140)}...` : compact
}
function httpStatusTone(status) {
const value = Number(status)
if (value >= 500) return 'text-error'
@ -453,9 +505,33 @@ const hiddenStructuredFields = new Set([
'level', 'ts', 'caller', 'msg', 'mod', 'sql', 'gorm_logger',
'http_method', 'http_path', 'http_route', 'http_status', 'latency_ms',
'bytes_in', 'bytes_out', 'ua', 'req_headers', 'req_body', 'resp_data',
'req_query', 'error_msg', 'stacktrace',
'service.id', 'service.name', 'service.version', 'node', 'app_id', 'env'
])
const detailFieldLabels = {
req_query: '查询参数',
req_body: '请求体',
resp_data: '响应数据',
req_headers: '请求头',
error_msg: '错误详情',
stacktrace: '调用堆栈'
}
function prettyDetailValue(value) {
const parsed = parseEmbeddedJSON(value)
if (parsed) return JSON.stringify(parsed, null, 2)
if (typeof value === 'object') return JSON.stringify(value, null, 2)
return String(value)
}
function detailFields(line) {
if (!line.json) return []
return Object.keys(detailFieldLabels)
.filter(key => line.json[key] !== '' && line.json[key] != null)
.map(key => ({ key, label: detailFieldLabels[key], value: prettyDetailValue(line.json[key]) }))
}
function displayFieldValue(value) {
if (typeof value === 'boolean') return value ? '是' : '否'
if (typeof value === 'object') return JSON.stringify(value)
@ -936,6 +1012,19 @@ onBeforeUnmount(() => window.removeEventListener('resize', updateRootHeight))
font-weight: 700;
}
.log-metrics .business-success {
border: 1px solid rgb(var(--success-color) / .35);
background: rgb(var(--success-color) / .16);
color: rgb(187 247 208);
}
.log-metrics .business-error {
border: 1px solid rgb(var(--error-color) / .45);
background: rgb(var(--error-color) / .18);
color: rgb(254 202 202);
font-weight: 700;
}
.log-fields {
margin-top: 8px;
align-items: stretch;
@ -963,6 +1052,67 @@ onBeforeUnmount(() => window.removeEventListener('resize', updateRootHeight))
white-space: nowrap;
}
.log-detail {
margin-top: 8px;
border-top: 1px solid rgb(255 255 255 / .12);
color: rgb(203 213 225);
}
.log-detail summary {
display: flex;
min-width: 0;
align-items: center;
gap: 8px;
padding-top: 7px;
cursor: pointer;
color: rgb(203 213 225);
font-size: 11px;
list-style-position: inside;
}
.log-detail summary code {
min-width: 0;
overflow: hidden;
color: rgb(148 163 184);
text-overflow: ellipsis;
white-space: nowrap;
}
.log-detail-content {
display: grid;
gap: 8px;
padding-top: 8px;
}
.log-detail-content section {
min-width: 0;
}
.log-detail-content b {
display: block;
margin-bottom: 4px;
color: rgb(226 232 240);
font-size: 11px;
}
.log-detail-content pre {
max-height: 240px;
margin: 0;
padding: 8px 10px;
overflow: auto;
border-radius: 5px;
background: rgb(0 0 0 / .22);
color: rgb(203 213 225);
font-size: 11px;
line-height: 1.6;
white-space: pre-wrap;
word-break: break-word;
}
.log-sql-detail .log-sql {
margin-bottom: 0;
}
.log-fallback {
margin: 0;
padding: 10px 12px;