github.com/grafana/pyroscope@v1.18.0/pkg/metrics/exporter.go (about)

     1  package metrics
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"net/http"
     7  	"net/url"
     8  	"strconv"
     9  	"sync"
    10  	"time"
    11  
    12  	"github.com/go-kit/log"
    13  	"github.com/go-kit/log/level"
    14  	"github.com/gogo/protobuf/proto"
    15  	"github.com/grafana/dskit/instrument"
    16  	"github.com/klauspost/compress/snappy"
    17  	"github.com/prometheus/client_golang/prometheus"
    18  	"github.com/prometheus/common/config"
    19  	"github.com/prometheus/common/model"
    20  	"github.com/prometheus/prometheus/prompb"
    21  	"github.com/prometheus/prometheus/storage/remote"
    22  
    23  	pyroscopemodel "github.com/grafana/pyroscope/pkg/model"
    24  	"github.com/grafana/pyroscope/pkg/tenant"
    25  )
    26  
    27  type StaticExporter struct {
    28  	client remote.WriteClient
    29  	wg     sync.WaitGroup
    30  
    31  	logger log.Logger
    32  
    33  	metrics *clientMetrics
    34  }
    35  
    36  const (
    37  	metricsExporterUserAgent = "pyroscope-metrics-exporter"
    38  )
    39  
    40  func NewExporter(remoteWriteAddress string, logger log.Logger, reg prometheus.Registerer) (Exporter, error) {
    41  	metrics := newMetrics(reg, remoteWriteAddress)
    42  	client, err := newClient(remoteWriteAddress, metrics)
    43  	if err != nil {
    44  		return nil, err
    45  	}
    46  	return &StaticExporter{
    47  		client:  client,
    48  		wg:      sync.WaitGroup{},
    49  		logger:  logger,
    50  		metrics: metrics,
    51  	}, nil
    52  }
    53  
    54  func (e *StaticExporter) Send(tenantId string, data []prompb.TimeSeries) error {
    55  	e.wg.Add(1)
    56  	go func(data []prompb.TimeSeries) {
    57  		defer e.wg.Done()
    58  		p := &prompb.WriteRequest{Timeseries: data}
    59  		buf := proto.NewBuffer(nil)
    60  		if err := buf.Marshal(p); err != nil {
    61  			level.Error(e.logger).Log("msg", "unable to marshal prompb.WriteRequest", "err", err)
    62  			return
    63  		}
    64  
    65  		ctx := tenant.InjectTenantID(context.Background(), tenantId)
    66  		_, err := e.client.Store(ctx, snappy.Encode(nil, buf.Bytes()), 0)
    67  		if err != nil {
    68  			level.Error(e.logger).Log("msg", "unable to store prompb.WriteRequest", "err", err)
    69  			return
    70  		}
    71  		seriesByRuleID := make(map[string]int)
    72  		for _, ts := range data {
    73  			ruleID := "unknown"
    74  			for _, l := range ts.Labels {
    75  				if l.Name == pyroscopemodel.RuleIDLabel {
    76  					ruleID = l.Value
    77  					break
    78  				}
    79  			}
    80  			seriesByRuleID[ruleID]++
    81  		}
    82  		for ruleID, count := range seriesByRuleID {
    83  			e.metrics.seriesSent.WithLabelValues(tenantId, ruleID).Add(float64(count))
    84  		}
    85  	}(data)
    86  	return nil
    87  }
    88  
    89  func (e *StaticExporter) Flush() {
    90  	e.wg.Wait()
    91  }
    92  
    93  func newClient(remoteUrl string, m *clientMetrics) (remote.WriteClient, error) {
    94  	wURL, err := url.Parse(remoteUrl)
    95  	if err != nil {
    96  		return nil, err
    97  	}
    98  
    99  	client, err := remote.NewWriteClient("exporter", &remote.ClientConfig{
   100  		URL:     &config.URL{URL: wURL},
   101  		Timeout: model.Duration(time.Second * 10),
   102  		Headers: map[string]string{
   103  			"User-Agent": metricsExporterUserAgent,
   104  		},
   105  		RetryOnRateLimit: false,
   106  	})
   107  	if err != nil {
   108  		return nil, err
   109  	}
   110  	t := client.(*remote.Client).Client.Transport
   111  	client.(*remote.Client).Client.Transport = &RoundTripper{m, t}
   112  	return client, nil
   113  }
   114  
   115  type clientMetrics struct {
   116  	requestDuration *prometheus.HistogramVec
   117  	requestBodySize *prometheus.CounterVec
   118  	seriesSent      *prometheus.CounterVec
   119  }
   120  
   121  func newMetrics(reg prometheus.Registerer, remoteUrl string) *clientMetrics {
   122  	m := &clientMetrics{
   123  		requestDuration: prometheus.NewHistogramVec(prometheus.HistogramOpts{
   124  			Namespace: "pyroscope",
   125  			Subsystem: "metrics_exporter",
   126  			Name:      "client_request_duration_seconds",
   127  			Help:      "Time (in seconds) spent on remote_write",
   128  			Buckets:   instrument.DefBuckets,
   129  		}, []string{"route", "status_code", "tenant"}),
   130  		requestBodySize: prometheus.NewCounterVec(prometheus.CounterOpts{
   131  			Namespace: "pyroscope",
   132  			Subsystem: "metrics_exporter",
   133  			Name:      "request_message_bytes_total",
   134  			Help:      "Size (in bytes) of messages sent on remote_write.",
   135  		}, []string{"route", "status_code", "tenant"}),
   136  		seriesSent: prometheus.NewCounterVec(prometheus.CounterOpts{
   137  			Namespace: "pyroscope",
   138  			Subsystem: "metrics_exporter",
   139  			Name:      "series_sent_total",
   140  			Help:      "Number of series sent on remote_write.",
   141  		}, []string{"tenant", "rule_id"}),
   142  	}
   143  	if reg != nil {
   144  		remoteUrlReg := prometheus.WrapRegistererWith(prometheus.Labels{"url": remoteUrl}, reg)
   145  		remoteUrlReg.MustRegister(
   146  			m.requestDuration,
   147  			m.requestBodySize,
   148  			m.seriesSent,
   149  		)
   150  	}
   151  	return m
   152  }
   153  
   154  type RoundTripper struct {
   155  	metrics *clientMetrics
   156  	next    http.RoundTripper
   157  }
   158  
   159  func (m *RoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
   160  	tenantId, err := tenant.ExtractTenantIDFromContext(req.Context())
   161  	if err != nil {
   162  		return nil, fmt.Errorf("unable to get tenant ID from context: %w", err)
   163  	}
   164  	req.Header.Set("X-Scope-OrgId", tenantId)
   165  
   166  	start := time.Now()
   167  	resp, err := m.next.RoundTrip(req)
   168  	duration := time.Since(start)
   169  
   170  	statusCode := ""
   171  	if resp != nil {
   172  		statusCode = strconv.Itoa(resp.StatusCode)
   173  	}
   174  
   175  	m.metrics.requestDuration.WithLabelValues(req.RequestURI, statusCode, tenantId).Observe(duration.Seconds())
   176  	m.metrics.requestBodySize.WithLabelValues(req.RequestURI, statusCode, tenantId).Add(float64(req.ContentLength))
   177  	return resp, err
   178  }