feat(collectors): 可编辑采集配置(B 档结构化编辑器)+ 修 host 标签 / syslog 时间戳
=== 一、两个线上 bug(172.19.16.8 真机取证)===
1) host 标签是字面量 ${HOSTNAME}
Promtail 不做环境变量展开(除非 -config.expand-env + systemd Environment),
实测 21,825 条日志的 host 全是 "${HOSTNAME}",无法按主机过滤。
→ 安装时 `hostname -s` 探测真实主机名并渲染进配置,结果存 Collector.ResolvedHostname,
让「预览配置」与线上一致。
2) 系统日志时间戳解析失效
TencentOS 4 的 /var/log/messages 是 ISO8601(2026-07-17T15:38:24.923784+08:00),
旧正则按传统 syslog(Oct 28 18:11:06)写死 → timestamp 阶段匹配不上,用的是采集时间,
回填历史日志时间全错。
→ 安装时探测目标机格式:iso8601 → RFC3339Nano;rfc3164 → Jan _2 15:04:05 + Local;
系统日志路径同时探测(Ubuntu 没有 messages,写死会一条都采不到)。
实测:写一条 10 分钟前的 ISO8601 探针行,Loki 里 loki_ts 与该行自身时间偏差 **0 秒**;
另写一条 2 小时前的被 Loki 以 entry too far behind 拒绝 —— 恰好反证时间戳是按事件时间
解析后推送的(旧实现会用采集时间,反而被接收)。
=== 二、B 档:结构化多 Job 编辑器 ===
后端
- models/scrape_job.go:ScrapeJob + ValidateScrapeJobs
校验 job 名格式、绝对路径、**敏感路径拦截**(/etc/shadow、/etc/sudoers、/root/.ssh、
id_rsa、*.pem…)、正则可编译、job/host/__path__ 保留标签名、数量上限。
- services/promtail_host_config.go:多 job 渲染;多路径展开成多个 static_configs;
pipeline 顺序硬约束 cri → multiline → json → timestamp → drop → limit;
encoding 写在 scrape_config 级(放进 static_configs 里 promtail 直接起不来)。
- services/host_profile.go:探测 distro / init / hostname / 时区 / 架构 / journald
与真实存在的系统日志路径。
- CollectorService:renderConfig 实时渲染、PreviewConfig(只渲染不落库)、
Update 支持 scrapeJobs 并校验、安装结果(hostname/syslog 格式/配置快照)落库。
- 新增 POST /api/v1/collectors/:id/preview → {config, warnings, hostname, syslogFormat}。
- installHost:配置先写 .new + `promtail -check-syntax` 通过才替换,重启后轮询
systemctl is-active,失败把 journalctl 尾部带回前端。
前端
- 采集项结构化编辑器:名称/路径/标签常驻,多行合并 / JSON / 自定义时间戳 / 命中丢弃 /
限流折叠在「高级」里;「预览 YAML」弹窗在保存前看到将要下发的配置。
- 本地即时校验 + 旧数据(只有 logPath)自动迁移成 app + system 两个采集项。
=== 三、验证(host-verify-promtail @172.19.16.8)===
- 预览:host=pre-cex-ops-jenkins(无 ${HOSTNAME}),2 job / 3 条系统日志路径 /
ISO8601 正则 / 多行 + drop + 限流 全部按填写渲染。
- 部署:promtail active、NRestarts=0、-check-syntax 通过,主机侧配置与预览逐行一致。
- Loki:新数据 host 全为 pre-cex-ops-jenkins;job=system 415 条/5min、job=app 40 条/5min。
- 负向 7/7 返回指名到字段的 400:/etc/shadow、/root/.ssh/id_rsa、相对路径、坏正则、
job 名带空格、保留标签名、空路径。
镜像:lghost/lokistack:latest + v0.1 = sha256:26172f51187d
This commit is contained in:
1 parent
55d6a74eb3
commit
c983d0e3c1
18 files changed
+1678
-254
No files matched your search
@@ -120,11 +120,52 @@ func (d *CollectorDeployer) installHost(ctx context.Context, endpoint string) er
|
||||
if logPath == "" {
|
||||
logPath = "/var/log/*.log"
|
||||
}
|
||||
conf := PromtailConfig(endpoint, logPath, "host", d.collector.Labels)
|
||||
d.log("info", "上传 Promtail 配置", "")
|
||||
if _, _, err := client.Execute(ctx, fmt.Sprintf("cat > /etc/promtail/promtail.yaml << 'EOF'\n%s\nEOF", conf)); err != nil {
|
||||
// 目标机真实身份与 syslog 格式只能在安装时探测(见 Bug3 / Bug4):
|
||||
// - host 标签写真实主机名,promtail 不做 ${HOSTNAME} 展开;
|
||||
// - 系统日志按实际格式(ISO8601 / 传统 syslog)选正则。
|
||||
hostname := detectHostname(ctx, client, host)
|
||||
syslogFormat := detectSyslogFormat(ctx, client)
|
||||
d.log("info", fmt.Sprintf("目标机 %s,syslog 格式 %s", hostname, syslogFormat), "")
|
||||
|
||||
// 系统日志路径同样必须探测:Ubuntu 只有 syslog/auth.log,没有 messages
|
||||
sysLogPaths := detectSystemLogPaths(ctx, client)
|
||||
d.log("info", fmt.Sprintf("系统日志路径:%s", strings.Join(sysLogPaths, ",")), "")
|
||||
|
||||
conf, err := PromtailConfigForHost(HostPromtailOptions{
|
||||
Endpoint: endpoint,
|
||||
Cluster: "host",
|
||||
LabelsJSON: d.collector.Labels,
|
||||
Hostname: hostname,
|
||||
SyslogFormat: syslogFormat,
|
||||
SystemLogPaths: sysLogPaths,
|
||||
LogPath: logPath,
|
||||
Jobs: d.collector.ScrapeJobs,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("生成 Promtail 配置失败(现有配置未改动): %w", err)
|
||||
}
|
||||
d.collector.ResolvedHostname = hostname
|
||||
d.collector.SyslogFormat = syslogFormat
|
||||
d.collector.SystemLogPaths = sysLogPaths
|
||||
|
||||
// 先写临时文件并做语法校验,通过后才替换正式配置 ——
|
||||
// 配置坏了会把正在跑的采集干掉,不能直接覆盖。
|
||||
d.log("info", "上传 Promtail 配置并校验", "")
|
||||
if _, _, err := client.Execute(ctx, fmt.Sprintf("cat > /etc/promtail/promtail.yaml.new << 'PROMTAIL_EOF'\n%s\nPROMTAIL_EOF", conf)); err != nil {
|
||||
return fmt.Errorf("上传配置失败: %w", err)
|
||||
}
|
||||
if out, _, err := client.Execute(ctx, "/usr/local/bin/promtail -check-syntax -config.file=/etc/promtail/promtail.yaml.new 2>&1"); err != nil {
|
||||
// 老版本 promtail 可能没有 -check-syntax:只有明确报配置错误才阻断
|
||||
msg := strings.TrimSpace(out)
|
||||
if strings.Contains(msg, "flag provided but not defined") {
|
||||
d.log("info", "当前 promtail 不支持 -check-syntax,跳过语法校验", "")
|
||||
} else {
|
||||
return fmt.Errorf("Promtail 配置校验未通过(现有配置未改动):%s", truncateForMessage(msg, 600))
|
||||
}
|
||||
}
|
||||
if _, _, err := client.Execute(ctx, "mv -f /etc/promtail/promtail.yaml.new /etc/promtail/promtail.yaml"); err != nil {
|
||||
return fmt.Errorf("替换 Promtail 配置失败: %w", err)
|
||||
}
|
||||
|
||||
// systemd 服务(生产级)
|
||||
service := `[Unit]
|
||||
@@ -201,6 +242,45 @@ func truncateForMessage(s string, max int) string {
|
||||
return s[:max] + "…"
|
||||
}
|
||||
|
||||
// detectHostname 取目标机真实主机名(Bug3 修复点)。
|
||||
// promtail 不做环境变量展开,把 ${HOSTNAME} 写进配置只会在 Loki 里留下一堆
|
||||
// 字面量标签,无法按主机过滤。优先 `hostname -s`,失败退回平台里登记的主机名/IP。
|
||||
func detectHostname(ctx context.Context, client *SSHClient, host *models.Host) string {
|
||||
if out, _, err := client.Execute(ctx, "hostname -s 2>/dev/null || hostname 2>/dev/null"); err == nil {
|
||||
if v := strings.TrimSpace(strings.SplitN(out, "\n", 2)[0]); v != "" && !strings.Contains(v, "${") {
|
||||
return v
|
||||
}
|
||||
}
|
||||
if host != nil {
|
||||
if v := strings.TrimSpace(host.Name); v != "" {
|
||||
return v
|
||||
}
|
||||
if v := strings.TrimSpace(host.HostIP); v != "" {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return "unknown-host"
|
||||
}
|
||||
|
||||
// detectSyslogFormat 探测目标机 /var/log 的时间戳格式(Bug4 修复点)。
|
||||
//
|
||||
// TencentOS / RHEL 系(rsyslog 新版): 2026-07-17T15:38:24.923784+08:00 → iso8601
|
||||
// Debian / 老 rsyslog: Oct 28 18:11:06 → syslog
|
||||
//
|
||||
// 探测不到(文件不存在/为空)时按传统 syslog 处理(保守默认)。
|
||||
func detectSyslogFormat(ctx context.Context, client *SSHClient) string {
|
||||
const probe = `f=""; for p in /var/log/messages /var/log/syslog /var/log/secure; do [ -s "$p" ] && f="$p" && break; done; [ -n "$f" ] && head -n 1 "$f"`
|
||||
out, _, err := client.Execute(ctx, probe)
|
||||
if err != nil || strings.TrimSpace(out) == "" {
|
||||
return SyslogFormatClassic
|
||||
}
|
||||
line := strings.TrimSpace(strings.SplitN(out, "\n", 2)[0])
|
||||
if len(line) >= 19 && line[4] == '-' && line[7] == '-' && line[10] == 'T' {
|
||||
return SyslogFormatISO8601
|
||||
}
|
||||
return SyslogFormatClassic
|
||||
}
|
||||
|
||||
func (d *CollectorDeployer) uninstallHost(ctx context.Context) error {
|
||||
host := d.collector.Host
|
||||
if host == nil {
|
||||
|
||||
Reference in new issue
Block a user