Skip to content

Commit 9a9eb73

Browse files
committed
Implement pattern eviction with LFU decay algorithm and memory-based eviction strategies
1 parent afdc8e1 commit 9a9eb73

14 files changed

Lines changed: 2243 additions & 0 deletions

pkg/config/setup/config.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1953,6 +1953,13 @@ func logsagent(config pkgconfigmodel.Setup) {
19531953
// Tag logs with their auto multiline detection label without aggregating them
19541954
config.BindEnvAndSetDefault("logs_config.auto_multi_line_detection_tagging", true)
19551955

1956+
// Log Pattern Eviction
1957+
config.BindEnvAndSetDefault("logs_config.patterns.max_pattern_count", 700)
1958+
config.BindEnvAndSetDefault("logs_config.patterns.max_memory_bytes", 4*1024*1024) // 4MB
1959+
config.BindEnvAndSetDefault("logs_config.patterns.eviction_high_watermark", 0.80) // Trigger eviction at 80% capacity
1960+
config.BindEnvAndSetDefault("logs_config.patterns.eviction_low_watermark", 0.70) // Evict back to 70%
1961+
config.BindEnvAndSetDefault("logs_config.patterns.age_decay_factor", 0.5)
1962+
19561963
// Number of logs pipeline instances. Defaults to number of logical CPU cores as defined by GOMAXPROCS or 4, whichever is lower.
19571964
logsPipelines := min(4, runtime.GOMAXPROCS(0))
19581965
config.BindEnvAndSetDefault("logs_config.pipelines", logsPipelines)
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
// Unless explicitly stated otherwise all files in this repository are licensed
2+
// under the Apache License Version 2.0.
3+
// This product includes software developed at Datadog (https://www.datadoghq.com/).
4+
// Copyright 2016-present Datadog, Inc.
5+
6+
// Package clustering provides clustering functionality for grouping similar TokenLists,
7+
// extracting patterns wildcard, and managing pattern lifecycle through eviction policies.
8+
package clustering
9+
10+
import (
11+
"time"
12+
13+
"github.com/DataDog/datadog-agent/pkg/logs/patterns/eviction"
14+
"github.com/DataDog/datadog-agent/pkg/logs/patterns/token"
15+
"github.com/DataDog/datadog-agent/pkg/util/log"
16+
)
17+
18+
// EvictionPolicy is the policy for evicting patterns from the cluster manager.
19+
type EvictionPolicy int
20+
21+
const (
22+
// EvictionPolicyLFUDecay uses LFU with exponential age decay
23+
EvictionPolicyLFUDecay EvictionPolicy = iota
24+
)
25+
26+
// CollectEvictables collects all patterns as evictables (implements eviction.EvictableCollection).
27+
func (cm *ClusterManager) CollectEvictables() []eviction.Evictable {
28+
cm.mu.RLock()
29+
defer cm.mu.RUnlock()
30+
31+
evictables := make([]eviction.Evictable, 0, cm.patternCount)
32+
for _, clusters := range cm.hashBuckets {
33+
for _, cluster := range clusters {
34+
for _, pattern := range cluster.Patterns {
35+
evictables = append(evictables, pattern)
36+
}
37+
}
38+
}
39+
return evictables
40+
}
41+
42+
// RemoveEvictable removes a specific pattern from the cluster manager (implements eviction.EvictableCollection).
43+
func (cm *ClusterManager) RemoveEvictable(item eviction.Evictable) {
44+
pattern := item.(*Pattern)
45+
cm.removePattern(pattern)
46+
}
47+
48+
// removePattern removes a specific pattern from the cluster manager's hash buckets.
49+
func (cm *ClusterManager) removePattern(pattern *Pattern) {
50+
var (
51+
targetHash uint64
52+
targetSig *token.Signature
53+
)
54+
55+
if pattern.Sample != nil && !pattern.Sample.IsEmpty() {
56+
sig := token.NewSignature(pattern.Sample)
57+
targetSig = &sig
58+
targetHash = sig.GetHashBucket()
59+
} else if pattern.Template != nil && !pattern.Template.IsEmpty() {
60+
sig := token.NewSignature(pattern.Template)
61+
targetSig = &sig
62+
targetHash = sig.GetHashBucket()
63+
}
64+
65+
cm.mu.Lock()
66+
defer cm.mu.Unlock()
67+
68+
removeFromBucket := func(hash uint64, clusters []*Cluster, verifySignature bool) bool {
69+
for ci := 0; ci < len(clusters); ci++ {
70+
cluster := clusters[ci]
71+
if verifySignature && targetSig != nil && !targetSig.Equals(cluster.Signature) {
72+
continue
73+
}
74+
75+
for pi := 0; pi < len(cluster.Patterns); pi++ {
76+
if cluster.Patterns[pi] != pattern {
77+
continue
78+
}
79+
80+
// Update counters before removing pattern
81+
cm.patternCount--
82+
cm.estimatedBytes -= pattern.EstimatedBytes()
83+
84+
// Remove pattern with swap delete for speed
85+
lastPattern := len(cluster.Patterns) - 1
86+
cluster.Patterns[pi] = cluster.Patterns[lastPattern]
87+
cluster.Patterns = cluster.Patterns[:lastPattern]
88+
cluster.UpdatedAt = time.Now()
89+
90+
if len(cluster.Patterns) == 0 {
91+
// Remove cluster from bucket
92+
lastCluster := len(clusters) - 1
93+
clusters[ci] = clusters[lastCluster]
94+
clusters = clusters[:lastCluster]
95+
96+
if len(clusters) == 0 {
97+
delete(cm.hashBuckets, hash)
98+
} else {
99+
cm.hashBuckets[hash] = clusters
100+
}
101+
} else {
102+
cm.hashBuckets[hash] = clusters
103+
}
104+
return true
105+
}
106+
}
107+
return false
108+
}
109+
110+
// Try targeted bucket first
111+
if targetSig != nil {
112+
if clusters, ok := cm.hashBuckets[targetHash]; ok {
113+
if removeFromBucket(targetHash, clusters, true) {
114+
return
115+
}
116+
}
117+
}
118+
119+
// Fallback: full scan if signature was missing or not found in target bucket
120+
for hash, clusters := range cm.hashBuckets {
121+
if removeFromBucket(hash, clusters, false) {
122+
return
123+
}
124+
}
125+
126+
log.Warnf("Pattern %d not found during eviction, may have been already removed", pattern.PatternID)
127+
}
128+
129+
// EvictLowestScoringPatterns evicts up to numToEvict patterns with the lowest eviction scores.
130+
// Returns the list of evicted patterns.
131+
func (cm *ClusterManager) EvictLowestScoringPatterns(numToEvict int, decayFactor float64) []*Pattern {
132+
evictables := eviction.EvictLowestScoring(cm, numToEvict, decayFactor)
133+
if len(evictables) == 0 {
134+
return nil
135+
}
136+
patterns := make([]*Pattern, len(evictables))
137+
for i, ev := range evictables {
138+
patterns[i] = ev.(*Pattern)
139+
}
140+
return patterns
141+
}
142+
143+
// EvictToMemoryTarget evicts patterns until the target memory is freed.
144+
// It uses actual pattern sizes rather than averages for precision.
145+
func (cm *ClusterManager) EvictToMemoryTarget(targetBytesToFree int64, decayFactor float64) []*Pattern {
146+
evictables := eviction.EvictToMemoryTarget(cm, targetBytesToFree, decayFactor)
147+
if len(evictables) == 0 {
148+
return nil
149+
}
150+
patterns := make([]*Pattern, len(evictables))
151+
for i, ev := range evictables {
152+
patterns[i] = ev.(*Pattern)
153+
}
154+
return patterns
155+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
// Unless explicitly stated otherwise all files in this repository are licensed
2+
// under the Apache License Version 2.0.
3+
// This product includes software developed at Datadog (https://www.datadoghq.com/).
4+
// Copyright 2016-present Datadog, Inc.
5+
6+
// Package clustering provides clustering functionality for grouping similar TokenLists,
7+
// extracting patterns wildcard, and managing pattern lifecycle through eviction policies.
8+
package clustering
9+
10+
import (
11+
pkgconfigsetup "github.com/DataDog/datadog-agent/pkg/config/setup"
12+
"github.com/DataDog/datadog-agent/pkg/logs/patterns/eviction"
13+
"github.com/DataDog/datadog-agent/pkg/util/log"
14+
)
15+
16+
// EvictionManager handles pattern eviction using dual watermark system.
17+
// It wraps the generic eviction.Manager with clustering-specific configuration.
18+
type EvictionManager struct {
19+
*eviction.Manager
20+
}
21+
22+
// NewEvictionManager creates a new EvictionManager from config
23+
func NewEvictionManager() *EvictionManager {
24+
cfg := pkgconfigsetup.Datadog()
25+
26+
return &EvictionManager{
27+
Manager: &eviction.Manager{
28+
MaxItemCount: cfg.GetInt("logs_config.patterns.max_pattern_count"),
29+
MaxMemoryBytes: int64(cfg.GetInt("logs_config.patterns.max_memory_bytes")),
30+
EvictionHighWatermark: cfg.GetFloat64("logs_config.patterns.eviction_high_watermark"),
31+
EvictionLowWatermark: cfg.GetFloat64("logs_config.patterns.eviction_low_watermark"),
32+
AgeDecayFactor: cfg.GetFloat64("logs_config.patterns.age_decay_factor"),
33+
},
34+
}
35+
}
36+
37+
// Evict performs eviction on the cluster manager based on which threshold was exceeded
38+
func (em *EvictionManager) Evict(cm *ClusterManager, patternCount int, estimatedBytes int64, countOverLimit, bytesOverLimit bool) {
39+
var evicted []*Pattern
40+
41+
numToEvict, bytesToFree, strategy := em.EvictionTargets(patternCount, estimatedBytes, countOverLimit, bytesOverLimit)
42+
43+
switch strategy {
44+
// Memory-based eviction
45+
case eviction.StrategyByBytes:
46+
evicted = cm.EvictToMemoryTarget(bytesToFree, em.AgeDecayFactor)
47+
48+
highWatermarkBytes := int64(float64(em.MaxMemoryBytes) * em.EvictionHighWatermark)
49+
targetBytes := int64(float64(em.MaxMemoryBytes) * em.EvictionLowWatermark)
50+
log.Infof("Evicted %d patterns: memory %d bytes exceeded high watermark %d bytes (%.0f%% of %d max), now targeting %d bytes (%.0f%%)",
51+
len(evicted), estimatedBytes, highWatermarkBytes,
52+
em.EvictionHighWatermark*100, em.MaxMemoryBytes,
53+
targetBytes, em.EvictionLowWatermark*100)
54+
55+
// Log count-based eviction
56+
case eviction.StrategyByCount:
57+
evicted = cm.EvictLowestScoringPatterns(numToEvict, em.AgeDecayFactor)
58+
59+
highWatermarkCount := int(float64(em.MaxItemCount) * em.EvictionHighWatermark)
60+
targetCount := int(float64(em.MaxItemCount) * em.EvictionLowWatermark)
61+
log.Infof("Evicted %d patterns: count %d exceeded high watermark %d (%.0f%% of %d max), now targeting %d (%.0f%%)",
62+
len(evicted), patternCount, highWatermarkCount,
63+
em.EvictionHighWatermark*100, em.MaxItemCount,
64+
targetCount, em.EvictionLowWatermark*100)
65+
}
66+
}

0 commit comments

Comments
 (0)