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 }