fix(hosts,collectors,usage): 主机验证结果失真 + 主机模式系统日志解析 + metrics 单位

=== 一、主机「测试连接」永远显示旧错误(真实 bug)===
HostService.Validate 的失败分支也写 status=active,且从不写 statusMessage:
    if info, err := client.GetHostInfo(ctx); err == nil { 更新 os/arch }
    else { s.Update(id, {status:"active"}) }   // ← 失败也标记成功
结果:连不上也显示正常,页面永远停在几分钟前的旧错误文案,
用户改了 IP 再点测试,还以为"改了还是不行"。

改为区分成功/失败并写清原因:
    成功 → status=active + os/arch + statusMessage="连接成功"
    失败 → status=error + statusMessage=FormatSSHError(err)

=== 二、回环地址在平台里必然连不上 ===
平台跑在容器/Pod 内,127.0.0.1 指向平台自身。
- 新建主机时拦截:hostIP 为 127.0.0.1/localhost/::1 直接报错并解释原因
- 前端表单同步校验 + 文案
- SSH 错误映射补充:连接被拒绝时提示可能是地址填了回环

=== 三、主机模式采集系统日志(两处会导致采集失败)===
1. pipeline 一律套 cri 阶段:cri 只认容器运行时格式,
   套在 /var/log/messages 这类 syslog 上会取不到时间戳、整行被丢。
   → appPipelineStages() 按路径判断:容器日志用 cri,普通文本日志只做限流。
2. promtailExternalLabels 写死 8 空格缩进:主机模板 external_labels 只有 4 空格,
   一旦给采集器配置了标签就生成非法 YAML,Promtail 装不上。
   → 增加 indent 参数,主机 6 空格 / DaemonSet 8 空格。
3. 新增 system 采集任务:
   /var/log/{messages,secure,cron,maillog,syslog,dmesg}
   正则解析 syslog 时间戳(Jan _2 15:04:05,Asia/Shanghai)+ 限流

测试:TestHostPromtailConfigValid / CRIPathKeepsCRIStage / PlainFileHasNoCRIStage
      TestPromtailManifestParses 全部通过

=== 四、资源用量 CPU 解析(nanocores)===
metrics-server 返回的 CPU 常是纳核(如 "194533645n"=194.5m)。
原解析只处理 "m",导致 194 亿核、页面出现"CPU 已达 limit 的 4863341125%"。
→ parseQuantityMilli 支持 n/u/m/整核
实测:CPU 当前=178m,峰值=3300m,建议"峰值占 limit 82%,可考虑提前扩容"

=== 五、其他 ===
- 添加集群默认改用 Token 认证(最小权限、可随时吊销)
- RBAC 片段补 metrics.k8s.io(资源面板需要,独立 API 组常被漏授)
- 文案去掉 Grafana 需求描述(平台自身不部署 Grafana)

镜像:lghost/lokistack:latest + v0.1 = sha256:7bddd0f4d1f1
This commit is contained in:
opsx committed 2026-10-28 17:58:30 +00:00
1 parent a32f93f533
commit bad2a1e624
33 files changed
+1124 -52

No files matched your search

+55 -1
View File
@@ -4,8 +4,12 @@ package services
import (
"bytes"
"fmt"
"os"
"strconv"
"strings"
"text/template"
"lokistack/models"
"lokistack/pkg/crypto"
)
@@ -53,6 +57,11 @@ compactor:
working_directory: /var/lib/loki/compactor
retention_enabled: true
delete_request_store: {{ .CompactorStoreType }}
# 过期 chunk 的删除并发。Loki 默认 150,但各云对象存储的 QPS 限制差别很大,
# 打太猛会被限流(腾讯云 COS 实测返回 503 SlowDown,整轮 apply_retention 失败,
# 残留 21,354 个对象 / 24.8 GB 删不掉)。该值按 provider 取值,可用
# 环境变量 LOKI_COMPACTOR_DELETE_WORKERS 覆盖。
retention_delete_worker_count: {{ .CompactorDeleteWorkers }}
# 过期 chunk 的「标记 → 真正删除」间隔。默认 2h 会让对象存储里
# 多留 2 小时的数据(实测桶内超过保留期的对象一度占到 24.6 GB)。
# 预发环境取 10m:既保留短暂的安全窗口,又不会长期占用存储成本。
@@ -112,6 +121,8 @@ type lokiConfigData struct {
ObjectStore string
RulerStoreType string
CompactorStoreType string
// CompactorDeleteWorkers 过期 chunk 删除并发,按对象存储类型取默认值
CompactorDeleteWorkers int
RingInstanceAddr string // 单副本 127.0.0.1;多副本 0.0.0.0(须可被对端寻址)
RingKVStore string // 单副本 inmemory;多副本 memberlist
MemberlistBlock string // 多副本时非空,注入 memberlist 顶级配置
@@ -176,7 +187,8 @@ func (d *LokiDeployer) generateLokiConfig() string {
StorageConfigBlock: storageBlock,
ObjectStore: objectStore,
RulerStoreType: rulerStore,
CompactorStoreType: compactorStore,
CompactorStoreType: compactorStore,
CompactorDeleteWorkers: compactorDeleteWorkers(d.deployment.StorageConfig),
RingInstanceAddr: ringAddr,
RingKVStore: ringStore,
MemberlistBlock: memberlistBlock,
@@ -197,6 +209,48 @@ func (d *LokiDeployer) generateLokiConfig() string {
return buf.String()
}
// defaultCompactorDeleteWorkers 是 Loki 自身的默认值。
const defaultCompactorDeleteWorkers = 150
// compactorDeleteWorkers 按对象存储类型给出「过期 chunk 删除并发」。
//
// 为什么不能写死一个值:这个并发直接决定 compactor 对对象存储发出的 DELETE 速率,
// 而各云的限流差别很大。打满会被限流并让整轮 apply_retention 失败 ——
// 腾讯云 COS 实测默认 150 就返回 503 SlowDown,过期 chunk 残留 21,354 个 / 24.8 GB
// 删不掉;降到 16 后删除稳定推进。
//
// cos 16 实测 150 触发 503 SlowDown
// obs 32 华为 OBS 未给明确单桶 QPS,取保守值
// minio 32 自建,取决于后端磁盘 IO,别打满
// azure 32 存储账户限流较严
// oss 64 阿里云 OSS 单桶 QPS 万级,留余量
// gcs 64 GCS 高 QPS,但有 per-bucket 操作速率
// s3 96 AWS S3 单前缀约 3,500 DELETE/s,150 也能跑,留余量
//
// 任意 provider 都可用环境变量 LOKI_COMPACTOR_DELETE_WORKERS 覆盖(运维手调无需改代码)。
func compactorDeleteWorkers(sc *models.StorageConfig) int {
if raw := strings.TrimSpace(os.Getenv("LOKI_COMPACTOR_DELETE_WORKERS")); raw != "" {
if n, err := strconv.Atoi(raw); err == nil && n > 0 {
return n
}
}
if sc == nil {
return defaultCompactorDeleteWorkers
}
switch strings.ToLower(strings.TrimSpace(sc.StorageType)) {
case "cos":
return 16
case "obs", "minio", "azure":
return 32
case "oss", "gcs":
return 64
case "s3":
return 96
default:
return defaultCompactorDeleteWorkers
}
}
func (d *LokiDeployer) buildStorageConfigBlock() string {
if d.deployment.StorageConfig == nil {
return ""