diff --git a/.chloggen/ottl-add-element-xml.yaml b/.chloggen/ottl-add-element-xml.yaml
new file mode 100644
index 000000000000..f74f7c3fd3c1
--- /dev/null
+++ b/.chloggen/ottl-add-element-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 InsertXML Converter
+
+# Mandatory: One or more tracking issues related to the change. You can use the PR number here if no issue exists.
+issues: [35436]
+
+# (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 b908319382a4..d1294b22bd96 100644
--- a/pkg/ottl/e2e/e2e_test.go
+++ b/pkg/ottl/e2e/e2e_test.go
@@ -420,6 +420,12 @@ func Test_e2e_converters(t *testing.T) {
tCtx.GetLogRecord().Attributes().PutDouble("test", 1.5)
},
},
+ {
+ statement: `set(attributes["test"], InsertXML("", "/a", ""))`,
+ want: func(tCtx ottllog.TransformContext) {
+ tCtx.GetLogRecord().Attributes().PutStr("test", "")
+ },
+ },
{
statement: `set(attributes["test"], Int(1.0))`,
want: func(tCtx ottllog.TransformContext) {
diff --git a/pkg/ottl/ottlfuncs/README.md b/pkg/ottl/ottlfuncs/README.md
index a4f0281c4d2c..ab36043ada24 100644
--- a/pkg/ottl/ottlfuncs/README.md
+++ b/pkg/ottl/ottlfuncs/README.md
@@ -414,6 +414,8 @@ Available Converters:
- [Concat](#concat)
- [ConvertCase](#convertcase)
- [Day](#day)
+- [Double](#double)
+- [Duration](#duration)
- [ExtractPatterns](#extractpatterns)
- [ExtractGrokPatterns](#extractgrokpatterns)
- [FNV](#fnv)
@@ -422,8 +424,7 @@ Available Converters:
- [Hex](#hex)
- [Hour](#hour)
- [Hours](#hours)
-- [Double](#double)
-- [Duration](#duration)
+- [InsertXML](#insertxml)
- [Int](#int)
- [IsBool](#isbool)
- [IsDouble](#isdouble)
@@ -829,6 +830,35 @@ Examples:
- `Hours(Duration("1h"))`
+### InsertXML
+
+`InsertXML(target, xpath, value)`
+
+The `InsertXML` Converter returns an edited version of an XML string with child elements added to selected elements.
+
+`target` is a Getter that returns a string. This string should be in XML format and represents the document which will
+be modified. If `target` is not a string, nil, or is not valid xml, `InsertXML` will return an error.
+
+`xpath` is a string that specifies an [XPath](https://www.w3.org/TR/1999/REC-xpath-19991116/) expression that
+selects one or more elements.
+
+`value` is a Getter that returns a string. This string should be in XML format and represents the document which will
+be inserted into `target`. If `value` is not a string, nil, or is not valid xml, `InsertXML` will return an error.
+
+Examples:
+
+Add an element "foo" to the root of the document
+
+- `InsertXML(body, "/", "")`
+
+Add an element "bar" to any element called "foo"
+
+- `InsertXML(body, "//foo", "")`
+
+Fetch and insert an xml document into another
+
+- `InsertXML(body, "/subdoc", attributes["subdoc"])`
+
### Int
`Int(value)`
diff --git a/pkg/ottl/ottlfuncs/func_insert_xml.go b/pkg/ottl/ottlfuncs/func_insert_xml.go
new file mode 100644
index 000000000000..778b16938a07
--- /dev/null
+++ b/pkg/ottl/ottlfuncs/func_insert_xml.go
@@ -0,0 +1,75 @@
+// 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"
+ "errors"
+ "fmt"
+
+ "github.com/antchfx/xmlquery"
+
+ "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl"
+)
+
+type InsertXMLArguments[K any] struct {
+ Target ottl.StringGetter[K]
+ XPath string
+ SubDocument ottl.StringGetter[K]
+}
+
+func NewInsertXMLFactory[K any]() ottl.Factory[K] {
+ return ottl.NewFactory("InsertXML", &InsertXMLArguments[K]{}, createInsertXMLFunction[K])
+}
+
+func createInsertXMLFunction[K any](_ ottl.FunctionContext, oArgs ottl.Arguments) (ottl.ExprFunc[K], error) {
+ args, ok := oArgs.(*InsertXMLArguments[K])
+
+ if !ok {
+ return nil, fmt.Errorf("InsertXML args must be of type *InsertXMLAguments[K]")
+ }
+
+ if err := validateXPath(args.XPath); err != nil {
+ return nil, err
+ }
+
+ return insertXML(args.Target, args.XPath, args.SubDocument), nil
+}
+
+// insertXML returns a XML formatted string that is a result of inserting another XML document into
+// the content of each selected target element.
+func insertXML[K any](target ottl.StringGetter[K], xPath string, subGetter ottl.StringGetter[K]) 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
+ }
+
+ var subDoc *xmlquery.Node
+ if subDocVal, err := subGetter.Get(ctx, tCtx); err != nil {
+ return nil, err
+ } else if subDoc, err = parseNodesXML(subDocVal); err != nil {
+ return nil, err
+ }
+
+ nodes, errs := xmlquery.QueryAll(doc, xPath)
+ for _, n := range nodes {
+ switch n.Type {
+ case xmlquery.ElementNode, xmlquery.DocumentNode:
+ var nextSibling *xmlquery.Node
+ for c := subDoc.FirstChild; c != nil; c = nextSibling {
+ // AddChild updates c.NextSibling but not subDoc.FirstChild
+ // so we need to get the handle to it prior to the update.
+ nextSibling = c.NextSibling
+ xmlquery.AddChild(n, c)
+ }
+ default:
+ errs = errors.Join(errs, fmt.Errorf("InsertXML XPath selected non-element: %q", n.Data))
+ }
+ }
+ return doc.OutputXML(false), errs
+ }
+}
diff --git a/pkg/ottl/ottlfuncs/func_insert_xml_test.go b/pkg/ottl/ottlfuncs/func_insert_xml_test.go
new file mode 100644
index 000000000000..32750d4c8feb
--- /dev/null
+++ b/pkg/ottl/ottlfuncs/func_insert_xml_test.go
@@ -0,0 +1,185 @@
+// 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_InsertXML(t *testing.T) {
+ tests := []struct {
+ name string
+ document string
+ xPath string
+ subdoc string
+ want string
+ expectErr string
+ }{
+ {
+ name: "add single element",
+ document: ``,
+ xPath: "/a",
+ subdoc: ``,
+ want: ``,
+ },
+ {
+ name: "add single element to multiple matches",
+ document: ``,
+ xPath: "/a",
+ subdoc: ``,
+ want: ``,
+ },
+ {
+ name: "add single element at multiple levels",
+ document: ``,
+ xPath: "//a",
+ subdoc: ``,
+ want: ``,
+ },
+ {
+ name: "add multiple elements at root",
+ document: ``,
+ xPath: "/",
+ subdoc: ``,
+ want: ``,
+ },
+ {
+ name: "add multiple elements to other element",
+ document: ``,
+ xPath: "/a",
+ subdoc: ``,
+ want: ``,
+ },
+ {
+ name: "add multiple elements to multiple elements",
+ document: ``,
+ xPath: "/a",
+ subdoc: ``,
+ want: ``,
+ },
+ {
+ name: "add multiple elements at multiple levels",
+ document: ``,
+ xPath: "//a",
+ subdoc: ``,
+ want: ``,
+ },
+ {
+ name: "add rich doc",
+ document: ``,
+ xPath: "/a",
+ subdoc: `text1`,
+ want: `text1`,
+ },
+ {
+ name: "add root element to empty document",
+ document: ``,
+ xPath: "/",
+ subdoc: ``,
+ want: ``,
+ },
+ {
+ name: "add root element to non-empty document",
+ document: ``,
+ xPath: "/",
+ subdoc: ``,
+ want: ``,
+ },
+ {
+ name: "err on attribute",
+ document: ``,
+ xPath: "/a/@foo",
+ subdoc: "",
+ want: ``,
+ expectErr: `InsertXML XPath selected non-element: "foo"`,
+ },
+ {
+ name: "err on text content",
+ document: `foo`,
+ xPath: "/a/text()",
+ subdoc: "",
+ want: `foo`,
+ expectErr: `InsertXML XPath selected non-element: "foo"`,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ f := NewInsertXMLFactory[any]()
+ exprFunc, err := f.CreateFunction(
+ ottl.FunctionContext{},
+ &InsertXMLArguments[any]{
+ Target: ottl.StandardStringGetter[any]{
+ Getter: func(_ context.Context, _ any) (any, error) {
+ return tt.document, nil
+ },
+ },
+ XPath: tt.xPath,
+ SubDocument: ottl.StandardStringGetter[any]{
+ Getter: func(_ context.Context, _ any) (any, error) {
+ return tt.subdoc, nil
+ },
+ },
+ })
+ assert.NoError(t, err)
+
+ result, err := exprFunc(context.Background(), nil)
+ if tt.expectErr == "" {
+ assert.NoError(t, err)
+ } else {
+ assert.EqualError(t, err, tt.expectErr)
+ }
+ assert.Equal(t, tt.want, result)
+ })
+ }
+}
+
+func TestCreateInsertXMLFunc(t *testing.T) {
+ factory := NewInsertXMLFactory[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, &InsertXMLArguments[any]{
+ XPath: "!",
+ })
+ assert.Error(t, err)
+ assert.Nil(t, exprFunc)
+
+ // Invalid XML target should error on function execution
+ exprFunc, err = factory.CreateFunction(
+ fCtx, &InsertXMLArguments[any]{
+ Target: invalidXMLGetter(),
+ XPath: "/",
+ })
+ assert.NoError(t, err)
+ assert.NotNil(t, exprFunc)
+ _, err = exprFunc(context.Background(), nil)
+ assert.Error(t, err)
+
+ // Invalid XML subdoc should error on function execution
+ exprFunc, err = factory.CreateFunction(
+ fCtx, &InsertXMLArguments[any]{
+ Target: ottl.StandardStringGetter[any]{
+ Getter: func(_ context.Context, _ any) (any, error) {
+ return "", nil
+ },
+ },
+ XPath: "/",
+ SubDocument: 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 fc61975c6a09..99bcd1ad3b8f 100644
--- a/pkg/ottl/ottlfuncs/functions.go
+++ b/pkg/ottl/ottlfuncs/functions.go
@@ -49,6 +49,7 @@ func converters[K any]() []ottl.Factory[K] {
NewGetXMLFactory[K](),
NewHourFactory[K](),
NewHoursFactory[K](),
+ NewInsertXMLFactory[K](),
NewIntFactory[K](),
NewIsBoolFactory[K](),
NewIsDoubleFactory[K](),