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  }