| 1 | // SPDX-License-Identifier: GPL-3.0-or-later |
| 2 | |
| 3 | package prometheus |
| 4 | |
| 5 | import ( |
| 6 | "context" |
| 7 | "fmt" |
| 8 | "strings" |
| 9 | |
| 10 | "github.com/netdata/netdata/go/plugins/pkg/prometheus" |
| 11 | "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/prometheus/relabel" |
| 12 | ) |
| 13 | |
| 14 | // collect scrapes the endpoint and writes the metric families to the metrix store. The |
| 15 | // store cycle (begin/commit) is driven by the framework around Collect, so this only |
| 16 | // writes observations and returns an error to abort the cycle. |
| 17 | func (c *Collector) collect(ctx context.Context) error { |
| 18 | mfs, err := c.scrape(ctx) |
| 19 | if err != nil { |
| 20 | return err |
| 21 | } |
| 22 | c.writer.writeMetricFamilies(mfs) |
| 23 | return nil |
| 24 | } |
| 25 | |
| 26 | // check probes the endpoint and enforces the startup gates the V1 collector applied once: |
| 27 | // the expected-prefix guard and the total time-series limit. Unlike V1 these are read-only |
| 28 | // (V1 mutated Config to make them one-shot); they run only at Check, i.e. autodetection. |
| 29 | func (c *Collector) check(ctx context.Context) error { |
| 30 | mfs, err := c.scrape(ctx) |
| 31 | if err != nil { |
| 32 | return err |
| 33 | } |
| 34 | if c.ExpectedPrefix != "" && !hasPrefix(mfs, c.ExpectedPrefix) { |
| 35 | return fmt.Errorf("'%s' metrics have no expected prefix (%s)", c.URL, c.ExpectedPrefix) |
| 36 | } |
| 37 | if c.MaxTS > 0 { |
| 38 | if n := calcMetrics(mfs); n > c.MaxTS { |
| 39 | return fmt.Errorf("'%s' num of time series (%d) > limit (%d)", c.URL, n, c.MaxTS) |
| 40 | } |
| 41 | } |
| 42 | if c.writer.countWritable(mfs) == 0 { |
| 43 | return fmt.Errorf("endpoint '%s' exposes no usable metrics", c.URL) |
| 44 | } |
| 45 | return nil |
| 46 | } |
| 47 | |
| 48 | // scrape fetches the endpoint and enforces the empty-scrape contract: an empty scrape is |
| 49 | // an error (endpoint down or exposing nothing), not silent no-data. |
| 50 | func (c *Collector) scrape(ctx context.Context) (prometheus.MetricFamilies, error) { |
| 51 | mfs, err := c.prom.ScrapeWithTransform(ctx, c.relabelTransform) |
| 52 | if err != nil { |
| 53 | return nil, err |
| 54 | } |
| 55 | if mfs.Len() == 0 { |
| 56 | return nil, fmt.Errorf("endpoint '%s' returned 0 metric families", c.URL) |
| 57 | } |
| 58 | return mfs, nil |
| 59 | } |
| 60 | |
| 61 | func hasPrefix(mfs prometheus.MetricFamilies, prefix string) bool { |
| 62 | for name := range mfs { |
| 63 | if strings.HasPrefix(name, prefix) { |
| 64 | return true |
| 65 | } |
| 66 | } |
| 67 | return false |
| 68 | } |
| 69 | |
| 70 | func calcMetrics(mfs prometheus.MetricFamilies) int { |
| 71 | var n int |
| 72 | for _, mf := range mfs { |
| 73 | n += len(mf.Metrics()) |
| 74 | } |
| 75 | return n |
| 76 | } |
| 77 | |
| 78 | // onRelabelDrop logs why a relabel rule dropped a sample, at debug level. It logs |
| 79 | // the metric name and the rule outcome, never label values (cardinality/PII). |
| 80 | func (c *Collector) onRelabelDrop(s prometheus.Sample, d relabel.DropInfo) { |
| 81 | c.When(d.RuleIndex >= 0). |
| 82 | Debugf("relabel dropped metric %q: %s (rule %d, action %q)", s.Name, d.Reason, d.RuleIndex, d.Action). |
| 83 | Else(). |
| 84 | Debugf("relabel dropped metric %q: %s", s.Name, d.Reason) |
| 85 | } |