github.com/pyroscope-io/pyroscope@v0.37.3-0.20230725203016-5f6947968bd0/pkg/server/ingest_test.go (about)

     1  package server
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"encoding/json"
     7  	"fmt"
     8  	"io"
     9  	"io/ioutil"
    10  	"mime/multipart"
    11  	"net/http"
    12  	"net/http/httptest"
    13  	"net/url"
    14  	"sort"
    15  	"strconv"
    16  	"time"
    17  
    18  	"github.com/klauspost/compress/gzip"
    19  	. "github.com/onsi/ginkgo/v2"
    20  	. "github.com/onsi/gomega"
    21  	"github.com/prometheus/client_golang/prometheus"
    22  	"github.com/pyroscope-io/pyroscope/pkg/flameql"
    23  	"github.com/sirupsen/logrus"
    24  
    25  	"github.com/pyroscope-io/pyroscope/pkg/config"
    26  	"github.com/pyroscope-io/pyroscope/pkg/exporter"
    27  	"github.com/pyroscope-io/pyroscope/pkg/health"
    28  	"github.com/pyroscope-io/pyroscope/pkg/parser"
    29  	"github.com/pyroscope-io/pyroscope/pkg/storage"
    30  	"github.com/pyroscope-io/pyroscope/pkg/storage/tree"
    31  	"github.com/pyroscope-io/pyroscope/pkg/testing"
    32  )
    33  
    34  func readTestdataFile(name string) string {
    35  	f, err := ioutil.ReadFile(name)
    36  	Expect(err).ToNot(HaveOccurred())
    37  	return string(f)
    38  }
    39  
    40  func jfrFromFile(name string) *bytes.Buffer {
    41  	b, err := ioutil.ReadFile(name)
    42  	Expect(err).ToNot(HaveOccurred())
    43  	b2, err := gzip.NewReader(bytes.NewBuffer(b))
    44  	Expect(err).ToNot(HaveOccurred())
    45  	b3, err := io.ReadAll(b2)
    46  	Expect(err).ToNot(HaveOccurred())
    47  	return bytes.NewBuffer(b3)
    48  }
    49  
    50  func jfrFormFromFiles(jfr, labels string) (*multipart.Writer, *bytes.Buffer) {
    51  	jfrGzip, err := ioutil.ReadFile(jfr)
    52  	Expect(err).ToNot(HaveOccurred())
    53  	jfrGzipReader, err := gzip.NewReader(bytes.NewBuffer(jfrGzip))
    54  	Expect(err).ToNot(HaveOccurred())
    55  	jfrBytes, err := ioutil.ReadAll(jfrGzipReader)
    56  	labelsJSONBytes, err := ioutil.ReadFile(labels)
    57  	Expect(err).ToNot(HaveOccurred())
    58  	bw := &bytes.Buffer{}
    59  	w := multipart.NewWriter(bw)
    60  	jw, err := w.CreateFormFile("jfr", "jfr")
    61  	Expect(err).ToNot(HaveOccurred())
    62  	_, err = jw.Write(jfrBytes)
    63  	Expect(err).ToNot(HaveOccurred())
    64  	lw, err := w.CreateFormFile("labels", "labels")
    65  	Expect(err).ToNot(HaveOccurred())
    66  	_, err = lw.Write(labelsJSONBytes)
    67  	Expect(err).ToNot(HaveOccurred())
    68  	err = w.Close()
    69  	Expect(err).ToNot(HaveOccurred())
    70  	return w, bw
    71  }
    72  
    73  func pprofFormFromFile(name string, cfg map[string]*tree.SampleTypeConfig) (*multipart.Writer, *bytes.Buffer) {
    74  	b, err := ioutil.ReadFile(name)
    75  	Expect(err).ToNot(HaveOccurred())
    76  	bw := &bytes.Buffer{}
    77  	w := multipart.NewWriter(bw)
    78  	fw, err := w.CreateFormFile("profile", "profile.pprof")
    79  	Expect(err).ToNot(HaveOccurred())
    80  	_, err = fw.Write(b)
    81  	Expect(err).ToNot(HaveOccurred())
    82  	if cfg != nil {
    83  		jsonb, err := json.Marshal(cfg)
    84  		Expect(err).ToNot(HaveOccurred())
    85  		jw, err := w.CreateFormFile("sample_type_config", "sample_type_config.json")
    86  		_, err = jw.Write(jsonb)
    87  		Expect(err).ToNot(HaveOccurred())
    88  	}
    89  	err = w.Close()
    90  	Expect(err).ToNot(HaveOccurred())
    91  	return w, bw
    92  }
    93  
    94  var _ = Describe("server", func() {
    95  	testing.WithConfig(func(cfg **config.Config) {
    96  		BeforeEach(func() {
    97  			(*cfg).Server.APIBindAddr = ":10043"
    98  		})
    99  
   100  		Describe("/ingest", func() {
   101  			var buf *bytes.Buffer
   102  			var format string
   103  			// var typeName string
   104  			var contentType string
   105  			var name string
   106  			var sleepDur time.Duration
   107  			var expectedKey string
   108  			headers := map[string]string{}
   109  			expectedTree := "foo;bar 2\nfoo;baz 3\n"
   110  
   111  			// this is an example of Shared Example pattern
   112  			//   see https://onsi.github.io/ginkgo/#shared-example-patterns
   113  			ItCorrectlyParsesIncomingData := func(expectedAppNames []string) {
   114  				It("correctly parses incoming data", func() {
   115  					done := make(chan interface{})
   116  					go func() {
   117  						defer GinkgoRecover()
   118  						defer close(done)
   119  
   120  						reg := prometheus.NewRegistry()
   121  
   122  						s, err := storage.New(storage.NewConfig(&(*cfg).Server), logrus.StandardLogger(), reg, new(health.Controller), storage.NoopApplicationMetadataService{})
   123  						Expect(err).ToNot(HaveOccurred())
   124  						defer s.Close()
   125  						e, _ := exporter.NewExporter(nil, nil)
   126  						c, _ := New(Config{
   127  							Configuration:           &(*cfg).Server,
   128  							Storage:                 s,
   129  							Ingester:                parser.New(logrus.StandardLogger(), s, e),
   130  							Logger:                  logrus.New(),
   131  							MetricsRegisterer:       prometheus.NewRegistry(),
   132  							ExportedMetricsRegistry: prometheus.NewRegistry(),
   133  							Notifier:                mockNotifier{},
   134  						})
   135  						h, _ := c.serverMux()
   136  						httpServer := httptest.NewServer(h)
   137  						defer httpServer.Close()
   138  
   139  						st := testing.ParseTime("2020-01-01-01:01:00")
   140  						et := testing.ParseTime("2020-01-01-01:01:10")
   141  
   142  						u, _ := url.Parse(httpServer.URL + "/ingest")
   143  						q := u.Query()
   144  						if name == "" {
   145  							name = "test.app{}"
   146  						}
   147  						q.Add("name", name)
   148  						q.Add("from", strconv.Itoa(int(st.Unix())))
   149  						q.Add("until", strconv.Itoa(int(et.Unix())))
   150  						if format != "" {
   151  							q.Add("format", format)
   152  						}
   153  						u.RawQuery = q.Encode()
   154  
   155  						fmt.Println(u.String())
   156  
   157  						req, err := http.NewRequest("POST", u.String(), buf)
   158  						Expect(err).ToNot(HaveOccurred())
   159  						if contentType == "" {
   160  							contentType = "text/plain"
   161  						}
   162  						for k, v := range headers {
   163  							req.Header.Set(k, v)
   164  						}
   165  						req.Header.Set("Content-Type", contentType)
   166  
   167  						res, err := http.DefaultClient.Do(req)
   168  						Expect(err).ToNot(HaveOccurred())
   169  						Expect(res.StatusCode).To(Equal(200))
   170  
   171  						if expectedKey == "" {
   172  							expectedKey = name
   173  						}
   174  						fq, err := flameql.ParseQuery(expectedKey)
   175  						Expect(err).ToNot(HaveOccurred())
   176  
   177  						_, exemplarSync := s.ExemplarsInternals()
   178  						exemplarSync()
   179  						time.Sleep(10 * time.Millisecond)
   180  						time.Sleep(sleepDur)
   181  
   182  						gOut, err := s.Get(context.TODO(), &storage.GetInput{
   183  							StartTime: st,
   184  							EndTime:   et,
   185  							Query:     fq,
   186  						})
   187  						Expect(err).ToNot(HaveOccurred())
   188  						if expectedTree != "" {
   189  							Expect(gOut).ToNot(BeNil())
   190  							Expect(gOut.Tree).ToNot(BeNil())
   191  
   192  							// Checks if only the expected app names were inserted
   193  							// Since we are comparing slices, let's sort them to have a deterministic order
   194  							sort.Strings(expectedAppNames)
   195  							Expect(s.GetAppNames(context.TODO())).To(Equal(expectedAppNames))
   196  
   197  							// Useful for debugging
   198  							fmt.Println("fq ", fq)
   199  							if gOut.Tree.String() != expectedTree {
   200  								fmt.Println(gOut.Tree.String())
   201  								fmt.Println(expectedTree)
   202  							}
   203  							// ioutil.WriteFile("/home/dmitry/pyroscope/pkg/server/testdata/jfr-"+typeName+".txt", []byte(gOut.Tree.String()), 0644)
   204  							Expect(gOut.Tree.String()).To(Equal(expectedTree))
   205  						} else {
   206  							Expect(gOut).To(BeNil())
   207  						}
   208  					}()
   209  					Eventually(done, 10).Should(BeClosed())
   210  				})
   211  			}
   212  
   213  			Context("default format", func() {
   214  				BeforeEach(func() {
   215  					buf = bytes.NewBuffer([]byte("foo;bar 2\nfoo;baz 3\n"))
   216  					format = ""
   217  					contentType = ""
   218  				})
   219  
   220  				ItCorrectlyParsesIncomingData([]string{`test.app`})
   221  			})
   222  
   223  			Context("lines format", func() {
   224  				BeforeEach(func() {
   225  					buf = bytes.NewBuffer([]byte("foo;bar\nfoo;bar\nfoo;baz\nfoo;baz\nfoo;baz\n"))
   226  					format = "lines"
   227  					contentType = ""
   228  				})
   229  
   230  				ItCorrectlyParsesIncomingData([]string{`test.app`})
   231  			})
   232  
   233  			Context("trie format", func() {
   234  				BeforeEach(func() {
   235  					buf = bytes.NewBuffer([]byte("\x00\x00\x01\x06foo;ba\x00\x02\x01r\x02\x00\x01z\x03\x00"))
   236  					format = "trie"
   237  					contentType = ""
   238  				})
   239  
   240  				ItCorrectlyParsesIncomingData([]string{`test.app`})
   241  			})
   242  
   243  			Context("tree format", func() {
   244  				BeforeEach(func() {
   245  					buf = bytes.NewBuffer([]byte("\x00\x00\x01\x03foo\x00\x02\x03bar\x02\x00\x03baz\x03\x00"))
   246  					format = "tree"
   247  					contentType = ""
   248  				})
   249  
   250  				ItCorrectlyParsesIncomingData([]string{`test.app`})
   251  			})
   252  
   253  			Context("trie format", func() {
   254  				BeforeEach(func() {
   255  					buf = bytes.NewBuffer([]byte("\x00\x00\x01\x06foo;ba\x00\x02\x01r\x02\x00\x01z\x03\x00"))
   256  					format = ""
   257  					contentType = "binary/octet-stream+trie"
   258  				})
   259  
   260  				ItCorrectlyParsesIncomingData([]string{`test.app`})
   261  			})
   262  
   263  			Context("tree format", func() {
   264  				BeforeEach(func() {
   265  					buf = bytes.NewBuffer([]byte("\x00\x00\x01\x03foo\x00\x02\x03bar\x02\x00\x03baz\x03\x00"))
   266  					format = ""
   267  					contentType = "binary/octet-stream+tree"
   268  				})
   269  
   270  				ItCorrectlyParsesIncomingData([]string{`test.app`})
   271  			})
   272  
   273  			Context("name with tags", func() {
   274  				BeforeEach(func() {
   275  					buf = bytes.NewBuffer([]byte("foo;bar 2\nfoo;baz 3\n"))
   276  					format = ""
   277  					contentType = ""
   278  					name = "test.app{foo=bar,baz=qux}"
   279  					expectedKey = `test.app{foo="bar", baz="qux"}`
   280  				})
   281  
   282  				ItCorrectlyParsesIncomingData([]string{`test.app`})
   283  			})
   284  
   285  			Context("jfr", func() {
   286  				BeforeEach(func() {
   287  					sleepDur = 100 * time.Millisecond
   288  					format = "jfr"
   289  				})
   290  				types := []string{
   291  					"cpu",
   292  					"wall",
   293  					"alloc_in_new_tlab_objects",
   294  					"alloc_in_new_tlab_bytes",
   295  					"alloc_outside_tlab_objects",
   296  					"alloc_outside_tlab_bytes",
   297  					"lock_count",
   298  					"lock_duration",
   299  				}
   300  				appNames := []string{}
   301  				for _, t := range types {
   302  					appNames = append(appNames, "test.app."+t)
   303  				}
   304  				Context("no labels", func() {
   305  					BeforeEach(func() {
   306  						name = "test.app{foo=bar,baz=qux}"
   307  						buf = jfrFromFile("./testdata/jfr/no_labels/jfr.bin.gz")
   308  					})
   309  					for _, t := range types {
   310  						func(t string) {
   311  							Context(t, func() {
   312  								BeforeEach(func() {
   313  									//typeName = t
   314  									expectedKey = `test.app.` + t + `{foo="bar", baz="qux"}`
   315  									expectedTree = readTestdataFile("./testdata/jfr/no_labels/jfr-" + t + ".txt")
   316  								})
   317  								ItCorrectlyParsesIncomingData(appNames)
   318  							})
   319  						}(t)
   320  					}
   321  				})
   322  				Context("with labels", func() {
   323  					BeforeEach(func() {
   324  						name = "test.app{foo=bar,baz=qux}"
   325  						var w *multipart.Writer
   326  						w, buf = jfrFormFromFiles("./testdata/jfr/with_labels/jfr.bin.gz", "./testdata/jfr/with_labels/labels.proto.bin")
   327  						contentType = w.FormDataContentType()
   328  					})
   329  					for _, t := range types {
   330  						func(t string) {
   331  							type contextID struct {
   332  								id  string
   333  								key string
   334  							}
   335  							cids := []contextID{
   336  								{id: "0", key: `test.app.` + t + `{foo="bar", baz="qux"}`},
   337  								{id: "1", key: `test.app.` + t + `{foo="bar", baz="qux", thread_name="pool-2-thread-8"}`},
   338  								{id: "2", key: `test.app.` + t + `{foo="bar", baz="qux", thread_name="pool-2-thread-8",profile_id="239239239239"}`},
   339  							}
   340  							for _, cid := range cids {
   341  								func(cid contextID) {
   342  									Context("contextID "+cid.id, func() {
   343  										Context(t, func() {
   344  											BeforeEach(func() {
   345  												// typeName = t
   346  												expectedKey = cid.key
   347  												expectedTree = readTestdataFile("./testdata/jfr/with_labels/" + cid.id + "/jfr-" + t + ".txt")
   348  											})
   349  											ItCorrectlyParsesIncomingData(appNames)
   350  										})
   351  									})
   352  								}(cid)
   353  							}
   354  							Context("non existent label query should return no data", func() {
   355  								Context(t, func() {
   356  									BeforeEach(func() {
   357  										expectedKey = `test.app.` + t + `{foo="bar"",baz="qux",non_existing="label"}`
   358  										expectedTree = ""
   359  									})
   360  									ItCorrectlyParsesIncomingData(appNames)
   361  								})
   362  							})
   363  						}(t)
   364  					}
   365  				})
   366  			})
   367  
   368  			Context("pprof", func() {
   369  				BeforeEach(func() {
   370  					format = ""
   371  					sleepDur = 100 * time.Millisecond // prof data is not updated immediately with pprof
   372  					name = "test.app{foo=bar,baz=qux}"
   373  					expectedKey = `test.app.cpu{foo="bar",baz="qux"}`
   374  					expectedTree = readTestdataFile("./testdata/pprof-string.txt")
   375  				})
   376  
   377  				Context("default sample type config", func() { // this is used in integrations
   378  					BeforeEach(func() {
   379  						var w *multipart.Writer
   380  						w, buf = pprofFormFromFile("../convert/testdata/cpu.pprof", nil)
   381  						contentType = w.FormDataContentType()
   382  					})
   383  
   384  					ItCorrectlyParsesIncomingData([]string{`test.app.cpu`})
   385  				})
   386  
   387  				Context("pprof format instead of content Type", func() { // this is described in docs
   388  					BeforeEach(func() {
   389  						format = "pprof"
   390  						buf = bytes.NewBuffer([]byte(readTestdataFile("../convert/testdata/cpu.pprof")))
   391  					})
   392  					ItCorrectlyParsesIncomingData([]string{`test.app.cpu`})
   393  				})
   394  
   395  				Context("custom sample type config", func() { // this is also described in docs
   396  					BeforeEach(func() {
   397  						var w *multipart.Writer
   398  						w, buf = pprofFormFromFile("../convert/testdata/cpu.pprof", map[string]*tree.SampleTypeConfig{
   399  							"samples": {
   400  								Units:       "samples",
   401  								DisplayName: "customName",
   402  							},
   403  						})
   404  						contentType = w.FormDataContentType()
   405  						expectedKey = `test.app.customName{foo="bar",baz="qux"}`
   406  					})
   407  
   408  					ItCorrectlyParsesIncomingData([]string{`test.app.customName`})
   409  				})
   410  
   411  				Context("non existent label query should return no data", func() {
   412  					BeforeEach(func() {
   413  						format = "pprof"
   414  						buf = bytes.NewBuffer([]byte(readTestdataFile("../convert/testdata/cpu.pprof")))
   415  						expectedKey = `test.app.cpu{foo="bar",baz="qux",non_existing="label"}`
   416  						expectedTree = ""
   417  					})
   418  					ItCorrectlyParsesIncomingData([]string{`test.app.cpu`})
   419  				})
   420  			})
   421  		})
   422  	})
   423  })