Skip to content

Commit bdb46e1

Browse files
authored
[processor/tailsampling] Do not panic when creating a string_attribute with invalid filter (open-telemetry#43735)
#### Description Avoids a panic, returning an error instead which can be handled gracefully by the application. <!--Describe what testing was performed and which tests were added.--> #### Testing Test case was added
1 parent 1cc6c48 commit bdb46e1

File tree

7 files changed

+87
-23
lines changed

7 files changed

+87
-23
lines changed
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Use this changelog template to create an entry for release notes.
2+
3+
# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix'
4+
change_type: bug_fix
5+
6+
# The name of the component, or a single word describing the area of concern, (e.g. receiver/filelog)
7+
component: processor/tail_sampling
8+
9+
# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`).
10+
note: Fix panic when invalid regex was sent to string_attribute sampler
11+
12+
# Mandatory: One or more tracking issues related to the change. You can use the PR number here if no issue exists.
13+
issues: [43735]
14+
15+
# (Optional) One or more lines of additional information to render under the primary note.
16+
# These lines will be padded with 2 spaces and then inserted directly into the document.
17+
# Use pipe (|) for multiline entries.
18+
subtext:
19+
20+
# If your change doesn't affect end users or the exported elements of any package,
21+
# you should instead start your pull request title with [chore] or use the "Skip Changelog" label.
22+
# Optional: The change log or logs in which this entry should be included.
23+
# e.g. '[user]' or '[user, api]'
24+
# Include 'user' if the change is relevant to end users.
25+
# Include 'api' if there is a change to a library API.
26+
# Default: '[user]'
27+
change_logs: [user]

processor/tailsamplingprocessor/internal/sampling/and_test.go

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ import (
1616
)
1717

1818
func TestAndEvaluatorNotSampled(t *testing.T) {
19-
n1 := NewStringAttributeFilter(componenttest.NewNopTelemetrySettings(), "name", []string{"value"}, false, 0, false)
19+
n1, err := NewStringAttributeFilter(componenttest.NewNopTelemetrySettings(), "name", []string{"value"}, false, 0, false)
20+
require.NoError(t, err)
2021
n2, err := NewStatusCodeFilter(componenttest.NewNopTelemetrySettings(), []string{"ERROR"})
2122
require.NoError(t, err)
2223

@@ -40,7 +41,8 @@ func TestAndEvaluatorNotSampled(t *testing.T) {
4041
}
4142

4243
func TestAndEvaluatorSampled(t *testing.T) {
43-
n1 := NewStringAttributeFilter(componenttest.NewNopTelemetrySettings(), "attribute_name", []string{"attribute_value"}, false, 0, false)
44+
n1, err := NewStringAttributeFilter(componenttest.NewNopTelemetrySettings(), "attribute_name", []string{"attribute_value"}, false, 0, false)
45+
require.NoError(t, err)
4446
n2, err := NewStatusCodeFilter(componenttest.NewNopTelemetrySettings(), []string{"ERROR"})
4547
require.NoError(t, err)
4648

@@ -65,7 +67,8 @@ func TestAndEvaluatorSampled(t *testing.T) {
6567
}
6668

6769
func TestAndEvaluatorStringInvertSampled(t *testing.T) {
68-
n1 := NewStringAttributeFilter(componenttest.NewNopTelemetrySettings(), "attribute_name", []string{"no_match"}, false, 0, true)
70+
n1, err := NewStringAttributeFilter(componenttest.NewNopTelemetrySettings(), "attribute_name", []string{"no_match"}, false, 0, true)
71+
require.NoError(t, err)
6972
n2, err := NewStatusCodeFilter(componenttest.NewNopTelemetrySettings(), []string{"ERROR"})
7073
require.NoError(t, err)
7174

@@ -90,7 +93,8 @@ func TestAndEvaluatorStringInvertSampled(t *testing.T) {
9093
}
9194

9295
func TestAndEvaluatorStringInvertNotSampled(t *testing.T) {
93-
n1 := NewStringAttributeFilter(componenttest.NewNopTelemetrySettings(), "attribute_name", []string{"attribute_value"}, false, 0, true)
96+
n1, err := NewStringAttributeFilter(componenttest.NewNopTelemetrySettings(), "attribute_name", []string{"attribute_value"}, false, 0, true)
97+
require.NoError(t, err)
9498
n2, err := NewStatusCodeFilter(componenttest.NewNopTelemetrySettings(), []string{"ERROR"})
9599
require.NoError(t, err)
96100

processor/tailsamplingprocessor/internal/sampling/composite_test.go

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -174,8 +174,10 @@ func TestCompositeEvaluatorSampled_AlwaysSampled(t *testing.T) {
174174

175175
func TestCompositeEvaluatorInverseSampled_AlwaysSampled(t *testing.T) {
176176
// The first policy does not match, the second matches through invert
177-
n1 := NewStringAttributeFilter(componenttest.NewNopTelemetrySettings(), "tag", []string{"foo"}, false, 0, false)
178-
n2 := NewStringAttributeFilter(componenttest.NewNopTelemetrySettings(), "tag", []string{"foo"}, false, 0, true)
177+
n1, err := NewStringAttributeFilter(componenttest.NewNopTelemetrySettings(), "tag", []string{"foo"}, false, 0, false)
178+
require.NoError(t, err)
179+
n2, err := NewStringAttributeFilter(componenttest.NewNopTelemetrySettings(), "tag", []string{"foo"}, false, 0, true)
180+
require.NoError(t, err)
179181
c := NewComposite(zap.NewNop(), 10, []SubPolicyEvalParams{{n1, 20, "eval-1"}, {n2, 20, "eval-2"}}, FakeTimeProvider{}, false)
180182

181183
for i := 1; i <= 10; i++ {
@@ -192,8 +194,10 @@ func TestCompositeEvaluatorInverseSampled_AlwaysSampled(t *testing.T) {
192194

193195
func TestCompositeEvaluatorInverseSampled_AlwaysSampled_RecordSubPolicy(t *testing.T) {
194196
// The first policy does not match, the second matches through invert
195-
n1 := NewStringAttributeFilter(componenttest.NewNopTelemetrySettings(), "tag", []string{"foo"}, false, 0, false)
196-
n2 := NewStringAttributeFilter(componenttest.NewNopTelemetrySettings(), "tag", []string{"foo"}, false, 0, true)
197+
n1, err := NewStringAttributeFilter(componenttest.NewNopTelemetrySettings(), "tag", []string{"foo"}, false, 0, false)
198+
require.NoError(t, err)
199+
n2, err := NewStringAttributeFilter(componenttest.NewNopTelemetrySettings(), "tag", []string{"foo"}, false, 0, true)
200+
require.NoError(t, err)
197201
c := NewComposite(zap.NewNop(), 10, []SubPolicyEvalParams{{n1, 20, "eval-1"}, {n2, 20, "eval-2"}}, FakeTimeProvider{}, true)
198202

199203
for i := 1; i <= 10; i++ {

processor/tailsamplingprocessor/internal/sampling/drop_test.go

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ import (
1616
)
1717

1818
func TestDropEvaluatorNotSampled(t *testing.T) {
19-
n1 := NewStringAttributeFilter(componenttest.NewNopTelemetrySettings(), "name", []string{"value"}, false, 0, false)
19+
n1, err := NewStringAttributeFilter(componenttest.NewNopTelemetrySettings(), "name", []string{"value"}, false, 0, false)
20+
require.NoError(t, err)
2021
n2, err := NewStatusCodeFilter(componenttest.NewNopTelemetrySettings(), []string{"ERROR"})
2122
require.NoError(t, err)
2223

@@ -40,7 +41,8 @@ func TestDropEvaluatorNotSampled(t *testing.T) {
4041
}
4142

4243
func TestDropEvaluatorSampled(t *testing.T) {
43-
n1 := NewStringAttributeFilter(componenttest.NewNopTelemetrySettings(), "attribute_name", []string{"attribute_value"}, false, 0, false)
44+
n1, err := NewStringAttributeFilter(componenttest.NewNopTelemetrySettings(), "attribute_name", []string{"attribute_value"}, false, 0, false)
45+
require.NoError(t, err)
4446
n2, err := NewStatusCodeFilter(componenttest.NewNopTelemetrySettings(), []string{"ERROR"})
4547
require.NoError(t, err)
4648

@@ -65,7 +67,8 @@ func TestDropEvaluatorSampled(t *testing.T) {
6567
}
6668

6769
func TestDropEvaluatorStringInvertMatch(t *testing.T) {
68-
n1 := NewStringAttributeFilter(componenttest.NewNopTelemetrySettings(), "attribute_name", []string{"no_match"}, false, 0, true)
70+
n1, err := NewStringAttributeFilter(componenttest.NewNopTelemetrySettings(), "attribute_name", []string{"no_match"}, false, 0, true)
71+
require.NoError(t, err)
6972
n2, err := NewStatusCodeFilter(componenttest.NewNopTelemetrySettings(), []string{"ERROR"})
7073
require.NoError(t, err)
7174

@@ -90,7 +93,8 @@ func TestDropEvaluatorStringInvertMatch(t *testing.T) {
9093
}
9194

9295
func TestDropEvaluatorStringInvertNotMatch(t *testing.T) {
93-
n1 := NewStringAttributeFilter(componenttest.NewNopTelemetrySettings(), "attribute_name", []string{"attribute_value"}, false, 0, true)
96+
n1, err := NewStringAttributeFilter(componenttest.NewNopTelemetrySettings(), "attribute_name", []string{"attribute_value"}, false, 0, true)
97+
require.NoError(t, err)
9498
n2, err := NewStatusCodeFilter(componenttest.NewNopTelemetrySettings(), []string{"ERROR"})
9599
require.NoError(t, err)
96100

processor/tailsamplingprocessor/internal/sampling/string_tag_filter.go

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ package sampling // import "github.com/open-telemetry/opentelemetry-collector-co
55

66
import (
77
"context"
8+
"fmt"
89
"regexp"
910

1011
"github.com/golang/groupcache/lru"
@@ -36,13 +37,16 @@ var _ samplingpolicy.Evaluator = (*stringAttributeFilter)(nil)
3637

3738
// NewStringAttributeFilter creates a policy evaluator that samples all traces with
3839
// the given attribute in the given numeric range.
39-
func NewStringAttributeFilter(settings component.TelemetrySettings, key string, values []string, regexMatchEnabled bool, evictSize int, invertMatch bool) samplingpolicy.Evaluator {
40+
func NewStringAttributeFilter(settings component.TelemetrySettings, key string, values []string, regexMatchEnabled bool, evictSize int, invertMatch bool) (samplingpolicy.Evaluator, error) {
4041
// initialize regex filter rules and LRU cache for matched results
4142
if regexMatchEnabled {
4243
if evictSize <= 0 {
4344
evictSize = defaultCacheSize
4445
}
45-
filterList := addFilters(values)
46+
filterList, err := addFilters(values)
47+
if err != nil {
48+
return nil, err
49+
}
4650
regexStrSetting := &regexStrSetting{
4751
matchedAttrs: lru.New(evictSize),
4852
filterList: filterList,
@@ -68,7 +72,7 @@ func NewStringAttributeFilter(settings component.TelemetrySettings, key string,
6872
return false
6973
},
7074
invertMatch: invertMatch,
71-
}
75+
}, nil
7276
}
7377

7478
// initialize the exact value map
@@ -87,7 +91,7 @@ func NewStringAttributeFilter(settings component.TelemetrySettings, key string,
8791
return matched
8892
},
8993
invertMatch: invertMatch,
90-
}
94+
}, nil
9195
}
9296

9397
// Evaluate looks at the trace data and returns a corresponding SamplingDecision.
@@ -151,11 +155,14 @@ func (saf *stringAttributeFilter) Evaluate(_ context.Context, _ pcommon.TraceID,
151155

152156
// addFilters compiles all the given filters and stores them as regexes.
153157
// All regexes are automatically anchored to enforce full string matches.
154-
func addFilters(exprs []string) []*regexp.Regexp {
158+
func addFilters(exprs []string) ([]*regexp.Regexp, error) {
155159
list := make([]*regexp.Regexp, 0, len(exprs))
156160
for _, entry := range exprs {
157-
rule := regexp.MustCompile(entry)
161+
rule, err := regexp.Compile(entry)
162+
if err != nil {
163+
return nil, fmt.Errorf("invalid regex `%s`: %w", entry, err)
164+
}
158165
list = append(list, rule)
159166
}
160-
return list
167+
return list, nil
161168
}

processor/tailsamplingprocessor/internal/sampling/string_tag_filter_test.go

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"testing"
88

99
"github.com/stretchr/testify/assert"
10+
"github.com/stretchr/testify/require"
1011
"go.opentelemetry.io/collector/component/componenttest"
1112
"go.opentelemetry.io/collector/featuregate"
1213
"go.opentelemetry.io/collector/pdata/pcommon"
@@ -226,17 +227,33 @@ func TestStringTagFilter(t *testing.T) {
226227
assert.NoError(t, err)
227228
}()
228229
}
229-
filter := NewStringAttributeFilter(componenttest.NewNopTelemetrySettings(), c.filterCfg.Key, c.filterCfg.Values, c.filterCfg.EnabledRegexMatching, c.filterCfg.CacheMaxSize, c.filterCfg.InvertMatch)
230+
filter, err := NewStringAttributeFilter(componenttest.NewNopTelemetrySettings(), c.filterCfg.Key, c.filterCfg.Values, c.filterCfg.EnabledRegexMatching, c.filterCfg.CacheMaxSize, c.filterCfg.InvertMatch)
231+
require.NoError(t, err)
230232
decision, err := filter.Evaluate(t.Context(), pcommon.TraceID([16]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}), c.Trace)
231233
assert.NoError(t, err)
232234
assert.Equal(t, decision, c.Decision)
233235
})
234236
}
235237
}
236238

239+
func TestStringTagFilter_InvalidRegex(t *testing.T) {
240+
t.Run("error when regex is enabled", func(t *testing.T) {
241+
filter, err := NewStringAttributeFilter(componenttest.NewNopTelemetrySettings(), "key", []string{"*invalid-regex"}, true, defaultCacheSize, false)
242+
assert.Error(t, err)
243+
assert.Nil(t, filter)
244+
})
245+
246+
t.Run("succeeds when regex is disabled", func(t *testing.T) {
247+
filter, err := NewStringAttributeFilter(componenttest.NewNopTelemetrySettings(), "key", []string{"*invalid-regex"}, false, defaultCacheSize, false)
248+
assert.NoError(t, err)
249+
assert.NotNil(t, filter)
250+
})
251+
}
252+
237253
func BenchmarkStringTagFilterEvaluatePlainText(b *testing.B) {
238254
trace := newTraceStringAttrs(map[string]any{"example": "value"}, "", "")
239-
filter := NewStringAttributeFilter(componenttest.NewNopTelemetrySettings(), "example", []string{"value"}, false, 0, false)
255+
filter, err := NewStringAttributeFilter(componenttest.NewNopTelemetrySettings(), "example", []string{"value"}, false, 0, false)
256+
require.NoError(b, err)
240257

241258
for b.Loop() {
242259
_, err := filter.Evaluate(b.Context(), pcommon.TraceID([16]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}), trace)
@@ -246,7 +263,8 @@ func BenchmarkStringTagFilterEvaluatePlainText(b *testing.B) {
246263

247264
func BenchmarkStringTagFilterEvaluateRegex(b *testing.B) {
248265
trace := newTraceStringAttrs(map[string]any{"example": "grpc.health.v1.HealthCheck"}, "", "")
249-
filter := NewStringAttributeFilter(componenttest.NewNopTelemetrySettings(), "example", []string{"v[0-9]+.HealthCheck$"}, true, 0, false)
266+
filter, err := NewStringAttributeFilter(componenttest.NewNopTelemetrySettings(), "example", []string{"v[0-9]+.HealthCheck$"}, true, 0, false)
267+
require.NoError(b, err)
250268

251269
for b.Loop() {
252270
_, err := filter.Evaluate(b.Context(), pcommon.TraceID([16]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}), trace)

processor/tailsamplingprocessor/processor.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -230,7 +230,7 @@ func getSharedPolicyEvaluator(settings component.TelemetrySettings, cfg *sharedP
230230
return sampling.NewProbabilisticSampler(settings, pCfg.HashSalt, pCfg.SamplingPercentage), nil
231231
case StringAttribute:
232232
safCfg := cfg.StringAttributeCfg
233-
return sampling.NewStringAttributeFilter(settings, safCfg.Key, safCfg.Values, safCfg.EnabledRegexMatching, safCfg.CacheMaxSize, safCfg.InvertMatch), nil
233+
return sampling.NewStringAttributeFilter(settings, safCfg.Key, safCfg.Values, safCfg.EnabledRegexMatching, safCfg.CacheMaxSize, safCfg.InvertMatch)
234234
case StatusCode:
235235
scfCfg := cfg.StatusCodeCfg
236236
return sampling.NewStatusCodeFilter(settings, scfCfg.StatusCodes)

0 commit comments

Comments
 (0)