/// <summary> /// Correlate the current HTTP request according to the previously configured <see cref="CorrelationInfoOptions"/>; /// returning an <paramref name="errorMessage"/> when the correlation failed. /// </summary> /// <param name="errorMessage">The failure message that describes why the correlation of the HTTP request wasn't successful.</param> /// <returns> /// <para>[true] when the HTTP request was successfully correlated and the HTTP response was altered accordingly;</para> /// <para>[false] there was a problem with the correlation, describing the failure in the <paramref name="errorMessage"/>.</para> /// </returns> /// <exception cref="ArgumentNullException">Thrown when the given <see cref="HttpContext"/> is not available to correlate the request with the response.</exception> /// <exception cref="ArgumentException">Thrown when the given <see cref="HttpContext"/> doesn't have any response headers to set the correlation headers.</exception> public bool TryHttpCorrelate(out string errorMessage) { HttpContext httpContext = _httpContextAccessor.HttpContext; Guard.NotNull(httpContext, nameof(httpContext), "Requires a HTTP context from the HTTP context accessor to start correlating the HTTP request"); Guard.For <ArgumentException>(() => httpContext.Response is null, "Requires a 'Response'"); Guard.For <ArgumentException>(() => httpContext.Response.Headers is null, "Requires a 'Response' object with headers"); if (httpContext.Request.Headers.TryGetValue(_options.Transaction.HeaderName, out StringValues transactionIds)) { if (!_options.Transaction.AllowInRequest) { _logger.LogError("No correlation request header '{HeaderName}' for transaction ID was allowed in request", _options.Transaction.HeaderName); errorMessage = $"No correlation transaction ID request header '{_options.Transaction.HeaderName}' was allowed in the request"; return(false); } _logger.LogTrace("Correlation request header '{HeaderName}' found with transaction ID '{TransactionId}'", _options.Transaction.HeaderName, transactionIds); } string operationId = DetermineOperationId(httpContext); string transactionId = DetermineTransactionId(transactionIds); var correlation = new CorrelationInfo(operationId, transactionId); httpContext.Features.Set(correlation); AddCorrelationResponseHeaders(httpContext); errorMessage = null; return(true); }
/// <summary>Request handling method.</summary> /// <param name="httpContext">The <see cref="T:Microsoft.AspNetCore.Http.HttpContext" /> for the current request.</param> /// <returns>A <see cref="T:System.Threading.Tasks.Task" /> that represents the execution of this middleware.</returns> public async Task Invoke(HttpContext httpContext) { Guard.NotNull(httpContext, nameof(httpContext)); Guard.For <ArgumentException>(() => httpContext.Response is null, "Requires a 'Response'"); Guard.For <ArgumentException>(() => httpContext.Response.Headers is null, "Requires a 'Response' object with headers"); if (httpContext.Request.Headers.TryGetValue(_options.Transaction.HeaderName, out StringValues transactionIds)) { if (!_options.Transaction.AllowInRequest) { _logger.LogError("No correlation request header '{HeaderName}' for transaction ID was allowed in request", _options.Transaction.HeaderName); httpContext.Response.StatusCode = StatusCodes.Status400BadRequest; await httpContext.Response.WriteAsync($"No correlation transaction ID request header '{_options.Transaction.HeaderName}' was allowed in the request"); return; } _logger.LogTrace("Correlation request header '{HeaderName}' found with transaction ID '{TransactionId}'", _options.Transaction.HeaderName, transactionIds); } string operationId = DetermineOperationId(httpContext); string transactionId = DetermineTransactionId(transactionIds); var correlation = new CorrelationInfo(operationId, transactionId); httpContext.Features.Set(correlation); AddCorrelationResponseHeaders(httpContext, operationId, transactionId); await _next(httpContext); }
public async Task SendRequest_WithSerilogCorrelationenrichment_ReturnsOkWithDifferentOperationIdAndDifferentTransactionId() { // Arrange using (HttpClient client = _testServer.CreateClient()) // Act using (HttpResponseMessage firstResponse = await client.GetAsync(Route)) { // Assert Assert.Equal(HttpStatusCode.OK, firstResponse.StatusCode); CorrelationInfo firstCorrelationInfo = await AssertAppCorrelationInfoAsync(firstResponse); AssertLoggedCorrelationProperties(firstCorrelationInfo); using (HttpResponseMessage secondResponse = await client.GetAsync(Route)) { Assert.Equal(HttpStatusCode.OK, secondResponse.StatusCode); CorrelationInfo secondCorrelationInfo = await AssertAppCorrelationInfoAsync(secondResponse); AssertLoggedCorrelationProperties(secondCorrelationInfo); Assert.NotEqual(firstCorrelationInfo.OperationId, secondCorrelationInfo.OperationId); Assert.NotEqual(firstCorrelationInfo.TransactionId, secondCorrelationInfo.TransactionId); } } }
public async Task SendRequestWithCorrelateInfo_SetsCorrelationInfo_ResponseWithUpdatedCorrelationInfo() { // Arrange var correlationInfo = new CorrelationInfo($"operation-{Guid.NewGuid()}", $"transaction-{Guid.NewGuid()}"); var options = new TestApiServerOptions() .ConfigureServices(services => services.AddHttpCorrelation()) .PreConfigure(app => app.UseHttpCorrelation()); await using (var server = await TestApiServer.StartNewAsync(options, _logger)) { var request = HttpRequestBuilder.Post(CorrelationController.SetCorrelationRoute) .WithHeader(DefaultOperationId, correlationInfo.OperationId) .WithHeader(DefaultTransactionId, correlationInfo.TransactionId); // Act using (HttpResponseMessage response = await server.SendAsync(request)) { // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); string actualOperationId = GetResponseHeader(response, DefaultOperationId); string actualTransactionId = GetResponseHeader(response, DefaultTransactionId); Assert.Equal(correlationInfo.OperationId, actualOperationId); Assert.Equal(correlationInfo.TransactionId, actualTransactionId); } } }
public async Task SendRequestWithCorrelateInfo_SetsCorrelationInfo_ResponseWithUpdatedCorrelationInfo() { // Arrange var correlationInfo = new CorrelationInfo($"operation-{Guid.NewGuid()}", $"transaction-{Guid.NewGuid()}"); using (HttpClient client = _testServer.CreateClient()) using (var request = new HttpRequestMessage(HttpMethod.Post, SetCorrelationRoute)) { request.Headers.Add(DefaultOperationId, correlationInfo.OperationId); request.Headers.Add(DefaultTransactionId, correlationInfo.TransactionId); // Act using (HttpResponseMessage response = await client.SendAsync(request)) { // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); string actualOperationId = GetResponseHeader(response, DefaultOperationId); string actualTransactionId = GetResponseHeader(response, DefaultTransactionId); Assert.Equal(correlationInfo.OperationId, actualOperationId); Assert.Equal(correlationInfo.TransactionId, actualTransactionId); } } }
public async Task SendRequest_WithSerilogCorrelationenrichment_ReturnsOkWithDifferentOperationIdAndSameTransactionId() { // Arrange using (HttpClient client = _testServer.CreateClient()) // Act using (HttpResponseMessage firstResponse = await client.GetAsync(DefaultRoute)) { // Assert Assert.Equal(HttpStatusCode.OK, firstResponse.StatusCode); CorrelationInfo firstCorrelationInfo = await AssertAppCorrelationInfoAsync(firstResponse); AssertLoggedCorrelationProperties(firstCorrelationInfo); var request = new HttpRequestMessage(HttpMethod.Get, DefaultRoute); request.Headers.Add("X-Transaction-ID", firstCorrelationInfo.TransactionId); using (HttpResponseMessage secondResponse = await client.SendAsync(request)) { Assert.Equal(HttpStatusCode.OK, secondResponse.StatusCode); CorrelationInfo secondCorrelationInfo = await AssertAppCorrelationInfoAsync(secondResponse); AssertLoggedCorrelationProperties(secondCorrelationInfo); Assert.NotEqual(firstCorrelationInfo.OperationId, secondCorrelationInfo.OperationId); Assert.Equal(firstCorrelationInfo.TransactionId, secondCorrelationInfo.TransactionId); } } }
public async Task SendRequest_WithSerilogCorrelationEnrichment_ReturnsOkWithEnrichedCorrelationLogProperties() { // Arrange var spySink = new InMemorySink(); var options = new TestApiServerOptions() .ConfigureServices(services => services.AddHttpCorrelation()) .PreConfigure(app => app.UseHttpCorrelation()) .ConfigureHost(host => host.UseSerilog((context, serviceProvider, config) => config.Enrich.WithHttpCorrelationInfo(serviceProvider) .WriteTo.Sink(spySink))); await using (var server = await TestApiServer.StartNewAsync(options, _logger)) { var request = HttpRequestBuilder.Get(CorrelationController.GetRoute); // Act using (HttpResponseMessage response = await server.SendAsync(request)) { // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); CorrelationInfo correlationInfo = await AssertAppCorrelationInfoAsync(response); AssertLoggedCorrelationProperties(spySink, correlationInfo); } } }
private void AssertLoggedCorrelationProperties(CorrelationInfo correlationInfo) { IEnumerable <KeyValuePair <string, LogEventPropertyValue> > properties = _testServer.LogSink.DequeueLogEvents() .SelectMany(ev => ev.Properties); var transactionIdProperties = properties.Where(prop => prop.Key == TransactionIdPropertyName); var operationIdProperties = properties.Where(prop => prop.Key == OperationIdPropertyName); Assert.Contains(transactionIdProperties, prop => correlationInfo.TransactionId == prop.Value.ToStringValue()); Assert.Contains(operationIdProperties, prop => correlationInfo.OperationId == prop.Value.ToStringValue()); }
public void Constructor_NoTransactionIdSpecified_Succeeds() { // Arrange var operationId = Guid.NewGuid().ToString(); // Act var messageCorrelationInfo = new CorrelationInfo(operationId, transactionId: null); // Assert Assert.Equal(operationId, messageCorrelationInfo.OperationId); Assert.Null(messageCorrelationInfo.TransactionId); }
public void Constructor_Valid_Succeeds() { // Arrange var transactionId = Guid.NewGuid().ToString(); var operationId = Guid.NewGuid().ToString(); // Act var messageCorrelationInfo = new CorrelationInfo(operationId, transactionId); // Assert Assert.Equal(operationId, messageCorrelationInfo.OperationId); Assert.Equal(transactionId, messageCorrelationInfo.TransactionId); }
public async Task SendRequest_WithSerilogCorrelationEnrichment_ReturnsOkWithEnrichedCorrelationLogProperties() { // Arrange using (HttpClient client = _testServer.CreateClient()) // Act using (HttpResponseMessage response = await client.GetAsync(Route)) { // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); CorrelationInfo correlationInfo = await AssertAppCorrelationInfoAsync(response); AssertLoggedCorrelationProperties(correlationInfo); } }
private static void AssertLoggedCorrelationProperties(InMemorySink testSink, CorrelationInfo correlationInfo) { KeyValuePair <string, LogEventPropertyValue>[] properties = testSink.DequeueLogEvents() .SelectMany(ev => ev.Properties) .ToArray(); Assert.Contains( properties.Where(prop => prop.Key == TransactionIdPropertyName), prop => correlationInfo.TransactionId == prop.Value.ToStringValue()); Assert.Contains( properties.Where(prop => prop.Key == OperationIdPropertyName), prop => correlationInfo.OperationId == prop.Value.ToStringValue()); }
public async Task SetCorrelationInfo_Twice_UsesMostRecentValue() { // Arrange var firstOperationId = $"operation-{Guid.NewGuid()}"; var secondOperationId = $"operation-{Guid.NewGuid()}"; var transactionId = $"transaction-{Guid.NewGuid()}"; await SetCorrelationInfo(firstOperationId, transactionId); // Act await SetCorrelationInfo(secondOperationId, transactionId); // Assert CorrelationInfo correlationInfo = DefaultCorrelationInfoAccessor.Instance.GetCorrelationInfo(); Assert.Equal(secondOperationId, correlationInfo.OperationId); Assert.Equal(transactionId, correlationInfo.TransactionId); }
public void Correlation_GetCorrelationInfo_UsesStubbedCorrelation() { // Arrange var expected = new CorrelationInfo($"operation-{Guid.NewGuid()}", $"transaction-{Guid.NewGuid()}"); var correlationAccessor = new DefaultCorrelationInfoAccessor(); correlationAccessor.SetCorrelationInfo(expected); IOptions <HttpCorrelationInfoOptions> options = Options.Create(new HttpCorrelationInfoOptions()); IHttpContextAccessor contextAccessor = Mock.Of <IHttpContextAccessor>(); ILogger <HttpCorrelation> logger = NullLogger <HttpCorrelation> .Instance; var correlation = new HttpCorrelation(options, contextAccessor, correlationAccessor, logger); // Act CorrelationInfo actual = correlation.GetCorrelationInfo(); // Assert Assert.Same(expected, actual); }
private void AddCorrelationResponseHeaders(HttpContext httpContext) { if (_options.Operation.IncludeInResponse) { httpContext.Response.OnStarting(() => { CorrelationInfo correlationInfo = _correlationInfoAccessor.GetCorrelationInfo(); AddResponseHeader(httpContext, _options.Operation.HeaderName, correlationInfo.OperationId); return(Task.CompletedTask); }); } if (_options.Transaction.IncludeInResponse) { httpContext.Response.OnStarting(() => { CorrelationInfo correlationInfo = _correlationInfoAccessor.GetCorrelationInfo(); AddResponseHeader(httpContext, _options.Transaction.HeaderName, correlationInfo.TransactionId); return(Task.CompletedTask); }); } }
public async Task SendRequest_WithSerilogCorrelationenrichment_ReturnsOkWithDifferentOperationIdAndSameTransactionId() { // Arrange var spySink = new InMemorySink(); var options = new TestApiServerOptions() .ConfigureServices(services => services.AddHttpCorrelation()) .PreConfigure(app => app.UseHttpCorrelation()) .ConfigureHost(host => host.UseSerilog((context, serviceProvider, config) => config.Enrich.WithHttpCorrelationInfo(serviceProvider) .WriteTo.Sink(spySink))); await using (var server = await TestApiServer.StartNewAsync(options, _logger)) { var firstRequest = HttpRequestBuilder.Get(CorrelationController.GetRoute); using (HttpResponseMessage firstResponse = await server.SendAsync(firstRequest)) { // Assert Assert.Equal(HttpStatusCode.OK, firstResponse.StatusCode); CorrelationInfo firstCorrelationInfo = await AssertAppCorrelationInfoAsync(firstResponse); AssertLoggedCorrelationProperties(spySink, firstCorrelationInfo); var secondRequest = HttpRequestBuilder .Get(CorrelationController.GetRoute) .WithHeader("X-Transaction-ID", firstCorrelationInfo.TransactionId); using (HttpResponseMessage secondResponse = await server.SendAsync(secondRequest)) { Assert.Equal(HttpStatusCode.OK, secondResponse.StatusCode); CorrelationInfo secondCorrelationInfo = await AssertAppCorrelationInfoAsync(secondResponse); AssertLoggedCorrelationProperties(spySink, secondCorrelationInfo); Assert.NotEqual(firstCorrelationInfo.OperationId, secondCorrelationInfo.OperationId); Assert.Equal(firstCorrelationInfo.TransactionId, secondCorrelationInfo.TransactionId); } } } }
private void AddCorrelationResponseHeaders(HttpContext httpContext) { if (_options.Operation.IncludeInResponse) { _logger.LogTrace("Prepare for the operation correlation ID to be included in the response..."); httpContext.Response.OnStarting(() => { CorrelationInfo correlationInfo = _correlationInfoAccessor.GetCorrelationInfo(); AddResponseHeader(httpContext, _options.Operation.HeaderName, correlationInfo.OperationId); return(Task.CompletedTask); }); } if (_options.Transaction.IncludeInResponse) { _logger.LogTrace("Prepare for the transactional correlation ID to be included in the response..."); httpContext.Response.OnStarting(() => { CorrelationInfo correlationInfo = _correlationInfoAccessor.GetCorrelationInfo(); AddResponseHeader(httpContext, _options.Transaction.HeaderName, correlationInfo.TransactionId); return(Task.CompletedTask); }); } }
/// <summary> /// Sets the current correlation information for this context. /// </summary> /// <param name="correlationInfo">The correlation model to set.</param> /// <exception cref="ArgumentNullException">Thrown when the <paramref name="correlationInfo"/> is <c>null</c>.</exception> public void SetCorrelationInfo(CorrelationInfo correlationInfo) { Guard.NotNull(correlationInfo, nameof(correlationInfo)); _correlationInfoAccessor.SetCorrelationInfo(correlationInfo); }
/// <summary> /// Sets the current correlation information for this context. /// </summary> /// <param name="correlationInfo">The correlation model to set.</param> public void SetCorrelationInfo(CorrelationInfo correlationInfo) { throw new NotSupportedException( $"The correlation information is automatically set during the application middleware '{nameof(CorrelationMiddleware)}' and is not supported to be altered afterwards"); }
/// <summary> /// Sets the current correlation information for this context. /// </summary> /// <param name="correlationInfo">The correlation model to set.</param> /// <exception cref="ArgumentNullException">Thrown when the <paramref name="correlationInfo"/> is <c>null</c>.</exception> public void SetCorrelationInfo(CorrelationInfo correlationInfo) { Guard.NotNull(correlationInfo, nameof(correlationInfo)); _httpContextAccessor.HttpContext?.Features?.Set(correlationInfo); }
public void SetCorrelationInfo(CorrelationInfo correlationInfo) { }