├── global.json ├── src ├── CommonAssemblyInfo.cs ├── JustEat.StatsD │ ├── IDisposableTimer.cs │ ├── EndpointLookups │ │ ├── IDnsEndpointMapper.cs │ │ ├── DnsEndpointProvider.cs │ │ └── CachedDnsEndpointMapper.cs │ ├── IStatsDUdpClient.cs │ ├── DateTimeExtensions.cs │ ├── IStatsDPublisher.cs │ ├── JustEat.StatsD.xproj │ ├── TimerExtensions.cs │ ├── Properties │ │ └── AssemblyInfo.cs │ ├── PoolAwareSocketAsyncEventArgs.cs │ ├── DisposableTimer.cs │ ├── JustEat.StatsD.v2.ncrunchproject │ ├── project.json │ ├── SimpleObjectPool.cs │ ├── PacketBuilder.cs │ ├── StatsDImmediatePublisher.cs │ ├── StatsDUdpClient.cs │ └── StatsDMessageFormatter.cs ├── PerfTestHarness │ ├── project.json │ ├── Program.cs │ ├── Properties │ │ └── AssemblyInfo.cs │ └── PerfTestHarness.xproj └── JustEat.StatsD.Tests │ ├── packages.config │ ├── PacketBuilderTests.cs │ ├── project.json │ ├── WhenSendingMetricsToStatsD.cs │ ├── Extensions │ ├── PublisherAssertions.cs │ ├── FakeStatsPublisher.cs │ ├── SimpleTimerStatNameTests.cs │ └── ExtensionsTests.cs │ ├── app.config │ ├── JustEat.StatsD.Tests.xproj │ ├── JustEat.StatsD.Tests.v2.ncrunchproject │ ├── Properties │ └── AssemblyInfo.cs │ ├── EndpointLookups │ └── CachedDnsEndpointMapper │ │ └── WhenSendingMetricsToStatsD.cs │ ├── WhenTestingTimers.cs │ ├── WhenTestingGauges.cs │ └── WhenTestingCounters.cs ├── .gitignore ├── deploy └── manifest.json ├── .editorconfig ├── project.sublime-project ├── LICENSE ├── .travis.yml ├── JustEat.StatsD.v2.ncrunchsolution ├── appveyor.yml ├── .gitattributes ├── CONTRIBUTING.md ├── release.ps1 ├── CHANGELOG.md ├── JustEat.StatsD.sln ├── README.md └── Build.ps1 /global.json: -------------------------------------------------------------------------------- 1 | { 2 | "projects": [ "src" ], 3 | "sdk": { 4 | "version": "1.0.0-preview2-003121" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/CommonAssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | 3 | [assembly: AssemblyDescription("JUST EAT statsd metrics-publishing")] 4 | [assembly: AssemblyCompany("JUST EAT")] 5 | [assembly: AssemblyInformationalVersion("1.0.0.0")] 6 | -------------------------------------------------------------------------------- /src/JustEat.StatsD/IDisposableTimer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace JustEat.StatsD 4 | { 5 | public interface IDisposableTimer : IDisposable 6 | { 7 | string StatName { get; set; } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/JustEat.StatsD/EndpointLookups/IDnsEndpointMapper.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | 3 | namespace JustEat.StatsD.EndpointLookups 4 | { 5 | public interface IDnsEndpointMapper 6 | { 7 | IPEndPoint GetIPEndPoint(string hostName, int port); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.cache 2 | *.log 3 | *.orig 4 | *.sublime-workspace 5 | *.suo 6 | *.user 7 | *_NCrunch* 8 | *_ReSharper* 9 | _ReSharper* 10 | .dotnetcli 11 | .vs 12 | artifacts 13 | bin 14 | obj 15 | out 16 | packages 17 | project.lock.json 18 | TestResult.xml 19 | !3rdparty/*/bin 20 | lint.db 21 | -------------------------------------------------------------------------------- /deploy/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "version": 1 4 | }, 5 | "feature": { 6 | "name": "justeat-statsd", 7 | "role": "library" 8 | }, 9 | "build": { 10 | "solution": "JustEat.StatsD.sln", 11 | "mainproject": "JustEat.StatsD\\JustEat.StatsD.csproj" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/JustEat.StatsD/IStatsDUdpClient.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace JustEat.StatsD 5 | { 6 | public interface IStatsDUdpClient : IDisposable 7 | { 8 | bool Send(string metric); 9 | bool Send(IEnumerable metrics); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | end_of_line = crlf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | indent_size = 2 11 | 12 | [*.cs] 13 | indent_size = 4 14 | 15 | [*.lock] 16 | end_of_line = lf 17 | 18 | [*.sln] 19 | indent_style = tab 20 | -------------------------------------------------------------------------------- /src/PerfTestHarness/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.1.0-*", 3 | 4 | "buildOptions": { 5 | "debugType": "portable", 6 | "emitEntryPoint": true 7 | }, 8 | 9 | "dependencies": { 10 | "JustEat.StatsD": "1.1.0-*" 11 | }, 12 | 13 | "frameworks": { 14 | "netcoreapp1.0": { 15 | "dependencies": { 16 | "Microsoft.NETCore.App": { 17 | "type": "platform", 18 | "version": "1.0.0" 19 | } 20 | }, 21 | "imports": "dnxcore50" 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/JustEat.StatsD/EndpointLookups/DnsEndpointProvider.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | 3 | namespace JustEat.StatsD.EndpointLookups 4 | { 5 | /// 6 | /// Base endpoint provider. 7 | /// Basically an adapter around IpEndpoint creation. 8 | /// 9 | public class DnsEndpointProvider : IDnsEndpointMapper 10 | { 11 | public IPEndPoint GetIPEndPoint(string hostName, int port) 12 | { 13 | var endpoints = Dns.GetHostAddressesAsync(hostName).Result; 14 | return new IPEndPoint(endpoints[0], port); 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /project.sublime-project: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [{ 3 | "path": ".", 4 | "file_exclude_patterns": [ 5 | "*.bin", 6 | "*.chm", 7 | "*.dll", 8 | "*.exe", 9 | "*.lex", 10 | "*.log", 11 | "*.orig", 12 | "*.user" 13 | ], 14 | "folder_exclude_patterns": [ 15 | "_NCrunch*", 16 | "_ReSharper*", 17 | "bin", 18 | "Debug", 19 | "logs", 20 | "obj", 21 | "out", 22 | "Release", 23 | "Source" 24 | ] 25 | }], 26 | "settings": { 27 | "tab_size": 2, 28 | "translate_tabs_to_spaces": true 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/JustEat.StatsD.Tests/packages.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2015 JUST EAT plc 2 | 3 | The JustEat.StatsD library is licensed under the 4 | Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | -------------------------------------------------------------------------------- /src/JustEat.StatsD.Tests/PacketBuilderTests.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using NUnit.Framework; 3 | 4 | namespace JustEat.StatsD.Tests 5 | { 6 | [TestFixture] 7 | public class WhenBuildingPackets 8 | { 9 | private byte[][] _bytes; 10 | 11 | [OneTimeSetUp] 12 | protected void SetBytes() 13 | { 14 | _bytes = new[] {Enumerable.Repeat("a", 512).ToString()}.ToMaximumBytePackets().ToArray(); 15 | } 16 | 17 | [Test] 18 | public void TheMetricShouldGetSent() 19 | { 20 | Assert.That(_bytes[0].Length, Is.InRange(1, 512)); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | dist: trusty 3 | 4 | env: 5 | global: 6 | - CLI_VERSION=1.0.0-preview2-003121 7 | - DOTNET_SKIP_FIRST_TIME_EXPERIENCE=true 8 | - NUGET_XMLDOC_MODE=skip 9 | 10 | branches: 11 | only: 12 | - master 13 | 14 | addons: 15 | apt: 16 | packages: 17 | - gettext 18 | - libcurl4-openssl-dev 19 | - libicu-dev 20 | - libssl-dev 21 | - libunwind8 22 | 23 | install: 24 | - export DOTNET_INSTALL_DIR="$PWD/.dotnetcli" 25 | - curl -sSL https://raw.githubusercontent.com/dotnet/cli/rel/1.0.0/scripts/obtain/dotnet-install.sh | bash /dev/stdin --version "$CLI_VERSION" --install-dir "$DOTNET_INSTALL_DIR" 26 | - export PATH="$DOTNET_INSTALL_DIR:$PATH" 27 | 28 | script: 29 | - ./build.sh 30 | -------------------------------------------------------------------------------- /src/JustEat.StatsD.Tests/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "authors": [ "JUST EAT" ], 3 | "copyright": "Copyright JUST EAT 2016", 4 | "title": "JustEat.StatsD.Tests", 5 | "version": "1.1.0-*", 6 | 7 | "buildOptions": { 8 | "warningsAsErrors": true, 9 | "xmlDoc": false 10 | }, 11 | 12 | "dependencies": { 13 | "AutoFixture": "3.50.2", 14 | "AutoFixture.AutoFakeItEasy": "3.50.2", 15 | "dotnet-test-nunit": "3.4.0-beta-3", 16 | "FakeItEasy": "1.25.3", 17 | "JustBehave": "1.0.0.12", 18 | "JustEat.StatsD": "1.1.0-*", 19 | "NLog": "5.0.0-beta03", 20 | "NUnit": "3.5.0", 21 | "Shouldly": "2.6.0" 22 | }, 23 | 24 | "frameworks": { 25 | "net452": { 26 | "dependencies": { 27 | "Microsoft.NETCore.Platforms": "1.0.1" 28 | } 29 | } 30 | }, 31 | 32 | "testRunner": "nunit" 33 | } 34 | -------------------------------------------------------------------------------- /src/JustEat.StatsD/DateTimeExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace JustEat.StatsD 4 | { 5 | /// 6 | /// Provides a simple set of extension on DateTime. 7 | /// 8 | public static class DateTimeExtensions 9 | { 10 | private static readonly DateTime Epoch = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc); 11 | 12 | /// Returns the DateTime as the number of seconds since the epoch (1970), which is Unix time. 13 | /// The dateTime to act on. 14 | /// A number of seconds since the epoch. 15 | public static double AsUnixTime(this DateTime dateTime) 16 | { 17 | return Math.Round(dateTime.ToUniversalTime().Subtract(Epoch).TotalSeconds); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /JustEat.StatsD.v2.ncrunchsolution: -------------------------------------------------------------------------------- 1 | 2 | 3 | 1 4 | false 5 | true 6 | true 7 | UseDynamicAnalysis 8 | UseStaticAnalysis 9 | UseStaticAnalysis 10 | UseStaticAnalysis 11 | UseDynamicAnalysis 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/JustEat.StatsD/IStatsDPublisher.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace JustEat.StatsD 4 | { 5 | public interface IStatsDPublisher : IDisposable 6 | { 7 | void Increment(string bucket); 8 | void Increment(long value, string bucket); 9 | void Increment(long value, double sampleRate, string bucket); 10 | void Increment(long value, double sampleRate, params string[] buckets); 11 | void Decrement(string bucket); 12 | void Decrement(long value, string bucket); 13 | void Decrement(long value, double sampleRate, string bucket); 14 | void Decrement(long value, double sampleRate, params string[] buckets); 15 | void Gauge(long value, string bucket); 16 | void Gauge(long value, string bucket, DateTime timestamp); 17 | void Timing(TimeSpan duration, string bucket); 18 | void Timing(TimeSpan duration, double sampleRate, string bucket); 19 | void MarkEvent(string name); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | os: Visual Studio 2015 2 | version: 1.1.0.{build} 3 | configuration: Release 4 | assembly_info: 5 | patch: true 6 | file: '**\*AssemblyInfo.*' 7 | assembly_version: 1.1.0 8 | assembly_file_version: '{version}' 9 | assembly_informational_version: '{version}' 10 | environment: 11 | CLI_VERSION: 1.0.0-preview2-003121 12 | DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true 13 | NUGET_XMLDOC_MODE: skip 14 | build_script: 15 | - ps: dotnet --info 16 | - ps: .\Build.ps1 17 | artifacts: 18 | - path: '**\$(APPVEYOR_PROJECT_NAME)*.nupkg' 19 | name: Nuget 20 | deploy: 21 | - provider: NuGet 22 | api_key: 23 | secure: 6MzbzEs4YdJKS67Gio5gEO8mNKmwfC4UHTCmECZ1KOutI6ndm4vAECazmVNB6an7 24 | artifact: /.*nupkg/ 25 | on: 26 | APPVEYOR_REPO_TAG: true 27 | notifications: 28 | - provider: HipChat 29 | room: 'Eng :: Open Source' 30 | auth_token: 31 | secure: eJWABMRPoyfEF9iLzFaTcUEqTc7/64v0FtS1qQe4yhs= 32 | on_build_success: false 33 | on_build_failure: false 34 | on_build_status_changed: false 35 | test: off 36 | -------------------------------------------------------------------------------- /src/PerfTestHarness/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using JustEat.StatsD; 6 | 7 | namespace PerfTestHarness 8 | { 9 | internal class Program 10 | { 11 | private static void Main(string[] args) 12 | { 13 | var iterations = Enumerable.Range(1, 500000); 14 | var client = new StatsDUdpClient(10, "localhost", 3128); 15 | var formatter = new StatsDMessageFormatter(); 16 | var watch = new Stopwatch(); 17 | 18 | Console.WriteLine("To start - hit ENTER."); 19 | Console.ReadLine(); 20 | Console.WriteLine("start"); 21 | watch.Start(); 22 | 23 | Parallel.ForEach(iterations, x => client.Send(formatter.Gauge(x, "bucket_sample" + "number-of-messages-to-be-sent"))); 24 | 25 | watch.Stop(); 26 | Console.WriteLine("end - " + watch.ElapsedMilliseconds); 27 | Console.ReadLine(); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Set default behaviour, in case users don't have core.autocrlf set. 2 | * text=auto 3 | 4 | # Explicitly declare text files we want to always be normalized and converted 5 | # to native line endings on checkout. 6 | *.cs text 7 | *.cshtml text 8 | *.csproj text 9 | *.dotsettings text 10 | *.erb text 11 | *.feature text 12 | *.fxcop text 13 | *.html text 14 | *.js text 15 | *.json text 16 | *.kproj text 17 | *.markdown text 18 | *.md text 19 | *.ncrunchproject text 20 | *.ncrunchsolution text 21 | *.ndepend text 22 | *.ndproj text 23 | *.rake text 24 | *.razor text 25 | *.rb text 26 | *.sublime-project text 27 | *.targets text 28 | *.txt text 29 | *.xml text 30 | *.xss text 31 | *.xslt text 32 | *.xsd text 33 | *.yaml text 34 | *.yml text 35 | 36 | # Declare files that will always have CRLF line endings on checkout. 37 | *.sln text eol=crlf 38 | 39 | *.DotSettings text eol=crlf 40 | *.lock text eol=lf 41 | 42 | # Denote all files that are truly binary and should not be modified. 43 | *.exe binary 44 | *.gif binary 45 | *.jpeg binary 46 | *.jpg binary 47 | *.png binary 48 | -------------------------------------------------------------------------------- /src/JustEat.StatsD.Tests/WhenSendingMetricsToStatsD.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using FakeItEasy; 3 | using FakeItEasy.ExtensionSyntax.Full; 4 | using JustBehave; 5 | using NUnit.Framework; 6 | using Ploeh.AutoFixture; 7 | using Shouldly; 8 | 9 | namespace JustEat.StatsD.Tests 10 | { 11 | [Ignore("The reason for this test being ignored is lost in the mists of time.")] 12 | public class WhenSendingMetricsToStatsD : BehaviourTest 13 | { 14 | private IEnumerable _metricToSend; 15 | private bool _result; 16 | 17 | protected override void Given() 18 | { 19 | _metricToSend = new string[1] {"test-bucket:100|c"}; 20 | Fixture.Freeze().CallsTo(x => x.Send(A>._)).Returns(true); 21 | } 22 | 23 | protected override void When() 24 | { 25 | _result = SystemUnderTest.Send(_metricToSend); 26 | SystemUnderTest.Dispose(); 27 | } 28 | 29 | [Then] 30 | public void TheMetricShouldGetSent() 31 | { 32 | _result.ShouldBe(true); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | The easier your PRs are to review and merge, the more likely your contribution will be accepted. :-) 4 | 5 | ## Develop 6 | 7 | * Work in a feature branch in a fork, PR to our master 8 | * One logical change per PR, please - do refactorings in separate PRs, ahead of your feature change(s) 9 | * Have [editorconfig plugin](http://editorconfig.org) for your editor(s) installed so that your file touches are consistent with ours and the diff is reduced. 10 | * Test coverage should not go down 11 | * Flag breaking changes in your PR description 12 | * Add a comment linking to passing tests in CI, proof in Kibana dashboards ("share temporary"), etc 13 | * Link to any specifications / JIRAs that you're working against if applicable 14 | * CI should be green! 15 | 16 | ## Releases 17 | 18 | * CI should be green on master 19 | * Bump the version number in CI - follow [SemVer rules](http://semver.org) 20 | * Bump the version in appveyor.yml to match 21 | * Update the CHANGELOG.md 22 | * Run `release.ps1` 23 | ```powershell 24 | ./release.ps1 -version 1.2.3 25 | ``` 26 | * CI should 27 | * build the tag 28 | * push nuget packages to nuget.org 29 | -------------------------------------------------------------------------------- /src/JustEat.StatsD.Tests/Extensions/PublisherAssertions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using NUnit.Framework; 3 | 4 | namespace JustEat.StatsD.Tests.Extensions 5 | { 6 | public static class PublisherAssertions 7 | { 8 | public static void SingleStatNameIs(FakeStatsPublisher publisher, string statName) 9 | { 10 | Assert.That(publisher.CallCount, Is.EqualTo(1)); 11 | Assert.That(publisher.DisposeCount, Is.EqualTo(0)); 12 | 13 | Assert.That(publisher.BucketNames.Count, Is.EqualTo(1)); 14 | Assert.That(publisher.BucketNames[0], Is.EqualTo(statName)); 15 | } 16 | 17 | public static void LastDurationIs(FakeStatsPublisher publisher, int expectedMillis) 18 | { 19 | DurationIsMoreOrLess(publisher.LastDuration, TimeSpan.FromMilliseconds(expectedMillis)); 20 | } 21 | 22 | private static void DurationIsMoreOrLess(TimeSpan expected, TimeSpan actual) 23 | { 24 | TimeSpan delta = TimeSpan.FromMilliseconds(100); 25 | 26 | var expectedLower = expected.Subtract(delta); 27 | var expectedUpper = expected.Add(delta); 28 | 29 | Assert.That(actual, Is.GreaterThanOrEqualTo(expectedLower)); 30 | Assert.That(actual, Is.LessThanOrEqualTo(expectedUpper)); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/JustEat.StatsD.Tests/app.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/JustEat.StatsD/JustEat.StatsD.xproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 14.0 5 | $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) 6 | 7 | 8 | 9 | 6a338d71-e28b-455a-9e9d-3667ee659542 10 | JustEat.StatsD 11 | ..\..\artifacts\obj\$(MSBuildProjectName) 12 | .\bin\ 13 | 14 | 15 | 2.0 16 | 17 | 18 | True 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/JustEat.StatsD.Tests/JustEat.StatsD.Tests.xproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 14.0 5 | $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) 6 | 7 | 8 | 9 | 8d9aa1a3-2619-4800-aaec-84ae4da15455 10 | JustEat.StatsD 11 | ..\..\artifacts\obj\$(MSBuildProjectName) 12 | .\bin\ 13 | 14 | 15 | 2.0 16 | 17 | 18 | True 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/JustEat.StatsD.Tests/JustEat.StatsD.Tests.v2.ncrunchproject: -------------------------------------------------------------------------------- 1 | 2 | 3 | false 4 | false 5 | false 6 | true 7 | false 8 | false 9 | false 10 | false 11 | true 12 | true 13 | false 14 | true 15 | true 16 | 60000 17 | 18 | 19 | 20 | AutoDetect 21 | STA 22 | x86 23 | 24 | -------------------------------------------------------------------------------- /src/JustEat.StatsD/TimerExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | 4 | namespace JustEat.StatsD 5 | { 6 | public static class TimerExtensions 7 | { 8 | public static IDisposableTimer StartTimer(this IStatsDPublisher publisher, string bucket) 9 | { 10 | return new DisposableTimer(publisher, bucket); 11 | } 12 | 13 | public static void Time(this IStatsDPublisher publisher, string bucket, Action action) 14 | { 15 | using (StartTimer(publisher, bucket)) 16 | { 17 | action(); 18 | } 19 | } 20 | 21 | public static async Task Time(this IStatsDPublisher publisher, string bucket, Func action) 22 | { 23 | using (StartTimer(publisher, bucket)) 24 | { 25 | await action(); 26 | } 27 | } 28 | 29 | public static T Time(this IStatsDPublisher publisher, string bucket, Func func) 30 | { 31 | using (StartTimer(publisher, bucket)) 32 | { 33 | return func(); 34 | } 35 | } 36 | 37 | public static async Task Time(this IStatsDPublisher publisher, string bucket, Func> func) 38 | { 39 | using (StartTimer(publisher, bucket)) 40 | { 41 | return await func(); 42 | } 43 | } 44 | 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/JustEat.StatsD/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.InteropServices; 3 | 4 | // General Information about an assembly is controlled through the following 5 | // set of attributes. Change these attribute values to modify the information 6 | // associated with an assembly. 7 | [assembly: AssemblyTitle("JustEat.StatsD")] 8 | [assembly: AssemblyConfiguration("")] 9 | [assembly: AssemblyProduct("JustEat.StatsD")] 10 | [assembly: AssemblyCopyright("Copyright © JUST EAT 2016")] 11 | [assembly: AssemblyTrademark("")] 12 | [assembly: AssemblyCulture("")] 13 | 14 | // Setting ComVisible to false makes the types in this assembly not visible 15 | // to COM components. If you need to access a type in this assembly from 16 | // COM, set the ComVisible attribute to true on that type. 17 | [assembly: ComVisible(false)] 18 | 19 | // The following GUID is for the ID of the typelib if this project is exposed to COM 20 | [assembly: Guid("8f4ff09e-4130-4872-a50f-b290e9ccb04b")] 21 | 22 | // Version information for an assembly consists of the following four values: 23 | // 24 | // Major Version 25 | // Minor Version 26 | // Build Number 27 | // Revision 28 | // 29 | // You can specify all the values or you can default the Build and Revision Numbers 30 | // by using the '*' as shown below: 31 | // [assembly: AssemblyVersion("1.0.*")] 32 | [assembly: AssemblyVersion("1.1.0.0")] 33 | [assembly: AssemblyFileVersion("1.1.0.0")] 34 | -------------------------------------------------------------------------------- /src/PerfTestHarness/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.InteropServices; 3 | 4 | // General Information about an assembly is controlled through the following 5 | // set of attributes. Change these attribute values to modify the information 6 | // associated with an assembly. 7 | [assembly: AssemblyTitle("JustEat.StatsD")] 8 | [assembly: AssemblyConfiguration("")] 9 | [assembly: AssemblyProduct("JustEat.StatsD")] 10 | [assembly: AssemblyCopyright("Copyright © JUST EAT 2015")] 11 | [assembly: AssemblyTrademark("")] 12 | [assembly: AssemblyCulture("")] 13 | 14 | // Setting ComVisible to false makes the types in this assembly not visible 15 | // to COM components. If you need to access a type in this assembly from 16 | // COM, set the ComVisible attribute to true on that type. 17 | [assembly: ComVisible(false)] 18 | 19 | // The following GUID is for the ID of the typelib if this project is exposed to COM 20 | [assembly: Guid("8f4ff09e-4130-4872-a50f-b290e9ccb04b")] 21 | 22 | // Version information for an assembly consists of the following four values: 23 | // 24 | // Major Version 25 | // Minor Version 26 | // Build Number 27 | // Revision 28 | // 29 | // You can specify all the values or you can default the Build and Revision Numbers 30 | // by using the '*' as shown below: 31 | // [assembly: AssemblyVersion("1.0.*")] 32 | [assembly: AssemblyVersion("1.0.0.0")] 33 | [assembly: AssemblyFileVersion("1.0.0.0")] 34 | -------------------------------------------------------------------------------- /src/JustEat.StatsD.Tests/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.InteropServices; 3 | 4 | // General Information about an assembly is controlled through the following 5 | // set of attributes. Change these attribute values to modify the information 6 | // associated with an assembly. 7 | [assembly: AssemblyTitle("JustEat.StatsD.Tests")] 8 | [assembly: AssemblyConfiguration("")] 9 | [assembly: AssemblyProduct("JustEat.StatsD")] 10 | [assembly: AssemblyCopyright("Copyright © JUST EAT 2016")] 11 | [assembly: AssemblyTrademark("")] 12 | [assembly: AssemblyCulture("")] 13 | 14 | // Setting ComVisible to false makes the types in this assembly not visible 15 | // to COM components. If you need to access a type in this assembly from 16 | // COM, set the ComVisible attribute to true on that type. 17 | [assembly: ComVisible(false)] 18 | 19 | // The following GUID is for the ID of the typelib if this project is exposed to COM 20 | [assembly: Guid("8f4ff09e-4130-4872-a50f-b290e9ccb04b")] 21 | 22 | // Version information for an assembly consists of the following four values: 23 | // 24 | // Major Version 25 | // Minor Version 26 | // Build Number 27 | // Revision 28 | // 29 | // You can specify all the values or you can default the Build and Revision Numbers 30 | // by using the '*' as shown below: 31 | // [assembly: AssemblyVersion("1.0.*")] 32 | [assembly: AssemblyVersion("1.1.0.0")] 33 | [assembly: AssemblyFileVersion("1.1.0.0")] 34 | -------------------------------------------------------------------------------- /src/JustEat.StatsD/PoolAwareSocketAsyncEventArgs.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net.Sockets; 3 | 4 | namespace JustEat.StatsD 5 | { 6 | /// 7 | /// A SocketAsyncEventArgs derived class that is aware that it needs to be returned to an object pool when OnCompleted has been called. 8 | /// 9 | public sealed class PoolAwareSocketAsyncEventArgs : SocketAsyncEventArgs 10 | { 11 | private readonly SimpleObjectPool _parentPool; 12 | 13 | /// Initializes a new instance of the PooledSocketAsyncEventArgs class. 14 | /// The pool that owns this instance. 15 | public PoolAwareSocketAsyncEventArgs(SimpleObjectPool parentPool) 16 | { 17 | if (null == parentPool) 18 | { 19 | throw new ArgumentNullException("parentPool"); 20 | } 21 | 22 | _parentPool = parentPool; 23 | } 24 | 25 | /// Represents a method that is called when an asynchronous operation completes. 26 | /// Adds the arguments back to the pool for future use. 27 | /// The event that is signaled. 28 | protected override void OnCompleted(SocketAsyncEventArgs e) 29 | { 30 | base.OnCompleted(e); 31 | _parentPool.Push(this); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/JustEat.StatsD/DisposableTimer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | 4 | namespace JustEat.StatsD 5 | { 6 | internal class DisposableTimer : IDisposableTimer 7 | { 8 | private bool _disposed; 9 | 10 | private IStatsDPublisher _publisher; 11 | private Stopwatch _stopwatch; 12 | 13 | public string StatName { get; set; } 14 | 15 | public DisposableTimer(IStatsDPublisher publisher, string statName) 16 | { 17 | if (publisher == null) 18 | { 19 | throw new ArgumentNullException("publisher"); 20 | } 21 | 22 | if (string.IsNullOrEmpty(statName)) 23 | { 24 | throw new ArgumentNullException("statName"); 25 | } 26 | 27 | _publisher = publisher; 28 | StatName = statName; 29 | _stopwatch = Stopwatch.StartNew(); 30 | } 31 | 32 | public void Dispose() 33 | { 34 | if (!_disposed) 35 | { 36 | _disposed = true; 37 | _stopwatch.Stop(); 38 | 39 | if (string.IsNullOrEmpty(StatName)) 40 | { 41 | throw new InvalidOperationException("StatName must be set"); 42 | } 43 | 44 | _publisher.Timing(_stopwatch.Elapsed, StatName); 45 | 46 | _stopwatch = null; 47 | _publisher = null; 48 | } 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/JustEat.StatsD/JustEat.StatsD.v2.ncrunchproject: -------------------------------------------------------------------------------- 1 | 2 | 3 | 1000 4 | false 5 | false 6 | false 7 | true 8 | false 9 | false 10 | false 11 | false 12 | false 13 | true 14 | true 15 | false 16 | true 17 | true 18 | true 19 | 60000 20 | 21 | 22 | 23 | AutoDetect 24 | STA 25 | x86 26 | 27 | -------------------------------------------------------------------------------- /src/JustEat.StatsD.Tests/EndpointLookups/CachedDnsEndpointMapper/WhenSendingMetricsToStatsD.cs: -------------------------------------------------------------------------------- 1 | using FakeItEasy; 2 | using FakeItEasy.ExtensionSyntax.Full; 3 | using JustBehave; 4 | using JustEat.StatsD.EndpointLookups; 5 | using NUnit.Framework; 6 | using Ploeh.AutoFixture; 7 | using Ploeh.AutoFixture.AutoFakeItEasy; 8 | 9 | namespace JustEat.StatsD.Tests.EndpointLookups.CachedDnsEndpointMapper 10 | { 11 | [TestFixture] 12 | public class WhenCacheIsEmpty : BehaviourTest 13 | { 14 | private int _port; 15 | private string _hostName; 16 | private IDnsEndpointMapper _mapper; 17 | 18 | protected override void CustomizeAutoFixture(IFixture fixture) 19 | { 20 | fixture.Customize(new AutoFakeItEasyCustomization()); 21 | base.CustomizeAutoFixture(fixture); 22 | } 23 | 24 | protected override void Given() 25 | { 26 | _port = 0; 27 | _hostName = "host"; 28 | _mapper = Fixture.Freeze(); 29 | _mapper.CallsTo(x => x.GetIPEndPoint(_hostName, _port)).Returns(null); 30 | } 31 | 32 | protected override void When() 33 | { 34 | SystemUnderTest.GetIPEndPoint(_hostName, _port); 35 | } 36 | 37 | [Then] 38 | public void EndpointIsProvidedByInnerService() 39 | { 40 | _mapper.CallsTo(x => x.GetIPEndPoint(_hostName, _port)).MustHaveHappened(); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/PerfTestHarness/PerfTestHarness.xproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 14.0 5 | $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) 6 | 7 | 8 | 9 | 8a601b6e-491c-4b97-92c3-75d399b0f023 10 | JustEat.StatsD 11 | ..\..\artifacts\obj\$(MSBuildProjectName) 12 | .\bin\ 13 | 14 | 15 | 2.0 16 | 17 | 18 | True 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /release.ps1: -------------------------------------------------------------------------------- 1 | param( 2 | [Parameter(Mandatory=$true, HelpMessage="The version number to publish, eg 1.2.3. Set this in CI first.")] 3 | [string] $version, 4 | [Parameter(Mandatory=$false, HelpMessage="CI project owner")] 5 | [string] $owner = "justeattech" 6 | ) 7 | 8 | if (($version -eq $null) -or ($version -eq '')) { 9 | # TODO: validate that a tag like this doesn't exist already 10 | throw "Must supply version number in semver format eg 1.2.3" 11 | } 12 | $manifest = get-content "deploy/manifest.json" -raw | ConvertFrom-Json 13 | $ci_name = "je-$($manifest.feature.name)" 14 | $ci_uri = "https://ci.appveyor.com/project/$owner/$ci_name" 15 | $tag = "v$version" 16 | # $release = "release-$version" 17 | write-host "Your current status" -foregroundcolor green 18 | & git status 19 | write-host "Stashing any work and checking out master" -foregroundcolor green 20 | & git stash 21 | & git checkout master 22 | & git pull upstream master --tags 23 | write-host "We'll pause now while you remember to bump the version number in appveyor.yml to match the version you're releasing ($version) ;-)" 24 | write-host " TODO: bounty - do this in code against appveyor's api" -foregroundcolor red 25 | write-host " http://www.appveyor.com/docs/api/projects-builds#update-project" -foregroundcolor red 26 | read-host "hit enter when you've done that..." 27 | write-host "Tagging & branching. tag: $tag / branch: $release" -foregroundcolor green 28 | & git tag -a $tag -m "Release $tag" 29 | & git checkout $tag 30 | write-host "Pushing" -foregroundcolor green 31 | & git push --tags upstream 32 | write-host "Done." 33 | write-host "Check $ci_uri" 34 | & git checkout master 35 | write-host "Putting you back on master branch" -foregroundcolor green 36 | exit 0 37 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 1.0.4 2 | ## Change 3 | 4 | Easy to use timers. Time a block of code with a `using` statement or time a lambda, with or without a return value. `async ... await` is also supported. 5 | 6 | usage: given an existing instance of `IStatsDPublisher` called `stats` you can do: 7 | 8 | ```csharp 9 | // timing a block of code in a using statement: 10 | using (stats.StartTimer("someStat")) 11 | { 12 | DoSomething(); 13 | } 14 | 15 | // timing a lambda without a return value: 16 | stats.Time("someStat", () => DoSomething()); 17 | 18 | // timing a lambda with a return value: 19 | var result = stats.Time("someStat", () => GetSomething()); 20 | 21 | // works with async 22 | using (stats.StartTimer("someStat")) 23 | { 24 | await DoSomethingAsync(); 25 | } 26 | 27 | // and correctly times async lambdas using the usual syntax: 28 | var result = await stats.Time("someStat", async () => await GetSomethingAsync()); 29 | 30 | ``` 31 | The idea of "disposable timers" comes from [this StatsD client](https://github.com/Pereingo/statsd-csharp-client). 32 | 33 | # 1.0.3 34 | ## Change 35 | Nuget metadata 36 | 37 | # 1.0.2 38 | ## Change 39 | * Move to Sys.Diag.Trace for logs to remove NLog dependency (so as to not force that on dependents) 40 | 41 | # 1.0.0 42 | ## Add 43 | * We've been in production with this for a while now. It deserves the 1.0 version tag. 44 | * Sort out references and dependencies such that nuget is relied upon. 45 | 46 | # 0.0.2 47 | ## Add 48 | * Ability to prefix each stat, so the prefix/namespace can be supplied one-time via configuration rather than built each time a stat is pushed. Eg, Mailman would want every stat to be prefixed with `{country}.mailman_{instance position}` 49 | 50 | # 0.0.1 51 | * First release 52 | -------------------------------------------------------------------------------- /src/JustEat.StatsD/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "authors": [ "JUST EAT" ], 3 | "copyright": "Copyright JUST EAT 2016", 4 | "title": "JustEat.StatsD", 5 | "version": "1.1.0-*", 6 | 7 | "buildOptions": { 8 | "emitEntryPoint": false, 9 | "warningsAsErrors": true, 10 | "xmlDoc": false 11 | }, 12 | 13 | "configurations": { 14 | "Debug": { 15 | "buildOptions": { 16 | "define": [ "DEBUG", "TRACE" ], 17 | "optimize": false 18 | } 19 | }, 20 | "Release": { 21 | "buildOptions": { 22 | "define": [ "RELEASE", "TRACE" ], 23 | "optimize": true 24 | } 25 | } 26 | }, 27 | 28 | "dependencies": { 29 | "Newtonsoft.Json": "9.0.1" 30 | }, 31 | 32 | "packOptions": { 33 | "iconUrl": "https://avatars3.githubusercontent.com/u/1516790?s=200", 34 | "licenseUrl": "https://github.com/justeat/JustEat.StatsD/LICENSE", 35 | "owners": [ "JUST EAT" ], 36 | "projectUrl": "https://github.com/justeat/JustEat.StatsD", 37 | "releaseNotes": "https://github.com/justeat/JustEat.StatsD/CHANGELOG.md", 38 | "repository": { 39 | "type": "git", 40 | "url": "https://github.com/justeat/JustEat.StatsD.git" 41 | }, 42 | "requireLicenseAcceptance": false, 43 | "tags": [ "statsd", "graphite", "metrics", "monitoring" ] 44 | }, 45 | 46 | "tooling": { 47 | "defaultNamespace": "JustEat.StatsD" 48 | }, 49 | 50 | "frameworks": { 51 | "net451": { 52 | }, 53 | "netstandard1.3": { 54 | "dependencies": { 55 | "System.Collections.Concurrent": "4.0.12", 56 | "System.Diagnostics.Tools": "4.0.1", 57 | "System.Diagnostics.TraceSource": "4.0.0", 58 | "System.Net.NameResolution": "4.0.0", 59 | "System.Net.Sockets": "4.1.0", 60 | "System.Runtime.InteropServices": "4.1.0" 61 | } 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/JustEat.StatsD/EndpointLookups/CachedDnsEndpointMapper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net; 3 | 4 | namespace JustEat.StatsD.EndpointLookups 5 | { 6 | /// 7 | /// Looks up an endpoint and holds the ip endpoint in memory for the specified duration (seconds) 8 | /// 9 | public class CachedDnsEndpointMapper : IDnsEndpointMapper 10 | { 11 | private readonly IDnsEndpointMapper _baseMapper; 12 | private readonly int _secondsCacheDuration; 13 | private CachedEndPoint _cachedEndPoint; 14 | 15 | /// 16 | /// 17 | /// Base endpoint provider 18 | /// Seconds to cache ip endpoints for 19 | public CachedDnsEndpointMapper(IDnsEndpointMapper baseMapper, int secondsCacheDuration) 20 | { 21 | _baseMapper = baseMapper; 22 | _secondsCacheDuration = secondsCacheDuration; 23 | } 24 | 25 | /// 26 | /// Get an IP Endpoint for the DNS host name 27 | /// 28 | /// 29 | /// 30 | /// 31 | public IPEndPoint GetIPEndPoint(string hostName, int port) 32 | { 33 | lock (this) 34 | { 35 | if (_cachedEndPoint == null || _cachedEndPoint.IsExpired(_secondsCacheDuration)) 36 | { 37 | _cachedEndPoint = new CachedEndPoint(_baseMapper.GetIPEndPoint(hostName, port)); 38 | } 39 | } 40 | return _cachedEndPoint.IpEndPoint; 41 | } 42 | 43 | #region Nested type: CachedEndPoint 44 | 45 | internal class CachedEndPoint 46 | { 47 | public CachedEndPoint(IPEndPoint endpoint) 48 | { 49 | IpEndPoint = endpoint; 50 | CreateTime = DateTime.UtcNow; 51 | } 52 | 53 | internal IPEndPoint IpEndPoint { get; private set; } 54 | internal DateTime CreateTime { get; private set; } 55 | 56 | public bool IsExpired(int secondsCacheDuration) 57 | { 58 | return CreateTime.AddSeconds(secondsCacheDuration) <= DateTime.UtcNow; 59 | } 60 | } 61 | 62 | #endregion 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /JustEat.StatsD.sln: -------------------------------------------------------------------------------- 1 | Microsoft Visual Studio Solution File, Format Version 12.00 2 | # Visual Studio 14 3 | VisualStudioVersion = 14.0.25420.1 4 | MinimumVisualStudioVersion = 10.0.40219.1 5 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Assets", "Assets", "{06BE4D4A-B0DF-465D-9BAC-6CC3C9779A37}" 6 | ProjectSection(SolutionItems) = preProject 7 | .editorconfig = .editorconfig 8 | .gitattributes = .gitattributes 9 | .gitignore = .gitignore 10 | .travis.yml = .travis.yml 11 | appveyor.yml = appveyor.yml 12 | Build.ps1 = Build.ps1 13 | build.sh = build.sh 14 | CHANGELOG.md = CHANGELOG.md 15 | CONTRIBUTING.md = CONTRIBUTING.md 16 | global.json = global.json 17 | LICENSE = LICENSE 18 | README.md = README.md 19 | release.ps1 = release.ps1 20 | EndProjectSection 21 | EndProject 22 | Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "JustEat.StatsD", "src\JustEat.StatsD\JustEat.StatsD.xproj", "{6A338D71-E28B-455A-9E9D-3667EE659542}" 23 | EndProject 24 | Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "PerfTestHarness", "src\PerfTestHarness\PerfTestHarness.xproj", "{8A601B6E-491C-4B97-92C3-75D399B0F023}" 25 | EndProject 26 | Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "JustEat.StatsD.Tests", "src\JustEat.StatsD.Tests\JustEat.StatsD.Tests.xproj", "{8D9AA1A3-2619-4800-AAEC-84AE4DA15455}" 27 | EndProject 28 | Global 29 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 30 | Debug|Any CPU = Debug|Any CPU 31 | Release|Any CPU = Release|Any CPU 32 | EndGlobalSection 33 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 34 | {6A338D71-E28B-455A-9E9D-3667EE659542}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 35 | {6A338D71-E28B-455A-9E9D-3667EE659542}.Debug|Any CPU.Build.0 = Debug|Any CPU 36 | {6A338D71-E28B-455A-9E9D-3667EE659542}.Release|Any CPU.ActiveCfg = Release|Any CPU 37 | {6A338D71-E28B-455A-9E9D-3667EE659542}.Release|Any CPU.Build.0 = Release|Any CPU 38 | {8A601B6E-491C-4B97-92C3-75D399B0F023}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 39 | {8A601B6E-491C-4B97-92C3-75D399B0F023}.Debug|Any CPU.Build.0 = Debug|Any CPU 40 | {8A601B6E-491C-4B97-92C3-75D399B0F023}.Release|Any CPU.ActiveCfg = Release|Any CPU 41 | {8A601B6E-491C-4B97-92C3-75D399B0F023}.Release|Any CPU.Build.0 = Release|Any CPU 42 | {8D9AA1A3-2619-4800-AAEC-84AE4DA15455}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 43 | {8D9AA1A3-2619-4800-AAEC-84AE4DA15455}.Debug|Any CPU.Build.0 = Debug|Any CPU 44 | {8D9AA1A3-2619-4800-AAEC-84AE4DA15455}.Release|Any CPU.ActiveCfg = Release|Any CPU 45 | {8D9AA1A3-2619-4800-AAEC-84AE4DA15455}.Release|Any CPU.Build.0 = Release|Any CPU 46 | EndGlobalSection 47 | GlobalSection(SolutionProperties) = preSolution 48 | HideSolutionNode = FALSE 49 | EndGlobalSection 50 | EndGlobal 51 | -------------------------------------------------------------------------------- /src/JustEat.StatsD/SimpleObjectPool.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.Diagnostics.CodeAnalysis; 4 | 5 | namespace JustEat.StatsD 6 | { 7 | /// A class that provides simple thread-safe object pooling semantics. 8 | public sealed class SimpleObjectPool 9 | where T : class 10 | { 11 | private readonly ConcurrentBag _pool; 12 | 13 | /// Constructor that populates a pool with the given number of items. 14 | /// Thrown when the constructor is null. 15 | /// Thrown when the constructor produces null objects. 16 | /// The capacity. 17 | /// The factory method used to create new instances of the object to populate the pool. 18 | [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "Necessary to nest generics to support passing a factory method")] 19 | public SimpleObjectPool(int capacity, Func, T> constructor) 20 | { 21 | if (null == constructor) 22 | { 23 | throw new ArgumentNullException("constructor"); 24 | } 25 | 26 | _pool = new ConcurrentBag(); 27 | for (var i = 0; i < capacity; ++i) 28 | { 29 | var instance = constructor(this); 30 | if (null == instance) 31 | { 32 | throw new ArgumentException("constructor produced null object", "constructor"); 33 | } 34 | 35 | _pool.Add(instance); 36 | } 37 | } 38 | 39 | /// Retrieves an object from the pool if one is available. 40 | /// An object or null if the pool has been exhausted. 41 | public T Pop() 42 | { 43 | T result; 44 | 45 | if (!_pool.TryTake(out result)) 46 | { 47 | result = null; 48 | } 49 | 50 | return result; 51 | } 52 | 53 | /// Pushes an object back into the pool. 54 | /// Thrown when the item is null. 55 | /// The T to push. 56 | public void Push(T item) 57 | { 58 | if (item == null) 59 | { 60 | throw new ArgumentNullException("item", "Items added to a SimpleObjectPool cannot be null"); 61 | } 62 | 63 | _pool.Add(item); 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/JustEat.StatsD.Tests/WhenTestingTimers.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | using JustBehave; 4 | using Shouldly; 5 | 6 | namespace JustEat.StatsD.Tests 7 | { 8 | public abstract class WhenTestingTimers : BehaviourTest 9 | { 10 | private string _result; 11 | private string _someBucketName; 12 | private CultureInfo _someCulture; 13 | private long _someValueToSend; 14 | 15 | protected override StatsDMessageFormatter CreateSystemUnderTest() 16 | { 17 | return new StatsDMessageFormatter(_someCulture); 18 | } 19 | 20 | protected override void Given() 21 | { 22 | var random = new Random(); 23 | _someBucketName = "timing-bucket"; 24 | _someValueToSend = random.Next(1000); 25 | _someCulture = new CultureInfo("en-US"); 26 | } 27 | 28 | [Then] 29 | public void NoExceptionsShouldHaveBeenThrown() 30 | { 31 | ThrownException.ShouldBe(null); 32 | } 33 | 34 | #region Nested type: WhenAddingASampleRateToATiming 35 | 36 | private class WhenAddingASampleRateToATiming : WhenTestingTimers 37 | { 38 | private double _sampleRate; 39 | 40 | protected override void Given() 41 | { 42 | base.Given(); 43 | _sampleRate = 0.9; 44 | } 45 | 46 | protected override void When() 47 | { 48 | // Need to mock the results... so random isnt so random here...otherwise we get a test fail when comparing the formatted string... 49 | SystemUnderTest.Timing(_someValueToSend, _sampleRate, _someBucketName); 50 | } 51 | 52 | // Not running this test till i add mocking over this object and introduce an interface bla bla bla. 53 | //[Then] 54 | //public void FormattedStringShouldBeCorrectlyFormatted() 55 | //{ 56 | // _result.ShouldBe(string.Format(_someCulture, "{0}:{1}|ms|@{2:f}", _someBucketName, _someValueToSend, _sampleRate)); 57 | //} 58 | } 59 | 60 | #endregion 61 | 62 | #region Nested type: WhenFormattingATimingMetric 63 | 64 | private class WhenFormattingATimingMetric : WhenTestingTimers 65 | { 66 | protected override void When() 67 | { 68 | _result = SystemUnderTest.Timing(_someValueToSend, _someBucketName); 69 | } 70 | 71 | [Then] 72 | public void FormattedStringShouldBeCorrectlyFormatted() 73 | { 74 | _result.ShouldBe(string.Format(_someCulture, "{0}:{1:d}|ms", _someBucketName, _someValueToSend)); 75 | } 76 | } 77 | 78 | #endregion 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/JustEat.StatsD.Tests/Extensions/FakeStatsPublisher.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace JustEat.StatsD.Tests.Extensions 5 | { 6 | public class FakeStatsPublisher : IStatsDPublisher 7 | { 8 | public int CallCount { get; set; } 9 | public int DisposeCount { get; set; } 10 | public TimeSpan LastDuration { get; set; } 11 | 12 | public List BucketNames { get; private set; } 13 | 14 | public FakeStatsPublisher() 15 | { 16 | BucketNames = new List(); 17 | } 18 | 19 | public void Dispose() 20 | { 21 | DisposeCount++; 22 | } 23 | 24 | public void Increment(string bucket) 25 | { 26 | CallCount++; 27 | BucketNames.Add(bucket); 28 | } 29 | 30 | public void Increment(long value, string bucket) 31 | { 32 | CallCount++; 33 | BucketNames.Add(bucket); 34 | } 35 | 36 | public void Increment(long value, double sampleRate, string bucket) 37 | { 38 | CallCount++; 39 | BucketNames.Add(bucket); 40 | } 41 | 42 | public void Increment(long value, double sampleRate, params string[] buckets) 43 | { 44 | CallCount++; 45 | BucketNames.AddRange(buckets); 46 | } 47 | 48 | public void Decrement(string bucket) 49 | { 50 | CallCount++; 51 | BucketNames.Add(bucket); 52 | } 53 | 54 | public void Decrement(long value, string bucket) 55 | { 56 | CallCount++; 57 | BucketNames.Add(bucket); 58 | } 59 | 60 | public void Decrement(long value, double sampleRate, string bucket) 61 | { 62 | CallCount++; 63 | BucketNames.Add(bucket); 64 | } 65 | 66 | public void Decrement(long value, double sampleRate, params string[] buckets) 67 | { 68 | CallCount++; 69 | BucketNames.AddRange(buckets); 70 | } 71 | 72 | public void Gauge(long value, string bucket) 73 | { 74 | CallCount++; 75 | BucketNames.Add(bucket); 76 | } 77 | 78 | public void Gauge(long value, string bucket, DateTime timestamp) 79 | { 80 | CallCount++; 81 | BucketNames.Add(bucket); 82 | } 83 | 84 | public void Timing(TimeSpan duration, string bucket) 85 | { 86 | CallCount++; 87 | LastDuration = duration; 88 | BucketNames.Add(bucket); 89 | } 90 | 91 | public void Timing(TimeSpan duration, double sampleRate, string bucket) 92 | { 93 | CallCount++; 94 | LastDuration = duration; 95 | BucketNames.Add(bucket); 96 | } 97 | 98 | public void MarkEvent(string name) 99 | { 100 | CallCount++; 101 | BucketNames.Add(name); 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/JustEat.StatsD.Tests/Extensions/SimpleTimerStatNameTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using NUnit.Framework; 4 | 5 | namespace JustEat.StatsD.Tests.Extensions 6 | { 7 | [TestFixture] 8 | public class SimpleTimerStatNameTests 9 | { 10 | [Test] 11 | public void DefaultIsToKeepStatName() 12 | { 13 | var publisher = new FakeStatsPublisher(); 14 | 15 | using (var timer = publisher.StartTimer("initialStat")) 16 | { 17 | Delay(); 18 | } 19 | 20 | PublisherAssertions.SingleStatNameIs(publisher, "initialStat"); 21 | } 22 | 23 | [Test] 24 | public void CanChangeStatNameDuringOperation() 25 | { 26 | var publisher = new FakeStatsPublisher(); 27 | 28 | using (var timer = publisher.StartTimer("initialStat")) 29 | { 30 | Delay(); 31 | timer.StatName = "changedValue"; 32 | } 33 | 34 | PublisherAssertions.SingleStatNameIs(publisher, "changedValue"); 35 | } 36 | 37 | [Test] 38 | public void StatNameCanBeAppended() 39 | { 40 | var publisher = new FakeStatsPublisher(); 41 | 42 | using (var timer = publisher.StartTimer("Some.")) 43 | { 44 | Delay(); 45 | timer.StatName += "More"; 46 | } 47 | 48 | PublisherAssertions.SingleStatNameIs(publisher, "Some.More"); 49 | } 50 | 51 | 52 | [Test] 53 | public void StatWithoutNameAtStartThrows() 54 | { 55 | var publisher = new FakeStatsPublisher(); 56 | 57 | Assert.Throws(() => publisher.StartTimer(string.Empty)); 58 | 59 | Assert.That(publisher.CallCount, Is.EqualTo(0)); 60 | Assert.That(publisher.BucketNames, Is.Empty); 61 | } 62 | 63 | [Test] 64 | public void StatWithoutNameAtEndThrows() 65 | { 66 | var publisher = new FakeStatsPublisher(); 67 | 68 | Assert.Throws(() => 69 | { 70 | using (var timer = publisher.StartTimer("valid.Stat")) 71 | { 72 | Delay(); 73 | timer.StatName = null; 74 | } 75 | }); 76 | 77 | Assert.That(publisher.CallCount, Is.EqualTo(0)); 78 | Assert.That(publisher.BucketNames, Is.Empty); 79 | } 80 | 81 | 82 | [Test] 83 | public void StatNameIsInitialValueWhenExceptionIsThrown() 84 | { 85 | var publisher = new FakeStatsPublisher(); 86 | var failCount = 0; 87 | try 88 | { 89 | using (var timer = publisher.StartTimer("initialStat")) 90 | { 91 | Fail(); 92 | timer.StatName = "changedValue"; 93 | } 94 | } 95 | catch (Exception) 96 | { 97 | failCount++; 98 | } 99 | 100 | Assert.That(failCount, Is.EqualTo(1)); 101 | PublisherAssertions.SingleStatNameIs(publisher, "initialStat"); 102 | } 103 | 104 | private void Delay() 105 | { 106 | const int standardDelayMillis = 200; 107 | Thread.Sleep(standardDelayMillis); 108 | } 109 | 110 | private void Fail() 111 | { 112 | throw new Exception("Deliberate fail"); 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/JustEat.StatsD/PacketBuilder.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace JustEat.StatsD 6 | { 7 | /// A helper class for turning a list of strings into a Udp packet. 8 | public static class PacketBuilder 9 | { 10 | //private static readonly byte[] Terminator = Encoding.UTF8.GetBytes("\n"); 11 | 12 | /// 13 | /// Takes a list of metric strings, separating them with newlines into a byte packet that is a maximum of 512 bytes in size. 14 | /// 15 | /// The metrics to act on. 16 | /// A streamed list of byte arrays, where each array is a maximum of 512 bytes. 17 | public static IEnumerable ToMaximumBytePackets(this IEnumerable metrics) 18 | { 19 | return ToMaximumBytePackets(metrics, 512); 20 | } 21 | 22 | /// 23 | /// Takes a list of metric strings, separating them with newlines into a byte packet of the maximum specified size. 24 | /// 25 | /// The metrics to act on. 26 | /// Maximum size of each packet (512 bytes recommended for Udp). 27 | /// A streamed list of byte arrays, where each array is a maximum of 512 bytes. 28 | public static IEnumerable ToMaximumBytePackets(this IEnumerable metrics, int packetSize) 29 | { 30 | var packet = new List(packetSize); 31 | 32 | foreach (var metric in AddNewLineToBatchMetrics(metrics)) 33 | { 34 | var bytes = Encoding.UTF8.GetBytes(metric); 35 | if (packet.Count + bytes.Length <= packetSize) 36 | { 37 | packet.AddRange(bytes); 38 | // packet.AddRange(Terminator); 39 | } 40 | else if (bytes.Length >= packetSize) 41 | { 42 | yield return bytes; 43 | } 44 | else 45 | { 46 | yield return packet.ToArray(); 47 | packet.Clear(); 48 | packet.AddRange(bytes); 49 | // packet.AddRange(Terminator); 50 | } 51 | } 52 | 53 | if (packet.Count > 0) 54 | { 55 | yield return packet.ToArray(); 56 | } 57 | } 58 | 59 | /// 60 | /// Had little option but to do this, we need newlines in our batch metrics, statsd don't really explain this clearly enough, but 61 | /// appending a new line to the end of a metric is not the correct way to go, you need to have newlines in between metrics but 62 | /// not at the end of a packet sent to statsd, as it results in bad lines getting sent to the statsd server. 63 | /// so for example metric1\nmetric2\nmetric3\n will throw a bad line error in the statsd logs, but metric1\nmetric2\nmetric3 wont. 64 | /// 65 | /// The metrics to act on. 66 | /// IEnumerable string list with appended terminators when we have more than one metric to send. 67 | private static IEnumerable AddNewLineToBatchMetrics(IEnumerable metrics) 68 | { 69 | IEnumerator en = metrics.GetEnumerator(); 70 | var metricList = new List(); 71 | if (en.MoveNext()) 72 | { 73 | // we have at least one item. 74 | metricList.Add(string.Empty + en.Current); 75 | } 76 | while (en.MoveNext()) 77 | { 78 | // second and subsequent get delimiter 79 | const string seperator = "\n"; 80 | metricList.Add(seperator + en.Current); 81 | } 82 | return metricList; 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/JustEat.StatsD.Tests/WhenTestingGauges.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | using JustBehave; 4 | using Shouldly; 5 | 6 | namespace JustEat.StatsD.Tests 7 | { 8 | public abstract class WhenTestingGauges : BehaviourTest 9 | { 10 | private string _result; 11 | private string _someBucketName; 12 | private CultureInfo _someCulture; 13 | private long _someValueToSend; 14 | 15 | protected override StatsDMessageFormatter CreateSystemUnderTest() 16 | { 17 | return new StatsDMessageFormatter(_someCulture); 18 | } 19 | 20 | protected override void Given() 21 | { 22 | var random = new Random(); 23 | _someBucketName = "gauge-bucket"; 24 | _someValueToSend = random.Next(100); 25 | _someCulture = new CultureInfo("en-US"); 26 | } 27 | 28 | [Then] 29 | public void NoExceptionsShouldHaveBeenThrown() 30 | { 31 | ThrownException.ShouldBe(null); 32 | } 33 | 34 | #region Nested type: WhenFormattingAGaugeMetric 35 | 36 | private class WhenFormattingAGaugeMetric : WhenTestingGauges 37 | { 38 | protected override void When() 39 | { 40 | _result = SystemUnderTest.Gauge(_someValueToSend, _someBucketName); 41 | } 42 | 43 | 44 | [Then] 45 | public void FormattedStringShouldBeCorrectlyFormatted() 46 | { 47 | _result.ShouldBe(string.Format(_someCulture, "{0}:{1}|g", _someBucketName, _someValueToSend)); 48 | } 49 | } 50 | 51 | #endregion 52 | 53 | #region Nested type: WhenFormattingAGaugeMetricWithABadCulture 54 | 55 | private class WhenFormattingAGaugeMetricWithABadCulture : WhenTestingGauges 56 | { 57 | private string _badFormatValueInMetric; 58 | 59 | protected override StatsDMessageFormatter CreateSystemUnderTest() 60 | { 61 | return new StatsDMessageFormatter(_someCulture); 62 | } 63 | 64 | protected override void Given() 65 | { 66 | base.Given(); 67 | // Creates a CultureInfo for DK which we know contains bad format for statsd - decimals are comma's. 68 | _someValueToSend = 100000000; 69 | var badCulture = new CultureInfo("da-DK"); 70 | // The Metric is in the dk culture i.e, 10000,000 this will fail in statsd, so we pass in a culture when formatting the string. 71 | _badFormatValueInMetric = _someValueToSend.ToString("0.00", badCulture); 72 | } 73 | 74 | 75 | protected override void When() 76 | { 77 | _result = SystemUnderTest.Gauge(_someValueToSend, _someBucketName); 78 | } 79 | 80 | 81 | [Then] 82 | public void FormattedStringShouldNotContainBadValue() 83 | { 84 | _result.ShouldNotContain(_badFormatValueInMetric); 85 | } 86 | 87 | [Then] 88 | public void FormattedStringShouldBeCorrectlyFormatted() 89 | { 90 | _result.ShouldBe(string.Format(_someCulture, "{0}:{1}|g", _someBucketName, _someValueToSend)); 91 | } 92 | } 93 | 94 | #endregion 95 | 96 | #region Nested type: WhenFormattingAGaugeMetricWithATimestamp 97 | 98 | private class WhenFormattingAGaugeMetricWithATimestamp : WhenTestingGauges 99 | { 100 | private DateTime _timeStamp; 101 | 102 | protected override void Given() 103 | { 104 | base.Given(); 105 | _timeStamp = DateTime.Now; 106 | } 107 | 108 | protected override void When() 109 | { 110 | _result = SystemUnderTest.Gauge(_someValueToSend, _someBucketName, _timeStamp); 111 | } 112 | 113 | [Then] 114 | public void FormattedStringShouldBeCorrectlyFormatted() 115 | { 116 | _result.ShouldBe(string.Format(_someCulture, "{0}:{1}|g|@{2}", _someBucketName, _someValueToSend, _timeStamp.AsUnixTime())); 117 | } 118 | } 119 | 120 | #endregion 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/JustEat.StatsD/StatsDImmediatePublisher.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | 4 | namespace JustEat.StatsD 5 | { 6 | /// 7 | /// Will synchronously publish stats at statsd as you make calls; will not batch sends. 8 | /// 9 | public class StatsDImmediatePublisher : IStatsDPublisher 10 | { 11 | private static readonly CultureInfo SafeDefaultCulture = new CultureInfo(StatsDMessageFormatter.SafeDefaultIsoCultureId); 12 | private readonly StatsDMessageFormatter _formatter; 13 | private readonly IStatsDUdpClient _transport; 14 | private bool _disposed; 15 | 16 | public StatsDImmediatePublisher(CultureInfo cultureInfo, string hostNameOrAddress, int port = 8125, string prefix = "") 17 | { 18 | _formatter = new StatsDMessageFormatter(cultureInfo, prefix); 19 | _transport = new StatsDUdpClient(hostNameOrAddress, port); 20 | } 21 | 22 | public StatsDImmediatePublisher(string hostNameOrAddress, int port = 8125, string prefix = "") : this(SafeDefaultCulture, hostNameOrAddress, port, prefix) {} 23 | 24 | public void Increment(string bucket) 25 | { 26 | _transport.Send(_formatter.Increment(bucket)); 27 | } 28 | 29 | public void Increment(long value, string bucket) 30 | { 31 | _transport.Send(_formatter.Increment(value, bucket)); 32 | } 33 | 34 | public void Increment(long value, double sampleRate, string bucket) 35 | { 36 | _transport.Send(_formatter.Increment(value, sampleRate, bucket)); 37 | } 38 | 39 | public void Increment(long value, double sampleRate, params string[] buckets) 40 | { 41 | _transport.Send(_formatter.Increment(value, sampleRate, buckets)); 42 | } 43 | 44 | public void Decrement(string bucket) 45 | { 46 | _transport.Send(_formatter.Decrement(bucket)); 47 | } 48 | 49 | public void Decrement(long value, string bucket) 50 | { 51 | _transport.Send(_formatter.Decrement(value, bucket)); 52 | } 53 | 54 | public void Decrement(long value, double sampleRate, string bucket) 55 | { 56 | _transport.Send(_formatter.Decrement(value, sampleRate, bucket)); 57 | } 58 | 59 | public void Decrement(long value, double sampleRate, params string[] buckets) 60 | { 61 | _transport.Send(_formatter.Decrement(value, sampleRate, buckets)); 62 | } 63 | 64 | public void Gauge(long value, string bucket) 65 | { 66 | _transport.Send(_formatter.Gauge(value, bucket)); 67 | } 68 | 69 | public void Gauge(long value, string bucket, DateTime timestamp) 70 | { 71 | _transport.Send(_formatter.Gauge(value, bucket, timestamp)); 72 | } 73 | 74 | public void Timing(TimeSpan duration, string bucket) 75 | { 76 | _transport.Send(_formatter.Timing(Convert.ToInt64(duration.TotalMilliseconds), bucket)); 77 | } 78 | 79 | public void Timing(TimeSpan duration, double sampleRate, string bucket) 80 | { 81 | _transport.Send(_formatter.Timing(Convert.ToInt64(duration.TotalMilliseconds), sampleRate, bucket)); 82 | } 83 | 84 | public void MarkEvent(string name) 85 | { 86 | _transport.Send(_formatter.Event(name)); 87 | } 88 | 89 | /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. 90 | public void Dispose() 91 | { 92 | if (!_disposed) 93 | { 94 | Dispose(true); 95 | GC.SuppressFinalize(this); 96 | } 97 | } 98 | 99 | /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. 100 | /// true if resources should be disposed, false if not. 101 | protected virtual void Dispose(bool disposing) 102 | { 103 | if (disposing) 104 | { 105 | if (null != _transport) 106 | { 107 | _transport.Dispose(); 108 | } 109 | _disposed = true; 110 | } 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JustEat.StatsD 2 | 3 | [![NuGet version](https://buildstats.info/nuget/JustEat.StatsD?includePreReleases=false)](http://www.nuget.org/packages/JustEat.StatsD) 4 | [![Build status](https://img.shields.io/appveyor/ci/justeattech/justeat-statsd/master.svg)](https://ci.appveyor.com/project/justeattech/JustEat.StatsD) 5 | 6 | ## The point 7 | 8 | ### TL;DR 9 | 10 | We use this within our components to publish [statsd](http://github.com/etsy/statsd) metrics from .NET code. We've been using this in most of our things, since around 2013. 11 | 12 | ### Features 13 | 14 | * statsd metrics formatter 15 | * UDP client handling 16 | 17 | #### Publishing statistics 18 | 19 | `IStatsDPublisher` is the interface that you will use in most circumstances. With this you can `Increment` or `Decrement` an event, and send values for a `Gauge` or `Timing`. 20 | 21 | The concrete class that implements `IStatsDPublisher` is `StatsDImmediatePublisher`. For the constructor parameters, you will need the statsd server host name. You can change the standard port (8125). You can also prepend a prefix to all stats. These values often come from configuration as the host name and/or prefix may vary between test and production environments. 22 | 23 | #### Example of setting up a StatsDPublisher 24 | 25 | An example of Ioc in NInject for statsd publisher with values from configuration: 26 | ```csharp 27 | string statsdHostName = ConfigurationManager.AppSettings["statsd.hostname"]; 28 | int statsdPort = int.Parse(ConfigurationManager.AppSettings["statsd.port"]); 29 | string statsdPrefix = ConfigurationManager.AppSettings["statsd.prefix"]; 30 | 31 | Bind().To() 32 | .WithConstructorArgument("cultureInfo", CultureInfo.InvariantCulture) 33 | .WithConstructorArgument("hostNameOrAddress",statsdHostName) 34 | .WithConstructorArgument("port", statsdPort) 35 | .WithConstructorArgument("prefix", statsdPrefix); 36 | ``` 37 | 38 | #### Example of using the interface 39 | 40 | Given an existing instance of `IStatsDPublisher` called `stats` you can do for e.g.: 41 | 42 | ```csharp 43 | stats.Increment("DoSomething.Attempt"); 44 | var stopWatch = Stopwatch.StartNew(); 45 | var success = DoSomething(); 46 | 47 | stopWatch.Stop(); 48 | 49 | var statName = "DoSomething." + success ? "Success" : "Failure"; 50 | stats.Timing(statName, stopWatch.Elapsed); 51 | ``` 52 | 53 | #### Simple timers 54 | 55 | 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: 56 | 57 | ```csharp 58 | // timing a block of code in a using statement: 59 | using (stats.StartTimer("someStat")) 60 | { 61 | DoSomething(); 62 | } 63 | 64 | // also works with async 65 | using (stats.StartTimer("someStat")) 66 | { 67 | await DoSomethingAsync(); 68 | } 69 | ``` 70 | 71 | The `StartTimer` returns an `IDisposable` that wraps a stopwatch. The stopwatch is automatically stopped and the metric sent when it falls out of scope on the closing `}` of the `using` statement. 72 | 73 | ##### Functional style 74 | 75 | ```csharp 76 | // timing a lambda without a return value: 77 | stats.Time("someStat", () => DoSomething()); 78 | 79 | // timing a lambda with a return value: 80 | var result = stats.Time("someStat", () => GetSomething()); 81 | 82 | // and correctly times async lambdas using the usual syntax: 83 | await stats.Time("someStat", async () => await DoSomethingAsync()); 84 | var result = await stats.Time("someStat", async () => await GetSomethingAsync()); 85 | ``` 86 | 87 | ##### Changing the name of simple timers 88 | 89 | 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. 90 | 91 | The timer has a `StatName` property to set or change the name of the stat. To use this you need a reference to the timer, e.g. `using (var timer = stats.StartTimer("statName"))` instead of `using (stats.StartTimer("statName"))` 92 | 93 | The stat name must be set to a non-empty string at the end of the `using` block. 94 | 95 | ```csharp 96 | using (var timer = stats.StartTimer("SomeHttpOperation.")) 97 | { 98 | var response = DoSomeHttpOperation(); 99 | timer.StatName = timer.StatName + response.StatusCode; 100 | return response; 101 | } 102 | ``` 103 | 104 | The idea of "disposable timers" for using statements comes from [this StatsD client](https://github.com/Pereingo/statsd-csharp-client). 105 | 106 | ### How to contribute 107 | 108 | See [CONTRIBUTING.md](CONTRIBUTING.md). 109 | 110 | ### How to release 111 | See [CONTRIBUTING.md](CONTRIBUTING.md). 112 | -------------------------------------------------------------------------------- /Build.ps1: -------------------------------------------------------------------------------- 1 | param( 2 | [Parameter(Mandatory=$false)][bool] $RestorePackages = $false, 3 | [Parameter(Mandatory=$false)][string] $Configuration = "Release", 4 | [Parameter(Mandatory=$false)][string] $VersionSuffix = "", 5 | [Parameter(Mandatory=$false)][string] $OutputPath = "", 6 | [Parameter(Mandatory=$false)][bool] $RunTests = $true, 7 | [Parameter(Mandatory=$false)][bool] $CreatePackages = $true 8 | ) 9 | 10 | $ErrorActionPreference = "Stop" 11 | 12 | $solutionPath = Split-Path $MyInvocation.MyCommand.Definition 13 | $getDotNet = Join-Path $solutionPath "tools\install.ps1" 14 | $dotnetVersion = $env:CLI_VERSION 15 | 16 | if ($OutputPath -eq "") { 17 | $OutputPath = "$(Convert-Path "$PSScriptRoot")\artifacts" 18 | } 19 | 20 | $env:DOTNET_INSTALL_DIR = "$(Convert-Path "$PSScriptRoot")\.dotnetcli" 21 | 22 | if ($env:CI -ne $null) { 23 | 24 | $RestorePackages = $true 25 | 26 | if (($VersionSuffix -eq "" -and $env:APPVEYOR_REPO_TAG -eq "false" -and $env:APPVEYOR_BUILD_NUMBER -ne "") -eq $true) { 27 | $ThisVersion = $env:APPVEYOR_BUILD_NUMBER -as [int] 28 | $VersionSuffix = "beta" + $ThisVersion.ToString("0000") 29 | } 30 | } 31 | 32 | if (!(Test-Path $env:DOTNET_INSTALL_DIR)) { 33 | mkdir $env:DOTNET_INSTALL_DIR | Out-Null 34 | $installScript = Join-Path $env:DOTNET_INSTALL_DIR "install.ps1" 35 | Invoke-WebRequest "https://raw.githubusercontent.com/dotnet/cli/rel/1.0.0/scripts/obtain/dotnet-install.ps1" -OutFile $installScript 36 | & $installScript -Version "$dotnetVersion" -InstallDir "$env:DOTNET_INSTALL_DIR" -NoPath 37 | } 38 | 39 | $env:PATH = "$env:DOTNET_INSTALL_DIR;$env:PATH" 40 | $dotnet = "$env:DOTNET_INSTALL_DIR\dotnet" 41 | 42 | function DotNetRestore { param([string]$Project) 43 | & $dotnet restore $Project --verbosity minimal 44 | if ($LASTEXITCODE -ne 0) { 45 | throw "dotnet restore failed with exit code $LASTEXITCODE" 46 | } 47 | } 48 | 49 | function DotNetBuild { param([string]$Project, [string]$Configuration, [string]$Framework, [string]$VersionSuffix) 50 | if ($VersionSuffix) { 51 | & $dotnet build $Project --output (Join-Path $OutputPath $Framework) --framework $Framework --configuration $Configuration --version-suffix "$VersionSuffix" 52 | } else { 53 | & $dotnet build $Project --output (Join-Path $OutputPath $Framework) --framework $Framework --configuration $Configuration 54 | } 55 | if ($LASTEXITCODE -ne 0) { 56 | throw "dotnet build failed with exit code $LASTEXITCODE" 57 | } 58 | } 59 | 60 | function DotNetTest { param([string]$Project) 61 | & $dotnet test $Project 62 | if ($LASTEXITCODE -ne 0) { 63 | throw "dotnet test failed with exit code $LASTEXITCODE" 64 | } 65 | } 66 | 67 | function DotNetPack { param([string]$Project, [string]$Configuration, [string]$VersionSuffix) 68 | if ($VersionSuffix) { 69 | & $dotnet pack $Project --output $OutputPath --configuration $Configuration --version-suffix "$VersionSuffix" --no-build 70 | } else { 71 | & $dotnet pack $Project --output $OutputPath --configuration $Configuration --no-build 72 | } 73 | if ($LASTEXITCODE -ne 0) { 74 | throw "dotnet pack failed with exit code $LASTEXITCODE" 75 | } 76 | } 77 | 78 | $projects = @( 79 | (Join-Path $solutionPath "src\JustEat.StatsD\project.json") 80 | ) 81 | 82 | $testProjects = @( 83 | (Join-Path $solutionPath "src\JustEat.StatsD.Tests\project.json") 84 | ) 85 | 86 | $packageProjects = @( 87 | (Join-Path $solutionPath "src\JustEat.StatsD\project.json") 88 | ) 89 | 90 | $restoreProjects = @( 91 | (Join-Path $solutionPath "src\JustEat.StatsD\project.json") 92 | (Join-Path $solutionPath "src\JustEat.StatsD.Tests\project.json") 93 | ) 94 | 95 | if ($RestorePackages -eq $true) { 96 | Write-Host "Restoring NuGet packages for $($restoreProjects.Count) projects..." -ForegroundColor Green 97 | ForEach ($project in $restoreProjects) { 98 | DotNetRestore $project 99 | } 100 | } 101 | 102 | Write-Host "Building $($projects.Count) projects..." -ForegroundColor Green 103 | ForEach ($project in $projects) { 104 | DotNetBuild $project $Configuration "netstandard1.3" $VersionSuffix 105 | DotNetBuild $project $Configuration "net451" $VersionSuffix 106 | } 107 | 108 | if ($RunTests -eq $true) { 109 | Write-Host "Testing $($testProjects.Count) project(s)..." -ForegroundColor Green 110 | ForEach ($project in $testProjects) { 111 | DotNetTest $project 112 | } 113 | } 114 | 115 | if ($CreatePackages -eq $true) { 116 | Write-Host "Creating $($packageProjects.Count) package(s)..." -ForegroundColor Green 117 | ForEach ($project in $packageProjects) { 118 | DotNetPack $project $Configuration $VersionSuffix 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/JustEat.StatsD/StatsDUdpClient.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.Diagnostics.CodeAnalysis; 5 | using System.Globalization; 6 | using System.Linq; 7 | using System.Net; 8 | using System.Net.Sockets; 9 | using JustEat.StatsD.EndpointLookups; 10 | 11 | namespace JustEat.StatsD 12 | { 13 | public class StatsDUdpClient : IStatsDUdpClient 14 | { 15 | private static readonly SimpleObjectPool EventArgsPool 16 | = new SimpleObjectPool(30, pool => new PoolAwareSocketAsyncEventArgs(pool)); 17 | 18 | private readonly IDnsEndpointMapper _endPointMapper; 19 | private readonly string _hostNameOrAddress; 20 | private readonly IPEndPoint _ipBasedEndpoint; 21 | private readonly int _port; 22 | private bool _disposed; 23 | 24 | public StatsDUdpClient(string hostNameOrAddress, int port) 25 | : this(new DnsEndpointProvider(), hostNameOrAddress, port) {} 26 | 27 | public StatsDUdpClient(int endpointCacheDuration, string hostNameOrAddress, int port) 28 | : this(new CachedDnsEndpointMapper(new DnsEndpointProvider(), endpointCacheDuration), hostNameOrAddress, port) {} 29 | 30 | private StatsDUdpClient(IDnsEndpointMapper endpointMapper, string hostNameOrAddress, int port) 31 | { 32 | _endPointMapper = endpointMapper; 33 | _hostNameOrAddress = hostNameOrAddress; 34 | _port = port; 35 | 36 | //if we were given an IP instead of a hostname, we can happily cache it off for the life of this class 37 | IPAddress address; 38 | if (IPAddress.TryParse(hostNameOrAddress, out address)) 39 | { 40 | _ipBasedEndpoint = new IPEndPoint(address, _port); 41 | } 42 | } 43 | 44 | public bool Send(string metric) 45 | { 46 | return Send(new[] {metric}); 47 | } 48 | 49 | [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "This is one of the rare cases where eating exceptions is OK")] 50 | public bool Send(IEnumerable metrics) 51 | { 52 | var data = EventArgsPool.Pop(); 53 | //firehose alert! -- keep it moving! 54 | if (null == data) 55 | { 56 | return false; 57 | } 58 | 59 | try 60 | { 61 | data.RemoteEndPoint = GetIPEndPoint(); 62 | data.SendPacketsElements = metrics.ToMaximumBytePackets() 63 | .Select(bytes => new SendPacketsElement(bytes, 0, bytes.Length, true)) 64 | .ToArray(); 65 | 66 | using (var udpClient = GetUdpClient()) 67 | { 68 | udpClient.Client.Connect(data.RemoteEndPoint); 69 | udpClient.Client.SendPacketsAsync(data); 70 | } 71 | 72 | Trace.TraceInformation("statsd: {0}", string.Join(",", metrics)); 73 | 74 | return true; 75 | } 76 | //fire and forget, so just eat intermittent failures / exceptions 77 | catch (Exception e) 78 | { 79 | Trace.TraceError("General Exception when sending metric data to statsD :- Message : {0}, Inner Exception {1}, StackTrace {2}.", e.Message, e.InnerException, e.StackTrace); 80 | } 81 | 82 | return false; 83 | } 84 | 85 | public UdpClient GetUdpClient() 86 | { 87 | UdpClient client = null; 88 | try 89 | { 90 | client = new UdpClient() 91 | { 92 | Client = { SendBufferSize = 0 } 93 | }; 94 | } 95 | catch (SocketException e) 96 | { 97 | Trace.TraceError(string.Format(CultureInfo.InvariantCulture, "Error Creating udpClient :- Message : {0}, Inner Exception {1}, StackTrace {2}.", e.Message, e.InnerException, e.StackTrace)); 98 | } 99 | return client; 100 | } 101 | 102 | /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. 103 | public void Dispose() 104 | { 105 | if (!_disposed) 106 | { 107 | Dispose(true); 108 | GC.SuppressFinalize(this); 109 | } 110 | } 111 | 112 | /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. 113 | /// true if resources should be disposed, false if not. 114 | protected virtual void Dispose(bool disposing) 115 | { 116 | if (disposing) 117 | { 118 | _disposed = true; 119 | } 120 | } 121 | 122 | private IPEndPoint GetIPEndPoint() 123 | { 124 | return _ipBasedEndpoint ?? _endPointMapper.GetIPEndPoint(_hostNameOrAddress, _port); // Only DNS resolve if we were given a hostname 125 | } 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/JustEat.StatsD/StatsDMessageFormatter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | using System.Linq; 4 | using System.Text; 5 | 6 | namespace JustEat.StatsD 7 | { 8 | #if NET45 9 | [Serializable] 10 | #endif 11 | public class StatsDMessageFormatter 12 | { 13 | public const string SafeDefaultIsoCultureId = "en-US"; 14 | private const double DefaultSampleRate = 1.0; 15 | 16 | [ThreadStatic] private static Random _random; 17 | 18 | private readonly CultureInfo _cultureInfo; 19 | private readonly string _prefix; 20 | 21 | public StatsDMessageFormatter() : this(new CultureInfo(SafeDefaultIsoCultureId), prefix: "") {} 22 | 23 | public StatsDMessageFormatter(string prefix = "") : this(new CultureInfo(SafeDefaultIsoCultureId), prefix) {} 24 | 25 | public StatsDMessageFormatter(CultureInfo ci, string prefix = "") 26 | { 27 | _cultureInfo = ci; 28 | _prefix = prefix; 29 | if (!string.IsNullOrWhiteSpace(_prefix)) 30 | { 31 | _prefix = _prefix + "."; // if we have something, then append a . to it to make concatenations easy. 32 | } 33 | } 34 | 35 | private static Random Random 36 | { 37 | get { return _random ?? (_random = new Random()); } 38 | } 39 | 40 | public string Timing(long milliseconds, string statBucket) 41 | { 42 | return Timing(milliseconds, DefaultSampleRate, statBucket); 43 | } 44 | 45 | public string Timing(long milliseconds, double sampleRate, string statBucket) 46 | { 47 | return Format(sampleRate, string.Format(_cultureInfo, "{0}{1}:{2:d}|ms", _prefix, statBucket, milliseconds)); 48 | } 49 | 50 | public string Decrement(string statBucket) 51 | { 52 | return Increment(-1, DefaultSampleRate, statBucket); 53 | } 54 | 55 | public string Decrement(long magnitude, string statBucket) 56 | { 57 | return Decrement(magnitude, DefaultSampleRate, statBucket); 58 | } 59 | 60 | 61 | public string Decrement(long magnitude, double sampleRate, string statBucket) 62 | { 63 | magnitude = magnitude < 0 ? magnitude : -magnitude; 64 | return Increment(magnitude, sampleRate, statBucket); 65 | } 66 | 67 | public string Decrement(params string[] statBuckets) 68 | { 69 | return Increment(-1, DefaultSampleRate, statBuckets); 70 | } 71 | 72 | public string Decrement(long magnitude, params string[] statBuckets) 73 | { 74 | magnitude = magnitude < 0 ? magnitude : -magnitude; 75 | return Increment(magnitude, DefaultSampleRate, statBuckets); 76 | } 77 | 78 | public string Decrement(long magnitude, double sampleRate, params string[] statBuckets) 79 | { 80 | magnitude = magnitude < 0 ? magnitude : -magnitude; 81 | return Increment(magnitude, sampleRate, statBuckets); 82 | } 83 | 84 | public string Increment(string statBucket) 85 | { 86 | return Increment(1, DefaultSampleRate, statBucket); 87 | } 88 | 89 | public string Increment(long magnitude, string statBucket) 90 | { 91 | return Increment(magnitude, DefaultSampleRate, statBucket); 92 | } 93 | 94 | public string Increment(long magnitude, double sampleRate, string statBucket) 95 | { 96 | var stat = string.Format(_cultureInfo, "{0}{1}:{2}|c", _prefix, statBucket, magnitude); 97 | return Format(stat, sampleRate); 98 | } 99 | 100 | public string Increment(long magnitude, params string[] statBuckets) 101 | { 102 | return Increment(magnitude, DefaultSampleRate, statBuckets); 103 | } 104 | 105 | public string Increment(long magnitude, double sampleRate, params string[] statBuckets) 106 | { 107 | return Format(sampleRate, statBuckets.Select(key => string.Format(_cultureInfo, "{0}{1}:{2}|c", _prefix, key, magnitude)).ToArray()); 108 | } 109 | 110 | public string Gauge(long magnitude, string statBucket) 111 | { 112 | var stat = string.Format(_cultureInfo, "{0}{1}:{2}|g", _prefix, statBucket, magnitude); 113 | return Format(stat, DefaultSampleRate); 114 | } 115 | 116 | public string Gauge(long magnitude, string statBucket, DateTime timestamp) 117 | { 118 | var stat = string.Format(_cultureInfo, "{0}{1}:{2}|g|@{3}", _prefix, statBucket, magnitude, timestamp.AsUnixTime()); 119 | return Format(stat, DefaultSampleRate); 120 | } 121 | 122 | public string Event(string name) 123 | { 124 | return Increment(name); 125 | } 126 | 127 | private string Format(String stat, double sampleRate) 128 | { 129 | return Format(sampleRate, stat); 130 | } 131 | 132 | private string Format(double sampleRate, params string[] stats) 133 | { 134 | var formatted = new StringBuilder(); 135 | if (sampleRate < DefaultSampleRate) 136 | { 137 | foreach (var stat in stats) 138 | { 139 | if (Random.NextDouble() <= sampleRate) 140 | { 141 | formatted.AppendFormat(_cultureInfo, "{0}|@{1:f}", stat, sampleRate); 142 | } 143 | } 144 | } 145 | else 146 | { 147 | foreach (var stat in stats) 148 | { 149 | formatted.AppendFormat(_cultureInfo, "{0}", stat); 150 | } 151 | } 152 | 153 | return formatted.ToString(); 154 | } 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /src/JustEat.StatsD.Tests/Extensions/ExtensionsTests.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using NUnit.Framework; 5 | 6 | namespace JustEat.StatsD.Tests.Extensions 7 | { 8 | [TestFixture] 9 | public class ExtensionsTests 10 | { 11 | private const int StandardDelayMillis = 200; 12 | 13 | [Test] 14 | public void CanRecordStat() 15 | { 16 | var publisher = new FakeStatsPublisher(); 17 | 18 | using (publisher.StartTimer("stat")) 19 | { 20 | Delay(); 21 | } 22 | 23 | PublisherAssertions.SingleStatNameIs(publisher, "stat"); 24 | PublisherAssertions.LastDurationIs(publisher, StandardDelayMillis); 25 | } 26 | 27 | [Test] 28 | public void ShouldNotDisposePublisherAfterStatSent() 29 | { 30 | var publisher = new FakeStatsPublisher(); 31 | 32 | using (publisher.StartTimer("stat")) 33 | { 34 | Delay(); 35 | } 36 | 37 | Assert.That(publisher.DisposeCount, Is.EqualTo(0)); 38 | } 39 | 40 | [Test] 41 | public void CanRecordTwoStats() 42 | { 43 | var publisher = new FakeStatsPublisher(); 44 | 45 | using (publisher.StartTimer("stat1")) 46 | { 47 | Delay(); 48 | } 49 | 50 | using (publisher.StartTimer("stat2")) 51 | { 52 | Delay(); 53 | } 54 | 55 | Assert.That(publisher.CallCount, Is.EqualTo(2)); 56 | Assert.That(publisher.BucketNames, Is.EquivalentTo(new List { "stat1", "stat2" })); 57 | PublisherAssertions.LastDurationIs(publisher, StandardDelayMillis); 58 | } 59 | 60 | [Test] 61 | public async Task CanRecordStatAsync() 62 | { 63 | var publisher = new FakeStatsPublisher(); 64 | 65 | using (publisher.StartTimer("statWithAsync")) 66 | { 67 | await DelayAsync(); 68 | } 69 | 70 | PublisherAssertions.SingleStatNameIs(publisher, "statWithAsync"); 71 | PublisherAssertions.LastDurationIs(publisher, StandardDelayMillis); 72 | } 73 | 74 | [Test] 75 | public async Task CanRecordTwoStatsAsync() 76 | { 77 | var publisher = new FakeStatsPublisher(); 78 | 79 | using (publisher.StartTimer("stat1")) 80 | { 81 | await DelayAsync(); 82 | } 83 | 84 | using (publisher.StartTimer("stat2")) 85 | { 86 | await DelayAsync(); 87 | } 88 | 89 | Assert.That(publisher.CallCount, Is.EqualTo(2)); 90 | Assert.That(publisher.BucketNames, Is.EquivalentTo(new List { "stat1", "stat2" })); 91 | PublisherAssertions.LastDurationIs(publisher, StandardDelayMillis); 92 | } 93 | 94 | [Test] 95 | public void CanRecordStatInAction() 96 | { 97 | var publisher = new FakeStatsPublisher(); 98 | publisher.Time("statOverAction", () => Delay()); 99 | 100 | PublisherAssertions.SingleStatNameIs(publisher, "statOverAction"); 101 | PublisherAssertions.LastDurationIs(publisher, StandardDelayMillis); 102 | } 103 | 104 | [Test] 105 | public void CanRecordStatInFunction() 106 | { 107 | var publisher = new FakeStatsPublisher(); 108 | var answer = publisher.Time("statOverFunc", () => DelayedAnswer()); 109 | 110 | Assert.That(answer, Is.EqualTo(42)); 111 | PublisherAssertions.SingleStatNameIs(publisher, "statOverFunc"); 112 | PublisherAssertions.LastDurationIs(publisher, StandardDelayMillis); 113 | } 114 | 115 | [Test] 116 | public async Task CanRecordStatInAsyncAction() 117 | { 118 | var publisher = new FakeStatsPublisher(); 119 | await publisher.Time("statOverAsyncAction", async () => await DelayAsync()); 120 | 121 | PublisherAssertions.SingleStatNameIs(publisher, "statOverAsyncAction"); 122 | } 123 | 124 | [Test] 125 | public async Task CorrectDurationForStatInAsyncAction() 126 | { 127 | var publisher = new FakeStatsPublisher(); 128 | await publisher.Time("stat", async () => await DelayAsync()); 129 | 130 | PublisherAssertions.LastDurationIs(publisher, StandardDelayMillis); 131 | } 132 | 133 | [Test] 134 | public async Task CanRecordStatInAsyncFunction() 135 | { 136 | var publisher = new FakeStatsPublisher(); 137 | var answer = await publisher.Time("statOverAsyncFunc", async () => await DelayedAnswerAsync()); 138 | 139 | Assert.That(answer, Is.EqualTo(42)); 140 | PublisherAssertions.SingleStatNameIs(publisher, "statOverAsyncFunc"); 141 | } 142 | 143 | [Test] 144 | public async Task CorrectDurationForStatInAsyncFunction() 145 | { 146 | var publisher = new FakeStatsPublisher(); 147 | await publisher.Time("stat", async () => await DelayedAnswerAsync()); 148 | 149 | PublisherAssertions.LastDurationIs(publisher, StandardDelayMillis); 150 | } 151 | 152 | private void Delay() 153 | { 154 | Thread.Sleep(StandardDelayMillis); 155 | } 156 | 157 | private async Task DelayAsync() 158 | { 159 | await Task.Delay(StandardDelayMillis); 160 | } 161 | 162 | private int DelayedAnswer() 163 | { 164 | Thread.Sleep(StandardDelayMillis); 165 | return 42; 166 | } 167 | 168 | private async Task DelayedAnswerAsync() 169 | { 170 | await Task.Delay(StandardDelayMillis); 171 | return 42; 172 | } 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /src/JustEat.StatsD.Tests/WhenTestingCounters.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | using System.Text; 4 | using JustBehave; 5 | using Shouldly; 6 | 7 | namespace JustEat.StatsD.Tests 8 | { 9 | public abstract class WhenTestingCounters : BehaviourTest 10 | { 11 | private string _result; 12 | private string _someBucketName; 13 | private CultureInfo _someCulture; 14 | private long _someValueToSend; 15 | 16 | protected override StatsDMessageFormatter CreateSystemUnderTest() 17 | { 18 | return new StatsDMessageFormatter(_someCulture); 19 | } 20 | 21 | protected override void Given() 22 | { 23 | var random = new Random(); 24 | _someValueToSend = random.Next(1,100); 25 | _someBucketName = "counter-bucket"; 26 | _someCulture = new CultureInfo("en-US"); 27 | } 28 | 29 | [Then] 30 | public void NoExceptionsShouldHaveBeenThrown() 31 | { 32 | ThrownException.ShouldBe(null); 33 | } 34 | 35 | private abstract class AndWeHaveAPrefix : WhenTestingCounters 36 | { 37 | private string _prefix; 38 | 39 | protected override void Given() 40 | { 41 | _prefix = "foo"; 42 | base.Given(); 43 | } 44 | 45 | protected override StatsDMessageFormatter CreateSystemUnderTest() 46 | { 47 | return new StatsDMessageFormatter(new CultureInfo("en-US"), _prefix); 48 | } 49 | 50 | [Then] 51 | public void ResultShouldBeCorrectlyPrefixed() 52 | { 53 | _result.ShouldStartWith(_prefix + "."); 54 | } 55 | 56 | private class WhenAdjustingGauge : AndWeHaveAPrefix 57 | { 58 | protected override void When() 59 | { 60 | _result = SystemUnderTest.Gauge(234, _someBucketName); 61 | } 62 | } 63 | 64 | private class WhenDecrementingCounter : AndWeHaveAPrefix 65 | { 66 | protected override void When() 67 | { 68 | _result = SystemUnderTest.Decrement(_someBucketName); 69 | } 70 | } 71 | 72 | private class WhenIncrementingCounter : AndWeHaveAPrefix 73 | { 74 | protected override void When() 75 | { 76 | _result = SystemUnderTest.Increment(_someBucketName); 77 | } 78 | } 79 | 80 | private class WhenSubmittingTiming : AndWeHaveAPrefix 81 | { 82 | protected override void When() 83 | { 84 | _result = SystemUnderTest.Timing(234, _someBucketName); 85 | } 86 | } 87 | } 88 | 89 | private class WhenAddingASampleRateToACounter : WhenTestingCounters 90 | { 91 | private double _sampleRate; 92 | 93 | protected override void Given() 94 | { 95 | base.Given(); 96 | _sampleRate = 0.9; 97 | } 98 | 99 | protected override void When() 100 | { 101 | SystemUnderTest.Increment(_someValueToSend, _sampleRate, _someBucketName); 102 | } 103 | 104 | // Again, this test is too flaky for now... 105 | //[Then] 106 | //public void FormattedStringShouldBeCorrectlyFormatted() 107 | //{ 108 | // _result.ShouldBe(string.Format(_someCulture, "{0}:{1}|c|@{2:f}", _someBucketName, _someValueToSend, _sampleRate)); 109 | //} 110 | } 111 | 112 | private class WhenDecrementingCounters : WhenTestingCounters 113 | { 114 | protected override void When() 115 | { 116 | _result = SystemUnderTest.Decrement(_someBucketName); 117 | } 118 | 119 | 120 | [Then] 121 | public void FormattedStringShouldBeCorrectlyFormatted() 122 | { 123 | _result.ShouldBe(string.Format(_someCulture, "{0}:-{1}|c", _someBucketName, 1)); 124 | } 125 | } 126 | 127 | private class WhenDecrementingCountersWithAValue : WhenTestingCounters 128 | { 129 | protected override void When() 130 | { 131 | _result = SystemUnderTest.Decrement(_someValueToSend, _someBucketName); 132 | } 133 | 134 | [Then] 135 | public void FormattedStringShouldBeCorrectlyFormatted() 136 | { 137 | _result.ShouldBe(string.Format(_someCulture, "{0}:-{1}|c", _someBucketName, _someValueToSend)); 138 | } 139 | } 140 | 141 | private class WhenDecrementingMultipleMetrics : WhenTestingCounters 142 | { 143 | private new string[] _someBucketName; 144 | 145 | protected override void Given() 146 | { 147 | base.Given(); 148 | _someBucketName = new[] {"counter-bucket-1", "counter-bucket-2"}; 149 | } 150 | 151 | protected override void When() 152 | { 153 | _result = SystemUnderTest.Decrement(_someValueToSend, _someBucketName); 154 | } 155 | 156 | [Then] 157 | public void FormattedStringShouldBeCorrectlyFormatted() 158 | { 159 | var expectedString = new StringBuilder(); 160 | 161 | foreach (var stat in _someBucketName) 162 | { 163 | expectedString.AppendFormat(_someCulture, "{0}:-{1}|c", stat, _someValueToSend); 164 | } 165 | 166 | _result.ShouldBe(expectedString.ToString()); 167 | } 168 | } 169 | 170 | private class WhenIncrementingCounters : WhenTestingCounters 171 | { 172 | protected override void When() 173 | { 174 | _result = SystemUnderTest.Increment(_someBucketName); 175 | } 176 | 177 | [Then] 178 | public void FormattedStringShouldBeCorrectlyFormatted() 179 | { 180 | _result.ShouldBe(string.Format(_someCulture, "{0}:{1}|c", _someBucketName, 1)); 181 | } 182 | } 183 | 184 | private class WhenIncrementingCountersWithAValue : WhenTestingCounters 185 | { 186 | protected override void When() 187 | { 188 | _result = SystemUnderTest.Increment(_someValueToSend, _someBucketName); 189 | } 190 | 191 | [Then] 192 | public void FormattedStringShouldBeCorrectlyFormatted() 193 | { 194 | _result.ShouldBe(string.Format(_someCulture, "{0}:{1}|c", _someBucketName, _someValueToSend)); 195 | } 196 | } 197 | 198 | private class WhenIncrementingMultipleMetrics : WhenTestingCounters 199 | { 200 | private new string[] _someBucketName; 201 | 202 | protected override void Given() 203 | { 204 | base.Given(); 205 | _someBucketName = new[] {"counter-bucket-1", "counter-bucket-2"}; 206 | } 207 | 208 | protected override void When() 209 | { 210 | _result = SystemUnderTest.Increment(_someValueToSend, _someBucketName); 211 | } 212 | 213 | [Then] 214 | public void FormattedStringShouldBeCorrectlyFormatted() 215 | { 216 | var expectedString = new StringBuilder(); 217 | 218 | foreach (var stat in _someBucketName) 219 | { 220 | expectedString.AppendFormat(_someCulture, "{0}:{1}|c", stat, _someValueToSend); 221 | } 222 | 223 | _result.ShouldBe(expectedString.ToString()); 224 | } 225 | } 226 | 227 | private class WhenRegisteringEvent : WhenTestingCounters 228 | { 229 | protected override void When() 230 | { 231 | _result = SystemUnderTest.Event("foo"); 232 | } 233 | 234 | [Then] 235 | public void ShouldReturncounterLikeString() 236 | { 237 | _result.ShouldEndWith("|c"); 238 | } 239 | } 240 | } 241 | } 242 | --------------------------------------------------------------------------------