Skip to content

Instantly share code, notes, and snippets.

@nblumhardt
Created November 17, 2015 10:50
Show Gist options
  • Save nblumhardt/d3baa1ea511242951e78 to your computer and use it in GitHub Desktop.
Save nblumhardt/d3baa1ea511242951e78 to your computer and use it in GitHub Desktop.
Proof of concept rate limiting filter for Serilog
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