diff --git a/receiver/pprofreceiver/go.mod b/receiver/pprofreceiver/go.mod index c00460637301e..49420c83a7371 100644 --- a/receiver/pprofreceiver/go.mod +++ b/receiver/pprofreceiver/go.mod @@ -3,12 +3,15 @@ module github.com/open-telemetry/opentelemetry-collector-contrib/receiver/pprofr go 1.23.0 require ( + github.com/google/pprof v0.0.0-20250607225305-033d6d78b36a github.com/stretchr/testify v1.10.0 go.opentelemetry.io/collector/component v1.34.0 go.opentelemetry.io/collector/component/componenttest v0.128.0 go.opentelemetry.io/collector/config/confighttp v0.128.0 go.opentelemetry.io/collector/confmap v1.34.0 go.opentelemetry.io/collector/consumer/xconsumer v0.128.0 + go.opentelemetry.io/collector/pdata v1.34.0 + go.opentelemetry.io/collector/pdata/pprofile v0.128.0 go.opentelemetry.io/collector/receiver v1.34.0 go.opentelemetry.io/collector/receiver/receivertest v0.128.0 go.opentelemetry.io/collector/receiver/xreceiver v0.128.0 @@ -54,8 +57,6 @@ require ( go.opentelemetry.io/collector/extension/extensionmiddleware v0.128.0 // indirect go.opentelemetry.io/collector/featuregate v1.34.0 // indirect go.opentelemetry.io/collector/internal/telemetry v0.128.0 // indirect - go.opentelemetry.io/collector/pdata v1.34.0 // indirect - go.opentelemetry.io/collector/pdata/pprofile v0.128.0 // indirect go.opentelemetry.io/collector/pipeline v0.128.0 // indirect go.opentelemetry.io/contrib/bridges/otelzap v0.11.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect diff --git a/receiver/pprofreceiver/go.sum b/receiver/pprofreceiver/go.sum index 8670a63904313..59163796c0736 100644 --- a/receiver/pprofreceiver/go.sum +++ b/receiver/pprofreceiver/go.sum @@ -32,6 +32,8 @@ github.com/google/go-tpm v0.9.5/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u github.com/google/go-tpm-tools v0.4.4 h1:oiQfAIkc6xTy9Fl5NKTeTJkBTlXdHsxAofmQyxBKY98= github.com/google/go-tpm-tools v0.4.4/go.mod h1:T8jXkp2s+eltnCDIsXR84/MTcVU9Ja7bh3Mit0pa4AY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20250607225305-033d6d78b36a h1://KbezygeMJZCSHH+HgUZiTeSoiuFspbMg1ge+eFj18= +github.com/google/pprof v0.0.0-20250607225305-033d6d78b36a/go.mod h1:5hDyRhoBCxViHszMt12TnOpEI4VVi+U8Gm9iphldiMA= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= diff --git a/receiver/pprofreceiver/internal/pprof_to_pdata.go b/receiver/pprofreceiver/internal/pprof_to_pdata.go new file mode 100644 index 0000000000000..a960386219718 --- /dev/null +++ b/receiver/pprofreceiver/internal/pprof_to_pdata.go @@ -0,0 +1,114 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package internal // import "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/pprofreceiver/internal" + +import ( + "crypto/md5" //nolint:gosec + "time" + + "github.com/google/pprof/profile" + "go.opentelemetry.io/collector/pdata/pcommon" + "go.opentelemetry.io/collector/pdata/pprofile" +) + +func pprofToPprofile(parsed *profile.Profile, originalPayload []byte) pprofile.Profiles { + result := pprofile.NewProfiles() + // create a new profile record + resultProfile := result.ResourceProfiles().AppendEmpty().ScopeProfiles().AppendEmpty().Profiles().AppendEmpty() + + var hash [16]byte + // set the original payload on the record + // per https://github.com/open-telemetry/semantic-conventions/blob/main/model/profile/registry.yaml + resultProfile.SetOriginalPayloadFormat("go") + resultProfile.OriginalPayload().FromRaw(originalPayload) + hash = md5.Sum(originalPayload) //nolint:gosec + + // set period + resultProfile.SetPeriod(parsed.Period) + // TODO set period type? + // TODO set doc url? + + // set timestamps - both start and duration + resultProfile.SetStartTime(pcommon.NewTimestampFromTime(time.Unix(0, parsed.TimeNanos))) + resultProfile.SetDuration(pcommon.NewTimestampFromTime(time.Unix(0, parsed.DurationNanos))) + + // set the default simple type. Given this is a string, it gets cached in the string table and referred to via its index. + result.ProfilesDictionary().StringTable().Append(parsed.DefaultSampleType) + resultProfile.SetDefaultSampleTypeIndex(int32(result.ProfilesDictionary().StringTable().Len() - 1)) + + // set the profile ID. + resultProfile.SetProfileID(hash) + + // add all mappings to the global dictionary + for _, m := range parsed.Mapping { + // create a mapping in the mapping table + resultM := result.ProfilesDictionary().MappingTable().AppendEmpty() + resultM.SetFileOffset(m.Offset) + resultM.SetMemoryStart(m.Start) + resultM.SetMemoryLimit(m.Limit) + resultM.SetHasFilenames(m.HasFilenames) + resultM.SetHasFunctions(m.HasFunctions) + resultM.SetHasInlineFrames(m.HasInlineFrames) + resultM.SetHasLineNumbers(m.HasLineNumbers) + result.ProfilesDictionary().StringTable().Append(m.File) + resultM.SetFilenameStrindex(int32(result.ProfilesDictionary().StringTable().Len() - 1)) + } + + // now iterate over each sample of the pprof data + for _, s := range parsed.Sample { + // for each sample create a corresponding sample record in our pprofile record + resultS := resultProfile.Sample().AppendEmpty() + // set the value of the sample. + resultS.Value().FromRaw(s.Value) + // now iterate over each location. + + // set the start index at which we will start to read locations. + resultS.SetLocationsStartIndex(int32(result.ProfilesDictionary().LocationTable().Len())) + + // read each location and map it over to pprofile + for _, l := range parsed.Location { + // add to the location table + resultL := result.ProfilesDictionary().LocationTable().AppendEmpty() + resultL.SetAddress(l.Address) + resultL.SetIsFolded(l.IsFolded) + for i, m := range result.ProfilesDictionary().MappingTable().All() { + if m.FileOffset() == l.Mapping.Offset && result.ProfilesDictionary().StringTable().At(int(m.FilenameStrindex())) == l.Mapping.File { + resultL.SetMappingIndex(int32(i)) + break + } + } + } + resultS.SetLocationsLength(int32(len(parsed.Location))) + } + + for _, f := range parsed.Function { + resultF := result.ProfilesDictionary().FunctionTable().AppendEmpty() + result.ProfilesDictionary().StringTable().Append(f.Filename) + resultF.SetFilenameStrindex(int32(result.ProfilesDictionary().StringTable().Len() - 1)) + result.ProfilesDictionary().StringTable().Append(f.Name) + resultF.SetNameStrindex(int32(result.ProfilesDictionary().StringTable().Len() - 1)) + resultF.SetStartLine(f.StartLine) + result.ProfilesDictionary().StringTable().Append(f.SystemName) + resultF.SetSystemNameStrindex(int32(result.ProfilesDictionary().StringTable().Len() - 1)) + } + + for _, s := range parsed.SampleType { + resultSampleType := resultProfile.SampleType().AppendEmpty() + result.ProfilesDictionary().StringTable().Append(s.Unit) + resultSampleType.SetUnitStrindex(int32(result.ProfilesDictionary().StringTable().Len() - 1)) + result.ProfilesDictionary().StringTable().Append(s.Type) + resultSampleType.SetTypeStrindex(int32(result.ProfilesDictionary().StringTable().Len() - 1)) + // TODO set aggregation temporality + } + + commentsIndices := make([]int32, len(parsed.Comments)) + start := result.ProfilesDictionary().StringTable().Len() - 1 + result.ProfilesDictionary().StringTable().Append(parsed.Comments...) + for i := range parsed.Comments { + commentsIndices[i] = int32(start + i) + } + resultProfile.CommentStrindices().FromRaw(commentsIndices) + + return result +} diff --git a/receiver/pprofreceiver/internal/pprof_to_pdata_test.go b/receiver/pprofreceiver/internal/pprof_to_pdata_test.go new file mode 100644 index 0000000000000..60d6e78863d8a --- /dev/null +++ b/receiver/pprofreceiver/internal/pprof_to_pdata_test.go @@ -0,0 +1,146 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package internal + +import ( + "bytes" + "runtime/pprof" + "testing" + + "github.com/google/pprof/profile" + "github.com/stretchr/testify/require" + "go.opentelemetry.io/collector/pdata/pprofile" +) + +func TestMapPprofToPdata(t *testing.T) { + p := pprof.Lookup("goroutine") + pprofData := new(bytes.Buffer) + err := p.WriteTo(pprofData, 0) + require.NoError(t, err) + parsed, err := profile.Parse(pprofData) + require.NoError(t, err) + converted := pprofToPprofile(parsed, pprofData.Bytes()) + require.NotNil(t, converted) + pprofBack := pprofileToPprof(converted) + require.Equal(t, parsed, pprofBack[0]) +} + +func pprofileToPprof(pdataProfiles pprofile.Profiles) []*profile.Profile { + result := []*profile.Profile{} + functions := make([]*profile.Function, pdataProfiles.ProfilesDictionary().FunctionTable().Len()) + for i, f := range pdataProfiles.ProfilesDictionary().FunctionTable().All() { + functions[i] = &profile.Function{ + ID: uint64(i + 1), // this is not preserved, so we just make it up. + Name: pdataProfiles.ProfilesDictionary().StringTable().At(int(f.NameStrindex())), + SystemName: pdataProfiles.ProfilesDictionary().StringTable().At(int(f.SystemNameStrindex())), + Filename: pdataProfiles.ProfilesDictionary().StringTable().At(int(f.FilenameStrindex())), + StartLine: f.StartLine(), + } + } + + mappings := make([]*profile.Mapping, pdataProfiles.ProfilesDictionary().MappingTable().Len()) + for i, m := range pdataProfiles.ProfilesDictionary().MappingTable().All() { + mappings[i] = &profile.Mapping{ + ID: uint64(i + 1), // this is not preserved, so we just make it up. + Start: m.MemoryStart(), + Limit: m.MemoryLimit(), + Offset: m.FileOffset(), + File: pdataProfiles.ProfilesDictionary().StringTable().At(int(m.FilenameStrindex())), + BuildID: "", // TODO no mapping + HasFunctions: m.HasFunctions(), + HasFilenames: m.HasFilenames(), + HasLineNumbers: m.HasLineNumbers(), + HasInlineFrames: m.HasInlineFrames(), + KernelRelocationSymbol: "", // TODO no mapping + } + } + for _, rp := range pdataProfiles.ResourceProfiles().All() { + for _, sp := range rp.ScopeProfiles().All() { + for _, p := range sp.Profiles().All() { + var comments []string + if p.CommentStrindices().Len() > 0 { + comments = make([]string, p.CommentStrindices().Len()) + for i, index := range p.CommentStrindices().All() { + comments[i] = pdataProfiles.ProfilesDictionary().StringTable().At(int(index)) + } + } + sampleTypes := make([]*profile.ValueType, p.SampleType().Len()) + for i, st := range p.SampleType().All() { + sampleTypes[i] = &profile.ValueType{ + Type: pdataProfiles.ProfilesDictionary().StringTable().At(int(st.TypeStrindex())), + Unit: pdataProfiles.ProfilesDictionary().StringTable().At(int(st.UnitStrindex())), + } + } + + locations := make([]*profile.Location, p.LocationIndices().Len()) + for i, l := range p.LocationIndices().All() { + pdataLocation := pdataProfiles.ProfilesDictionary().LocationTable().At(int(l)) + lines := make([]profile.Line, pdataLocation.Line().Len()) + for j, k := range pdataLocation.Line().All() { + lines[j] = profile.Line{ + Function: functions[k.FunctionIndex()], + Line: k.Line(), + Column: k.Column(), + } + } + locations[i] = &profile.Location{ + ID: uint64(l + 1), // TODO we don't map this over. + Mapping: mappings[pdataLocation.MappingIndex()], + Address: pdataLocation.Address(), + Line: lines, + IsFolded: pdataLocation.IsFolded(), + } + } + + samples := make([]*profile.Sample, p.Sample().Len()) + for i, s := range p.Sample().All() { + sampleLocations := make([]*profile.Location, s.LocationsLength()) + for li := 0; li < int(s.LocationsLength()); li++ { + pdataLocation := pdataProfiles.ProfilesDictionary().LocationTable().At(i + int(s.LocationsStartIndex())) + lines := make([]profile.Line, pdataLocation.Line().Len()) + for j, k := range pdataLocation.Line().All() { + lines[j] = profile.Line{ + Function: functions[k.FunctionIndex()], + Line: k.Line(), + Column: k.Column(), + } + } + sampleLocations[li] = &profile.Location{ + ID: uint64(li + 1), // TODO we don't map this over. + Mapping: mappings[pdataLocation.MappingIndex()], + Address: pdataLocation.Address(), + Line: lines, + IsFolded: pdataLocation.IsFolded(), + } + } + samples[i] = &profile.Sample{ + Location: sampleLocations, + Value: s.Value().AsRaw(), + Label: nil, // TODO no mapping + NumLabel: nil, // TODO no mapping + NumUnit: nil, // TODO no mapping + } + } + + result = append(result, &profile.Profile{ + SampleType: sampleTypes, + DefaultSampleType: pdataProfiles.ProfilesDictionary().StringTable().At(int(p.DefaultSampleTypeIndex())), + Sample: samples, + Mapping: mappings, + Location: locations, + Function: functions, + Comments: comments, + DocURL: "", // TODO no mapping + DropFrames: "", // TODO no mapping + KeepFrames: "", // TODO no mapping + TimeNanos: p.Time().AsTime().UnixNano(), + DurationNanos: p.Duration().AsTime().UnixNano(), + PeriodType: nil, + Period: p.Period(), + }) + } + } + } + return result +}