diff --git a/exporter/internal/otlptext/databuffer.go b/exporter/internal/otlptext/databuffer.go index cf7fd630e78..32f59220266 100644 --- a/exporter/internal/otlptext/databuffer.go +++ b/exporter/internal/otlptext/databuffer.go @@ -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" ) @@ -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", @@ -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()) } diff --git a/exporter/internal/otlptext/profiles.go b/exporter/internal/otlptext/profiles.go new file mode 100644 index 00000000000..d0d52640ac1 --- /dev/null +++ b/exporter/internal/otlptext/profiles.go @@ -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 +} diff --git a/exporter/internal/otlptext/profiles_test.go b/exporter/internal/otlptext/profiles_test.go new file mode 100644 index 00000000000..0a44252b3a9 --- /dev/null +++ b/exporter/internal/otlptext/profiles_test.go @@ -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 +} diff --git a/exporter/internal/otlptext/testdata/profiles/empty.out b/exporter/internal/otlptext/testdata/profiles/empty.out new file mode 100644 index 00000000000..e69de29bb2d diff --git a/exporter/internal/otlptext/testdata/profiles/two_profiles.out b/exporter/internal/otlptext/testdata/profiles/two_profiles.out new file mode 100644 index 00000000000..81ef23433b4 --- /dev/null +++ b/exporter/internal/otlptext/testdata/profiles/two_profiles.out @@ -0,0 +1,84 @@ +ResourceProfiles #0 +Resource SchemaURL: +Resource attributes: + -> resource-attr: Str(resource-attr-val-1) +ScopeProfiles #0 +ScopeProfiles SchemaURL: +InstrumentationScope +Profile #0 + Profile ID : 0102030405060708090a0b0c0d0e0f10 + Start time : 2020-02-11 20:26:12.000000321 +0000 UTC + End time : 2020-02-11 20:26:13.000000789 +0000 UTC + Dropped attributes count: 1 + Location indices: [1] + Drop frames: 0 + Keep frames: 0 + Sample #0 + Location index: [1] + Location length: 10 + Stacktrace ID index: 3 + Label #0 + -> Key: 1 + -> Str: 2 + -> Num: 3 + -> Num unit: 4 + Value: [4] + Attributes: [5] + Link: 42 + Location #0 + ID: 2 + Mapping index: 3 + Address: 4 + Line #0 + Function index: 1 + Line: 2 + Column: 3 + Is folded: true + Type index: 5 + Attributes: [6 7] + Attribute table: + -> value: Map({"intValue":"42"}) + Attribute units: + -> attributeKey: Int(1) + -> unit: Int(5) + String table: + foobar +Profile #1 + Profile ID : 0202030405060708090a0b0c0d0e0f10 + Start time : 2020-02-11 20:26:12.000000321 +0000 UTC + End time : 2020-02-11 20:26:13.000000789 +0000 UTC + Dropped attributes count: 0 + Location indices: [] + Drop frames: 1 + Keep frames: 2 + Sample #0 + Location index: [6] + Location length: 20 + Stacktrace ID index: 8 + Value: [9] + Attributes: [10] + Link: 44 + Mapping #0 + ID: 1 + Memory start: 2 + Memory limit: 3 + File offset: 4 + File name: 5 + Build ID: 6 + Attributes: [7 8] + Has functions: true + Has filenames: true + Has line numbers: true + Has inline frames: true + Function #0 + ID: 1 + Name: 2 + System name: 3 + Filename: 4 + Start line: 5 + Link table: + -> Trace ID: Str(0302030405060708090a0b0c0d0e0f10) + -> Span ID: Str(1112131415161718) + Comment: + 1 + 2