Skip to content

Commit

Permalink
[pkg/ottl]: add IsRootSpan converter (#33729)
Browse files Browse the repository at this point in the history
**Description:** <Describe what has changed.>
<!--Ex. Fixing a bug - Describe the bug and how this fixes the issue.
Ex. Adding a feature - Explain what this achieves.-->

Introduced `IsRootSpan()` converter function which returns `true` if the
span in the corresponding context is root, that means its
`parent_span_id` equals to hexadecimal representation of zero. In all
other scenarios function returns `false`.

**Link to tracking Issue:** #32918

**Testing:** 

- unit tests
- e2e tests

**Documentation:** <Describe the documentation added.>

- README entry

---------

Signed-off-by: odubajDT <[email protected]>
Co-authored-by: Tiffany Hrabusa <[email protected]>
  • Loading branch information
odubajDT and tiffany76 authored Jul 14, 2024
1 parent 6a5b9e6 commit 3c64a31
Show file tree
Hide file tree
Showing 9 changed files with 192 additions and 2 deletions.
27 changes: 27 additions & 0 deletions .chloggen/add-isrootspan.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 IsRootSpan() converter function."

# Mandatory: One or more tracking issues related to the change. You can use the PR number here if no issue exists.
issues: [32918]

# (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: Converter `IsRootSpan()` returns `true` if the span in the corresponding context is root, that means its `parent_span_id` equals to hexadecimal representation of zero. In all other scenarios function returns `false`.

# 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: []
5 changes: 4 additions & 1 deletion internal/filter/filterottl/functions.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,10 @@ import (
)

func StandardSpanFuncs() map[string]ottl.Factory[ottlspan.TransformContext] {
return ottlfuncs.StandardConverters[ottlspan.TransformContext]()
m := ottlfuncs.StandardConverters[ottlspan.TransformContext]()
isRootSpanFactory := ottlfuncs.NewIsRootSpanFactory()
m[isRootSpanFactory.Name()] = isRootSpanFactory
return m
}

func StandardSpanEventFuncs() map[string]ottl.Factory[ottlspanevent.TransformContext] {
Expand Down
66 changes: 66 additions & 0 deletions pkg/ottl/e2e/e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,13 @@ import (
"go.opentelemetry.io/collector/component/componenttest"
"go.opentelemetry.io/collector/pdata/pcommon"
"go.opentelemetry.io/collector/pdata/plog"
"go.opentelemetry.io/collector/pdata/ptrace"

"github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl/contexts/ottllog"
"github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl/contexts/ottlspan"
"github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl/ottlfuncs"
"github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatatest/plogtest"
"github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatatest/ptracetest"
)

var (
Expand Down Expand Up @@ -840,6 +843,41 @@ func Test_e2e_ottl_features(t *testing.T) {
}
}

func Test_ProcessTraces_TraceContext(t *testing.T) {
tests := []struct {
statement string
want func(_ ottlspan.TransformContext)
}{
{
statement: `set(attributes["entrypoint-root"], name) where IsRootSpan()`,
want: func(tCtx ottlspan.TransformContext) {
tCtx.GetSpan().Attributes().PutStr("entrypoint-root", "operationB")
},
},
}

for _, tt := range tests {
t.Run(tt.statement, func(t *testing.T) {
settings := componenttest.NewNopTelemetrySettings()
funcs := ottlfuncs.StandardFuncs[ottlspan.TransformContext]()
isRootSpanFactory := ottlfuncs.NewIsRootSpanFactory()
funcs[isRootSpanFactory.Name()] = isRootSpanFactory
spanParser, err := ottlspan.NewParser(funcs, settings)
assert.NoError(t, err)
spanStatements, err := spanParser.ParseStatement(tt.statement)
assert.NoError(t, err)

tCtx := constructSpanTransformContext()
_, _, _ = spanStatements.Execute(context.Background(), tCtx)

exTCtx := constructSpanTransformContext()
tt.want(exTCtx)

assert.NoError(t, ptracetest.CompareResourceSpans(newResourceSpans(exTCtx), newResourceSpans(tCtx)))
})
}
}

func constructLogTransformContext() ottllog.TransformContext {
resource := pcommon.NewResource()
resource.Attributes().PutStr("host.name", "localhost")
Expand Down Expand Up @@ -873,6 +911,18 @@ func constructLogTransformContext() ottllog.TransformContext {
return ottllog.NewTransformContext(logRecord, scope, resource, plog.NewScopeLogs(), plog.NewResourceLogs())
}

func constructSpanTransformContext() ottlspan.TransformContext {
resource := pcommon.NewResource()

scope := pcommon.NewInstrumentationScope()
scope.SetName("scope")

td := ptrace.NewSpan()
fillSpanOne(td)

return ottlspan.NewTransformContext(td, scope, resource, ptrace.NewScopeSpans(), ptrace.NewResourceSpans())
}

func newResourceLogs(tCtx ottllog.TransformContext) plog.ResourceLogs {
rl := plog.NewResourceLogs()
tCtx.GetResource().CopyTo(rl.Resource())
Expand All @@ -882,3 +932,19 @@ func newResourceLogs(tCtx ottllog.TransformContext) plog.ResourceLogs {
tCtx.GetLogRecord().CopyTo(l)
return rl
}

func newResourceSpans(tCtx ottlspan.TransformContext) ptrace.ResourceSpans {
rl := ptrace.NewResourceSpans()
tCtx.GetResource().CopyTo(rl.Resource())
sl := rl.ScopeSpans().AppendEmpty()
tCtx.GetInstrumentationScope().CopyTo(sl.Scope())
l := sl.Spans().AppendEmpty()
tCtx.GetSpan().CopyTo(l)
return rl
}

func fillSpanOne(span ptrace.Span) {
span.SetName("operationB")
span.SetSpanID(spanID)
span.SetTraceID(traceID)
}
18 changes: 18 additions & 0 deletions pkg/ottl/ottlfuncs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -424,6 +424,7 @@ Available Converters:
- [IsBool](#isbool)
- [IsDouble](#isdouble)
- [IsInt](#isint)
- [IsRootSpan](#isrootspan)
- [IsMap](#ismap)
- [IsMatch](#ismatch)
- [IsList](#islist)
Expand Down Expand Up @@ -744,6 +745,23 @@ Examples:

- `IsInt(attributes["maybe a int"])`

### IsRootSpan

`IsRootSpan()`

The `IsRootSpan` Converter returns `true` if the span in the corresponding context is root, which means
its `parent_span_id` is equal to hexadecimal representation of zero.

This function is supported with [OTTL span context](../contexts/ottlspan/README.md). In any other context it is not supported.

The function returns `false` in all other scenarios, including `parent_span_id == ""` or `parent_span_id == nil`.

Examples:

- `IsRootSpan()`

- `set(attributes["isRoot"], "true") where IsRootSpan()`

### IsMap

`IsMap(value)`
Expand Down
25 changes: 25 additions & 0 deletions pkg/ottl/ottlfuncs/func_is_root_span.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// 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"

"github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl"
"github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl/contexts/ottlspan"
)

func NewIsRootSpanFactory() ottl.Factory[ottlspan.TransformContext] {
return ottl.NewFactory("IsRootSpan", nil, createIsRootSpanFunction)
}

func createIsRootSpanFunction(_ ottl.FunctionContext, _ ottl.Arguments) (ottl.ExprFunc[ottlspan.TransformContext], error) {
return isRootSpan()
}

func isRootSpan() (ottl.ExprFunc[ottlspan.TransformContext], error) {
return func(_ context.Context, tCtx ottlspan.TransformContext) (any, error) {
return tCtx.GetSpan().ParentSpanID().IsEmpty(), nil
}, nil
}
40 changes: 40 additions & 0 deletions pkg/ottl/ottlfuncs/func_is_root_span_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

package ottlfuncs // import "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl/ottlfuncs"

import (
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.opentelemetry.io/collector/pdata/pcommon"
"go.opentelemetry.io/collector/pdata/ptrace"

"github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl/contexts/ottlspan"
)

func Test_IsRootSpan(t *testing.T) {
exprFunc, err := isRootSpan()
assert.NoError(t, err)

// root span
spanRoot := ptrace.NewSpan()
spanRoot.SetParentSpanID(pcommon.SpanID{
0, 0, 0, 0, 0, 0, 0, 0,
})

value, err := exprFunc(nil, ottlspan.NewTransformContext(spanRoot, pcommon.NewInstrumentationScope(), pcommon.NewResource(), ptrace.NewScopeSpans(), ptrace.NewResourceSpans()))
assert.NoError(t, err)
require.Equal(t, true, value)

// non root span
spanNonRoot := ptrace.NewSpan()
spanNonRoot.SetParentSpanID(pcommon.SpanID{
1, 0, 0, 0, 0, 0, 0, 0,
})

value, err = exprFunc(nil, ottlspan.NewTransformContext(spanNonRoot, pcommon.NewInstrumentationScope(), pcommon.NewResource(), ptrace.NewScopeSpans(), ptrace.NewResourceSpans()))
assert.NoError(t, err)
require.Equal(t, false, value)
}
5 changes: 4 additions & 1 deletion processor/transformprocessor/internal/traces/functions.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@ import (

func SpanFunctions() map[string]ottl.Factory[ottlspan.TransformContext] {
// No trace-only functions yet.
return ottlfuncs.StandardFuncs[ottlspan.TransformContext]()
m := ottlfuncs.StandardFuncs[ottlspan.TransformContext]()
isRootSpanFactory := ottlfuncs.NewIsRootSpanFactory()
m[isRootSpanFactory.Name()] = isRootSpanFactory
return m
}

func SpanEventFunctions() map[string]ottl.Factory[ottlspanevent.TransformContext] {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import (

func Test_SpanFunctions(t *testing.T) {
expected := ottlfuncs.StandardFuncs[ottlspan.TransformContext]()
isRootSpanFactory := ottlfuncs.NewIsRootSpanFactory()
expected[isRootSpanFactory.Name()] = isRootSpanFactory
actual := SpanFunctions()
require.Equal(t, len(expected), len(actual))
for k := range actual {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,12 @@ func Test_ProcessTraces_TraceContext(t *testing.T) {
td.ResourceSpans().At(0).ScopeSpans().At(0).Spans().At(1).Attributes().PutStr("entrypoint", "operationB")
},
},
{
statement: `set(attributes["entrypoint-root"], name) where IsRootSpan()`,
want: func(td ptrace.Traces) {
td.ResourceSpans().At(0).ScopeSpans().At(0).Spans().At(1).Attributes().PutStr("entrypoint-root", "operationB")
},
},
{
statement: `set(attributes["test"], ConvertCase(name, "lower")) where name == "operationA"`,
want: func(td ptrace.Traces) {
Expand Down

0 comments on commit 3c64a31

Please sign in to comment.