diff --git a/src/ServiceControl.AcceptanceTests/Recoverability/MessageFailures/When_ingesting_failed_message_with_missing_headers.cs b/src/ServiceControl.AcceptanceTests/Recoverability/MessageFailures/When_ingesting_failed_message_with_missing_headers.cs new file mode 100644 index 0000000000..d79b88333c --- /dev/null +++ b/src/ServiceControl.AcceptanceTests/Recoverability/MessageFailures/When_ingesting_failed_message_with_missing_headers.cs @@ -0,0 +1,132 @@ +namespace ServiceControl.AcceptanceTests.Recoverability.MessageFailures; + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using AcceptanceTesting; +using AcceptanceTesting.EndpointTemplates; +using Infrastructure; +using NServiceBus; +using NServiceBus.AcceptanceTesting; +using NServiceBus.Faults; +using NServiceBus.Routing; +using NServiceBus.Transport; +using NUnit.Framework; +using ServiceControl.MessageFailures.Api; + +class When_ingesting_failed_message_with_missing_headers : AcceptanceTest +{ + [Test] + public async Task Should_be_ingested_when_minimal_required_headers_is_present() + { + var testStartTime = DateTime.UtcNow; + + var context = await Define(c => c.AddMinimalRequiredHeaders()) + .WithEndpoint() + .Done(async c => await TryGetFailureFromApi(c)) + .Run(); + + var failure = context.Failure; + + Assert.That(failure, Is.Not.Null); + Assert.That(failure.TimeSent, Is.Null); + + //No failure time will result in utc now being used + Assert.That(failure.TimeOfFailure, Is.GreaterThan(testStartTime)); + + // Both host and endpoint name is currently needed so this will be null since no host can be detected from the failed q header + Assert.That(failure.ReceivingEndpoint, Is.Null); + } + + [Test] + public async Task Should_include_headers_required_by_ServicePulse() + { + var context = await Define(c => + { + c.AddMinimalRequiredHeaders(); + + // This is needed for ServiceControl to be able to detect both endpoint (via failed q header) and host via the processing machine header + // Missing endpoint or host will cause a null ref in ServicePulse + c.Headers[Headers.ProcessingMachine] = "MyMachine"; + + c.Headers[FaultsHeaderKeys.ExceptionType] = "SomeExceptionType"; + c.Headers[FaultsHeaderKeys.Message] = "Some message"; + }) + .WithEndpoint() + .Done(async c => await TryGetFailureFromApi(c)) + .Run(); + + var failure = context.Failure; + + Assert.That(failure, Is.Not.Null); + + // ServicePulse assumes that the receiving endpoint name is present + Assert.That(failure.ReceivingEndpoint, Is.Not.Null); + Assert.That(failure.ReceivingEndpoint.Name, Is.EqualTo(context.EndpointNameOfReceivingEndpoint)); + Assert.That(failure.ReceivingEndpoint.Host, Is.EqualTo("MyMachine")); + + // ServicePulse needs both an exception type and description to render the UI in a resonable way + Assert.That(failure.Exception.ExceptionType, Is.EqualTo("SomeExceptionType")); + Assert.That(failure.Exception.Message, Is.EqualTo("Some message")); + } + + [Test] + public async Task TimeSent_should_not_be_casted() + { + var sentTime = DateTime.Parse("2014-11-11T02:26:58.000462Z"); + + var context = await Define(c => + { + c.AddMinimalRequiredHeaders(); + c.Headers.Add(Headers.TimeSent, DateTimeOffsetHelper.ToWireFormattedString(sentTime)); + }) + .WithEndpoint() + .Done(async c => await TryGetFailureFromApi(c)) + .Run(); + + var failure = context.Failure; + + Assert.That(failure, Is.Not.Null); + Assert.That(failure.TimeSent, Is.EqualTo(sentTime)); + } + + async Task TryGetFailureFromApi(TestContext context) + { + context.Failure = await this.TryGet($"/api/errors/last/{context.UniqueMessageId}"); + return context.Failure != null; + } + + class TestContext : ScenarioContext + { + public string MessageId { get; } = Guid.NewGuid().ToString(); + + public string EndpointNameOfReceivingEndpoint => "MyEndpoint"; + + public string UniqueMessageId => DeterministicGuid.MakeId(MessageId, EndpointNameOfReceivingEndpoint).ToString(); + + public Dictionary Headers { get; } = []; + + public FailedMessageView Failure { get; set; } + + public void AddMinimalRequiredHeaders() + { + Headers[FaultsHeaderKeys.FailedQ] = EndpointNameOfReceivingEndpoint; + Headers[NServiceBus.Headers.MessageId] = MessageId; + } + } + + class FailingEndpoint : EndpointConfigurationBuilder + { + public FailingEndpoint() => EndpointSetup(); + + class SendFailedMessage : DispatchRawMessages + { + protected override TransportOperations CreateMessage(TestContext context) + { + var outgoingMessage = new OutgoingMessage(context.MessageId, context.Headers, Array.Empty()); + + return new TransportOperations(new TransportOperation(outgoingMessage, new UnicastAddressTag("error"))); + } + } + } +} \ No newline at end of file diff --git a/src/ServiceControl.AcceptanceTests/Recoverability/MessageFailures/When_processing_message_with_missing_metadata_failed.cs b/src/ServiceControl.AcceptanceTests/Recoverability/MessageFailures/When_processing_message_with_missing_metadata_failed.cs deleted file mode 100644 index 6fad5fd287..0000000000 --- a/src/ServiceControl.AcceptanceTests/Recoverability/MessageFailures/When_processing_message_with_missing_metadata_failed.cs +++ /dev/null @@ -1,126 +0,0 @@ -namespace ServiceControl.AcceptanceTests.Recoverability.MessageFailures -{ - using System; - using System.Collections.Generic; - using System.Threading.Tasks; - using AcceptanceTesting; - using AcceptanceTesting.EndpointTemplates; - using Infrastructure; - using NServiceBus; - using NServiceBus.AcceptanceTesting; - using NServiceBus.Routing; - using NServiceBus.Transport; - using NUnit.Framework; - using ServiceControl.MessageFailures.Api; - using Conventions = NServiceBus.AcceptanceTesting.Customization.Conventions; - - class When_processing_message_with_missing_metadata_failed : AcceptanceTest - { - [Test] - public async Task Null_TimeSent_should_not_be_cast_to_DateTimeMin() - { - FailedMessageView failure = null; - - await Define() - .WithEndpoint() - .Done(async c => - { - var result = await this.TryGetSingle("/api/errors/", m => m.Id == c.UniqueMessageId); - failure = result; - return result; - }) - .Run(); - - Assert.That(failure, Is.Not.Null); - Assert.That(failure.TimeSent, Is.Null); - } - - [Test] - public async Task TimeSent_should_not_be_casted() - { - FailedMessageView failure = null; - - var sentTime = DateTime.Parse("2014-11-11T02:26:58.000462Z"); - - await Define(ctx => { ctx.TimeSent = sentTime; }) - .WithEndpoint() - .Done(async c => - { - var result = await this.TryGet($"/api/errors/last/{c.UniqueMessageId}"); - failure = result; - return c.UniqueMessageId != null & result; - }) - .Run(); - - Assert.That(failure, Is.Not.Null); - Assert.That(failure.TimeSent, Is.EqualTo(sentTime)); - } - - [Test] - public async Task Should_be_able_to_get_the_message_by_id() - { - FailedMessageView failure = null; - - await Define() - .WithEndpoint() - .Done(async c => - { - var result = await this.TryGet($"/api/errors/last/{c.UniqueMessageId}"); - failure = result; - return c.UniqueMessageId != null & result; - }) - .Run(); - - Assert.That(failure, Is.Not.Null); - } - - public class Failing : EndpointConfigurationBuilder - { - public Failing() => EndpointSetup(c => { c.Recoverability().Delayed(x => x.NumberOfRetries(0)); }); - - class SendFailedMessage : DispatchRawMessages - { - protected override TransportOperations CreateMessage(MyContext context) - { - context.EndpointNameOfReceivingEndpoint = Conventions.EndpointNamingConvention(typeof(Failing)); - context.MessageId = Guid.NewGuid().ToString(); - context.UniqueMessageId = DeterministicGuid.MakeId(context.MessageId, context.EndpointNameOfReceivingEndpoint).ToString(); - - var headers = new Dictionary - { - [Headers.MessageId] = context.MessageId, - [Headers.ProcessingEndpoint] = context.EndpointNameOfReceivingEndpoint, - ["NServiceBus.ExceptionInfo.ExceptionType"] = "2014-11-11 02:26:57:767462 Z", - ["NServiceBus.ExceptionInfo.Message"] = "An error occurred while attempting to extract logical messages from transport message NServiceBus.TransportMessage", - ["NServiceBus.ExceptionInfo.InnerExceptionType"] = "System.Exception", - ["NServiceBus.ExceptionInfo.Source"] = "NServiceBus.Core", - ["NServiceBus.ExceptionInfo.StackTrace"] = string.Empty, - ["NServiceBus.FailedQ"] = Conventions.EndpointNamingConvention(typeof(Failing)), - ["NServiceBus.TimeOfFailure"] = "2014-11-11 02:26:58:000462 Z" - }; - if (context.TimeSent.HasValue) - { - headers["NServiceBus.TimeSent"] = DateTimeOffsetHelper.ToWireFormattedString(context.TimeSent.Value); - } - - var outgoingMessage = new OutgoingMessage(context.MessageId, headers, new byte[0]); - - return new TransportOperations( - new TransportOperation(outgoingMessage, new UnicastAddressTag("error")) - ); - } - } - } - - public class MyContext : ScenarioContext - { - public string MessageId { get; set; } - - public string EndpointNameOfReceivingEndpoint { get; set; } - - public string UniqueMessageId { get; set; } - - public DateTime? TimeSent { get; set; } - } - } -} \ No newline at end of file diff --git a/src/ServiceControl.Audit/Recoverability/DetectSuccessfulRetriesEnricher.cs b/src/ServiceControl.Audit/Recoverability/DetectSuccessfulRetriesEnricher.cs index d3f17194c5..618c36c765 100644 --- a/src/ServiceControl.Audit/Recoverability/DetectSuccessfulRetriesEnricher.cs +++ b/src/ServiceControl.Audit/Recoverability/DetectSuccessfulRetriesEnricher.cs @@ -7,6 +7,7 @@ using Contracts.MessageFailures; using Infrastructure; using NServiceBus; + using NServiceBus.Faults; using NServiceBus.Routing; using NServiceBus.Transport; using ServiceControl.Audit.Persistence.Infrastructure; @@ -64,7 +65,7 @@ IEnumerable GetAlternativeUniqueMessageId(IReadOnlyDictionary endpointDetails.Name = s); - DictionaryExtensions.CheckIfKeyExists("NServiceBus.OriginatingMachine", headers, s => endpointDetails.Host = s); + DictionaryExtensions.CheckIfKeyExists(Headers.OriginatingMachine, headers, s => endpointDetails.Host = s); DictionaryExtensions.CheckIfKeyExists(Headers.OriginatingHostId, headers, s => endpointDetails.HostId = Guid.Parse(s)); if (!string.IsNullOrEmpty(endpointDetails.Name) && !string.IsNullOrEmpty(endpointDetails.Host)) @@ -50,7 +51,7 @@ public static EndpointDetails ReceivingEndpoint(IReadOnlyDictionary endpoint.Host = s); + DictionaryExtensions.CheckIfKeyExists(Headers.ProcessingMachine, headers, s => endpoint.Host = s); } DictionaryExtensions.CheckIfKeyExists(Headers.ProcessingEndpoint, headers, s => endpoint.Name = s); @@ -62,7 +63,7 @@ public static EndpointDetails ReceivingEndpoint(IReadOnlyDictionary address = s); + DictionaryExtensions.CheckIfKeyExists(FaultsHeaderKeys.FailedQ, headers, s => address = s); // If we have a failed queue, then construct an endpoint from the failed queue information if (address != null)