Logs UI in Server Admin (#374)

* add in ui

* add in logging viewer

* Revert "add in ui"

This reverts commit 9614721fa8a439f7097adca69772b5d41f6585d6.

* finish basic feature

* clean up

* improve and fix build

* add in debug log level command option

* use paging for log file list, use extension to select log files, show message for setting up logging

* make paging a little better

* add very basic UT for logs

* Update ServerController.cs
This commit is contained in:
Andrew Camilleri
2018-11-07 14:29:35 +01:00
committed by Nicolas Dorier
parent d152d5cd90
commit c9c7316b7d
10 changed files with 167 additions and 9 deletions

View File

@@ -43,8 +43,10 @@ using System.Security.Cryptography.X509Certificates;
using BTCPayServer.Lightning; using BTCPayServer.Lightning;
using BTCPayServer.Models.WalletViewModels; using BTCPayServer.Models.WalletViewModels;
using System.Security.Claims; using System.Security.Claims;
using BTCPayServer.Models.ServerViewModels;
using BTCPayServer.Security; using BTCPayServer.Security;
using NBXplorer.Models; using NBXplorer.Models;
using RatesViewModel = BTCPayServer.Models.StoreViewModels.RatesViewModel;
namespace BTCPayServer.Tests namespace BTCPayServer.Tests
{ {
@@ -1772,6 +1774,22 @@ namespace BTCPayServer.Tests
} }
} }
[Fact]
[Trait("Integration", "Integration")]
public async Task CheckLogsRoute()
{
using (var tester = ServerTester.Create())
{
tester.Start();
var user = tester.NewAccount();
user.GrantAccess();
user.RegisterDerivationScheme("BTC");
var serverController = user.GetController<ServerController>();
var vm = Assert.IsType<LogsViewModel>(Assert.IsType<ViewResult>(await serverController.LogsView()).Model);
}
}
[Fact] [Fact]
[Trait("Fast", "Fast")] [Trait("Fast", "Fast")]
public void CheckRatesProvider() public void CheckRatesProvider()

View File

@@ -16,6 +16,7 @@ using NBitcoin.DataEncoders;
using BTCPayServer.SSH; using BTCPayServer.SSH;
using BTCPayServer.Lightning; using BTCPayServer.Lightning;
using BTCPayServer.Configuration.External; using BTCPayServer.Configuration.External;
using Serilog.Events;
namespace BTCPayServer.Configuration namespace BTCPayServer.Configuration
{ {
@@ -37,6 +38,12 @@ namespace BTCPayServer.Configuration
get; get;
private set; private set;
} }
public string LogFile
{
get;
private set;
}
public string DataDir public string DataDir
{ {
get; get;
@@ -54,6 +61,16 @@ namespace BTCPayServer.Configuration
set; set;
} = new List<NBXplorerConnectionSetting>(); } = new List<NBXplorerConnectionSetting>();
public static string GetDebugLog(IConfiguration configuration)
{
return configuration.GetValue<string>("debuglog", null);
}
public static LogEventLevel GetDebugLogLevel(IConfiguration configuration)
{
var raw = configuration.GetValue("debugloglevel", nameof(LogEventLevel.Debug));
return (LogEventLevel)Enum.Parse(typeof(LogEventLevel), raw, true);
}
public void LoadArgs(IConfiguration conf) public void LoadArgs(IConfiguration conf)
{ {
NetworkType = DefaultConfiguration.GetNetworkType(conf); NetworkType = DefaultConfiguration.GetNetworkType(conf);
@@ -174,6 +191,13 @@ namespace BTCPayServer.Configuration
var old = conf.GetOrDefault<Uri>("internallightningnode", null); var old = conf.GetOrDefault<Uri>("internallightningnode", null);
if (old != null) if (old != null)
throw new ConfigException($"internallightningnode should not be used anymore, use btclightning instead"); throw new ConfigException($"internallightningnode should not be used anymore, use btclightning instead");
LogFile = GetDebugLog(conf);
if (!string.IsNullOrEmpty(LogFile))
{
Logs.Configuration.LogInformation("LogFile: " + LogFile);
Logs.Configuration.LogInformation("Log Level: " + GetDebugLogLevel(conf));
}
} }
private SSHSettings ParseSSHConfiguration(IConfiguration conf) private SSHSettings ParseSSHConfiguration(IConfiguration conf)

View File

@@ -41,6 +41,7 @@ namespace BTCPayServer.Configuration
app.Option("--sshkeyfilepassword", "Password of the SSH keyfile (default: empty)", CommandOptionType.SingleValue); app.Option("--sshkeyfilepassword", "Password of the SSH keyfile (default: empty)", CommandOptionType.SingleValue);
app.Option("--sshtrustedfingerprints", "SSH Host public key fingerprint or sha256 (default: empty, it will allow untrusted connections)", CommandOptionType.SingleValue); app.Option("--sshtrustedfingerprints", "SSH Host public key fingerprint or sha256 (default: empty, it will allow untrusted connections)", CommandOptionType.SingleValue);
app.Option("--debuglog", "A rolling log file for debug messages.", CommandOptionType.SingleValue); app.Option("--debuglog", "A rolling log file for debug messages.", CommandOptionType.SingleValue);
app.Option("--debugloglevel", "The severity you log (default:information)", CommandOptionType.SingleValue);
foreach (var network in provider.GetAll()) foreach (var network in provider.GetAll())
{ {
var crypto = network.CryptoCode.ToLowerInvariant(); var crypto = network.CryptoCode.ToLowerInvariant();

View File

@@ -17,6 +17,7 @@ using NBitcoin.DataEncoders;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.IO;
using System.Linq; using System.Linq;
using System.Net; using System.Net;
using System.Net.Http; using System.Net.Http;
@@ -167,6 +168,7 @@ namespace BTCPayServer.Controllers
vm.DNSDomain = null; vm.DNSDomain = null;
return View(vm); return View(vm);
} }
[Route("server/maintenance")] [Route("server/maintenance")]
[HttpPost] [HttpPost]
public async Task<IActionResult> Maintenance(MaintenanceViewModel vm, string command) public async Task<IActionResult> Maintenance(MaintenanceViewModel vm, string command)
@@ -625,5 +627,60 @@ namespace BTCPayServer.Controllers
return View(model); return View(model);
} }
} }
[Route("server/logs/{file?}")]
public async Task<IActionResult> LogsView(string file = null, int offset = 0)
{
if (offset < 0)
{
offset = 0;
}
var vm = new LogsViewModel();
if (string.IsNullOrEmpty(_Options.LogFile))
{
vm.StatusMessage = "Error: File Logging Option not specified. " +
"You need to set debuglog and optionally " +
"debugloglevel in the configuration or through runtime arguments";
}
else
{
var di = Directory.GetParent(_Options.LogFile);
if (di == null)
{
vm.StatusMessage = "Error: Could not load log files";
}
var fileNameWithoutExtension = Path.GetFileNameWithoutExtension(_Options.LogFile);
var fileExtension = Path.GetExtension(_Options.LogFile) ?? string.Empty;
var logFiles = di.GetFiles($"{fileNameWithoutExtension}*{fileExtension}");
vm.LogFileCount = logFiles.Length;
vm.LogFiles = logFiles
.OrderBy(info => info.LastWriteTime)
.Skip(offset)
.Take(5)
.ToList();
vm.LogFileOffset = offset;
if (string.IsNullOrEmpty(file)) return View("Logs", vm);
vm.Log = "";
var path = Path.Combine(di.FullName, file);
using (var fileStream = new FileStream(
path,
FileMode.Open,
FileAccess.Read,
FileShare.ReadWrite))
{
using (var reader = new StreamReader(fileStream))
{
vm.Log = await reader.ReadToEndAsync();
}
}
}
return View("Logs", vm);
}
} }
} }

View File

@@ -0,0 +1,19 @@
using System.Collections.Generic;
using System.IO;
namespace BTCPayServer.Models.ServerViewModels
{
public class LogsViewModel
{
public string StatusMessage
{
get; set;
}
public List<FileInfo> LogFiles { get; set; } = new List<FileInfo>();
public string Log { get; set; }
public int LogFileCount { get; set; }
public int LogFileOffset{ get; set; }
}
}

View File

@@ -14,6 +14,7 @@ namespace BTCPayServer.Models.ServerViewModels
public LndTypes Type { get; set; } public LndTypes Type { get; set; }
public int Index { get; set; } public int Index { get; set; }
} }
public List<LNDServiceViewModel> LNDServices { get; set; } = new List<LNDServiceViewModel>(); public List<LNDServiceViewModel> LNDServices { get; set; } = new List<LNDServiceViewModel>();
public bool HasSSH { get; set; } public bool HasSSH { get; set; }
} }

View File

@@ -55,18 +55,15 @@ namespace BTCPayServer
l.AddProvider(new CustomConsoleLogProvider(processor)); l.AddProvider(new CustomConsoleLogProvider(processor));
// Use Serilog for debug log file. // Use Serilog for debug log file.
string debugLogFile = conf.GetOrDefault<string>("debuglog", null); var debugLogFile = BTCPayServerOptions.GetDebugLog(conf);
if (String.IsNullOrEmpty(debugLogFile) == false) if (string.IsNullOrEmpty(debugLogFile) != false) return;
{
Serilog.Log.Logger = new LoggerConfiguration() Serilog.Log.Logger = new LoggerConfiguration()
.Enrich.FromLogContext() .Enrich.FromLogContext()
.MinimumLevel.Debug() .MinimumLevel.Is(BTCPayServerOptions.GetDebugLogLevel(conf))
.WriteTo.File(debugLogFile, rollingInterval: RollingInterval.Day, fileSizeLimitBytes: MAX_DEBUG_LOG_FILE_SIZE, rollOnFileSizeLimit: true, retainedFileCountLimit: 1) .WriteTo.File(debugLogFile, rollingInterval: RollingInterval.Day, fileSizeLimitBytes: MAX_DEBUG_LOG_FILE_SIZE, rollOnFileSizeLimit: true, retainedFileCountLimit: 1)
.CreateLogger(); .CreateLogger();
l.AddSerilog(Serilog.Log.Logger); l.AddSerilog(Serilog.Log.Logger);
logger.LogDebug($"Debug log file configured for {debugLogFile}.");
}
}) })
.UseStartup<Startup>() .UseStartup<Startup>()
.Build(); .Build();

View File

@@ -0,0 +1,40 @@
@model BTCPayServer.Models.ServerViewModels.LogsViewModel
@{
ViewData.SetActivePageAndTitle(ServerNavPages.Logs);
}
<h4>@ViewData["Title"]</h4>
<partial name="_StatusMessage" for="StatusMessage"/>
<div class="row">
<ul>
@foreach (var file in Model.LogFiles)
{
<li>
<a asp-action="LogsView" asp-route-file="@file.Name">@file.Name</a>
</li>
}
<li>
@if (Model.LogFileOffset > 0)
{
<a asp-action="LogsView" asp-route-offset="@(Model.LogFileOffset - 5)"><<</a>
}
Showing @Model.LogFileOffset - (@Model.LogFileOffset+@Model.LogFiles.Count) of @Model.LogFileCount
@if ((Model.LogFileOffset+ Model.LogFiles.Count) < Model.LogFileCount)
{
<a asp-action="LogsView" asp-route-offset="@(Model.LogFileOffset + Model.LogFiles.Count)">>></a>
}
</li>
</ul>
@if (!string.IsNullOrEmpty(Model.Log))
{
<pre>
@Model.Log
</pre>
}
</div>
@section Scripts {
@await Html.PartialAsync("_ValidationScriptsPartial")
}

View File

@@ -7,6 +7,6 @@ namespace BTCPayServer.Views.Server
{ {
public enum ServerNavPages public enum ServerNavPages
{ {
Index, Users, Rates, Emails, Policies, Theme, Hangfire, Services, Maintenance Index, Users, Rates, Emails, Policies, Theme, Hangfire, Services, Maintenance, Logs
} }
} }

View File

@@ -6,6 +6,7 @@
<a class="nav-link @ViewData.IsActivePage(ServerNavPages.Services)" asp-action="Services">Services</a> <a class="nav-link @ViewData.IsActivePage(ServerNavPages.Services)" asp-action="Services">Services</a>
<a class="nav-link @ViewData.IsActivePage(ServerNavPages.Theme)" asp-action="Theme">Theme</a> <a class="nav-link @ViewData.IsActivePage(ServerNavPages.Theme)" asp-action="Theme">Theme</a>
<a class="nav-link @ViewData.IsActivePage(ServerNavPages.Maintenance)" asp-action="Maintenance">Maintenance</a> <a class="nav-link @ViewData.IsActivePage(ServerNavPages.Maintenance)" asp-action="Maintenance">Maintenance</a>
<a class="nav-link @ViewData.IsActivePage(ServerNavPages.Logs)" asp-action="Logs">Logs</a>
<a class="nav-link @ViewData.IsActivePage(ServerNavPages.Hangfire)" href="~/hangfire" target="_blank">Hangfire</a> <a class="nav-link @ViewData.IsActivePage(ServerNavPages.Hangfire)" href="~/hangfire" target="_blank">Hangfire</a>
</div> </div>