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:
opsx committed 2026-10-28 18:41:39 +00:00
1 parent 55d6a74eb3
commit c983d0e3c1
18 files changed
+1678 -254

No files matched your search

+2 -93
View File
@@ -17,7 +17,7 @@ import (
// - DaemonSet 模板(6 空格)→ 子项 8 空格
// 早先写死 8 空格,主机模式一旦配了标签就会生成非法 YAML,Promtail 直接装不上。
func promtailExternalLabels(cluster string, labelsJSON string, indent string) string {
lines := []string{fmt.Sprintf("%scluster: %q", indent, cluster)}
lines := []string{fmt.Sprintf("%scluster: %s", indent, yamlScalar(cluster))}
if labelsJSON != "" {
var m map[string]string
if err := json.Unmarshal([]byte(labelsJSON), &m); err == nil {
@@ -27,7 +27,7 @@ func promtailExternalLabels(cluster string, labelsJSON string, indent string) st
}
sort.Strings(keys)
for _, k := range keys {
lines = append(lines, fmt.Sprintf("%s%s: %q", indent, k, m[k]))
lines = append(lines, fmt.Sprintf("%s%s: %s", indent, k, yamlScalar(m[k])))
}
}
}
@@ -43,97 +43,6 @@ func isCRILogPath(logPath string) bool {
strings.Contains(p, "/docker/")
}
// appPipelineStages 按日志来源生成 pipeline:
// - 容器日志(CRI 格式)→ cri 解析 + 提取时间戳
// - 其它文本日志(应用文件、系统日志)→ 不套 cri,只做限流,避免解析失败丢行
//
// 原实现不区分来源、一律塞 cri 阶段,导致主机模式下采集 /var/log/messages
// 或普通应用文件时时间戳解析失败、日志被丢弃。
func appPipelineStages(logPath string) string {
if isCRILogPath(logPath) {
return ` - cri: {}
- match:
selector: '{job="app"}'
stages:
- timestamp:
source: time
format: RFC3339Nano
- limit:
rate: 1000
burst: 10000
drop: true`
}
return ` - limit:
rate: 2000
burst: 4000
drop: true`
}
func PromtailConfig(lokiEndpoint, logPath, cluster, labelsJSON string) string {
conf := fmt.Sprintf(`server:
http_listen_port: 9080
grpc_listen_port: 0
log_level: info
log_format: logfmt
positions:
filename: /var/lib/promtail/positions.yaml
sync_period: 10s
clients:
- url: %s/loki/api/v1/push
batchwait: 1s
batchsize: 1048576
timeout: 10s
backoff_config:
min_period: 500ms
max_period: 5m
max_retries: 10
external_labels:
job: host-logs
__EXT_LABELS__
limits_config:
readline_rate_enabled: false
readline_rate: 1000
readline_burst: 10000
max_streams: 0
scrape_configs:
- job_name: app
static_configs:
- targets: [localhost]
labels:
job: app
host: ${HOSTNAME}
__path__: %s
pipeline_stages:
%s
# 系统日志(/var/log/messages、secure、cron 等)是 syslog 文本格式,
# 绝不能用 cri 阶段解析 —— cri 只认 K8s/容器运行时那种 "时间 流 内容" 的行,
# 套在 syslog 上会取不到时间戳、甚至整行被丢。这里单独一个 job 走正则解析。
- job_name: system
static_configs:
- targets: [localhost]
labels:
job: system
host: ${HOSTNAME}
__path__: /var/log/{messages,secure,cron,maillog,syslog,dmesg}
pipeline_stages:
- regex:
expression: '^(?P<ts>[A-Z][a-z]{2}\s+\d{1,2}\s+\d{2}:\d{2}:\d{2})\s+(?P<hostname>\S+)\s+(?P<unit>[^:\[]+)(?:\[(?P<pid>\d+)\])?:\s*(?P<msg>.*)$'
- timestamp:
source: ts
format: Jan _2 15:04:05
location: Asia/Shanghai
- limit:
rate: 2000
burst: 4000
drop: true
`, lokiEndpoint, logPath, appPipelineStages(logPath))
return strings.Replace(conf, "__EXT_LABELS__", promtailExternalLabels(cluster, labelsJSON, " "), 1)
}
// PromtailDaemonsetYAML DaemonSet 模式纯 YAML 安装(SA/RBAC/ConfigMap/DaemonSet),
// 经 Kubernetes API 直接提交,后端无需任何额外二进制。
// relabel 体系与主流 Promtail 部署惯例对齐:namespace/pod/container/node_name + __path__ 按 pod uid 精确匹配。