Added Serilog to manage logs

Handle required fields while starting
This commit is contained in:
Tech Garage 2024-07-21 00:31:22 +03:30
parent 6fbfaa1007
commit b128154c60
17 changed files with 749 additions and 154 deletions

View file

@ -1,15 +1,12 @@
using Hangfire;
using Hangfire.Storage.SQLite;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.IdentityModel.Tokens;
using MTWireGuard.Application.Mapper;
using MTWireGuard.Application.Repositories;
using MTWireGuard.Application.Services;
using Serilog;
using Serilog.Ui.SqliteDataProvider;
using Serilog.Ui.Web;
using System.Reflection;
using System.Text;
namespace MTWireGuard.Application
{
@ -17,16 +14,15 @@ namespace MTWireGuard.Application
{
public static void AddApplicationServices(this IServiceCollection services)
{
// Add Serilog
services.AddLogging(loggingBuilder =>
{
loggingBuilder.AddSerilog(Helper.LoggerConfiguration(), dispose: true);
});
// Add DBContext
services.AddDbContext<DBContext>();
// Add HangFire
services.AddHangfire(config =>
{
config.UseSQLiteStorage(Path.Join(AppDomain.CurrentDomain.BaseDirectory, "MikrotikWireguard.db"));
});
services.AddHangfireServer();
// Auto Mapper Configurations
services.AddSingleton<PeerMapping>();
services.AddSingleton<ServerMapping>();
@ -79,6 +75,9 @@ namespace MTWireGuard.Application
o.Conventions.AllowAnonymousToPage("/Login");
});
// Add HttpContextAccessor
services.AddHttpContextAccessor();
// Add Session
services.AddDistributedMemoryCache();
@ -91,6 +90,12 @@ namespace MTWireGuard.Application
// Add CORS
services.AddCors();
// Add SerilogUI
services.AddSerilogUi(options =>
{
options.UseSqliteServer($"Data Source={Helper.GetLogPath("logs.db")}", "Logs");
});
}
}
}

View file

@ -0,0 +1,30 @@
using Serilog.Configuration;
using Serilog;
using Serilog.Core;
using Serilog.Events;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MTWireGuard.Application
{
public class ClientIdEnricher(string clientId) : ILogEventEnricher
{
private readonly string _clientId = clientId;
public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory)
{
logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty("ClientId", _clientId));
}
}
public static class LoggingExtensions
{
public static LoggerConfiguration WithClientId(this LoggerEnrichmentConfiguration enrichmentConfiguration, string clientId)
{
return enrichmentConfiguration.With(new ClientIdEnricher(clientId));
}
}
}

View file

@ -0,0 +1,59 @@
using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MTWireGuard.Application
{
public class ExceptionHandler(Serilog.ILogger logger, IHttpContextAccessor contextAccessor) : IExceptionHandler
{
private readonly Serilog.ILogger logger = logger;
private readonly IHttpContextAccessor contextAccessor = contextAccessor;
public ValueTask<bool> TryHandleAsync(HttpContext httpContext, Exception exception, CancellationToken cancellationToken)
{
try
{
string exceptionType = exception.GetType().Name,
message = exception.Message,
stackTrace = exception.StackTrace,
//details = JsonConvert.SerializeObject(exception)!;
details = exception.Source;
ExceptionHandlerContext.Message = message;
ExceptionHandlerContext.StackTrace = stackTrace;
ExceptionHandlerContext.Details = details;
if (SetupValidator.IsValid)
{
logger.Error(exception, "Unhandled error");
}
else
{
logger.Error("Error in configuration: {Title}, {Description}", SetupValidator.Title, SetupValidator.Description);
}
contextAccessor.HttpContext.Response.Redirect("/Error", true);
}
catch (Exception ex)
{
logger.Fatal(ex, "Error In Exception Handler");
}
return ValueTask.FromResult(true);
}
}
public static class ExceptionHandlerContext
{
public static string Message { get; internal set; }
public static string StackTrace { get; internal set; }
public static string Details { get; internal set; }
}
}

View file

@ -1,19 +1,23 @@
using AutoMapper;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Routing;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using MTWireGuard.Application.MinimalAPI;
using MTWireGuard.Application.Models;
using MTWireGuard.Application.Repositories;
using Serilog;
using Serilog.Events;
using Serilog.Exceptions;
using Serilog.Exceptions.Core;
using Serilog.Filters;
using System.IO.Compression;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.RegularExpressions;
using System.Text.Json;
using System.Text.RegularExpressions;
namespace MTWireGuard.Application
{
@ -33,6 +37,11 @@ namespace MTWireGuard.Application
return $"/tool fetch mode=http url=\"{apiURL}\" http-method=post check-certificate=no http-data=([/interface/wireguard/peers/print show-ids proplist=rx,tx as-value]);";
}
public static string UserExpirationScript(string userID)
{
return $"/interface/wireguard/peers/disable {userID}";
}
public static int ParseEntityID(string entityID)
{
return Convert.ToInt32(entityID[1..], 16);
@ -70,6 +79,11 @@ namespace MTWireGuard.Application
SizeSuffixes[mag]);
}
public static long GigabyteToByte(int gigabyte)
{
return Convert.ToInt64(gigabyte * (1024L * 1024 * 1024));
}
#region API Section
public static List<UsageObject> ParseTrafficUsage(string input)
{
@ -118,17 +132,20 @@ namespace MTWireGuard.Application
}
#endregion
public static async void HandleUserTraffics(List<DataUsage> updates, DBContext dbContext, IMikrotikRepository API)
public static async void HandleUserTraffics(List<DataUsage> updates, DBContext dbContext, IMikrotikRepository API, ILogger logger)
{
var dataUsages = await dbContext.DataUsages.ToListAsync();
var existingItems = dataUsages.OrderBy(x => x.CreationTime).ToList();
var lastKnownTraffics = dbContext.LastKnownTraffic.ToList();
var users = await dbContext.Users.ToListAsync();
foreach (var item in updates)
{
var tempUser = users.Find(x => x.Id == item.UserID);
if (tempUser == null) continue;
using var transaction = await dbContext.Database.BeginTransactionAsync();
using var transactionDbContext = new DBContext(); // Create a new DbContext for each transaction
using var transaction = await transactionDbContext.Database.BeginTransactionAsync();
try
{
LastKnownTraffic lastKnown = lastKnownTraffics.Find(x => x.UserID == item.UserID);
@ -137,7 +154,7 @@ namespace MTWireGuard.Application
var old = existingItems.FindLast(oldItem => oldItem.UserID == item.UserID);
if (old == null)
{
await dbContext.DataUsages.AddAsync(item);
await transactionDbContext.DataUsages.AddAsync(item);
tempUser.RX = item.RX + lastKnown.RX;
tempUser.TX = item.TX + lastKnown.TX;
}
@ -146,41 +163,58 @@ namespace MTWireGuard.Application
if ((old.RX <= item.RX || old.TX <= item.TX) &&
(old.RX != item.RX && old.TX != item.TX)) // Normal Data (and not duplicate)
{
await dbContext.DataUsages.AddAsync(item);
await transactionDbContext.DataUsages.AddAsync(item);
}
else if (old.RX > item.RX || old.TX > item.TX) // Server Reset
{
lastKnown.RX = old.RX;
lastKnown.TX = old.TX;
lastKnown.CreationTime = DateTime.Now;
dbContext.LastKnownTraffic.Update(lastKnown);
transactionDbContext.LastKnownTraffic.Update(lastKnown);
item.ResetNotes = $"System reset detected at: {DateTime.Now}";
await dbContext.DataUsages.AddAsync(item);
await transactionDbContext.DataUsages.AddAsync(item);
}
if (item.RX > old.RX) tempUser.RX = item.RX + lastKnown.RX;
if (item.TX > old.TX) tempUser.TX = item.TX + lastKnown.TX;
}
if (tempUser.TrafficLimit > 0 && tempUser.RX + tempUser.TX >= tempUser.TrafficLimit)
if (tempUser.TrafficLimit > 0 && tempUser.RX + tempUser.TX >= GigabyteToByte(tempUser.TrafficLimit))
{
// Disable User
logger.Information($"User #{tempUser.Id} reached {tempUser.RX + tempUser.TX} of {GigabyteToByte(tempUser.TrafficLimit)} bandwidth.");
var disable = await API.DisableUser(item.UserID);
if (disable.Code != "200")
{
Console.WriteLine("Failed disabling user");
logger.Error("Failed disabling user", new
{
userId = item.UserID,
disable.Code,
disable.Title,
disable.Description
});
}
else
{
logger.Information("Disabled user due to bandwidth limit", new
{
item.UserID,
TrafficUsed = Helper.ConvertByteSize(tempUser.RX + tempUser.TX),
tempUser.TrafficLimit
});
}
}
dbContext.Users.Update(tempUser);
await dbContext.SaveChangesAsync();
transaction.Commit();
transactionDbContext.Users.Update(tempUser);
await transactionDbContext.SaveChangesAsync();
await transaction.CommitAsync();
}
catch (DbUpdateException ex)
{
Console.WriteLine(ex.Message);
transaction.Rollback();
logger.Error(ex.Message);
await transaction.RollbackAsync();
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
logger.Error(ex.Message);
await transaction.RollbackAsync();
}
}
}
@ -190,6 +224,63 @@ namespace MTWireGuard.Application
return System.Reflection.Assembly.GetExecutingAssembly().GetName().Version.ToString();
}
/// <summary>
/// Return full path of requested file in app's home directory
/// </summary>
/// <param name="filename">requested file name</param>
/// <returns></returns>
public static string GetHomePath(string filename)
{
return RuntimeInformation.IsOSPlatform(OSPlatform.Linux) ? "/home/app" : Path.Join(AppDomain.CurrentDomain.BaseDirectory, filename);
}
/// <summary>
/// Return full path of requested file in log files directory
/// </summary>
/// <param name="filename">requested file name</param>
/// <returns></returns>
public static string GetLogPath(string filename)
{
return RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? Path.Join(AppDomain.CurrentDomain.BaseDirectory, "log", filename) : Path.Join("/var/log/mtwireguard", filename);
}
public static string GetIDFile() => GetHomePath("identifier.id");
public static string GetIDContent() => File.ReadAllText(GetIDFile());
public static Serilog.Core.Logger LoggerConfiguration()
{
return new LoggerConfiguration()
.Enrich.WithExceptionDetails(new DestructuringOptionsBuilder()
.WithDefaultDestructurers()
.WithRootName("Message").WithRootName("Exception").WithRootName("Exception"))
.Enrich.WithProperty("App.Version", System.Reflection.Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? "0.0.0.0")
.Enrich.WithMachineName()
.Enrich.WithEnvironmentUserName()
.Enrich.WithClientId(GetIDContent())
.WriteTo.Logger(lc => lc
.Filter.ByIncludingOnly(AspNetCoreRequestLogging())
.WriteTo.File(
GetLogPath("access.log"),
rollingInterval: RollingInterval.Day,
retainedFileCountLimit: 31
))
.WriteTo.Logger(lc => lc
.Filter.ByIncludingOnly(LogEvent => LogEvent.Exception != null)
.WriteTo.Seq("https://mtwglogger.techgarage.ir/"))
.WriteTo.Logger(lc => lc
.Filter.ByExcluding(AspNetCoreRequestLogging())
.WriteTo.SQLite(GetLogPath("logs.db")))
.CreateLogger();
}
private static Func<LogEvent, bool> AspNetCoreRequestLogging()
{
return e =>
Matching.FromSource("Microsoft.AspNetCore.Hosting.Diagnostics").Invoke(e) ||
Matching.FromSource("Microsoft.AspNetCore.StaticFiles.StaticFileMiddleware").Invoke(e) ||
Matching.FromSource("Microsoft.AspNetCore.Routing.EndpointMiddleware").Invoke(e);
}
public static TimeSpan ConvertToTimeSpan(string input)
{
int weeks = 0;

View file

@ -7,6 +7,7 @@ using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.IdentityModel.Tokens;
using MikrotikAPI;
using MTWireGuard.Application.Models;
@ -115,11 +116,11 @@ namespace MTWireGuard.Application.MinimalAPI
/// </summary>
public static async Task<Results<Accepted, ProblemHttpResult>> TrafficUsageUpdate(
[FromServices] IMapper mapper,
[FromServices] Serilog.ILogger logger,
[FromServices] DBContext dbContext,
[FromServices] IMikrotikRepository mikrotikRepository,
HttpContext context)
{
StreamReader reader = new(context.Request.Body);
string body = await reader.ReadToEndAsync();
@ -128,7 +129,7 @@ namespace MTWireGuard.Application.MinimalAPI
if (updates == null || updates.Count < 1) return TypedResults.Problem("Empty data");
Helper.HandleUserTraffics(updates, dbContext, mikrotikRepository);
Helper.HandleUserTraffics(updates, dbContext, mikrotikRepository, logger);
return TypedResults.Accepted("Done");
}

View file

@ -0,0 +1,18 @@
using Microsoft.AspNetCore.Http;
using Serilog.Ui.Web.Authorization;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MTWireGuard.Application
{
public class SerilogUiAuthorizeFilter : IUiAuthorizationFilter
{
public bool Authorize(HttpContext httpContext)
{
return httpContext.User.Identity is { IsAuthenticated: true };
}
}
}

View file

@ -1,86 +1,77 @@
using AutoMapper;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using MTWireGuard.Application.Models;
using Microsoft.Extensions.DependencyInjection;
using MTWireGuard.Application.Repositories;
using MTWireGuard.Application.Services;
using System;
using System.Collections.Generic;
using System.Linq;
using Serilog;
using System.Net.NetworkInformation;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;
using static System.Runtime.InteropServices.JavaScript.JSType;
namespace MTWireGuard.Application
{
public class SetupValidator(IServiceProvider serviceProvider)
{
private IMikrotikRepository api;
private ILogger logger;
public async Task Validate()
public static bool IsValid { get; private set; }
public static string Title { get; private set; }
public static string Description { get; private set; }
public async Task<bool> Validate()
{
var envVariables = ValidateEnvironmentVariables();
if (envVariables)
InitializeServices();
if (ValidateEnvironmentVariables())
{
Console.BackgroundColor = ConsoleColor.Black;
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine($"[-] Environment variables are not set!");
Console.WriteLine($"[!] Please set \"MT_IP\", \"MT_USER\", \"MT_PASS\", \"MT_PUBLIC_IP\" variables in container environment.");
Console.ResetColor();
Shutdown();
LogAndDisplayError("Environment variables are not set!", "Please set \"MT_IP\", \"MT_USER\", \"MT_PASS\", \"MT_PUBLIC_IP\" variables in container environment.");
IsValid = false;
return false;
}
serviceProvider.GetService<DBContext>().Database.EnsureCreated();
api = serviceProvider.GetService<IMikrotikRepository>();
if (!File.Exists(Helper.GetIDFile()))
{
using var fs = File.OpenWrite(Helper.GetIDFile());
var id = Guid.NewGuid().ToString();
id = id[(id.LastIndexOf('-') + 1)..];
byte[] identifier = new UTF8Encoding(true).GetBytes(id);
fs.Write(identifier, 0, identifier.Length);
}
var (apiConnection, apiConnectionMessage) = await ValidateAPIConnection();
if (!apiConnection)
{
Console.BackgroundColor = ConsoleColor.Black;
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine($"[-] Error connecting to the router api!");
Console.WriteLine($"[!] {apiConnectionMessage}");
Console.ResetColor();
Shutdown();
var MT_IP = Environment.GetEnvironmentVariable("MT_IP");
var ping = new Ping();
var reply = ping.Send(MT_IP, 60 * 1000);
if (reply.Status == IPStatus.Success)
{
LogAndDisplayError("Error connecting to the router api!", apiConnectionMessage);
}
else
{
LogAndDisplayError("Error connecting to the router api!", $"Can't find Mikrotik API server at address: {MT_IP}\r\nping status: {reply.Status}");
}
IsValid = false;
return false;
}
var ip = GetIPAddress();
if (string.IsNullOrEmpty(ip))
{
Console.BackgroundColor = ConsoleColor.Black;
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine($"[-] Error getting container IP address!");
Console.ResetColor();
Shutdown();
LogAndDisplayError("Error getting container IP address!", "Invalid container IP address.");
IsValid = false;
return false;
}
var scripts = await api.GetScripts();
var schedulers = await api.GetSchedulers();
var trafficScript = scripts.Find(x => x.Name == "SendTrafficUsage");
var trafficScheduler = schedulers.Find(x => x.Name == "TrafficUsage");
if (trafficScript == null)
if (!await api.TryConnectAsync())
{
var create = await api.CreateScript(new()
{
Name = "SendTrafficUsage",
Policies = ["write", "read", "test"],
DontRequiredPermissions = false,
Source = Helper.PeersTrafficUsageScript($"http://{ip}/api/usage")
});
var result = create.Code;
}
if (trafficScheduler == null)
{
var create = await api.CreateScheduler(new()
{
Name = "TrafficUsage",
Interval = new TimeSpan(0, 5, 0),
OnEvent = "SendTrafficUsage",
Policies = ["write", "read", "test"]
});
var result = create.Code;
LogAndDisplayError("Error connecting to the router api!", "Connecting to API failed.");
IsValid = false;
return false;
}
await EnsureTrafficScripts(ip);
IsValid = true;
return true;
}
private static bool ValidateEnvironmentVariables()
@ -105,7 +96,7 @@ namespace MTWireGuard.Application
}
}
private static string GetIPAddress()
private string GetIPAddress()
{
try
{
@ -115,11 +106,69 @@ namespace MTWireGuard.Application
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
logger.Error(ex, "Error getting container IP address.");
return string.Empty;
}
}
private void LogAndDisplayError(string title, string description)
{
Title = title;
Description = description;
Console.BackgroundColor = ConsoleColor.Black;
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine($"[-] {Title}");
Console.WriteLine($"[!] {Description}");
Console.ResetColor();
logger.Error("Error in container configuration", new { Error = Title, Description });
}
private void InitializeServices()
{
serviceProvider.GetService<DBContext>().Database.EnsureCreated();
api = serviceProvider.GetService<IMikrotikRepository>();
logger = serviceProvider.GetService<ILogger>();
}
private async Task EnsureTrafficScripts(string ip)
{
var scripts = await api.GetScripts();
var schedulers = await api.GetSchedulers();
//if (scripts.Find(x => x.Name == "SendTrafficUsage") == null)
//{
// var create = await api.CreateScript(new()
// {
// Name = "SendTrafficUsage",
// Policies = ["write", "read", "test", "ftp"],
// DontRequiredPermissions = false,
// Source = Helper.PeersTrafficUsageScript($"http://{ip}/api/usage")
// });
// var result = create.Code;
// logger.Information("Created TrafficUsage Script", new
// {
// result = create
// });
//}
if (schedulers.Find(x => x.Name == "TrafficUsage") == null)
{
var create = await api.CreateScheduler(new()
{
Name = "TrafficUsage",
Interval = new TimeSpan(0, 5, 0),
//OnEvent = "SendTrafficUsage",
OnEvent = Helper.PeersTrafficUsageScript($"http://{ip}/api/usage"),
Policies = ["write", "read", "test", "ftp"],
Comment = "update wireguard peers traffic usage"
});
var result = create.Code;
logger.Information("Created TrafficUsage Scheduler", new
{
result = create
});
}
}
private static void Shutdown()
{
Environment.Exit(0);

View file

@ -14,6 +14,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MTWireGuard.Application", "
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MikrotikAPI", "MikrotikAPI\MikrotikAPI.csproj", "{357EE40B-AA30-482C-94CF-34854BE24D61}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Serilog.Ui.SqliteProvider", "Serilog.Ui.SqliteProvider\Serilog.Ui.SqliteProvider.csproj", "{4D0ED34E-E84B-4861-82DC-C2149DCD6E14}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -32,6 +34,10 @@ Global
{357EE40B-AA30-482C-94CF-34854BE24D61}.Debug|Any CPU.Build.0 = Debug|Any CPU
{357EE40B-AA30-482C-94CF-34854BE24D61}.Release|Any CPU.ActiveCfg = Release|Any CPU
{357EE40B-AA30-482C-94CF-34854BE24D61}.Release|Any CPU.Build.0 = Release|Any CPU
{4D0ED34E-E84B-4861-82DC-C2149DCD6E14}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4D0ED34E-E84B-4861-82DC-C2149DCD6E14}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4D0ED34E-E84B-4861-82DC-C2149DCD6E14}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4D0ED34E-E84B-4861-82DC-C2149DCD6E14}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE

View file

@ -0,0 +1,47 @@
using Microsoft.Extensions.DependencyInjection;
using Serilog.Ui.Core;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Serilog.Ui.SqliteDataProvider
{
/// <summary>
/// Sqlite data provider specific extension methods for <see cref="SerilogUiOptionsBuilder"/>.
/// </summary>
public static class SerilogUiOptionBuilderExtensions
{
/// <summary>
/// Configures the SerilogUi to connect to a Sqlite database.
/// </summary>
/// <param name="optionsBuilder"> The options builder. </param>
/// <param name="connectionString"> The connection string. </param>
/// <param name="tableName"> Name of the table. </param>
/// <exception cref="ArgumentNullException"> throw if connectionString is null </exception>
/// <exception cref="ArgumentNullException"> throw is tableName is null </exception>
public static void UseSqliteServer(
this SerilogUiOptionsBuilder optionsBuilder,
string connectionString,
string tableName
)
{
if (string.IsNullOrWhiteSpace(connectionString))
throw new ArgumentNullException(nameof(connectionString));
if (string.IsNullOrWhiteSpace(tableName))
throw new ArgumentNullException(nameof(tableName));
var relationProvider = new RelationalDbOptions
{
ConnectionString = connectionString,
TableName = tableName
};
((ISerilogUiOptionsBuilder)optionsBuilder).Services
.AddScoped<IDataProvider, SqliteDataProvider>(p => ActivatorUtilities.CreateInstance<SqliteDataProvider>(p, relationProvider));
}
}
}

View file

@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Dapper" Version="2.1.35" />
<PackageReference Include="Microsoft.Data.Sqlite.Core" Version="8.0.5" />
<PackageReference Include="Serilog.UI" Version="2.6.0" />
<PackageReference Include="SQLite" Version="3.13.0" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,134 @@
using Dapper;
using Microsoft.Data.Sqlite;
using Serilog.Ui.Core;
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
namespace Serilog.Ui.SqliteDataProvider
{
public class SqliteDataProvider(RelationalDbOptions options) : IDataProvider
{
private readonly RelationalDbOptions _options = options ?? throw new ArgumentNullException(nameof(options));
public async Task<(IEnumerable<LogModel>, int)> FetchDataAsync(
int page,
int count,
string level = null,
string searchCriteria = null,
DateTime? startDate = null,
DateTime? endDate = null
)
{
var logsTask = GetLogs(page - 1, count, level, searchCriteria, startDate, endDate);
var logCountTask = CountLogs(level, searchCriteria, startDate, endDate);
await Task.WhenAll(logsTask, logCountTask);
return (await logsTask, await logCountTask);
}
public string Name => _options.ToDataProviderName("SQLite");
private Task<IEnumerable<LogModel>> GetLogs(
int page,
int count,
string level,
string searchCriteria,
DateTime? startDate,
DateTime? endDate)
{
var queryBuilder = new StringBuilder();
queryBuilder.Append("SELECT Id, RenderedMessage AS Message, Level, Timestamp, Exception, Properties FROM ");
queryBuilder.Append(_options.TableName);
queryBuilder.Append(" ");
GenerateWhereClause(queryBuilder, level, searchCriteria, startDate, endDate);
queryBuilder.Append("ORDER BY Id DESC LIMIT @Offset, @Count");
using (var connection = new SqliteConnection(_options.ConnectionString))
{
var param = new
{
Offset = page * count,
Count = count,
Level = level,
Search = searchCriteria != null ? $"%{searchCriteria}%" : null,
StartDate = startDate,
EndDate = endDate
};
var logs = connection.Query<LogModel>(queryBuilder.ToString(), param);
var index = 1;
foreach (var log in logs)
log.RowNo = (page * count) + index++;
return Task.FromResult(logs);
}
}
private Task<int> CountLogs(
string level,
string searchCriteria,
DateTime? startDate = null,
DateTime? endDate = null)
{
var queryBuilder = new StringBuilder();
queryBuilder.Append("SELECT COUNT(Id) FROM ");
queryBuilder.Append(_options.TableName);
queryBuilder.Append(" ");
GenerateWhereClause(queryBuilder, level, searchCriteria, startDate, endDate);
using var connection = new SqliteConnection(_options.ConnectionString);
return Task.FromResult(connection.QueryFirstOrDefault<int>(queryBuilder.ToString(),
new
{
Level = level,
Search = searchCriteria != null ? "%" + searchCriteria + "%" : null,
StartDate = startDate,
EndDate = endDate
}));
}
private void GenerateWhereClause(
StringBuilder queryBuilder,
string level,
string searchCriteria,
DateTime? startDate = null,
DateTime? endDate = null)
{
var whereIncluded = false;
if (!string.IsNullOrEmpty(level))
{
queryBuilder.Append("WHERE Level = @Level ");
whereIncluded = true;
}
if (!string.IsNullOrEmpty(searchCriteria))
{
queryBuilder.Append(whereIncluded
? "AND (RenderedMessage LIKE @Search OR Exception LIKE @Search) "
: "WHERE (RenderedMessage LIKE @Search OR Exception LIKE @Search) ");
whereIncluded = true;
}
if (startDate != null)
{
queryBuilder.Append(whereIncluded
? "AND Timestamp >= @StartDate "
: "WHERE Timestamp >= @StartDate ");
whereIncluded = true;
}
if (endDate != null)
{
queryBuilder.Append(whereIncluded
? "AND Timestamp <= @EndDate "
: "WHERE Timestamp <= @EndDate ");
}
}
}
}

View file

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
@ -11,18 +11,23 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AutoMapper" Version="12.0.1" />
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="12.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.1">
<PackageReference Include="AutoMapper" Version="13.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.6">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.19.6" />
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="8.0.0" />
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.21.0" />
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="8.0.2" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="NuGet.Protocol" Version="6.8.0" />
<PackageReference Include="Razor.Templating.Core" Version="1.9.0" />
<PackageReference Include="System.Drawing.Common" Version="8.0.1" />
<PackageReference Include="NuGet.Protocol" Version="6.10.1" />
<PackageReference Include="Razor.Templating.Core" Version="2.0.0" />
<PackageReference Include="Serilog.AspNetCore" Version="8.0.1" />
<PackageReference Include="Serilog.Enrichers.Environment" Version="3.0.1" />
<PackageReference Include="Serilog.Exceptions" Version="8.4.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
<PackageReference Include="Serilog.Sinks.Seq" Version="8.0.0" />
<PackageReference Include="Serilog.Sinks.SQLite" Version="6.0.0" />
<PackageReference Include="System.Drawing.Common" Version="8.0.6" />
</ItemGroup>
<ItemGroup>

View file

@ -1,7 +1,12 @@
@page
@model ErrorModel
@inject IHttpContextAccessor contextAccessor
@{
Layout = null;
var httpContext = contextAccessor.HttpContext;
string title = Application.ExceptionHandlerContext.Message,
message = Application.ExceptionHandlerContext.StackTrace,
details = Application.ExceptionHandlerContext.Details;
}
<!DOCTYPE html>
@ -32,6 +37,16 @@
<!-- Theme Style Switcher-->
<script src="assets/js/themeSwitcher.js"></script>
<style>
.accordion-body {
overflow-y: auto;
}
.accordion-body p {
max-height: 30vh;
}
</style>
</head>
<body>
<div class="min-vh-100 d-flex flex-row align-items-center">
@ -39,10 +54,45 @@
<div class="row justify-content-center">
<div class="col-md-6">
<div class="clearfix">
<h1 class="float-start display-3 me-4">500</h1>
<h4 class="pt-3">@ViewBag.Title</h4>
<h6>@ViewBag.Message</h6>
<p class="text-medium-emphasis">@Html.Raw(ViewBag.Details)</p>
<h1 class="float-start display-3 mx-2 mt-4">500</h1>
<h4 class="pt-3">@title</h4>
@if (!Application.SetupValidator.IsValid)
{
<hr />
<h4 class="text-danger"><i class='bx bxs-chevrons-right'></i> Invalid Setup Variables</h4>
<strong>@Application.SetupValidator.Title</strong>
<br />
<p>@Application.SetupValidator.Description</p>
}
<div class="accordion mb-3" id="infoAccordion">
<div class="accordion-item">
<h2 class="accordion-header">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#titleAccordion" aria-expanded="true" aria-controls="titleAccordion">
Error Message
</button>
</h2>
<div id="titleAccordion" class="accordion-collapse collapse" data-bs-parent="#infoAccordion">
<div class="accordion-body">
<p class="text-medium-emphasis text-break">@message</p>
</div>
</div>
</div>
<div class="accordion-item">
<h2 class="accordion-header">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#detailsAccordion" aria-expanded="true" aria-controls="detailsAccordion">
Error Details
</button>
</h2>
<div id="detailsAccordion" class="accordion-collapse collapse" data-bs-parent="#infoAccordion">
<div class="accordion-body">
<p class="text-medium-emphasis text-break">@Html.Raw(details)</p>
</div>
</div>
</div>
</div>
<a href="/Debug" class="btn btn-primary">
<i class="bx bx-notepad me-1"></i> <span class="d-none d-lg-inline-block">View Logs</span>
</a>
</div>
</div>
</div>

View file

@ -1,15 +1,19 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.VisualStudio.Web.CodeGenerators.Mvc.Templates.Blazor;
using System.Diagnostics;
namespace MTWireGuard.Pages
{
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
[IgnoreAntiforgeryToken]
[AllowAnonymous]
public class ErrorModel : PageModel
{
public ErrorModel()
{
}

View file

@ -3,19 +3,29 @@ using MTWireGuard.Middlewares;
using MTWireGuard.Application;
using Microsoft.Extensions.Caching.Memory;
using MTWireGuard.Application.MinimalAPI;
using Serilog;
using Serilog.Exceptions.Core;
using Serilog.Exceptions;
using System.Configuration;
using Serilog.Ui.Web;
using Serilog.Ui.Web.Authorization;
internal class Program
{
public static bool isValid { get; private set; }
public static string validationMessage { get; private set; }
private static async Task Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllersWithViews();
builder.Services.AddExceptionHandler<ExceptionHandler>();
builder.Services.AddProblemDetails();
builder.Services.AddApplicationServices();
var app = builder.Build();
builder.Host.UseSerilog(Helper.LoggerConfiguration());
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
var app = builder.Build();
app.UseHttpsRedirection();
@ -23,10 +33,13 @@ var serviceScope = app.Services.CreateScope().ServiceProvider;
// Validate Prerequisite
var validator = new SetupValidator(serviceScope);
await validator.Validate();
isValid = await validator.Validate();
if (!app.Environment.IsDevelopment())
{
app.UseStaticFiles();
app.UseHsts();
}
else
app.UseStaticFiles(new StaticFileOptions()
{
@ -37,11 +50,9 @@ else
}
});
app.UseDependencyCheck();
app.UseExceptionHandler();
app.UseClientReporting();
app.UseExceptionHandling();
//app.UseAntiForgery();
app.UseRouting();
app.UseAuthentication();
@ -62,4 +73,21 @@ app.UseCors(options =>
options.AllowAnyOrigin();
});
app.UseSerilogRequestLogging();
app.UseSerilogUi(options =>
{
options.RoutePrefix = "Debug";
options.InjectStylesheet("/assets/lib/boxicons/css/boxicons.min.css");
options.InjectStylesheet("/assets/css/serilogui.css");
options.InjectJavascript("/assets/js/serilogui.js");
options.Authorization.AuthenticationType = AuthenticationType.Jwt;
options.Authorization.Filters =
[
new SerilogUiAuthorizeFilter()
];
});
app.Run();
}
}

View file

@ -0,0 +1,46 @@
#sidebar {
background: #343a40;
}
#sidebar .logo {
font-size: 2rem;
}
#sidebar ul li a {
padding: 0;
border-radius: .5rem;
border-bottom: 3px solid;
outline: 0;
display: flex;
flex-direction: row;
align-items: center;
margin: .5rem;
}
#sidebar ul li a span {
margin-right: 15px;
width: 50px;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 40px;
font-size: 1.25rem;
margin-right: .25rem;
}
#sidebar ul.components li a {
--bs-primary: #4582ec;
padding: 10px 0;
color: var(--bs-primary);
background-color: rgba(105,108,255,.16) !important;
border-color: var(--bs-primary);
}
#sidebar.active ul.components li a {
flex-direction: column;
}
#logTable .log-level {
border-radius: 5px;
user-select: none;
}

View file

@ -0,0 +1,6 @@
let favicon = document.createElement('link');
favicon.href = 'img/favicon.ico';
favicon.rel = 'icon';
document.getElementsByTagName('head')[0].appendChild(favicon);
document.querySelector('#sidebar.active .logo:first-child').innerHTML = 'MW';
document.querySelector('#sidebar.active .logo:last-child').innerHTML = 'MTWireguard';