Add client log summary and dashboard shortcut
TaxBaik CI/CD / build-and-deploy (push) Successful in 4m51s

This commit is contained in:
2026-07-08 01:59:40 +09:00
parent 441336394c
commit d09c58d3f8
3 changed files with 131 additions and 0 deletions
@@ -0,0 +1,107 @@
using FastEndpoints;
namespace TaxBaik.Web.Endpoints.ClientLogs;
public sealed class ClientLogSummary
{
public int Total { get; set; }
public int ErrorCount { get; set; }
public int WarningCount { get; set; }
public int InfoCount { get; set; }
public IReadOnlyList<KeyValuePair<string, int>> TopSources { get; set; } = [];
public IReadOnlyList<KeyValuePair<string, int>> TopRoutes { get; set; } = [];
}
public sealed class GetSummaryEndpoint : EndpointWithoutRequest<ClientLogSummary>
{
private readonly IWebHostEnvironment _environment;
public GetSummaryEndpoint(IWebHostEnvironment environment)
{
_environment = environment;
}
public override void Configure()
{
Get("/api/client-logs/summary");
Policies("AdminOnly");
}
public override async Task HandleAsync(CancellationToken ct)
{
var logDir = Path.Combine(_environment.ContentRootPath, "logs");
var summary = new ClientLogSummary();
if (!Directory.Exists(logDir))
{
await SendAsync(summary, cancellation: ct);
return;
}
var files = Directory.GetFiles(logDir, "taxbaik-*.log")
.OrderByDescending(File.GetLastWriteTimeUtc)
.Take(3)
.ToArray();
var sourceCounts = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
var routeCounts = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
foreach (var file in files)
{
foreach (var line in await File.ReadAllLinesAsync(file, ct))
{
if (!line.Contains("ClientLog ", StringComparison.OrdinalIgnoreCase))
continue;
summary.Total++;
var level = ExtractLevel(line);
if (string.Equals(level, "error", StringComparison.OrdinalIgnoreCase))
summary.ErrorCount++;
else if (string.Equals(level, "warning", StringComparison.OrdinalIgnoreCase))
summary.WarningCount++;
else
summary.InfoCount++;
var source = ExtractSegment(line, "ClientLog ", 2) ?? "unknown";
var route = ExtractNamedValue(line, "Route=") ?? "unknown";
sourceCounts[source] = sourceCounts.TryGetValue(source, out var sourceCount) ? sourceCount + 1 : 1;
routeCounts[route] = routeCounts.TryGetValue(route, out var routeCount) ? routeCount + 1 : 1;
}
}
summary.TopSources = sourceCounts
.OrderByDescending(kvp => kvp.Value)
.Take(5)
.ToList();
summary.TopRoutes = routeCounts
.OrderByDescending(kvp => kvp.Value)
.Take(5)
.ToList();
await SendAsync(summary, cancellation: ct);
}
private static string? ExtractLevel(string line)
{
var start = line.IndexOf("ClientLog ", StringComparison.OrdinalIgnoreCase);
if (start < 0) return null;
var tokens = line[start..].Split(' ', StringSplitOptions.RemoveEmptyEntries);
return tokens.Length > 1 ? tokens[1] : null;
}
private static string? ExtractSegment(string text, string start, int tokenIndex)
{
var index = text.IndexOf(start, StringComparison.OrdinalIgnoreCase);
if (index < 0) return null;
var tokens = text[index..].Split(' ', StringSplitOptions.RemoveEmptyEntries);
return tokens.Length > tokenIndex ? tokens[tokenIndex] : null;
}
private static string? ExtractNamedValue(string text, string prefix)
{
var index = text.IndexOf(prefix, StringComparison.OrdinalIgnoreCase);
if (index < 0) return null;
var slice = text[(index + prefix.Length)..];
var end = slice.IndexOf(' ');
return end < 0 ? slice : slice[..end];
}
}