From 3a115bc7b8116a42ce4a1eef6acac252a83be829 Mon Sep 17 00:00:00 2001 From: Brian Bicknell Date: Mon, 20 Oct 2025 23:32:38 -0400 Subject: [PATCH] Added logging. --- JSMR.Api/JSMR.Api.csproj | 4 + JSMR.Api/Program.cs | 42 ++++++++- JSMR.Api/Properties/launchSettings.json | 6 +- JSMR.Api/appsettings.json | 19 +++- .../Queries/Search/SearchCirclesHandler.cs | 21 ++++- .../Queries/Search/SearchCreatorsHandler.cs | 33 +++---- JSMR.Application/JSMR.Application.csproj | 1 + .../Logging/CriteriaLoggingExtensions.cs | 51 +++++++++++ JSMR.Application/Logging/LogEvents.cs | 59 +++++++++++++ JSMR.Application/Logging/LogObjectBuilder.cs | 36 ++++++++ JSMR.Application/Logging/LoggingExtensions.cs | 87 +++++++++++++++++++ .../Scanning/ScanVoiceWorksHandler.cs | 2 +- .../Tags/Queries/Search/SearchTagsHandler.cs | 33 +++---- .../Queries/Search/SearchVoiceWorksHandler.cs | 35 ++++---- .../Queries/Search/VoiceWorkSearchCriteria.cs | 4 +- .../VoiceWorks/VoiceWorkSearchProvider.cs | 4 +- .../VoiceWorkSearchProviderTests.cs | 4 +- JSMR.Tests/JSMR.Tests.csproj | 4 +- 18 files changed, 381 insertions(+), 64 deletions(-) create mode 100644 JSMR.Application/Logging/CriteriaLoggingExtensions.cs create mode 100644 JSMR.Application/Logging/LogEvents.cs create mode 100644 JSMR.Application/Logging/LogObjectBuilder.cs create mode 100644 JSMR.Application/Logging/LoggingExtensions.cs diff --git a/JSMR.Api/JSMR.Api.csproj b/JSMR.Api/JSMR.Api.csproj index 03310f9..e72e0ef 100644 --- a/JSMR.Api/JSMR.Api.csproj +++ b/JSMR.Api/JSMR.Api.csproj @@ -9,6 +9,10 @@ + + + + diff --git a/JSMR.Api/Program.cs b/JSMR.Api/Program.cs index d044abb..611210d 100644 --- a/JSMR.Api/Program.cs +++ b/JSMR.Api/Program.cs @@ -7,6 +7,9 @@ using JSMR.Infrastructure.Data; using JSMR.Infrastructure.DI; using Microsoft.AspNetCore.Http.Json; using Microsoft.EntityFrameworkCore; +using Serilog; +using Serilog.Events; +using System.Diagnostics; using System.Text.Json; using System.Text.Json.Serialization; @@ -37,6 +40,15 @@ builder.Services.Configure(options => new JsonStringEnumConverter(JsonNamingPolicy.CamelCase)); // or null for exact names }); +// Serilog bootstrap (before Build) +Log.Logger = new LoggerConfiguration() + .ReadFrom.Configuration(builder.Configuration) + .MinimumLevel.Override("Microsoft", LogEventLevel.Warning) + .Enrich.WithProperty("Service", "JSMR.Api") + .Enrich.WithProperty("Environment", builder.Environment.EnvironmentName) + .CreateLogger(); +builder.Host.UseSerilog(); + var app = builder.Build(); // Configure the HTTP request pipeline. @@ -51,6 +63,25 @@ app.UseAuthorization(); app.MapControllers(); +// Request logging with latency, status, path +app.UseSerilogRequestLogging(opts => +{ + opts.MessageTemplate = "HTTP {RequestMethod} {RequestPath} responded {StatusCode} in {Elapsed:0.0000} ms"; +}); + +// Correlation: ensure every log has traceId and return it to clients +app.Use(async (ctx, next) => +{ + // Use current Activity if present (W3C trace context), else fall back + var traceId = Activity.Current?.TraceId.ToString() ?? ctx.TraceIdentifier; + using (Serilog.Context.LogContext.PushProperty("TraceId", traceId)) + { + ctx.Response.Headers["x-trace-id"] = traceId; + await next(); + } +}); + + // Health check app.MapGet("/health", () => Results.Ok(new { status = "ok" })); @@ -62,9 +93,16 @@ app.MapPost("/api/circles/search", async ( SearchCirclesHandler handler, CancellationToken cancallationToken) => { - var result = await handler.HandleAsync(request, cancallationToken); + try + { + SearchCirclesResponse result = await handler.HandleAsync(request, cancallationToken); - return Results.Ok(result); + return Results.Ok(result); + } + catch (OperationCanceledException) when (cancallationToken.IsCancellationRequested) + { + return Results.StatusCode(StatusCodes.Status499ClientClosedRequest); + } }); // Voice Works Search diff --git a/JSMR.Api/Properties/launchSettings.json b/JSMR.Api/Properties/launchSettings.json index 63877de..b43b0c9 100644 --- a/JSMR.Api/Properties/launchSettings.json +++ b/JSMR.Api/Properties/launchSettings.json @@ -7,7 +7,8 @@ "launchBrowser": false, "applicationUrl": "http://localhost:5226", "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" + "ASPNETCORE_ENVIRONMENT": "Development", + "SEQ_URL": "http://localhost:5341" } }, "https": { @@ -16,7 +17,8 @@ "launchBrowser": false, "applicationUrl": "https://localhost:7277;http://localhost:5226", "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" + "ASPNETCORE_ENVIRONMENT": "Development", + "SEQ_URL": "http://localhost:5341" } } } diff --git a/JSMR.Api/appsettings.json b/JSMR.Api/appsettings.json index 10f68b8..d7310f9 100644 --- a/JSMR.Api/appsettings.json +++ b/JSMR.Api/appsettings.json @@ -5,5 +5,22 @@ "Microsoft.AspNetCore": "Warning" } }, - "AllowedHosts": "*" + "AllowedHosts": "*", + "Serilog": { + "MinimumLevel": { + "Default": "Information", + "Override": { + "Microsoft": "Warning", + "System": "Warning" + } + }, + "Enrich": [ "FromLogContext", "WithMachineName", "WithProcessId", "WithThreadId" ], + "WriteTo": [ + { "Name": "Console" }, + { + "Name": "Seq", + "Args": { "serverUrl": "%SEQ_URL%" } + } + ] + } } diff --git a/JSMR.Application/Circles/Queries/Search/SearchCirclesHandler.cs b/JSMR.Application/Circles/Queries/Search/SearchCirclesHandler.cs index d20daaa..eb43340 100644 --- a/JSMR.Application/Circles/Queries/Search/SearchCirclesHandler.cs +++ b/JSMR.Application/Circles/Queries/Search/SearchCirclesHandler.cs @@ -1,14 +1,33 @@ using JSMR.Application.Common.Search; +using JSMR.Application.Creators.Queries.Search; +using JSMR.Application.Logging; +using Microsoft.Extensions.Logging; +using System.Diagnostics; namespace JSMR.Application.Circles.Queries.Search; -public sealed class SearchCirclesHandler(ICircleSearchProvider searchProvider) +public sealed class SearchCirclesHandler(ICircleSearchProvider searchProvider, ILogger logger) { public async Task HandleAsync(SearchCirclesRequest request, CancellationToken cancellationToken) { SearchOptions searchOptions = request.Options; + Stopwatch stopWatch = Stopwatch.StartNew(); + LogEvents.SearchStart(logger, searchOptions.PageNumber, searchOptions.PageSize, searchOptions.Criteria); + SearchResult results = await searchProvider.SearchAsync(searchOptions, cancellationToken); + long elapsedMilliseconds = stopWatch.ElapsedMilliseconds; + + LogEvents.SearchCompleted( + logger, + Elapsed: elapsedMilliseconds, + Items: results.Items.Length, + Total: results.TotalItems, + Page: searchOptions.PageNumber, + Size: searchOptions.PageSize, + Sort: searchOptions.SortOptions.ToLogObject(), + Criteria: searchOptions.Criteria.ToLogObject() + ); return new SearchCirclesResponse(results); } diff --git a/JSMR.Application/Creators/Queries/Search/SearchCreatorsHandler.cs b/JSMR.Application/Creators/Queries/Search/SearchCreatorsHandler.cs index f9bc5bc..5dcaec9 100644 --- a/JSMR.Application/Creators/Queries/Search/SearchCreatorsHandler.cs +++ b/JSMR.Application/Creators/Queries/Search/SearchCreatorsHandler.cs @@ -1,31 +1,32 @@ -using JSMR.Application.Common.Caching; -using JSMR.Application.Common.Search; +using JSMR.Application.Common.Search; using JSMR.Application.Creators.Queries.Search.Contracts; using JSMR.Application.Creators.Queries.Search.Ports; +using JSMR.Application.Logging; +using Microsoft.Extensions.Logging; +using System.Diagnostics; namespace JSMR.Application.Creators.Queries.Search; -public sealed class SearchCreatorsHandler(ICreatorSearchProvider searchProvider, ICache cache) +public sealed class SearchCreatorsHandler(ICreatorSearchProvider searchProvider, ILogger logger) { public async Task HandleAsync(SearchCreatorsRequest request, CancellationToken cancellationToken) { SearchOptions searchOptions = request.Options; - - string cacheKey = $"creator:{searchOptions.GetHashCode()}"; - - CreatorSearchResults? cachedResults = await cache.GetAsync(cacheKey, cancellationToken); - - if (cachedResults != null) - return new SearchCreatorsResponse(cachedResults); + Stopwatch stopWatch = Stopwatch.StartNew(); SearchResult results = await searchProvider.SearchAsync(searchOptions, cancellationToken); + long elapsedMilliseconds = stopWatch.ElapsedMilliseconds; - CacheEntryOptions cacheEntryOptions = new() - { - SlidingExpiration = TimeSpan.FromMinutes(10) - }; - - await cache.SetAsync(cacheKey, results, cacheEntryOptions, cancellationToken); + LogEvents.SearchCompleted( + logger, + Elapsed: elapsedMilliseconds, + Items: results.Items.Length, + Total: results.TotalItems, + Page: searchOptions.PageNumber, + Size: searchOptions.PageSize, + Sort: searchOptions.SortOptions.ToLogObject(), + Criteria: searchOptions.Criteria.ToLogObject() + ); return new SearchCreatorsResponse(results); } diff --git a/JSMR.Application/JSMR.Application.csproj b/JSMR.Application/JSMR.Application.csproj index 4136680..1ee20ff 100644 --- a/JSMR.Application/JSMR.Application.csproj +++ b/JSMR.Application/JSMR.Application.csproj @@ -12,6 +12,7 @@ + diff --git a/JSMR.Application/Logging/CriteriaLoggingExtensions.cs b/JSMR.Application/Logging/CriteriaLoggingExtensions.cs new file mode 100644 index 0000000..2b15b25 --- /dev/null +++ b/JSMR.Application/Logging/CriteriaLoggingExtensions.cs @@ -0,0 +1,51 @@ +using JSMR.Application.Circles.Queries.Search; +using JSMR.Application.Creators.Queries.Search.Contracts; +using JSMR.Application.Tags.Queries.Search.Contracts; +using JSMR.Application.VoiceWorks.Queries.Search; + +namespace JSMR.Application.Logging; + +public static class CriteriaLoggingExtensions +{ + //public static object ToLogObject(this VoiceWorkSearchCriteria criteria) + //{ + // return new LogObjectBuilder() + // .AddIfNotEmpty("Keywords", criteria.Keywords) + // .AddIfNotEmpty("Title", criteria.Title) + // .AddIfNotEmpty("Circle", criteria.Circle) + // .Add("Locale", criteria.Locale) + // .AddIfNotEmpty("AgeRatings", criteria.AgeRatings) + // .AddIfNotEmpty("Languages", criteria.SupportedLanguages) + // .AddIfNotEmpty("TagIds", criteria.TagIds, preview: 5) + // .AddIfNotEmpty("CreatorIds", criteria.CreatorIds, preview: 5) + // .Add("IncludeAllTags", criteria.IncludeAllTags ? true : null) + // .Add("IncludeAllCreators", criteria.IncludeAllCreators ? true : null) + // .Add("MinDownloads", criteria.MinDownloads) + // .Add("MaxDownloads", criteria.MaxDownloads) + // .Add("ReleaseDateStart", criteria.ReleaseDateStart) + // .Add("ReleaseDateEnd", criteria.ReleaseDateEnd) + // .Build(); + //} + + //public static object ToLogObject(this CircleSearchCriteria criteria) + //{ + // return new LogObjectBuilder() + // .AddIfNotEmpty("Name", criteria.Name) + // .AddIfNotEmpty("Status", criteria.Status?.ToString()) + // .Build(); + //} + + //public static object ToLogObject(this TagSearchCriteria criteria) + //{ + // return new LogObjectBuilder() + // .AddIfNotEmpty("Name", criteria.Name) + // .Build(); + //} + + //public static object ToLogObject(this CreatorSearchCriteria criteria) + //{ + // return new LogObjectBuilder() + // .AddIfNotEmpty("Name", criteria.Name) + // .Build(); + //} +} \ No newline at end of file diff --git a/JSMR.Application/Logging/LogEvents.cs b/JSMR.Application/Logging/LogEvents.cs new file mode 100644 index 0000000..68604f5 --- /dev/null +++ b/JSMR.Application/Logging/LogEvents.cs @@ -0,0 +1,59 @@ +using Microsoft.Extensions.Logging; + +namespace JSMR.Application.Logging; + +public static partial class LogEvents +{ + public static void SearchCompleted(ILogger logger, long Elapsed, int Items, int Total, int Page, int Size, object Sort, object Criteria) + { + logger.LogInformation( + new EventId(1000, "SearchCompleted"), + "Search returned {Items} items (out of {Total} total) results in {Elapsed} ms on page {Page} with Size {Size}, sorted by {@Sort}, based on the following criteria: {@Criteria}", + Items, + Total, + Elapsed, + Page, + Size, + Sort, + Criteria + ); + } + + // Search + [LoggerMessage(EventId = 1000, Level = LogLevel.Information, + Message = "Search started Page={Page} Size={Size} Criteria={@Criteria}")] + public static partial void SearchStart(ILogger logger, int Page, int Size, object Criteria); + + //[LoggerMessage(EventId = 1001, Level = LogLevel.Information, + // Message = "Search completed in {ElapsedMs} ms, Total={Total}")] + //public static partial void SearchComplete(ILogger logger, long ElapsedMs, int Total); + + [LoggerMessage(EventId = 1001, Level = LogLevel.Information, + Message = "Search returned {Items} items (out of {Total} total) results in {Elapsed} ms on page {Page} with Size {Size}, sorted by {@Sort}, based on the following criteria: {@Criteria}")] + public static partial void SearchComplete(ILogger logger, long Elapsed, int Items, int Total, int Page, int Size, object Sort, object Criteria); + + [LoggerMessage(EventId = 1002, Level = LogLevel.Warning, + Message = "External search provider timeout after {ElapsedMs} ms")] + public static partial void ExternalSearchTimeout(ILogger logger, long ElapsedMs); + + [LoggerMessage(EventId = 1003, Level = LogLevel.Warning, + Message = "Slow Search Detected: Elapsed {ElapsedMs} ms (threshold {ThresholdMs} ms) Path={Path}")] + public static partial void SlowSearchDetected(ILogger logger, long ElapsedMs, long ThresholdMs, string Path); + + [LoggerMessage(EventId = 1004, Level = LogLevel.Error, + Message = "Very Slow Search Detected: Elapsed {ElapsedMs} ms (threshold {ThresholdMs} ms) Path={Path}")] + public static partial void VerySlowSearchDetected(ILogger logger, long ElapsedMs, long ThresholdMs, string Path); + + // Worker scan batch + [LoggerMessage(EventId = 2000, Level = LogLevel.Information, + Message = "Scan batch {BatchId} started")] + public static partial void ScanBatchStart(ILogger logger, string BatchId); + + [LoggerMessage(EventId = 2001, Level = LogLevel.Information, + Message = "Scan batch {BatchId} completed in {ElapsedMs} ms. Processed={Processed} Failures={Failures}")] + public static partial void ScanBatchComplete(ILogger logger, string BatchId, int Processed, int Failures, long ElapsedMs); + + [LoggerMessage(EventId = 2002, Level = LogLevel.Error, + Message = "Scan batch {BatchId} failed")] + public static partial void ScanBatchFailed(ILogger logger, string BatchId, Exception ex); +} \ No newline at end of file diff --git a/JSMR.Application/Logging/LogObjectBuilder.cs b/JSMR.Application/Logging/LogObjectBuilder.cs new file mode 100644 index 0000000..96afa48 --- /dev/null +++ b/JSMR.Application/Logging/LogObjectBuilder.cs @@ -0,0 +1,36 @@ +namespace JSMR.Application.Logging; + +public sealed class LogObjectBuilder +{ + private readonly Dictionary _d = []; + + public LogObjectBuilder Add(string key, object? value) + { + if (value is null) + return this; + + _d[key] = value; + + return this; + } + + public LogObjectBuilder AddIfNotEmpty(string key, string? value) + => string.IsNullOrWhiteSpace(value) ? this : Add(key, value); + + public LogObjectBuilder AddIfNotEmpty(string key, IReadOnlyCollection? value, int? preview = null) + { + if (value is null || value.Count == 0) + return this; + + if (preview is null || value.Count <= preview) + return Add(key, value); + + // Store small preview + count so logs stay compact + return Add(key, new { Preview = value.Take(preview.Value), value.Count }); + } + + public LogObjectBuilder AddIfNotDefault(string key, T value) where T : struct, IEquatable + => value.Equals(default) ? this : Add(key, value); + + public object Build() => _d; +} \ No newline at end of file diff --git a/JSMR.Application/Logging/LoggingExtensions.cs b/JSMR.Application/Logging/LoggingExtensions.cs new file mode 100644 index 0000000..26e0338 --- /dev/null +++ b/JSMR.Application/Logging/LoggingExtensions.cs @@ -0,0 +1,87 @@ +using JSMR.Application.Circles.Queries.Search; +using JSMR.Application.Common.Search; +using JSMR.Application.Creators.Queries.Search.Contracts; +using JSMR.Application.Tags.Queries.Search.Contracts; +using JSMR.Application.VoiceWorks.Queries.Search; + +namespace JSMR.Application.Logging; + +public static class LoggingExtensions +{ + public static object ToLogObject(this SearchOptions searchOptions) + where TCriteria : new() + where TSortField : struct, Enum + { + return new LogObjectBuilder() + .Add("PageNumber", searchOptions.PageNumber) + .Add("PageSize", searchOptions.PageSize) + .Add("Criteria", MapCriteriaToLogObject(searchOptions.Criteria)) + //.AddIfNotEmpty("Sort", searchOptions.SortOptions.ToLogObject()) + .Build(); + } + + private static object MapCriteriaToLogObject(TCriteria criteria) => criteria switch + { + VoiceWorkSearchCriteria voiceWorkSearchCriteria => voiceWorkSearchCriteria.ToLogObject(), + CircleSearchCriteria circleSearchCriteria => circleSearchCriteria.ToLogObject(), + TagSearchCriteria tagSearchCriteria => tagSearchCriteria.ToLogObject(), + CreatorSearchCriteria creatorSearchCriteria => creatorSearchCriteria.ToLogObject(), + _ => criteria! + }; + + public record SortLog(int Index, string Field, string Direction); + + public static object ToLogObject(this IEnumerable> sort) + where TSortField : struct, Enum + { + return sort.Select((sortOption, index) => + new { + Index = index, + Field = sortOption.Field.ToString(), + Direction = sortOption.Direction.ToString() + } + ).ToList(); + } + + public static object ToLogObject(this VoiceWorkSearchCriteria criteria) + { + return new LogObjectBuilder() + .AddIfNotEmpty("Keywords", criteria.Keywords) + .AddIfNotEmpty("Title", criteria.Title) + .AddIfNotEmpty("Circle", criteria.Circle) + .Add("Locale", criteria.Locale) + .AddIfNotEmpty("AgeRatings", criteria.AgeRatings) + .AddIfNotEmpty("Languages", criteria.SupportedLanguages) + .AddIfNotEmpty("TagIds", criteria.TagIds, preview: 5) + .AddIfNotEmpty("CreatorIds", criteria.CreatorIds, preview: 5) + .Add("IncludeAllTags", criteria.IncludeAllTags ? true : null) + .Add("IncludeAllCreators", criteria.IncludeAllCreators ? true : null) + .Add("MinDownloads", criteria.MinDownloads) + .Add("MaxDownloads", criteria.MaxDownloads) + .Add("ReleaseDateStart", criteria.ReleaseDateStart) + .Add("ReleaseDateEnd", criteria.ReleaseDateEnd) + .Build(); + } + + public static object ToLogObject(this CircleSearchCriteria criteria) + { + return new LogObjectBuilder() + .AddIfNotEmpty("Name", criteria.Name) + .AddIfNotEmpty("Status", criteria.Status?.ToString()) + .Build(); + } + + public static object ToLogObject(this TagSearchCriteria criteria) + { + return new LogObjectBuilder() + .AddIfNotEmpty("Name", criteria.Name) + .Build(); + } + + public static object ToLogObject(this CreatorSearchCriteria criteria) + { + return new LogObjectBuilder() + .AddIfNotEmpty("Name", criteria.Name) + .Build(); + } +} \ No newline at end of file diff --git a/JSMR.Application/Scanning/ScanVoiceWorksHandler.cs b/JSMR.Application/Scanning/ScanVoiceWorksHandler.cs index f72d385..582cb7f 100644 --- a/JSMR.Application/Scanning/ScanVoiceWorksHandler.cs +++ b/JSMR.Application/Scanning/ScanVoiceWorksHandler.cs @@ -38,7 +38,7 @@ public sealed class ScanVoiceWorksHandler( string[] productIds = [.. works.Where(x => !string.IsNullOrWhiteSpace(x.ProductId)).Select(x => x.ProductId!)]; VoiceWorkDetailCollection voiceWorkDetails = await dlsiteClient.GetVoiceWorkDetailsAsync(productIds, cancellationToken); - List ingests = [.. works.Select(work => + VoiceWorkIngest[] ingests = [.. works.Select(work => { voiceWorkDetails.TryGetValue(work.ProductId!, out VoiceWorkDetails? value); return new VoiceWorkIngest(work, value); diff --git a/JSMR.Application/Tags/Queries/Search/SearchTagsHandler.cs b/JSMR.Application/Tags/Queries/Search/SearchTagsHandler.cs index 9e209b2..fde8a63 100644 --- a/JSMR.Application/Tags/Queries/Search/SearchTagsHandler.cs +++ b/JSMR.Application/Tags/Queries/Search/SearchTagsHandler.cs @@ -1,31 +1,32 @@ -using JSMR.Application.Common.Caching; -using JSMR.Application.Common.Search; +using JSMR.Application.Common.Search; +using JSMR.Application.Logging; using JSMR.Application.Tags.Queries.Search.Contracts; using JSMR.Application.Tags.Queries.Search.Ports; +using Microsoft.Extensions.Logging; +using System.Diagnostics; namespace JSMR.Application.Tags.Queries.Search; -public sealed class SearchTagsHandler(ITagSearchProvider searchProvider) +public sealed class SearchTagsHandler(ITagSearchProvider searchProvider, ILogger logger) { public async Task HandleAsync(SearchTagsRequest request, CancellationToken cancellationToken) { SearchOptions searchOptions = request.Options; - - //string cacheKey = $"tag:{searchOptions.GetHashCode()}"; - - //TagSearchResults? cachedResults = await cache.GetAsync(cacheKey, cancellationToken); - - //if (cachedResults != null) - // return new SearchTagsResponse(cachedResults); + Stopwatch stopWatch = Stopwatch.StartNew(); SearchResult results = await searchProvider.SearchAsync(searchOptions, cancellationToken); + long elapsedMilliseconds = stopWatch.ElapsedMilliseconds; - //CacheEntryOptions cacheEntryOptions = new() - //{ - // SlidingExpiration = TimeSpan.FromMinutes(10) - //}; - - //await cache.SetAsync(cacheKey, results, cacheEntryOptions, cancellationToken); + LogEvents.SearchCompleted( + logger, + Elapsed: elapsedMilliseconds, + Items: results.Items.Length, + Total: results.TotalItems, + Page: searchOptions.PageNumber, + Size: searchOptions.PageSize, + Sort: searchOptions.SortOptions.ToLogObject(), + Criteria: searchOptions.Criteria.ToLogObject() + ); return new SearchTagsResponse(results); } diff --git a/JSMR.Application/VoiceWorks/Queries/Search/SearchVoiceWorksHandler.cs b/JSMR.Application/VoiceWorks/Queries/Search/SearchVoiceWorksHandler.cs index 7ce332a..1b2e8aa 100644 --- a/JSMR.Application/VoiceWorks/Queries/Search/SearchVoiceWorksHandler.cs +++ b/JSMR.Application/VoiceWorks/Queries/Search/SearchVoiceWorksHandler.cs @@ -1,30 +1,31 @@ -using JSMR.Application.Common.Caching; -using JSMR.Application.VoiceWorks.Ports; +using JSMR.Application.Common.Search; +using JSMR.Application.Logging; +using Microsoft.Extensions.Logging; +using System.Diagnostics; namespace JSMR.Application.VoiceWorks.Queries.Search; // TODO: Caching? -public sealed class SearchVoiceWorksHandler(IVoiceWorkSearchProvider provider) +public sealed class SearchVoiceWorksHandler(IVoiceWorkSearchProvider provider, ILogger logger) { public async Task HandleAsync(SearchVoiceWorksRequest request, CancellationToken cancellationToken) { var searchOptions = request.Options; - //string cacheKey = $"vw:{searchOptions.GetHashCode()}"; + Stopwatch stopWatch = Stopwatch.StartNew(); + SearchResult results = await provider.SearchAsync(searchOptions, cancellationToken); + long elapsedMilliseconds = stopWatch.ElapsedMilliseconds; - //VoiceWorkSearchResults? cachedResults = await cache.GetAsync(cacheKey, cancellationToken); - - //if (cachedResults != null) - // return new SearchVoiceWorksResponse(cachedResults); - - var results = await provider.SearchAsync(searchOptions, cancellationToken); - - //CacheEntryOptions cacheEntryOptions = new() - //{ - // SlidingExpiration = TimeSpan.FromMinutes(10) - //}; - - //await cache.SetAsync(cacheKey, results, cacheEntryOptions, cancellationToken); + LogEvents.SearchCompleted( + logger, + Elapsed: elapsedMilliseconds, + Items: results.Items.Length, + Total: results.TotalItems, + Page: searchOptions.PageNumber, + Size: searchOptions.PageSize, + Sort: searchOptions.SortOptions.ToLogObject(), + Criteria: searchOptions.Criteria.ToLogObject() + ); return new SearchVoiceWorksResponse(results); } diff --git a/JSMR.Application/VoiceWorks/Queries/Search/VoiceWorkSearchCriteria.cs b/JSMR.Application/VoiceWorks/Queries/Search/VoiceWorkSearchCriteria.cs index 75be8b5..14c83f5 100644 --- a/JSMR.Application/VoiceWorks/Queries/Search/VoiceWorkSearchCriteria.cs +++ b/JSMR.Application/VoiceWorks/Queries/Search/VoiceWorkSearchCriteria.cs @@ -16,8 +16,8 @@ public class VoiceWorkSearchCriteria public int[] CreatorIds { get; init; } = []; public bool IncludeAllCreators { get; init; } public Locale Locale { get; init; } = Locale.Japanese; - public DateTime? ReleaseDateStart { get; init; } - public DateTime? ReleaseDateEnd { get; init; } + public DateOnly? ReleaseDateStart { get; init; } + public DateOnly? ReleaseDateEnd { get; init; } public AgeRating[] AgeRatings { get; init; } = []; public Language[] SupportedLanguages { get; init; } = []; public AIGeneration[] AIGenerationOptions { get; init; } = []; diff --git a/JSMR.Infrastructure/Data/Repositories/VoiceWorks/VoiceWorkSearchProvider.cs b/JSMR.Infrastructure/Data/Repositories/VoiceWorks/VoiceWorkSearchProvider.cs index 9344d18..6ebf13d 100644 --- a/JSMR.Infrastructure/Data/Repositories/VoiceWorks/VoiceWorkSearchProvider.cs +++ b/JSMR.Infrastructure/Data/Repositories/VoiceWorks/VoiceWorkSearchProvider.cs @@ -62,10 +62,10 @@ public class VoiceWorkSearchProvider(AppDbContext context, IVoiceWorkFullTextSea } if (criteria.ReleaseDateStart is not null) - filteredQuery = filteredQuery.Where(x => x.VoiceWork.SalesDate >= criteria.ReleaseDateStart.Value); + filteredQuery = filteredQuery.Where(x => x.VoiceWork.SalesDate >= criteria.ReleaseDateStart.Value.ToDateTime(TimeOnly.MinValue)); if (criteria.ReleaseDateEnd is not null) - filteredQuery = filteredQuery.Where(x => x.VoiceWork.SalesDate <= criteria.ReleaseDateEnd.Value); + filteredQuery = filteredQuery.Where(x => x.VoiceWork.SalesDate <= criteria.ReleaseDateEnd.Value.ToDateTime(TimeOnly.MinValue)); if (criteria.AgeRatings.Length > 0) filteredQuery = filteredQuery.Where(x => criteria.AgeRatings.Contains((AgeRating)x.VoiceWork.Rating)); diff --git a/JSMR.Tests/Integration/VoiceWorkSearchProviderTests.cs b/JSMR.Tests/Integration/VoiceWorkSearchProviderTests.cs index 0c7490f..f135702 100644 --- a/JSMR.Tests/Integration/VoiceWorkSearchProviderTests.cs +++ b/JSMR.Tests/Integration/VoiceWorkSearchProviderTests.cs @@ -657,8 +657,8 @@ public class VoiceWorkSearchProviderTests(VoiceWorkSearchProviderFixture fixture { Criteria = new() { - ReleaseDateStart = new DateTime(2025, 1, 1), - ReleaseDateEnd = new DateTime(2025, 1, 2) + ReleaseDateStart = new DateOnly(2025, 1, 1), + ReleaseDateEnd = new DateOnly(2025, 1, 2) } }; diff --git a/JSMR.Tests/JSMR.Tests.csproj b/JSMR.Tests/JSMR.Tests.csproj index 74dd7a5..70ee19a 100644 --- a/JSMR.Tests/JSMR.Tests.csproj +++ b/JSMR.Tests/JSMR.Tests.csproj @@ -28,8 +28,8 @@ - - + + all