diff --git a/src/ServiceControl.Audit.Persistence.InMemory/InMemoryAuditDataStore.cs b/src/ServiceControl.Audit.Persistence.InMemory/InMemoryAuditDataStore.cs index 622cbb5437..feaab3d080 100644 --- a/src/ServiceControl.Audit.Persistence.InMemory/InMemoryAuditDataStore.cs +++ b/src/ServiceControl.Audit.Persistence.InMemory/InMemoryAuditDataStore.cs @@ -42,7 +42,7 @@ public async Task> QuerySagaHistoryById(Guid input, Can return await Task.FromResult(new QueryResult(sagaHistory, new QueryStatsInfo(string.Empty, 1))); } - public async Task>> GetMessages(bool includeSystemMessages, PagingInfo pagingInfo, SortInfo sortInfo, CancellationToken cancellationToken) + public async Task>> GetMessages(bool includeSystemMessages, PagingInfo pagingInfo, SortInfo sortInfo, DateTimeRange timeSentRange, CancellationToken cancellationToken) { var matched = messageViews .Where(w => !w.IsSystemMessage || includeSystemMessages) @@ -51,7 +51,7 @@ public async Task>> GetMessages(bool includeSyst return await Task.FromResult(new QueryResult>(matched, new QueryStatsInfo(string.Empty, matched.Count))); } - public async Task>> QueryMessages(string keyword, PagingInfo pagingInfo, SortInfo sortInfo, CancellationToken cancellationToken) + public async Task>> QueryMessages(string keyword, PagingInfo pagingInfo, SortInfo sortInfo, DateTimeRange timeSentRange, CancellationToken cancellationToken) { var messages = GetMessageIdsMatchingQuery(keyword); @@ -61,7 +61,7 @@ public async Task>> QueryMessages(string keyword return await Task.FromResult(new QueryResult>(matched, new QueryStatsInfo(string.Empty, matched.Count()))); } - public async Task>> QueryMessagesByReceivingEndpointAndKeyword(string endpoint, string keyword, PagingInfo pagingInfo, SortInfo sortInfo, CancellationToken cancellationToken) + public async Task>> QueryMessagesByReceivingEndpointAndKeyword(string endpoint, string keyword, PagingInfo pagingInfo, SortInfo sortInfo, DateTimeRange timeSentRange, CancellationToken cancellationToken) { var messages = GetMessageIdsMatchingQuery(keyword); @@ -69,7 +69,7 @@ public async Task>> QueryMessagesByReceivingEndp return await Task.FromResult(new QueryResult>(matched, new QueryStatsInfo(string.Empty, matched.Count))); } - public async Task>> QueryMessagesByReceivingEndpoint(bool includeSystemMessages, string endpointName, PagingInfo pagingInfo, SortInfo sortInfo, CancellationToken cancellationToken) + public async Task>> QueryMessagesByReceivingEndpoint(bool includeSystemMessages, string endpointName, PagingInfo pagingInfo, SortInfo sortInfo, DateTimeRange timeSentRange, CancellationToken cancellationToken) { var matched = messageViews.Where(w => w.ReceivingEndpoint.Name == endpointName).ToList(); return await Task.FromResult(new QueryResult>(matched, new QueryStatsInfo(string.Empty, matched.Count))); diff --git a/src/ServiceControl.Audit.Persistence.RavenDB/Extensions/RavenQueryExtensions.cs b/src/ServiceControl.Audit.Persistence.RavenDB/Extensions/RavenQueryExtensions.cs index 40e5da425a..c9e36f377d 100644 --- a/src/ServiceControl.Audit.Persistence.RavenDB/Extensions/RavenQueryExtensions.cs +++ b/src/ServiceControl.Audit.Persistence.RavenDB/Extensions/RavenQueryExtensions.cs @@ -19,6 +19,26 @@ static class RavenQueryExtensions return source; } + public static IQueryable FilterBySentTimeRange(this IQueryable source, DateTimeRange range) + { + if (range == null) + { + return source; + } + + if (range.From.HasValue) + { + source = source.Where(m => m.TimeSent >= range.From); + } + + if (range.To.HasValue) + { + source = source.Where(m => m.TimeSent <= range.To); + } + + return source; + } + public static IQueryable Paging(this IQueryable source, PagingInfo pagingInfo) => source.Skip(pagingInfo.Offset).Take(pagingInfo.PageSize); diff --git a/src/ServiceControl.Audit.Persistence.RavenDB/RavenAuditDataStore.cs b/src/ServiceControl.Audit.Persistence.RavenDB/RavenAuditDataStore.cs index 9f6a120613..8a465fb029 100644 --- a/src/ServiceControl.Audit.Persistence.RavenDB/RavenAuditDataStore.cs +++ b/src/ServiceControl.Audit.Persistence.RavenDB/RavenAuditDataStore.cs @@ -31,11 +31,12 @@ public async Task> QuerySagaHistoryById(Guid input, Can return sagaHistory == null ? QueryResult.Empty() : new QueryResult(sagaHistory, new QueryStatsInfo($"{stats.ResultEtag}", stats.TotalResults)); } - public async Task>> GetMessages(bool includeSystemMessages, PagingInfo pagingInfo, SortInfo sortInfo, CancellationToken cancellationToken) + public async Task>> GetMessages(bool includeSystemMessages, PagingInfo pagingInfo, SortInfo sortInfo, DateTimeRange timeSentRange, CancellationToken cancellationToken) { using var session = await sessionProvider.OpenSession(cancellationToken: cancellationToken); var results = await session.Query(GetIndexName(isFullTextSearchEnabled)) .Statistics(out var stats) + .FilterBySentTimeRange(timeSentRange) .IncludeSystemMessagesWhere(includeSystemMessages) .Sort(sortInfo) .Paging(pagingInfo) @@ -45,12 +46,13 @@ public async Task>> GetMessages(bool includeSyst return new QueryResult>(results, stats.ToQueryStatsInfo()); } - public async Task>> QueryMessages(string searchParam, PagingInfo pagingInfo, SortInfo sortInfo, CancellationToken cancellationToken) + public async Task>> QueryMessages(string searchParam, PagingInfo pagingInfo, SortInfo sortInfo, DateTimeRange timeSentRange, CancellationToken cancellationToken) { using var session = await sessionProvider.OpenSession(cancellationToken: cancellationToken); var results = await session.Query(GetIndexName(isFullTextSearchEnabled)) .Statistics(out var stats) .Search(x => x.Query, searchParam) + .FilterBySentTimeRange(timeSentRange) .Sort(sortInfo) .Paging(pagingInfo) .ToMessagesView() @@ -59,13 +61,14 @@ public async Task>> QueryMessages(string searchP return new QueryResult>(results, stats.ToQueryStatsInfo()); } - public async Task>> QueryMessagesByReceivingEndpointAndKeyword(string endpoint, string keyword, PagingInfo pagingInfo, SortInfo sortInfo, CancellationToken cancellationToken) + public async Task>> QueryMessagesByReceivingEndpointAndKeyword(string endpoint, string keyword, PagingInfo pagingInfo, SortInfo sortInfo, DateTimeRange timeSentRange, CancellationToken cancellationToken) { using var session = await sessionProvider.OpenSession(cancellationToken: cancellationToken); var results = await session.Query(GetIndexName(isFullTextSearchEnabled)) .Statistics(out var stats) .Search(x => x.Query, keyword) .Where(m => m.ReceivingEndpointName == endpoint) + .FilterBySentTimeRange(timeSentRange) .Sort(sortInfo) .Paging(pagingInfo) .ToMessagesView() @@ -74,13 +77,14 @@ public async Task>> QueryMessagesByReceivingEndp return new QueryResult>(results, stats.ToQueryStatsInfo()); } - public async Task>> QueryMessagesByReceivingEndpoint(bool includeSystemMessages, string endpointName, PagingInfo pagingInfo, SortInfo sortInfo, CancellationToken cancellationToken) + public async Task>> QueryMessagesByReceivingEndpoint(bool includeSystemMessages, string endpointName, PagingInfo pagingInfo, SortInfo sortInfo, DateTimeRange timeSentRange, CancellationToken cancellationToken) { using var session = await sessionProvider.OpenSession(cancellationToken: cancellationToken); var results = await session.Query(GetIndexName(isFullTextSearchEnabled)) .Statistics(out var stats) .IncludeSystemMessagesWhere(includeSystemMessages) .Where(m => m.ReceivingEndpointName == endpointName) + .FilterBySentTimeRange(timeSentRange) .Sort(sortInfo) .Paging(pagingInfo) .ToMessagesView() diff --git a/src/ServiceControl.Audit.Persistence.Tests.RavenDB/RetentionTests.cs b/src/ServiceControl.Audit.Persistence.Tests.RavenDB/RetentionTests.cs index 29c6938bd9..9fef1ed7db 100644 --- a/src/ServiceControl.Audit.Persistence.Tests.RavenDB/RetentionTests.cs +++ b/src/ServiceControl.Audit.Persistence.Tests.RavenDB/RetentionTests.cs @@ -28,15 +28,13 @@ public async Task AuditMessageRetention() { var message = MakeMessage("MyMessageId"); - await IngestProcessedMessagesAudits( - message - ); + await IngestProcessedMessagesAudits(message); - var queryResultBeforeExpiration = await DataStore.QueryMessages("MyMessageId", new PagingInfo(), new SortInfo("Id", "asc"), TestContext.CurrentContext.CancellationToken); + var queryResultBeforeExpiration = await DataStore.QueryMessages("MyMessageId", new PagingInfo(), new SortInfo("Id", "asc"), cancellationToken: TestContext.CurrentContext.CancellationToken); await Task.Delay(4000); - var queryResultAfterExpiration = await DataStore.QueryMessages("MyMessageId", new PagingInfo(), new SortInfo("Id", "asc"), TestContext.CurrentContext.CancellationToken); + var queryResultAfterExpiration = await DataStore.QueryMessages("MyMessageId", new PagingInfo(), new SortInfo("Id", "asc"), cancellationToken: TestContext.CurrentContext.CancellationToken); Assert.That(queryResultBeforeExpiration.Results, Has.Count.EqualTo(1)); Assert.Multiple(() => diff --git a/src/ServiceControl.Audit.Persistence.Tests/AuditTests.cs b/src/ServiceControl.Audit.Persistence.Tests/AuditTests.cs index 212a3b8621..4d89044420 100644 --- a/src/ServiceControl.Audit.Persistence.Tests/AuditTests.cs +++ b/src/ServiceControl.Audit.Persistence.Tests/AuditTests.cs @@ -30,7 +30,7 @@ await IngestProcessedMessagesAudits( message ); - var queryResult = await DataStore.QueryMessages("MyMessageId", new PagingInfo(), new SortInfo("Id", "asc"), TestContext.CurrentContext.CancellationToken); + var queryResult = await DataStore.QueryMessages("MyMessageId", new PagingInfo(), new SortInfo("Id", "asc"), cancellationToken: TestContext.CurrentContext.CancellationToken); Assert.That(queryResult.Results, Has.Count.EqualTo(1)); Assert.That(queryResult.Results[0].MessageId, Is.EqualTo("MyMessageId")); @@ -40,7 +40,7 @@ await IngestProcessedMessagesAudits( public async Task Handles_no_results_gracefully() { var nonExistingMessage = Guid.NewGuid().ToString(); - var queryResult = await DataStore.QueryMessages(nonExistingMessage, new PagingInfo(), new SortInfo("Id", "asc"), TestContext.CurrentContext.CancellationToken); + var queryResult = await DataStore.QueryMessages(nonExistingMessage, new PagingInfo(), new SortInfo("Id", "asc"), cancellationToken: TestContext.CurrentContext.CancellationToken); Assert.That(queryResult.Results, Is.Empty); } @@ -73,7 +73,7 @@ await IngestProcessedMessagesAudits( ); var queryResult = await DataStore.QueryMessages("MyMessageType", new PagingInfo(), - new SortInfo("message_id", "asc"), TestContext.CurrentContext.CancellationToken); + new SortInfo("message_id", "asc"), cancellationToken: TestContext.CurrentContext.CancellationToken); Assert.That(queryResult.Results, Has.Count.EqualTo(2)); } @@ -158,7 +158,7 @@ public async Task Deduplicates_messages_in_same_batch() await configuration.CompleteDBOperation(); - var queryResult = await DataStore.GetMessages(false, new PagingInfo(), new SortInfo("message_id", "asc"), TestContext.CurrentContext.CancellationToken); + var queryResult = await DataStore.GetMessages(false, new PagingInfo(), new SortInfo("message_id", "asc"), cancellationToken: TestContext.CurrentContext.CancellationToken); Assert.That(queryResult.QueryStats.TotalCount, Is.EqualTo(1)); } @@ -182,7 +182,7 @@ public async Task Deduplicates_messages_in_different_batches() await configuration.CompleteDBOperation(); - var queryResult = await DataStore.GetMessages(false, new PagingInfo(), new SortInfo("message_id", "asc"), TestContext.CurrentContext.CancellationToken); + var queryResult = await DataStore.GetMessages(false, new PagingInfo(), new SortInfo("message_id", "asc"), cancellationToken: TestContext.CurrentContext.CancellationToken); Assert.That(queryResult.QueryStats.TotalCount, Is.EqualTo(1)); } @@ -205,7 +205,7 @@ public async Task Does_not_deduplicate_with_different_processing_started_header( await configuration.CompleteDBOperation(); - var queryResult = await DataStore.GetMessages(false, new PagingInfo(), new SortInfo("message_id", "asc"), TestContext.CurrentContext.CancellationToken); + var queryResult = await DataStore.GetMessages(false, new PagingInfo(), new SortInfo("message_id", "asc"), cancellationToken: TestContext.CurrentContext.CancellationToken); Assert.That(queryResult.QueryStats.TotalCount, Is.EqualTo(2)); } diff --git a/src/ServiceControl.Audit.Persistence/IAuditDataStore.cs b/src/ServiceControl.Audit.Persistence/IAuditDataStore.cs index 9408ceeac7..bc7258ffc3 100644 --- a/src/ServiceControl.Audit.Persistence/IAuditDataStore.cs +++ b/src/ServiceControl.Audit.Persistence/IAuditDataStore.cs @@ -14,10 +14,10 @@ public interface IAuditDataStore { Task>> QueryKnownEndpoints(CancellationToken cancellationToken); Task> QuerySagaHistoryById(Guid input, CancellationToken cancellationToken); - Task>> GetMessages(bool includeSystemMessages, PagingInfo pagingInfo, SortInfo sortInfo, CancellationToken cancellationToken); - Task>> QueryMessages(string searchParam, PagingInfo pagingInfo, SortInfo sortInfo, CancellationToken cancellationToken); - Task>> QueryMessagesByReceivingEndpointAndKeyword(string endpoint, string keyword, PagingInfo pagingInfo, SortInfo sortInfo, CancellationToken cancellationToken); - Task>> QueryMessagesByReceivingEndpoint(bool includeSystemMessages, string endpointName, PagingInfo pagingInfo, SortInfo sortInfo, CancellationToken cancellationToken); + Task>> GetMessages(bool includeSystemMessages, PagingInfo pagingInfo, SortInfo sortInfo, DateTimeRange timeSentRange = null, CancellationToken cancellationToken = default); + Task>> QueryMessages(string searchParam, PagingInfo pagingInfo, SortInfo sortInfo, DateTimeRange timeSentRange = null, CancellationToken cancellationToken = default); + Task>> QueryMessagesByReceivingEndpointAndKeyword(string endpoint, string keyword, PagingInfo pagingInfo, SortInfo sortInfo, DateTimeRange timeSentRange = null, CancellationToken cancellationToken = default); + Task>> QueryMessagesByReceivingEndpoint(bool includeSystemMessages, string endpointName, PagingInfo pagingInfo, SortInfo sortInfo, DateTimeRange timeSentRange = null, CancellationToken cancellationToken = default); Task>> QueryMessagesByConversationId(string conversationId, PagingInfo pagingInfo, SortInfo sortInfo, CancellationToken cancellationToken); Task GetMessageBody(string messageId, CancellationToken cancellationToken); Task>> QueryAuditCounts(string endpointName, CancellationToken cancellationToken); diff --git a/src/ServiceControl.Audit.Persistence/Infrastructure/DateTimeRange.cs b/src/ServiceControl.Audit.Persistence/Infrastructure/DateTimeRange.cs new file mode 100644 index 0000000000..eb641d437b --- /dev/null +++ b/src/ServiceControl.Audit.Persistence/Infrastructure/DateTimeRange.cs @@ -0,0 +1,28 @@ +namespace ServiceControl.Audit.Infrastructure; + +using System; +using System.Globalization; + +public class DateTimeRange +{ + public DateTime? From { get; } + public DateTime? To { get; } + + public DateTimeRange(string from = null, string to = null) + { + if (from != null) + { + From = DateTime.Parse(from, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind); + } + if (to != null) + { + To = DateTime.Parse(to, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind); + } + } + + public DateTimeRange(DateTime? from = null, DateTime? to = null) + { + From = from; + To = to; + } +} \ No newline at end of file diff --git a/src/ServiceControl.Audit.UnitTests/ApprovalFiles/APIApprovals.HttpApiRoutes.approved.txt b/src/ServiceControl.Audit.UnitTests/ApprovalFiles/APIApprovals.HttpApiRoutes.approved.txt index 67a59c4d88..c9303ead9c 100644 --- a/src/ServiceControl.Audit.UnitTests/ApprovalFiles/APIApprovals.HttpApiRoutes.approved.txt +++ b/src/ServiceControl.Audit.UnitTests/ApprovalFiles/APIApprovals.HttpApiRoutes.approved.txt @@ -12,4 +12,5 @@ GET /messages => ServiceControl.Audit.Auditing.MessagesView.GetMessagesControlle GET /messages/{id}/body => ServiceControl.Audit.Auditing.MessagesView.GetMessagesController:Get(String id, CancellationToken cancellationToken) GET /messages/search => ServiceControl.Audit.Auditing.MessagesView.GetMessagesController:Search(PagingInfo pagingInfo, SortInfo sortInfo, String q, CancellationToken cancellationToken) GET /messages/search/{keyword} => ServiceControl.Audit.Auditing.MessagesView.GetMessagesController:SearchByKeyWord(PagingInfo pagingInfo, SortInfo sortInfo, String keyword, CancellationToken cancellationToken) +GET /messages2 => ServiceControl.Audit.Auditing.MessagesView.GetMessages2Controller:GetAllMessages(SortInfo sortInfo, Int32 pageSize, String endpointName, String from, String to, String q, CancellationToken cancellationToken) GET /sagas/{id} => ServiceControl.Audit.SagaAudit.SagasController:Sagas(PagingInfo pagingInfo, Guid id, CancellationToken cancellationToken) diff --git a/src/ServiceControl.Audit/Auditing/MessagesView/GetMessages2Controller.cs b/src/ServiceControl.Audit/Auditing/MessagesView/GetMessages2Controller.cs new file mode 100644 index 0000000000..db187bcd77 --- /dev/null +++ b/src/ServiceControl.Audit/Auditing/MessagesView/GetMessages2Controller.cs @@ -0,0 +1,57 @@ +namespace ServiceControl.Audit.Auditing.MessagesView; + +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Infrastructure; +using Infrastructure.WebApi; +using Microsoft.AspNetCore.Mvc; +using Persistence; + +[ApiController] +[Route("api")] +public class GetMessages2Controller(IAuditDataStore dataStore) : ControllerBase +{ + [Route("messages2")] + [HttpGet] + public async Task> GetAllMessages( + [FromQuery] SortInfo sortInfo, + [FromQuery(Name = "page_size")] int pageSize, + [FromQuery(Name = "endpoint_name")] string endpointName, + [FromQuery(Name = "from")] string from, + [FromQuery(Name = "to")] string to, + string q, + CancellationToken cancellationToken) + { + QueryResult> result; + var pagingInfo = new PagingInfo(pageSize: pageSize); + if (string.IsNullOrWhiteSpace(endpointName)) + { + if (string.IsNullOrWhiteSpace(q)) + { + result = await dataStore.GetMessages(false, pagingInfo, sortInfo, new DateTimeRange(from, to), cancellationToken); + } + else + { + result = await dataStore.QueryMessages(q, pagingInfo, sortInfo, new DateTimeRange(from, to), cancellationToken); + } + } + else + { + if (string.IsNullOrWhiteSpace(q)) + { + result = await dataStore.QueryMessagesByReceivingEndpoint(false, endpointName, pagingInfo, sortInfo, + new DateTimeRange(from, to), cancellationToken); + } + else + { + result = await dataStore.QueryMessagesByReceivingEndpointAndKeyword(endpointName, q, pagingInfo, + sortInfo, new DateTimeRange(from, to), cancellationToken); + } + } + + Response.WithTotalCount(result.QueryStats.TotalCount); + + return result.Results; + } +} \ No newline at end of file diff --git a/src/ServiceControl.Audit/Auditing/MessagesView/GetMessagesController.cs b/src/ServiceControl.Audit/Auditing/MessagesView/GetMessagesController.cs index 7c33da8d3e..ca3183c1b9 100644 --- a/src/ServiceControl.Audit/Auditing/MessagesView/GetMessagesController.cs +++ b/src/ServiceControl.Audit/Auditing/MessagesView/GetMessagesController.cs @@ -17,7 +17,7 @@ public class GetMessagesController(IAuditDataStore dataStore) : ControllerBase [HttpGet] public async Task> GetAllMessages([FromQuery] PagingInfo pagingInfo, [FromQuery] SortInfo sortInfo, [FromQuery(Name = "include_system_messages")] bool includeSystemMessages, CancellationToken cancellationToken) { - var result = await dataStore.GetMessages(includeSystemMessages, pagingInfo, sortInfo, cancellationToken); + var result = await dataStore.GetMessages(includeSystemMessages, pagingInfo, sortInfo, cancellationToken: cancellationToken); Response.WithQueryStatsAndPagingInfo(result.QueryStats, pagingInfo); return result.Results; } @@ -26,7 +26,7 @@ public async Task> GetAllMessages([FromQuery] PagingInfo pag [HttpGet] public async Task> GetEndpointMessages([FromQuery] PagingInfo pagingInfo, [FromQuery] SortInfo sortInfo, [FromQuery(Name = "include_system_messages")] bool includeSystemMessages, string endpoint, CancellationToken cancellationToken) { - var result = await dataStore.QueryMessagesByReceivingEndpoint(includeSystemMessages, endpoint, pagingInfo, sortInfo, cancellationToken); + var result = await dataStore.QueryMessagesByReceivingEndpoint(includeSystemMessages, endpoint, pagingInfo, sortInfo, cancellationToken: cancellationToken); Response.WithQueryStatsAndPagingInfo(result.QueryStats, pagingInfo); return result.Results; } @@ -70,7 +70,7 @@ public async Task Get(string id, CancellationToken cancellationTo [HttpGet] public async Task> Search([FromQuery] PagingInfo pagingInfo, [FromQuery] SortInfo sortInfo, string q, CancellationToken cancellationToken) { - var result = await dataStore.QueryMessages(q, pagingInfo, sortInfo, cancellationToken); + var result = await dataStore.QueryMessages(q, pagingInfo, sortInfo, cancellationToken: cancellationToken); Response.WithQueryStatsAndPagingInfo(result.QueryStats, pagingInfo); return result.Results; } @@ -79,7 +79,7 @@ public async Task> Search([FromQuery] PagingInfo pagingInfo, [HttpGet] public async Task> SearchByKeyWord([FromQuery] PagingInfo pagingInfo, [FromQuery] SortInfo sortInfo, string keyword, CancellationToken cancellationToken) { - var result = await dataStore.QueryMessages(keyword, pagingInfo, sortInfo, cancellationToken); + var result = await dataStore.QueryMessages(keyword, pagingInfo, sortInfo, cancellationToken: cancellationToken); Response.WithQueryStatsAndPagingInfo(result.QueryStats, pagingInfo); return result.Results; } @@ -88,7 +88,7 @@ public async Task> SearchByKeyWord([FromQuery] PagingInfo pa [HttpGet] public async Task> Search([FromQuery] PagingInfo pagingInfo, [FromQuery] SortInfo sortInfo, string endpoint, string q, CancellationToken cancellationToken) { - var result = await dataStore.QueryMessagesByReceivingEndpointAndKeyword(endpoint, q, pagingInfo, sortInfo, cancellationToken); + var result = await dataStore.QueryMessagesByReceivingEndpointAndKeyword(endpoint, q, pagingInfo, sortInfo, cancellationToken: cancellationToken); Response.WithQueryStatsAndPagingInfo(result.QueryStats, pagingInfo); return result.Results; } @@ -97,7 +97,7 @@ public async Task> Search([FromQuery] PagingInfo pagingInfo, [HttpGet] public async Task> SearchByKeyword([FromQuery] PagingInfo pagingInfo, [FromQuery] SortInfo sortInfo, string endpoint, string keyword, CancellationToken cancellationToken) { - var result = await dataStore.QueryMessagesByReceivingEndpointAndKeyword(endpoint, keyword, pagingInfo, sortInfo, cancellationToken); + var result = await dataStore.QueryMessagesByReceivingEndpointAndKeyword(endpoint, keyword, pagingInfo, sortInfo, cancellationToken: cancellationToken); Response.WithQueryStatsAndPagingInfo(result.QueryStats, pagingInfo); return result.Results; } diff --git a/src/ServiceControl.Persistence.RavenDB/ErrorMessagesDataStore.cs b/src/ServiceControl.Persistence.RavenDB/ErrorMessagesDataStore.cs index 4b9cdfb9d5..4c3f77bf6d 100644 --- a/src/ServiceControl.Persistence.RavenDB/ErrorMessagesDataStore.cs +++ b/src/ServiceControl.Persistence.RavenDB/ErrorMessagesDataStore.cs @@ -34,12 +34,14 @@ class ErrorMessagesDataStore( public async Task>> GetAllMessages( PagingInfo pagingInfo, SortInfo sortInfo, - bool includeSystemMessages + bool includeSystemMessages, + DateTimeRange timeSentRange ) { using var session = await sessionProvider.OpenSession(); var query = session.Query() .IncludeSystemMessagesWhere(includeSystemMessages) + .FilterBySentTimeRange(timeSentRange) .Statistics(out var stats) .Sort(sortInfo) .Paging(pagingInfo) @@ -55,12 +57,14 @@ public async Task>> GetAllMessagesForEndpoint( string endpointName, PagingInfo pagingInfo, SortInfo sortInfo, - bool includeSystemMessages + bool includeSystemMessages, + DateTimeRange timeSentRange ) { using var session = await sessionProvider.OpenSession(); var query = session.Query() .IncludeSystemMessagesWhere(includeSystemMessages) + .FilterBySentTimeRange(timeSentRange) .Where(m => m.ReceivingEndpointName == endpointName) .Statistics(out var stats) .Sort(sortInfo) @@ -78,7 +82,8 @@ public async Task>> SearchEndpointMessages( string endpointName, string searchKeyword, PagingInfo pagingInfo, - SortInfo sortInfo + SortInfo sortInfo, + DateTimeRange timeSentRange ) { using var session = await sessionProvider.OpenSession(); @@ -86,6 +91,7 @@ SortInfo sortInfo .Statistics(out var stats) .Search(x => x.Query, searchKeyword) .Where(m => m.ReceivingEndpointName == endpointName) + .FilterBySentTimeRange(timeSentRange) .Sort(sortInfo) .Paging(pagingInfo) .OfType() @@ -120,13 +126,15 @@ bool includeSystemMessages public async Task>> GetAllMessagesForSearch( string searchTerms, PagingInfo pagingInfo, - SortInfo sortInfo + SortInfo sortInfo, + DateTimeRange timeSentRange ) { using var session = await sessionProvider.OpenSession(); var query = session.Query() .Statistics(out var stats) .Search(x => x.Query, searchTerms) + .FilterBySentTimeRange(timeSentRange) .Sort(sortInfo) .Paging(pagingInfo) .OfType() diff --git a/src/ServiceControl.Persistence.RavenDB/RavenQueryExtensions.cs b/src/ServiceControl.Persistence.RavenDB/RavenQueryExtensions.cs index 24ed511940..47afe4dcd1 100644 --- a/src/ServiceControl.Persistence.RavenDB/RavenQueryExtensions.cs +++ b/src/ServiceControl.Persistence.RavenDB/RavenQueryExtensions.cs @@ -174,6 +174,26 @@ public static IAsyncDocumentQuery FilterByLastModifiedRange(this IAsyncDoc return source; } + public static IRavenQueryable FilterBySentTimeRange(this IRavenQueryable source, DateTimeRange range) + { + if (range == null) + { + return source; + } + + if (range.From.HasValue) + { + source = source.Where(m => m.TimeSent >= range.From); + } + + if (range.To.HasValue) + { + source = source.Where(m => m.TimeSent <= range.To); + } + + return source; + } + public static IAsyncDocumentQuery FilterByQueueAddress(this IAsyncDocumentQuery source, string queueAddress) { if (string.IsNullOrWhiteSpace(queueAddress)) diff --git a/src/ServiceControl.Persistence.Tests/Recoverability/ReturnToSenderDequeuerTests.cs b/src/ServiceControl.Persistence.Tests/Recoverability/ReturnToSenderDequeuerTests.cs index 1bdf6c9fb6..6df26ed702 100644 --- a/src/ServiceControl.Persistence.Tests/Recoverability/ReturnToSenderDequeuerTests.cs +++ b/src/ServiceControl.Persistence.Tests/Recoverability/ReturnToSenderDequeuerTests.cs @@ -168,19 +168,25 @@ public Task Dispatch(TransportOperations outgoingMessages, TransportTransaction class FakeErrorMessageDataStore : IErrorMessageDataStore { - public Task FetchFromFailedMessage(string bodyId) => Task.FromResult(Encoding.UTF8.GetBytes(bodyId)); - - public Task>> GetAllMessages(PagingInfo pagingInfo, SortInfo sortInfo, bool includeSystemMessages) => throw new NotImplementedException(); + public Task>> GetAllMessages(PagingInfo pagingInfo, SortInfo sortInfo, bool includeSystemMessages, + DateTimeRange timeSentRange = null) => + throw new NotImplementedException(); public Task>> GetAllMessagesForEndpoint(string endpointName, PagingInfo pagingInfo, SortInfo sortInfo, - bool includeSystemMessages) => + bool includeSystemMessages, DateTimeRange timeSentRange = null) => throw new NotImplementedException(); public Task>> GetAllMessagesByConversation(string conversationId, PagingInfo pagingInfo, SortInfo sortInfo, bool includeSystemMessages) => throw new NotImplementedException(); - public Task>> GetAllMessagesForSearch(string searchTerms, PagingInfo pagingInfo, SortInfo sortInfo) => throw new NotImplementedException(); + public Task>> GetAllMessagesForSearch(string searchTerms, PagingInfo pagingInfo, SortInfo sortInfo, + DateTimeRange timeSentRange = null) => + throw new NotImplementedException(); + + public Task>> SearchEndpointMessages(string endpointName, string searchKeyword, PagingInfo pagingInfo, SortInfo sortInfo, + DateTimeRange timeSentRange = null) => + throw new NotImplementedException(); public Task FailedMessageMarkAsArchived(string failedMessageId) => throw new NotImplementedException(); @@ -234,10 +240,9 @@ public Task>> ErrorsByEndpointName(string s public Task GetRetryPendingMessages(DateTime from, DateTime to, string queueAddress) => throw new NotImplementedException(); + public Task FetchFromFailedMessage(string bodyId) => Task.FromResult(Encoding.UTF8.GetBytes(bodyId)); public Task StoreEventLogItem(EventLogItem logItem) => throw new NotImplementedException(); - public Task>> SearchEndpointMessages(string endpointName, string searchKeyword, PagingInfo pagingInfo, SortInfo sortInfo) => throw new NotImplementedException(); - public Task StoreFailedMessagesForTestsOnly(params FailedMessage[] failedMessages) => throw new NotImplementedException(); } } diff --git a/src/ServiceControl.Persistence/IErrorMessageDatastore.cs b/src/ServiceControl.Persistence/IErrorMessageDatastore.cs index 2282319835..0dd2c6aad0 100644 --- a/src/ServiceControl.Persistence/IErrorMessageDatastore.cs +++ b/src/ServiceControl.Persistence/IErrorMessageDatastore.cs @@ -13,10 +13,11 @@ public interface IErrorMessageDataStore { - Task>> GetAllMessages(PagingInfo pagingInfo, SortInfo sortInfo, bool includeSystemMessages); - Task>> GetAllMessagesForEndpoint(string endpointName, PagingInfo pagingInfo, SortInfo sortInfo, bool includeSystemMessages); + Task>> GetAllMessages(PagingInfo pagingInfo, SortInfo sortInfo, bool includeSystemMessages, DateTimeRange timeSentRange = null); + Task>> GetAllMessagesForEndpoint(string endpointName, PagingInfo pagingInfo, SortInfo sortInfo, bool includeSystemMessages, DateTimeRange timeSentRange = null); Task>> GetAllMessagesByConversation(string conversationId, PagingInfo pagingInfo, SortInfo sortInfo, bool includeSystemMessages); - Task>> GetAllMessagesForSearch(string searchTerms, PagingInfo pagingInfo, SortInfo sortInfo); + Task>> GetAllMessagesForSearch(string searchTerms, PagingInfo pagingInfo, SortInfo sortInfo, DateTimeRange timeSentRange = null); + Task>> SearchEndpointMessages(string endpointName, string searchKeyword, PagingInfo pagingInfo, SortInfo sortInfo, DateTimeRange timeSentRange = null); Task FailedMessageMarkAsArchived(string failedMessageId); Task FailedMessagesFetch(Guid[] ids); Task StoreFailedErrorImport(FailedErrorImport failure); @@ -71,8 +72,6 @@ public interface IErrorMessageDataStore // AuditEventLogWriter Task StoreEventLogItem(EventLogItem logItem); - Task>> SearchEndpointMessages(string endpointName, string searchKeyword, PagingInfo pagingInfo, SortInfo sortInfo); - Task StoreFailedMessagesForTestsOnly(params FailedMessage[] failedMessages); } } \ No newline at end of file diff --git a/src/ServiceControl.Persistence/Infrastructure/DateTimeRange.cs b/src/ServiceControl.Persistence/Infrastructure/DateTimeRange.cs new file mode 100644 index 0000000000..562df20c90 --- /dev/null +++ b/src/ServiceControl.Persistence/Infrastructure/DateTimeRange.cs @@ -0,0 +1,28 @@ +namespace ServiceControl.Persistence.Infrastructure; + +using System; +using System.Globalization; + +public class DateTimeRange +{ + public DateTime? From { get; } + public DateTime? To { get; } + + public DateTimeRange(string from = null, string to = null) + { + if (from != null) + { + From = DateTime.Parse(from, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind); + } + if (to != null) + { + To = DateTime.Parse(to, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind); + } + } + + public DateTimeRange(DateTime? from = null, DateTime? to = null) + { + From = from; + To = to; + } +} \ No newline at end of file diff --git a/src/ServiceControl.UnitTests/ApprovalFiles/APIApprovals.HttpApiRoutes.approved.txt b/src/ServiceControl.UnitTests/ApprovalFiles/APIApprovals.HttpApiRoutes.approved.txt index dc3a14e4ad..c33a62237d 100644 --- a/src/ServiceControl.UnitTests/ApprovalFiles/APIApprovals.HttpApiRoutes.approved.txt +++ b/src/ServiceControl.UnitTests/ApprovalFiles/APIApprovals.HttpApiRoutes.approved.txt @@ -45,6 +45,7 @@ GET /messages => ServiceControl.CompositeViews.Messages.GetMessagesController:Me GET /messages/{id}/body => ServiceControl.CompositeViews.Messages.GetMessagesController:Get(String id, String instanceId) GET /messages/search => ServiceControl.CompositeViews.Messages.GetMessagesController:Search(PagingInfo pagingInfo, SortInfo sortInfo, String q) GET /messages/search/{keyword} => ServiceControl.CompositeViews.Messages.GetMessagesController:SearchByKeyWord(PagingInfo pagingInfo, SortInfo sortInfo, String keyword) +GET /messages2 => ServiceControl.CompositeViews.Messages.GetMessages2Controller:Messages(SortInfo sortInfo, Int32 pageSize, String endpointName, String from, String to, String q) GET /notifications/email => ServiceControl.Notifications.Api.NotificationsController:GetEmailNotificationsSettings() POST /notifications/email => ServiceControl.Notifications.Api.NotificationsController:UpdateSettings(UpdateEmailNotificationsSettingsRequest request) POST /notifications/email/test => ServiceControl.Notifications.Api.NotificationsController:SendTestEmail() diff --git a/src/ServiceControl.UnitTests/ScatterGather/MessageView_ScatterGatherTest.cs b/src/ServiceControl.UnitTests/ScatterGather/MessageView_ScatterGatherTest.cs index 7ddb2cbdbf..d74e2cac8c 100644 --- a/src/ServiceControl.UnitTests/ScatterGather/MessageView_ScatterGatherTest.cs +++ b/src/ServiceControl.UnitTests/ScatterGather/MessageView_ScatterGatherTest.cs @@ -17,7 +17,7 @@ public void SetUp() { var api = new TestApi(null, null, null); - Results = api.AggregateResults(new ScatterGatherApiMessageViewContext(null, new SortInfo()), GetData()); + Results = api.AggregateResults(new ScatterGatherApiMessageViewContext(new PagingInfo(), new SortInfo()), GetData()); } protected abstract QueryResult>[] GetData(); diff --git a/src/ServiceControl/CompositeViews/Messages/GetAllMessagesApi.cs b/src/ServiceControl/CompositeViews/Messages/GetAllMessagesApi.cs index 1055c7c8e5..37d83ac49d 100644 --- a/src/ServiceControl/CompositeViews/Messages/GetAllMessagesApi.cs +++ b/src/ServiceControl/CompositeViews/Messages/GetAllMessagesApi.cs @@ -16,7 +16,7 @@ public GetAllMessagesApi(IErrorMessageDataStore dataStore, Settings settings, protected override Task>> LocalQuery(ScatterGatherApiMessageViewWithSystemMessagesContext input) { - return DataStore.GetAllMessages(input.PagingInfo, input.SortInfo, input.IncludeSystemMessages); + return DataStore.GetAllMessages(input.PagingInfo, input.SortInfo, input.IncludeSystemMessages, input.TimeSentRange); } } } \ No newline at end of file diff --git a/src/ServiceControl/CompositeViews/Messages/GetAllMessagesForEndpointApi.cs b/src/ServiceControl/CompositeViews/Messages/GetAllMessagesForEndpointApi.cs index c49919ff49..1fdd327913 100644 --- a/src/ServiceControl/CompositeViews/Messages/GetAllMessagesForEndpointApi.cs +++ b/src/ServiceControl/CompositeViews/Messages/GetAllMessagesForEndpointApi.cs @@ -11,8 +11,9 @@ public record AllMessagesForEndpointContext( PagingInfo PagingInfo, SortInfo SortInfo, bool IncludeSystemMessages, - string EndpointName) - : ScatterGatherApiMessageViewWithSystemMessagesContext(PagingInfo, SortInfo, IncludeSystemMessages); + string EndpointName, + DateTimeRange TimeSentRange = null) + : ScatterGatherApiMessageViewWithSystemMessagesContext(PagingInfo, SortInfo, IncludeSystemMessages, TimeSentRange); public class GetAllMessagesForEndpointApi : ScatterGatherApiMessageView { @@ -21,6 +22,6 @@ public GetAllMessagesForEndpointApi(IErrorMessageDataStore dataStore, Settings s { } - protected override Task>> LocalQuery(AllMessagesForEndpointContext input) => DataStore.GetAllMessagesForEndpoint(input.EndpointName, input.PagingInfo, input.SortInfo, input.IncludeSystemMessages); + protected override Task>> LocalQuery(AllMessagesForEndpointContext input) => DataStore.GetAllMessagesForEndpoint(input.EndpointName, input.PagingInfo, input.SortInfo, input.IncludeSystemMessages, input.TimeSentRange); } } \ No newline at end of file diff --git a/src/ServiceControl/CompositeViews/Messages/GetMessages2Controller.cs b/src/ServiceControl/CompositeViews/Messages/GetMessages2Controller.cs new file mode 100644 index 0000000000..1ee79cfbbc --- /dev/null +++ b/src/ServiceControl/CompositeViews/Messages/GetMessages2Controller.cs @@ -0,0 +1,67 @@ +namespace ServiceControl.CompositeViews.Messages; + +using System.Collections.Generic; +using System.Threading.Tasks; +using Infrastructure.WebApi; +using Microsoft.AspNetCore.Http.Extensions; +using Microsoft.AspNetCore.Mvc; +using Persistence.Infrastructure; + +[ApiController] +[Route("api")] +public class GetMessages2Controller( + GetAllMessagesApi allMessagesApi, + GetAllMessagesForEndpointApi allMessagesEndpointApi, + SearchApi searchApi, + SearchEndpointApi searchEndpointApi) + : ControllerBase +{ + [Route("messages2")] + [HttpGet] + public async Task> Messages( + [FromQuery] SortInfo sortInfo, + [FromQuery(Name = "page_size")] int pageSize, + [FromQuery(Name = "endpoint_name")] string endpointName, + [FromQuery(Name = "from")] string from, + [FromQuery(Name = "to")] string to, + string q) + { + QueryResult> result; + var pagingInfo = new PagingInfo(pageSize: pageSize); + if (string.IsNullOrWhiteSpace(endpointName)) + { + if (string.IsNullOrWhiteSpace(q)) + { + result = await allMessagesApi.Execute( + new ScatterGatherApiMessageViewWithSystemMessagesContext(pagingInfo, + sortInfo, false, new DateTimeRange(from, to)), + Request.GetEncodedPathAndQuery()); + } + else + { + result = await searchApi.Execute( + new SearchApiContext(pagingInfo, sortInfo, q, new DateTimeRange(from, to)), + Request.GetEncodedPathAndQuery()); + } + } + else + { + if (string.IsNullOrWhiteSpace(q)) + { + result = await allMessagesEndpointApi.Execute( + new AllMessagesForEndpointContext(pagingInfo, sortInfo, false, + endpointName, new DateTimeRange(from, to)), + Request.GetEncodedPathAndQuery()); + } + else + { + result = await searchEndpointApi.Execute(new SearchEndpointContext(pagingInfo, sortInfo, q, endpointName, new DateTimeRange(from, to)), + Request.GetEncodedPathAndQuery()); + } + } + + Response.WithTotalCount(result.QueryStats.TotalCount); + + return result.Results; + } +} \ No newline at end of file diff --git a/src/ServiceControl/CompositeViews/Messages/GetMessagesController.cs b/src/ServiceControl/CompositeViews/Messages/GetMessagesController.cs index 502b48bddf..95c82bf5ac 100644 --- a/src/ServiceControl/CompositeViews/Messages/GetMessagesController.cs +++ b/src/ServiceControl/CompositeViews/Messages/GetMessagesController.cs @@ -106,7 +106,7 @@ public async Task Get(string id, [FromQuery(Name = "instance_id") var forwarderError = await forwarder.SendAsync(HttpContext, remote.BaseAddress, httpMessageInvoker); if (forwarderError != ForwarderError.None && HttpContext.GetForwarderErrorFeature()?.Exception is { } exception) { - logger.Warn($"Failed to forward the request ot remote instance at {remote.BaseAddress + HttpContext.Request.GetEncodedPathAndQuery()}.", exception); + logger.Warn($"Failed to forward the request to remote instance at {remote.BaseAddress}{HttpContext.Request.GetEncodedPathAndQuery()}.", exception); } return Empty; diff --git a/src/ServiceControl/CompositeViews/Messages/ScatterGatherApiMessageView.cs b/src/ServiceControl/CompositeViews/Messages/ScatterGatherApiMessageView.cs index 1388ad45a4..542ba6daba 100644 --- a/src/ServiceControl/CompositeViews/Messages/ScatterGatherApiMessageView.cs +++ b/src/ServiceControl/CompositeViews/Messages/ScatterGatherApiMessageView.cs @@ -9,9 +9,10 @@ namespace ServiceControl.CompositeViews.Messages public record ScatterGatherApiMessageViewWithSystemMessagesContext( PagingInfo PagingInfo, SortInfo SortInfo, - bool IncludeSystemMessages) : ScatterGatherApiMessageViewContext(PagingInfo, SortInfo); + bool IncludeSystemMessages, + DateTimeRange TimeSentRange = null) : ScatterGatherApiMessageViewContext(PagingInfo, SortInfo, TimeSentRange); - public record ScatterGatherApiMessageViewContext(PagingInfo PagingInfo, SortInfo SortInfo) : ScatterGatherContext(PagingInfo); + public record ScatterGatherApiMessageViewContext(PagingInfo PagingInfo, SortInfo SortInfo, DateTimeRange TimeSentRange = null) : ScatterGatherContext(PagingInfo); public abstract class ScatterGatherApiMessageView : ScatterGatherApi> where TInput : ScatterGatherApiMessageViewContext @@ -51,7 +52,7 @@ protected override IList ProcessResults(TInput input, QueryResult< combined.Sort(comparer); } - return combined; + return combined.Take(input.PagingInfo.PageSize).ToList(); } IComparer FinalOrder(SortInfo sortInfo) => MessageViewComparer.FromSortInfo(sortInfo); diff --git a/src/ServiceControl/CompositeViews/Messages/SearchApi.cs b/src/ServiceControl/CompositeViews/Messages/SearchApi.cs index 97b63b7e81..dd6ba8afb3 100644 --- a/src/ServiceControl/CompositeViews/Messages/SearchApi.cs +++ b/src/ServiceControl/CompositeViews/Messages/SearchApi.cs @@ -10,8 +10,9 @@ namespace ServiceControl.CompositeViews.Messages public record SearchApiContext( PagingInfo PagingInfo, SortInfo SortInfo, - string SearchQuery) - : ScatterGatherApiMessageViewContext(PagingInfo, SortInfo); + string SearchQuery, + DateTimeRange TimeSentRange = null) + : ScatterGatherApiMessageViewContext(PagingInfo, SortInfo, TimeSentRange); public class SearchApi : ScatterGatherApiMessageView { @@ -21,6 +22,6 @@ public SearchApi(IErrorMessageDataStore dataStore, Settings settings, IHttpClien } protected override Task>> LocalQuery(SearchApiContext input) => - DataStore.GetAllMessagesForSearch(input.SearchQuery, input.PagingInfo, input.SortInfo); + DataStore.GetAllMessagesForSearch(input.SearchQuery, input.PagingInfo, input.SortInfo, input.TimeSentRange); } } \ No newline at end of file diff --git a/src/ServiceControl/CompositeViews/Messages/SearchEndpointApi.cs b/src/ServiceControl/CompositeViews/Messages/SearchEndpointApi.cs index b94a902812..990beae427 100644 --- a/src/ServiceControl/CompositeViews/Messages/SearchEndpointApi.cs +++ b/src/ServiceControl/CompositeViews/Messages/SearchEndpointApi.cs @@ -11,8 +11,9 @@ public record SearchEndpointContext( PagingInfo PagingInfo, SortInfo SortInfo, string Keyword, - string Endpoint) - : ScatterGatherApiMessageViewContext(PagingInfo, SortInfo); + string Endpoint, + DateTimeRange TimeSentRange = null) + : ScatterGatherApiMessageViewContext(PagingInfo, SortInfo, TimeSentRange); public class SearchEndpointApi : ScatterGatherApiMessageView { @@ -22,6 +23,6 @@ public SearchEndpointApi(IErrorMessageDataStore dataStore, Settings settings, } protected override Task>> LocalQuery(SearchEndpointContext input) => - DataStore.SearchEndpointMessages(input.Endpoint, input.Keyword, input.PagingInfo, input.SortInfo); + DataStore.SearchEndpointMessages(input.Endpoint, input.Keyword, input.PagingInfo, input.SortInfo, input.TimeSentRange); } } \ No newline at end of file