diff --git a/.chloggen/add-isrootspan.yaml b/.chloggen/add-isrootspan.yaml new file mode 100644 index 000000000000..5500aea116c4 --- /dev/null +++ b/.chloggen/add-isrootspan.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 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: [] diff --git a/internal/filter/filterottl/functions.go b/internal/filter/filterottl/functions.go index c3ce56ce4abf..3612d184966f 100644 --- a/internal/filter/filterottl/functions.go +++ b/internal/filter/filterottl/functions.go @@ -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] { diff --git a/pkg/ottl/e2e/e2e_test.go b/pkg/ottl/e2e/e2e_test.go index bcdc42802c4a..5b9d52b8ab39 100644 --- a/pkg/ottl/e2e/e2e_test.go +++ b/pkg/ottl/e2e/e2e_test.go @@ -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 ( @@ -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") @@ -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()) @@ -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) +} diff --git a/pkg/ottl/ottlfuncs/README.md b/pkg/ottl/ottlfuncs/README.md index dc52f27c999e..519de40bc45a 100644 --- a/pkg/ottl/ottlfuncs/README.md +++ b/pkg/ottl/ottlfuncs/README.md @@ -424,6 +424,7 @@ Available Converters: - [IsBool](#isbool) - [IsDouble](#isdouble) - [IsInt](#isint) +- [IsRootSpan](#isrootspan) - [IsMap](#ismap) - [IsMatch](#ismatch) - [IsList](#islist) @@ -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)` diff --git a/pkg/ottl/ottlfuncs/func_is_root_span.go b/pkg/ottl/ottlfuncs/func_is_root_span.go new file mode 100644 index 000000000000..1aaba5a7bb4b --- /dev/null +++ b/pkg/ottl/ottlfuncs/func_is_root_span.go @@ -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 +} diff --git a/pkg/ottl/ottlfuncs/func_is_root_span_test.go b/pkg/ottl/ottlfuncs/func_is_root_span_test.go new file mode 100644 index 000000000000..68dc177cbe1e --- /dev/null +++ b/pkg/ottl/ottlfuncs/func_is_root_span_test.go @@ -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) +} diff --git a/processor/transformprocessor/internal/traces/functions.go b/processor/transformprocessor/internal/traces/functions.go index b64b33275808..91803c4d1cf5 100644 --- a/processor/transformprocessor/internal/traces/functions.go +++ b/processor/transformprocessor/internal/traces/functions.go @@ -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] { diff --git a/processor/transformprocessor/internal/traces/functions_test.go b/processor/transformprocessor/internal/traces/functions_test.go index 40d14a7e90b5..3281d91abbfc 100644 --- a/processor/transformprocessor/internal/traces/functions_test.go +++ b/processor/transformprocessor/internal/traces/functions_test.go @@ -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 { diff --git a/processor/transformprocessor/internal/traces/processor_test.go b/processor/transformprocessor/internal/traces/processor_test.go index 134182716c81..e6928ba9fa38 100644 --- a/processor/transformprocessor/internal/traces/processor_test.go +++ b/processor/transformprocessor/internal/traces/processor_test.go @@ -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) {