Custom NLog target with async writing

Update AsyncTaskTarget with NLog 4.6:

public class MyCustomTarget : AsyncTaskTarget
{
     protected override Task WriteAsyncTask(LogEventInfo logEvent, CancellationToken token)
     {
          return await MyLogMethodAsync(logEvent.LogEvent).ConfigureAwait(false);
     }   
}

See also: https://github.com/NLog/NLog/wiki/How-to-write-a-custom-async-target

Previous Answer:

If order of LogEvents are not important and correct flush is not important, then you can do the following:

public class MyCustomTarget : TargetWithLayout
{
    protected override async void Write(AsyncLogEventInfo logEvent)
    {
        try
        {
           await MyLogMethodAsync(logEvent.LogEvent).ConfigureAwait(false);
           logEvent.Continuation(null);
        }
        catch (Exception ex)
        {
           logEvent.Continuation(ex);
        }
    }
}

Implemented an abstract class, that ensures correct ordering (and doesn't exhaust the threadpool):

using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using NLog.Common;

/// <summary>
/// Abstract Target with async Task support
/// </summary>
public abstract class AsyncTaskTarget : Target
{
    private readonly CancellationTokenSource _cancelTokenSource;
    private readonly Queue<AsyncLogEventInfo> _requestQueue;
    private readonly Action _taskStartNext;
    private readonly Action<Task, object> _taskCompletion;
    private Task _previousTask;

    /// <summary>
    /// Constructor
    /// </summary>
    protected AsyncTaskTarget()
    {
        _taskStartNext = TaskStartNext;
        _taskCompletion = TaskCompletion;
        _cancelTokenSource = new CancellationTokenSource();
        _requestQueue = new Queue<AsyncLogEventInfo>(10000);
    }

    /// <summary>
    /// Override this to create the actual logging task
    /// <example>
    /// Example of how to override this method, and call custom async method
    /// <code>
    /// protected override Task WriteAsyncTask(LogEventInfo logEvent, CancellationToken token)
    /// {
    ///    return CustomWriteAsync(logEvent, token);
    /// }
    /// 
    /// private async Task CustomWriteAsync(LogEventInfo logEvent, CancellationToken token)
    /// {
    ///     await MyLogMethodAsync(logEvent, token).ConfigureAwait(false);
    /// }
    /// </code></example>
    /// </summary>
    /// <param name="logEvent">The log event.</param>
    /// <param name="cancellationToken">The cancellation token</param>
    /// <returns></returns>
    protected abstract Task WriteAsyncTask(LogEventInfo logEvent, CancellationToken cancellationToken);

    /// <summary>
    /// Schedules the LogEventInfo for async writing
    /// </summary>
    /// <param name="logEvent">The log event.</param>
    protected override void Write(AsyncLogEventInfo logEvent)
    {
        if (_cancelTokenSource.IsCancellationRequested)
        {
            logEvent.Continuation(null);
            return;
        }

        this.MergeEventProperties(logEvent.LogEvent);
        this.PrecalculateVolatileLayouts(logEvent.LogEvent);

        _requestQueue.Enqueue(logEvent);
        if (_previousTask == null)
        {
            _previousTask = Task.Factory.StartNew(_taskStartNext, _cancelTokenSource.Token, TaskCreationOptions.None, TaskScheduler.Default);
        }
    }

    /// <summary>
    /// Schedules notification of when all messages has been written
    /// </summary>
    /// <param name="asyncContinuation"></param>
    protected override void FlushAsync(AsyncContinuation asyncContinuation)
    {
        if (_previousTask == null)
        {
            InternalLogger.Debug("{0} Flushing Nothing", this.Name);
            asyncContinuation(null);
        }
        else
        {
            InternalLogger.Debug("{0} Flushing {1} items", this.Name, _requestQueue.Count + 1);
            _requestQueue.Enqueue(new AsyncLogEventInfo(null, asyncContinuation));
        }
    }

    /// <summary>
    /// Closes Target by updating CancellationToken 
    /// </summary>
    protected override void CloseTarget()
    {
        _cancelTokenSource.Cancel();
        _requestQueue.Clear();
        _previousTask = null;
        base.CloseTarget();
    }

    /// <summary>
    /// Releases any managed resources
    /// </summary>
    /// <param name="disposing"></param>
    protected override void Dispose(bool disposing)
    {
        base.Dispose(disposing);
        if (disposing)
            _cancelTokenSource.Dispose();
    }

    private void TaskStartNext()
    {
        AsyncLogEventInfo logEvent;
        do
        {
            lock (this.SyncRoot)
            {
                if (_requestQueue.Count == 0)
                {
                    _previousTask = null;
                    break;
                }

                logEvent = _requestQueue.Dequeue();
            }
        } while (!TaskCreation(logEvent));
    }

    private bool TaskCreation(AsyncLogEventInfo logEvent)
    {
        try
        {
            if (_cancelTokenSource.IsCancellationRequested)
            {
                logEvent.Continuation(null);
                return false;
            }

            if (logEvent.LogEvent == null)
            {
                InternalLogger.Debug("{0} Flush Completed", this.Name);
                logEvent.Continuation(null);
                return false;
            }

            var newTask = WriteAsyncTask(logEvent.LogEvent, _cancelTokenSource.Token);
            if (newTask == null)
            {
                InternalLogger.Debug("{0} WriteAsync returned null", this.Name);
            }
            else
            {
                lock (this.SyncRoot)
                {
                    _previousTask = newTask;
                    _previousTask.ContinueWith(_taskCompletion, logEvent.Continuation, _cancelTokenSource.Token);
                    if (_previousTask.Status == TaskStatus.Created)
                        _previousTask.Start(TaskScheduler.Default);
                }
                return true;
            }
        }
        catch (Exception ex)
        {
            try
            {
                InternalLogger.Error(ex, "{0} WriteAsync failed on creation", this.Name);
                logEvent.Continuation(ex);
            }
            catch
            {
                // Don't wanna die
            }
        }
        return false;
    }

    private void TaskCompletion(Task completedTask, object continuation)
    {
        try
        {
            if (completedTask.IsCanceled)
            {
                if (completedTask.Exception != null)
                    InternalLogger.Warn(completedTask.Exception, "{0} WriteAsync was cancelled", this.Name);
                else
                    InternalLogger.Info("{0} WriteAsync was cancelled", this.Name);
            }
            else if (completedTask.Exception != null)
            {
                InternalLogger.Warn(completedTask.Exception, "{0} WriteAsync failed on completion", this.Name);
            }
            ((AsyncContinuation)continuation)(completedTask.Exception);
        }
        finally
        {
            TaskStartNext();
        }
    }
}

There is WriteAsyncLogEvent

public void WriteAsyncLogEvent(
    AsyncLogEventInfo logEvent
)

Look at the usage in the tests section, for example ConsoleTargetTests

try
{
    var exceptions = new List<Exception>();
    target.Initialize(null);
    target.WriteAsyncLogEvent(new LogEventInfo(LogLevel.Info, "Logger1", "message1").WithContinuation(exceptions.Add));
    target.WriteAsyncLogEvent(new LogEventInfo(LogLevel.Info, "Logger1", "message2").WithContinuation(exceptions.Add));
    target.WriteAsyncLogEvents(
        new LogEventInfo(LogLevel.Info, "Logger1", "message3").WithContinuation(exceptions.Add),
        new LogEventInfo(LogLevel.Info, "Logger2", "message4").WithContinuation(exceptions.Add),
        new LogEventInfo(LogLevel.Info, "Logger2", "message5").WithContinuation(exceptions.Add),
        new LogEventInfo(LogLevel.Info, "Logger1", "message6").WithContinuation(exceptions.Add));
    Assert.Equal(6, exceptions.Count);
    target.Close();
}
finally
{
    Console.SetOut(oldConsoleOutWriter);
}

Look also at the wiki section Asynchronous processing and wrapper targets for more details

Because asynchronous processing is a common scenario, NLog supports a shorthand notation to enable it for all targets without the need to specify explicit wrappers. You can simply set async="true" on targets element and all your targets within that element will be wrapped with the AsyncWrapper target.

<nlog>
  <targets async="true">
    <!-- all targets in this section will automatically be asynchronous -->
  </targets>
</nlog>

Tags:

C#

Nlog

.Net Core