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/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) + }) + } +} 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