github.com/grafana/synthetic-monitoring-agent@v0.64.2/internal/metamonitoring/metrics.go (about) 1 package metamonitoring 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 "time" 8 9 logproto "github.com/grafana/loki/pkg/push" 10 "github.com/grafana/synthetic-monitoring-agent/internal/model" 11 "github.com/grafana/synthetic-monitoring-agent/internal/pusher" 12 "github.com/grafana/synthetic-monitoring-agent/pkg/pb/synthetic_monitoring" 13 "github.com/prometheus/client_golang/prometheus" 14 dto "github.com/prometheus/client_model/go" 15 "github.com/prometheus/prometheus/prompb" 16 "github.com/rs/zerolog" 17 ) 18 19 const defaultInterval = time.Minute 20 21 var errTenantTimeout = errors.New("timed out waiting for probes tenant id") 22 23 type HandlerOpts struct { 24 Logger zerolog.Logger 25 Registry prometheus.Gatherer 26 Publisher pusher.Publisher 27 TenantID model.GlobalID 28 Interval time.Duration 29 ProbeCh chan *synthetic_monitoring.Probe 30 } 31 32 type metricsHandler struct { 33 logger zerolog.Logger 34 registry prometheus.Gatherer 35 publisher pusher.Publisher 36 tenantID model.GlobalID 37 probeName string 38 interval time.Duration 39 probeCh chan *synthetic_monitoring.Probe 40 } 41 type Handler interface { 42 Run(ctx context.Context) error 43 } 44 45 func NewHandler(opts HandlerOpts) Handler { 46 interval := opts.Interval 47 if interval == 0 { 48 interval = defaultInterval 49 } 50 51 return &metricsHandler{ 52 logger: opts.Logger, 53 registry: opts.Registry, 54 publisher: opts.Publisher, 55 interval: interval, 56 probeCh: opts.ProbeCh, 57 tenantID: opts.TenantID, 58 } 59 } 60 61 func (m *metricsHandler) Run(ctx context.Context) error { 62 if m.tenantID == 0 { 63 err := m.waitForTenantID(ctx) 64 if err != nil { 65 return err 66 } 67 } 68 69 ticker := time.NewTicker(m.interval) 70 defer ticker.Stop() 71 72 m.logger = m.logger.With().Int64("tenantID", int64(m.tenantID)).Logger() 73 m.logger.Info().Msg("starting to report metrics") 74 75 for { 76 select { 77 case <-ctx.Done(): 78 return nil 79 case <-ticker.C: 80 if err := m.reportUsage(); err != nil { 81 m.logger.Error().Err(err).Msg("failed to report metrics") 82 } 83 } 84 } 85 } 86 87 func (m *metricsHandler) waitForTenantID(ctx context.Context) error { 88 select { 89 case probe := <-m.probeCh: 90 m.tenantID = model.GlobalID(probe.TenantId) 91 m.probeName = probe.Name 92 93 return nil 94 case <-ctx.Done(): 95 return fmt.Errorf("%w %w", errTenantTimeout, ctx.Err()) 96 } 97 } 98 99 func (m *metricsHandler) reportUsage() error { 100 mfs, err := m.registry.Gather() 101 if err != nil { 102 return err 103 } 104 105 now := time.Now() 106 107 ts := mfsToTimeseries(now, mfs, m.probeName) 108 if len(ts) == 0 { 109 return nil 110 } 111 112 m.publisher.Publish(&payload{ 113 tenantID: m.tenantID, 114 metrics: ts, 115 }) 116 117 return nil 118 } 119 120 func mfsToTimeseries(t time.Time, mfs []*dto.MetricFamily, probeName string) []prompb.TimeSeries { 121 stamp := t.UnixNano() / 1e6 122 123 var ts []prompb.TimeSeries 124 125 for _, mf := range mfs { 126 name := mf.GetName() 127 mType := mf.GetType() 128 129 for _, metric := range mf.GetMetric() { 130 ml := metric.GetLabel() 131 labels := make([]prompb.Label, 0, 1+len(ml)) 132 labels = append(labels, prompb.Label{Name: "__name__", Value: name}) 133 134 labels = append(labels, prompb.Label{Name: "probe", Value: probeName}) 135 for _, l := range ml { 136 labels = append(labels, prompb.Label{Name: l.GetName(), Value: l.GetValue()}) 137 } 138 139 // Histograms and summaries are intentionally skipped: they decompose 140 // into multiple series (_bucket, _sum, _count) that require special 141 // handling. Only scalar metric types (counter, gauge, untyped) are 142 // forwarded as-is. 143 var value *float64 144 145 switch mType { 146 case dto.MetricType_COUNTER: 147 if v := metric.GetCounter(); v != nil { 148 value = v.Value 149 } 150 case dto.MetricType_GAUGE: 151 if v := metric.GetGauge(); v != nil { 152 value = v.Value 153 } 154 case dto.MetricType_UNTYPED: 155 if v := metric.GetUntyped(); v != nil { 156 value = v.Value 157 } 158 } 159 160 if value != nil { 161 ts = append(ts, prompb.TimeSeries{ 162 Labels: labels, 163 Samples: []prompb.Sample{{Timestamp: stamp, Value: *value}}, 164 }) 165 } 166 } 167 } 168 169 return ts 170 } 171 172 type payload struct { 173 tenantID model.GlobalID 174 metrics []prompb.TimeSeries 175 } 176 177 func (p *payload) Tenant() model.GlobalID { return p.tenantID } 178 func (p *payload) Metrics() []prompb.TimeSeries { return p.metrics } 179 func (p *payload) Streams() []logproto.Stream { return nil }