Added logging.
This commit is contained in:
@@ -9,6 +9,10 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.10" />
|
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.10" />
|
||||||
|
<PackageReference Include="Serilog" Version="4.3.0" />
|
||||||
|
<PackageReference Include="Serilog.AspNetCore" Version="9.0.0" />
|
||||||
|
<PackageReference Include="Serilog.Settings.Configuration" Version="9.0.0" />
|
||||||
|
<PackageReference Include="Serilog.Sinks.Seq" Version="9.0.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -7,6 +7,9 @@ using JSMR.Infrastructure.Data;
|
|||||||
using JSMR.Infrastructure.DI;
|
using JSMR.Infrastructure.DI;
|
||||||
using Microsoft.AspNetCore.Http.Json;
|
using Microsoft.AspNetCore.Http.Json;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Serilog;
|
||||||
|
using Serilog.Events;
|
||||||
|
using System.Diagnostics;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
@@ -37,6 +40,15 @@ builder.Services.Configure<JsonOptions>(options =>
|
|||||||
new JsonStringEnumConverter(JsonNamingPolicy.CamelCase)); // or null for exact names
|
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();
|
var app = builder.Build();
|
||||||
|
|
||||||
// Configure the HTTP request pipeline.
|
// Configure the HTTP request pipeline.
|
||||||
@@ -51,6 +63,25 @@ app.UseAuthorization();
|
|||||||
|
|
||||||
app.MapControllers();
|
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
|
// Health check
|
||||||
app.MapGet("/health", () => Results.Ok(new { status = "ok" }));
|
app.MapGet("/health", () => Results.Ok(new { status = "ok" }));
|
||||||
|
|
||||||
@@ -62,9 +93,16 @@ app.MapPost("/api/circles/search", async (
|
|||||||
SearchCirclesHandler handler,
|
SearchCirclesHandler handler,
|
||||||
CancellationToken cancallationToken) =>
|
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
|
// Voice Works Search
|
||||||
|
|||||||
@@ -7,7 +7,8 @@
|
|||||||
"launchBrowser": false,
|
"launchBrowser": false,
|
||||||
"applicationUrl": "http://localhost:5226",
|
"applicationUrl": "http://localhost:5226",
|
||||||
"environmentVariables": {
|
"environmentVariables": {
|
||||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||||
|
"SEQ_URL": "http://localhost:5341"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"https": {
|
"https": {
|
||||||
@@ -16,7 +17,8 @@
|
|||||||
"launchBrowser": false,
|
"launchBrowser": false,
|
||||||
"applicationUrl": "https://localhost:7277;http://localhost:5226",
|
"applicationUrl": "https://localhost:7277;http://localhost:5226",
|
||||||
"environmentVariables": {
|
"environmentVariables": {
|
||||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||||
|
"SEQ_URL": "http://localhost:5341"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,5 +5,22 @@
|
|||||||
"Microsoft.AspNetCore": "Warning"
|
"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%" }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,33 @@
|
|||||||
using JSMR.Application.Common.Search;
|
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;
|
namespace JSMR.Application.Circles.Queries.Search;
|
||||||
|
|
||||||
public sealed class SearchCirclesHandler(ICircleSearchProvider searchProvider)
|
public sealed class SearchCirclesHandler(ICircleSearchProvider searchProvider, ILogger<SearchCreatorsHandler> logger)
|
||||||
{
|
{
|
||||||
public async Task<SearchCirclesResponse> HandleAsync(SearchCirclesRequest request, CancellationToken cancellationToken)
|
public async Task<SearchCirclesResponse> HandleAsync(SearchCirclesRequest request, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
SearchOptions<CircleSearchCriteria, CircleSortField> searchOptions = request.Options;
|
SearchOptions<CircleSearchCriteria, CircleSortField> searchOptions = request.Options;
|
||||||
|
|
||||||
|
Stopwatch stopWatch = Stopwatch.StartNew();
|
||||||
|
LogEvents.SearchStart(logger, searchOptions.PageNumber, searchOptions.PageSize, searchOptions.Criteria);
|
||||||
|
|
||||||
SearchResult<CircleSearchItem> results = await searchProvider.SearchAsync(searchOptions, cancellationToken);
|
SearchResult<CircleSearchItem> 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);
|
return new SearchCirclesResponse(results);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.Contracts;
|
||||||
using JSMR.Application.Creators.Queries.Search.Ports;
|
using JSMR.Application.Creators.Queries.Search.Ports;
|
||||||
|
using JSMR.Application.Logging;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
namespace JSMR.Application.Creators.Queries.Search;
|
namespace JSMR.Application.Creators.Queries.Search;
|
||||||
|
|
||||||
public sealed class SearchCreatorsHandler(ICreatorSearchProvider searchProvider, ICache cache)
|
public sealed class SearchCreatorsHandler(ICreatorSearchProvider searchProvider, ILogger<SearchCreatorsHandler> logger)
|
||||||
{
|
{
|
||||||
public async Task<SearchCreatorsResponse> HandleAsync(SearchCreatorsRequest request, CancellationToken cancellationToken)
|
public async Task<SearchCreatorsResponse> HandleAsync(SearchCreatorsRequest request, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
SearchOptions<CreatorSearchCriteria, CreatorSortField> searchOptions = request.Options;
|
SearchOptions<CreatorSearchCriteria, CreatorSortField> searchOptions = request.Options;
|
||||||
|
Stopwatch stopWatch = Stopwatch.StartNew();
|
||||||
string cacheKey = $"creator:{searchOptions.GetHashCode()}";
|
|
||||||
|
|
||||||
CreatorSearchResults? cachedResults = await cache.GetAsync<CreatorSearchResults>(cacheKey, cancellationToken);
|
|
||||||
|
|
||||||
if (cachedResults != null)
|
|
||||||
return new SearchCreatorsResponse(cachedResults);
|
|
||||||
|
|
||||||
SearchResult<CreatorSearchItem> results = await searchProvider.SearchAsync(searchOptions, cancellationToken);
|
SearchResult<CreatorSearchItem> results = await searchProvider.SearchAsync(searchOptions, cancellationToken);
|
||||||
|
long elapsedMilliseconds = stopWatch.ElapsedMilliseconds;
|
||||||
|
|
||||||
CacheEntryOptions cacheEntryOptions = new()
|
LogEvents.SearchCompleted(
|
||||||
{
|
logger,
|
||||||
SlidingExpiration = TimeSpan.FromMinutes(10)
|
Elapsed: elapsedMilliseconds,
|
||||||
};
|
Items: results.Items.Length,
|
||||||
|
Total: results.TotalItems,
|
||||||
await cache.SetAsync(cacheKey, results, cacheEntryOptions, cancellationToken);
|
Page: searchOptions.PageNumber,
|
||||||
|
Size: searchOptions.PageSize,
|
||||||
|
Sort: searchOptions.SortOptions.ToLogObject(),
|
||||||
|
Criteria: searchOptions.Criteria.ToLogObject()
|
||||||
|
);
|
||||||
|
|
||||||
return new SearchCreatorsResponse(results);
|
return new SearchCreatorsResponse(results);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.10" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.10" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.10" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
51
JSMR.Application/Logging/CriteriaLoggingExtensions.cs
Normal file
51
JSMR.Application/Logging/CriteriaLoggingExtensions.cs
Normal file
@@ -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();
|
||||||
|
//}
|
||||||
|
}
|
||||||
59
JSMR.Application/Logging/LogEvents.cs
Normal file
59
JSMR.Application/Logging/LogEvents.cs
Normal file
@@ -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);
|
||||||
|
}
|
||||||
36
JSMR.Application/Logging/LogObjectBuilder.cs
Normal file
36
JSMR.Application/Logging/LogObjectBuilder.cs
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
namespace JSMR.Application.Logging;
|
||||||
|
|
||||||
|
public sealed class LogObjectBuilder
|
||||||
|
{
|
||||||
|
private readonly Dictionary<string, object> _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<T>(string key, IReadOnlyCollection<T>? 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<T>(string key, T value) where T : struct, IEquatable<T>
|
||||||
|
=> value.Equals(default) ? this : Add(key, value);
|
||||||
|
|
||||||
|
public object Build() => _d;
|
||||||
|
}
|
||||||
87
JSMR.Application/Logging/LoggingExtensions.cs
Normal file
87
JSMR.Application/Logging/LoggingExtensions.cs
Normal file
@@ -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<TCriteria, TSortField>(this SearchOptions<TCriteria, TSortField> 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>(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<TSortField>(this IEnumerable<SortOption<TSortField>> 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -38,7 +38,7 @@ public sealed class ScanVoiceWorksHandler(
|
|||||||
string[] productIds = [.. works.Where(x => !string.IsNullOrWhiteSpace(x.ProductId)).Select(x => x.ProductId!)];
|
string[] productIds = [.. works.Where(x => !string.IsNullOrWhiteSpace(x.ProductId)).Select(x => x.ProductId!)];
|
||||||
VoiceWorkDetailCollection voiceWorkDetails = await dlsiteClient.GetVoiceWorkDetailsAsync(productIds, cancellationToken);
|
VoiceWorkDetailCollection voiceWorkDetails = await dlsiteClient.GetVoiceWorkDetailsAsync(productIds, cancellationToken);
|
||||||
|
|
||||||
List<VoiceWorkIngest> ingests = [.. works.Select(work =>
|
VoiceWorkIngest[] ingests = [.. works.Select(work =>
|
||||||
{
|
{
|
||||||
voiceWorkDetails.TryGetValue(work.ProductId!, out VoiceWorkDetails? value);
|
voiceWorkDetails.TryGetValue(work.ProductId!, out VoiceWorkDetails? value);
|
||||||
return new VoiceWorkIngest(work, value);
|
return new VoiceWorkIngest(work, value);
|
||||||
|
|||||||
@@ -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.Contracts;
|
||||||
using JSMR.Application.Tags.Queries.Search.Ports;
|
using JSMR.Application.Tags.Queries.Search.Ports;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
namespace JSMR.Application.Tags.Queries.Search;
|
namespace JSMR.Application.Tags.Queries.Search;
|
||||||
|
|
||||||
public sealed class SearchTagsHandler(ITagSearchProvider searchProvider)
|
public sealed class SearchTagsHandler(ITagSearchProvider searchProvider, ILogger<SearchTagsHandler> logger)
|
||||||
{
|
{
|
||||||
public async Task<SearchTagsResponse> HandleAsync(SearchTagsRequest request, CancellationToken cancellationToken)
|
public async Task<SearchTagsResponse> HandleAsync(SearchTagsRequest request, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
SearchOptions<TagSearchCriteria, TagSortField> searchOptions = request.Options;
|
SearchOptions<TagSearchCriteria, TagSortField> searchOptions = request.Options;
|
||||||
|
Stopwatch stopWatch = Stopwatch.StartNew();
|
||||||
//string cacheKey = $"tag:{searchOptions.GetHashCode()}";
|
|
||||||
|
|
||||||
//TagSearchResults? cachedResults = await cache.GetAsync<TagSearchResults>(cacheKey, cancellationToken);
|
|
||||||
|
|
||||||
//if (cachedResults != null)
|
|
||||||
// return new SearchTagsResponse(cachedResults);
|
|
||||||
|
|
||||||
SearchResult<TagSearchItem> results = await searchProvider.SearchAsync(searchOptions, cancellationToken);
|
SearchResult<TagSearchItem> results = await searchProvider.SearchAsync(searchOptions, cancellationToken);
|
||||||
|
long elapsedMilliseconds = stopWatch.ElapsedMilliseconds;
|
||||||
|
|
||||||
//CacheEntryOptions cacheEntryOptions = new()
|
LogEvents.SearchCompleted(
|
||||||
//{
|
logger,
|
||||||
// SlidingExpiration = TimeSpan.FromMinutes(10)
|
Elapsed: elapsedMilliseconds,
|
||||||
//};
|
Items: results.Items.Length,
|
||||||
|
Total: results.TotalItems,
|
||||||
//await cache.SetAsync(cacheKey, results, cacheEntryOptions, cancellationToken);
|
Page: searchOptions.PageNumber,
|
||||||
|
Size: searchOptions.PageSize,
|
||||||
|
Sort: searchOptions.SortOptions.ToLogObject(),
|
||||||
|
Criteria: searchOptions.Criteria.ToLogObject()
|
||||||
|
);
|
||||||
|
|
||||||
return new SearchTagsResponse(results);
|
return new SearchTagsResponse(results);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,30 +1,31 @@
|
|||||||
using JSMR.Application.Common.Caching;
|
using JSMR.Application.Common.Search;
|
||||||
using JSMR.Application.VoiceWorks.Ports;
|
using JSMR.Application.Logging;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
namespace JSMR.Application.VoiceWorks.Queries.Search;
|
namespace JSMR.Application.VoiceWorks.Queries.Search;
|
||||||
|
|
||||||
// TODO: Caching?
|
// TODO: Caching?
|
||||||
public sealed class SearchVoiceWorksHandler(IVoiceWorkSearchProvider provider)
|
public sealed class SearchVoiceWorksHandler(IVoiceWorkSearchProvider provider, ILogger<SearchVoiceWorksHandler> logger)
|
||||||
{
|
{
|
||||||
public async Task<SearchVoiceWorksResponse> HandleAsync(SearchVoiceWorksRequest request, CancellationToken cancellationToken)
|
public async Task<SearchVoiceWorksResponse> HandleAsync(SearchVoiceWorksRequest request, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var searchOptions = request.Options;
|
var searchOptions = request.Options;
|
||||||
|
|
||||||
//string cacheKey = $"vw:{searchOptions.GetHashCode()}";
|
Stopwatch stopWatch = Stopwatch.StartNew();
|
||||||
|
SearchResult<VoiceWorkSearchResult> results = await provider.SearchAsync(searchOptions, cancellationToken);
|
||||||
|
long elapsedMilliseconds = stopWatch.ElapsedMilliseconds;
|
||||||
|
|
||||||
//VoiceWorkSearchResults? cachedResults = await cache.GetAsync<VoiceWorkSearchResults>(cacheKey, cancellationToken);
|
LogEvents.SearchCompleted(
|
||||||
|
logger,
|
||||||
//if (cachedResults != null)
|
Elapsed: elapsedMilliseconds,
|
||||||
// return new SearchVoiceWorksResponse(cachedResults);
|
Items: results.Items.Length,
|
||||||
|
Total: results.TotalItems,
|
||||||
var results = await provider.SearchAsync(searchOptions, cancellationToken);
|
Page: searchOptions.PageNumber,
|
||||||
|
Size: searchOptions.PageSize,
|
||||||
//CacheEntryOptions cacheEntryOptions = new()
|
Sort: searchOptions.SortOptions.ToLogObject(),
|
||||||
//{
|
Criteria: searchOptions.Criteria.ToLogObject()
|
||||||
// SlidingExpiration = TimeSpan.FromMinutes(10)
|
);
|
||||||
//};
|
|
||||||
|
|
||||||
//await cache.SetAsync(cacheKey, results, cacheEntryOptions, cancellationToken);
|
|
||||||
|
|
||||||
return new SearchVoiceWorksResponse(results);
|
return new SearchVoiceWorksResponse(results);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ public class VoiceWorkSearchCriteria
|
|||||||
public int[] CreatorIds { get; init; } = [];
|
public int[] CreatorIds { get; init; } = [];
|
||||||
public bool IncludeAllCreators { get; init; }
|
public bool IncludeAllCreators { get; init; }
|
||||||
public Locale Locale { get; init; } = Locale.Japanese;
|
public Locale Locale { get; init; } = Locale.Japanese;
|
||||||
public DateTime? ReleaseDateStart { get; init; }
|
public DateOnly? ReleaseDateStart { get; init; }
|
||||||
public DateTime? ReleaseDateEnd { get; init; }
|
public DateOnly? ReleaseDateEnd { get; init; }
|
||||||
public AgeRating[] AgeRatings { get; init; } = [];
|
public AgeRating[] AgeRatings { get; init; } = [];
|
||||||
public Language[] SupportedLanguages { get; init; } = [];
|
public Language[] SupportedLanguages { get; init; } = [];
|
||||||
public AIGeneration[] AIGenerationOptions { get; init; } = [];
|
public AIGeneration[] AIGenerationOptions { get; init; } = [];
|
||||||
|
|||||||
@@ -62,10 +62,10 @@ public class VoiceWorkSearchProvider(AppDbContext context, IVoiceWorkFullTextSea
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (criteria.ReleaseDateStart is not null)
|
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)
|
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)
|
if (criteria.AgeRatings.Length > 0)
|
||||||
filteredQuery = filteredQuery.Where(x => criteria.AgeRatings.Contains((AgeRating)x.VoiceWork.Rating));
|
filteredQuery = filteredQuery.Where(x => criteria.AgeRatings.Contains((AgeRating)x.VoiceWork.Rating));
|
||||||
|
|||||||
@@ -657,8 +657,8 @@ public class VoiceWorkSearchProviderTests(VoiceWorkSearchProviderFixture fixture
|
|||||||
{
|
{
|
||||||
Criteria = new()
|
Criteria = new()
|
||||||
{
|
{
|
||||||
ReleaseDateStart = new DateTime(2025, 1, 1),
|
ReleaseDateStart = new DateOnly(2025, 1, 1),
|
||||||
ReleaseDateEnd = new DateTime(2025, 1, 2)
|
ReleaseDateEnd = new DateOnly(2025, 1, 2)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -28,8 +28,8 @@
|
|||||||
<PackageReference Include="NSubstitute" Version="5.3.0" />
|
<PackageReference Include="NSubstitute" Version="5.3.0" />
|
||||||
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="9.0.0" />
|
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="9.0.0" />
|
||||||
<PackageReference Include="Shouldly" Version="4.3.0" />
|
<PackageReference Include="Shouldly" Version="4.3.0" />
|
||||||
<PackageReference Include="Testcontainers" Version="4.7.0" />
|
<PackageReference Include="Testcontainers" Version="4.8.0" />
|
||||||
<PackageReference Include="Testcontainers.MariaDb" Version="4.7.0" />
|
<PackageReference Include="Testcontainers.MariaDb" Version="4.8.0" />
|
||||||
<PackageReference Include="xunit" Version="2.9.3" />
|
<PackageReference Include="xunit" Version="2.9.3" />
|
||||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5">
|
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
|||||||
Reference in New Issue
Block a user