package services import ( "fmt" "strings" "gopkg.in/yaml.v3" lokimodels "lokistack/models" ) // 「采集 job 片段的 YAML 编辑」支持(方案 A)。 // // 设计边界(很重要): // 这里开放的是**采集项本身**的 YAML 表示,字段与 models.ScrapeJob 一一对应, // **不是**原始 promtail 配置。clients / positions / server / scrape_configs 外层 // 仍由平台生成。 // // 为什么不做"编辑完整 promtail.yaml": // 用户一旦删掉 positions,重启后会从头重采(大量重复 + entry too far behind 被 // Loki 拒绝);删掉 clients 则完全不外发。这两类故障在页面上看不出来, // 只能登机器看日志,排障成本极高。所以只开放"采集规则"这一层, // 它写坏了最多是某条日志没采到,不会让采集器整体失效。 // jobsYAML 是 ScrapeJob 的 YAML 投影(小写键,便于手写)。 type jobsYAML struct { Name string `yaml:"name"` Paths []string `yaml:"paths"` Labels map[string]string `yaml:"labels,omitempty"` Encoding string `yaml:"encoding,omitempty"` System bool `yaml:"system,omitempty"` JSON bool `yaml:"json,omitempty"` Multiline *multilineYAML `yaml:"multiline,omitempty"` Timestamp *timestampYAML `yaml:"timestamp,omitempty"` DropIfMatch []string `yaml:"dropIfMatch,omitempty"` RateLimit *rateLimitYAML `yaml:"rateLimit,omitempty"` } type multilineYAML struct { FirstLine string `yaml:"firstline"` MaxLines int `yaml:"maxLines,omitempty"` } type timestampYAML struct { Source string `yaml:"source"` Format string `yaml:"format"` Location string `yaml:"location,omitempty"` } type rateLimitYAML struct { Rate int `yaml:"rate,omitempty"` Burst int `yaml:"burst,omitempty"` } // RenderJobsYAML 把采集项渲染成可编辑 YAML(带头部注释,说明这是什么)。 func RenderJobsYAML(jobs []lokimodels.ScrapeJob) (string, error) { out := make([]jobsYAML, 0, len(jobs)) for _, job := range jobs { item := jobsYAML{ Name: job.Name, Paths: job.Paths, Labels: job.Labels, Encoding: job.Encoding, System: job.System, JSON: job.JSON, DropIfMatch: job.DropIfMatch, } if job.Multiline != nil && strings.TrimSpace(job.Multiline.FirstLine) != "" { item.Multiline = &multilineYAML{FirstLine: job.Multiline.FirstLine, MaxLines: job.Multiline.MaxLines} } if job.Timestamp != nil && strings.TrimSpace(job.Timestamp.Source) != "" { item.Timestamp = ×tampYAML{ Source: job.Timestamp.Source, Format: job.Timestamp.Format, Location: job.Timestamp.Location, } } if job.RateLimit != nil && (job.RateLimit.Rate > 0 || job.RateLimit.Burst > 0) { item.RateLimit = &rateLimitYAML{Rate: job.RateLimit.Rate, Burst: job.RateLimit.Burst} } out = append(out, item) } body, err := yaml.Marshal(out) if err != nil { return "", fmt.Errorf("渲染采集项 YAML 失败: %w", err) } var b strings.Builder b.WriteString("# 采集项(scrape_configs)编辑区 —— 只描述「采哪些文件、怎么解析、怎么过滤」\n") b.WriteString("# clients / positions / server 等由平台自动生成,不在此处修改\n") b.WriteString("# 字段:name(必填) paths(必填) labels encoding json system multiline timestamp dropIfMatch rateLimit\n") b.WriteString(string(body)) return b.String(), nil } // ParseJobsYAML 把用户编辑的 YAML 解析回采集项,并做与表单完全相同的校验。 // // 报错要具体:YAML 语法错给行号,语义错给"第几个采集项/哪个字段", // 不要笼统地说"格式错误"——否则用户只能靠猜。 func ParseJobsYAML(text string) ([]lokimodels.ScrapeJob, error) { trimmed := strings.TrimSpace(text) if trimmed == "" { return nil, fmt.Errorf("YAML 为空:至少需要一个采集项") } var raw []jobsYAML if err := yaml.Unmarshal([]byte(trimmed), &raw); err != nil { return nil, fmt.Errorf("YAML 语法错误:%s", prettifyYAMLError(err)) } if len(raw) == 0 { return nil, fmt.Errorf("YAML 里没有采集项(应为一个以 - 开头的列表)") } jobs := make([]lokimodels.ScrapeJob, 0, len(raw)) for _, item := range raw { job := lokimodels.ScrapeJob{ Name: strings.TrimSpace(item.Name), Paths: trimAll(item.Paths), Labels: item.Labels, Encoding: strings.TrimSpace(item.Encoding), System: item.System, JSON: item.JSON, DropIfMatch: trimAll(item.DropIfMatch), } if item.Multiline != nil { job.Multiline = &lokimodels.JobMultiline{ FirstLine: item.Multiline.FirstLine, MaxLines: item.Multiline.MaxLines, } } if item.Timestamp != nil { job.Timestamp = &lokimodels.JobTimestamp{ Source: item.Timestamp.Source, Format: item.Timestamp.Format, Location: item.Timestamp.Location, } } if item.RateLimit != nil { job.RateLimit = &lokimodels.JobRateLimit{Rate: item.RateLimit.Rate, Burst: item.RateLimit.Burst} } jobs = append(jobs, job) } if err := lokimodels.ValidateScrapeJobs(jobs); err != nil { return nil, err } return jobs, nil } func trimAll(values []string) []string { out := make([]string, 0, len(values)) for _, v := range values { if s := strings.TrimSpace(v); s != "" { out = append(out, s) } } return out } // prettifyYAMLError 把 yaml.v3 的多行错误压成一行,保留行号信息。 func prettifyYAMLError(err error) string { var lines []string for _, line := range strings.Split(err.Error(), "\n") { if s := strings.TrimSpace(line); s != "" { lines = append(lines, s) } } return strings.Join(lines, "; ") }