diff --git a/Bench.ps1 b/Bench.ps1 index 3dd58ee..c931cda 100644 --- a/Bench.ps1 +++ b/Bench.ps1 @@ -9,7 +9,7 @@ foreach ($test in ls test/*.PerformanceTests) { echo "bench: Benchmarking project in $test" - & dotnet test -c Release --framework net4.5.2 + & dotnet test -c Release --framework net46 if($LASTEXITCODE -ne 0) { exit 3 } Pop-Location diff --git a/Build.ps1 b/Build.ps1 index 7c5a85f..9b2d878 100644 --- a/Build.ps1 +++ b/Build.ps1 @@ -18,9 +18,14 @@ echo "build: Version suffix is $suffix" foreach ($src in ls src/*) { Push-Location $src - echo "build: Packaging project in $src" - - & dotnet pack -c Release -o ..\..\artifacts --version-suffix=$suffix + echo "build: Packaging project in $src" + + if ($suffix) { + & dotnet pack -c Release -o ..\..\artifacts --version-suffix=$suffix --include-source + } else { + & dotnet pack -c Release -o ..\..\artifacts --include-source + } + if($LASTEXITCODE -ne 0) { exit 1 } Pop-Location @@ -29,7 +34,7 @@ foreach ($src in ls src/*) { foreach ($test in ls test/*.PerformanceTests) { Push-Location $test - echo "build: Building performance test project in $test" + echo "build: Building performance test project in $test" & dotnet build -c Release if($LASTEXITCODE -ne 0) { exit 2 } @@ -40,7 +45,7 @@ foreach ($test in ls test/*.PerformanceTests) { foreach ($test in ls test/*.Tests) { Push-Location $test - echo "build: Testing project in $test" + echo "build: Testing project in $test" & dotnet test -c Release if($LASTEXITCODE -ne 0) { exit 3 } diff --git a/README.md b/README.md index b8b1d31..12b9210 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # Serilog.Sinks.Async [![Build status](https://ci.appveyor.com/api/projects/status/gvk0wl7aows14spn?svg=true)](https://ci.appveyor.com/project/serilog/serilog-sinks-async) [![NuGet](https://img.shields.io/nuget/v/Serilog.Sinks.Async.svg)](https://www.nuget.org/packages/Serilog.Sinks.Async) [![Join the chat at https://gitter.im/serilog/serilog](https://img.shields.io/gitter/room/serilog/serilog.svg)](https://gitter.im/serilog/serilog) -An asynchronous wrapper for other [Serilog](https://serilog.net) sinks. Use this sink to reduce the overhead of logging calls by delegating work to a background thread. This is especially suited to non-batching sinks like the [File](https://github.com/serilog/serilog-sinks-file) and [RollingFile](https://github.com/serilog-serilog-sinks-rollingfile) sinks that may be affected by I/O bottlenecks. +An asynchronous wrapper for other [Serilog](https://serilog.net) sinks. Use this sink to reduce the overhead of logging calls by delegating work to a background thread. This is especially suited to non-batching sinks like the [File](https://github.com/serilog/serilog-sinks-file) and [RollingFile](https://github.com/serilog/serilog-sinks-rollingfile) sinks that may be affected by I/O bottlenecks. -**Note:** many of the network-based sinks (_CouchDB_, _Elasticsearch_, _MongoDB_, _Seq_, _Splunk_...) already perform asychronous batching natively and do not benefit from this wrapper. +**Note:** many of the network-based sinks (_CouchDB_, _Elasticsearch_, _MongoDB_, _Seq_, _Splunk_...) already perform asynchronous batching natively and do not benefit from this wrapper. ### Getting started @@ -12,36 +12,94 @@ Install from [NuGet](https://nuget.org/packages/serilog.sinks.async): Install-Package Serilog.Sinks.Async ``` -Assuming you have already installed the target sink, such as the rolling file sink, move the wrapped sink's configuration within a `WriteTo.Async()` statement: +Assuming you have already installed the target sink, such as the file sink, move the wrapped sink's configuration within a `WriteTo.Async()` statement: ```csharp Log.Logger = new LoggerConfiguration() - .WriteTo.Async(a => a.RollingFile("logs/myapp-{Date}.txt")) + .WriteTo.Async(a => a.File("logs/myapp.log")) // Other logger configuration .CreateLogger() - + Log.Information("This will be written to disk on the worker thread"); -// At application shutdown +// At application shutdown (results in monitors getting StopMonitoring calls) Log.CloseAndFlush(); ``` -The wrapped sink (`RollingFile` in this case) will be invoked on a worker thread while your application's thread gets on with more important stuff. +The wrapped sink (`File` in this case) will be invoked on a worker thread while your application's thread gets on with more important stuff. Because the memory buffer may contain events that have not yet been written to the target sink, it is important to call `Log.CloseAndFlush()` or `Logger.Dispose()` when the application exits. -### Buffering +### Buffering & Dropping + +The default memory buffer feeding the worker thread is capped to 10,000 items, after which arriving events will be dropped. To increase or decrease this limit, specify it when configuring the async sink. One can determine whether events have been dropped via `Serilog.Async.IAsyncLogEventSinkInspector.DroppedMessagesCount` (see Sink State Inspection interface below). + +```csharp +// Reduce the buffer to 500 events +.WriteTo.Async(a => a.File("logs/myapp.log"), bufferSize: 500) +``` + +### Health Monitoring via the Monitor and Inspector interfaces -The default memory buffer feeding the worker thread is capped to 10,000 items, after which arriving events will be dropped. To increase or decrease this limit, specify it when configuring the async sink. +The `Async` wrapper is primarily intended to allow one to achieve minimal logging latency at all times, even when writing to sinks that may momentarily block during the course of their processing (e.g., a `File` Sink might block for a low number of ms while flushing). The dropping behavior is an important failsafe; it avoids having an unbounded buffering behaviour should logging throughput overwhelm the sink, or the sink ingestion throughput degrade. + +In practice, this configuration (assuming one provisions an adequate `bufferSize`) achieves an efficient and resilient logging configuration that can safely handle load without impacting processing throughput. The risk is of course that events get be dropped if the buffer threshold gets breached. The inspection interface, `IAsyncLogEventSinkInspector` (obtained by providing an `IAsyncLogEventSinkMonitor` when configuring the `Async` Sink), enables a health monitoring mechanism to actively validate that the buffer allocation is not being exceeded in practice. ```csharp - // Reduce the buffer to 500 events - .WriteTo.Async(a => a.RollingFile("logs/myapp-{Date}.txt"), 500) +// Example check: log message to an out of band alarm channel if logging is showing signs of getting overwhelmed +void ExecuteAsyncBufferCheck(IAsyncLogEventSinkInspector inspector) +{ + var usagePct = inspector.Count * 100 / inspector.BoundedCapacity; + if (usagePct > 50) SelfLog.WriteLine("Log buffer exceeded {0:p0} usage (limit: {1})", usagePct, inspector.BoundedCapacity); +} + +class MonitorConfiguration : IAsyncLogEventSinkMonitor +{ + public void StartMonitoring(IAsyncLogEventSinkInspector inspector) => + HealthMonitor.AddPeriodicCheck(() => ExecuteAsyncBufferCheck(inspector)); + + public void StopMonitoring(IAsyncLogEventSinkInspector inspector) + { /* reverse of StartMonitoring */ } +} + +// Provide monitor so we can wire the health check to the inspector +var monitor = new MonitorConfiguration(); +// Use default config (drop events if >10,000 backlog) +.WriteTo.Async(a => a.File("logs/myapp.log"), monitor: monitor) ... +``` + +### Blocking + +Warning: For the same reason one typically does not want exceptions from logging to leak into the execution path, one typically does not want a logger to be able to have the side-efect of actually interrupting application processing until the log propagation has been unblocked. + +When the buffer size limit is reached, the default behavior is to drop any further attempted writes until the queue abates, reporting each such failure to the `Serilog.Debugging.SelfLog`. To replace this with a blocking behaviour, set `blockWhenFull` to `true`. + +```csharp +// Wait for any queued event to be accepted by the `File` log before allowing the calling thread to resume its +// application work after a logging call when there are 10,000 LogEvents awaiting ingestion by the pipeline +.WriteTo.Async(a => a.File("logs/myapp.log"), blockWhenFull: true) ``` ### XML `` and JSON configuration -XML and JSON configuration support has not yet been added for this wrapper. +Using [Serilog.Settings.Configuration](https://github.com/serilog/serilog-settings-configuration) JSON: + +```json +{ + "Serilog": { + "WriteTo": [{ + "Name": "Async", + "Args": { + "configure": [{ + "Name": "Console" + }] + } + }] + } +} +``` + +XML configuration support has not yet been added for this wrapper. ### About this sink diff --git a/appveyor.yml b/appveyor.yml index 19d0d28..0e93876 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,13 +1,7 @@ version: '{build}' skip_tags: true -image: Visual Studio 2015 +image: Visual Studio 2017 configuration: Release -install: - - ps: mkdir -Force ".\build\" | Out-Null - - ps: Invoke-WebRequest "https://raw.githubusercontent.com/dotnet/cli/rel/1.0.0-preview2/scripts/obtain/dotnet-install.ps1" -OutFile ".\build\installcli.ps1" - - ps: $env:DOTNET_INSTALL_DIR = "$pwd\.dotnetcli" - - ps: '& .\build\installcli.ps1 -InstallDir "$env:DOTNET_INSTALL_DIR" -NoPath -Version 1.0.0-preview2-003121' - - ps: $env:Path = "$env:DOTNET_INSTALL_DIR;$env:Path" build_script: - ps: ./Build.ps1 test: off @@ -16,7 +10,7 @@ artifacts: deploy: - provider: NuGet api_key: - secure: nvZ/z+pMS91b3kG4DgfES5AcmwwGoBYQxr9kp4XiJHj25SAlgdIxFx++1N0lFH2x + secure: bd9z4P73oltOXudAjPehwp9iDKsPtC+HbgshOrSgoyQKr5xVK+bxJQngrDJkHdY8 skip_symbols: true on: branch: /^(master|dev)$/ diff --git a/global.json b/global.json deleted file mode 100644 index a2b2a41..0000000 --- a/global.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "projects": [ "src", "test" ], - "sdk": { - "version": "1.0.0-preview2-003121" - } -} diff --git a/serilog-sinks-async.sln b/serilog-sinks-async.sln index 29c66c6..53c3f7e 100644 --- a/serilog-sinks-async.sln +++ b/serilog-sinks-async.sln @@ -1,7 +1,6 @@ - Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 14 -VisualStudioVersion = 14.0.25420.1 +# Visual Studio 15 +VisualStudioVersion = 15.0.26430.14 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{76C9F320-3DBC-4613-83AA-3CCD0D9012D9}" EndProject @@ -12,16 +11,15 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "global", "global", "{154C7C appveyor.yml = appveyor.yml Bench.ps1 = Bench.ps1 Build.ps1 = Build.ps1 - global.json = global.json LICENSE = LICENSE README.md = README.md EndProjectSection EndProject -Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Serilog.Sinks.Async", "src\Serilog.Sinks.Async\Serilog.Sinks.Async.xproj", "{803CD13A-D54B-4CEC-A55F-E22AE3D93B3C}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Serilog.Sinks.Async", "src\Serilog.Sinks.Async\Serilog.Sinks.Async.csproj", "{003F6AB2-79F8-4A63-B501-5C564B4A4655}" EndProject -Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Serilog.Sinks.Async.Tests", "test\Serilog.Sinks.Async.Tests\Serilog.Sinks.Async.Tests.xproj", "{3C2D8E01-5580-426A-BDD9-EC59CD98E618}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Serilog.Sinks.Async.PerformanceTests", "test\Serilog.Sinks.Async.PerformanceTests\Serilog.Sinks.Async.PerformanceTests.csproj", "{19E64565-3BE1-43FE-9E8B-7800C7061877}" EndProject -Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Serilog.Sinks.Async.PerformanceTests", "test\Serilog.Sinks.Async.PerformanceTests\Serilog.Sinks.Async.PerformanceTests.xproj", "{D7A37F73-BBA3-4DAE-9648-1A753A86F968}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Serilog.Sinks.Async.Tests", "test\Serilog.Sinks.Async.Tests\Serilog.Sinks.Async.Tests.csproj", "{E8AE4DDD-6C28-4239-A752-075309A86D41}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -29,25 +27,25 @@ Global Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {803CD13A-D54B-4CEC-A55F-E22AE3D93B3C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {803CD13A-D54B-4CEC-A55F-E22AE3D93B3C}.Debug|Any CPU.Build.0 = Debug|Any CPU - {803CD13A-D54B-4CEC-A55F-E22AE3D93B3C}.Release|Any CPU.ActiveCfg = Release|Any CPU - {803CD13A-D54B-4CEC-A55F-E22AE3D93B3C}.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 - {D7A37F73-BBA3-4DAE-9648-1A753A86F968}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D7A37F73-BBA3-4DAE-9648-1A753A86F968}.Debug|Any CPU.Build.0 = Debug|Any CPU - {D7A37F73-BBA3-4DAE-9648-1A753A86F968}.Release|Any CPU.ActiveCfg = Release|Any CPU - {D7A37F73-BBA3-4DAE-9648-1A753A86F968}.Release|Any CPU.Build.0 = Release|Any CPU + {003F6AB2-79F8-4A63-B501-5C564B4A4655}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {003F6AB2-79F8-4A63-B501-5C564B4A4655}.Debug|Any CPU.Build.0 = Debug|Any CPU + {003F6AB2-79F8-4A63-B501-5C564B4A4655}.Release|Any CPU.ActiveCfg = Release|Any CPU + {003F6AB2-79F8-4A63-B501-5C564B4A4655}.Release|Any CPU.Build.0 = Release|Any CPU + {19E64565-3BE1-43FE-9E8B-7800C7061877}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {19E64565-3BE1-43FE-9E8B-7800C7061877}.Debug|Any CPU.Build.0 = Debug|Any CPU + {19E64565-3BE1-43FE-9E8B-7800C7061877}.Release|Any CPU.ActiveCfg = Release|Any CPU + {19E64565-3BE1-43FE-9E8B-7800C7061877}.Release|Any CPU.Build.0 = Release|Any CPU + {E8AE4DDD-6C28-4239-A752-075309A86D41}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E8AE4DDD-6C28-4239-A752-075309A86D41}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E8AE4DDD-6C28-4239-A752-075309A86D41}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E8AE4DDD-6C28-4239-A752-075309A86D41}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution - {803CD13A-D54B-4CEC-A55F-E22AE3D93B3C} = {76C9F320-3DBC-4613-83AA-3CCD0D9012D9} - {3C2D8E01-5580-426A-BDD9-EC59CD98E618} = {C36755AA-CED6-482B-B7B1-AE483BC3D273} - {D7A37F73-BBA3-4DAE-9648-1A753A86F968} = {C36755AA-CED6-482B-B7B1-AE483BC3D273} + {003F6AB2-79F8-4A63-B501-5C564B4A4655} = {76C9F320-3DBC-4613-83AA-3CCD0D9012D9} + {19E64565-3BE1-43FE-9E8B-7800C7061877} = {C36755AA-CED6-482B-B7B1-AE483BC3D273} + {E8AE4DDD-6C28-4239-A752-075309A86D41} = {C36755AA-CED6-482B-B7B1-AE483BC3D273} EndGlobalSection EndGlobal diff --git a/src/Serilog.Sinks.Async/LoggerConfigurationAsyncExtensions.cs b/src/Serilog.Sinks.Async/LoggerConfigurationAsyncExtensions.cs index a1be59f..aa3cdf4 100644 --- a/src/Serilog.Sinks.Async/LoggerConfigurationAsyncExtensions.cs +++ b/src/Serilog.Sinks.Async/LoggerConfigurationAsyncExtensions.cs @@ -1,6 +1,6 @@ using System; +using System.ComponentModel; using Serilog.Configuration; -using Serilog.Events; using Serilog.Sinks.Async; namespace Serilog @@ -19,19 +19,59 @@ public static class LoggerConfigurationAsyncExtensions /// the thread is unable to process events quickly enough and the queue is filled, subsequent events will be /// dropped until room is made in the queue. /// A allowing configuration to continue. + [EditorBrowsable(EditorBrowsableState.Never)] public static LoggerConfiguration Async( this LoggerSinkConfiguration loggerSinkConfiguration, Action configure, - int bufferSize = 10000) + int bufferSize) { - var sublogger = new LoggerConfiguration(); - sublogger.MinimumLevel.Is(LevelAlias.Minimum); - - configure(sublogger.WriteTo); + return loggerSinkConfiguration.Async(configure, bufferSize, false); + } - var wrapper = new BackgroundWorkerSink(sublogger.CreateLogger(), bufferSize); + /// + /// Configure a sink to be invoked asynchronously, on a background worker thread. + /// + /// The being configured. + /// An action that configures the wrapped sink. + /// The size of the concurrent queue used to feed the background worker thread. If + /// the thread is unable to process events quickly enough and the queue is filled, depending on + /// the queue will block or subsequent events will be dropped until + /// room is made in the queue. + /// Block when the queue is full, instead of dropping events. + /// A allowing configuration to continue. + public static LoggerConfiguration Async( + this LoggerSinkConfiguration loggerSinkConfiguration, + Action configure, + int bufferSize = 10000, + bool blockWhenFull = false) + { + return loggerSinkConfiguration.Async(configure, null, bufferSize, blockWhenFull); + } - return loggerSinkConfiguration.Sink(wrapper); + /// + /// Configure a sink to be invoked asynchronously, on a background worker thread. + /// Accepts a reference to a that will be supplied the internal state interface for health monitoring purposes. + /// + /// The being configured. + /// An action that configures the wrapped sink. + /// The size of the concurrent queue used to feed the background worker thread. If + /// the thread is unable to process events quickly enough and the queue is filled, depending on + /// the queue will block or subsequent events will be dropped until + /// room is made in the queue. + /// Block when the queue is full, instead of dropping events. + /// Monitor to supply buffer information to. + /// A allowing configuration to continue. + public static LoggerConfiguration Async( + this LoggerSinkConfiguration loggerSinkConfiguration, + Action configure, + IAsyncLogEventSinkMonitor monitor, + int bufferSize = 10000, + bool blockWhenFull = false) + { + return LoggerSinkConfiguration.Wrap( + loggerSinkConfiguration, + wrappedSink => new BackgroundWorkerSink(wrappedSink, bufferSize, blockWhenFull, monitor), + configure); } } -} +} \ No newline at end of file diff --git a/src/Serilog.Sinks.Async/Properties/AssemblyInfo.cs b/src/Serilog.Sinks.Async/Properties/AssemblyInfo.cs index ab2df5b..c93ed16 100644 --- a/src/Serilog.Sinks.Async/Properties/AssemblyInfo.cs +++ b/src/Serilog.Sinks.Async/Properties/AssemblyInfo.cs @@ -2,7 +2,6 @@ using System.Reflection; using System.Runtime.CompilerServices; -[assembly: AssemblyVersion("1.0.0.0")] [assembly: CLSCompliant(true)] [assembly: InternalsVisibleTo("Serilog.Sinks.Async.Tests, PublicKey=" + "0024000004800000940000000602000000240000525341310004000001000100fb8d13fd344a1c" + diff --git a/src/Serilog.Sinks.Async/Serilog.Sinks.Async.csproj b/src/Serilog.Sinks.Async/Serilog.Sinks.Async.csproj new file mode 100644 index 0000000..ace381e --- /dev/null +++ b/src/Serilog.Sinks.Async/Serilog.Sinks.Async.csproj @@ -0,0 +1,38 @@ + + + + Asynchronous sink wrapper for Serilog. + 1.0.0 + 1.2.0 + Jezz Santos;Serilog Contributors + net45;netstandard1.1 + true + true + Serilog.Sinks.Async + ../../assets/Serilog.snk + true + true + Serilog.Sinks.Async + serilog;async + http://serilog.net/images/serilog-sink-nuget.png + https://serilog.net + http://www.apache.org/licenses/LICENSE-2.0 + True + + true + + + + + + + + + + + + + + + + diff --git a/src/Serilog.Sinks.Async/Serilog.Sinks.Async.xproj b/src/Serilog.Sinks.Async/Serilog.Sinks.Async.xproj deleted file mode 100644 index 865914c..0000000 --- a/src/Serilog.Sinks.Async/Serilog.Sinks.Async.xproj +++ /dev/null @@ -1,17 +0,0 @@ - - - - 14.0 - $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) - - - - 803cd13a-d54b-4cec-a55f-e22ae3d93b3c - Serilog - .\obj - .\bin\ - - 2.0 - - - diff --git a/src/Serilog.Sinks.Async/Sinks/Async/BackgroundWorkerSink.cs b/src/Serilog.Sinks.Async/Sinks/Async/BackgroundWorkerSink.cs index 4d150c9..b77f32b 100644 --- a/src/Serilog.Sinks.Async/Sinks/Async/BackgroundWorkerSink.cs +++ b/src/Serilog.Sinks.Async/Sinks/Async/BackgroundWorkerSink.cs @@ -1,66 +1,81 @@ using System; using System.Collections.Concurrent; using System.Threading; +using System.Threading.Tasks; using Serilog.Core; using Serilog.Debugging; using Serilog.Events; -using System.Threading.Tasks; namespace Serilog.Sinks.Async { - sealed class BackgroundWorkerSink : ILogEventSink, IDisposable + sealed class BackgroundWorkerSink : ILogEventSink, IAsyncLogEventSinkInspector, IDisposable { - readonly Logger _pipeline; - readonly int _bufferCapacity; - volatile bool _disposed; - readonly CancellationTokenSource _cancel = new CancellationTokenSource(); + readonly ILogEventSink _pipeline; + readonly bool _blockWhenFull; readonly BlockingCollection _queue; readonly Task _worker; + readonly IAsyncLogEventSinkMonitor _monitor; - public BackgroundWorkerSink(Logger pipeline, int bufferCapacity) + long _droppedMessages; + + public BackgroundWorkerSink(ILogEventSink pipeline, int bufferCapacity, bool blockWhenFull, IAsyncLogEventSinkMonitor monitor = null) { - if (pipeline == null) throw new ArgumentNullException(nameof(pipeline)); if (bufferCapacity <= 0) throw new ArgumentOutOfRangeException(nameof(bufferCapacity)); - _pipeline = pipeline; - _bufferCapacity = bufferCapacity; - _queue = new BlockingCollection(_bufferCapacity); + _pipeline = pipeline ?? throw new ArgumentNullException(nameof(pipeline)); + _blockWhenFull = blockWhenFull; + _queue = new BlockingCollection(bufferCapacity); _worker = Task.Factory.StartNew(Pump, CancellationToken.None, TaskCreationOptions.LongRunning | TaskCreationOptions.DenyChildAttach, TaskScheduler.Default); + _monitor = monitor; + monitor?.StartMonitoring(this); } public void Emit(LogEvent logEvent) { - // The disposed check is racy, but only ensures we don't prevent flush from - // completing by pushing more events. - if (!_disposed && !_queue.TryAdd(logEvent)) - SelfLog.WriteLine("{0} unable to enqueue, capacity {1}", typeof(BackgroundWorkerSink), _bufferCapacity); + if (_queue.IsAddingCompleted) + return; + + try + { + if (_blockWhenFull) + { + _queue.Add(logEvent); + } + else + { + if (!_queue.TryAdd(logEvent)) + { + Interlocked.Increment(ref _droppedMessages); + SelfLog.WriteLine("{0} unable to enqueue, capacity {1}", typeof(BackgroundWorkerSink), _queue.BoundedCapacity); + } + } + } + catch (InvalidOperationException) + { + // Thrown in the event of a race condition when we try to add another event after + // CompleteAdding has been called + } } public void Dispose() { - _disposed = true; - _cancel.Cancel(); - _worker.Wait(); - _pipeline.Dispose(); - // _cancel not disposed, because it will make _cancel.Cancel() non-idempotent + // Prevent any more events from being added + _queue.CompleteAdding(); + + // Allow queued events to be flushed + _worker.Wait(); + + (_pipeline as IDisposable)?.Dispose(); + + _monitor?.StopMonitoring(this); } void Pump() { try { - try - { - while (true) - { - var next = _queue.Take(_cancel.Token); - _pipeline.Write(next); - } - } - catch (OperationCanceledException) + foreach (var next in _queue.GetConsumingEnumerable()) { - LogEvent next; - while (_queue.TryTake(out next)) - _pipeline.Write(next); + _pipeline.Emit(next); } } catch (Exception ex) @@ -68,5 +83,11 @@ void Pump() SelfLog.WriteLine("{0} fatal error in worker thread: {1}", typeof(BackgroundWorkerSink), ex); } } + + int IAsyncLogEventSinkInspector.BufferSize => _queue.BoundedCapacity; + + int IAsyncLogEventSinkInspector.Count => _queue.Count; + + long IAsyncLogEventSinkInspector.DroppedMessagesCount => _droppedMessages; } -} +} \ No newline at end of file diff --git a/src/Serilog.Sinks.Async/Sinks/Async/IAsyncLogEventSinkInspector.cs b/src/Serilog.Sinks.Async/Sinks/Async/IAsyncLogEventSinkInspector.cs new file mode 100644 index 0000000..0edccdc --- /dev/null +++ b/src/Serilog.Sinks.Async/Sinks/Async/IAsyncLogEventSinkInspector.cs @@ -0,0 +1,25 @@ +namespace Serilog.Sinks.Async +{ + /// + /// Provides a way to inspect the state of Async wrapper's ingestion queue. + /// + public interface IAsyncLogEventSinkInspector + { + /// + /// Configured maximum number of items permitted to be held in the buffer awaiting ingestion. + /// + /// The Sink has been disposed. + int BufferSize { get; } + + /// + /// Current moment-in-time Count of items currently awaiting ingestion. + /// + /// The Sink has been disposed. + int Count { get; } + + /// + /// Accumulated number of messages dropped due to breaches of limit. + /// + long DroppedMessagesCount { get; } + } +} \ No newline at end of file diff --git a/src/Serilog.Sinks.Async/Sinks/Async/IAsyncLogEventSinkMonitor.cs b/src/Serilog.Sinks.Async/Sinks/Async/IAsyncLogEventSinkMonitor.cs new file mode 100644 index 0000000..26a43d1 --- /dev/null +++ b/src/Serilog.Sinks.Async/Sinks/Async/IAsyncLogEventSinkMonitor.cs @@ -0,0 +1,20 @@ +namespace Serilog.Sinks.Async +{ + /// + /// Defines a mechanism for the Async Sink to afford Health Checks a buffer metadata inspection mechanism. + /// + public interface IAsyncLogEventSinkMonitor + { + /// + /// Invoked by Sink to supply the inspector to the monitor. + /// + /// The Async Sink's inspector. + void StartMonitoring(IAsyncLogEventSinkInspector inspector); + + /// + /// Invoked by Sink to indicate that it is being Disposed. + /// + /// The Async Sink's inspector. + void StopMonitoring(IAsyncLogEventSinkInspector inspector); + } +} \ No newline at end of file diff --git a/src/Serilog.Sinks.Async/project.json b/src/Serilog.Sinks.Async/project.json deleted file mode 100644 index 37fcd51..0000000 --- a/src/Serilog.Sinks.Async/project.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "version": "1.1.0-*", - - "description": "Asynchronous sink wrapper for Serilog.", - "authors": [ "Jezz Santos", "Serilog Contributors" ], - - "packOptions": { - "tags": [ "serilog", "async" ], - "projectUrl": "https://serilog.net", - "licenseUrl": "http://www.apache.org/licenses/LICENSE-2.0", - "iconUrl": "http://serilog.net/images/serilog-sink-nuget.png" - }, - - "buildOptions": { - "keyFile": "../../assets/Serilog.snk", - "xmlDoc": true, - "warningsAsErrors": true - }, - - "dependencies": { - "Serilog": "2.1" - }, - - "frameworks": { - "net4.5": { - }, - "netstandard1.1": { - "dependencies": { - "System.Collections.Concurrent": "4.0.12" - } - } - } -} diff --git a/test/Serilog.Sinks.Async.PerformanceTests/Serilog.Sinks.Async.PerformanceTests.csproj b/test/Serilog.Sinks.Async.PerformanceTests/Serilog.Sinks.Async.PerformanceTests.csproj new file mode 100644 index 0000000..54ec36f --- /dev/null +++ b/test/Serilog.Sinks.Async.PerformanceTests/Serilog.Sinks.Async.PerformanceTests.csproj @@ -0,0 +1,31 @@ + + + + net46;netcoreapp1.1 + Serilog.Sinks.Async.PerformanceTests + ../../assets/Serilog.snk + true + true + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/Serilog.Sinks.Async.PerformanceTests/Serilog.Sinks.Async.PerformanceTests.xproj b/test/Serilog.Sinks.Async.PerformanceTests/Serilog.Sinks.Async.PerformanceTests.xproj deleted file mode 100644 index f4df042..0000000 --- a/test/Serilog.Sinks.Async.PerformanceTests/Serilog.Sinks.Async.PerformanceTests.xproj +++ /dev/null @@ -1,18 +0,0 @@ - - - - 14.0 - $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) - - - - d7a37f73-bba3-4dae-9648-1a753a86f968 - Serilog.PerformanceTests - .\obj - .\bin\ - - - 2.0 - - - diff --git a/test/Serilog.Sinks.Async.PerformanceTests/project.json b/test/Serilog.Sinks.Async.PerformanceTests/project.json deleted file mode 100644 index 5e76d51..0000000 --- a/test/Serilog.Sinks.Async.PerformanceTests/project.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "testRunner": "xunit", - - "dependencies": { - "Serilog.Sinks.Async": { "target": "project" }, - "Serilog.Sinks.File": "2.2.0", - "xunit": "2.1.0", - "dotnet-test-xunit": "1.0.0-rc2-build10025", - "BenchmarkDotNet": "0.9.7-beta" - }, - "buildOptions": { - "keyFile": "../../assets/Serilog.snk" - }, - "frameworks": { - "net4.5.2": { - }, - "netcoreapp1.0": { - "dependencies": { - "Microsoft.NETCore.App": { - "type": "platform", - "version": "1.0.0" - } - }, - "imports": [ - "dnxcore50", - "portable-net45+win8" - ] - } - } -} diff --git a/test/Serilog.Sinks.Async.PerformanceTests/xunit.runner.json b/test/Serilog.Sinks.Async.PerformanceTests/xunit.runner.json new file mode 100644 index 0000000..34b2fe2 --- /dev/null +++ b/test/Serilog.Sinks.Async.PerformanceTests/xunit.runner.json @@ -0,0 +1,3 @@ +{ + "shadowCopy": false +} \ No newline at end of file diff --git a/test/Serilog.Sinks.Async.Tests/BackgroundWorkerSinkIntegrationSpec.cs b/test/Serilog.Sinks.Async.Tests/BackgroundWorkerSinkIntegrationSpec.cs index 7e664d4..14c0441 100644 --- a/test/Serilog.Sinks.Async.Tests/BackgroundWorkerSinkIntegrationSpec.cs +++ b/test/Serilog.Sinks.Async.Tests/BackgroundWorkerSinkIntegrationSpec.cs @@ -130,7 +130,7 @@ public void WhenAuditSingle_ThenQueued() var result = RetrieveEvents(_memorySink, 1); - Assert.Equal(1, result.Count); + Assert.Single(result); } [Fact] diff --git a/test/Serilog.Sinks.Async.Tests/BackgroundWorkerSinkSpec.cs b/test/Serilog.Sinks.Async.Tests/BackgroundWorkerSinkSpec.cs index 119eccc..5b5876d 100644 --- a/test/Serilog.Sinks.Async.Tests/BackgroundWorkerSinkSpec.cs +++ b/test/Serilog.Sinks.Async.Tests/BackgroundWorkerSinkSpec.cs @@ -1,83 +1,238 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; +using System.Threading; using System.Threading.Tasks; using Serilog.Core; using Serilog.Events; using Serilog.Parsing; -using Serilog.Sinks.Async.Tests; using Serilog.Sinks.Async.Tests.Support; using Xunit; namespace Serilog.Sinks.Async.Tests { - public class BackgroundWorkerSinkSpec : IDisposable + public class BackgroundWorkerSinkSpec { + readonly Logger _logger; readonly MemorySink _innerSink; - readonly BackgroundWorkerSink _sink; public BackgroundWorkerSinkSpec() { _innerSink = new MemorySink(); - var logger = new LoggerConfiguration().WriteTo.Sink(_innerSink).CreateLogger(); - _sink = new BackgroundWorkerSink(logger, 10000); - } - - public void Dispose() - { - _sink.Dispose(); + _logger = new LoggerConfiguration().WriteTo.Sink(_innerSink).CreateLogger(); } [Fact] public void WhenCtorWithNullSink_ThenThrows() { - Assert.Throws(() => new BackgroundWorkerSink(null, 10000)); + Assert.Throws(() => new BackgroundWorkerSink(null, 10000, false, null)); } [Fact] public async Task WhenEmitSingle_ThenRelaysToInnerSink() { - var logEvent = CreateEvent(); - _sink.Emit(logEvent); + using (var sink = this.CreateSinkWithDefaultOptions()) + { + var logEvent = CreateEvent(); - await Task.Delay(TimeSpan.FromSeconds(3)); + sink.Emit(logEvent); - Assert.Equal(1, _innerSink.Events.Count); + await Task.Delay(TimeSpan.FromSeconds(3)); + + Assert.Single(_innerSink.Events); + } } [Fact] public async Task WhenInnerEmitThrows_ThenContinuesRelaysToInnerSink() { - _innerSink.ThrowAfterCollecting = true; - - var events = new List + using (var sink = this.CreateSinkWithDefaultOptions()) { - CreateEvent(), - CreateEvent(), - CreateEvent() - }; - events.ForEach(e => _sink.Emit(e)); + _innerSink.ThrowAfterCollecting = true; + + var events = new List + { + CreateEvent(), + CreateEvent(), + CreateEvent() + }; + events.ForEach(e => sink.Emit(e)); - await Task.Delay(TimeSpan.FromSeconds(3)); + await Task.Delay(TimeSpan.FromSeconds(3)); - Assert.Equal(3, _innerSink.Events.Count); + Assert.Equal(3, _innerSink.Events.Count); + } } [Fact] public async Task WhenEmitMultipleTimes_ThenRelaysToInnerSink() { - var events = new List + using (var sink = this.CreateSinkWithDefaultOptions()) { - CreateEvent(), - CreateEvent(), - CreateEvent() - }; + var events = new List + { + CreateEvent(), + CreateEvent(), + CreateEvent() + }; + events.ForEach(e => { sink.Emit(e); }); + + await Task.Delay(TimeSpan.FromSeconds(3)); + + Assert.Equal(3, _innerSink.Events.Count); + } + } - events.ForEach(e => { _sink.Emit(e); }); + [Fact] + public async Task GivenDefaultConfig_WhenRequestsExceedCapacity_DoesNotBlock() + { + var batchTiming = Stopwatch.StartNew(); + using (var sink = new BackgroundWorkerSink(_logger, 1, blockWhenFull: false /*default*/)) + { + // Cause a delay when emitting to the inner sink, allowing us to easily fill the queue to capacity + // while the first event is being propagated + var acceptInterval = TimeSpan.FromMilliseconds(500); + _innerSink.DelayEmit = acceptInterval; + var tenSecondsWorth = 10_000 / acceptInterval.TotalMilliseconds + 1; + for (int i = 0; i < tenSecondsWorth; i++) + { + var emissionTiming = Stopwatch.StartNew(); + sink.Emit(CreateEvent()); + emissionTiming.Stop(); + + // Should not block the caller when the queue is full + Assert.InRange(emissionTiming.ElapsedMilliseconds, 0, 200); + } + + // Allow at least one to propagate + await Task.Delay(TimeSpan.FromSeconds(1)).ConfigureAwait(false); + Assert.NotEqual(0, ((IAsyncLogEventSinkInspector)sink).DroppedMessagesCount); + } + // Sanity check the overall timing + batchTiming.Stop(); + // Need to add a significant fudge factor as AppVeyor build can result in `await` taking quite some time + Assert.InRange(batchTiming.ElapsedMilliseconds, 950, 2050); + } - await Task.Delay(TimeSpan.FromSeconds(3)); + [Fact] + public async Task GivenDefaultConfig_WhenRequestsExceedCapacity_ThenDropsEventsAndRecovers() + { + using (var sink = new BackgroundWorkerSink(_logger, 1, blockWhenFull: false /*default*/)) + { + var acceptInterval = TimeSpan.FromMilliseconds(200); + _innerSink.DelayEmit = acceptInterval; + + for (int i = 0; i < 2; i++) + { + sink.Emit(CreateEvent()); + sink.Emit(CreateEvent()); + await Task.Delay(acceptInterval); + sink.Emit(CreateEvent()); + } + // Wait for the buffer and propagation to complete + await Task.Delay(TimeSpan.FromSeconds(1)); + // Now verify things are back to normal; emit an event... + var finalEvent = CreateEvent(); + sink.Emit(finalEvent); + // ... give adequate time for it to be guaranteed to have percolated through + await Task.Delay(TimeSpan.FromSeconds(1)); + + // At least one of the preceding events should not have made it through + var propagatedExcludingFinal = + from e in _innerSink.Events + where !Object.ReferenceEquals(finalEvent, e) + select e; + Assert.InRange(2, 2 * 3 / 2 - 1, propagatedExcludingFinal.Count()); + // Final event should have made it through + Assert.Contains(_innerSink.Events, x => Object.ReferenceEquals(finalEvent, x)); + Assert.NotEqual(0, ((IAsyncLogEventSinkInspector)sink).DroppedMessagesCount); + } + } + + [Fact] + public async Task GivenConfiguredToBlock_WhenQueueFilled_ThenBlocks() + { + using (var sink = new BackgroundWorkerSink(_logger, 1, blockWhenFull: true)) + { + // Cause a delay when emmitting to the inner sink, allowing us to fill the queue to capacity + // after the first event is popped + _innerSink.DelayEmit = TimeSpan.FromMilliseconds(300); + + var events = new List + { + CreateEvent(), + CreateEvent(), + CreateEvent() + }; + + int i = 0; + events.ForEach(e => + { + var sw = Stopwatch.StartNew(); + sink.Emit(e); + sw.Stop(); + + // Emit should return immediately the first time, since the queue is not yet full. On + // subsequent calls, the queue should be full, so we should be blocked + if (i > 0) + { + Assert.True(sw.ElapsedMilliseconds > 200, "Should block the caller when the queue is full"); + } + }); + + await Task.Delay(TimeSpan.FromSeconds(2)); + + // No events should be dropped + Assert.Equal(3, _innerSink.Events.Count); + Assert.Equal(0, ((IAsyncLogEventSinkInspector)sink).DroppedMessagesCount); + } + } - Assert.Equal(3, _innerSink.Events.Count); + [Fact] + public void MonitorParameterAffordsSinkInspectorSuitableForHealthChecking() + { + var collector = new MemorySink { DelayEmit = TimeSpan.FromSeconds(2) }; + // 2 spaces in queue; 1 would make the second log entry eligible for dropping if consumer does not activate instantaneously + var bufferSize = 2; + var monitor = new DummyMonitor(); + using (var logger = new LoggerConfiguration() + .WriteTo.Async(w => w.Sink(collector), bufferSize: 2, monitor: monitor) + .CreateLogger()) + { + // Construction of BackgroundWorkerSink triggers StartMonitoring + var inspector = monitor.Inspector; + Assert.Equal(bufferSize, inspector.BufferSize); + Assert.Equal(0, inspector.Count); + Assert.Equal(0, inspector.DroppedMessagesCount); + logger.Information("Something to freeze the processing for 2s"); + // Can be taken from queue either instantanously or be awaiting consumer to take + Assert.InRange(inspector.Count, 0, 1); + Assert.Equal(0, inspector.DroppedMessagesCount); + logger.Information("Something that will sit in the queue"); + Assert.InRange(inspector.Count, 1, 2); + logger.Information("Something that will probably also sit in the queue (but could get dropped if first message has still not been picked up)"); + Assert.InRange(inspector.Count, 1, 2); + logger.Information("Something that will get dropped unless we get preempted for 2s during our execution"); + const string droppedMessage = "Something that will definitely get dropped"; + logger.Information(droppedMessage); + Assert.InRange(inspector.Count, 1, 2); + // Unless we are put to sleep for a Rip Van Winkle period, either: + // a) the BackgroundWorker will be emitting the item [and incurring the 2s delay we established], leaving a single item in the buffer + // or b) neither will have been picked out of the buffer yet. + Assert.InRange(inspector.Count, 1, 2); + Assert.Equal(bufferSize, inspector.BufferSize); + Assert.DoesNotContain(collector.Events, x => x.MessageTemplate.Text == droppedMessage); + // Because messages wait 2 seconds, the only real way to get one into the buffer is with a debugger breakpoint or a sleep + Assert.InRange(collector.Events.Count, 0, 3); + } + // Dispose should trigger a StopMonitoring call + Assert.Null(monitor.Inspector); + } + + private BackgroundWorkerSink CreateSinkWithDefaultOptions() + { + return new BackgroundWorkerSink(_logger, 10000, false); } private static LogEvent CreateEvent() diff --git a/test/Serilog.Sinks.Async.Tests/BackgroundWorkerSinkTests.cs b/test/Serilog.Sinks.Async.Tests/BackgroundWorkerSinkTests.cs index a54f92a..598be5b 100644 --- a/test/Serilog.Sinks.Async.Tests/BackgroundWorkerSinkTests.cs +++ b/test/Serilog.Sinks.Async.Tests/BackgroundWorkerSinkTests.cs @@ -1,9 +1,4 @@ -using System; -using System.Threading; -using Serilog.Core; -using Serilog.Events; -using Serilog.Parsing; -using Serilog.Sinks.Async.Tests.Support; +using Serilog.Sinks.Async.Tests.Support; using Xunit; namespace Serilog.Sinks.Async.Tests @@ -37,7 +32,23 @@ public void DisposeCompletesWithoutWorkPerformed() { } - Assert.Equal(0, collector.Events.Count); + Assert.Empty(collector.Events); + } + + [Fact] + public void CtorAndDisposeInformMonitor() + { + var collector = new MemorySink(); + var monitor = new DummyMonitor(); + + using (new LoggerConfiguration() + .WriteTo.Async(w => w.Sink(collector), monitor: monitor) + .CreateLogger()) + { + Assert.NotNull(monitor.Inspector); + } + + Assert.Null(monitor.Inspector); } } -} +} \ No newline at end of file diff --git a/test/Serilog.Sinks.Async.Tests/Serilog.Sinks.Async.Tests.csproj b/test/Serilog.Sinks.Async.Tests/Serilog.Sinks.Async.Tests.csproj new file mode 100644 index 0000000..8b7a497 --- /dev/null +++ b/test/Serilog.Sinks.Async.Tests/Serilog.Sinks.Async.Tests.csproj @@ -0,0 +1,26 @@ + + + + net452;netcoreapp1.0 + Serilog.Sinks.Async.Tests + ../../assets/Serilog.snk + true + true + Serilog.Sinks.Async.Tests + + + + + + + + + + + + + + + + + diff --git a/test/Serilog.Sinks.Async.Tests/Serilog.Sinks.Async.Tests.xproj b/test/Serilog.Sinks.Async.Tests/Serilog.Sinks.Async.Tests.xproj deleted file mode 100644 index 2f81baf..0000000 --- a/test/Serilog.Sinks.Async.Tests/Serilog.Sinks.Async.Tests.xproj +++ /dev/null @@ -1,18 +0,0 @@ - - - - 14.0 - $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) - - - - 3c2d8e01-5580-426a-bdd9-ec59cd98e618 - Serilog.Sinks.Async.Tests - .\obj - .\bin\ - - - 2.0 - - - \ No newline at end of file diff --git a/test/Serilog.Sinks.Async.Tests/Support/DummyMonitor.cs b/test/Serilog.Sinks.Async.Tests/Support/DummyMonitor.cs new file mode 100644 index 0000000..2d630cd --- /dev/null +++ b/test/Serilog.Sinks.Async.Tests/Support/DummyMonitor.cs @@ -0,0 +1,14 @@ +namespace Serilog.Sinks.Async.Tests.Support +{ + class DummyMonitor : IAsyncLogEventSinkMonitor + { + IAsyncLogEventSinkInspector inspector; + public IAsyncLogEventSinkInspector Inspector => inspector; + + void IAsyncLogEventSinkMonitor.StartMonitoring(IAsyncLogEventSinkInspector inspector) => + this.inspector = inspector; + + void IAsyncLogEventSinkMonitor.StopMonitoring(IAsyncLogEventSinkInspector inspector) => + System.Threading.Interlocked.CompareExchange(ref this.inspector, null, inspector); + } +} \ No newline at end of file diff --git a/test/Serilog.Sinks.Async.Tests/Support/MemorySink.cs b/test/Serilog.Sinks.Async.Tests/Support/MemorySink.cs index 4d756f6..2945863 100644 --- a/test/Serilog.Sinks.Async.Tests/Support/MemorySink.cs +++ b/test/Serilog.Sinks.Async.Tests/Support/MemorySink.cs @@ -2,6 +2,7 @@ using Serilog.Core; using System.Collections.Concurrent; using System; +using System.Threading.Tasks; namespace Serilog.Sinks.Async.Tests.Support { @@ -9,9 +10,13 @@ public class MemorySink : ILogEventSink { public ConcurrentBag Events { get; } = new ConcurrentBag(); public bool ThrowAfterCollecting { get; set; } + public TimeSpan? DelayEmit { get; set; } public void Emit(LogEvent logEvent) { + if (DelayEmit.HasValue) + Task.Delay(DelayEmit.Value).Wait(); + Events.Add(logEvent); if (ThrowAfterCollecting) diff --git a/test/Serilog.Sinks.Async.Tests/project.json b/test/Serilog.Sinks.Async.Tests/project.json deleted file mode 100644 index 6452b45..0000000 --- a/test/Serilog.Sinks.Async.Tests/project.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "testRunner": "xunit", - - "dependencies": { - "Serilog.Sinks.Async": { "target": "project" }, - "xunit": "2.1.0", - "dotnet-test-xunit": "1.0.0-rc2-build10025" - }, - - "buildOptions": { - "keyFile": "../../assets/Serilog.snk" - }, - "frameworks": { - "net4.5.2": {}, - "netcoreapp1.0": { - "dependencies": { - "Microsoft.NETCore.App": { - "type": "platform", - "version": "1.0.0" - } - }, - "imports": [ - "dnxcore50", - "portable-net45+win8" - ] - } - } -}