Add date filtering to client logs diagnostics
TaxBaik CI/CD / build-and-deploy (push) Has been cancelled

This commit is contained in:
2026-07-08 02:04:15 +09:00
parent f502e88f89
commit ae8de9763b
2 changed files with 66 additions and 33 deletions
@@ -23,24 +23,6 @@
<div><strong>Info</strong> @_summary.InfoCount</div> <div><strong>Info</strong> @_summary.InfoCount</div>
</div> </div>
} }
@if (_groups.Count > 0)
{
<div class="admin-log-group-list">
@foreach (var group in _groups)
{
<article class="admin-log-group-item">
<div class="admin-log-item-header">
<strong>@group.Count</strong>
<span>@group.Level</span>
<span>@group.Source</span>
<span>@group.Route</span>
</div>
<p>@group.Message</p>
<small>@group.LastSeen</small>
</article>
}
</div>
}
<div class="admin-log-filter-row"> <div class="admin-log-filter-row">
<input class="mud-input-slot" placeholder="검색어" @bind="_query" @bind:event="oninput" /> <input class="mud-input-slot" placeholder="검색어" @bind="_query" @bind:event="oninput" />
<select class="mud-input-slot" @bind="_level"> <select class="mud-input-slot" @bind="_level">
@@ -49,6 +31,8 @@
<option value="warning">warning</option> <option value="warning">warning</option>
<option value="info">info</option> <option value="info">info</option>
</select> </select>
<input class="mud-input-slot" placeholder="시작 ISO 시각" @bind="_start" @bind:event="oninput" />
<input class="mud-input-slot" placeholder="종료 ISO 시각" @bind="_end" @bind:event="oninput" />
<button type="button" class="mud-button-root" @onclick="LoadAsync">새로고침</button> <button type="button" class="mud-button-root" @onclick="LoadAsync">새로고침</button>
</div> </div>
@if (_loading) @if (_loading)
@@ -76,6 +60,27 @@
} }
</div> </div>
} }
@if (_groups.Count > 0)
{
<details class="admin-log-group-details">
<summary>반복 오류 그룹 보기</summary>
<div class="admin-log-group-list">
@foreach (var group in _groups)
{
<article class="admin-log-group-item">
<div class="admin-log-item-header">
<strong>@group.Count</strong>
<span>@group.Level</span>
<span>@group.Source</span>
<span>@group.Route</span>
</div>
<p>@group.Message</p>
<small>@group.LastSeen</small>
</article>
}
</div>
</details>
}
</div> </div>
</section> </section>
@@ -85,6 +90,8 @@
private bool _loading = true; private bool _loading = true;
private string _query = ""; private string _query = "";
private string _level = ""; private string _level = "";
private string _start = "";
private string _end = "";
private ClientLogSummaryDto? _summary; private ClientLogSummaryDto? _summary;
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
@@ -101,7 +108,7 @@
try try
{ {
_summary = await Http.GetFromJsonAsync<ClientLogSummaryDto>("/taxbaik/api/client-logs/summary"); _summary = await Http.GetFromJsonAsync<ClientLogSummaryDto>("/taxbaik/api/client-logs/summary");
var url = $"/taxbaik/api/client-logs/recent?q={Uri.EscapeDataString(_query ?? string.Empty)}&level={Uri.EscapeDataString(_level ?? string.Empty)}"; var url = $"/taxbaik/api/client-logs/recent?q={Uri.EscapeDataString(_query ?? string.Empty)}&level={Uri.EscapeDataString(_level ?? string.Empty)}&start={Uri.EscapeDataString(_start ?? string.Empty)}&end={Uri.EscapeDataString(_end ?? string.Empty)}";
var result = await Http.GetFromJsonAsync<List<ClientLogDto>>(url); var result = await Http.GetFromJsonAsync<List<ClientLogDto>>(url);
var grouped = await Http.GetFromJsonAsync<List<ClientLogGroupDto>>("/taxbaik/api/client-logs/groups"); var grouped = await Http.GetFromJsonAsync<List<ClientLogGroupDto>>("/taxbaik/api/client-logs/groups");
if (result is not null) if (result is not null)
@@ -5,6 +5,7 @@ namespace TaxBaik.Web.Endpoints.ClientLogs;
public sealed class RecentClientLogEntry public sealed class RecentClientLogEntry
{ {
public string? Timestamp { get; set; } public string? Timestamp { get; set; }
public DateTimeOffset? OccurredAt { get; set; }
public string? Level { get; set; } public string? Level { get; set; }
public string? Source { get; set; } public string? Source { get; set; }
public string? Message { get; set; } public string? Message { get; set; }
@@ -37,6 +38,8 @@ public sealed class GetRecentLogsEndpoint : EndpointWithoutRequest<IReadOnlyList
var source = Query<string>("source"); var source = Query<string>("source");
var route = Query<string>("route"); var route = Query<string>("route");
var term = Query<string>("q"); var term = Query<string>("q");
var start = ParseDate(Query<string>("start"));
var end = ParseDate(Query<string>("end"));
var logDir = Path.Combine(_environment.ContentRootPath, "logs"); var logDir = Path.Combine(_environment.ContentRootPath, "logs");
if (!Directory.Exists(logDir)) if (!Directory.Exists(logDir))
@@ -60,7 +63,7 @@ public sealed class GetRecentLogsEndpoint : EndpointWithoutRequest<IReadOnlyList
continue; continue;
var entry = Parse(line); var entry = Parse(line);
if (Matches(entry, level, source, route, term)) if (Matches(entry, level, source, route, term, start, end))
entries.Add(entry); entries.Add(entry);
} }
} }
@@ -70,18 +73,21 @@ public sealed class GetRecentLogsEndpoint : EndpointWithoutRequest<IReadOnlyList
private static RecentClientLogEntry Parse(string line) private static RecentClientLogEntry Parse(string line)
{ {
var entry = new RecentClientLogEntry(); var timestamp = ExtractTimestamp(line);
entry.Timestamp = ExtractTimestamp(line); return new RecentClientLogEntry
entry.Level = ExtractBetween(line, "] [", "]"); {
entry.Source = ExtractTokenAfter(line, "ClientLog ", 1); Timestamp = timestamp,
entry.Route = ExtractNamedValue(line, "Route="); OccurredAt = ParseDate(timestamp),
entry.Screen = ExtractNamedValue(line, "Screen="); Level = ExtractBetween(line, "] [", "]"),
entry.Feature = ExtractNamedValue(line, "Feature="); Source = ExtractTokenAfter(line, "ClientLog ", 1),
entry.Action = ExtractNamedValue(line, "Action="); Route = ExtractNamedValue(line, "Route="),
entry.Step = ExtractNamedValue(line, "Step="); Screen = ExtractNamedValue(line, "Screen="),
entry.BuildVersion = ExtractNamedValue(line, "BuildVersion="); Feature = ExtractNamedValue(line, "Feature="),
entry.Message = ExtractMessage(line); Action = ExtractNamedValue(line, "Action="),
return entry; Step = ExtractNamedValue(line, "Step="),
BuildVersion = ExtractNamedValue(line, "BuildVersion="),
Message = ExtractMessage(line)
};
} }
private static string? ExtractTimestamp(string line) private static string? ExtractTimestamp(string line)
@@ -120,7 +126,7 @@ public sealed class GetRecentLogsEndpoint : EndpointWithoutRequest<IReadOnlyList
return clientIndex >= 0 ? line[(clientIndex + "ClientLog ".Length)..] : line; return clientIndex >= 0 ? line[(clientIndex + "ClientLog ".Length)..] : line;
} }
private static bool Matches(RecentClientLogEntry entry, string? level, string? source, string? route, string? term) private static bool Matches(RecentClientLogEntry entry, string? level, string? source, string? route, string? term, DateTimeOffset? start, DateTimeOffset? end)
{ {
if (!string.IsNullOrWhiteSpace(level) && if (!string.IsNullOrWhiteSpace(level) &&
!string.Equals(entry.Level, level, StringComparison.OrdinalIgnoreCase)) !string.Equals(entry.Level, level, StringComparison.OrdinalIgnoreCase))
@@ -147,9 +153,29 @@ public sealed class GetRecentLogsEndpoint : EndpointWithoutRequest<IReadOnlyList
return false; return false;
} }
if (start is not null && (entry.OccurredAt is null || entry.OccurredAt < start))
{
return false;
}
if (end is not null && (entry.OccurredAt is null || entry.OccurredAt > end))
{
return false;
}
return true; return true;
} }
private static DateTimeOffset? ParseDate(string? value)
{
if (string.IsNullOrWhiteSpace(value))
{
return null;
}
return DateTimeOffset.TryParse(value, out var parsed) ? parsed : null;
}
private static string? ExtractNamedValue(string text, string prefix) private static string? ExtractNamedValue(string text, string prefix)
{ {
var index = text.IndexOf(prefix, StringComparison.OrdinalIgnoreCase); var index = text.IndexOf(prefix, StringComparison.OrdinalIgnoreCase);