Skip to content

Commit c2c5d4b

Browse files
committed
Use prometheus metrics implementations
1 parent cb78268 commit c2c5d4b

7 files changed

Lines changed: 256 additions & 214 deletions

File tree

config/metrics_mapping.go

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,21 @@ import (
66

77
type MetricsMapping struct {
88
// Counter, Gauge
9-
Name string `yaml:"name"`
10-
Description string `yaml:"description"`
11-
Namespace string `yaml:"namespace"`
12-
Subsystem string `yaml:"subsystem"`
13-
Labels []string `yaml:"labels"`
9+
Name string `yaml:"name"`
10+
Description string `yaml:"description"`
11+
Namespace string `yaml:"namespace"`
12+
Subsystem string `yaml:"subsystem"`
13+
Labels []string `yaml:"labels"`
14+
ConstLabels map[string]string `yaml:"constLabels"`
1415
// Histogram
1516
Buckets []float64 `yaml:"buckets"`
1617
NativeHistogramBucketFactor float64 `yaml:"nativeHistogramBucketFactor"`
1718
NativeHistogramZeroThreshold float64 `yaml:"nativeHistogramZeroThreshold"`
1819
NativeHistogramMaxBucketNumber uint32 `yaml:"nativeHistogramMaxBucketNumber"`
1920
NativeHistogramMinResetDuration time.Duration `yaml:"nativeHistogramMinResetDuration"`
2021
NativeHistogramMaxZeroThreshold float64 `yaml:"nativeHistogramMaxZeroThreshold"`
22+
NativeHistogramMaxExemplars int `yaml:"nativeHistogramMaxExemplars"`
23+
NativeHistogramExemplarTTL time.Duration `yaml:"nativeHistogramExemplarTTL"`
2124
// Summary
2225
Objectives map[float64]float64 `yaml:"objectives"`
2326
MaxAge time.Duration `yaml:"maxAge"`

input/transform/impl/bletomqtt/ble-to-mqtt.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,10 @@ func (btm *bleToMqtt) Transform(_ context.Context, data *input.Data) (*input.Dat
3636
return nil, fmt.Errorf("ble-to-mqtt transform: failed to json decode input event data: %w", err)
3737
}
3838

39-
logrus.WithField("payload", inputEventData).Debug("input transformed")
39+
logrus.
40+
WithField("payload", inputEventData).
41+
Debug("input transformed")
42+
4043
if serviceData, exists := inputEventData.ServiceData[btm.sdk]; exists {
4144
payload, err := hex.DecodeString(serviceData)
4245
if err != nil {

metrics/collector.go

Lines changed: 181 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,85 +1,216 @@
11
package metrics
22

33
import (
4+
"context"
45
"fmt"
56
"strings"
7+
"time"
68

79
"github.com/prometheus/client_golang/prometheus"
10+
"github.com/sirupsen/logrus"
811

912
"github.com/nikiforov-soft/yasp/config"
1013
"github.com/nikiforov-soft/yasp/internal/syncx"
1114
)
1215

1316
type collector struct {
14-
metricsMapping *config.MetricsMapping
15-
metrics *syncx.Map[string, *metric]
17+
closeCtx context.Context
18+
closeCtxCancel context.CancelFunc
19+
desc *prometheus.Desc
20+
metricVec any
21+
metrics *syncx.Map[string, metricHistory]
22+
metricsMapping *config.MetricsMapping
23+
stalenessInterval time.Duration
24+
}
25+
26+
func newCollector(metricsMapping *config.MetricsMapping, stalenessInterval time.Duration) (*collector, error) {
27+
metricVec, err := newMetricVec(metricsMapping)
28+
if err != nil {
29+
return nil, err
30+
}
31+
32+
closeCtx, closeCtxCancel := context.WithCancel(context.Background())
33+
c := &collector{
34+
closeCtx: closeCtx,
35+
closeCtxCancel: closeCtxCancel,
36+
desc: newDesc(metricsMapping),
37+
metricVec: metricVec,
38+
metrics: &syncx.Map[string, metricHistory]{},
39+
metricsMapping: metricsMapping,
40+
stalenessInterval: stalenessInterval,
41+
}
42+
43+
if err := prometheus.Register(c); err != nil {
44+
return nil, fmt.Errorf("failed to register metrics collector: %w", err)
45+
}
46+
47+
if stalenessInterval > 0 {
48+
go c.runMetricsCleanupTask()
49+
}
50+
51+
return c, nil
1652
}
1753

1854
func (c *collector) Describe(ch chan<- *prometheus.Desc) {
19-
ch <- c.buildDesc()
55+
ch <- c.desc
2056
}
2157

2258
func (c *collector) Collect(ch chan<- prometheus.Metric) {
23-
c.metrics.Range(func(_ string, m *metric) bool {
24-
metricValue, err := c.collectMetrics(m)
59+
c.metrics.Range(func(_ string, m metricHistory) bool {
60+
metricValue, err := c.getMetric(m.labels)
2561
if err != nil {
26-
ch <- prometheus.NewInvalidMetric(c.buildDesc(), err)
62+
ch <- prometheus.NewInvalidMetric(c.desc, err)
2763
} else {
28-
ch <- prometheus.NewMetricWithTimestamp(m.timestamp, metricValue)
64+
ch <- prometheus.NewMetricWithTimestamp(m.lastUpdatedAt, metricValue)
2965
}
3066
return true
3167
})
3268
}
3369

34-
func (c *collector) buildDesc() *prometheus.Desc {
35-
return prometheus.NewDesc(
36-
prometheus.BuildFQName(c.metricsMapping.Namespace, c.metricsMapping.Subsystem, c.metricsMapping.Name),
37-
c.metricsMapping.Description,
38-
c.metricsMapping.Labels,
39-
nil,
40-
)
70+
func (c *collector) Close() {
71+
c.closeCtxCancel()
72+
prometheus.Unregister(c)
73+
}
74+
75+
func (c *collector) Observe(value float64, labels prometheus.Labels) error {
76+
switch m := c.metricVec.(type) {
77+
case *prometheus.CounterVec:
78+
m.With(labels).Add(value)
79+
case *prometheus.GaugeVec:
80+
m.With(labels).Set(value)
81+
case *prometheus.HistogramVec:
82+
m.With(labels).Observe(value)
83+
case *prometheus.SummaryVec:
84+
m.With(labels).Observe(value)
85+
default:
86+
return fmt.Errorf("unknown metricVec type: %T", c.metricVec)
87+
}
88+
89+
c.metrics.Store(computeHash(c.metricsMapping, labels), metricHistory{
90+
labels: labels,
91+
lastUpdatedAt: time.Now(),
92+
})
93+
94+
return nil
95+
}
96+
97+
func (c *collector) getMetric(labels prometheus.Labels) (prometheus.Metric, error) {
98+
switch m := c.metricVec.(type) {
99+
case *prometheus.CounterVec:
100+
return m.GetMetricWith(labels)
101+
case *prometheus.GaugeVec:
102+
return m.GetMetricWith(labels)
103+
case *prometheus.HistogramVec:
104+
return m.MetricVec.GetMetricWith(labels)
105+
case *prometheus.SummaryVec:
106+
return m.MetricVec.GetMetricWith(labels)
107+
default:
108+
return nil, fmt.Errorf("unsupported metricVec type: %T", m)
109+
}
110+
}
111+
112+
func (c *collector) runMetricsCleanupTask() {
113+
ticker := time.NewTicker(time.Second)
114+
for {
115+
select {
116+
case <-c.closeCtx.Done():
117+
return
118+
case <-ticker.C:
119+
c.pruneStaleMetrics()
120+
}
121+
}
41122
}
42123

43-
func (c *collector) collectMetrics(m *metric) (prometheus.Metric, error) {
44-
desc := c.buildDesc()
45-
labels := flattenLabels(c.metricsMapping.Labels, m.labels)
124+
func (c *collector) pruneStaleMetrics() {
125+
var keysToDelete []string
126+
c.metrics.Range(func(key string, value metricHistory) bool {
127+
if time.Since(value.lastUpdatedAt) > c.stalenessInterval {
128+
keysToDelete = append(keysToDelete, key)
129+
}
130+
return true
131+
})
132+
133+
for _, key := range keysToDelete {
134+
mh, ok := c.metrics.LoadAndDelete(key)
135+
if !ok {
136+
continue
137+
}
138+
139+
logrus.
140+
WithField("name", c.metricsMapping.Name).
141+
WithField("labels", mh.labels).
142+
WithField("updatedAt", mh.lastUpdatedAt).
143+
Debug("stale metric removed")
46144

47-
switch strings.ToLower(c.metricsMapping.Type) {
145+
switch m := c.metricVec.(type) {
146+
case *prometheus.CounterVec:
147+
m.Delete(mh.labels)
148+
case *prometheus.GaugeVec:
149+
m.Delete(mh.labels)
150+
case *prometheus.HistogramVec:
151+
m.Delete(mh.labels)
152+
case *prometheus.SummaryVec:
153+
m.Delete(mh.labels)
154+
}
155+
}
156+
}
157+
158+
func newMetricVec(mapping *config.MetricsMapping) (any, error) {
159+
switch strings.ToLower(mapping.Type) {
48160
case "counter":
49-
return prometheus.NewConstMetric(
50-
desc,
51-
prometheus.CounterValue,
52-
m.value,
53-
labels...,
54-
)
161+
return prometheus.NewCounterVec(prometheus.CounterOpts{
162+
Namespace: mapping.Namespace,
163+
Subsystem: mapping.Subsystem,
164+
Name: mapping.Name,
165+
ConstLabels: mapping.ConstLabels,
166+
Help: mapping.Description,
167+
}, mapping.Labels), nil
55168
case "gauge":
56-
return prometheus.NewConstMetric(
57-
desc,
58-
prometheus.GaugeValue,
59-
m.value,
60-
labels...,
61-
)
62-
case "histogram":
63-
buckets := make(map[float64]uint64)
64-
for bucket, count := range m.histogramBuckets {
65-
buckets[bucket] = uint64(count)
66-
}
67-
return prometheus.NewConstHistogram(
68-
desc,
69-
m.histogramCount,
70-
m.histogramSum,
71-
buckets,
72-
labels...,
73-
)
169+
return prometheus.NewGaugeVec(prometheus.GaugeOpts{
170+
Namespace: mapping.Namespace,
171+
Subsystem: mapping.Subsystem,
172+
Name: mapping.Name,
173+
ConstLabels: mapping.ConstLabels,
174+
Help: mapping.Description,
175+
}, mapping.Labels), nil
74176
case "summary":
75-
return prometheus.NewConstSummary(
76-
desc,
77-
m.summaryCount,
78-
m.summarySum,
79-
m.summaryQuantiles,
80-
labels...,
81-
)
177+
return prometheus.NewSummaryVec(prometheus.SummaryOpts{
178+
Namespace: mapping.Namespace,
179+
Subsystem: mapping.Subsystem,
180+
Name: mapping.Name,
181+
Help: mapping.Description,
182+
ConstLabels: mapping.ConstLabels,
183+
Objectives: mapping.Objectives,
184+
MaxAge: mapping.MaxAge,
185+
AgeBuckets: mapping.AgeBuckets,
186+
BufCap: mapping.BufCap,
187+
}, mapping.Labels), nil
188+
case "histogram":
189+
return prometheus.NewHistogramVec(prometheus.HistogramOpts{
190+
Namespace: mapping.Namespace,
191+
Subsystem: mapping.Subsystem,
192+
Name: mapping.Name,
193+
Help: mapping.Description,
194+
ConstLabels: mapping.ConstLabels,
195+
Buckets: mapping.Buckets,
196+
NativeHistogramBucketFactor: mapping.NativeHistogramBucketFactor,
197+
NativeHistogramZeroThreshold: mapping.NativeHistogramZeroThreshold,
198+
NativeHistogramMaxBucketNumber: mapping.NativeHistogramMaxBucketNumber,
199+
NativeHistogramMinResetDuration: mapping.NativeHistogramMinResetDuration,
200+
NativeHistogramMaxZeroThreshold: mapping.NativeHistogramMaxZeroThreshold,
201+
NativeHistogramMaxExemplars: mapping.NativeHistogramMaxExemplars,
202+
NativeHistogramExemplarTTL: mapping.NativeHistogramExemplarTTL,
203+
}, mapping.Labels), nil
82204
default:
83-
return nil, fmt.Errorf("unsupported metric type: %s", c.metricsMapping.Type)
205+
return nil, fmt.Errorf("unsupported type: %s", mapping.Type)
84206
}
85207
}
208+
209+
func newDesc(mapping *config.MetricsMapping) *prometheus.Desc {
210+
return prometheus.NewDesc(
211+
prometheus.BuildFQName(mapping.Namespace, mapping.Subsystem, mapping.Name),
212+
mapping.Description,
213+
mapping.Labels,
214+
mapping.ConstLabels,
215+
)
216+
}

metrics/helper.go

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ package metrics
33
import (
44
"crypto/sha256"
55
"encoding/hex"
6+
"fmt"
7+
"slices"
68

79
"github.com/prometheus/client_golang/prometheus"
810

@@ -17,14 +19,40 @@ func flattenLabels(keys []string, labels map[string]string) []string {
1719
return result
1820
}
1921

20-
func computeHash(key Key, mapping *config.MetricsMapping, labels prometheus.Labels) string {
22+
func computeHash(mapping *config.MetricsMapping, labels prometheus.Labels) string {
2123
hash := sha256.New()
22-
hash.Write([]byte(key.String()))
23-
for i, v := range flattenLabels(mapping.Labels, labels) {
24-
if i != 0 {
25-
hash.Write([]byte("_"))
24+
hash.Write([]byte(mapping.Namespace))
25+
hash.Write([]byte(","))
26+
hash.Write([]byte(mapping.Subsystem))
27+
hash.Write([]byte(","))
28+
hash.Write([]byte(mapping.Name))
29+
if len(mapping.Labels) != 0 {
30+
hash.Write([]byte(","))
31+
for i, v := range flattenLabels(mapping.Labels, labels) {
32+
if i != 0 {
33+
hash.Write([]byte("_"))
34+
}
35+
hash.Write([]byte(v))
2636
}
27-
hash.Write([]byte(v))
2837
}
2938
return hex.EncodeToString(hash.Sum(nil))
3039
}
40+
41+
func validateLabels(key Key, mapping *config.MetricsMapping, labels prometheus.Labels) error {
42+
if len(labels) != len(mapping.Labels) {
43+
return fmt.Errorf("mismatched label name/value count for %s expected %d got %d", key, len(mapping.Labels), len(labels))
44+
}
45+
46+
for _, k := range mapping.Labels {
47+
if _, exists := labels[k]; !exists {
48+
return fmt.Errorf("missing required label %s for: %s", k, key)
49+
}
50+
}
51+
52+
for k := range labels {
53+
if !slices.Contains(mapping.Labels, k) {
54+
return fmt.Errorf("provided unknown label %s for: %s", k, key)
55+
}
56+
}
57+
return nil
58+
}

0 commit comments

Comments
 (0)