=== 一、两个线上 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
53 lines
1.6 KiB
Go
53 lines
1.6 KiB
Go
package services
|
||
|
||
import (
|
||
"context"
|
||
"strings"
|
||
)
|
||
|
||
// systemLogCandidates 是主流 Linux 发行版上系统日志的候选路径。
|
||
//
|
||
// 为什么要探测而不是写死:
|
||
//
|
||
// RHEL / CentOS / TencentOS / AWS Linux / Rocky : messages, secure, cron, dmesg
|
||
// Ubuntu / Debian : syslog, auth.log, kern.log(**没有 messages**)
|
||
// Ubuntu 最小安装(无 rsyslog) : 以上全没有,只能读 systemd journal
|
||
//
|
||
// 早期实现固定采 messages/secure/cron,在 Ubuntu 上采集器"装成功了但一条系统日志都没有"。
|
||
var systemLogCandidates = []string{
|
||
"/var/log/messages",
|
||
"/var/log/syslog",
|
||
"/var/log/secure",
|
||
"/var/log/auth.log",
|
||
"/var/log/cron",
|
||
"/var/log/kern.log",
|
||
"/var/log/dmesg",
|
||
}
|
||
|
||
// detectSystemLogPaths 在目标机上挑出真实存在且非空的系统日志文件。
|
||
// 探测失败时返回 RHEL 系默认值(保守兜底)。
|
||
func detectSystemLogPaths(ctx context.Context, client *SSHClient) []string {
|
||
var quoted []string
|
||
for _, p := range systemLogCandidates {
|
||
quoted = append(quoted, "'"+p+"'")
|
||
}
|
||
// 只输出存在且大小 > 0 的文件,一行一个
|
||
script := "for p in " + strings.Join(quoted, " ") + "; do [ -s \"$p\" ] && echo \"$p\"; done"
|
||
out, _, err := client.Execute(ctx, script)
|
||
if err != nil {
|
||
return defaultSystemLogPaths
|
||
}
|
||
|
||
var paths []string
|
||
for _, line := range strings.Split(out, "\n") {
|
||
line = strings.TrimSpace(line)
|
||
if strings.HasPrefix(line, "/var/log/") {
|
||
paths = append(paths, line)
|
||
}
|
||
}
|
||
if len(paths) == 0 {
|
||
return nil // 交给上层走 journald 兜底
|
||
}
|
||
return paths
|
||
}
|