mirror of
https://github.com/aliasvault/aliasvault.git
synced 2026-05-15 10:55:31 -04:00
* Enable logging non-warnings to database log and adjust warnings (#443) * Add log level filter (#443) * Update General.razor (#443)
412 lines
14 KiB
Plaintext
412 lines
14 KiB
Plaintext
@page "/logging/general"
|
|
@using AliasVault.RazorComponents.Tables
|
|
@inherits MainBase
|
|
|
|
<LayoutPageTitle>System logs</LayoutPageTitle>
|
|
|
|
<PageHeader
|
|
BreadcrumbItems="@BreadcrumbItems"
|
|
Title="@(TotalRecords > 0 ? $"General logs ({TotalRecords:N0})" : "General logs")"
|
|
Description="This page shows an overview of recent system logs.">
|
|
<CustomActions>
|
|
<DeleteButton OnClick="DeleteLogsWithConfirmation" ButtonText="Delete all logs" />
|
|
<RefreshButton OnClick="() => RefreshData(CancellationToken.None)" ButtonText="Refresh" />
|
|
</CustomActions>
|
|
</PageHeader>
|
|
|
|
@if (IsInitialized)
|
|
{
|
|
<div class="px-4">
|
|
<Paginator CurrentPage="CurrentPage" PageSize="PageSize" TotalRecords="TotalRecords" OnPageChanged="HandlePageChanged" />
|
|
|
|
<div class="mb-3 flex space-x-4">
|
|
<div class="flex w-full">
|
|
<div class="w-1/2 pr-2">
|
|
<div class="relative">
|
|
<SearchIcon />
|
|
<input type="text" @bind-value="SearchTerm" @bind-value:event="oninput" id="search" placeholder="Search logs..." class="w-full px-4 ps-10 py-2 border rounded text-sm text-gray-700 focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white">
|
|
</div>
|
|
</div>
|
|
<div class="w-1/4 px-2">
|
|
<select @bind="SelectedServiceName" class="w-full px-4 py-2 border rounded text-sm text-gray-700 focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white">
|
|
<option value="">All Services</option>
|
|
@foreach (var service in ServiceNames)
|
|
{
|
|
<option value="@service">@service</option>
|
|
}
|
|
</select>
|
|
</div>
|
|
<div class="w-1/4 pl-2">
|
|
<select @bind="SelectedLogLevel" class="w-full px-4 py-2 border rounded text-sm text-gray-700 focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white">
|
|
<option value="">All Levels</option>
|
|
@foreach (var level in LogLevels)
|
|
{
|
|
<option value="@level">@level</option>
|
|
}
|
|
</select>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
}
|
|
|
|
@if (IsLoading)
|
|
{
|
|
<LoadingIndicator />
|
|
}
|
|
else
|
|
{
|
|
<div class="px-4">
|
|
<SortableTable Columns="@_tableColumns" SortColumn="@SortColumn" SortDirection="@SortDirection" OnSortChanged="HandleSortChanged">
|
|
@foreach (var log in LogList)
|
|
{
|
|
<SortableTableRow>
|
|
<SortableTableColumn IsPrimary="true">@log.Id</SortableTableColumn>
|
|
<SortableTableColumn>@log.TimeStamp.ToString("yyyy-MM-dd HH:mm")</SortableTableColumn>
|
|
<SortableTableColumn>@log.Application</SortableTableColumn>
|
|
<SortableTableColumn>
|
|
@{
|
|
string bgColor = log.Level switch
|
|
{
|
|
"Verbose" => "bg-gray-500",
|
|
"Debug" => "bg-green-500",
|
|
"Information" => "bg-blue-500",
|
|
"Warning" => "bg-yellow-500",
|
|
"Error" => "bg-red-500",
|
|
"Fatal" => "bg-red-700",
|
|
_ => "bg-gray-500"
|
|
};
|
|
}
|
|
<span class="px-2 py-1 rounded-full text-white @bgColor">
|
|
@log.Level
|
|
</span>
|
|
</SortableTableColumn>
|
|
<SortableTableColumn Title="@log.Exception">
|
|
@if (log.SourceContext.Length > 0)
|
|
{
|
|
<span>@log.SourceContext: </span>
|
|
}
|
|
@log.Message
|
|
</SortableTableColumn>
|
|
</SortableTableRow>
|
|
}
|
|
</SortableTable>
|
|
</div>
|
|
}
|
|
|
|
@code {
|
|
private readonly List<TableColumn> _tableColumns = [
|
|
new TableColumn { Title = "ID", PropertyName = "Id" },
|
|
new TableColumn { Title = "Time", PropertyName = "Timestamp" },
|
|
new TableColumn { Title = "Application", PropertyName = "Application" },
|
|
new TableColumn { Title = "Level", PropertyName = "Level" },
|
|
new TableColumn { Title = "Message", PropertyName = "Message" },
|
|
];
|
|
|
|
private List<Log> LogList { get; set; } = [];
|
|
private bool IsInitialized { get; set; } = false;
|
|
|
|
private bool IsLoading { get; set; } = true;
|
|
private int CurrentPage { get; set; } = 1;
|
|
private int PageSize { get; set; } = 50;
|
|
private int TotalRecords { get; set; }
|
|
|
|
private string _searchTerm = string.Empty;
|
|
private string _lastSearchTerm = string.Empty;
|
|
private CancellationTokenSource? _searchCancellationTokenSource;
|
|
|
|
private string SearchTerm
|
|
{
|
|
get => _searchTerm;
|
|
set
|
|
{
|
|
if (_searchTerm != value)
|
|
{
|
|
_searchTerm = value;
|
|
_searchCancellationTokenSource?.Cancel();
|
|
_searchCancellationTokenSource = new CancellationTokenSource();
|
|
_ = RefreshData(_searchCancellationTokenSource.Token);
|
|
}
|
|
}
|
|
}
|
|
|
|
private string _selectedServiceName = string.Empty;
|
|
private string SelectedServiceName
|
|
{
|
|
get => _selectedServiceName;
|
|
set
|
|
{
|
|
if (_selectedServiceName != value)
|
|
{
|
|
_selectedServiceName = value;
|
|
_searchCancellationTokenSource?.Cancel();
|
|
_searchCancellationTokenSource = new CancellationTokenSource();
|
|
_ = RefreshData(_searchCancellationTokenSource.Token);
|
|
}
|
|
}
|
|
}
|
|
|
|
private List<string> ServiceNames { get; set; } = [];
|
|
private List<string> LogLevels { get; set; } = [];
|
|
|
|
private string _selectedLogLevel = string.Empty;
|
|
private string SelectedLogLevel
|
|
{
|
|
get => _selectedLogLevel;
|
|
set
|
|
{
|
|
if (_selectedLogLevel != value)
|
|
{
|
|
_selectedLogLevel = value;
|
|
_ = RefreshData();
|
|
}
|
|
}
|
|
}
|
|
|
|
private string SortColumn { get; set; } = "Id";
|
|
private SortDirection SortDirection { get; set; } = SortDirection.Descending;
|
|
|
|
private async Task HandleSortChanged((string column, SortDirection direction) sort)
|
|
{
|
|
SortColumn = sort.column;
|
|
SortDirection = sort.direction;
|
|
await RefreshData(CancellationToken.None);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
protected override async Task OnInitializedAsync()
|
|
{
|
|
await base.OnInitializedAsync();
|
|
BreadcrumbItems.Add(new BreadcrumbItem { DisplayName = "General logs" });
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
|
{
|
|
if (firstRender)
|
|
{
|
|
await using var dbContext = await DbContextFactory.CreateDbContextAsync();
|
|
ServiceNames = await dbContext.Logs.Select(l => l.Application).Distinct().OrderBy(x => x).ToListAsync();
|
|
|
|
// Get log levels and sort by severity (highest to lowest)
|
|
var levels = await dbContext.Logs.Select(l => l.Level).Distinct().ToListAsync();
|
|
LogLevels = levels.OrderBy(GetLogLevelSeverityOrder).ToList();
|
|
|
|
await RefreshData(CancellationToken.None);
|
|
}
|
|
}
|
|
|
|
private void HandlePageChanged(int newPage)
|
|
{
|
|
CurrentPage = newPage;
|
|
_ = RefreshData(CancellationToken.None);
|
|
}
|
|
|
|
private async Task RefreshData(CancellationToken cancellationToken = default)
|
|
{
|
|
try
|
|
{
|
|
IsLoading = true;
|
|
StateHasChanged();
|
|
|
|
await using var dbContext = await DbContextFactory.CreateDbContextAsync(cancellationToken);
|
|
var query = dbContext.Logs.AsQueryable();
|
|
|
|
query = ApplyFilters(query);
|
|
query = ApplySorting(query);
|
|
|
|
TotalRecords = await query.CountAsync(cancellationToken);
|
|
LogList = await query
|
|
.Skip((CurrentPage - 1) * PageSize)
|
|
.Take(PageSize)
|
|
.ToListAsync(cancellationToken);
|
|
|
|
if (cancellationToken.IsCancellationRequested)
|
|
{
|
|
return;
|
|
}
|
|
|
|
IsLoading = false;
|
|
IsInitialized = true;
|
|
StateHasChanged();
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
// Expected when cancellation is requested, do nothing
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Applies all filters to the query.
|
|
/// </summary>
|
|
private IQueryable<Log> ApplyFilters(IQueryable<Log> query)
|
|
{
|
|
query = ApplySearchTermFilter(query);
|
|
query = ApplyServiceNameFilter(query);
|
|
|
|
if (!string.IsNullOrEmpty(SelectedLogLevel))
|
|
{
|
|
query = query.Where(x => x.Level == SelectedLogLevel);
|
|
}
|
|
|
|
return query;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Applies sorting to the query based on SortColumn and SortDirection.
|
|
/// </summary>
|
|
private IQueryable<Log> ApplySorting(IQueryable<Log> query)
|
|
{
|
|
switch (SortColumn)
|
|
{
|
|
case "Application":
|
|
query = SortDirection == SortDirection.Ascending
|
|
? query.OrderBy(x => x.Application)
|
|
: query.OrderByDescending(x => x.Application);
|
|
break;
|
|
case "Message":
|
|
query = SortDirection == SortDirection.Ascending
|
|
? query.OrderBy(x => x.Message)
|
|
: query.OrderByDescending(x => x.Message);
|
|
break;
|
|
case "Level":
|
|
query = ApplyLevelSorting(query);
|
|
break;
|
|
case "Timestamp":
|
|
query = SortDirection == SortDirection.Ascending
|
|
? query.OrderBy(x => x.TimeStamp)
|
|
: query.OrderByDescending(x => x.TimeStamp);
|
|
break;
|
|
default:
|
|
query = SortDirection == SortDirection.Ascending
|
|
? query.OrderBy(x => x.Id)
|
|
: query.OrderByDescending(x => x.Id);
|
|
break;
|
|
}
|
|
|
|
return query;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Applies special sorting for log levels based on severity.
|
|
/// </summary>
|
|
private IQueryable<Log> ApplyLevelSorting(IQueryable<Log> query)
|
|
{
|
|
if (SortDirection == SortDirection.Ascending)
|
|
{
|
|
// Sort from lowest severity (Verbose) to highest (Fatal)
|
|
query = query
|
|
.OrderBy(x => x.Level != "Verbose")
|
|
.ThenBy(x => x.Level != "Debug")
|
|
.ThenBy(x => x.Level != "Information")
|
|
.ThenBy(x => x.Level != "Warning")
|
|
.ThenBy(x => x.Level != "Error")
|
|
.ThenBy(x => x.Level != "Fatal")
|
|
.ThenBy(x => x.Id);
|
|
}
|
|
else
|
|
{
|
|
// Sort from highest severity (Fatal) to lowest (Verbose)
|
|
query = query
|
|
.OrderBy(x => x.Level != "Fatal")
|
|
.ThenBy(x => x.Level != "Error")
|
|
.ThenBy(x => x.Level != "Warning")
|
|
.ThenBy(x => x.Level != "Information")
|
|
.ThenBy(x => x.Level != "Debug")
|
|
.ThenBy(x => x.Level != "Verbose")
|
|
.ThenByDescending(x => x.Id);
|
|
}
|
|
|
|
return query;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Applies a search term filter to the query.
|
|
/// </summary>
|
|
/// <param name="query">The query to apply the filter to.</param>
|
|
private IQueryable<Log> ApplySearchTermFilter(IQueryable<Log> query)
|
|
{
|
|
if (!string.IsNullOrEmpty(SearchTerm))
|
|
{
|
|
// Reset page number back to 1 if the search term has changed.
|
|
if (SearchTerm != _lastSearchTerm)
|
|
{
|
|
CurrentPage = 1;
|
|
}
|
|
_lastSearchTerm = SearchTerm;
|
|
|
|
var searchTerm = SearchTerm.Trim().ToLower();
|
|
query = query.Where(x => EF.Functions.Like(x.Application.ToLower(), "%" + searchTerm + "%") ||
|
|
EF.Functions.Like(x.Message.ToLower(), "%" + searchTerm + "%") ||
|
|
EF.Functions.Like(x.Level.ToLower(), "%" + searchTerm + "%") ||
|
|
EF.Functions.Like(x.SourceContext.ToLower(), "%" + searchTerm + "%"));
|
|
}
|
|
|
|
return query;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Applies a service name filter to the query.
|
|
/// </summary>
|
|
/// <param name="query">The query to apply the filter to.</param>
|
|
private IQueryable<Log> ApplyServiceNameFilter(IQueryable<Log> query)
|
|
{
|
|
if (!string.IsNullOrEmpty(SelectedServiceName))
|
|
{
|
|
query = query.Where(x => x.Application == SelectedServiceName);
|
|
}
|
|
|
|
return query;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the severity order for a log level (lower number = higher severity).
|
|
/// Used for sorting log levels in dropdown by severity instead of alphabetically.
|
|
/// </summary>
|
|
private static int GetLogLevelSeverityOrder(string level)
|
|
{
|
|
return level switch
|
|
{
|
|
"Fatal" => 0, // Highest severity
|
|
"Error" => 1,
|
|
"Warning" => 2,
|
|
"Information" => 3,
|
|
"Debug" => 4,
|
|
"Verbose" => 5, // Lowest severity
|
|
_ => 6
|
|
};
|
|
}
|
|
|
|
private async Task DeleteLogsWithConfirmation()
|
|
{
|
|
if (await ConfirmModalService.ShowConfirmation("Confirm Delete", "Are you sure you want to delete all logs? This action cannot be undone."))
|
|
{
|
|
await DeleteLogs();
|
|
}
|
|
}
|
|
|
|
private async Task DeleteLogs()
|
|
{
|
|
IsLoading = true;
|
|
StateHasChanged();
|
|
|
|
await using var dbContext = await DbContextFactory.CreateDbContextAsync();
|
|
dbContext.Logs.RemoveRange(dbContext.Logs);
|
|
await dbContext.SaveChangesAsync();
|
|
await RefreshData(CancellationToken.None);
|
|
|
|
IsLoading = false;
|
|
StateHasChanged();
|
|
}
|
|
|
|
protected override void Dispose(bool disposing)
|
|
{
|
|
if (disposing)
|
|
{
|
|
_searchCancellationTokenSource?.Cancel();
|
|
_searchCancellationTokenSource?.Dispose();
|
|
}
|
|
base.Dispose(disposing);
|
|
}
|
|
}
|