diff --git a/docs/go-prometheus-metrics-guide.md b/docs/go-prometheus-metrics-guide.md index c41bd80..0716745 100644 --- a/docs/go-prometheus-metrics-guide.md +++ b/docs/go-prometheus-metrics-guide.md @@ -207,7 +207,7 @@ Guidelines: **Owner**: go-metrics-assistant **Applies when**: a `prometheus.CounterOpts` struct literal sets a `Name:` field whose string value does not end with `_total`. -**Enforcement**: judgment (ast-grep follow-up) +**Enforcement**: `rules/go/counter-total-suffix.yml` **Why**: Prometheus naming convention; newer `client_golang` versions enforce this at registration time (panic). Counters without `_total` also fail the OpenMetrics spec and confuse Grafana auto-completion. #### Bad diff --git a/rules/go/counter-total-suffix.yml b/rules/go/counter-total-suffix.yml new file mode 100644 index 0000000..cf4955b --- /dev/null +++ b/rules/go/counter-total-suffix.yml @@ -0,0 +1,34 @@ +id: go-prometheus/counter-total-suffix +language: go +severity: error +message: | + Counter metric Name must end with "_total". + Prometheus naming convention; newer client_golang versions panic at registration without it. + See docs/go-prometheus-metrics-guide.md (RULE go-prometheus/counter-total-suffix). +rule: + # ast-grep 0.43.0 shape (verified against /tmp test fixtures): + # - pattern.context binds a structural match anchored on prometheus.CounterOpts + # so the `selector: keyed_element` only fires inside Counter literals. + # - inside.pattern + stopBy: end re-anchors on CounterOpts to avoid false + # positives on GaugeOpts / HistogramOpts / SummaryOpts which share the + # `Name: "..."` field shape. + # - constraints.V.not.regex rejects values ending in `_total"` (the closing + # quote of an interpreted_string_literal). Name field position-agnostic — + # works whether Name comes first, middle, or last in the struct literal. + pattern: + context: 'prometheus.CounterOpts{Name: $V, $$$}' + selector: 'keyed_element' + inside: + pattern: 'prometheus.CounterOpts{$$$}' + stopBy: end +constraints: + V: + not: + regex: '_total"$' +ignores: + - "main.go" + - "**/main.go" + - "**/*_test.go" + - "vendor/**" + - "**/vendor/**" + - "**/mocks/**" diff --git a/rules/index.json b/rules/index.json index 5b67976..5e2d292 100644 --- a/rules/index.json +++ b/rules/index.json @@ -210,7 +210,7 @@ "anchor": "go-prometheus/counter-total-suffix", "applies_when": "a `prometheus.CounterOpts` struct literal sets a `Name:` field whose string value does not end with `_total`.", "doc_path": "docs/go-prometheus-metrics-guide.md", - "enforcement": "judgment (ast-grep follow-up)", + "enforcement": "`rules/go/counter-total-suffix.yml`", "id": "go-prometheus/counter-total-suffix", "level": "MUST", "owner": "go-metrics-assistant"