github.com/crowdsecurity/crowdsec@v1.6.1/pkg/leakybucket/bucket.go (about) 1 package leakybucket 2 3 import ( 4 "fmt" 5 "sync" 6 "sync/atomic" 7 "time" 8 9 "github.com/crowdsecurity/go-cs-lib/trace" 10 11 "github.com/crowdsecurity/crowdsec/pkg/time/rate" 12 "github.com/crowdsecurity/crowdsec/pkg/types" 13 "github.com/davecgh/go-spew/spew" 14 "github.com/mohae/deepcopy" 15 "github.com/prometheus/client_golang/prometheus" 16 log "github.com/sirupsen/logrus" 17 "gopkg.in/tomb.v2" 18 ) 19 20 // those constants are now defined in types/constants 21 // const ( 22 // LIVE = iota 23 // TIMEMACHINE 24 // ) 25 26 // Leaky represents one instance of a bucket 27 type Leaky struct { 28 Name string 29 Mode int //LIVE or TIMEMACHINE 30 //the limiter is what holds the proper "leaky aspect", it determines when/if we can pour objects 31 Limiter rate.RateLimiter `json:"-"` 32 SerializedState rate.Lstate 33 //Queue is used to hold the cache of objects in the bucket, it is used to know 'how many' objects we have in buffer. 34 Queue *types.Queue 35 //Leaky buckets are receiving message through a chan 36 In chan *types.Event `json:"-"` 37 //Leaky buckets are pushing their overflows through a chan 38 Out chan *types.Queue `json:"-"` 39 // shared for all buckets (the idea is to kill this afterward) 40 AllOut chan types.Event `json:"-"` 41 //max capacity (for burst) 42 Capacity int 43 //CacheRatio is the number of elements that should be kept in memory (compared to capacity) 44 CacheSize int 45 //the unique identifier of the bucket (a hash) 46 Mapkey string 47 // chan for signaling 48 Signal chan bool `json:"-"` 49 Suicide chan bool `json:"-"` 50 Reprocess bool 51 Simulated bool 52 Uuid string 53 First_ts time.Time 54 Last_ts time.Time 55 Ovflw_ts time.Time 56 Total_count int 57 Leakspeed time.Duration 58 BucketConfig *BucketFactory 59 Duration time.Duration 60 Pour func(*Leaky, types.Event) `json:"-"` 61 //Profiling when set to true enables profiling of bucket 62 Profiling bool 63 timedOverflow bool 64 conditionalOverflow bool 65 logger *log.Entry 66 scopeType types.ScopeType 67 hash string 68 scenarioVersion string 69 tomb *tomb.Tomb 70 wgPour *sync.WaitGroup 71 wgDumpState *sync.WaitGroup 72 mutex *sync.Mutex //used only for TIMEMACHINE mode to allow garbage collection without races 73 orderEvent bool 74 } 75 76 var BucketsPour = prometheus.NewCounterVec( 77 prometheus.CounterOpts{ 78 Name: "cs_bucket_poured_total", 79 Help: "Total events were poured in bucket.", 80 }, 81 []string{"source", "type", "name"}, 82 ) 83 84 var BucketsOverflow = prometheus.NewCounterVec( 85 prometheus.CounterOpts{ 86 Name: "cs_bucket_overflowed_total", 87 Help: "Total buckets overflowed.", 88 }, 89 []string{"name"}, 90 ) 91 92 var BucketsCanceled = prometheus.NewCounterVec( 93 prometheus.CounterOpts{ 94 Name: "cs_bucket_canceled_total", 95 Help: "Total buckets canceled.", 96 }, 97 []string{"name"}, 98 ) 99 100 var BucketsUnderflow = prometheus.NewCounterVec( 101 prometheus.CounterOpts{ 102 Name: "cs_bucket_underflowed_total", 103 Help: "Total buckets underflowed.", 104 }, 105 []string{"name"}, 106 ) 107 108 var BucketsInstantiation = prometheus.NewCounterVec( 109 prometheus.CounterOpts{ 110 Name: "cs_bucket_created_total", 111 Help: "Total buckets were instantiated.", 112 }, 113 []string{"name"}, 114 ) 115 116 var BucketsCurrentCount = prometheus.NewGaugeVec( 117 prometheus.GaugeOpts{ 118 Name: "cs_buckets", 119 Help: "Number of buckets that currently exist.", 120 }, 121 []string{"name"}, 122 ) 123 124 var LeakyRoutineCount int64 125 126 // Newleaky creates a new leaky bucket from a BucketFactory 127 // Events created by the bucket (overflow, bucket empty) are sent to a chan defined by BucketFactory 128 // The leaky bucket implementation is based on rate limiter (see https://godoc.org/golang.org/x/time/rate) 129 // There's a trick to have an event said when the bucket gets empty to allow its destruction 130 func NewLeaky(bucketFactory BucketFactory) *Leaky { 131 bucketFactory.logger.Tracef("Instantiating live bucket %s", bucketFactory.Name) 132 return FromFactory(bucketFactory) 133 } 134 135 func FromFactory(bucketFactory BucketFactory) *Leaky { 136 var limiter rate.RateLimiter 137 //golang rate limiter. It's mainly intended for http rate limiter 138 Qsize := bucketFactory.Capacity 139 if bucketFactory.CacheSize > 0 { 140 //cache is smaller than actual capacity 141 if bucketFactory.CacheSize <= bucketFactory.Capacity { 142 Qsize = bucketFactory.CacheSize 143 //bucket might be counter (infinite size), allow cache limitation 144 } else if bucketFactory.Capacity == -1 { 145 Qsize = bucketFactory.CacheSize 146 } 147 } 148 if bucketFactory.Capacity == -1 { 149 //In this case we allow all events to pass. 150 //maybe in the future we could avoid using a limiter 151 limiter = &rate.AlwaysFull{} 152 } else { 153 limiter = rate.NewLimiter(rate.Every(bucketFactory.leakspeed), bucketFactory.Capacity) 154 } 155 BucketsInstantiation.With(prometheus.Labels{"name": bucketFactory.Name}).Inc() 156 157 //create the leaky bucket per se 158 l := &Leaky{ 159 Name: bucketFactory.Name, 160 Limiter: limiter, 161 Uuid: seed.Generate(), 162 Queue: types.NewQueue(Qsize), 163 CacheSize: bucketFactory.CacheSize, 164 Out: make(chan *types.Queue, 1), 165 Suicide: make(chan bool, 1), 166 AllOut: bucketFactory.ret, 167 Capacity: bucketFactory.Capacity, 168 Leakspeed: bucketFactory.leakspeed, 169 BucketConfig: &bucketFactory, 170 Pour: Pour, 171 Reprocess: bucketFactory.Reprocess, 172 Profiling: bucketFactory.Profiling, 173 Mode: types.LIVE, 174 scopeType: bucketFactory.ScopeType, 175 scenarioVersion: bucketFactory.ScenarioVersion, 176 hash: bucketFactory.hash, 177 Simulated: bucketFactory.Simulated, 178 tomb: bucketFactory.tomb, 179 wgPour: bucketFactory.wgPour, 180 wgDumpState: bucketFactory.wgDumpState, 181 mutex: &sync.Mutex{}, 182 orderEvent: bucketFactory.orderEvent, 183 } 184 if l.BucketConfig.Capacity > 0 && l.BucketConfig.leakspeed != time.Duration(0) { 185 l.Duration = time.Duration(l.BucketConfig.Capacity+1) * l.BucketConfig.leakspeed 186 } 187 if l.BucketConfig.duration != time.Duration(0) { 188 l.Duration = l.BucketConfig.duration 189 l.timedOverflow = true 190 } 191 192 if l.BucketConfig.Type == "conditional" { 193 l.conditionalOverflow = true 194 l.Duration = l.BucketConfig.leakspeed 195 } 196 197 if l.BucketConfig.Type == "bayesian" { 198 l.Duration = l.BucketConfig.leakspeed 199 } 200 return l 201 } 202 203 /* for now mimic a leak routine */ 204 //LeakRoutine us the life of a bucket. It dies when the bucket underflows or overflows 205 func LeakRoutine(leaky *Leaky) error { 206 207 var ( 208 durationTickerChan = make(<-chan time.Time) 209 durationTicker *time.Ticker 210 firstEvent = true 211 ) 212 213 defer trace.CatchPanic(fmt.Sprintf("crowdsec/LeakRoutine/%s", leaky.Name)) 214 215 BucketsCurrentCount.With(prometheus.Labels{"name": leaky.Name}).Inc() 216 defer BucketsCurrentCount.With(prometheus.Labels{"name": leaky.Name}).Dec() 217 218 /*todo : we create a logger at runtime while we want leakroutine to be up asap, might not be a good idea*/ 219 leaky.logger = leaky.BucketConfig.logger.WithFields(log.Fields{"partition": leaky.Mapkey, "bucket_id": leaky.Uuid}) 220 221 //We copy the processors, as they are coming from the BucketFactory, and thus are shared between buckets 222 //If we don't copy, processors using local cache (such as Uniq) are subject to race conditions 223 //This can lead to creating buckets that will discard their first events, preventing the underflow ticker from being initialized 224 //and preventing them from being destroyed 225 processors := deepcopy.Copy(leaky.BucketConfig.processors).([]Processor) 226 227 leaky.Signal <- true 228 atomic.AddInt64(&LeakyRoutineCount, 1) 229 defer atomic.AddInt64(&LeakyRoutineCount, -1) 230 231 for _, f := range processors { 232 err := f.OnBucketInit(leaky.BucketConfig) 233 if err != nil { 234 leaky.logger.Errorf("Problem at bucket initializiation. Bail out %T : %v", f, err) 235 close(leaky.Signal) 236 return fmt.Errorf("Problem at bucket initializiation. Bail out %T : %v", f, err) 237 } 238 } 239 240 leaky.logger.Debugf("Leaky routine starting, lifetime : %s", leaky.Duration) 241 for { 242 select { 243 /*receiving an event*/ 244 case msg := <-leaky.In: 245 /*the msg var use is confusing and is redeclared in a different type :/*/ 246 for _, processor := range processors { 247 msg = processor.OnBucketPour(leaky.BucketConfig)(*msg, leaky) 248 // if &msg == nil we stop processing 249 if msg == nil { 250 if leaky.orderEvent { 251 orderEvent[leaky.Mapkey].Done() 252 } 253 goto End 254 } 255 } 256 if leaky.logger.Level >= log.TraceLevel { 257 leaky.logger.Tracef("Pour event: %s", spew.Sdump(msg)) 258 } 259 BucketsPour.With(prometheus.Labels{"name": leaky.Name, "source": msg.Line.Src, "type": msg.Line.Module}).Inc() 260 261 leaky.Pour(leaky, *msg) // glue for now 262 263 for _, processor := range processors { 264 msg = processor.AfterBucketPour(leaky.BucketConfig)(*msg, leaky) 265 if msg == nil { 266 if leaky.orderEvent { 267 orderEvent[leaky.Mapkey].Done() 268 } 269 goto End 270 } 271 } 272 273 //Clear cache on behalf of pour 274 275 // if durationTicker isn't initialized, then we're pouring our first event 276 277 // reinitialize the durationTicker when it's not a counter bucket 278 if !leaky.timedOverflow || firstEvent { 279 if firstEvent { 280 durationTicker = time.NewTicker(leaky.Duration) 281 durationTickerChan = durationTicker.C 282 defer durationTicker.Stop() 283 } else { 284 durationTicker.Reset(leaky.Duration) 285 } 286 } 287 firstEvent = false 288 /*we overflowed*/ 289 if leaky.orderEvent { 290 orderEvent[leaky.Mapkey].Done() 291 } 292 case ofw := <-leaky.Out: 293 leaky.overflow(ofw) 294 return nil 295 /*suiciiiide*/ 296 case <-leaky.Suicide: 297 close(leaky.Signal) 298 BucketsCanceled.With(prometheus.Labels{"name": leaky.Name}).Inc() 299 leaky.logger.Debugf("Suicide triggered") 300 leaky.AllOut <- types.Event{Type: types.OVFLW, Overflow: types.RuntimeAlert{Mapkey: leaky.Mapkey}} 301 leaky.logger.Tracef("Returning from leaky routine.") 302 return nil 303 /*we underflow or reach bucket deadline (timers)*/ 304 case <-durationTickerChan: 305 var ( 306 alert types.RuntimeAlert 307 err error 308 ) 309 leaky.Ovflw_ts = time.Now().UTC() 310 close(leaky.Signal) 311 ofw := leaky.Queue 312 alert = types.RuntimeAlert{Mapkey: leaky.Mapkey} 313 314 if leaky.timedOverflow { 315 BucketsOverflow.With(prometheus.Labels{"name": leaky.Name}).Inc() 316 317 alert, err = NewAlert(leaky, ofw) 318 if err != nil { 319 log.Errorf("%s", err) 320 } 321 for _, f := range leaky.BucketConfig.processors { 322 alert, ofw = f.OnBucketOverflow(leaky.BucketConfig)(leaky, alert, ofw) 323 if ofw == nil { 324 leaky.logger.Debugf("Overflow has been discarded (%T)", f) 325 break 326 } 327 } 328 leaky.logger.Infof("Timed Overflow") 329 } else { 330 leaky.logger.Debugf("bucket underflow, destroy") 331 BucketsUnderflow.With(prometheus.Labels{"name": leaky.Name}).Inc() 332 333 } 334 if leaky.logger.Level >= log.TraceLevel { 335 /*don't sdump if it's not going to be printed, it's expensive*/ 336 leaky.logger.Tracef("Overflow event: %s", spew.Sdump(types.Event{Overflow: alert})) 337 } 338 339 leaky.AllOut <- types.Event{Overflow: alert, Type: types.OVFLW} 340 leaky.logger.Tracef("Returning from leaky routine.") 341 return nil 342 case <-leaky.tomb.Dying(): 343 leaky.logger.Debugf("Bucket externally killed, return") 344 for len(leaky.Out) > 0 { 345 ofw := <-leaky.Out 346 leaky.overflow(ofw) 347 } 348 leaky.AllOut <- types.Event{Type: types.OVFLW, Overflow: types.RuntimeAlert{Mapkey: leaky.Mapkey}} 349 return nil 350 351 } 352 End: 353 } 354 } 355 356 func Pour(leaky *Leaky, msg types.Event) { 357 leaky.wgDumpState.Wait() 358 leaky.wgPour.Add(1) 359 defer leaky.wgPour.Done() 360 361 leaky.Total_count += 1 362 if leaky.First_ts.IsZero() { 363 leaky.First_ts = time.Now().UTC() 364 } 365 leaky.Last_ts = time.Now().UTC() 366 367 if leaky.Limiter.Allow() || leaky.conditionalOverflow { 368 leaky.Queue.Add(msg) 369 } else { 370 leaky.Ovflw_ts = time.Now().UTC() 371 leaky.logger.Debugf("Last event to be poured, bucket overflow.") 372 leaky.Queue.Add(msg) 373 leaky.Out <- leaky.Queue 374 } 375 } 376 377 func (leaky *Leaky) overflow(ofw *types.Queue) { 378 close(leaky.Signal) 379 alert, err := NewAlert(leaky, ofw) 380 if err != nil { 381 log.Errorf("%s", err) 382 } 383 leaky.logger.Tracef("Overflow hooks time : %v", leaky.BucketConfig.processors) 384 for _, f := range leaky.BucketConfig.processors { 385 alert, ofw = f.OnBucketOverflow(leaky.BucketConfig)(leaky, alert, ofw) 386 if ofw == nil { 387 leaky.logger.Debugf("Overflow has been discarded (%T)", f) 388 break 389 } 390 } 391 if leaky.logger.Level >= log.TraceLevel { 392 leaky.logger.Tracef("Overflow event: %s", spew.Sdump(alert)) 393 } 394 mt, _ := leaky.Ovflw_ts.MarshalText() 395 leaky.logger.Tracef("overflow time : %s", mt) 396 397 BucketsOverflow.With(prometheus.Labels{"name": leaky.Name}).Inc() 398 399 leaky.AllOut <- types.Event{Overflow: alert, Type: types.OVFLW, MarshaledTime: string(mt)} 400 }