Skip to content

Add support for sending NServiceBus logging through Serilog

License

Notifications You must be signed in to change notification settings

janpieterz/NServiceBus.Serilog

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

NServiceBus.Serilog

Build status NuGet Status

Add support for sending NServiceBus logging through Serilog

Already a Patron? skip past this section

Community backed

It is expected that all developers either become a Patron or have a Tidelift Subscription to use NServiceBusExtensions. Go to licensing FAQ

Sponsors

Support this project by becoming a Sponsor. The company avatar will show up here with a website link. The avatar will also be added to all GitHub repositories under the NServiceBusExtensions organization.

Patrons

Thanks to all the backing developers. Support this project by becoming a patron.

Support via TideLift

Support is available via a Tidelift Subscription.

NuGet package

https://nuget.org/packages/NServiceBus.Serilog/

Usage

Log.Logger = new LoggerConfiguration()
    .WriteTo.File("log.txt")
    .CreateLogger();

LogManager.Use<SerilogFactory>();

snippet source | anchor

Filtering

NServiceBus can write a significant amount of information to the log. To limit this information use the filtering features of the underlying logging framework.

For example to limit log output to a specific namespace.

Here is a code configuration example for adding a Filter.

Log.Logger = new LoggerConfiguration()
    .WriteTo.File(
        path:"log.txt",
        restrictedToMinimumLevel: LogEventLevel.Debug
    )
    .Filter.ByIncludingOnly(
        inclusionPredicate: Matching.FromSource("MyNamespace"))
    .CreateLogger();

LogManager.Use<SerilogFactory>();

snippet source | anchor

Tracing

Writing diagnostic log entries to Serilog. Plugs into the low level pipeline to give more detailed diagnostics.

When using Serilog for tracing, it is optional to use Serilog as the main NServiceBus logger. i.e. there is no need to include LogManager.Use<SerilogFactory>();.

Create an instance of a Serilog logger

var tracingLog = new LoggerConfiguration()
    .WriteTo.File("log.txt")
    .MinimumLevel.Information()
    .CreateLogger();

snippet source | anchor

Configure the tracing feature to use that logger

var serilogTracing = configuration.EnableSerilogTracing(tracingLog);
serilogTracing.EnableMessageTracing();

snippet source | anchor

Contextual logger

Serilog tracing injects a contextual Serilog.Ilogger into the NServiceBus pipeline.

NOTE: Saga and message tracing will use the current contextual logger.

There are several layers of enrichment based on the pipeline phase.

Endpoint enrichment

All loggers for an endpoint will have the the property ProcessingEndpoint added that contains the current endpoint name.

Incoming message enrichment

When a message is received, the following enrichment properties are added:

Handler enrichment

When a handler is invoked, a new logger is forked from the above enriched physical logger with a new enriched property named Handler that contains the the FullName of the current handler.

Outgoing message enrichment

When a message is sent, the same properties as described in "Incoming message enrichment" will be added to the outgoing pipeline. Note that if a handler sends a message, the logger injected into the outgoing pipeline will be forked from the logger instance as described in "Handler enrichment". As such it will contain a property Handler for the handler that sent the message.

Accessing the logger

The contextual logger instance can be accessed from anywhere in the pipeline via SerilogTracingExtensions.Logger(this IPipelineContext context).

public class HandlerUsingLogger :
    IHandleMessages<TheMessage>
{
    public Task Handle(TheMessage message, IMessageHandlerContext context)
    {
        var logger = context.Logger();
        logger.Information("Hello from {@Handler}.");
        return Task.CompletedTask;
    }
}

snippet source | anchor

Log extension methods

IPipelineContext also has extension methods added to expose direct Log* methods

public class HandlerUsingLog :
    IHandleMessages<TheMessage>
{
    public Task Handle(TheMessage message, IMessageHandlerContext context)
    {
        context.LogInformation("Hello from {@Handler}.");
        return Task.CompletedTask;
    }
}

snippet source | anchor

Example result of a contextual log entry

Exception enrichment

When an exception occurs in the message processing pipeline, the current pipeline state is added to the exception. When that exception is logged that state can be add to the log entry.

When a pipeline exception is logged, it will be enriched with the following properties:

  • ProcessingEndpoint will be the current endpoint name.
  • IncomingMessageId will be the value of the MessageId header.
  • IncomingTransportMessageId will be the MessageId from the underlying transport if it exist.
  • IncomingHeaders will be the value of the Message headers.
  • IncomingMessageType will be the message type FullName extracted from the EnclosedMessageTypes header. UnknownMessageType will be used if no header exists.
  • CorrelationId will be the value of the CorrelationId header if it exists.
  • ConversationId will be the value of the ConversationId header if it exists.
  • HandlerType will be type name for the current handler if it exists.
  • IncomingMessage will be the value of current logical message if it exists.
  • HandlerStartTime the UTC timestamp for when the handler started.
  • HandlerFailureTime the UTC timestamp for when the handler threw the exception.

Saga tracing

var serilogTracing = configuration.EnableSerilogTracing(logger);
serilogTracing.EnableSagaTracing();

snippet source | anchor

Example Logs

{
  logsForTarget: [
    {
      MessageTemplate: Hello from {@Saga}. Message: {@Message},
      Level: Information,
      Properties: {
        Saga: TheSaga,
        Message: {
          TypeTag: StartSaga,
          Properties: [
            {
              Property: TheProperty
            }
          ]
        },
        Handler: TheSaga,
        IncomingMessageId: Guid_1,
        IncomingMessageType: StartSaga,
        CorrelationId: Guid_1,
        ConversationId: Guid_2,
        SourceContext: StartSaga,
        ProcessingEndpoint: SerilogTestsStartSaga
      }
    },
    {
      MessageTemplate: Receive message {IncomingMessageType} {IncomingMessageId}.,
      Level: Information,
      Properties: {
        IncomingMessage: {
          TypeTag: StartSaga,
          Properties: [
            {
              Property: TheProperty
            }
          ]
        },
        OriginatingHostId: Guid_3,
        MessageIntent: Send,
        OriginatingEndpoint: SerilogTestsStartSaga,
        OriginatingMachine: TheMachineName,
        ReplyToAddress: SerilogTestsStartSaga,
        TimeSent: DateTime_1,
        IncomingMessageId: Guid_1,
        IncomingMessageType: StartSaga,
        CorrelationId: Guid_1,
        ConversationId: Guid_2,
        SourceContext: StartSaga,
        ProcessingEndpoint: SerilogTestsStartSaga
      }
    },
    {
      MessageTemplate: Saga execution {SagaType} {SagaId}.,
      Level: Information,
      Properties: {
        SagaType: TheSaga,
        SagaId: Guid_4,
        StartTime: DateTimeOffset_1,
        FinishTime: DateTimeOffset_2,
        IsCompleted: false,
        IsNew: true,
        Initiator: {
          Elements: {
            "IsSagaTimeout": false,
            "MessageId": Guid_1,
            "OriginatingMachine": TheMachineName,
            "OriginatingEndpoint": SerilogTestsStartSaga,
            "MessageType": StartSaga,
            "TimeSent": DateTime_1,
            "Intent": Send
          }
        },
        ResultingMessages: {
          Elements: [
            {
              Elements: {
                "Id": Guid_5,
                "Type": BackIntoSaga,
                "Intent": Send,
                "Destination": SerilogTestsStartSaga
              }
            }
          ]
        },
        Entity: {
          TypeTag: TheSagaData,
          Properties: [
            {
              Property: TheProperty
            },
            {
              Id: Guid_4
            },
            {
              Originator: SerilogTestsStartSaga
            },
            {
              OriginalMessageId: Guid_1
            }
          ]
        },
        IncomingMessageId: Guid_1,
        IncomingMessageType: StartSaga,
        CorrelationId: Guid_1,
        ConversationId: Guid_2,
        SourceContext: StartSaga,
        ProcessingEndpoint: SerilogTestsStartSaga
      }
    },
    {
      MessageTemplate: Sent message {OutgoingMessageType} {OutgoingMessageId}.,
      Level: Information,
      Properties: {
        OutgoingMessage: {
          TypeTag: StartSaga,
          Properties: [
            {
              Property: TheProperty
            }
          ]
        },
        UnicastRoutes: {
          Elements: [
            SerilogTestsStartSaga
          ]
        },
        OriginatingHostId: Guid_3,
        MessageIntent: Send,
        OriginatingEndpoint: SerilogTestsStartSaga,
        OriginatingMachine: TheMachineName,
        ReplyToAddress: SerilogTestsStartSaga,
        OutgoingMessageId: Guid_1,
        OutgoingMessageType: StartSaga,
        CorrelationId: Guid_1,
        ConversationId: Guid_2,
        SourceContext: StartSaga,
        ProcessingEndpoint: SerilogTestsStartSaga
      }
    },
    {
      MessageTemplate: Sent message {OutgoingMessageType} {OutgoingMessageId}.,
      Level: Information,
      Properties: {
        OutgoingMessage: {
          TypeTag: BackIntoSaga,
          Properties: [
            {
              Property: TheProperty
            }
          ]
        },
        UnicastRoutes: {
          Elements: [
            SerilogTestsStartSaga
          ]
        },
        OriginatingHostId: Guid_3,
        MessageIntent: Send,
        OriginatingEndpoint: SerilogTestsStartSaga,
        OriginatingMachine: TheMachineName,
        OriginatingSagaId: Guid_4,
        OriginatingSagaType: TheSaga,
        RelatedTo: Guid_1,
        ReplyToAddress: SerilogTestsStartSaga,
        OutgoingMessageId: Guid_5,
        OutgoingMessageType: BackIntoSaga,
        CorrelationId: Guid_1,
        ConversationId: Guid_2,
        IncomingMessageId: Guid_1,
        IncomingMessageType: StartSaga,
        SourceContext: StartSaga,
        ProcessingEndpoint: SerilogTestsStartSaga
      }
    }
  ]
}

snippet source | anchor

Message tracing

Both incoming and outgoing messages will be logged at the Information level. The current message will be included in a property named Message. For outgoing messages any unicast routes will be included in a property named UnicastRoutes.

var serilogTracing = configuration.EnableSerilogTracing(logger);
serilogTracing.EnableMessageTracing();

snippet source | anchor

Example Logs

{
  logsForTarget: [
    {
      MessageTemplate: Hello from {@Handler}.,
      Level: Information,
      Properties: {
        Handler: TheHandler,
        IncomingMessageId: Guid_1,
        IncomingMessageType: StartHandler,
        CorrelationId: Guid_1,
        ConversationId: Guid_2,
        SourceContext: StartHandler,
        ProcessingEndpoint: SerilogTestsStartHandler
      }
    },
    {
      MessageTemplate: Receive message {IncomingMessageType} {IncomingMessageId}.,
      Level: Information,
      Properties: {
        IncomingMessage: {
          TypeTag: StartHandler,
          Properties: [
            {
              Property: TheProperty
            }
          ]
        },
        OriginatingHostId: Guid_3,
        MessageIntent: Send,
        OriginatingEndpoint: SerilogTestsStartHandler,
        OriginatingMachine: TheMachineName,
        ReplyToAddress: SerilogTestsStartHandler,
        TimeSent: DateTime_1,
        IncomingMessageId: Guid_1,
        IncomingMessageType: StartHandler,
        CorrelationId: Guid_1,
        ConversationId: Guid_2,
        SourceContext: StartHandler,
        ProcessingEndpoint: SerilogTestsStartHandler
      }
    },
    {
      MessageTemplate: Sent message {OutgoingMessageType} {OutgoingMessageId}.,
      Level: Information,
      Properties: {
        OutgoingMessage: {
          TypeTag: StartHandler,
          Properties: [
            {
              Property: TheProperty
            }
          ]
        },
        UnicastRoutes: {
          Elements: [
            SerilogTestsStartHandler
          ]
        },
        OriginatingHostId: Guid_3,
        MessageIntent: Send,
        OriginatingEndpoint: SerilogTestsStartHandler,
        OriginatingMachine: TheMachineName,
        ReplyToAddress: SerilogTestsStartHandler,
        OutgoingMessageId: Guid_1,
        OutgoingMessageType: StartHandler,
        CorrelationId: Guid_1,
        ConversationId: Guid_2,
        SourceContext: StartHandler,
        ProcessingEndpoint: SerilogTestsStartHandler
      }
    }
  ]
}

snippet source | anchor

Startup diagnostics

Startup diagnostics is, in addition to its default file location, also written to Serilog with the level of Warning.

class StartupDiagnostics :
    FeatureStartupTask
{
    public StartupDiagnostics(ReadOnlySettings settings, ILogger logger)
    {
        this.settings = settings;
        this.logger = logger.ForContext<StartupDiagnostics>();
    }

    protected override Task OnStart(IMessageSession session)
    {
        var properties = BuildProperties(settings, logger);

        MessageTemplateParser templateParser = new();
        var messageTemplate = templateParser.Parse("DiagnosticEntries");
        LogEvent logEvent = new(
            timestamp: DateTimeOffset.Now,
            level: LogEventLevel.Warning,
            exception: null,
            messageTemplate: messageTemplate,
            properties: properties);
        logger.Write(logEvent);
        return Task.CompletedTask;
    }

    static IEnumerable<LogEventProperty> BuildProperties(
        ReadOnlySettings settings,
        ILogger logger)
    {
        var entries = settings.ReadStartupDiagnosticEntries();
        foreach (var entry in entries)
        {
            if (entry.Name == "Features")
            {
                continue;
            }

            var name = CleanEntry(entry.Name);
            if (logger.BindProperty(name, entry.Data, out var property))
            {
                yield return property!;
            }
        }
    }

    internal static string CleanEntry(string entry)
    {
        if (entry.StartsWith("NServiceBus."))
        {
            return entry.Substring(12);
        }

        return entry;
    }

    protected override Task OnStop(IMessageSession session)
    {
        return Task.CompletedTask;
    }

    ReadOnlySettings settings;
    ILogger logger;
}

snippet source | anchor

Logging to Seq

To log to Seq:

var tracingLog = new LoggerConfiguration()
    .WriteTo.Seq("http://localhost:5341")
    .MinimumLevel.Information()
    .CreateLogger();

snippet source | anchor

Sample

The sample illustrates how to customize logging by configuring Serilog targets and rules.

Configure Serilog

Log.Logger = new LoggerConfiguration()
    .WriteTo.Console()
    .CreateLogger();

snippet source | anchor

var tracingLog = new LoggerConfiguration()
    .WriteTo.Seq("http://localhost:5341")
    .MinimumLevel.Information()
    .CreateLogger();
var serilogFactory = LogManager.Use<SerilogFactory>();
serilogFactory.WithLogger(tracingLog);

snippet source | anchor

Pass the configuration to NServiceBus

LogManager.Use<SerilogFactory>();

EndpointConfiguration configuration = new("SerilogSample");

snippet source | anchor

EndpointConfiguration configuration = new("SeqSample");
var serilogTracing = configuration.EnableSerilogTracing(tracingLog);
serilogTracing.EnableSagaTracing();
serilogTracing.EnableMessageTracing();

snippet source | anchor

Ensure logging is flushed on shutdown

await endpoint.Stop();
Log.CloseAndFlush();

snippet source | anchor

await endpoint.Stop();
Log.CloseAndFlush();

snippet source | anchor

Seq Sample

Illustrates customizing Serilog usage to log to Seq.

Prerequisites

An instance of Seq running one http://localhost:5341.

Configure Serilog

Log.Logger = new LoggerConfiguration()
    .WriteTo.Console()
    .CreateLogger();

snippet source | anchor

var tracingLog = new LoggerConfiguration()
    .WriteTo.Seq("http://localhost:5341")
    .MinimumLevel.Information()
    .CreateLogger();
var serilogFactory = LogManager.Use<SerilogFactory>();
serilogFactory.WithLogger(tracingLog);

snippet source | anchor

Pass that configuration to NServiceBus

LogManager.Use<SerilogFactory>();

EndpointConfiguration configuration = new("SerilogSample");

snippet source | anchor

EndpointConfiguration configuration = new("SeqSample");
var serilogTracing = configuration.EnableSerilogTracing(tracingLog);
serilogTracing.EnableSagaTracing();
serilogTracing.EnableMessageTracing();

snippet source | anchor

Ensure logging is flushed on shutdown

await endpoint.Stop();
Log.CloseAndFlush();

snippet source | anchor

await endpoint.Stop();
Log.CloseAndFlush();

snippet source | anchor

Security contact information

To report a security vulnerability, use the Tidelift security contact. Tidelift will coordinate the fix and disclosure.

Icon

Brain designed by Rémy Médard from The Noun Project.

About

Add support for sending NServiceBus logging through Serilog

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages

  • C# 100.0%