Skip to content

Commit 2197af2

Browse files
committed
Add pattern isolation invariant to adaptive sampling spec
This commit specifies when two sequences will not match, adding a fuzz test to ensure the claim. Technically this is implied by inversion with the pre-existing ThresholdComparison however -- belt and suspenders -- in cases where multiple inversions are possible my preference is to specify the contrapositive. No changes here to production code.
1 parent 5040896 commit 2197af2

3 files changed

Lines changed: 56 additions & 1 deletion

File tree

pkg/logs/internal/decoder/preprocessor/adaptive_sampler.allium

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,16 @@ contract PatternMatching {
8282
-- compared for equality. The sequences match iff
8383
-- positional_matches >= required_matches.
8484

85+
@invariant Isolation
86+
-- Contrapositive of ThresholdComparison. Two sequences are
87+
-- guaranteed not to match when the number of differing
88+
-- positions exceeds the tolerance:
89+
-- comparison_length = min(a.length, b.length)
90+
-- tolerance = comparison_length - round(threshold * comparison_length)
91+
-- differing_positions > tolerance implies is_match = false
92+
-- This bounds the minimum edit distance required to ensure
93+
-- two patterns never interfere under a given threshold.
94+
8595
@invariant MonotonicThreshold
8696
-- For fixed a and b, if is_match(a, b, t1) and t2 <= t1,
8797
-- then is_match(a, b, t2). Lowering the threshold cannot

pkg/logs/internal/decoder/preprocessor/sampler_test.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,12 @@ func requireSampledCountTag(t *testing.T, msg *message.Message, want int64) {
4848

4949
func requireNoSampledCountTag(t *testing.T, msg *message.Message) {
5050
t.Helper()
51+
// Use the production function with a sentinel value to derive the prefix,
52+
// so this helper stays in sync if the tag name changes.
53+
prefix := adaptiveSamplerSampledCountTag(0) // "adaptive_sampler_sampled_count:0"
54+
prefix = prefix[:len(prefix)-1] // strip the trailing "0"
5155
for _, tag := range msg.ParsingExtra.Tags {
52-
assert.NotContains(t, tag, "adaptive_sampler_sampled_count:")
56+
assert.NotContains(t, tag, prefix)
5357
}
5458
}
5559

pkg/logs/internal/decoder/preprocessor/tokenizer_test.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
package preprocessor
88

99
import (
10+
"math"
1011
"testing"
1112

1213
"github.com/stretchr/testify/assert"
@@ -422,6 +423,46 @@ func FuzzIsMatchMonotonicity(f *testing.F) {
422423
})
423424
}
424425

426+
// PatternMatching.Isolation: two sequences differing at more than
427+
// tolerance = len - round(threshold * len) positions are guaranteed
428+
// not to match. Contrapositive of ThresholdComparison.
429+
func FuzzIsMatchIsolation(f *testing.F) {
430+
f.Add([]byte("2024-01-15 10:30:45 INFO request"), uint8(90), uint8(0))
431+
f.Add([]byte("error at line 42 in module"), uint8(75), uint8(3))
432+
f.Add([]byte("GET /api/v2/users 200 42ms"), uint8(50), uint8(1))
433+
f.Fuzz(func(t *testing.T, input []byte, threshPct, startPos uint8) {
434+
thresh := float64(threshPct%101) / 100.0
435+
tok := NewTokenizer(0)
436+
tokens, _ := tok.Tokenize(input)
437+
n := len(tokens)
438+
if n < 2 {
439+
return
440+
}
441+
442+
required := int(math.Round(thresh * float64(n)))
443+
tolerance := n - required
444+
445+
// Construct a mutated sequence differing at tolerance+1 positions.
446+
// This should guarantee no match.
447+
diffs := tolerance + 1
448+
if diffs > n {
449+
return // threshold so low that everything matches
450+
}
451+
452+
mutated := make([]Token, n)
453+
copy(mutated, tokens)
454+
start := int(startPos) % n
455+
for i := range diffs {
456+
pos := (start + i) % n
457+
mutated[pos] = (tokens[pos] + 1) % End
458+
}
459+
460+
assert.False(t, IsMatch(tokens, mutated, thresh),
461+
"sequences differing at %d positions (tolerance=%d) must not match: thresh=%.2f len=%d",
462+
diffs, tolerance, thresh, n)
463+
})
464+
}
465+
425466
func TestIsMatch(t *testing.T) {
426467
tokenizer := NewTokenizer(0)
427468
// A string of 10 tokens to make math easier.

0 commit comments

Comments
 (0)