github.com/grafana/pyroscope@v1.18.0/cmd/profilecli/canary_exporter_probes.go (about) 1 package main 2 3 import ( 4 "bytes" 5 "context" 6 "encoding/json" 7 "fmt" 8 "io" 9 "math" 10 "net/http" 11 "net/url" 12 "strings" 13 "time" 14 15 "connectrpc.com/connect" 16 "github.com/go-kit/log/level" 17 "github.com/google/go-cmp/cmp" 18 gprofile "github.com/google/pprof/profile" 19 "github.com/google/uuid" 20 "github.com/pkg/errors" 21 22 profilesv1 "go.opentelemetry.io/proto/otlp/collector/profiles/v1development" 23 commonv1 "go.opentelemetry.io/proto/otlp/common/v1" 24 otlpprofiles "go.opentelemetry.io/proto/otlp/profiles/v1development" 25 resourcev1 "go.opentelemetry.io/proto/otlp/resource/v1" 26 "google.golang.org/protobuf/encoding/protojson" 27 "google.golang.org/protobuf/proto" 28 29 googlev1 "github.com/grafana/pyroscope/api/gen/proto/go/google/v1" 30 pushv1 "github.com/grafana/pyroscope/api/gen/proto/go/push/v1" 31 querierv1 "github.com/grafana/pyroscope/api/gen/proto/go/querier/v1" 32 typesv1 "github.com/grafana/pyroscope/api/gen/proto/go/types/v1" 33 "github.com/grafana/pyroscope/pkg/model" 34 "github.com/grafana/pyroscope/pkg/og/structs/flamebearer" 35 "github.com/grafana/pyroscope/pkg/pprof/testhelper" 36 ) 37 38 const ( 39 profileTypeID = "deadmans_switch:made_up:profilos:made_up:profilos" 40 otlpProfileTypeID = "otlp_test:otlp_test:count:otlp_test:samples" 41 canaryExporterServiceName = "pyroscope-canary-exporter" 42 ) 43 44 func (ce *canaryExporter) testIngestProfile(ctx context.Context, now time.Time) error { 45 p := testhelper.NewProfileBuilder(now.UnixNano()) 46 p.Labels = p.Labels[:0] 47 p.CustomProfile("deadmans_switch", "made_up", "profilos", "made_up", "profilos") 48 p.WithLabels( 49 "service_name", canaryExporterServiceName, 50 "job", "canary-exporter", 51 "instance", ce.hostname, 52 ) 53 p.UUID = uuid.New() 54 p.ForStacktraceString("func1", "func2").AddSamples(10) 55 p.ForStacktraceString("func1").AddSamples(20) 56 57 // for testing the span selection 58 p.StringTable = append(p.StringTable, "profile_id", "00000bac2a5ab0c7") 59 p.Sample[1].Label = []*googlev1.Label{{Key: int64(len(p.StringTable) - 2), Str: int64(len(p.StringTable) - 1)}} 60 61 data, err := p.MarshalVT() 62 if err != nil { 63 return err 64 } 65 66 if _, err := ce.params.pusherClient().Push(ctx, connect.NewRequest(&pushv1.PushRequest{ 67 Series: []*pushv1.RawProfileSeries{ 68 { 69 Labels: p.Labels, 70 Samples: []*pushv1.RawSample{{ 71 ID: p.UUID.String(), 72 RawProfile: data, 73 }}, 74 }, 75 }, 76 })); err != nil { 77 return err 78 } 79 80 level.Info(logger).Log("msg", "successfully ingested profile", "uuid", p.UUID.String()) 81 return nil 82 } 83 84 // generateOTLPProfile creates an OTLP profile with the specified ingestion method label 85 86 func (ce *canaryExporter) generateOTLPProfile(now time.Time, ingestionMethod string) *profilesv1.ExportProfilesServiceRequest { 87 // Sanitize the ingestion method label value by replacing "/" with "_" 88 sanitizedMethod := strings.ReplaceAll(ingestionMethod, "/", "_") 89 90 // Create the profile dictionary with custom profile type similar to pprof probe 91 dictionary := &otlpprofiles.ProfilesDictionary{ 92 StringTable: []string{ 93 "", // 0: empty string 94 "otlp_test", // 1 95 "samples", // 2 96 "count", // 3 97 "func1", // 4 98 "func2", // 5 99 "ingestion_method", // 6 100 sanitizedMethod, // 7 101 }, 102 MappingTable: []*otlpprofiles.Mapping{ 103 {}, // 0: empty mapping (required null entry) 104 }, 105 FunctionTable: []*otlpprofiles.Function{ 106 {NameStrindex: 0}, // 0: empty 107 {NameStrindex: 4}, // 1: func1 108 {NameStrindex: 5}, // 2: func2 109 }, 110 LocationTable: []*otlpprofiles.Location{ 111 {Lines: []*otlpprofiles.Line{{FunctionIndex: 1}}}, // 0: func1 112 {Lines: []*otlpprofiles.Line{{FunctionIndex: 2}}}, // 1: func2 113 }, 114 StackTable: []*otlpprofiles.Stack{ 115 {LocationIndices: []int32{}}, // 0: empty (required null entry) 116 {LocationIndices: []int32{1, 0}}, // 1: func2, func1 stack 117 {LocationIndices: []int32{0}}, // 2: func1 stack 118 }, 119 } 120 121 // Create profile with two samples matching the original pprof profile 122 profile := &otlpprofiles.Profile{ 123 TimeUnixNano: uint64(now.UnixNano()), 124 DurationNano: 0, 125 Period: 1, 126 SampleType: &otlpprofiles.ValueType{ 127 TypeStrindex: 1, // "otlp_test" 128 UnitStrindex: 3, // "count" 129 }, 130 PeriodType: &otlpprofiles.ValueType{ 131 TypeStrindex: 1, // "otlp_test" 132 UnitStrindex: 2, // "samples" 133 }, 134 Samples: []*otlpprofiles.Sample{ 135 { 136 // func1>func2 with value 10 137 StackIndex: 1, // stack_table[1] 138 Values: []int64{10}, 139 }, 140 { 141 // func1 with value 20 142 StackIndex: 2, // stack_table[2] 143 Values: []int64{20}, 144 }, 145 }, 146 } 147 148 // Create the resource attributes 149 resourceAttrs := []*commonv1.KeyValue{ 150 { 151 Key: "service.name", 152 Value: &commonv1.AnyValue{Value: &commonv1.AnyValue_StringValue{StringValue: canaryExporterServiceName}}, 153 }, 154 { 155 Key: "job", 156 Value: &commonv1.AnyValue{Value: &commonv1.AnyValue_StringValue{StringValue: "canary-exporter"}}, 157 }, 158 { 159 Key: "instance", 160 Value: &commonv1.AnyValue{Value: &commonv1.AnyValue_StringValue{StringValue: ce.hostname}}, 161 }, 162 { 163 Key: "ingestion_method", 164 Value: &commonv1.AnyValue{Value: &commonv1.AnyValue_StringValue{StringValue: sanitizedMethod}}, 165 }, 166 } 167 168 // Create the OTLP request 169 req := &profilesv1.ExportProfilesServiceRequest{ 170 Dictionary: dictionary, 171 ResourceProfiles: []*otlpprofiles.ResourceProfiles{ 172 { 173 Resource: &resourcev1.Resource{ 174 Attributes: resourceAttrs, 175 }, 176 ScopeProfiles: []*otlpprofiles.ScopeProfiles{ 177 { 178 Scope: &commonv1.InstrumentationScope{ 179 Name: "pyroscope-canary-exporter", 180 }, 181 Profiles: []*otlpprofiles.Profile{profile}, 182 }, 183 }, 184 }, 185 }, 186 } 187 188 return req 189 } 190 191 /* 192 func (ce *canaryExporter) testIngestOTLPGrpc(ctx context.Context, now time.Time) error { 193 // Generate the OTLP profile with the appropriate ingestion method label 194 req := ce.generateOTLPProfile(now, "otlp/grpc") 195 196 // Parse URL to extract host and port 197 parsedURL, err := url.Parse(ce.params.URL) 198 if err != nil { 199 return fmt.Errorf("failed to parse URL: %w", err) 200 } 201 port := parsedURL.Port() 202 if port == "" && parsedURL.Scheme == "http" { 203 port = "80" 204 } else if port == "" && parsedURL.Scheme == "https" { 205 port = "443" 206 } else { 207 port = "4317" // default OTLP gRPC port 208 } 209 grpcAddr := fmt.Sprintf("%s:%s", parsedURL.Hostname(), port) 210 211 // Create gRPC connection 212 conn, err := grpc.NewClient(grpcAddr, 213 grpc.WithTransportCredentials(insecure.NewCredentials())) 214 if err != nil { 215 return fmt.Errorf("failed to connect to gRPC server: %w", err) 216 } 217 defer conn.Close() 218 219 // Create OTLP profiles service client 220 client := profilesv1.NewProfilesServiceClient(conn) 221 222 // Send the profile 223 _, err = client.Export(ctx, req) 224 if err != nil { 225 return fmt.Errorf("failed to export OTLP profile via gRPC: %w", err) 226 } 227 228 level.Info(logger).Log("msg", "successfully ingested OTLP profile via gRPC") 229 return nil 230 } 231 */ 232 233 func (ce *canaryExporter) testIngestOTLPHttpJson(ctx context.Context, now time.Time) error { 234 // Generate the OTLP profile with the appropriate ingestion method label 235 req := ce.generateOTLPProfile(now, "otlp/http/json") 236 237 // Marshal to JSON 238 jsonData, err := protojson.Marshal(req) 239 if err != nil { 240 return fmt.Errorf("failed to marshal OTLP profile to JSON: %w", err) 241 } 242 243 // Create HTTP request using the instrumented client 244 url := ce.params.URL + "/v1development/profiles" 245 httpReq, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewReader(jsonData)) 246 if err != nil { 247 return fmt.Errorf("failed to create HTTP request: %w", err) 248 } 249 httpReq.Header.Set("Content-Type", "application/json") 250 251 // Send the request using the instrumented client (ce.params.client is set by doTrace) 252 resp, err := ce.params.client.Do(httpReq) 253 if err != nil { 254 return fmt.Errorf("failed to send HTTP request: %w", err) 255 } 256 defer resp.Body.Close() 257 258 // Read the body to ensure the transport is fully traced 259 body, err := io.ReadAll(resp.Body) 260 if err != nil { 261 return fmt.Errorf("failed to read response body: %w", err) 262 } 263 264 // Check response status 265 if resp.StatusCode != http.StatusOK { 266 return fmt.Errorf("unexpected status code %d: %s", resp.StatusCode, string(body)) 267 } 268 269 level.Info(logger).Log("msg", "successfully ingested OTLP profile via HTTP/JSON") 270 return nil 271 } 272 273 func (ce *canaryExporter) testIngestOTLPHttpProtobuf(ctx context.Context, now time.Time) error { 274 // Generate the OTLP profile with the appropriate ingestion method label 275 req := ce.generateOTLPProfile(now, "otlp/http/protobuf") 276 277 // Marshal to protobuf 278 protoData, err := proto.Marshal(req) 279 if err != nil { 280 return fmt.Errorf("failed to marshal OTLP profile to protobuf: %w", err) 281 } 282 283 // Create HTTP request using the instrumented client 284 url := ce.params.URL + "/v1development/profiles" 285 httpReq, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewReader(protoData)) 286 if err != nil { 287 return fmt.Errorf("failed to create HTTP request: %w", err) 288 } 289 httpReq.Header.Set("Content-Type", "application/x-protobuf") 290 291 // Send the request using the instrumented client (ce.params.client is set by doTrace) 292 resp, err := ce.params.client.Do(httpReq) 293 if err != nil { 294 return fmt.Errorf("failed to send HTTP request: %w", err) 295 } 296 defer resp.Body.Close() 297 298 // Read the body to ensure the transport is fully traced 299 body, err := io.ReadAll(resp.Body) 300 if err != nil { 301 return fmt.Errorf("failed to read response body: %w", err) 302 } 303 304 // Check response status 305 if resp.StatusCode != http.StatusOK { 306 return fmt.Errorf("unexpected status code %d: %s", resp.StatusCode, string(body)) 307 } 308 309 level.Info(logger).Log("msg", "successfully ingested OTLP profile via HTTP/Protobuf") 310 return nil 311 } 312 func (ce *canaryExporter) testSelectMergeProfile(ctx context.Context, now time.Time) error { 313 respQuery, err := ce.params.queryClient().SelectMergeProfile(ctx, connect.NewRequest(&querierv1.SelectMergeProfileRequest{ 314 Start: now.UnixMilli(), 315 End: now.Add(5 * time.Second).UnixMilli(), 316 LabelSelector: ce.createLabelSelector(), 317 ProfileTypeID: profileTypeID, 318 })) 319 if err != nil { 320 return err 321 } 322 323 buf, err := respQuery.Msg.MarshalVT() 324 if err != nil { 325 return errors.Wrap(err, "failed to marshal protobuf") 326 } 327 328 gp, err := gprofile.Parse(bytes.NewReader(buf)) 329 if err != nil { 330 return errors.Wrap(err, "failed to parse profile") 331 } 332 333 expected := map[string]int64{ 334 "func1>func2": 10, 335 "func1": 20, 336 } 337 actual := make(map[string]int64) 338 339 var sb strings.Builder 340 for _, s := range gp.Sample { 341 sb.Reset() 342 for _, loc := range s.Location { 343 if sb.Len() != 0 { 344 _, err := sb.WriteRune('>') 345 if err != nil { 346 return err 347 } 348 } 349 for _, line := range loc.Line { 350 _, err := sb.WriteString(line.Function.Name) 351 if err != nil { 352 return err 353 } 354 } 355 } 356 actual[sb.String()] = actual[sb.String()] + s.Value[0] 357 } 358 359 if diff := cmp.Diff(expected, actual); diff != "" { 360 return fmt.Errorf("query mismatch (-expected, +actual):\n%s", diff) 361 } 362 363 return nil 364 } 365 366 func (ce *canaryExporter) testSelectMergeOTLPProfile(ctx context.Context, now time.Time) error { 367 // Query specifically for OTLP gRPC ingested profiles using the custom profile type 368 //labelSelector := fmt.Sprintf(`{service_name="%s", job="canary-exporter", instance="%s"}`, canaryExporterServiceName, ce.hostname) 369 370 respQuery, err := ce.params.queryClient().SelectMergeProfile(ctx, connect.NewRequest(&querierv1.SelectMergeProfileRequest{ 371 Start: now.UnixMilli(), 372 End: now.Add(5 * time.Second).UnixMilli(), 373 LabelSelector: ce.createLabelSelector(), 374 ProfileTypeID: otlpProfileTypeID, 375 })) 376 if err != nil { 377 return fmt.Errorf("failed to query OTLP profile: %w", err) 378 } 379 380 buf, err := respQuery.Msg.MarshalVT() 381 if err != nil { 382 return errors.Wrap(err, "failed to marshal protobuf") 383 } 384 385 gp, err := gprofile.Parse(bytes.NewReader(buf)) 386 if err != nil { 387 return errors.Wrap(err, "failed to parse profile") 388 } 389 390 // Verify the expected stacktraces from the OTLP profile 391 expected := map[string]int64{ 392 "func2>func1": 20, // 10 samples from each of the 2 ingestion methods 393 "func1": 40, // 20 samples * 2 394 } 395 actual := make(map[string]int64) 396 397 var sb strings.Builder 398 for _, s := range gp.Sample { 399 sb.Reset() 400 for _, loc := range s.Location { 401 if sb.Len() != 0 { 402 _, err := sb.WriteRune('>') 403 if err != nil { 404 return err 405 } 406 } 407 for _, line := range loc.Line { 408 _, err := sb.WriteString(line.Function.Name) 409 if err != nil { 410 return err 411 } 412 } 413 } 414 actual[sb.String()] = actual[sb.String()] + s.Value[0] 415 } 416 417 if diff := cmp.Diff(expected, actual); diff != "" { 418 return fmt.Errorf("OTLP profile query mismatch (-expected, +actual):\n%s", diff) 419 } 420 421 level.Info(logger).Log("msg", "successfully queried OTLP profile via gRPC") 422 return nil 423 } 424 425 func (ce *canaryExporter) testProfileTypes(ctx context.Context, now time.Time) error { 426 respQuery, err := ce.params.queryClient().ProfileTypes(ctx, connect.NewRequest(&querierv1.ProfileTypesRequest{ 427 Start: now.UnixMilli(), 428 End: now.Add(5 * time.Second).UnixMilli(), 429 })) 430 if err != nil { 431 return err 432 } 433 434 for _, pt := range respQuery.Msg.ProfileTypes { 435 if pt.ID == profileTypeID { 436 level.Info(logger).Log("msg", "found expected profile type", "id", pt.ID) 437 return nil 438 } 439 } 440 441 return fmt.Errorf("expected profile type %s not found", profileTypeID) 442 } 443 444 func (ce *canaryExporter) testSeries(ctx context.Context, now time.Time) error { 445 respQuery, err := ce.params.queryClient().Series(ctx, connect.NewRequest(&querierv1.SeriesRequest{ 446 Start: now.UnixMilli(), 447 End: now.Add(5 * time.Second).UnixMilli(), 448 LabelNames: []string{model.LabelNameServiceName, model.LabelNameProfileType}, 449 })) 450 451 if err != nil { 452 return err 453 } 454 labelSets := respQuery.Msg.LabelsSet 455 456 if len(labelSets) < 1 { 457 return fmt.Errorf("expected at least 1 label set, got %d", len(labelSets)) 458 } 459 460 for _, ls := range labelSets { 461 labels := model.Labels(ls.Labels) 462 463 serviceName := labels.Get(model.LabelNameServiceName) 464 if serviceName == "" { 465 return fmt.Errorf("expected service_name label to be set") 466 } 467 if serviceName != canaryExporterServiceName { 468 continue 469 } 470 profileType := labels.Get(model.LabelNameProfileType) 471 if profileType == "" { 472 return fmt.Errorf("expected profile_type label to be set") 473 } 474 if profileType != profileTypeID { 475 continue 476 } 477 return nil // found the expected series 478 } 479 480 return fmt.Errorf("expected series with service_name=%s and profile_type=%s not found", canaryExporterServiceName, profileTypeID) 481 } 482 483 func (ce *canaryExporter) testLabelNames(ctx context.Context, now time.Time) error { 484 respQuery, err := ce.params.queryClient().LabelNames(ctx, connect.NewRequest(&typesv1.LabelNamesRequest{ 485 Start: now.UnixMilli(), 486 End: now.Add(5 * time.Second).UnixMilli(), 487 // we have to pass this matcher to skip the tenant-wide index in v2 which is not ready until after compaction 488 Matchers: []string{fmt.Sprintf(`{service_name="%s"}`, canaryExporterServiceName)}, 489 })) 490 491 if err != nil { 492 return err 493 } 494 495 labelNames := respQuery.Msg.Names 496 497 expectedLabelNames := []string{ 498 model.LabelNameProfileName, 499 model.LabelNamePeriodType, 500 model.LabelNamePeriodUnit, 501 model.LabelNameProfileType, 502 model.LabelNameServiceNamePrivate, 503 model.LabelNameType, 504 model.LabelNameUnit, 505 //"service.name", // todo: return once write path is ready for utf8 labels 506 model.LabelNameServiceName, 507 } 508 509 // Use map as set for O(1) lookups 510 labelNamesSet := make(map[string]struct{}, len(labelNames)) 511 for _, label := range labelNames { 512 labelNamesSet[label] = struct{}{} 513 } 514 515 missingLabels := []string{} 516 for _, expectedLabel := range expectedLabelNames { 517 if _, exists := labelNamesSet[expectedLabel]; !exists { 518 missingLabels = append(missingLabels, expectedLabel) 519 } 520 } 521 if len(missingLabels) > 0 { 522 return fmt.Errorf("missing expected labels: %s", missingLabels) 523 } 524 525 return nil 526 } 527 528 func (ce *canaryExporter) testLabelValues(ctx context.Context, now time.Time) error { 529 respQuery, err := ce.params.queryClient().LabelValues(ctx, connect.NewRequest(&typesv1.LabelValuesRequest{ 530 Start: now.UnixMilli(), 531 End: now.Add(5 * time.Second).UnixMilli(), 532 Name: model.LabelNameServiceName, 533 // we have to pass this matcher to skip the tenant-wide index in v2 which is not ready until after compaction 534 Matchers: []string{fmt.Sprintf(`{service_name="%s"}`, canaryExporterServiceName)}, 535 })) 536 537 if err != nil { 538 return err 539 } 540 541 if len(respQuery.Msg.Names) != 1 { 542 return fmt.Errorf("expected 1 label value, got %d", len(respQuery.Msg.Names)) 543 } 544 545 serviceName := respQuery.Msg.Names[0] 546 547 if serviceName != canaryExporterServiceName { 548 return fmt.Errorf("expected service_name label to be %s, got %s", canaryExporterServiceName, serviceName) 549 } 550 551 return nil 552 } 553 554 func (ce *canaryExporter) testSelectSeries(ctx context.Context, now time.Time) error { 555 respQuery, err := ce.params.queryClient().SelectSeries(ctx, connect.NewRequest(&querierv1.SelectSeriesRequest{ 556 Start: now.UnixMilli(), 557 End: now.Add(5 * time.Second).UnixMilli(), 558 Step: 1000, 559 LabelSelector: ce.createLabelSelector(), 560 ProfileTypeID: profileTypeID, 561 GroupBy: []string{model.LabelNameServiceName}, 562 })) 563 564 if err != nil { 565 return err 566 } 567 568 if len(respQuery.Msg.Series) != 1 { 569 return fmt.Errorf("expected 1 series, got %d", len(respQuery.Msg.Series)) 570 } 571 572 series := respQuery.Msg.Series[0] 573 574 if len(series.Points) != 1 { 575 return fmt.Errorf("expected 2 points, got %d", len(series.Points)) 576 } 577 578 labels := model.Labels(series.Labels) 579 580 if len(labels) != 1 { 581 return fmt.Errorf("expected 1 labels, got %d", len(labels)) 582 } 583 584 serviceName := labels.Get(model.LabelNameServiceName) 585 if serviceName == "" { 586 return fmt.Errorf("expected service_name label to be set") 587 } 588 if serviceName != canaryExporterServiceName { 589 return fmt.Errorf("expected service_name label to be %s, got %s", canaryExporterServiceName, serviceName) 590 } 591 592 return nil 593 } 594 595 func (ce *canaryExporter) testSelectMergeStacktraces(ctx context.Context, now time.Time) error { 596 respQuery, err := ce.params.queryClient().SelectMergeStacktraces(ctx, connect.NewRequest(&querierv1.SelectMergeStacktracesRequest{ 597 Start: now.UnixMilli(), 598 End: now.Add(5 * time.Second).UnixMilli(), 599 LabelSelector: ce.createLabelSelector(), 600 ProfileTypeID: profileTypeID, 601 })) 602 603 if err != nil { 604 return err 605 } 606 607 flamegraph := respQuery.Msg.Flamegraph 608 609 if len(flamegraph.Names) != 3 { 610 return fmt.Errorf("expected 3 names in flamegraph, got %d", len(flamegraph.Names)) 611 } 612 613 if len(flamegraph.Levels) != 3 { 614 return fmt.Errorf("expected 3 levels in flamegraph, got %d", len(flamegraph.Levels)) 615 } 616 617 return nil 618 } 619 620 func (ce *canaryExporter) testSelectMergeSpanProfile(ctx context.Context, now time.Time) error { 621 respQuery, err := ce.params.queryClient().SelectMergeSpanProfile(ctx, connect.NewRequest(&querierv1.SelectMergeSpanProfileRequest{ 622 Start: now.UnixMilli(), 623 End: now.Add(5 * time.Second).UnixMilli(), 624 LabelSelector: ce.createLabelSelector(), 625 ProfileTypeID: profileTypeID, 626 SpanSelector: []string{"00000bac2a5ab0c7"}, 627 })) 628 629 if err != nil { 630 return err 631 } 632 633 flamegraph := respQuery.Msg.Flamegraph 634 635 if flamegraph == nil { 636 return fmt.Errorf("expected flamegraph to be set") 637 } 638 639 if len(flamegraph.Names) != 2 { 640 return fmt.Errorf("expected 2 names in flamegraph, got %d", len(flamegraph.Names)) 641 } 642 643 if len(flamegraph.Levels) != 2 { 644 return fmt.Errorf("expected 2 levels in flamegraph, got %d", len(flamegraph.Levels)) 645 } 646 647 return nil 648 } 649 650 func (ce *canaryExporter) testRender(ctx context.Context, now time.Time) error { 651 query := profileTypeID + ce.createLabelSelector() 652 startTime := now.UnixMilli() 653 endTime := now.Add(5 * time.Second).UnixMilli() 654 655 baseURL, err := url.Parse(ce.params.URL) 656 if err != nil { 657 return err 658 } 659 baseURL.Path = "/pyroscope/render" 660 661 params := url.Values{} 662 params.Add("query", query) 663 params.Add("from", fmt.Sprintf("%d", startTime)) 664 params.Add("until", fmt.Sprintf("%d", endTime)) 665 666 baseURL.RawQuery = params.Encode() 667 reqURL := baseURL.String() 668 level.Debug(logger).Log("msg", "requesting render", "url", reqURL) 669 670 req, err := http.NewRequestWithContext(ctx, "GET", reqURL, nil) 671 if err != nil { 672 return err 673 } 674 675 resp, err := ce.params.httpClient().Do(req) 676 if err != nil { 677 return err 678 } 679 defer resp.Body.Close() 680 681 if resp.StatusCode != http.StatusOK { 682 body, _ := io.ReadAll(resp.Body) 683 return fmt.Errorf("unexpected status code: %d, body: %s", resp.StatusCode, string(body)) 684 } 685 686 var flamebearerProfile flamebearer.FlamebearerProfile 687 if err := json.NewDecoder(resp.Body).Decode(&flamebearerProfile); err != nil { 688 return err 689 } 690 691 if len(flamebearerProfile.Flamebearer.Names) != 3 { 692 return fmt.Errorf("expected 3 names in flamegraph, got %d", len(flamebearerProfile.Flamebearer.Names)) 693 } 694 695 if len(flamebearerProfile.Flamebearer.Levels) != 3 { 696 return fmt.Errorf("expected 3 levels in flamegraph, got %d", len(flamebearerProfile.Flamebearer.Levels)) 697 } 698 699 return nil 700 } 701 702 func (ce *canaryExporter) testRenderDiff(ctx context.Context, now time.Time) error { 703 query := profileTypeID + ce.createLabelSelector() 704 startTime := now.UnixMilli() 705 endTime := now.Add(5 * time.Second).UnixMilli() 706 707 baseURL, err := url.Parse(ce.params.URL) 708 if err != nil { 709 return err 710 } 711 baseURL.Path = "/pyroscope/render-diff" 712 713 params := url.Values{} 714 params.Add("leftQuery", query) 715 params.Add("leftFrom", fmt.Sprintf("%d", startTime)) 716 params.Add("leftUntil", fmt.Sprintf("%d", endTime)) 717 params.Add("rightQuery", query) 718 params.Add("rightFrom", fmt.Sprintf("%d", startTime)) 719 params.Add("rightUntil", fmt.Sprintf("%d", endTime)) 720 721 baseURL.RawQuery = params.Encode() 722 reqURL := baseURL.String() 723 level.Debug(logger).Log("msg", "requesting diff", "url", reqURL) 724 725 req, err := http.NewRequestWithContext(ctx, "GET", reqURL, nil) 726 if err != nil { 727 return err 728 } 729 730 resp, err := ce.params.httpClient().Do(req) 731 if err != nil { 732 return err 733 } 734 defer resp.Body.Close() 735 736 if resp.StatusCode != http.StatusOK { 737 body, _ := io.ReadAll(resp.Body) 738 return fmt.Errorf("unexpected status code: %d, body: %s", resp.StatusCode, string(body)) 739 } 740 741 var flamebearerProfile flamebearer.FlamebearerProfile 742 if err := json.NewDecoder(resp.Body).Decode(&flamebearerProfile); err != nil { 743 return err 744 } 745 746 if len(flamebearerProfile.Flamebearer.Names) != 3 { 747 return fmt.Errorf("expected 3 names in flamegraph, got %d", len(flamebearerProfile.Flamebearer.Names)) 748 } 749 750 if len(flamebearerProfile.Flamebearer.Levels) != 3 { 751 return fmt.Errorf("expected 3 levels in flamegraph, got %d", len(flamebearerProfile.Flamebearer.Levels)) 752 } 753 754 return nil 755 } 756 757 func (ce *canaryExporter) testGetProfileStats(ctx context.Context, now time.Time) error { 758 resp, err := ce.params.queryClient().GetProfileStats(ctx, connect.NewRequest(&typesv1.GetProfileStatsRequest{})) 759 760 if err != nil { 761 return err 762 } 763 764 if !resp.Msg.DataIngested { 765 return fmt.Errorf("expected data to be ingested") 766 } 767 768 if resp.Msg.OldestProfileTime == math.MinInt64 { 769 return fmt.Errorf("expected oldest profile time to be set") 770 } 771 772 if resp.Msg.NewestProfileTime == math.MaxInt64 { 773 return fmt.Errorf("expected newest profile time to be set") 774 } 775 776 return nil 777 } 778 779 func (ce *canaryExporter) createLabelSelector() string { 780 return fmt.Sprintf(`{service_name="%s", job="canary-exporter", instance="%s"}`, canaryExporterServiceName, ce.hostname) 781 }