优化结构
This commit is contained in:
parent
a1fea67e79
commit
e22301f8f6
|
|
@ -88,7 +88,7 @@ admin:
|
||||||
router_prefix: ""
|
router_prefix: ""
|
||||||
jwt:
|
jwt:
|
||||||
# Production deployments must override this value with a private secret.
|
# 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
|
expires_time: 604800s
|
||||||
buffer_time: 86400s
|
buffer_time: 86400s
|
||||||
issuer: kra
|
issuer: kra
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,6 @@ func DefaultIgnoredAPIs(staticPath string) []IgnoredAPI {
|
||||||
{Method: "POST", Path: "/base/captcha"},
|
{Method: "POST", Path: "/base/captcha"},
|
||||||
{Method: "POST", Path: "/init/initdb"},
|
{Method: "POST", Path: "/init/initdb"},
|
||||||
{Method: "POST", Path: "/init/checkdb"},
|
{Method: "POST", Path: "/init/checkdb"},
|
||||||
{Method: "GET", Path: "/info/getInfoDataSource"},
|
|
||||||
{Method: "GET", Path: "/info/getInfoPublic"},
|
{Method: "GET", Path: "/info/getInfoPublic"},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -137,6 +137,10 @@
|
||||||
<el-switch v-model="wrapLines" aria-label="自动换行" />
|
<el-switch v-model="wrapLines" aria-label="自动换行" />
|
||||||
自动换行
|
自动换行
|
||||||
</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="日志显示模式">
|
<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 === 'structured' }" @click="viewMode = 'structured'">结构化</button>
|
||||||
<button type="button" :class="{ active: viewMode === 'raw' }" @click="viewMode = 'raw'">原始</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>
|
<span v-if="line.json.caller" class="log-caller">{{ line.json.caller }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="log-message">{{ line.json.msg || '未命名日志' }}</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">
|
<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_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="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.latency_ms != null">耗时 {{ line.json.latency_ms }} ms</span>
|
||||||
<span v-if="line.json.bytes_out != null">响应 {{ formatFileSize(Number(line.json.bytes_out)) }}</span>
|
<span v-if="line.json.bytes_out != null">响应 {{ formatFileSize(Number(line.json.bytes_out)) }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -220,6 +233,15 @@
|
||||||
<b>{{ field.label }}</b><span :title="field.value">{{ field.value }}</span>
|
<b>{{ field.label }}</b><span :title="field.value">{{ field.value }}</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
<pre v-else class="log-fallback" :class="wrapLines ? 'whitespace-pre-wrap break-all' : 'whitespace-pre'">{{ line.text }}</pre>
|
<pre v-else class="log-fallback" :class="wrapLines ? 'whitespace-pre-wrap break-all' : 'whitespace-pre'">{{ line.text }}</pre>
|
||||||
</article>
|
</article>
|
||||||
|
|
@ -286,6 +308,7 @@ const limitedByBytes = ref(false)
|
||||||
const windowLimited = ref(false)
|
const windowLimited = ref(false)
|
||||||
const searchText = ref('')
|
const searchText = ref('')
|
||||||
const wrapLines = ref(false)
|
const wrapLines = ref(false)
|
||||||
|
const showSqlLogs = ref(false)
|
||||||
const viewMode = ref('structured')
|
const viewMode = ref('structured')
|
||||||
const loadingDates = ref(false)
|
const loadingDates = ref(false)
|
||||||
const loadingFiles = ref(false)
|
const loadingFiles = ref(false)
|
||||||
|
|
@ -340,8 +363,10 @@ const allLines = computed(() => {
|
||||||
})
|
})
|
||||||
const displayLines = computed(() => {
|
const displayLines = computed(() => {
|
||||||
const keyword = searchText.value.trim().toLowerCase()
|
const keyword = searchText.value.trim().toLowerCase()
|
||||||
if (!keyword) return allLines.value
|
return allLines.value.filter(line => {
|
||||||
return allLines.value.filter(line => line.text.toLowerCase().includes(keyword))
|
if (viewMode.value === 'structured' && !showSqlLogs.value && isSqlLog(line.json)) return false
|
||||||
|
return !keyword || line.text.toLowerCase().includes(keyword)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
const emptyContentText = computed(() => {
|
const emptyContentText = computed(() => {
|
||||||
if (!activePath.value) return '请先选择日志日期和文件'
|
if (!activePath.value) return '请先选择日志日期和文件'
|
||||||
|
|
@ -396,6 +421,8 @@ function parseLogLine(text) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function logEntryTone(log) {
|
function logEntryTone(log) {
|
||||||
|
const response = responseSummary(log)
|
||||||
|
if (response && Number(response.code) !== 0) return 'entry-error'
|
||||||
const level = log?.level
|
const level = log?.level
|
||||||
if (/^(error|dpanic|panic|fatal)$/i.test(level || '')) return 'entry-error'
|
if (/^(error|dpanic|panic|fatal)$/i.test(level || '')) return 'entry-error'
|
||||||
if (/^warn$/i.test(level || '')) return 'entry-warn'
|
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))
|
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) {
|
function httpStatusTone(status) {
|
||||||
const value = Number(status)
|
const value = Number(status)
|
||||||
if (value >= 500) return 'text-error'
|
if (value >= 500) return 'text-error'
|
||||||
|
|
@ -453,9 +505,33 @@ const hiddenStructuredFields = new Set([
|
||||||
'level', 'ts', 'caller', 'msg', 'mod', 'sql', 'gorm_logger',
|
'level', 'ts', 'caller', 'msg', 'mod', 'sql', 'gorm_logger',
|
||||||
'http_method', 'http_path', 'http_route', 'http_status', 'latency_ms',
|
'http_method', 'http_path', 'http_route', 'http_status', 'latency_ms',
|
||||||
'bytes_in', 'bytes_out', 'ua', 'req_headers', 'req_body', 'resp_data',
|
'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'
|
'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) {
|
function displayFieldValue(value) {
|
||||||
if (typeof value === 'boolean') return value ? '是' : '否'
|
if (typeof value === 'boolean') return value ? '是' : '否'
|
||||||
if (typeof value === 'object') return JSON.stringify(value)
|
if (typeof value === 'object') return JSON.stringify(value)
|
||||||
|
|
@ -936,6 +1012,19 @@ onBeforeUnmount(() => window.removeEventListener('resize', updateRootHeight))
|
||||||
font-weight: 700;
|
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 {
|
.log-fields {
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
|
|
@ -963,6 +1052,67 @@ onBeforeUnmount(() => window.removeEventListener('resize', updateRootHeight))
|
||||||
white-space: nowrap;
|
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 {
|
.log-fallback {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 10px 12px;
|
padding: 10px 12px;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue