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

     1  package metrics
     2  
     3  import (
     4  	"github.com/parquet-go/parquet-go"
     5  	"github.com/prometheus/common/model"
     6  	"github.com/prometheus/prometheus/model/labels"
     7  	"github.com/prometheus/prometheus/prompb"
     8  
     9  	"github.com/grafana/pyroscope/pkg/block"
    10  	phlaremodel "github.com/grafana/pyroscope/pkg/model"
    11  	schemav1 "github.com/grafana/pyroscope/pkg/phlaredb/schemas/v1"
    12  )
    13  
    14  type SampleObserver struct {
    15  	state *observerState
    16  
    17  	recordingTime  int64
    18  	externalLabels labels.Labels
    19  
    20  	exporter Exporter
    21  	ruler    Ruler
    22  }
    23  
    24  type observerState struct {
    25  	// tenant state
    26  	tenant     string
    27  	recordings []*recording
    28  
    29  	// dataset state
    30  	dataset           string
    31  	targetRecordings  []*recording
    32  	targetStrings     map[string][]*recording
    33  	targetLocations   map[uint32]map[*recording]struct{}
    34  	seenLocations     int
    35  	targetStacktraces map[uint32]map[*recording]struct{}
    36  
    37  	// series state
    38  	fingerprint   model.Fingerprint
    39  	recordSymbols bool
    40  }
    41  
    42  type recording struct {
    43  	rule  *phlaremodel.RecordingRule
    44  	data  map[model.Fingerprint]*prompb.TimeSeries
    45  	state *recordingState
    46  }
    47  
    48  type recordingState struct {
    49  	matches bool
    50  	sample  *prompb.Sample
    51  }
    52  
    53  type Ruler interface {
    54  	// RecordingRules return a validated set of rules for a tenant, with the following guarantees:
    55  	// - a "__name__" label is present among ExternalLabels. It contains a valid prometheus metric name, and starts
    56  	//   with `profiles_recorded_`
    57  	// - a "profiles_rule_id" label is present among ExternalLabels. It identifies the rule.
    58  	// - a matcher with name "__profile__type__" is present in Matchers
    59  	RecordingRules(tenant string) []*phlaremodel.RecordingRule
    60  }
    61  
    62  type Exporter interface {
    63  	Send(tenant string, series []prompb.TimeSeries) error
    64  	Flush()
    65  }
    66  
    67  func NewSampleObserver(recordingTime int64, exporter Exporter, ruler Ruler, labels labels.Labels) *SampleObserver {
    68  	return &SampleObserver{
    69  		recordingTime:  recordingTime,
    70  		externalLabels: labels,
    71  		exporter:       exporter,
    72  		ruler:          ruler,
    73  		state: &observerState{
    74  			recordings:        make([]*recording, 0),
    75  			targetRecordings:  make([]*recording, 0),
    76  			targetStrings:     make(map[string][]*recording),
    77  			targetLocations:   make(map[uint32]map[*recording]struct{}),
    78  			targetStacktraces: make(map[uint32]map[*recording]struct{}),
    79  		},
    80  	}
    81  }
    82  
    83  func (o *SampleObserver) initTenantState(tenant string) {
    84  	o.state.tenant = tenant
    85  	recordingRules := o.ruler.RecordingRules(tenant)
    86  
    87  	for _, rule := range recordingRules {
    88  		o.state.recordings = append(o.state.recordings, &recording{
    89  			rule:  rule,
    90  			data:  make(map[model.Fingerprint]*prompb.TimeSeries),
    91  			state: &recordingState{},
    92  		})
    93  	}
    94  
    95  	// force a dataset reset
    96  	o.state.dataset = ""
    97  }
    98  
    99  func (o *SampleObserver) initDatasetState(dataset string) {
   100  	// New dataset imply new symbols, and new subset of rules that can target the dataset
   101  	o.state.targetStrings = make(map[string][]*recording)
   102  	o.state.targetLocations = make(map[uint32]map[*recording]struct{})
   103  	o.state.targetStacktraces = make(map[uint32]map[*recording]struct{})
   104  	o.state.seenLocations = 0
   105  	o.state.dataset = dataset
   106  	o.state.targetRecordings = o.state.targetRecordings[:0]
   107  	for _, rec := range o.state.recordings {
   108  		// storing the subset of the recording that matter to this dataset:
   109  		if rec.matchesServiceName(dataset) {
   110  			o.state.targetRecordings = append(o.state.targetRecordings, rec)
   111  			if rec.rule.FunctionName != "" {
   112  				// create a lookup for functions names that matter
   113  				if _, exists := o.state.targetStrings[rec.rule.FunctionName]; !exists {
   114  					o.state.targetStrings[rec.rule.FunctionName] = make([]*recording, 0)
   115  				}
   116  				o.state.targetStrings[rec.rule.FunctionName] = append(o.state.targetStrings[rec.rule.FunctionName], rec)
   117  			}
   118  		}
   119  	}
   120  }
   121  
   122  // Evaluate manages three kind of states.
   123  //   - Per tenant state:
   124  //     Gets initialized on new tenant. It fetches tenant's rules and creates a new recording for each rule.
   125  //     Data of old state is flushed to the exporter.
   126  //   - Per dataset state:
   127  //     Gets initialized on new dataset. It holds the subset of rules that matter to that dataset, and some set of
   128  //     pointers symbol-to-rule.
   129  //   - Per series (or batch of rows) state:
   130  //     Holds the fingerprint of the series (every batch of rows of the same fingerprint), and whether there's a matching
   131  //     rule that requires symbols to be observed.
   132  //     In addition, the state of every recording is computed, i.e. whether the rule matches the new batch of rows, and
   133  //     a reference of the sample to be aggregated to. (Note that every rule will eventually create multiple single-sample (aggregated) series,
   134  //     depending on the rule.GroupBy space. More info in initState).
   135  //
   136  // This call is not thread-safe
   137  func (o *SampleObserver) Evaluate(row block.ProfileEntry) func() {
   138  	// Detect a tenant switch
   139  	tenant := row.Dataset.TenantID()
   140  	if o.state.tenant != row.Dataset.TenantID() {
   141  		// new tenant to observe, flush data of previous tenant and init new tenant state
   142  		o.flush()
   143  		o.initTenantState(tenant)
   144  	}
   145  
   146  	// Detect a dataset switch
   147  	if o.state.dataset != row.Dataset.Name() {
   148  		o.initDatasetState(row.Dataset.Name())
   149  	}
   150  
   151  	// Detect a series switch
   152  	if o.state.fingerprint != row.Fingerprint {
   153  		// New series. Handle state.
   154  		o.initSeriesState(row)
   155  	}
   156  	return func() {
   157  		o.observe(row)
   158  	}
   159  }
   160  
   161  func (o *SampleObserver) initSeriesState(row block.ProfileEntry) {
   162  	o.state.fingerprint = row.Fingerprint
   163  	o.state.recordSymbols = false
   164  
   165  	sb := labels.NewScratchBuilder(len(row.Labels))
   166  	for _, label := range row.Labels {
   167  		sb.Add(label.Name, label.Value)
   168  	}
   169  	sb.Sort()
   170  	blockLabels := sb.Labels()
   171  	lb := labels.NewBuilder(labels.EmptyLabels())
   172  	for _, rec := range o.state.targetRecordings {
   173  		rec.initState(lb, blockLabels, o.externalLabels, o.recordingTime)
   174  		if rec.state.matches && rec.rule.FunctionName != "" {
   175  			o.state.recordSymbols = true
   176  		}
   177  	}
   178  }
   179  
   180  // ObserveSymbols will observe symbols as soon as there's a function rule targeting this dataset.
   181  // Symbols are observed only once, and the current rule may have symbols that a future matching rule needs.
   182  // This is suboptimal as we may read more symbols than needed. However, the current interface does not let us do better,
   183  // and a bigger refactor may be needed to address this issue.
   184  // At the end of this process we'll have a map stacktraceId -> matching rule, so later we can get stacktraces from the
   185  // row and quickly look up for matching rules
   186  func (o *SampleObserver) ObserveSymbols(strings []string, functions []schemav1.InMemoryFunction, locations []schemav1.InMemoryLocation, stacktraceValues [][]int32, stacktraceIds []uint32) {
   187  	if len(o.state.targetStrings) == 0 {
   188  		return
   189  	}
   190  
   191  	for ; o.state.seenLocations < len(locations); o.state.seenLocations++ {
   192  		for _, line := range locations[o.state.seenLocations].Line {
   193  			recs, hit := o.state.targetStrings[strings[functions[line.FunctionId].Name]]
   194  			if hit {
   195  				targetLocation, exists := o.state.targetLocations[uint32(o.state.seenLocations)]
   196  				if !exists {
   197  					targetLocation = make(map[*recording]struct{})
   198  					o.state.targetLocations[uint32(o.state.seenLocations)] = targetLocation
   199  				}
   200  				for _, rec := range recs {
   201  					targetLocation[rec] = struct{}{}
   202  				}
   203  			}
   204  		}
   205  	}
   206  	if len(o.state.targetLocations) == 0 {
   207  		return
   208  	}
   209  	for i, stacktrace := range stacktraceValues {
   210  		for _, locationId := range stacktrace {
   211  			recs, hit := o.state.targetLocations[uint32(locationId)]
   212  			if hit {
   213  				targetStacktrace, exists := o.state.targetStacktraces[stacktraceIds[i]]
   214  				if !exists {
   215  					targetStacktrace = make(map[*recording]struct{})
   216  					o.state.targetStacktraces[stacktraceIds[i]] = targetStacktrace
   217  				}
   218  				for rec := range recs {
   219  					targetStacktrace[rec] = struct{}{}
   220  				}
   221  			}
   222  		}
   223  	}
   224  }
   225  
   226  func (o *SampleObserver) observe(row block.ProfileEntry) {
   227  	// Totals are computed as follows: for every rule that matches the series, we add the TotalValue
   228  	for _, rec := range o.state.targetRecordings {
   229  		if rec.state.matches && rec.rule.FunctionName == "" {
   230  			rec.state.sample.Value += float64(row.Row.TotalValue())
   231  		}
   232  	}
   233  	// On the other hand, functions are computed from the lookup tables only if the series hit some rule.
   234  	if o.state.recordSymbols {
   235  		row.Row.ForStacktraceIdsAndValues(func(ids []parquet.Value, values []parquet.Value) {
   236  			for i, id := range ids {
   237  				for rec := range o.state.targetStacktraces[id.Uint32()] {
   238  					if rec.state.matches {
   239  						rec.state.sample.Value += float64(values[i].Int64())
   240  					}
   241  				}
   242  			}
   243  		})
   244  	}
   245  }
   246  
   247  func (o *SampleObserver) flush() {
   248  	if len(o.state.recordings) == 0 {
   249  		return
   250  	}
   251  	timeSeries := make([]prompb.TimeSeries, 0)
   252  	for _, rec := range o.state.recordings {
   253  		for _, series := range rec.data {
   254  			timeSeries = append(timeSeries, *series)
   255  		}
   256  	}
   257  	if len(timeSeries) > 0 {
   258  		_ = o.exporter.Send(o.state.tenant, timeSeries)
   259  	}
   260  	o.state.recordings = o.state.recordings[:0]
   261  }
   262  
   263  func (o *SampleObserver) Close() {
   264  	o.flush()
   265  }
   266  
   267  // initState checks whether row matches the filters
   268  // if filters match, then labels to export are computed, and fetch/create the series where the value needs to be
   269  // aggregated. This state is hold for the following rows with the same fingerprint, so we can observe those faster
   270  func (r *recording) initState(lb *labels.Builder, blockLabels labels.Labels, externalLabels labels.Labels, recordingTime int64) {
   271  	r.state.matches = r.matches(blockLabels)
   272  	if !r.state.matches {
   273  		return
   274  	}
   275  
   276  	lblCount := setExportedLabels(lb, blockLabels, r, externalLabels)
   277  	exportedLabels := lb.Labels()
   278  	aggregatedFp := model.Fingerprint(exportedLabels.Hash())
   279  
   280  	series, ok := r.data[aggregatedFp]
   281  	if !ok {
   282  		series = newTimeSeries(exportedLabels, lblCount, recordingTime)
   283  		r.data[aggregatedFp] = series
   284  	}
   285  	r.state.sample = &series.Samples[0]
   286  }
   287  
   288  func newTimeSeries(exportedLabels labels.Labels, exportedLabelCount int, recordingTime int64) *prompb.TimeSeries {
   289  	pbLabels := make([]prompb.Label, 0, exportedLabelCount)
   290  	pbLabels = prompb.FromLabels(exportedLabels, pbLabels)
   291  	series := &prompb.TimeSeries{
   292  		Labels: pbLabels,
   293  		Samples: []prompb.Sample{
   294  			{
   295  				Value: 0, Timestamp: recordingTime,
   296  			},
   297  		},
   298  	}
   299  	return series
   300  }
   301  
   302  func setExportedLabels(lb *labels.Builder, blockLabels labels.Labels, rec *recording, externalLabels labels.Labels) int {
   303  	count := 0
   304  	set := func(l labels.Label) {
   305  		count += 1
   306  		lb.Set(l.Name, l.Value)
   307  	}
   308  	// reset label builder to contain both externalLabels
   309  	lb.Reset(labels.EmptyLabels())
   310  	externalLabels.Range(set)
   311  	rec.rule.ExternalLabels.Range(set)
   312  
   313  	// Keep the groupBy labels if present
   314  	for _, label := range rec.rule.GroupBy {
   315  		labelValue := blockLabels.Get(label)
   316  		if labelValue != "" {
   317  			set(labels.Label{Name: label, Value: labelValue})
   318  		}
   319  	}
   320  	return count
   321  }
   322  
   323  func (r *recording) matchesServiceName(dataset string) bool {
   324  	for _, matcher := range r.rule.Matchers {
   325  		if matcher.Name == "service_name" && !matcher.Matches(dataset) {
   326  			return false
   327  		}
   328  	}
   329  	return true
   330  }
   331  
   332  func (r *recording) matches(lbls labels.Labels) bool {
   333  	for _, matcher := range r.rule.Matchers {
   334  		if !matcher.Matches(lbls.Get(matcher.Name)) {
   335  			return false
   336  		}
   337  	}
   338  	return true
   339  }