diff --git a/configs/config.yaml b/configs/config.yaml index 53fce49..493d9c7 100644 --- a/configs/config.yaml +++ b/configs/config.yaml @@ -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 diff --git a/internal/data/repository/bootstrap.go b/internal/data/repository/bootstrap.go index 0ddff95..9c3df24 100644 --- a/internal/data/repository/bootstrap.go +++ b/internal/data/repository/bootstrap.go @@ -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"}, } } diff --git a/internal/data/repository/system_init_ignore_test.go b/internal/data/repository/system_init_ignore_test.go index 3dedc04..6276d11 100644 --- a/internal/data/repository/system_init_ignore_test.go +++ b/internal/data/repository/system_init_ignore_test.go @@ -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") + } + } +} diff --git a/web/src/view/systemTools/logViewer/index.vue b/web/src/view/systemTools/logViewer/index.vue index 66408f0..0a32788 100644 --- a/web/src/view/systemTools/logViewer/index.vue +++ b/web/src/view/systemTools/logViewer/index.vue @@ -137,6 +137,10 @@ 自动换行 +
@@ -208,10 +212,19 @@ {{ line.json.caller }}
{{ line.json.msg || '未命名日志' }}
-
{{ line.json.sql }}
+
+ + SQL + {{ sqlSummary(line.json.sql) }} + +
{{ line.json.sql }}
+
{{ line.json.http_method || '-' }} {{ line.json.http_path || '-' }} HTTP {{ line.json.http_status }} + + 业务 {{ responseSummary(line.json).code }} · {{ responseSummary(line.json).msg || '无消息' }} + 耗时 {{ line.json.latency_ms }} ms 响应 {{ formatFileSize(Number(line.json.bytes_out)) }}
@@ -220,6 +233,15 @@ {{ field.label }}{{ field.value }} +
+ 查看请求与响应详情({{ detailFields(line).length }}) +
+
+ {{ field.label }} +
{{ field.value }}
+
+
+
{{ line.text }}
@@ -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;