Created
November 17, 2015 10:50
-
-
Save nblumhardt/d3baa1ea511242951e78 to your computer and use it in GitHub Desktop.
Proof of concept rate limiting filter for Serilog
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using Serilog; | |
using Serilog.Configuration; | |
namespace ConsoleApplication8 | |
{ | |
static class Program | |
{ | |
static void Main() | |
{ | |
Log.Logger = new LoggerConfiguration() | |
.Filter.PerMinuteInExcessOf(1000) | |
.WriteTo.Console() | |
.CreateLogger(); | |
Log.Information("Hello!"); | |
} | |
static LoggerConfiguration PerMinuteInExcessOf(this LoggerFilterConfiguration filterConfiguration, int maximumRate) | |
{ | |
var counter = new RateCounter(); | |
return filterConfiguration.ByExcluding(e => | |
{ | |
counter.Measure(1); | |
return counter.RatePerMinute > maximumRate; | |
}); | |
} | |
} | |
class RateCounter | |
{ | |
// For simplicity only per-minute is implemented | |
const int WindowSeconds = 60; | |
readonly object _sync = new object(); | |
// Each bucket counts events in a one-second interval | |
readonly int[] _buckets; | |
// A running sum of all buckets | |
int _sum; | |
// The index of the current ('latest') bucket | |
int _currentBucket; | |
// The date/time, rounded down to the second, corresponding | |
// with the latest bucket. | |
DateTime _currentBucketSecondUtc; | |
public RateCounter() | |
{ | |
_buckets = new int[WindowSeconds]; | |
_currentBucketSecondUtc = DateTime.UtcNow; | |
_currentBucket = 0; | |
} | |
public void Measure(int count) | |
{ | |
lock (_sync) | |
{ | |
var eventBucket = SlideWindow(); | |
_buckets[eventBucket] += count; | |
_sum += count; | |
} | |
} | |
public int RatePerMinute | |
{ | |
get | |
{ | |
lock (_sync) | |
{ | |
SlideWindow(); | |
return _sum; | |
} | |
} | |
} | |
int SlideWindow() | |
{ | |
var bucketDateTimeUtc = DateTime.UtcNow; | |
var eventSeconds = SecondsFloor(bucketDateTimeUtc); | |
var distance = (int)(eventSeconds - _currentBucketSecondUtc).TotalSeconds; | |
if (distance < 0) | |
{ | |
if (distance <= -_buckets.Length) | |
{ | |
// Log.Error("A clock change of {Distance} sec. has invalidated a rate counter; metrics will be inflated until the counter stabilizes", distance); | |
return _currentBucket; | |
} | |
return (_currentBucket + distance) % _buckets.Length; | |
} | |
if (distance > 0) | |
{ | |
var newCurrent = (_currentBucket + distance) % _buckets.Length; | |
var firstReused = (_currentBucket + 1) % _buckets.Length; | |
if (distance >= _buckets.Length) | |
{ | |
_sum = 0; | |
for (var i = 0; i < _buckets.Length; i++) | |
{ | |
_buckets[i] = 0; | |
} | |
} | |
else if (newCurrent >= firstReused) | |
{ | |
for (var i = firstReused; i <= newCurrent; i++) | |
{ | |
_sum -= _buckets[i]; | |
_buckets[i] = 0; | |
} | |
} | |
else | |
{ | |
for (var i = firstReused; i < _buckets.Length; i++) | |
{ | |
_sum -= _buckets[i]; | |
_buckets[i] = 0; | |
} | |
for (var i = 0; i <= newCurrent; i++) | |
{ | |
_sum -= _buckets[i]; | |
_buckets[i] = 0; | |
} | |
} | |
_currentBucket = newCurrent; | |
_currentBucketSecondUtc = eventSeconds; | |
} | |
return _currentBucket; | |
} | |
// Returns the provided date time rounded down to the nearest second | |
static DateTime SecondsFloor(DateTime dateTime) | |
{ | |
return dateTime - TimeSpan.FromTicks(dateTime.Ticks % TimeSpan.TicksPerSecond); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment