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 })