From 89725708805e7e3724bb81be87d4a8520d223da4 Mon Sep 17 00:00:00 2001 From: Edmo Vamerlatti Costa <11836452+edmocosta@users.noreply.github.com> Date: Fri, 20 Dec 2024 22:07:36 +0100 Subject: [PATCH] [pkg/ottl] Change OTTL contexts to handle paths with context (#36820) Co-authored-by: Evan Bradley <11745660+evan-bradley@users.noreply.github.com> --- ...contexts-to-support-path-with-context.yaml | 27 ++++ pkg/ottl/contexts/internal/metric.go | 6 +- pkg/ottl/contexts/internal/resource.go | 6 +- pkg/ottl/contexts/internal/scope.go | 7 +- pkg/ottl/contexts/internal/scope_test.go | 12 ++ pkg/ottl/contexts/internal/span.go | 17 ++- pkg/ottl/contexts/ottldatapoint/datapoint.go | 60 ++++++-- .../contexts/ottldatapoint/datapoint_test.go | 143 ++++++++++++++++++ pkg/ottl/contexts/ottllog/log.go | 55 +++++-- pkg/ottl/contexts/ottllog/log_test.go | 68 +++++++++ pkg/ottl/contexts/ottlmetric/metrics.go | 48 +++++- pkg/ottl/contexts/ottlmetric/metrics_test.go | 72 +++++++++ pkg/ottl/contexts/ottlresource/resource.go | 20 +++ .../contexts/ottlresource/resource_test.go | 11 ++ pkg/ottl/contexts/ottlscope/scope.go | 43 +++++- pkg/ottl/contexts/ottlscope/scope_test.go | 58 +++++++ pkg/ottl/contexts/ottlspan/span.go | 48 +++++- pkg/ottl/contexts/ottlspan/span_test.go | 72 +++++++++ .../contexts/ottlspanevent/span_events.go | 59 +++++++- .../ottlspanevent/span_events_test.go | 85 +++++++++++ 20 files changed, 870 insertions(+), 47 deletions(-) create mode 100644 .chloggen/change-contexts-to-support-path-with-context.yaml diff --git a/.chloggen/change-contexts-to-support-path-with-context.yaml b/.chloggen/change-contexts-to-support-path-with-context.yaml new file mode 100644 index 000000000000..451da0538236 --- /dev/null +++ b/.chloggen/change-contexts-to-support-path-with-context.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: "Change OTTL contexts to properly handle `ottl.Path` with context" + +# Mandatory: One or more tracking issues related to the change. You can use the PR number here if no issue exists. +issues: [29017] + +# (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: "The OTTL contexts have a new option `EnablePathContextNames` to enable support for expressing a statement's context via path names in the statement" + +# 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: [api] diff --git a/pkg/ottl/contexts/internal/metric.go b/pkg/ottl/contexts/internal/metric.go index 92dbee9b3374..0b299f89189e 100644 --- a/pkg/ottl/contexts/internal/metric.go +++ b/pkg/ottl/contexts/internal/metric.go @@ -11,6 +11,10 @@ import ( "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl" ) +const ( + MetricContextName = "metric" +) + type MetricContext interface { GetMetric() pmetric.Metric } @@ -29,7 +33,7 @@ var MetricSymbolTable = map[ottl.EnumSymbol]ottl.Enum{ func MetricPathGetSetter[K MetricContext](path ottl.Path[K]) (ottl.GetSetter[K], error) { if path == nil { - return nil, FormatDefaultErrorMessage("metric", "metric", "Metric", MetricRef) + return nil, FormatDefaultErrorMessage(MetricContextName, MetricContextName, "Metric", MetricRef) } switch path.Name() { case "name": diff --git a/pkg/ottl/contexts/internal/resource.go b/pkg/ottl/contexts/internal/resource.go index 101bbf178244..a3ae9d149f3f 100644 --- a/pkg/ottl/contexts/internal/resource.go +++ b/pkg/ottl/contexts/internal/resource.go @@ -11,6 +11,10 @@ import ( "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl" ) +const ( + ResourceContextName = "resource" +) + type ResourceContext interface { GetResource() pcommon.Resource GetResourceSchemaURLItem() SchemaURLItem @@ -18,7 +22,7 @@ type ResourceContext interface { func ResourcePathGetSetter[K ResourceContext](path ottl.Path[K]) (ottl.GetSetter[K], error) { if path == nil { - return nil, FormatDefaultErrorMessage("resource", "resource", "Resource", ResourceContextRef) + return nil, FormatDefaultErrorMessage(ResourceContextName, ResourceContextName, "Resource", ResourceContextRef) } switch path.Name() { case "attributes": diff --git a/pkg/ottl/contexts/internal/scope.go b/pkg/ottl/contexts/internal/scope.go index 7a698ebc533f..4fabf9ee7d89 100644 --- a/pkg/ottl/contexts/internal/scope.go +++ b/pkg/ottl/contexts/internal/scope.go @@ -11,6 +11,11 @@ import ( "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl" ) +const ( + InstrumentationScopeContextName = "instrumentation_scope" + ScopeContextName = "scope" +) + type InstrumentationScopeContext interface { GetInstrumentationScope() pcommon.InstrumentationScope GetScopeSchemaURLItem() SchemaURLItem @@ -18,7 +23,7 @@ type InstrumentationScopeContext interface { func ScopePathGetSetter[K InstrumentationScopeContext](path ottl.Path[K]) (ottl.GetSetter[K], error) { if path == nil { - return nil, FormatDefaultErrorMessage("instrumentation_scope", "instrumentation_scope", "Instrumentation Scope", InstrumentationScopeRef) + return nil, FormatDefaultErrorMessage(InstrumentationScopeContextName, InstrumentationScopeContextName, "Instrumentation Scope", InstrumentationScopeRef) } switch path.Name() { case "name": diff --git a/pkg/ottl/contexts/internal/scope_test.go b/pkg/ottl/contexts/internal/scope_test.go index 84f0c653b587..788924115bd5 100644 --- a/pkg/ottl/contexts/internal/scope_test.go +++ b/pkg/ottl/contexts/internal/scope_test.go @@ -334,6 +334,18 @@ func TestScopePathGetSetter(t *testing.T) { s.AppendEmpty().SetEmptySlice().AppendEmpty().SetStr("new") }, }, + { + name: "scope with context", + path: &TestPath[*instrumentationScopeContext]{ + C: "scope", + N: "name", + }, + orig: refIS.Name(), + newVal: "newname", + modified: func(is pcommon.InstrumentationScope) { + is.SetName("newname") + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/pkg/ottl/contexts/internal/span.go b/pkg/ottl/contexts/internal/span.go index c1ebf5a9b109..4669adb7b8fe 100644 --- a/pkg/ottl/contexts/internal/span.go +++ b/pkg/ottl/contexts/internal/span.go @@ -18,7 +18,8 @@ import ( ) const ( - SpanContextName = "Span" + SpanContextName = "span" + SpanContextNameDescription = "Span" ) type SpanContext interface { @@ -39,7 +40,7 @@ var SpanSymbolTable = map[ottl.EnumSymbol]ottl.Enum{ func SpanPathGetSetter[K SpanContext](path ottl.Path[K]) (ottl.GetSetter[K], error) { if path == nil { - return nil, FormatDefaultErrorMessage("span", "span", SpanContextName, SpanRef) + return nil, FormatDefaultErrorMessage(SpanContextName, SpanContextName, SpanContextNameDescription, SpanRef) } switch path.Name() { case "trace_id": @@ -48,7 +49,7 @@ func SpanPathGetSetter[K SpanContext](path ottl.Path[K]) (ottl.GetSetter[K], err if nextPath.Name() == "string" { return accessStringTraceID[K](), nil } - return nil, FormatDefaultErrorMessage(nextPath.Name(), nextPath.String(), SpanContextName, SpanRef) + return nil, FormatDefaultErrorMessage(nextPath.Name(), nextPath.String(), SpanContextNameDescription, SpanRef) } return accessTraceID[K](), nil case "span_id": @@ -57,7 +58,7 @@ func SpanPathGetSetter[K SpanContext](path ottl.Path[K]) (ottl.GetSetter[K], err if nextPath.Name() == "string" { return accessStringSpanID[K](), nil } - return nil, FormatDefaultErrorMessage(nextPath.Name(), nextPath.String(), SpanContextName, SpanRef) + return nil, FormatDefaultErrorMessage(nextPath.Name(), nextPath.String(), SpanContextNameDescription, SpanRef) } return accessSpanID[K](), nil case "trace_state": @@ -72,7 +73,7 @@ func SpanPathGetSetter[K SpanContext](path ottl.Path[K]) (ottl.GetSetter[K], err if nextPath.Name() == "string" { return accessStringParentSpanID[K](), nil } - return nil, FormatDefaultErrorMessage(nextPath.Name(), nextPath.String(), SpanContextName, SpanRef) + return nil, FormatDefaultErrorMessage(nextPath.Name(), nextPath.String(), SpanContextNameDescription, SpanRef) } return accessParentSpanID[K](), nil case "name": @@ -86,7 +87,7 @@ func SpanPathGetSetter[K SpanContext](path ottl.Path[K]) (ottl.GetSetter[K], err case "deprecated_string": return accessDeprecatedStringKind[K](), nil default: - return nil, FormatDefaultErrorMessage(nextPath.Name(), nextPath.String(), SpanContextName, SpanRef) + return nil, FormatDefaultErrorMessage(nextPath.Name(), nextPath.String(), SpanContextNameDescription, SpanRef) } } return accessKind[K](), nil @@ -123,12 +124,12 @@ func SpanPathGetSetter[K SpanContext](path ottl.Path[K]) (ottl.GetSetter[K], err case "message": return accessStatusMessage[K](), nil default: - return nil, FormatDefaultErrorMessage(nextPath.Name(), nextPath.String(), SpanContextName, SpanRef) + return nil, FormatDefaultErrorMessage(nextPath.Name(), nextPath.String(), SpanContextNameDescription, SpanRef) } } return accessStatus[K](), nil default: - return nil, FormatDefaultErrorMessage(path.Name(), path.String(), SpanContextName, SpanRef) + return nil, FormatDefaultErrorMessage(path.Name(), path.String(), SpanContextNameDescription, SpanRef) } } diff --git a/pkg/ottl/contexts/ottldatapoint/datapoint.go b/pkg/ottl/contexts/ottldatapoint/datapoint.go index 9c50d85cd723..d231c4210813 100644 --- a/pkg/ottl/contexts/ottldatapoint/datapoint.go +++ b/pkg/ottl/contexts/ottldatapoint/datapoint.go @@ -20,7 +20,9 @@ import ( ) const ( - contextName = "DataPoint" + // Experimental: *NOTE* this constant is subject to change or removal in the future. + ContextName = "datapoint" + contextNameDescription = "DataPoint" ) var ( @@ -124,6 +126,22 @@ func NewParser(functions map[string]ottl.Factory[TransformContext], telemetrySet return p, nil } +// EnablePathContextNames enables the support to path's context names on statements. +// When this option is configured, all statement's paths must have a valid context prefix, +// otherwise an error is reported. +// +// Experimental: *NOTE* this option is subject to change or removal in the future. +func EnablePathContextNames() Option { + return func(p *ottl.Parser[TransformContext]) { + ottl.WithPathContextNames[TransformContext]([]string{ + ContextName, + internal.ResourceContextName, + internal.InstrumentationScopeContextName, + internal.MetricContextName, + })(p) + } +} + type StatementSequenceOption func(*ottl.StatementSequence[TransformContext]) func WithStatementSequenceErrorMode(errorMode ottl.ErrorMode) StatementSequenceOption { @@ -185,18 +203,23 @@ func (pep *pathExpressionParser) parsePath(path ottl.Path[TransformContext]) (ot if path == nil { return nil, fmt.Errorf("path cannot be nil") } + // Higher contexts parsing + if path.Context() != "" && path.Context() != ContextName { + return pep.parseHigherContextPath(path.Context(), path) + } + // Backward compatibility with paths without context + if path.Context() == "" && (path.Name() == internal.ResourceContextName || + path.Name() == internal.InstrumentationScopeContextName || + path.Name() == internal.MetricContextName) { + return pep.parseHigherContextPath(path.Name(), path.Next()) + } + switch path.Name() { case "cache": if path.Keys() == nil { return accessCache(), nil } return accessCacheKey(path.Keys()), nil - case "resource": - return internal.ResourcePathGetSetter[TransformContext](path.Next()) - case "instrumentation_scope": - return internal.ScopePathGetSetter[TransformContext](path.Next()) - case "metric": - return internal.MetricPathGetSetter[TransformContext](path.Next()) case "attributes": if path.Keys() == nil { return accessAttributes(), nil @@ -239,7 +262,7 @@ func (pep *pathExpressionParser) parsePath(path ottl.Path[TransformContext]) (ot case "bucket_counts": return accessPositiveBucketCounts(), nil default: - return nil, internal.FormatDefaultErrorMessage(nextPath.Name(), path.String(), contextName, internal.DataPointRef) + return nil, internal.FormatDefaultErrorMessage(nextPath.Name(), path.String(), contextNameDescription, internal.DataPointRef) } } return accessPositive(), nil @@ -252,14 +275,31 @@ func (pep *pathExpressionParser) parsePath(path ottl.Path[TransformContext]) (ot case "bucket_counts": return accessNegativeBucketCounts(), nil default: - return nil, internal.FormatDefaultErrorMessage(nextPath.Name(), path.String(), contextName, internal.DataPointRef) + return nil, internal.FormatDefaultErrorMessage(nextPath.Name(), path.String(), contextNameDescription, internal.DataPointRef) } } return accessNegative(), nil case "quantile_values": return accessQuantileValues(), nil default: - return nil, internal.FormatDefaultErrorMessage(path.Name(), path.String(), contextName, internal.DataPointRef) + return nil, internal.FormatDefaultErrorMessage(path.Name(), path.String(), contextNameDescription, internal.DataPointRef) + } +} + +func (pep *pathExpressionParser) parseHigherContextPath(context string, path ottl.Path[TransformContext]) (ottl.GetSetter[TransformContext], error) { + switch context { + case internal.ResourceContextName: + return internal.ResourcePathGetSetter(path) + case internal.InstrumentationScopeContextName: + return internal.ScopePathGetSetter(path) + case internal.MetricContextName: + return internal.MetricPathGetSetter(path) + default: + var fullPath string + if path != nil { + fullPath = path.String() + } + return nil, internal.FormatDefaultErrorMessage(context, fullPath, contextNameDescription, internal.DataPointRef) } } diff --git a/pkg/ottl/contexts/ottldatapoint/datapoint_test.go b/pkg/ottl/contexts/ottldatapoint/datapoint_test.go index a63542f74c99..604f008865ec 100644 --- a/pkg/ottl/contexts/ottldatapoint/datapoint_test.go +++ b/pkg/ottl/contexts/ottldatapoint/datapoint_test.go @@ -5,10 +5,12 @@ package ottldatapoint import ( "context" + "slices" "testing" "time" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "go.opentelemetry.io/collector/pdata/pcommon" "go.opentelemetry.io/collector/pdata/pmetric" @@ -57,6 +59,16 @@ func Test_newPathGetSetter_Cache(t *testing.T) { }, }, } + // Copy all tests cases and sets the path.Context value to the generated ones. + // It ensures all exiting field access also work when the path context is set. + for _, tt := range slices.Clone(tests) { + testWithContext := tt + testWithContext.name = "with_path_context:" + tt.name + pathWithContext := *tt.path.(*internal.TestPath[TransformContext]) + pathWithContext.C = ContextName + testWithContext.path = ottl.Path[TransformContext](&pathWithContext) + tests = append(tests, testWithContext) + } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { pep := pathExpressionParser{} @@ -475,6 +487,16 @@ func Test_newPathGetSetter_NumberDataPoint(t *testing.T) { }, }, } + // Copy all tests cases and sets the path.Context value to the generated ones. + // It ensures all exiting field access also work when the path context is set. + for _, tt := range slices.Clone(tests) { + testWithContext := tt + testWithContext.name = "with_path_context:" + tt.name + pathWithContext := *tt.path.(*internal.TestPath[TransformContext]) + pathWithContext.C = ContextName + testWithContext.path = ottl.Path[TransformContext](&pathWithContext) + tests = append(tests, testWithContext) + } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { pep := pathExpressionParser{} @@ -909,6 +931,16 @@ func Test_newPathGetSetter_HistogramDataPoint(t *testing.T) { }, }, } + // Copy all tests cases and sets the path.Context value to the generated ones. + // It ensures all exiting field access also work when the path context is set. + for _, tt := range slices.Clone(tests) { + testWithContext := tt + testWithContext.name = "with_path_context:" + tt.name + pathWithContext := *tt.path.(*internal.TestPath[TransformContext]) + pathWithContext.C = ContextName + testWithContext.path = ottl.Path[TransformContext](&pathWithContext) + tests = append(tests, testWithContext) + } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { pep := pathExpressionParser{} @@ -1427,6 +1459,16 @@ func Test_newPathGetSetter_ExpoHistogramDataPoint(t *testing.T) { }, }, } + // Copy all tests cases and sets the path.Context value to the generated ones. + // It ensures all exiting field access also work when the path context is set. + for _, tt := range slices.Clone(tests) { + testWithContext := tt + testWithContext.name = "with_path_context:" + tt.name + pathWithContext := *tt.path.(*internal.TestPath[TransformContext]) + pathWithContext.C = ContextName + testWithContext.path = ottl.Path[TransformContext](&pathWithContext) + tests = append(tests, testWithContext) + } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { pep := pathExpressionParser{} @@ -1846,6 +1888,16 @@ func Test_newPathGetSetter_SummaryDataPoint(t *testing.T) { }, }, } + // Copy all tests cases and sets the path.Context value to the generated ones. + // It ensures all exiting field access also work when the path context is set. + for _, tt := range slices.Clone(tests) { + testWithContext := tt + testWithContext.name = "with_path_context:" + tt.name + pathWithContext := *tt.path.(*internal.TestPath[TransformContext]) + pathWithContext.C = ContextName + testWithContext.path = ottl.Path[TransformContext](&pathWithContext) + tests = append(tests, testWithContext) + } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { pep := pathExpressionParser{} @@ -2016,6 +2068,17 @@ func Test_newPathGetSetter_Metric(t *testing.T) { metric.Sum().SetIsMonotonic(false) }, }, + { + name: "metric field with context", + path: &internal.TestPath[TransformContext]{ + C: "metric", + N: "type", + }, + orig: int64(pmetric.MetricTypeSum), + newVal: int64(pmetric.MetricTypeSum), + modified: func(_ pmetric.Metric) { + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -2143,3 +2206,83 @@ func Test_ParseEnum_False(t *testing.T) { }) } } + +func Test_newPathGetSetter_higherContextPath(t *testing.T) { + resource := pcommon.NewResource() + resource.Attributes().PutStr("foo", "bar") + + metric := pmetric.NewMetric() + metric.SetName("metric") + + instrumentationScope := pcommon.NewInstrumentationScope() + instrumentationScope.SetName("instrumentation_scope") + + ctx := NewTransformContext( + pmetric.NewNumberDataPoint(), + metric, + pmetric.NewMetricSlice(), + instrumentationScope, + resource, + pmetric.NewScopeMetrics(), + pmetric.NewResourceMetrics()) + + tests := []struct { + name string + path ottl.Path[TransformContext] + expected any + }{ + { + name: "resource", + path: &internal.TestPath[TransformContext]{C: "", N: "resource", NextPath: &internal.TestPath[TransformContext]{ + N: "attributes", + KeySlice: []ottl.Key[TransformContext]{ + &internal.TestKey[TransformContext]{ + S: ottltest.Strp("foo"), + }, + }, + }}, + expected: "bar", + }, + { + name: "resource with context", + path: &internal.TestPath[TransformContext]{C: "resource", N: "attributes", KeySlice: []ottl.Key[TransformContext]{ + &internal.TestKey[TransformContext]{ + S: ottltest.Strp("foo"), + }, + }}, + expected: "bar", + }, + { + name: "metric", + path: &internal.TestPath[TransformContext]{N: "metric", NextPath: &internal.TestPath[TransformContext]{N: "name"}}, + expected: metric.Name(), + }, + { + name: "metric with context", + path: &internal.TestPath[TransformContext]{C: "metric", N: "name"}, + expected: metric.Name(), + }, + { + name: "instrumentation_scope", + path: &internal.TestPath[TransformContext]{N: "instrumentation_scope", NextPath: &internal.TestPath[TransformContext]{N: "name"}}, + expected: instrumentationScope.Name(), + }, + { + name: "instrumentation_scope with context", + path: &internal.TestPath[TransformContext]{C: "instrumentation_scope", N: "name"}, + expected: instrumentationScope.Name(), + }, + } + + pep := pathExpressionParser{} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + accessor, err := pep.parsePath(tt.path) + require.NoError(t, err) + + got, err := accessor.Get(context.Background(), ctx) + assert.NoError(t, err) + assert.Equal(t, tt.expected, got) + }) + } +} diff --git a/pkg/ottl/contexts/ottllog/log.go b/pkg/ottl/contexts/ottllog/log.go index 7ca056730cc7..4983e60ddc6c 100644 --- a/pkg/ottl/contexts/ottllog/log.go +++ b/pkg/ottl/contexts/ottllog/log.go @@ -22,7 +22,9 @@ import ( ) const ( - contextName = "Log" + // Experimental: *NOTE* this constant is subject to change or removal in the future. + ContextName = "log" + contextNameDescription = "Log" ) var ( @@ -121,6 +123,21 @@ func NewParser(functions map[string]ottl.Factory[TransformContext], telemetrySet return p, nil } +// EnablePathContextNames enables the support to path's context names on statements. +// When this option is configured, all statement's paths must have a valid context prefix, +// otherwise an error is reported. +// +// Experimental: *NOTE* this option is subject to change or removal in the future. +func EnablePathContextNames() Option { + return func(p *ottl.Parser[TransformContext]) { + ottl.WithPathContextNames[TransformContext]([]string{ + ContextName, + internal.InstrumentationScopeContextName, + internal.ResourceContextName, + })(p) + } +} + type StatementSequenceOption func(*ottl.StatementSequence[TransformContext]) func WithStatementSequenceErrorMode(errorMode ottl.ErrorMode) StatementSequenceOption { @@ -199,16 +216,21 @@ func (pep *pathExpressionParser) parsePath(path ottl.Path[TransformContext]) (ot if path == nil { return nil, fmt.Errorf("path cannot be nil") } + // Higher contexts parsing + if path.Context() != "" && path.Context() != ContextName { + return pep.parseHigherContextPath(path.Context(), path) + } + // Backward compatibility with paths without context + if path.Context() == "" && (path.Name() == internal.ResourceContextName || path.Name() == internal.InstrumentationScopeContextName) { + return pep.parseHigherContextPath(path.Name(), path.Next()) + } + switch path.Name() { case "cache": if path.Keys() == nil { return accessCache(), nil } return accessCacheKey(path.Keys()), nil - case "resource": - return internal.ResourcePathGetSetter[TransformContext](path.Next()) - case "instrumentation_scope": - return internal.ScopePathGetSetter[TransformContext](path.Next()) case "time_unix_nano": return accessTimeUnixNano(), nil case "observed_time_unix_nano": @@ -227,7 +249,7 @@ func (pep *pathExpressionParser) parsePath(path ottl.Path[TransformContext]) (ot if nextPath.Name() == "string" { return accessStringBody(), nil } - return nil, internal.FormatDefaultErrorMessage(nextPath.Name(), nextPath.String(), contextName, internal.LogRef) + return nil, internal.FormatDefaultErrorMessage(nextPath.Name(), nextPath.String(), contextNameDescription, internal.LogRef) } if path.Keys() == nil { return accessBody(), nil @@ -248,7 +270,7 @@ func (pep *pathExpressionParser) parsePath(path ottl.Path[TransformContext]) (ot if nextPath.Name() == "string" { return accessStringTraceID(), nil } - return nil, internal.FormatDefaultErrorMessage(nextPath.Name(), nextPath.String(), contextName, internal.LogRef) + return nil, internal.FormatDefaultErrorMessage(nextPath.Name(), nextPath.String(), contextNameDescription, internal.LogRef) } return accessTraceID(), nil case "span_id": @@ -257,11 +279,26 @@ func (pep *pathExpressionParser) parsePath(path ottl.Path[TransformContext]) (ot if nextPath.Name() == "string" { return accessStringSpanID(), nil } - return nil, internal.FormatDefaultErrorMessage(nextPath.Name(), path.String(), contextName, internal.LogRef) + return nil, internal.FormatDefaultErrorMessage(nextPath.Name(), path.String(), contextNameDescription, internal.LogRef) } return accessSpanID(), nil default: - return nil, internal.FormatDefaultErrorMessage(path.Name(), path.String(), contextName, internal.LogRef) + return nil, internal.FormatDefaultErrorMessage(path.Name(), path.String(), contextNameDescription, internal.LogRef) + } +} + +func (pep *pathExpressionParser) parseHigherContextPath(context string, path ottl.Path[TransformContext]) (ottl.GetSetter[TransformContext], error) { + switch context { + case internal.ResourceContextName: + return internal.ResourcePathGetSetter(path) + case internal.InstrumentationScopeContextName: + return internal.ScopePathGetSetter(path) + default: + var fullPath string + if path != nil { + fullPath = path.String() + } + return nil, internal.FormatDefaultErrorMessage(context, fullPath, contextNameDescription, internal.LogRef) } } diff --git a/pkg/ottl/contexts/ottllog/log_test.go b/pkg/ottl/contexts/ottllog/log_test.go index 43c2c848dddf..0e440052f1bd 100644 --- a/pkg/ottl/contexts/ottllog/log_test.go +++ b/pkg/ottl/contexts/ottllog/log_test.go @@ -7,10 +7,12 @@ import ( "context" "encoding/hex" "fmt" + "slices" "testing" "time" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "go.opentelemetry.io/collector/pdata/pcommon" "go.opentelemetry.io/collector/pdata/plog" @@ -597,6 +599,17 @@ func Test_newPathGetSetter(t *testing.T) { }, }, } + // Copy all tests cases and sets the path.Context value to the generated ones. + // It ensures all exiting field access also work when the path context is set. + for _, tt := range slices.Clone(tests) { + testWithContext := tt + testWithContext.name = "with_path_context:" + tt.name + pathWithContext := *tt.path.(*internal.TestPath[TransformContext]) + pathWithContext.C = ContextName + testWithContext.path = &pathWithContext + tests = append(tests, testWithContext) + } + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { pep := pathExpressionParser{} @@ -626,6 +639,61 @@ func Test_newPathGetSetter(t *testing.T) { } } +func Test_newPathGetSetter_higherContextPath(t *testing.T) { + logRec, instrumentationScope, resource := createTelemetry("string") + ctx := NewTransformContext(logRec, instrumentationScope, resource, plog.NewScopeLogs(), plog.NewResourceLogs()) + + tests := []struct { + name string + path ottl.Path[TransformContext] + expected any + }{ + { + name: "resource", + path: &internal.TestPath[TransformContext]{C: "", N: "resource", NextPath: &internal.TestPath[TransformContext]{ + N: "attributes", + KeySlice: []ottl.Key[TransformContext]{ + &internal.TestKey[TransformContext]{ + S: ottltest.Strp("str"), + }, + }, + }}, + expected: "val", + }, + { + name: "resource with context", + path: &internal.TestPath[TransformContext]{C: "resource", N: "attributes", KeySlice: []ottl.Key[TransformContext]{ + &internal.TestKey[TransformContext]{ + S: ottltest.Strp("str"), + }, + }}, + expected: "val", + }, + { + name: "instrumentation_scope", + path: &internal.TestPath[TransformContext]{N: "instrumentation_scope", NextPath: &internal.TestPath[TransformContext]{N: "name"}}, + expected: instrumentationScope.Name(), + }, + { + name: "instrumentation_scope with context", + path: &internal.TestPath[TransformContext]{C: "instrumentation_scope", N: "name"}, + expected: instrumentationScope.Name(), + }, + } + + pep := pathExpressionParser{} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + accessor, err := pep.parsePath(tt.path) + require.NoError(t, err) + + got, err := accessor.Get(context.Background(), ctx) + assert.NoError(t, err) + assert.Equal(t, tt.expected, got) + }) + } +} + func createTelemetry(bodyType string) (plog.LogRecord, pcommon.InstrumentationScope, pcommon.Resource) { log := plog.NewLogRecord() log.SetTimestamp(pcommon.NewTimestampFromTime(time.UnixMilli(100))) diff --git a/pkg/ottl/contexts/ottlmetric/metrics.go b/pkg/ottl/contexts/ottlmetric/metrics.go index eba931c74404..b8c26bac2e9d 100644 --- a/pkg/ottl/contexts/ottlmetric/metrics.go +++ b/pkg/ottl/contexts/ottlmetric/metrics.go @@ -15,6 +15,11 @@ import ( "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl/contexts/internal" ) +const ( + // Experimental: *NOTE* this constant is subject to change or removal in the future. + ContextName = internal.MetricContextName +) + var ( _ internal.ResourceContext = TransformContext{} _ internal.InstrumentationScopeContext = TransformContext{} @@ -90,6 +95,21 @@ func NewParser(functions map[string]ottl.Factory[TransformContext], telemetrySet return p, err } +// EnablePathContextNames enables the support to path's context names on statements. +// When this option is configured, all statement's paths must have a valid context prefix, +// otherwise an error is reported. +// +// Experimental: *NOTE* this option is subject to change or removal in the future. +func EnablePathContextNames() Option { + return func(p *ottl.Parser[TransformContext]) { + ottl.WithPathContextNames[TransformContext]([]string{ + ContextName, + internal.InstrumentationScopeContextName, + internal.ResourceContextName, + })(p) + } +} + type StatementSequenceOption func(*ottl.StatementSequence[TransformContext]) func WithStatementSequenceErrorMode(errorMode ottl.ErrorMode) StatementSequenceOption { @@ -142,21 +162,41 @@ func (pep *pathExpressionParser) parsePath(path ottl.Path[TransformContext]) (ot if path == nil { return nil, fmt.Errorf("path cannot be nil") } + // Higher contexts parsing + if path.Context() != "" && path.Context() != ContextName { + return pep.parseHigherContextPath(path.Context(), path) + } + // Backward compatibility with paths without context + if path.Context() == "" && (path.Name() == internal.ResourceContextName || path.Name() == internal.InstrumentationScopeContextName) { + return pep.parseHigherContextPath(path.Name(), path.Next()) + } + switch path.Name() { case "cache": if path.Keys() == nil { return accessCache(), nil } return accessCacheKey(path.Keys()), nil - case "resource": - return internal.ResourcePathGetSetter[TransformContext](path.Next()) - case "instrumentation_scope": - return internal.ScopePathGetSetter[TransformContext](path.Next()) default: return internal.MetricPathGetSetter[TransformContext](path) } } +func (pep *pathExpressionParser) parseHigherContextPath(context string, path ottl.Path[TransformContext]) (ottl.GetSetter[TransformContext], error) { + switch context { + case internal.ResourceContextName: + return internal.ResourcePathGetSetter(path) + case internal.InstrumentationScopeContextName: + return internal.ScopePathGetSetter(path) + default: + var fullPath string + if path != nil { + fullPath = path.String() + } + return nil, internal.FormatDefaultErrorMessage(context, fullPath, internal.MetricContextName, internal.MetricRef) + } +} + func accessCache() ottl.StandardGetSetter[TransformContext] { return ottl.StandardGetSetter[TransformContext]{ Getter: func(_ context.Context, tCtx TransformContext) (any, error) { diff --git a/pkg/ottl/contexts/ottlmetric/metrics_test.go b/pkg/ottl/contexts/ottlmetric/metrics_test.go index f83ead9e3a4b..c434ab5735e9 100644 --- a/pkg/ottl/contexts/ottlmetric/metrics_test.go +++ b/pkg/ottl/contexts/ottlmetric/metrics_test.go @@ -5,9 +5,11 @@ package ottlmetric import ( "context" + "slices" "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "go.opentelemetry.io/collector/pdata/pcommon" "go.opentelemetry.io/collector/pdata/pmetric" @@ -140,6 +142,16 @@ func Test_newPathGetSetter(t *testing.T) { }, }, } + // Copy all tests cases and sets the path.Context value to the generated ones. + // It ensures all exiting field access also work when the path context is set. + for _, tt := range slices.Clone(tests) { + testWithContext := tt + testWithContext.name = "with_path_context:" + tt.name + pathWithContext := *tt.path.(*internal.TestPath[TransformContext]) + pathWithContext.C = ContextName + testWithContext.path = ottl.Path[TransformContext](&pathWithContext) + tests = append(tests, testWithContext) + } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { pep := pathExpressionParser{} @@ -167,6 +179,66 @@ func Test_newPathGetSetter(t *testing.T) { } } +func Test_newPathGetSetter_higherContextPath(t *testing.T) { + resource := pcommon.NewResource() + resource.Attributes().PutStr("foo", "bar") + + instrumentationScope := pcommon.NewInstrumentationScope() + instrumentationScope.SetName("instrumentation_scope") + + ctx := NewTransformContext(pmetric.NewMetric(), pmetric.NewMetricSlice(), instrumentationScope, resource, pmetric.NewScopeMetrics(), pmetric.NewResourceMetrics()) + + tests := []struct { + name string + path ottl.Path[TransformContext] + expected any + }{ + { + name: "resource", + path: &internal.TestPath[TransformContext]{C: "", N: "resource", NextPath: &internal.TestPath[TransformContext]{ + N: "attributes", + KeySlice: []ottl.Key[TransformContext]{ + &internal.TestKey[TransformContext]{ + S: ottltest.Strp("foo"), + }, + }, + }}, + expected: "bar", + }, + { + name: "resource with context", + path: &internal.TestPath[TransformContext]{C: "resource", N: "attributes", KeySlice: []ottl.Key[TransformContext]{ + &internal.TestKey[TransformContext]{ + S: ottltest.Strp("foo"), + }, + }}, + expected: "bar", + }, + { + name: "instrumentation_scope", + path: &internal.TestPath[TransformContext]{N: "instrumentation_scope", NextPath: &internal.TestPath[TransformContext]{N: "name"}}, + expected: instrumentationScope.Name(), + }, + { + name: "instrumentation_scope with context", + path: &internal.TestPath[TransformContext]{C: "instrumentation_scope", N: "name"}, + expected: instrumentationScope.Name(), + }, + } + + pep := pathExpressionParser{} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + accessor, err := pep.parsePath(tt.path) + require.NoError(t, err) + + got, err := accessor.Get(context.Background(), ctx) + assert.NoError(t, err) + assert.Equal(t, tt.expected, got) + }) + } +} + func createMetricTelemetry() pmetric.Metric { metric := pmetric.NewMetric() metric.SetName("name") diff --git a/pkg/ottl/contexts/ottlresource/resource.go b/pkg/ottl/contexts/ottlresource/resource.go index da3a8ceea1b6..fef87f37b54e 100644 --- a/pkg/ottl/contexts/ottlresource/resource.go +++ b/pkg/ottl/contexts/ottlresource/resource.go @@ -17,6 +17,11 @@ import ( "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl/contexts/internal/logging" ) +const ( + // Experimental: *NOTE* this constant is subject to change or removal in the future. + ContextName = internal.ResourceContextName +) + var ( _ internal.ResourceContext = (*TransformContext)(nil) _ zapcore.ObjectMarshaler = (*TransformContext)(nil) @@ -73,6 +78,17 @@ func NewParser(functions map[string]ottl.Factory[TransformContext], telemetrySet return p, nil } +// EnablePathContextNames enables the support to path's context names on statements. +// When this option is configured, all statement's paths must have a valid context prefix, +// otherwise an error is reported. +// +// Experimental: *NOTE* this option is subject to change or removal in the future. +func EnablePathContextNames() Option { + return func(p *ottl.Parser[TransformContext]) { + ottl.WithPathContextNames[TransformContext]([]string{ContextName})(p) + } +} + type StatementSequenceOption func(*ottl.StatementSequence[TransformContext]) func WithStatementSequenceErrorMode(errorMode ottl.ErrorMode) StatementSequenceOption { @@ -117,6 +133,10 @@ func (pep *pathExpressionParser) parsePath(path ottl.Path[TransformContext]) (ot if path == nil { return nil, fmt.Errorf("path cannot be nil") } + if path.Context() != "" && path.Context() != ContextName { + return nil, internal.FormatDefaultErrorMessage(path.Context(), path.String(), "Resource", internal.ResourceContextRef) + } + switch path.Name() { case "cache": if path.Keys() == nil { diff --git a/pkg/ottl/contexts/ottlresource/resource_test.go b/pkg/ottl/contexts/ottlresource/resource_test.go index 9d490a362c3d..f37bcdf90c80 100644 --- a/pkg/ottl/contexts/ottlresource/resource_test.go +++ b/pkg/ottl/contexts/ottlresource/resource_test.go @@ -5,6 +5,7 @@ package ottlresource import ( "context" + "slices" "testing" "github.com/stretchr/testify/assert" @@ -361,6 +362,16 @@ func Test_newPathGetSetter(t *testing.T) { }, }, } + // Copy all tests cases and sets the path.Context value to the generated ones. + // It ensures all exiting field access also work when the path context is set. + for _, tt := range slices.Clone(tests) { + testWithContext := tt + testWithContext.name = "with_path_context:" + tt.name + pathWithContext := *tt.path.(*internal.TestPath[TransformContext]) + pathWithContext.C = ContextName + testWithContext.path = ottl.Path[TransformContext](&pathWithContext) + tests = append(tests, testWithContext) + } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { pep := pathExpressionParser{} diff --git a/pkg/ottl/contexts/ottlscope/scope.go b/pkg/ottl/contexts/ottlscope/scope.go index 3ae5e0976446..13f2c7a83e83 100644 --- a/pkg/ottl/contexts/ottlscope/scope.go +++ b/pkg/ottl/contexts/ottlscope/scope.go @@ -17,6 +17,11 @@ import ( "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl/contexts/internal/logging" ) +const ( + // Experimental: *NOTE* this constant is subject to change or removal in the future. + ContextName = internal.ScopeContextName +) + var ( _ internal.ResourceContext = (*TransformContext)(nil) _ internal.InstrumentationScopeContext = (*TransformContext)(nil) @@ -85,6 +90,20 @@ func NewParser(functions map[string]ottl.Factory[TransformContext], telemetrySet return p, nil } +// EnablePathContextNames enables the support to path's context names on statements. +// When this option is configured, all statement's paths must have a valid context prefix, +// otherwise an error is reported. +// +// Experimental: *NOTE* this option is subject to change or removal in the future. +func EnablePathContextNames() Option { + return func(p *ottl.Parser[TransformContext]) { + ottl.WithPathContextNames[TransformContext]([]string{ + ContextName, + internal.ResourceContextName, + })(p) + } +} + type StatementSequenceOption func(*ottl.StatementSequence[TransformContext]) func WithStatementSequenceErrorMode(errorMode ottl.ErrorMode) StatementSequenceOption { @@ -129,19 +148,39 @@ func (pep *pathExpressionParser) parsePath(path ottl.Path[TransformContext]) (ot if path == nil { return nil, fmt.Errorf("path cannot be nil") } + // Higher contexts parsing + if path.Context() != "" && path.Context() != ContextName { + return pep.parseHigherContextPath(path.Context(), path) + } + // Backward compatibility with paths without context + if path.Context() == "" && path.Name() == internal.ResourceContextName { + return pep.parseHigherContextPath(path.Name(), path.Next()) + } + switch path.Name() { case "cache": if path.Keys() == nil { return accessCache(), nil } return accessCacheKey(path.Keys()), nil - case "resource": - return internal.ResourcePathGetSetter[TransformContext](path.Next()) default: return internal.ScopePathGetSetter[TransformContext](path) } } +func (pep *pathExpressionParser) parseHigherContextPath(context string, path ottl.Path[TransformContext]) (ottl.GetSetter[TransformContext], error) { + switch context { + case internal.ResourceContextName: + return internal.ResourcePathGetSetter[TransformContext](path) + default: + var fullPath string + if path != nil { + fullPath = path.String() + } + return nil, internal.FormatDefaultErrorMessage(context, fullPath, "Instrumentation Scope", internal.InstrumentationScopeRef) + } +} + func accessCache() ottl.StandardGetSetter[TransformContext] { return ottl.StandardGetSetter[TransformContext]{ Getter: func(_ context.Context, tCtx TransformContext) (any, error) { diff --git a/pkg/ottl/contexts/ottlscope/scope_test.go b/pkg/ottl/contexts/ottlscope/scope_test.go index 4b392a43ece1..4f4d8cf7c3b5 100644 --- a/pkg/ottl/contexts/ottlscope/scope_test.go +++ b/pkg/ottl/contexts/ottlscope/scope_test.go @@ -5,9 +5,11 @@ package ottlscope import ( "context" + "slices" "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "go.opentelemetry.io/collector/pdata/pcommon" "go.opentelemetry.io/collector/pdata/plog" @@ -383,6 +385,16 @@ func Test_newPathGetSetter(t *testing.T) { }, }, } + // Copy all tests cases and sets the path.Context value to the generated ones. + // It ensures all exiting field access also work when the path context is set. + for _, tt := range slices.Clone(tests) { + testWithContext := tt + testWithContext.name = "with_path_context:" + tt.name + pathWithContext := *tt.path.(*internal.TestPath[TransformContext]) + pathWithContext.C = ContextName + testWithContext.path = ottl.Path[TransformContext](&pathWithContext) + tests = append(tests, testWithContext) + } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { pep := pathExpressionParser{} @@ -410,6 +422,52 @@ func Test_newPathGetSetter(t *testing.T) { } } +func Test_newPathGetSetter_higherContextPath(t *testing.T) { + resource := pcommon.NewResource() + resource.Attributes().PutStr("foo", "bar") + ctx := NewTransformContext(pcommon.NewInstrumentationScope(), resource, plog.NewScopeLogs()) + + tests := []struct { + name string + path ottl.Path[TransformContext] + expected any + }{ + { + name: "resource", + path: &internal.TestPath[TransformContext]{C: "", N: "resource", NextPath: &internal.TestPath[TransformContext]{ + N: "attributes", + KeySlice: []ottl.Key[TransformContext]{ + &internal.TestKey[TransformContext]{ + S: ottltest.Strp("foo"), + }, + }, + }}, + expected: "bar", + }, + { + name: "resource with context", + path: &internal.TestPath[TransformContext]{C: "resource", N: "attributes", KeySlice: []ottl.Key[TransformContext]{ + &internal.TestKey[TransformContext]{ + S: ottltest.Strp("foo"), + }, + }}, + expected: "bar", + }, + } + + pep := pathExpressionParser{} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + accessor, err := pep.parsePath(tt.path) + require.NoError(t, err) + + got, err := accessor.Get(context.Background(), ctx) + assert.NoError(t, err) + assert.Equal(t, tt.expected, got) + }) + } +} + func createTelemetry() (pcommon.InstrumentationScope, pcommon.Resource) { is := pcommon.NewInstrumentationScope() is.SetName("library") diff --git a/pkg/ottl/contexts/ottlspan/span.go b/pkg/ottl/contexts/ottlspan/span.go index aa3283124bbd..be11495e8dde 100644 --- a/pkg/ottl/contexts/ottlspan/span.go +++ b/pkg/ottl/contexts/ottlspan/span.go @@ -18,6 +18,11 @@ import ( "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl/contexts/internal/logging" ) +const ( + // Experimental: *NOTE* this constant is subject to change or removal in the future. + ContextName = internal.SpanContextName +) + var ( _ internal.ResourceContext = (*TransformContext)(nil) _ internal.InstrumentationScopeContext = (*TransformContext)(nil) @@ -95,6 +100,21 @@ func NewParser(functions map[string]ottl.Factory[TransformContext], telemetrySet return p, nil } +// EnablePathContextNames enables the support to path's context names on statements. +// When this option is configured, all statement's paths must have a valid context prefix, +// otherwise an error is reported. +// +// Experimental: *NOTE* this option is subject to change or removal in the future. +func EnablePathContextNames() Option { + return func(p *ottl.Parser[TransformContext]) { + ottl.WithPathContextNames[TransformContext]([]string{ + ContextName, + internal.ResourceContextName, + internal.InstrumentationScopeContextName, + })(p) + } +} + type StatementSequenceOption func(*ottl.StatementSequence[TransformContext]) func WithStatementSequenceErrorMode(errorMode ottl.ErrorMode) StatementSequenceOption { @@ -145,21 +165,41 @@ func (pep *pathExpressionParser) parsePath(path ottl.Path[TransformContext]) (ot if path == nil { return nil, fmt.Errorf("path cannot be nil") } + // Higher contexts parsing + if path.Context() != "" && path.Context() != ContextName { + return pep.parseHigherContextPath(path.Context(), path) + } + // Backward compatibility with paths without context + if path.Context() == "" && (path.Name() == internal.ResourceContextName || path.Name() == internal.InstrumentationScopeContextName) { + return pep.parseHigherContextPath(path.Name(), path.Next()) + } + switch path.Name() { case "cache": if path.Keys() == nil { return accessCache(), nil } return accessCacheKey(path.Keys()), nil - case "resource": - return internal.ResourcePathGetSetter[TransformContext](path.Next()) - case "instrumentation_scope": - return internal.ScopePathGetSetter[TransformContext](path.Next()) default: return internal.SpanPathGetSetter[TransformContext](path) } } +func (pep *pathExpressionParser) parseHigherContextPath(context string, path ottl.Path[TransformContext]) (ottl.GetSetter[TransformContext], error) { + switch context { + case internal.ResourceContextName: + return internal.ResourcePathGetSetter[TransformContext](path) + case internal.InstrumentationScopeContextName: + return internal.ScopePathGetSetter[TransformContext](path) + default: + var fullPath string + if path != nil { + fullPath = path.String() + } + return nil, internal.FormatDefaultErrorMessage(context, fullPath, internal.SpanContextName, internal.SpanRef) + } +} + func accessCache() ottl.StandardGetSetter[TransformContext] { return ottl.StandardGetSetter[TransformContext]{ Getter: func(_ context.Context, tCtx TransformContext) (any, error) { diff --git a/pkg/ottl/contexts/ottlspan/span_test.go b/pkg/ottl/contexts/ottlspan/span_test.go index dd4e7de5d69d..24609de32b06 100644 --- a/pkg/ottl/contexts/ottlspan/span_test.go +++ b/pkg/ottl/contexts/ottlspan/span_test.go @@ -6,10 +6,12 @@ package ottlspan import ( "context" "encoding/hex" + "slices" "testing" "time" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "go.opentelemetry.io/collector/pdata/pcommon" "go.opentelemetry.io/collector/pdata/ptrace" @@ -650,6 +652,16 @@ func Test_newPathGetSetter(t *testing.T) { }, }, } + // Copy all tests cases and sets the path.Context value to the generated ones. + // It ensures all exiting field access also work when the path context is set. + for _, tt := range slices.Clone(tests) { + testWithContext := tt + testWithContext.name = "with_path_context:" + tt.name + pathWithContext := *tt.path.(*internal.TestPath[TransformContext]) + pathWithContext.C = ContextName + testWithContext.path = ottl.Path[TransformContext](&pathWithContext) + tests = append(tests, testWithContext) + } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { pep := pathExpressionParser{} @@ -679,6 +691,66 @@ func Test_newPathGetSetter(t *testing.T) { } } +func Test_newPathGetSetter_higherContextPath(t *testing.T) { + resource := pcommon.NewResource() + resource.Attributes().PutStr("foo", "bar") + + instrumentationScope := pcommon.NewInstrumentationScope() + instrumentationScope.SetName("instrumentation_scope") + + ctx := NewTransformContext(ptrace.NewSpan(), instrumentationScope, resource, ptrace.NewScopeSpans(), ptrace.NewResourceSpans()) + + tests := []struct { + name string + path ottl.Path[TransformContext] + expected any + }{ + { + name: "resource", + path: &internal.TestPath[TransformContext]{C: "", N: "resource", NextPath: &internal.TestPath[TransformContext]{ + N: "attributes", + KeySlice: []ottl.Key[TransformContext]{ + &internal.TestKey[TransformContext]{ + S: ottltest.Strp("foo"), + }, + }, + }}, + expected: "bar", + }, + { + name: "resource with context", + path: &internal.TestPath[TransformContext]{C: "resource", N: "attributes", KeySlice: []ottl.Key[TransformContext]{ + &internal.TestKey[TransformContext]{ + S: ottltest.Strp("foo"), + }, + }}, + expected: "bar", + }, + { + name: "instrumentation_scope", + path: &internal.TestPath[TransformContext]{N: "instrumentation_scope", NextPath: &internal.TestPath[TransformContext]{N: "name"}}, + expected: instrumentationScope.Name(), + }, + { + name: "instrumentation_scope with context", + path: &internal.TestPath[TransformContext]{C: "instrumentation_scope", N: "name"}, + expected: instrumentationScope.Name(), + }, + } + + pep := pathExpressionParser{} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + accessor, err := pep.parsePath(tt.path) + require.NoError(t, err) + + got, err := accessor.Get(context.Background(), ctx) + assert.NoError(t, err) + assert.Equal(t, tt.expected, got) + }) + } +} + func createTelemetry() (ptrace.Span, pcommon.InstrumentationScope, pcommon.Resource) { span := ptrace.NewSpan() span.SetTraceID(traceID) diff --git a/pkg/ottl/contexts/ottlspanevent/span_events.go b/pkg/ottl/contexts/ottlspanevent/span_events.go index b3826f690d2d..6d654b058004 100644 --- a/pkg/ottl/contexts/ottlspanevent/span_events.go +++ b/pkg/ottl/contexts/ottlspanevent/span_events.go @@ -19,6 +19,12 @@ import ( "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl/contexts/internal/logging" ) +const ( + // Experimental: *NOTE* this constant is subject to change or removal in the future. + ContextName = "spanevent" + contextNameDescription = "Span Event" +) + var ( _ internal.ResourceContext = (*TransformContext)(nil) _ internal.InstrumentationScopeContext = (*TransformContext)(nil) @@ -103,6 +109,22 @@ func NewParser(functions map[string]ottl.Factory[TransformContext], telemetrySet return p, nil } +// EnablePathContextNames enables the support to path's context names on statements. +// When this option is configured, all statement's paths must have a valid context prefix, +// otherwise an error is reported. +// +// Experimental: *NOTE* this option is subject to change or removal in the future. +func EnablePathContextNames() Option { + return func(p *ottl.Parser[TransformContext]) { + ottl.WithPathContextNames[TransformContext]([]string{ + ContextName, + internal.SpanContextName, + internal.ResourceContextName, + internal.InstrumentationScopeContextName, + })(p) + } +} + type StatementSequenceOption func(*ottl.StatementSequence[TransformContext]) func WithStatementSequenceErrorMode(errorMode ottl.ErrorMode) StatementSequenceOption { @@ -153,18 +175,24 @@ func (pep *pathExpressionParser) parsePath(path ottl.Path[TransformContext]) (ot if path == nil { return nil, fmt.Errorf("path cannot be nil") } + // Higher contexts parsing + if path.Context() != "" && path.Context() != ContextName { + return pep.parseHigherContextPath(path.Context(), path) + } + // Backward compatibility with paths without context + if path.Context() == "" && + (path.Name() == internal.ResourceContextName || + path.Name() == internal.InstrumentationScopeContextName || + path.Name() == internal.SpanContextName) { + return pep.parseHigherContextPath(path.Name(), path.Next()) + } + switch path.Name() { case "cache": if path.Keys() == nil { return accessCache(), nil } return accessCacheKey(path.Keys()), nil - case "resource": - return internal.ResourcePathGetSetter[TransformContext](path.Next()) - case "instrumentation_scope": - return internal.ScopePathGetSetter[TransformContext](path.Next()) - case "span": - return internal.SpanPathGetSetter[TransformContext](path.Next()) case "time_unix_nano": return accessSpanEventTimeUnixNano(), nil case "time": @@ -179,7 +207,24 @@ func (pep *pathExpressionParser) parsePath(path ottl.Path[TransformContext]) (ot case "dropped_attributes_count": return accessSpanEventDroppedAttributeCount(), nil default: - return nil, internal.FormatDefaultErrorMessage(path.Name(), path.String(), "Span Event", internal.SpanEventRef) + return nil, internal.FormatDefaultErrorMessage(path.Name(), path.String(), contextNameDescription, internal.SpanEventRef) + } +} + +func (pep *pathExpressionParser) parseHigherContextPath(context string, path ottl.Path[TransformContext]) (ottl.GetSetter[TransformContext], error) { + switch context { + case internal.ResourceContextName: + return internal.ResourcePathGetSetter(path) + case internal.InstrumentationScopeContextName: + return internal.ScopePathGetSetter(path) + case internal.SpanContextName: + return internal.SpanPathGetSetter(path) + default: + var fullPath string + if path != nil { + fullPath = path.String() + } + return nil, internal.FormatDefaultErrorMessage(context, fullPath, contextNameDescription, internal.SpanEventRef) } } diff --git a/pkg/ottl/contexts/ottlspanevent/span_events_test.go b/pkg/ottl/contexts/ottlspanevent/span_events_test.go index daf5ce3229ca..29abc6abce1d 100644 --- a/pkg/ottl/contexts/ottlspanevent/span_events_test.go +++ b/pkg/ottl/contexts/ottlspanevent/span_events_test.go @@ -5,10 +5,12 @@ package ottlspanevent import ( "context" + "slices" "testing" "time" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "go.opentelemetry.io/collector/pdata/pcommon" "go.opentelemetry.io/collector/pdata/ptrace" @@ -406,6 +408,16 @@ func Test_newPathGetSetter(t *testing.T) { }, }, } + // Copy all tests cases and sets the path.Context value to the generated ones. + // It ensures all exiting field access also work when the path context is set. + for _, tt := range slices.Clone(tests) { + testWithContext := tt + testWithContext.name = "with_path_context:" + tt.name + pathWithContext := *tt.path.(*internal.TestPath[TransformContext]) + pathWithContext.C = ContextName + testWithContext.path = ottl.Path[TransformContext](&pathWithContext) + tests = append(tests, testWithContext) + } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { pep := pathExpressionParser{} @@ -435,6 +447,79 @@ func Test_newPathGetSetter(t *testing.T) { } } +func Test_newPathGetSetter_higherContextPath(t *testing.T) { + resource := pcommon.NewResource() + resource.Attributes().PutStr("foo", "bar") + + span := ptrace.NewSpan() + span.SetName("span") + + instrumentationScope := pcommon.NewInstrumentationScope() + instrumentationScope.SetName("instrumentation_scope") + + ctx := NewTransformContext(ptrace.NewSpanEvent(), span, instrumentationScope, resource, ptrace.NewScopeSpans(), ptrace.NewResourceSpans()) + + tests := []struct { + name string + path ottl.Path[TransformContext] + expected any + }{ + { + name: "resource", + path: &internal.TestPath[TransformContext]{C: "", N: "resource", NextPath: &internal.TestPath[TransformContext]{ + N: "attributes", + KeySlice: []ottl.Key[TransformContext]{ + &internal.TestKey[TransformContext]{ + S: ottltest.Strp("foo"), + }, + }, + }}, + expected: "bar", + }, + { + name: "resource with context", + path: &internal.TestPath[TransformContext]{C: "resource", N: "attributes", KeySlice: []ottl.Key[TransformContext]{ + &internal.TestKey[TransformContext]{ + S: ottltest.Strp("foo"), + }, + }}, + expected: "bar", + }, + { + name: "instrumentation_scope", + path: &internal.TestPath[TransformContext]{N: "instrumentation_scope", NextPath: &internal.TestPath[TransformContext]{N: "name"}}, + expected: instrumentationScope.Name(), + }, + { + name: "instrumentation_scope with context", + path: &internal.TestPath[TransformContext]{C: "instrumentation_scope", N: "name"}, + expected: instrumentationScope.Name(), + }, + { + name: "span", + path: &internal.TestPath[TransformContext]{N: "span", NextPath: &internal.TestPath[TransformContext]{N: "name"}}, + expected: span.Name(), + }, + { + name: "span with context", + path: &internal.TestPath[TransformContext]{C: "span", N: "name"}, + expected: span.Name(), + }, + } + + pep := pathExpressionParser{} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + accessor, err := pep.parsePath(tt.path) + require.NoError(t, err) + + got, err := accessor.Get(context.Background(), ctx) + assert.NoError(t, err) + assert.Equal(t, tt.expected, got) + }) + } +} + func createTelemetry() (ptrace.SpanEvent, ptrace.Span, pcommon.InstrumentationScope, pcommon.Resource) { spanEvent := ptrace.NewSpanEvent()