github.com/pyroscope-io/pyroscope@v0.37.3-0.20230725203016-5f6947968bd0/pkg/storage/storage_exemplars_test.go (about) 1 //go:build !windows 2 // +build !windows 3 4 package storage 5 6 import ( 7 "context" 8 "encoding/hex" 9 "math/big" 10 "math/rand" 11 "os" 12 "sync" 13 "time" 14 15 . "github.com/onsi/ginkgo/v2" 16 . "github.com/onsi/gomega" 17 "github.com/prometheus/client_golang/prometheus" 18 "github.com/sirupsen/logrus" 19 20 "github.com/pyroscope-io/pyroscope/pkg/config" 21 "github.com/pyroscope-io/pyroscope/pkg/flameql" 22 "github.com/pyroscope-io/pyroscope/pkg/health" 23 "github.com/pyroscope-io/pyroscope/pkg/storage/dict" 24 "github.com/pyroscope-io/pyroscope/pkg/storage/metadata" 25 "github.com/pyroscope-io/pyroscope/pkg/storage/segment" 26 "github.com/pyroscope-io/pyroscope/pkg/storage/tree" 27 "github.com/pyroscope-io/pyroscope/pkg/testing" 28 ) 29 30 var _ = Describe("Exemplars retrieval", func() { 31 st := time.Now() 32 et := st.Add(10 * time.Second) 33 34 put := func(s *Storage, m map[string]string) { 35 tree := tree.New() 36 tree.Insert([]byte("a;b"), uint64(1)) 37 tree.Insert([]byte("a;c"), uint64(2)) 38 39 Expect(s.Put(context.TODO(), &PutInput{ 40 StartTime: st, 41 EndTime: et, 42 Key: segment.NewKey(m), 43 Val: tree.Clone(big.NewRat(1, 1)), 44 SpyName: "debugspy", 45 SampleRate: 100, 46 Units: metadata.SamplesUnits, 47 AggregationType: metadata.AverageAggregationType, 48 })).ToNot(HaveOccurred()) 49 } 50 51 testing.WithConfig(func(cfg **config.Config) { 52 JustBeforeEach(func() { 53 var err error 54 s, err = New(NewConfig(&(*cfg).Server), logrus.StandardLogger(), prometheus.NewRegistry(), new(health.Controller), NoopApplicationMetadataService{}) 55 Expect(err).ToNot(HaveOccurred()) 56 57 put(s, map[string]string{ 58 "__name__": "app.cpu", 59 "span_name": "foo", 60 "profile_id": "a", 61 }) 62 put(s, map[string]string{ 63 "__name__": "app.cpu", 64 "span_name": "foo", 65 "profile_id": "a", 66 }) 67 put(s, map[string]string{ 68 "__name__": "app.cpu", 69 "span_name": "foo", 70 "profile_id": "b", 71 }) 72 73 s.exemplars.Sync() 74 }) 75 76 Context("Get", func() { 77 It("merges profiling data correctly", func() { 78 defer s.Close() 79 80 o, err := s.Get(context.Background(), &GetInput{ 81 Query: &flameql.Query{ 82 AppName: "app.cpu", 83 Matchers: []*flameql.TagMatcher{ 84 {Key: segment.ProfileIDLabelName, Value: "a", Op: flameql.OpEqual}, 85 {Key: segment.ProfileIDLabelName, Value: "b", Op: flameql.OpEqual}, 86 }, 87 }, 88 }) 89 90 Expect(err).ToNot(HaveOccurred()) 91 Expect(o.Tree).ToNot(BeNil()) 92 Expect(o.Tree.Samples()).To(Equal(uint64(4))) 93 Expect(o.Count).To(Equal(uint64(2))) 94 Expect(o.SpyName).To(Equal("debugspy")) 95 Expect(o.SampleRate).To(Equal(uint32(100))) 96 Expect(o.Units).To(Equal(metadata.SamplesUnits)) 97 Expect(o.AggregationType).To(Equal(metadata.AverageAggregationType)) 98 }) 99 }) 100 101 Context("GetExemplar", func() { 102 It("fetches exemplar data correctly", func() { 103 defer s.Close() 104 105 o, err := s.GetExemplar(context.Background(), GetExemplarInput{ 106 AppName: "app.cpu", 107 ProfileID: "a", 108 }) 109 110 Expect(err).ToNot(HaveOccurred()) 111 Expect(o.Tree).ToNot(BeNil()) 112 Expect(o.Tree.Samples()).To(Equal(uint64(6))) 113 114 Expect(o.StartTime).Should(BeTemporally("~", st, time.Second)) 115 Expect(o.EndTime).Should(BeTemporally("~", et, time.Second)) 116 117 Expect(o.Labels).To(Equal(map[string]string{"span_name": "foo"})) 118 Expect(o.Metadata).To(Equal(metadata.Metadata{ 119 SpyName: "debugspy", 120 SampleRate: uint32(100), 121 Units: metadata.SamplesUnits, 122 AggregationType: metadata.AverageAggregationType, 123 })) 124 }) 125 }) 126 127 Context("MergeExemplars", func() { 128 It("merges profiling data correctly", func() { 129 defer s.Close() 130 131 o, err := s.MergeExemplars(context.Background(), MergeExemplarsInput{ 132 AppName: "app.cpu", 133 ProfileIDs: []string{"a"}, 134 }) 135 Expect(err).ToNot(HaveOccurred()) 136 Expect(o.Tree).ToNot(BeNil()) 137 Expect(o.Tree.Samples()).To(Equal(uint64(6))) 138 139 o, err = s.MergeExemplars(context.Background(), MergeExemplarsInput{ 140 AppName: "app.cpu", 141 ProfileIDs: []string{"b"}, 142 }) 143 Expect(err).ToNot(HaveOccurred()) 144 Expect(o.Tree).ToNot(BeNil()) 145 Expect(o.Tree.Samples()).To(Equal(uint64(3))) 146 147 o, err = s.MergeExemplars(context.Background(), MergeExemplarsInput{ 148 AppName: "app.cpu", 149 ProfileIDs: []string{"a", "b"}, 150 }) 151 152 Expect(err).ToNot(HaveOccurred()) 153 Expect(o.Tree).ToNot(BeNil()) 154 Expect(o.Tree.Samples()).To(Equal(uint64(4))) 155 Expect(o.Count).To(Equal(uint64(2))) 156 Expect(o.Metadata).To(Equal(metadata.Metadata{ 157 SpyName: "debugspy", 158 SampleRate: uint32(100), 159 Units: metadata.SamplesUnits, 160 AggregationType: metadata.AverageAggregationType, 161 })) 162 }) 163 }) 164 }) 165 }) 166 167 var _ = Describe("Exemplars retention policy", func() { 168 testing.WithConfig(func(cfg **config.Config) { 169 JustBeforeEach(func() { 170 var err error 171 s, err = New(NewConfig(&(*cfg).Server), logrus.StandardLogger(), prometheus.NewRegistry(), new(health.Controller), NoopApplicationMetadataService{}) 172 Expect(err).ToNot(HaveOccurred()) 173 }) 174 Context("when time-based retention policy is defined", func() { 175 It("removes profiling data outside the period", func() { 176 defer s.Close() 177 178 tree := tree.New() 179 tree.Insert([]byte("a;b"), uint64(1)) 180 tree.Insert([]byte("a;c"), uint64(2)) 181 182 k1, _ := segment.ParseKey("app.cpu{profile_id=a}") 183 t1 := time.Now() 184 t2 := t1.Add(10 * time.Second) 185 Expect(s.Put(context.TODO(), &PutInput{ 186 StartTime: t1, 187 EndTime: t2, 188 Key: k1, 189 Val: tree.Clone(big.NewRat(1, 1)), 190 })).ToNot(HaveOccurred()) 191 192 t3 := t2.Add(10 * time.Second) 193 t4 := t3.Add(10 * time.Second) 194 k2, _ := segment.ParseKey("app.cpu{profile_id=b}") 195 Expect(s.Put(context.TODO(), &PutInput{ 196 StartTime: t3, 197 EndTime: t4, 198 Key: k2, 199 Val: tree.Clone(big.NewRat(1, 1)), 200 })).ToNot(HaveOccurred()) 201 202 s.exemplars.Sync() 203 rp := &segment.RetentionPolicy{ExemplarsRetentionTime: t3} 204 s.exemplars.enforceRetentionPolicy(context.Background(), rp) 205 206 o, err := s.MergeExemplars(context.Background(), MergeExemplarsInput{ 207 AppName: "app.cpu", 208 ProfileIDs: []string{"a", "b"}, 209 }) 210 Expect(err).ToNot(HaveOccurred()) 211 Expect(o.Tree).ToNot(BeNil()) 212 Expect(o.Tree.Samples()).To(Equal(uint64(3))) 213 214 gi := new(GetInput) 215 gi.Query, _ = flameql.ParseQuery(`app.cpu{profile_id="b"}`) 216 o2, err := s.Get(context.TODO(), gi) 217 Expect(err).ToNot(HaveOccurred()) 218 Expect(o2.Tree).ToNot(BeNil()) 219 Expect(o2.Tree.Samples()).To(Equal(uint64(3))) 220 }) 221 }) 222 }) 223 }) 224 225 var _ = Describe("Concurrent exemplars insertion", func() { 226 testing.WithConfig(func(cfg **config.Config) { 227 JustBeforeEach(func() { 228 var err error 229 s, err = New(NewConfig(&(*cfg).Server), logrus.StandardLogger(), prometheus.NewRegistry(), new(health.Controller), NoopApplicationMetadataService{}) 230 Expect(err).ToNot(HaveOccurred()) 231 }) 232 Context("when exemplars ingested concurrently", func() { 233 It("does not race with sync and periodic flush", func() { 234 defer s.Close() 235 const ( 236 n = 4 237 c = 100 238 ) 239 240 stop := make(chan struct{}) 241 done := make(chan struct{}) 242 go func() { 243 defer close(done) 244 for { 245 select { 246 case <-stop: 247 return 248 case <-time.After(10 * time.Millisecond): 249 s.exemplars.Sync() 250 } 251 } 252 }() 253 254 var wg sync.WaitGroup 255 wg.Add(n) 256 257 for i := 0; i < n; i++ { 258 go func() { 259 defer wg.Done() 260 for j := 0; j < c; j++ { 261 tree := tree.New() 262 tree.Insert([]byte("a;b"), uint64(1)) 263 tree.Insert([]byte("a;c"), uint64(2)) 264 Expect(s.Put(context.TODO(), &PutInput{ 265 StartTime: testing.SimpleTime(0), 266 EndTime: testing.SimpleTime(30), 267 Val: tree, 268 Key: segment.NewKey(map[string]string{ 269 "__name__": "app.cpu", 270 "profile_id": randomBytesHex(8), 271 }), 272 })).ToNot(HaveOccurred()) 273 } 274 }() 275 } 276 277 wg.Wait() 278 close(stop) 279 <-done 280 }) 281 }) 282 }) 283 }) 284 285 var _ = Describe("Exemplar serialization", func() { 286 Context("exemplars serialisation", func() { 287 It("can be serialized and deserialized", func() { 288 const appName = "app.cpu" 289 profileID := randomBytesHex(8) 290 t := tree.New() 291 t.Insert([]byte("a;b"), uint64(1)) 292 t.Insert([]byte("a;c"), uint64(2)) 293 294 e := exemplarEntry{ 295 Key: exemplarKey(appName, profileID), 296 AppName: appName, 297 ProfileID: profileID, 298 299 StartTime: testing.SimpleTime(123).UnixNano(), 300 EndTime: testing.SimpleTime(456).UnixNano(), 301 Tree: t, 302 Labels: map[string]string{ 303 "__name__": appName, 304 "profile_id": profileID, 305 "foo": "bar", 306 "baz": "qux", 307 }, 308 } 309 310 d := dict.New() 311 b, err := e.Serialize(d, 1<<10) 312 Expect(err).ToNot(HaveOccurred()) 313 314 var n exemplarEntry 315 Expect(n.Deserialize(d, b)).ToNot(HaveOccurred()) 316 317 Expect(e.StartTime).To(Equal(n.StartTime)) 318 Expect(e.EndTime).To(Equal(n.EndTime)) 319 Expect(e.Tree.String()).To(Equal(n.Tree.String())) 320 Expect(n.Labels).To(Equal(map[string]string{ 321 "foo": "bar", 322 "baz": "qux", 323 })) 324 }) 325 }) 326 327 Context("exemplars v1 compatibility", func() { 328 It("can deserialize exemplars v1", func() { 329 b, err := os.ReadFile("./testdata/exemplar.v1.bin") 330 Expect(err).ToNot(HaveOccurred()) 331 332 var n exemplarEntry 333 Expect(n.Deserialize(dict.New(), b)).ToNot(HaveOccurred()) 334 335 Expect(n.Tree.Samples()).To(Equal(uint64(81255))) 336 Expect(n.StartTime).To(BeZero()) 337 Expect(n.EndTime).To(BeZero()) 338 Expect(n.Labels).To(BeNil()) 339 }) 340 }) 341 }) 342 343 var _ = Describe("Exemplar timestamps", func() { 344 Context("exemplars query", func() { 345 It("selects all entries if no time range is provided or timestamps are not present", func() { 346 for i := 0; i < 0xF; i++ { 347 e := exemplarEntry{ 348 StartTime: bitAt(i, 3), 349 EndTime: bitAt(i, 2), 350 } 351 startTime := bitAt(i, 1) 352 endTime := bitAt(i, 0) 353 354 Expect(exemplarMatchesTimeRange(e, startTime, endTime)).To(BeTrue()) 355 } 356 }) 357 358 It("selects matched entries", func() { 359 startTime := time.Now().UnixNano() 360 endTime := startTime + 3 361 e := exemplarEntry{ 362 StartTime: startTime, 363 EndTime: endTime, 364 } 365 366 for _, r := range [][2]int64{ 367 {0, 0}, 368 {1, -1}, 369 {-1, 1}, 370 371 {0, 1}, 372 {1, 0}, 373 {1, 1}, 374 375 {0, -1}, 376 {-1, 0}, 377 {-1, -1}, 378 } { 379 Expect(exemplarMatchesTimeRange(e, startTime+r[0], endTime+r[1])).To(BeTrue()) 380 } 381 382 for _, r := range [][2]int64{ 383 {endTime, endTime}, 384 {endTime, endTime + 1}, 385 {startTime, startTime}, 386 {startTime - 1, startTime}, 387 } { 388 Expect(exemplarMatchesTimeRange(e, r[0], r[1])).To(BeFalse()) 389 } 390 }) 391 }) 392 }) 393 394 func randomBytesHex(n int) string { 395 b := make([]byte, n) 396 rand.Read(b) 397 return hex.EncodeToString(b) 398 } 399 400 func bitAt(n, b int) int64 { 401 if n&(1<<b) > 0 { 402 return 1 403 } 404 return 0 405 }