├── .gitignore ├── CHANGELOG.md ├── LICENCE.txt ├── README.md ├── StatsdClient.sln ├── StatsdClient ├── AsyncLock.cs ├── ConnectionType.cs ├── IOutputChannel.cs ├── IStatsd.cs ├── MetricType.cs ├── NullOutputChannel.cs ├── OutputChannelExtensions.cs ├── Properties │ └── AssemblyInfo.cs ├── Statsd.cs ├── StatsdClient.csproj ├── StatsdClient.nuspec ├── StatsdClientExtensions.cs ├── StatsdExtensions.cs ├── TcpOutputChannel.cs ├── TimingToken.cs └── UdpOutputChannel.cs ├── StatsdClientTests ├── StatsdClientTests.csproj ├── StatsdExtensionsTests.cs ├── StatsdTests.cs └── TestData.cs ├── appveyor.yml └── build └── Replace-FileString.ps1 /.gitignore: -------------------------------------------------------------------------------- 1 | nuget/lib 2 | 3 | # Build Folders (you can keep bin if you'd like, to store dlls and pdbs) 4 | [Bb]in/ 5 | [Oo]bj/ 6 | 7 | # mstest test results 8 | TestResults 9 | 10 | ## Ignore Visual Studio temporary files, build results, and 11 | ## files generated by popular Visual Studio add-ons. 12 | 13 | # User-specific files 14 | *.suo 15 | *.user 16 | *.sln.docstates 17 | 18 | # Build results 19 | [Dd]ebug/ 20 | [Rr]elease/ 21 | x64/ 22 | *_i.c 23 | *_p.c 24 | *.ilk 25 | *.meta 26 | *.obj 27 | *.pch 28 | *.pdb 29 | *.pgc 30 | *.pgd 31 | *.rsp 32 | *.sbr 33 | *.tlb 34 | *.tli 35 | *.tlh 36 | *.tmp 37 | *.log 38 | *.vspscc 39 | *.vssscc 40 | .builds 41 | 42 | # Visual C++ cache files 43 | ipch/ 44 | *.aps 45 | *.ncb 46 | *.opensdf 47 | *.sdf 48 | 49 | # Visual Studio profiler 50 | *.psess 51 | *.vsp 52 | *.vspx 53 | 54 | # Guidance Automation Toolkit 55 | *.gpState 56 | 57 | # ReSharper is a .NET coding add-in 58 | _ReSharper* 59 | 60 | # NCrunch 61 | *.ncrunch* 62 | .*crunch*.local.xml 63 | 64 | # Installshield output folder 65 | [Ee]xpress 66 | 67 | # DocProject is a documentation generator add-in 68 | DocProject/buildhelp/ 69 | DocProject/Help/*.HxT 70 | DocProject/Help/*.HxC 71 | DocProject/Help/*.hhc 72 | DocProject/Help/*.hhk 73 | DocProject/Help/*.hhp 74 | DocProject/Help/Html2 75 | DocProject/Help/html 76 | 77 | # Click-Once directory 78 | publish 79 | 80 | # Publish Web Output 81 | *.Publish.xml 82 | 83 | # NuGet Packages Directory 84 | packages 85 | 86 | # Windows Azure Build Output 87 | csx 88 | *.build.csdef 89 | 90 | # Windows Store app package directory 91 | AppPackages/ 92 | 93 | # Others 94 | [Bb]in 95 | [Oo]bj 96 | sql 97 | TestResults 98 | [Tt]est[Rr]esult* 99 | *.Cache 100 | ClientBin 101 | [Ss]tyle[Cc]op.* 102 | ~$* 103 | *.dbmdl 104 | Generated_Code #added for RIA/Silverlight projects 105 | 106 | # Backup & report files from converting an old project file to a newer 107 | # Visual Studio version. Backup files are not needed, because we have git ;-) 108 | _UpgradeReport_Files/ 109 | Backup*/ 110 | UpgradeLog*.XML 111 | 112 | Nuget Releases/ -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # statsd-csharp-client Changelog 2 | 3 | ## v1.5.0.0 4 | * Targeting .NET Standard 2.0 5 | 6 | ## v1.4.0.0 7 | * Added async support 8 | 9 | ## v1.3.0.0 10 | * Added support for Calendargrams 11 | 12 | ## v1.2.1.0 13 | * Fixed a bug in the tcp output channel's retry logic 14 | * Skip DNS resolution on the UDP client if already an IP Address 15 | * Fall back to the Null Output Channel if the client is created with an empty host name. 16 | 17 | ## v1.2.0.0 18 | * Support the Raw metric format 19 | * A few more unit tests 20 | * Fixed a bug where you couldn't start up if the host could not be resolved 21 | 22 | ## v1.1.0.0 23 | * Added a TCP output channel 24 | * Added a simpler default constructor for connecting via UDP 25 | 26 | ## v1.0.1.0 27 | * Added support for .net 3.5 and .net 4.0 users 28 | 29 | ## v1.0.0.0 30 | * First upload of the client library 31 | * Supports counts, gauges and timers 32 | * Can output to any statsd-compatible service -------------------------------------------------------------------------------- /LICENCE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Luke Venediger 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # statsd-csharp-client 2 | [![Build status](https://ci.appveyor.com/api/projects/status/gjbhn5h0go7xmdn0/branch/master?svg=true)](https://ci.appveyor.com/project/Exceptionless/statsd-csharp-client) 3 | 4 | A simple c# client library for [statsd.net](https://github.com/lukevenediger/statsd.net/) and [statsd](https://github.com/etsy/statsd/). 5 | 6 | # Features 7 | * Log counts, timings, gauges, sets, calendargrams and raw metrics 8 | * Has an additional API that uses dynamics to create and submit stats 9 | * Fault-tolerant client that can be configured to fail silently (with a warning) if misconfigured 10 | * IStatsdClient interface for easy mocking in unit tests 11 | * Allows for customisation of every output stat to do things like screen metrics before sending 12 | * Supports a user-defined prefix to prepend to every metric 13 | * Send metrics over a UDP or TCP connection 14 | 15 | Coming soon: 16 | * Support for count sampling and histograms 17 | * batch-and-pump - collecting stats and sending them out in a batch at regular intervals 18 | * Output to an HTTP endpoint 19 | 20 | # Download and Install 21 | Install the [StatsdCsharpClient](https://nuget.org/packages/StatsdCsharpClient/) via nuget: 22 | ```bash 23 | PM> Install-Package StatsdCsharpClient 24 | ``` 25 | 26 | # Quickstart 27 | Assuming your server is running on localhost and listening on port 12000: 28 | ```csharp 29 | using StatsdClient; 30 | ... 31 | var statsd = new Statsd("localhost", 12000); 32 | // Log a count 33 | statsd.LogCount( "site.hits" ); 34 | // Log a gauge 35 | statsd.LogGauge( "site.activeUsers", numActiveUsers ); 36 | // Log a timing 37 | statsd.LogTiming( "site.pageLoad", 100 /* milliseconds */ ); 38 | // Log a raw metric 39 | statsd.LogRaw ("already.aggregated", 982, 1885837485 /* epoch timestamp */ ); 40 | // Log a calendargram 41 | statsd.LogCalendargram("order.completed", "user_13143", CalendargramRetentionPeriod.HOUR); 42 | ``` 43 | 44 | You can also wrap your code in a `using` block to measure the latency by using the LogTiming(string) extension method: 45 | ```csharp 46 | using StatsdClient; 47 | ... 48 | using (statsd.LogTiming( "site.db.fetchReport" )) 49 | { 50 | // do some work 51 | } 52 | // At this point your latency has been sent to the server 53 | ``` 54 | 55 | ## Dynamic Stats Builder 56 | There's also a nifty set of extension methods that let you define your stats without using strings. Using the example provided above, but now using the builder: 57 | ```csharp 58 | var statsd = new StatsdClient("localhost", 12000); 59 | // Log a count 60 | statsd.count.site.hits += 1; 61 | // Log a gauge 62 | statsd.gauge.site.activeUsers += numActiveUsers; 63 | // Log a timing 64 | statsd.site.pageLoad += 100; /* milliseconds */ 65 | ``` 66 | 67 | ## TCP Output Channel 68 | Metrics can be delivered over a TCP connection by specifying ConnectionType.Tcp during construction: 69 | ```csharp 70 | var statsd = new Statsd("localhost", 12001); 71 | // Continue as normal 72 | ``` 73 | 74 | The connection will attempt to reconnect if something goes wrong, and will try three times before giving up. Use the retryOnDisconnect parameter to enable/disable this, and the retryAttempts parameter to specify the number of times to try the request again. 75 | 76 | # Project Information 77 | 78 | ## Target Runtimes 79 | * .Net 4.5 80 | 81 | ## Authors 82 | Luke Venediger - lukev@lukev.net and [@lukevenediger](http://twitter.com/lukevenediger) 83 | 84 | ## See Also 85 | * [statsd.net](https://github.com/lukevenediger/statsd.net/) 86 | * [statsd](https://github.com/etsy/statsd) 87 | * [graphite](https://github.com/graphite-project) 88 | * [StatsdCsharpClient on nuget.org](https://nuget.org/packages/StatsdCsharpClient/) -------------------------------------------------------------------------------- /StatsdClient.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.27703.2018 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{3981BC36-0C55-4127-827F-292024E6A8E9}" 7 | ProjectSection(SolutionItems) = preProject 8 | appveyor.yml = appveyor.yml 9 | CHANGELOG.md = CHANGELOG.md 10 | README.md = README.md 11 | EndProjectSection 12 | EndProject 13 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StatsdClient", "StatsdClient\StatsdClient.csproj", "{D9C7B372-074B-4649-B45E-01FE10724AF9}" 14 | EndProject 15 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StatsdClientTests", "StatsdClientTests\StatsdClientTests.csproj", "{92D8C6A7-E3FE-4F33-8A14-74C484C3B0C3}" 16 | EndProject 17 | Global 18 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 19 | Debug|Any CPU = Debug|Any CPU 20 | Release|Any CPU = Release|Any CPU 21 | EndGlobalSection 22 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 23 | {D9C7B372-074B-4649-B45E-01FE10724AF9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 24 | {D9C7B372-074B-4649-B45E-01FE10724AF9}.Debug|Any CPU.Build.0 = Debug|Any CPU 25 | {D9C7B372-074B-4649-B45E-01FE10724AF9}.Release|Any CPU.ActiveCfg = Release|Any CPU 26 | {D9C7B372-074B-4649-B45E-01FE10724AF9}.Release|Any CPU.Build.0 = Release|Any CPU 27 | {92D8C6A7-E3FE-4F33-8A14-74C484C3B0C3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 28 | {92D8C6A7-E3FE-4F33-8A14-74C484C3B0C3}.Debug|Any CPU.Build.0 = Debug|Any CPU 29 | {92D8C6A7-E3FE-4F33-8A14-74C484C3B0C3}.Release|Any CPU.ActiveCfg = Release|Any CPU 30 | {92D8C6A7-E3FE-4F33-8A14-74C484C3B0C3}.Release|Any CPU.Build.0 = Release|Any CPU 31 | EndGlobalSection 32 | GlobalSection(SolutionProperties) = preSolution 33 | HideSolutionNode = FALSE 34 | EndGlobalSection 35 | GlobalSection(ExtensibilityGlobals) = postSolution 36 | SolutionGuid = {58DCC75C-9AC1-4997-8510-8229F312BFA7} 37 | EndGlobalSection 38 | EndGlobal 39 | -------------------------------------------------------------------------------- /StatsdClient/AsyncLock.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | 5 | namespace StatsdClient 6 | { 7 | internal sealed class AsyncLock 8 | { 9 | private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1); 10 | private readonly Task _releaser; 11 | 12 | public AsyncLock() 13 | { 14 | _releaser = Task.FromResult((IDisposable)new Releaser(this)); 15 | } 16 | 17 | public Task LockAsync() 18 | { 19 | var wait = _semaphore.WaitAsync(); 20 | return wait.IsCompleted ? 21 | _releaser : 22 | wait.ContinueWith((_, state) => (IDisposable)state, 23 | _releaser.Result, CancellationToken.None, 24 | TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default); 25 | } 26 | 27 | private sealed class Releaser : IDisposable 28 | { 29 | private readonly AsyncLock _toRelease; 30 | 31 | internal Releaser(AsyncLock toRelease) 32 | { 33 | _toRelease = toRelease; 34 | } 35 | 36 | public void Dispose() 37 | { 38 | _toRelease._semaphore.Release(); 39 | } 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /StatsdClient/ConnectionType.cs: -------------------------------------------------------------------------------- 1 | namespace StatsdClient 2 | { 3 | /// 4 | /// The network connection type 5 | /// 6 | public enum ConnectionType 7 | { 8 | /// 9 | /// Udp (recommended) 10 | /// 11 | Udp, 12 | /// 13 | /// Tcp 14 | /// 15 | Tcp 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /StatsdClient/IOutputChannel.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace StatsdClient 4 | { 5 | /// 6 | /// Contract for sending raw statds lines to the server 7 | /// 8 | public interface IOutputChannel 9 | { 10 | /// 11 | /// Sends a line of stats data to the server asynchronously. 12 | /// 13 | Task SendAsync(string line); 14 | } 15 | } -------------------------------------------------------------------------------- /StatsdClient/IStatsd.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace StatsdClient 4 | { 5 | /// 6 | /// Interface for the statsd.net client 7 | /// 8 | public interface IStatsd 9 | { 10 | /// 11 | /// Log a count for a metric 12 | /// 13 | Task LogCountAsync(string name, long count = 1); 14 | 15 | /// 16 | /// Log a gauge value 17 | /// 18 | Task LogGaugeAsync(string name, long value); 19 | 20 | /// 21 | /// Log a gauge value 22 | /// 23 | Task LogGaugeAsync(string name, double value); 24 | 25 | /// 26 | /// Log a gauge value 27 | /// 28 | Task LogGaugeAsync(string name, decimal value); 29 | 30 | /// 31 | /// Log a latency / Timing 32 | /// 33 | Task LogTimingAsync(string name, long milliseconds); 34 | 35 | /// 36 | /// Log the number of unique occurrances of something 37 | /// 38 | /// 39 | /// 40 | Task LogSetAsync(string name, long value); 41 | 42 | /// 43 | /// Log a calendargram metric 44 | /// 45 | /// The metric namespace 46 | /// The unique value to be counted in the time period 47 | /// The time period, can be one of h,d,dow,w,m 48 | Task LogCalendargramAsync(string name, string value, string period); 49 | 50 | /// 51 | /// Log a calendargram metric 52 | /// 53 | /// The metric namespace 54 | /// The unique value to be counted in the time period 55 | /// The time period, can be one of h,d,dow,w,m 56 | Task LogCalendargramAsync(string name, long value, string period); 57 | 58 | /// 59 | /// Log a raw metric that will not get aggregated on the server. 60 | /// 61 | /// The metric name. 62 | /// The metric value. 63 | /// (optional) The epoch timestamp. Leave this blank to have the server assign an epoch for you. 64 | Task LogRawAsync(string name, long value, long? epoch = null); 65 | } 66 | } -------------------------------------------------------------------------------- /StatsdClient/MetricType.cs: -------------------------------------------------------------------------------- 1 | namespace StatsdClient 2 | { 3 | /// 4 | /// A list of metric types that statsd.net supports 5 | /// 6 | public static class MetricType 7 | { 8 | /// 9 | /// The number of times something happened. 10 | /// 11 | public const string COUNT = "c"; 12 | /// 13 | /// The time it took for something to happen. 14 | /// 15 | public const string TIMING = "ms"; 16 | /// 17 | /// The value of some measurement at this very moment. 18 | /// 19 | public const string GAUGE = "g"; 20 | /// 21 | /// The number of times each event has been seen. 22 | /// 23 | public const string SET = "s"; 24 | /// 25 | /// A raw metric that won't be aggregated on the server. 26 | /// 27 | public const string RAW = "r"; 28 | /// 29 | /// A metric that calculates unique hits per hour, day, day-of-week, week or month 30 | /// 31 | public const string CALENDARGRAM = "cg"; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /StatsdClient/NullOutputChannel.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace StatsdClient 4 | { 5 | internal sealed class NullOutputChannel : IOutputChannel 6 | { 7 | public Task SendAsync(string line) 8 | { 9 | return Task.FromResult(0); 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /StatsdClient/OutputChannelExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace StatsdClient 2 | { 3 | public static class OutputChannelExtensions 4 | { 5 | /// 6 | /// 7 | /// 8 | /// 9 | /// 10 | public static void Send(this IOutputChannel outputChannel, string line) 11 | { 12 | outputChannel.SendAsync(line).ConfigureAwait(false).GetAwaiter().GetResult(); 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /StatsdClient/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.InteropServices; 3 | 4 | // General Information about an assembly is controlled through the following 5 | // set of attributes. Change these attribute values to modify the information 6 | // associated with an assembly. 7 | [assembly: AssemblyTitle("StatsdClient")] 8 | [assembly: AssemblyDescription("A statsd.net and statsd client for c#")] 9 | [assembly: AssemblyConfiguration("")] 10 | [assembly: AssemblyCompany("Luke Venediger")] 11 | [assembly: AssemblyProduct("StatsdClient")] 12 | [assembly: AssemblyCopyright("Copyright © Luke Venediger 2017")] 13 | [assembly: AssemblyTrademark("")] 14 | [assembly: AssemblyCulture("")] 15 | 16 | // Setting ComVisible to false makes the types in this assembly not visible 17 | // to COM components. If you need to access a type in this assembly from 18 | // COM, set the ComVisible attribute to true on that type. 19 | [assembly: ComVisible(false)] 20 | 21 | // The following GUID is for the ID of the typelib if this project is exposed to COM 22 | [assembly: Guid("8ac00668-c22c-4335-bc35-fc310f50d42a")] 23 | 24 | [assembly: AssemblyVersion("1.5.0.0")] 25 | [assembly: AssemblyFileVersion("1.5.0.0")] -------------------------------------------------------------------------------- /StatsdClient/Statsd.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Threading.Tasks; 4 | 5 | namespace StatsdClient 6 | { 7 | /// 8 | /// The statsd client library. 9 | /// 10 | public class Statsd : IStatsd 11 | { 12 | private string _prefix; 13 | private IOutputChannel _outputChannel; 14 | 15 | /// 16 | /// Creates a new instance of the Statsd client. 17 | /// 18 | /// The statsd or statsd.net server. 19 | /// 20 | public Statsd(string host, int port) 21 | { 22 | if (String.IsNullOrEmpty(host)) 23 | { 24 | Trace.TraceWarning("Statsd client initialised with empty host address. Dropping back to NullOutputChannel."); 25 | InitialiseInternal(() => new NullOutputChannel(), "", false); 26 | } 27 | else 28 | { 29 | InitialiseInternal(() => new UdpOutputChannel(host, port), "", false); 30 | } 31 | } 32 | 33 | /// 34 | /// Creates a new instance of the Statsd client. 35 | /// 36 | /// The statsd or statsd.net server. 37 | /// 38 | /// A string prefix to prepend to every metric. 39 | /// If True, rethrows any exceptions caught due to bad configuration. 40 | /// Choose between a UDP (recommended) or TCP connection. 41 | /// Retry the connection if it fails (TCP only). 42 | /// Number of times to retry before giving up (TCP only). 43 | public Statsd(string host, 44 | int port, 45 | ConnectionType connectionType = ConnectionType.Udp, 46 | string prefix = null, 47 | bool rethrowOnError = false, 48 | bool retryOnDisconnect = true, 49 | int retryAttempts = 3) 50 | { 51 | InitialiseInternal(() => 52 | { 53 | return connectionType == ConnectionType.Tcp 54 | ? (IOutputChannel)new TcpOutputChannel(host, port, retryOnDisconnect, retryAttempts) 55 | : (IOutputChannel)new UdpOutputChannel(host, port); 56 | }, 57 | prefix, 58 | rethrowOnError); 59 | } 60 | 61 | /// 62 | /// Creates a new instance of the Statsd client. 63 | /// 64 | /// The statsd or statsd.net server. 65 | /// 66 | /// A string prefix to prepend to every metric. 67 | /// If True, rethrows any exceptions caught due to bad configuration. 68 | /// Optional output channel (useful for mocking / testing). 69 | public Statsd(string host, int port, string prefix = null, bool rethrowOnError = false, IOutputChannel outputChannel = null) 70 | { 71 | if (outputChannel == null) 72 | { 73 | InitialiseInternal(() => new UdpOutputChannel(host, port), prefix, rethrowOnError); 74 | } 75 | else 76 | { 77 | InitialiseInternal(() => outputChannel, prefix, rethrowOnError); 78 | } 79 | } 80 | 81 | private void InitialiseInternal(Func createOutputChannel, string prefix, bool rethrowOnError) 82 | { 83 | _prefix = prefix; 84 | if (_prefix != null && _prefix.EndsWith(".")) 85 | { 86 | _prefix = _prefix.Substring(0, _prefix.Length - 1); 87 | } 88 | try 89 | { 90 | _outputChannel = createOutputChannel(); 91 | } 92 | catch (Exception ex) 93 | { 94 | if (rethrowOnError) 95 | { 96 | throw; 97 | } 98 | Trace.TraceError("Could not initialise the Statsd client: {0} - falling back to NullOutputChannel.", ex.Message); 99 | _outputChannel = new NullOutputChannel(); 100 | } 101 | } 102 | 103 | /// 104 | /// Log a counter. 105 | /// 106 | /// The metric name. 107 | /// The counter value (defaults to 1). 108 | public async Task LogCountAsync(string name, long count = 1) 109 | { 110 | await SendMetricAsync(MetricType.COUNT, name, _prefix, count); 111 | } 112 | 113 | /// 114 | /// Log a timing / latency 115 | /// 116 | /// The metric name. 117 | /// The duration, in milliseconds, for this metric. 118 | public async Task LogTimingAsync(string name, long milliseconds) 119 | { 120 | await SendMetricAsync(MetricType.TIMING, name, _prefix, milliseconds); 121 | } 122 | 123 | /// 124 | /// Log a gauge. 125 | /// 126 | /// The metric name 127 | /// The value for this gauge 128 | public async Task LogGaugeAsync(string name, long value) 129 | { 130 | await SendMetricAsync(MetricType.GAUGE, name, _prefix, value); 131 | } 132 | 133 | /// 134 | /// Log a gauge. 135 | /// 136 | /// The metric name 137 | /// The value for this gauge 138 | public async Task LogGaugeAsync(string name, double value) { 139 | await SendMetricAsync(MetricType.GAUGE, name, _prefix, value); 140 | } 141 | 142 | /// 143 | /// Log a gauge. 144 | /// 145 | /// The metric name 146 | /// The value for this gauge 147 | public async Task LogGaugeAsync(string name, decimal value) { 148 | await SendMetricAsync(MetricType.GAUGE, name, _prefix, value); 149 | } 150 | 151 | /// 152 | /// Log to a set 153 | /// 154 | /// The metric name. 155 | /// The value to log. 156 | /// 157 | /// Logging to a set is about counting the number 158 | /// of occurrences of each event. 159 | /// 160 | public async Task LogSetAsync(string name, long value) 161 | { 162 | await SendMetricAsync(MetricType.SET, name, _prefix, value); 163 | } 164 | 165 | /// 166 | /// Log a raw metric that will not get aggregated on the server. 167 | /// 168 | /// The metric name. 169 | /// The metric value. 170 | /// (optional) The epoch timestamp. Leave this blank to have the server assign an epoch for you. 171 | public async Task LogRawAsync(string name, long value, long? epoch = null) 172 | { 173 | await SendMetricAsync(MetricType.RAW, name, String.Empty, value, epoch.HasValue ? epoch.ToString() : null); 174 | } 175 | 176 | /// 177 | /// Log a calendargram metric 178 | /// 179 | /// The metric namespace 180 | /// The unique value to be counted in the time period 181 | /// The time period, can be one of h,d,dow,w,m 182 | public async Task LogCalendargramAsync(string name, string value, string period) 183 | { 184 | await SendMetricAsync(MetricType.CALENDARGRAM, name, _prefix, value, period); 185 | } 186 | 187 | /// 188 | /// Log a calendargram metric 189 | /// 190 | /// The metric namespace 191 | /// The unique value to be counted in the time period 192 | /// The time period, can be one of h,d,dow,w,m 193 | public async Task LogCalendargramAsync(string name, long value, string period) 194 | { 195 | await SendMetricAsync(MetricType.CALENDARGRAM, name, _prefix, value, period); 196 | } 197 | 198 | private async Task SendMetricAsync(string metricType, string name, string prefix, long value, string postFix = null) 199 | { 200 | if (value < 0) 201 | { 202 | Trace.TraceWarning("Metric value for {0} was less than zero: {1}. Not sending.", name, value); 203 | return; 204 | } 205 | 206 | await SendMetricAsync(metricType, name, prefix, value.ToString(), postFix); 207 | } 208 | 209 | private async Task SendMetricAsync(string metricType, string name, string prefix, double value, string postFix = null) 210 | { 211 | if (value < 0) 212 | { 213 | Trace.TraceWarning("Metric value for {0} was less than zero: {1}. Not sending.", name, value); 214 | return; 215 | } 216 | 217 | await SendMetricAsync(metricType, name, prefix, value.ToString(), postFix); 218 | } 219 | 220 | private async Task SendMetricAsync(string metricType, string name, string prefix, decimal value, string postFix = null) 221 | { 222 | if (value < 0) 223 | { 224 | Trace.TraceWarning("Metric value for {0} was less than zero: {1}. Not sending.", name, value); 225 | return; 226 | } 227 | 228 | await SendMetricAsync(metricType, name, prefix, value.ToString(), postFix); 229 | } 230 | 231 | private async Task SendMetricAsync(string metricType, string name, string prefix, string value, string postFix = null) 232 | { 233 | if (String.IsNullOrEmpty(name)) 234 | { 235 | throw new ArgumentNullException("name"); 236 | } 237 | await _outputChannel.SendAsync(PrepareMetric(metricType, name, prefix, value, postFix)); 238 | } 239 | 240 | /// 241 | /// Prepare a metric prior to sending it off ot the Graphite server. 242 | /// 243 | /// 244 | /// 245 | /// 246 | /// 247 | /// A value to append to the end of the line. 248 | /// The formatted metric 249 | protected virtual string PrepareMetric(string metricType, string name, string prefix, string value, string postFix = null) 250 | { 251 | return (String.IsNullOrEmpty(prefix) ? name : (prefix + "." + name)) 252 | + ":" + value 253 | + "|" + metricType 254 | + (postFix == null ? String.Empty : "|" + postFix); 255 | } 256 | } 257 | } -------------------------------------------------------------------------------- /StatsdClient/StatsdClient.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0 5 | true 6 | true 7 | snupkg 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | false 16 | 17 | 18 | -------------------------------------------------------------------------------- /StatsdClient/StatsdClient.nuspec: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | StatsdCsharpClient 5 | $version$ 6 | Simple StatsD Client 7 | Luke Venediger 8 | Luke Venediger 9 | https://github.com/lukevenediger/statsd-csharp-client/ 10 | A simple c# client library for statsd and statsd.net 11 | 12 | The simple statsd client for .Net is a robust, easy-to-use way of feeding metrics into a statsd-compatible server. You can use it to log counts, timings and gauges. 13 | 14 | Features include: 15 | * Log counts, timings, gauges, sets and raw metrics 16 | * Has an additional API that uses dynamics to create and submit stats 17 | * Fault-tolerant client that can be configured to fail silently (with a warning) if misconfigured 18 | * IStatsdClient interface for easy mocking in unit tests 19 | * Allows for customisation of every output stat to do things like screen metrics before sending 20 | * Supports a user-defined prefix to prepend to every metric 21 | * Outputs to UDP or TCP 22 | 23 | .Net 4.5 Users: 24 | * Use the StatsdExtensions to define metrics without having to manipulate strings 25 | * The dynamic stats builder interface provides a cleaner alternative to creating and logging metrics 26 | 27 | Licence: MIT 28 | 29 | 30 | * Fixed a bug in the tcp output channel's retry logic 31 | * Skip DNS resolution on the UDP client if already an IP Address 32 | * Fall back to the Null Output Channel if the client is created with an empty host name. 33 | 34 | false 35 | Copyright 2013 Luke Venediger 36 | statsd statsd.net graphite etsy metrics 37 | 38 | -------------------------------------------------------------------------------- /StatsdClient/StatsdClientExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace StatsdClient 4 | { 5 | /// 6 | /// A set of extensions for use with the StatsdClient library. 7 | /// 8 | public static class StatsdClientExtensions 9 | { 10 | /// 11 | /// Log a counter. 12 | /// 13 | /// The statsd client instance. 14 | /// The metric name. 15 | /// The counter value (defaults to 1). 16 | public static void LogCount(this IStatsd client, string name, int count = 1) 17 | { 18 | client.LogCountAsync(name, count).ConfigureAwait(false).GetAwaiter().GetResult(); 19 | } 20 | 21 | /// 22 | /// Log a timing / latency 23 | /// 24 | /// The statsd client instance. 25 | /// The metric name. 26 | /// The duration, in milliseconds, for this metric. 27 | public static void LogTiming(this IStatsd client, string name, long milliseconds) 28 | { 29 | client.LogTimingAsync(name, milliseconds).ConfigureAwait(false).GetAwaiter().GetResult(); 30 | } 31 | 32 | /// 33 | /// Log a gauge. 34 | /// 35 | /// The statsd client instance. 36 | /// The metric name 37 | /// The value for this gauge 38 | public static void LogGauge(this IStatsd client, string name, int value) 39 | { 40 | client.LogGaugeAsync(name, value).ConfigureAwait(false).GetAwaiter().GetResult(); 41 | } 42 | 43 | /// 44 | /// Log a gauge. 45 | /// 46 | /// The statsd client instance. 47 | /// The metric name 48 | /// The value for this gauge 49 | public static void LogGauge(this IStatsd client, string name, double value) { 50 | client.LogGaugeAsync(name, value).ConfigureAwait(false).GetAwaiter().GetResult(); 51 | } 52 | 53 | /// 54 | /// Log a gauge. 55 | /// 56 | /// The statsd client instance. 57 | /// The metric name 58 | /// The value for this gauge 59 | public static void LogGauge(this IStatsd client, string name, decimal value) { 60 | client.LogGaugeAsync(name, value).ConfigureAwait(false).GetAwaiter().GetResult(); 61 | } 62 | 63 | /// 64 | /// Log to a set 65 | /// 66 | /// The statsd client instance. 67 | /// The metric name. 68 | /// The value to log. 69 | /// 70 | /// Logging to a set is about counting the number of occurrences of each event. 71 | /// 72 | public static void LogSet(this IStatsd client, string name, int value) 73 | { 74 | client.LogSetAsync(name, value).ConfigureAwait(false).GetAwaiter().GetResult(); 75 | } 76 | 77 | /// 78 | /// Log a raw metric that will not get aggregated on the server. 79 | /// 80 | /// The statsd client instance. 81 | /// The metric name. 82 | /// The metric value. 83 | /// (optional) The epoch timestamp. Leave this blank to have the server assign an epoch for you. 84 | public static void LogRaw(this IStatsd client, string name, int value, long? epoch = null) 85 | { 86 | client.LogRawAsync(name, value, epoch).ConfigureAwait(false).GetAwaiter().GetResult(); 87 | } 88 | 89 | /// 90 | /// Log a calendargram metric 91 | /// 92 | /// The statsd client instance. 93 | /// The metric namespace 94 | /// The unique value to be counted in the time period 95 | /// The time period, can be one of h,d,dow,w,m 96 | public static void LogCalendargram(this IStatsd client, string name, string value, string period) 97 | { 98 | client.LogCalendargramAsync(name, value, period).ConfigureAwait(false).GetAwaiter().GetResult(); 99 | } 100 | 101 | /// 102 | /// Log a calendargram metric 103 | /// 104 | /// The statsd client instance. 105 | /// The metric namespace 106 | /// The unique value to be counted in the time period 107 | /// The time period, can be one of h,d,dow,w,m 108 | public static void LogCalendargram(this IStatsd client, string name, long value, string period) 109 | { 110 | client.LogCalendargramAsync(name, value, period).ConfigureAwait(false).GetAwaiter().GetResult(); 111 | } 112 | 113 | /// 114 | /// Log a timing metric 115 | /// 116 | /// The statsd client instance. 117 | /// The namespace of the timing metric. 118 | /// The duration to log (will be converted into milliseconds) 119 | public static void LogTiming(this IStatsd client, string name, TimeSpan duration) 120 | { 121 | client.LogTiming(name, (long)duration.TotalMilliseconds); 122 | } 123 | 124 | /// 125 | /// Starts a timing metric that will be logged when the TimingToken is disposed. 126 | /// 127 | /// The statsd clien instance. 128 | /// The namespace of the timing metric. 129 | /// A timing token that has been initialised with a start datetime. 130 | /// 131 | /// Wrap the code you want to measure in a using() {} block. The TimingToken instance will log the duration when it is disposed. 132 | /// 133 | public static TimingToken LogTiming(this IStatsd client, string name) 134 | { 135 | return new TimingToken(client, name); 136 | } 137 | } 138 | } -------------------------------------------------------------------------------- /StatsdClient/StatsdExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Dynamic; 4 | using System.Linq.Expressions; 5 | 6 | namespace StatsdClient 7 | { 8 | /// 9 | /// A set of extensions for building up metrics using dynamic objects. 10 | /// 11 | public static class StatsdExtensions 12 | { 13 | /// 14 | /// Start logging a count 15 | /// 16 | public static dynamic count(this IStatsd statsd) 17 | { 18 | return new StatsBuilderInternal(statsd, MetricType.COUNT); 19 | } 20 | 21 | /// 22 | /// Start logging a timing / latency 23 | /// 24 | public static dynamic timing(this IStatsd statsd) 25 | { 26 | return new StatsBuilderInternal(statsd, MetricType.TIMING); 27 | } 28 | 29 | /// 30 | /// Start logging a gauge 31 | /// 32 | public static dynamic gauge(this IStatsd statsd) 33 | { 34 | return new StatsBuilderInternal(statsd, MetricType.GAUGE); 35 | } 36 | 37 | /// 38 | /// Start logging a set 39 | /// 40 | public static dynamic set(this IStatsd statsd) 41 | { 42 | return new StatsBuilderInternal(statsd, MetricType.SET); 43 | } 44 | 45 | private class StatsBuilderInternal : DynamicObject 46 | { 47 | private IStatsd _statsd; 48 | private List _parts; 49 | private string _metricType; 50 | private static Dictionary _handlerMap = 51 | new Dictionary(); 52 | 53 | private delegate void BinaryOperationHandler(IStatsd client, string metricType, string name, object arg); 54 | 55 | static StatsBuilderInternal() 56 | { 57 | _handlerMap.Add(typeof(int), IntHandler); 58 | _handlerMap.Add(typeof(decimal), DecimalHandler); 59 | _handlerMap.Add(typeof(double), DoubleHandler); 60 | _handlerMap.Add(typeof(float), DoubleHandler); 61 | } 62 | 63 | private static void IntHandler(IStatsd client, string metricType, string name, object arg) 64 | { 65 | var value = Convert.ToInt32(arg); 66 | switch (metricType) 67 | { 68 | case MetricType.COUNT: 69 | client.LogCount(name, value); 70 | break; 71 | case MetricType.GAUGE: 72 | client.LogGauge(name, value); 73 | break; 74 | case MetricType.TIMING: 75 | client.LogTiming(name, value); 76 | break; 77 | case MetricType.SET: 78 | client.LogSet(name, value); 79 | break; 80 | } 81 | } 82 | 83 | private static void DoubleHandler(IStatsd client, string metricType, string name, object arg) 84 | { 85 | var value = Convert.ToDouble(arg); 86 | switch (metricType) 87 | { 88 | case MetricType.COUNT: 89 | throw new NotSupportedException(); 90 | case MetricType.GAUGE: 91 | client.LogGauge(name, value); 92 | break; 93 | case MetricType.TIMING: 94 | throw new NotSupportedException(); 95 | case MetricType.SET: 96 | throw new NotSupportedException(); 97 | } 98 | } 99 | 100 | private static void DecimalHandler(IStatsd client, string metricType, string name, object arg) 101 | { 102 | var value = Convert.ToDecimal(arg); 103 | switch (metricType) 104 | { 105 | case MetricType.COUNT: 106 | throw new NotSupportedException(); 107 | case MetricType.GAUGE: 108 | client.LogGauge(name, value); 109 | break; 110 | case MetricType.TIMING: 111 | throw new NotSupportedException(); 112 | case MetricType.SET: 113 | throw new NotSupportedException(); 114 | } 115 | } 116 | 117 | public StatsBuilderInternal(IStatsd statsd, string metricType) 118 | { 119 | _statsd = statsd; 120 | _parts = new List(); 121 | _metricType = metricType; 122 | } 123 | 124 | public override bool TryGetMember(GetMemberBinder binder, out object result) 125 | { 126 | if (binder.Name != "_") 127 | { 128 | _parts.Add(binder.Name); 129 | } 130 | result = this; 131 | return true; 132 | } 133 | 134 | public override bool TrySetMember(SetMemberBinder binder, object value) 135 | { 136 | return true; 137 | } 138 | 139 | public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result) 140 | { 141 | if (binder.Name == "_" && args.Length == 1) 142 | { 143 | _parts.Add(args[0].ToString()); 144 | result = this; 145 | return true; 146 | } 147 | result = null; 148 | return false; 149 | } 150 | 151 | public override bool TryBinaryOperation(BinaryOperationBinder binder, object arg, out object result) 152 | { 153 | if (binder.Operation == ExpressionType.AddAssign) 154 | { 155 | var argType = arg.GetType(); 156 | var name = String.Join(".", _parts); 157 | 158 | if (!_handlerMap.ContainsKey(argType)) 159 | { 160 | throw new ApplicationException("Handler not supported for type: " + argType); 161 | } 162 | 163 | var handler = _handlerMap[argType]; 164 | handler(_statsd, _metricType, name, arg); 165 | 166 | result = null; 167 | return true; 168 | } 169 | result = null; 170 | return false; 171 | } 172 | } 173 | } 174 | } -------------------------------------------------------------------------------- /StatsdClient/TcpOutputChannel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.IO; 4 | using System.Net.Sockets; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace StatsdClient 9 | { 10 | internal sealed class TcpOutputChannel : IOutputChannel 11 | { 12 | private readonly TcpClient _tcpClient; 13 | private NetworkStream _stream; 14 | private readonly string _host; 15 | private readonly int _port; 16 | private readonly bool _reconnectEnabled; 17 | private readonly int _retryAttempts; 18 | private readonly AsyncLock _asyncLock; 19 | 20 | public TcpOutputChannel(string host, int port, bool reconnectEnabled = true, int retryAttempts = 3) 21 | { 22 | _host = host; 23 | _port = port; 24 | _reconnectEnabled = reconnectEnabled; 25 | _retryAttempts = retryAttempts; 26 | _tcpClient = new TcpClient(); 27 | _asyncLock = new AsyncLock(); 28 | } 29 | 30 | public async Task SendAsync(string line) 31 | { 32 | await SendWithRetryAsync(line, _reconnectEnabled ? _retryAttempts - 1 : 0); 33 | } 34 | 35 | private async Task SendWithRetryAsync(string line, int attemptsLeft) 36 | { 37 | string errorMessage = null; 38 | try 39 | { 40 | if (!_tcpClient.Connected) 41 | { 42 | await RestoreConnectionAsync(); 43 | } 44 | 45 | var bytesToSend = Encoding.UTF8.GetBytes(line + Environment.NewLine); 46 | await _stream.WriteAsync(bytesToSend, 0, bytesToSend.Length); 47 | } 48 | catch (IOException ex) 49 | { 50 | errorMessage = String.Format("Sending metrics via TCP failed with an IOException: {0}", ex.Message); 51 | } 52 | catch (SocketException ex) 53 | { 54 | // No more attempts left, so log it and continue 55 | errorMessage = String.Format("Sending metrics via TCP failed with a SocketException: {0}, code: {1}", ex.Message, ex.SocketErrorCode); 56 | } 57 | 58 | if (errorMessage != null) 59 | { 60 | if (attemptsLeft > 0) 61 | { 62 | await SendWithRetryAsync(line, --attemptsLeft); 63 | } 64 | else 65 | { 66 | // No more attempts left, so log it and continue 67 | Trace.TraceWarning(errorMessage); 68 | } 69 | } 70 | } 71 | 72 | private async Task RestoreConnectionAsync() 73 | { 74 | if (!_tcpClient.Connected) 75 | { 76 | using (await _asyncLock.LockAsync()) 77 | { 78 | if (!_tcpClient.Connected) 79 | { 80 | await _tcpClient.ConnectAsync(_host, _port); 81 | _stream = _tcpClient.GetStream(); 82 | } 83 | } 84 | } 85 | } 86 | } 87 | } -------------------------------------------------------------------------------- /StatsdClient/TimingToken.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | 4 | namespace StatsdClient 5 | { 6 | /// 7 | /// A class that is used to measure a latency wrapped in a using block. 8 | /// 9 | [DebuggerDisplay("{_name} - IsActive = {_stopwatch.IsRunning}")] 10 | public sealed class TimingToken : IDisposable 11 | { 12 | private IStatsd _client; 13 | private string _name; 14 | private Stopwatch _stopwatch; 15 | 16 | internal TimingToken(IStatsd client, string name) 17 | { 18 | _stopwatch = Stopwatch.StartNew(); 19 | _client = client; 20 | _name = name; 21 | } 22 | 23 | /// 24 | /// Stops the internal timer and logs a latency metric. 25 | /// 26 | public void Dispose() 27 | { 28 | _stopwatch.Stop(); 29 | _client.LogTiming(_name, (int)_stopwatch.ElapsedMilliseconds); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /StatsdClient/UdpOutputChannel.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using System.Net; 3 | using System.Net.Sockets; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace StatsdClient 8 | { 9 | internal sealed class UdpOutputChannel : IOutputChannel 10 | { 11 | private readonly UdpClient _udpClient; 12 | 13 | public Socket ClientSocket 14 | { 15 | get 16 | { 17 | return _udpClient.Client; 18 | } 19 | } 20 | 21 | public UdpOutputChannel(string hostOrIPAddress, int port) 22 | { 23 | IPAddress ipAddress; 24 | // Is this an IP address already? 25 | if (!IPAddress.TryParse(hostOrIPAddress, out ipAddress)) 26 | { 27 | // Convert to ipv4 address 28 | ipAddress = Dns.GetHostAddresses(hostOrIPAddress).First(p => p.AddressFamily == AddressFamily.InterNetwork); 29 | } 30 | _udpClient = new UdpClient(); 31 | _udpClient.Connect(ipAddress, port); 32 | } 33 | 34 | public async Task SendAsync(string line) 35 | { 36 | var payload = Encoding.UTF8.GetBytes(line); 37 | await _udpClient.SendAsync(payload, payload.Length); 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /StatsdClientTests/StatsdClientTests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Library 5 | netcoreapp2.0 6 | 7 | 8 | 9 | 10 | 11 | Copyright © 2013 12 | 1.1.0.0 13 | 1.1.0.0 14 | 1.1.0.0 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /StatsdClientTests/StatsdExtensionsTests.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Microsoft.VisualStudio.TestTools.UnitTesting; 3 | using Moq; 4 | using StatsdClient; 5 | 6 | namespace StatsdClientTests 7 | { 8 | [TestClass] 9 | public class StatsdExtensionsTests 10 | { 11 | private Mock _outputChannel; 12 | private Statsd _statsd; 13 | 14 | [TestInitialize] 15 | public void Initialise() 16 | { 17 | _outputChannel = new Mock(); 18 | _statsd = new Statsd("localhost", 12000, outputChannel: _outputChannel.Object); 19 | } 20 | 21 | [TestMethod] 22 | public void Count_SendToStatsd_Success() 23 | { 24 | _outputChannel.Setup(p => p.SendAsync("foo.bar:1|c")).Returns(Task.FromResult(false)).Verifiable(); 25 | _statsd.count().foo.bar += 1; 26 | _outputChannel.VerifyAll(); 27 | } 28 | 29 | [TestMethod] 30 | public void Gauge_SendToStatsd_Success() 31 | { 32 | _outputChannel.Setup(p => p.SendAsync("foo.bar:1|g")).Returns(Task.FromResult(false)).Verifiable(); 33 | _statsd.gauge().foo.bar += 1; 34 | _outputChannel.VerifyAll(); 35 | } 36 | 37 | [TestMethod] 38 | public void Gauge_SendFloatToStatsd_Success() 39 | { 40 | _outputChannel.Setup(p => p.SendAsync("foo.bar:1.5|g")).Returns(Task.FromResult(false)).Verifiable(); 41 | _statsd.gauge().foo.bar += 1.5f; 42 | _outputChannel.VerifyAll(); 43 | } 44 | 45 | [TestMethod] 46 | public void Gauge_SendDoubleToStatsd_Success() 47 | { 48 | _outputChannel.Setup(p => p.SendAsync("foo.bar:2.5|g")).Returns(Task.FromResult(false)).Verifiable(); 49 | _statsd.gauge().foo.bar += 2.5d; 50 | _outputChannel.VerifyAll(); 51 | } 52 | 53 | [TestMethod] 54 | public void Gauge_SendDecimalToStatsd_Success() 55 | { 56 | _outputChannel.Setup(p => p.SendAsync("foo.bar:2.5|g")).Returns(Task.FromResult(false)).Verifiable(); 57 | _statsd.gauge().foo.bar += 2.5M; 58 | _outputChannel.VerifyAll(); 59 | } 60 | 61 | [TestMethod] 62 | public void Timing_SendToStatsd_Success() 63 | { 64 | _outputChannel.Setup(p => p.SendAsync("foo.bar:1|ms")).Returns(Task.FromResult(false)).Verifiable(); 65 | _statsd.timing().foo.bar += 1; 66 | _outputChannel.VerifyAll(); 67 | } 68 | 69 | [TestMethod] 70 | public void Count_AddNamePartAsString_Success() 71 | { 72 | _outputChannel.Setup(p => p.SendAsync("foo.bar:1|ms")).Returns(Task.FromResult(false)).Verifiable(); 73 | _statsd.timing().foo._("bar")._ += 1; 74 | _outputChannel.VerifyAll(); 75 | } 76 | } 77 | } -------------------------------------------------------------------------------- /StatsdClientTests/StatsdTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Microsoft.VisualStudio.TestTools.UnitTesting; 4 | using StatsdClient; 5 | using Moq; 6 | 7 | namespace StatsdClientTests { 8 | [TestClass] 9 | public class StatsdTests { 10 | private Mock _outputChannel; 11 | private Statsd _statsd; 12 | private readonly TestData _testData; 13 | 14 | public StatsdTests() { 15 | _testData = new TestData(); 16 | } 17 | 18 | [TestInitialize] 19 | public void Initialise() { 20 | _outputChannel = new Mock(); 21 | _statsd = new Statsd("localhost", 12000, outputChannel: _outputChannel.Object); 22 | } 23 | 24 | #region Parameter Checks 25 | 26 | [TestMethod] 27 | [ExpectedException(typeof(ArgumentNullException))] 28 | public void LogCount_NameIsNull_ExpectArgumentNullException() { 29 | _statsd.LogCount(null); 30 | } 31 | 32 | [TestMethod] 33 | [ExpectedException(typeof(ArgumentNullException))] 34 | public void LogGauge_NameIsNull_ExpectArgumentNullException() { 35 | _statsd.LogGauge(null, _testData.NextInteger); 36 | } 37 | 38 | [TestMethod] 39 | [ExpectedException(typeof(ArgumentNullException))] 40 | public void LogTiming_NameIsNull_ExpectArgumentNullException() { 41 | _statsd.LogTiming(null, _testData.NextInteger); 42 | } 43 | 44 | #endregion 45 | 46 | [TestMethod] 47 | public void LogCount_ValidInput_Success() { 48 | var stat = _testData.NextStatName; 49 | var count = _testData.NextInteger; 50 | _outputChannel.Setup(p => p.SendAsync(stat + ":" + count.ToString() + "|c")).Returns(Task.FromResult(false)).Verifiable(); 51 | 52 | _statsd.LogCount(stat, count); 53 | 54 | _outputChannel.VerifyAll(); 55 | } 56 | 57 | [TestMethod] 58 | public void LogTiming_ValidInput_Success() { 59 | var stat = _testData.NextStatName; 60 | var count = _testData.NextInteger; 61 | _outputChannel.Setup(p => p.SendAsync(stat + ":" + count.ToString() + "|ms")).Returns(Task.FromResult(false)).Verifiable(); 62 | 63 | _statsd.LogTiming(stat, count); 64 | 65 | _outputChannel.VerifyAll(); 66 | } 67 | 68 | [TestMethod] 69 | public void LogGauge_ValidInput_Success() { 70 | var stat = _testData.NextStatName; 71 | var count = _testData.NextInteger; 72 | _outputChannel.Setup(p => p.SendAsync(stat + ":" + count.ToString() + "|g")).Returns(Task.FromResult(false)).Verifiable(); 73 | 74 | _statsd.LogGauge(stat, count); 75 | 76 | _outputChannel.VerifyAll(); 77 | } 78 | 79 | [TestMethod] 80 | public void Constructor_PrefixEndsInPeriod_RemovePeriod() { 81 | var statsd = new Statsd("localhost", 12000, "foo.", outputChannel: _outputChannel.Object); 82 | var stat = _testData.NextStatName; 83 | var count = _testData.NextInteger; 84 | _outputChannel.Setup(p => p.SendAsync("foo." + stat + ":" + count.ToString() + "|c")).Returns(Task.FromResult(false)).Verifiable(); 85 | 86 | statsd.LogCount(stat, count); 87 | 88 | _outputChannel.VerifyAll(); 89 | } 90 | 91 | [TestMethod] 92 | public void LogCount_NullPrefix_DoesNotStartNameWithPeriod() { 93 | var statsd = new Statsd("localhost", 12000, prefix: null, outputChannel: _outputChannel.Object); 94 | var inputStat = "some.stat:1|c"; 95 | _outputChannel.Setup(p => p.SendAsync(It.Is(q => q == inputStat))).Returns(Task.FromResult(false)).Verifiable(); 96 | statsd.LogCount("some.stat"); 97 | _outputChannel.VerifyAll(); 98 | } 99 | 100 | [TestMethod] 101 | public void LogCount_EmptyStringPrefix_DoesNotStartNameWithPeriod() { 102 | var statsd = new Statsd("localhost", 12000, prefix: "", outputChannel: _outputChannel.Object); 103 | var inputStat = "some.stat:1|c"; 104 | _outputChannel.Setup(p => p.SendAsync(It.Is(q => q == inputStat))).Returns(Task.FromResult(false)).Verifiable(); 105 | statsd.LogCount("some.stat"); 106 | _outputChannel.VerifyAll(); 107 | } 108 | 109 | [TestMethod] 110 | public void LogRaw_WithoutEpoch_Valid() { 111 | var statsd = new Statsd("localhost", 12000, prefix: "", outputChannel: _outputChannel.Object); 112 | var inputStat = "my.raw.stat:12934|r"; 113 | _outputChannel.Setup(p => p.SendAsync(It.Is(q => q == inputStat))).Returns(Task.FromResult(false)).Verifiable(); 114 | statsd.LogRaw("my.raw.stat", 12934); 115 | _outputChannel.VerifyAll(); 116 | } 117 | 118 | [TestMethod] 119 | public void LogRaw_WithEpoch_Valid() { 120 | var statsd = new Statsd("localhost", 12000, prefix: "", outputChannel: _outputChannel.Object); 121 | var almostAnEpoch = DateTime.Now.Ticks; 122 | var inputStat = "my.raw.stat:12934|r|" + almostAnEpoch; 123 | _outputChannel.Setup(p => p.SendAsync(It.Is(q => q == inputStat))).Returns(Task.FromResult(false)).Verifiable(); 124 | statsd.LogRaw("my.raw.stat", 12934, almostAnEpoch); 125 | _outputChannel.VerifyAll(); 126 | } 127 | 128 | [TestMethod] 129 | public void CreateClient_WithInvalidHostName_DoesNotError() { 130 | var statsd = new Statsd("nowhere.here.or.anywhere", 12000); 131 | statsd.LogCount("test.stat"); 132 | } 133 | 134 | [TestMethod] 135 | public void CreateClient_WithIPAddress_DoesNotError() { 136 | var statsd = new Statsd("127.0.0.1", 12000); 137 | statsd.LogCount("test.stat"); 138 | } 139 | 140 | [TestMethod] 141 | public void CreateClient_WithInvalidCharactersInHostName_DoesNotError() { 142 | var statsd = new Statsd("@%)(F(FSDLKDEQ423t0-vbdfb", 12000); 143 | statsd.LogCount("test.foo"); 144 | } 145 | } 146 | } -------------------------------------------------------------------------------- /StatsdClientTests/TestData.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace StatsdClientTests 4 | { 5 | public class TestData 6 | { 7 | private static string[] WORDS = "Lorem ipsum dolor sit amet consectetur adipisicing elit sed do eiusmod tempor incididunt ut labore et dolore magna aliqua Ut enim ad minim veniam quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur Excepteur sint occaecat cupidatat non proident sunt in culpa qui officia deserunt mollit anim id est laborum".Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); 8 | 9 | private Random _random; 10 | private int _lastInteger; 11 | 12 | public TestData() 13 | { 14 | _random = new Random(); 15 | } 16 | 17 | public int NextInteger 18 | { 19 | get { return (_lastInteger = _random.Next(0, Int32.MaxValue)); } 20 | } 21 | 22 | public int LastInteger 23 | { 24 | get { return _lastInteger; } 25 | } 26 | 27 | public string NextStatName 28 | { 29 | get 30 | { 31 | var length = _random.Next(1, 5); 32 | var stat = new string[length]; 33 | for (int i = 0; i < length; i++) 34 | { 35 | stat[i] = WORDS[_random.Next(0, WORDS.Length - 1)]; 36 | } 37 | return String.Join(".", stat); 38 | } 39 | } 40 | 41 | public string NextWord 42 | { 43 | get 44 | { 45 | return WORDS[_random.Next(0, WORDS.Length - 1)]; 46 | } 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | version: 1.4.{build} 2 | os: Visual Studio 2015 3 | clone_depth: 2 4 | configuration: Release 5 | 6 | pull_requests: 7 | do_not_increment_build_number: true 8 | 9 | init: 10 | - git config --global core.autocrlf true 11 | - ps: $env:GIT_HASH=$env:APPVEYOR_REPO_COMMIT.Substring(0, 10) 12 | - ps: If ("$env:APPVEYOR_REPO_BRANCH" -ne "master") { $env:VERSION_SUFFIX="-pre" } 13 | 14 | install: 15 | - choco install gitlink 16 | 17 | assembly_info: 18 | patch: true 19 | file: AssemblyInfo.* 20 | assembly_version: "{version}.0" 21 | assembly_file_version: "{version}.0" 22 | assembly_informational_version: "{version}$(VERSION_SUFFIX) $(GIT_HASH)" 23 | 24 | before_build: 25 | - nuget restore 26 | - ps: .\build\Replace-FileString -Pattern '\$version\$' -Replacement "$($env:appveyor_build_version)$($env:VERSION_SUFFIX)" -Path StatsdClient\StatsdClient.nuspec -Overwrite 27 | 28 | build: 29 | project: StatsdClient.sln 30 | verbosity: minimal 31 | publish_nuget: true 32 | 33 | before_package: 34 | - ps: if (-not $env:APPVEYOR_PULL_REQUEST_NUMBER) { & "GitLink" .\ } 35 | 36 | #on_failure: 37 | # - ps: $blockRdp = $true; iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1')) 38 | 39 | artifacts: 40 | - path: artifacts\*.nupkg 41 | name: ReleaseArtifacts 42 | 43 | deploy: 44 | - provider: Environment 45 | name: NuGet 46 | - provider: GitHub 47 | auth_token: 48 | secure: 0s81q7bweVLTFSOKxnIhan7el6bIFiN8HJ1kYJzOkeFXX7wgGSq9bs/rV53X9qpf 49 | draft: true 50 | on: 51 | branch: master 52 | appveyor_repo_tag: true -------------------------------------------------------------------------------- /build/Replace-FileString.ps1: -------------------------------------------------------------------------------- 1 | # Replace-FileString.ps1 2 | # Written by Bill Stewart (bstewart@iname.com) 3 | # 4 | # Replaces strings in files using a regular expression. Supports 5 | # multi-line searching and replacing. 6 | 7 | #requires -version 2 8 | 9 | <# 10 | .SYNOPSIS 11 | Replaces strings in files using a regular expression. 12 | 13 | .DESCRIPTION 14 | Replaces strings in files using a regular expression. Supports 15 | multi-line searching and replacing. 16 | 17 | .PARAMETER Pattern 18 | Specifies the regular expression pattern. 19 | 20 | .PARAMETER Replacement 21 | Specifies the regular expression replacement pattern. 22 | 23 | .PARAMETER Path 24 | Specifies the path to one or more files. Wildcards are permitted. Each 25 | file is read entirely into memory to support multi-line searching and 26 | replacing, so performance may be slow for large files. 27 | 28 | .PARAMETER LiteralPath 29 | Specifies the path to one or more files. The value of the this 30 | parameter is used exactly as it is typed. No characters are interpreted 31 | as wildcards. Each file is read entirely into memory to support 32 | multi-line searching and replacing, so performance may be slow for 33 | large files. 34 | 35 | .PARAMETER CaseSensitive 36 | Specifies case-sensitive matching. The default is to ignore case. 37 | 38 | .PARAMETER Multiline 39 | Changes the meaning of ^ and $ so they match at the beginning and end, 40 | respectively, of any line, and not just the beginning and end of the 41 | entire file. The default is that ^ and $, respectively, match the 42 | beginning and end of the entire file. 43 | 44 | .PARAMETER UnixText 45 | Causes $ to match only linefeed (\n) characters. By default, $ matches 46 | carriage return+linefeed (\r\n). (Windows-based text files usually use 47 | \r\n as line terminators, while Unix-based text files usually use only 48 | \n.) 49 | 50 | .PARAMETER Overwrite 51 | Overwrites a file by creating a temporary file containing all 52 | replacements and then replacing the original file with the temporary 53 | file. The default is to output but not overwrite. 54 | 55 | .PARAMETER Force 56 | Allows overwriting of read-only files. Note that this parameter cannot 57 | override security restrictions. 58 | 59 | .PARAMETER Encoding 60 | Specifies the encoding for the file when -Overwrite is used. Possible 61 | values are: ASCII, BigEndianUnicode, Unicode, UTF32, UTF7, or UTF8. The 62 | default value is ASCII. 63 | 64 | .INPUTS 65 | System.IO.FileInfo. 66 | 67 | .OUTPUTS 68 | System.String without the -Overwrite parameter, or nothing with the 69 | -Overwrite parameter. 70 | 71 | .LINK 72 | about_Regular_Expressions 73 | 74 | .EXAMPLE 75 | C:\>Replace-FileString.ps1 '(Ferb) and (Phineas)' '$2 and $1' Story.txt 76 | This command replaces the string 'Ferb and Phineas' with the string 77 | 'Phineas and Ferb' in the file Story.txt and outputs the file. Note 78 | that the pattern and replacement strings are enclosed in single quotes 79 | to prevent variable expansion. 80 | 81 | .EXAMPLE 82 | C:\>Replace-FileString.ps1 'Perry' 'Agent P' Ferb.txt -Overwrite 83 | This command replaces the string 'Perry' with the string 'Agent P' in 84 | the file Ferb.txt and overwrites the file. 85 | #> 86 | 87 | [CmdletBinding(DefaultParameterSetName="Path", 88 | SupportsShouldProcess=$TRUE)] 89 | param( 90 | [parameter(Mandatory=$TRUE,Position=0)] 91 | [String] $Pattern, 92 | [parameter(Mandatory=$TRUE,Position=1)] 93 | [String] [AllowEmptyString()] $Replacement, 94 | [parameter(Mandatory=$TRUE,ParameterSetName="Path", 95 | Position=2,ValueFromPipeline=$TRUE)] 96 | [String[]] $Path, 97 | [parameter(Mandatory=$TRUE,ParameterSetName="LiteralPath", 98 | Position=2)] 99 | [String[]] $LiteralPath, 100 | [Switch] $CaseSensitive, 101 | [Switch] $Multiline, 102 | [Switch] $UnixText, 103 | [Switch] $Overwrite, 104 | [Switch] $Force, 105 | [String] $Encoding="ASCII" 106 | ) 107 | 108 | begin { 109 | # Throw an error if $Encoding is not valid. 110 | $encodings = @("ASCII","BigEndianUnicode","Unicode","UTF32","UTF7", 111 | "UTF8") 112 | if ($encodings -notcontains $Encoding) { 113 | throw "Encoding must be one of the following: $encodings" 114 | } 115 | 116 | # Extended test-path: Check the parameter set name to see if we 117 | # should use -literalpath or not. 118 | function test-pathEx($path) { 119 | switch ($PSCmdlet.ParameterSetName) { 120 | "Path" { 121 | test-path $path 122 | } 123 | "LiteralPath" { 124 | test-path -literalpath $path 125 | } 126 | } 127 | } 128 | 129 | # Extended get-childitem: Check the parameter set name to see if we 130 | # should use -literalpath or not. 131 | function get-childitemEx($path) { 132 | switch ($PSCmdlet.ParameterSetName) { 133 | "Path" { 134 | get-childitem $path -force 135 | } 136 | "LiteralPath" { 137 | get-childitem -literalpath $path -force 138 | } 139 | } 140 | } 141 | 142 | # Outputs the full name of a temporary file in the specified path. 143 | function get-tempname($path) { 144 | do { 145 | $tempname = join-path $path ([IO.Path]::GetRandomFilename()) 146 | } 147 | while (test-path $tempname) 148 | $tempname 149 | } 150 | 151 | # Use '\r$' instead of '$' unless -UnixText specified because 152 | # '$' alone matches '\n', not '\r\n'. Ignore '\$' (literal '$'). 153 | if (-not $UnixText) { 154 | $Pattern = $Pattern -replace '(?