├── ci ├── user-seed.conf ├── server_ssl.conf └── inputs_ssl.conf ├── src ├── Splunk.Logging.TraceListener │ ├── Splunk.Logging.TraceListener.csproj │ ├── Splunk.Logging.TraceListener.nuspec │ ├── UdpTraceListener.cs │ ├── TcpTraceListener.cs │ └── HttpEventCollectorTraceListener.cs ├── Splunk.Logging.Common │ ├── Splunk.Logging.Common.csproj │ ├── Splunk.Logging.Common.nuspec │ ├── TcpReconnectFailureException.cs │ ├── Util.cs │ ├── ExponentialBackoffTcpReconnectionPolicy.cs │ ├── FixedSizeQueue.cs │ ├── HttpEventCollectorException.cs │ ├── TcpReconnectionPolicy.cs │ ├── HttpEventCollectorResendMiddleware.cs │ ├── HttpEventCollectorEventInfo.cs │ ├── TcpSocketWriter.cs │ └── HttpEventCollectorSender.cs └── Splunk.Logging.SLAB │ ├── Splunk.Logging.SLAB.csproj │ ├── Splunk.Logging.SLAB.nuspec │ ├── Util.cs │ ├── UdpEventSink.cs │ ├── TcpEventSink.cs │ └── HttpEventCollectorSink.cs ├── examples ├── App.config ├── Properties │ └── AssemblyInfo.cs ├── Program.cs └── examples.csproj ├── standalone-test ├── App.config ├── Properties │ └── AssemblyInfo.cs ├── Program.cs └── standalone-test.csproj ├── test └── unit-tests │ ├── unit-tests.csproj │ ├── TestUdp.cs │ ├── Util.cs │ ├── TestTcp.cs │ ├── TestTcpSocketWriter.cs │ └── TestWithSplunk.cs ├── CHANGELOG.md ├── CONTRIBUTING.md ├── .gitignore ├── appveyor.yml ├── Splunk.Logging.sln ├── LICENSE └── README.md /ci/user-seed.conf: -------------------------------------------------------------------------------- 1 | # create admin user 2 | [user_info] 3 | USERNAME = admin 4 | PASSWORD = changeme -------------------------------------------------------------------------------- /src/Splunk.Logging.TraceListener/Splunk.Logging.TraceListener.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net45 5 | Splunk, Inc. 6 | Copyright © Splunk, Inc. 2015 7 | 1.7.2.0 8 | 1.7.2.0 9 | 1.7.2 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /examples/App.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /ci/server_ssl.conf: -------------------------------------------------------------------------------- 1 | [sslConfig] 2 | enableSplunkdSSL = true 3 | useClientSSLCompression = true 4 | useSplunkdClientSSLCompression = true 5 | # enableSplunkSearchSSL has been moved to web.conf, and changed to enableSplunkWebSSL 6 | 7 | #By default, allow both v2 and v3 connections to the HTTP server 8 | supportSSLV3Only = false 9 | 10 | # For the HTTP server, Diable ciphers lower than 128-bit and disallow ciphers that 11 | # don't provide authentication and/or encryption. 12 | # Use 'openssl ciphers -v' to generate a list of supported ciphers 13 | cipherSuite = ALL:!aNULL:!eNULL:!LOW:!EXP:RC4+RSA:+HIGH:+MEDIUM 14 | -------------------------------------------------------------------------------- /standalone-test/App.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/Splunk.Logging.Common/Splunk.Logging.Common.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net45 5 | Splunk, Inc. 6 | Copyright © Splunk, Inc. 2015 7 | 1.7.2.0 8 | 1.7.2.0 9 | 1.7.2 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/Splunk.Logging.SLAB/Splunk.Logging.SLAB.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net45 5 | Splunk, Inc. 6 | Copyright © Splunk, Inc. 2015 7 | 1.7.2.0 8 | 1.7.2.0 9 | 1.7.2 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/Splunk.Logging.Common/Splunk.Logging.Common.nuspec: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Splunk.Logging.Common 5 | $version$ 6 | Splunk Logging for .NET - Common Components 7 | Splunk, Inc. 8 | Splunk, Inc. 9 | http://docs.splunk.com/skins/splunk/images/icon-default.png 10 | https://github.com/splunk/splunk-library-dotnetlogging/blob/master/LICENSE 11 | https://github.com/splunk/splunk-library-dotnetlogging 12 | false 13 | This package contains common code used by Splunk.Logging.SLAB and Splunk.Logging.TraceListener. 14 | Copyright 2015 15 | Splunk 16 | en-US 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/Splunk.Logging.SLAB/Splunk.Logging.SLAB.nuspec: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Splunk.Logging.SLAB 5 | $version$ 6 | Splunk Logging for .NET - SLAB 7 | Splunk, Inc. 8 | Splunk, Inc. 9 | http://docs.splunk.com/skins/splunk/images/icon-default.png 10 | https://github.com/splunk/splunk-library-dotnetlogging/blob/master/LICENSE 11 | https://github.com/splunk/splunk-library-dotnetlogging 12 | false 13 | This package contains EventSinks to log to Splunk from SLAB. 14 | Copyright 2015 15 | Splunk 16 | en-US 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/Splunk.Logging.TraceListener/Splunk.Logging.TraceListener.nuspec: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Splunk.Logging.TraceListener 5 | $version$ 6 | Splunk Logging for .NET - TraceListener 7 | Splunk, Inc. 8 | Splunk, Inc. 9 | http://docs.splunk.com/skins/splunk/images/icon-default.png 10 | https://github.com/splunk/splunk-library-dotnetlogging/blob/master/LICENSE 11 | https://github.com/splunk/splunk-library-dotnetlogging 12 | false 13 | This package contains code to log to Splunk via TraceListeners. 14 | Copyright 2015 15 | Splunk 16 | en-US 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /test/unit-tests/unit-tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net45 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/Splunk.Logging.Common/TcpReconnectFailureException.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Splunk, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"): you may 5 | * not use this file except in compliance with the License. You may obtain 6 | * a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations 14 | * under the License. 15 | */ 16 | 17 | namespace Splunk.Logging 18 | { 19 | /// 20 | /// Exception thrown when a TcpConnectionPolicy.Reconnect method declares 21 | /// that is cannot get a new connection and will no longer try. 22 | /// 23 | public class TcpReconnectFailureException : System.Exception 24 | { 25 | public TcpReconnectFailureException(string message) : base(message) { } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /ci/inputs_ssl.conf: -------------------------------------------------------------------------------- 1 | [sslConfig] 2 | enableSplunkdSSL = true 3 | useClientSSLCompression = true 4 | useSplunkdClientSSLCompression = true 5 | # enableSplunkSearchSSL has been moved to web.conf, and changed to enableSplunkWebSSL 6 | 7 | #By default, allow both v2 and v3 connections to the HTTP server 8 | supportSSLV3Only = false 9 | 10 | # For the HTTP server, Diable ciphers lower than 128-bit and disallow ciphers that 11 | # don't provide authentication and/or encryption. 12 | # Use 'openssl ciphers -v' to generate a list of supported ciphers 13 | cipherSuite = ALL:!aNULL:!eNULL:!LOW:!EXP:RC4+RSA:+HIGH:+MEDIUM 14 | sslPassword = $1$wtoLubkif9F8 15 | 16 | [general] 17 | pass4SymmKey = $1$lZZf5fxkNIN8 18 | 19 | [lmpool:auto_generated_pool_download-trial] 20 | description = auto_generated_pool_download-trial 21 | quota = MAX 22 | slaves = * 23 | stack_id = download-trial 24 | 25 | [lmpool:auto_generated_pool_forwarder] 26 | description = auto_generated_pool_forwarder 27 | quota = MAX 28 | slaves = * 29 | stack_id = forwarder 30 | 31 | [lmpool:auto_generated_pool_free] 32 | description = auto_generated_pool_free 33 | quota = MAX 34 | slaves = * 35 | stack_id = free -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Splunk logging for .NET Changelog 2 | 3 | ## Version 1.7.2 4 | * Updated all packages to use the new version of the csproj file that allows multi-targetting of .NET versions. 5 | * Add new examples of using `TraceListener` 6 | 7 | ## Version 1.7.1 8 | 9 | * Bump version number. 10 | 11 | ## Version 1.7.0 12 | 13 | * Update version of Newtonsoft.JSON to 11.0.2 (GitHub pull request #34). 14 | * Make HEC timestamp invariant to culture (GitHub pull request #20). 15 | 16 | ## Version 1.6.1 17 | 18 | * Add support for overriding metadata with `HttpEventCollectorSender.Send()`. 19 | 20 | ## Version 1.6.0 21 | 22 | * Add support for custom HTTP Event Collector formatter function for TraceListener. 23 | * Add support for setting timestamp other than UtcNow (GitHub Pull request #15). 24 | 25 | ## Version 1.5.0 26 | 27 | * Add support for HTTP Event Collector. 28 | 29 | ## Version 1.1 30 | 31 | ### Performance improvements 32 | 33 | * `TcpSocketWriter` now uses a `BlockingCollection` instead of a `ConcurrentQueue` internally, resulting in significantly less CPU utilization. 34 | 35 | ### Minor changes 36 | 37 | * Added xunit.runner as a dependency. 38 | 39 | ## Version 1.0 40 | 41 | * Add support for logging via TCP. 42 | * Fix behavior of TraceListeners. Now they write to the network on every invocation of Write or WriteLine 43 | and no longer try to insert timestamps. 44 | 45 | ## Version 0.8 (beta) 46 | 47 | * Initial release. 48 | -------------------------------------------------------------------------------- /examples/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("examples")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("examples")] 13 | [assembly: AssemblyCopyright("Copyright © 2019")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("05f10c76-eb18-4413-98da-b62a574e1d9b")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion("1.0.0.0")] 36 | [assembly: AssemblyFileVersion("1.0.0.0")] 37 | -------------------------------------------------------------------------------- /standalone-test/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("standalone-test")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("standalone-test")] 13 | [assembly: AssemblyCopyright("Copyright © 2015")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("b3643515-5c9f-448e-ae13-b068fae734d5")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion("1.0.0.0")] 36 | [assembly: AssemblyFileVersion("1.0.0.0")] 37 | -------------------------------------------------------------------------------- /src/Splunk.Logging.Common/Util.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Net; 4 | using System.Net.Sockets; 5 | 6 | namespace Splunk.Logging 7 | { 8 | public static class Util 9 | { 10 | /// 11 | /// Get an IPAddress corresponding to the given hostname. If no address is available, 12 | /// throws an Exception. If more than one is available, returns only one of them. 13 | /// 14 | /// a hostname or IP address to turn into an IPAddress instance 15 | /// an IPAddress instance 16 | public static IPAddress HostnameToIPAddress(this string hostname) 17 | { 18 | // If we can parse and IP address from hostname, use that. 19 | IPAddress addr; 20 | if (IPAddress.TryParse(hostname, out addr)) 21 | { 22 | return addr; 23 | } 24 | 25 | // Otherwise, use DNS lookup to get at least one IP address. 26 | IPHostEntry addresses = Dns.GetHostEntry(hostname); 27 | if (addresses.AddressList.Count() < 1) 28 | throw new Exception(string.Format("No IP address corresponding to hostname {0} found.", hostname)); 29 | else 30 | return addresses.AddressList[0]; 31 | } 32 | 33 | public static Socket OpenUdpSocket(IPAddress host, int port) 34 | { 35 | var socket = new Socket(SocketType.Dgram, ProtocolType.Udp); 36 | socket.Connect(host, port); 37 | return socket; 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | ## How to contribute 4 | 5 | If you would like to contribute to this project, see [Contributions to Splunk][indivcontrib] for more information. 6 | 7 | ## Issues & Bug Reports 8 | 9 | If you're seeing some unexpected behavior with this project, please create an [issue on GitHub][issues] with the following information: 10 | 11 | 1. Version of this project you're using (ex: 1.1) 12 | 1. Platform version (ex: Windows Server 2012) 13 | 1. Framework version (ex: .Net 4.5, Visual Studio 2013) 14 | 1. Splunk version (ex: 6.2.2) 15 | 1. Other relevant information (ex: local/remote environment, Splunk network configuration) 16 | 17 | Alternatively, if you have a Splunk question please ask on [Splunk Answers][answers] 18 | 19 | ## Pull requests 20 | 21 | We love to see pull requests! 22 | 23 | To create a pull request: 24 | 25 | 1. Fill out the [Individual Contributor Agreement][indivcontrib]. 26 | 1. Fork [the repository][repo]. 27 | 1. Make changes to the **`develop`** branch, preferably with tests. 28 | 1. Create a [pull request][pulls] against the **`develop`** branch. 29 | 30 | ## Contact us 31 | 32 | You can [contact support][contact] if you have Splunk related questions. 33 | 34 | You can reach the Developer Platform team at _devinfo@splunk.com_. 35 | 36 | [contributions]: http://dev.splunk.com/view/opensource/SP-CAAAEDM 37 | [indivcontrib]: http://dev.splunk.com/goto/individualcontributions 38 | [companycontrib]: http://dev.splunk.com/view/companycontributions/SP-CAAAEDR 39 | [answers]: http://answers.splunk.com/ 40 | [repo]: https://github.com/splunk/splunk-library-dotnetlogging 41 | [issues]: https://github.com/splunk/splunk-library-dotnetlogging/issues 42 | [pulls]: https://github.com/splunk/splunk-library-dotnetlogging/pulls 43 | [contact]: https://www.splunk.com/en_us/support-and-services.html 44 | -------------------------------------------------------------------------------- /src/Splunk.Logging.SLAB/Util.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Splunk, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"): you may 5 | * not use this file except in compliance with the License. You may obtain 6 | * a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations 14 | * under the License. 15 | */ 16 | using Microsoft.Practices.EnterpriseLibrary.SemanticLogging; 17 | using Microsoft.Practices.EnterpriseLibrary.SemanticLogging.Formatters; 18 | using System; 19 | using System.IO; 20 | 21 | namespace Splunk.Logging 22 | { 23 | /// 24 | /// An IEventTextFormatter for SLAB that writes in the form 25 | /// {timestamp} EventId={...} EventName={...} Level={...} "FormattedMessage={...}" 26 | /// 27 | public class SimpleEventTextFormatter : IEventTextFormatter 28 | { 29 | void IEventTextFormatter.WriteEvent(EventEntry eventEntry, TextWriter writer) 30 | { 31 | writer.Write(eventEntry.GetFormattedTimestamp("o") + " "); 32 | writer.Write("EventId=" + eventEntry.EventId + " "); 33 | writer.Write("EventName=" + eventEntry.Schema.EventName + " "); 34 | writer.Write("Level=" + eventEntry.Schema.Level + " "); 35 | writer.Write("\"FormattedMessage=" + eventEntry.FormattedMessage + "\" "); 36 | for (int i = 0; i < eventEntry.Payload.Count; i++) 37 | { 38 | if (i != 0) writer.Write(" "); 39 | writer.Write("\"{0}={1}\"", eventEntry.Schema.Payload[i], eventEntry.Payload[i]); 40 | } 41 | writer.WriteLine(); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /standalone-test/Program.cs: -------------------------------------------------------------------------------- 1 | using Splunk.Logging; 2 | using System; 3 | using System.Net; 4 | 5 | namespace standalone_test 6 | { 7 | /// 8 | /// Playground for Splunk logging .NET library. 9 | /// 10 | class Program 11 | { 12 | static void Main(string[] args) 13 | { 14 | DoIt(); 15 | Console.ReadLine(); 16 | } 17 | 18 | static async void DoIt() 19 | { 20 | ServicePointManager.ServerCertificateValidationCallback += (sender, certificate, chain, sslPolicyErrors) => 21 | { 22 | return true; 23 | }; 24 | 25 | var middleware = new HttpEventCollectorResendMiddleware(100); 26 | var ecSender = new HttpEventCollectorSender(new Uri("https://localhost:8088"), "92A93306-354C-46A5-9790-055C688EB0C4", null, HttpEventCollectorSender.SendMode.Sequential, 5000, 0, 0, middleware.Plugin); 27 | ecSender.OnError += o => Console.WriteLine(o.Message); 28 | ecSender.Send(DateTime.UtcNow.AddDays(-1), Guid.NewGuid().ToString(), "INFO", null, 29 | new { Foo = "Bar", test2 = "Testit2", time = ConvertToEpoch(DateTime.UtcNow.AddHours(-2)).ToString(), anotherkey="anothervalue" }); 30 | ecSender.Send(Guid.NewGuid().ToString(), "INFO", null, 31 | new { Foo = "Bar", test2 = "Testit2", time = ConvertToEpoch(DateTime.UtcNow.AddHours(-2)).ToString(), anotherkey = "anothervalue!!" }); 32 | await ecSender.FlushAsync(); 33 | } 34 | 35 | private static double ConvertToEpoch(DateTime date) 36 | { 37 | DateTime origin = new DateTime(1970, 1, 1, 0, 0, 0, 0); 38 | TimeSpan diff = date.ToUniversalTime() - origin; 39 | return Math.Floor(diff.TotalSeconds); 40 | } 41 | 42 | private static void EcSender_OnError(HttpEventCollectorException obj) 43 | { 44 | throw new NotImplementedException(); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /examples/Program.cs: -------------------------------------------------------------------------------- 1 | using Splunk.Logging; 2 | using System; 3 | using System.Diagnostics; 4 | 5 | namespace examples 6 | { 7 | class Program 8 | { 9 | static void Main(string[] args) 10 | { 11 | EnableSelfSignedCertificates(); 12 | 13 | TraceListenerExample(); 14 | } 15 | 16 | private static void TraceListenerExample() 17 | { 18 | // Replace with your HEC token 19 | string token = "1ff51387-405a-4566-9f6a-87bc3fb44424"; 20 | 21 | // TraceListener 22 | var trace = new TraceSource("demo-logger"); 23 | trace.Switch.Level = SourceLevels.All; 24 | var listener = new HttpEventCollectorTraceListener( 25 | uri: new Uri("https://127.0.0.1:8088"), 26 | token: token, 27 | batchSizeCount: 1); 28 | trace.Listeners.Add(listener); 29 | 30 | // Send some events 31 | trace.TraceEvent(TraceEventType.Error, 0, "hello world 0"); 32 | trace.TraceEvent(TraceEventType.Information, 1, "hello world 1"); 33 | trace.TraceData(TraceEventType.Information, 2, "hello world 2"); 34 | trace.TraceData(TraceEventType.Error, 3, "hello world 3"); 35 | trace.TraceData(TraceEventType.Information, 4, "hello world 4"); 36 | trace.Close(); 37 | 38 | // Now search splunk index that used by your HEC token you should see above 5 events are indexed 39 | } 40 | 41 | private static void EnableSelfSignedCertificates() 42 | { 43 | // Enable self signed certificates 44 | System.Net.ServicePointManager.ServerCertificateValidationCallback += 45 | delegate (object sender, System.Security.Cryptography.X509Certificates.X509Certificate certificate, 46 | System.Security.Cryptography.X509Certificates.X509Chain chain, 47 | System.Net.Security.SslPolicyErrors sslPolicyErrors) 48 | { 49 | return true; 50 | }; 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Build Folders (you can keep bin if you'd like, to store dlls and pdbs) 2 | [Bb]in/ 3 | [Oo]bj/ 4 | 5 | # mstest test results 6 | TestResults 7 | 8 | ## Ignore Visual Studio temporary files, build results, and 9 | ## files generated by popular Visual Studio add-ons. 10 | 11 | # User-specific files 12 | *.suo 13 | *.user 14 | *.sln.docstates 15 | 16 | # Build results 17 | [Dd]ebug/ 18 | [Rr]elease/ 19 | x64/ 20 | *_i.c 21 | *_p.c 22 | *.ilk 23 | *.meta 24 | *.obj 25 | *.pch 26 | *.pdb 27 | *.pgc 28 | *.pgd 29 | *.rsp 30 | *.sbr 31 | *.tlb 32 | *.tli 33 | *.tlh 34 | *.tmp 35 | *.log 36 | *.vspscc 37 | *.vssscc 38 | .builds 39 | 40 | # Visual C++ cache files 41 | ipch/ 42 | *.aps 43 | *.ncb 44 | *.opensdf 45 | *.sdf 46 | 47 | # Visual Studio profiler 48 | *.psess 49 | *.vsp 50 | *.vspx 51 | 52 | # Guidance Automation Toolkit 53 | *.gpState 54 | 55 | # ReSharper is a .NET coding add-in 56 | _ReSharper* 57 | 58 | # NCrunch 59 | *.ncrunch* 60 | .*crunch*.local.xml 61 | 62 | # Installshield output folder 63 | [Ee]xpress 64 | 65 | # DocProject is a documentation generator add-in 66 | DocProject/buildhelp/ 67 | DocProject/Help/*.HxT 68 | DocProject/Help/*.HxC 69 | DocProject/Help/*.hhc 70 | DocProject/Help/*.hhk 71 | DocProject/Help/*.hhp 72 | DocProject/Help/Html2 73 | DocProject/Help/html 74 | 75 | # Click-Once directory 76 | publish 77 | 78 | # Publish Web Output 79 | *.Publish.xml 80 | 81 | # NuGet Packages Directory 82 | packages 83 | 84 | # Windows Azure Build Output 85 | csx 86 | *.build.csdef 87 | 88 | # Windows Store app package directory 89 | AppPackages/ 90 | 91 | # Xamarin Studio and OS X 92 | *.DS_Store* 93 | *.userprefs 94 | 95 | # Others 96 | [Bb]in 97 | [Oo]bj 98 | sql 99 | TestResults 100 | [Tt]est[Rr]esult* 101 | *.Cache 102 | ClientBin 103 | [Ss]tyle[Cc]op.* 104 | ~$* 105 | *.dbmdl 106 | Generated_Code #added for RIA/Silverlight projects 107 | 108 | # Backup & report files from converting an old project file to a newer 109 | # Visual Studio version. Backup files are not needed, because we have git ;-) 110 | _UpgradeReport_Files/ 111 | Backup*/ 112 | UpgradeLog*.XML 113 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | version: "{build}" 2 | 3 | image: 4 | - Visual Studio 2017 5 | 6 | 7 | # Make sure we always have RDP details 8 | init: 9 | - ps: iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1')) 10 | 11 | # Build 6.2 and 6.3 12 | environment: 13 | matrix: 14 | - SPLUNK_VERSION: 7.0.1 15 | SPLUNK_BUILD: 2b5b15c4ee89 16 | SDK_APP_VERSION: 1.0.0 17 | - SPLUNK_VERSION: 7.2.0 18 | SPLUNK_BUILD: 8c86330ac18 19 | SDK_APP_VERSION: 1.0.0 20 | 21 | VisualStudioVersion: 15.0 22 | 23 | install: 24 | # Download Splunk and the SDK App 25 | - cmd: appveyor DownloadFile https://download.splunk.com/products/splunk/releases/%SPLUNK_VERSION%/splunk/windows/splunk-%SPLUNK_VERSION%-%SPLUNK_BUILD%-windows-64.zip -FileName C:\splunk.zip 26 | - cmd: appveyor DownloadFile https://github.com/splunk/sdk-app-collection/archive/%SDK_APP_VERSION%.zip -FileName C:\sdkapp.zip 27 | # Unzip It 28 | - cmd: 7z x C:\splunk.zip -oc:\splunk > NUL 29 | - cmd: 7z x C:\sdkapp.zip -oc:\splunk\splunk\etc\apps > NUL 30 | # Unzip it, rename it, and make sure it's installed 31 | - cmd: rename c:\splunk\splunk\etc\apps\sdk-app-collection-%SDK_APP_VERSION% sdk-app-collection 32 | - cmd: dir c:\splunk\splunk\etc\apps 33 | 34 | # temp workaround to resolve tsl1.2 not supported by .net, downgrade splunk to use tsl1.0 35 | - cmd: type ci\server_ssl.conf >> C:\splunk\splunk\etc\system\local\server.conf 36 | - cmd: type ci\inputs_ssl.conf >> C:\splunk\splunk\etc\system\local\inputs.conf 37 | # Create admin:changme user 38 | - cmd: type ci\user-seed.conf >> C:\splunk\splunk\etc\system\local\user-seed.conf 39 | 40 | # Install the Splunk service and start it 41 | - cmd: cmd /C "set PATH=C:\splunk\splunk && C:\splunk\splunk\bin\splunk.exe enable boot-start" 42 | - cmd: cmd /C "set PATH=C:\splunk\splunk && C:\splunk\splunk\bin\splunk.exe start --accept-license --answer-yes --no-prompt" 43 | 44 | before_build: 45 | # Restore our nuget packages 46 | - cmd: nuget restore 47 | # Set our timezone correctly 48 | - cmd: time /t 49 | - cmd: tzutil /s "Pacific Standard Time" 50 | - cmd: time /t 51 | 52 | build: 53 | verbosity: minimal 54 | 55 | # Avoid test failures from debug assertions by building in release mode 56 | configuration: 57 | - Release 58 | 59 | # test_script: 60 | # test will be automatically discovered 61 | # # Run the unit tests and acceptance tests 62 | # - ps: xunit.console.clr4.x86 test\unit-tests\bin\Debug\unit-tests.dll /appveyor -------------------------------------------------------------------------------- /src/Splunk.Logging.Common/ExponentialBackoffTcpReconnectionPolicy.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Splunk, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"): you may 5 | * not use this file except in compliance with the License. You may obtain 6 | * a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations 14 | * under the License. 15 | */ 16 | using System; 17 | using System.Net; 18 | using System.Net.Sockets; 19 | using System.Threading; 20 | using System.Threading.Tasks; 21 | 22 | namespace Splunk.Logging 23 | { 24 | /// 25 | /// TcpConnectionPolicy implementation that tries to reconnect after 26 | /// increasingly long intervals. 27 | /// 28 | /// 29 | /// The intervals double every time, starting from 0s, 1s, 2s, 4s, ... 30 | /// until 10 minutes between connections, when it plateaus and does 31 | /// not increase the interval length any further. 32 | /// 33 | public class ExponentialBackoffTcpReconnectionPolicy : ITcpReconnectionPolicy 34 | { 35 | private int ceiling = 10 * 60; // 10 minutes in seconds 36 | 37 | public Socket Connect(Func connect, IPAddress host, int port, CancellationToken cancellationToken) 38 | { 39 | int delay = 1; // in seconds 40 | while (!cancellationToken.IsCancellationRequested) 41 | { 42 | try 43 | { 44 | return connect(host, port); 45 | } 46 | catch (SocketException) { } 47 | 48 | // If this is cancelled via the cancellationToken instead of 49 | // completing its delay, the next while-loop test will fail, 50 | // the loop will terminate, and the method will return null 51 | // with no additional connection attempts. 52 | Task.Delay(delay * 1000, cancellationToken).Wait(); 53 | // The nth delay is min(10 minutes, 2^n - 1 seconds). 54 | delay = Math.Min((delay + 1) * 2 - 1, ceiling); 55 | } 56 | 57 | // cancellationToken has been cancelled. 58 | return null; 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /test/unit-tests/TestUdp.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Practices.EnterpriseLibrary.SemanticLogging; 2 | using Splunk.Logging; 3 | using System.Diagnostics; 4 | using System.Diagnostics.Tracing; 5 | using System.Net; 6 | using System.Net.Sockets; 7 | using System.Text; 8 | using System.Threading.Tasks; 9 | using Xunit; 10 | 11 | namespace integration_tests 12 | { 13 | public class TestUdp 14 | { 15 | [Trait("integration-tests", "Splunk.Logging.UdpTraceListener")] 16 | [Fact] 17 | public async Task TestUdpTraceListener() 18 | { 19 | int port = 11000; 20 | var udpclient = new UdpClient(port); 21 | 22 | var traceSource = new TraceSource("UnitTestLogger"); 23 | traceSource.Listeners.Remove("Default"); 24 | traceSource.Switch.Level = SourceLevels.All; 25 | traceSource.Listeners.Add(new UdpTraceListener(IPAddress.Loopback, port)); 26 | 27 | traceSource.TraceEvent(TraceEventType.Information, 100, "Boris"); 28 | 29 | var dgram = await udpclient.ReceiveAsync(); 30 | var receivedText = Encoding.UTF8.GetString(dgram.Buffer); 31 | Assert.Equal("UnitTestLogger Information: 100 : ", receivedText); 32 | 33 | dgram = await udpclient.ReceiveAsync(); 34 | receivedText = Encoding.UTF8.GetString(dgram.Buffer); 35 | Assert.Equal("Boris\r\n", receivedText); 36 | 37 | udpclient.Close(); 38 | traceSource.Close(); 39 | } 40 | 41 | [Trait("integration-tests", "Splunk.Logging.UdpEventSink")] 42 | [Fact] 43 | public async Task TestUdpEventSink() 44 | { 45 | int port = 11001; 46 | var udpclient = new UdpClient(port); 47 | 48 | var slabListener = new ObservableEventListener(); 49 | slabListener.Subscribe(new UdpEventSink(IPAddress.Loopback, port, new TestEventFormatter())); 50 | var source = TestEventSource.GetInstance(); 51 | slabListener.EnableEvents(source, EventLevel.LogAlways, Keywords.All); 52 | 53 | var t = udpclient.ReceiveAsync(); 54 | 55 | source.Message("Boris", "Meep"); 56 | 57 | var receivedText = Encoding.UTF8.GetString((await t).Buffer); 58 | 59 | Assert.Equal( 60 | "EventId=1 EventName=MessageInfo Level=Error \"FormattedMessage=Meep - Boris\" \"message=Boris\" \"caller=Meep\"\r\n", 61 | receivedText); 62 | 63 | udpclient.Close(); 64 | slabListener.Dispose(); 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Splunk.Logging.Common/FixedSizeQueue.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Splunk, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"): you may 5 | * not use this file except in compliance with the License. You may obtain 6 | * a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations 14 | * under the License. 15 | */ 16 | using System; 17 | using System.Collections.Concurrent; 18 | using System.Threading; 19 | 20 | namespace Splunk.Logging 21 | { 22 | /// 23 | /// A queue with a maximum size. When the queue is at its maximum size 24 | /// and a new item is queued, the oldest item in the queue is dropped. 25 | /// 26 | /// 27 | internal class FixedSizeQueue 28 | { 29 | public int Size { get; private set; } 30 | public IProgress Progress = new Progress(); 31 | public bool IsCompleted { get; private set; } 32 | 33 | private readonly BlockingCollection _collection = new BlockingCollection(); 34 | 35 | public FixedSizeQueue(int size) 36 | : base() 37 | { 38 | Size = size; 39 | IsCompleted = false; 40 | } 41 | 42 | public void Enqueue(T obj) 43 | { 44 | lock (this) 45 | { 46 | if (IsCompleted) 47 | { 48 | throw new InvalidOperationException("Tried to add an item to a completed queue."); 49 | } 50 | 51 | _collection.Add(obj); 52 | 53 | while (_collection.Count > Size) 54 | { 55 | _collection.Take(); 56 | } 57 | Progress.Report(true); 58 | } 59 | } 60 | 61 | public void CompleteAdding() 62 | { 63 | lock (this) 64 | { 65 | IsCompleted = true; 66 | } 67 | } 68 | 69 | public T Dequeue(CancellationToken cancellationToken) 70 | { 71 | return _collection.Take(cancellationToken); 72 | } 73 | 74 | public T Dequeue() 75 | { 76 | return _collection.Take(); 77 | } 78 | 79 | 80 | public decimal Count { get { return _collection.Count; } } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /examples/examples.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {05F10C76-EB18-4413-98DA-B62A574E1D9B} 8 | Exe 9 | examples 10 | examples 11 | v4.5 12 | 512 13 | true 14 | 15 | 16 | AnyCPU 17 | true 18 | full 19 | false 20 | bin\Debug\ 21 | DEBUG;TRACE 22 | prompt 23 | 4 24 | 25 | 26 | AnyCPU 27 | pdbonly 28 | true 29 | bin\Release\ 30 | TRACE 31 | prompt 32 | 4 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | {b776a5c5-a301-472e-9d21-a62306358b2c} 49 | Splunk.Logging.Common 50 | 51 | 52 | {17f12964-d0c9-41a9-9dee-a4a5a52b24ae} 53 | Splunk.Logging.TraceListener 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /src/Splunk.Logging.TraceListener/UdpTraceListener.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Splunk, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"): you may 5 | * not use this file except in compliance with the License. You may obtain 6 | * a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations 14 | * under the License. 15 | */ 16 | using System; 17 | using System.Diagnostics; 18 | using System.Net; 19 | using System.Net.Sockets; 20 | using System.Text; 21 | 22 | namespace Splunk.Logging 23 | { 24 | /// 25 | /// Write event traces to a UDP port. 26 | /// 27 | public class UdpTraceListener : TraceListener 28 | { 29 | private Socket socket; 30 | private StringBuilder buffer = new StringBuilder(); 31 | 32 | public UdpTraceListener(Socket socket) : base() 33 | { 34 | this.socket = socket; 35 | } 36 | 37 | /// 38 | /// Constructor. 39 | /// 40 | /// IP address to write to. 41 | /// UDP port to log to on the remote host. 42 | public UdpTraceListener(IPAddress host, int port) : 43 | this(Util.OpenUdpSocket(host, port)) { } 44 | 45 | /// 46 | /// Constructor. 47 | /// 48 | /// Hostname to write to. 49 | /// UDP port to log to on the remote host. 50 | public UdpTraceListener(string host, int port) : 51 | this(host.HostnameToIPAddress(), port) { } 52 | 53 | public override void Write(string message) 54 | { 55 | // Note: not thread-safe, since the threading is handled by the TraceListener machinery that 56 | // invokes this method. 57 | if (NeedIndent) 58 | WriteIndent(); 59 | socket.Send(Encoding.UTF8.GetBytes(message)); 60 | } 61 | 62 | // This is factored out so it can be overridden in the test suite. 63 | protected virtual string GetTimestamp() 64 | { 65 | return DateTime.UtcNow.ToLocalTime().ToString("o"); 66 | } 67 | 68 | public override void WriteLine(string message) 69 | { 70 | // Note: not thread-safe, since the threading is handled by the TraceListener machinery that 71 | // invokes this method. 72 | if (NeedIndent) WriteIndent(); 73 | socket.Send(Encoding.UTF8.GetBytes(message + Environment.NewLine)); 74 | } 75 | 76 | public override void Close() 77 | { 78 | socket.Close(); 79 | socket.Dispose(); 80 | base.Close(); 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/Splunk.Logging.Common/HttpEventCollectorException.cs: -------------------------------------------------------------------------------- 1 | /** 2 | * @copyright 3 | * 4 | * Copyright 2013-2015 Splunk, Inc. 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"): you may 7 | * not use this file except in compliance with the License. You may obtain 8 | * a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 14 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 15 | * License for the specific language governing permissions and limitations 16 | * under the License. 17 | */ 18 | 19 | using System; 20 | using System.Collections.Generic; 21 | using System.Net; 22 | using System.Net.Http; 23 | 24 | namespace Splunk.Logging 25 | { 26 | /// 27 | /// HTTP event collector exception. This class is used when an HTTP event collector client is 28 | /// unable to send events to the server; 29 | /// 30 | public class HttpEventCollectorException : Exception 31 | { 32 | /// 33 | /// HTTP status code. 34 | /// 35 | public HttpStatusCode StatusCode { get; private set; } 36 | 37 | /// 38 | /// Exception thrown by HTTP client when sending the data. This value 39 | /// can be null. 40 | /// 41 | public Exception WebException { get; private set; } 42 | 43 | /// 44 | /// Reply from the Splunk server. This value can be null. 45 | /// 46 | public string ServerReply { get; private set; } 47 | 48 | /// 49 | /// Reply from the Splunk server. This value can be null. 50 | /// 51 | public HttpResponseMessage Response { get; private set; } 52 | 53 | /// 54 | /// List of events that caused the problem. This value is set by HttpEventCollectorSender. 55 | /// 56 | public List Events { get; set; } 57 | 58 | /// 59 | /// HTTP event collector exception container. 60 | /// 61 | /// HTTP status code. 62 | /// Exception thrown by HTTP client when sending the data. 63 | /// Splunk server reply. 64 | /// HTTP response. 65 | /// List of events that caused the problem. 66 | public HttpEventCollectorException( 67 | HttpStatusCode code, 68 | Exception webException = null, 69 | string reply = null, 70 | HttpResponseMessage response = null, 71 | List events = null) 72 | { 73 | this.StatusCode = code; 74 | this.WebException = webException; 75 | this.ServerReply = reply; 76 | this.Response = response; 77 | this.Events = events; 78 | } 79 | } 80 | } -------------------------------------------------------------------------------- /src/Splunk.Logging.Common/TcpReconnectionPolicy.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Splunk, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"): you may 5 | * not use this file except in compliance with the License. You may obtain 6 | * a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations 14 | * under the License. 15 | */ 16 | using System; 17 | using System.Net; 18 | using System.Net.Sockets; 19 | using System.Threading; 20 | 21 | namespace Splunk.Logging 22 | { 23 | /// 24 | /// TcpConnectionPolicy encapsulates a policy for what logging via TCP should 25 | /// do when there is a socket error. 26 | /// 27 | /// 28 | /// TCP loggers in this library (TcpTraceListener and TcpEventSink) take a 29 | /// TcpConnectionPolicy as an argument to their constructor. When the TCP 30 | /// session the logger uses has an error, the logger suspends logging and calls 31 | /// the Reconnect method of an implementation of TcpConnectionPolicy to get a 32 | /// new socket. 33 | /// 34 | public interface ITcpReconnectionPolicy 35 | { 36 | // A blocking method that should eventually return a Socket when it finally 37 | // manages to get a connection, or throw a TcpReconnectFailure if the policy 38 | // says to give up trying to connect. 39 | /// 40 | /// Try to reestablish a TCP connection. 41 | /// 42 | /// 43 | /// The method should block until it either 44 | /// 45 | /// 1. succeeds and returns a connected TCP socket, or 46 | /// 2. fails and throws a TcpReconnectFailure exception, or 47 | /// 3. the cancellationToken is cancelled, in which case the method should 48 | /// return null. 49 | /// 50 | /// The method takes a zero-parameter function that encapsulates trying to 51 | /// make a single connection and a cancellation token to stop the method 52 | /// if the logging system that invoked it is disposed. 53 | /// 54 | /// For example, the default ExponentialBackoffTcpConnectionPolicy invokes 55 | /// connect after increasingly long intervals until it makes a successful 56 | /// connnection, or is cancelled by the cancellationToken, at which point 57 | /// it returns null. 58 | /// 59 | /// A zero-parameter function that tries once to 60 | /// establish a connection. 61 | /// A token used to cancel the reconnect 62 | /// attempt when the invoking logger is disposed. 63 | /// A connected TCP socket. 64 | Socket Connect(Func connect, IPAddress host, int port, CancellationToken cancellationToken); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/Splunk.Logging.SLAB/UdpEventSink.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Splunk, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"): you may 5 | * not use this file except in compliance with the License. You may obtain 6 | * a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations 14 | * under the License. 15 | */ 16 | using Microsoft.Practices.EnterpriseLibrary.SemanticLogging; 17 | using Microsoft.Practices.EnterpriseLibrary.SemanticLogging.Formatters; 18 | using System; 19 | using System.IO; 20 | using System.Net; 21 | using System.Net.Sockets; 22 | using System.Text; 23 | 24 | namespace Splunk.Logging 25 | { 26 | /// 27 | /// SLAB event to write to a UDP socket. 28 | /// 29 | public class UdpEventSink : IObserver 30 | { 31 | private Socket socket = null; 32 | private IEventTextFormatter formatter; 33 | 34 | public UdpEventSink(Socket socket, IEventTextFormatter formatter = null) 35 | { 36 | this.socket = socket; 37 | this.formatter = this.formatter = formatter != null ? formatter : new SimpleEventTextFormatter(); 38 | } 39 | 40 | /// 41 | /// Set up a sink. 42 | /// 43 | /// IP address to write to. 44 | /// UDP port on the target machine. 45 | /// An object controlling the formatting 46 | /// of the event (defaults to {timestamp} EventId={...} EventName={...} Level={...} "FormattedMessage={...}"). 47 | public UdpEventSink(IPAddress host, int port, IEventTextFormatter formatter = null) : 48 | this(Util.OpenUdpSocket(host, port), formatter) {} 49 | 50 | /// 51 | /// Set up a sink. 52 | /// 53 | /// Hostname to write to. 54 | /// UDP port on the target machine. 55 | /// An object controlling the formatting 56 | /// of the event (defaults to {timestamp} EventId={...} EventName={...} Level={...} "FormattedMessage={...}"). 57 | public UdpEventSink(string host, int port, IEventTextFormatter formatter = null) : 58 | this(host.HostnameToIPAddress(), port, formatter) { } 59 | 60 | public void OnCompleted() 61 | { 62 | socket.Close(); 63 | socket.Dispose(); 64 | } 65 | 66 | public void OnError(Exception error) 67 | { 68 | socket.Close(); 69 | socket.Dispose(); 70 | } 71 | 72 | public void OnNext(EventEntry value) 73 | { 74 | var sw = new StringWriter(); 75 | formatter.WriteEvent(value, sw); 76 | socket.Send(Encoding.UTF8.GetBytes(sw.ToString())); 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /standalone-test/standalone-test.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {027FFE79-69FE-4A69-8B71-7364F9D1D99B} 8 | Exe 9 | Properties 10 | standalone_test 11 | standalone-test 12 | v4.5 13 | 512 14 | true 15 | 16 | 17 | AnyCPU 18 | true 19 | full 20 | false 21 | bin\Debug\ 22 | DEBUG;TRACE 23 | prompt 24 | 4 25 | 26 | 27 | AnyCPU 28 | pdbonly 29 | true 30 | bin\Release\ 31 | TRACE 32 | prompt 33 | 4 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | {b776a5c5-a301-472e-9d21-a62306358b2c} 55 | Splunk.Logging.Common 56 | 57 | 58 | {17f12964-d0c9-41a9-9dee-a4a5a52b24ae} 59 | Splunk.Logging.TraceListener 60 | 61 | 62 | 63 | 70 | -------------------------------------------------------------------------------- /test/unit-tests/Util.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Splunk, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"): you may 5 | * not use this file except in compliance with the License. You may obtain 6 | * a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations 14 | * under the License. 15 | */ 16 | using Microsoft.Practices.EnterpriseLibrary.SemanticLogging; 17 | using Microsoft.Practices.EnterpriseLibrary.SemanticLogging.Formatters; 18 | using System; 19 | using System.Diagnostics.Tracing; 20 | using System.Threading.Tasks; 21 | 22 | namespace Splunk.Logging 23 | { 24 | [EventSource(Name = "TestEventSource")] 25 | public class TestEventSource : EventSource 26 | { 27 | private static TestEventSource instance = null; 28 | 29 | public class Keywords 30 | { 31 | } 32 | 33 | public class Tasks 34 | { 35 | } 36 | 37 | [Event(1, Message = "{1} - {0}", Level = EventLevel.Error)] 38 | public void Message(string message, string caller) 39 | { 40 | this.WriteEvent(1, message, caller); 41 | } 42 | 43 | public static TestEventSource GetInstance() 44 | { 45 | if (instance == null) 46 | { 47 | instance = new TestEventSource(); 48 | } 49 | return instance; 50 | } 51 | } 52 | 53 | public class AwaitableProgress : IProgress 54 | { 55 | private event Action Handler = (T x) => { }; 56 | 57 | public void Report(T value) 58 | { 59 | this.Handler(value); 60 | } 61 | 62 | public async Task AwaitProgressAsync() 63 | { 64 | var source = new TaskCompletionSource(); 65 | Action onReport = null; 66 | onReport = (T x) => 67 | { 68 | Handler -= onReport; 69 | source.SetResult(x); 70 | }; 71 | Handler += onReport; 72 | return await source.Task; 73 | } 74 | 75 | public T AwaitValue() 76 | { 77 | var task = this.AwaitProgressAsync(); 78 | task.Wait(); 79 | return task.Result; 80 | } 81 | } 82 | 83 | public static class ExtensionMethods 84 | { 85 | public static async Task AwaitValue(this AwaitableProgress progress, int value) 86 | { 87 | int reached; 88 | do 89 | { 90 | reached = await progress.AwaitProgressAsync(); 91 | } while (reached < value); 92 | } 93 | } 94 | 95 | public class TestEventFormatter : IEventTextFormatter 96 | { 97 | public void WriteEvent(EventEntry eventEntry, System.IO.TextWriter writer) 98 | { 99 | writer.Write("EventId=" + eventEntry.EventId + " "); 100 | writer.Write("EventName=" + eventEntry.Schema.EventName + " "); 101 | writer.Write("Level=" + eventEntry.Schema.Level + " "); 102 | writer.Write("\"FormattedMessage=" + eventEntry.FormattedMessage + "\" "); 103 | for (int i = 0; i < eventEntry.Payload.Count; i++) 104 | { 105 | try 106 | { 107 | if (i != 0) writer.Write(" "); 108 | writer.Write("\"{0}={1}\"", eventEntry.Schema.Payload[i], eventEntry.Payload[i]); 109 | } 110 | catch (Exception) { } 111 | } 112 | writer.WriteLine(); 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /test/unit-tests/TestTcp.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Practices.EnterpriseLibrary.SemanticLogging; 2 | using System.Diagnostics; 3 | using System.Diagnostics.Tracing; 4 | using System.IO; 5 | using System.Net; 6 | using System.Net.Sockets; 7 | using System.Threading.Tasks; 8 | using Xunit; 9 | 10 | namespace Splunk.Logging 11 | { 12 | public class TestTcp 13 | { 14 | [Trait("integration-tests", "Splunk.Logging.TcpSocketWriter")] 15 | [Fact] 16 | public async Task TestTcpSocketWriter() 17 | { 18 | var listener = new TcpListener(IPAddress.Loopback, 0); 19 | listener.Start(); 20 | int port = ((IPEndPoint) listener.Server.LocalEndPoint).Port; 21 | 22 | 23 | var writer = new TcpSocketWriter(IPAddress.Loopback, port, new ExponentialBackoffTcpReconnectionPolicy(), 24 | 10); 25 | 26 | var listenerClient = await listener.AcceptTcpClientAsync(); 27 | 28 | writer.Enqueue("This is a test.\r\n"); 29 | 30 | var receiverReader = new StreamReader(listenerClient.GetStream()); 31 | var line = await receiverReader.ReadLineAsync(); 32 | 33 | Assert.Equal(line, "This is a test."); 34 | 35 | listenerClient.Close(); 36 | listener.Stop(); 37 | } 38 | 39 | [Trait("integration-tests", "Splunk.Logging.TcpTraceListener")] 40 | [Fact] 41 | public async Task TestTcpTraceListener() 42 | { 43 | var listener = new TcpListener(IPAddress.Loopback, 0); 44 | listener.Start(); 45 | int port = ((IPEndPoint) listener.Server.LocalEndPoint).Port; 46 | 47 | 48 | var traceSource = new TraceSource("UnitTestLogger"); 49 | traceSource.Listeners.Remove("Default"); 50 | traceSource.Switch.Level = SourceLevels.All; 51 | traceSource.Listeners.Add(new TcpTraceListener( 52 | IPAddress.Loopback, port, 53 | new ExponentialBackoffTcpReconnectionPolicy())); 54 | 55 | var listenerClient = await listener.AcceptTcpClientAsync(); 56 | 57 | traceSource.TraceEvent(TraceEventType.Information, 100, "Boris"); 58 | 59 | var receiverReader = new StreamReader(listenerClient.GetStream()); 60 | var line = await receiverReader.ReadLineAsync(); 61 | 62 | Assert.True(line.EndsWith("UnitTestLogger Information: 100 : Boris")); 63 | 64 | listenerClient.Close(); 65 | listener.Stop(); 66 | traceSource.Close(); 67 | } 68 | 69 | [Trait("integration-tests", "Splunk.Logging.TcpEventSink")] 70 | [Fact] 71 | public async Task TestEventSink() 72 | { 73 | var listener = new TcpListener(IPAddress.Loopback, 0); 74 | listener.Start(); 75 | int port = ((IPEndPoint)listener.Server.LocalEndPoint).Port; 76 | 77 | var slabListener = new ObservableEventListener(); 78 | slabListener.Subscribe(new TcpEventSink(IPAddress.Loopback, port, 79 | new ExponentialBackoffTcpReconnectionPolicy(), 80 | new TestEventFormatter())); 81 | var source = TestEventSource.GetInstance(); 82 | slabListener.EnableEvents(source, EventLevel.LogAlways, Keywords.All); 83 | 84 | var listenerClient = await listener.AcceptTcpClientAsync(); 85 | 86 | source.Message("Boris", "Meep"); 87 | 88 | var receiverReader = new StreamReader(listenerClient.GetStream()); 89 | var line = await receiverReader.ReadLineAsync(); 90 | 91 | Assert.Equal( 92 | "EventId=1 EventName=MessageInfo Level=Error \"FormattedMessage=Meep - Boris\" \"message=Boris\" \"caller=Meep\"", 93 | line); 94 | 95 | listenerClient.Close(); 96 | listener.Stop(); 97 | slabListener.Dispose(); 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/Splunk.Logging.TraceListener/TcpTraceListener.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Splunk, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"): you may 5 | * not use this file except in compliance with the License. You may obtain 6 | * a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations 14 | * under the License. 15 | */ 16 | using System; 17 | using System.Diagnostics; 18 | using System.Net; 19 | using System.Net.Sockets; 20 | using System.Text; 21 | 22 | namespace Splunk.Logging 23 | { 24 | /// 25 | /// Send trace events to a TCP port. 26 | /// 27 | public class TcpTraceListener : TraceListener 28 | { 29 | private TcpSocketWriter writer; 30 | 31 | /// 32 | /// Constructor. 33 | /// 34 | /// IP address to log to. 35 | /// Port on remote host. 36 | /// An object embodying a reconnection policy for if the 37 | /// TCP session drops (defaults to ExponentialBackoffTcpConnectionPolicy). 38 | /// The maximum number of events to queue if the 39 | /// TCP session goes down. If more events are queued, old ones will be dropped 40 | /// (defaults to 10,000). 41 | /// An IProgress instance that will be triggered when 42 | /// an event is pulled from the queue and written to the TCP port (defaults to a new 43 | /// Progress object accessible via the Progress property). 44 | public TcpTraceListener(IPAddress host, int port, ITcpReconnectionPolicy policy, 45 | int maxQueueSize = 10000) : base() 46 | { 47 | this.writer = new TcpSocketWriter(host, port, policy, maxQueueSize); 48 | } 49 | 50 | /// 51 | /// Constructor. 52 | /// 53 | /// Hostname to log to. 54 | /// Port on remote host. 55 | /// An object embodying a reconnection policy for if the 56 | /// TCP session drops (defaults to ExponentialBackoffTcpConnectionPolicy). 57 | /// The maximum number of events to queue if the 58 | /// TCP session goes down. If more events are queued, old ones will be dropped 59 | /// (defaults to 10,000). 60 | /// An IProgress instance that will be triggered when 61 | /// an event is pulled from the queue and written to the TCP port (defaults to a new 62 | /// Progress object accessible via the Progress property). 63 | public TcpTraceListener(string host, int port, ITcpReconnectionPolicy policy, 64 | int maxQueueSize = 10000) : 65 | this(host.HostnameToIPAddress(), port, policy, maxQueueSize) { } 66 | 67 | /// 68 | /// Add a handler to be invoked when exceptions are thrown during the operation of 69 | /// TCP logging, in particular when SocketErrors cause reconnect attempts or when 70 | /// the reconnect policy gives up entirely. 71 | /// 72 | /// A function to handle the exception. 73 | public void AddLoggingFailureHandler(Action handler) 74 | { 75 | this.writer.LoggingFailureHandler += handler; 76 | } 77 | 78 | public override void Write(string message) 79 | { 80 | // Note: not thread-safe, since the threading is handled by the TraceListener machinery that 81 | // invokes this method. 82 | if (NeedIndent) WriteIndent(); 83 | writer.Enqueue(message); 84 | } 85 | 86 | public override void WriteLine(string message) 87 | { 88 | // Note: not thread-safe, since the threading is handled by the TraceListener machinery that 89 | // invokes this method. 90 | if (NeedIndent) WriteIndent(); 91 | writer.Enqueue(message + Environment.NewLine); 92 | } 93 | 94 | public override void Close() 95 | { 96 | writer.Dispose(); 97 | base.Close(); 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /Splunk.Logging.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{5142E63D-CF4A-48EA-8527-F6B2DB4718C0}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{2F34BC99-9753-46CC-A386-33B543EFF916}" 9 | EndProject 10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "unit-tests", "test\unit-tests\unit-tests.csproj", "{2218CC33-B1C9-4D4F-A0DA-AE6CEA0CA617}" 11 | EndProject 12 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{D6E75991-4735-43C5-8271-80F4D50BFA3C}" 13 | ProjectSection(SolutionItems) = preProject 14 | .gitignore = .gitignore 15 | CHANGELOG.md = CHANGELOG.md 16 | LICENSE = LICENSE 17 | README.md = README.md 18 | EndProjectSection 19 | EndProject 20 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Splunk.Logging.TraceListener", "src\Splunk.Logging.TraceListener\Splunk.Logging.TraceListener.csproj", "{17F12964-D0C9-41A9-9DEE-A4A5A52B24AE}" 21 | EndProject 22 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Splunk.Logging.SLAB", "src\Splunk.Logging.SLAB\Splunk.Logging.SLAB.csproj", "{74D8F7A7-8125-400D-B744-C30562F23BE0}" 23 | EndProject 24 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Splunk.Logging.Common", "src\Splunk.Logging.Common\Splunk.Logging.Common.csproj", "{B776A5C5-A301-472E-9D21-A62306358B2C}" 25 | EndProject 26 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "standalone-test", "standalone-test\standalone-test.csproj", "{027FFE79-69FE-4A69-8B71-7364F9D1D99B}" 27 | EndProject 28 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "examples", "examples\examples.csproj", "{05F10C76-EB18-4413-98DA-B62A574E1D9B}" 29 | EndProject 30 | Global 31 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 32 | Debug|Any CPU = Debug|Any CPU 33 | Release|Any CPU = Release|Any CPU 34 | EndGlobalSection 35 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 36 | {2218CC33-B1C9-4D4F-A0DA-AE6CEA0CA617}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 37 | {2218CC33-B1C9-4D4F-A0DA-AE6CEA0CA617}.Debug|Any CPU.Build.0 = Debug|Any CPU 38 | {2218CC33-B1C9-4D4F-A0DA-AE6CEA0CA617}.Release|Any CPU.ActiveCfg = Release|Any CPU 39 | {2218CC33-B1C9-4D4F-A0DA-AE6CEA0CA617}.Release|Any CPU.Build.0 = Release|Any CPU 40 | {17F12964-D0C9-41A9-9DEE-A4A5A52B24AE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 41 | {17F12964-D0C9-41A9-9DEE-A4A5A52B24AE}.Debug|Any CPU.Build.0 = Debug|Any CPU 42 | {17F12964-D0C9-41A9-9DEE-A4A5A52B24AE}.Release|Any CPU.ActiveCfg = Release|Any CPU 43 | {17F12964-D0C9-41A9-9DEE-A4A5A52B24AE}.Release|Any CPU.Build.0 = Release|Any CPU 44 | {74D8F7A7-8125-400D-B744-C30562F23BE0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 45 | {74D8F7A7-8125-400D-B744-C30562F23BE0}.Debug|Any CPU.Build.0 = Debug|Any CPU 46 | {74D8F7A7-8125-400D-B744-C30562F23BE0}.Release|Any CPU.ActiveCfg = Release|Any CPU 47 | {74D8F7A7-8125-400D-B744-C30562F23BE0}.Release|Any CPU.Build.0 = Release|Any CPU 48 | {B776A5C5-A301-472E-9D21-A62306358B2C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 49 | {B776A5C5-A301-472E-9D21-A62306358B2C}.Debug|Any CPU.Build.0 = Debug|Any CPU 50 | {B776A5C5-A301-472E-9D21-A62306358B2C}.Release|Any CPU.ActiveCfg = Release|Any CPU 51 | {B776A5C5-A301-472E-9D21-A62306358B2C}.Release|Any CPU.Build.0 = Release|Any CPU 52 | {027FFE79-69FE-4A69-8B71-7364F9D1D99B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 53 | {027FFE79-69FE-4A69-8B71-7364F9D1D99B}.Debug|Any CPU.Build.0 = Debug|Any CPU 54 | {027FFE79-69FE-4A69-8B71-7364F9D1D99B}.Release|Any CPU.ActiveCfg = Release|Any CPU 55 | {027FFE79-69FE-4A69-8B71-7364F9D1D99B}.Release|Any CPU.Build.0 = Release|Any CPU 56 | {05F10C76-EB18-4413-98DA-B62A574E1D9B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 57 | {05F10C76-EB18-4413-98DA-B62A574E1D9B}.Debug|Any CPU.Build.0 = Debug|Any CPU 58 | {05F10C76-EB18-4413-98DA-B62A574E1D9B}.Release|Any CPU.ActiveCfg = Release|Any CPU 59 | {05F10C76-EB18-4413-98DA-B62A574E1D9B}.Release|Any CPU.Build.0 = Release|Any CPU 60 | EndGlobalSection 61 | GlobalSection(SolutionProperties) = preSolution 62 | HideSolutionNode = FALSE 63 | EndGlobalSection 64 | GlobalSection(NestedProjects) = preSolution 65 | {2218CC33-B1C9-4D4F-A0DA-AE6CEA0CA617} = {2F34BC99-9753-46CC-A386-33B543EFF916} 66 | {17F12964-D0C9-41A9-9DEE-A4A5A52B24AE} = {5142E63D-CF4A-48EA-8527-F6B2DB4718C0} 67 | {74D8F7A7-8125-400D-B744-C30562F23BE0} = {5142E63D-CF4A-48EA-8527-F6B2DB4718C0} 68 | {B776A5C5-A301-472E-9D21-A62306358B2C} = {5142E63D-CF4A-48EA-8527-F6B2DB4718C0} 69 | {027FFE79-69FE-4A69-8B71-7364F9D1D99B} = {2F34BC99-9753-46CC-A386-33B543EFF916} 70 | EndGlobalSection 71 | GlobalSection(ExtensibilityGlobals) = postSolution 72 | SolutionGuid = {94C12565-E78F-448C-95F0-D0D0EACAE996} 73 | EndGlobalSection 74 | EndGlobal 75 | -------------------------------------------------------------------------------- /src/Splunk.Logging.SLAB/TcpEventSink.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Splunk, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"): you may 5 | * not use this file except in compliance with the License. You may obtain 6 | * a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations 14 | * under the License. 15 | */ 16 | using Microsoft.Practices.EnterpriseLibrary.SemanticLogging; 17 | using Microsoft.Practices.EnterpriseLibrary.SemanticLogging.Formatters; 18 | using System; 19 | using System.Collections.Generic; 20 | using System.IO; 21 | using System.Linq; 22 | using System.Net; 23 | using System.Net.Sockets; 24 | using System.Text; 25 | using System.Threading.Tasks; 26 | 27 | namespace Splunk.Logging 28 | { 29 | /// 30 | /// SLAB event sink to write events to a TCP socket. 31 | /// 32 | public class TcpEventSink : IObserver 33 | { 34 | private TcpSocketWriter writer; 35 | private IEventTextFormatter formatter; 36 | 37 | /// 38 | /// Set up a sink. 39 | /// 40 | /// IP address of the host to write to. 41 | /// TCP port to write to. 42 | /// An object to specify the format events should be 43 | /// written in (defaults to {timestamp} EventId={...} EventName={...} Level={...} "FormattedMessage={...}"). 44 | /// An object defining the reconnect policy in the event 45 | /// of TCP errors (default: an ExponentialBackoffTcpConnectionPolicy object). 46 | /// The maximum number of events to queue in the event of 47 | /// the TCP session dropping before events start to be dropped (defaults to 10,000). 48 | /// A progress reporter that triggers when events are written from 49 | /// the queue to the TCP port (defaults to a new Progress object). It is reachable 50 | /// via the Progress property. 51 | public TcpEventSink(IPAddress host, int port, ITcpReconnectionPolicy policy, 52 | IEventTextFormatter formatter = null, int maxQueueSize = 10000) 53 | { 54 | this.writer = new TcpSocketWriter(host, port, policy, maxQueueSize); 55 | this.formatter = formatter; 56 | } 57 | 58 | /// 59 | /// Add a handler to be invoked when exceptions are thrown during the operation of 60 | /// TCP logging, in particular when SocketErrors cause reconnect attempts or when 61 | /// the reconnect policy gives up entirely. 62 | /// 63 | /// A function to handle the exception. 64 | public void AddLoggingFailureHandler(Action handler) 65 | { 66 | this.writer.LoggingFailureHandler += handler; 67 | } 68 | 69 | /// 70 | /// Set up a sink. 71 | /// 72 | /// Hostname of the host to write to. 73 | /// TCP port to write to. 74 | /// An object to specify the format events should be 75 | /// written in (default to {timestamp} EventId={...} EventName={...} Level={...} "FormattedMessage={...}"). 76 | /// An object defining the reconnect policy in the event 77 | /// of TCP errors (default: an ExponentialBackoffTcpConnectionPolicy object). 78 | /// The maximum number of events to queue in the event of 79 | /// the TCP session dropping before events start to be dropped (defaults to 10,000). 80 | /// A progress reporter that triggers when events are written from 81 | /// the queue to the TCP port (defaults to a new Progress object). It is reachable 82 | /// via the Progress property. 83 | public TcpEventSink(string host, int port, ITcpReconnectionPolicy policy, 84 | IEventTextFormatter formatter = null, int maxQueueSize = 10000) : 85 | this(host.HostnameToIPAddress(), port, policy, formatter, maxQueueSize) { } 86 | 87 | public void OnCompleted() 88 | { 89 | this.writer.Dispose(); 90 | } 91 | 92 | public void OnError(Exception error) 93 | { 94 | this.writer.Dispose(); 95 | } 96 | 97 | public void OnNext(EventEntry value) 98 | { 99 | var sw = new StringWriter(); 100 | formatter.WriteEvent(value, sw); 101 | this.writer.Enqueue(sw.ToString()); 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/Splunk.Logging.Common/HttpEventCollectorResendMiddleware.cs: -------------------------------------------------------------------------------- 1 | /** 2 | * @copyright 3 | * 4 | * Copyright 2013-2015 Splunk, Inc. 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"): you may 7 | * not use this file except in compliance with the License. You may obtain 8 | * a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 14 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 15 | * License for the specific language governing permissions and limitations 16 | * under the License. 17 | */ 18 | 19 | using System; 20 | using System.Collections.Generic; 21 | using System.Net; 22 | using System.Net.Http; 23 | using System.Threading.Tasks; 24 | 25 | namespace Splunk.Logging 26 | { 27 | /// 28 | /// HTTP event collector middleware plug in that implements a simple resend policy. 29 | /// When HTTP post reply isn't an application error we try to resend the data. 30 | /// Usage: 31 | /// 32 | /// trace.listeners.Add(new HttpEventCollectorTraceListener( 33 | /// uri: new Uri("https://localhost:8088"), 34 | /// token: "E6099437-3E1F-4793-90AB-0E5D9438A918", 35 | /// new HttpEventCollectorResendMiddleware(10).Plugin // retry 10 times 36 | /// ); 37 | /// 38 | /// 39 | public class HttpEventCollectorResendMiddleware 40 | { 41 | // List of HTTP event collector server application error statuses. These statuses 42 | // indicate non-transient problems that cannot be fixed by resending the 43 | // data. 44 | private static readonly HttpStatusCode[] HttpEventCollectorApplicationErrors = 45 | { 46 | HttpStatusCode.Forbidden, 47 | HttpStatusCode.MethodNotAllowed, 48 | HttpStatusCode.BadRequest 49 | }; 50 | 51 | private const int RetryDelayCeiling = 60 * 1000; // 1 minute 52 | private int retriesOnError = 0; 53 | 54 | /// 55 | /// Max number of retries before reporting an error. 56 | /// 57 | public HttpEventCollectorResendMiddleware(int retriesOnError) 58 | { 59 | this.retriesOnError = retriesOnError; 60 | } 61 | 62 | /// 63 | /// Callback that should be used as middleware in HttpEventCollectorSender 64 | /// 65 | /// 66 | /// 67 | /// 68 | public async Task Plugin( 69 | string token, 70 | List events, 71 | HttpEventCollectorSender.HttpEventCollectorHandler next) 72 | { 73 | HttpResponseMessage response = null; 74 | HttpStatusCode statusCode = HttpStatusCode.OK; 75 | Exception webException = null; 76 | string serverReply = null; 77 | int retryDelay = 1000; // start with 1 second 78 | // retry sending data until success 79 | for (int retriesCount = 0; retriesCount <= retriesOnError; retriesCount++) 80 | { 81 | try 82 | { 83 | response = await next(token, events); 84 | statusCode = response.StatusCode; 85 | if (statusCode == HttpStatusCode.OK) 86 | { 87 | // the data has been sent successfully 88 | webException = null; 89 | break; 90 | } 91 | else if (Array.IndexOf(HttpEventCollectorApplicationErrors, statusCode) >= 0) 92 | { 93 | // HTTP event collector application error detected - resend wouldn't help 94 | // in this case. Record server reply and break. 95 | if (response.Content != null) 96 | { 97 | serverReply = await response.Content.ReadAsStringAsync(); 98 | } 99 | break; 100 | } 101 | else 102 | { 103 | // retry 104 | } 105 | } 106 | catch (Exception e) 107 | { 108 | // connectivity problem - record exception and retry 109 | webException = e; 110 | } 111 | // wait before next retry 112 | await Task.Delay(retryDelay); 113 | // increase delay exponentially 114 | retryDelay = Math.Min(RetryDelayCeiling, retryDelay * 2); 115 | } 116 | if (statusCode != HttpStatusCode.OK || webException != null) 117 | { 118 | throw new HttpEventCollectorException( 119 | code: statusCode, 120 | webException: webException, 121 | reply: serverReply, 122 | response: response 123 | ); 124 | } 125 | return response; 126 | } 127 | } 128 | } -------------------------------------------------------------------------------- /src/Splunk.Logging.Common/HttpEventCollectorEventInfo.cs: -------------------------------------------------------------------------------- 1 | /** 2 | * @copyright 3 | * 4 | * Copyright 2013-2015 Splunk, Inc. 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"): you may 7 | * not use this file except in compliance with the License. You may obtain 8 | * a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 14 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 15 | * License for the specific language governing permissions and limitations 16 | * under the License. 17 | */ 18 | 19 | using Newtonsoft.Json; 20 | using System; 21 | using System.Threading; 22 | 23 | namespace Splunk.Logging 24 | { 25 | /// 26 | /// HttpEventCollectorEventInfo is a wrapper container for .NET events information. 27 | /// An instance of HttpEventCollectorEventInfo can be easily serialized into json 28 | /// format using JsonConvert.SerializeObject. 29 | /// 30 | public class HttpEventCollectorEventInfo 31 | { 32 | #region metadata 33 | 34 | /// 35 | /// Common metadata tags that can be specified by HTTP event collector logger. 36 | /// 37 | public const string MetadataTimeTag = "time"; 38 | public const string MetadataIndexTag = "index"; 39 | public const string MetadataSourceTag = "source"; 40 | public const string MetadataSourceTypeTag = "sourcetype"; 41 | public const string MetadataHostTag = "host"; 42 | 43 | /// 44 | /// Metadata container 45 | /// 46 | public class Metadata 47 | { 48 | public string Index { get; private set; } 49 | public string Source { get; private set; } 50 | public string SourceType { get; private set; } 51 | public string Host { get; private set; } 52 | 53 | public Metadata( 54 | string index = null, 55 | string source = null, 56 | string sourceType = null, 57 | string host = null) 58 | { 59 | this.Index = index; 60 | this.Source = source; 61 | this.SourceType = sourceType; 62 | this.Host = host; 63 | } 64 | } 65 | 66 | private Metadata metadata; 67 | 68 | #endregion 69 | 70 | /// 71 | /// A wrapper for logger event information. 72 | /// 73 | public struct LoggerEvent 74 | { 75 | /// 76 | /// Logging event id. 77 | /// 78 | [JsonProperty(PropertyName = "id", DefaultValueHandling = DefaultValueHandling.Ignore)] 79 | public string Id { get; private set; } 80 | 81 | /// 82 | /// Logging event severity info. 83 | /// 84 | [JsonProperty(PropertyName = "severity", DefaultValueHandling = DefaultValueHandling.Ignore)] 85 | public string Severity { get; private set; } 86 | 87 | /// 88 | /// Logging event message. 89 | /// 90 | [JsonProperty(PropertyName = "message", DefaultValueHandling = DefaultValueHandling.Ignore)] 91 | public string Message { get; private set; } 92 | 93 | /// 94 | /// Auxiliary event data. 95 | /// 96 | [JsonProperty(PropertyName = "data", DefaultValueHandling = DefaultValueHandling.Ignore)] 97 | public object Data { get; private set; } 98 | 99 | /// 100 | /// LoggerEvent c-or. 101 | /// 102 | /// Event id. 103 | /// Event severity info. 104 | /// Event message. 105 | /// Event data. 106 | internal LoggerEvent(string id, string severity, string message, object data) : this() 107 | { 108 | this.Id = id; 109 | this.Severity = severity; 110 | this.Message = message; 111 | this.Data = data; 112 | } 113 | } 114 | 115 | /// 116 | /// Event timestamp in epoch format. 117 | /// 118 | [JsonProperty(PropertyName = MetadataTimeTag)] 119 | public string Timestamp { get; private set; } 120 | 121 | /// 122 | /// Event metadata index. 123 | /// 124 | [JsonProperty(PropertyName = MetadataIndexTag, DefaultValueHandling = DefaultValueHandling.Ignore)] 125 | public string Index { get { return metadata.Index; } } 126 | 127 | /// 128 | /// Event metadata source. 129 | /// 130 | [JsonProperty(PropertyName = MetadataSourceTag, DefaultValueHandling = DefaultValueHandling.Ignore)] 131 | public string Source { get { return metadata.Source; } } 132 | 133 | /// 134 | /// Event metadata sourcetype. 135 | /// 136 | [JsonProperty(PropertyName = MetadataSourceTypeTag, DefaultValueHandling = DefaultValueHandling.Ignore)] 137 | public string SourceType { get { return metadata.SourceType; } } 138 | 139 | /// 140 | /// Event metadata host. 141 | /// 142 | [JsonProperty(PropertyName = MetadataHostTag, DefaultValueHandling = DefaultValueHandling.Ignore)] 143 | public string Host { get { return metadata.Host; } } 144 | 145 | /// 146 | /// Logger event info. 147 | /// 148 | [JsonProperty(PropertyName = "event", DefaultValueHandling = DefaultValueHandling.Ignore)] 149 | public dynamic Event { get; set; } 150 | 151 | /// 152 | /// HttpEventCollectorEventInfo c-or. 153 | /// 154 | /// Event id. 155 | /// Event severity info. 156 | /// Event message text. 157 | /// Event auxiliary data. 158 | /// Logger metadata. 159 | public HttpEventCollectorEventInfo( 160 | string id, string severity, string message, object data, Metadata metadata) 161 | : this(DateTime.UtcNow, id, severity, message, data, metadata) 162 | { 163 | } 164 | 165 | /// 166 | /// HttpEventCollectorEventInfo c-or. 167 | /// 168 | /// Time value to use. 169 | /// Event id. 170 | /// Event severity info. 171 | /// Event message text. 172 | /// Event auxiliary data. 173 | /// Logger metadata. 174 | public HttpEventCollectorEventInfo( 175 | DateTime datetime, string id, string severity, string message, object data, Metadata metadata) 176 | { 177 | double epochTime = (datetime - new DateTime(1970, 1, 1)).TotalSeconds; 178 | // truncate to 3 digits after floating point 179 | Timestamp = epochTime.ToString("#.000", Thread.CurrentThread.CurrentCulture); 180 | this.metadata = metadata ?? new Metadata(); 181 | Event = new LoggerEvent(id, severity, message, data); 182 | } 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /test/unit-tests/TestTcpSocketWriter.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Splunk, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"): you may 5 | * not use this file except in compliance with the License. You may obtain 6 | * a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations 14 | * under the License. 15 | */ 16 | using System; 17 | using System.Collections.Generic; 18 | using System.Diagnostics; 19 | using System.IO; 20 | using System.Linq; 21 | using System.Net; 22 | using System.Net.Sockets; 23 | using System.Threading; 24 | using System.Threading.Tasks; 25 | using Xunit; 26 | 27 | namespace Splunk.Logging 28 | { 29 | public class TestTcpSocketWriter 30 | { 31 | class TryOnceTcpConnectionPolicy : ITcpReconnectionPolicy 32 | { 33 | public Socket Connect(Func connect, 34 | System.Net.IPAddress host, int port, 35 | System.Threading.CancellationToken cancellationToken) 36 | { 37 | try 38 | { 39 | // In a proper implementation, we would check for cancellation here 40 | // and return null if cancellationToken is cancelled. However, we want 41 | // to use Dispose on a TcpSocketWriter to block until all activity 42 | // has ended and still get a TcpReconnectFailureException. 43 | return connect(host, port); 44 | } 45 | catch (SocketException e) 46 | { 47 | throw new TcpReconnectFailureException("Reconnect failed: " + e.Message); 48 | } 49 | } 50 | } 51 | 52 | [Fact] 53 | public void TestReconnectFailure() 54 | { 55 | var listener = new TcpListener(IPAddress.Loopback, 0); 56 | listener.Start(); 57 | int port = ((IPEndPoint)listener.Server.LocalEndPoint).Port; 58 | 59 | var writer = new TcpSocketWriter(IPAddress.Loopback, port, new TryOnceTcpConnectionPolicy(), 2); 60 | var receiver = listener.AcceptTcpClient(); 61 | receiver.Close(); 62 | 63 | var errors = new List(); 64 | var errorThrown = false; 65 | writer.LoggingFailureHandler += (ex) => { 66 | errorThrown = true; 67 | errors.Add(ex); 68 | }; 69 | listener.Stop(); 70 | 71 | while (!errorThrown) 72 | { 73 | writer.Enqueue("boris\r\n"); 74 | } 75 | writer.Dispose(); 76 | 77 | Assert.Equal(3, errors.Count()); 78 | Assert.True(errors[0] is SocketException); 79 | Assert.True(errors[1] is SocketException); 80 | Assert.True(errors[2] is TcpReconnectFailureException); 81 | } 82 | 83 | [Fact] 84 | public async Task TestEventsQueuedWhileWaitingForInitialConnection() 85 | { 86 | var listener = new TcpListener(IPAddress.Loopback, 0); 87 | listener.Start(); 88 | int port = ((IPEndPoint)listener.Server.LocalEndPoint).Port; 89 | 90 | var writer = new TcpSocketWriter(IPAddress.Loopback, port, new ExponentialBackoffTcpReconnectionPolicy(), 100); 91 | 92 | writer.Enqueue("Event 1\r\n"); 93 | writer.Enqueue("Event 2\r\n"); 94 | 95 | listener.Start(); 96 | var listenerClient = listener.AcceptTcpClient(); 97 | var receiverReader = new StreamReader(listenerClient.GetStream()); 98 | 99 | Assert.Equal("Event 1", await receiverReader.ReadLineAsync()); 100 | Assert.Equal("Event 2", await receiverReader.ReadLineAsync()); 101 | 102 | listener.Stop(); 103 | listenerClient.Close(); 104 | writer.Dispose(); 105 | } 106 | 107 | public class TriggeredTcpReconnectionPolicy : ITcpReconnectionPolicy 108 | { 109 | private AutoResetEvent trigger = new AutoResetEvent(false); 110 | public Socket Connect(Func connect, IPAddress host, int port, CancellationToken cancellationToken) 111 | { 112 | while (true) 113 | { 114 | try 115 | { 116 | trigger.WaitOne(); 117 | return connect(host, port); 118 | } 119 | catch (SocketException) { 120 | Thread.Sleep(150); 121 | } 122 | } 123 | } 124 | 125 | public void Trigger() 126 | { 127 | trigger.Set(); 128 | } 129 | } 130 | 131 | [Fact] 132 | public async Task TestEventsQueuedCanBeDropped() 133 | { 134 | var listener = new TcpListener(IPAddress.Loopback, 0); 135 | listener.Start(); 136 | int port = ((IPEndPoint)listener.Server.LocalEndPoint).Port; 137 | 138 | var policy = new TriggeredTcpReconnectionPolicy(); 139 | policy.Trigger(); 140 | var writer = new TcpSocketWriter(IPAddress.Loopback, port, policy, 2); 141 | 142 | var listenerClient = await listener.AcceptTcpClientAsync(); 143 | listenerClient.Close(); 144 | 145 | var errors = new List(); 146 | var errorThrown = false; 147 | writer.LoggingFailureHandler += (ex) => 148 | { 149 | errorThrown = true; 150 | errors.Add(ex); 151 | }; 152 | 153 | while (!errorThrown) 154 | { 155 | writer.Enqueue("boris\r\n"); 156 | } 157 | for (int i = 0; i < 10; i++) 158 | { 159 | writer.Enqueue("boris\r\n"); 160 | } 161 | 162 | policy.Trigger(); 163 | listenerClient = await listener.AcceptTcpClientAsync(); 164 | 165 | writer.Dispose(); 166 | 167 | var receiverReader = new StreamReader(listenerClient.GetStream()); 168 | 169 | // Then check what was left in the queue when we disconnected 170 | Assert.Equal("boris", await receiverReader.ReadLineAsync()); 171 | Assert.Equal("boris", await receiverReader.ReadLineAsync()); 172 | Assert.Equal(0, listenerClient.Available); 173 | } 174 | 175 | [Fact] 176 | public void TestCPUUsage() 177 | { 178 | var cpuCounter = new PerformanceCounter("Process", "% Processor Time", Process.GetCurrentProcess().ProcessName); 179 | cpuCounter.NextValue(); 180 | 181 | Thread.Sleep(TimeSpan.FromSeconds(2)); 182 | 183 | // Read inital CPU usage 184 | var startProcessorPercent = cpuCounter.NextValue(); 185 | 186 | // Setup writer 187 | 188 | var listener = new TcpListener(IPAddress.Loopback, 0); 189 | listener.Start(); 190 | int port = ((IPEndPoint)listener.Server.LocalEndPoint).Port; 191 | 192 | var policy = new TriggeredTcpReconnectionPolicy(); 193 | policy.Trigger(); 194 | using (var writer = new TcpSocketWriter(IPAddress.Loopback, port, policy, 2)) 195 | { 196 | 197 | Thread.Sleep(TimeSpan.FromSeconds(2)); 198 | 199 | // Read new CPU usage value 200 | var newProcessorPercent = cpuCounter.NextValue(); 201 | 202 | // Assert difference in CPU usage 203 | Assert.InRange(newProcessorPercent - startProcessorPercent, -5, 5); 204 | } 205 | } 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /src/Splunk.Logging.Common/TcpSocketWriter.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Splunk, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"): you may 5 | * not use this file except in compliance with the License. You may obtain 6 | * a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations 14 | * under the License. 15 | */ 16 | using System; 17 | using System.Net; 18 | using System.Net.Sockets; 19 | using System.Text; 20 | using System.Threading; 21 | using System.Threading.Tasks; 22 | 23 | namespace Splunk.Logging 24 | { 25 | /// 26 | /// TcpSocketWriter encapsulates queueing strings to be written to a TCP socket 27 | /// and handling reconnections (according to a TcpConnectionPolicy object passed 28 | /// to it) when a TCP session drops. 29 | /// 30 | /// 31 | /// TcpSocketWriter maintains a fixed sized queue of strings to be sent via 32 | /// the TCP port and, while the socket is open, sends them as quickly as possible. 33 | /// 34 | /// If the TCP session drops, TcpSocketWriter will stop pulling strings off the 35 | /// queue until it can reestablish a connection. Any SocketErrors emitted during this 36 | /// process will be passed as arguments to invocations of LoggingFailureHandler. 37 | /// If the TcpConnectionPolicy.Connect method throws an exception (in particular, 38 | /// TcpReconnectFailure to indicate that the policy has reached a point where it 39 | /// will no longer try to establish a connection) then the LoggingFailureHandler 40 | /// event is invoked, and no further attempt to log anything will be made. 41 | /// 42 | public class TcpSocketWriter : IDisposable 43 | { 44 | private FixedSizeQueue eventQueue; 45 | private Thread queueListener; 46 | private ITcpReconnectionPolicy reconnectPolicy; 47 | private CancellationTokenSource tokenSource; // Must be private or Dispose will not function properly. 48 | private Func tryOpenSocket; 49 | private TaskCompletionSource disposed = new TaskCompletionSource(); 50 | 51 | private Socket socket; 52 | private IPAddress host; 53 | private int port; 54 | 55 | /// 56 | /// Event that is invoked when reconnecting after a TCP session is dropped fails. 57 | /// 58 | public event Action LoggingFailureHandler = (ex) => { }; 59 | 60 | /// 61 | /// Construct a TCP socket writer that writes to the given host and port. 62 | /// 63 | /// IPAddress of the host to open a TCP socket to. 64 | /// TCP port to use on the target host. 65 | /// A TcpConnectionPolicy object defining reconnect behavior. 66 | /// The maximum number of log entries to queue before starting to drop entries. 67 | /// An IProgress object that reports when the queue of entries to be written reaches empty or there is 68 | /// a reconnection failure. This is used for testing purposes only. 69 | public TcpSocketWriter(IPAddress host, int port, ITcpReconnectionPolicy policy, 70 | int maxQueueSize, Func connect = null) 71 | { 72 | this.host = host; 73 | this.port = port; 74 | this.reconnectPolicy = policy; 75 | this.eventQueue = new FixedSizeQueue(maxQueueSize); 76 | this.tokenSource = new CancellationTokenSource(); 77 | 78 | if (connect == null) 79 | { 80 | this.tryOpenSocket = (h, p) => 81 | { 82 | try 83 | { 84 | var socket = new Socket(SocketType.Stream, ProtocolType.Tcp); 85 | socket.Connect(host, port); 86 | return socket; 87 | } 88 | catch (SocketException e) 89 | { 90 | LoggingFailureHandler(e); 91 | throw; 92 | } 93 | }; 94 | } 95 | else 96 | { 97 | this.tryOpenSocket = (h, p) => 98 | { 99 | try 100 | { 101 | return connect(h, p); 102 | } 103 | catch (SocketException e) 104 | { 105 | LoggingFailureHandler(e); 106 | throw; 107 | } 108 | }; 109 | } 110 | 111 | var threadReady = new TaskCompletionSource(); 112 | 113 | queueListener = new Thread(() => 114 | { 115 | try 116 | { 117 | this.socket = this.reconnectPolicy.Connect(tryOpenSocket, host, port, tokenSource.Token); 118 | threadReady.SetResult(true); // Signal the calling thread that we are ready. 119 | 120 | string entry = null; 121 | while (this.socket != null) // null indicates that the thread has been cancelled and cleaned up. 122 | { 123 | if (tokenSource.Token.IsCancellationRequested) 124 | { 125 | eventQueue.CompleteAdding(); 126 | // Post-condition: no further items will be added to the queue, so there will be a finite number of items to handle. 127 | while (eventQueue.Count > 0) 128 | { 129 | entry = eventQueue.Dequeue(); 130 | try 131 | { 132 | this.socket.Send(Encoding.UTF8.GetBytes(entry)); 133 | } 134 | catch (SocketException ex) 135 | { 136 | LoggingFailureHandler(ex); 137 | } 138 | } 139 | break; 140 | } 141 | else if (entry == null) 142 | { 143 | entry = eventQueue.Dequeue(tokenSource.Token); 144 | } 145 | else if (entry != null) 146 | { 147 | try 148 | { 149 | if (this.socket.Send(Encoding.UTF8.GetBytes(entry)) != -1) 150 | { 151 | entry = null; 152 | } 153 | } 154 | catch (SocketException ex) 155 | { 156 | LoggingFailureHandler(ex); 157 | this.socket = this.reconnectPolicy.Connect(tryOpenSocket, host, port, tokenSource.Token); 158 | } 159 | } 160 | } 161 | } 162 | catch (Exception e) 163 | { 164 | LoggingFailureHandler(e); 165 | } 166 | finally 167 | { 168 | if (socket != null) 169 | { 170 | socket.Close(); 171 | socket.Dispose(); 172 | } 173 | 174 | disposed.SetResult(true); 175 | } 176 | }); 177 | queueListener.IsBackground = true; // Prevent the thread from blocking the process from exiting. 178 | queueListener.Start(); 179 | threadReady.Task.Wait(); 180 | } 181 | 182 | public void Dispose() 183 | { 184 | // The following operations are idempotent. Issue a cancellation to tell the 185 | // writer thread to stop the queue from accepting entries and write what it has 186 | // before cleaning up, then wait until that cleanup is finished. 187 | this.tokenSource.Cancel(); 188 | Task.Run(async () => await disposed.Task).Wait(); 189 | } 190 | 191 | /// 192 | /// Push a string onto the queue to be written. 193 | /// 194 | /// The string to be written to the TCP socket. 195 | public void Enqueue(string entry) 196 | { 197 | this.eventQueue.Enqueue(entry); 198 | } 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /src/Splunk.Logging.SLAB/HttpEventCollectorSink.cs: -------------------------------------------------------------------------------- 1 | /** 2 | * @copyright 3 | * 4 | * Copyright 2013-2015 Splunk, Inc. 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"): you may 7 | * not use this file except in compliance with the License. You may obtain 8 | * a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 14 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 15 | * License for the specific language governing permissions and limitations 16 | * under the License. 17 | */ 18 | 19 | using Microsoft.Practices.EnterpriseLibrary.SemanticLogging; 20 | using Microsoft.Practices.EnterpriseLibrary.SemanticLogging.Formatters; 21 | using System; 22 | using System.IO; 23 | 24 | namespace Splunk.Logging 25 | { 26 | /// 27 | /// Trace sink implementation for Splunk HTTP event collector. 28 | /// Usage example: 29 | /// 30 | /// var listener = new ObservableEventListener(); 31 | /// var sink = new HttpEventCollectorEventSink( 32 | /// uri: new Uri("https://localhost:8088"), 33 | /// token: "E6099437-3E1F-4793-90AB-0E5D9438A918", 34 | /// formatter: new AppEventFormatter() 35 | /// ); 36 | /// listener.Subscribe(sink); 37 | /// var eventSource = new AppEventSource(); 38 | /// listener.EnableEvents(eventSource, EventLevel.LogAlways, Keywords.All); 39 | /// eventSource.Message("Hello world"); 40 | /// 41 | /// AppEventFormatter and AppEventSource have to be implemented by user. See 42 | /// TestHttpEventCollector.cs for a working example. 43 | /// 44 | /// Trace sink supports events batching (off by default) that allows to 45 | /// decrease number of HTTP requests to Splunk server. The batching is 46 | /// controlled by three parameters: "batch size count", "batch size bytes" 47 | /// and "batch interval". If batch size parameters are specified then 48 | /// Send(...) multiple events are sending simultaneously batch exceeds its limits. 49 | /// Batch interval controls a timer that forcefully sends events batch 50 | /// regardless of its size. 51 | /// 52 | /// var listener = new ObservableEventListener(); 53 | /// var sink = new HttpEventCollectorEventSink( 54 | /// uri: new Uri("https://localhost:8088"), 55 | /// token: "E6099437-3E1F-4793-90AB-0E5D9438A918", 56 | /// formatter: new AppEventFormatter(), 57 | /// batchInterval: 1000, // send events at least every second 58 | /// batchSizeBytes: 1024, // 1KB 59 | /// batchSizeCount: 10) // events batch contains at most 10 individual events 60 | /// ); 61 | /// listener.Subscribe(sink); 62 | /// var eventSource = new AppEventSource(); 63 | /// listener.EnableEvents(eventSource, EventLevel.LogAlways, Keywords.All); 64 | /// eventSource.Message("Hello batching 1"); 65 | /// eventSource.Message("Hello batching 2"); 66 | /// eventSource.Message("Hello batching 3"); 67 | /// 68 | /// 69 | /// To improve system performance tracing events are sent asynchronously and 70 | /// events with the same timestamp (that has 1 millisecond resolution) may 71 | /// be indexed out of order by Splunk. sendMode parameter triggers 72 | /// "sequential mode" that guarantees preserving events order. In 73 | /// "sequential mode" performance of sending events to the server is lower. 74 | /// 75 | /// There is an ability to plug middleware components that act before and 76 | /// after posting data. 77 | /// For example: 78 | /// 79 | /// var sink = new HttpEventCollectorEventSink( 80 | /// uri: new Uri("https://localhost:8088"), 81 | /// token: "E6099437-3E1F-4793-90AB-0E5D9438A918", 82 | /// formatter: new AppEventFormatter(), 83 | /// middleware: (request, next) => { 84 | /// // preprocess request 85 | /// var response = next(request); // post data 86 | /// // process response 87 | /// return response; 88 | /// } 89 | /// ... 90 | /// ) 91 | /// 92 | /// Middleware components can apply additional logic before and after posting 93 | /// the data to Splunk server. See HttpEventCollectorResendMiddleware. 94 | /// 95 | /// 96 | /// A user application code can register an error handler that is invoked 97 | /// when HTTP event collector isn't able to send data. 98 | /// 99 | /// var sink = new HttpEventCollectorEventSink( 100 | /// uri: new Uri("https://localhost:8088"), 101 | /// token: "E6099437-3E1F-4793-90AB-0E5D9438A918", 102 | /// formatter: new AppEventFormatter(), 103 | /// ); 104 | /// sink.AddLoggingFailureHandler((sender, HttpEventCollectorException e) => 105 | /// { 106 | /// // do something 107 | /// }); 108 | /// 109 | /// HttpEventCollectorException contains information about the error and the list of 110 | /// events caused the problem. 111 | /// 112 | public class HttpEventCollectorSink : IObserver 113 | { 114 | private HttpEventCollectorSender sender; 115 | private IEventTextFormatter formatter; 116 | 117 | /// 118 | /// HttpEventCollectorEventSink c-or with middleware parameter. 119 | /// 120 | /// Splunk server uri, for example https://localhost:8088. 121 | /// HTTP event collector authorization token. 122 | /// Event formatter converting EventEntry instance into a string. 123 | /// Logger metadata. 124 | /// Send mode of the events. 125 | /// Batch interval in milliseconds. 126 | /// Batch max size. 127 | /// MNax number of individual events in batch. 128 | /// 129 | /// HTTP client middleware. This allows to plug an HttpClient handler that 130 | /// intercepts logging HTTP traffic. 131 | /// 132 | public HttpEventCollectorSink( 133 | Uri uri, string token, 134 | IEventTextFormatter formatter, 135 | HttpEventCollectorEventInfo.Metadata metadata = null, 136 | HttpEventCollectorSender.SendMode sendMode = HttpEventCollectorSender.SendMode.Sequential, 137 | int batchInterval = HttpEventCollectorSender.DefaultBatchInterval, 138 | int batchSizeBytes = HttpEventCollectorSender.DefaultBatchSize, 139 | int batchSizeCount = HttpEventCollectorSender.DefaultBatchCount, 140 | HttpEventCollectorSender.HttpEventCollectorMiddleware middleware = null) 141 | { 142 | this.formatter = formatter; 143 | sender = new HttpEventCollectorSender( 144 | uri, token, metadata, 145 | sendMode, 146 | batchInterval, batchSizeBytes, batchSizeCount, 147 | middleware); 148 | } 149 | 150 | /// 151 | /// HttpEventCollectorEventSink c-or. Instantiates HttpEventCollectorEventSink 152 | /// when retriesOnError parameter is specified. 153 | /// 154 | /// Splunk server uri, for example https://localhost:8088. 155 | /// HTTP event collector authorization token. 156 | /// Event formatter converting EventEntry instance into a string. 157 | /// Number of retries when network problem is detected 158 | /// Logger metadata. 159 | /// Send mode of the events. 160 | /// Batch interval in milliseconds. 161 | /// Batch max size. 162 | /// MNax number of individual events in batch. 163 | public HttpEventCollectorSink( 164 | Uri uri, string token, 165 | IEventTextFormatter formatter, 166 | int retriesOnError, 167 | HttpEventCollectorEventInfo.Metadata metadata = null, 168 | HttpEventCollectorSender.SendMode sendMode = HttpEventCollectorSender.SendMode.Sequential, 169 | int batchInterval = HttpEventCollectorSender.DefaultBatchInterval, 170 | int batchSizeBytes = HttpEventCollectorSender.DefaultBatchSize, 171 | int batchSizeCount = HttpEventCollectorSender.DefaultBatchCount) 172 | : this(uri, token, formatter, metadata, 173 | sendMode, 174 | batchInterval, batchSizeBytes, batchSizeCount, 175 | (new HttpEventCollectorResendMiddleware(retriesOnError)).Plugin) 176 | { 177 | } 178 | 179 | /// 180 | /// Add a handler to be invoked when some problem is detected during the 181 | /// operation of HTTP event collector and it cannot be fixed by resending the data. 182 | /// 183 | /// A function to handle the exception. 184 | public void AddLoggingFailureHandler(Action handler) 185 | { 186 | sender.OnError += handler; 187 | } 188 | 189 | #region IObserver 190 | 191 | public void OnCompleted() 192 | { 193 | sender.FlushSync(); 194 | } 195 | 196 | public void OnError(Exception error) 197 | { 198 | sender.Dispose(); 199 | } 200 | 201 | /// 202 | /// All events are going through OnNext callback. 203 | /// 204 | /// An event. 205 | public void OnNext(EventEntry value) 206 | { 207 | using (var sw = new StringWriter()) 208 | { 209 | formatter.WriteEvent(value, sw); 210 | sender.Send( 211 | id: value.EventId.ToString(), 212 | severity: value.Schema.Level.ToString(), 213 | message: sw.ToString() 214 | ); 215 | } 216 | } 217 | 218 | #endregion 219 | } 220 | } 221 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /src/Splunk.Logging.TraceListener/HttpEventCollectorTraceListener.cs: -------------------------------------------------------------------------------- 1 | /** 2 | * @copyright 3 | * 4 | * Copyright 2013-2015 Splunk, Inc. 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"): you may 7 | * not use this file except in compliance with the License. You may obtain 8 | * a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 14 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 15 | * License for the specific language governing permissions and limitations 16 | * under the License. 17 | */ 18 | 19 | using System; 20 | using System.Collections.Generic; 21 | using System.Diagnostics; 22 | using System.Globalization; 23 | using System.Net.Http; 24 | using System.Threading.Tasks; 25 | 26 | namespace Splunk.Logging 27 | { 28 | /// 29 | /// Trace listener implementation for Splunk HTTP event collector. 30 | /// Usage example: 31 | /// 32 | /// var trace = new TraceSource("logger"); 33 | /// trace.listeners.Add(new HttpEventCollectorTraceListener( 34 | /// uri: new Uri("https://localhost:8088"), 35 | /// token: "E6099437-3E1F-4793-90AB-0E5D9438A918")); 36 | /// trace.TraceEvent(TraceEventType.Information, 1, "hello world"); 37 | /// 38 | /// 39 | /// Trace listener supports events batching (off by default) that allows to 40 | /// decrease number of HTTP requests to Splunk server. The batching is 41 | /// controlled by three parameters: "batch size count", "batch size bytes" 42 | /// and "batch interval". If batch size parameters are specified then 43 | /// Send(...) multiple events are sending simultaneously batch exceeds its limits. 44 | /// Batch interval controls a timer that forcefully sends events batch 45 | /// regardless of its size. 46 | /// 47 | /// var trace = new TraceSource("logger"); 48 | /// trace.listeners.Add(new HttpEventCollectorTraceListener( 49 | /// uri: new Uri("https://localhost:8088"), 50 | /// token: "E6099437-3E1F-4793-90AB-0E5D9438A918", 51 | /// batchInterval: 1000, // send events at least every second 52 | /// batchSizeBytes: 1024, // 1KB 53 | /// batchSizeCount: 10) // events batch contains at most 10 individual events 54 | /// ); 55 | /// trace.TraceEvent(TraceEventType.Information, 1, "hello batching"); 56 | /// 57 | /// 58 | /// To improve system performance tracing events are sent asynchronously and 59 | /// events with the same timestamp (that has 1 millisecond resolution) may 60 | /// be indexed out of order by Splunk. sendMode parameter triggers 61 | /// "sequential mode" that guarantees preserving events order. In 62 | /// "sequential mode" performance of sending events to the server is lower. 63 | /// 64 | /// There is an ability to plug middleware components that act before and 65 | /// after posting data. 66 | /// For example: 67 | /// 68 | /// new HttpEventCollectorTraceListener( 69 | /// uri: new Uri("https://localhost:8088"), 70 | /// token: "E6099437-3E1F-4793-90AB-0E5D9438A918, 71 | /// middleware: (request, next) => { 72 | /// // preprocess request 73 | /// var response = next(request); // post data 74 | /// // process response 75 | /// return response; 76 | /// } 77 | /// ... 78 | /// ) 79 | /// 80 | /// Middleware components can apply additional logic before and after posting 81 | /// the data to Splunk server. See HttpEventCollectorResendMiddleware. 82 | /// 83 | /// 84 | /// A user application code can register an error handler that is invoked 85 | /// when HTTP event collector isn't able to send data. 86 | /// 87 | /// var listener = new HttpEventCollectorTraceListener( 88 | /// uri: new Uri("https://localhost:8088"), 89 | /// token: "E6099437-3E1F-4793-90AB-0E5D9438A918") 90 | /// ); 91 | /// listener.AddLoggingFailureHandler((sender, HttpEventCollectorException e) => 92 | /// { 93 | /// // do something 94 | /// }); 95 | /// trace.listeners.Add(listener); 96 | /// 97 | /// HttpEventCollectorException contains information about the error and the list of 98 | /// events caused the problem. 99 | /// 100 | public class HttpEventCollectorTraceListener : TraceListener 101 | { 102 | private HttpEventCollectorSender sender; 103 | public HttpEventCollectorSender.HttpEventCollectorFormatter formatter; 104 | 105 | /// 106 | /// HttpEventCollectorTraceListener c-or. 107 | /// 108 | /// Splunk server uri, for example https://localhost:8088. 109 | /// HTTP event collector authorization token. 110 | /// Logger metadata. 111 | /// Send mode of the events. 112 | /// Batch interval in milliseconds. 113 | /// Batch max size. 114 | /// MNax number of individual events in batch. 115 | /// 116 | /// HTTP client middleware. This allows to plug an HttpClient handler that 117 | /// intercepts logging HTTP traffic. 118 | /// 119 | public HttpEventCollectorTraceListener( 120 | Uri uri, string token, 121 | HttpEventCollectorEventInfo.Metadata metadata = null, 122 | HttpEventCollectorSender.SendMode sendMode = HttpEventCollectorSender.SendMode.Sequential, 123 | int batchInterval = HttpEventCollectorSender.DefaultBatchInterval, 124 | int batchSizeBytes = HttpEventCollectorSender.DefaultBatchSize, 125 | int batchSizeCount = HttpEventCollectorSender.DefaultBatchCount, 126 | HttpEventCollectorSender.HttpEventCollectorMiddleware middleware = null, 127 | HttpEventCollectorSender.HttpEventCollectorFormatter formatter = null) 128 | { 129 | this.formatter = formatter; 130 | sender = new HttpEventCollectorSender( 131 | uri, token, metadata, 132 | sendMode, 133 | batchInterval, batchSizeBytes, batchSizeCount, 134 | middleware, 135 | formatter); 136 | } 137 | 138 | /// 139 | /// HttpEventCollectorTraceListener c-or. Instantiates HttpEventCollectorTraceListener 140 | /// when retriesOnError parameter is specified. 141 | /// 142 | /// Splunk server uri, for example https://localhost:8088. 143 | /// HTTP event collector authorization token. 144 | /// Number of retries when network problem is detected 145 | /// Logger metadata. 146 | /// Send mode of the events. 147 | /// Batch interval in milliseconds. 148 | /// Batch max size. 149 | /// MNax number of individual events in batch. 150 | public HttpEventCollectorTraceListener( 151 | Uri uri, string token, 152 | int retriesOnError, 153 | HttpEventCollectorEventInfo.Metadata metadata = null, 154 | HttpEventCollectorSender.SendMode sendMode = HttpEventCollectorSender.SendMode.Sequential, 155 | int batchInterval = HttpEventCollectorSender.DefaultBatchInterval, 156 | int batchSizeBytes = HttpEventCollectorSender.DefaultBatchSize, 157 | int batchSizeCount = HttpEventCollectorSender.DefaultBatchCount) 158 | : this(uri, token, metadata, 159 | sendMode, 160 | batchInterval, batchSizeBytes, batchSizeCount, 161 | (new HttpEventCollectorResendMiddleware(retriesOnError)).Plugin) 162 | { 163 | } 164 | 165 | /// 166 | /// Add a handler to be invoked when some problem is detected during the 167 | /// operation of HTTP event collector and it cannot be fixed by resending the data. 168 | /// 169 | /// A function to handle the exception. 170 | public void AddLoggingFailureHandler(Action handler) 171 | { 172 | sender.OnError += handler; 173 | } 174 | 175 | #region TraceListener output callbacks 176 | 177 | public override void Write(string message) 178 | { 179 | sender.Send(message: message); 180 | } 181 | 182 | public override void WriteLine(string message) 183 | { 184 | sender.Send(message: message); 185 | } 186 | 187 | public override void TraceData( 188 | TraceEventCache eventCache, 189 | string source, 190 | TraceEventType eventType, 191 | int id, 192 | params object[] data) 193 | { 194 | sender.Send( 195 | id: id.ToString(), 196 | severity: eventType.ToString(), 197 | data: data 198 | ); 199 | } 200 | 201 | public override void TraceEvent( 202 | TraceEventCache eventCache, 203 | string source, 204 | TraceEventType eventType, 205 | int id) 206 | { 207 | sender.Send( 208 | id: id.ToString(), 209 | severity: eventType.ToString() 210 | ); 211 | } 212 | 213 | public override void TraceEvent( 214 | TraceEventCache eventCache, 215 | string source, 216 | TraceEventType eventType, 217 | int id, 218 | string message) 219 | { 220 | sender.Send( 221 | id: id.ToString(), 222 | severity: eventType.ToString(), 223 | message: message 224 | ); 225 | } 226 | 227 | public override void TraceEvent( 228 | TraceEventCache eventCache, 229 | string source, 230 | TraceEventType eventType, 231 | int id, 232 | string format, 233 | params object[] args) 234 | { 235 | string message = args != null ? string.Format(CultureInfo.InvariantCulture, format, args) : format; 236 | sender.Send( 237 | id: id.ToString(), 238 | severity: eventType.ToString(), 239 | message: message 240 | ); 241 | } 242 | 243 | public override void TraceTransfer( 244 | TraceEventCache eventCache, 245 | string source, 246 | int id, 247 | string message, 248 | Guid relatedActivityId) 249 | { 250 | sender.Send( 251 | id: id.ToString(), 252 | message: message, 253 | data: relatedActivityId 254 | ); 255 | } 256 | 257 | #endregion 258 | 259 | /// 260 | /// Flush all events. 261 | /// 262 | public Task FlushAsync() 263 | { 264 | return sender.FlushAsync(); 265 | } 266 | 267 | public override void Close() 268 | { 269 | sender.FlushSync(); 270 | } 271 | 272 | override protected void Dispose(bool disposing) 273 | { 274 | Close(); 275 | } 276 | 277 | ~HttpEventCollectorTraceListener() 278 | { 279 | Dispose(false); 280 | } 281 | } 282 | } 283 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Splunk logging for .NET 2 | 3 | #### Version 1.7.2 4 | 5 | Splunk logging for .NET enables you to configure [HTTP Event Collector](http://dev.splunk.com/view/event-collector/SP-CAAAE6M), UDP or TCP 6 | logging of events to a Splunk Enterprise instance from within your .NET 7 | applications, via a .NET TraceListener or a Semantic Logging Application Block 8 | (SLAB) event sink. 9 | 10 | Each library consists of several extensions for existing .NET logging 11 | frameworks. Specifically, there are two libraries available, along with a third 12 | common library that is required by both main libraries: 13 | 14 | * `Splunk.Logging.TraceListener` 15 | * `Splunk.Logging.SLAB` 16 | * `Splunk.Logging.Common` 17 | 18 | ## Get started 19 | 20 | The information in this Readme provides steps to get going quickly, but for 21 | more in-depth information be sure to visit [Splunk logging for 22 | .NET](http://dev.splunk.com/view/splunk-loglib-dotnet/SP-CAAAEX4) page on 23 | [Splunk Developer Portal](http://dev.splunk.com). 24 | 25 | ### Requirements 26 | 27 | Here's what you need to use Splunk logging for .NET: 28 | 29 | * **.NET Framework 4.5 or later**: Splunk logging for .NET 30 | requires the .NET Framework version 4.5 or later. 31 | * **Splunk Enterprise** or **Splunk Cloud**: If you haven't already installed Splunk Enterprise, 32 | download it at [http://www.splunk.com/download](http://www.splunk.com/download). Otherwise, you 33 | should have at least a trial subscription to [Splunk Cloud](http://www.splunkcloud.com). 34 | 35 | If you want to build the libraries and run the test suite, you will also 36 | need: 37 | 38 | * **xUnit runner**: If you use ReSharper, install its 39 | [xUnit.net Test Support](https://resharper-plugins.jetbrains.com/packages/xunitcontrib/). 40 | Otherwise, install the [xUnit.net runner for Visual Studio 2012 and 2013](http://visualstudiogallery.msdn.microsoft.com/463c5987-f82b-46c8-a97e-b1cde42b9099). 41 | * **Visual Studio**: Splunk logging for .NET supports 42 | development in [Microsoft Visual Studio 2017 or later](http://www.microsoft.com/visualstudio/downloads). 43 | 44 | ### Install 45 | 46 | You have several options for installing Splunk logging for .NET. 47 | The most common method is through NuGet. Add the package you want after 48 | searching for "splunk" in the Manage NuGet Packages window in Visual Studio. 49 | 50 | For more information, and for information about other ways to install 51 | Splunk logging for .NET, see [Install Splunk logging 52 | for .NET](http://dev.splunk.com/view/splunk-loglib-dotnet/SP-CAAAEYC) 53 | 54 | 55 | #### Solution layout 56 | 57 | The solution is organized into `src` and `test` directories. The `src` 58 | directory contains three libraries: `Splunk.Logging.TraceListener` (which 59 | contains [.NET trace listeners](http://msdn.microsoft.com/library/4y5y10s7) 60 | that log events to Splunk Enterprise over UDP or TCP), `Splunk.Logging.SLAB` 61 | (which contains [Semantic Logging Application Block (SLAB) event sinks](http://msdn.microsoft.com/library/dn440729#sec29) 62 | that log ETW events to Splunk Enterprise over UDP or TCP), and 63 | `Splunk.Logging.Common` (a common library that contains resources required by 64 | both logging libraries). The `test` directory contains a single project, 65 | `unit-tests`. 66 | 67 | #### Examples and unit tests 68 | 69 | Splunk logging for .NET include full unit tests which run 70 | using [xunit](https://github.com/xunit/xunit). 71 | 72 | ### Example code 73 | 74 | #### Add logging to Splunk via a TraceListener 75 | 76 | Below is a snippet showing creating a `TraceSource` and then attaching a 77 | `UdpTraceListener` (or `TcpTraceListener`) configured to talk to localhost 78 | on port 10000. Next an event is generated which is sent to Splunk. 79 | 80 | ```csharp 81 | //setup 82 | var traceSource = new TraceSource("TestLogger"); 83 | traceSource.Listeners.Remove("Default"); 84 | traceSource.Switch.Level = SourceLevels.All; 85 | traceSource.Listeners.Add(new UdpTraceListener(IPAddress.Loopback, 10000)); 86 | // or, for TCP: 87 | // traceSource.Listeners.Add(new TcpTraceListener(IPAddress.Loopback, 10000, new ExponentialBackoffTcpReconnectionPolicy())); 88 | 89 | //log an event 90 | traceSource.TraceEvent(TraceEventType.Information, 1, "Test event"); 91 | 92 | ``` 93 | 94 | 95 | #### Add logging to Splunk via a SLAB event sink 96 | Below is a snippet showing how to create an `ObservableEventListener` and then 97 | subscribe to events with a `UdpEventSink` (or `TcpEventSink`) configured 98 | to talk to localhost on port 10000. Next a `SimpleEventSource` is 99 | instantiated and a test event is generated. 100 | 101 | ```csharp 102 | //setup 103 | var listener = new ObservableEventListener(); 104 | listener.Subscribe(new UdpEventSink(IPAddress.Loopback, 10000)); 105 | // or, for TCP: 106 | // listener.Subscribe(new TcpEventSink(IPAddress.Loopback, 10000, new ExponentialBackoffReconnectionPolicy())); 107 | 108 | var eventSource = new SimpleEventSource(); 109 | listener.EnableEvents(eventSource, EventLevel.LogAlways, Keywords.All); 110 | 111 | //log an event 112 | eventSource.Message("Test event"); 113 | 114 | [EventSource(Name = "TestEventSource")] 115 | public class SimpleEventSource : EventSource 116 | { 117 | public class Keywords { } 118 | public class Tasks { } 119 | 120 | [Event(1, Message = "{0}", Level = EventLevel.Error)] 121 | internal void Message(string message) 122 | { 123 | this.WriteEvent(1, message); 124 | } 125 | } 126 | ``` 127 | 128 | In both the example above, the TCP listeners took an extra argument, which specifies 129 | how they should handle dropped TCP sessions. You can specify a custom reconnection 130 | policy by defining an implementation of `Splunk.Logging.ITcpReconnectionPolicy` and passing it 131 | to the constructors of the `TcpTraceListener` or `TcpEventSink` classes. If you have 132 | no particular policy in mind, use the ExponentialBackoffReconnectionPolicy provided by 133 | the library, which retries after increasingly long intervals, starting from a delay of 134 | one second and going to a plateau of ten minutes. 135 | 136 | `TcpConnectionPolicy` has a single method, Connect, which tries to establish a 137 | connection or throws a `TcpReconnectFailure` if it cannot do so acceptably. Here is 138 | annotated source code of the default, exponential backoff policy: 139 | 140 | ```csharp 141 | public class ExponentialBackoffTcpReconnectionPolicy : ITcpReconnectionPolicy 142 | { 143 | private int ceiling = 10 * 60; // 10 minutes in seconds 144 | 145 | // The arguments are: 146 | // 147 | // connect - a function that attempts a TCP connection given a host, port number. 148 | // host - the host to connect to 149 | // port - the port to connect on 150 | // cancellationToken - used by TcpTraceListener and TcpEventSink to cancel this method 151 | // when they are disposed. 152 | public Socket Connect(Func connect, IPAddress host, int port, CancellationToken cancellationToken) 153 | { 154 | int delay = 1; // in seconds 155 | while (!cancellationToken.IsCancellationRequested) 156 | { 157 | try 158 | { 159 | return connect(host, port); 160 | } 161 | catch (SocketException) { } 162 | 163 | // If this is cancelled via the cancellationToken instead of 164 | // completing its delay, the next while-loop test will fail, 165 | // the loop will terminate, and the method will return null 166 | // with no additional connection attempts. 167 | Task.Delay(delay * 1000, cancellationToken).Wait(); 168 | // The nth delay is min(10 minutes, 2^n - 1 seconds). 169 | delay = Math.Min((delay + 1) * 2 - 1, ceiling); 170 | } 171 | 172 | // cancellationToken has been cancelled. 173 | return null; 174 | } 175 | } 176 | ``` 177 | 178 | Another, simpler policy, would be trying to reconnect once, and then failing: 179 | 180 | ```csharp 181 | class TryOnceTcpConnectionPolicy : ITcpReconnectionPolicy 182 | { 183 | public Socket Connect(Func connect, 184 | System.Net.IPAddress host, int port, 185 | System.Threading.CancellationToken cancellationToken) 186 | { 187 | try 188 | { 189 | if (cancellationToken.IsCancellationRequested) 190 | return null; 191 | return connect(host, port); 192 | } 193 | catch (SocketException e) 194 | { 195 | throw new TcpReconnectFailureException("Reconnect failed: " + e.Message); 196 | } 197 | } 198 | } 199 | ``` 200 | 201 | ### Handling errors from the TCP logging system 202 | 203 | It can be difficult to diagnose connection problems in TCP logging without seeing 204 | the exceptions that are actually thrown. The exceptions thrown during connection 205 | attempts and by the reconnection policy are available by adding a handler to 206 | `TcpEventSink` or `TcpTraceListener`. 207 | 208 | Both `TcpEventSink` and `TcpTraceListener` have a method that takes an action 209 | to be executed on each exception thrown in the logging system: 210 | 211 | ```csharp 212 | public void AddLoggingFailureHandler(Action handler) 213 | ``` 214 | 215 | For example, to write them to a local console, you would write: 216 | 217 | ```csharp 218 | TcpTraceListener listener = ...; 219 | listener.AddLoggingFailureHandler((ex) => { 220 | Console.WriteLine("{0}", ex); 221 | }); 222 | ``` 223 | 224 | ### Sending events to HTTP Event Collector 225 | 226 | This feature requires Splunk 6.3.0 and later. 227 | 228 | After enabling [HTTP Event Collector](http://dev.splunk.com/view/event-collector/SP-CAAAE6M) 229 | and creating an application token sending events is very simple: 230 | 231 | ```csharp 232 | // TraceListener 233 | var trace = new TraceSource("demo-logger"); 234 | trace.Listeners.Add(new HttpEventCollectorTraceListener( 235 | uri: new Uri("https://splunk-server:8088"), 236 | token: "")); 237 | trace.TraceEvent(TraceEventType.Information, 0, "hello world"); 238 | 239 | // SLAB 240 | var listener = new ObservableEventListener(); 241 | var sink = new HttpEventCollectorEventSink( 242 | uri: new Uri("https://splunk-server:8088"), 243 | token: "token-guid", 244 | formatter: new AppEventFormatter()); 245 | listener.Subscribe(sink); 246 | var eventSource = new AppEventSource(); 247 | listener.EnableEvents(eventSource, EventLevel.LogAlways, Keywords.All); 248 | eventSource.Message("hello world"); 249 | ``` 250 | 251 | #### Error Handling 252 | 253 | A user application code can register an error handler that is invoked when 254 | HTTP Event Collector isn't able to send data. 255 | 256 | ```csharp 257 | listener.AddLoggingFailureHandler((sender, HttpEventCollectorException e) => 258 | { 259 | // handle the error 260 | }); 261 | ``` 262 | 263 | ### Changelog 264 | 265 | The `CHANGELOG.md` file in the root of the repository contains a description 266 | of changes for each version of Splunk logging for .NET. You can also 267 | find it online at 268 | 269 | https://github.com/splunk/splunk-library-dotnetlogging/blob/master/CHANGELOG.md 270 | 271 | ### Branches 272 | 273 | The `master` branch always represents a stable and released version of the 274 | Splunk logging for .NET. You can read more about our branching model 275 | on our Wiki at 276 | 277 | https://github.com/splunk/splunk-sdk-python/wiki/Branching-Model 278 | 279 | ## Documentation and resources 280 | 281 | If you need to know more: 282 | 283 | * For all things developer with Splunk, your main resource is the [Splunk Developer Portal](http://dev.splunk.com). 284 | * For more about the Splunk REST API, see the [REST API Reference](http://docs.splunk.com/Documentation/Splunk/latest/RESTAPI). 285 | * For more about about Splunk in general, see [Splunk>Docs](http://docs.splunk.com/Documentation/Splunk). 286 | 287 | ## Community 288 | 289 | Stay connected with other developers building on Splunk. 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 302 | 303 | 304 | 305 | 308 | 309 | 310 | 311 | 313 | 314 | 315 | 316 | 318 | 319 | 320 |
Emaildevinfo@splunk.com
Issues 300 | 301 | https://github.com/splunk/splunk-library-dotnetlogging
Answers 306 | 307 | http://splunk-base.splunk.com/tags/csharp/
Blog 312 | http://blogs.splunk.com/dev/
Twitter 317 | @splunkdev
321 | 322 | 323 | ### Contributions 324 | 325 | If you want to make a code contribution, go to the 326 | [Open Source](http://dev.splunk.com/view/opensource/SP-CAAAEDM) 327 | page for more information. 328 | 329 | ### Support 330 | 331 | The Splunk logging library for .NET is community-supported. 332 | 333 | 1. You can find help through our community on [Splunk Answers](https://community.splunk.com/t5/tag/logging-library-dotnet/tg-p) (use the `logging-library-dotnet` tag to identify your questions). 334 | 2. File issues on [GitHub](https://github.com/splunk/splunk-library-dotnetlogging/issues). 335 | 336 | ## License 337 | 338 | Splunk logging for .NET is licensed under the Apache License 2.0. Details can be found in the LICENSE file. 339 | 340 | [contact]: https://www.splunk.com/en_us/support-and-services.html 341 | -------------------------------------------------------------------------------- /src/Splunk.Logging.Common/HttpEventCollectorSender.cs: -------------------------------------------------------------------------------- 1 | /** 2 | * @copyright 3 | * 4 | * Copyright 2013-2015 Splunk, Inc. 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"): you may 7 | * not use this file except in compliance with the License. You may obtain 8 | * a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 14 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 15 | * License for the specific language governing permissions and limitations 16 | * under the License. 17 | */ 18 | 19 | using Newtonsoft.Json; 20 | using Newtonsoft.Json.Serialization; 21 | using System; 22 | using System.Collections.Generic; 23 | using System.Net; 24 | using System.Net.Http; 25 | using System.Net.Http.Headers; 26 | using System.Text; 27 | using System.Threading; 28 | using System.Threading.Tasks; 29 | 30 | namespace Splunk.Logging 31 | { 32 | /// 33 | /// HTTP event collector client side implementation that collects, serializes and send 34 | /// events to Splunk HTTP event collector endpoint. This class shouldn't be used directly 35 | /// by user applications. 36 | /// 37 | /// 38 | /// * HttpEventCollectorSender is thread safe and Send(...) method may be called from 39 | /// different threads. 40 | /// * Events are sending asynchronously and Send(...) method doesn't 41 | /// block the caller code. 42 | /// * HttpEventCollectorSender has an ability to plug middleware components that act 43 | /// before posting data. 44 | /// For example: 45 | /// 46 | /// new HttpEventCollectorSender(uri: ..., token: ..., 47 | /// middleware: (request, next) => { 48 | /// // preprocess request 49 | /// var response = next(request); // post data 50 | /// // process response 51 | /// return response; 52 | /// } 53 | /// ... 54 | /// ) 55 | /// 56 | /// Middleware components can apply additional logic before and after posting 57 | /// the data to Splunk server. See HttpEventCollectorResendMiddleware. 58 | /// 59 | public class HttpEventCollectorSender : IDisposable 60 | { 61 | /// 62 | /// Post request delegate. 63 | /// 64 | /// HTTP request. 65 | /// Server HTTP response. 66 | public delegate Task HttpEventCollectorHandler( 67 | string token, List events); 68 | 69 | /// 70 | /// HTTP event collector middleware plugin. 71 | /// 72 | /// HTTP request. 73 | /// A handler that posts data to the server. 74 | /// Server HTTP response. 75 | public delegate Task HttpEventCollectorMiddleware( 76 | string token, List events, HttpEventCollectorHandler next); 77 | 78 | /// 79 | /// Override the default event format. 80 | /// 81 | /// A dynamic type to be serialized. 82 | public delegate dynamic HttpEventCollectorFormatter(HttpEventCollectorEventInfo eventInfo); 83 | 84 | /// 85 | /// Recommended default values for events batching 86 | /// 87 | public const int DefaultBatchInterval = 10 * 1000; // 10 seconds 88 | public const int DefaultBatchSize = 10 * 1024; // 10KB 89 | public const int DefaultBatchCount = 10; 90 | 91 | /// 92 | /// Sender operation mode. Parallel means that all HTTP requests are 93 | /// asynchronous and may be indexed out of order. Sequential mode guarantees 94 | /// sequential order of the indexed events. 95 | /// 96 | public enum SendMode 97 | { 98 | Parallel, 99 | Sequential 100 | }; 101 | 102 | private const string HttpContentTypeMedia = "application/json"; 103 | private const string HttpEventCollectorPath = "/services/collector/event/1.0"; 104 | private const string AuthorizationHeaderScheme = "Splunk"; 105 | private Uri httpEventCollectorEndpointUri; // HTTP event collector endpoint full uri 106 | private HttpEventCollectorEventInfo.Metadata metadata; // logger metadata 107 | private string token; // authorization token 108 | private JsonSerializer serializer; 109 | 110 | // events batching properties and collection 111 | private int batchInterval = 0; 112 | private int batchSizeBytes = 0; 113 | private int batchSizeCount = 0; 114 | private SendMode sendMode = SendMode.Parallel; 115 | private Task activePostTask = null; 116 | private object eventsBatchLock = new object(); 117 | private List eventsBatch = new List(); 118 | private StringBuilder serializedEventsBatch = new StringBuilder(); 119 | private Timer timer; 120 | 121 | private HttpClient httpClient = null; 122 | private HttpEventCollectorMiddleware middleware = null; 123 | private HttpEventCollectorFormatter formatter = null; 124 | // counter for bookkeeping the async tasks 125 | private long activeAsyncTasksCount = 0; 126 | 127 | /// 128 | /// On error callbacks. 129 | /// 130 | public event Action OnError = (e) => { }; 131 | 132 | /// Splunk server uri, for example https://localhost:8088. 133 | /// HTTP event collector authorization token. 134 | /// Logger metadata. 135 | /// Send mode of the events. 136 | /// Batch interval in milliseconds. 137 | /// Batch max size. 138 | /// Max number of individual events in batch. 139 | /// 140 | /// HTTP client middleware. This allows to plug an HttpClient handler that 141 | /// intercepts logging HTTP traffic. 142 | /// 143 | /// 144 | /// Zero values for the batching params mean that batching is off. 145 | /// 146 | public HttpEventCollectorSender( 147 | Uri uri, string token, HttpEventCollectorEventInfo.Metadata metadata, 148 | SendMode sendMode, 149 | int batchInterval, int batchSizeBytes, int batchSizeCount, 150 | HttpEventCollectorMiddleware middleware, 151 | HttpEventCollectorFormatter formatter = null) 152 | { 153 | this.serializer = new JsonSerializer(); 154 | serializer.NullValueHandling = NullValueHandling.Ignore; 155 | serializer.ContractResolver = new CamelCasePropertyNamesContractResolver(); 156 | 157 | this.httpEventCollectorEndpointUri = new Uri(uri, HttpEventCollectorPath); 158 | this.sendMode = sendMode; 159 | this.batchInterval = batchInterval; 160 | this.batchSizeBytes = batchSizeBytes; 161 | this.batchSizeCount = batchSizeCount; 162 | this.metadata = metadata; 163 | this.token = token; 164 | this.middleware = middleware; 165 | this.formatter = formatter; 166 | 167 | // special case - if batch interval is specified without size and count 168 | // they are set to "infinity", i.e., batch may have any size 169 | if (this.batchInterval > 0 && this.batchSizeBytes == 0 && this.batchSizeCount == 0) 170 | { 171 | this.batchSizeBytes = this.batchSizeCount = int.MaxValue; 172 | } 173 | 174 | // when size configuration setting is missing it's treated as "infinity", 175 | // i.e., any value is accepted. 176 | if (this.batchSizeCount == 0 && this.batchSizeBytes > 0) 177 | { 178 | this.batchSizeCount = int.MaxValue; 179 | } 180 | else if (this.batchSizeBytes == 0 && this.batchSizeCount > 0) 181 | { 182 | this.batchSizeBytes = int.MaxValue; 183 | } 184 | 185 | // setup the timer 186 | if (batchInterval != 0) // 0 means - no timer 187 | { 188 | timer = new Timer(OnTimer, null, batchInterval, batchInterval); 189 | } 190 | 191 | // setup HTTP client 192 | httpClient = new HttpClient(); 193 | httpClient.DefaultRequestHeaders.Authorization = 194 | new AuthenticationHeaderValue(AuthorizationHeaderScheme, token); 195 | } 196 | 197 | /// 198 | /// Send an event to Splunk HTTP endpoint. Actual event send is done 199 | /// asynchronously and this method doesn't block client application. 200 | /// 201 | /// Event id. 202 | /// Event severity info. 203 | /// Event message text. 204 | /// Additional event data. 205 | /// Metadata to use for this send. 206 | public void Send( 207 | string id = null, 208 | string severity = null, 209 | string message = null, 210 | object data = null, 211 | HttpEventCollectorEventInfo.Metadata metadataOverride = null) 212 | { 213 | HttpEventCollectorEventInfo ei = 214 | new HttpEventCollectorEventInfo(id, severity, message, data, metadataOverride ?? metadata); 215 | 216 | DoSerialization(ei); 217 | } 218 | 219 | /// 220 | /// Send an event to Splunk HTTP endpoint. Actual event send is done 221 | /// asynchronously and this method doesn't block client application. 222 | /// 223 | /// Timestamp to use. 224 | /// Event id. 225 | /// Event severity info. 226 | /// Event message text. 227 | /// Additional event data. 228 | /// Metadata to use for this send. 229 | public void Send( 230 | DateTime timestamp, 231 | string id = null, 232 | string severity = null, 233 | string message = null, 234 | object data = null, 235 | HttpEventCollectorEventInfo.Metadata metadataOverride = null) 236 | { 237 | HttpEventCollectorEventInfo ei = 238 | new HttpEventCollectorEventInfo(timestamp, id, severity, message, data, metadataOverride ?? metadata); 239 | 240 | DoSerialization(ei); 241 | } 242 | 243 | private void DoSerialization(HttpEventCollectorEventInfo ei) 244 | { 245 | 246 | string serializedEventInfo; 247 | if (formatter == null) 248 | { 249 | serializedEventInfo = SerializeEventInfo(ei); 250 | } 251 | else 252 | { 253 | var formattedEvent = formatter(ei); 254 | ei.Event = formattedEvent; 255 | serializedEventInfo = JsonConvert.SerializeObject(ei); 256 | } 257 | 258 | // we use lock serializedEventsBatch to synchronize both 259 | // serializedEventsBatch and serializedEvents 260 | lock (eventsBatchLock) 261 | { 262 | eventsBatch.Add(ei); 263 | serializedEventsBatch.Append(serializedEventInfo); 264 | if (eventsBatch.Count >= batchSizeCount || 265 | serializedEventsBatch.Length >= batchSizeBytes) 266 | { 267 | // there are enough events in the batch 268 | FlushInternal(); 269 | } 270 | } 271 | } 272 | 273 | /// 274 | /// Flush all events synchronously, i.e., flush and wait until all events 275 | /// are sent. 276 | /// 277 | public void FlushSync() 278 | { 279 | Flush(); 280 | // wait until all pending tasks are done 281 | while(Interlocked.CompareExchange(ref activeAsyncTasksCount, 0, 0) != 0) 282 | { 283 | // wait for 100ms - not CPU intensive and doesn't delay process 284 | // exit too much 285 | Thread.Sleep(100); 286 | } 287 | } 288 | 289 | /// 290 | /// Flush all event. 291 | /// 292 | public Task FlushAsync() 293 | { 294 | return new Task(() => 295 | { 296 | FlushSync(); 297 | }); 298 | } 299 | 300 | /// 301 | /// Serialize event info into a json string 302 | /// 303 | /// 304 | /// 305 | public static string SerializeEventInfo(HttpEventCollectorEventInfo eventInfo) 306 | { 307 | return JsonConvert.SerializeObject(eventInfo); 308 | } 309 | 310 | /// 311 | /// Flush all batched events immediately. 312 | /// 313 | private void Flush() 314 | { 315 | lock (eventsBatchLock) 316 | { 317 | FlushInternal(); 318 | } 319 | } 320 | 321 | private void FlushInternal() 322 | { 323 | // FlushInternal method is called only in contexts locked on eventsBatchLock 324 | // therefore it's thread safe and doesn't need additional synchronization. 325 | 326 | if (serializedEventsBatch.Length == 0) 327 | return; // there is nothing to send 328 | 329 | // flush events according to the system operation mode 330 | if (this.sendMode == SendMode.Sequential) 331 | FlushInternalSequentialMode(this.eventsBatch, this.serializedEventsBatch.ToString()); 332 | else 333 | FlushInternalSingleBatch(this.eventsBatch, this.serializedEventsBatch.ToString()); 334 | 335 | // we explicitly create new objects instead to clear and reuse 336 | // the old ones because Flush works in async mode 337 | // and can use "previous" containers 338 | this.serializedEventsBatch = new StringBuilder(); 339 | this.eventsBatch = new List(); 340 | } 341 | 342 | private void FlushInternalSequentialMode( 343 | List events, 344 | String serializedEvents) 345 | { 346 | // post events only after the current post task is done 347 | if (this.activePostTask == null) 348 | { 349 | this.activePostTask = Task.Factory.StartNew(() => 350 | { 351 | FlushInternalSingleBatch(events, serializedEvents).Wait(); 352 | }); 353 | } 354 | else 355 | { 356 | this.activePostTask = this.activePostTask.ContinueWith((_) => 357 | { 358 | FlushInternalSingleBatch(events, serializedEvents).Wait(); 359 | }); 360 | } 361 | } 362 | 363 | private Task FlushInternalSingleBatch( 364 | List events, 365 | String serializedEvents) 366 | { 367 | // post data and update tasks counter 368 | Interlocked.Increment(ref activeAsyncTasksCount); 369 | Task task = PostEvents(events, serializedEvents); 370 | task.ContinueWith((_) => 371 | { 372 | Interlocked.Decrement(ref activeAsyncTasksCount); 373 | }); 374 | return task; 375 | } 376 | 377 | private async Task PostEvents( 378 | List events, 379 | String serializedEvents) 380 | { 381 | // encode data 382 | HttpResponseMessage response = null; 383 | string serverReply = null; 384 | HttpStatusCode responseCode = HttpStatusCode.OK; 385 | try 386 | { 387 | // post data 388 | HttpEventCollectorHandler next = (t, e) => 389 | { 390 | HttpContent content = new StringContent(serializedEvents, Encoding.UTF8, HttpContentTypeMedia); 391 | return httpClient.PostAsync(httpEventCollectorEndpointUri, content); 392 | }; 393 | HttpEventCollectorHandler postEvents = (t, e) => 394 | { 395 | return middleware == null ? 396 | next(t, e) : middleware(t, e, next); 397 | }; 398 | response = await postEvents(token, events); 399 | responseCode = response.StatusCode; 400 | if (responseCode != HttpStatusCode.OK && response.Content != null) 401 | { 402 | // record server reply 403 | serverReply = await response.Content.ReadAsStringAsync(); 404 | OnError(new HttpEventCollectorException( 405 | code: responseCode, 406 | webException: null, 407 | reply: serverReply, 408 | response: response, 409 | events: events 410 | )); 411 | } 412 | } 413 | catch (HttpEventCollectorException e) 414 | { 415 | e.Events = events; 416 | OnError(e); 417 | } 418 | catch (Exception e) 419 | { 420 | OnError(new HttpEventCollectorException( 421 | code: responseCode, 422 | webException: e, 423 | reply: serverReply, 424 | response: response, 425 | events: events 426 | )); 427 | } 428 | return responseCode; 429 | } 430 | 431 | private void OnTimer(object state) 432 | { 433 | Flush(); 434 | } 435 | 436 | #region HttpClientHandler.IDispose 437 | 438 | private bool disposed = false; 439 | 440 | public void Dispose() 441 | { 442 | Dispose(true); 443 | } 444 | 445 | protected virtual void Dispose(bool disposing) 446 | { 447 | if (disposed) 448 | return; 449 | if (disposing) 450 | { 451 | if (timer != null) 452 | { 453 | timer.Dispose(); 454 | } 455 | httpClient.Dispose(); 456 | } 457 | disposed = true; 458 | } 459 | 460 | ~HttpEventCollectorSender() 461 | { 462 | Dispose(false); 463 | } 464 | 465 | #endregion 466 | } 467 | } -------------------------------------------------------------------------------- /test/unit-tests/TestWithSplunk.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.Globalization; 5 | using System.IO; 6 | using System.Linq; 7 | using System.Net; 8 | using System.Text; 9 | using System.Threading; 10 | using Xunit; 11 | 12 | namespace Splunk.Logging 13 | { 14 | class SplunkCliWrapper 15 | { 16 | private string splunkCmd = string.Empty; 17 | private string userName = string.Empty, password = string.Empty; 18 | 19 | private static void EnableSelfSignedCertificates() 20 | { 21 | // Enable self signed certificates 22 | System.Net.ServicePointManager.ServerCertificateValidationCallback += 23 | delegate(object sender, System.Security.Cryptography.X509Certificates.X509Certificate certificate, 24 | System.Security.Cryptography.X509Certificates.X509Chain chain, 25 | System.Net.Security.SslPolicyErrors sslPolicyErrors) 26 | { 27 | return true; 28 | }; 29 | } 30 | 31 | public void DeleteIndex(string indexName) 32 | { 33 | string stdOut, stdError; 34 | ExecuteSplunkCli(string.Format(CultureInfo.InvariantCulture, "remove index {0}", indexName), out stdOut, out stdError); 35 | } 36 | 37 | public void CreateIndex(string indexName) 38 | { 39 | string stdOut, stdError; 40 | if (!ExecuteSplunkCli(string.Format(CultureInfo.InvariantCulture, "add index {0}", indexName), out stdOut, out stdError)) 41 | { 42 | Console.WriteLine("Failed to create index. {0} {1}", stdOut, stdError); 43 | Environment.Exit(2); 44 | } 45 | } 46 | 47 | public bool SplunkIsRunning() 48 | { 49 | string stdOut, stdError; 50 | ExecuteSplunkCli("status", out stdOut, out stdError); 51 | return stdOut.Contains("Splunkd: Running"); 52 | } 53 | 54 | public void StartServer() 55 | { 56 | if (SplunkIsRunning()) 57 | return; 58 | 59 | string stdOut, stdError; 60 | Console.WriteLine("Starting Splunk server."); 61 | Process splunkProcess = new Process 62 | { 63 | StartInfo = new ProcessStartInfo 64 | { 65 | FileName = "net.exe", 66 | Arguments = "start splunkd", 67 | UseShellExecute = false, 68 | RedirectStandardOutput = true, 69 | RedirectStandardError = true, 70 | CreateNoWindow = true 71 | } 72 | }; 73 | if (!ExecuteProcess(splunkProcess, out stdOut, out stdError)) 74 | { 75 | Console.WriteLine("Failed to start Splunk. {0} {1}", stdOut, stdError); 76 | Environment.Exit(2); 77 | } 78 | Console.WriteLine("Splunk started."); 79 | } 80 | 81 | public void StopServer() 82 | { 83 | if (!SplunkIsRunning()) 84 | return; 85 | 86 | string stdOut, stdError; 87 | Console.WriteLine("Stopping Splunk server."); 88 | if (!ExecuteSplunkCli("stop", out stdOut, out stdError)) 89 | { 90 | Console.WriteLine("Failed to stop Splunk. {0} {1}", stdOut, stdError); 91 | Environment.Exit(2); 92 | } 93 | Console.WriteLine("Splunk stopped."); 94 | } 95 | 96 | public int GetSearchCount(string searchQuery) 97 | { 98 | string stdOut, stdError; 99 | if (!ExecuteSplunkCli(string.Format(CultureInfo.InvariantCulture, "search \"{0} | stats count\" -preview false", searchQuery), out stdOut, out stdError)) 100 | { 101 | Console.WriteLine("Failed to run search query '{0}'. {1} {2}", searchQuery, stdOut, stdError); 102 | Environment.Exit(2); 103 | } 104 | return Convert.ToInt32(stdOut.Split(new char[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries)[2], CultureInfo.InvariantCulture); 105 | } 106 | 107 | public List GetSearchResults(string searchQuery) 108 | { 109 | string stdOut, stdError; 110 | if (!ExecuteSplunkCli(string.Format(CultureInfo.InvariantCulture, "search \"{0}\" -preview false -maxout 0", searchQuery), out stdOut, out stdError)) 111 | { 112 | Console.WriteLine("Failed to run search query '{0}'. {1} {2}", searchQuery, stdOut, stdError); 113 | Environment.Exit(2); 114 | } 115 | return new List(stdOut.Split(new char[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries)); 116 | } 117 | 118 | public List GetMetadataResults(string searchQuery) 119 | { 120 | string stdOut, stdError; 121 | if (!ExecuteSplunkCli(string.Format(CultureInfo.InvariantCulture, "search \"{0} | stats count by host, sourcetype, source\" -preview false", searchQuery), out stdOut, out stdError)) 122 | { 123 | Console.WriteLine("Failed to run search query '{0}'. {1} {2}", searchQuery, stdOut, stdError); 124 | Environment.Exit(2); 125 | } 126 | string metaData = stdOut.Split(new char[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries)[2]; 127 | return new List(metaData.Split(new char[] { ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries)); 128 | } 129 | 130 | public static readonly DateTime UnixEpoch = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc); 131 | public static double GetEpochTime() 132 | { 133 | return (DateTime.UtcNow - UnixEpoch).TotalSeconds; 134 | } 135 | 136 | public void WaitForIndexingToComplete(string indexName, double startTime = 0, int stabilityPeriod = 10) 137 | { 138 | string query = string.Format(CultureInfo.InvariantCulture, "index={0} earliest={1:F2}", indexName, startTime); 139 | int eventCount = GetSearchCount(query); 140 | for (int i = 0; i < stabilityPeriod; i++) // Exit only if there were no indexing activities for %stabilityPeriod% straight seconds 141 | { 142 | do 143 | { 144 | Thread.Sleep(1000); 145 | int updatedEventCount = GetSearchCount(query); 146 | if (updatedEventCount == eventCount) 147 | break; 148 | eventCount = updatedEventCount; 149 | i = 0; // Indexing is still goes on, reset waiting 150 | } while (true); 151 | } 152 | } 153 | 154 | public static bool ExecuteProcess(Process proc, out string stdOut, out string stdError) 155 | { 156 | proc.Start(); 157 | stdOut = proc.StandardOutput.ReadToEnd(); 158 | stdError = proc.StandardError.ReadToEnd(); 159 | if (proc.ExitCode != 0) 160 | { 161 | return false; 162 | } 163 | return true; 164 | } 165 | 166 | private bool ExecuteSplunkCli(string command, out string stdOut, out string stdError) 167 | { 168 | Process splunkProcess = new Process 169 | { 170 | StartInfo = new ProcessStartInfo 171 | { 172 | FileName = this.splunkCmd, 173 | Arguments = string.Format(CultureInfo.InvariantCulture, "{0} -auth {1}:{2}", command, userName, password), 174 | UseShellExecute = false, 175 | RedirectStandardOutput = true, 176 | RedirectStandardError = true, 177 | CreateNoWindow = true 178 | } 179 | }; 180 | return ExecuteProcess(splunkProcess, out stdOut, out stdError); 181 | } 182 | 183 | private bool ExecuteSplunkCliHttpEventCollector(string command, out string stdOut, out string stdError) 184 | { 185 | return ExecuteSplunkCli(string.Format(CultureInfo.InvariantCulture, "http-event-collector {0} -uri https://127.0.0.1:8089", command), out stdOut, out stdError); 186 | } 187 | 188 | public void EnableHttp() 189 | { 190 | string stdOut, stdError; 191 | if (!ExecuteSplunkCliHttpEventCollector("enable", out stdOut, out stdError)) 192 | { 193 | Console.WriteLine("Failed to execute 'enable' command. {0} {1}", stdOut, stdError); 194 | Environment.Exit(2); 195 | } 196 | } 197 | 198 | public void DeleteToken(string tokenName) 199 | { 200 | string stdOut, stdError; 201 | ExecuteSplunkCliHttpEventCollector(string.Format(CultureInfo.InvariantCulture, "delete -name {0}", tokenName), out stdOut, out stdError); 202 | } 203 | 204 | public string CreateToken(string tokenName, string indexes = null, string index = null) 205 | { 206 | string stdOut, stdError; 207 | string createCmd = string.Format(CultureInfo.InvariantCulture, "create -name {0}", tokenName); 208 | if (!string.IsNullOrEmpty(indexes)) 209 | createCmd += string.Format(CultureInfo.InvariantCulture, " -indexes {0}", indexes); 210 | if (!string.IsNullOrEmpty(index)) 211 | createCmd += string.Format(CultureInfo.InvariantCulture, " -index {0}", index); 212 | if (!ExecuteSplunkCliHttpEventCollector(createCmd, out stdOut, out stdError)) 213 | { 214 | Console.WriteLine("Failed to create token. {0} {1}", stdOut, stdError); 215 | Environment.Exit(2); 216 | } 217 | int idx1 = stdOut.IndexOf("token=", StringComparison.Ordinal) + 6, idx2 = idx1; 218 | while (stdOut[idx2] != '\r') 219 | idx2++; 220 | string result = stdOut.Substring(idx1, idx2 - idx1); 221 | return result; 222 | } 223 | 224 | public SplunkCliWrapper(string userName = "admin", string password = "changeme") 225 | { 226 | this.userName = userName; 227 | this.password = password; 228 | 229 | // Get splunkd location 230 | Process serviceQuery = new Process 231 | { 232 | StartInfo = new ProcessStartInfo 233 | { 234 | FileName = "sc.exe", 235 | Arguments = "qc splunkd", 236 | UseShellExecute = false, 237 | RedirectStandardOutput = true, 238 | CreateNoWindow = true 239 | } 240 | }; 241 | serviceQuery.Start(); 242 | string output = serviceQuery.StandardOutput.ReadToEnd(); 243 | if (serviceQuery.ExitCode != 0) 244 | { 245 | Console.WriteLine("Failed to execute query command, exit code {0}. Output is {1}", serviceQuery.ExitCode, output); 246 | Environment.Exit(1); 247 | } 248 | int idx1 = output.IndexOf("BINARY_PATH_NAME", StringComparison.Ordinal); 249 | idx1 = output.IndexOf(":", idx1 + 1, StringComparison.Ordinal) + 1; 250 | while (output[idx1] == ' ') 251 | idx1++; 252 | int idx2 = output.IndexOf("service", idx1, StringComparison.Ordinal) - 1; 253 | while (output[idx2] == ' ') 254 | idx2--; 255 | this.splunkCmd = output.Substring(idx1, idx2 - idx1 + 1).Replace("splunkd.exe", "splunk.exe"); 256 | this.StartServer(); 257 | EnableSelfSignedCertificates(); 258 | } 259 | } 260 | 261 | public class TestWithSplunk 262 | { 263 | #region Methods used by tests 264 | private static void GenerateDataWaitForIndexingCompletion(SplunkCliWrapper splunk, string indexName, double testStartTime, TraceSource trace) 265 | { 266 | // Generate data 267 | int eventCounter = GenerateData(trace); 268 | string searchQuery = "index=" + indexName; 269 | Console.WriteLine("{0} events were created, waiting for indexing to complete.", eventCounter); 270 | splunk.WaitForIndexingToComplete(indexName); 271 | int eventsFound = splunk.GetSearchCount(searchQuery); 272 | Console.WriteLine("Indexing completed, {0} events were found. Elapsed time {1:F2} seconds", eventsFound, SplunkCliWrapper.GetEpochTime() - testStartTime); 273 | // Verify all events were indexed correctly 274 | Assert.Equal(eventCounter, eventsFound); 275 | List searchResults = splunk.GetSearchResults(searchQuery); 276 | Assert.Equal(searchResults.Count, eventsFound); 277 | for (int eventId = 0; eventId < eventCounter; eventId++) 278 | { 279 | string expected = string.Format("This is event {0}", eventId); 280 | Assert.NotNull(searchResults.FirstOrDefault(s => s.Contains(expected))); 281 | } 282 | // Verify metadata 283 | List metaData = splunk.GetMetadataResults(searchQuery); 284 | Assert.Equal(metaData[0], "customhostname"); 285 | Assert.Equal(metaData[1], "log"); 286 | Assert.Equal(metaData[2], "host"); 287 | } 288 | private static int GenerateData(TraceSource trace, int eventsPerLoop = 50) 289 | { 290 | int eventCounter = 0, id = 0; 291 | foreach (TraceEventType eventType in new TraceEventType[] { TraceEventType.Error, TraceEventType.Information, TraceEventType.Warning }) 292 | { 293 | for (int i = 0; i < eventsPerLoop; i++, id++, eventCounter++) 294 | { 295 | trace.TraceData(eventType, id, new string[] { "TraceData", eventType.ToString(), string.Format("This is event {0}", id) }); 296 | } 297 | } 298 | foreach (TraceEventType eventType in new TraceEventType[] { TraceEventType.Error, TraceEventType.Information, TraceEventType.Warning }) 299 | { 300 | for (int i = 0; i < eventsPerLoop; i++, id++, eventCounter++) 301 | { 302 | trace.TraceEvent(eventType, id, "TraceEvent " + eventType.ToString() + string.Format(" This is event {0}", id)); 303 | } 304 | } 305 | for (int i = 0; i < eventsPerLoop; i++, id++, eventCounter++) 306 | { 307 | trace.TraceInformation(string.Format("TraceInformation. This is event {0}", id)); 308 | } 309 | return eventCounter; 310 | } 311 | 312 | private static string CreateIndexAndToken(SplunkCliWrapper splunk, string tokenName, string indexName) 313 | { 314 | splunk.EnableHttp(); 315 | Console.WriteLine("Enabled HTTP event collector."); 316 | splunk.DeleteToken(tokenName); 317 | string token = splunk.CreateToken(tokenName, indexes: indexName, index: indexName); 318 | Console.WriteLine("Created token {0}.", tokenName); 319 | splunk.DeleteIndex(indexName); 320 | splunk.CreateIndex(indexName); 321 | Console.WriteLine("Created index {0}.", indexName); 322 | return token; 323 | } 324 | #endregion 325 | 326 | #region Tests implementation 327 | [Trait("functional-tests", "StallWrite")] 328 | [Fact] 329 | public static void StallWrite() 330 | { 331 | string tokenName = "stallwritetoken"; 332 | string indexName = "stallwriteindex"; 333 | SplunkCliWrapper splunk = new SplunkCliWrapper(); 334 | double testStartTime = SplunkCliWrapper.GetEpochTime(); 335 | string token = CreateIndexAndToken(splunk, tokenName, indexName); 336 | 337 | const string baseUrl = "https://127.0.0.1:8088"; 338 | string postData = "{ \"event\": { \"data\": \"test event\" } }"; 339 | 340 | List requests = new List(); 341 | List streams = new List(); 342 | byte[] byteArray = Encoding.UTF8.GetBytes(postData); 343 | for (int i = 0; i < 10; i++) 344 | { 345 | var request = WebRequest.Create(baseUrl + "/services/collector") as HttpWebRequest; 346 | requests.Add(request); 347 | request.Timeout = 60 * 1000; 348 | request.KeepAlive = true; 349 | request.Method = "POST"; 350 | request.Headers.Add("Authorization", "Splunk " + token); 351 | request.ContentType = "application/x-www-form-urlencoded"; 352 | request.ContentLength = byteArray.Length * 2; 353 | try 354 | { 355 | Stream dataStream = request.GetRequestStream(); 356 | streams.Add(dataStream); 357 | dataStream.Write(byteArray, 0, byteArray.Length); 358 | } 359 | catch(WebException webErr) 360 | { 361 | //restart splunk 362 | Console.WriteLine("restart splunk due to " + webErr); 363 | splunk.StopServer(); 364 | splunk.StartServer(); 365 | 366 | } 367 | } 368 | } 369 | [Trait("functional-tests", "SendEventsBatchedByTime")] 370 | [Fact] 371 | public static void SendEventsBatchedByTime() 372 | { 373 | string tokenName = "batchedbytimetoken"; 374 | string indexName = "batchedbytimeindex"; 375 | SplunkCliWrapper splunk = new SplunkCliWrapper(); 376 | double testStartTime = SplunkCliWrapper.GetEpochTime(); 377 | string token = CreateIndexAndToken(splunk, tokenName, indexName); 378 | 379 | var trace = new TraceSource("HttpEventCollectorLogger"); 380 | trace.Switch.Level = SourceLevels.All; 381 | var meta = new HttpEventCollectorEventInfo.Metadata(index: indexName, source: "host", sourceType: "log", host: "customhostname"); 382 | var listener = new HttpEventCollectorTraceListener( 383 | uri: new Uri("https://127.0.0.1:8088"), 384 | token: token, 385 | metadata: meta, 386 | batchInterval: 1000); 387 | trace.Listeners.Add(listener); 388 | 389 | GenerateDataWaitForIndexingCompletion(splunk, indexName, testStartTime, trace); 390 | trace.Close(); 391 | } 392 | 393 | [Trait("functional-tests", "SendEventsBatchedBySize")] 394 | [Fact] 395 | static void SendEventsBatchedBySize() 396 | { 397 | string tokenName = "batchedbysizetoken"; 398 | string indexName = "batchedbysizeindex"; 399 | SplunkCliWrapper splunk = new SplunkCliWrapper(); 400 | double testStartTime = SplunkCliWrapper.GetEpochTime(); 401 | string token = CreateIndexAndToken(splunk, tokenName, indexName); 402 | 403 | var trace = new TraceSource("HttpEventCollectorLogger"); 404 | trace.Switch.Level = SourceLevels.All; 405 | var meta = new HttpEventCollectorEventInfo.Metadata(index: indexName, source: "host", sourceType: "log", host: "customhostname"); 406 | var listener = new HttpEventCollectorTraceListener( 407 | uri: new Uri("https://127.0.0.1:8088"), 408 | token: token, 409 | metadata: meta, 410 | batchSizeCount: 50); 411 | trace.Listeners.Add(listener); 412 | 413 | GenerateDataWaitForIndexingCompletion(splunk, indexName, testStartTime, trace); 414 | trace.Close(); 415 | } 416 | 417 | [Trait("functional-tests", "SendEventsBatchedBySizeAndTime")] 418 | [Fact] 419 | static void SendEventsBatchedBySizeAndTime() 420 | { 421 | string tokenName = "batchedbysizeandtimetoken"; 422 | string indexName = "batchedbysizeandtimeindex"; 423 | SplunkCliWrapper splunk = new SplunkCliWrapper(); 424 | double testStartTime = SplunkCliWrapper.GetEpochTime(); 425 | string token = CreateIndexAndToken(splunk, tokenName, indexName); 426 | 427 | var trace = new TraceSource("HttpEventCollectorLogger"); 428 | trace.Switch.Level = SourceLevels.All; 429 | var meta = new HttpEventCollectorEventInfo.Metadata(index: indexName, source: "host", sourceType: "log", host: "customhostname"); 430 | var listener = new HttpEventCollectorTraceListener( 431 | uri: new Uri("https://127.0.0.1:8088"), 432 | token: token, 433 | metadata: meta, 434 | batchSizeCount: 60, batchInterval: 2000); 435 | trace.Listeners.Add(listener); 436 | 437 | GenerateDataWaitForIndexingCompletion(splunk, indexName, testStartTime, trace); 438 | trace.Close(); 439 | } 440 | 441 | [Trait("functional-tests", "SendEventsUnBatched")] 442 | [Fact] 443 | static void SendEventsUnBatched() 444 | { 445 | string tokenName = "unbatchedtoken"; 446 | string indexName = "unbatchedindex"; 447 | SplunkCliWrapper splunk = new SplunkCliWrapper(); 448 | double testStartTime = SplunkCliWrapper.GetEpochTime(); 449 | string token = CreateIndexAndToken(splunk, tokenName, indexName); 450 | 451 | var trace = new TraceSource("HttpEventCollectorLogger"); 452 | trace.Switch.Level = SourceLevels.All; 453 | var meta = new HttpEventCollectorEventInfo.Metadata(index: indexName, source: "host", sourceType: "log", host: "customhostname"); 454 | var listener = new HttpEventCollectorTraceListener( 455 | uri: new Uri("https://127.0.0.1:8088"), 456 | token: token, 457 | metadata: meta); 458 | trace.Listeners.Add(listener); 459 | 460 | GenerateDataWaitForIndexingCompletion(splunk, indexName, testStartTime, trace); 461 | trace.Close(); 462 | } 463 | 464 | [Trait("functional-tests", "VerifyErrorsAreRaised")] 465 | [Fact] 466 | static void VerifyErrorsAreRaised() 467 | { 468 | string indexName = "errorindex"; 469 | string tokenName = "errortoken"; 470 | double testStartTime = SplunkCliWrapper.GetEpochTime(); 471 | SplunkCliWrapper splunk = new SplunkCliWrapper(); 472 | string token = CreateIndexAndToken(splunk, tokenName, indexName); 473 | 474 | var trace = new TraceSource("HttpEventCollectorLogger"); 475 | trace.Switch.Level = SourceLevels.All; 476 | 477 | var validMetaData = new HttpEventCollectorEventInfo.Metadata(index: indexName, source: "host", sourceType: "log", host: "customhostname"); 478 | var invalidMetaData = new HttpEventCollectorEventInfo.Metadata(index: "notexistingindex", source: "host", sourceType: "log", host: "customhostname"); 479 | 480 | var listenerWithWrongToken = new HttpEventCollectorTraceListener( 481 | uri: new Uri("https://127.0.0.1:8088"), 482 | token: "notexistingtoken", 483 | metadata: validMetaData); 484 | var listenerWithWrongUri = new HttpEventCollectorTraceListener( 485 | uri: new Uri("https://127.0.0.1:8087"), 486 | token: token, 487 | metadata: validMetaData); 488 | var listenerWithWrongMetadata = new HttpEventCollectorTraceListener( 489 | uri: new Uri("https://127.0.0.1:8088"), 490 | token: token, 491 | metadata: invalidMetaData); 492 | 493 | bool wrongTokenWasRaised = false; 494 | listenerWithWrongToken.AddLoggingFailureHandler((HttpEventCollectorException e) => 495 | { 496 | wrongTokenWasRaised = true; 497 | }); 498 | bool wrongUriWasRaised = false; 499 | listenerWithWrongUri.AddLoggingFailureHandler((HttpEventCollectorException e) => 500 | { 501 | wrongUriWasRaised = true; 502 | }); 503 | bool wrongMetaDataWasRaised = false; 504 | listenerWithWrongMetadata.AddLoggingFailureHandler((HttpEventCollectorException e) => 505 | { 506 | wrongMetaDataWasRaised = true; 507 | }); 508 | 509 | trace.Listeners.Add(listenerWithWrongToken); 510 | trace.Listeners.Add(listenerWithWrongUri); 511 | trace.Listeners.Add(listenerWithWrongMetadata); 512 | // Generate data 513 | int eventCounter = GenerateData(trace); 514 | Console.WriteLine("{0} events were created, waiting for errors to be raised.", eventCounter); 515 | Thread.Sleep(30 * 1000); 516 | trace.Close(); 517 | Assert.True(wrongTokenWasRaised); 518 | Assert.True(wrongUriWasRaised); 519 | Assert.True(wrongMetaDataWasRaised); 520 | } 521 | 522 | [Trait("functional-tests", "VerifyFlushEvents")] 523 | [Fact] 524 | public void VerifyFlushEvents() 525 | { 526 | string tokenName = "flusheventtoken"; 527 | string indexName = "flusheventdindex"; 528 | SplunkCliWrapper splunk = new SplunkCliWrapper(); 529 | double testStartTime = SplunkCliWrapper.GetEpochTime(); 530 | 531 | string token = CreateIndexAndToken(splunk, tokenName, indexName); 532 | splunk.StopServer(); 533 | Thread.Sleep(5 * 1000); 534 | 535 | var trace = new TraceSource("HttpEventCollectorLogger"); 536 | trace.Switch.Level = SourceLevels.All; 537 | var meta = new HttpEventCollectorEventInfo.Metadata(index: indexName, source: "host", sourceType: "log", host: "customhostname"); 538 | var listener = new HttpEventCollectorTraceListener( 539 | uri: new Uri("https://127.0.0.1:8088"), 540 | token: token, 541 | retriesOnError: int.MaxValue, 542 | metadata: meta); 543 | trace.Listeners.Add(listener); 544 | 545 | // Generate data, wait a little bit so retries are happenning and start Splunk. Expecting to see all data make it 546 | const int eventsToGeneratePerLoop = 250; 547 | const int expectedCount = eventsToGeneratePerLoop * 7; 548 | int eventCounter = GenerateData(trace, eventsPerLoop: eventsToGeneratePerLoop); 549 | splunk.StartServer(); 550 | trace.Close(); 551 | 552 | // Verify every event made to Splunk 553 | splunk.WaitForIndexingToComplete(indexName, stabilityPeriod: 30); 554 | int eventsFound = splunk.GetSearchCount("index=" + indexName); 555 | Assert.Equal(expectedCount, eventsFound); 556 | } 557 | 558 | [Trait("functional-tests", "VerifyEventsAreInOrder")] 559 | [Fact] 560 | static void VerifyEventsAreInOrder() 561 | { 562 | string tokenName = "verifyeventsareinordertoken"; 563 | string indexName = "verifyeventsareinorderindex"; 564 | SplunkCliWrapper splunk = new SplunkCliWrapper(); 565 | double testStartTime = SplunkCliWrapper.GetEpochTime(); 566 | string token = CreateIndexAndToken(splunk, tokenName, indexName); 567 | 568 | var trace = new TraceSource("HttpEventCollectorLogger"); 569 | trace.Switch.Level = SourceLevels.All; 570 | var meta = new HttpEventCollectorEventInfo.Metadata(index: indexName, source: "host", sourceType: "log", host: "customhostname"); 571 | var listener = new HttpEventCollectorTraceListener( 572 | uri: new Uri("https://127.0.0.1:8088"), 573 | token: token, 574 | metadata: meta); 575 | trace.Listeners.Add(listener); 576 | 577 | // Generate data 578 | int totalEvents = 1000; 579 | string[] filer = new string[2]; 580 | filer[0] = new string('s', 1); 581 | filer[1] = new string('l', 100000); 582 | for (int i = 0; i < totalEvents; i++) 583 | { 584 | trace.TraceInformation(string.Format("TraceInformation. This is event {0}. {1}", i, filer[i%2])); 585 | } 586 | 587 | string searchQuery = "index=" + indexName; 588 | Console.WriteLine("{0} events were created, waiting for indexing to complete.", totalEvents); 589 | splunk.WaitForIndexingToComplete(indexName); 590 | int eventsFound = splunk.GetSearchCount(searchQuery); 591 | Console.WriteLine("Indexing completed, {0} events were found. Elapsed time {1:F2} seconds", eventsFound, SplunkCliWrapper.GetEpochTime() - testStartTime); 592 | // Verify all events were indexed correctly and in order 593 | Assert.Equal(totalEvents, eventsFound); 594 | List searchResults = splunk.GetSearchResults(searchQuery); 595 | Assert.Equal(searchResults.Count, eventsFound); 596 | for (int i = 0; i< totalEvents; i++) 597 | { 598 | int id = totalEvents - i - 1; 599 | string expected = string.Format("TraceInformation. This is event {0}", id); 600 | Assert.True(searchResults[i].Contains(expected)); 601 | } 602 | trace.Close(); 603 | } 604 | 605 | #endregion 606 | } 607 | } 608 | --------------------------------------------------------------------------------