Skip to content

Commit

Permalink
[chore]: Add Profiles Marshaler to otlptext (open-telemetry#11161)
Browse files Browse the repository at this point in the history
<!--Ex. Fixing a bug - Describe the bug and how this fixes the issue.
Ex. Adding a feature - Explain what this achieves.-->
#### Description
This is the initial work towards open-telemetry#11155.
This PR adds profiles marshalers to `otlptext`, which will be used in
the `debugexporter` later on.

To continue the work on the `debugexporter` I'd wait on the PR open-telemetry#11131,
as it adds some shared components.

<!--Describe what testing was performed and which tests were added.-->
#### Testing
Added `TestProfilesText`, testing the following scenarios: 
- `empty_profiles`
- `two_profiles`
  • Loading branch information
julianocosta89 authored and jackgopack4 committed Oct 8, 2024
1 parent f44b048 commit 1283000
Show file tree
Hide file tree
Showing 5 changed files with 437 additions and 0 deletions.
160 changes: 160 additions & 0 deletions exporter/internal/otlptext/databuffer.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (

"go.opentelemetry.io/collector/pdata/pcommon"
"go.opentelemetry.io/collector/pdata/pmetric"
"go.opentelemetry.io/collector/pdata/pprofile"
"go.opentelemetry.io/collector/pdata/ptrace"
)

Expand Down Expand Up @@ -47,6 +48,28 @@ func (b *dataBuffer) logAttributes(header string, m pcommon.Map) {
})
}

func (b *dataBuffer) logAttributesWithIndentation(header string, m pcommon.Map, indentVal int) {
if m.Len() == 0 {
return
}

indent := strings.Repeat(" ", indentVal)

b.logEntry("%s%s:", indent, header)
attrPrefix := indent + " ->"

// Add offset to attributes if needed.
headerParts := strings.Split(header, "->")
if len(headerParts) > 1 {
attrPrefix = headerParts[0] + attrPrefix
}

m.Range(func(k string, v pcommon.Value) bool {
b.logEntry("%s %s: %s", attrPrefix, k, valueToString(v))
return true
})
}

func (b *dataBuffer) logInstrumentationScope(il pcommon.InstrumentationScope) {
b.logEntry(
"InstrumentationScope %s %s",
Expand Down Expand Up @@ -280,6 +303,143 @@ func (b *dataBuffer) logExemplars(description string, se pmetric.ExemplarSlice)
}
}

func (b *dataBuffer) logProfileSamples(ss pprofile.SampleSlice) {
if ss.Len() == 0 {
return
}

for i := 0; i < ss.Len(); i++ {
b.logEntry(" Sample #%d", i)
sample := ss.At(i)

b.logEntry(" Location index: %d", sample.LocationIndex().AsRaw())
b.logEntry(" Location length: %d", sample.LocationsLength())
b.logEntry(" Stacktrace ID index: %d", sample.StacktraceIdIndex())
if lb := sample.Label().Len(); lb > 0 {
for j := 0; j < lb; j++ {
b.logEntry(" Label #%d", j)
b.logEntry(" -> Key: %d", sample.Label().At(j).Key())
b.logEntry(" -> Str: %d", sample.Label().At(j).Str())
b.logEntry(" -> Num: %d", sample.Label().At(j).Num())
b.logEntry(" -> Num unit: %d", sample.Label().At(j).NumUnit())
}
}
b.logEntry(" Value: %d", sample.Value().AsRaw())
b.logEntry(" Attributes: %d", sample.Attributes().AsRaw())
b.logEntry(" Link: %d", sample.Link())
}
}

func (b *dataBuffer) logProfileMappings(ms pprofile.MappingSlice) {
if ms.Len() == 0 {
return
}

for i := 0; i < ms.Len(); i++ {
b.logEntry(" Mapping #%d", i)
mapping := ms.At(i)

b.logEntry(" ID: %d", mapping.ID())
b.logEntry(" Memory start: %d", mapping.MemoryStart())
b.logEntry(" Memory limit: %d", mapping.MemoryLimit())
b.logEntry(" File offset: %d", mapping.FileOffset())
b.logEntry(" File name: %d", mapping.Filename())
b.logEntry(" Build ID: %d", mapping.BuildID())
b.logEntry(" Attributes: %d", mapping.Attributes().AsRaw())
b.logEntry(" Has functions: %t", mapping.HasFunctions())
b.logEntry(" Has filenames: %t", mapping.HasFilenames())
b.logEntry(" Has line numbers: %t", mapping.HasLineNumbers())
b.logEntry(" Has inline frames: %t", mapping.HasInlineFrames())

}
}

func (b *dataBuffer) logProfileLocations(ls pprofile.LocationSlice) {
if ls.Len() == 0 {
return
}

for i := 0; i < ls.Len(); i++ {
b.logEntry(" Location #%d", i)
location := ls.At(i)

b.logEntry(" ID: %d", location.ID())
b.logEntry(" Mapping index: %d", location.MappingIndex())
b.logEntry(" Address: %d", location.Address())
if ll := location.Line().Len(); ll > 0 {
for j := 0; j < ll; j++ {
b.logEntry(" Line #%d", j)
line := location.Line().At(j)
b.logEntry(" Function index: %d", line.FunctionIndex())
b.logEntry(" Line: %d", line.Line())
b.logEntry(" Column: %d", line.Column())
}
}
b.logEntry(" Is folded: %t", location.IsFolded())
b.logEntry(" Type index: %d", location.TypeIndex())
b.logEntry(" Attributes: %d", location.Attributes().AsRaw())
}
}

func (b *dataBuffer) logProfileFunctions(fs pprofile.FunctionSlice) {
if fs.Len() == 0 {
return
}

for i := 0; i < fs.Len(); i++ {
b.logEntry(" Function #%d", i)
function := fs.At(i)

b.logEntry(" ID: %d", function.ID())
b.logEntry(" Name: %d", function.Name())
b.logEntry(" System name: %d", function.SystemName())
b.logEntry(" Filename: %d", function.Filename())
b.logEntry(" Start line: %d", function.StartLine())
}
}

func (b *dataBuffer) logStringTable(ss pcommon.StringSlice) {
if ss.Len() == 0 {
return
}

b.logEntry(" String table:")
for i := 0; i < ss.Len(); i++ {
b.logEntry(" %s", ss.At(i))
}
}

func (b *dataBuffer) logComment(c pcommon.Int64Slice) {
if c.Len() == 0 {
return
}

b.logEntry(" Comment:")
for i := 0; i < c.Len(); i++ {
b.logEntry(" %d", c.At(i))
}
}

func attributeUnitsToMap(aus pprofile.AttributeUnitSlice) pcommon.Map {
m := pcommon.NewMap()
for i := 0; i < aus.Len(); i++ {
au := aus.At(i)
m.PutInt("attributeKey", au.AttributeKey())
m.PutInt("unit", au.Unit())
}
return m
}

func linkTableToMap(ls pprofile.LinkSlice) pcommon.Map {
m := pcommon.NewMap()
for i := 0; i < ls.Len(); i++ {
l := ls.At(i)
m.PutStr("Trace ID", l.TraceID().String())
m.PutStr("Span ID", l.SpanID().String())
}
return m
}

func valueToString(v pcommon.Value) string {
return fmt.Sprintf("%s(%s)", v.Type().String(), v.AsString())
}
74 changes: 74 additions & 0 deletions exporter/internal/otlptext/profiles.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

package otlptext // import "go.opentelemetry.io/collector/exporter/internal/otlptext"

import (
"strconv"

"go.opentelemetry.io/collector/pdata/pprofile"
)

// NewTextProfilesMarshaler returns a pprofile.Marshaler to encode to OTLP text bytes.
func NewTextProfilesMarshaler() pprofile.Marshaler {
return textProfilesMarshaler{}
}

type textProfilesMarshaler struct{}

// MarshalProfiles pprofile.Profiles to OTLP text.
func (textProfilesMarshaler) MarshalProfiles(pd pprofile.Profiles) ([]byte, error) {
buf := dataBuffer{}
rps := pd.ResourceProfiles()
for i := 0; i < rps.Len(); i++ {
buf.logEntry("ResourceProfiles #%d", i)
rp := rps.At(i)
buf.logEntry("Resource SchemaURL: %s", rp.SchemaUrl())
buf.logAttributes("Resource attributes", rp.Resource().Attributes())
ilps := rp.ScopeProfiles()
for j := 0; j < ilps.Len(); j++ {
buf.logEntry("ScopeProfiles #%d", j)
ilp := ilps.At(j)
buf.logEntry("ScopeProfiles SchemaURL: %s", ilp.SchemaUrl())
buf.logInstrumentationScope(ilp.Scope())
profiles := ilp.Profiles()
for k := 0; k < profiles.Len(); k++ {
buf.logEntry("Profile #%d", k)
profile := profiles.At(k)
buf.logAttr("Profile ID", profile.ProfileID())
buf.logAttr("Start time", profile.StartTime().String())
buf.logAttr("End time", profile.EndTime().String())
buf.logAttributes("Attributes", profile.Attributes())
buf.logAttr("Dropped attributes count", strconv.FormatUint(uint64(profile.DroppedAttributesCount()), 10))
buf.logEntry(" Location indices: %d", profile.Profile().LocationIndices().AsRaw())
buf.logEntry(" Drop frames: %d", profile.Profile().DropFrames())
buf.logEntry(" Keep frames: %d", profile.Profile().KeepFrames())

buf.logProfileSamples(profile.Profile().Sample())
buf.logProfileMappings(profile.Profile().Mapping())
buf.logProfileLocations(profile.Profile().Location())
buf.logProfileFunctions(profile.Profile().Function())

buf.logAttributesWithIndentation(
"Attribute table",
profile.Profile().AttributeTable(),
4)

buf.logAttributesWithIndentation(
"Attribute units",
attributeUnitsToMap(profile.Profile().AttributeUnits()),
4)

buf.logAttributesWithIndentation(
"Link table",
linkTableToMap(profile.Profile().LinkTable()),
4)

buf.logStringTable(profile.Profile().StringTable())
buf.logComment(profile.Profile().Comment())
}
}
}

return buf.buf.Bytes(), nil
}
119 changes: 119 additions & 0 deletions exporter/internal/otlptext/profiles_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

package otlptext

import (
"os"
"path/filepath"
"strings"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"go.opentelemetry.io/collector/pdata/pprofile"
"go.opentelemetry.io/collector/pdata/testdata"
)

func TestProfilesText(t *testing.T) {
tests := []struct {
name string
in pprofile.Profiles
out string
}{
{
name: "empty_profiles",
in: pprofile.NewProfiles(),
out: "empty.out",
},
{
name: "two_profiles",
in: extendProfiles(testdata.GenerateProfiles(2)),
out: "two_profiles.out",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := NewTextProfilesMarshaler().MarshalProfiles(tt.in)
require.NoError(t, err)
out, err := os.ReadFile(filepath.Join("testdata", "profiles", tt.out))
require.NoError(t, err)
expected := strings.ReplaceAll(string(out), "\r", "")
assert.Equal(t, expected, string(got))
})
}
}

// GenerateExtendedProfiles generates dummy profiling data with extended values for tests
func extendProfiles(profiles pprofile.Profiles) pprofile.Profiles {
sc := profiles.ResourceProfiles().At(0).ScopeProfiles().At(0)
profilesCount := profiles.ResourceProfiles().At(0).ScopeProfiles().At(0).Profiles().Len()
for i := 0; i < profilesCount; i++ {
switch i % 2 {
case 0:
profile := sc.Profiles().At(i)
profile.Profile().LocationIndices().FromRaw([]int64{1})
label := profile.Profile().Sample().At(0).Label().AppendEmpty()
label.SetKey(1)
label.SetStr(2)
label.SetNum(3)
label.SetNumUnit(4)

location := profile.Profile().Location().AppendEmpty()
location.SetID(2)
location.SetMappingIndex(3)
location.SetAddress(4)
line := location.Line().AppendEmpty()
line.SetFunctionIndex(1)
line.SetLine(2)
line.SetColumn(3)
location.SetIsFolded(true)
location.SetTypeIndex(5)
location.Attributes().FromRaw([]uint64{6, 7})

_ = profile.Profile().AttributeTable().FromRaw(map[string]any{
"value": map[string]any{
"intValue": "42",
},
})

attributeUnits := profile.Profile().AttributeUnits().AppendEmpty()
attributeUnits.SetAttributeKey(1)
attributeUnits.SetUnit(5)

profile.Profile().StringTable().Append("foobar")
case 1:
profile := sc.Profiles().At(i)
profile.Profile().SetDropFrames(1)
profile.Profile().SetKeepFrames(2)

mapping := profile.Profile().Mapping().AppendEmpty()
mapping.SetID(1)
mapping.SetMemoryStart(2)
mapping.SetMemoryLimit(3)
mapping.SetFileOffset(4)
mapping.SetFilename(5)
mapping.SetBuildID(6)
mapping.Attributes().FromRaw([]uint64{7, 8})
mapping.SetHasFunctions(true)
mapping.SetHasFilenames(true)
mapping.SetHasLineNumbers(true)
mapping.SetHasInlineFrames(true)

function := profile.Profile().Function().AppendEmpty()
function.SetID(1)
function.SetName(2)
function.SetSystemName(3)
function.SetFilename(4)
function.SetStartLine(5)

linkTable := profile.Profile().LinkTable().AppendEmpty()
linkTable.SetTraceID([16]byte{0x03, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10})
linkTable.SetSpanID([8]byte{0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18})

profile.Profile().Comment().FromRaw([]int64{1, 2})
}
}
return profiles
}
Empty file.
Loading

0 comments on commit 1283000

Please sign in to comment.