From 116c1812fe3b55d874853c078e28934fc1ceb4da Mon Sep 17 00:00:00 2001 From: Damien Mathieu <42@dmathieu.com> Date: Wed, 19 Nov 2025 11:58:00 +0100 Subject: [PATCH 1/2] Introduce `switchDictionary` methods for profiles (#14075) This is the next step towards https://github.com/open-telemetry/opentelemetry-collector/issues/13106 It introduces private `switchDictionary` methods for all profile structs, so their base dictionary can be switched, which is a requirement (and maybe the biggest bit) of merging profiles. Right now, the private method isn't called yet, because this PR is already very big, and I believe introducing `MergeTo` should be in another PR. But the gist is that to merge profiles together, we need to change the base dictionary for the profiles that are being moved before we can merge the two slices of resource profiles into one. --- pdata/pprofile/function.go | 44 +++++ pdata/pprofile/function_test.go | 189 +++++++++++++++++++ pdata/pprofile/keyvalueandunit.go | 32 ++++ pdata/pprofile/keyvalueandunit_test.go | 140 ++++++++++++++ pdata/pprofile/line.go | 26 +++ pdata/pprofile/line_test.go | 103 +++++++++++ pdata/pprofile/location.go | 49 +++++ pdata/pprofile/location_test.go | 208 +++++++++++++++++++++ pdata/pprofile/mapping.go | 37 ++++ pdata/pprofile/mapping_test.go | 153 ++++++++++++++++ pdata/pprofile/profile.go | 45 +++++ pdata/pprofile/profile_test.go | 212 ++++++++++++++++++++++ pdata/pprofile/profiles.go | 15 ++ pdata/pprofile/profiles_test.go | 78 ++++++++ pdata/pprofile/resourceprofiles.go | 19 ++ pdata/pprofile/resourceprofiles_test.go | 91 ++++++++++ pdata/pprofile/sample.go | 59 ++++++ pdata/pprofile/sample_test.go | 231 ++++++++++++++++++++++++ pdata/pprofile/scopeprofiles.go | 19 ++ pdata/pprofile/scopeprofiles_test.go | 91 ++++++++++ pdata/pprofile/stack.go | 27 +++ pdata/pprofile/stack_test.go | 125 +++++++++++++ pdata/pprofile/valuetype.go | 36 ++++ pdata/pprofile/valuetype_test.go | 150 +++++++++++++++ 24 files changed, 2179 insertions(+) create mode 100644 pdata/pprofile/profile.go create mode 100644 pdata/pprofile/profile_test.go create mode 100644 pdata/pprofile/resourceprofiles.go create mode 100644 pdata/pprofile/resourceprofiles_test.go create mode 100644 pdata/pprofile/sample.go create mode 100644 pdata/pprofile/sample_test.go create mode 100644 pdata/pprofile/scopeprofiles.go create mode 100644 pdata/pprofile/scopeprofiles_test.go create mode 100644 pdata/pprofile/valuetype.go create mode 100644 pdata/pprofile/valuetype_test.go diff --git a/pdata/pprofile/function.go b/pdata/pprofile/function.go index 4da4e605333..021cb68a167 100644 --- a/pdata/pprofile/function.go +++ b/pdata/pprofile/function.go @@ -3,6 +3,8 @@ package pprofile // import "go.opentelemetry.io/collector/pdata/pprofile" +import "fmt" + // Equal checks equality with another Function func (fn Function) Equal(val Function) bool { return fn.NameStrindex() == val.NameStrindex() && @@ -10,3 +12,45 @@ func (fn Function) Equal(val Function) bool { fn.FilenameStrindex() == val.FilenameStrindex() && fn.StartLine() == val.StartLine() } + +// switchDictionary updates the Function, switching its indices from one +// dictionary to another. +func (fn Function) switchDictionary(src, dst ProfilesDictionary) error { + if fn.NameStrindex() > 0 { + if src.StringTable().Len() < int(fn.NameStrindex()) { + return fmt.Errorf("invalid name index %d", fn.NameStrindex()) + } + + idx, err := SetString(dst.StringTable(), src.StringTable().At(int(fn.NameStrindex()))) + if err != nil { + return fmt.Errorf("couldn't set name: %w", err) + } + fn.SetNameStrindex(idx) + } + + if fn.SystemNameStrindex() > 0 { + if src.StringTable().Len() < int(fn.SystemNameStrindex()) { + return fmt.Errorf("invalid system name index %d", fn.SystemNameStrindex()) + } + + idx, err := SetString(dst.StringTable(), src.StringTable().At(int(fn.SystemNameStrindex()))) + if err != nil { + return fmt.Errorf("couldn't set system name: %w", err) + } + fn.SetSystemNameStrindex(idx) + } + + if fn.FilenameStrindex() > 0 { + if src.StringTable().Len() < int(fn.FilenameStrindex()) { + return fmt.Errorf("invalid filename index %d", fn.FilenameStrindex()) + } + + idx, err := SetString(dst.StringTable(), src.StringTable().At(int(fn.FilenameStrindex()))) + if err != nil { + return fmt.Errorf("couldn't set filename: %w", err) + } + fn.SetFilenameStrindex(idx) + } + + return nil +} diff --git a/pdata/pprofile/function_test.go b/pdata/pprofile/function_test.go index a952005a5f7..1de4f9d4603 100644 --- a/pdata/pprofile/function_test.go +++ b/pdata/pprofile/function_test.go @@ -4,9 +4,11 @@ package pprofile import ( + "errors" "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestFunctionEqual(t *testing.T) { @@ -63,6 +65,193 @@ func TestFunctionEqual(t *testing.T) { } } +func TestFunctionSwitchDictionary(t *testing.T) { + for _, tt := range []struct { + name string + function Function + + src ProfilesDictionary + dst ProfilesDictionary + + wantFunction Function + wantDictionary ProfilesDictionary + wantErr error + }{ + { + name: "with an empty key value and unit", + function: NewFunction(), + + src: NewProfilesDictionary(), + dst: NewProfilesDictionary(), + + wantFunction: NewFunction(), + wantDictionary: NewProfilesDictionary(), + }, + { + name: "with an existing name", + function: func() Function { + fn := NewFunction() + fn.SetNameStrindex(1) + return fn + }(), + + src: func() ProfilesDictionary { + d := NewProfilesDictionary() + d.StringTable().Append("", "test") + return d + }(), + dst: func() ProfilesDictionary { + d := NewProfilesDictionary() + d.StringTable().Append("", "foo") + return d + }(), + + wantFunction: func() Function { + fn := NewFunction() + fn.SetNameStrindex(2) + return fn + }(), + wantDictionary: func() ProfilesDictionary { + d := NewProfilesDictionary() + d.StringTable().Append("", "foo", "test") + return d + }(), + }, + { + name: "with a name index that does not match anything", + function: func() Function { + fn := NewFunction() + fn.SetNameStrindex(1) + return fn + }(), + + src: NewProfilesDictionary(), + dst: NewProfilesDictionary(), + + wantFunction: func() Function { + fn := NewFunction() + fn.SetNameStrindex(1) + return fn + }(), + wantDictionary: NewProfilesDictionary(), + wantErr: errors.New("invalid name index 1"), + }, + { + name: "with an existing system name", + function: func() Function { + fn := NewFunction() + fn.SetSystemNameStrindex(1) + return fn + }(), + + src: func() ProfilesDictionary { + d := NewProfilesDictionary() + d.StringTable().Append("", "test") + return d + }(), + dst: func() ProfilesDictionary { + d := NewProfilesDictionary() + d.StringTable().Append("", "foo") + return d + }(), + + wantFunction: func() Function { + fn := NewFunction() + fn.SetSystemNameStrindex(2) + return fn + }(), + wantDictionary: func() ProfilesDictionary { + d := NewProfilesDictionary() + d.StringTable().Append("", "foo", "test") + return d + }(), + }, + { + name: "with a system name index that does not match anything", + function: func() Function { + fn := NewFunction() + fn.SetSystemNameStrindex(1) + return fn + }(), + + src: NewProfilesDictionary(), + dst: NewProfilesDictionary(), + + wantFunction: func() Function { + fn := NewFunction() + fn.SetSystemNameStrindex(1) + return fn + }(), + wantDictionary: NewProfilesDictionary(), + wantErr: errors.New("invalid system name index 1"), + }, + { + name: "with an existing filename", + function: func() Function { + fn := NewFunction() + fn.SetFilenameStrindex(1) + return fn + }(), + + src: func() ProfilesDictionary { + d := NewProfilesDictionary() + d.StringTable().Append("", "test") + return d + }(), + dst: func() ProfilesDictionary { + d := NewProfilesDictionary() + d.StringTable().Append("", "foo") + return d + }(), + + wantFunction: func() Function { + fn := NewFunction() + fn.SetFilenameStrindex(2) + return fn + }(), + wantDictionary: func() ProfilesDictionary { + d := NewProfilesDictionary() + d.StringTable().Append("", "foo", "test") + return d + }(), + }, + { + name: "with a filename index that does not match anything", + function: func() Function { + fn := NewFunction() + fn.SetFilenameStrindex(1) + return fn + }(), + + src: NewProfilesDictionary(), + dst: NewProfilesDictionary(), + + wantFunction: func() Function { + fn := NewFunction() + fn.SetFilenameStrindex(1) + return fn + }(), + wantDictionary: NewProfilesDictionary(), + wantErr: errors.New("invalid filename index 1"), + }, + } { + t.Run(tt.name, func(t *testing.T) { + fn := tt.function + dst := tt.dst + err := fn.switchDictionary(tt.src, dst) + + if tt.wantErr == nil { + require.NoError(t, err) + } else { + require.Equal(t, tt.wantErr, err) + } + + assert.Equal(t, tt.wantFunction, fn) + assert.Equal(t, tt.wantDictionary, dst) + }) + } +} + func buildFunction(name, sName, fileName int32, startLine int64) Function { f := NewFunction() f.SetNameStrindex(name) diff --git a/pdata/pprofile/keyvalueandunit.go b/pdata/pprofile/keyvalueandunit.go index 3405074ba36..4ec70d911d7 100644 --- a/pdata/pprofile/keyvalueandunit.go +++ b/pdata/pprofile/keyvalueandunit.go @@ -3,6 +3,8 @@ package pprofile // import "go.opentelemetry.io/collector/pdata/pprofile" +import "fmt" + // Equal checks equality with another KeyValueAndUnit // It assumes both structs refer to the same dictionary. func (ms KeyValueAndUnit) Equal(val KeyValueAndUnit) bool { @@ -10,3 +12,33 @@ func (ms KeyValueAndUnit) Equal(val KeyValueAndUnit) bool { ms.UnitStrindex() == val.UnitStrindex() && ms.Value().Equal(val.Value()) } + +// switchDictionary updates the KeyValueAndUnit, switching its indices from one +// dictionary to another. +func (ms KeyValueAndUnit) switchDictionary(src, dst ProfilesDictionary) error { + if ms.KeyStrindex() > 0 { + if src.StringTable().Len() < int(ms.KeyStrindex()) { + return fmt.Errorf("invalid key index %d", ms.KeyStrindex()) + } + + idx, err := SetString(dst.StringTable(), src.StringTable().At(int(ms.KeyStrindex()))) + if err != nil { + return fmt.Errorf("couldn't set key: %w", err) + } + ms.SetKeyStrindex(idx) + } + + if ms.UnitStrindex() > 0 { + if src.StringTable().Len() < int(ms.UnitStrindex()) { + return fmt.Errorf("invalid unit index %d", ms.UnitStrindex()) + } + + idx, err := SetString(dst.StringTable(), src.StringTable().At(int(ms.UnitStrindex()))) + if err != nil { + return fmt.Errorf("couldn't set unit: %w", err) + } + ms.SetUnitStrindex(idx) + } + + return nil +} diff --git a/pdata/pprofile/keyvalueandunit_test.go b/pdata/pprofile/keyvalueandunit_test.go index 704db7bf506..131449d3f22 100644 --- a/pdata/pprofile/keyvalueandunit_test.go +++ b/pdata/pprofile/keyvalueandunit_test.go @@ -4,9 +4,11 @@ package pprofile import ( + "errors" "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "go.opentelemetry.io/collector/pdata/pcommon" ) @@ -59,6 +61,144 @@ func TestKeyValueAndUnitEqual(t *testing.T) { } } +func TestKeyValueAndUnitSwitchDictionary(t *testing.T) { + for _, tt := range []struct { + name string + keyValueAndUnit KeyValueAndUnit + + src ProfilesDictionary + dst ProfilesDictionary + + wantKeyValueAndUnit KeyValueAndUnit + wantDictionary ProfilesDictionary + wantErr error + }{ + { + name: "with an empty key value and unit", + keyValueAndUnit: NewKeyValueAndUnit(), + + src: NewProfilesDictionary(), + dst: NewProfilesDictionary(), + + wantKeyValueAndUnit: NewKeyValueAndUnit(), + wantDictionary: NewProfilesDictionary(), + }, + { + name: "with an existing key", + keyValueAndUnit: func() KeyValueAndUnit { + kvu := NewKeyValueAndUnit() + kvu.SetKeyStrindex(1) + return kvu + }(), + + src: func() ProfilesDictionary { + d := NewProfilesDictionary() + d.StringTable().Append("", "test") + return d + }(), + dst: func() ProfilesDictionary { + d := NewProfilesDictionary() + d.StringTable().Append("", "foo") + return d + }(), + + wantKeyValueAndUnit: func() KeyValueAndUnit { + kvu := NewKeyValueAndUnit() + kvu.SetKeyStrindex(2) + return kvu + }(), + wantDictionary: func() ProfilesDictionary { + d := NewProfilesDictionary() + d.StringTable().Append("", "foo", "test") + return d + }(), + }, + { + name: "with a key index that does not match anything", + keyValueAndUnit: func() KeyValueAndUnit { + kvu := NewKeyValueAndUnit() + kvu.SetKeyStrindex(1) + return kvu + }(), + + src: NewProfilesDictionary(), + dst: NewProfilesDictionary(), + + wantKeyValueAndUnit: func() KeyValueAndUnit { + kvu := NewKeyValueAndUnit() + kvu.SetKeyStrindex(1) + return kvu + }(), + wantDictionary: NewProfilesDictionary(), + wantErr: errors.New("invalid key index 1"), + }, + { + name: "with an existing unit", + keyValueAndUnit: func() KeyValueAndUnit { + kvu := NewKeyValueAndUnit() + kvu.SetUnitStrindex(1) + return kvu + }(), + + src: func() ProfilesDictionary { + d := NewProfilesDictionary() + d.StringTable().Append("", "test") + return d + }(), + dst: func() ProfilesDictionary { + d := NewProfilesDictionary() + d.StringTable().Append("", "foo") + return d + }(), + + wantKeyValueAndUnit: func() KeyValueAndUnit { + kvu := NewKeyValueAndUnit() + kvu.SetUnitStrindex(2) + return kvu + }(), + wantDictionary: func() ProfilesDictionary { + d := NewProfilesDictionary() + d.StringTable().Append("", "foo", "test") + return d + }(), + }, + { + name: "with a unit index that does not match anything", + keyValueAndUnit: func() KeyValueAndUnit { + kvu := NewKeyValueAndUnit() + kvu.SetUnitStrindex(1) + return kvu + }(), + + src: NewProfilesDictionary(), + dst: NewProfilesDictionary(), + + wantKeyValueAndUnit: func() KeyValueAndUnit { + kvu := NewKeyValueAndUnit() + kvu.SetUnitStrindex(1) + return kvu + }(), + wantDictionary: NewProfilesDictionary(), + wantErr: errors.New("invalid unit index 1"), + }, + } { + t.Run(tt.name, func(t *testing.T) { + kvu := tt.keyValueAndUnit + dst := tt.dst + err := kvu.switchDictionary(tt.src, dst) + + if tt.wantErr == nil { + require.NoError(t, err) + } else { + require.Equal(t, tt.wantErr, err) + } + + assert.Equal(t, tt.wantKeyValueAndUnit, kvu) + assert.Equal(t, tt.wantDictionary, dst) + }) + } +} + func buildKeyValueAndUnit(keyIdx, unitIdx int32, val pcommon.Value) KeyValueAndUnit { kvu := NewKeyValueAndUnit() kvu.SetKeyStrindex(keyIdx) diff --git a/pdata/pprofile/line.go b/pdata/pprofile/line.go index ae97b8f839e..57ba32bcfe6 100644 --- a/pdata/pprofile/line.go +++ b/pdata/pprofile/line.go @@ -3,6 +3,8 @@ package pprofile // import "go.opentelemetry.io/collector/pdata/pprofile" +import "fmt" + // Equal checks equality with another LineSlice func (l LineSlice) Equal(val LineSlice) bool { if l.Len() != val.Len() { @@ -24,3 +26,27 @@ func (l Line) Equal(val Line) bool { l.FunctionIndex() == val.FunctionIndex() && l.Line() == val.Line() } + +// switchDictionary updates the Line, switching its indices from one +// dictionary to another. +func (l Line) switchDictionary(src, dst ProfilesDictionary) error { + if l.FunctionIndex() > 0 { + if src.FunctionTable().Len() < int(l.FunctionIndex()) { + return fmt.Errorf("invalid function index %d", l.FunctionIndex()) + } + + fn := src.FunctionTable().At(int(l.FunctionIndex())) + err := fn.switchDictionary(src, dst) + if err != nil { + return fmt.Errorf("couldn't switch function dictionary: %w", err) + } + + idx, err := SetFunction(dst.FunctionTable(), fn) + if err != nil { + return fmt.Errorf("couldn't set function: %w", err) + } + l.SetFunctionIndex(idx) + } + + return nil +} diff --git a/pdata/pprofile/line_test.go b/pdata/pprofile/line_test.go index e40068487bd..a9f14d3cc7b 100644 --- a/pdata/pprofile/line_test.go +++ b/pdata/pprofile/line_test.go @@ -4,9 +4,11 @@ package pprofile import ( + "errors" "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestLineSliceEqual(t *testing.T) { @@ -119,6 +121,107 @@ func TestLineEqual(t *testing.T) { } } +func TestLineSwitchDictionary(t *testing.T) { + for _, tt := range []struct { + name string + line Line + + src ProfilesDictionary + dst ProfilesDictionary + + wantLine Line + wantDictionary ProfilesDictionary + wantErr error + }{ + { + name: "with an empty line", + line: NewLine(), + + src: NewProfilesDictionary(), + dst: NewProfilesDictionary(), + + wantLine: NewLine(), + wantDictionary: NewProfilesDictionary(), + }, + { + name: "with an existing function", + line: func() Line { + l := NewLine() + l.SetFunctionIndex(1) + return l + }(), + + src: func() ProfilesDictionary { + d := NewProfilesDictionary() + d.StringTable().Append("", "test") + + d.FunctionTable().AppendEmpty() + f := d.FunctionTable().AppendEmpty() + f.SetNameStrindex(1) + return d + }(), + dst: func() ProfilesDictionary { + d := NewProfilesDictionary() + d.StringTable().Append("", "foo") + + d.FunctionTable().AppendEmpty() + d.FunctionTable().AppendEmpty() + return d + }(), + + wantLine: func() Line { + l := NewLine() + l.SetFunctionIndex(2) + return l + }(), + wantDictionary: func() ProfilesDictionary { + d := NewProfilesDictionary() + d.StringTable().Append("", "foo", "test") + + d.FunctionTable().AppendEmpty() + d.FunctionTable().AppendEmpty() + f := d.FunctionTable().AppendEmpty() + f.SetNameStrindex(2) + return d + }(), + }, + { + name: "with a function index that does not match anything", + line: func() Line { + l := NewLine() + l.SetFunctionIndex(1) + return l + }(), + + src: NewProfilesDictionary(), + dst: NewProfilesDictionary(), + + wantLine: func() Line { + l := NewLine() + l.SetFunctionIndex(1) + return l + }(), + wantDictionary: NewProfilesDictionary(), + wantErr: errors.New("invalid function index 1"), + }, + } { + t.Run(tt.name, func(t *testing.T) { + line := tt.line + dst := tt.dst + err := line.switchDictionary(tt.src, dst) + + if tt.wantErr == nil { + require.NoError(t, err) + } else { + require.Equal(t, tt.wantErr, err) + } + + assert.Equal(t, tt.wantLine, line) + assert.Equal(t, tt.wantDictionary, dst) + }) + } +} + func buildLine(col int64, funcIdx int32, line int64) Line { l := NewLine() l.SetColumn(col) diff --git a/pdata/pprofile/location.go b/pdata/pprofile/location.go index 65ea38fac82..e63f52ce709 100644 --- a/pdata/pprofile/location.go +++ b/pdata/pprofile/location.go @@ -3,6 +3,8 @@ package pprofile // import "go.opentelemetry.io/collector/pdata/pprofile" +import "fmt" + // Equal checks equality with another Location func (ms Location) Equal(val Location) bool { return ms.MappingIndex() == val.MappingIndex() && @@ -10,3 +12,50 @@ func (ms Location) Equal(val Location) bool { ms.AttributeIndices().Equal(val.AttributeIndices()) && ms.Lines().Equal(val.Lines()) } + +// switchDictionary updates the Location, switching its indices from one +// dictionary to another. +func (ms Location) switchDictionary(src, dst ProfilesDictionary) error { + if ms.MappingIndex() > 0 { + if src.MappingTable().Len() < int(ms.MappingIndex()) { + return fmt.Errorf("invalid mapping index %d", ms.MappingIndex()) + } + + mapping := src.MappingTable().At(int(ms.MappingIndex())) + err := mapping.switchDictionary(src, dst) + if err != nil { + return fmt.Errorf("couldn't switch dictionary for mapping: %w", err) + } + idx, err := SetMapping(dst.MappingTable(), mapping) + if err != nil { + return fmt.Errorf("couldn't set mapping: %w", err) + } + ms.SetMappingIndex(idx) + } + + for i, v := range ms.AttributeIndices().All() { + if src.AttributeTable().Len() < int(v) { + return fmt.Errorf("invalid attribute index %d", v) + } + + attr := src.AttributeTable().At(int(v)) + err := attr.switchDictionary(src, dst) + if err != nil { + return fmt.Errorf("couldn't switch dictionary for attribute %d: %w", i, err) + } + idx, err := SetAttribute(dst.AttributeTable(), attr) + if err != nil { + return fmt.Errorf("couldn't set attribute %d: %w", i, err) + } + ms.AttributeIndices().SetAt(i, idx) + } + + for i, v := range ms.Lines().All() { + err := v.switchDictionary(src, dst) + if err != nil { + return fmt.Errorf("couldn't switch dictionary for line %d: %w", i, err) + } + } + + return nil +} diff --git a/pdata/pprofile/location_test.go b/pdata/pprofile/location_test.go index 2ac78706460..a057ad08eb7 100644 --- a/pdata/pprofile/location_test.go +++ b/pdata/pprofile/location_test.go @@ -4,9 +4,11 @@ package pprofile import ( + "errors" "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestLocationEqual(t *testing.T) { @@ -63,6 +65,212 @@ func TestLocationEqual(t *testing.T) { } } +func TestLocationSwitchDictionary(t *testing.T) { + for _, tt := range []struct { + name string + location Location + + src ProfilesDictionary + dst ProfilesDictionary + + wantLocation Location + wantDictionary ProfilesDictionary + wantErr error + }{ + { + name: "with an empty location", + location: NewLocation(), + + src: NewProfilesDictionary(), + dst: NewProfilesDictionary(), + + wantLocation: NewLocation(), + wantDictionary: NewProfilesDictionary(), + }, + { + name: "with an existing mapping", + location: func() Location { + l := NewLocation() + l.SetMappingIndex(1) + return l + }(), + + src: func() ProfilesDictionary { + d := NewProfilesDictionary() + d.StringTable().Append("", "test") + + d.MappingTable().AppendEmpty() + m := d.MappingTable().AppendEmpty() + m.SetFilenameStrindex(1) + return d + }(), + dst: func() ProfilesDictionary { + d := NewProfilesDictionary() + d.StringTable().Append("", "foo") + + d.MappingTable().AppendEmpty() + d.MappingTable().AppendEmpty() + return d + }(), + + wantLocation: func() Location { + l := NewLocation() + l.SetMappingIndex(2) + return l + }(), + wantDictionary: func() ProfilesDictionary { + d := NewProfilesDictionary() + d.StringTable().Append("", "foo", "test") + + d.MappingTable().AppendEmpty() + d.MappingTable().AppendEmpty() + m := d.MappingTable().AppendEmpty() + m.SetFilenameStrindex(2) + return d + }(), + }, + { + name: "with a mapping that cannot be found", + location: func() Location { + l := NewLocation() + l.SetMappingIndex(1) + return l + }(), + + src: NewProfilesDictionary(), + dst: NewProfilesDictionary(), + + wantLocation: func() Location { + l := NewLocation() + l.SetMappingIndex(1) + return l + }(), + wantDictionary: NewProfilesDictionary(), + wantErr: errors.New("invalid mapping index 1"), + }, + { + name: "with an existing attribute", + location: func() Location { + l := NewLocation() + l.AttributeIndices().Append(1) + return l + }(), + + src: func() ProfilesDictionary { + d := NewProfilesDictionary() + d.StringTable().Append("", "test") + + d.AttributeTable().AppendEmpty() + a := d.AttributeTable().AppendEmpty() + a.SetKeyStrindex(1) + + return d + }(), + dst: func() ProfilesDictionary { + d := NewProfilesDictionary() + d.StringTable().Append("", "foo") + + d.AttributeTable().AppendEmpty() + d.AttributeTable().AppendEmpty() + return d + }(), + + wantLocation: func() Location { + l := NewLocation() + l.AttributeIndices().Append(2) + return l + }(), + wantDictionary: func() ProfilesDictionary { + d := NewProfilesDictionary() + d.StringTable().Append("", "foo", "test") + + d.AttributeTable().AppendEmpty() + d.AttributeTable().AppendEmpty() + a := d.AttributeTable().AppendEmpty() + a.SetKeyStrindex(2) + return d + }(), + }, + { + name: "with an attribute index that does not match anything", + location: func() Location { + l := NewLocation() + l.AttributeIndices().Append(1) + return l + }(), + + src: NewProfilesDictionary(), + dst: NewProfilesDictionary(), + + wantLocation: func() Location { + l := NewLocation() + l.AttributeIndices().Append(1) + return l + }(), + wantDictionary: NewProfilesDictionary(), + wantErr: errors.New("invalid attribute index 1"), + }, + { + name: "with an existing line", + location: func() Location { + l := NewLocation() + l.Lines().AppendEmpty().SetFunctionIndex(1) + return l + }(), + + src: func() ProfilesDictionary { + d := NewProfilesDictionary() + d.StringTable().Append("", "test") + + d.FunctionTable().AppendEmpty() + f := d.FunctionTable().AppendEmpty() + f.SetNameStrindex(1) + + return d + }(), + dst: func() ProfilesDictionary { + d := NewProfilesDictionary() + d.StringTable().Append("", "foo") + + d.FunctionTable().AppendEmpty() + d.FunctionTable().AppendEmpty() + return d + }(), + + wantLocation: func() Location { + l := NewLocation() + l.Lines().AppendEmpty().SetFunctionIndex(2) + return l + }(), + wantDictionary: func() ProfilesDictionary { + d := NewProfilesDictionary() + d.StringTable().Append("", "foo", "test") + + d.FunctionTable().AppendEmpty() + d.FunctionTable().AppendEmpty() + f := d.FunctionTable().AppendEmpty() + f.SetNameStrindex(2) + return d + }(), + }, + } { + t.Run(tt.name, func(t *testing.T) { + l := tt.location + dst := tt.dst + err := l.switchDictionary(tt.src, dst) + + if tt.wantErr == nil { + require.NoError(t, err) + } else { + require.Equal(t, tt.wantErr, err) + } + + assert.Equal(t, tt.wantLocation, l) + assert.Equal(t, tt.wantDictionary, dst) + }) + } +} + func buildLocation(mapIdx int32, addr uint64, attrIdxs []int32, line Line) Location { l := NewLocation() l.SetMappingIndex(mapIdx) diff --git a/pdata/pprofile/mapping.go b/pdata/pprofile/mapping.go index 238a54e3f2b..7989f3c14ab 100644 --- a/pdata/pprofile/mapping.go +++ b/pdata/pprofile/mapping.go @@ -3,6 +3,8 @@ package pprofile // import "go.opentelemetry.io/collector/pdata/pprofile" +import "fmt" + // Equal checks equality with another Mapping func (ms Mapping) Equal(val Mapping) bool { return ms.MemoryStart() == val.MemoryStart() && @@ -11,3 +13,38 @@ func (ms Mapping) Equal(val Mapping) bool { ms.FilenameStrindex() == val.FilenameStrindex() && ms.AttributeIndices().Equal(val.AttributeIndices()) } + +// switchDictionary updates the Mapping, switching its indices from one +// dictionary to another. +func (ms Mapping) switchDictionary(src, dst ProfilesDictionary) error { + if ms.FilenameStrindex() > 0 { + if src.StringTable().Len() < int(ms.FilenameStrindex()) { + return fmt.Errorf("invalid filename index %d", ms.FilenameStrindex()) + } + + idx, err := SetString(dst.StringTable(), src.StringTable().At(int(ms.FilenameStrindex()))) + if err != nil { + return fmt.Errorf("couldn't set filename: %w", err) + } + ms.SetFilenameStrindex(idx) + } + + for i, v := range ms.AttributeIndices().All() { + if src.AttributeTable().Len() < int(v) { + return fmt.Errorf("invalid attribute index %d", v) + } + + attr := src.AttributeTable().At(int(v)) + err := attr.switchDictionary(src, dst) + if err != nil { + return fmt.Errorf("couldn't switch dictionary for attribute %d: %w", i, err) + } + idx, err := SetAttribute(dst.AttributeTable(), attr) + if err != nil { + return fmt.Errorf("couldn't set attribute %d: %w", i, err) + } + ms.AttributeIndices().SetAt(i, idx) + } + + return nil +} diff --git a/pdata/pprofile/mapping_test.go b/pdata/pprofile/mapping_test.go index 8e43e9b71a6..25c186f5da0 100644 --- a/pdata/pprofile/mapping_test.go +++ b/pdata/pprofile/mapping_test.go @@ -4,9 +4,11 @@ package pprofile import ( + "errors" "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestMappingEqual(t *testing.T) { @@ -69,6 +71,157 @@ func TestMappingEqual(t *testing.T) { } } +func TestMappingSwitchDictionary(t *testing.T) { + for _, tt := range []struct { + name string + mapping Mapping + + src ProfilesDictionary + dst ProfilesDictionary + + wantMapping Mapping + wantDictionary ProfilesDictionary + wantErr error + }{ + { + name: "with an empty mapping", + mapping: NewMapping(), + + src: NewProfilesDictionary(), + dst: NewProfilesDictionary(), + + wantMapping: NewMapping(), + wantDictionary: NewProfilesDictionary(), + }, + { + name: "with an existing filename", + mapping: func() Mapping { + m := NewMapping() + m.SetFilenameStrindex(1) + return m + }(), + + src: func() ProfilesDictionary { + d := NewProfilesDictionary() + d.StringTable().Append("", "test") + return d + }(), + dst: func() ProfilesDictionary { + d := NewProfilesDictionary() + d.StringTable().Append("", "foo") + return d + }(), + + wantMapping: func() Mapping { + m := NewMapping() + m.SetFilenameStrindex(2) + return m + }(), + wantDictionary: func() ProfilesDictionary { + d := NewProfilesDictionary() + d.StringTable().Append("", "foo", "test") + return d + }(), + }, + { + name: "with a filename index that does not match anything", + mapping: func() Mapping { + m := NewMapping() + m.SetFilenameStrindex(1) + return m + }(), + + src: NewProfilesDictionary(), + dst: NewProfilesDictionary(), + + wantMapping: func() Mapping { + m := NewMapping() + m.SetFilenameStrindex(1) + return m + }(), + wantDictionary: NewProfilesDictionary(), + wantErr: errors.New("invalid filename index 1"), + }, + { + name: "with an existing attribute", + mapping: func() Mapping { + m := NewMapping() + m.AttributeIndices().Append(1) + return m + }(), + + src: func() ProfilesDictionary { + d := NewProfilesDictionary() + d.StringTable().Append("", "test") + + d.AttributeTable().AppendEmpty() + a := d.AttributeTable().AppendEmpty() + a.SetKeyStrindex(1) + + return d + }(), + dst: func() ProfilesDictionary { + d := NewProfilesDictionary() + d.StringTable().Append("", "foo") + + d.AttributeTable().AppendEmpty() + d.AttributeTable().AppendEmpty() + return d + }(), + + wantMapping: func() Mapping { + m := NewMapping() + m.AttributeIndices().Append(2) + return m + }(), + wantDictionary: func() ProfilesDictionary { + d := NewProfilesDictionary() + d.StringTable().Append("", "foo", "test") + + d.AttributeTable().AppendEmpty() + d.AttributeTable().AppendEmpty() + a := d.AttributeTable().AppendEmpty() + a.SetKeyStrindex(2) + return d + }(), + }, + { + name: "with an attribute index that does not match anything", + mapping: func() Mapping { + m := NewMapping() + m.AttributeIndices().Append(1) + return m + }(), + + src: NewProfilesDictionary(), + dst: NewProfilesDictionary(), + + wantMapping: func() Mapping { + m := NewMapping() + m.AttributeIndices().Append(1) + return m + }(), + wantDictionary: NewProfilesDictionary(), + wantErr: errors.New("invalid attribute index 1"), + }, + } { + t.Run(tt.name, func(t *testing.T) { + m := tt.mapping + dst := tt.dst + err := m.switchDictionary(tt.src, dst) + + if tt.wantErr == nil { + require.NoError(t, err) + } else { + require.Equal(t, tt.wantErr, err) + } + + assert.Equal(t, tt.wantMapping, m) + assert.Equal(t, tt.wantDictionary, dst) + }) + } +} + func buildMapping(memStart, memLimit, fileOffset uint64, filenameIdx int32, attrIdxs []int32) Mapping { m := NewMapping() m.SetMemoryStart(memStart) diff --git a/pdata/pprofile/profile.go b/pdata/pprofile/profile.go new file mode 100644 index 00000000000..6b216a9e386 --- /dev/null +++ b/pdata/pprofile/profile.go @@ -0,0 +1,45 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package pprofile // import "go.opentelemetry.io/collector/pdata/pprofile" + +import "fmt" + +// switchDictionary updates the Profile, switching its indices from one +// dictionary to another. +func (ms Profile) switchDictionary(src, dst ProfilesDictionary) error { + for i, v := range ms.AttributeIndices().All() { + if src.AttributeTable().Len() < int(v) { + return fmt.Errorf("invalid attribute index %d", v) + } + + attr := src.AttributeTable().At(int(v)) + err := attr.switchDictionary(src, dst) + if err != nil { + return fmt.Errorf("couldn't switch dictionary for attribute %d: %w", i, err) + } + idx, err := SetAttribute(dst.AttributeTable(), attr) + if err != nil { + return fmt.Errorf("couldn't set attribute %d: %w", i, err) + } + ms.AttributeIndices().SetAt(i, idx) + } + + for i, v := range ms.Samples().All() { + err := v.switchDictionary(src, dst) + if err != nil { + return fmt.Errorf("error switching dictionary for sample %d: %w", i, err) + } + } + + err := ms.PeriodType().switchDictionary(src, dst) + if err != nil { + return fmt.Errorf("error switching dictionary for period type: %w", err) + } + err = ms.SampleType().switchDictionary(src, dst) + if err != nil { + return fmt.Errorf("error switching dictionary for sample type: %w", err) + } + + return nil +} diff --git a/pdata/pprofile/profile_test.go b/pdata/pprofile/profile_test.go new file mode 100644 index 00000000000..0dd380782f7 --- /dev/null +++ b/pdata/pprofile/profile_test.go @@ -0,0 +1,212 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package pprofile + +import ( + "errors" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "go.opentelemetry.io/collector/pdata/pcommon" +) + +func TestProfileSwitchDictionary(t *testing.T) { + for _, tt := range []struct { + name string + profile Profile + + src ProfilesDictionary + dst ProfilesDictionary + + wantProfile Profile + wantDictionary ProfilesDictionary + wantErr error + }{ + { + name: "with an empty profile", + profile: NewProfile(), + + src: NewProfilesDictionary(), + dst: NewProfilesDictionary(), + + wantProfile: NewProfile(), + wantDictionary: NewProfilesDictionary(), + }, + { + name: "with an existing attribute", + profile: func() Profile { + p := NewProfile() + p.AttributeIndices().Append(1) + return p + }(), + + src: func() ProfilesDictionary { + d := NewProfilesDictionary() + d.StringTable().Append("", "test") + + d.AttributeTable().AppendEmpty() + a := d.AttributeTable().AppendEmpty() + a.SetKeyStrindex(1) + + return d + }(), + dst: func() ProfilesDictionary { + d := NewProfilesDictionary() + d.StringTable().Append("", "foo") + + d.AttributeTable().AppendEmpty() + d.AttributeTable().AppendEmpty() + return d + }(), + + wantProfile: func() Profile { + p := NewProfile() + p.AttributeIndices().Append(2) + return p + }(), + wantDictionary: func() ProfilesDictionary { + d := NewProfilesDictionary() + d.StringTable().Append("", "foo", "test") + + d.AttributeTable().AppendEmpty() + d.AttributeTable().AppendEmpty() + a := d.AttributeTable().AppendEmpty() + a.SetKeyStrindex(2) + return d + }(), + }, + { + name: "with an attribute index that does not match anything", + profile: func() Profile { + p := NewProfile() + p.AttributeIndices().Append(1) + return p + }(), + + src: NewProfilesDictionary(), + dst: NewProfilesDictionary(), + + wantProfile: func() Profile { + p := NewProfile() + p.AttributeIndices().Append(1) + return p + }(), + wantDictionary: NewProfilesDictionary(), + wantErr: errors.New("invalid attribute index 1"), + }, + { + name: "with a profile that has a sample", + profile: func() Profile { + p := NewProfile() + p.Samples().AppendEmpty().SetLinkIndex(1) + return p + }(), + + src: func() ProfilesDictionary { + d := NewProfilesDictionary() + d.LinkTable().AppendEmpty() + l := d.LinkTable().AppendEmpty() + l.SetSpanID(pcommon.SpanID([8]byte{1, 2, 3, 4, 5, 6, 7, 8})) + return d + }(), + dst: func() ProfilesDictionary { + d := NewProfilesDictionary() + d.LinkTable().AppendEmpty() + d.LinkTable().AppendEmpty() + return d + }(), + + wantProfile: func() Profile { + p := NewProfile() + p.Samples().AppendEmpty().SetLinkIndex(2) + return p + }(), + wantDictionary: func() ProfilesDictionary { + d := NewProfilesDictionary() + d.LinkTable().AppendEmpty() + d.LinkTable().AppendEmpty() + l := d.LinkTable().AppendEmpty() + l.SetSpanID(pcommon.SpanID([8]byte{1, 2, 3, 4, 5, 6, 7, 8})) + return d + }(), + }, + { + name: "with a profile that has a period type", + profile: func() Profile { + p := NewProfile() + p.PeriodType().SetTypeStrindex(1) + return p + }(), + + src: func() ProfilesDictionary { + d := NewProfilesDictionary() + d.StringTable().Append("", "test") + return d + }(), + dst: func() ProfilesDictionary { + d := NewProfilesDictionary() + d.StringTable().Append("", "foo") + return d + }(), + + wantProfile: func() Profile { + p := NewProfile() + p.PeriodType().SetTypeStrindex(2) + return p + }(), + wantDictionary: func() ProfilesDictionary { + d := NewProfilesDictionary() + d.StringTable().Append("", "foo", "test") + return d + }(), + }, + { + name: "with a profile that has a sample type", + profile: func() Profile { + p := NewProfile() + p.SampleType().SetTypeStrindex(1) + return p + }(), + + src: func() ProfilesDictionary { + d := NewProfilesDictionary() + d.StringTable().Append("", "test") + return d + }(), + dst: func() ProfilesDictionary { + d := NewProfilesDictionary() + d.StringTable().Append("", "foo") + return d + }(), + + wantProfile: func() Profile { + p := NewProfile() + p.SampleType().SetTypeStrindex(2) + return p + }(), + wantDictionary: func() ProfilesDictionary { + d := NewProfilesDictionary() + d.StringTable().Append("", "foo", "test") + return d + }(), + }, + } { + t.Run(tt.name, func(t *testing.T) { + profile := tt.profile + dst := tt.dst + err := profile.switchDictionary(tt.src, dst) + + if tt.wantErr == nil { + require.NoError(t, err) + } else { + require.Equal(t, tt.wantErr, err) + } + + assert.Equal(t, tt.wantProfile, profile) + assert.Equal(t, tt.wantDictionary, dst) + }) + } +} diff --git a/pdata/pprofile/profiles.go b/pdata/pprofile/profiles.go index f6d50148bb7..285ee1516cb 100644 --- a/pdata/pprofile/profiles.go +++ b/pdata/pprofile/profiles.go @@ -3,6 +3,8 @@ package pprofile // import "go.opentelemetry.io/collector/pdata/pprofile" +import "fmt" + // MarkReadOnly marks the ResourceProfiles as shared so that no further modifications can be done on it. func (ms Profiles) MarkReadOnly() { ms.getState().MarkReadOnly() @@ -29,3 +31,16 @@ func (ms Profiles) SampleCount() int { } return sampleCount } + +// switchDictionary updates the Profiles, switching its indices from one +// dictionary to another. +func (ms Profiles) switchDictionary(src, dst ProfilesDictionary) error { + for i, v := range ms.ResourceProfiles().All() { + err := v.switchDictionary(src, dst) + if err != nil { + return fmt.Errorf("error switching dictionary for resource profile %d: %w", i, err) + } + } + + return nil +} diff --git a/pdata/pprofile/profiles_test.go b/pdata/pprofile/profiles_test.go index 41ab52ba165..bb718b6ceea 100644 --- a/pdata/pprofile/profiles_test.go +++ b/pdata/pprofile/profiles_test.go @@ -90,6 +90,84 @@ func TestSampleCountWithEmpty(t *testing.T) { }, new(internal.State)).SampleCount()) } +func TestProfilesSwitchDictionary(t *testing.T) { + for _, tt := range []struct { + name string + profiles Profiles + + src ProfilesDictionary + dst ProfilesDictionary + + wantProfiles Profiles + wantDictionary ProfilesDictionary + wantErr error + }{ + { + name: "with an empty profiles", + profiles: NewProfiles(), + + src: NewProfilesDictionary(), + dst: NewProfilesDictionary(), + + wantProfiles: NewProfiles(), + wantDictionary: NewProfilesDictionary(), + }, + { + name: "with a profiles that has a profile", + profiles: func() Profiles { + p := NewProfiles() + profile := p.ResourceProfiles().AppendEmpty().ScopeProfiles().AppendEmpty().Profiles().AppendEmpty() + profile.Samples().AppendEmpty().SetLinkIndex(1) + return p + }(), + + src: func() ProfilesDictionary { + d := NewProfilesDictionary() + d.LinkTable().AppendEmpty() + l := d.LinkTable().AppendEmpty() + l.SetSpanID(pcommon.SpanID([8]byte{1, 2, 3, 4, 5, 6, 7, 8})) + return d + }(), + dst: func() ProfilesDictionary { + d := NewProfilesDictionary() + d.LinkTable().AppendEmpty() + d.LinkTable().AppendEmpty() + return d + }(), + + wantProfiles: func() Profiles { + p := NewProfiles() + profile := p.ResourceProfiles().AppendEmpty().ScopeProfiles().AppendEmpty().Profiles().AppendEmpty() + profile.Samples().AppendEmpty().SetLinkIndex(2) + return p + }(), + wantDictionary: func() ProfilesDictionary { + d := NewProfilesDictionary() + d.LinkTable().AppendEmpty() + d.LinkTable().AppendEmpty() + l := d.LinkTable().AppendEmpty() + l.SetSpanID(pcommon.SpanID([8]byte{1, 2, 3, 4, 5, 6, 7, 8})) + return d + }(), + }, + } { + t.Run(tt.name, func(t *testing.T) { + p := tt.profiles + dst := tt.dst + err := p.switchDictionary(tt.src, dst) + + if tt.wantErr == nil { + require.NoError(t, err) + } else { + require.Equal(t, tt.wantErr, err) + } + + assert.Equal(t, tt.wantProfiles, p) + assert.Equal(t, tt.wantDictionary, dst) + }) + } +} + func BenchmarkProfilesUsage(b *testing.B) { pd := generateTestProfiles() ts := pcommon.NewTimestampFromTime(time.Now()) diff --git a/pdata/pprofile/resourceprofiles.go b/pdata/pprofile/resourceprofiles.go new file mode 100644 index 00000000000..3866b5f719c --- /dev/null +++ b/pdata/pprofile/resourceprofiles.go @@ -0,0 +1,19 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package pprofile // import "go.opentelemetry.io/collector/pdata/pprofile" + +import "fmt" + +// switchDictionary updates the ResourceProfiles, switching its indices from one +// dictionary to another. +func (ms ResourceProfiles) switchDictionary(src, dst ProfilesDictionary) error { + for i, v := range ms.ScopeProfiles().All() { + err := v.switchDictionary(src, dst) + if err != nil { + return fmt.Errorf("error switching dictionary for scope profile %d: %w", i, err) + } + } + + return nil +} diff --git a/pdata/pprofile/resourceprofiles_test.go b/pdata/pprofile/resourceprofiles_test.go new file mode 100644 index 00000000000..bdf1deee956 --- /dev/null +++ b/pdata/pprofile/resourceprofiles_test.go @@ -0,0 +1,91 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package pprofile + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "go.opentelemetry.io/collector/pdata/pcommon" +) + +func TestResourceProfilesSwitchDictionary(t *testing.T) { + for _, tt := range []struct { + name string + resourceProfiles ResourceProfiles + + src ProfilesDictionary + dst ProfilesDictionary + + wantResourceProfiles ResourceProfiles + wantDictionary ProfilesDictionary + wantErr error + }{ + { + name: "with an empty resource profile", + resourceProfiles: NewResourceProfiles(), + + src: NewProfilesDictionary(), + dst: NewProfilesDictionary(), + + wantResourceProfiles: NewResourceProfiles(), + wantDictionary: NewProfilesDictionary(), + }, + { + name: "with a resource profiles that has a profile", + resourceProfiles: func() ResourceProfiles { + r := NewResourceProfiles() + profile := r.ScopeProfiles().AppendEmpty().Profiles().AppendEmpty() + profile.Samples().AppendEmpty().SetLinkIndex(1) + return r + }(), + + src: func() ProfilesDictionary { + d := NewProfilesDictionary() + d.LinkTable().AppendEmpty() + l := d.LinkTable().AppendEmpty() + l.SetSpanID(pcommon.SpanID([8]byte{1, 2, 3, 4, 5, 6, 7, 8})) + return d + }(), + dst: func() ProfilesDictionary { + d := NewProfilesDictionary() + d.LinkTable().AppendEmpty() + d.LinkTable().AppendEmpty() + return d + }(), + + wantResourceProfiles: func() ResourceProfiles { + r := NewResourceProfiles() + profile := r.ScopeProfiles().AppendEmpty().Profiles().AppendEmpty() + profile.Samples().AppendEmpty().SetLinkIndex(2) + return r + }(), + wantDictionary: func() ProfilesDictionary { + d := NewProfilesDictionary() + d.LinkTable().AppendEmpty() + d.LinkTable().AppendEmpty() + l := d.LinkTable().AppendEmpty() + l.SetSpanID(pcommon.SpanID([8]byte{1, 2, 3, 4, 5, 6, 7, 8})) + return d + }(), + }, + } { + t.Run(tt.name, func(t *testing.T) { + rp := tt.resourceProfiles + dst := tt.dst + err := rp.switchDictionary(tt.src, dst) + + if tt.wantErr == nil { + require.NoError(t, err) + } else { + require.Equal(t, tt.wantErr, err) + } + + assert.Equal(t, tt.wantResourceProfiles, rp) + assert.Equal(t, tt.wantDictionary, dst) + }) + } +} diff --git a/pdata/pprofile/sample.go b/pdata/pprofile/sample.go new file mode 100644 index 00000000000..a0bbd29ffea --- /dev/null +++ b/pdata/pprofile/sample.go @@ -0,0 +1,59 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package pprofile // import "go.opentelemetry.io/collector/pdata/pprofile" + +import "fmt" + +// switchDictionary updates the Sample, switching its indices from one +// dictionary to another. +func (ms Sample) switchDictionary(src, dst ProfilesDictionary) error { + for i, v := range ms.AttributeIndices().All() { + if src.AttributeTable().Len() < int(v) { + return fmt.Errorf("invalid attribute index %d", v) + } + + attr := src.AttributeTable().At(int(v)) + err := attr.switchDictionary(src, dst) + if err != nil { + return fmt.Errorf("couldn't switch dictionary for attribute %d: %w", i, err) + } + idx, err := SetAttribute(dst.AttributeTable(), attr) + if err != nil { + return fmt.Errorf("couldn't set attribute %d: %w", i, err) + } + ms.AttributeIndices().SetAt(i, idx) + } + + if ms.LinkIndex() > 0 { + if src.LinkTable().Len() < int(ms.LinkIndex()) { + return fmt.Errorf("invalid link index %d", ms.LinkIndex()) + } + + idx, err := SetLink(dst.LinkTable(), src.LinkTable().At(int(ms.LinkIndex()))) + if err != nil { + return fmt.Errorf("couldn't set link: %w", err) + } + ms.SetLinkIndex(idx) + } + + if ms.StackIndex() > 0 { + if src.StackTable().Len() < int(ms.StackIndex()) { + return fmt.Errorf("invalid stack index %d", ms.StackIndex()) + } + + stack := src.StackTable().At(int(ms.StackIndex())) + err := stack.switchDictionary(src, dst) + if err != nil { + return fmt.Errorf("couldn't switch stack dictionary: %w", err) + } + + idx, err := SetStack(dst.StackTable(), stack) + if err != nil { + return fmt.Errorf("couldn't set stack: %w", err) + } + ms.SetStackIndex(idx) + } + + return nil +} diff --git a/pdata/pprofile/sample_test.go b/pdata/pprofile/sample_test.go new file mode 100644 index 00000000000..548ba2d2f51 --- /dev/null +++ b/pdata/pprofile/sample_test.go @@ -0,0 +1,231 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package pprofile + +import ( + "errors" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "go.opentelemetry.io/collector/pdata/pcommon" +) + +func TestSampleSwitchDictionary(t *testing.T) { + for _, tt := range []struct { + name string + sample Sample + + src ProfilesDictionary + dst ProfilesDictionary + + wantSample Sample + wantDictionary ProfilesDictionary + wantErr error + }{ + { + name: "with an empty sample", + sample: NewSample(), + + src: NewProfilesDictionary(), + dst: NewProfilesDictionary(), + + wantSample: NewSample(), + wantDictionary: NewProfilesDictionary(), + }, + { + name: "with an existing attribute", + sample: func() Sample { + s := NewSample() + s.AttributeIndices().Append(1) + return s + }(), + + src: func() ProfilesDictionary { + d := NewProfilesDictionary() + d.StringTable().Append("", "test") + + d.AttributeTable().AppendEmpty() + a := d.AttributeTable().AppendEmpty() + a.SetKeyStrindex(1) + + return d + }(), + dst: func() ProfilesDictionary { + d := NewProfilesDictionary() + d.StringTable().Append("", "foo") + + d.AttributeTable().AppendEmpty() + d.AttributeTable().AppendEmpty() + return d + }(), + + wantSample: func() Sample { + s := NewSample() + s.AttributeIndices().Append(2) + return s + }(), + wantDictionary: func() ProfilesDictionary { + d := NewProfilesDictionary() + d.StringTable().Append("", "foo", "test") + + d.AttributeTable().AppendEmpty() + d.AttributeTable().AppendEmpty() + a := d.AttributeTable().AppendEmpty() + a.SetKeyStrindex(2) + return d + }(), + }, + { + name: "with an attribute index that does not match anything", + sample: func() Sample { + s := NewSample() + s.AttributeIndices().Append(1) + return s + }(), + + src: NewProfilesDictionary(), + dst: NewProfilesDictionary(), + + wantSample: func() Sample { + s := NewSample() + s.AttributeIndices().Append(1) + return s + }(), + wantDictionary: NewProfilesDictionary(), + wantErr: errors.New("invalid attribute index 1"), + }, + { + name: "with an existing link", + sample: func() Sample { + s := NewSample() + s.SetLinkIndex(1) + return s + }(), + + src: func() ProfilesDictionary { + d := NewProfilesDictionary() + d.LinkTable().AppendEmpty() + l := d.LinkTable().AppendEmpty() + l.SetSpanID(pcommon.SpanID([8]byte{1, 2, 3, 4, 5, 6, 7, 8})) + return d + }(), + dst: func() ProfilesDictionary { + d := NewProfilesDictionary() + d.LinkTable().AppendEmpty() + d.LinkTable().AppendEmpty() + return d + }(), + + wantSample: func() Sample { + s := NewSample() + s.SetLinkIndex(2) + return s + }(), + wantDictionary: func() ProfilesDictionary { + d := NewProfilesDictionary() + d.LinkTable().AppendEmpty() + d.LinkTable().AppendEmpty() + l := d.LinkTable().AppendEmpty() + l.SetSpanID(pcommon.SpanID([8]byte{1, 2, 3, 4, 5, 6, 7, 8})) + return d + }(), + }, + { + name: "with a link index that does not match anything", + sample: func() Sample { + s := NewSample() + s.SetLinkIndex(1) + return s + }(), + + src: NewProfilesDictionary(), + dst: NewProfilesDictionary(), + + wantSample: func() Sample { + s := NewSample() + s.SetLinkIndex(1) + return s + }(), + wantDictionary: NewProfilesDictionary(), + wantErr: errors.New("invalid link index 1"), + }, + { + name: "with an existing stack", + sample: func() Sample { + s := NewSample() + s.SetStackIndex(1) + return s + }(), + + src: func() ProfilesDictionary { + d := NewProfilesDictionary() + d.LocationTable().AppendEmpty().SetAddress(1) + d.LocationTable().AppendEmpty().SetAddress(2) + + d.StackTable().AppendEmpty() + s := d.StackTable().AppendEmpty() + s.LocationIndices().Append(1) + return d + }(), + dst: func() ProfilesDictionary { + d := NewProfilesDictionary() + d.StackTable().AppendEmpty() + d.StackTable().AppendEmpty() + return d + }(), + + wantSample: func() Sample { + s := NewSample() + s.SetStackIndex(2) + return s + }(), + wantDictionary: func() ProfilesDictionary { + d := NewProfilesDictionary() + d.LocationTable().AppendEmpty().SetAddress(2) + + d.StackTable().AppendEmpty() + d.StackTable().AppendEmpty() + s := d.StackTable().AppendEmpty() + s.LocationIndices().Append(0) + return d + }(), + }, + { + name: "with a stack index that does not match anything", + sample: func() Sample { + s := NewSample() + s.SetStackIndex(1) + return s + }(), + + src: NewProfilesDictionary(), + dst: NewProfilesDictionary(), + + wantSample: func() Sample { + s := NewSample() + s.SetStackIndex(1) + return s + }(), + wantDictionary: NewProfilesDictionary(), + wantErr: errors.New("invalid stack index 1"), + }, + } { + t.Run(tt.name, func(t *testing.T) { + sample := tt.sample + dst := tt.dst + err := sample.switchDictionary(tt.src, dst) + + if tt.wantErr == nil { + require.NoError(t, err) + } else { + require.Equal(t, tt.wantErr, err) + } + + assert.Equal(t, tt.wantSample, sample) + assert.Equal(t, tt.wantDictionary, dst) + }) + } +} diff --git a/pdata/pprofile/scopeprofiles.go b/pdata/pprofile/scopeprofiles.go new file mode 100644 index 00000000000..89d098c23f5 --- /dev/null +++ b/pdata/pprofile/scopeprofiles.go @@ -0,0 +1,19 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package pprofile // import "go.opentelemetry.io/collector/pdata/pprofile" + +import "fmt" + +// switchDictionary updates the ScopeProfiles, switching its indices from one +// dictionary to another. +func (ms ScopeProfiles) switchDictionary(src, dst ProfilesDictionary) error { + for i, v := range ms.Profiles().All() { + err := v.switchDictionary(src, dst) + if err != nil { + return fmt.Errorf("error switching dictionary for profile %d: %w", i, err) + } + } + + return nil +} diff --git a/pdata/pprofile/scopeprofiles_test.go b/pdata/pprofile/scopeprofiles_test.go new file mode 100644 index 00000000000..784146eb245 --- /dev/null +++ b/pdata/pprofile/scopeprofiles_test.go @@ -0,0 +1,91 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package pprofile + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "go.opentelemetry.io/collector/pdata/pcommon" +) + +func TestScopeProfilesSwitchDictionary(t *testing.T) { + for _, tt := range []struct { + name string + scopeProfiles ScopeProfiles + + src ProfilesDictionary + dst ProfilesDictionary + + wantScopeProfiles ScopeProfiles + wantDictionary ProfilesDictionary + wantErr error + }{ + { + name: "with an empty scope profile", + scopeProfiles: NewScopeProfiles(), + + src: NewProfilesDictionary(), + dst: NewProfilesDictionary(), + + wantScopeProfiles: NewScopeProfiles(), + wantDictionary: NewProfilesDictionary(), + }, + { + name: "with a scope profiles that has a profile", + scopeProfiles: func() ScopeProfiles { + s := NewScopeProfiles() + profile := s.Profiles().AppendEmpty() + profile.Samples().AppendEmpty().SetLinkIndex(1) + return s + }(), + + src: func() ProfilesDictionary { + d := NewProfilesDictionary() + d.LinkTable().AppendEmpty() + l := d.LinkTable().AppendEmpty() + l.SetSpanID(pcommon.SpanID([8]byte{1, 2, 3, 4, 5, 6, 7, 8})) + return d + }(), + dst: func() ProfilesDictionary { + d := NewProfilesDictionary() + d.LinkTable().AppendEmpty() + d.LinkTable().AppendEmpty() + return d + }(), + + wantScopeProfiles: func() ScopeProfiles { + s := NewScopeProfiles() + profile := s.Profiles().AppendEmpty() + profile.Samples().AppendEmpty().SetLinkIndex(2) + return s + }(), + wantDictionary: func() ProfilesDictionary { + d := NewProfilesDictionary() + d.LinkTable().AppendEmpty() + d.LinkTable().AppendEmpty() + l := d.LinkTable().AppendEmpty() + l.SetSpanID(pcommon.SpanID([8]byte{1, 2, 3, 4, 5, 6, 7, 8})) + return d + }(), + }, + } { + t.Run(tt.name, func(t *testing.T) { + sp := tt.scopeProfiles + dst := tt.dst + err := sp.switchDictionary(tt.src, dst) + + if tt.wantErr == nil { + require.NoError(t, err) + } else { + require.Equal(t, tt.wantErr, err) + } + + assert.Equal(t, tt.wantScopeProfiles, sp) + assert.Equal(t, tt.wantDictionary, dst) + }) + } +} diff --git a/pdata/pprofile/stack.go b/pdata/pprofile/stack.go index 3629b0ee995..87fd216ef3e 100644 --- a/pdata/pprofile/stack.go +++ b/pdata/pprofile/stack.go @@ -3,6 +3,10 @@ package pprofile // import "go.opentelemetry.io/collector/pdata/pprofile" +import ( + "fmt" +) + // Equal checks equality with another Stack func (ms Stack) Equal(val Stack) bool { if ms.LocationIndices().Len() != val.LocationIndices().Len() { @@ -17,3 +21,26 @@ func (ms Stack) Equal(val Stack) bool { return true } + +// switchDictionary updates the Stack, switching its indices from one +// dictionary to another. +func (ms Stack) switchDictionary(src, dst ProfilesDictionary) error { + for i, v := range ms.LocationIndices().All() { + if src.LocationTable().Len() < int(v) { + return fmt.Errorf("invalid location index %d", v) + } + + loc := src.LocationTable().At(int(v)) + err := loc.switchDictionary(src, dst) + if err != nil { + return fmt.Errorf("couldn't switch dictionary for location: %w", err) + } + idx, err := SetLocation(dst.LocationTable(), loc) + if err != nil { + return fmt.Errorf("couldn't set location %d: %w", i, err) + } + ms.LocationIndices().SetAt(i, idx) + } + + return nil +} diff --git a/pdata/pprofile/stack_test.go b/pdata/pprofile/stack_test.go index 887638b8cdc..ee2927c1732 100644 --- a/pdata/pprofile/stack_test.go +++ b/pdata/pprofile/stack_test.go @@ -4,9 +4,11 @@ package pprofile import ( + "errors" "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestStackEqual(t *testing.T) { @@ -70,3 +72,126 @@ func TestStackEqual(t *testing.T) { }) } } + +func TestStackSwitchDictionary(t *testing.T) { + for _, tt := range []struct { + name string + stack Stack + + src ProfilesDictionary + dst ProfilesDictionary + + wantStack Stack + wantDictionary ProfilesDictionary + wantErr error + }{ + { + name: "with an empty stack", + stack: NewStack(), + + src: NewProfilesDictionary(), + dst: NewProfilesDictionary(), + + wantStack: NewStack(), + wantDictionary: NewProfilesDictionary(), + }, + { + name: "with an existing location", + stack: func() Stack { + s := NewStack() + s.LocationIndices().Append(0) + return s + }(), + + src: func() ProfilesDictionary { + d := NewProfilesDictionary() + loc := d.LocationTable().AppendEmpty() + loc.SetAddress(42) + return d + }(), + dst: NewProfilesDictionary(), + + wantStack: func() Stack { + s := NewStack() + s.LocationIndices().Append(0) + return s + }(), + wantDictionary: func() ProfilesDictionary { + d := NewProfilesDictionary() + loc := d.LocationTable().AppendEmpty() + loc.SetAddress(42) + return d + }(), + }, + { + name: "with an existing location that needs a new indice", + stack: func() Stack { + s := NewStack() + s.LocationIndices().Append(0) + return s + }(), + + src: func() ProfilesDictionary { + d := NewProfilesDictionary() + loc := d.LocationTable().AppendEmpty() + loc.SetAddress(42) + return d + }(), + dst: func() ProfilesDictionary { + d := NewProfilesDictionary() + loc := d.LocationTable().AppendEmpty() + loc.SetAddress(2) + return d + }(), + + wantStack: func() Stack { + s := NewStack() + s.LocationIndices().Append(1) + return s + }(), + wantDictionary: func() ProfilesDictionary { + d := NewProfilesDictionary() + loc := d.LocationTable().AppendEmpty() + loc.SetAddress(2) + loc = d.LocationTable().AppendEmpty() + loc.SetAddress(42) + return d + }(), + }, + { + name: "with a location index that does not match anything", + stack: func() Stack { + s := NewStack() + s.LocationIndices().Append(2) + return s + }(), + + src: NewProfilesDictionary(), + dst: NewProfilesDictionary(), + + wantStack: func() Stack { + s := NewStack() + s.LocationIndices().Append(2) + return s + }(), + wantDictionary: NewProfilesDictionary(), + + wantErr: errors.New("invalid location index 2"), + }, + } { + t.Run(tt.name, func(t *testing.T) { + stack := tt.stack + dst := tt.dst + err := stack.switchDictionary(tt.src, dst) + + if tt.wantErr == nil { + require.NoError(t, err) + } else { + require.Equal(t, tt.wantErr, err) + } + + assert.Equal(t, tt.wantStack, stack) + assert.Equal(t, tt.wantDictionary, dst) + }) + } +} diff --git a/pdata/pprofile/valuetype.go b/pdata/pprofile/valuetype.go new file mode 100644 index 00000000000..2d2e735c018 --- /dev/null +++ b/pdata/pprofile/valuetype.go @@ -0,0 +1,36 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package pprofile // import "go.opentelemetry.io/collector/pdata/pprofile" + +import "fmt" + +// switchDictionary updates the ValueType, switching its indices from one +// dictionary to another. +func (ms ValueType) switchDictionary(src, dst ProfilesDictionary) error { + if ms.TypeStrindex() > 0 { + if src.StringTable().Len() < int(ms.TypeStrindex()) { + return fmt.Errorf("invalid type index %d", ms.TypeStrindex()) + } + + idx, err := SetString(dst.StringTable(), src.StringTable().At(int(ms.TypeStrindex()))) + if err != nil { + return fmt.Errorf("couldn't set type: %w", err) + } + ms.SetTypeStrindex(idx) + } + + if ms.UnitStrindex() > 0 { + if src.StringTable().Len() < int(ms.UnitStrindex()) { + return fmt.Errorf("invalid unit index %d", ms.UnitStrindex()) + } + + idx, err := SetString(dst.StringTable(), src.StringTable().At(int(ms.UnitStrindex()))) + if err != nil { + return fmt.Errorf("couldn't set unit: %w", err) + } + ms.SetUnitStrindex(idx) + } + + return nil +} diff --git a/pdata/pprofile/valuetype_test.go b/pdata/pprofile/valuetype_test.go new file mode 100644 index 00000000000..1d704693193 --- /dev/null +++ b/pdata/pprofile/valuetype_test.go @@ -0,0 +1,150 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package pprofile + +import ( + "errors" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestValueTypeSwitchDictionary(t *testing.T) { + for _, tt := range []struct { + name string + valueType ValueType + + src ProfilesDictionary + dst ProfilesDictionary + + wantValueType ValueType + wantDictionary ProfilesDictionary + wantErr error + }{ + { + name: "with an empty value type", + valueType: NewValueType(), + + src: NewProfilesDictionary(), + dst: NewProfilesDictionary(), + + wantValueType: NewValueType(), + wantDictionary: NewProfilesDictionary(), + }, + { + name: "with an existing type", + valueType: func() ValueType { + vt := NewValueType() + vt.SetTypeStrindex(1) + return vt + }(), + + src: func() ProfilesDictionary { + d := NewProfilesDictionary() + d.StringTable().Append("", "test") + return d + }(), + dst: func() ProfilesDictionary { + d := NewProfilesDictionary() + d.StringTable().Append("", "foo") + return d + }(), + + wantValueType: func() ValueType { + vt := NewValueType() + vt.SetTypeStrindex(2) + return vt + }(), + wantDictionary: func() ProfilesDictionary { + d := NewProfilesDictionary() + d.StringTable().Append("", "foo", "test") + return d + }(), + }, + { + name: "with a type index that does not match anything", + valueType: func() ValueType { + vt := NewValueType() + vt.SetTypeStrindex(1) + return vt + }(), + + src: NewProfilesDictionary(), + dst: NewProfilesDictionary(), + + wantValueType: func() ValueType { + vt := NewValueType() + vt.SetTypeStrindex(1) + return vt + }(), + wantDictionary: NewProfilesDictionary(), + wantErr: errors.New("invalid type index 1"), + }, + { + name: "with an existing unit", + valueType: func() ValueType { + vt := NewValueType() + vt.SetUnitStrindex(1) + return vt + }(), + + src: func() ProfilesDictionary { + d := NewProfilesDictionary() + d.StringTable().Append("", "test") + return d + }(), + dst: func() ProfilesDictionary { + d := NewProfilesDictionary() + d.StringTable().Append("", "foo") + return d + }(), + + wantValueType: func() ValueType { + vt := NewValueType() + vt.SetUnitStrindex(2) + return vt + }(), + wantDictionary: func() ProfilesDictionary { + d := NewProfilesDictionary() + d.StringTable().Append("", "foo", "test") + return d + }(), + }, + { + name: "with a unit index that does not match anything", + valueType: func() ValueType { + vt := NewValueType() + vt.SetUnitStrindex(1) + return vt + }(), + + src: NewProfilesDictionary(), + dst: NewProfilesDictionary(), + + wantValueType: func() ValueType { + vt := NewValueType() + vt.SetUnitStrindex(1) + return vt + }(), + wantDictionary: NewProfilesDictionary(), + wantErr: errors.New("invalid unit index 1"), + }, + } { + t.Run(tt.name, func(t *testing.T) { + vt := tt.valueType + dst := tt.dst + err := vt.switchDictionary(tt.src, dst) + + if tt.wantErr == nil { + require.NoError(t, err) + } else { + require.Equal(t, tt.wantErr, err) + } + + assert.Equal(t, tt.wantValueType, vt) + assert.Equal(t, tt.wantDictionary, dst) + }) + } +} From 716e11a67c5da5b3dcf895026a06f27d36715ba8 Mon Sep 17 00:00:00 2001 From: Florian Lehner Date: Wed, 19 Nov 2025 14:18:45 +0100 Subject: [PATCH 2/2] [xscraper] add Profiles support (#13915) #### Description Implement scraper for Profiles. This will help implement receiver parts like https://github.com/open-telemetry/opentelemetry-collector-contrib/pull/42843. --------- Signed-off-by: Florian Lehner Co-authored-by: Pablo Baeyens --- .chloggen/config.yaml | 1 + .chloggen/scraper-profiles.yaml | 25 ++++++ .github/CODEOWNERS | 1 + .github/workflows/utils/cspell.json | 2 + scraper/xscraper/Makefile | 1 + scraper/xscraper/doc.go | 7 ++ scraper/xscraper/factory.go | 97 ++++++++++++++++++++++ scraper/xscraper/factory_test.go | 88 ++++++++++++++++++++ scraper/xscraper/generated_package_test.go | 13 +++ scraper/xscraper/go.mod | 51 ++++++++++++ scraper/xscraper/go.sum | 68 +++++++++++++++ scraper/xscraper/metadata.yaml | 11 +++ scraper/xscraper/profiles.go | 44 ++++++++++ scraper/xscraper/profiles_test.go | 79 ++++++++++++++++++ scraper/xscraper/scraper.go | 58 +++++++++++++ versions.yaml | 1 + 16 files changed, 547 insertions(+) create mode 100644 .chloggen/scraper-profiles.yaml create mode 100644 scraper/xscraper/Makefile create mode 100644 scraper/xscraper/doc.go create mode 100644 scraper/xscraper/factory.go create mode 100644 scraper/xscraper/factory_test.go create mode 100644 scraper/xscraper/generated_package_test.go create mode 100644 scraper/xscraper/go.mod create mode 100644 scraper/xscraper/go.sum create mode 100644 scraper/xscraper/metadata.yaml create mode 100644 scraper/xscraper/profiles.go create mode 100644 scraper/xscraper/profiles_test.go create mode 100644 scraper/xscraper/scraper.go diff --git a/.chloggen/config.yaml b/.chloggen/config.yaml index d5d46d80099..621f2958056 100644 --- a/.chloggen/config.yaml +++ b/.chloggen/config.yaml @@ -50,6 +50,7 @@ components: - pkg/xexporterhelper - pkg/xprocessor - pkg/xreceiver + - pkg/xscraper - processor/batch - processor/memory_limiter - processor/sample diff --git a/.chloggen/scraper-profiles.yaml b/.chloggen/scraper-profiles.yaml new file mode 100644 index 00000000000..587bee22e73 --- /dev/null +++ b/.chloggen/scraper-profiles.yaml @@ -0,0 +1,25 @@ +# Use this changelog template to create an entry for release notes. + +# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix' +change_type: 'enhancement' + +# The name of the component, or a single word describing the area of concern, (e.g. otlpreceiver) +component: pkg/xscraper + +# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`). +note: Implement xscraper for Profiles. + +# One or more tracking issues or pull requests related to the change +issues: [13915] + +# (Optional) One or more lines of additional information to render under the primary note. +# These lines will be padded with 2 spaces and then inserted directly into the document. +# Use pipe (|) for multiline entries. +subtext: + +# Optional: The change log or logs in which this entry should be included. +# e.g. '[user]' or '[user, api]' +# Include 'user' if the change is relevant to end users. +# Include 'api' if there is a change to a library API. +# Default: '[user]' +change_logs: [user] diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 80aea4f3cd3..8c5b4f8b1a0 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -60,6 +60,7 @@ receiver/otlpreceiver/ @open-telemetry/collector-approvers receiver/receiverhelper/ @open-telemetry/collector-approvers receiver/xreceiver/ @open-telemetry/collector-approvers @mx-psi @dmathieu scraper/ @open-telemetry/collector-approvers +scraper/xscraper @open-telemetry/collector-approvers scraper/scraperhelper/ @open-telemetry/collector-approvers service/ @open-telemetry/collector-approvers service/internal/graph/ @open-telemetry/collector-approvers diff --git a/.github/workflows/utils/cspell.json b/.github/workflows/utils/cspell.json index 3bd34c790bf..dcec12199e6 100644 --- a/.github/workflows/utils/cspell.json +++ b/.github/workflows/utils/cspell.json @@ -221,6 +221,7 @@ "fileprovider", "filterprocessor", "filterset", + "florianl", "fluentbit", "fluentforward", "forwardconnector", @@ -503,6 +504,7 @@ "xprocessor", "xprocessorhelper", "xreceiver", + "xscraper", "yamlmapprovider", "yamlprovider", "yamls", diff --git a/scraper/xscraper/Makefile b/scraper/xscraper/Makefile new file mode 100644 index 00000000000..ded7a36092d --- /dev/null +++ b/scraper/xscraper/Makefile @@ -0,0 +1 @@ +include ../../Makefile.Common diff --git a/scraper/xscraper/doc.go b/scraper/xscraper/doc.go new file mode 100644 index 00000000000..b7c428e4886 --- /dev/null +++ b/scraper/xscraper/doc.go @@ -0,0 +1,7 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +//go:generate mdatagen metadata.yaml + +// Package xscraper allows to define pull based receivers that can be configured using the scraperreceiver. +package xscraper // import "go.opentelemetry.io/collector/scraper/xscraper" diff --git a/scraper/xscraper/factory.go b/scraper/xscraper/factory.go new file mode 100644 index 00000000000..a848c9635b1 --- /dev/null +++ b/scraper/xscraper/factory.go @@ -0,0 +1,97 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package xscraper // import "go.opentelemetry.io/collector/scraper/xscraper" + +import ( + "context" + + "go.opentelemetry.io/collector/component" + "go.opentelemetry.io/collector/pipeline" + "go.opentelemetry.io/collector/scraper" +) + +type Factory interface { + scraper.Factory + + // CreateProfiles creates a Profiles scraper based on this config. + // If the scraper type does not support profiles, + // this function returns the error [pipeline.ErrSignalNotSupported]. + CreateProfiles(ctx context.Context, set scraper.Settings, cfg component.Config) (Profiles, error) + + // ProfilesStability gets the stability level of the Profiles scraper. + ProfilesStability() component.StabilityLevel +} + +// FactoryOption apply changes to Options. +type FactoryOption interface { + // applyOption applies the option. + applyOption(o *factoryOpts) +} + +type factoryOpts struct { + opts []scraper.FactoryOption + *factory +} + +var _ FactoryOption = (*factoryOptionFunc)(nil) + +// factoryOptionFunc is a FactoryOption created through a function. +type factoryOptionFunc func(*factoryOpts) + +func (f factoryOptionFunc) applyOption(o *factoryOpts) { + f(o) +} + +type factory struct { + scraper.Factory + + createProfilesFunc CreateProfilesFunc + profilesStabilityLevel component.StabilityLevel +} + +func (f *factory) ProfilesStability() component.StabilityLevel { + return f.profilesStabilityLevel +} + +func (f *factory) CreateProfiles(ctx context.Context, set scraper.Settings, cfg component.Config) (Profiles, error) { + if f.createProfilesFunc == nil { + return nil, pipeline.ErrSignalNotSupported + } + return f.createProfilesFunc(ctx, set, cfg) +} + +// WithLogs overrides the default "error not supported" implementation for CreateLogs and the default "undefined" stability level. +func WithLogs(createLogs scraper.CreateLogsFunc, sl component.StabilityLevel) FactoryOption { + return factoryOptionFunc(func(o *factoryOpts) { + o.opts = append(o.opts, scraper.WithLogs(createLogs, sl)) + }) +} + +// WithMetrics overrides the default "error not supported" implementation for CreateMetrics and the default "undefined" stability level. +func WithMetrics(createMetrics scraper.CreateMetricsFunc, sl component.StabilityLevel) FactoryOption { + return factoryOptionFunc(func(o *factoryOpts) { + o.opts = append(o.opts, scraper.WithMetrics(createMetrics, sl)) + }) +} + +// CreateProfilesFunc is the equivalent of Factory.CreateProfiles(). +type CreateProfilesFunc func(context.Context, scraper.Settings, component.Config) (Profiles, error) + +// WithProfiles overrides the default "error not supported" implementation for CreateProfiles and the default "undefined" stability level. +func WithProfiles(createProfiles CreateProfilesFunc, sl component.StabilityLevel) FactoryOption { + return factoryOptionFunc(func(o *factoryOpts) { + o.profilesStabilityLevel = sl + o.createProfilesFunc = createProfiles + }) +} + +// NewFactory returns a Factory. +func NewFactory(cfgType component.Type, createDefaultConfig component.CreateDefaultConfigFunc, options ...FactoryOption) Factory { + opts := factoryOpts{factory: &factory{}} + for _, opt := range options { + opt.applyOption(&opts) + } + opts.Factory = scraper.NewFactory(cfgType, createDefaultConfig, opts.opts...) + return opts.factory +} diff --git a/scraper/xscraper/factory_test.go b/scraper/xscraper/factory_test.go new file mode 100644 index 00000000000..35cabef6f52 --- /dev/null +++ b/scraper/xscraper/factory_test.go @@ -0,0 +1,88 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package xscraper + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "go.opentelemetry.io/collector/component" + "go.opentelemetry.io/collector/component/componenttest" + "go.opentelemetry.io/collector/pdata/plog" + "go.opentelemetry.io/collector/pdata/pmetric" + "go.opentelemetry.io/collector/pipeline" + "go.opentelemetry.io/collector/scraper" +) + +var testType = component.MustNewType("test") + +func nopSettings() scraper.Settings { + return scraper.Settings{ + ID: component.NewID(testType), + TelemetrySettings: componenttest.NewNopTelemetrySettings(), + } +} + +func TestNewFactory(t *testing.T) { + defaultCfg := struct{}{} + f := NewFactory( + testType, + func() component.Config { return &defaultCfg }) + assert.Equal(t, testType, f.Type()) + assert.EqualValues(t, &defaultCfg, f.CreateDefaultConfig()) + _, err := f.CreateProfiles(context.Background(), nopSettings(), &defaultCfg) + require.ErrorIs(t, err, pipeline.ErrSignalNotSupported) +} + +func TestNewFactoryWithOptions(t *testing.T) { + testType := component.MustNewType("test") + defaultCfg := struct{}{} + f := NewFactory( + testType, + func() component.Config { return &defaultCfg }, + WithLogs(createLogs, component.StabilityLevelAlpha), + WithMetrics(createMetrics, component.StabilityLevelAlpha), + WithProfiles(createProfiles, component.StabilityLevelDevelopment)) + assert.Equal(t, testType, f.Type()) + assert.EqualValues(t, &defaultCfg, f.CreateDefaultConfig()) + + assert.Equal(t, component.StabilityLevelDevelopment, f.ProfilesStability()) + _, err := f.CreateProfiles(context.Background(), scraper.Settings{}, &defaultCfg) + require.NoError(t, err) + + assert.Equal(t, component.StabilityLevelAlpha, f.LogsStability()) + _, err = f.CreateLogs(context.Background(), scraper.Settings{}, &defaultCfg) + require.NoError(t, err) + + assert.Equal(t, component.StabilityLevelAlpha, f.MetricsStability()) + _, err = f.CreateMetrics(context.Background(), scraper.Settings{}, &defaultCfg) + require.NoError(t, err) +} + +func createProfiles(context.Context, scraper.Settings, component.Config) (Profiles, error) { + return NewProfiles(newTestScrapeProfilesFunc(nil)) +} + +func createLogs(context.Context, scraper.Settings, component.Config) (scraper.Logs, error) { + return scraper.NewLogs(newTestScrapeLogsFunc(nil)) +} + +func createMetrics(context.Context, scraper.Settings, component.Config) (scraper.Metrics, error) { + return scraper.NewMetrics(newTestScrapeMetricsFunc(nil)) +} + +func newTestScrapeLogsFunc(retError error) scraper.ScrapeLogsFunc { + return func(_ context.Context) (plog.Logs, error) { + return plog.NewLogs(), retError + } +} + +func newTestScrapeMetricsFunc(retError error) scraper.ScrapeMetricsFunc { + return func(_ context.Context) (pmetric.Metrics, error) { + return pmetric.NewMetrics(), retError + } +} diff --git a/scraper/xscraper/generated_package_test.go b/scraper/xscraper/generated_package_test.go new file mode 100644 index 00000000000..81b72880614 --- /dev/null +++ b/scraper/xscraper/generated_package_test.go @@ -0,0 +1,13 @@ +// Code generated by mdatagen. DO NOT EDIT. + +package xscraper + +import ( + "testing" + + "go.uber.org/goleak" +) + +func TestMain(m *testing.M) { + goleak.VerifyTestMain(m) +} diff --git a/scraper/xscraper/go.mod b/scraper/xscraper/go.mod new file mode 100644 index 00000000000..ec886f3eab4 --- /dev/null +++ b/scraper/xscraper/go.mod @@ -0,0 +1,51 @@ +module go.opentelemetry.io/collector/scraper/xscraper + +go 1.24.0 + +require ( + github.com/stretchr/testify v1.11.1 + go.opentelemetry.io/collector/component v1.46.0 + go.opentelemetry.io/collector/component/componenttest v0.140.0 + go.opentelemetry.io/collector/pdata v1.46.0 + go.opentelemetry.io/collector/pdata/pprofile v0.136.0 + go.opentelemetry.io/collector/pipeline v1.46.0 + go.opentelemetry.io/collector/scraper v0.136.0 + go.uber.org/goleak v1.3.0 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/hashicorp/go-version v1.7.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/collector/featuregate v1.46.0 // indirect + go.opentelemetry.io/otel v1.38.0 // indirect + go.opentelemetry.io/otel/metric v1.38.0 // indirect + go.opentelemetry.io/otel/sdk v1.38.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.38.0 // indirect + go.opentelemetry.io/otel/trace v1.38.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.27.0 // indirect + golang.org/x/sys v0.37.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) + +replace go.opentelemetry.io/collector/pipeline => ../../pipeline + +replace go.opentelemetry.io/collector/pdata/pprofile => ../../pdata/pprofile + +replace go.opentelemetry.io/collector/component => ../../component + +replace go.opentelemetry.io/collector/component/componenttest => ../../component/componenttest + +replace go.opentelemetry.io/collector/scraper => ../../scraper + +replace go.opentelemetry.io/collector/featuregate => ../../featuregate + +replace go.opentelemetry.io/collector/pdata => ../../pdata diff --git a/scraper/xscraper/go.sum b/scraper/xscraper/go.sum new file mode 100644 index 00000000000..898d0c8bf05 --- /dev/null +++ b/scraper/xscraper/go.sum @@ -0,0 +1,68 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +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= +github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= +go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= +go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= +go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= +go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= +go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= +go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= +go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= +go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= +go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= +go.opentelemetry.io/proto/slim/otlp v1.9.0 h1:fPVMv8tP3TrsqlkH1HWYUpbCY9cAIemx184VGkS6vlE= +go.opentelemetry.io/proto/slim/otlp v1.9.0/go.mod h1:xXdeJJ90Gqyll+orzUkY4bOd2HECo5JofeoLpymVqdI= +go.opentelemetry.io/proto/slim/otlp/collector/profiles/v1development v0.2.0 h1:o13nadWDNkH/quoDomDUClnQBpdQQ2Qqv0lQBjIXjE8= +go.opentelemetry.io/proto/slim/otlp/collector/profiles/v1development v0.2.0/go.mod h1:Gyb6Xe7FTi/6xBHwMmngGoHqL0w29Y4eW8TGFzpefGA= +go.opentelemetry.io/proto/slim/otlp/profiles/v1development v0.2.0 h1:EiUYvtwu6PMrMHVjcPfnsG3v+ajPkbUeH+IL93+QYyk= +go.opentelemetry.io/proto/slim/otlp/profiles/v1development v0.2.0/go.mod h1:mUUHKFiN2SST3AhJ8XhJxEoeVW12oqfXog0Bo8W3Ec4= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= +golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= +google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/scraper/xscraper/metadata.yaml b/scraper/xscraper/metadata.yaml new file mode 100644 index 00000000000..9097af6f76e --- /dev/null +++ b/scraper/xscraper/metadata.yaml @@ -0,0 +1,11 @@ +type: xscraper +github_project: open-telemetry/opentelemetry-collector + +status: + class: pkg + codeowners: + active: + - dmathieu + - florianl + stability: + development: [profiles] diff --git a/scraper/xscraper/profiles.go b/scraper/xscraper/profiles.go new file mode 100644 index 00000000000..c6100e1af6b --- /dev/null +++ b/scraper/xscraper/profiles.go @@ -0,0 +1,44 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package xscraper // import "go.opentelemetry.io/collector/scraper/xscraper" + +import ( + "context" + + "go.opentelemetry.io/collector/component" + "go.opentelemetry.io/collector/pdata/pprofile" + "go.opentelemetry.io/collector/scraper" +) + +// Profiles is the base interface for profiles scrapers. +type Profiles interface { + component.Component + + // ScrapeProfiles is the base interface to indicate that how should profiles be scraped. + ScrapeProfiles(context.Context) (pprofile.Profiles, error) +} + +// ScrapeProfilesFunc is a helper function. +type ScrapeProfilesFunc scraper.ScrapeFunc[pprofile.Profiles] + +func (sf ScrapeProfilesFunc) ScrapeProfiles(ctx context.Context) (pprofile.Profiles, error) { + return sf(ctx) +} + +type profiles struct { + baseScraper + ScrapeProfilesFunc +} + +// NewProfiles creates a new Profiles scraper. +func NewProfiles(scrape ScrapeProfilesFunc, options ...Option) (Profiles, error) { + if scrape == nil { + return nil, errNilFunc + } + bs := &profiles{ + baseScraper: newBaseScraper(options), + ScrapeProfilesFunc: scrape, + } + return bs, nil +} diff --git a/scraper/xscraper/profiles_test.go b/scraper/xscraper/profiles_test.go new file mode 100644 index 00000000000..ac98158ad86 --- /dev/null +++ b/scraper/xscraper/profiles_test.go @@ -0,0 +1,79 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package xscraper + +import ( + "context" + "errors" + "sync" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "go.opentelemetry.io/collector/component" + "go.opentelemetry.io/collector/component/componenttest" + "go.opentelemetry.io/collector/pdata/pprofile" +) + +func TestNewProfiles(t *testing.T) { + mp, err := NewProfiles(newTestScrapeProfilesFunc(nil)) + require.NoError(t, err) + + require.NoError(t, mp.Start(context.Background(), componenttest.NewNopHost())) + md, err := mp.ScrapeProfiles(context.Background()) + require.NoError(t, err) + assert.Equal(t, pprofile.NewProfiles(), md) + require.NoError(t, mp.Shutdown(context.Background())) +} + +func TestNewProfiles_WithOptions(t *testing.T) { + want := errors.New("my_error") + mp, err := NewProfiles(newTestScrapeProfilesFunc(nil), + WithStart(func(context.Context, component.Host) error { return want }), + WithShutdown(func(context.Context) error { return want })) + require.NoError(t, err) + + assert.Equal(t, want, mp.Start(context.Background(), componenttest.NewNopHost())) + assert.Equal(t, want, mp.Shutdown(context.Background())) +} + +func TestNewProfiles_NilRequiredFields(t *testing.T) { + _, err := NewProfiles(nil) + require.Error(t, err) +} + +func TestNewProfiles_ProcessProfilesError(t *testing.T) { + want := errors.New("my_error") + mp, err := NewProfiles(newTestScrapeProfilesFunc(want)) + require.NoError(t, err) + _, err = mp.ScrapeProfiles(context.Background()) + require.ErrorIs(t, err, want) +} + +func TestProfilesConcurrency(t *testing.T) { + mp, err := NewProfiles(newTestScrapeProfilesFunc(nil)) + require.NoError(t, err) + require.NoError(t, mp.Start(context.Background(), componenttest.NewNopHost())) + + var wg sync.WaitGroup + for range 10 { + wg.Add(1) + go func() { + defer wg.Done() + for range 10000 { + _, errScrape := mp.ScrapeProfiles(context.Background()) + assert.NoError(t, errScrape) + } + }() + } + wg.Wait() + require.NoError(t, mp.Shutdown(context.Background())) +} + +func newTestScrapeProfilesFunc(retError error) ScrapeProfilesFunc { + return func(_ context.Context) (pprofile.Profiles, error) { + return pprofile.NewProfiles(), retError + } +} diff --git a/scraper/xscraper/scraper.go b/scraper/xscraper/scraper.go new file mode 100644 index 00000000000..96e988bdb14 --- /dev/null +++ b/scraper/xscraper/scraper.go @@ -0,0 +1,58 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package xscraper // import "go.opentelemetry.io/collector/scraper/xscraper" + +import ( + "context" + "errors" + + "go.opentelemetry.io/collector/component" +) + +var errNilFunc = errors.New("nil scrape func") + +// ScrapeFunc scrapes data. +type ScrapeFunc[T any] func(context.Context) (T, error) + +// Option apply changes to internal options. +type Option interface { + apply(*baseScraper) +} + +type scraperOptionFunc func(*baseScraper) + +func (of scraperOptionFunc) apply(e *baseScraper) { + of(e) +} + +// WithStart sets the function that will be called on startup. +func WithStart(start component.StartFunc) Option { + return scraperOptionFunc(func(o *baseScraper) { + o.StartFunc = start + }) +} + +// WithShutdown sets the function that will be called on shutdown. +func WithShutdown(shutdown component.ShutdownFunc) Option { + return scraperOptionFunc(func(o *baseScraper) { + o.ShutdownFunc = shutdown + }) +} + +type baseScraper struct { + component.StartFunc + component.ShutdownFunc +} + +// newBaseScraper returns the internal settings starting from the default and applying all options. +func newBaseScraper(options []Option) baseScraper { + // Start from the default options: + bs := baseScraper{} + + for _, op := range options { + op.apply(&bs) + } + + return bs +} diff --git a/versions.yaml b/versions.yaml index 9e4c65c8175..4384eec2c3c 100644 --- a/versions.yaml +++ b/versions.yaml @@ -92,6 +92,7 @@ module-sets: - go.opentelemetry.io/collector/scraper - go.opentelemetry.io/collector/scraper/scraperhelper - go.opentelemetry.io/collector/scraper/scrapertest + - go.opentelemetry.io/collector/scraper/xscraper - go.opentelemetry.io/collector/service - go.opentelemetry.io/collector/service/hostcapabilities - go.opentelemetry.io/collector/service/telemetry/telemetrytest