Browse Source

reimplement log viewer (#1094)

pull/1096/head
Jason Dove 3 years ago committed by GitHub
parent
commit
ab7051f075
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 16
      ErsatzTV.Application/Libraries/Commands/CallLibraryScannerHandler.cs
  2. 8
      ErsatzTV.Application/Logs/LogEntryViewModel.cs
  3. 31
      ErsatzTV.Application/Logs/Mapper.cs
  4. 3
      ErsatzTV.Application/Logs/PagedLogEntriesViewModel.cs
  5. 9
      ErsatzTV.Application/Logs/Queries/GetRecentLogEntries.cs
  6. 65
      ErsatzTV.Application/Logs/Queries/GetRecentLogEntriesHandler.cs
  7. 9
      ErsatzTV.Core/Domain/LogEntry.cs
  8. 2
      ErsatzTV.Core/FFmpeg/FFmpegLocator.cs
  9. 109
      ErsatzTV/Pages/Logs.razor
  10. 5
      ErsatzTV/Program.cs
  11. 1
      ErsatzTV/Shared/MainLayout.razor

16
ErsatzTV.Application/Libraries/Commands/CallLibraryScannerHandler.cs

@ -1,4 +1,3 @@ @@ -1,4 +1,3 @@
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Threading.Channels;
using CliWrap;
@ -9,6 +8,7 @@ using ErsatzTV.Core.Metadata; @@ -9,6 +8,7 @@ using ErsatzTV.Core.Metadata;
using ErsatzTV.FFmpeg.Runtime;
using Newtonsoft.Json;
using Serilog;
using Serilog.Events;
using Serilog.Formatting.Compact.Reader;
namespace ErsatzTV.Application.Libraries;
@ -69,7 +69,17 @@ public abstract class CallLibraryScannerHandler @@ -69,7 +69,17 @@ public abstract class CallLibraryScannerHandler
{
try
{
Log.Write(LogEventReader.ReadFromString(s));
// make a new log event to force using local time
// because the compact json writer used by the scanner
// writes in UTC
LogEvent logEvent = LogEventReader.ReadFromString(s);
Log.Write(
new LogEvent(
logEvent.Timestamp.ToLocalTime(),
logEvent.Level,
logEvent.Exception,
logEvent.MessageTemplate,
logEvent.Properties.Map(pair => new LogEventProperty(pair.Key, pair.Value))));
}
catch
{
@ -127,7 +137,7 @@ public abstract class CallLibraryScannerHandler @@ -127,7 +137,7 @@ public abstract class CallLibraryScannerHandler
? "ErsatzTV.Scanner.exe"
: "ErsatzTV.Scanner";
string processFileName = Process.GetCurrentProcess().MainModule?.FileName ?? string.Empty;
string processFileName = Environment.ProcessPath ?? string.Empty;
if (!string.IsNullOrWhiteSpace(processFileName))
{
string localFileName = Path.Combine(Path.GetDirectoryName(processFileName) ?? string.Empty, executable);

8
ErsatzTV.Application/Logs/LogEntryViewModel.cs

@ -0,0 +1,8 @@ @@ -0,0 +1,8 @@
using Serilog.Events;
namespace ErsatzTV.Application.Logs;
public record LogEntryViewModel(
DateTimeOffset Timestamp,
LogEventLevel Level,
string Message);

31
ErsatzTV.Application/Logs/Mapper.cs

@ -0,0 +1,31 @@ @@ -0,0 +1,31 @@
using System.Text.RegularExpressions;
using Serilog.Events;
namespace ErsatzTV.Application.Logs;
internal partial class Mapper
{
[GeneratedRegex(@"(.*)\[(DBG|INF|WRN|ERR|FTL)\](.*)")]
private static partial Regex LogEntryRegex();
internal static Option<LogEntryViewModel> ProjectToViewModel(string line)
{
Match match = LogEntryRegex().Match(line);
if (!match.Success)
{
return None;
}
var timestamp = DateTimeOffset.Parse(match.Groups[1].Value);
LogEventLevel level = match.Groups[2].Value switch
{
"FTL" => LogEventLevel.Fatal,
"ERR" => LogEventLevel.Error,
"WRN" => LogEventLevel.Warning,
"INF" => LogEventLevel.Information,
_ => LogEventLevel.Debug
};
return new LogEntryViewModel(timestamp, level, match.Groups[3].Value);
}
}

3
ErsatzTV.Application/Logs/PagedLogEntriesViewModel.cs

@ -0,0 +1,3 @@ @@ -0,0 +1,3 @@
namespace ErsatzTV.Application.Logs;
public record PagedLogEntriesViewModel(int TotalCount, List<LogEntryViewModel> Page);

9
ErsatzTV.Application/Logs/Queries/GetRecentLogEntries.cs

@ -0,0 +1,9 @@ @@ -0,0 +1,9 @@
using System.Linq.Expressions;
namespace ErsatzTV.Application.Logs;
public record GetRecentLogEntries(int PageNum, int PageSize, string Filter) : IRequest<PagedLogEntriesViewModel>
{
public Expression<Func<LogEntryViewModel, object>> SortExpression { get; init; }
public Option<bool> SortDescending { get; init; }
}

65
ErsatzTV.Application/Logs/Queries/GetRecentLogEntriesHandler.cs

@ -0,0 +1,65 @@ @@ -0,0 +1,65 @@
using ErsatzTV.Core;
using ErsatzTV.Core.Interfaces.Metadata;
using static ErsatzTV.Application.Logs.Mapper;
namespace ErsatzTV.Application.Logs;
public class GetRecentLogEntriesHandler : IRequestHandler<GetRecentLogEntries, PagedLogEntriesViewModel>
{
private readonly ILocalFileSystem _localFileSystem;
public GetRecentLogEntriesHandler(ILocalFileSystem localFileSystem)
{
_localFileSystem = localFileSystem;
}
public Task<PagedLogEntriesViewModel> Handle(
GetRecentLogEntries request,
CancellationToken cancellationToken)
{
// get most recent file
string logFileName = _localFileSystem.ListFiles(FileSystemLayout.LogsFolder)
.OrderDescending()
.FirstOrDefault();
if (logFileName is not null)
{
IQueryable<LogEntryViewModel> entries = ReadFrom(logFileName)
.Bind(line => ProjectToViewModel(line))
.AsQueryable();
if (!string.IsNullOrWhiteSpace(request.Filter))
{
entries = entries.Filter(
le => le.Level.ToString().Contains(request.Filter, StringComparison.OrdinalIgnoreCase) ||
le.Message.Contains(request.Filter, StringComparison.OrdinalIgnoreCase));
}
int count = entries.Count();
IOrderedQueryable<LogEntryViewModel> ordered = request.SortDescending.Match(
descending => descending
? entries.OrderByDescending(request.SortExpression).ThenByDescending(le => le.Timestamp)
: entries.OrderBy(request.SortExpression).ThenByDescending(le => le.Timestamp),
() => entries.OrderByDescending(le => le.Timestamp));
var page = ordered
.Skip(request.PageNum * request.PageSize)
.Take(request.PageSize)
.ToList();
return new PagedLogEntriesViewModel(count, page).AsTask();
}
return new PagedLogEntriesViewModel(0, new List<LogEntryViewModel>()).AsTask();
}
private static IEnumerable<string> ReadFrom(string file)
{
using StreamReader reader = File.OpenText(file);
while (reader.ReadLine() is { } line)
{
yield return line;
}
}
}

9
ErsatzTV.Core/Domain/LogEntry.cs

@ -1,9 +0,0 @@ @@ -1,9 +0,0 @@
namespace ErsatzTV.Core.Domain;
public record LogEntry(
int Id,
DateTime Timestamp,
string Level,
string Exception,
string RenderedMessage,
string Properties);

2
ErsatzTV.Core/FFmpeg/FFmpegLocator.cs

@ -51,7 +51,7 @@ public class FFmpegLocator : IFFmpegLocator @@ -51,7 +51,7 @@ public class FFmpegLocator : IFFmpegLocator
? $"{executableBase}.exe"
: executableBase;
string processFileName = Process.GetCurrentProcess().MainModule?.FileName ?? string.Empty;
string processFileName = Environment.ProcessPath ?? string.Empty;
if (!string.IsNullOrWhiteSpace(processFileName))
{
string localFileName = Path.Combine(Path.GetDirectoryName(processFileName) ?? string.Empty, executable);

109
ErsatzTV/Pages/Logs.razor

@ -0,0 +1,109 @@ @@ -0,0 +1,109 @@
@page "/system/logs"
@using ErsatzTV.Application.Logs
@using ErsatzTV.Application.Configuration
@implements IDisposable
@inject IMediator Mediator
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8">
<MudTable FixedHeader="true"
@bind-RowsPerPage="@_rowsPerPage"
ServerData="@(new Func<TableState, Task<TableData<LogEntryViewModel>>>(ServerReload))"
Dense="true"
@ref="_table">
<ToolBarContent>
<MudText Typo="Typo.h6">Logs</MudText>
<MudSpacer/>
<MudTextField T="string" ValueChanged="@(s => OnSearch(s))" Placeholder="Search" Adornment="Adornment.Start"
AdornmentIcon="@Icons.Material.Filled.Search" IconSize="Size.Medium" Class="mt-0"></MudTextField>
</ToolBarContent>
<HeaderContent>
<MudTh>
<MudTableSortLabel T="LogEntryViewModel" SortLabel="Timestamp">
Timestamp
</MudTableSortLabel>
</MudTh>
<MudTh>
<MudTableSortLabel T="LogEntryViewModel" SortLabel="Level">
Level
</MudTableSortLabel>
</MudTh>
<MudTh>Message</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd DataLabel="Timestamp">@context.Timestamp</MudTd>
<MudTd DataLabel="Level">@context.Level</MudTd>
<MudTd DataLabel="Message">@context.Message</MudTd>
</RowTemplate>
<NoRecordsContent>
<MudText>No matching records found</MudText>
</NoRecordsContent>
<LoadingContent>
<MudText>Loading...</MudText>
</LoadingContent>
<PagerContent>
<MudTablePager/>
</PagerContent>
</MudTable>
</MudContainer>
@code {
private readonly CancellationTokenSource _cts = new();
private MudTable<LogEntryViewModel> _table;
private int _rowsPerPage;
private string _searchString;
public void Dispose()
{
_cts.Cancel();
_cts.Dispose();
}
protected override async Task OnParametersSetAsync() => _rowsPerPage =
await Mediator.Send(new GetConfigElementByKey(ConfigElementKey.LogsPageSize), _cts.Token)
.Map(maybeRows => maybeRows.Match(ce => int.TryParse(ce.Value, out int rows) ? rows : 10, () => 10));
private async Task<TableData<LogEntryViewModel>> ServerReload(TableState state)
{
await Mediator.Send(new SaveConfigElementByKey(ConfigElementKey.LogsPageSize, state.PageSize.ToString()), _cts.Token);
PagedLogEntriesViewModel data;
switch (state.SortLabel?.ToLowerInvariant())
{
case "timestamp":
data = await Mediator.Send(new GetRecentLogEntries(state.Page, state.PageSize, _searchString)
{
SortExpression = le => le.Timestamp,
SortDescending = state.SortDirection == SortDirection.None
? Option<bool>.None
: state.SortDirection == SortDirection.Descending
}, _cts.Token);
break;
case "level":
data = await Mediator.Send(new GetRecentLogEntries(state.Page, state.PageSize, _searchString)
{
SortExpression = le => le.Level,
SortDescending = state.SortDirection == SortDirection.None
? Option<bool>.None
: state.SortDirection == SortDirection.Descending
}, _cts.Token);
break;
default:
data = await Mediator.Send(new GetRecentLogEntries(state.Page, state.PageSize, _searchString)
{
SortDescending = Option<bool>.None
}, _cts.Token);
break;
}
return new TableData<LogEntryViewModel> { TotalItems = data.TotalCount, Items = data.Page };
}
private void OnSearch(string text)
{
_searchString = text;
_table.ReloadServerData();
}
}

5
ErsatzTV/Program.cs

@ -1,4 +1,3 @@ @@ -1,4 +1,3 @@
using System.Diagnostics;
using Destructurama;
using ErsatzTV.Core;
using Serilog;
@ -13,7 +12,7 @@ public class Program @@ -13,7 +12,7 @@ public class Program
static Program()
{
string executablePath = Process.GetCurrentProcess().MainModule.FileName;
string executablePath = Environment.ProcessPath ?? string.Empty;
string executable = Path.GetFileNameWithoutExtension(executablePath);
IConfigurationBuilder builder = new ConfigurationBuilder();
@ -48,7 +47,7 @@ public class Program @@ -48,7 +47,7 @@ public class Program
.MinimumLevel.ControlledBy(LoggingLevelSwitch)
.Destructure.UsingAttributes()
.Enrich.FromLogContext()
.WriteTo.File(FileSystemLayout.LogFilePath, rollingInterval: RollingInterval.Day, shared: true)
.WriteTo.File(FileSystemLayout.LogFilePath, rollingInterval: RollingInterval.Day)
.CreateLogger();
try

1
ErsatzTV/Shared/MainLayout.razor

@ -102,6 +102,7 @@ @@ -102,6 +102,7 @@
<MudNavLink Href="schedules">Schedules</MudNavLink>
<MudNavLink Href="playouts">Playouts</MudNavLink>
<MudNavLink Href="settings">Settings</MudNavLink>
<MudNavLink Href="system/logs">Logs</MudNavLink>
<MudDivider Class="my-6" DividerType="DividerType.Middle"/>
<MudContainer Style="text-align: right" Class="mr-6">
<MudText Typo="Typo.body2">ErsatzTV Version</MudText>

Loading…
Cancel
Save