master
go 360 lines 11.8 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 package prometheus
4
5 import (
6 "slices"
7 "strconv"
8 "strings"
9
10 "github.com/netdata/netdata/go/plugins/logger"
11 "github.com/netdata/netdata/go/plugins/pkg/matcher"
12 "github.com/netdata/netdata/go/plugins/pkg/metrix"
13 prompkg "github.com/netdata/netdata/go/plugins/pkg/prometheus"
14 commonmodel "github.com/prometheus/common/model"
15 )
16
17 // seriesCacheRetentionCycles bounds the per-series instrument cache: a cached handle not observed for
18 // this many successful cycles is evicted. This value mirrors two other retention windows that are NOT
19 // compiler-linked and MUST be kept in agreement: metrix's default store retention (so a cached handle
20 // lives as long as the series it writes) and the chart template's expiry (chartExpireAfterCycles, so a
21 // chart is not removed before the series feeding it). The cache also stays bounded under label churn.
22 const seriesCacheRetentionCycles = 10
23
24 type metricFamilyWriterPolicy struct {
25 labelPrefix string
26 maxTSPerMetric int
27 isFallbackTypeGauge matcher.Matcher
28 isFallbackTypeCounter matcher.Matcher
29 }
30
31 type metricFamilyWriter struct {
32 store metrix.CollectorStore
33 policy metricFamilyWriterPolicy
34 handles map[string]*metricFamilyHandle
35 cycle uint64
36 *logger.Logger
37 }
38
39 // cachedInstrument is a per-series instrument handle plus the last cycle it was observed, so handles
40 // for series that stop appearing in scrapes can be evicted.
41 type cachedInstrument[T any] struct {
42 inst T
43 lastSeen uint64
44 }
45
46 // metricFamilyHandle caches, per metric name, the canonical distribution schema, instrument options,
47 // and the per-series instrument handles. The family handle is created once per name and KEPT for the
48 // job's lifetime on purpose: metrix registers an instrument descriptor per name permanently (no
49 // unregister API), so if a name reappeared with a changed contract (kind, summary quantiles, or
50 // histogram bounds) and the writer re-registered it, metrix would panic on the mismatch. Keeping the
51 // handle lets ensureHandle detect that drift and skip it instead of re-registering.
52 //
53 // The per-series instrument handles inside ARE evicted: they are reused across cycles (skipping
54 // per-series instrument re-resolution) and dropped once a series goes unobserved for
55 // seriesCacheRetentionCycles, so the cache stays bounded under label-value churn — unlike a metrix
56 // vec, whose internal handle cache is unbounded. A family may mix label-key sets; each series is
57 // cached and written by its own full label tuple. Per-name state (this handle plus the metrix
58 // descriptor) is bounded by metric-name cardinality; metric-NAME churn (a Prometheus anti-pattern)
59 // grows it — an accepted metrix-store limit.
60 type metricFamilyHandle struct {
61 name string
62 typ commonmodel.MetricType
63 summaryQuantiles []float64
64 histogramBounds []float64
65 opts []metrix.InstrumentOption
66
67 gauges map[string]*cachedInstrument[metrix.SnapshotGauge]
68 counters map[string]*cachedInstrument[metrix.SnapshotCounter]
69 summaries map[string]*cachedInstrument[metrix.SnapshotSummary]
70 histograms map[string]*cachedInstrument[metrix.SnapshotHistogram]
71 }
72
73 func newMetricFamilyWriter(store metrix.CollectorStore, policy metricFamilyWriterPolicy, log *logger.Logger) *metricFamilyWriter {
74 // A family with no configured fallback matcher uses a never-matching matcher, so
75 // resolveFamilyType can call MatchString unconditionally (no per-cycle nil check).
76 if policy.isFallbackTypeGauge == nil {
77 policy.isFallbackTypeGauge = matcher.FALSE()
78 }
79 if policy.isFallbackTypeCounter == nil {
80 policy.isFallbackTypeCounter = matcher.FALSE()
81 }
82 return &metricFamilyWriter{
83 store: store,
84 policy: policy,
85 handles: make(map[string]*metricFamilyHandle),
86 Logger: log,
87 }
88 }
89
90 // countWritable reports how many series across all families could be written. Used at Check to
91 // confirm the endpoint exposes usable metrics, before any cycle has run.
92 func (w *metricFamilyWriter) countWritable(mfs prompkg.MetricFamilies) int {
93 count := 0
94 for _, mf := range mfs {
95 if w.skipMetricFamily(mf) {
96 continue
97 }
98
99 typ, ok := w.resolveFamilyType(mf)
100 if !ok {
101 continue
102 }
103
104 schema, ok := deriveMetricFamilySchema(mf, typ)
105 if !ok {
106 continue
107 }
108
109 for _, metric := range mf.Metrics() {
110 if metricIsWritable(metric, typ, schema) {
111 count++
112 }
113 }
114 }
115 return count
116 }
117
118 func (w *metricFamilyWriter) writeMetricFamilies(mfs prompkg.MetricFamilies) int {
119 w.cycle++
120
121 written := 0
122 for _, mf := range mfs {
123 if w.skipMetricFamily(mf) {
124 continue
125 }
126
127 typ, ok := w.resolveFamilyType(mf)
128 if !ok {
129 continue
130 }
131
132 handle, ok := w.ensureHandle(mf, typ)
133 if !ok {
134 continue
135 }
136
137 for _, metric := range mf.Metrics() {
138 if w.observeMetric(handle, metric) {
139 written++
140 }
141 }
142 }
143
144 w.evictStaleSeries()
145 return written
146 }
147
148 func (w *metricFamilyWriter) skipMetricFamily(mf *prompkg.MetricFamily) bool {
149 if strings.HasSuffix(mf.Name(), "_info") {
150 return true
151 }
152 if w.policy.maxTSPerMetric > 0 && len(mf.Metrics()) > w.policy.maxTSPerMetric {
153 w.Debugf("metric '%s' num of time series (%d) > limit (%d), skipping it",
154 mf.Name(), len(mf.Metrics()), w.policy.maxTSPerMetric)
155 return true
156 }
157 return false
158 }
159
160 func (w *metricFamilyWriter) resolveFamilyType(mf *prompkg.MetricFamily) (commonmodel.MetricType, bool) {
161 switch mf.Type() {
162 case commonmodel.MetricTypeGauge,
163 commonmodel.MetricTypeCounter,
164 commonmodel.MetricTypeSummary,
165 commonmodel.MetricTypeHistogram:
166 return mf.Type(), true
167 case commonmodel.MetricTypeUnknown:
168 if w.policy.isFallbackTypeGauge.MatchString(mf.Name()) {
169 return commonmodel.MetricTypeGauge, true
170 }
171 if w.policy.isFallbackTypeCounter.MatchString(mf.Name()) || strings.HasSuffix(mf.Name(), "_total") {
172 return commonmodel.MetricTypeCounter, true
173 }
174 return "", false
175 default:
176 return "", false
177 }
178 }
179
180 func (w *metricFamilyWriter) ensureHandle(mf *prompkg.MetricFamily, typ commonmodel.MetricType) (*metricFamilyHandle, bool) {
181 if handle, ok := w.handles[mf.Name()]; ok {
182 if handle.typ != typ {
183 w.Debugf("skip metric family '%s': metric type drift (%s -> %s)", mf.Name(), handle.typ, typ)
184 return nil, false
185 }
186 return handle, true
187 }
188
189 schema, ok := deriveMetricFamilySchema(mf, typ)
190 if !ok {
191 return nil, false
192 }
193
194 opts := []metrix.InstrumentOption{
195 metrix.WithChartFamily(getChartFamily(mf.Name())),
196 metrix.WithChartPriority(getChartPriority(mf.Name())),
197 metrix.WithUnit(instrumentUnit(mf.Name(), typ)),
198 metrix.WithFloat(true),
199 metrix.WithDescription(getChartTitle(mf.Name(), mf.Help())),
200 }
201
202 handle := &metricFamilyHandle{
203 name: mf.Name(),
204 typ: typ,
205 summaryQuantiles: slices.Clone(schema.summaryQuantiles),
206 histogramBounds: slices.Clone(schema.histogramBounds),
207 opts: opts,
208 }
209
210 switch typ {
211 case commonmodel.MetricTypeGauge:
212 handle.gauges = make(map[string]*cachedInstrument[metrix.SnapshotGauge])
213 case commonmodel.MetricTypeCounter:
214 handle.counters = make(map[string]*cachedInstrument[metrix.SnapshotCounter])
215 case commonmodel.MetricTypeSummary:
216 handle.opts = append(handle.opts, metrix.WithSummaryQuantiles(schema.summaryQuantiles...))
217 handle.summaries = make(map[string]*cachedInstrument[metrix.SnapshotSummary])
218 case commonmodel.MetricTypeHistogram:
219 handle.opts = append(handle.opts, metrix.WithHistogramBounds(schema.histogramBounds...))
220 handle.histograms = make(map[string]*cachedInstrument[metrix.SnapshotHistogram])
221 }
222
223 w.handles[mf.Name()] = handle
224 return handle, true
225 }
226
227 func (w *metricFamilyWriter) observeMetric(handle *metricFamilyHandle, metric prompkg.Metric) bool {
228 schema, ok := deriveMetricSchema(metric, handle.typ)
229 if !ok {
230 return false
231 }
232 if !slices.Equal(handle.summaryQuantiles, schema.summaryQuantiles) || !slices.Equal(handle.histogramBounds, schema.histogramBounds) {
233 w.Debugf("skip a series of metric '%s': distribution schema drift", handle.name)
234 return false
235 }
236
237 sig := w.seriesSig(metric)
238
239 switch handle.typ {
240 case commonmodel.MetricTypeGauge:
241 value, ok := metricScalarValue(metric, commonmodel.MetricTypeGauge)
242 if !ok {
243 return false
244 }
245 inst := getOrCreateInstrument(handle.gauges, sig, w.cycle, func() metrix.SnapshotGauge {
246 return w.store.Write().SnapshotMeter("").WithLabels(w.seriesLabels(metric)...).Gauge(handle.name, handle.opts...)
247 })
248 inst.Observe(value)
249 return true
250 case commonmodel.MetricTypeCounter:
251 value, ok := metricScalarValue(metric, commonmodel.MetricTypeCounter)
252 if !ok {
253 return false
254 }
255 inst := getOrCreateInstrument(handle.counters, sig, w.cycle, func() metrix.SnapshotCounter {
256 return w.store.Write().SnapshotMeter("").WithLabels(w.seriesLabels(metric)...).Counter(handle.name, handle.opts...)
257 })
258 inst.ObserveTotal(value)
259 return true
260 case commonmodel.MetricTypeSummary:
261 point, ok := toSummaryPoint(metric.Summary())
262 if !ok {
263 return false
264 }
265 inst := getOrCreateInstrument(handle.summaries, sig, w.cycle, func() metrix.SnapshotSummary {
266 return w.store.Write().SnapshotMeter("").WithLabels(w.seriesLabels(metric)...).Summary(handle.name, handle.opts...)
267 })
268 inst.ObservePoint(point)
269 return true
270 case commonmodel.MetricTypeHistogram:
271 point, ok := toHistogramPoint(metric.Histogram())
272 if !ok {
273 return false
274 }
275 inst := getOrCreateInstrument(handle.histograms, sig, w.cycle, func() metrix.SnapshotHistogram {
276 return w.store.Write().SnapshotMeter("").WithLabels(w.seriesLabels(metric)...).Histogram(handle.name, handle.opts...)
277 })
278 inst.ObservePoint(point)
279 return true
280 default:
281 return false
282 }
283 }
284
285 // getOrCreateInstrument returns the cached instrument handle for a series signature, creating and
286 // caching it on first use, and stamps it as observed in the current cycle.
287 func getOrCreateInstrument[T any](m map[string]*cachedInstrument[T], sig string, cycle uint64, create func() T) T {
288 e, ok := m[sig]
289 if !ok {
290 e = &cachedInstrument[T]{inst: create()}
291 m[sig] = e
292 }
293 e.lastSeen = cycle
294 return e.inst
295 }
296
297 // evictStaleSeries drops cached instrument handles for series not observed within the retention
298 // window, keeping the cache bounded under label-value churn.
299 func (w *metricFamilyWriter) evictStaleSeries() {
300 for _, h := range w.handles {
301 switch h.typ {
302 case commonmodel.MetricTypeGauge:
303 evictStaleInstruments(h.gauges, w.cycle)
304 case commonmodel.MetricTypeCounter:
305 evictStaleInstruments(h.counters, w.cycle)
306 case commonmodel.MetricTypeSummary:
307 evictStaleInstruments(h.summaries, w.cycle)
308 case commonmodel.MetricTypeHistogram:
309 evictStaleInstruments(h.histograms, w.cycle)
310 }
311 }
312 }
313
314 func evictStaleInstruments[T any](m map[string]*cachedInstrument[T], cycle uint64) {
315 for sig, e := range m {
316 if e.lastSeen+seriesCacheRetentionCycles <= cycle {
317 delete(m, sig)
318 }
319 }
320 }
321
322 // seriesSig builds a collision-safe key identifying a scraped series by its (prefixed) label tuple.
323 // Prometheus labels are sorted by name, so the key is stable for a given series.
324 func (w *metricFamilyWriter) seriesSig(metric prompkg.Metric) string {
325 lbs := metric.Labels()
326 if len(lbs) == 0 {
327 return ""
328 }
329 var b strings.Builder
330 for _, l := range lbs {
331 key := l.Name
332 if w.policy.labelPrefix != "" {
333 key = w.policy.labelPrefix + "_" + l.Name
334 }
335 b.WriteString(strconv.Itoa(len(key)))
336 b.WriteByte(':')
337 b.WriteString(key)
338 b.WriteByte('=')
339 b.WriteString(strconv.Itoa(len(l.Value)))
340 b.WriteByte(':')
341 b.WriteString(l.Value)
342 b.WriteByte('\xff')
343 }
344 return b.String()
345 }
346
347 // seriesLabels converts a scraped series' labels into metrix labels, applying the configured
348 // label_prefix to each label key (V1 prepended "<prefix>_" to label keys).
349 func (w *metricFamilyWriter) seriesLabels(metric prompkg.Metric) []metrix.Label {
350 lbs := metric.Labels()
351 out := make([]metrix.Label, 0, len(lbs))
352 for _, l := range lbs {
353 key := l.Name
354 if w.policy.labelPrefix != "" {
355 key = w.policy.labelPrefix + "_" + l.Name
356 }
357 out = append(out, metrix.Label{Key: key, Value: l.Value})
358 }
359 return out
360 }