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 }