Skip to content

Commit f2aa2fb

Browse files
authored
fix: Handle Speedscope sample types in ingestion (#4568)
1 parent 1daaeb5 commit f2aa2fb

File tree

4 files changed

+147
-1
lines changed

4 files changed

+147
-1
lines changed

pkg/ingester/pyroscope/ingest_adapter.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,14 @@ func convertMetadata(pi *storage.PutInput) (metricName, stType, stUnit, app stri
283283
metricName = "exceptions"
284284
stType = stTypeSamples
285285
stUnit = stUnitCount
286+
case "seconds", "nanoseconds", "microseconds", "milliseconds":
287+
metricName = metricWall
288+
stType = stTypeSamples
289+
stUnit = stUnitCount
290+
case "bytes":
291+
metricName = metricMemory
292+
stType = stTypeSamples
293+
stUnit = stUnitBytes
286294
default:
287295
err = fmt.Errorf("unknown profile type: %s", stType)
288296
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
{
2+
"exporter": "[email protected]",
3+
"$schema": "https://www.speedscope.app/file-format-schema.json",
4+
"name": "Two Samples",
5+
"activeProfileIndex": 1,
6+
"profiles": [
7+
{
8+
"type": "sampled",
9+
"name": "one",
10+
"unit": "bytes",
11+
"startValue": 0,
12+
"endValue": 14,
13+
"samples": [
14+
[0, 1, 2],
15+
[0, 1, 2],
16+
[0, 1, 3],
17+
[0, 1, 2],
18+
[0, 1]
19+
],
20+
"weights": [1, 1, 4, 3, 5]
21+
},
22+
{
23+
"type": "sampled",
24+
"name": "two",
25+
"unit": "bytes",
26+
"startValue": 0,
27+
"endValue": 14,
28+
"samples": [
29+
[0, 1, 2],
30+
[0, 1, 2],
31+
[0, 1, 3],
32+
[0, 1, 2],
33+
[0, 1]
34+
],
35+
"weights": [1, 1, 4, 3, 5]
36+
}
37+
],
38+
"shared": {
39+
"frames": [
40+
{ "name": "a" },
41+
{ "name": "b" },
42+
{ "name": "c" },
43+
{ "name": "d" }
44+
]
45+
}
46+
}

pkg/test/integration/helper.go

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -244,7 +244,7 @@ func (p *PyroscopeTest) NewRequestBuilder(t *testing.T) *RequestBuilder {
244244
}
245245

246246
func (p *PyroscopeTest) TempAppName() string {
247-
return fmt.Sprintf("pprof.integration.%d",
247+
return fmt.Sprintf("pprof-integration-%d",
248248
rand.Uint64())
249249
}
250250

@@ -362,6 +362,18 @@ func (b *RequestBuilder) IngestJFRRequestBody(jfr []byte, labels []byte) *http.R
362362
return req
363363
}
364364

365+
func (b *RequestBuilder) IngestSpeedscopeRequest(speedscopePath string) *http.Request {
366+
speedscopeData, err := os.ReadFile(speedscopePath)
367+
require.NoError(b.t, err)
368+
369+
url := b.url + "/ingest?name=" + b.AppName + "&format=speedscope"
370+
req, err := http.NewRequest("POST", url, bytes.NewReader(speedscopeData))
371+
require.NoError(b.t, err)
372+
req.Header.Set("Content-Type", "application/json")
373+
374+
return req
375+
}
376+
365377
func (b *RequestBuilder) Render(metric string) *flamebearer.FlamebearerProfile {
366378
queryURL := b.url + "/pyroscope/render?query=" + createRenderQuery(metric, b.AppName) + "&from=946656000&until=now&format=collapsed"
367379
fmt.Println(queryURL)
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
package integration
2+
3+
import (
4+
"testing"
5+
6+
"connectrpc.com/connect"
7+
"github.com/stretchr/testify/assert"
8+
"github.com/stretchr/testify/require"
9+
10+
profilev1 "github.com/grafana/pyroscope/api/gen/proto/go/google/v1"
11+
)
12+
13+
type speedscopeTestDataStruct struct {
14+
name string
15+
speedscopeFile string
16+
expectStatus int
17+
expectedMetrics []expectedMetric
18+
}
19+
20+
const (
21+
testdataDirSpeedscope = repoRoot + "pkg/og/convert/speedscope/testdata"
22+
)
23+
24+
var (
25+
speedscopeTestData = []speedscopeTestDataStruct{
26+
{
27+
name: "single profile evented",
28+
speedscopeFile: testdataDirSpeedscope + "/simple.speedscope.json",
29+
expectStatus: 200,
30+
expectedMetrics: []expectedMetric{
31+
// The difference between the metric name here and in the other test is a quirk in
32+
// how the speedscope parsing logic. Only multi profile uploads will
33+
// append the unit to the metric name which is parsed differently downstream.
34+
{"process_cpu:cpu:nanoseconds:cpu:nanoseconds", nil, 0},
35+
},
36+
},
37+
{
38+
name: "multi profile sampled",
39+
speedscopeFile: testdataDirSpeedscope + "/two-sampled.speedscope.json",
40+
expectStatus: 200,
41+
expectedMetrics: []expectedMetric{
42+
{"wall:wall:nanoseconds:cpu:nanoseconds", nil, 0},
43+
},
44+
},
45+
{
46+
name: "multi profile samples bytes units",
47+
speedscopeFile: testdataDirSpeedscope + "/two-sampled-bytes.speedscope.json",
48+
expectStatus: 200,
49+
expectedMetrics: []expectedMetric{
50+
{"memory:samples:bytes::", nil, 0},
51+
},
52+
},
53+
}
54+
)
55+
56+
func TestIngestSpeedscope(t *testing.T) {
57+
EachPyroscopeTest(t, func(p *PyroscopeTest, t *testing.T) {
58+
for _, td := range speedscopeTestData {
59+
t.Run(td.name, func(t *testing.T) {
60+
rb := p.NewRequestBuilder(t)
61+
req := rb.IngestSpeedscopeRequest(td.speedscopeFile)
62+
p.Ingest(t, req, td.expectStatus)
63+
64+
if td.expectStatus == 200 {
65+
for _, metric := range td.expectedMetrics {
66+
rb.Render(metric.name)
67+
profile := rb.SelectMergeProfile(metric.name, metric.query)
68+
assertSpeedscopeProfile(t, profile)
69+
}
70+
}
71+
})
72+
}
73+
})
74+
}
75+
76+
func assertSpeedscopeProfile(t *testing.T, resp *connect.Response[profilev1.Profile]) {
77+
assert.Equal(t, 1, len(resp.Msg.SampleType), "SampleType should be set")
78+
require.Greater(t, len(resp.Msg.Sample), 0, "Profile should contain samples")
79+
assert.Greater(t, resp.Msg.Sample[0].Value[0], int64(0), "Sample value should be positive")
80+
}

0 commit comments

Comments
 (0)