diff --git a/.chloggen/elementize-attributes-xml.yaml b/.chloggen/elementize-attributes-xml.yaml
new file mode 100644
index 000000000000..e46c4221ff35
--- /dev/null
+++ b/.chloggen/elementize-attributes-xml.yaml
@@ -0,0 +1,27 @@
+# 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. filelogreceiver)
+component: pkg/ottl
+
+# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`).
+note: Add ConvertAttributesToElementsXML Converter
+
+# Mandatory: One or more tracking issues related to the change. You can use the PR number here if no issue exists.
+issues: [35328]
+
+# (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:
+
+# If your change doesn't affect end users or the exported elements of any package,
+# you should instead start your pull request title with [chore] or use the "Skip Changelog" label.
+# 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: []
diff --git a/pkg/ottl/e2e/e2e_test.go b/pkg/ottl/e2e/e2e_test.go
index ede1e329bdb1..f7c3fe7324fe 100644
--- a/pkg/ottl/e2e/e2e_test.go
+++ b/pkg/ottl/e2e/e2e_test.go
@@ -347,6 +347,12 @@ func Test_e2e_converters(t *testing.T) {
tCtx.GetLogRecord().Attributes().PutStr("test", "FooBar")
},
},
+ {
+ statement: `set(attributes["test"], ConvertAttributesToElementsXML("This is a log message!"))`,
+ want: func(tCtx ottllog.TransformContext) {
+ tCtx.GetLogRecord().Attributes().PutStr("test", `This is a log message!1`)
+ },
+ },
{
statement: `set(attributes["test"], Double(1.0))`,
want: func(tCtx ottllog.TransformContext) {
diff --git a/pkg/ottl/ottlfuncs/README.md b/pkg/ottl/ottlfuncs/README.md
index 380cb41d8c50..11a61d101691 100644
--- a/pkg/ottl/ottlfuncs/README.md
+++ b/pkg/ottl/ottlfuncs/README.md
@@ -413,6 +413,7 @@ Available Converters:
- [Decode](#decode)
- [Concat](#concat)
- [ConvertCase](#convertcase)
+- [ConvertAttributesToElementsXML](#convertattributestoelementsxml)
- [Day](#day)
- [Double](#double)
- [Duration](#duration)
@@ -547,6 +548,31 @@ Examples:
- `ConvertCase(metric.name, "snake")`
+### ConvertAttributesToElementsXML
+
+`ConvertAttributesToElementsXML(target, Optional[xpath])`
+
+The `ConvertAttributesToElementsXML` Converter returns an edited version of an XML string where attributes are converted into child elements.
+
+`target` is a Getter that returns a string. This string should be in XML format.
+If `target` is not a string, nil, or cannot be parsed as XML, `ConvertAttributesToElementsXML` will return an error.
+
+`xpath` (optional) is a string that specifies an [XPath](https://www.w3.org/TR/1999/REC-xpath-19991116/) expression that
+selects one or more elements. Attributes will only be converted within the result(s) of the xpath.
+
+For example, `baz` will be converted to `bazbar`.
+
+Examples:
+
+Convert all attributes in a document
+
+- `ConvertAttributesToElementsXML(body)`
+
+Convert only attributes within "Record" elements
+
+- `ConvertAttributesToElementsXML(body, "/Log/Record")`
+
+
### Day
`Day(value)`
diff --git a/pkg/ottl/ottlfuncs/func_convert_attributes_to_elements_xml.go b/pkg/ottl/ottlfuncs/func_convert_attributes_to_elements_xml.go
new file mode 100644
index 000000000000..64d4ecc5fde5
--- /dev/null
+++ b/pkg/ottl/ottlfuncs/func_convert_attributes_to_elements_xml.go
@@ -0,0 +1,69 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+package ottlfuncs // import "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl/ottlfuncs"
+
+import (
+ "context"
+ "fmt"
+
+ "github.com/antchfx/xmlquery"
+
+ "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl"
+)
+
+type ConvertAttributesToElementsXMLArguments[K any] struct {
+ Target ottl.StringGetter[K]
+ XPath ottl.Optional[string]
+}
+
+func NewConvertAttributesToElementsXMLFactory[K any]() ottl.Factory[K] {
+ return ottl.NewFactory("ConvertAttributesToElementsXML", &ConvertAttributesToElementsXMLArguments[K]{}, createConvertAttributesToElementsXMLFunction[K])
+}
+
+func createConvertAttributesToElementsXMLFunction[K any](_ ottl.FunctionContext, oArgs ottl.Arguments) (ottl.ExprFunc[K], error) {
+ args, ok := oArgs.(*ConvertAttributesToElementsXMLArguments[K])
+
+ if !ok {
+ return nil, fmt.Errorf("ConvertAttributesToElementsXML args must be of type *ConvertAttributesToElementsXMLAguments[K]")
+ }
+
+ xPath := args.XPath.Get()
+ if xPath == "" {
+ xPath = "//@*" // All attributes in the document
+ }
+ if err := validateXPath(xPath); err != nil {
+ return nil, err
+ }
+
+ return convertAttributesToElementsXML(args.Target, xPath), nil
+}
+
+// convertAttributesToElementsXML returns a string that is a result of converting all attributes of the
+// target XML into child elements. These new elements are added as the last child elements of the parent.
+// e.g. -> worldbar
+func convertAttributesToElementsXML[K any](target ottl.StringGetter[K], xPath string) ottl.ExprFunc[K] {
+ return func(ctx context.Context, tCtx K) (any, error) {
+ var doc *xmlquery.Node
+ if targetVal, err := target.Get(ctx, tCtx); err != nil {
+ return nil, err
+ } else if doc, err = parseNodesXML(targetVal); err != nil {
+ return nil, err
+ }
+ for _, n := range xmlquery.Find(doc, xPath) {
+ if n.Type != xmlquery.AttributeNode {
+ continue
+ }
+ xmlquery.AddChild(n.Parent, &xmlquery.Node{
+ Type: xmlquery.ElementNode,
+ Data: n.Data,
+ FirstChild: &xmlquery.Node{
+ Type: xmlquery.TextNode,
+ Data: n.InnerText(),
+ },
+ })
+ n.Parent.RemoveAttr(n.Data)
+ }
+ return doc.OutputXML(false), nil
+ }
+}
diff --git a/pkg/ottl/ottlfuncs/func_convert_attributes_to_elements_xml_test.go b/pkg/ottl/ottlfuncs/func_convert_attributes_to_elements_xml_test.go
new file mode 100644
index 000000000000..11bc2c5a29bd
--- /dev/null
+++ b/pkg/ottl/ottlfuncs/func_convert_attributes_to_elements_xml_test.go
@@ -0,0 +1,126 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+package ottlfuncs // import "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl/ottlfuncs"
+
+import (
+ "context"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+
+ "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl"
+)
+
+func Test_ConvertAttributesToElementsXML(t *testing.T) {
+ tests := []struct {
+ name string
+ document string
+ xPath string
+ want string
+ }{
+ {
+ name: "nop",
+ document: ``,
+ want: ``,
+ },
+ {
+ name: "nop declaration",
+ document: ``,
+ want: ``,
+ },
+ {
+ name: "single attribute",
+ document: ``,
+ want: `bar`,
+ },
+ {
+ name: "multiple attributes - order 1",
+ document: ``,
+ want: `barworld`,
+ },
+ {
+ name: "multiple attributes - order 2",
+ document: ``,
+ want: `worldbar`,
+ },
+ {
+ name: "with child elements",
+ document: ``,
+ want: `worldbar`,
+ },
+ {
+ name: "with child value",
+ document: `free value`,
+ want: `free valueworldbar`,
+ },
+ {
+ name: "with child elements and values",
+ document: `free value2`,
+ want: `free value2worldbar`,
+ },
+ {
+ name: "multiple levels",
+ document: ``,
+ want: `www.example.comworldbar`,
+ },
+ {
+ name: "xpath filtered",
+ document: ``,
+ xPath: "/a/b/@*", // only convert attributes of b
+ want: `www.example.com`,
+ },
+ {
+ name: "attributes found with non-attributes xpath",
+ document: ``,
+ xPath: "/a/b", // convert b (the attributes of b, even though the element b was selected)
+ want: ``,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ args := &ConvertAttributesToElementsXMLArguments[any]{
+ Target: ottl.StandardStringGetter[any]{
+ Getter: func(_ context.Context, _ any) (any, error) {
+ return tt.document, nil
+ },
+ },
+ XPath: ottl.NewTestingOptional(tt.xPath),
+ }
+ exprFunc, err := createConvertAttributesToElementsXMLFunction[any](ottl.FunctionContext{}, args)
+ assert.NoError(t, err)
+
+ result, err := exprFunc(context.Background(), nil)
+ assert.NoError(t, err)
+ assert.Equal(t, tt.want, result)
+ })
+ }
+}
+
+func TestCreateConvertAttributesToElementsXMLFunc(t *testing.T) {
+ factory := NewConvertAttributesToElementsXMLFactory[any]()
+ fCtx := ottl.FunctionContext{}
+
+ // Invalid arg type
+ exprFunc, err := factory.CreateFunction(fCtx, nil)
+ assert.Error(t, err)
+ assert.Nil(t, exprFunc)
+
+ // Invalid XPath should error on function creation
+ exprFunc, err = factory.CreateFunction(
+ fCtx, &ConvertAttributesToElementsXMLArguments[any]{
+ XPath: ottl.NewTestingOptional("!"),
+ })
+ assert.Error(t, err)
+ assert.Nil(t, exprFunc)
+
+ // Invalid XML should error on function execution
+ exprFunc, err = factory.CreateFunction(
+ fCtx, &ConvertAttributesToElementsXMLArguments[any]{
+ Target: invalidXMLGetter(),
+ })
+ assert.NoError(t, err)
+ assert.NotNil(t, exprFunc)
+ _, err = exprFunc(context.Background(), nil)
+ assert.Error(t, err)
+}
diff --git a/pkg/ottl/ottlfuncs/functions.go b/pkg/ottl/ottlfuncs/functions.go
index 5e3aa6741cad..b30c950ed947 100644
--- a/pkg/ottl/ottlfuncs/functions.go
+++ b/pkg/ottl/ottlfuncs/functions.go
@@ -40,6 +40,7 @@ func converters[K any]() []ottl.Factory[K] {
NewDecodeFactory[K](),
NewConcatFactory[K](),
NewConvertCaseFactory[K](),
+ NewConvertAttributesToElementsXMLFactory[K](),
NewDayFactory[K](),
NewDoubleFactory[K](),
NewDurationFactory[K](),