feat(collectors): 采集项支持「表单 / YAML」双模式编辑(方案 A)
CI / Backend (go vet + test) (push) Canceled after 0s
CI / Frontend (lint + build + test) (push) Canceled after 0s

=== 设计边界(刻意不做"编辑完整 promtail.yaml")===
开放的是**采集项本身**的 YAML 表示(字段与 models.ScrapeJob 一一对应):
  name / paths / labels / encoding / json / system
  multiline(firstline,maxLines) / timestamp / dropIfMatch / rateLimit
clients、positions、server、scrape_configs 外层仍由平台生成。

理由(真实故障面):
  用户删掉 positions → 重启后从头重采,大量重复 + Loki 以
  entry too far behind 拒绝整批;删掉 clients → 完全不外发。
  这两类问题在页面上看不出来,只能登机器看日志,排障成本极高。
  只开放采集规则,写坏了最多某条日志没采到,不会让采集器整体失效。

=== 后端 ===
新增 services/collector_jobs_yaml.go:
  RenderJobsYAML(jobs) → 带边界说明注释的 YAML(明确写"clients/positions 不在此修改")
  ParseJobsYAML(text)  → 复用 models.ValidateScrapeJobs,与表单完全同一套校验
错误信息必须具体:
  YAML 语法错 → 「YAML 语法错误:yaml: line 1: did not find expected ',' or ']'」
  语义错     → 「采集项「app」至少需要填写一个日志路径」
新增路由(用 /collector-jobs/* 前缀,避开与 /collectors/:id 的 gin 通配冲突):
  POST /api/v1/collector-jobs/render
  POST /api/v1/collector-jobs/parse

=== 前端 ===
编辑弹框(主机模式)加「表单 / YAML」切换:
  - 切到 YAML:调 render 把当前采集项渲染出来
  - 切回表单:调 parse 回填;解析失败**留在 YAML 模式**并显示原因
    (绝不能悄悄丢掉用户改动)
  - 停留在 YAML 模式点保存:先 parse,失败则中止保存
  - 编辑区 min-h 固定、等宽字体、可拖拽高度

=== 测试 ===
TestRenderParseJobsYAMLRoundTrip:
  表单→YAML→表单 无损往返,逐字段核对
  (multiline.maxLines / rateLimit / dropIfMatch / timestamp / labels / system)
TestParseJobsYAMLErrors:空内容、语法错、空列表、缺名称、缺路径、名称非法
  → 6 种输入都给出具体错误,不是笼统"格式错误"

=== 实测(c11d2badce64)===
  render: code=0,17 行,含边界说明
  parse : code=0,1 个采集项;往返校验 name=app paths=1 multiline=True
          rateLimit={rate:2000,burst:4000} drop=1 json=True
  错误: 「采集项「app」至少需要填写一个日志路径」
        「YAML 语法错误:yaml: line 1: did not find expected ',' or ']'」
  Pod = Hub latest = c11d2badce64
This commit is contained in:
opsx committed 2026-10-30 16:10:51 +00:00
1 parent c8d026adc7
commit 3b297d0429
8 files changed
+396 -2

No files matched your search

+32
View File
@@ -19,6 +19,38 @@ func NewCollectorController() *CollectorController {
return &CollectorController{service: services.NewCollectorService()}
}
// RenderJobsYAML 把结构化采集项渲染成可编辑 YAML(前端切到 YAML 模式时调用)。
func (ctrl *CollectorController) RenderJobsYAML(c *gin.Context) {
var req struct {
Jobs []models.ScrapeJob `json:"jobs"`
}
if !utils.BindAndValidate(c, &req) {
return
}
text, err := services.RenderJobsYAML(req.Jobs)
if err != nil {
utils.Error(c, 400, err.Error())
return
}
utils.Success(c, gin.H{"yaml": text})
}
// ParseJobsYAML 把用户编辑的 YAML 解析回结构化采集项(切回表单 / 保存前调用)。
func (ctrl *CollectorController) ParseJobsYAML(c *gin.Context) {
var req struct {
YAML string `json:"yaml"`
}
if !utils.BindAndValidate(c, &req) {
return
}
jobs, err := services.ParseJobsYAML(req.YAML)
if err != nil {
utils.Error(c, 400, err.Error())
return
}
utils.Success(c, gin.H{"jobs": jobs})
}
func (ctrl *CollectorController) Create(c *gin.Context) {
var req dto.CreateCollectorRequest
if !utils.BindAndValidate(c, &req) {