diff --git a/.travis.yml b/.travis.yml index 16ba7d9..4a81e54 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,13 +18,13 @@ addons: - zlib1g os: - - osx + #- osx - linux env: matrix: - CLI_VERSION=1.0.0-preview2-003121 - - CLI_VERSION=Latest + # - CLI_VERSION=Latest matrix: allow_failures: diff --git a/Build.ps1 b/Build.ps1 index d28c393..7c5a85f 100644 --- a/Build.ps1 +++ b/Build.ps1 @@ -1,16 +1,51 @@ +echo "build: Build started" + Push-Location $PSScriptRoot -if(Test-Path .\artifacts) { Remove-Item .\artifacts -Force -Recurse } +if(Test-Path .\artifacts) { + echo "build: Cleaning .\artifacts" + Remove-Item .\artifacts -Force -Recurse +} -& dotnet restore +& dotnet restore --no-cache -$revision = @{ $true = $env:APPVEYOR_BUILD_NUMBER; $false = 1 }[$env:APPVEYOR_BUILD_NUMBER -ne $NULL]; +$branch = @{ $true = $env:APPVEYOR_REPO_BRANCH; $false = $(git symbolic-ref --short -q HEAD) }[$env:APPVEYOR_REPO_BRANCH -ne $NULL]; +$revision = @{ $true = "{0:00000}" -f [convert]::ToInt32("0" + $env:APPVEYOR_BUILD_NUMBER, 10); $false = "local" }[$env:APPVEYOR_BUILD_NUMBER -ne $NULL]; +$suffix = @{ $true = ""; $false = "$($branch.Substring(0, [math]::Min(10,$branch.Length)))-$revision"}[$branch -eq "master" -and $revision -ne "local"] -Push-Location src/Serilog.Sinks.Splunk +echo "build: Version suffix is $suffix" -& dotnet pack -c Release -o ..\..\.\artifacts --version-suffix=$revision -if($LASTEXITCODE -ne 0) { exit 1 } +foreach ($src in ls src/*) { + Push-Location $src + echo "build: Packaging project in $src" + + & dotnet pack -c Release -o ..\..\artifacts --version-suffix=$suffix + if($LASTEXITCODE -ne 0) { exit 1 } + + Pop-Location +} + +foreach ($test in ls test/*.PerformanceTests) { + Push-Location $test + + echo "build: Building performance test project in $test" + + & dotnet build -c Release + if($LASTEXITCODE -ne 0) { exit 2 } + + Pop-Location +} + +foreach ($test in ls test/*.Tests) { + Push-Location $test + + echo "build: Testing project in $test" + + & dotnet test -c Release + if($LASTEXITCODE -ne 0) { exit 3 } + + Pop-Location +} -Pop-Location Pop-Location diff --git a/README.md b/README.md index 1588fd2..5f3aa74 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,14 @@ -# serilog-sinks-splunk -[![Package Logo](http://serilog.net/images/serilog-sink-nuget.png)](http://nuget.org/packages/serilog.sinks.splunk) +# Serilog.Sinks.Splunk [![Build status](https://ci.appveyor.com/api/projects/status/yt40wg34t8oj61al?svg=true)](https://ci.appveyor.com/project/serilog/serilog-sinks-splunk) [![NuGet Version](http://img.shields.io/nuget/v/Serilog.Sinks.Splunk.svg?style=flat)](https://www.nuget.org/packages/Serilog.Sinks.Splunk/) + [![Join the chat at https://gitter.im/serilog/serilog](https://img.shields.io/gitter/room/serilog/serilog.svg)](https://gitter.im/serilog/serilog) + +A Serilog sink that writes events to the [Splunk](https://splunk.com). Supports .NET 4.5+, .NET Core, and platforms compatible with the [.NET Platform Standard](https://github.com/dotnet/corefx/blob/master/Documentation/architecture/net-platform-standard.md) 1.1 including Windows 8 & UWP, Windows Phone and Xamarin. -A sink for Serilog that writes events to [Splunk](https://splunk.com). Moved from the [main Serilog repository](https://github.com/serilog/serilog) for independent versioning. Published to [NuGet](http://www.nuget.org/packages/serilog.sinks.splunk). +[![Package Logo](http://serilog.net/images/serilog-sink-nuget.png)](http://nuget.org/packages/serilog.sinks.splunk) **Package** - [Serilog.Sinks.Splunk](http://nuget.org/packages/serilog.sinks.splunk) -| **Platforms** - .NET 4.5+, PCL ## Getting started @@ -17,7 +18,7 @@ To get started install the *Serilog.Sinks.Splunk* package from Visual Studio's * PM> Install-Package Serilog.Sinks.Splunk ``` -Using the new Event Collector in Splunk 6.3 +Using the Event Collector (Splunk 6.3 and above) ```csharp var log = new LoggerConfiguration() @@ -25,4 +26,13 @@ var log = new LoggerConfiguration() .CreateLogger(); ``` -More information is available [here](https://github.com/serilog/serilog-sinks-splunk/wiki). \ No newline at end of file +More information is available on the [wiki](https://github.com/serilog/serilog-sinks-splunk/wiki). + +### Build status + +Branch | AppVeyor | Travis +------------- | ------------- |------------- +master | [![Build status](https://ci.appveyor.com/api/projects/status/yt40wg34t8oj61al/branch/master?svg=true)](https://ci.appveyor.com/project/serilog/serilog-sinks-splunk/branch/dev) | ![](https://travis-ci.org/serilog/serilog-sinks-splunk.svg?branch=master) +dev | [![Build status](https://ci.appveyor.com/api/projects/status/yt40wg34t8oj61al/branch/dev?svg=true)](https://ci.appveyor.com/project/serilog/serilog-sinks-splunk/branch/master) | ![](https://travis-ci.org/serilog/serilog-sinks-splunk.svg?branch=dev) + +_Serilog is copyright © 2013-2016 Serilog Contributors - Provided under the [Apache License, Version 2.0](http://apache.org/licenses/LICENSE-2.0.html). Needle and thread logo a derivative of work by [Kenneth Appiah](http://www.kensets.com/)._ \ No newline at end of file diff --git a/appveyor.yml b/appveyor.yml index e41635e..19d0d28 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -19,10 +19,10 @@ deploy: secure: nvZ/z+pMS91b3kG4DgfES5AcmwwGoBYQxr9kp4XiJHj25SAlgdIxFx++1N0lFH2x skip_symbols: true on: - branch: master + branch: /^(master|dev)$/ - provider: GitHub auth_token: - secure: ggZTqqV1z0xecDoQbeoy3A7xikShCt9FWZIGp95dG9Fo0p5RAT9oGU0ZekHfUIwk + secure: p4LpVhBKxGS5WqucHxFQ5c7C8cP74kbNB0Z8k9Oxx/PMaDQ1+ibmoexNqVU5ZlmX artifact: /Serilog.*\.nupkg/ tag: v$(appveyor_build_version) on: diff --git a/build.sh b/build.sh index b874474..308817a 100755 --- a/build.sh +++ b/build.sh @@ -1,4 +1,8 @@ #!/bin/bash + +# Ensure any exit code exits TravisCI +set -e + dotnet restore for path in src/*/project.json; do dirname="$(dirname "${path}")" @@ -8,4 +12,10 @@ done for path in sample/project.json; do dirname="$(dirname "${path}")" dotnet build ${dirname} -done \ No newline at end of file +done + +for path in test/Serilog.Sinks.Splunk.Tests/project.json; do + dirname="$(dirname "${path}")" + dotnet build ${dirname} -f netcoreapp1.0 -c Release + dotnet test ${dirname} -f netcoreapp1.0 -c Release +done \ No newline at end of file diff --git a/global.json b/global.json index b51e28b..a2b2a41 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ -{ +{ "projects": [ "src", "test" ], "sdk": { - "version": "1.0.0-preview1-002702" + "version": "1.0.0-preview2-003121" } } diff --git a/sample/Program.cs b/sample/Program.cs index 8ac2af5..305543f 100755 --- a/sample/Program.cs +++ b/sample/Program.cs @@ -7,7 +7,7 @@ namespace Sample { public class Program { - public static string EventCollectorToken = "04B42E81-100E-4BED-8AE9-FC5EE4E08602"; + public static string EventCollectorToken = "1A4D65C9-601A-4717-AD6C-E1EC36A46B69"; public static void Main(string[] args) { @@ -32,10 +32,11 @@ public static void Main(string[] args) foreach (var i in Enumerable.Range(0, eventsToCreate)) { - Log.Information("Running vanilla without extended endpoint loop {Counter}", i); - Thread.Sleep(5); + Log.Information("Running vanilla without extended endpoint loop {Counter}", i); } + Log.CloseAndFlush(); + // Vanilla Test with full uri specified Log.Logger = new LoggerConfiguration() .MinimumLevel.Debug() @@ -49,10 +50,11 @@ public static void Main(string[] args) foreach (var i in Enumerable.Range(0, eventsToCreate)) { - Log.Information("Running vanilla loop {Counter}", i); - Thread.Sleep(5); + Log.Information("Running vanilla loop {Counter}", i); } + Log.CloseAndFlush(); + // Override Source Log.Logger = new LoggerConfiguration() .MinimumLevel.Debug() @@ -67,9 +69,10 @@ public static void Main(string[] args) foreach (var i in Enumerable.Range(0, eventsToCreate)) { - Log.Information("Running source override loop {Counter}", i); - Thread.Sleep(5); + Log.Information("Running source override loop {Counter}", i); } + + Log.CloseAndFlush(); // Override Host Log.Logger = new LoggerConfiguration() @@ -85,10 +88,10 @@ public static void Main(string[] args) foreach (var i in Enumerable.Range(0, eventsToCreate)) { - Log.Information("Running host override loop {Counter}", i); - Thread.Sleep(5); + Log.Information("Running host override loop {Counter}", i); } - + Log.CloseAndFlush(); + // No Template Log.Logger = new LoggerConfiguration() .MinimumLevel.Debug() @@ -103,23 +106,28 @@ public static void Main(string[] args) foreach (var i in Enumerable.Range(0, eventsToCreate)) { - Log.Information("Running no template loop {Counter}", i); - Thread.Sleep(5); + Log.Information("Running no template loop {Counter}", i); } - // SSL - Log.Logger = new LoggerConfiguration() - .MinimumLevel.Debug() - .WriteTo.LiterateConsole() - .WriteTo.EventCollector( - "https://localhost:8088", - Program.EventCollectorToken) - .Enrich.WithProperty("Serilog.Sinks.Splunk.Sample", "ViaEventCollector") - .Enrich.WithProperty("Serilog.Sinks.Splunk.Sample.TestType", "HTTPS") - .CreateLogger(); + Log.CloseAndFlush(); + + // // SSL + // Log.Logger = new LoggerConfiguration() + // .MinimumLevel.Debug() + // .WriteTo.LiterateConsole() + // .WriteTo.EventCollector( + // "https://localhost:8088", + // Program.EventCollectorToken) + // .Enrich.WithProperty("Serilog.Sinks.Splunk.Sample", "ViaEventCollector") + // .Enrich.WithProperty("Serilog.Sinks.Splunk.Sample.TestType", "HTTPS") + // .CreateLogger(); - Log.Debug("Waiting for Events to Flush"); - Thread.Sleep(5000); + // foreach (var i in Enumerable.Range(0, eventsToCreate)) + // { + // Log.Information("HTTPS {Counter}", i); + // } + // Log.CloseAndFlush(); + Log.Debug("Done"); } diff --git a/serilog-sinks-splunk.sln b/serilog-sinks-splunk.sln index a8948f7..ae40934 100644 --- a/serilog-sinks-splunk.sln +++ b/serilog-sinks-splunk.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 14 -VisualStudioVersion = 14.0.24720.0 +VisualStudioVersion = 14.0.25420.1 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{7A774CBB-A6E9-4854-B4DB-4CF860B0C1C5}" EndProject @@ -25,6 +25,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "sample", "sample", "{1C75E4 EndProject Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Sample", "src\sample\Sample.xproj", "{17497155-5D94-45DF-81D9-BD960E8CF217}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{B9451AD8-09B9-4C09-A152-FBAE24806614}" +EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Serilog.Sinks.Splunk.Tests", "test\Serilog.Sinks.Splunk.Tests\Serilog.Sinks.Splunk.Tests.xproj", "{3C2D8E01-5580-426A-BDD9-EC59CD98E618}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -39,6 +43,10 @@ Global {17497155-5D94-45DF-81D9-BD960E8CF217}.Debug|Any CPU.Build.0 = Debug|Any CPU {17497155-5D94-45DF-81D9-BD960E8CF217}.Release|Any CPU.ActiveCfg = Release|Any CPU {17497155-5D94-45DF-81D9-BD960E8CF217}.Release|Any CPU.Build.0 = Release|Any CPU + {3C2D8E01-5580-426A-BDD9-EC59CD98E618}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3C2D8E01-5580-426A-BDD9-EC59CD98E618}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3C2D8E01-5580-426A-BDD9-EC59CD98E618}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3C2D8E01-5580-426A-BDD9-EC59CD98E618}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -46,5 +54,6 @@ Global GlobalSection(NestedProjects) = preSolution {32CF915C-3ECD-496C-8FDB-1214581274A6} = {7A774CBB-A6E9-4854-B4DB-4CF860B0C1C5} {17497155-5D94-45DF-81D9-BD960E8CF217} = {1C75E4A9-4CB1-497C-AD17-B438882051A1} + {3C2D8E01-5580-426A-BDD9-EC59CD98E618} = {B9451AD8-09B9-4C09-A152-FBAE24806614} EndGlobalSection EndGlobal diff --git a/src/Serilog.Sinks.Splunk/Properties/AssemblyInfo.cs b/src/Serilog.Sinks.Splunk/Properties/AssemblyInfo.cs index f7d5b25..e5f0e08 100644 --- a/src/Serilog.Sinks.Splunk/Properties/AssemblyInfo.cs +++ b/src/Serilog.Sinks.Splunk/Properties/AssemblyInfo.cs @@ -2,3 +2,5 @@ using System.Runtime.CompilerServices; [assembly: AssemblyVersion("2.0.0.0")] + +[assembly: InternalsVisibleTo("Serilog.Sinks.Splunk.Tests")] \ No newline at end of file diff --git a/src/Serilog.Sinks.Splunk/Sinks/Splunk/EventCollectorRequest.cs b/src/Serilog.Sinks.Splunk/Sinks/Splunk/EventCollectorRequest.cs index e4667e1..6b9dfd8 100644 --- a/src/Serilog.Sinks.Splunk/Sinks/Splunk/EventCollectorRequest.cs +++ b/src/Serilog.Sinks.Splunk/Sinks/Splunk/EventCollectorRequest.cs @@ -14,55 +14,11 @@ using System; -using System.Globalization; using System.Net.Http; using System.Text; namespace Serilog.Sinks.Splunk { - internal class SplunkEvent - { - private string _payload; - - internal SplunkEvent(string logEvent, string source, string sourceType, string host, string index, double time) - { - _payload = string.Empty; - - var jsonPayLoad = @"{""event"":" + logEvent - .Replace("\r\n", string.Empty); - - if (!string.IsNullOrWhiteSpace(source)) - { - jsonPayLoad = jsonPayLoad + @",""source"":""" + source + @""""; - } - if (!string.IsNullOrWhiteSpace(sourceType)) - { - jsonPayLoad = jsonPayLoad + @",""sourceType"":""" + sourceType + @""""; - } - if (!string.IsNullOrWhiteSpace(host)) - { - jsonPayLoad = jsonPayLoad + @",""host"":""" + host + @""""; - } - if (!string.IsNullOrWhiteSpace(index)) - { - jsonPayLoad = jsonPayLoad + @",""index"":""" + index + @""""; - } - - if (time > 0) - { - jsonPayLoad = jsonPayLoad + @",""time"":" + time.ToString(CultureInfo.InvariantCulture); - } - - jsonPayLoad = jsonPayLoad + "}"; - _payload = jsonPayLoad; - } - - public string Payload - { - get { return _payload; } - } - } - internal class EventCollectorRequest : HttpRequestMessage { internal EventCollectorRequest(string splunkHost, string jsonPayLoad, string uri ="services/collector") diff --git a/src/Serilog.Sinks.Splunk/Sinks/Splunk/EventCollectorSink.cs b/src/Serilog.Sinks.Splunk/Sinks/Splunk/EventCollectorSink.cs index ee9f10d..1e86e19 100644 --- a/src/Serilog.Sinks.Splunk/Sinks/Splunk/EventCollectorSink.cs +++ b/src/Serilog.Sinks.Splunk/Sinks/Splunk/EventCollectorSink.cs @@ -33,20 +33,11 @@ namespace Serilog.Sinks.Splunk public class EventCollectorSink : ILogEventSink, IDisposable { private readonly string _splunkHost; - private readonly string _eventCollectorToken; - private readonly string _source; - private readonly string _sourceType; - private readonly string _host; - private readonly string _index; private readonly string _uriPath; private readonly int _batchSizeLimitLimit; private readonly SplunkJsonFormatter _jsonFormatter; private readonly ConcurrentQueue _queue; private readonly EventCollectorClient _httpClient; - private const string DefaultSource = ""; - private const string DefaultSourceType = ""; - private const string DefaultHost = ""; - private const string DefaultIndex = ""; /// /// Taken from Splunk.Logging.Common @@ -56,7 +47,7 @@ public class EventCollectorSink : ILogEventSink, IDisposable HttpStatusCode.Forbidden, HttpStatusCode.MethodNotAllowed, HttpStatusCode.BadRequest - }; + }; /// /// Creates a new instance of the sink @@ -73,25 +64,17 @@ public EventCollectorSink( int batchIntervalInSeconds = 5, int batchSizeLimit = 100, IFormatProvider formatProvider = null, - bool renderTemplate = true - ) + bool renderTemplate = true) + : this( + splunkHost, + eventCollectorToken, + null, null, null, null, null, + batchIntervalInSeconds, + batchSizeLimit, + formatProvider, + renderTemplate) { - _splunkHost = splunkHost; - _eventCollectorToken = eventCollectorToken; - _queue = new ConcurrentQueue(); - _jsonFormatter = new SplunkJsonFormatter(renderMessage: true, formatProvider: formatProvider, renderTemplate: renderTemplate); - _batchSizeLimitLimit = batchSizeLimit; - - var batchInterval = TimeSpan.FromSeconds(batchIntervalInSeconds); - _httpClient = new EventCollectorClient(_eventCollectorToken); - - var cancellationToken = new CancellationToken(); - - RepeatAction.OnInterval( - batchInterval, - async () => await ProcessQueue(), - cancellationToken); - } + } /// /// Creates a new instance of the sink @@ -107,6 +90,7 @@ public EventCollectorSink( /// The source of the event /// The source type of the event /// The host of the event + /// The handler used to send HTTP requests public EventCollectorSink( string splunkHost, string eventCollectorToken, @@ -118,19 +102,26 @@ public EventCollectorSink( int batchIntervalInSeconds, int batchSizeLimit, IFormatProvider formatProvider = null, - bool renderTemplate = true - ) : this(splunkHost, - eventCollectorToken, - batchIntervalInSeconds, - batchSizeLimit, - formatProvider, - renderTemplate) + bool renderTemplate = true, + HttpMessageHandler messageHandler = null) { - _source = source; - _sourceType = sourceType; - _host = host; - _index = index; _uriPath = uriPath; + _splunkHost = splunkHost; + _queue = new ConcurrentQueue(); + _jsonFormatter = new SplunkJsonFormatter(renderTemplate, formatProvider, source, sourceType, host, index); + _batchSizeLimitLimit = batchSizeLimit; + + var batchInterval = TimeSpan.FromSeconds(batchIntervalInSeconds); + _httpClient = messageHandler != null + ? new EventCollectorClient(eventCollectorToken, messageHandler) + : new EventCollectorClient(eventCollectorToken); + + var cancellationToken = new CancellationToken(); + + RepeatAction.OnInterval( + batchInterval, + async () => await ProcessQueue(), + cancellationToken); } /// @@ -175,21 +166,14 @@ private async Task ProcessQueue() private async Task Send(IEnumerable events) { - string allEvents = string.Empty; + var allEvents = new StringWriter(); foreach (var logEvent in events) { - var sw = new StringWriter(); - _jsonFormatter.Format(logEvent, sw); - - var serialisedEvent = sw.ToString(); - - var splunkEvent = new SplunkEvent(serialisedEvent, _source, _sourceType, _host, _index, logEvent.Timestamp.ToEpoch()); - - allEvents = $"{allEvents}{splunkEvent.Payload}"; + _jsonFormatter.Format(logEvent, allEvents); } - var request = new EventCollectorRequest(_splunkHost, allEvents, _uriPath); + var request = new EventCollectorRequest(_splunkHost, allEvents.ToString(), _uriPath); var response = await _httpClient.SendAsync(request); if (response.IsSuccessStatusCode) diff --git a/src/Serilog.Sinks.Splunk/Sinks/Splunk/SplunkJsonFormatter.cs b/src/Serilog.Sinks.Splunk/Sinks/Splunk/SplunkJsonFormatter.cs index fdbc68d..b2f2e6f 100644 --- a/src/Serilog.Sinks.Splunk/Sinks/Splunk/SplunkJsonFormatter.cs +++ b/src/Serilog.Sinks.Splunk/Sinks/Splunk/SplunkJsonFormatter.cs @@ -1,4 +1,4 @@ -// Copyright 2014 Serilog Contriutors +// Copyright 2016 Serilog Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -10,56 +10,140 @@ // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and -// limitations under the License.using System; +// limitations under the License. using System; +using System.Collections.Generic; +using System.Globalization; using System.IO; +using Serilog.Events; +using Serilog.Formatting; using Serilog.Formatting.Json; namespace Serilog.Sinks.Splunk { /// - /// A json formatter to allow conditional rendering of properties + /// Renders log events into a default JSON format for consumption by Splunk. /// - public class SplunkJsonFormatter : JsonFormatter + public class SplunkJsonFormatter : ITextFormatter { - private readonly bool _renderTemplate; + static readonly JsonValueFormatter ValueFormatter = new JsonValueFormatter(); + + readonly bool _renderTemplate; + readonly IFormatProvider _formatProvider; + readonly string _suffix; /// - /// Construct a . + /// Construct a . /// - /// If true, the properties of the event will be written to - /// the output without enclosing braces. Otherwise, if false, each event will be written as a well-formed - /// JSON object. - /// A string that will be written after each log event is formatted. - /// If null, will be used. Ignored if - /// is true. - /// If true, the message will be rendered and written to the output as a - /// property named RenderedMessage. /// Supplies culture-specific formatting information, or null. /// If true, the template used will be rendered and written to the output as a property named MessageTemplate public SplunkJsonFormatter( - bool omitEnclosingObject = false, - string closingDelimiter = null, - bool renderMessage = false, - IFormatProvider formatProvider = null, - bool renderTemplate = true) - :base(omitEnclosingObject,closingDelimiter,renderMessage,formatProvider) + bool renderTemplate, + IFormatProvider formatProvider) + : this(renderTemplate, formatProvider, null, null, null, null) { - _renderTemplate = renderTemplate; } - /// - /// Writes the message with or without the message template + /// Construct a . /// - /// - /// - /// - protected override void WriteMessageTemplate(string template, ref string delim, TextWriter output) + /// Supplies culture-specific formatting information, or null. + /// If true, the template used will be rendered and written to the output as a property named MessageTemplate + /// The Splunk index to log to + /// The source of the event + /// The source type of the event + /// The host of the event + public SplunkJsonFormatter( + bool renderTemplate, + IFormatProvider formatProvider, + string source, + string sourceType, + string host, + string index) { - if(_renderTemplate) - base.WriteMessageTemplate(template, ref delim, output); + _renderTemplate = renderTemplate; + _formatProvider = formatProvider; + + var suffixWriter = new StringWriter(); + suffixWriter.Write("}"); // Terminates "event" + + if (!string.IsNullOrWhiteSpace(source)) + { + suffixWriter.Write(",\"source\":"); + JsonValueFormatter.WriteQuotedJsonString(source, suffixWriter); + } + + if (!string.IsNullOrWhiteSpace(sourceType)) + { + suffixWriter.Write(",\"sourceType\":"); + JsonValueFormatter.WriteQuotedJsonString(sourceType, suffixWriter); + } + + if (!string.IsNullOrWhiteSpace(host)) + { + suffixWriter.Write(",\"host\":"); + JsonValueFormatter.WriteQuotedJsonString(host, suffixWriter); + } + + if (!string.IsNullOrWhiteSpace(index)) + { + suffixWriter.Write(",\"index\":"); + JsonValueFormatter.WriteQuotedJsonString(index, suffixWriter); + } + suffixWriter.Write('}'); // Terminates the payload + _suffix = suffixWriter.ToString(); + } + + /// + public void Format(LogEvent logEvent, TextWriter output) + { + if (logEvent == null) throw new ArgumentNullException(nameof(logEvent)); + if (output == null) throw new ArgumentNullException(nameof(output)); + + output.Write("{\"time\":\""); + output.Write(logEvent.Timestamp.ToEpoch().ToString(CultureInfo.InvariantCulture)); + output.Write("\",\"event\":{\"Level\":\""); + output.Write(logEvent.Level); + output.Write('"'); + + if (_renderTemplate) + { + output.Write(",\"MessageTemplate\":"); + JsonValueFormatter.WriteQuotedJsonString(logEvent.MessageTemplate.Text, output); + } + + output.Write(",\"RenderedMessage\":"); + JsonValueFormatter.WriteQuotedJsonString(logEvent.RenderMessage(_formatProvider), output); + + if (logEvent.Exception != null) + { + output.Write(",\"Exception\":"); + JsonValueFormatter.WriteQuotedJsonString(logEvent.Exception.ToString(), output); + } + + if (logEvent.Properties.Count != 0) + WriteProperties(logEvent.Properties, output); + + output.WriteLine(_suffix); + } + + static void WriteProperties(IReadOnlyDictionary properties, TextWriter output) + { + output.Write(",\"Properties\":{"); + + var precedingDelimiter = ""; + foreach (var property in properties) + { + output.Write(precedingDelimiter); + precedingDelimiter = ","; + + JsonValueFormatter.WriteQuotedJsonString(property.Key, output); + output.Write(':'); + ValueFormatter.Format(property.Value, output); + } + + output.Write('}'); } } -} \ No newline at end of file +} diff --git a/src/Serilog.Sinks.Splunk/Sinks/Splunk/TcpSink.cs b/src/Serilog.Sinks.Splunk/Sinks/Splunk/TcpSink.cs index 5583ab4..5c7931e 100644 --- a/src/Serilog.Sinks.Splunk/Sinks/Splunk/TcpSink.cs +++ b/src/Serilog.Sinks.Splunk/Sinks/Splunk/TcpSink.cs @@ -102,7 +102,7 @@ private static TcpSocketWriter CreateSocketWriter(SplunkTcpSinkConnectionInfo co private static SplunkJsonFormatter CreateDefaultFormatter(IFormatProvider formatProvider, bool renderTemplate) { - return new SplunkJsonFormatter(renderMessage: true, formatProvider: formatProvider, renderTemplate: renderTemplate); + return new SplunkJsonFormatter(renderTemplate, formatProvider); } /// diff --git a/src/Serilog.Sinks.Splunk/Sinks/Splunk/UdpSink.cs b/src/Serilog.Sinks.Splunk/Sinks/Splunk/UdpSink.cs index e8a64f0..30dde26 100644 --- a/src/Serilog.Sinks.Splunk/Sinks/Splunk/UdpSink.cs +++ b/src/Serilog.Sinks.Splunk/Sinks/Splunk/UdpSink.cs @@ -96,7 +96,7 @@ public UdpSink(IPAddress hostAddress, int port, IFormatProvider formatProvider = private static SplunkJsonFormatter CreateDefaultFormatter(IFormatProvider formatProvider, bool renderTemplate) { - return new SplunkJsonFormatter(renderMessage: true, formatProvider: formatProvider, renderTemplate: renderTemplate); + return new SplunkJsonFormatter(renderTemplate, formatProvider); } /// diff --git a/src/Serilog.Sinks.Splunk/SplunkLoggingConfigurationExtensions.cs b/src/Serilog.Sinks.Splunk/SplunkLoggingConfigurationExtensions.cs index 3db143b..b14dec1 100644 --- a/src/Serilog.Sinks.Splunk/SplunkLoggingConfigurationExtensions.cs +++ b/src/Serilog.Sinks.Splunk/SplunkLoggingConfigurationExtensions.cs @@ -51,6 +51,7 @@ public static class SplunkLoggingConfigurationExtensions /// If ture, the message template will be rendered /// The interval in seconds that the queue should be instpected for batching /// The size of the batch + /// The handler used to send HTTP requests /// public static LoggerConfiguration EventCollector( this LoggerSinkConfiguration configuration, @@ -66,7 +67,8 @@ public static LoggerConfiguration EventCollector( IFormatProvider formatProvider = null, bool renderTemplate = true, int batchIntervalInSeconds = 2, - int batchSizeLimit = 100) + int batchSizeLimit = 100, + HttpMessageHandler messageHandler = null) { if (configuration == null) throw new ArgumentNullException(nameof(configuration)); if (outputTemplate == null) throw new ArgumentNullException(nameof(outputTemplate)); @@ -82,7 +84,8 @@ public static LoggerConfiguration EventCollector( batchIntervalInSeconds, batchSizeLimit, formatProvider, - renderTemplate); + renderTemplate, + messageHandler); return configuration.Sink(eventCollectorSink, restrictedToMinimumLevel); } diff --git a/src/Serilog.Sinks.Splunk/project.json b/src/Serilog.Sinks.Splunk/project.json index 1fc7953..bd4fbdf 100644 --- a/src/Serilog.Sinks.Splunk/project.json +++ b/src/Serilog.Sinks.Splunk/project.json @@ -1,5 +1,5 @@ { - "version": "2.0.1", + "version": "2.1.0-*", "description": "The Splunk Sink for Serilog", "authors": [ "Matthew Erbs, Serilog Contributors" @@ -21,7 +21,7 @@ "xmlDoc": true }, "dependencies": { - "Serilog": "2.0.0" + "Serilog": "2.2.0" }, "frameworks": { "net4.5": { diff --git a/test/Serilog.Sinks.Splunk.Tests/Properties/launchSettings.json b/test/Serilog.Sinks.Splunk.Tests/Properties/launchSettings.json new file mode 100644 index 0000000..3ab0635 --- /dev/null +++ b/test/Serilog.Sinks.Splunk.Tests/Properties/launchSettings.json @@ -0,0 +1,11 @@ +{ + "profiles": { + "test": { + "commandName": "test" + }, + "test-dnxcore50": { + "commandName": "test", + "sdkVersion": "dnx-coreclr-win-x86.1.0.0-rc1-final" + } + } +} \ No newline at end of file diff --git a/test/Serilog.Sinks.Splunk.Tests/Serilog.Sinks.Splunk.Tests.xproj b/test/Serilog.Sinks.Splunk.Tests/Serilog.Sinks.Splunk.Tests.xproj new file mode 100644 index 0000000..e1971a9 --- /dev/null +++ b/test/Serilog.Sinks.Splunk.Tests/Serilog.Sinks.Splunk.Tests.xproj @@ -0,0 +1,18 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + 3c2d8e01-5580-426a-bdd9-ec59cd98e618 + Serilog.Sinks.Splunk.Tests + .\obj + .\bin\ + + + 2.0 + + + \ No newline at end of file diff --git a/test/Serilog.Sinks.Splunk.Tests/SplunkJsonFormatterTests.cs b/test/Serilog.Sinks.Splunk.Tests/SplunkJsonFormatterTests.cs new file mode 100644 index 0000000..6f0fcb7 --- /dev/null +++ b/test/Serilog.Sinks.Splunk.Tests/SplunkJsonFormatterTests.cs @@ -0,0 +1,85 @@ +using System; +using System.IO; +using Newtonsoft.Json.Linq; +using Xunit; +using Serilog.Sinks.Splunk.Tests.Support; + +namespace Serilog.Sinks.Splunk.Tests +{ + public class SplunkJsonFormatterTests + { + void AssertValidJson(Action act, + string source = "", + string sourceType= "", + string host= "", + string index= "") + { + StringWriter outputRendered = new StringWriter(), output = new StringWriter(); + var log = new LoggerConfiguration() + .WriteTo.Sink(new TextWriterSink(output, new SplunkJsonFormatter(false, null, source, sourceType, host, index))) + .WriteTo.Sink(new TextWriterSink(outputRendered, new SplunkJsonFormatter(true, null, source, sourceType, host, index))) + .CreateLogger(); + + act(log); + + // Unfortunately this will not detect all JSON formatting issues; better than nothing however. + JObject.Parse(output.ToString()); + JObject.Parse(outputRendered.ToString()); + } + + [Fact] + public void AnEmptyEventIsValidJson() + { + AssertValidJson(log => log.Information("No properties")); + } + + [Fact] + public void AMinimalEventIsValidJson() + { + AssertValidJson(log => log.Information("One {Property}", 42)); + } + + [Fact] + public void MultiplePropertiesAreDelimited() + { + AssertValidJson(log => log.Information("Property {First} and {Second}", "One", "Two")); + } + + [Fact] + public void ExceptionsAreFormattedToValidJson() + { + AssertValidJson(log => log.Information(new DivideByZeroException(), "With exception")); + } + + [Fact] + public void ExceptionAndPropertiesAreValidJson() + { + AssertValidJson(log => log.Information(new DivideByZeroException(), "With exception and {Property}", 42)); + } + + [Fact] + public void AMinimalEventWithSourceIsValidJson() + { + AssertValidJson(log => log.Information("One {Property}", 42), source: "A Test Source"); + } + + [Fact] + public void AMinimalEventWithSourceTypeIsValidJson() + { + AssertValidJson(log => log.Information("One {Property}", 42), sourceType: "A Test SourceType"); + } + + [Fact] + public void AMinimalEventWithHostIsValidJson() + { + AssertValidJson(log => log.Information("One {Property}", 42), host: "A Test Host"); + } + + [Fact] + public void AMinimalEventWithIndexIsValidJson() + { + AssertValidJson(log => log.Information("One {Property}", 42), host: "testindex"); + } + + } +} \ No newline at end of file diff --git a/test/Serilog.Sinks.Splunk.Tests/Support/TextWriterSink.cs b/test/Serilog.Sinks.Splunk.Tests/Support/TextWriterSink.cs new file mode 100644 index 0000000..96adf2d --- /dev/null +++ b/test/Serilog.Sinks.Splunk.Tests/Support/TextWriterSink.cs @@ -0,0 +1,24 @@ +using System.IO; +using Serilog.Core; +using Serilog.Events; +using Serilog.Formatting; + +namespace Serilog.Sinks.Splunk.Tests.Support +{ + public class TextWriterSink : ILogEventSink + { + readonly StringWriter _output; + readonly ITextFormatter _formatter; + + public TextWriterSink(StringWriter output, ITextFormatter formatter) + { + _output = output; + _formatter = formatter; + } + + public void Emit(LogEvent logEvent) + { + _formatter.Format(logEvent, _output); + } + } +} \ No newline at end of file diff --git a/test/Serilog.Sinks.Splunk.Tests/project.json b/test/Serilog.Sinks.Splunk.Tests/project.json new file mode 100644 index 0000000..1d6c4dc --- /dev/null +++ b/test/Serilog.Sinks.Splunk.Tests/project.json @@ -0,0 +1,24 @@ +{ + "testRunner": "xunit", + "dependencies": { + "Serilog.Sinks.Splunk": { "target": "project" }, + "xunit": "2.1.0", + "dotnet-test-xunit": "1.0.0-rc2-build10025", + "Newtonsoft.Json": "8.0.3" + }, + "frameworks": { + "net4.5.2": { }, + "netcoreapp1.0": { + "dependencies": { + "Microsoft.NETCore.App": { + "type": "platform", + "version": "1.0.0" + } + }, + "imports": [ + "dnxcore50", + "portable-net45+win8" + ] + } + } +}