From 77489ae44b7c216d95ac5a1bb2f65e18c7722489 Mon Sep 17 00:00:00 2001 From: stijnmoreels <9039753+stijnmoreels@users.noreply.github.com> Date: Mon, 23 Mar 2020 13:56:07 +0100 Subject: [PATCH] Feature - move correlation access information to observability (#43) * Feature - move correlation access information to observability * pr-sug: update correlation info enricher with accessor directly * pr-fix: update with correct target framework package references * temp commit * pr-sug: update with custom correlation info model * pr-sug: use custom correlation info to the enricher * pr-sug: use get/set property io throwing for test correlation info * pr-sug: more info about custom correlation accessing * pr-sug: update w/ merging master and docs * pr-sug: use custom enricher for custom correlation enrichment * Update docs/features/correlation.md Co-Authored-By: Tom Kerkhove * pr-fix: use netcoreapp2.2 package references * pr-docs: update with PR comments * pr-sug: use updated correlation info enricher * pr-sug: use custom options to configure custom correlation info * pr-sug: use 'new' member modifier for hiding inherited default accessor * pr-sug: add xml docs for internal proxy implementation * pr-sug: use protected correlation info enricher * pr-fix: use 3.0.0 packages * pr-sug: use methods to get/set correlation info * pr-fix: update with correlation get/set as methods * Update telemetry-enrichment.md * pr-sug: update with more extensible correlation enrichment * pr-docs: update with custom correlation info options * pr-fix: use correct overriding * pr-docs: remove default instance value * pr-style: place generic restrictions on diff line Co-authored-by: Tom Kerkhove --- docs/features/correlation.md | 151 +++++++++++++++ docs/features/telemetry-enrichment.md | 46 ++++- .../Arcus.Observability.Correlation.csproj | 9 + .../CorrelationInfoAccessorProxy.cs | 44 +++++ .../CorrelationInfoOperationOptions.cs | 48 +++++ .../CorrelationInfoOptions.cs | 18 ++ .../CorrelationInfoTransactionOptions.cs | 61 ++++++ .../DefaultCorrelationInfoAccessor.cs | 20 ++ .../DefaultCorrelationInfoAccessorT.cs | 42 +++++ .../ICorrelationInfoAccessor.cs | 9 + .../ICorrelationInfoAccessorT.cs | 20 ++ .../IServiceCollectionExtensions.cs | 158 ++++++++++++++++ .../Arcus.Observability.Telemetry.Core.csproj | 11 +- .../ContextProperties.cs | 1 + ...ability.Telemetry.Serilog.Enrichers.csproj | 1 + .../CorrelationInfoEnricher.cs | 71 +++++++ ...LoggerEnrichmentConfigurationExtensions.cs | 76 +++++++- .../Arcus.Observability.Tests.Unit.csproj | 3 +- .../IServiceCollectionExtensionsTests.cs | 173 ++++++++++++++++++ .../Correlation/TestCorrelationInfo.cs | 24 +++ .../TestCorrelationInfoAccessor.cs | 30 +++ .../Correlation/TestCorrelationInfoOptions.cs | 15 ++ .../Serilog/CorrelationInfoEnricherTests.cs | 142 ++++++++++++++ .../Serilog/TestCorrelationInfoEnricher.cs | 40 ++++ 24 files changed, 1205 insertions(+), 8 deletions(-) create mode 100644 src/Arcus.Observability.Correlation/CorrelationInfoAccessorProxy.cs create mode 100644 src/Arcus.Observability.Correlation/CorrelationInfoOperationOptions.cs create mode 100644 src/Arcus.Observability.Correlation/CorrelationInfoOptions.cs create mode 100644 src/Arcus.Observability.Correlation/CorrelationInfoTransactionOptions.cs create mode 100644 src/Arcus.Observability.Correlation/DefaultCorrelationInfoAccessor.cs create mode 100644 src/Arcus.Observability.Correlation/DefaultCorrelationInfoAccessorT.cs create mode 100644 src/Arcus.Observability.Correlation/ICorrelationInfoAccessor.cs create mode 100644 src/Arcus.Observability.Correlation/ICorrelationInfoAccessorT.cs create mode 100644 src/Arcus.Observability.Correlation/IServiceCollectionExtensions.cs create mode 100644 src/Arcus.Observability.Telemetry.Serilog.Enrichers/CorrelationInfoEnricher.cs create mode 100644 src/Arcus.Observability.Tests.Unit/Correlation/IServiceCollectionExtensionsTests.cs create mode 100644 src/Arcus.Observability.Tests.Unit/Correlation/TestCorrelationInfo.cs create mode 100644 src/Arcus.Observability.Tests.Unit/Correlation/TestCorrelationInfoAccessor.cs create mode 100644 src/Arcus.Observability.Tests.Unit/Correlation/TestCorrelationInfoOptions.cs create mode 100644 src/Arcus.Observability.Tests.Unit/Serilog/CorrelationInfoEnricherTests.cs create mode 100644 src/Arcus.Observability.Tests.Unit/Serilog/TestCorrelationInfoEnricher.cs diff --git a/docs/features/correlation.md b/docs/features/correlation.md index 18f556cf..0d1feb84 100644 --- a/docs/features/correlation.md +++ b/docs/features/correlation.md @@ -18,4 +18,155 @@ This feature requires to install our NuGet package PM > Install-Package Arcus.Observability.Correlation ``` +## What We Provide + +The `Arcus.Observability.Correlation` library provides a way to get access to correlation information across your application. +What it **DOES NOT** provide is how this correlation information is initially set. + +It uses the the Microsoft dependency injection mechanism to register an `ICorrelationInfoAccessor` and `ICorrelationInfoAccessor<>` implementation that is available. + +**Example** + +```csharp +public class Startup +{ + public void ConfigureServices(IServiceCollection services) + { + // Adds operation and transaction correlation to the application, + // using the `DefaultCorrelationInfoAccessor` as `ICorrelationInfoAccessor` that stores the `CorrelationInfo` model internally. + services.AddCorrelation(); + } +} +``` +## Custom Correlation + +We register two interfaces during the registration of the correlation: `ICorrealtionInfoAccessor` and `ICorrelationInfoAccessor<>`. +The reason is because some applications require a custom `CorrelationInfo` model, and with using the generic interface `ICorrelationInfoAccessor<>` we can support this. + +**Example** + +```csharp +public class OrderCorrelationInfo : CorrelationInfo +{ + public string OrderId { get; } +} + +public class Startup +{ + public void ConfigureService(IServiceCollection services) + { + services.AddCorrelation(); + } +} +``` + +## Accessing Correlation Throughout the Application + +When a part of the application needs access to the correlation information, you can inject one of the two interfaces: + +```csharp +public class OrderService +{ + public OrderService(ICorrelationInfoAccessor accessor) + { + CorrelationInfo correlationInfo = accessor.CorrelationInfo; + } +} +``` + +Or, alternatively when using custom correlation: + +```csharp +public class OrderService +{ + public OrderService(ICorrelationInfoAccessor accessor) + { + OrderCorrelationInfo correlationInfo = accessor.CorrelationInfo; + } +} +``` + +## Configuration + +The library also provides a way configure some correlation specific options that you can later retrieve during get/set of the correlation information in your application. + +```csharp +public void ConfigureServices(IServiceCollection services) +{ + services.AddCorrelation(options => + { + // Configuration on the transaction ID (`X-Transaction-ID`) request/response header. + // --------------------------------------------------------------------------------- + + // Whether the transaction ID can be specified in the request, and will be used throughout the request handling. + // The request will return early when the `.AllowInRequest` is set to `false` and the request does contain the header (default: true). + options.Transaction.AllowInRequest = true; + + // Whether or not the transaction ID should be generated when there isn't any transaction ID found in the request. + // When the `.GenerateWhenNotSpecified` is set to `false` and the request doesn't contain the header, no value will be available for the transaction ID; + // otherwise a GUID will be generated (default: true). + options.Transaction.GenerateWhenNotSpecified = true; + + // Whether to include the transaction ID in the response (default: true). + options.Transaction.IncludeInResponse = true; + + // The header to look for in the request, and will be set in the response (default: X-Transaction-ID). + options.Transaction.HeaderName = "X-Transaction-ID"; + + // The function that will generate the transaction ID, when the `.GenerateWhenNotSpecified` is set to `false` and the request doesn't contain the header. + // (default: new `Guid`). + options.Transaction.GenerateId = () => $"Transaction-{Guid.NewGuid()}"; + + // Configuration on the operation ID (`RequestId`) response header. + // ---------------------------------------------------------------- + + // Whether to include the operation ID in the response (default: true). + options.Operation.IncludeInResponse = true; + + // The header that will contain the operation ID in the response (default: RequestId). + options.Operation.HeaderName = "RequestId"; + + // The function that will generate the operation ID header value. + // (default: new `Guid`). + options.Operation.GenerateId = () => $"Operation-{Guid.NewGuid()}"; + }); +} +``` + +Later in the application, the options can be retrieved by injecting the `IOptions` type. + +### Custom Configuration + +We also provide a way to provide custom configuration options when the application uses a custom correlation model. + +For example, with a custom correlation model: + +```csharp +public class OrderCorrelationInfo : CorrelationInfo +{ + public string OrderId { get; } +} +``` + +We could introduce an `OrderCorrelationInfoOptions` model: + +```csharp +public class OrderCorrelationInfoOptions : CorrelationInfoOptions +{ + public bool IncludeOrderId { get; set; } +} +``` + +This custom options model can then be included when registering the correlation: + +```csharp +public class Startup +{ + public void ConfigureSerivces(IServiceCollection services) + { + services.AddCorrelation(options => options.IncludeOrderId = true); + } +} +``` + [← back](/) diff --git a/docs/features/telemetry-enrichment.md b/docs/features/telemetry-enrichment.md index 830c324a..9356a450 100644 --- a/docs/features/telemetry-enrichment.md +++ b/docs/features/telemetry-enrichment.md @@ -16,12 +16,12 @@ We provide a variety of enrichers for Serilog: This feature requires to install our NuGet package ```shell -PM > Install-Package Arcus.Observability.Telemetry.Serilog +PM > Install-Package Arcus.Observability.Telemetry.Serilog.Enrichers ``` ## Application Enricher -The `Arcus.Observability.Telemetry.Serilog` library provides a [Serilog enricher](https://github.com/serilog/serilog/wiki/Enrichment) +The `Arcus.Observability.Telemetry.Serilog.Enrichers` library provides a [Serilog enricher](https://github.com/serilog/serilog/wiki/Enrichment) that adds the application's component name to the log event as a log property with the name `ComponentName`. **Example** @@ -40,7 +40,7 @@ logger.Information("This event will be enriched with the application component's ## Kubernetes Enricher -The `Arcus.Observability.Telemetry.Serilog` library provides a [Kubernetes](https://kubernetes.io/) [Serilog enricher](https://github.com/serilog/serilog/wiki/Enrichment) +The `Arcus.Observability.Telemetry.Serilog.Enrichers` library provides a [Kubernetes](https://kubernetes.io/) [Serilog enricher](https://github.com/serilog/serilog/wiki/Enrichment) that adds several machine information from the environment (variables). **Example** @@ -108,7 +108,7 @@ spec: ## Version Enricher -The `Arcus.Observability.Telemetry.Serilog` library provides a [Serilog enricher](https://github.com/serilog/serilog/wiki/Enrichment) +The `Arcus.Observability.Telemetry.Serilog.Enrichers` library provides a [Serilog enricher](https://github.com/serilog/serilog/wiki/Enrichment) that adds the current runtime assembly version of the product to the log event as a log property with the name `version`. **Example** @@ -125,4 +125,42 @@ ILogger logger = new LoggerConfiguration() logger.Information("This event will be enriched with the runtime assembly product version"); ``` +## Correlation Enricher + +The `Arcus.ObservabilityTelemetry.Serilog.Enrichers` library provides a [Serilog enricher](https://github.com/serilog/serilog/wiki/Enrichment) +that adds the `CorrelationInfo` information from the current context as log properties with the names `OperationId` and `TransactionId`. + +You can use your own `ICorrelationInfoAccessor` implementation to retrieve this `CorrelationInfo` model, +or use the default `DefaultCorrelationInfoAccessor` implementation that stores this model + +**Example** +Name: `OperationId` +Value: `52EE2C00-53EE-476E-9DAB-C1234EB4AD0B` + +Name: `TransactionId` +Value: `0477E377-414D-47CD-8756-BCBE3DBE3ACB` + +**Usage** + +```csharp +ILogger logger = new LoggerConfiguration() + .Enrich.WithCorrelationInfo() + .CreateLogger(); + +logger.Information("This event will be enriched with the correlation information"); +``` + +Or alternatively, with a custom `ICorrelationInfoAccessor`: + +```csharp +ICorrelationInfoAccessor myCustomAccessor = ... + +ILogger logger = new LoggerConfiguration() + .Enrich.WithCorrelationInfo(myCustomAccessor) + .CreateLogger(); + +logger.Information("This event will be enriched with the correlation information"); +// Output: This event will be enriched with the correlation information {OperationId: 52EE2C00-53EE-476E-9DAB-C1234EB4AD0B, TransactionId: 0477E377-414D-47CD-8756-BCBE3DBE3ACB} +``` + [← back](/) diff --git a/src/Arcus.Observability.Correlation/Arcus.Observability.Correlation.csproj b/src/Arcus.Observability.Correlation/Arcus.Observability.Correlation.csproj index 0259d77d..38545f7e 100644 --- a/src/Arcus.Observability.Correlation/Arcus.Observability.Correlation.csproj +++ b/src/Arcus.Observability.Correlation/Arcus.Observability.Correlation.csproj @@ -17,6 +17,15 @@ true + + + + + + + + + diff --git a/src/Arcus.Observability.Correlation/CorrelationInfoAccessorProxy.cs b/src/Arcus.Observability.Correlation/CorrelationInfoAccessorProxy.cs new file mode 100644 index 00000000..a38e99c0 --- /dev/null +++ b/src/Arcus.Observability.Correlation/CorrelationInfoAccessorProxy.cs @@ -0,0 +1,44 @@ +using GuardNet; + +namespace Arcus.Observability.Correlation +{ + /// + /// Internal proxy implementation to register an as an . + /// + /// The custom model. + internal class CorrelationInfoAccessorProxy : ICorrelationInfoAccessor + where TCorrelationInfo : CorrelationInfo + { + private readonly ICorrelationInfoAccessor _correlationInfoAccessor; + + /// + /// Initializes a new instance of the class. + /// + internal CorrelationInfoAccessorProxy(ICorrelationInfoAccessor correlationInfoAccessor) + { + Guard.NotNull(correlationInfoAccessor, nameof(correlationInfoAccessor)); + + _correlationInfoAccessor = correlationInfoAccessor; + } + + /// + /// Gets the current correlation information initialized in this context. + /// + public CorrelationInfo GetCorrelationInfo() + { + return _correlationInfoAccessor.GetCorrelationInfo(); + } + + /// + /// Sets the current correlation information for this context. + /// + /// The correlation model to set. + public void SetCorrelationInfo(CorrelationInfo correlationInfo) + { + if (correlationInfo is TCorrelationInfo typedCorrelationInfo) + { + _correlationInfoAccessor.SetCorrelationInfo(typedCorrelationInfo); + } + } + } +} diff --git a/src/Arcus.Observability.Correlation/CorrelationInfoOperationOptions.cs b/src/Arcus.Observability.Correlation/CorrelationInfoOperationOptions.cs new file mode 100644 index 00000000..346c06e0 --- /dev/null +++ b/src/Arcus.Observability.Correlation/CorrelationInfoOperationOptions.cs @@ -0,0 +1,48 @@ +using System; +using GuardNet; + +namespace Arcus.Observability.Correlation +{ + /// + /// Correlation options specific for the operation ID. + /// + public class CorrelationInfoOperationOptions + { + private string _headerName = "RequestId"; + private Func _generateId = () => Guid.NewGuid().ToString(); + + /// + /// Gets or sets whether to include the operation ID in the response. + /// + /// + /// A common use case is to disable tracing info in edge services, so that such details are not exposed to the outside world. + /// + public bool IncludeInResponse { get; set; } = true; + + /// + /// Gets or sets the header that will contain the response operation ID. + /// + public string HeaderName + { + get => _headerName; + set + { + Guard.NotNullOrWhitespace(value, nameof(value), "Correlation operation header cannot be blank"); + _headerName = value; + } + } + + /// + /// Gets or sets the function to generate the operation ID when the is set to true (default: true). + /// + public Func GenerateId + { + get => _generateId; + set + { + Guard.NotNull(value, nameof(value), "Correlation function to generate an operation ID cannot be 'null'"); + _generateId = value; + } + } + } +} diff --git a/src/Arcus.Observability.Correlation/CorrelationInfoOptions.cs b/src/Arcus.Observability.Correlation/CorrelationInfoOptions.cs new file mode 100644 index 00000000..bc4e78a8 --- /dev/null +++ b/src/Arcus.Observability.Correlation/CorrelationInfoOptions.cs @@ -0,0 +1,18 @@ +namespace Arcus.Observability.Correlation +{ + /// + /// Options for handling correlation id on incoming requests. + /// + public class CorrelationInfoOptions + { + /// + /// Gets the correlation options specific for the transaction ID. + /// + public CorrelationInfoTransactionOptions Transaction { get; } = new CorrelationInfoTransactionOptions(); + + /// + /// Gets the correlation options specific for the operation ID. + /// + public CorrelationInfoOperationOptions Operation { get; } = new CorrelationInfoOperationOptions(); + } +} diff --git a/src/Arcus.Observability.Correlation/CorrelationInfoTransactionOptions.cs b/src/Arcus.Observability.Correlation/CorrelationInfoTransactionOptions.cs new file mode 100644 index 00000000..b570683a --- /dev/null +++ b/src/Arcus.Observability.Correlation/CorrelationInfoTransactionOptions.cs @@ -0,0 +1,61 @@ +using System; +using GuardNet; + +namespace Arcus.Observability.Correlation +{ + /// + /// Correlation options specific to the transaction ID. + /// + public class CorrelationInfoTransactionOptions + { + private string _headerName = "X-Transaction-ID"; + private Func _generateId = () => Guid.NewGuid().ToString(); + + /// + /// Get or sets whether the transaction ID can be specified in the request, and will be used throughout the request handling. + /// + public bool AllowInRequest { get; set; } = true; + + /// + /// Gets or sets whether or not the transaction ID should be generated when there isn't any transaction ID found in the request. + /// + public bool GenerateWhenNotSpecified { get; set; } = true; + + /// + /// Gets or sets whether to include the transaction ID in the response. + /// + /// + /// A common use case is to disable tracing info in edge services, so that such details are not exposed to the outside world. + /// + public bool IncludeInResponse { get; set; } = true; + + /// + /// Gets or sets the header that will contain the request/response transaction ID. + /// + public string HeaderName + { + get => _headerName; + set + { + Guard.NotNullOrWhitespace(value, nameof(value), "Correlation transaction header cannot be blank"); + _headerName = value; + } + } + + /// + /// Gets or sets the function to generate the transaction ID when + /// (1) the request doesn't have already an transaction ID, and + /// (2) the is set to true (default: true), and + /// (3) the is set to true (default: true). + /// + public Func GenerateId + { + get => _generateId; + set + { + Guard.NotNull(value, nameof(value), "Correlation function to generate an transaction ID cannot be 'null'"); + _generateId = value; + } + } + } +} diff --git a/src/Arcus.Observability.Correlation/DefaultCorrelationInfoAccessor.cs b/src/Arcus.Observability.Correlation/DefaultCorrelationInfoAccessor.cs new file mode 100644 index 00000000..b5034990 --- /dev/null +++ b/src/Arcus.Observability.Correlation/DefaultCorrelationInfoAccessor.cs @@ -0,0 +1,20 @@ +namespace Arcus.Observability.Correlation +{ + /// + /// Default implementation to access the in the current context. + /// + public class DefaultCorrelationInfoAccessor : DefaultCorrelationInfoAccessor, ICorrelationInfoAccessor + { + /// + /// Prevents a new instance of the class from being created. + /// + private DefaultCorrelationInfoAccessor() + { + } + + /// + /// Gets the default instance for the class. + /// + public new static DefaultCorrelationInfoAccessor Instance { get; } = new DefaultCorrelationInfoAccessor(); + } +} \ No newline at end of file diff --git a/src/Arcus.Observability.Correlation/DefaultCorrelationInfoAccessorT.cs b/src/Arcus.Observability.Correlation/DefaultCorrelationInfoAccessorT.cs new file mode 100644 index 00000000..be199740 --- /dev/null +++ b/src/Arcus.Observability.Correlation/DefaultCorrelationInfoAccessorT.cs @@ -0,0 +1,42 @@ +using System.Threading; + +namespace Arcus.Observability.Correlation +{ + /// + /// Default implementation to access the in the current context. + /// + public class DefaultCorrelationInfoAccessor : ICorrelationInfoAccessor + where TCorrelationInfo : CorrelationInfo + { + private static readonly AsyncLocal CorrelationInfoLocalData = new AsyncLocal(); + + /// + /// Prevents a new instance of the class from being created. + /// + private protected DefaultCorrelationInfoAccessor() + { + } + + /// + /// Gets the current correlation information initialized in this context. + /// + public TCorrelationInfo GetCorrelationInfo() + { + return CorrelationInfoLocalData.Value; + } + + /// + /// Sets the current correlation information for this context. + /// + /// The correlation model to set. + public void SetCorrelationInfo(TCorrelationInfo correlationInfo) + { + CorrelationInfoLocalData.Value = correlationInfo; + } + + /// + /// Gets the default instance for the class. + /// + public static DefaultCorrelationInfoAccessor Instance { get; } = new DefaultCorrelationInfoAccessor(); + } +} diff --git a/src/Arcus.Observability.Correlation/ICorrelationInfoAccessor.cs b/src/Arcus.Observability.Correlation/ICorrelationInfoAccessor.cs new file mode 100644 index 00000000..0a4d1dc6 --- /dev/null +++ b/src/Arcus.Observability.Correlation/ICorrelationInfoAccessor.cs @@ -0,0 +1,9 @@ +namespace Arcus.Observability.Correlation +{ + /// + /// Represents a contract to access the model. + /// + public interface ICorrelationInfoAccessor : ICorrelationInfoAccessor + { + } +} \ No newline at end of file diff --git a/src/Arcus.Observability.Correlation/ICorrelationInfoAccessorT.cs b/src/Arcus.Observability.Correlation/ICorrelationInfoAccessorT.cs new file mode 100644 index 00000000..9cb1ffe7 --- /dev/null +++ b/src/Arcus.Observability.Correlation/ICorrelationInfoAccessorT.cs @@ -0,0 +1,20 @@ +namespace Arcus.Observability.Correlation +{ + /// + /// Represents a contract to access the model. + /// + public interface ICorrelationInfoAccessor + where TCorrelationInfo : CorrelationInfo + { + /// + /// Gets the current correlation information initialized in this context. + /// + TCorrelationInfo GetCorrelationInfo(); + + /// + /// Sets the current correlation information for this context. + /// + /// The correlation model to set. + void SetCorrelationInfo(TCorrelationInfo correlationInfo); + } +} diff --git a/src/Arcus.Observability.Correlation/IServiceCollectionExtensions.cs b/src/Arcus.Observability.Correlation/IServiceCollectionExtensions.cs new file mode 100644 index 00000000..b2016e29 --- /dev/null +++ b/src/Arcus.Observability.Correlation/IServiceCollectionExtensions.cs @@ -0,0 +1,158 @@ +using System; +using Arcus.Observability.Correlation; +using GuardNet; + +// ReSharper disable once CheckNamespace +namespace Microsoft.Extensions.DependencyInjection +{ + /// + /// Extensions to set correlation access information on the . + /// + public static class IServiceCollectionExtensions + { + /// + /// Adds operation and transaction correlation to the application using the + /// + /// The services collection containing the dependency injection services. + /// The function to configure additional options how the correlation works. + public static IServiceCollection AddCorrelation( + this IServiceCollection services, + Action configureOptions = null) + { + Guard.NotNull(services, nameof(services)); + + return AddCorrelation(services, DefaultCorrelationInfoAccessor.Instance, configureOptions); + } + + /// + /// Adds operation and transaction correlation to the application using the + /// + /// The type of the model. + /// The services collection containing the dependency injection services. + /// The function to configure additional options how the correlation works. + public static IServiceCollection AddCorrelation( + this IServiceCollection services, + Action configureOptions = null) + where TCorrelationInfo : CorrelationInfo + { + Guard.NotNull(services, nameof(services)); + + return AddCorrelation(services, configureOptions); + } + + /// + /// Adds operation and transaction correlation to the application using the . + /// + /// The type of the model. + /// The type of the custom model. + /// The services collection containing the dependency injection services. + /// The function to configure additional options how the correlation works. + public static IServiceCollection AddCorrelation( + this IServiceCollection services, + Action configureOptions = null) + where TCorrelationInfo : CorrelationInfo + where TOptions : CorrelationInfoOptions + { + Guard.NotNull(services, nameof(services)); + + return AddCorrelation, TCorrelationInfo, TOptions>( + services, + serviceProvider => DefaultCorrelationInfoAccessor.Instance, + configureOptions); + } + + /// + /// Adds operation and transaction correlation to the application. + /// + /// The type of the implementation. + /// The services collection containing the dependency injection services. + /// The custom implementation to retrieve the . + /// The function to configure additional options how the correlation works. + public static IServiceCollection AddCorrelation( + this IServiceCollection services, + TAccessor customCorrelationAccessor, + Action configureOptions = null) + where TAccessor : class, ICorrelationInfoAccessor + { + Guard.NotNull(services, nameof(services)); + Guard.NotNull(customCorrelationAccessor, nameof(customCorrelationAccessor)); + + return AddCorrelation(services, serviceProvider => customCorrelationAccessor, configureOptions); + } + + /// + /// Adds operation and transaction correlation to the application. + /// + /// The type of the implementation. + /// The services collection containing the dependency injection services. + /// The custom implementation factory to retrieve the . + /// The function to configure additional options how the correlation works. + public static IServiceCollection AddCorrelation( + this IServiceCollection services, + Func createCustomCorrelationAccessor, + Action configureOptions = null) + where TAccessor : class, ICorrelationInfoAccessor + { + Guard.NotNull(services, nameof(services)); + Guard.NotNull(createCustomCorrelationAccessor, nameof(createCustomCorrelationAccessor)); + + return AddCorrelation(services, createCustomCorrelationAccessor, configureOptions); + } + + /// + /// Adds operation and transaction correlation to the application. + /// + /// The type of the implementation. + /// The type of the custom model. + /// The services collection containing the dependency injection services. + /// The custom implementation factory to retrieve the . + /// The function to configure additional options how the correlation works. + public static IServiceCollection AddCorrelation( + this IServiceCollection services, + Func createCustomCorrelationAccessor, + Action configureOptions = null) + where TAccessor : class, ICorrelationInfoAccessor + where TCorrelationInfo : CorrelationInfo + { + Guard.NotNull(services, nameof(services)); + Guard.NotNull(createCustomCorrelationAccessor, nameof(createCustomCorrelationAccessor)); + + return AddCorrelation(services, createCustomCorrelationAccessor, configureOptions); + } + + /// + /// Adds operation and transaction correlation to the application. + /// + /// The type of the implementation. + /// The type of the custom model. + /// The type of the custom model. + /// The services collection containing the dependency injection services. + /// The custom implementation factory to retrieve the . + /// The function to configure additional options how the correlation works. + public static IServiceCollection AddCorrelation( + this IServiceCollection services, + Func createCustomCorrelationAccessor, + Action configureOptions = null) + where TAccessor : class, ICorrelationInfoAccessor + where TCorrelationInfo : CorrelationInfo + where TOptions : CorrelationInfoOptions + { + Guard.NotNull(services, nameof(services)); + Guard.NotNull(createCustomCorrelationAccessor, nameof(createCustomCorrelationAccessor)); + + services.AddSingleton>(createCustomCorrelationAccessor); + services.AddSingleton(serviceProvider => + { + return new CorrelationInfoAccessorProxy( + serviceProvider.GetRequiredService>()); + }); + + if (configureOptions != null) + { + services.Configure(configureOptions); + } + + return services; + } + } +} \ No newline at end of file diff --git a/src/Arcus.Observability.Telemetry.Core/Arcus.Observability.Telemetry.Core.csproj b/src/Arcus.Observability.Telemetry.Core/Arcus.Observability.Telemetry.Core.csproj index 2ca6bb68..33bc45d4 100644 --- a/src/Arcus.Observability.Telemetry.Core/Arcus.Observability.Telemetry.Core.csproj +++ b/src/Arcus.Observability.Telemetry.Core/Arcus.Observability.Telemetry.Core.csproj @@ -19,10 +19,17 @@ Arcus.Observability.Telemetry.Core + + + + + + + + + - - diff --git a/src/Arcus.Observability.Telemetry.Core/ContextProperties.cs b/src/Arcus.Observability.Telemetry.Core/ContextProperties.cs index 1a2fe625..96349d2f 100644 --- a/src/Arcus.Observability.Telemetry.Core/ContextProperties.cs +++ b/src/Arcus.Observability.Telemetry.Core/ContextProperties.cs @@ -6,6 +6,7 @@ public static class ContextProperties public static class Correlation { public const string OperationId = "OperationId"; + public const string TransactionId = "TransactionId"; } public static class DependencyTracking diff --git a/src/Arcus.Observability.Telemetry.Serilog.Enrichers/Arcus.Observability.Telemetry.Serilog.Enrichers.csproj b/src/Arcus.Observability.Telemetry.Serilog.Enrichers/Arcus.Observability.Telemetry.Serilog.Enrichers.csproj index 20466298..7ea4a79e 100644 --- a/src/Arcus.Observability.Telemetry.Serilog.Enrichers/Arcus.Observability.Telemetry.Serilog.Enrichers.csproj +++ b/src/Arcus.Observability.Telemetry.Serilog.Enrichers/Arcus.Observability.Telemetry.Serilog.Enrichers.csproj @@ -23,6 +23,7 @@ + diff --git a/src/Arcus.Observability.Telemetry.Serilog.Enrichers/CorrelationInfoEnricher.cs b/src/Arcus.Observability.Telemetry.Serilog.Enrichers/CorrelationInfoEnricher.cs new file mode 100644 index 00000000..24d49b68 --- /dev/null +++ b/src/Arcus.Observability.Telemetry.Serilog.Enrichers/CorrelationInfoEnricher.cs @@ -0,0 +1,71 @@ +using System; +using Arcus.Observability.Correlation; +using Arcus.Observability.Telemetry.Core; +using GuardNet; +using Serilog.Core; +using Serilog.Events; + +namespace Arcus.Observability.Telemetry.Serilog.Enrichers +{ + /// + /// Enriches the log events with the correlation information. + /// + public class CorrelationInfoEnricher : ILogEventEnricher where TCorrelationInfo : CorrelationInfo + { + /// + /// Initializes a new instance of the class. + /// + /// The accessor implementation for the custom model. + public CorrelationInfoEnricher(ICorrelationInfoAccessor correlationInfoAccessor) + { + Guard.NotNull(correlationInfoAccessor, nameof(correlationInfoAccessor)); + + CorrelationInfoAccessor = correlationInfoAccessor; + } + + /// + /// Gets the that provides the correlation model. + /// + protected ICorrelationInfoAccessor CorrelationInfoAccessor { get; } + + /// + /// Enrich the log event. + /// + /// The log event to enrich. + /// Factory for creating new properties to add to the event. + public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory) + { + Guard.NotNull(logEvent, nameof(logEvent)); + Guard.NotNull(propertyFactory, nameof(propertyFactory)); + + TCorrelationInfo correlationInfo = CorrelationInfoAccessor.GetCorrelationInfo(); + if (correlationInfo is null) + { + return; + } + + EnrichCorrelationInfo(logEvent, propertyFactory, correlationInfo); + } + + /// + /// Enrich the with the given model. + /// + /// The log event to enrich with correlation information. + /// The log property factory to create log properties with correlation information. + /// The correlation model that contains the current correlation information. + protected virtual void EnrichCorrelationInfo(LogEvent logEvent, ILogEventPropertyFactory propertyFactory, TCorrelationInfo correlationInfo) + { + if (!String.IsNullOrEmpty(correlationInfo.OperationId)) + { + LogEventProperty property = propertyFactory.CreateProperty(ContextProperties.Correlation.OperationId, correlationInfo.OperationId); + logEvent.AddPropertyIfAbsent(property); + } + + if (!String.IsNullOrEmpty(correlationInfo.TransactionId)) + { + LogEventProperty property = propertyFactory.CreateProperty(ContextProperties.Correlation.TransactionId, correlationInfo.TransactionId); + logEvent.AddPropertyIfAbsent(property); + } + } + } +} diff --git a/src/Arcus.Observability.Telemetry.Serilog.Enrichers/Extensions/LoggerEnrichmentConfigurationExtensions.cs b/src/Arcus.Observability.Telemetry.Serilog.Enrichers/Extensions/LoggerEnrichmentConfigurationExtensions.cs index 6647ee0c..97d0138e 100644 --- a/src/Arcus.Observability.Telemetry.Serilog.Enrichers/Extensions/LoggerEnrichmentConfigurationExtensions.cs +++ b/src/Arcus.Observability.Telemetry.Serilog.Enrichers/Extensions/LoggerEnrichmentConfigurationExtensions.cs @@ -1,6 +1,10 @@ -using Arcus.Observability.Telemetry.Serilog.Enrichers; +using System; +using Arcus.Observability.Correlation; +using Arcus.Observability.Telemetry.Serilog.Enrichers; using GuardNet; using Serilog.Configuration; +using Serilog.Core; +using Serilog.Events; // ReSharper disable once CheckNamespace namespace Serilog @@ -44,5 +48,75 @@ public static LoggerConfiguration WithKubernetesInfo(this LoggerEnrichmentConfig return enrichmentConfiguration.With(); } + + /// + /// Adds the to the logger enrichment configuration which adds the information from the current context. + /// + /// The configuration to add the enricher. + public static LoggerConfiguration WithCorrelationInfo(this LoggerEnrichmentConfiguration enrichmentConfiguration) + { + Guard.NotNull(enrichmentConfiguration, nameof(enrichmentConfiguration)); + + return WithCorrelationInfo(enrichmentConfiguration, DefaultCorrelationInfoAccessor.Instance); + } + + /// + /// Adds the to the logger enrichment configuration which adds the information from the current context. + /// + /// The configuration to add the enricher. + public static LoggerConfiguration WithCorrelationInfo(this LoggerEnrichmentConfiguration enrichmentConfiguration) + where TCorrelationInfo : CorrelationInfo + { + Guard.NotNull(enrichmentConfiguration, nameof(enrichmentConfiguration)); + + return WithCorrelationInfo(enrichmentConfiguration, DefaultCorrelationInfoAccessor.Instance); + } + + /// + /// Adds the to the logger enrichment configuration which adds the information from the current context. + /// + /// The configuration to add the enricher. + /// The accessor implementation for the model. + public static LoggerConfiguration WithCorrelationInfo(this LoggerEnrichmentConfiguration enrichmentConfiguration, ICorrelationInfoAccessor correlationInfoAccessor) + { + Guard.NotNull(enrichmentConfiguration, nameof(enrichmentConfiguration)); + Guard.NotNull(correlationInfoAccessor, nameof(correlationInfoAccessor)); + + return WithCorrelationInfo(enrichmentConfiguration, correlationInfoAccessor); + } + + /// + /// Adds the to the logger enrichment configuration which adds the custom information from the current context. + /// + /// The type of the custom model. + /// The configuration to add the enricher. + /// The accessor implementation for the model. + public static LoggerConfiguration WithCorrelationInfo( + this LoggerEnrichmentConfiguration enrichmentConfiguration, + ICorrelationInfoAccessor correlationInfoAccessor) + where TCorrelationInfo : CorrelationInfo + { + Guard.NotNull(enrichmentConfiguration, nameof(enrichmentConfiguration)); + Guard.NotNull(correlationInfoAccessor, nameof(correlationInfoAccessor)); + + return enrichmentConfiguration.With(new CorrelationInfoEnricher(correlationInfoAccessor)); + } + + /// + /// Adds the to the logger enrichment configuration which adds the custom information from the current context. + /// + /// The type of the custom model. + /// The configuration to add the enricher. + /// The custom correlation enricher implementation for the model. + public static LoggerConfiguration WithCorrelationInfo( + this LoggerEnrichmentConfiguration enrichmentConfiguration, + CorrelationInfoEnricher correlationInfoEnricher) + where TCorrelationInfo : CorrelationInfo + { + Guard.NotNull(enrichmentConfiguration, nameof(enrichmentConfiguration)); + Guard.NotNull(correlationInfoEnricher, nameof(correlationInfoEnricher)); + + return enrichmentConfiguration.With(correlationInfoEnricher); + } } } diff --git a/src/Arcus.Observability.Tests.Unit/Arcus.Observability.Tests.Unit.csproj b/src/Arcus.Observability.Tests.Unit/Arcus.Observability.Tests.Unit.csproj index a2fd1f9b..1cd436e5 100644 --- a/src/Arcus.Observability.Tests.Unit/Arcus.Observability.Tests.Unit.csproj +++ b/src/Arcus.Observability.Tests.Unit/Arcus.Observability.Tests.Unit.csproj @@ -1,4 +1,4 @@ - + netcoreapp3.0 @@ -7,6 +7,7 @@ + diff --git a/src/Arcus.Observability.Tests.Unit/Correlation/IServiceCollectionExtensionsTests.cs b/src/Arcus.Observability.Tests.Unit/Correlation/IServiceCollectionExtensionsTests.cs new file mode 100644 index 00000000..79ff49df --- /dev/null +++ b/src/Arcus.Observability.Tests.Unit/Correlation/IServiceCollectionExtensionsTests.cs @@ -0,0 +1,173 @@ +using System; +using Arcus.Observability.Correlation; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Moq; +using Xunit; + +namespace Arcus.Observability.Tests.Unit.Correlation +{ + [Trait("Category", "Unit")] + public class IServiceCollectionExtensionsTests + { + [Fact] + public void AddCorrelation_DefaultSetup_RegistersDefaultAccessor() + { + // Arrange + var services = new ServiceCollection(); + services.AddCorrelation(); + + // Act + IServiceProvider serviceProvider = services.BuildServiceProvider(); + + // Assert + var correlationInfoAccessor = serviceProvider.GetService(); + Assert.NotNull(correlationInfoAccessor); + } + + [Fact] + public void AddCorrelation_DefaultSetupWithOptions_RegistersDefaultAccessorWithOptions() + { + // Arrange + var services = new ServiceCollection(); + services.AddCorrelation(options => options.Operation.IncludeInResponse = true); + + // Act + IServiceProvider serviceProvider = services.BuildServiceProvider(); + + // Assert + var correlationInfoAccessor = serviceProvider.GetService(); + Assert.NotNull(correlationInfoAccessor); + Assert.NotNull(serviceProvider.GetService>()); + } + + [Fact] + public void AddCorrelation_CustomAccessor_RegistersDefaultAccessor() + { + // Arrange + var services = new ServiceCollection(); + var stubAccessor = Mock.Of(); + services.AddCorrelation(stubAccessor); + + // Act + IServiceProvider serviceProvider = services.BuildServiceProvider(); + + // Assert + var correlationInfoAccessor = serviceProvider.GetService(); + Assert.NotNull(correlationInfoAccessor); + } + + [Fact] + public void AddCorrelation_CustomAccessorWithOptions_RegistersCustomAccessorWithOptions() + { + // Arrange + var services = new ServiceCollection(); + var stubAccessor = Mock.Of(); + services.AddCorrelation(stubAccessor, options => options.Transaction.IncludeInResponse = true); + + // Act + IServiceProvider serviceProvider = services.BuildServiceProvider(); + + // Assert + var correlationInfoAccessor = serviceProvider.GetService(); + Assert.NotNull(correlationInfoAccessor); + Assert.NotNull(serviceProvider.GetService>()); + } + + [Fact] + public void AddCorrelation_CustomAccessorFactory_RegistersCustomAccessor() + { + // Arrange + var services = new ServiceCollection(); + var stubAccessor = Mock.Of(); + services.AddCorrelation(provider => stubAccessor); + + // Act + IServiceProvider serviceProvider = services.BuildServiceProvider(); + + // Assert + var correlationInfoAccessor = serviceProvider.GetService(); + Assert.NotNull(correlationInfoAccessor); + } + + [Fact] + public void AddCorrelation_CustomAccessorFactoryWithOptions_RegistersCustomAccessorWithOptions() + { + // Arrange + var services = new ServiceCollection(); + var stubAccessor = Mock.Of(); + services.AddCorrelation(provider => stubAccessor, options => options.Operation.IncludeInResponse = true); + + // Act + IServiceProvider serviceProvider = services.BuildServiceProvider(); + + // Assert + var correlationInfoAccessor = serviceProvider.GetService(); + Assert.NotNull(correlationInfoAccessor); + Assert.NotNull(serviceProvider.GetService>()); + } + + [Fact] + public void AddCorrelation_CustomCorrelationFactory_RegisterCustomCorrelation() + { + // Arrange + var services = new ServiceCollection(); + var testCorrelationInfoAccessor = new TestCorrelationInfoAccessor(); + services.AddCorrelation(provider => testCorrelationInfoAccessor); + + // Act + IServiceProvider serviceProvider = services.BuildServiceProvider(); + + // Assert + var correlationInfoAccessor = serviceProvider.GetService(); + Assert.NotNull(correlationInfoAccessor); + var correlationInfoAccessorT = serviceProvider.GetService>(); + Assert.NotNull(correlationInfoAccessorT); + Assert.Same(testCorrelationInfoAccessor, correlationInfoAccessorT); + } + + [Fact] + public void AddCorrelation_CustomCorrelationInfoOptionsWithDefaultAccessor_RegisterCustomOptions() + { + // Arrange + string expectedTestOption = $"test-option-{Guid.NewGuid()}"; + var services = new ServiceCollection(); + services.AddCorrelation(options => options.TestOption = expectedTestOption); + + // Act + IServiceProvider serviceProvider = services.BuildServiceProvider(); + + // Assert + Assert.NotNull(serviceProvider.GetService()); + Assert.NotNull(serviceProvider.GetService>()); + var testOptions = serviceProvider.GetService>(); + Assert.NotNull(testOptions); + Assert.NotNull(testOptions.Value); + Assert.Equal(expectedTestOption, testOptions.Value.TestOption); + } + + [Fact] + public void AddCorrelation_CustomCorrelationAccessorWithCustomOptions_RegisterCustomAccessorAndOptions() + { + // Arrange + string expectedTestOption = $"test-option-{Guid.NewGuid()}"; + var testCorrelationInfoProvider = new TestCorrelationInfoAccessor(); + var services = new ServiceCollection(); + services.AddCorrelation( + provider => testCorrelationInfoProvider, + options => options.TestOption = expectedTestOption); + + // Act + IServiceProvider serviceProvider = services.BuildServiceProvider(); + + // Assert + Assert.NotNull(serviceProvider.GetService()); + var correlationInfoAccessor = serviceProvider.GetService>(); + Assert.Same(testCorrelationInfoProvider, correlationInfoAccessor); + var testOptions = serviceProvider.GetService>(); + Assert.NotNull(testOptions); + Assert.NotNull(testOptions.Value); + Assert.Equal(expectedTestOption, testOptions.Value.TestOption); + } + } +} diff --git a/src/Arcus.Observability.Tests.Unit/Correlation/TestCorrelationInfo.cs b/src/Arcus.Observability.Tests.Unit/Correlation/TestCorrelationInfo.cs new file mode 100644 index 00000000..83f207d8 --- /dev/null +++ b/src/Arcus.Observability.Tests.Unit/Correlation/TestCorrelationInfo.cs @@ -0,0 +1,24 @@ +using Arcus.Observability.Correlation; + +namespace Arcus.Observability.Tests.Unit.Correlation +{ + /// + /// Test model to extend the . + /// + public class TestCorrelationInfo : CorrelationInfo + { + /// + /// Initializes a new instance of the class. + /// + public TestCorrelationInfo(string operationId, string transactionId, string testId) + : base(operationId, transactionId) + { + TestId = testId; + } + + /// + /// Gets the test identifier for this correlation information model. + /// + public string TestId { get; } + } +} \ No newline at end of file diff --git a/src/Arcus.Observability.Tests.Unit/Correlation/TestCorrelationInfoAccessor.cs b/src/Arcus.Observability.Tests.Unit/Correlation/TestCorrelationInfoAccessor.cs new file mode 100644 index 00000000..a68e929d --- /dev/null +++ b/src/Arcus.Observability.Tests.Unit/Correlation/TestCorrelationInfoAccessor.cs @@ -0,0 +1,30 @@ +using System; +using Arcus.Observability.Correlation; + +namespace Arcus.Observability.Tests.Unit.Correlation +{ + /// + /// Test implementation to access the model + /// + public class TestCorrelationInfoAccessor : ICorrelationInfoAccessor + { + private TestCorrelationInfo _correlationInfo; + + /// + /// Gets the current correlation information initialized in this context. + /// + public TestCorrelationInfo GetCorrelationInfo() + { + return _correlationInfo; + } + + /// + /// Sets the current correlation information for this context. + /// + /// The correlation model to set. + public void SetCorrelationInfo(TestCorrelationInfo correlationInfo) + { + _correlationInfo = correlationInfo; + } + } +} \ No newline at end of file diff --git a/src/Arcus.Observability.Tests.Unit/Correlation/TestCorrelationInfoOptions.cs b/src/Arcus.Observability.Tests.Unit/Correlation/TestCorrelationInfoOptions.cs new file mode 100644 index 00000000..972672a0 --- /dev/null +++ b/src/Arcus.Observability.Tests.Unit/Correlation/TestCorrelationInfoOptions.cs @@ -0,0 +1,15 @@ +using Arcus.Observability.Correlation; + +namespace Arcus.Observability.Tests.Unit.Correlation +{ + /// + /// Test implementation of the . + /// + public class TestCorrelationInfoOptions : CorrelationInfoOptions + { + /// + /// Gets or sets the test option on this testing variant. + /// + public string TestOption { get; set; } + } +} \ No newline at end of file diff --git a/src/Arcus.Observability.Tests.Unit/Serilog/CorrelationInfoEnricherTests.cs b/src/Arcus.Observability.Tests.Unit/Serilog/CorrelationInfoEnricherTests.cs new file mode 100644 index 00000000..a74ad267 --- /dev/null +++ b/src/Arcus.Observability.Tests.Unit/Serilog/CorrelationInfoEnricherTests.cs @@ -0,0 +1,142 @@ +using System; +using Arcus.Observability.Correlation; +using Arcus.Observability.Telemetry.Core; +using Arcus.Observability.Telemetry.Serilog.Enrichers; +using Arcus.Observability.Tests.Unit.Correlation; +using Moq; +using Serilog; +using Serilog.Events; +using Xunit; + +namespace Arcus.Observability.Tests.Unit.Serilog +{ + [Trait("Category", "Unit")] + public class CorrelationInfoEnricherTests + { + [Fact] + public void LogEvent_WithDefaultCorrelationInfoAccessor_HasOperationIdAndTransactionId() + { + // Arrange + string expectedOperationId = $"operation-{Guid.NewGuid()}"; + string expectedTransactionId = $"transaction-{Guid.NewGuid()}"; + + var spySink = new InMemoryLogSink(); + var correlationInfoAccessor = DefaultCorrelationInfoAccessor.Instance; + correlationInfoAccessor.SetCorrelationInfo(new CorrelationInfo(expectedOperationId, expectedTransactionId)); + + ILogger logger = new LoggerConfiguration() + .Enrich.WithCorrelationInfo() + .WriteTo.Sink(spySink) + .CreateLogger(); + + // Act + logger.Information("This message will be enriched with correlation information"); + + // Assert + LogEvent logEvent = Assert.Single(spySink.CurrentLogEmits); + Assert.True( + logEvent.ContainsProperty(ContextProperties.Correlation.OperationId, expectedOperationId), + $"Expected to have a log property operation ID '{ContextProperties.Correlation.OperationId}' with the value '{expectedOperationId}'"); + Assert.True( + logEvent.ContainsProperty(ContextProperties.Correlation.TransactionId, expectedTransactionId), + $"Expected to have a log property transaction ID '{ContextProperties.Correlation.TransactionId}' with the value '{expectedTransactionId}'"); + } + + [Fact] + public void LogEvent_WithDefaultCorrelationInfoAccessorT_HasOperationIdAndTransactionId() + { + // Arrange + string expectedOperationId = $"operation-{Guid.NewGuid()}"; + string expectedTransactionId = $"transaction-{Guid.NewGuid()}"; + string expectedTestId = $"test-{Guid.NewGuid()}"; + + var spySink = new InMemoryLogSink(); + var correlationInfoAccessor = DefaultCorrelationInfoAccessor.Instance; + correlationInfoAccessor.SetCorrelationInfo(new TestCorrelationInfo(expectedOperationId, expectedTransactionId, expectedTestId)); + + ILogger logger = new LoggerConfiguration() + .Enrich.WithCorrelationInfo() + .WriteTo.Sink(spySink) + .CreateLogger(); + + // Act + logger.Information("This message will be enriched with correlation information"); + + // Assert + LogEvent logEvent = Assert.Single(spySink.CurrentLogEmits); + Assert.True( + logEvent.ContainsProperty(ContextProperties.Correlation.OperationId, expectedOperationId), + $"Expected to have a log property operation ID '{ContextProperties.Correlation.OperationId}' with the value '{expectedOperationId}'"); + Assert.True( + logEvent.ContainsProperty(ContextProperties.Correlation.TransactionId, expectedTransactionId), + $"Expected to have a log property transaction ID '{ContextProperties.Correlation.TransactionId}' with the value '{expectedTransactionId}'"); + Assert.False( + logEvent.ContainsProperty(TestCorrelationInfoEnricher.TestId, expectedTestId), + $"Expected to have a log property test ID '{TestCorrelationInfoEnricher.TestId}' with the value '{expectedTestId}'"); + } + + [Fact] + public void LogEvent_WithCorrelationInfo_HasOperationIdAndTransactionId() + { + // Arrange + string expectedOperationId = $"operation-{Guid.NewGuid()}"; + string expectedTransactionId = $"transaction-{Guid.NewGuid()}"; + + var spySink = new InMemoryLogSink(); + var stubAccessor = new Mock(); + stubAccessor.Setup(accessor => accessor.GetCorrelationInfo()) + .Returns(new CorrelationInfo(expectedOperationId, expectedTransactionId)); + + ILogger logger = new LoggerConfiguration() + .Enrich.WithCorrelationInfo(stubAccessor.Object) + .WriteTo.Sink(spySink) + .CreateLogger(); + + // Act + logger.Information("This message will be enriched with correlation information"); + + // Assert + LogEvent logEvent = Assert.Single(spySink.CurrentLogEmits); + Assert.True( + logEvent.ContainsProperty(ContextProperties.Correlation.OperationId, expectedOperationId), + $"Expected to have a log property operation ID '{ContextProperties.Correlation.OperationId}' with the value '{expectedOperationId}'"); + Assert.True( + logEvent.ContainsProperty(ContextProperties.Correlation.TransactionId, expectedTransactionId), + $"Expected to have a log property transaction ID '{ContextProperties.Correlation.TransactionId}' with the value '{expectedTransactionId}'"); + } + + [Fact] + public void LogEvent_WithCustomCorrelationInfoEnricher_HasCustomEnrichedInformation() + { + // Arrange + string expectedOperationId = $"operation-{Guid.NewGuid()}"; + string expectedTransactionId = $"transaction-{Guid.NewGuid()}"; + string expectedTestId = $"test-{Guid.NewGuid()}"; + + var spySink = new InMemoryLogSink(); + var stubAccessor = new Mock>(); + stubAccessor.Setup(accessor => accessor.GetCorrelationInfo()) + .Returns(new TestCorrelationInfo(expectedOperationId, expectedTransactionId, expectedTestId)); + + ILogger logger = new LoggerConfiguration() + .Enrich.WithCorrelationInfo(new TestCorrelationInfoEnricher(stubAccessor.Object)) + .WriteTo.Sink(spySink) + .CreateLogger(); + + // Act + logger.Information("This message will be enriched with custom correlation information"); + + // Assert + LogEvent logEvent = Assert.Single(spySink.CurrentLogEmits); + Assert.False( + logEvent.ContainsProperty(ContextProperties.Correlation.OperationId, expectedOperationId), + $"Expected not to have a log property operation ID '{ContextProperties.Correlation.OperationId}' with the value '{expectedOperationId}'"); + Assert.False( + logEvent.ContainsProperty(ContextProperties.Correlation.TransactionId, expectedTransactionId), + $"Expected not to have a log property transaction ID '{ContextProperties.Correlation.TransactionId}' with the value '{expectedTransactionId}'"); + Assert.True( + logEvent.ContainsProperty(TestCorrelationInfoEnricher.TestId, expectedTestId), + $"Expected to have a log property test ID '{TestCorrelationInfoEnricher.TestId}' with the value '{expectedTestId}'"); + } + } +} diff --git a/src/Arcus.Observability.Tests.Unit/Serilog/TestCorrelationInfoEnricher.cs b/src/Arcus.Observability.Tests.Unit/Serilog/TestCorrelationInfoEnricher.cs new file mode 100644 index 00000000..7d80a391 --- /dev/null +++ b/src/Arcus.Observability.Tests.Unit/Serilog/TestCorrelationInfoEnricher.cs @@ -0,0 +1,40 @@ +using Arcus.Observability.Correlation; +using Arcus.Observability.Telemetry.Serilog.Enrichers; +using Arcus.Observability.Tests.Unit.Correlation; +using Serilog.Core; +using Serilog.Events; + +namespace Arcus.Observability.Tests.Unit.Serilog +{ + /// + /// Test implementation for the using the test model. + /// + public class TestCorrelationInfoEnricher : CorrelationInfoEnricher + { + public const string TestId = "TestId"; + + /// + /// Initializes a new instance of the class. + /// + /// The accessor implementation for the custom model. + public TestCorrelationInfoEnricher(ICorrelationInfoAccessor correlationInfoAccessor) : + base(correlationInfoAccessor) + { + } + + /// + /// Enrich the with the given model. + /// + /// The log event to enrich with correlation information. + /// The log property factory to create log properties with correlation information. + /// The correlation model that contains the current correlation information. + protected override void EnrichCorrelationInfo( + LogEvent logEvent, + ILogEventPropertyFactory propertyFactory, + TestCorrelationInfo correlationInfo) + { + LogEventProperty property = propertyFactory.CreateProperty(TestId, correlationInfo.TestId); + logEvent.AddPropertyIfAbsent(property); + } + } +} \ No newline at end of file