package services import ( "fmt" "strings" "lokistack/models" ) // seedPositionsCmd 生成「把 positions 预置到文件末尾」的远端命令。 // // 为什么必须做: // // Promtail 在 positions 里没有某个文件记录时,会从**文件开头**开始读。 // 主机上的 /var/log/messages 往往有几周历史,而 Loki 只接受最近 // `reject_old_samples_max_age`(默认 1h)的条目 —— 一旦 promtail 把新旧日志 // 混在一个 batch 里推送,整批会被 400 拒绝(entry too far behind), // **连新日志也发不出去**,容器日志里只会看到无尽的 // `msg="final error sending batch" status=400`。 // // 实测:容器模式首次安装后 10 分钟只进 47 条,其余全是这个错误。 // // 只补 positions 里**还没有**的路径,因此重装/升级不会破坏已有采集进度, // 也不会重复采集。 func seedPositionsCmd(patterns []string) string { // 去重,保持顺序 seen := map[string]bool{} var uniq []string for _, p := range patterns { p = strings.TrimSpace(p) if p == "" || seen[p] { continue } seen[p] = true uniq = append(uniq, p) } if len(uniq) == 0 { return "" } quoted := make([]string, 0, len(uniq)) for _, p := range uniq { quoted = append(quoted, "'"+strings.ReplaceAll(p, "'", `'\''`)+"'") } return fmt.Sprintf(`mkdir -p /var/lib/promtail POS=/var/lib/promtail/positions.yaml if [ ! -s "$POS" ]; then printf 'positions:\n' > "$POS" fi for f in %s; do [ -f "$f" ] || continue if grep -qF " $f:" "$POS" 2>/dev/null; then continue; fi printf ' %%s: "%%s"\n' "$f" "$(stat -c %%s "$f")" >> "$POS" done echo "positions 已预置: $(grep -c ':' "$POS" 2>/dev/null || echo 0) 条记录"`, strings.Join(quoted, " ")) } // collectorPositionPatterns 收集该采集器涉及的所有路径(含通配符),用于预置 positions。 func collectorPositionPatterns(c *models.Collector, systemPaths []string) []string { var out []string out = append(out, systemPaths...) if p := strings.TrimSpace(c.LogPath); p != "" { out = append(out, splitPaths(p)...) } for _, job := range c.ScrapeJobs { paths := job.Paths // 内置系统日志项不写死路径时,用探测结果 if job.System && len(paths) == 0 { paths = systemPaths } out = append(out, paths...) } return out }