=== 一、两个线上 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
98 lines
4.8 KiB
Go
98 lines
4.8 KiB
Go
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
|
||
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,
|
||
"status": c.Status,
|
||
"statusMessage": c.StatusMessage,
|
||
"createdAt": c.CreatedAt,
|
||
"updatedAt": c.UpdatedAt,
|
||
}
|
||
}
|