├── .gitignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── MIT-LICENCE.md ├── README.md ├── StatsdClient.sln ├── appveyor.yml ├── global.json ├── src ├── StatsdClient │ ├── AdressResolution.cs │ ├── IStatsd.cs │ ├── IStatsdClient.cs │ ├── Metrics.cs │ ├── MetricsConfig.cs │ ├── MetricsTimer.cs │ ├── NullStatsd.cs │ ├── Properties │ │ └── AssemblyInfo.cs │ ├── RandomGenerator.cs │ ├── Statsd.cs │ ├── StatsdClient.xproj │ ├── StatsdTCPClient.cs │ ├── StatsdUDPClient.cs │ ├── StopWatchFactory.cs │ ├── Stopwatch.cs │ └── project.json └── Tests │ ├── A_MetricsTests.cs │ ├── IPV4ParsingTests.cs │ ├── MetricIntegrationTests.cs │ ├── MetricsTests.cs │ ├── Properties │ └── AssemblyInfo.cs │ ├── RandomGeneratorUnitTests.cs │ ├── StatsdTests.cs │ ├── StatsdUDPTests.cs │ ├── TCPSmokeTests.cs │ ├── Tests.xproj │ ├── UDPSmokeTests.cs │ ├── UdpListener.cs │ └── project.json └── update-projectjson.ps1 /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.suo 8 | *.user 9 | *.userosscache 10 | *.sln.docstates 11 | *.vcxproj.filters 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Build results 17 | [Dd]ebug/ 18 | [Dd]ebugPublic/ 19 | [Rr]elease/ 20 | [Rr]eleases/ 21 | x64/ 22 | x86/ 23 | bld/ 24 | [Bb]in/ 25 | [Oo]bj/ 26 | [Ll]og/ 27 | 28 | # Visual Studio 2015 cache/options directory 29 | .vs/ 30 | # Uncomment if you have tasks that create the project's static files in wwwroot 31 | #wwwroot/ 32 | 33 | # MSTest test Results 34 | [Tt]est[Rr]esult*/ 35 | [Bb]uild[Ll]og.* 36 | 37 | # NUNIT 38 | *.VisualState.xml 39 | TestResult.xml 40 | 41 | # Build Results of an ATL Project 42 | [Dd]ebugPS/ 43 | [Rr]eleasePS/ 44 | dlldata.c 45 | 46 | # .NET Core 47 | project.lock.json 48 | project.fragment.lock.json 49 | artifacts/ 50 | **/Properties/launchSettings.json 51 | 52 | *_i.c 53 | *_p.c 54 | *_i.h 55 | *.ilk 56 | *.meta 57 | *.obj 58 | *.pch 59 | *.pdb 60 | *.pgc 61 | *.pgd 62 | *.rsp 63 | *.sbr 64 | *.tlb 65 | *.tli 66 | *.tlh 67 | *.tmp 68 | *.tmp_proj 69 | *.log 70 | *.vspscc 71 | *.vssscc 72 | .builds 73 | *.pidb 74 | *.svclog 75 | *.scc 76 | 77 | # Chutzpah Test files 78 | _Chutzpah* 79 | 80 | # Visual C++ cache files 81 | ipch/ 82 | *.aps 83 | *.ncb 84 | *.opendb 85 | *.opensdf 86 | *.sdf 87 | *.cachefile 88 | *.VC.db 89 | *.VC.VC.opendb 90 | 91 | # Visual Studio profiler 92 | *.psess 93 | *.vsp 94 | *.vspx 95 | *.sap 96 | 97 | # TFS 2012 Local Workspace 98 | $tf/ 99 | 100 | # Guidance Automation Toolkit 101 | *.gpState 102 | 103 | # ReSharper is a .NET coding add-in 104 | _ReSharper*/ 105 | *.[Rr]e[Ss]harper 106 | *.DotSettings.user 107 | 108 | # JustCode is a .NET coding add-in 109 | .JustCode 110 | 111 | # TeamCity is a build add-in 112 | _TeamCity* 113 | 114 | # DotCover is a Code Coverage Tool 115 | *.dotCover 116 | 117 | # Visual Studio code coverage results 118 | *.coverage 119 | *.coveragexml 120 | 121 | # NCrunch 122 | _NCrunch_* 123 | .*crunch*.local.xml 124 | nCrunchTemp_* 125 | 126 | # MightyMoose 127 | *.mm.* 128 | AutoTest.Net/ 129 | 130 | # Web workbench (sass) 131 | .sass-cache/ 132 | 133 | # Installshield output folder 134 | [Ee]xpress/ 135 | 136 | # DocProject is a documentation generator add-in 137 | DocProject/buildhelp/ 138 | DocProject/Help/*.HxT 139 | DocProject/Help/*.HxC 140 | DocProject/Help/*.hhc 141 | DocProject/Help/*.hhk 142 | DocProject/Help/*.hhp 143 | DocProject/Help/Html2 144 | DocProject/Help/html 145 | 146 | # Click-Once directory 147 | publish/ 148 | 149 | # Publish Web Output 150 | *.[Pp]ublish.xml 151 | *.azurePubxml 152 | # TODO: Comment the next line if you want to checkin your web deploy settings 153 | # but database connection strings (with potential passwords) will be unencrypted 154 | *.pubxml 155 | *.publishproj 156 | 157 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 158 | # checkin your Azure Web App publish settings, but sensitive information contained 159 | # in these scripts will be unencrypted 160 | PublishScripts/ 161 | 162 | # NuGet Packages 163 | *.nupkg 164 | # The packages folder can be ignored because of Package Restore 165 | **/packages/* 166 | # except build/, which is used as an MSBuild target. 167 | !**/packages/build/ 168 | # Uncomment if necessary however generally it will be regenerated when needed 169 | #!**/packages/repositories.config 170 | # NuGet v3's project.json files produces more ignoreable files 171 | *.nuget.props 172 | *.nuget.targets 173 | 174 | # Microsoft Azure Build Output 175 | csx/ 176 | *.build.csdef 177 | 178 | # Microsoft Azure Emulator 179 | ecf/ 180 | rcf/ 181 | 182 | # Windows Store app package directories and files 183 | AppPackages/ 184 | BundleArtifacts/ 185 | Package.StoreAssociation.xml 186 | _pkginfo.txt 187 | 188 | # Visual Studio cache files 189 | # files ending in .cache can be ignored 190 | *.[Cc]ache 191 | # but keep track of directories ending in .cache 192 | !*.[Cc]ache/ 193 | 194 | # Others 195 | ClientBin/ 196 | ~$* 197 | *~ 198 | *.dbmdl 199 | *.dbproj.schemaview 200 | *.jfm 201 | *.pfx 202 | *.publishsettings 203 | node_modules/ 204 | orleans.codegen.cs 205 | 206 | # Since there are multiple workflows, uncomment next line to ignore bower_components 207 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 208 | #bower_components/ 209 | 210 | # RIA/Silverlight projects 211 | Generated_Code/ 212 | 213 | # Backup & report files from converting an old project file 214 | # to a newer Visual Studio version. Backup files are not needed, 215 | # because we have git ;-) 216 | _UpgradeReport_Files/ 217 | Backup*/ 218 | UpgradeLog*.XML 219 | UpgradeLog*.htm 220 | 221 | # SQL Server files 222 | *.mdf 223 | *.ldf 224 | 225 | # Business Intelligence projects 226 | *.rdl.data 227 | *.bim.layout 228 | *.bim_*.settings 229 | 230 | # Microsoft Fakes 231 | FakesAssemblies/ 232 | 233 | # GhostDoc plugin setting file 234 | *.GhostDoc.xml 235 | 236 | # Node.js Tools for Visual Studio 237 | .ntvs_analysis.dat 238 | 239 | # Visual Studio 6 build log 240 | *.plg 241 | 242 | # Visual Studio 6 workspace options file 243 | *.opt 244 | 245 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 246 | *.vbw 247 | 248 | # Visual Studio LightSwitch build output 249 | **/*.HTMLClient/GeneratedArtifacts 250 | **/*.DesktopClient/GeneratedArtifacts 251 | **/*.DesktopClient/ModelManifest.xml 252 | **/*.Server/GeneratedArtifacts 253 | **/*.Server/ModelManifest.xml 254 | _Pvt_Extensions 255 | 256 | # Paket dependency manager 257 | .paket/paket.exe 258 | paket-files/ 259 | 260 | # FAKE - F# Make 261 | .fake/ 262 | 263 | # JetBrains Rider 264 | .idea/ 265 | *.sln.iml 266 | 267 | # CodeRush 268 | .cr/ 269 | 270 | # Python Tools for Visual Studio (PTVS) 271 | __pycache__/ 272 | *.pyc 273 | 274 | # Cake - Uncomment if you are using it 275 | # tools/ 276 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 3.0.86 2 | - **BREAKING:** Changes to lower level interfaces unlikely to affect most users, including `IStatsd` and `IStopwatch` 3 | - Fix threading bug when calling `Metrics.Send()` from multiple threads (thanks for helping track down @[bronumski](https://github.com/bronumski)!) 4 | - Fix timing reporting bug that significantly under reported multiple-millisecond timings (thanks @[arexsutton](https://github.com/arexsutton)!) 5 | - Fix casing for .NET Core dependency (thanks for pointing out @[mikemitchellrightside](https://github.com/mikemitchellrightside)!) 6 | 7 | ## 2.0.68 8 | - **BREAKING:** Drops support for < .NET 4.5 9 | - Add .NET Standard 1.3 support (thanks @[TerribleDev](https://github.com/TerribleDev)!) 10 | - Fix async support (previously would only measure async creation time) 11 | 12 | ## 1.4.51 13 | - Add a `Metrics.IsConfigured()` method which returns whether the Metrics class has been initialised yet (thanks @[dkhanaferov](https://github.com/dkhanaferov)!) 14 | 15 | ## 1.3.44 16 | - Add support for [TCP](https://github.com/etsy/statsd/blob/master/docs/server.md) sending via the `MetricsConfig.UseTcpProtocol` property (thanks @[pekiZG](https://github.com/pekiZG)!) 17 | 18 | ## 1.2.32 19 | - Fix the `Stopwatch` class to make it more consisten with .NET's and fix an overflow bug (thanks @[knocte](https://github.com/knocte)!) 20 | - Fix bug in how IPv4 addresses are resolved 21 | 22 | ## 1.1.0 23 | - Add support for [gauge delta values](https://github.com/etsy/statsd/blob/master/docs/metric_types.md#gauges) (thanks @[crunchie84](https://github.com/crunchie84)!) 24 | - Mark the `Metrics.Gauge()` method obsolete in favour of the new `Metrics.GaugeAbsoluteValue()` 25 | - Mark the `StatsdClient.Configuration.Naming` class obsolete in favour of setting `MetricsConfig.Prefix` when you call `Metrics.Configure()`, which reduces the code you need when actually sending metrics each time 26 | - Expose a sample rate for the disposable version of the timer (`Metrics.StartTimer()`) 27 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Thanks for thinking about contributing something to the project! All suggestions, bugs, even typo fixes are most appreciated. 2 | 3 | Please open an issue and have a chat about any features you're thinking of implementing before you start, so we can discuss the best way to go about it. 4 | 5 | ## Purpose 6 | 7 | Our aim when making this library was to keep it small, lightweight, and very easy to configure and use. We'd rather not have more complex features that'd bloat its size or complexity and are usually just for edgecases or much higher scale. We're also in favour of statics where possible for that ease of use with the idea that you should (almost) never need to test the values you're sending for metrics - they should be incidental to the code and not run during tests. 8 | 9 | Others have implemented features like this, such as the [high performance](https://github.com/Kyle2123/statsd-csharp-client) fork which does in-memory batching, or [StatsN](https://github.com/TryStatsN/StatsN) which skips statics in favour of testability. 10 | 11 | ## Running 12 | 13 | Clone out the repo, fire up with Visual Studio 2015+, and ideally use ReSharper to run all the tests. If you don't have ReSharper, no worries, just grab the matching NUnit binary and either run it from the command line or use its GUI. 14 | 15 | ## Deploying 16 | 17 | * Change major/minor versions in `appveyor.yml` if needed (build number is handled by AppVeyor) 18 | * Update `CHANGELOG.md` to note expected build number after AppVeyor runs 19 | * The NuGet package is generated as an artefact on AppVeyor. Grab that `*.nupkg` and upload it to NuGet.org. 20 | * Create a git tag in the `v1.2.3` format 21 | -------------------------------------------------------------------------------- /MIT-LICENCE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 Goncalo Pereira (github@goncalopereira.com) and all contributors 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Statsd Client - DEPRECATED 2 | ========================== 3 | 4 | This project is deprecated as of March 4, 2019. There's no official maintainer, and better alternatives exist. Security only related updates will be considered going forward. Currently active versions will remain on NuGet. 5 | 6 | We suggest migrating to [JustEat.StatsD](https://github.com/justeat/JustEat.StatsD), which has a very similar API to this project, plenty of additional features, and is actively maintained. 7 | 8 | Thanks to all the contributors and your many PRs and reported issues over the years, the numerous developers that have forked or been inspired by this client, and everyone that used it successfully in production! 9 | 10 | # THIS PROJECT IS DEPRECATED 11 | 12 | Original readme... 13 | 14 | --- 15 | 16 | [![Build status](https://ci.appveyor.com/api/projects/status/fklgn25u3k66qu3v?svg=true)](https://ci.appveyor.com/project/DarrellMozingo/statsd-csharp-client) 17 | [![NuGet Version](http://img.shields.io/nuget/v/StatsdClient.svg?style=flat)](https://www.nuget.org/packages/StatsdClient/) 18 | 19 | A .NET Standard compatible C# client to interface with Etsy's excellent [statsd](https://github.com/etsy/statsd) server. 20 | 21 | Install the client via NuGet with the [StatsdClient package](http://nuget.org/packages/StatsdClient). 22 | 23 | ## Usage 24 | 25 | At app startup, configure the `Metrics` class: 26 | 27 | ``` C# 28 | Metrics.Configure(new MetricsConfig 29 | { 30 | StatsdServerName = "hostname", 31 | Prefix = "myApp.prod" 32 | }); 33 | ``` 34 | 35 | Start measuring all the things! 36 | 37 | ``` C# 38 | Metrics.Counter("stat-name"); 39 | Metrics.Time(() => myMethod(), "timer-name"); 40 | var result = Metrics.Time(() => GetResult(), "timer-name"); 41 | var result = await Metrics.Time(async () => await myAsyncMethod(), "timer-name"); 42 | Metrics.GaugeAbsoluteValue("gauge-name", 35); 43 | Metrics.GaugeDelta("gauge-name", -5); 44 | Metrics.Set("something-special", "3"); 45 | 46 | using (Metrics.StartTimer("stat-name")) 47 | { 48 | // Lots of code here 49 | } 50 | ``` 51 | 52 | ## Advanced Features 53 | 54 | To enable these, see the `MetricsConfig` class discussed above. 55 | 56 | * `UseTcpProtocol`: sends metrics to statsd via TCP. While supported, UDP is recommended in most cases. If you need TCP reliability, a relay service running locally on the server which you'd send UDP to, and it would relay via TCP, is advised. 57 | 58 | ## Contributing 59 | 60 | See the [Contributing](CONTRIBUTING.md) guidelines. 61 | -------------------------------------------------------------------------------- /StatsdClient.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 14 4 | VisualStudioVersion = 14.0.25420.1 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "StatsdClient", "src\StatsdClient\StatsdClient.xproj", "{0000B49C-9D92-4C54-B902-7067E13A09BD}" 7 | EndProject 8 | Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Tests", "src\Tests\Tests.xproj", "{35C84FBC-41B2-4F48-9F24-6B98AA94001B}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Release|Any CPU = Release|Any CPU 14 | EndGlobalSection 15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 16 | {0000B49C-9D92-4C54-B902-7067E13A09BD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 17 | {0000B49C-9D92-4C54-B902-7067E13A09BD}.Debug|Any CPU.Build.0 = Debug|Any CPU 18 | {0000B49C-9D92-4C54-B902-7067E13A09BD}.Release|Any CPU.ActiveCfg = Release|Any CPU 19 | {0000B49C-9D92-4C54-B902-7067E13A09BD}.Release|Any CPU.Build.0 = Release|Any CPU 20 | {35C84FBC-41B2-4F48-9F24-6B98AA94001B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {35C84FBC-41B2-4F48-9F24-6B98AA94001B}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {35C84FBC-41B2-4F48-9F24-6B98AA94001B}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {35C84FBC-41B2-4F48-9F24-6B98AA94001B}.Release|Any CPU.Build.0 = Release|Any CPU 24 | EndGlobalSection 25 | GlobalSection(SolutionProperties) = preSolution 26 | HideSolutionNode = FALSE 27 | EndGlobalSection 28 | EndGlobal 29 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | version: 3.0.{build} 2 | configuration: Release 3 | before_build: 4 | # matches pinned version in global.json 5 | - ps: invoke-webrequest https://go.microsoft.com/fwlink/?LinkID=830694 -OutFile core.exe 6 | - ps: .\core.exe /install /quiet /norestart 7 | build_script: 8 | - ps: dotnet restore 9 | - ps: dotnet build -c Release .\src\StatsdClient 10 | - ps: dotnet build -c Release .\src\Tests 11 | test_script: 12 | - dotnet test .\src\Tests 13 | after_test: 14 | - ps: powershell -File .\update-projectjson.ps1 15 | - ps: dotnet pack -c Release .\src\StatsdClient --output . 16 | artifacts: 17 | - path: StatsdClient.*.nupkg 18 | -------------------------------------------------------------------------------- /global.json: -------------------------------------------------------------------------------- 1 | { 2 | "sdk": { 3 | "version": "1.0.0-preview2-003131" 4 | } 5 | } -------------------------------------------------------------------------------- /src/StatsdClient/AdressResolution.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using System.Net; 3 | using System.Net.Sockets; 4 | 5 | namespace StatsdClient 6 | { 7 | public class AddressResolution 8 | { 9 | public static IPAddress GetIpv4Address(string name) 10 | { 11 | IPAddress ipAddress; 12 | var isValidIpAddress = IPAddress.TryParse(name, out ipAddress); 13 | 14 | if (!isValidIpAddress) 15 | { 16 | ipAddress = GetIpFromHostname(name); 17 | } 18 | 19 | return ipAddress; 20 | } 21 | 22 | private static IPAddress GetIpFromHostname(string name) 23 | { 24 | #if NETFULL 25 | var addressList = Dns.GetHostEntry(name).AddressList; 26 | #else 27 | var addressList = Dns.GetHostEntryAsync(name).GetAwaiter().GetResult().AddressList; 28 | #endif 29 | var ipv4Addresses = addressList.Where(x => x.AddressFamily != AddressFamily.InterNetworkV6); 30 | 31 | return ipv4Addresses.First(); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/StatsdClient/IStatsd.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace StatsdClient 4 | { 5 | public interface IStatsd 6 | { 7 | void Send(string name, int value) where TCommandType : IAllowsInteger; 8 | void Add(string name, int value) where TCommandType : IAllowsInteger; 9 | 10 | void Send(string name, double value) where TCommandType : IAllowsDouble; 11 | void Add(string name, double value) where TCommandType : IAllowsDouble; 12 | void Send(string name, double value, bool isDeltaValue) where TCommandType : IAllowsDouble, IAllowsDelta; 13 | 14 | void Send(string name, int value, double sampleRate) where TCommandType : IAllowsInteger, IAllowsSampleRate; 15 | void Add(string name, int value, double sampleRate) where TCommandType : IAllowsInteger, IAllowsSampleRate; 16 | 17 | void Send(string name, string value) where TCommandType : IAllowsString; 18 | 19 | void Send(); 20 | 21 | void Add(Action actionToTime, string statName, double sampleRate=1); 22 | void Send(Action actionToTime, string statName, double sampleRate=1); 23 | } 24 | } -------------------------------------------------------------------------------- /src/StatsdClient/IStatsdClient.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace StatsdClient 4 | { 5 | public interface IStatsdClient : IDisposable 6 | { 7 | void Send(string command); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/StatsdClient/Metrics.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | 4 | namespace StatsdClient 5 | { 6 | public static class Metrics 7 | { 8 | private static IStatsd _statsD = new NullStatsd(); 9 | private static IStatsdClient _statsdClient; 10 | private static string _prefix; 11 | 12 | /// 13 | /// Configures the Metric class with a configuration. Call this once at application startup (Main(), Global.asax, etc). 14 | /// 15 | /// Configuration settings. 16 | public static void Configure(MetricsConfig config) 17 | { 18 | if (config == null) 19 | { 20 | throw new ArgumentNullException("config"); 21 | } 22 | 23 | _prefix = config.Prefix ?? ""; 24 | _prefix = _prefix.TrimEnd('.'); 25 | CreateStatsD(config); 26 | } 27 | 28 | private static void CreateStatsD(MetricsConfig config) 29 | { 30 | if (_statsdClient != null) 31 | { 32 | _statsdClient.Dispose(); 33 | } 34 | 35 | _statsdClient = null; 36 | 37 | if (!string.IsNullOrEmpty(config.StatsdServerName)) 38 | { 39 | if (config.UseTcpProtocol) 40 | { 41 | _statsdClient = new StatsdTCPClient(config.StatsdServerName, config.StatsdServerPort); 42 | } 43 | else 44 | { 45 | _statsdClient = new StatsdUDPClient(config.StatsdServerName, config.StatsdServerPort, config.StatsdMaxUDPPacketSize); 46 | } 47 | _statsD = new Statsd(_statsdClient); 48 | } 49 | } 50 | 51 | /// 52 | /// Send a counter value. 53 | /// 54 | /// Name of the metric. 55 | /// Value of the counter. Defaults to 1. 56 | /// Sample rate to reduce the load on your metric server. Defaults to 1 (100%). 57 | public static void Counter(string statName, int value = 1, double sampleRate = 1) 58 | { 59 | _statsD.Send(BuildNamespacedStatName(statName), value, sampleRate); 60 | } 61 | 62 | /// 63 | /// Modify the current value of the gauge with the given value. 64 | /// 65 | /// Name of the metric. 66 | /// 67 | public static void GaugeDelta(string statName, double deltaValue) 68 | { 69 | _statsD.Send(BuildNamespacedStatName(statName), deltaValue, true); 70 | } 71 | 72 | /// 73 | /// Set the gauge to the given absolute value. 74 | /// 75 | /// Name of the metric. 76 | /// Absolute value of the gauge to set. 77 | public static void GaugeAbsoluteValue(string statName, double absoluteValue) 78 | { 79 | _statsD.Send(BuildNamespacedStatName(statName), absoluteValue, false); 80 | } 81 | 82 | /// 83 | /// Send a manually timed value. 84 | /// 85 | /// Name of the metric. 86 | /// Elapsed miliseconds of the event. 87 | /// Sample rate to reduce the load on your metric server. Defaults to 1 (100%). 88 | public static void Timer(string statName, int value, double sampleRate = 1) 89 | { 90 | _statsD.Send(BuildNamespacedStatName(statName), value, sampleRate); 91 | } 92 | 93 | /// 94 | /// Time a given piece of code (with a using block) and send the elapsed miliseconds 95 | /// 96 | /// Name of the metric. 97 | /// A disposable object that will record & send the metric. 98 | /// Sample rate to reduce the load on your metric server. Defaults to 1 (100%). 99 | public static IDisposable StartTimer(string name, double sampleRate = 1) 100 | { 101 | return new MetricsTimer(name, sampleRate); 102 | } 103 | 104 | /// 105 | /// Time a given piece of code (with a lambda) and send the elapsed miliseconds. 106 | /// 107 | /// The code to time. 108 | /// Name of the metric. 109 | /// Sample rate to reduce the load on your metric server. Defaults to 1 (100%). 110 | public static void Time(Action action, string statName, double sampleRate = 1) 111 | { 112 | _statsD.Send(action, BuildNamespacedStatName(statName), sampleRate); 113 | } 114 | 115 | /// 116 | /// Time a given piece of async code and send the elapsed miliseconds. 117 | /// 118 | /// The code to time. 119 | /// Name of the metric. 120 | /// Sample rate to reduce the load on your metric server. Defaults to 1 (100%). 121 | public static async Task Time(Func func, string statName, double sampleRate = 1) 122 | { 123 | using (StartTimer(statName, sampleRate)) 124 | { 125 | await func(); 126 | } 127 | } 128 | 129 | /// 130 | /// Time a given piece of code and send the elapsed miliseconds. 131 | /// 132 | /// The code to time. 133 | /// Name of the metric. 134 | /// Sample rate to reduce the load on your metric server. Defaults to 1 (100%). 135 | /// Return value of the function. 136 | public static T Time(Func func, string statName, double sampleRate = 1) 137 | { 138 | using (StartTimer(statName, sampleRate)) 139 | { 140 | return func(); 141 | } 142 | } 143 | 144 | /// 145 | /// Time a given piece of async code and send the elapsed miliseconds. 146 | /// 147 | /// The code to time. 148 | /// Name of the metric. 149 | /// Sample rate to reduce the load on your metric server. Defaults to 1 (100%). 150 | /// Return value of the function. 151 | public static async Task Time(Func> func, string statName, double sampleRate = 1) 152 | { 153 | using (StartTimer(statName, sampleRate)) 154 | { 155 | return await func(); 156 | } 157 | } 158 | 159 | /// 160 | /// Store a unique occurence of an event between flushes. 161 | /// 162 | /// Name of the metric. 163 | /// Value to set. 164 | public static void Set(string statName, string value) 165 | { 166 | _statsD.Send(BuildNamespacedStatName(statName), value); 167 | } 168 | 169 | private static string BuildNamespacedStatName(string statName) 170 | { 171 | if (string.IsNullOrEmpty(_prefix)) 172 | { 173 | return statName; 174 | } 175 | 176 | return _prefix + "." + statName; 177 | } 178 | 179 | /// 180 | /// Determine if the Metrics instance has been configured previously. 181 | /// 182 | public static bool IsConfigured() 183 | { 184 | return _statsD != null && !(_statsD is NullStatsd); 185 | } 186 | } 187 | } -------------------------------------------------------------------------------- /src/StatsdClient/MetricsConfig.cs: -------------------------------------------------------------------------------- 1 | namespace StatsdClient 2 | { 3 | public class MetricsConfig 4 | { 5 | /// 6 | /// The full host name of your statsd server. 7 | /// 8 | public string StatsdServerName { get; set; } 9 | 10 | /// 11 | /// Uses the statsd default port if not specified (8125). 12 | /// 13 | public int StatsdServerPort { get; set; } 14 | 15 | /// 16 | /// Allows you to override the maximum UDP packet size (in bytes) if your setup requires that. Defaults to 512. 17 | /// 18 | public int StatsdMaxUDPPacketSize { get; set; } 19 | 20 | /// 21 | /// Allows you to use TCP client 22 | /// 23 | public bool UseTcpProtocol { get; set; } 24 | 25 | /// 26 | /// Allows you to optionally specify a stat name prefix for all your stats. 27 | /// Eg setting it to "Production.MyApp", then sending a counter with the name "Value" will result in a final stat name of "Production.MyApp.Value". 28 | /// 29 | public string Prefix { get; set; } 30 | 31 | public const int DefaultStatsdServerPort = 8125; 32 | public const int DefaultStatsdMaxUDPPacketSize = 512; 33 | 34 | public MetricsConfig() 35 | { 36 | StatsdServerPort = DefaultStatsdServerPort; 37 | StatsdMaxUDPPacketSize = DefaultStatsdMaxUDPPacketSize; 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/StatsdClient/MetricsTimer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace StatsdClient 4 | { 5 | public class MetricsTimer : IDisposable 6 | { 7 | private readonly string _name; 8 | private readonly IStopwatch _stopWatch; 9 | private bool _disposed; 10 | private readonly double _sampleRate; 11 | 12 | public MetricsTimer(string name, double sampleRate) 13 | { 14 | _name = name; 15 | _sampleRate = sampleRate; 16 | _stopWatch = new Stopwatch(); 17 | _stopWatch.Start(); 18 | } 19 | 20 | public void Dispose() 21 | { 22 | if (!_disposed) 23 | { 24 | _disposed = true; 25 | _stopWatch.Stop(); 26 | Metrics.Timer(_name, _stopWatch.ElapsedMilliseconds, _sampleRate); 27 | } 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /src/StatsdClient/NullStatsd.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace StatsdClient 5 | { 6 | public class NullStatsd : IStatsd 7 | { 8 | public NullStatsd() 9 | { 10 | Commands = new List(); 11 | } 12 | 13 | public List Commands { get; private set; } 14 | 15 | public void Send(string name, int value) where TCommandType : IAllowsInteger 16 | { 17 | } 18 | 19 | public void Add(string name, int value) where TCommandType : IAllowsInteger 20 | { 21 | } 22 | 23 | public void Send(string name, double value) where TCommandType : IAllowsDouble 24 | { 25 | } 26 | 27 | public void Add(string name, double value) where TCommandType : IAllowsDouble 28 | { 29 | } 30 | 31 | public void Send(string name, int value, double sampleRate) 32 | where TCommandType : IAllowsInteger, IAllowsSampleRate 33 | { 34 | } 35 | 36 | public void Add(string name, int value, double sampleRate) 37 | where TCommandType : IAllowsInteger, IAllowsSampleRate 38 | { 39 | } 40 | 41 | public void Send(string name, string value) where TCommandType : IAllowsString 42 | { 43 | } 44 | 45 | public void Send() 46 | { 47 | } 48 | 49 | public void Add(Action actionToTime, string statName, double sampleRate = 1) 50 | { 51 | actionToTime(); 52 | } 53 | 54 | public void Send(Action actionToTime, string statName, double sampleRate = 1) 55 | { 56 | actionToTime(); 57 | } 58 | 59 | public void Send(string name, double value, bool isDeltaValue) where TCommandType : IAllowsDouble, IAllowsDelta 60 | { 61 | } 62 | } 63 | } -------------------------------------------------------------------------------- /src/StatsdClient/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("StatsdClient")] 9 | [assembly: AssemblyDescription("Statsd Client")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("StatsdClient")] 13 | [assembly: AssemblyCopyright("Copyright © 2012")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | [assembly: InternalsVisibleTo("Tests")] 17 | 18 | // Setting ComVisible to false makes the types in this assembly not visible 19 | // to COM components. If you need to access a type in this assembly from 20 | // COM, set the ComVisible attribute to true on that type. 21 | [assembly: ComVisible(false)] 22 | 23 | // The following GUID is for the ID of the typelib if this project is exposed to COM 24 | [assembly: Guid("ecebfa48-5557-4fe6-84a6-c0b1e3ece14c")] -------------------------------------------------------------------------------- /src/StatsdClient/RandomGenerator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace StatsdClient 4 | { 5 | public interface IRandomGenerator 6 | { 7 | bool ShouldSend(double sampleRate); 8 | } 9 | 10 | public class RandomGenerator : IRandomGenerator 11 | { 12 | [ThreadStatic] 13 | static Random _random; 14 | 15 | private static Random Random 16 | { 17 | get 18 | { 19 | if (_random != null) return _random; 20 | return _random = new Random(Guid.NewGuid().GetHashCode()); 21 | } 22 | } 23 | 24 | public bool ShouldSend(double sampleRate) 25 | { 26 | return Random.NextDouble() < sampleRate; 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /src/StatsdClient/Statsd.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.Collections.Generic; 4 | using System.Diagnostics; 5 | using System.Globalization; 6 | 7 | namespace StatsdClient 8 | { 9 | public interface IAllowsSampleRate { } 10 | public interface IAllowsDelta { } 11 | 12 | public interface IAllowsDouble { } 13 | public interface IAllowsInteger { } 14 | public interface IAllowsString { } 15 | 16 | public class Statsd : IStatsd 17 | { 18 | private readonly object _commandCollectionLock = new object(); 19 | 20 | private IStopWatchFactory StopwatchFactory { get; set; } 21 | private IStatsdClient StatsdClient { get; set; } 22 | private IRandomGenerator RandomGenerator { get; set; } 23 | 24 | private readonly string _prefix; 25 | 26 | internal ConcurrentQueue Commands { get; private set; } 27 | 28 | public class Counting : IAllowsSampleRate, IAllowsInteger { } 29 | public class Timing : IAllowsSampleRate, IAllowsInteger { } 30 | public class Gauge : IAllowsDouble, IAllowsDelta { } 31 | public class Histogram : IAllowsInteger { } 32 | public class Meter : IAllowsInteger { } 33 | public class Set : IAllowsString { } 34 | 35 | private readonly IDictionary _commandToUnit = new Dictionary 36 | { 37 | {typeof (Counting), "c"}, 38 | {typeof (Timing), "ms"}, 39 | {typeof (Gauge), "g"}, 40 | {typeof (Histogram), "h"}, 41 | {typeof (Meter), "m"}, 42 | {typeof (Set), "s"} 43 | }; 44 | 45 | public Statsd(IStatsdClient statsdClient, IRandomGenerator randomGenerator, IStopWatchFactory stopwatchFactory, string prefix) 46 | { 47 | Commands = new ConcurrentQueue(); 48 | StopwatchFactory = stopwatchFactory; 49 | StatsdClient = statsdClient; 50 | RandomGenerator = randomGenerator; 51 | _prefix = prefix; 52 | } 53 | 54 | public Statsd(IStatsdClient statsdClient, IRandomGenerator randomGenerator, IStopWatchFactory stopwatchFactory) 55 | : this(statsdClient, randomGenerator, stopwatchFactory, string.Empty) { } 56 | 57 | public Statsd(IStatsdClient statsdClient) 58 | : this(statsdClient, new RandomGenerator(), new StopWatchFactory(), string.Empty) { } 59 | 60 | public void Send(string name, int value) where TCommandType : IAllowsInteger 61 | { 62 | var command = GetCommand(name, value.ToString(CultureInfo.InvariantCulture), _commandToUnit[typeof(TCommandType)], 1); 63 | SendSingle(command); 64 | } 65 | 66 | public void Send(string name, double value) where TCommandType : IAllowsDouble 67 | { 68 | var formattedValue = string.Format(CultureInfo.InvariantCulture,"{0:F15}", value); 69 | var command = GetCommand(name, formattedValue, _commandToUnit[typeof(TCommandType)], 1); 70 | SendSingle(command); 71 | } 72 | 73 | public void Send(string name, double value, bool isDeltaValue) where TCommandType : IAllowsDouble, IAllowsDelta 74 | { 75 | if (isDeltaValue) 76 | { 77 | // Sending delta values to StatsD requires a value modifier sign (+ or -) which we append 78 | // using this custom format with a different formatting rule for negative/positive and zero values 79 | // https://msdn.microsoft.com/en-us/library/0c899ak8.aspx#SectionSeparator 80 | const string deltaValueStringFormat = "{0:+#.###;-#.###;+0}"; 81 | var formattedValue = string.Format(CultureInfo.InvariantCulture, deltaValueStringFormat, value); 82 | var command = GetCommand(name, formattedValue, _commandToUnit[typeof(TCommandType)], 1); 83 | SendSingle(command); 84 | } 85 | else 86 | { 87 | Send(name, value); 88 | } 89 | } 90 | 91 | public void Send(string name, string value) where TCommandType : IAllowsString 92 | { 93 | var command = GetCommand(name, Convert.ToString(value, CultureInfo.InvariantCulture), _commandToUnit[typeof(TCommandType)], 1); 94 | SendSingle(command); 95 | } 96 | 97 | public void Add(string name, int value) where TCommandType : IAllowsInteger 98 | { 99 | Commands.Enqueue(GetCommand(name, value.ToString(CultureInfo.InvariantCulture), _commandToUnit[typeof (TCommandType)], 1)); 100 | } 101 | 102 | public void Add(string name, double value) where TCommandType : IAllowsDouble 103 | { 104 | Commands.Enqueue(GetCommand(name, String.Format(CultureInfo.InvariantCulture,"{0:F15}", value), _commandToUnit[typeof(TCommandType)], 1)); 105 | } 106 | 107 | public void Send(string name, int value, double sampleRate) where TCommandType : IAllowsInteger, IAllowsSampleRate 108 | { 109 | if (!RandomGenerator.ShouldSend(sampleRate)) 110 | { 111 | return; 112 | } 113 | 114 | var command = GetCommand(name, value.ToString(CultureInfo.InvariantCulture), _commandToUnit[typeof(TCommandType)], sampleRate); 115 | SendSingle(command); 116 | } 117 | 118 | public void Add(string name, int value, double sampleRate) where TCommandType : IAllowsInteger, IAllowsSampleRate 119 | { 120 | if (RandomGenerator.ShouldSend(sampleRate)) 121 | { 122 | Commands.Enqueue(GetCommand(name, value.ToString(CultureInfo.InvariantCulture), _commandToUnit[typeof(TCommandType)], sampleRate)); 123 | } 124 | } 125 | 126 | private void SendSingle(string command) 127 | { 128 | try 129 | { 130 | StatsdClient.Send(command); 131 | } 132 | catch (Exception e) 133 | { 134 | Debug.WriteLine(e.Message); 135 | } 136 | } 137 | 138 | public void Send() 139 | { 140 | try 141 | { 142 | StatsdClient.Send(string.Join("\n", Commands.ToArray())); 143 | AtomicallyClearQueue(); 144 | } 145 | catch(Exception e) 146 | { 147 | Debug.WriteLine(e.Message); 148 | } 149 | } 150 | 151 | private void AtomicallyClearQueue() 152 | { 153 | lock (_commandCollectionLock) 154 | { 155 | Commands = new ConcurrentQueue(); 156 | } 157 | } 158 | 159 | private string GetCommand(string name, string value, string unit, double sampleRate) 160 | { 161 | var format = sampleRate == 1 ? "{0}:{1}|{2}" : "{0}:{1}|{2}|@{3}"; 162 | return string.Format(CultureInfo.InvariantCulture, format, _prefix + name, value, unit, sampleRate); 163 | } 164 | 165 | public void Add(Action actionToTime, string statName, double sampleRate=1) 166 | { 167 | HandleTiming(actionToTime, statName, sampleRate, Add); 168 | } 169 | 170 | public void Send(Action actionToTime, string statName, double sampleRate=1) 171 | { 172 | HandleTiming(actionToTime, statName, sampleRate, Send); 173 | } 174 | 175 | private void HandleTiming(Action actionToTime, string statName, double sampleRate, Action actionToStore) 176 | { 177 | var stopwatch = StopwatchFactory.Get(); 178 | 179 | try 180 | { 181 | stopwatch.Start(); 182 | actionToTime(); 183 | } 184 | finally 185 | { 186 | stopwatch.Stop(); 187 | if (RandomGenerator.ShouldSend(sampleRate)) 188 | { 189 | actionToStore(statName, stopwatch.ElapsedMilliseconds); 190 | } 191 | } 192 | } 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /src/StatsdClient/StatsdClient.xproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 14.0 5 | $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) 6 | 7 | 8 | 9 | 10 | 0000b49c-9d92-4c54-b902-7067e13a09bd 11 | StatsdClient.Core 12 | .\obj 13 | .\bin\ 14 | v4.5.2 15 | 16 | 17 | 18 | 2.0 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/StatsdClient/StatsdTCPClient.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Net; 4 | using System.Net.Sockets; 5 | using System.Text; 6 | 7 | namespace StatsdClient 8 | { 9 | public class StatsdTCPClient : IStatsdClient 10 | { 11 | private IPEndPoint IpEndpoint { get; } 12 | private readonly Socket _clientSocket; 13 | 14 | public StatsdTCPClient(string name, int port = 8125) 15 | { 16 | try 17 | { 18 | _clientSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); 19 | IpEndpoint = new IPEndPoint(AddressResolution.GetIpv4Address(name), port); 20 | } 21 | catch (Exception ex) 22 | { 23 | Debug.WriteLine(ex.Message); 24 | } 25 | } 26 | 27 | public void Send(string command) 28 | { 29 | Send(Encoding.ASCII.GetBytes(command)); 30 | } 31 | 32 | private void Send(byte[] encodedCommand) 33 | { 34 | try 35 | { 36 | _clientSocket.Connect(IpEndpoint); 37 | _clientSocket.SendTo(encodedCommand, encodedCommand.Length, SocketFlags.None, IpEndpoint); 38 | } 39 | catch (Exception ex) 40 | { 41 | Debug.WriteLine(ex.Message); 42 | } 43 | finally 44 | { 45 | _clientSocket.Shutdown(SocketShutdown.Both); 46 | CloseSocket(_clientSocket); 47 | } 48 | } 49 | 50 | private static void CloseSocket(Socket socket) 51 | { 52 | #if NETFULL 53 | socket.Close(); 54 | #else 55 | socket.Dispose(); 56 | #endif 57 | } 58 | 59 | #region IDisposable Support 60 | private bool _disposed; 61 | 62 | protected virtual void Dispose(bool disposing) 63 | { 64 | if (_disposed) return; 65 | 66 | if (disposing) 67 | { 68 | if (_clientSocket != null) 69 | { 70 | try 71 | { 72 | CloseSocket(_clientSocket); 73 | } 74 | catch (Exception ex) 75 | { 76 | Debug.WriteLine(ex.Message); 77 | } 78 | } 79 | } 80 | _disposed = true; 81 | } 82 | 83 | ~StatsdTCPClient() { 84 | Dispose(false); 85 | } 86 | 87 | public void Dispose() 88 | { 89 | Dispose(true); 90 | GC.SuppressFinalize(this); 91 | } 92 | #endregion 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/StatsdClient/StatsdUDPClient.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net; 3 | using System.Net.Sockets; 4 | using System.Text; 5 | 6 | namespace StatsdClient 7 | { 8 | public class StatsdUDPClient : IStatsdClient 9 | { 10 | public IPEndPoint IPEndpoint { get; private set; } 11 | 12 | private readonly int _maxUdpPacketSizeBytes; 13 | private readonly Socket _clientSocket; 14 | 15 | /// 16 | /// Creates a new StatsdUDP class for lower level access to statsd. 17 | /// 18 | /// Hostname or IP (v4) address of the statsd server. 19 | /// Port of the statsd server. Default is 8125. 20 | /// Max packet size, in bytes. This is useful to tweak if your MTU size is different than normal. Set to 0 for no limit. Default is MetricsConfig.DefaultStatsdMaxUDPPacketSize. 21 | public StatsdUDPClient(string name, int port = 8125, int maxUdpPacketSizeBytes = MetricsConfig.DefaultStatsdMaxUDPPacketSize) 22 | { 23 | _maxUdpPacketSizeBytes = maxUdpPacketSizeBytes; 24 | 25 | _clientSocket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); 26 | 27 | IPEndpoint = new IPEndPoint(AddressResolution.GetIpv4Address(name), port); 28 | } 29 | 30 | public void Send(string command) 31 | { 32 | Send(Encoding.ASCII.GetBytes(command)); 33 | } 34 | 35 | private void Send(byte[] encodedCommand) 36 | { 37 | if (_maxUdpPacketSizeBytes > 0 && encodedCommand.Length > _maxUdpPacketSizeBytes) 38 | { 39 | // If the command is too big to send, linear search backwards from the maximum 40 | // packet size to see if we can find a newline delimiting two stats. If we can, 41 | // split the message across the newline and try sending both componenets individually 42 | var newline = Encoding.ASCII.GetBytes("\n")[0]; 43 | for (var i = _maxUdpPacketSizeBytes; i > 0; i--) 44 | { 45 | if (encodedCommand[i] != newline) 46 | { 47 | continue; 48 | } 49 | 50 | var encodedCommandFirst = new byte[i]; 51 | Array.Copy(encodedCommand, encodedCommandFirst, encodedCommandFirst.Length); // encodedCommand[0..i-1] 52 | Send(encodedCommandFirst); 53 | 54 | var remainingCharacters = encodedCommand.Length - i - 1; 55 | if (remainingCharacters > 0) 56 | { 57 | var encodedCommandSecond = new byte[remainingCharacters]; 58 | Array.Copy(encodedCommand, i + 1, encodedCommandSecond, 0, encodedCommandSecond.Length); // encodedCommand[i+1..end] 59 | Send(encodedCommandSecond); 60 | } 61 | 62 | return; // We're done here if we were able to split the message. 63 | // At this point we found an oversized message but we weren't able to find a 64 | // newline to split upon. We'll still send it to the UDP socket, which upon sending an oversized message 65 | // will fail silently if the user is running in release mode or report a SocketException if the user is 66 | // running in debug mode. 67 | // Since we're conservative with our MAX_UDP_PACKET_SIZE, the oversized message might even 68 | // be sent without issue. 69 | } 70 | } 71 | 72 | _clientSocket.SendTo(encodedCommand, encodedCommand.Length, SocketFlags.None, IPEndpoint); 73 | } 74 | 75 | //reference : https://lostechies.com/chrispatterson/2012/11/29/idisposable-done-right/ 76 | private bool _disposed; 77 | public void Dispose() 78 | { 79 | Dispose(true); 80 | GC.SuppressFinalize(this); 81 | } 82 | ~StatsdUDPClient() 83 | { 84 | // Finalizer calls Dispose(false) 85 | Dispose(false); 86 | } 87 | protected virtual void Dispose(bool disposing) 88 | { 89 | if (_disposed) 90 | return; 91 | 92 | if (disposing) 93 | { 94 | if (_clientSocket != null) 95 | { 96 | try 97 | { 98 | #if NETFULL 99 | _clientSocket.Close(); 100 | #else 101 | _clientSocket.Dispose(); 102 | #endif 103 | } 104 | catch (Exception) 105 | { 106 | //Swallow since we are not using a logger, should we add LibLog and start logging?? 107 | } 108 | 109 | } 110 | } 111 | 112 | _disposed = true; 113 | } 114 | } 115 | } -------------------------------------------------------------------------------- /src/StatsdClient/StopWatchFactory.cs: -------------------------------------------------------------------------------- 1 | namespace StatsdClient 2 | { 3 | public interface IStopWatchFactory 4 | { 5 | IStopwatch Get(); 6 | } 7 | 8 | public class StopWatchFactory : IStopWatchFactory 9 | { 10 | public IStopwatch Get() 11 | { 12 | return new Stopwatch(); 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /src/StatsdClient/Stopwatch.cs: -------------------------------------------------------------------------------- 1 | namespace StatsdClient 2 | { 3 | public interface IStopwatch 4 | { 5 | void Start(); 6 | void Stop(); 7 | int ElapsedMilliseconds { get; } 8 | } 9 | 10 | public class Stopwatch : IStopwatch 11 | { 12 | private readonly System.Diagnostics.Stopwatch _stopwatch = new System.Diagnostics.Stopwatch(); 13 | 14 | public void Start() 15 | { 16 | _stopwatch.Start(); 17 | } 18 | 19 | public void Stop() 20 | { 21 | _stopwatch.Stop(); 22 | } 23 | 24 | public int ElapsedMilliseconds 25 | { 26 | get { return (int)_stopwatch.ElapsedMilliseconds; } 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /src/StatsdClient/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.0-*", 3 | "authors": [ "Goncalo Pereira", "Darrell Mozingo" ], 4 | "title": "Statsd C# Client", 5 | "description": "Statsd client for C#, providing a full set of counter/timer/gauge/set functionality in an easy to use static. Helpers are provided for easily timing chunks of code too.", 6 | "name": "StatsdClient", 7 | "packOptions": { 8 | "releaseNotes": "See CHANGELOG.md in the project's GitHub repository.", 9 | "tags": [ "stats", "statsd", "metrics" ], 10 | "licenseUrl": "https://github.com/Pereingo/statsd-csharp-client/blob/master/MIT-LICENSE.md", 11 | "projectUrl": "https://github.com/Pereingo/statsd-csharp-client", 12 | "repository": { 13 | "type:": "git", 14 | "url": "https://github.com/Pereingo/statsd-csharp-client" 15 | } 16 | }, 17 | 18 | "buildOptions": { 19 | "warningsAsErrors": true 20 | }, 21 | 22 | "frameworks": { 23 | "net45": { 24 | "buildOptions": { 25 | "define": [ "NETFULL" ] 26 | } 27 | }, 28 | "netstandard1.3": { 29 | "dependencies": { 30 | "NETStandard.Library": "1.6.0", 31 | "System.Net.NameResolution": "4.0.0" 32 | }, 33 | "imports": "dnxcore50" 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Tests/A_MetricsTests.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using StatsdClient; 3 | 4 | namespace Tests 5 | { 6 | /// 7 | /// Metrics is static (not thread static), so to consistently test this before .Configure() is called just doing a cheeky A_* naming so it's first alphabetically. 8 | /// 9 | public class A_MetricsTests 10 | { 11 | [Test] 12 | public void defaults_to_null_statsd_to_not_blow_up_when_configure_is_not_called() 13 | { 14 | Assert.DoesNotThrow(() => Metrics.Counter("stat")); 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /src/Tests/IPV4ParsingTests.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using StatsdClient; 3 | 4 | namespace Tests 5 | { 6 | [TestFixture] 7 | public class IPV4ParsingTests 8 | { 9 | private const int RandomUnusedLocalPort = 23483; 10 | 11 | [Test] 12 | public void ipv4_parsing_works_with_hostname() 13 | { 14 | var statsdUdp = new StatsdUDPClient("localhost", RandomUnusedLocalPort); 15 | Assert.That(statsdUdp.IPEndpoint.Address.ToString(), Does.Contain("127.0.0.1")); 16 | } 17 | 18 | [Test] 19 | public void ipv4_parsing_works_with_ip() 20 | { 21 | var statsdUdp = new StatsdUDPClient("127.0.0.1", RandomUnusedLocalPort); 22 | Assert.That(statsdUdp.IPEndpoint.Address.ToString(), Does.Contain("127.0.0.1")); 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /src/Tests/MetricIntegrationTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text.RegularExpressions; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using NUnit.Framework; 6 | using StatsdClient; 7 | using Tests.Helpers; 8 | 9 | namespace Tests 10 | { 11 | [TestFixture] 12 | public class MetricIntegrationTests 13 | { 14 | private UdpListener _udpListener; 15 | private Thread _listenThread; 16 | private const int _randomUnusedLocalPort = 23483; 17 | private const string _localhostAddress = "127.0.0.1"; 18 | private MetricsConfig _defaultMetricsConfig; 19 | 20 | const string _expectedTestPrefixRegex = @"test_prefix\."; 21 | const string _expectedTimeRegEx = @"time:(\d+)\|ms"; 22 | const string _expectedMultiSecondTimeRegEx = @"time:1\d{3}\|ms"; // Expect 1xxx milliseconds reported due to the 1+ second delay below 23 | private static readonly TimeSpan SleepDelay = TimeSpan.FromMilliseconds(200); 24 | private static readonly TimeSpan MultiSecondSleepDelay = TimeSpan.FromMilliseconds(1200); 25 | 26 | [OneTimeSetUp] 27 | public void SetUpUdpListener() 28 | { 29 | _udpListener = new UdpListener(_localhostAddress, _randomUnusedLocalPort); 30 | } 31 | 32 | [OneTimeTearDown] 33 | public void TearDownUdpListener() 34 | { 35 | _udpListener.Dispose(); 36 | } 37 | 38 | [SetUp] 39 | public void StartUdpListenerThread() 40 | { 41 | _defaultMetricsConfig = new MetricsConfig 42 | { 43 | StatsdServerName = _localhostAddress, 44 | StatsdServerPort = _randomUnusedLocalPort 45 | }; 46 | 47 | _listenThread = new Thread(_udpListener.Listen); 48 | _listenThread.Start(); 49 | } 50 | 51 | private string LastPacketMessageReceived() 52 | { 53 | // Stall until the the listener receives a message or times out. 54 | while(_listenThread.IsAlive) {} 55 | 56 | var lastMessages = _udpListener.GetAndClearLastMessages(); 57 | try 58 | { 59 | return lastMessages[0]; 60 | } 61 | catch (ArgumentOutOfRangeException) 62 | { 63 | return null; 64 | } 65 | } 66 | 67 | public class SanityCheck : MetricIntegrationTests 68 | { 69 | [Test] 70 | public void udp_listener_works() 71 | { 72 | var client = new StatsdUDPClient(_localhostAddress, _randomUnusedLocalPort); 73 | client.Send("iamnotinsane!"); 74 | 75 | Assert.That(LastPacketMessageReceived(), Is.EqualTo("iamnotinsane!")); 76 | } 77 | } 78 | 79 | public class Counter : MetricIntegrationTests 80 | { 81 | [Test] 82 | public void counter() 83 | { 84 | Metrics.Configure(_defaultMetricsConfig); 85 | 86 | Metrics.Counter("counter"); 87 | Assert.That(LastPacketMessageReceived(), Is.EqualTo("counter:1|c")); 88 | } 89 | 90 | [Test] 91 | public void counter_with_value() 92 | { 93 | Metrics.Configure(_defaultMetricsConfig); 94 | 95 | Metrics.Counter("counter", 10); 96 | Assert.That(LastPacketMessageReceived(), Is.EqualTo("counter:10|c")); 97 | } 98 | 99 | [Test] 100 | public void counter_with_prefix() 101 | { 102 | _defaultMetricsConfig.Prefix = "test_prefix"; 103 | Metrics.Configure(_defaultMetricsConfig); 104 | 105 | Metrics.Counter("counter"); 106 | Assert.That(LastPacketMessageReceived(), Is.EqualTo("test_prefix.counter:1|c")); 107 | } 108 | 109 | [Test] 110 | public void counter_with_prefix_having_a_trailing_dot() 111 | { 112 | _defaultMetricsConfig.Prefix = "test_prefix."; 113 | Metrics.Configure(_defaultMetricsConfig); 114 | 115 | Metrics.Counter("counter"); 116 | Assert.That(LastPacketMessageReceived(), Is.EqualTo("test_prefix.counter:1|c")); 117 | } 118 | 119 | [Test] 120 | public void counter_with_value_and_sampleRate() 121 | { 122 | Metrics.Configure(_defaultMetricsConfig); 123 | 124 | Metrics.Counter("counter", 10, 0.9999); 125 | Assert.That(LastPacketMessageReceived(), Is.EqualTo("counter:10|c|@0.9999")); 126 | } 127 | 128 | [Test] 129 | public void counter_with_no_config_setup_should_not_send_metric() 130 | { 131 | Metrics.Configure(new MetricsConfig()); 132 | 133 | Metrics.Counter("counter"); 134 | Assert.That(LastPacketMessageReceived(), Is.Null); 135 | } 136 | } 137 | 138 | public class Timer : MetricIntegrationTests 139 | { 140 | [Test] 141 | public void timer() 142 | { 143 | Metrics.Configure(_defaultMetricsConfig); 144 | 145 | Metrics.Timer("timer", 6); 146 | Assert.That(LastPacketMessageReceived(), Is.EqualTo("timer:6|ms")); 147 | } 148 | 149 | [Test] 150 | public void timer_with_prefix() 151 | { 152 | _defaultMetricsConfig.Prefix = "test_prefix"; 153 | Metrics.Configure(_defaultMetricsConfig); 154 | 155 | Metrics.Timer("timer", 6); 156 | Assert.That(LastPacketMessageReceived(), Is.EqualTo("test_prefix.timer:6|ms")); 157 | } 158 | 159 | [Test] 160 | public void timer_with_prefix_having_a_trailing_dot() 161 | { 162 | _defaultMetricsConfig.Prefix = "test_prefix."; 163 | Metrics.Configure(_defaultMetricsConfig); 164 | 165 | Metrics.Timer("timer", 6); 166 | Assert.That(LastPacketMessageReceived(), Is.EqualTo("test_prefix.timer:6|ms")); 167 | } 168 | 169 | [Test] 170 | public void timer_with_no_config_setup_should_not_send_metric() 171 | { 172 | Metrics.Configure(new MetricsConfig()); 173 | 174 | Metrics.Timer("timer", 6); 175 | Assert.That(LastPacketMessageReceived(), Is.Null); 176 | } 177 | } 178 | 179 | public class DisposableTimer : MetricIntegrationTests 180 | { 181 | [Test] 182 | public void disposable_timer() 183 | { 184 | Metrics.Configure(_defaultMetricsConfig); 185 | 186 | using (Metrics.StartTimer("time")) 187 | { 188 | Thread.Sleep(MultiSecondSleepDelay); 189 | } 190 | 191 | Assert.That(LastPacketMessageReceived(), Does.Match(_expectedMultiSecondTimeRegEx)); 192 | } 193 | } 194 | 195 | public class Time : MetricIntegrationTests 196 | { 197 | [Test] 198 | public void time() 199 | { 200 | Metrics.Configure(_defaultMetricsConfig); 201 | 202 | Metrics.Time(() => Thread.Sleep(MultiSecondSleepDelay), "time"); 203 | Assert.That(LastPacketMessageReceived(), Does.Match(_expectedMultiSecondTimeRegEx)); 204 | } 205 | 206 | [Test] 207 | public void time_add() 208 | { 209 | var statsd = new Statsd(new StatsdUDPClient(_localhostAddress, _randomUnusedLocalPort)); 210 | 211 | statsd.Add(() => Thread.Sleep(MultiSecondSleepDelay), "time"); 212 | statsd.Send(); 213 | Assert.That(LastPacketMessageReceived(), Does.Match(_expectedMultiSecondTimeRegEx)); 214 | } 215 | 216 | [Test] 217 | public async Task time_async() 218 | { 219 | Metrics.Configure(_defaultMetricsConfig); 220 | 221 | await Metrics.Time(async () => await Task.Delay(SleepDelay), "time"); 222 | 223 | AssertTimerLength(); 224 | } 225 | 226 | [Test] 227 | public void time_with_prefix() 228 | { 229 | _defaultMetricsConfig.Prefix = "test_prefix"; 230 | Metrics.Configure(_defaultMetricsConfig); 231 | 232 | Metrics.Time(() => Thread.Sleep(SleepDelay), "time"); 233 | Assert.That(LastPacketMessageReceived(), Does.Match(_expectedTestPrefixRegex + _expectedTimeRegEx)); 234 | } 235 | 236 | [Test] 237 | public void time_with_prefix_having_trailing_dot() 238 | { 239 | _defaultMetricsConfig.Prefix = "test_prefix."; 240 | Metrics.Configure(_defaultMetricsConfig); 241 | 242 | Metrics.Time(() => Thread.Sleep(SleepDelay), "time"); 243 | Assert.That(LastPacketMessageReceived(), Does.Match(_expectedTestPrefixRegex + _expectedTimeRegEx)); 244 | } 245 | 246 | [Test] 247 | public void time_with_no_config_setup_should_not_send_metric_but_still_run_action() 248 | { 249 | Metrics.Configure(new MetricsConfig()); 250 | 251 | var someValue = 5; 252 | Metrics.Time(() => { someValue = 10; }, "timer"); 253 | 254 | Assert.That(someValue, Is.EqualTo(10)); 255 | Assert.That(LastPacketMessageReceived(), Is.Null); 256 | } 257 | 258 | [Test] 259 | public async Task time_with_async_return_value() 260 | { 261 | Metrics.Configure(_defaultMetricsConfig); 262 | 263 | var returnValue = await Metrics.Time(async () => 264 | { 265 | await Task.Delay(SleepDelay); 266 | return 20; 267 | }, "time"); 268 | 269 | AssertTimerLength(); 270 | Assert.That(returnValue, Is.EqualTo(20)); 271 | } 272 | 273 | [Test] 274 | public void time_with_return_value() 275 | { 276 | Metrics.Configure(_defaultMetricsConfig); 277 | 278 | var returnValue = Metrics.Time(() => 279 | { 280 | Thread.Sleep(SleepDelay); 281 | return 5; 282 | }, "time"); 283 | 284 | Assert.That(LastPacketMessageReceived(), Does.Match(_expectedTimeRegEx)); 285 | Assert.That(returnValue, Is.EqualTo(5)); 286 | } 287 | 288 | [Test] 289 | public void time_with_return_value_and_prefix() 290 | { 291 | _defaultMetricsConfig.Prefix = "test_prefix"; 292 | Metrics.Configure(_defaultMetricsConfig); 293 | 294 | var returnValue = Metrics.Time(() => 295 | { 296 | Thread.Sleep(SleepDelay); 297 | return 5; 298 | }, "time"); 299 | 300 | Assert.That(LastPacketMessageReceived(), Does.Match(_expectedTestPrefixRegex + _expectedTimeRegEx)); 301 | Assert.That(returnValue, Is.EqualTo(5)); 302 | } 303 | 304 | [Test] 305 | public void time_with_return_value_and_prefix_having_a_trailing_dot() 306 | { 307 | _defaultMetricsConfig.Prefix = "test_prefix."; 308 | Metrics.Configure(_defaultMetricsConfig); 309 | 310 | var returnValue = Metrics.Time(() => 311 | { 312 | Thread.Sleep(SleepDelay); 313 | return 5; 314 | }, "time"); 315 | 316 | Assert.That(LastPacketMessageReceived(), Does.Match(_expectedTestPrefixRegex + _expectedTimeRegEx)); 317 | Assert.That(returnValue, Is.EqualTo(5)); 318 | } 319 | 320 | [Test] 321 | public void time_with_return_value_and_no_config_setup_should_not_send_metric_but_still_return_value() 322 | { 323 | Metrics.Configure(new MetricsConfig()); 324 | 325 | var returnValue = Metrics.Time(() => 5, "time"); 326 | 327 | Assert.That(LastPacketMessageReceived(), Is.Null); 328 | Assert.That(returnValue, Is.EqualTo(5)); 329 | } 330 | 331 | private void AssertTimerLength() 332 | { 333 | var lastPacketMessageReceived = LastPacketMessageReceived(); 334 | Assert.That(lastPacketMessageReceived, Does.Match(_expectedTimeRegEx)); 335 | 336 | var match = Regex.Match(lastPacketMessageReceived, _expectedTimeRegEx); 337 | var timerValue = Convert.ToInt32(match.Groups[1].Value); 338 | Assert.That(timerValue, Is.EqualTo(SleepDelay.Milliseconds).Within(100)); 339 | } 340 | } 341 | 342 | public class GaugeDelta : MetricIntegrationTests 343 | { 344 | [Test] 345 | [TestCase(123d, "gauge:+123|g")] 346 | [TestCase(-123d, "gauge:-123|g")] 347 | [TestCase(0d, "gauge:+0|g")] 348 | public void GaugeDelta_EmitsCorrect_Format(double gaugeDeltaValue, string expectedPacketMessageFormat) 349 | { 350 | Metrics.Configure(_defaultMetricsConfig); 351 | 352 | Metrics.GaugeDelta("gauge", gaugeDeltaValue); 353 | Assert.That(LastPacketMessageReceived(), Is.EqualTo(expectedPacketMessageFormat)); 354 | } 355 | } 356 | 357 | public class GaugeAbsolute : MetricIntegrationTests 358 | { 359 | [Test] 360 | public void absolute_gauge_with_double_value() 361 | { 362 | Metrics.Configure(_defaultMetricsConfig); 363 | 364 | const double value = 12345678901234567890; 365 | Metrics.GaugeAbsoluteValue("gauge", value); 366 | Assert.That(LastPacketMessageReceived(), Is.EqualTo("gauge:12345678901234600000.000000000000000|g")); 367 | } 368 | 369 | [Test] 370 | public void absolute_gauge_with_double_value_with_floating_point() 371 | { 372 | Metrics.Configure(_defaultMetricsConfig); 373 | 374 | const double value = 1.234567890123456; 375 | Metrics.GaugeAbsoluteValue("gauge", value); 376 | Assert.That(LastPacketMessageReceived(), Is.EqualTo("gauge:1.234567890123460|g")); 377 | } 378 | 379 | [Test] 380 | public void absolute_gauge_with_prefix() 381 | { 382 | _defaultMetricsConfig.Prefix = "test_prefix"; 383 | Metrics.Configure(_defaultMetricsConfig); 384 | 385 | Metrics.GaugeAbsoluteValue("gauge", 3); 386 | Assert.That(LastPacketMessageReceived(), Is.EqualTo("test_prefix.gauge:3.000000000000000|g")); 387 | } 388 | 389 | [Test] 390 | public void absolute_gauge_with_prefix_having_a_trailing_dot() 391 | { 392 | _defaultMetricsConfig.Prefix = "test_prefix."; 393 | Metrics.Configure(_defaultMetricsConfig); 394 | 395 | Metrics.GaugeAbsoluteValue("gauge", 3); 396 | Assert.That(LastPacketMessageReceived(), Is.EqualTo("test_prefix.gauge:3.000000000000000|g")); 397 | } 398 | 399 | [Test] 400 | public void gauge_with_no_config_setup_should_not_send_metric() 401 | { 402 | Metrics.Configure(new MetricsConfig()); 403 | 404 | Metrics.GaugeAbsoluteValue("gauge", 3); 405 | Assert.That(LastPacketMessageReceived(), Is.Null); 406 | } 407 | } 408 | 409 | public class Set : MetricIntegrationTests 410 | { 411 | [Test] 412 | public void set() 413 | { 414 | Metrics.Configure(_defaultMetricsConfig); 415 | 416 | Metrics.Set("timer", "value"); 417 | Assert.That(LastPacketMessageReceived(), Is.EqualTo("timer:value|s")); 418 | } 419 | 420 | [Test] 421 | public void set_with_prefix() 422 | { 423 | _defaultMetricsConfig.Prefix = "test_prefix"; 424 | Metrics.Configure(_defaultMetricsConfig); 425 | 426 | Metrics.Set("timer", "value"); 427 | Assert.That(LastPacketMessageReceived(), Is.EqualTo("test_prefix.timer:value|s")); 428 | } 429 | 430 | [Test] 431 | public void set_with_prefix_having_a_trailing_dot() 432 | { 433 | _defaultMetricsConfig.Prefix = "test_prefix."; 434 | Metrics.Configure(_defaultMetricsConfig); 435 | 436 | Metrics.Set("timer", "value"); 437 | Assert.That(LastPacketMessageReceived(), Is.EqualTo("test_prefix.timer:value|s")); 438 | } 439 | 440 | [Test] 441 | public void set_with_no_config_setup_should_not_send_metric() 442 | { 443 | Metrics.Configure(new MetricsConfig()); 444 | 445 | Metrics.Set("timer", "value"); 446 | Assert.That(LastPacketMessageReceived(), Is.Null); 447 | } 448 | } 449 | } 450 | } -------------------------------------------------------------------------------- /src/Tests/MetricsTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reflection; 3 | using NUnit.Framework; 4 | using StatsdClient; 5 | 6 | namespace Tests 7 | { 8 | public class MetricsTests 9 | { 10 | /// 11 | /// Since Metrics is a static class withs static fields, in order to test functionality 12 | /// of methods this cleanup method will clean up the static state of the Metrics class between each 13 | /// test iteration. 14 | /// 15 | [SetUp] 16 | public void SetUp() 17 | { 18 | foreach (var field in typeof(Metrics).GetFields(BindingFlags.Static | BindingFlags.NonPublic)) 19 | { 20 | field.SetValue(null, null); 21 | } 22 | } 23 | 24 | [Test] 25 | public void throws_when_configured_with_a_null_configuration() 26 | { 27 | Assert.That(() => Metrics.Configure(null), Throws.Exception.TypeOf()); 28 | } 29 | 30 | [Test] 31 | public void IsConfigured_returns_false_when_configuration_is_null() 32 | { 33 | Assert.That(Metrics.IsConfigured(), Is.False); 34 | } 35 | 36 | [Test] 37 | public void IsConfigured_returns_true_when_configuration_is_not_null() 38 | { 39 | Metrics.Configure(new MetricsConfig {StatsdServerName = "localhost"}); 40 | 41 | Assert.That(Metrics.IsConfigured(), Is.True); 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /src/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: AssemblyConfiguration("")] 8 | [assembly: AssemblyCompany("")] 9 | [assembly: AssemblyProduct("Tests")] 10 | [assembly: AssemblyTrademark("")] 11 | 12 | // Setting ComVisible to false makes the types in this assembly not visible 13 | // to COM components. If you need to access a type in this assembly from 14 | // COM, set the ComVisible attribute to true on that type. 15 | [assembly: ComVisible(false)] 16 | 17 | // The following GUID is for the ID of the typelib if this project is exposed to COM 18 | [assembly: Guid("35c84fbc-41b2-4f48-9f24-6b98aa94001b")] 19 | -------------------------------------------------------------------------------- /src/Tests/RandomGeneratorUnitTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using NUnit.Framework; 3 | using StatsdClient; 4 | 5 | namespace Tests 6 | { 7 | [TestFixture] 8 | public class RandomGeneratorUnitTests 9 | { 10 | private readonly RandomGenerator _randomGenerator = new RandomGenerator(); 11 | private const int NumberOfTestsToRun = 1000; 12 | 13 | [Test] 14 | public void If_sample_rate_is_1_then_always_true() 15 | { 16 | for (var i = 0; i < NumberOfTestsToRun; i++) 17 | { 18 | Assert.True(_randomGenerator.ShouldSend(1)); 19 | } 20 | } 21 | 22 | [Test] 23 | public void If_sample_rate_is_0_5_then_have_half_true() 24 | { 25 | var numberOfTrues = 0; 26 | var randomGenerator = new RandomGenerator(); 27 | 28 | for (var i = 0; i < NumberOfTestsToRun; i++) 29 | { 30 | if (randomGenerator.ShouldSend(0.5)) 31 | { 32 | numberOfTrues++; 33 | } 34 | } 35 | 36 | Assert.That( Math.Round(numberOfTrues/(double)NumberOfTestsToRun,1),Is.EqualTo(0.5)); 37 | } 38 | 39 | [Test] 40 | public void If_sample_rate_is_one_quarter_then_have_one_quarter_true() 41 | { 42 | var numberOfTrues = 0; 43 | var randomGenerator = new RandomGenerator(); 44 | const int sampleRate = 1/4; 45 | 46 | for (var i = 0; i < NumberOfTestsToRun; i++) 47 | { 48 | if (randomGenerator.ShouldSend(sampleRate)) 49 | { 50 | numberOfTrues++; 51 | } 52 | } 53 | 54 | Assert.That(Math.Round(numberOfTrues / (double)NumberOfTestsToRun, 1), Is.EqualTo(sampleRate)); 55 | } 56 | 57 | [Test] 58 | public void If_sample_rate_is_one_tenth_of_pct_then_have_one_tenth_of_pct() 59 | { 60 | var numberOfTrues = 0; 61 | var randomGenerator = new RandomGenerator(); 62 | const int sampleRate = 1/1000; 63 | 64 | for (var i = 0; i < NumberOfTestsToRun; i++) 65 | { 66 | if (randomGenerator.ShouldSend(sampleRate)) 67 | { 68 | numberOfTrues++; 69 | } 70 | } 71 | 72 | Assert.That(Math.Round(numberOfTrues / (double)NumberOfTestsToRun, 1), Is.EqualTo(sampleRate)); 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/Tests/StatsdTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Threading.Tasks; 4 | using NSubstitute; 5 | using NUnit.Framework; 6 | using StatsdClient; 7 | 8 | namespace Tests 9 | { 10 | [TestFixture] 11 | public class StatsdTests 12 | { 13 | private IStatsdClient _udp; 14 | private IRandomGenerator _randomGenerator; 15 | private IStopWatchFactory _stopwatch; 16 | 17 | [SetUp] 18 | public void Setup() 19 | { 20 | _udp = Substitute.For(); 21 | _stopwatch = Substitute.For(); 22 | _randomGenerator = Substitute.For(); 23 | _randomGenerator.ShouldSend(0).ReturnsForAnyArgs(true); 24 | } 25 | 26 | public class Counter : StatsdTests 27 | { 28 | [Test] 29 | public void increases_counter_with_value_of_X() 30 | { 31 | var s = new Statsd(_udp, _randomGenerator, _stopwatch); 32 | s.Send("counter", 5); 33 | _udp.Received().Send("counter:5|c"); 34 | } 35 | 36 | [Test] 37 | public void increases_counter_with_value_of_X_and_sample_rate() 38 | { 39 | var s = new Statsd(_udp, _randomGenerator, _stopwatch); 40 | s.Send("counter", 5, 0.1); 41 | _udp.Received().Send("counter:5|c|@0.1"); 42 | } 43 | 44 | [Test] 45 | public void counting_exception_fails_silently() 46 | { 47 | GivenUdpSendFails(); 48 | var s = new Statsd(_udp, _randomGenerator, _stopwatch); 49 | s.Send("counter", 5); 50 | } 51 | } 52 | 53 | public class Timer : StatsdTests 54 | { 55 | [Test] 56 | public void adds_timing() 57 | { 58 | var s = new Statsd(_udp, _randomGenerator, _stopwatch); 59 | s.Send("timer", 5); 60 | _udp.Received().Send("timer:5|ms"); 61 | } 62 | 63 | [Test] 64 | public void timing_with_value_of_X_and_sample_rate() 65 | { 66 | var s = new Statsd(_udp, _randomGenerator, _stopwatch); 67 | s.Send("timer", 5, 0.1); 68 | _udp.Received().Send("timer:5|ms|@0.1"); 69 | } 70 | 71 | [Test] 72 | public void timing_exception_fails_silently() 73 | { 74 | GivenUdpSendFails(); 75 | var s = new Statsd(_udp); 76 | s.Send("timer", 5); 77 | } 78 | 79 | [Test] 80 | public void add_timer_with_lamba() 81 | { 82 | const string statName = "name"; 83 | 84 | var stopwatch = Substitute.For(); 85 | stopwatch.ElapsedMilliseconds.Returns(500); 86 | _stopwatch.Get().Returns(stopwatch); 87 | 88 | var statsd = new Statsd(_udp, _randomGenerator, _stopwatch); 89 | statsd.Add(() => TestMethod(), statName); 90 | 91 | Assert.That(statsd.Commands.Single(), Is.EqualTo("name:500|ms")); 92 | } 93 | 94 | [Test] 95 | public void add_timer_with_lamba_and_sampleRate_passes() 96 | { 97 | const string statName = "name"; 98 | 99 | var stopwatch = Substitute.For(); 100 | stopwatch.ElapsedMilliseconds.Returns(500); 101 | _stopwatch.Get().Returns(stopwatch); 102 | _randomGenerator = Substitute.For(); 103 | _randomGenerator.ShouldSend(0).ReturnsForAnyArgs(true); 104 | 105 | var statsd = new Statsd(_udp, _randomGenerator, _stopwatch); 106 | statsd.Add(() => TestMethod(), statName, 0.1); 107 | 108 | Assert.That(statsd.Commands.Single(), Is.EqualTo("name:500|ms")); 109 | } 110 | 111 | [Test] 112 | public void add_timer_with_lamba_and_sampleRate_fails() 113 | { 114 | const string statName = "name"; 115 | 116 | var stopwatch = Substitute.For(); 117 | stopwatch.ElapsedMilliseconds.Returns(500); 118 | _stopwatch.Get().Returns(stopwatch); 119 | _randomGenerator = Substitute.For(); 120 | _randomGenerator.ShouldSend(0).ReturnsForAnyArgs(false); 121 | 122 | var s = new Statsd(_udp, _randomGenerator, _stopwatch); 123 | s.Add(() => TestMethod(), statName, 0.1); 124 | 125 | Assert.That(s.Commands.Count, Is.EqualTo(0)); 126 | } 127 | 128 | [Test] 129 | public void add_timer_with_lamba_still_records_on_error_and_still_bubbles_up_exception() 130 | { 131 | const string statName = "name"; 132 | 133 | var stopwatch = Substitute.For(); 134 | stopwatch.ElapsedMilliseconds.Returns(500); 135 | _stopwatch.Get().Returns(stopwatch); 136 | 137 | var s = new Statsd(_udp, _randomGenerator, _stopwatch); 138 | 139 | Assert.Throws(() => s.Add(() => { throw new InvalidOperationException(); }, statName)); 140 | 141 | Assert.That(s.Commands.Count, Is.EqualTo(1)); 142 | Assert.That(s.Commands.ToArray()[0], Is.EqualTo("name:500|ms")); 143 | } 144 | 145 | [Test] 146 | public void send_timer_with_lambda() 147 | { 148 | const string statName = "name"; 149 | var stopwatch = Substitute.For(); 150 | stopwatch.ElapsedMilliseconds.Returns(500); 151 | _stopwatch.Get().Returns(stopwatch); 152 | 153 | var s = new Statsd(_udp, _randomGenerator, _stopwatch); 154 | s.Send(() => TestMethod(), statName); 155 | 156 | _udp.Received().Send("name:500|ms"); 157 | } 158 | 159 | [Test] 160 | public void send_timer_with_lambda_and_sampleRate_passes() 161 | { 162 | const string statName = "name"; 163 | var stopwatch = Substitute.For(); 164 | stopwatch.ElapsedMilliseconds.Returns(500); 165 | _stopwatch.Get().Returns(stopwatch); 166 | _randomGenerator = Substitute.For(); 167 | _randomGenerator.ShouldSend(0).ReturnsForAnyArgs(true); 168 | 169 | var s = new Statsd(_udp, _randomGenerator, _stopwatch); 170 | s.Send(() => TestMethod(), statName); 171 | 172 | _udp.Received().Send("name:500|ms"); 173 | } 174 | 175 | 176 | [Test] 177 | public void send_timer_with_lambda_and_sampleRate_fails() 178 | { 179 | const string statName = "name"; 180 | var stopwatch = Substitute.For(); 181 | stopwatch.ElapsedMilliseconds.Returns(500); 182 | _stopwatch.Get().Returns(stopwatch); 183 | _randomGenerator = Substitute.For(); 184 | _randomGenerator.ShouldSend(0).ReturnsForAnyArgs(false); 185 | 186 | var s = new Statsd(_udp, _randomGenerator, _stopwatch); 187 | s.Send(() => TestMethod(), statName); 188 | 189 | _udp.DidNotReceive().Send("name:500|ms"); 190 | } 191 | 192 | [Test] 193 | public void send_timer_with_lamba_still_records_on_error_and_still_bubbles_up_exception() 194 | { 195 | const string statName = "name"; 196 | var stopwatch = Substitute.For(); 197 | stopwatch.ElapsedMilliseconds.Returns(500); 198 | _stopwatch.Get().Returns(stopwatch); 199 | 200 | var s = new Statsd(_udp, _randomGenerator, _stopwatch); 201 | Assert.Throws(() => s.Send(() => { throw new InvalidOperationException(); }, statName)); 202 | 203 | _udp.Received().Send("name:500|ms"); 204 | } 205 | 206 | [Test] 207 | public void set_return_value_with_send_timer_with_lambda() 208 | { 209 | const string statName = "name"; 210 | var stopwatch = Substitute.For(); 211 | stopwatch.ElapsedMilliseconds.Returns(500); 212 | _stopwatch.Get().Returns(stopwatch); 213 | 214 | var s = new Statsd(_udp, _randomGenerator, _stopwatch); 215 | var returnValue = 0; 216 | s.Send(() => returnValue = TestMethod(), statName); 217 | 218 | _udp.Received().Send("name:500|ms"); 219 | Assert.That(returnValue, Is.EqualTo(5)); 220 | } 221 | } 222 | 223 | public class Guage : StatsdTests 224 | { 225 | [Test] 226 | public void adds_gauge_with_large_double_values() 227 | { 228 | var s = new Statsd(_udp, _randomGenerator, _stopwatch); 229 | s.Send("gauge", 34563478564785); 230 | _udp.Received().Send("gauge:34563478564785.000000000000000|g"); 231 | } 232 | 233 | [Test] 234 | public void gauge_exception_fails_silently() 235 | { 236 | GivenUdpSendFails(); 237 | var s = new Statsd(_udp); 238 | s.Send("gauge", 5.0); 239 | } 240 | 241 | [Test] 242 | [TestCase(true, 10d, "delta-gauge:+10|g")] 243 | [TestCase(true, -10d, "delta-gauge:-10|g")] 244 | [TestCase(true, 0d, "delta-gauge:+0|g")] 245 | [TestCase(false, 10d, "delta-gauge:10.000000000000000|g")]//because it is looped through to original Gauge send function 246 | public void adds_gauge_with_deltaValue_formatsCorrectly(bool isDeltaValue, double value, string expectedFormattedStatsdMessage) 247 | { 248 | var s = new Statsd(_udp, _randomGenerator, _stopwatch); 249 | s.Send("delta-gauge", value, isDeltaValue); 250 | _udp.Received().Send(expectedFormattedStatsdMessage); 251 | } 252 | } 253 | 254 | public class Meter : StatsdTests 255 | { 256 | [Test] 257 | public void adds_meter() 258 | { 259 | var s = new Statsd(_udp, _randomGenerator, _stopwatch); 260 | s.Send("meter", 5); 261 | _udp.Received().Send("meter:5|m"); 262 | } 263 | 264 | [Test] 265 | public void meter_exception_fails_silently() 266 | { 267 | GivenUdpSendFails(); 268 | var s = new Statsd(_udp); 269 | s.Send("meter", 5); 270 | } 271 | } 272 | 273 | public class Historgram : StatsdTests 274 | { 275 | [Test] 276 | public void adds_histogram() 277 | { 278 | var s = new Statsd(_udp, _randomGenerator, _stopwatch); 279 | s.Send("histogram", 5); 280 | _udp.Received().Send("histogram:5|h"); 281 | } 282 | 283 | [Test] 284 | public void histrogram_exception_fails_silently() 285 | { 286 | GivenUdpSendFails(); 287 | var s = new Statsd(_udp); 288 | s.Send("histogram", 5); 289 | } 290 | } 291 | 292 | public class Set : StatsdTests 293 | { 294 | [Test] 295 | public void adds_set_with_string_value() 296 | { 297 | var s = new Statsd(_udp, _randomGenerator, _stopwatch); 298 | s.Send("set", "34563478564785xyz"); 299 | _udp.Received().Send("set:34563478564785xyz|s"); 300 | } 301 | 302 | [Test] 303 | public void set_exception_fails_silently() 304 | { 305 | GivenUdpSendFails(); 306 | var s = new Statsd(_udp); 307 | s.Send("set", "silent-exception-test"); 308 | } 309 | } 310 | 311 | public class Combination : StatsdTests 312 | { 313 | [Test] 314 | public void add_one_counter_and_one_gauge_shows_in_commands() 315 | { 316 | var s = new Statsd(_udp, _randomGenerator, _stopwatch); 317 | s.Add("counter", 1, 0.1); 318 | s.Add("timer", 1); 319 | 320 | Assert.That(s.Commands.Count, Is.EqualTo(2)); 321 | Assert.That(s.Commands.ToArray()[0], Is.EqualTo("counter:1|c|@0.1")); 322 | Assert.That(s.Commands.ToArray()[1], Is.EqualTo("timer:1|ms")); 323 | } 324 | 325 | [Test] 326 | public void add_one_counter_and_one_gauge_with_no_sample_rate_shows_in_commands() 327 | { 328 | var s = new Statsd(_udp, _randomGenerator, _stopwatch); 329 | s.Add("counter", 1); 330 | s.Add("timer", 1); 331 | 332 | Assert.That(s.Commands.Count, Is.EqualTo(2)); 333 | Assert.That(s.Commands.ToArray()[0], Is.EqualTo("counter:1|c")); 334 | Assert.That(s.Commands.ToArray()[1], Is.EqualTo("timer:1|ms")); 335 | } 336 | 337 | [Test] 338 | public void add_one_counter_and_one_timer_sends_in_one_go() 339 | { 340 | var s = new Statsd(_udp, _randomGenerator, _stopwatch); 341 | s.Add("counter", 1, 0.1); 342 | s.Add("timer", 1); 343 | s.Send(); 344 | 345 | _udp.Received().Send("counter:1|c|@0.1\ntimer:1|ms"); 346 | } 347 | 348 | [Test] 349 | public void add_one_counter_and_one_timer_sends_and_removes_commands() 350 | { 351 | var s = new Statsd(_udp, _randomGenerator, _stopwatch); 352 | s.Add("counter", 1, 0.1); 353 | s.Add("timer", 1); 354 | s.Send(); 355 | 356 | Assert.That(s.Commands.Count, Is.EqualTo(0)); 357 | } 358 | 359 | [Test] 360 | public void add_one_counter_and_send_one_timer_sends_only_sends_the_last() 361 | { 362 | var s = new Statsd(_udp, _randomGenerator, _stopwatch); 363 | s.Add("counter", 1); 364 | s.Send("timer", 1); 365 | 366 | _udp.Received().Send("timer:1|ms"); 367 | } 368 | } 369 | 370 | public class NamePrefixing : StatsdTests 371 | { 372 | [Test] 373 | public void set_prefix_on_stats_name_when_calling_send() 374 | { 375 | var s = new Statsd(_udp, _randomGenerator, _stopwatch, "a.prefix."); 376 | s.Send("counter", 5); 377 | s.Send("counter", 5); 378 | 379 | _udp.Received(2).Send("a.prefix.counter:5|c"); 380 | } 381 | 382 | [Test] 383 | public void add_counter_sets_prefix_on_name() 384 | { 385 | var s = new Statsd(_udp, _randomGenerator, _stopwatch, "another.prefix."); 386 | 387 | s.Add("counter", 1, 0.1); 388 | s.Add("timer", 1); 389 | s.Send(); 390 | 391 | _udp.Received().Send("another.prefix.counter:1|c|@0.1\nanother.prefix.timer:1|ms"); 392 | } 393 | } 394 | 395 | public class ThreadSafety : StatsdTests 396 | { 397 | private const int ThreadCount = 10000; 398 | private Statsd _stats; 399 | 400 | [SetUp] 401 | public void Before_each() 402 | { 403 | _stats = new Statsd(_udp, _randomGenerator, _stopwatch); 404 | } 405 | 406 | [Test] 407 | public void add_counters() 408 | { 409 | Parallel.For(0, ThreadCount, x => Assert.DoesNotThrow(() => _stats.Add("random-name", 5))); 410 | } 411 | 412 | [Test] 413 | public void add_gauges() 414 | { 415 | Parallel.For(0, ThreadCount, x => Assert.DoesNotThrow(() => _stats.Add("random-name", 5d))); 416 | } 417 | 418 | [Test] 419 | public void send_counters() 420 | { 421 | Parallel.For(0, ThreadCount, x => _stats.Send(Guid.NewGuid().ToString(), 5)); 422 | Assert.That(DistinctMetricsSent(), Is.EqualTo(ThreadCount)); 423 | } 424 | 425 | [Test] 426 | public void send_absolute_gauge() 427 | { 428 | Parallel.For(0, ThreadCount, x => _stats.Send(Guid.NewGuid().ToString(), 5d)); 429 | Assert.That(DistinctMetricsSent(), Is.EqualTo(ThreadCount)); 430 | } 431 | 432 | [Test] 433 | public void send_delta_gauge() 434 | { 435 | Parallel.For(0, ThreadCount, x => _stats.Send(Guid.NewGuid().ToString(), 5d, true)); 436 | Assert.That(DistinctMetricsSent(), Is.EqualTo(ThreadCount)); 437 | } 438 | 439 | [Test] 440 | public void send_set() 441 | { 442 | Parallel.For(0, ThreadCount, x => _stats.Send(Guid.NewGuid().ToString(), "foo")); 443 | Assert.That(DistinctMetricsSent(), Is.EqualTo(ThreadCount)); 444 | } 445 | 446 | [Test] 447 | public void send_sampled_timer() 448 | { 449 | Parallel.For(0, ThreadCount, x => _stats.Send(Guid.NewGuid().ToString(), 5, 1d)); 450 | Assert.That(DistinctMetricsSent(), Is.EqualTo(ThreadCount)); 451 | } 452 | 453 | private int DistinctMetricsSent() 454 | { 455 | return _udp.ReceivedCalls().Select(x => x.GetArguments()[0]).Distinct().Count(); 456 | } 457 | } 458 | 459 | private static int TestMethod() 460 | { 461 | return 5; 462 | } 463 | 464 | private void GivenUdpSendFails() 465 | { 466 | _udp.When(x => x.Send(Arg.Any())).Do(x => { throw new Exception(); }); 467 | } 468 | } 469 | } -------------------------------------------------------------------------------- /src/Tests/StatsdUDPTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Collections.Generic; 4 | using NUnit.Framework; 5 | using StatsdClient; 6 | using Tests.Helpers; 7 | 8 | namespace Tests 9 | { 10 | // Most of StatsUDP is tested in StatsdUnitTests. This is mainly to test the splitting of oversized UDP packets 11 | [TestFixture] 12 | public class StatsUDPTests 13 | { 14 | private UdpListener udpListener; 15 | private Thread listenThread; 16 | private const int serverPort = 23483; 17 | private const string serverName = "127.0.0.1"; 18 | private StatsdUDPClient udp; 19 | private Statsd statsd; 20 | private List lastPulledMessages; 21 | 22 | [OneTimeSetUp] 23 | public void SetUpUdpListenerAndStatsd() 24 | { 25 | udpListener = new UdpListener(serverName, serverPort); 26 | var metricsConfig = new MetricsConfig { StatsdServerName = serverName }; 27 | StatsdClient.Metrics.Configure(metricsConfig); 28 | udp = new StatsdUDPClient(serverName, serverPort); 29 | statsd = new Statsd(udp); 30 | } 31 | 32 | [OneTimeTearDown] 33 | public void TearDownUdpListener() 34 | { 35 | udpListener.Dispose(); 36 | udp.Dispose(); 37 | } 38 | 39 | [SetUp] 40 | public void UdpListenerThread() 41 | { 42 | lastPulledMessages = new List(); 43 | listenThread = new Thread(new ParameterizedThreadStart(udpListener.Listen)); 44 | } 45 | 46 | [TearDown] 47 | public void ClearUdpListenerMessages() 48 | { 49 | udpListener.GetAndClearLastMessages(); // just to be sure that nothing is left over 50 | } 51 | 52 | // Test helper. Waits until the listener is done receiving a message, 53 | // then asserts that the passed string is equal to the message received. 54 | private void AssertWasReceived(string shouldBe, int index = 0) 55 | { 56 | if (lastPulledMessages.Count == 0) 57 | { 58 | // Stall until the the listener receives a message or times out 59 | while(listenThread.IsAlive); 60 | lastPulledMessages = udpListener.GetAndClearLastMessages(); 61 | } 62 | 63 | string actual; 64 | 65 | try 66 | { 67 | actual = lastPulledMessages[index]; 68 | } 69 | catch (System.ArgumentOutOfRangeException) 70 | { 71 | actual = null; 72 | } 73 | Assert.AreEqual(shouldBe, actual); 74 | } 75 | 76 | [Test] 77 | public void send() 78 | { 79 | // (Sanity test) 80 | listenThread.Start(); 81 | udp.Send("test-metric"); 82 | AssertWasReceived("test-metric"); 83 | } 84 | 85 | [Test] 86 | public void send_equal_to_udp_packet_limit_is_still_sent() 87 | { 88 | var msg = new String('f', MetricsConfig.DefaultStatsdMaxUDPPacketSize); 89 | listenThread.Start(); 90 | udp.Send(msg); 91 | // As long as we're at or below the limit, the packet should still be sent 92 | AssertWasReceived(msg); 93 | } 94 | 95 | [Test] 96 | public void send_unsplittable_oversized_udp_packets_are_not_split_or_sent_and_no_exception_is_raised() 97 | { 98 | // This message will be one byte longer than the theoretical limit of a UDP packet 99 | var msg = new String('f', 65508); 100 | listenThread.Start(); 101 | statsd.Add(msg, 1); 102 | statsd.Send(); 103 | // It shouldn't be split or sent, and no exceptions should be raised. 104 | AssertWasReceived(null); 105 | } 106 | 107 | [Test] 108 | public void send_oversized_udp_packets_are_split_if_possible() 109 | { 110 | var msg = new String('f', MetricsConfig.DefaultStatsdMaxUDPPacketSize - 15); 111 | listenThread.Start(3); // Listen for 3 messages 112 | statsd.Add(msg, 1); 113 | statsd.Add(msg, 2); 114 | statsd.Send(); 115 | // These two metrics should be split as their combined lengths exceed the maximum packet size 116 | AssertWasReceived(String.Format("{0}:1|c", msg), 0); 117 | AssertWasReceived(String.Format("{0}:2|ms", msg), 1); 118 | // No extra metric should be sent at the end 119 | AssertWasReceived(null, 2); 120 | } 121 | 122 | [Test] 123 | public void send_oversized_udp_packets_are_split_if_possible_with_multiple_messages_in_one_packet() 124 | { 125 | var msg = new String('f', MetricsConfig.DefaultStatsdMaxUDPPacketSize / 2); 126 | listenThread.Start(3); 127 | statsd.Add("counter", 1); 128 | statsd.Add(msg, 2); 129 | statsd.Add(msg, 3); 130 | statsd.Send(); 131 | // Make sure that a split packet can contain mulitple metrics 132 | AssertWasReceived(String.Format("counter:1|c\n{0}:2|c", msg), 0); 133 | AssertWasReceived(String.Format("{0}:3|c", msg), 1); 134 | AssertWasReceived(null, 2); 135 | } 136 | 137 | [Test] 138 | public void set_max_udp_packet_size() 139 | { 140 | // Make sure that we can set the max UDP packet size 141 | udp = new StatsdUDPClient(serverName, serverPort, 10); 142 | statsd = new Statsd(udp); 143 | var msg = new String('f', 5); 144 | listenThread.Start(2); 145 | statsd.Add(msg, 1); 146 | statsd.Add(msg, 2); 147 | statsd.Send(); 148 | // Since our packet size limit is now 10, this (short) message should still be split 149 | AssertWasReceived(String.Format("{0}:1|c", msg), 0); 150 | AssertWasReceived(String.Format("{0}:2|ms", msg), 1); 151 | } 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/Tests/TCPSmokeTests.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using System.Net.Sockets; 3 | using NUnit.Framework; 4 | using StatsdClient; 5 | 6 | namespace Tests 7 | { 8 | [TestFixture] 9 | public class TCPSmokeTests 10 | { 11 | // Smoke test should hit the real thing, but for the purpose of passing the appveyor build we are only checking if the client connects. 12 | // If you want to test against an actual system, change the host/port. 13 | 14 | private TcpListener _tcpListener; 15 | private static readonly IPAddress ServerHostname = IPAddress.Loopback; 16 | private int _serverPort; 17 | 18 | [OneTimeSetUp] 19 | public void UdpListenerThread() 20 | { 21 | const int nextAvailablePort = 0; 22 | _tcpListener = new TcpListener(ServerHostname, nextAvailablePort); 23 | _tcpListener.Start(); 24 | 25 | _serverPort = ((IPEndPoint) _tcpListener.LocalEndpoint).Port; 26 | } 27 | 28 | [OneTimeTearDown] 29 | public void TearDownUdpListener() 30 | { 31 | _tcpListener.Stop(); 32 | } 33 | 34 | [Test] 35 | public void Sends_counter_text() 36 | { 37 | using (var client = new StatsdTCPClient(ServerHostname.ToString(), _serverPort)) 38 | { 39 | client.Send("statsd-client.tcp-smoke-test:6|c"); 40 | } 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /src/Tests/Tests.xproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 14.0 5 | $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) 6 | 7 | 8 | 9 | 35c84fbc-41b2-4f48-9f24-6b98aa94001b 10 | StatsdClient.UnitTests 11 | .\obj 12 | .\bin\ 13 | v4.5.2 14 | 15 | 16 | 2.0 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/Tests/UDPSmokeTests.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using NUnit.Framework; 3 | using StatsdClient; 4 | 5 | namespace Tests 6 | { 7 | [TestFixture] 8 | public class UDPSmokeTests 9 | { 10 | // Smoke test should hit the real thing, but for the purpose of passing the appveyor build we are only checking if the client connects. 11 | // If you want to test against an actual system, change the host/port. 12 | 13 | private static readonly IPAddress ServerHostname = IPAddress.Loopback; 14 | 15 | [Test] 16 | public void Sends_counter_text() 17 | { 18 | using (var client = new StatsdUDPClient(ServerHostname.ToString())) 19 | { 20 | client.Send("statsd-client.udp-smoke-test:6|c"); 21 | } 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Tests/UdpListener.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net; 3 | using System.Net.Sockets; 4 | using System.Text; 5 | using System.Collections.Generic; 6 | 7 | namespace Tests 8 | { 9 | namespace Helpers 10 | { 11 | // A small UDP server that can be used for testing. 12 | // Stores a list of the last messages that were received by the server 13 | // until they are accessed using GetAndClearLastMessages(). 14 | 15 | // By design received messages can only be read once. This 16 | // allows one instance of the listener to be used across 17 | // multiple tests without risk of the results of previous tests 18 | // affecting the current one. 19 | 20 | // Intended use: 21 | // udpListener = new UdpListener(serverName, serverPort); 22 | // listenThread = new Thread(new ParameterizedThreadStart(udpListener.Listen)); 23 | // listenThread.Start(n); 24 | // { send n messages to the listener } 25 | // while(listenThread.IsAlive); // wait for listen thread to receive message or time out 26 | // List receivedMessage = udpListener.GetAndClearLastMessages() 27 | // { make sure that the received messages are what was expected } 28 | public class UdpListener : IDisposable 29 | { 30 | List lastReceivedMessages; 31 | IPEndPoint localIpEndPoint; 32 | IPEndPoint senderIpEndPoint; 33 | UdpClient socket; 34 | 35 | public UdpListener(string hostname, int port) 36 | { 37 | lastReceivedMessages = new List(); 38 | localIpEndPoint = new IPEndPoint(IPAddress.Parse(hostname), port); 39 | socket = new UdpClient(localIpEndPoint); 40 | socket.Client.ReceiveTimeout = 2000; 41 | senderIpEndPoint = new IPEndPoint(IPAddress.Any, 0); 42 | } 43 | 44 | // Receive messages until it receives count of them or times out. 45 | // This call is blocking; you may want to run it in a 46 | // thread while you send the message. 47 | public void Listen(object count = null) 48 | { 49 | try 50 | { 51 | if (count == null) 52 | count = 1; 53 | for (int i = 0; i < (int)count; i++) 54 | { 55 | byte[] lastReceivedBytes = socket.Receive(ref senderIpEndPoint); 56 | lastReceivedMessages.Add(Encoding.ASCII.GetString(lastReceivedBytes, 0, 57 | lastReceivedBytes.Length)); 58 | } 59 | } 60 | catch (SocketException ex) 61 | { 62 | // If we timeout, stop listening. 63 | // If we get another error, propagate it upwards. 64 | if (ex.ErrorCode == 10060) // WSAETIMEDOUT; Timeout error 65 | return; 66 | else 67 | throw ex; 68 | } 69 | } 70 | 71 | // Clear and return the message list. Clearing the list allows us to use the 72 | // same UdpListener instance for several tests; we never have to worry about a 73 | // message received from a previous test giving us a false positive. 74 | public List GetAndClearLastMessages() 75 | { 76 | List messagesToReturn = lastReceivedMessages; 77 | lastReceivedMessages = new List(); 78 | return messagesToReturn; 79 | } 80 | 81 | public void Dispose() 82 | { 83 | socket.Close(); 84 | } 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/Tests/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.0-*", 3 | "testRunner": "nunit", 4 | 5 | "buildOptions": { 6 | "warningsAsErrors": true 7 | }, 8 | 9 | "dependencies": { 10 | "dotnet-test-nunit": "3.4.0-beta-3", 11 | "StatsdClient": "1.0.*", 12 | "NUnit": "3.6.0", 13 | "NSubstitute": "1.10.0" 14 | }, 15 | 16 | "frameworks": { 17 | "net452": { 18 | "imports": "dnxcore50" 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /update-projectjson.ps1: -------------------------------------------------------------------------------- 1 | # This script sets the project.json version to to be the version of the appveyor build. This is so our nupkg's will have the appveyor build number in them 2 | 3 | $projectJsonFileLocation = "src/StatsdClient/project.json" 4 | $newVersion = $env:APPVEYOR_BUILD_VERSION 5 | if($newVersion -eq $null) 6 | { 7 | return 8 | } 9 | 10 | Write-Host "$projectJsonFileLocation will be update with new version '$newVersion'" 11 | 12 | $json = (Get-Content $projectJsonFileLocation -Raw) | ConvertFrom-Json 13 | $json.version = $newVersion 14 | $json | ConvertTo-Json -depth 100 | Out-File $projectJsonFileLocation --------------------------------------------------------------------------------