Skip to content

Commit

Permalink
[pkg/ottl] Add AddElementXML Converter
Browse files Browse the repository at this point in the history
  • Loading branch information
djaglowski committed Sep 26, 2024
1 parent 5fc4370 commit 2fe7e84
Show file tree
Hide file tree
Showing 7 changed files with 219 additions and 16 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 AddElementXML 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 @@ -305,6 +305,12 @@ func Test_e2e_converters(t *testing.T) {
statement string
want func(tCtx ottllog.TransformContext)
}{
{
statement: `set(attributes["test"], AddElementXML("<a></a>", "/a", "b"))`,
want: func(tCtx ottllog.TransformContext) {
tCtx.GetLogRecord().Attributes().PutStr("test", "<a><b></b></a>")
},
},
{
statement: `set(attributes["test"], Base64Decode("cGFzcw=="))`,
want: func(tCtx ottllog.TransformContext) {
Expand Down
25 changes: 25 additions & 0 deletions pkg/ottl/ottlfuncs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,7 @@ Unlike functions, they do not modify any input telemetry and always return a val

Available Converters:

- [AddElementXML](#addelementxml)
- [Base64Decode](#base64decode)
- [Decode](#decode)
- [Concat](#concat)
Expand Down Expand Up @@ -468,6 +469,30 @@ Available Converters:
- [UUID](#UUID)
- [Year](#year)

### AddElementXML

`AddElementXML(target, xpath, name)`

The `AddElementXML` 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.
If `target` is not a string, nil, or is not valid xml, `AddElementXML` 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.

`name` is a string that specifies the name of the child element to add.

Examples:

Add an element "foo" to the root of the document

- `AddElementXML(body, "/", "foo")`

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

- `AddElementXML(body, "//foo", "bar")`

### Base64Decode (Deprecated)

*This function has been deprecated. Please use the [Decode](#decode) function instead.*
Expand Down
34 changes: 19 additions & 15 deletions pkg/ottl/ottlfuncs/func_add_element_xml.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package ottlfuncs // import "github.com/open-telemetry/opentelemetry-collector-c

import (
"context"
"errors"
"fmt"

"github.com/antchfx/xmlquery"
Expand All @@ -15,6 +16,7 @@ import (
type AddElementXMLArguments[K any] struct {
Target ottl.StringGetter[K]
XPath string
Name string
}

func NewAddElementXMLFactory[K any]() ottl.Factory[K] {
Expand All @@ -32,33 +34,35 @@ func createAddElementXMLFunction[K any](_ ottl.FunctionContext, oArgs ottl.Argum
return nil, err
}

return addElementXML(args.Target, args.XPath), nil
if args.Name == "" {
return nil, fmt.Errorf("AddElementXML name must be non-empty")
}

return addElementXML(args.Target, args.XPath, args.Name), nil
}

// addElementXML returns a XML formatted string that is a result of removing all matching nodes from the target XML.
// This currently supports removal of elements, attributes, text values, comments, and CharData.
func addElementXML[K any](target ottl.StringGetter[K], xPath string) ottl.ExprFunc[K] {
// addElementXML returns a XML formatted string that is a result of adding a child element to all matching elements
// in the target XML.
func addElementXML[K any](target ottl.StringGetter[K], xPath string, name 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
}
var errs []error
xmlquery.FindEach(doc, xPath, func(_ int, n *xmlquery.Node) {
switch n.Type {
case xmlquery.ElementNode:
xmlquery.RemoveFromTree(n)
case xmlquery.AttributeNode:
n.Parent.RemoveAttr(n.Data)
case xmlquery.TextNode:
n.Data = ""
case xmlquery.CommentNode:
xmlquery.RemoveFromTree(n)
case xmlquery.CharDataNode:
xmlquery.RemoveFromTree(n)
case xmlquery.ElementNode, xmlquery.DocumentNode:
xmlquery.AddChild(n, &xmlquery.Node{
Type: xmlquery.ElementNode,
Data: name,
})
default:
errs = append(errs, fmt.Errorf("AddElementXML XPath selected non-element: %q", n.Data))
}
})
return doc.OutputXML(false), nil
return doc.OutputXML(false), errors.Join(errs...)
}
}
140 changes: 140 additions & 0 deletions pkg/ottl/ottlfuncs/func_add_element_xml_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
// 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_AddElementXML(t *testing.T) {
tests := []struct {
name string
document string
xPath string
elementName string
want string
expectErr string
}{
{
name: "add single element",
document: `<a></a>`,
xPath: "/a",
elementName: "b",
want: `<a><b></b></a>`,
},
{
name: "add to multiple elements",
document: `<a></a><a></a>`,
xPath: "/a",
elementName: "b",
want: `<a><b></b></a><a><b></b></a>`,
},
{
name: "add at multiple levels",
document: `<a></a><b><a></a></b>`,
xPath: "//a",
elementName: "c",
want: `<a><c></c></a><b><a><c></c></a></b>`,
},
{
name: "add root element to empty document",
document: ``,
xPath: "/",
elementName: "a",
want: `<a></a>`,
},
{
name: "add root element to non-empty document",
document: `<a></a>`,
xPath: "/",
elementName: "a",
want: `<a></a><a></a>`,
},
{
name: "err on attribute",
document: `<a foo="bar"></a>`,
xPath: "/a/@foo",
elementName: "b",
want: `<a foo="bar"></a>`,
expectErr: `AddElementXML XPath selected non-element: "foo"`,
},
{
name: "err on text content",
document: `<a>foo</a>`,
xPath: "/a/text()",
elementName: "b",
want: `<a>foo</a>`,
expectErr: `AddElementXML XPath selected non-element: "foo"`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
f := NewAddElementXMLFactory[any]()
exprFunc, err := f.CreateFunction(
ottl.FunctionContext{},
&AddElementXMLArguments[any]{
Target: ottl.StandardStringGetter[any]{
Getter: func(_ context.Context, _ any) (any, error) {
return tt.document, nil
},
},
XPath: tt.xPath,
Name: tt.elementName,
})
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 TestCreateAddElementXMLFunc(t *testing.T) {
factory := NewAddElementXMLFactory[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, &AddElementXMLArguments[any]{
XPath: "!",
Name: "foo",
})
assert.Error(t, err)
assert.Nil(t, exprFunc)

// Empty Name should error on function creation
exprFunc, err = factory.CreateFunction(
fCtx, &AddElementXMLArguments[any]{
XPath: "/",
})
assert.Error(t, err)
assert.Nil(t, exprFunc)

// Invalid XML should error on function execution
exprFunc, err = factory.CreateFunction(
fCtx, &AddElementXMLArguments[any]{
Target: invalidXMLGetter(),
XPath: "/",
Name: "foo",
})
assert.NoError(t, err)
assert.NotNil(t, exprFunc)
_, err = exprFunc(context.Background(), nil)
assert.Error(t, err)
}
2 changes: 1 addition & 1 deletion pkg/ottl/ottlfuncs/func_remove_xml.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ func parseNodesXML(targetVal string) (*xmlquery.Node, error) {
if err != nil {
return nil, fmt.Errorf("parse xml: %w", err)
}
if !preserveDeclearation {
if !preserveDeclearation && top.FirstChild != nil {
xmlquery.RemoveFromTree(top.FirstChild)
}
return top, nil
Expand Down
1 change: 1 addition & 0 deletions pkg/ottl/ottlfuncs/functions.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ func StandardConverters[K any]() map[string]ottl.Factory[K] {
func converters[K any]() []ottl.Factory[K] {
return []ottl.Factory[K]{
// Converters
NewAddElementXMLFactory[K](),
NewBase64DecodeFactory[K](),
NewDecodeFactory[K](),
NewConcatFactory[K](),
Expand Down

0 comments on commit 2fe7e84

Please sign in to comment.