Skip to content

Commit

Permalink
[pkg/ottl] Add InsertXML Converter (#35436)
Browse files Browse the repository at this point in the history
  • Loading branch information
djaglowski authored Sep 30, 2024
1 parent c8f2553 commit 36733ce
Show file tree
Hide file tree
Showing 6 changed files with 326 additions and 2 deletions.
27 changes: 27 additions & 0 deletions .chloggen/ottl-add-element-xml.yaml
Original file line number Diff line number Diff line change
@@ -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: []
6 changes: 6 additions & 0 deletions pkg/ottl/e2e/e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,12 @@ func Test_e2e_converters(t *testing.T) {
tCtx.GetLogRecord().Attributes().PutDouble("test", 1.5)
},
},
{
statement: `set(attributes["test"], InsertXML("<a></a>", "/a", "<b></b>"))`,
want: func(tCtx ottllog.TransformContext) {
tCtx.GetLogRecord().Attributes().PutStr("test", "<a><b></b></a>")
},
},
{
statement: `set(attributes["test"], Int(1.0))`,
want: func(tCtx ottllog.TransformContext) {
Expand Down
34 changes: 32 additions & 2 deletions pkg/ottl/ottlfuncs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,8 @@ Available Converters:
- [Concat](#concat)
- [ConvertCase](#convertcase)
- [Day](#day)
- [Double](#double)
- [Duration](#duration)
- [ExtractPatterns](#extractpatterns)
- [ExtractGrokPatterns](#extractgrokpatterns)
- [FNV](#fnv)
Expand All @@ -422,8 +424,7 @@ Available Converters:
- [Hex](#hex)
- [Hour](#hour)
- [Hours](#hours)
- [Double](#double)
- [Duration](#duration)
- [InsertXML](#insertxml)
- [Int](#int)
- [IsBool](#isbool)
- [IsDouble](#isdouble)
Expand Down Expand Up @@ -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, "/", "<foo/>")`

Add an element "bar" to any element called "foo"

- `InsertXML(body, "//foo", "<bar/>")`

Fetch and insert an xml document into another

- `InsertXML(body, "/subdoc", attributes["subdoc"])`

### Int

`Int(value)`
Expand Down
75 changes: 75 additions & 0 deletions pkg/ottl/ottlfuncs/func_insert_xml.go
Original file line number Diff line number Diff line change
@@ -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
}
}
185 changes: 185 additions & 0 deletions pkg/ottl/ottlfuncs/func_insert_xml_test.go
Original file line number Diff line number Diff line change
@@ -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: `<a></a>`,
xPath: "/a",
subdoc: `<b/>`,
want: `<a><b></b></a>`,
},
{
name: "add single element to multiple matches",
document: `<a></a><a></a>`,
xPath: "/a",
subdoc: `<b/>`,
want: `<a><b></b></a><a><b></b></a>`,
},
{
name: "add single element at multiple levels",
document: `<a></a><z><a></a></z>`,
xPath: "//a",
subdoc: `<b/>`,
want: `<a><b></b></a><z><a><b></b></a></z>`,
},
{
name: "add multiple elements at root",
document: `<a></a>`,
xPath: "/",
subdoc: `<b/><c/>`,
want: `<a></a><b></b><c></c>`,
},
{
name: "add multiple elements to other element",
document: `<a></a>`,
xPath: "/a",
subdoc: `<b/><c/>`,
want: `<a><b></b><c></c></a>`,
},
{
name: "add multiple elements to multiple elements",
document: `<a></a><a></a>`,
xPath: "/a",
subdoc: `<b/><c/>`,
want: `<a><b></b><c></c></a><a><b></b><c></c></a>`,
},
{
name: "add multiple elements at multiple levels",
document: `<a></a><z><a></a></z>`,
xPath: "//a",
subdoc: `<b/><c/>`,
want: `<a><b></b><c></c></a><z><a><b></b><c></c></a></z>`,
},
{
name: "add rich doc",
document: `<a></a>`,
xPath: "/a",
subdoc: `<x foo="bar"><b>text</b><c><d><e>1</e><e><![CDATA[two]]></e></d></c></x>`,
want: `<a><x foo="bar"><b>text</b><c><d><e>1</e><e><![CDATA[two]]></e></d></c></x></a>`,
},
{
name: "add root element to empty document",
document: ``,
xPath: "/",
subdoc: `<a/>`,
want: `<a></a>`,
},
{
name: "add root element to non-empty document",
document: `<a></a>`,
xPath: "/",
subdoc: `<a/>`,
want: `<a></a><a></a>`,
},
{
name: "err on attribute",
document: `<a foo="bar"></a>`,
xPath: "/a/@foo",
subdoc: "<b/>",
want: `<a foo="bar"></a>`,
expectErr: `InsertXML XPath selected non-element: "foo"`,
},
{
name: "err on text content",
document: `<a>foo</a>`,
xPath: "/a/text()",
subdoc: "<b/>",
want: `<a>foo</a>`,
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 "<a/>", nil
},
},
XPath: "/",
SubDocument: invalidXMLGetter(),
})
assert.NoError(t, err)
assert.NotNil(t, exprFunc)
_, err = exprFunc(context.Background(), nil)
assert.Error(t, err)
}
1 change: 1 addition & 0 deletions pkg/ottl/ottlfuncs/functions.go
Original file line number Diff line number Diff line change
Expand Up @@ -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](),
Expand Down

0 comments on commit 36733ce

Please sign in to comment.