diff --git a/.travis.yml b/.travis.yml index 58905ad9..12c3bf7a 100755 --- a/.travis.yml +++ b/.travis.yml @@ -13,6 +13,7 @@ env: branches: only: - master + - release/3xx addons: apt: @@ -23,5 +24,9 @@ addons: - libssl-dev - libunwind8 +install: + - git clone https://github.com/etsy/statsd.git + - node ./statsd/stats.js ./src/JustEat.StatsD.Tests/statsdconfig.js & + script: - ./build.sh diff --git a/appveyor.yml b/appveyor.yml index 4a0ede90..027c1c62 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,10 +1,12 @@ os: Visual Studio 2017 -version: 3.2.1.{build} +version: 3.2.2.{build} configuration: Release environment: DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true NUGET_XMLDOC_MODE: skip install: + - git clone https://github.com/etsy/statsd.git ..\statsd + - ps: Start-Process "node" -ArgumentList "..\statsd\stats.js .\src\JustEat.StatsD.Tests\statsdconfig.js" -WindowStyle Hidden - ps: .\SetAppVeyorBuildVersion.ps1 build_script: - ps: .\Build.ps1 diff --git a/global.json b/global.json index 8a567cfb..55e448df 100644 --- a/global.json +++ b/global.json @@ -1,5 +1,5 @@ { "sdk": { - "version": "2.1.402" + "version": "2.1.403" } } diff --git a/src/Benchmark/UdpTransportBenchmark.cs b/src/Benchmark/UdpTransportBenchmark.cs index d15f70bc..5002ff84 100644 --- a/src/Benchmark/UdpTransportBenchmark.cs +++ b/src/Benchmark/UdpTransportBenchmark.cs @@ -9,7 +9,7 @@ namespace Benchmark [MemoryDiagnoser] public class UdpTransportBenchmark { - private const string MetricName = "this.is.a.metric"; + private const string MetricName = "this.is.a.metric:1|c"; private PooledUdpTransport _pooledTransport; private PooledUdpTransport _pooledTransportSwitched; diff --git a/src/JustEat.StatsD.Tests/EndpointLookups/DnsLookupIpEndpointSourceTests.cs b/src/JustEat.StatsD.Tests/EndpointLookups/DnsLookupIpEndpointSourceTests.cs new file mode 100644 index 00000000..92e12f56 --- /dev/null +++ b/src/JustEat.StatsD.Tests/EndpointLookups/DnsLookupIpEndpointSourceTests.cs @@ -0,0 +1,26 @@ +using System.Net; +using System.Net.Sockets; +using Shouldly; +using Xunit; + +namespace JustEat.StatsD.EndpointLookups +{ + public static class DnsLookupIpEndpointSourceTests + { + [Fact] + public static void GetEndpointPrefersIPV4WhenHostnameIsLocalhost() + { + // Arrange + var target = new DnsLookupIpEndpointSource("localhost", 8125); + + // Act + IPEndPoint actual = target.GetEndpoint(); + + // Assert + actual.ShouldNotBeNull(); + actual.AddressFamily.ShouldBe(AddressFamily.InterNetwork); + actual.Address.ShouldBe(IPAddress.Parse("127.0.0.1")); + actual.Port.ShouldBe(8125); + } + } +} diff --git a/src/JustEat.StatsD.Tests/IntegrationTests.cs b/src/JustEat.StatsD.Tests/IntegrationTests.cs new file mode 100644 index 00000000..702fb8c3 --- /dev/null +++ b/src/JustEat.StatsD.Tests/IntegrationTests.cs @@ -0,0 +1,106 @@ +using System; +using System.Net.Sockets; +using System.Text; +using System.Threading.Tasks; +using Newtonsoft.Json.Linq; +using Shouldly; +using Xunit; + +namespace JustEat.StatsD +{ + public static class IntegrationTests + { + [SkippableFact] + public static async Task Can_Send_Metrics_To_StatsD() + { + Skip.If(Environment.GetEnvironmentVariable("CI") == null, "By default, this test is only run during continuous integration."); + + // Arrange + var config = new StatsDConfiguration + { + Host = "localhost", + Prefix = Guid.NewGuid().ToString().Replace("-", string.Empty) + }; + + var publisher = new StatsDPublisher(config); + + // Act - Create a counter + publisher.Increment("apple"); + + // Act - Create and change a counter + publisher.Increment("bear"); // 1 + publisher.Increment(10, "bear"); // 11 + publisher.Increment(10, 0, "bear"); // 11 + publisher.Decrement("bear"); // 10 + publisher.Decrement(5, "bear"); // 5 + publisher.Decrement(5, 0, "bear"); // 5 + + // Act - Mark an event (which is a counter) + publisher.MarkEvent("fish"); + + // Act - Create a gauge + publisher.Gauge(3.141, "circle"); + + // Act - Create and change a gauge + publisher.Gauge(10, "dog"); + publisher.Gauge(42, "dog"); + + // Act - Create a timer + publisher.Timing(123, "elephant"); + publisher.Timing(TimeSpan.FromSeconds(2), "fox"); + publisher.Timing(456, 1, "goose"); + publisher.Timing(TimeSpan.FromSeconds(3.5), 1, "hen"); + + // Act - Increment multiple counters + publisher.Increment(7, 1, "green", "red"); // 7 + publisher.Increment(2, 0, "green", "red"); // 7 + publisher.Decrement(1, 0, "red", "green"); // 7 + publisher.Decrement(4, 1, "red", "green"); // 3 + + // Assert + var result = await SendCommandAsync("counters"); + result.Value(config.Prefix + ".apple").ShouldBe(1); + result.Value(config.Prefix + ".bear").ShouldBe(5); + result.Value(config.Prefix + ".fish").ShouldBe(1); + result.Value(config.Prefix + ".green").ShouldBe(3); + result.Value(config.Prefix + ".red").ShouldBe(3); + + result = await SendCommandAsync("gauges"); + result.Value(config.Prefix + ".circle").ShouldBe(3.141); + result.Value(config.Prefix + ".dog").ShouldBe(42); + + result = await SendCommandAsync("timers"); + result[config.Prefix + ".elephant"].Values().ShouldBe(new[] { 123 }); + result[config.Prefix + ".fox"].Values().ShouldBe(new[] { 2000 }); + result[config.Prefix + ".goose"].Values().ShouldBe(new[] { 456 }); + result[config.Prefix + ".hen"].Values().ShouldBe(new[] { 3500 }); + } + + private static async Task SendCommandAsync(string command) + { + string json; + + using (var client = new TcpClient()) + { + client.Connect("localhost", 8126); + + byte[] input = Encoding.UTF8.GetBytes(command); + byte[] output = new byte[client.ReceiveBufferSize]; + + int bytesRead; + + using (var stream = client.GetStream()) + { + await stream.WriteAsync(input); + bytesRead = await stream.ReadAsync(output); + } + + output = output.AsSpan(0, bytesRead).ToArray(); + + json = Encoding.UTF8.GetString(output).Replace("END", string.Empty); + } + + return JObject.Parse(json); + } + } +} diff --git a/src/JustEat.StatsD.Tests/JustEat.StatsD.Tests.csproj b/src/JustEat.StatsD.Tests/JustEat.StatsD.Tests.csproj index ae97cea8..9f4d1f8c 100644 --- a/src/JustEat.StatsD.Tests/JustEat.StatsD.Tests.csproj +++ b/src/JustEat.StatsD.Tests/JustEat.StatsD.Tests.csproj @@ -16,6 +16,7 @@ + diff --git a/src/JustEat.StatsD.Tests/StringBasedStatsDPublisherTests.cs b/src/JustEat.StatsD.Tests/StringBasedStatsDPublisherTests.cs new file mode 100644 index 00000000..d716dfbc --- /dev/null +++ b/src/JustEat.StatsD.Tests/StringBasedStatsDPublisherTests.cs @@ -0,0 +1,89 @@ +using System; +using Moq; +using Xunit; + +namespace JustEat.StatsD +{ + public static class StringBasedStatsDPublisherTests + { + [Fact] + public static void Decrement_Sends_Multiple_Metrics() + { + // Arrange + var mock = new Mock(); + + var config = new StatsDConfiguration + { + Prefix = "red", + }; + + var publisher = new StringBasedStatsDPublisher(config, mock.Object); + + // Act + publisher.Decrement(10, 1, "white", "blue"); + + // Assert + mock.Verify((p) => p.Send("red.white:-10|c"), Times.Once()); + mock.Verify((p) => p.Send("red.blue:-10|c"), Times.Once()); + } + + [Fact] + public static void Increment_Sends_Multiple_Metrics() + { + // Arrange + var mock = new Mock(); + + var config = new StatsDConfiguration + { + Prefix = "red", + }; + + var publisher = new StringBasedStatsDPublisher(config, mock.Object); + + // Act + publisher.Increment(10, 1, "white", "blue"); + + // Assert + mock.Verify((p) => p.Send("red.white:10|c"), Times.Once()); + mock.Verify((p) => p.Send("red.blue:10|c"), Times.Once()); + } + + [Fact] + public static void Metrics_Not_Sent_If_Array_Is_Null_Or_Empty() + { + // Arrange + var mock = new Mock(); + var config = new StatsDConfiguration(); + + var publisher = new StringBasedStatsDPublisher(config, mock.Object); + + // Act + publisher.Decrement(1, 1, null as string[]); + publisher.Increment(1, 1, null as string[]); + publisher.Decrement(1, 1, Array.Empty()); + publisher.Increment(1, 1, Array.Empty()); + publisher.Decrement(1, 1, new[] { string.Empty }); + publisher.Increment(1, 1, new[] { string.Empty }); + + // Assert + mock.Verify((p) => p.Send(It.IsAny()), Times.Never()); + } + + [Fact] + public static void Metrics_Not_Sent_If_No_Metrics() + { + // Arrange + var mock = new Mock(); + var config = new StatsDConfiguration(); + + var publisher = new StringBasedStatsDPublisher(config, mock.Object); + + // Act + publisher.Decrement(1, 0, new[] { "foo" }); + publisher.Increment(1, 0, new[] { "bar" }); + + // Assert + mock.Verify((p) => p.Send(It.IsAny()), Times.Never()); + } + } +} diff --git a/src/JustEat.StatsD.Tests/WhenUsingPooledUdpTransport.cs b/src/JustEat.StatsD.Tests/WhenUsingPooledUdpTransport.cs index 91e2e51e..a76f5c16 100644 --- a/src/JustEat.StatsD.Tests/WhenUsingPooledUdpTransport.cs +++ b/src/JustEat.StatsD.Tests/WhenUsingPooledUdpTransport.cs @@ -24,7 +24,7 @@ public void AMetricCanBeSentWithoutAnExceptionBeingThrown() using (var target = new PooledUdpTransport(endPointSource)) { // Act and Assert - target.Send("mycustommetric"); + target.Send("mycustommetric:1|c"); } } @@ -41,7 +41,7 @@ public void MultipleMetricsCanBeSentWithoutAnExceptionBeingThrownSerial() for (int i = 0; i < 10_000; i++) { // Act and Assert - target.Send("mycustommetric"); + target.Send("mycustommetric:1|c"); } } } @@ -59,7 +59,7 @@ public void MultipleMetricsCanBeSentWithoutAnExceptionBeingThrownParallel() Parallel.For(0, 10_000, _ => { // Act and Assert - target.Send("mycustommetric"); + target.Send("mycustommetric:1|c"); }); } } @@ -81,7 +81,7 @@ public static void EndpointSwitchShouldNotCauseExceptionsSequential() for (int i = 0; i < 10_000; i++) { // Act and Assert - target.Send("mycustommetric"); + target.Send("mycustommetric:1|c"); } } } @@ -103,7 +103,7 @@ public static void EndpointSwitchShouldNotCauseExceptionsParallel() Parallel.For(0, 10_000, _ => { // Act and Assert - target.Send("mycustommetric"); + target.Send("mycustommetric:1|c"); }); } } diff --git a/src/JustEat.StatsD.Tests/WhenUsingUdpTransport.cs b/src/JustEat.StatsD.Tests/WhenUsingUdpTransport.cs index 3d039497..a570a3b3 100644 --- a/src/JustEat.StatsD.Tests/WhenUsingUdpTransport.cs +++ b/src/JustEat.StatsD.Tests/WhenUsingUdpTransport.cs @@ -14,7 +14,7 @@ public static void AMetricCanBeSentWithoutAnExceptionBeingThrown() var target = new UdpTransport(endPointSource); // Act and Assert - target.Send("mycustommetric"); + target.Send("mycustommetric:1|c"); } [Fact] @@ -28,7 +28,7 @@ public static void MultipleMetricsCanBeSentWithoutAnExceptionBeingThrownSerial() for (int i = 0; i < 10_000; i++) { // Act and Assert - target.Send("mycustommetric"); + target.Send("mycustommetric:1|c"); } } @@ -42,7 +42,7 @@ public static void MultipleMetricsCanBeSentWithoutAnExceptionBeingThrownParallel Parallel.For(0, 10_000, (_) => { - target.Send("mycustommetric"); + target.Send("mycustommetric:1|c"); }); } } diff --git a/src/JustEat.StatsD.Tests/statsdconfig.js b/src/JustEat.StatsD.Tests/statsdconfig.js new file mode 100644 index 00000000..3dd800e5 --- /dev/null +++ b/src/JustEat.StatsD.Tests/statsdconfig.js @@ -0,0 +1,5 @@ +{ + graphiteHost: "", + port: 8125, + backends: [] +} diff --git a/src/JustEat.StatsD/EndpointLookups/DnsLookupIpEndpointSource.cs b/src/JustEat.StatsD/EndpointLookups/DnsLookupIpEndpointSource.cs index 8d38805c..0ed3d2e9 100644 --- a/src/JustEat.StatsD/EndpointLookups/DnsLookupIpEndpointSource.cs +++ b/src/JustEat.StatsD/EndpointLookups/DnsLookupIpEndpointSource.cs @@ -1,5 +1,7 @@ -using System; +using System; +using System.Linq; using System.Net; +using System.Net.Sockets; namespace JustEat.StatsD.EndpointLookups { @@ -31,7 +33,20 @@ private static IPAddress GetIpAddressOfHost(string hostName) { throw new Exception($"DNS did not find any addresses for statsd host '${hostName}'"); } - return endpoints[0]; + + IPAddress result = null; + + if (endpoints.Length > 1) + { + result = endpoints.FirstOrDefault(p => p.AddressFamily == AddressFamily.InterNetwork); + } + + if (result == null) + { + result = endpoints[0]; + } + + return result; } } -} \ No newline at end of file +} diff --git a/src/JustEat.StatsD/StatsDMessageFormatter.cs b/src/JustEat.StatsD/StatsDMessageFormatter.cs index 28b047f0..7d0d4702 100644 --- a/src/JustEat.StatsD/StatsDMessageFormatter.cs +++ b/src/JustEat.StatsD/StatsDMessageFormatter.cs @@ -16,15 +16,17 @@ public class StatsDMessageFormatter private readonly string _prefix; public StatsDMessageFormatter() - : this(string.Empty) {} + : this(string.Empty) + { + } - public StatsDMessageFormatter(string prefix) + public StatsDMessageFormatter(string prefix) { _prefix = prefix; if (!string.IsNullOrWhiteSpace(_prefix)) { - _prefix = _prefix + "."; // if we have something, then append a . to it to make concatenations easy. + _prefix = _prefix + "."; // If we have something, then append a '.' to it to make concatenations easy. } } @@ -51,7 +53,6 @@ public string Decrement(long magnitude, string statBucket) return Decrement(magnitude, DefaultSampleRate, statBucket); } - public string Decrement(long magnitude, double sampleRate, string statBucket) { magnitude = magnitude < 0 ? magnitude : -magnitude; diff --git a/src/JustEat.StatsD/StringBasedStatsDPublisher.cs b/src/JustEat.StatsD/StringBasedStatsDPublisher.cs index 12a2186f..9500fcf0 100644 --- a/src/JustEat.StatsD/StringBasedStatsDPublisher.cs +++ b/src/JustEat.StatsD/StringBasedStatsDPublisher.cs @@ -59,7 +59,18 @@ public void Increment(long value, double sampleRate, string bucket) public void Increment(long value, double sampleRate, params string[] buckets) { - Send(_formatter.Increment(value, sampleRate, buckets)); + if (buckets == null || buckets.Length == 0) + { + return; + } + + foreach (string bucket in buckets) + { + if (!string.IsNullOrEmpty(bucket)) + { + Send(_formatter.Increment(value, sampleRate, bucket)); + } + } } public void Decrement(string bucket) @@ -79,7 +90,18 @@ public void Decrement(long value, double sampleRate, string bucket) public void Decrement(long value, double sampleRate, params string[] buckets) { - Send(_formatter.Decrement(value, sampleRate, buckets)); + if (buckets == null || buckets.Length == 0) + { + return; + } + + foreach (string bucket in buckets) + { + if (!string.IsNullOrEmpty(bucket)) + { + Send(_formatter.Decrement(value, sampleRate, bucket)); + } + } } public void Gauge(double value, string bucket) @@ -129,6 +151,11 @@ public void MarkEvent(string name) private void Send(string metric) { + if (metric.Length == 0) + { + return; + } + try { _transport.Send(metric); diff --git a/version.props b/version.props index 80d89ed5..58d4f9ff 100644 --- a/version.props +++ b/version.props @@ -1,7 +1,7 @@ $(MSBuildAllProjects);$(MSBuildThisFileFullPath) - 3.2.1 + 3.2.2 $(APPVEYOR_BUILD_NUMBER)