mirror of
https://github.com/immense/Remotely.git
synced 2025-10-26 11:27:15 +00:00
Finish implementing Serilog, LogManager, and full-screen loader.
This commit is contained in:
parent
d1c432efd9
commit
db19e92823
@ -14,7 +14,6 @@ namespace Remotely.Server.API
|
||||
[ApiController]
|
||||
public class ServerLogsController : ControllerBase
|
||||
{
|
||||
private readonly JsonSerializerOptions _jsonOptions = new() { WriteIndented = true };
|
||||
private readonly ILogsManager _logsManager;
|
||||
private readonly ILogger<ServerLogsController> _logger;
|
||||
|
||||
|
||||
29
Server/Components/LoaderHarness.razor
Normal file
29
Server/Components/LoaderHarness.razor
Normal file
@ -0,0 +1,29 @@
|
||||
@using Nihs.SimpleMessenger;
|
||||
@using Remotely.Server.Models.Messages;
|
||||
@inject IMessenger Messenger
|
||||
|
||||
@if (_loaderShown)
|
||||
{
|
||||
<LoadingSignal StatusMessage="@_statusMessage" />
|
||||
}
|
||||
|
||||
@code {
|
||||
private bool _loaderShown;
|
||||
private string _statusMessage = string.Empty;
|
||||
|
||||
protected override Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (firstRender)
|
||||
{
|
||||
Messenger.Register<ShowLoaderMessage>(this, HandleShowLoaderMessage);
|
||||
}
|
||||
return base.OnAfterRenderAsync(firstRender);
|
||||
}
|
||||
|
||||
private async Task HandleShowLoaderMessage(ShowLoaderMessage message)
|
||||
{
|
||||
_loaderShown = message.IsShown;
|
||||
_statusMessage = message.StatusMessage;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
}
|
||||
@ -1,21 +1,31 @@
|
||||
@inject IClientAppState AppState
|
||||
|
||||
|
||||
@if (_theme == Theme.Dark)
|
||||
{
|
||||
<div class="signal" style="border-color: whitesmoke"></div>
|
||||
}
|
||||
else if (_theme == Theme.Light)
|
||||
{
|
||||
<div class="signal" style="border-color: #333"></div>
|
||||
}
|
||||
<div class="signal-background">
|
||||
<div class="signal-wrapper text-center">
|
||||
@if (!string.IsNullOrEmpty(StatusMessage))
|
||||
{
|
||||
<h4>
|
||||
@StatusMessage
|
||||
</h4>
|
||||
}
|
||||
<div class="signal @(GetSignalClass())"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private Theme _theme;
|
||||
|
||||
[Parameter]
|
||||
public string StatusMessage { get; set; } = string.Empty;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await base.OnInitializedAsync();
|
||||
_theme = await AppState.GetEffectiveTheme();
|
||||
await base.OnInitializedAsync();
|
||||
}
|
||||
|
||||
private string GetSignalClass()
|
||||
{
|
||||
return _theme == Theme.Dark ? "signal-dark" : "signal-light";
|
||||
}
|
||||
}
|
||||
@ -1,18 +1,42 @@
|
||||
.signal {
|
||||
.signal-background {
|
||||
position: fixed;
|
||||
top: 45vh;
|
||||
left: calc(50% - 25px);
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
background-color: rgba(0,0,0,0.5);
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.signal-wrapper {
|
||||
position: absolute;
|
||||
display: inline-block;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.signal {
|
||||
display: inline-block;
|
||||
border-width: 8px;
|
||||
border-style: solid;
|
||||
margin-top: 10px;
|
||||
border-radius: 100%;
|
||||
height: 50px;
|
||||
width: 50px;
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
transform-origin: center;
|
||||
animation: pulsate .85s ease-out;
|
||||
animation-iteration-count: infinite;
|
||||
}
|
||||
|
||||
.signal.signal-dark {
|
||||
border-color: whitesmoke;
|
||||
}
|
||||
.signal.signal-light {
|
||||
border-color: #333;
|
||||
}
|
||||
|
||||
@keyframes pulsate {
|
||||
0% {
|
||||
transform: scale(.1);
|
||||
|
||||
@ -49,8 +49,8 @@
|
||||
{
|
||||
_displayStyle = "block";
|
||||
await InvokeAsync(StateHasChanged);
|
||||
// The fade animation won't work without a delay here.
|
||||
await Task.Delay(100);
|
||||
// The fade animation won't work without a delay here.
|
||||
await Task.Delay(100);
|
||||
_showClass = "show";
|
||||
await InvokeAsync(StateHasChanged);
|
||||
};
|
||||
|
||||
@ -57,9 +57,6 @@ namespace Remotely.Server.Data
|
||||
builder.Entity<Organization>()
|
||||
.HasMany(x => x.RemotelyUsers)
|
||||
.WithOne(x => x.Organization);
|
||||
builder.Entity<Organization>()
|
||||
.HasMany(x => x.EventLogs)
|
||||
.WithOne(x => x.Organization);
|
||||
builder.Entity<Organization>()
|
||||
.HasMany(x => x.DeviceGroups)
|
||||
.WithOne(x => x.Organization);
|
||||
|
||||
13
Server/Models/Messages/ShowLoaderMessage.cs
Normal file
13
Server/Models/Messages/ShowLoaderMessage.cs
Normal file
@ -0,0 +1,13 @@
|
||||
namespace Remotely.Server.Models.Messages;
|
||||
|
||||
public class ShowLoaderMessage
|
||||
{
|
||||
public ShowLoaderMessage(bool isShown, string statusMessage)
|
||||
{
|
||||
IsShown = isShown;
|
||||
StatusMessage = statusMessage;
|
||||
}
|
||||
|
||||
public bool IsShown { get; }
|
||||
public string StatusMessage { get; }
|
||||
}
|
||||
@ -1,4 +1,5 @@
|
||||
@page "/server-logs"
|
||||
@using System.Collections.ObjectModel;
|
||||
@attribute [Authorize]
|
||||
@inherits AuthComponentBase
|
||||
|
||||
@ -6,6 +7,7 @@
|
||||
@inject IToastService ToastService
|
||||
@inject IJsInterop JsInterop
|
||||
@inject ILogsManager LogsManager
|
||||
@inject ILoaderService LoaderService
|
||||
|
||||
<h3 class="mb-3">Server Logs</h3>
|
||||
|
||||
@ -35,9 +37,9 @@
|
||||
<div style="display:inline-block">
|
||||
<strong>Type:</strong>
|
||||
<br />
|
||||
<select class="form-control-sm" @bind="_eventType">
|
||||
<select class="form-control-sm" @bind="LogLevel">
|
||||
<option value="">All</option>
|
||||
@foreach (var eventType in Enum.GetValues(typeof(EventType)))
|
||||
@foreach (var eventType in Enum.GetValues(typeof(LogLevel)))
|
||||
{
|
||||
<option @key="eventType" value="@eventType">@eventType</option>
|
||||
}
|
||||
@ -46,51 +48,23 @@
|
||||
<div style="display:inline-block">
|
||||
<strong>Filter:</strong>
|
||||
<br />
|
||||
<input type="text" @bind="_messageFilter" />
|
||||
<input type="text" @bind="MessageFilter" @bind:event="oninput" />
|
||||
</div>
|
||||
<div style="display:inline-block">
|
||||
<strong>From:</strong>
|
||||
<br />
|
||||
<input type="date" @bind="_fromDate" />
|
||||
<input type="date" @bind="FromDate" />
|
||||
</div>
|
||||
<div style="display:inline-block">
|
||||
<strong>To:</strong>
|
||||
<br />
|
||||
<input type="date" @bind-value="_toDate" />
|
||||
<input type="date" @bind="ToDate" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table class="table table-hover table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Type</th>
|
||||
<th>Timestamp</th>
|
||||
<th>Message</th>
|
||||
<th>Source</th>
|
||||
<th>Stack Trace</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var eventLog in FilteredLogs)
|
||||
{
|
||||
<tr @key="eventLog">
|
||||
<td>@eventLog.EventType</td>
|
||||
<td>@eventLog.TimeStamp</td>
|
||||
<td>
|
||||
@if (!string.IsNullOrWhiteSpace(eventLog.Message))
|
||||
{
|
||||
foreach (var line in eventLog.Message.Split("\n", StringSplitOptions.RemoveEmptyEntries))
|
||||
{
|
||||
<div>@line</div>
|
||||
}
|
||||
}
|
||||
</td>
|
||||
<td>@eventLog.Source</td>
|
||||
<td>@eventLog.StackTrace</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
<textarea readonly class="logs-content bg-dark text-white">
|
||||
@(string.Join(Environment.NewLine, _filteredLogs))
|
||||
</textarea>
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -98,35 +72,104 @@ else
|
||||
}
|
||||
|
||||
@code {
|
||||
private readonly List<EventLog> _filteredLogs = new();
|
||||
private EventType? _eventType;
|
||||
private string _messageFilter;
|
||||
private readonly string _messageDebounceKey = Guid.NewGuid().ToString();
|
||||
private readonly List<string> _filteredLogs = new();
|
||||
private LogLevel? _logLevel;
|
||||
private string _messageFilter = string.Empty;
|
||||
|
||||
private DateTimeOffset _fromDate = DateTimeOffset.Now.AddDays(-7);
|
||||
private DateTimeOffset _toDate = DateTimeOffset.Now;
|
||||
|
||||
|
||||
|
||||
private IEnumerable<EventLog> FilteredLogs
|
||||
private DateTimeOffset FromDate
|
||||
{
|
||||
get
|
||||
get => _fromDate;
|
||||
set
|
||||
{
|
||||
return Enumerable.Empty<EventLog>();
|
||||
//return DataService.GetEventLogs(User.UserName,
|
||||
// _fromDate,
|
||||
// _toDate,
|
||||
// _eventType,
|
||||
// _messageFilter);
|
||||
if (value > _toDate)
|
||||
{
|
||||
ToastService.ShowToast("Invalid date range.", classString: "bg-warning");
|
||||
return;
|
||||
}
|
||||
_fromDate = value;
|
||||
_ = RefreshLogs();
|
||||
}
|
||||
}
|
||||
|
||||
private LogLevel? LogLevel
|
||||
{
|
||||
get => _logLevel;
|
||||
set
|
||||
{
|
||||
_logLevel = value;
|
||||
_ = RefreshLogs();
|
||||
}
|
||||
}
|
||||
|
||||
private string MessageFilter
|
||||
{
|
||||
get => _messageFilter;
|
||||
set
|
||||
{
|
||||
_messageFilter = value;
|
||||
|
||||
Debouncer.Debounce(
|
||||
TimeSpan.FromSeconds(1),
|
||||
() => _ = RefreshLogs(),
|
||||
_messageDebounceKey);
|
||||
}
|
||||
}
|
||||
|
||||
private DateTimeOffset ToDate
|
||||
{
|
||||
get => _toDate;
|
||||
set
|
||||
{
|
||||
if (value < _fromDate)
|
||||
{
|
||||
ToastService.ShowToast("Invalid date range.", classString: "bg-warning");
|
||||
return;
|
||||
}
|
||||
_toDate = value;
|
||||
_ = RefreshLogs();
|
||||
}
|
||||
}
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await RefreshLogs();
|
||||
await base.OnInitializedAsync();
|
||||
}
|
||||
|
||||
private async Task ClearAllLogs()
|
||||
{
|
||||
var result = await JsInterop.Confirm("Are you sure you want to delete all logs?");
|
||||
if (result)
|
||||
{
|
||||
await LogsManager.DeleteLogs();
|
||||
using (var _ = LoaderService.ShowLoader("Deleting logs"))
|
||||
{
|
||||
await LogsManager.DeleteLogs();
|
||||
}
|
||||
await RefreshLogs();
|
||||
ToastService.ShowToast("Logs deleted.");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RefreshLogs()
|
||||
{
|
||||
using var _ = await LoaderService.ShowLoader("Refreshing logs");
|
||||
_filteredLogs.Clear();
|
||||
|
||||
var logsAsync = LogsManager.GetLogs(
|
||||
_fromDate,
|
||||
_toDate,
|
||||
_messageFilter,
|
||||
_logLevel);
|
||||
|
||||
await foreach (var line in logsAsync)
|
||||
{
|
||||
_filteredLogs.Add(line);
|
||||
}
|
||||
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,13 +4,18 @@
|
||||
grid-column-gap: 2em;
|
||||
}
|
||||
|
||||
|
||||
.filters-row {
|
||||
display: grid;
|
||||
grid-template-columns: auto auto auto 1fr;
|
||||
grid-column-gap: 1em;
|
||||
}
|
||||
|
||||
.logs-content {
|
||||
width: 100%;
|
||||
white-space: pre;
|
||||
height: 500px;
|
||||
}
|
||||
|
||||
|
||||
@media (max-width: 641px) {
|
||||
.buttons-row {
|
||||
|
||||
@ -36,6 +36,7 @@ using Remotely.Shared.Services;
|
||||
using System;
|
||||
using Immense.RemoteControl.Server.Services;
|
||||
using Serilog;
|
||||
using Nihs.SimpleMessenger;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
var configuration = builder.Configuration;
|
||||
@ -43,8 +44,6 @@ var services = builder.Services;
|
||||
|
||||
ConfigureSerilog(builder);
|
||||
|
||||
builder.Host.UseSerilog();
|
||||
|
||||
builder.Logging.AddConfiguration(builder.Configuration.GetSection("Logging"));
|
||||
|
||||
if (OperatingSystem.IsWindows() &&
|
||||
@ -190,13 +189,14 @@ services.AddSingleton<IApplicationConfig, ApplicationConfig>();
|
||||
services.AddScoped<ApiAuthorizationFilter>();
|
||||
services.AddScoped<LocalOnlyFilter>();
|
||||
services.AddScoped<ExpiringTokenFilter>();
|
||||
services.AddHostedService<DbCleanupService>();
|
||||
services.AddHostedService<DataCleanupService>();
|
||||
services.AddHostedService<ScriptScheduler>();
|
||||
services.AddSingleton<IUpgradeService, UpgradeService>();
|
||||
services.AddScoped<IToastService, ToastService>();
|
||||
services.AddScoped<IModalService, ModalService>();
|
||||
services.AddScoped<IJsInterop, JsInterop>();
|
||||
services.AddScoped<ICircuitConnection, CircuitConnection>();
|
||||
services.AddScoped<ILoaderService, LoaderService>();
|
||||
services.AddScoped(x => (CircuitHandler)x.GetRequiredService<ICircuitConnection>());
|
||||
services.AddSingleton<ICircuitManager, CircuitManager>();
|
||||
services.AddScoped<IAuthService, AuthService>();
|
||||
@ -206,6 +206,7 @@ services.AddScoped<IScriptScheduleDispatcher, ScriptScheduleDispatcher>();
|
||||
services.AddSingleton<IOtpProvider, OtpProvider>();
|
||||
services.AddSingleton<IEmbeddedServerDataSearcher, EmbeddedServerDataSearcher>();
|
||||
services.AddSingleton<ILogsManager>(LogsManager.Default);
|
||||
services.AddSingleton(WeakReferenceMessenger.Default);
|
||||
|
||||
services.AddRemoteControlServer(config =>
|
||||
{
|
||||
@ -336,9 +337,11 @@ void ConfigureSerilog(WebApplicationBuilder webAppBuilder)
|
||||
{
|
||||
loggerConfiguration
|
||||
.Enrich.FromLogContext()
|
||||
.MinimumLevel.Debug()
|
||||
.WriteTo.Console()
|
||||
.WriteTo.File($"{logPath}/Remotely_Server.log", rollingInterval: RollingInterval.Day, retainedFileTimeLimit: TimeSpan.FromDays(dataRetentionDays));
|
||||
.WriteTo.File($"{logPath}/Remotely_Server.log",
|
||||
rollingInterval: RollingInterval.Day,
|
||||
retainedFileTimeLimit: TimeSpan.FromDays(dataRetentionDays),
|
||||
shared: true);
|
||||
}
|
||||
|
||||
var loggerConfig = new LoggerConfiguration();
|
||||
|
||||
@ -31,6 +31,7 @@
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.EventLog" Version="7.0.0" />
|
||||
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.18.1" />
|
||||
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="7.0.6" />
|
||||
<PackageReference Include="Nihs.SimpleMessenger" Version="1.0.0" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="7.0.4" />
|
||||
<PackageReference Include="Serilog.AspNetCore" Version="7.0.0" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
|
||||
|
||||
62
Server/Services/DataCleanupService.cs
Normal file
62
Server/Services/DataCleanupService.cs
Normal file
@ -0,0 +1,62 @@
|
||||
using Microsoft.Build.Framework;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Remotely.Server.Services
|
||||
{
|
||||
public class DataCleanupService : IHostedService, IDisposable
|
||||
{
|
||||
private readonly ILogger<DataCleanupService> _logger;
|
||||
|
||||
private readonly IServiceProvider _services;
|
||||
|
||||
private System.Timers.Timer _cleanupTimer = new(TimeSpan.FromDays(1));
|
||||
|
||||
|
||||
public DataCleanupService(
|
||||
IServiceProvider serviceProvider,
|
||||
ILogger<DataCleanupService> logger)
|
||||
{
|
||||
_services = serviceProvider;
|
||||
_logger = logger;
|
||||
|
||||
_cleanupTimer.Elapsed += CleanupTimer_Elapsed;
|
||||
}
|
||||
public void Dispose()
|
||||
{
|
||||
_cleanupTimer?.Dispose();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_cleanupTimer.Start();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_cleanupTimer.Stop();
|
||||
_cleanupTimer.Dispose();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private void CleanupTimer_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var scope = _services.CreateScope();
|
||||
var dataService = scope.ServiceProvider.GetRequiredService<IDataService>();
|
||||
dataService.CleanupOldRecords();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error during data cleanup.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,48 +0,0 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Remotely.Server.Services
|
||||
{
|
||||
public class DbCleanupService : IHostedService, IDisposable
|
||||
{
|
||||
public DbCleanupService(IServiceProvider serviceProvider)
|
||||
{
|
||||
Services = serviceProvider;
|
||||
}
|
||||
|
||||
private IServiceProvider Services { get; }
|
||||
private System.Timers.Timer CleanupTimer { get; set; }
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
CleanupTimer?.Dispose();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
CleanupTimer?.Dispose();
|
||||
CleanupTimer = new System.Timers.Timer(TimeSpan.FromDays(1).TotalMilliseconds);
|
||||
CleanupTimer.Elapsed += CleanupTimer_Elapsed;
|
||||
CleanupTimer.Start();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
CleanupTimer?.Dispose();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private void CleanupTimer_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
|
||||
{
|
||||
using var scope = Services.CreateScope();
|
||||
var dataService = scope.ServiceProvider.GetRequiredService<IDataService>();
|
||||
|
||||
dataService.CleanupOldRecords();
|
||||
}
|
||||
}
|
||||
}
|
||||
34
Server/Services/LoaderService.cs
Normal file
34
Server/Services/LoaderService.cs
Normal file
@ -0,0 +1,34 @@
|
||||
using Nihs.SimpleMessenger;
|
||||
using Remotely.Server.Models.Messages;
|
||||
using Remotely.Shared.Primitives;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Remotely.Server.Services;
|
||||
|
||||
public interface ILoaderService
|
||||
{
|
||||
Task<IDisposable> ShowLoader(string statusMessage);
|
||||
void HideLoader();
|
||||
}
|
||||
|
||||
public class LoaderService : ILoaderService
|
||||
{
|
||||
private readonly IMessenger _messenger;
|
||||
|
||||
public LoaderService(IMessenger messenger)
|
||||
{
|
||||
_messenger = messenger;
|
||||
}
|
||||
|
||||
public async Task<IDisposable> ShowLoader(string statusMessage)
|
||||
{
|
||||
await _messenger.Send(new ShowLoaderMessage(true, statusMessage));
|
||||
return new CallbackDisposable(HideLoader);
|
||||
}
|
||||
|
||||
public void HideLoader()
|
||||
{
|
||||
_messenger.Send(new ShowLoaderMessage(false, string.Empty));
|
||||
}
|
||||
}
|
||||
@ -1,9 +1,16 @@
|
||||
using Remotely.Shared.Extensions;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Remotely.Shared.Extensions;
|
||||
using Serilog;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Remotely.Server.Services
|
||||
@ -13,6 +20,11 @@ namespace Remotely.Server.Services
|
||||
string GetLogsDirectory();
|
||||
Task<FileInfo> ZipAllLogs();
|
||||
Task DeleteLogs();
|
||||
IAsyncEnumerable<string> GetLogs(
|
||||
DateTimeOffset startDate,
|
||||
DateTimeOffset endDate,
|
||||
string messageFilter,
|
||||
LogLevel? logLevelFilter);
|
||||
}
|
||||
|
||||
public class LogsManager : ILogsManager
|
||||
@ -72,9 +84,123 @@ namespace Remotely.Server.Services
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine("Failed to delete log file: {filename}. Message: {exMessage}", file, ex.Message);
|
||||
Console.WriteLine($"Failed to delete log file: {file}. Message: {ex.Message}");
|
||||
try
|
||||
{
|
||||
Console.WriteLine("Attempting to zero out log contents.");
|
||||
using var fs = File.Open(file, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite);
|
||||
fs.SetLength(0);
|
||||
}
|
||||
catch (Exception ex2)
|
||||
{
|
||||
Console.WriteLine($"Failed to clear log contents: {file}. Message: {ex2.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async IAsyncEnumerable<string> GetLogs(
|
||||
DateTimeOffset startDate,
|
||||
DateTimeOffset endDate,
|
||||
string messageFilter,
|
||||
LogLevel? logLevelFilter)
|
||||
{
|
||||
var fromDate = startDate.UtcDateTime.Date;
|
||||
var toDate = endDate.UtcDateTime.Date.AddDays(1);
|
||||
|
||||
var result = new StringBuilder();
|
||||
var logsDir = GetLogsDirectory();
|
||||
|
||||
var files = Directory
|
||||
.GetFiles(logsDir)
|
||||
.Select(x => new FileInfo(x))
|
||||
.Where(x =>
|
||||
x.LastWriteTimeUtc >= fromDate &&
|
||||
x.LastWriteTimeUtc <= toDate);
|
||||
|
||||
foreach (var file in files)
|
||||
{
|
||||
var linesAsync = GetLines(file, messageFilter, logLevelFilter);
|
||||
await foreach (var line in linesAsync)
|
||||
{
|
||||
yield return line;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private async IAsyncEnumerable<string> GetLines(
|
||||
FileInfo file,
|
||||
string messageFilter,
|
||||
LogLevel? logLevelFilter)
|
||||
{
|
||||
LogLevel? currentLogLevel = null;
|
||||
|
||||
using var fs = File.Open(file.FullName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
|
||||
using var sr = new StreamReader(fs);
|
||||
|
||||
while (true)
|
||||
{
|
||||
var currentLine = await sr.ReadLineAsync();
|
||||
|
||||
if (currentLine is null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (logLevelFilter is not null)
|
||||
{
|
||||
if (TryGetLogLevel(currentLine, out var parsedLevel))
|
||||
{
|
||||
currentLogLevel = parsedLevel;
|
||||
}
|
||||
|
||||
if (currentLogLevel != logLevelFilter)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(messageFilter) &&
|
||||
!currentLine.Contains(messageFilter, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
yield return currentLine;
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryGetLogLevel(
|
||||
string lineContent,
|
||||
[NotNullWhen(true)] out LogLevel? logLevel)
|
||||
{
|
||||
try
|
||||
{
|
||||
var logLevelTag = lineContent[31..36];
|
||||
if (_logLevelMap.TryGetValue(logLevelTag, out var result))
|
||||
{
|
||||
logLevel = result;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignored.
|
||||
}
|
||||
|
||||
logLevel = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static readonly ReadOnlyDictionary<string, LogLevel> _logLevelMap = new(new Dictionary<string, LogLevel>()
|
||||
{
|
||||
["[VRB]"] = LogLevel.Trace,
|
||||
["[DBG]"] = LogLevel.Debug,
|
||||
["[INF]"] = LogLevel.Information,
|
||||
["[WRN]"] = LogLevel.Warning,
|
||||
["[ERR]"] = LogLevel.Error,
|
||||
["[FTL]"] = LogLevel.Critical
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -38,13 +38,13 @@ namespace Remotely.Server.Services
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.LogInformation("Script Schedule Dispatcher started.");
|
||||
_logger.LogDebug("Script Schedule Dispatcher started.");
|
||||
|
||||
var schedules = await _dataService.GetScriptSchedulesDue();
|
||||
|
||||
if (schedules?.Any() != true)
|
||||
{
|
||||
_logger.LogInformation("No schedules are due.");
|
||||
_logger.LogDebug("No schedules are due.");
|
||||
return;
|
||||
}
|
||||
|
||||
@ -52,18 +52,18 @@ namespace Remotely.Server.Services
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.LogInformation("Considering {scheduleName}. Interval: {interval}. Next Run: {nextRun}.",
|
||||
_logger.LogDebug("Considering {scheduleName}. Interval: {interval}. Next Run: {nextRun}.",
|
||||
schedule.Name,
|
||||
schedule.Interval,
|
||||
schedule.NextRun);
|
||||
|
||||
if (!AdvanceSchedule(schedule))
|
||||
{
|
||||
_logger.LogInformation("Schedule is not due.");
|
||||
_logger.LogDebug("Schedule is not due.");
|
||||
continue;
|
||||
}
|
||||
|
||||
_logger.LogInformation("Creating script run for schedule {scheduleName}.", schedule.Name);
|
||||
_logger.LogDebug("Creating script run for schedule {scheduleName}.", schedule.Name);
|
||||
|
||||
var scriptRun = new ScriptRun()
|
||||
{
|
||||
@ -100,7 +100,7 @@ namespace Remotely.Server.Services
|
||||
|
||||
await _circuitConnection.RunScript(onlineDevices, schedule.SavedScriptId, scriptRun.Id, ScriptInputType.ScheduledScript, true);
|
||||
|
||||
_logger.LogInformation("Created script run for schedule {scheduleName}.", schedule.Name);
|
||||
_logger.LogDebug("Created script run for schedule {scheduleName}.", schedule.Name);
|
||||
|
||||
schedule.LastRun = Time.Now;
|
||||
await _dataService.AddOrUpdateScriptSchedule(schedule);
|
||||
|
||||
@ -16,7 +16,7 @@
|
||||
</div>
|
||||
|
||||
<ToastHarness />
|
||||
|
||||
<LoaderHarness />
|
||||
<ModalHarness />
|
||||
</div>
|
||||
</Authorized>
|
||||
|
||||
@ -83,14 +83,14 @@
|
||||
<span class="oi oi-key" aria-hidden="true"></span> API Keys
|
||||
</NavLink>
|
||||
</li>
|
||||
<li class="nav-item px-3">
|
||||
<NavLink class="nav-link" href="server-logs">
|
||||
<span class="oi oi-document" aria-hidden="true"></span> Server Logs
|
||||
</NavLink>
|
||||
</li>
|
||||
|
||||
@if (_user?.IsServerAdmin == true)
|
||||
{
|
||||
<li class="nav-item px-3">
|
||||
<NavLink class="nav-link" href="server-logs">
|
||||
<span class="oi oi-document" aria-hidden="true"></span> Server Logs
|
||||
</NavLink>
|
||||
</li>
|
||||
<li class="nav-item px-3">
|
||||
<NavLink class="nav-link" href="server-config">
|
||||
<span class="oi oi-wrench" aria-hidden="true"></span> Server Config
|
||||
|
||||
@ -10,6 +10,15 @@
|
||||
"Default": "Information"
|
||||
}
|
||||
},
|
||||
"Serilog": {
|
||||
"MinimumLevel": {
|
||||
"Default": "Information",
|
||||
"Override": {
|
||||
"Microsoft": "Warning",
|
||||
"System": "Warning"
|
||||
}
|
||||
}
|
||||
},
|
||||
"ApplicationOptions": {
|
||||
"AllowApiLogin": false,
|
||||
"BannedDevices": [],
|
||||
|
||||
@ -1,23 +0,0 @@
|
||||
using Remotely.Shared.Enums;
|
||||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Remotely.Shared.Models
|
||||
{
|
||||
public class EventLog
|
||||
{
|
||||
[Key]
|
||||
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
|
||||
public string ID { get; set; }
|
||||
public EventType EventType { get; set; }
|
||||
public string Message { get; set; }
|
||||
public string Source { get; set; }
|
||||
public string StackTrace { get; set; }
|
||||
public string OrganizationID { get; set; }
|
||||
public DateTimeOffset TimeStamp { get; set; } = DateTimeOffset.Now;
|
||||
[JsonIgnore]
|
||||
public Organization Organization { get; set; }
|
||||
}
|
||||
}
|
||||
@ -26,8 +26,6 @@ namespace Remotely.Shared.Models
|
||||
|
||||
public ICollection<Device> Devices { get; set; }
|
||||
|
||||
public ICollection<EventLog> EventLogs { get; set; }
|
||||
|
||||
[Key]
|
||||
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
|
||||
public string ID { get; set; }
|
||||
|
||||
@ -39,10 +39,18 @@ namespace Remotely.Tests
|
||||
var viewerHub = new Mock<IHubContext<ViewerHub>>();
|
||||
var expiringTokenService = new Mock<IExpiringTokenService>();
|
||||
var serviceSessionCache = new Mock<IServiceHubSessionCache>();
|
||||
var logger = new Mock<ILogger<AgentHub>>();
|
||||
|
||||
appConfig.Setup(x => x.BannedDevices).Returns(new string[] { _testData.Device1.DeviceName });
|
||||
|
||||
var hub = new AgentHub(DataService, appConfig.Object, serviceSessionCache.Object, viewerHub.Object, circuitManager.Object, expiringTokenService.Object);
|
||||
var hub = new AgentHub(
|
||||
DataService,
|
||||
appConfig.Object,
|
||||
serviceSessionCache.Object,
|
||||
viewerHub.Object,
|
||||
circuitManager.Object,
|
||||
expiringTokenService.Object,
|
||||
logger.Object);
|
||||
|
||||
var hubClients = new Mock<IHubCallerClients>();
|
||||
var caller = new Mock<ISingleClientProxy>();
|
||||
@ -54,6 +62,8 @@ namespace Remotely.Tests
|
||||
caller.Verify(x => x.SendCoreAsync("UninstallAgent", It.IsAny<object[]>(), It.IsAny<CancellationToken>()), Times.Once);
|
||||
}
|
||||
|
||||
// TODO: Checking of device ban should be pulled out into
|
||||
// a separate service that's better testable.
|
||||
[TestMethod]
|
||||
[DoNotParallelize]
|
||||
public async Task DeviceCameOnline_BannedById()
|
||||
@ -66,10 +76,18 @@ namespace Remotely.Tests
|
||||
var viewerHub = new Mock<IHubContext<ViewerHub>>();
|
||||
var expiringTokenService = new Mock<IExpiringTokenService>();
|
||||
var serviceSessionCache = new Mock<IServiceHubSessionCache>();
|
||||
var logger = new Mock<ILogger<AgentHub>>();
|
||||
|
||||
appConfig.Setup(x => x.BannedDevices).Returns(new string[] { _testData.Device1.ID });
|
||||
|
||||
var hub = new AgentHub(DataService, appConfig.Object, serviceSessionCache.Object, viewerHub.Object, circuitManager.Object, expiringTokenService.Object);
|
||||
var hub = new AgentHub(
|
||||
DataService,
|
||||
appConfig.Object,
|
||||
serviceSessionCache.Object,
|
||||
viewerHub.Object,
|
||||
circuitManager.Object,
|
||||
expiringTokenService.Object,
|
||||
logger.Object);
|
||||
|
||||
var hubClients = new Mock<IHubCallerClients>();
|
||||
var caller = new Mock<ISingleClientProxy>();
|
||||
@ -81,51 +99,6 @@ namespace Remotely.Tests
|
||||
caller.Verify(x => x.SendCoreAsync("UninstallAgent", It.IsAny<object[]>(), It.IsAny<CancellationToken>()), Times.Once);
|
||||
}
|
||||
|
||||
|
||||
[TestMethod]
|
||||
[DoNotParallelize]
|
||||
public async Task DeviceCameOnline_NotBanned()
|
||||
{
|
||||
var appConfig = new Mock<IApplicationConfig>();
|
||||
|
||||
var circuitManager = new Mock<ICircuitManager>();
|
||||
var circuitConnection = new Mock<ICircuitConnection>();
|
||||
circuitManager.Setup(x => x.Connections).Returns(new[] { circuitConnection.Object });
|
||||
circuitConnection.Setup(x => x.User).Returns(_testData.Admin1);
|
||||
var browserHubClients = new Mock<IHubClients>();
|
||||
var expiringTokenService = new Mock<IExpiringTokenService>();
|
||||
var serviceSessionCache = new Mock<IServiceHubSessionCache>();
|
||||
|
||||
var viewerHub = new Mock<IHubContext<ViewerHub>>();
|
||||
|
||||
appConfig.Setup(x => x.BannedDevices).Returns(Array.Empty<string>());
|
||||
|
||||
var hub = new AgentHub(DataService, appConfig.Object, serviceSessionCache.Object, viewerHub.Object, circuitManager.Object, expiringTokenService.Object)
|
||||
{
|
||||
Context = new CallerContext()
|
||||
};
|
||||
|
||||
|
||||
var agentHubClients = new Mock<IHubCallerClients>();
|
||||
var agentHubCaller = new Mock<ISingleClientProxy>();
|
||||
var agentClientsProxy = new Mock<IClientProxy>();
|
||||
agentHubClients.Setup(x => x.Caller).Returns(agentHubCaller.Object);
|
||||
agentHubClients.Setup(x => x.Clients(It.IsAny<IReadOnlyList<string>>())).Returns(agentClientsProxy.Object);
|
||||
hub.Clients = agentHubClients.Object;
|
||||
|
||||
var result = await hub.DeviceCameOnline(_testData.Device1);
|
||||
|
||||
Assert.IsTrue(result);
|
||||
|
||||
agentHubClients.Verify(x => x.Caller, Times.Never);
|
||||
agentHubCaller.Verify(x => x.SendCoreAsync("UninstallAgent", It.IsAny<object[]>(), It.IsAny<CancellationToken>()), Times.Never);
|
||||
|
||||
circuitConnection.Verify(x => x.InvokeCircuitEvent(
|
||||
CircuitEventName.DeviceUpdate,
|
||||
It.Is<Device>(x => x.ID == _testData.Device1.ID)),
|
||||
Times.Once);
|
||||
}
|
||||
|
||||
[TestCleanup]
|
||||
public void TestCleanup()
|
||||
{
|
||||
|
||||
Loading…
Reference in New Issue
Block a user