现象 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 一致
96 lines
3.1 KiB
Go
96 lines
3.1 KiB
Go
package services
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// 托管方式选择是「卸载时删容器还是删 unit」的唯一依据,四条分支都要钉死。
|
|
func TestChooseHostRunMode(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
caps *HostCapabilities
|
|
want string
|
|
wantErr string
|
|
}{
|
|
{
|
|
name: "docker 可用 → 容器(即使同时有 systemd 也优先容器)",
|
|
caps: &HostCapabilities{HasDocker: true, DockerUsable: true, HasSystemd: true},
|
|
want: HostRunModeDocker,
|
|
},
|
|
{
|
|
name: "装了 docker 但 daemon 没起来 → 回退 systemd",
|
|
caps: &HostCapabilities{HasDocker: true, DockerUsable: false, HasSystemd: true},
|
|
want: HostRunModeSystemd,
|
|
},
|
|
{
|
|
name: "没有 docker 也没有 systemd → 明确报错,不裸跑",
|
|
caps: &HostCapabilities{HasDocker: false, DockerUsable: false, HasSystemd: false},
|
|
wantErr: "既没有可用的 Docker",
|
|
},
|
|
{
|
|
name: "探测结果缺失 → 报错而不是猜一个",
|
|
caps: nil,
|
|
wantErr: "缺少目标机能力探测结果",
|
|
},
|
|
}
|
|
for _, c := range cases {
|
|
got, err := chooseHostRunMode(c.caps)
|
|
if c.wantErr != "" {
|
|
if err == nil || !strings.Contains(err.Error(), c.wantErr) {
|
|
t.Fatalf("%s: 期望报错含 %q,实际 got=%q err=%v", c.name, c.wantErr, got, err)
|
|
}
|
|
continue
|
|
}
|
|
if err != nil {
|
|
t.Fatalf("%s: 不应报错,实际 %v", c.name, err)
|
|
}
|
|
if got != c.want {
|
|
t.Fatalf("%s: 期望 %q,实际 %q", c.name, c.want, got)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestNormalizeArch(t *testing.T) {
|
|
for in, want := range map[string]string{
|
|
"x86_64": "amd64", "amd64": "amd64", "aarch64": "arm64", "arm64": "arm64",
|
|
} {
|
|
got, err := normalizeArch(in)
|
|
if err != nil || got != want {
|
|
t.Fatalf("%s → %q (err=%v),期望 %q", in, got, err, want)
|
|
}
|
|
}
|
|
if _, err := normalizeArch("mips64"); err == nil {
|
|
t.Fatal("未知架构应报错")
|
|
}
|
|
}
|
|
|
|
// 容器名必须带上采集器 ID:同一台机器可能装多个主机采集器,
|
|
// 用固定名字会互相覆盖(第二个部署把第一个容器删掉)。
|
|
func TestPromtailContainerNameIsPerCollector(t *testing.T) {
|
|
a, b := promtailContainerName(2), promtailContainerName(3)
|
|
if a == b {
|
|
t.Fatalf("不同采集器不能共用容器名: %s", a)
|
|
}
|
|
if !strings.HasPrefix(a, "lokistack-promtail-") {
|
|
t.Fatalf("容器名不符合约定: %s", a)
|
|
}
|
|
}
|
|
|
|
// 探测摘要要能一眼区分「没装 docker」和「装了但起不来」——这两种处置完全不同。
|
|
func TestDockerSummaryDistinguishesStates(t *testing.T) {
|
|
missing := (&HostCapabilities{}).DockerSummary()
|
|
broken := (&HostCapabilities{HasDocker: true}).DockerSummary()
|
|
ok := (&HostCapabilities{HasDocker: true, DockerUsable: true, DockerVersion: "27.5.1", DockerEnabled: "enabled"}).DockerSummary()
|
|
|
|
if !strings.Contains(missing, "未安装") {
|
|
t.Fatalf("未安装 docker 的摘要不对: %s", missing)
|
|
}
|
|
if !strings.Contains(broken, "不可用") || strings.Contains(broken, "未安装") {
|
|
t.Fatalf("装了但不可用的摘要不对: %s", broken)
|
|
}
|
|
if !strings.Contains(ok, "27.5.1") || !strings.Contains(ok, "enabled") {
|
|
t.Fatalf("可用状态的摘要不对: %s", ok)
|
|
}
|
|
}
|