feat(collectors): 主机采集器容器优先托管 + 采集器日志查看 + 推送地址纠正

现象
1. 主机采集器只有 systemd 一条路,跨发行版差异大(这轮已踩过 param 被删、日志路径、
   时间戳格式三处);进程存活/开机自启也没有统一保证。
2. 「采集器日志」在主机模式下无法从平台查看,排「起没起来/在报什么错」必须 SSH。
3. 实测发现:Loki 部署把 listenIP 填成 0.0.0.0 时,部署记录 endpoint 是
   http://0.0.0.0:3100,采集器照抄去推 -> connect: connection refused,
   而容器/systemd 都是 running、平台 status=active —— 日志静默丢失。

根因
1. 托管方式写死在代码里(promtail 二进制 + systemd 单元),没有能力探测。
2. 没有对外暴露采集器自身日志的接口。
3. 渲染 promtail 配置时直接用 deployment.Endpoint,没有校验它是不是「可连接的推送地址」;
   另外容器用 bridge 网络,127.0.0.1 在容器里指向容器自身,同机 Loki 也会推失败。

修法
- services/collector_host_runtime.go(新增)
  * detectHostCapabilities:一次 SSH 往返探测 架构/init/systemd/docker可用/开机自启/SELinux/journald
  * chooseHostRunMode:docker 优先 -> systemd 兜底 -> 两者都没有则明确报错(不裸跑进程)
  * installHostDocker:grafana/promtail:3.4.0 容器,--restart=always --memory=256m --cpus=1
    --log-opt max-size=10m,max-file=3,挂 /etc/promtail(:ro) /var/lib/promtail /var/log(:ro)
    (+journald 相关),SELinux Enforcing 时 --security-opt label=disable;
    先 docker run --rm -check-syntax 校验配置,再替换正式配置
  * waitContainerRunning:看 Status + RestartCount,起来就崩立即报错并带回 docker logs
  * hostPushEndpoint:0.0.0.0/:: 换真实主机 IP;容器模式下 127.0.0.1/localhost 也换;
    换不了明确报错
  * CollectorLogs:主机 docker logs / journalctl,K8s DaemonSet Pod 日志、Sidecar fluent-bit 容器日志
- collector_deployer.go:installHost 探测->选模式->渲染配置->分发;systemd 路径从 docker 迁回时
  清理同名容器;卸载时容器与 systemd 两种方式都清(RunMode 对老记录是空的)
- 拉不到镜像(内网无外网)时降级 systemd 二进制托管,其它错误原样抛出
- models/collector.go:新增 runMode 并纳入 Sanitized;services.Update 白名单同步
- 接口 GET /api/v1/collectors/:id/logs?tail=N;前端卡片加「运行方式」+「日志」弹窗
- build.sh:detect_go 的 INFO 走 stderr(原来被打进 GO_CMD,构建必失败)

验证(172.19.16.8 / pre-cex-ops-jenkins,collector id=2)
- 安装后 runMode=docker,容器 Up,RestartPolicy=always,systemd promtail 自动停用
- 推送地址自动纠正为 http://172.19.16.8:3100,Loki 近 10 分钟查到 4919 条
  job=system/job=app、host=pre-cex-ops-jenkins
- docker 垫片模拟「docker 不可用」-> 自动回退 runMode=systemd、unit active+enabled、
  日志接口 source=systemd;移除垫片重装 -> 切回 docker
- 卸载:容器 0 个、unit 文件不存在、/etc/promtail 不存在、runMode 清空;重装恢复 running/0 重启
- K8s 采集器日志接口 source=kubernetes(顺带暴露 429 限流,见后续日志量治理)
- go vet 干净;go test ./... 全绿(新增 chooseHostRunMode / hostPushEndpoint 等 6 组用例);
  tsc -b 通过
- 镜像 lghost/lokistack:latest = :v0.1 = sha256:906a6d5369d7,Pod imageID 一致
This commit is contained in:
hermes-agent committed 2026-10-28 20:01:38 +00:00
1 parent c983d0e3c1
commit 7e3356e32b
16 files changed
+1095 -69

No files matched your search

+30 -2
View File
@@ -107,8 +107,17 @@ func (s *CollectorService) renderConfig(c *models.Collector) (string, error) {
if hostname == "" && c.Host != nil {
hostname = c.Host.HostIP
}
// 预览要与线上一致:这里也走一遍推送地址纠正(0.0.0.0 / 容器内回环)
pushEndpoint := endpoint
hostIP := ""
if c.Host != nil {
hostIP = c.Host.HostIP
}
if v, perr := hostPushEndpoint(endpoint, hostIP, c.RunMode == HostRunModeDocker); perr == nil {
pushEndpoint = v
}
return PromtailConfigForHost(HostPromtailOptions{
Endpoint: endpoint,
Endpoint: pushEndpoint,
Cluster: "host",
LabelsJSON: c.Labels,
Hostname: hostname,
@@ -184,6 +193,19 @@ func (s *CollectorService) PreviewConfig(id uint, updates map[string]interface{}
if c.ResolvedHostname == "" {
warnings = append(warnings, "尚未成功安装过:host 标签将使用平台登记的主机名,实际安装时会替换为目标机真实 hostname")
}
// 推送地址填写了通配/回环地址时提前提示:这类地址采集器推不上去,
// 表现是「容器/systemd 都 running,但 Loki 里一条日志都没有」。
rawEndpoint := ""
if c.LokiDeployment != nil {
rawEndpoint = c.LokiDeployment.Endpoint
}
hostIP := ""
if c.Host != nil {
hostIP = c.Host.HostIP
}
if fixed, perr := hostPushEndpoint(rawEndpoint, hostIP, c.RunMode == HostRunModeDocker); perr == nil && fixed != rawEndpoint {
warnings = append(warnings, fmt.Sprintf("Loki 推送地址 %s 不可用,部署时会自动改为 %s(建议修正 Loki 部署的监听地址)", rawEndpoint, fixed))
}
}
return map[string]interface{}{"config": cfg, "warnings": warnings, "hostname": c.ResolvedHostname, "syslogFormat": c.SyslogFormat}, nil
}
@@ -412,6 +434,10 @@ func (s *CollectorService) Update(id uint, updates map[string]interface{}) error
if v, ok := updates["systemLogPaths"].([]string); ok && len(v) > 0 {
db.Collectors[i].SystemLogPaths = v
}
// runMode:安装时探测出的托管方式(docker | systemd),卸载后清空。
if v, ok := updates["runMode"].(string); ok {
db.Collectors[i].RunMode = v
}
if v, ok := updates["config"].(string); ok && v != "" {
db.Collectors[i].Config = v
}
@@ -478,6 +504,8 @@ func (s *CollectorService) Install(id uint) {
if len(collector.SystemLogPaths) > 0 {
updates["systemLogPaths"] = collector.SystemLogPaths
}
// 托管方式(docker | systemd)由安装流程探测后写回,前端据此展示运行方式
updates["runMode"] = collector.RunMode
s.Update(id, updates)
}
@@ -493,5 +521,5 @@ func (s *CollectorService) Uninstall(id uint) {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
defer cancel()
NewCollectorDeployer(collector, collector.LokiDeployment, nil).Uninstall(ctx)
s.Update(id, map[string]interface{}{"status": "pending", "statusMessage": "采集器已卸载"})
s.Update(id, map[string]interface{}{"status": "pending", "statusMessage": "采集器已卸载", "runMode": ""})
}