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 @@
{{ sqlSummary(line.json.sql) }}
+ {{ line.json.sql }}
+ {{ 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;