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  }