Skip to content

Commit 71187cc

Browse files
chore(otel): support ingesting offcpu (#3875)
--------- Co-authored-by: Marc Sanmiquel <[email protected]>
1 parent 1a57c65 commit 71187cc

File tree

10 files changed

+339922
-94
lines changed

10 files changed

+339922
-94
lines changed

examples/tracing/golang-push/go.work.sum

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ go.opentelemetry.io/otel/trace v1.31.0/go.mod h1:TXZkRk7SM2ZQLtR6eoAWQFIHPvzQ06F
5858
golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
5959
golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
6060
golang.org/x/crypto v0.30.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
61+
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
62+
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
6163
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
6264
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
6365
golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0=
@@ -67,11 +69,13 @@ golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbht
6769
golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
6870
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
6971
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
72+
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
7073
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
7174
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
7275
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
7376
golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24=
7477
golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M=
78+
golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
7579
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
7680
golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
7781
golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=

pkg/ingester/otlp/convert.go

Lines changed: 90 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,20 @@ import (
55
"time"
66

77
googleProfile "github.com/grafana/pyroscope/api/gen/proto/go/google/v1"
8+
typesv1 "github.com/grafana/pyroscope/api/gen/proto/go/types/v1"
89
otelProfile "github.com/grafana/pyroscope/api/otlp/profiles/v1development"
10+
pyromodel "github.com/grafana/pyroscope/pkg/model"
911
)
1012

1113
const serviceNameKey = "service.name"
1214

15+
type convertedProfile struct {
16+
profile *googleProfile.Profile
17+
name *typesv1.LabelPair
18+
}
19+
1320
// ConvertOtelToGoogle converts an OpenTelemetry profile to a Google profile.
14-
func ConvertOtelToGoogle(src *otelProfile.Profile) map[string]*googleProfile.Profile {
21+
func ConvertOtelToGoogle(src *otelProfile.Profile) (map[string]convertedProfile, error) {
1522
svc2Profile := make(map[string]*profileBuilder)
1623
for _, sample := range src.Sample {
1724
svc := serviceNameFromSample(src, sample)
@@ -20,17 +27,27 @@ func ConvertOtelToGoogle(src *otelProfile.Profile) map[string]*googleProfile.Pro
2027
p = newProfileBuilder(src)
2128
svc2Profile[svc] = p
2229
}
23-
p.convertSampleBack(sample)
30+
if _, err := p.convertSampleBack(sample); err != nil {
31+
return nil, err
32+
}
2433
}
2534

26-
result := make(map[string]*googleProfile.Profile)
35+
result := make(map[string]convertedProfile)
2736
for svc, p := range svc2Profile {
28-
result[svc] = p.dst
37+
result[svc] = convertedProfile{p.dst, p.name}
2938
}
3039

31-
return result
40+
return result, nil
3241
}
3342

43+
type sampleConversionType int
44+
45+
const (
46+
sampleConversionTypeNone sampleConversionType = 0
47+
sampleConversionTypeSamplesToNanos sampleConversionType = 1
48+
sampleConversionTypeSumEvents sampleConversionType = 2
49+
)
50+
3451
type profileBuilder struct {
3552
src *otelProfile.Profile
3653
dst *googleProfile.Profile
@@ -39,7 +56,9 @@ type profileBuilder struct {
3956
unsymbolziedFuncNameMap map[string]uint64
4057
locationMap map[*otelProfile.Location]uint64
4158
mappingMap map[*otelProfile.Mapping]uint64
42-
cpuConversion bool
59+
60+
sampleProcessingTypes []sampleConversionType
61+
name *typesv1.LabelPair
4362
}
4463

4564
func newProfileBuilder(src *otelProfile.Profile) *profileBuilder {
@@ -66,19 +85,36 @@ func newProfileBuilder(src *otelProfile.Profile) *profileBuilder {
6685
Unit: res.addstr("ms"),
6786
}}
6887
res.dst.DefaultSampleType = res.addstr("samples")
69-
} else if len(res.dst.SampleType) == 1 && res.dst.PeriodType != nil && res.dst.Period != 0 {
70-
profileType := fmt.Sprintf("%s:%s:%s:%s",
71-
res.dst.StringTable[res.dst.SampleType[0].Type],
72-
res.dst.StringTable[res.dst.SampleType[0].Unit],
73-
res.dst.StringTable[res.dst.PeriodType.Type],
74-
res.dst.StringTable[res.dst.PeriodType.Unit],
75-
)
88+
}
89+
res.sampleProcessingTypes = make([]sampleConversionType, len(res.dst.SampleType))
90+
for i := 0; i < len(res.dst.SampleType); i++ {
91+
profileType := res.profileType(i)
7692
if profileType == "samples:count:cpu:nanoseconds" {
77-
res.dst.SampleType = []*googleProfile.ValueType{{
93+
res.dst.SampleType[i] = &googleProfile.ValueType{
7894
Type: res.addstr("cpu"),
7995
Unit: res.addstr("nanoseconds"),
80-
}}
81-
res.cpuConversion = true
96+
}
97+
if len(res.dst.SampleType) == 1 {
98+
res.name = &typesv1.LabelPair{
99+
Name: pyromodel.LabelNameProfileName,
100+
Value: "process_cpu",
101+
}
102+
}
103+
res.sampleProcessingTypes[i] = sampleConversionTypeSamplesToNanos
104+
}
105+
// Identify off cpu profiles
106+
if profileType == "events:nanoseconds::" && len(res.dst.SampleType) == 1 {
107+
res.sampleProcessingTypes[i] = sampleConversionTypeSumEvents
108+
res.name = &typesv1.LabelPair{
109+
Name: pyromodel.LabelNameProfileName,
110+
Value: pyromodel.ProfileNameOffCpu,
111+
}
112+
}
113+
}
114+
if res.name == nil {
115+
res.name = &typesv1.LabelPair{
116+
Name: pyromodel.LabelNameProfileName,
117+
Value: "process_cpu", // guess
82118
}
83119
}
84120

@@ -91,6 +127,22 @@ func newProfileBuilder(src *otelProfile.Profile) *profileBuilder {
91127
return res
92128
}
93129

130+
func (p *profileBuilder) profileType(idx int) string {
131+
var (
132+
periodType, periodUnit string
133+
)
134+
if p.dst.PeriodType != nil && p.dst.Period != 0 {
135+
periodType = p.dst.StringTable[p.dst.PeriodType.Type]
136+
periodUnit = p.dst.StringTable[p.dst.PeriodType.Unit]
137+
}
138+
return fmt.Sprintf("%s:%s:%s:%s",
139+
p.dst.StringTable[p.dst.SampleType[idx].Type],
140+
p.dst.StringTable[p.dst.SampleType[idx].Unit],
141+
periodType,
142+
periodUnit,
143+
)
144+
}
145+
94146
func (p *profileBuilder) addstr(s string) int64 {
95147
if i, ok := p.stringMap[s]; ok {
96148
return i
@@ -198,16 +250,32 @@ func (p *profileBuilder) convertFunctionBack(of *otelProfile.Function) uint64 {
198250
return gf.Id
199251
}
200252

201-
func (p *profileBuilder) convertSampleBack(os *otelProfile.Sample) *googleProfile.Sample {
253+
func (p *profileBuilder) convertSampleBack(os *otelProfile.Sample) (*googleProfile.Sample, error) {
202254
gs := &googleProfile.Sample{
203255
Value: os.Value,
204256
}
205-
206257
if len(gs.Value) == 0 {
207-
gs.Value = []int64{int64(len(os.TimestampsUnixNano))}
208-
} else if len(gs.Value) == 1 && p.cpuConversion {
209-
gs.Value[0] *= p.src.Period
258+
return nil, fmt.Errorf("sample value is required")
210259
}
260+
261+
for i, typ := range p.sampleProcessingTypes {
262+
switch typ {
263+
case sampleConversionTypeSamplesToNanos:
264+
gs.Value[i] *= p.src.Period
265+
case sampleConversionTypeSumEvents:
266+
// For off-CPU profiles, aggregate all sample values into a single sum
267+
// since pprof cannot represent variable-length sample values
268+
sum := int64(0)
269+
for _, v := range gs.Value {
270+
sum += v
271+
}
272+
gs.Value = []int64{sum}
273+
}
274+
}
275+
if p.dst.Period != 0 && p.dst.PeriodType != nil && len(gs.Value) != len(p.dst.SampleType) {
276+
return nil, fmt.Errorf("sample values length mismatch %d %d", len(gs.Value), len(p.dst.SampleType))
277+
}
278+
211279
p.convertSampleAttributesToLabelsBack(os, gs)
212280

213281
for i := os.LocationsStartIndex; i < os.LocationsStartIndex+os.LocationsLength; i++ {
@@ -216,7 +284,7 @@ func (p *profileBuilder) convertSampleBack(os *otelProfile.Sample) *googleProfil
216284

217285
p.dst.Sample = append(p.dst.Sample, gs)
218286

219-
return gs
287+
return gs, nil
220288
}
221289

222290
func (p *profileBuilder) convertSampleAttributesToLabelsBack(os *otelProfile.Sample, gs *googleProfile.Sample) {

pkg/ingester/otlp/ingest_handler.go

Lines changed: 12 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,6 @@ import (
66
"net/http"
77
"strings"
88

9-
distirbutormodel "github.com/grafana/pyroscope/pkg/distributor/model"
10-
pyromodel "github.com/grafana/pyroscope/pkg/model"
11-
"github.com/grafana/pyroscope/pkg/pprof"
12-
139
"connectrpc.com/connect"
1410
"github.com/go-kit/log"
1511
"github.com/go-kit/log/level"
@@ -21,6 +17,9 @@ import (
2117
typesv1 "github.com/grafana/pyroscope/api/gen/proto/go/types/v1"
2218
pprofileotlp "github.com/grafana/pyroscope/api/otlp/collector/profiles/v1development"
2319
v1 "github.com/grafana/pyroscope/api/otlp/common/v1"
20+
distirbutormodel "github.com/grafana/pyroscope/pkg/distributor/model"
21+
pyromodel "github.com/grafana/pyroscope/pkg/model"
22+
"github.com/grafana/pyroscope/pkg/pprof"
2423
"github.com/grafana/pyroscope/pkg/tenant"
2524
)
2625

@@ -97,7 +96,10 @@ func (h *ingestHandler) Export(ctx context.Context, er *pprofileotlp.ExportProfi
9796
for k := 0; k < len(sp.Profiles); k++ {
9897
p := sp.Profiles[k]
9998

100-
pprofProfiles := ConvertOtelToGoogle(p)
99+
pprofProfiles, err := ConvertOtelToGoogle(p)
100+
if err != nil {
101+
return &pprofileotlp.ExportProfilesServiceResponse{}, fmt.Errorf("failed to convert otel profile: %w", err)
102+
}
101103

102104
req := &distirbutormodel.PushRequest{
103105
RawProfileSize: p.Size(),
@@ -106,15 +108,16 @@ func (h *ingestHandler) Export(ctx context.Context, er *pprofileotlp.ExportProfi
106108

107109
for samplesServiceName, pprofProfile := range pprofProfiles {
108110
labels := getDefaultLabels()
109-
processedKeys := make(map[string]bool)
111+
labels = append(labels, pprofProfile.name)
112+
processedKeys := map[string]bool{pyromodel.LabelNameProfileName: true}
110113
labels = appendAttributesUnique(labels, rp.Resource.GetAttributes(), processedKeys)
111114
labels = appendAttributesUnique(labels, sp.Scope.GetAttributes(), processedKeys)
112115
svc := samplesServiceName
113116
if svc == "" {
114117
svc = serviceName
115118
}
116119
labels = append(labels, &typesv1.LabelPair{
117-
Name: "service_name",
120+
Name: pyromodel.LabelNameServiceName,
118121
Value: svc,
119122
})
120123

@@ -123,7 +126,7 @@ func (h *ingestHandler) Export(ctx context.Context, er *pprofileotlp.ExportProfi
123126
Samples: []*distirbutormodel.ProfileSample{
124127
{
125128
RawProfile: nil,
126-
Profile: pprof.RawFromProto(pprofProfile),
129+
Profile: pprof.RawFromProto(pprofProfile.profile),
127130
ID: uuid.New().String(),
128131
},
129132
},
@@ -133,7 +136,7 @@ func (h *ingestHandler) Export(ctx context.Context, er *pprofileotlp.ExportProfi
133136
if len(req.Series) == 0 {
134137
continue
135138
}
136-
_, err := h.svc.PushParsed(ctx, req)
139+
_, err = h.svc.PushParsed(ctx, req)
137140
if err != nil {
138141
h.log.Log("msg", "failed to push profile", "err", err)
139142
return &pprofileotlp.ExportProfilesServiceResponse{}, fmt.Errorf("failed to make a GRPC request: %w", err)
@@ -163,10 +166,6 @@ func getServiceNameFromAttributes(attrs []v1.KeyValue) string {
163166
// getDefaultLabels returns the required base labels for Pyroscope profiles
164167
func getDefaultLabels() []*typesv1.LabelPair {
165168
return []*typesv1.LabelPair{
166-
{
167-
Name: pyromodel.LabelNameProfileName,
168-
Value: "process_cpu",
169-
},
170169
{
171170
Name: pyromodel.LabelNameDelta,
172171
Value: "false",
@@ -175,10 +174,6 @@ func getDefaultLabels() []*typesv1.LabelPair {
175174
Name: pyromodel.LabelNameOTEL,
176175
Value: "true",
177176
},
178-
{
179-
Name: "pyroscope_spy",
180-
Value: "unknown",
181-
},
182177
}
183178
}
184179

0 commit comments

Comments
 (0)