|
| 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 | + "sync" |
| 12 | + |
| 13 | + "github.com/DataDog/datadog-agent/pkg/logs/patterns/clustering/merging" |
| 14 | + "github.com/DataDog/datadog-agent/pkg/logs/patterns/token" |
| 15 | + "github.com/DataDog/datadog-agent/pkg/trace/log" |
| 16 | +) |
| 17 | + |
| 18 | +// PatternChangeType indicates what changed when adding a TokenList to the cluster manager |
| 19 | +type PatternChangeType int |
| 20 | + |
| 21 | +const ( |
| 22 | + // PatternNoChange means the TokenList was added to an existing cluster without structural changes |
| 23 | + PatternNoChange PatternChangeType = iota |
| 24 | + // PatternNew means a brand new pattern was created (first time seeing this signature) |
| 25 | + PatternNew |
| 26 | + // PatternUpdated means an existing pattern's structure changed (more wildcards added) |
| 27 | + PatternUpdated |
| 28 | +) |
| 29 | + |
| 30 | +// ClusterManager manages the clustering of TokenLists using hash-based bucketing. |
| 31 | +type ClusterManager struct { |
| 32 | + mu sync.RWMutex |
| 33 | + hashBuckets map[uint64][]*Cluster |
| 34 | + nextID uint64 |
| 35 | + |
| 36 | + // patternCount tracks the total number of patterns across all clusters. |
| 37 | + // This is maintained incrementally to avoid O(N) scans on every Add(). |
| 38 | + patternCount int |
| 39 | + |
| 40 | + // estimatedBytes tracks an approximate memory footprint of patterns stored in this manager. |
| 41 | + // This is an estimate (not exact Go heap usage) and is intended for threshold-based eviction triggers. |
| 42 | + estimatedBytes int64 |
| 43 | +} |
| 44 | + |
| 45 | +// NewClusterManager creates a new ClusterManager. |
| 46 | +func NewClusterManager() *ClusterManager { |
| 47 | + return &ClusterManager{ |
| 48 | + hashBuckets: make(map[uint64][]*Cluster), |
| 49 | + nextID: 1, |
| 50 | + } |
| 51 | +} |
| 52 | + |
| 53 | +// PatternCount returns the total number of patterns currently stored. |
| 54 | +func (cm *ClusterManager) PatternCount() int { |
| 55 | + cm.mu.RLock() |
| 56 | + defer cm.mu.RUnlock() |
| 57 | + return cm.patternCount |
| 58 | +} |
| 59 | + |
| 60 | +// EstimatedBytes returns the approximate memory footprint (in bytes) of patterns currently stored. |
| 61 | +func (cm *ClusterManager) EstimatedBytes() int64 { |
| 62 | + cm.mu.RLock() |
| 63 | + defer cm.mu.RUnlock() |
| 64 | + return cm.estimatedBytes |
| 65 | +} |
| 66 | + |
| 67 | +// Add processes a TokenList and adds it to the appropriate cluster. |
| 68 | +// Returns: |
| 69 | +// - pattern: the pattern that was created/updated |
| 70 | +// - changeType: what changed (new/updated/no change) |
| 71 | +// - patternCount: total patterns after this addition |
| 72 | +// - estimatedBytes: total estimated memory after this addition |
| 73 | +func (cm *ClusterManager) Add(tokenList *token.TokenList) (*Pattern, PatternChangeType, int, int64) { |
| 74 | + if tokenList == nil || tokenList.IsEmpty() { |
| 75 | + log.Errorf("Cluster Manager failed to add log: %v for patterning. Token list is empty or nil.", tokenList.String()) |
| 76 | + return nil, PatternNoChange, 0, 0 |
| 77 | + } |
| 78 | + |
| 79 | + // Lock the cluster manager to prevent concurrent access to the hash buckets. Current implementation is single-threaded on local pipeline, but we will eventually build a shared cluster manager across multiple pipelines. |
| 80 | + // todo: implement a shared cluster manager across multiple pipelines |
| 81 | + cm.mu.Lock() |
| 82 | + defer cm.mu.Unlock() |
| 83 | + |
| 84 | + // Create new signature and hash it |
| 85 | + signature := token.NewSignature(tokenList) |
| 86 | + hash := signature.Hash |
| 87 | + |
| 88 | + // Get hash bucket |
| 89 | + clusters := cm.hashBuckets[hash] |
| 90 | + |
| 91 | + // Look for existing cluster with matching signature |
| 92 | + for _, cluster := range clusters { |
| 93 | + if !cluster.Signature.Equals(signature) { |
| 94 | + continue |
| 95 | + } |
| 96 | + |
| 97 | + // Find which pattern within the cluster the tokenList will match |
| 98 | + var matchedPattern *Pattern |
| 99 | + var oldWildcardCount int |
| 100 | + oldPatternCount := len(cluster.Patterns) |
| 101 | + var oldMatchedBytes int64 |
| 102 | + for _, p := range cluster.Patterns { |
| 103 | + if p.Sample != nil && merging.CanMergeTokenLists(tokenList, p.Sample) { |
| 104 | + matchedPattern = p |
| 105 | + oldWildcardCount = p.GetWildcardCount() |
| 106 | + oldMatchedBytes = p.EstimatedBytes() |
| 107 | + break |
| 108 | + } |
| 109 | + } |
| 110 | + |
| 111 | + // Add the tokenList to the cluster (merges or creates new pattern) |
| 112 | + pattern := cluster.AddTokenListToPatterns(tokenList, cm) |
| 113 | + |
| 114 | + // Update counters (pattern count + estimated bytes) |
| 115 | + // If a new pattern was appended to this cluster, increment counts. |
| 116 | + if len(cluster.Patterns) > oldPatternCount { |
| 117 | + cm.patternCount++ |
| 118 | + cm.estimatedBytes += pattern.EstimatedBytes() |
| 119 | + } else if matchedPattern != nil && matchedPattern.PatternID == pattern.PatternID { |
| 120 | + // Existing pattern updated; adjust bytes based on template evolution. |
| 121 | + cm.estimatedBytes += pattern.EstimatedBytes() - oldMatchedBytes |
| 122 | + } |
| 123 | + |
| 124 | + // Check if a new pattern was created (no match found or merge failed) |
| 125 | + if matchedPattern == nil || matchedPattern.PatternID != pattern.PatternID { |
| 126 | + return pattern, PatternNew, cm.patternCount, cm.estimatedBytes |
| 127 | + } |
| 128 | + |
| 129 | + // Check if wildcard count changed (pattern evolved) |
| 130 | + if pattern.GetWildcardCount() != oldWildcardCount { |
| 131 | + return pattern, PatternUpdated, cm.patternCount, cm.estimatedBytes |
| 132 | + } |
| 133 | + |
| 134 | + return pattern, PatternNoChange, cm.patternCount, cm.estimatedBytes |
| 135 | + } |
| 136 | + |
| 137 | + // If no matching pattern was found, create a new cluster and pattern. |
| 138 | + newCluster := NewCluster(signature) |
| 139 | + // Add the token list to create the first pattern |
| 140 | + pattern := newCluster.AddTokenListToPatterns(tokenList, cm) |
| 141 | + cm.hashBuckets[hash] = append(clusters, newCluster) |
| 142 | + |
| 143 | + // New cluster always creates exactly one new pattern |
| 144 | + cm.patternCount++ |
| 145 | + cm.estimatedBytes += pattern.EstimatedBytes() |
| 146 | + |
| 147 | + return pattern, PatternNew, cm.patternCount, cm.estimatedBytes |
| 148 | +} |
| 149 | + |
| 150 | +// Clear removes all clusters. |
| 151 | +func (cm *ClusterManager) Clear() { |
| 152 | + cm.mu.Lock() |
| 153 | + defer cm.mu.Unlock() |
| 154 | + cm.hashBuckets = make(map[uint64][]*Cluster) |
| 155 | + cm.patternCount = 0 |
| 156 | + cm.estimatedBytes = 0 |
| 157 | +} |
| 158 | + |
| 159 | +// GetStats returns the current pattern count and estimated memory usage. |
| 160 | +// This is a read-only operation that acquires a read lock for thread safety. |
| 161 | +func (cm *ClusterManager) GetStats() (patternCount int, estimatedBytes int64) { |
| 162 | + cm.mu.RLock() |
| 163 | + defer cm.mu.RUnlock() |
| 164 | + return cm.patternCount, cm.estimatedBytes |
| 165 | +} |
| 166 | + |
| 167 | +// generatePatternID generates a unique pattern ID using a monotonic counter. |
| 168 | +// Must be called while holding the ClusterManager lock. |
| 169 | +func (cm *ClusterManager) generatePatternID() uint64 { |
| 170 | + id := cm.nextID |
| 171 | + cm.nextID++ |
| 172 | + return id |
| 173 | +} |
0 commit comments