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 }