github.com/pyroscope-io/pyroscope@v0.37.3-0.20230725203016-5f6947968bd0/pkg/exporter/exporter.go (about) 1 package exporter 2 3 import ( 4 "fmt" 5 "regexp" 6 7 "github.com/prometheus/client_golang/prometheus" 8 "github.com/prometheus/common/model" 9 "github.com/pyroscope-io/pyroscope/pkg/agent/spy" 10 11 "github.com/pyroscope-io/pyroscope/pkg/config" 12 "github.com/pyroscope-io/pyroscope/pkg/flameql" 13 "github.com/pyroscope-io/pyroscope/pkg/storage" 14 "github.com/pyroscope-io/pyroscope/pkg/storage/segment" 15 ) 16 17 // MetricsExporter exports profiling metrics via Prometheus. 18 // It is safe for concurrent use. 19 type MetricsExporter struct{ rules map[string]*rule } 20 21 type rule struct { 22 name string 23 qry *flameql.Query 24 node *regexp.Regexp 25 labels []string 26 ctr *prometheus.CounterVec 27 } 28 29 type observer struct { 30 // Regular expressions of matched rules. 31 expr []*regexp.Regexp 32 // Counters corresponding matching rules. 33 ctr []prometheus.Counter 34 // Sample value multiplier. 35 m float64 36 } 37 38 // NewExporter validates configuration and creates a new prometheus MetricsExporter. 39 // It is safe to initialize MetricsExporter without rules, then registry can be nil, 40 // Evaluate call will be a noop. 41 func NewExporter(rules config.MetricsExportRules, reg prometheus.Registerer) (*MetricsExporter, error) { 42 e := MetricsExporter{ 43 rules: make(map[string]*rule), 44 } 45 if rules == nil { 46 return &e, nil 47 } 48 for name, r := range rules { 49 if !model.IsValidMetricName(model.LabelValue(name)) { 50 return nil, fmt.Errorf("%q is not a valid metric name", name) 51 } 52 qry, err := flameql.ParseQuery(r.Expr) 53 if err != nil { 54 return nil, fmt.Errorf("rule %q: invalid expression %q: %w", name, r.Expr, err) 55 } 56 var node *regexp.Regexp 57 if !(r.Node == "total" || r.Node == "") { 58 node, err = regexp.Compile(r.Node) 59 if err != nil { 60 return nil, fmt.Errorf("node must be either 'total' or a valid regexp: %w", err) 61 } 62 } 63 if err = validateTagKeys(r.GroupBy); err != nil { 64 return nil, fmt.Errorf("rule %q: invalid label: %w", name, err) 65 } 66 c := prometheus.NewCounterVec(prometheus.CounterOpts{Name: name}, r.GroupBy) 67 if err = reg.Register(c); err != nil { 68 return nil, err 69 } 70 e.rules[name] = &rule{ 71 qry: qry, 72 node: node, 73 labels: r.GroupBy, 74 ctr: c, 75 } 76 } 77 return &e, nil 78 } 79 80 func (e MetricsExporter) Evaluate(input *storage.PutInput) (storage.SampleObserver, bool) { 81 if len(e.rules) == 0 { 82 return nil, false 83 } 84 o := observer{m: 1} 85 for _, r := range e.rules { 86 if !input.Key.Match(r.qry) { 87 continue 88 } 89 o.expr = append(o.expr, r.node) 90 o.ctr = append(o.ctr, r.ctr.With(r.promLabels(input.Key))) 91 } 92 if len(o.expr) == 0 { 93 // No rules matched. 94 return nil, false 95 } 96 if input.Units == spy.ProfileCPU.Units() { 97 // Sample duration in seconds. 98 o.m = 1 / float64(input.SampleRate) 99 } 100 return o, true 101 } 102 103 func (o observer) Observe(k []byte, v int) { 104 if k == nil || v == 0 { 105 return 106 } 107 for i, e := range o.expr { 108 if e != nil && !e.Match(k) { 109 continue 110 } 111 o.ctr[i].Add(float64(v) * o.m) 112 } 113 } 114 115 func validateTagKeys(tagKeys []string) error { 116 for _, l := range tagKeys { 117 if err := flameql.ValidateTagKey(l); err != nil { 118 return err 119 } 120 } 121 return nil 122 } 123 124 // promLabels converts key to prometheus.Labels ignoring reserved tag keys. 125 // Only explicitly listed labels are converted. 126 func (r rule) promLabels(key *segment.Key) prometheus.Labels { 127 if len(r.labels) == 0 { 128 return nil 129 } 130 // labels are guarantied to be valid. 131 l := key.Labels() 132 p := make(prometheus.Labels, len(r.labels)) 133 for _, k := range r.labels { 134 p[k] = l[k] 135 } 136 return p 137 }