Custom AzureDevOps Serilog Sink

The other day I was wondering how I can add warnings and error indicators to pipeline summaries of Azure DevOps. It turns out that you can write simple echo statements to trigger all sorts of cool commands. I was even more stoked when I realized that I could write a custom sink for my beloved logging framework Serilog to send pipeline error/warning commands right from a C# application. It's nothing fancy, but for some reason this little code snippet brought me joy:

using Serilog.Configuration;
using Serilog.Core;
using Serilog.Events;
using Serilog;

namespace Example;

public static class Program
{
    public static void Main()
    {
        Log.Logger = new LoggerConfiguration()
            .WriteTo.AzureDevOpsOr(l => l.Console())
            .MinimumLevel.Debug()
            .CreateLogger();

        try
        {
            Log.Warning("A warning message");
            Log.Error("An error message");

            throw new NotSupportedException("An example exception");
        }
        catch (Exception e)
        {
            Log.Error(e, "Unexpected error");
            Environment.ExitCode = 1;
        }
        finally
        {
            Log.CloseAndFlush();
        }
    }
}

public sealed class AzureDevOpsSink : ILogEventSink
{
    public void Emit(LogEvent logEvent)
    {
        var rendered = logEvent.RenderMessage();
        var annotated = logEvent.Level switch
        {
            LogEventLevel.Verbose => $"##[debug]{rendered}",
            LogEventLevel.Debug => $"##[debug]{rendered}",
            LogEventLevel.Information => rendered,
            LogEventLevel.Warning => $"##vso[task.logissue type=warning]{rendered}",
            LogEventLevel.Error => $"##vso[task.logissue type=error]{rendered}",
            LogEventLevel.Fatal => $"##vso[task.logissue type=error]{rendered}",
            _ => throw new NotImplementedException()
        };

        Console.WriteLine(annotated);

        if (logEvent.Exception != null)
        {
            Console.WriteLine($"##[debug]{logEvent.Exception}");
        }
    }
}

public static class AzureDevOpsSinkExtensions
{
    public static LoggerConfiguration AzureDevOpsOr(
        this LoggerSinkConfiguration config,
        Func<LoggerSinkConfiguration, LoggerConfiguration> orFunc)
    {
        var runningOnAzureDevops = !string.IsNullOrEmpty(
            Environment.GetEnvironmentVariable("BUILD_BUILDNUMBER"));

        return runningOnAzureDevops
            ? config.Sink(new AzureDevOpsSink())
            : orFunc(config);
    }
}

Published: 2024-09-12