feat(deployments,analysis): 保留弹框重设计 + 分析页命名空间筛选 + 过期 chunk 真正删除

=== 一、保留策略弹框重设计(RetentionDialog)===
旧版问题:
1. 主输入是 YAML textarea,用户必须会写 `limits_config.retention_period: 30d`
2. 三个按钮:取消 / 仅保存 / 保存并应用 —— 「仅保存」只改部署记录不重载 Loki,
   今天已经造成一次真实故障:DB 记录 7 天、运行时 ConfigMap 是 24h,配置漂移
3. 「查看完整配置」是独立按钮 + 独立状态,多一次点击
4. 硬编码提示 `retention_delete_delay: 2h`,而实际已改成 10m,属于误导

新版:
- 主输入改为**保留天数数字框**(1–3650),YAML 由前端拼,用户不用写
- 快捷预设 1/7/14/30/90,实时显示「= 720 小时」
- >7 天给出存储成本提示
- 完整运行 config 改成 `<details>` 折叠,展开即自动加载(去掉独立按钮)
- **按钮只剩 2 个:取消 / 保存并应用**
- 删掉硬编码的 compactor 提示块

=== 二、日志分析页加命名空间筛选 ===
根因:analysis 页写死 namespace: '',等价 `{namespace=~".+"}` —— 一次分析扫全集群
(实测约 370 GB/天),5 条 count_over_time 跑 30s 都出不来,页面永远转圈。

- 新增命名空间下拉(从 Loki label values 拉取,localStorage 记忆)
- 默认取业务命名空间 astralx(其次列表首个),不再默认全集群
- 「全部命名空间(较慢)」降级为显式选项
- 命名空间未就绪时先显示 loading,避免先发一次全集群查询
- 5 条查询改**分批(每批 2 条)**执行 + 429 自动重试一次

=== 三、后端并发/超时调整 ===
- 重查询并发上限 2 → 4,且拿不到令牌时**排队最多 10s**再拒绝
  (原来立即 429,导致分析页 5 条并发里有 3 条直接失败)
- maxLogQueryLimit 1000 → 5000,与前端下拉完全对齐(原来前端选 5000 会被静默截断)

=== 四、过期 chunk 真正删除(关键修复)===
实测发现:索引表已从 COS 删除,但 chunk 对象仍残留 21,354 个 / 24.8 GB,
最老的 45 小时前 —— 因为 Loki 默认 `retention_delete_delay` = **2h**,
标记后要等 2 小时才真正删 chunk。

- 配置显式设置 `compactor.retention_delete_delay: 10m`
- 已用 `loki -verify-config` 校验(config is valid)
- 删除前后对比:
    对象数 46,536 → 23,934;占用 53.61 GB → 27.25 GB
    超期对象 21,354 个 / 24.8 GB → **0 个**
    桶内最老对象 45.2h → 24.4h(= 保留期 1 天)
- deployment_config_test 同步调整:retention_delete_delay 从"禁止出现"移入"必须存在"

镜像:lghost/lokistack:latest + v0.1 = sha256:b4d98e016bca
This commit is contained in:
opsx committed 2026-10-28 17:05:57 +00:00
1 parent 53c7d64b35
commit a32f93f533
12 files changed
+265 -97

No files matched your search

+57 -3
View File
@@ -25,6 +25,9 @@ import { IngressAnalysisSection } from './components/IngressAnalysisSection'
const toLocal = (date: Date) => new Date(date.getTime() - date.getTimezoneOffset() * 60000).toISOString().slice(0, 19)
/** "全部命名空间"哨兵值:对应后端 {namespace=~".+"},代价高,仅作显式选项 */
const ALL_NAMESPACES = '__all__'
/**
* 日志分析页(admin 专用):SLS 风格仪表盘 — 统计卡 + 趋势折线 + 命名空间饼图 +
* 应用流图 + 应用排行表;数据来自 5 条 LogQL 指标查询(POST /logs/metrics)。
@@ -39,6 +42,16 @@ const AnalysisPage = memo(function AnalysisPage() {
const [start, setStart] = useState(() => toLocal(new Date(Date.now() - 3600000)))
const [end, setEnd] = useState(() => toLocal(new Date()))
/**
* 命名空间筛选。
* 之前这里写死 namespace: '',等价于 {namespace=~".+"} —— 一次分析就扫**全集群**
* (实测约 370 GB/天),5 条 count_over_time 跑 30 秒都出不来,页面永远转圈。
* 现在默认只查业务命名空间,用户可下拉切换;"全部命名空间"作为显式选项并列提示较慢。
*/
const [namespaces, setNamespaces] = useState<string[]>([])
const [namespace, setNamespace] = useState<string>(() => localStorage.getItem('analysisNamespace') ?? '')
const [namespaceReady, setNamespaceReady] = useState(false)
useEffect(() => {
logsApi.sources()
.then(result => {
@@ -52,9 +65,32 @@ const AnalysisPage = memo(function AnalysisPage() {
.catch(() => setSources([]))
}, [])
const { data, loading, error, appLabel, refresh } = useAnalysisMetrics({ sourceId, start, end, namespace: '' })
// 拉取可用命名空间;默认优先业务命名空间 astralx,其次列表第一个
useEffect(() => {
if (!sourceId) return
let cancelled = false
logsApi.labelValues(sourceId, 'namespace')
.then(list => {
if (cancelled) return
const values = (list || []).filter(Boolean).sort()
setNamespaces(values)
setNamespace(current => {
if (current && (current === ALL_NAMESPACES || values.includes(current))) return current
const saved = localStorage.getItem('analysisNamespace')
if (saved && (saved === ALL_NAMESPACES || values.includes(saved))) return saved
return values.includes('astralx') ? 'astralx' : values[0] ?? ''
})
})
.catch(() => { if (!cancelled) setNamespaces([]) })
.finally(() => { if (!cancelled) setNamespaceReady(true) })
return () => { cancelled = true }
}, [sourceId])
// hook 内部把空串当作 {namespace=~".+"}(全部),这里做哨兵转换
const scopedNamespace = namespace === ALL_NAMESPACES ? '' : namespace
const { data, loading, error, appLabel, refresh } = useAnalysisMetrics({ sourceId, start, end, namespace: scopedNamespace })
// 每日统计固定近 7 天,不随上方时间范围变化;错误就地展示,不影响主区
const daily = useDailyVolume({ sourceId, namespace: '', appLabel })
const daily = useDailyVolume({ sourceId, namespace: scopedNamespace, appLabel })
// 非 admin:菜单不可见,直访时给明确提示(后端 AdminAuth 403 兜底)
if (!isAdmin) {
@@ -92,10 +128,28 @@ const AnalysisPage = memo(function AnalysisPage() {
))}
</SelectContent>
</Select>
<Select
value={namespace || undefined}
onValueChange={value => {
setNamespace(value)
try { localStorage.setItem('analysisNamespace', value) } catch { /* 隐私模式忽略 */ }
}}
>
<SelectTrigger className="h-9 min-w-44" aria-label={t('filter.namespace')}>
<SelectValue placeholder={t('filter.namespaceEmpty')} />
</SelectTrigger>
<SelectContent>
{namespaces.map(item => <SelectItem key={item} value={item}>{item}</SelectItem>)}
<SelectItem value={ALL_NAMESPACES}>{t('filter.namespaceAll')}</SelectItem>
</SelectContent>
</Select>
<TimeRangePicker start={start} end={end} onChange={(nextStart, nextEnd) => { setStart(nextStart); setEnd(nextEnd) }} />
</div>
{sources.length === 0 ? (
{!namespaceReady && sourceId ? (
/* 命名空间列表未就绪先等待:否则会先用空串发起一次"全集群"查询,非常慢 */
<LoadingSpinner className="h-64" />
) : sources.length === 0 ? (
<EmptyState
icon={BarChart3}
title={t('emptySource.title')}