Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tagging #312

Merged
merged 9 commits into from
Mar 8, 2021
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 47 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,16 @@ We use this library within our components to publish [StatsD](http://github.com/

### Features

* Easy to use
* Robust and proven
* Easy to use.
* Robust and proven.
* Tuned for high performance and low resource usage using [BenchmarkDotNet](https://benchmarkdotnet.org/). Typically zero allocation on sending a metric on target frameworks where `Span<T>` is available.
* Works well with modern .NET apps - `async ... await`, .NET Core, .NET Standard 2.0.
* Supports standard StatsD primitives: `Increment`, `Decrement`, `Timing` and `Gauge`.
* Supports tagging on `Increment`, `Decrement`, `Timing` and `Gauge`.
* Supports sample rate for cutting down of sends of high-volume metrics.
* Helpers to make it easy to time a delegate such as a `Func<T>` or `Action<T>`, or a code block inside a `using` statement.
* Send stats over UDP or IP
* Send stats to a server by name or IP address
* Send stats over UDP or IP.
* Send stats to a server by name or IP address.

#### Publishing statistics

Expand Down Expand Up @@ -143,15 +144,16 @@ Bind<StatsDConfiguration>().ToConstant(statsDConfig>);
Bind<IStatsDPublisher>().To<StatsDPublisher>().InSingletonScope();
```

## StatsDConfiguration fields
### StatsDConfiguration fields

| Name | Type | Default | Comments |
|-------------------|-------------------------|--------------------------------|---------------------------------------------------------------------------------------------------------|
| Host | `string` | | The host name or IP address of the StatsD server. There is no default, this must be set. |
| Port | `int` | `8125` | The StatsD port. |
| DnsLookupInterval | `TimeSpan?` | `5 minutes` | Length of time to cache the host name to IP address lookup. Only used when "Host" contains a host name. |
| Prefix | `string` | `string.Empty` | Prepend a prefix to all stats.
| SocketProtocol | `SocketProtocol`, one of `Udp`, `IP`| `Udp` | Type of socket to use when sending stats to the server. |
| Prefix | `string` | `string.Empty` | Prepend a prefix to all stats. |
| SocketProtocol | `SocketProtocol`, one of `Udp`, `IP`| `Udp` | Type of socket to use when sending stats to the server. |
| TagsFormatter | `IStatsDTagsFormatter` | `NoOpTagsFormatter` | Format used for tags for the different providers. Out-of-the-box formatters can be accessed using the `TagsFormatter` class. |
| OnError | `Func<Exception, bool>` | `null` | Function to receive notification of any exceptions. |

`OnError` is a function to receive notification of any errors that occur when trying to publish a metric. This function should return:
Expand All @@ -161,7 +163,39 @@ Bind<IStatsDPublisher>().To<StatsDPublisher>().InSingletonScope();

The default behaviour is to ignore the error.

#### Example of using the interface
#### Tagging support

Tags or dimensions are not covered by the StatsD specification. Providers supporting tags have implemented their own flavours. Some of the major providers are supported out-of-the-box from 5.0.0+ are:

* [CloudWatch](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch-Agent-custom-metrics-statsd.html).
* [DataDog](https://docs.datadoghq.com/developers/dogstatsd/datagram_shell/?tab=metrics)
* [InfluxDB](https://www.influxdata.com/blog/getting-started-with-sending-statsd-metrics-to-telegraf-influxdb/#introducing-influx-statsd).
* [Librato](https://github.com/librato/statsd-librato-backend#tags).
* [SignalFX](https://docs.signalfx.com/en/latest/integrations/agent/monitors/collectd-statsd.html#adding-dimensions-to-statsd-metrics).
* [Splunk](https://docs.splunk.com/Documentation/Splunk/8.0.5/Metrics/GetMetricsInStatsd).

```csharp
var config = new StatsDConfiguration
{
Prefix = "prefix",
Host = "127.0.0.1",
TagsFormatter = TagsFormatter.CloudWatch,
};
```

##### Extending tags formatter

As tags are not part of the StatsD specification, the `IStatsDTagsFormatter` used can be extended and specified by the `StatsDConfiguration`.

The template class `StatsDTagsFormatter` can be inherited from providing the `StatsDTagsFormatterConfiguration`:

* **Prefix**: the string that will appear before the tag(s).
* **Suffix**: the string that will appear after the tag(s).
* **AreTrailing**: a boolean indicating if the tag(s) are placed at the end of the StatsD message (like it is supported by AWS CloudWatch, DataDog and Splunk) or otherwise they are right after the bucket name (like it is supported by InfluxDB, Librato and SignalFX).
* **TagsSeparator**: the string that will be placed between tags.
* **KeyValueSeparator**: the string that will be placed between the tag key and its value.

### Example of using the interface

Given an existing instance of `IStatsDPublisher` called `stats` you can do for e.g.:

Expand All @@ -176,7 +210,7 @@ var statName = "DoSomething." + success ? "Success" : "Failure";
stats.Timing(statName, stopWatch.Elapsed);
```

#### Simple timers
### Simple timers

This syntax for timers less typing in simple cases, where you always want to time the operation, and always with the same stat name. Given an existing instance of `IStatsDPublisher` you can do:

Expand All @@ -197,7 +231,7 @@ using (stats.StartTimer("someStat"))
The `StartTimer` returns an `IDisposableTimer` that wraps a stopwatch and implements `IDisposable`.
The stopwatch is automatically stopped and the metric sent when it falls out of scope and is disposed on the closing `}` of the `using` statement.

##### Changing the name of simple timers
#### Changing the name of simple timers

Sometimes the decision of which stat to send should not be taken before the operation completes. e.g. When you are timing http operations and want different status codes to be logged under different stats.

Expand All @@ -214,7 +248,7 @@ using (var timer = stats.StartTimer("SomeHttpOperation."))
}
```

##### Functional style
#### Functional style

```csharp
// timing an action without a return value:
Expand All @@ -228,9 +262,9 @@ await stats.Time("someStat", async t => await DoSomethingAsync());
var result = await stats.Time("someStat", async t => await GetSomethingAsync());
```

In all these cases the function or delegate is supplied with a `IDisposableTimer t` so that the stat name can be changed if need be.
In all these cases the function or delegate is supplied with an `IDisposableTimer t` so that the stat name can be changed if need be.

##### Credits
#### Credits

The idea of "disposable timers" for using statements is an old one, see for example [this StatsD client](https://github.com/Pereingo/statsd-csharp-client) and [MiniProfiler](https://miniprofiler.com/dotnet/HowTo/ProfileCode).

Expand Down
6 changes: 3 additions & 3 deletions src/JustEat.StatsD/Buffered/Buffer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@

namespace JustEat.StatsD.Buffered
{
internal ref struct Buffer
internal ref struct Buffer<T>
{
public Buffer(Span<byte> source)
public Buffer(Span<T> source)
{
Tail = source;
Written = 0;
}

public Span<byte> Tail;
public Span<T> Tail;
public int Written;
}
}
19 changes: 10 additions & 9 deletions src/JustEat.StatsD/Buffered/BufferBasedStatsDPublisher.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;

namespace JustEat.StatsD.Buffered
{
Expand All @@ -9,12 +10,12 @@ internal sealed class BufferBasedStatsDPublisher : IStatsDPublisher

[ThreadStatic]
private static byte[]? _buffer;
private static byte[] Buffer => _buffer ?? (_buffer = new byte[SafeUdpPacketSize]);
private static byte[] Buffer => _buffer ??= new byte[SafeUdpPacketSize];

#pragma warning disable CA5394
[ThreadStatic]
private static Random? _random;
private static Random Random => _random ?? (_random = new Random());
private static Random Random => _random ??= new Random();
#pragma warning disable CA5394

private readonly StatsDUtf8Formatter _formatter;
Expand All @@ -30,22 +31,22 @@ internal BufferBasedStatsDPublisher(StatsDConfiguration configuration, IStatsDTr

_onError = configuration.OnError;
_transport = transport;
_formatter = new StatsDUtf8Formatter(configuration.Prefix);
_formatter = new StatsDUtf8Formatter(configuration.Prefix, configuration.TagsFormatter);
}

public void Increment(long value, double sampleRate, string bucket)
public void Increment(long value, double sampleRate, string bucket, Dictionary<string, string?>? tags)
{
SendMessage(sampleRate, StatsDMessage.Counter(value, bucket));
SendMessage(sampleRate, StatsDMessage.Counter(value, bucket, tags));
}

public void Gauge(double value, string bucket)
public void Gauge(double value, string bucket, Dictionary<string, string?>? tags)
{
SendMessage(DefaultSampleRate, StatsDMessage.Gauge(value, bucket));
SendMessage(DefaultSampleRate, StatsDMessage.Gauge(value, bucket, tags));
}

public void Timing(long duration, double sampleRate, string bucket)
public void Timing(long duration, double sampleRate, string bucket, Dictionary<string, string?>? tags)
{
SendMessage(sampleRate, StatsDMessage.Timing(duration, bucket));
SendMessage(sampleRate, StatsDMessage.Timing(duration, bucket, tags));
}

private void SendMessage(double sampleRate, in StatsDMessage msg)
Expand Down
54 changes: 46 additions & 8 deletions src/JustEat.StatsD/Buffered/BufferExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ namespace JustEat.StatsD.Buffered
internal static class BufferExtensions
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool TryWriteBytes(this ref Buffer src, Span<byte> destination)
public static bool TryWrite<T>(this ref Buffer<T> src, ReadOnlySpan<T> destination)
{
if (destination.Length > src.Tail.Length)
{
Expand All @@ -23,11 +23,16 @@ public static bool TryWriteBytes(this ref Buffer src, Span<byte> destination)
return true;
}

public static bool TryWriteUtf8String(this ref Buffer src, string str)
public static bool TryWriteUtf8String(this ref Buffer<byte> src, string str)
{
if (string.IsNullOrEmpty(str))
{
return true;
}

#if NETSTANDARD2_0 || NET461
var bucketBytes = Encoding.UTF8.GetBytes(str);
return src.TryWriteBytes(bucketBytes);
return src.TryWrite(bucketBytes);
#else
int written = 0;
try
Expand All @@ -47,8 +52,37 @@ public static bool TryWriteUtf8String(this ref Buffer src, string str)
#endif
}

public static bool TryWriteUtf8Chars(this ref Buffer<byte> src, ReadOnlySpan<char> chars)
{
if (chars.Length == 0)
{
return true;
}

#if NETSTANDARD2_0 || NET461
var bytes = Encoding.UTF8.GetBytes(chars.ToArray());
return src.TryWrite(bytes);
#else
int written = 0;
try
{
written = Encoding.UTF8.GetBytes(chars, src.Tail);
}
#pragma warning disable CA1031
catch (ArgumentException)
#pragma warning restore CA1031
{
return false;
}

src.Tail = src.Tail.Slice(written);
src.Written += written;
return true;
#endif
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool TryWriteByte(this ref Buffer src, byte ch)
public static bool TryWrite<T>(this ref Buffer<T> src, T ch)
{
const int OneByte = 1;
if (src.Tail.Length < OneByte)
Expand All @@ -64,7 +98,7 @@ public static bool TryWriteByte(this ref Buffer src, byte ch)
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool TryWriteBytes(this ref Buffer src, byte ch1, byte ch2)
public static bool TryWrite<T>(this ref Buffer<T> src, T ch1, T ch2)
{
const int TwoBytes = 2;
if (src.Tail.Length < TwoBytes)
Expand All @@ -81,7 +115,7 @@ public static bool TryWriteBytes(this ref Buffer src, byte ch1, byte ch2)
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool TryWriteBytes(this ref Buffer src, byte ch1, byte ch2, byte ch3)
public static bool TryWrite<T>(this ref Buffer<T> src, T ch1, T ch2, T ch3)
{
const int ThreeBytes = 3;
if (src.Tail.Length < ThreeBytes)
Expand All @@ -99,7 +133,7 @@ public static bool TryWriteBytes(this ref Buffer src, byte ch1, byte ch2, byte c
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool TryWriteInt64(this ref Buffer src, long val)
public static bool TryWriteInt64(this ref Buffer<byte> src, long val)
{
if (!Utf8Formatter.TryFormat(val, src.Tail, out var consumed))
{
Expand All @@ -113,7 +147,7 @@ public static bool TryWriteInt64(this ref Buffer src, long val)
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool TryWriteDouble(this ref Buffer src, double val)
public static bool TryWriteDouble(this ref Buffer<byte> src, double val)
{
if (!Utf8Formatter.TryFormat((decimal)val, src.Tail, out var consumed))
{
Expand All @@ -125,5 +159,9 @@ public static bool TryWriteDouble(this ref Buffer src, double val)

return true;
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool TryWriteString(this ref Buffer<char> src, string str) =>
TryWrite(ref src, str.AsSpan());
}
}
31 changes: 24 additions & 7 deletions src/JustEat.StatsD/Buffered/StatsDMessage.cs
Original file line number Diff line number Diff line change
@@ -1,31 +1,48 @@
using System.Collections.Generic;

namespace JustEat.StatsD.Buffered
{
internal readonly struct StatsDMessage
{
public readonly string StatBucket;
public readonly double Magnitude;
public readonly StatsDMessageKind MessageKind;
public readonly Dictionary<string, string?>? Tags;

private StatsDMessage(string statBucket, double magnitude, StatsDMessageKind messageKind)
private StatsDMessage(
string statBucket,
double magnitude,
StatsDMessageKind messageKind,
Dictionary<string, string?>? tags)
{
StatBucket = statBucket;
Magnitude = magnitude;
MessageKind = messageKind;
Tags = tags;
}

public static StatsDMessage Timing(long milliseconds, string statBucket)
public static StatsDMessage Timing(
long milliseconds,
string statBucket,
Dictionary<string, string?>? tags)
{
return new StatsDMessage(statBucket, milliseconds, StatsDMessageKind.Timing);
return new StatsDMessage(statBucket, milliseconds, StatsDMessageKind.Timing, tags);
}

public static StatsDMessage Counter(long magnitude, string statBucket)
public static StatsDMessage Counter(
long magnitude,
string statBucket,
Dictionary<string, string?>? tags)
{
return new StatsDMessage(statBucket, magnitude, StatsDMessageKind.Counter);
return new StatsDMessage(statBucket, magnitude, StatsDMessageKind.Counter, tags);
}

public static StatsDMessage Gauge(double magnitude, string statBucket)
public static StatsDMessage Gauge(
double magnitude,
string statBucket,
Dictionary<string, string?>? tags)
{
return new StatsDMessage(statBucket, magnitude, StatsDMessageKind.Gauge);
return new StatsDMessage(statBucket, magnitude, StatsDMessageKind.Gauge, tags);
}
}
}
Loading