Files
hermes-agent 7e3356e32b 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 一致
2026-10-28 20:01:38 +00:00

103 lines
5.1 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package models
import "time"
// Collector 日志采集器(部署客户端),关联一个已部署的 Loki 作为推送目标
type Collector struct {
ID uint `json:"id"`
Name string `json:"name"`
Type string `json:"type"` // host-promtail | k8s-daemonset | k8s-fluentbit-sidecar
LokiDeploymentID uint `json:"lokiDeploymentID"`
LokiDeployment *LokiDeployment `json:"lokiDeployment,omitempty"`
RefType string `json:"refType"` // host | cluster
HostID *uint `json:"hostID,omitempty"`
Host *Host `json:"host,omitempty"`
ClusterID *uint `json:"clusterID,omitempty"`
Cluster *Cluster `json:"cluster,omitempty"`
TargetDeployment string `json:"targetDeployment"` // sidecar 专用:目标 deployment 名
Namespace string `json:"namespace"` // 采集器自身部署所在的 k8s 命名空间
// AppLabelSource 决定 app 标签从哪里取值(可填私有标签,便于按自己的规范匹配)。
// 留空 = 默认:app.kubernetes.io/name → app → Pod 的 controller_name(去 hash) → pod 名
// 可填:controller_name / app.kubernetes.io/name / app / k8s.kuboard.cn/name 等任意 Pod 标签
AppLabelSource string `json:"appLabelSource"`
// AppInclude 只采集这些 app 的日志(白名单,按 AppLabelSource 解析出的 app 值匹配)。
// 留空 = 不限制。配合 TargetNamespaces 可显著收敛日志量。
AppInclude []string `json:"appInclude"`
// TargetNamespaces 只采集这些命名空间的 Pod 日志;为空表示采集全集群。
// 对应 promtail 里 kubernetes_sd_configs.namespaces.names
TargetNamespaces []string `json:"targetNamespaces"`
LogPath string `json:"logPath"` // 采集日志路径
Labels string `json:"labels"` // 额外标签(JSON)
// ScrapeJobs 主机模式的结构化采集项(多 job / 多路径 / 多行合并 / 丢弃 / 限流)。
// 为空时按 LogPath 生成默认的两个 job(app + system),保持旧行为。
ScrapeJobs []ScrapeJob `json:"scrapeJobs,omitempty"`
// ResolvedHostname / SyslogFormat 是上一次安装到目标机时探测到的真实值。
// 用途:① host 标签写真实主机名(不再写 ${HOSTNAME});② 系统日志按目标机
// 实际格式(ISO8601 vs 传统 syslog)选正则 —— 这两条是安装期才能知道的信息,
// 存下来才能让「配置预览」和线上真实配置一致。
// SystemLogPaths 安装时探测到的系统日志绝对路径(Ubuntu/Debian 与 RHEL 系不同),
// 存下来让「预览配置」和实际下发保持一致。
SystemLogPaths []string `json:"systemLogPaths,omitempty"`
ResolvedHostname string `json:"resolvedHostname,omitempty"`
SyslogFormat string `json:"syslogFormat,omitempty"` // iso8601 | syslog
// RunMode 主机采集器的托管方式:docker | systemd。
// 安装时由目标机能力探测决定(docker 优先、systemd 兜底),必须落库:
// 卸载/升级要知道该删容器还是删 systemd 单元,否则会留孤儿进程继续采日志。
RunMode string `json:"runMode,omitempty"`
Status string `json:"status"` // pending | active | error
StatusMessage string `json:"statusMessage"`
Config string `json:"config"` // 生成的配置快照
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
func (c *Collector) Sanitized() map[string]interface{} {
var loki map[string]interface{}
if c.LokiDeployment != nil {
loki = map[string]interface{}{
"id": c.LokiDeployment.ID,
"name": c.LokiDeployment.Name,
"endpoint": c.LokiDeployment.Endpoint,
"status": c.LokiDeployment.Status,
}
}
var host map[string]interface{}
if c.Host != nil {
host = c.Host.Sanitized()
}
var cluster map[string]interface{}
if c.Cluster != nil {
cluster = c.Cluster.Sanitized()
}
return map[string]interface{}{
"id": c.ID,
"name": c.Name,
"type": c.Type,
"lokiDeploymentID": c.LokiDeploymentID,
"lokiDeployment": loki,
"refType": c.RefType,
"hostID": c.HostID,
"host": host,
"clusterID": c.ClusterID,
"cluster": cluster,
"targetDeployment": c.TargetDeployment,
"namespace": c.Namespace,
// 采集范围配置:必须一起输出,否则前端编辑框读不到(显示为空),
// 用户一保存就会把现有配置写空 —— 这是重构加 Sanitized() 时漏字段导致的回归。
"targetNamespaces": c.TargetNamespaces,
"appLabelSource": c.AppLabelSource,
"appInclude": c.AppInclude,
"logPath": c.LogPath,
"labels": c.Labels,
"scrapeJobs": c.ScrapeJobs,
"systemLogPaths": c.SystemLogPaths,
"resolvedHostname": c.ResolvedHostname,
"syslogFormat": c.SyslogFormat,
"runMode": c.RunMode,
"status": c.Status,
"statusMessage": c.StatusMessage,
"createdAt": c.CreatedAt,
"updatedAt": c.UpdatedAt,
}
}