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  }