mirror of
https://github.com/aliasvault/aliasvault.git
synced 2026-01-27 07:22:14 -05:00
171 lines
7.4 KiB
C#
171 lines
7.4 KiB
C#
//-----------------------------------------------------------------------
|
|
// <copyright file="LoggingConfiguration.cs" company="aliasvault">
|
|
// Copyright (c) aliasvault. All rights reserved.
|
|
// Licensed under the AGPLv3 license. See LICENSE.md file in the project root for full license information.
|
|
// </copyright>
|
|
//-----------------------------------------------------------------------
|
|
|
|
namespace AliasVault.Logging;
|
|
|
|
using System.Globalization;
|
|
using AliasServerDb;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Microsoft.Extensions.Configuration;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using Serilog;
|
|
using Serilog.Events;
|
|
using Serilog.Filters;
|
|
|
|
/// <summary>
|
|
/// Extension methods for configuring logging.
|
|
/// </summary>
|
|
public static class LoggingConfiguration
|
|
{
|
|
private const string SourceContextKey = "SourceContext";
|
|
|
|
/// <summary>
|
|
/// List of source contexts that are allowed to log Information level to the database.
|
|
/// These are important operational events that should be persisted to the database (in addition to file logging).
|
|
/// </summary>
|
|
private static readonly HashSet<string> AllowedInformationSourcesForDatabase = new()
|
|
{
|
|
// Service lifecycle events
|
|
"AliasVault.TaskRunner.Workers.TaskRunnerWorker",
|
|
"AliasVault.SmtpService.Workers.SmtpServerWorker",
|
|
|
|
// Task completion events
|
|
"AliasVault.TaskRunner.Tasks.EmailQuotaCleanupTask",
|
|
"AliasVault.TaskRunner.Tasks.DisabledEmailCleanupTask",
|
|
"AliasVault.TaskRunner.Tasks.EmailCleanupTask",
|
|
"AliasVault.TaskRunner.Tasks.LogCleanupTask",
|
|
"AliasVault.TaskRunner.Tasks.RefreshTokenCleanupTask",
|
|
|
|
// Admin actions
|
|
"AliasVault.Admin.Main.Pages.Users.Delete",
|
|
"AliasVault.Admin.Main.Pages.Account.Manage.Disable2fa",
|
|
"AliasVault.Admin.Main.Pages.Account.Manage.EnableAuthenticator",
|
|
"AliasVault.Admin.Auth.Pages.Login",
|
|
"AliasVault.Admin.Auth.Pages.LoginWith2fa",
|
|
"AliasVault.Admin.Auth.Pages.LoginWithRecoveryCode",
|
|
"AliasVault.Admin.Main.Pages.Settings.Server",
|
|
"AliasVault.Admin.Main.Pages.Users.View.Index",
|
|
|
|
// Email processing events
|
|
"AliasVault.SmtpService.Handlers.DatabaseMessageStore",
|
|
};
|
|
|
|
/// <summary>
|
|
/// Configures Serilog logging for the application.
|
|
/// </summary>
|
|
/// <param name="services">IServiceCollection.</param>
|
|
/// <param name="configuration">IConfiguration.</param>
|
|
/// <param name="applicationName">The application name to include in the log.</param>
|
|
/// <param name="logFolder">The folder to log to.</param>
|
|
/// <returns>IServiceCollection instance.</returns>
|
|
public static IServiceCollection ConfigureLogging(this IServiceCollection services, IConfiguration configuration, string applicationName, string logFolder = "logs/")
|
|
{
|
|
services.AddSerilog(new LoggerConfiguration()
|
|
.ReadFrom.Configuration(configuration)
|
|
.Enrich.FromLogContext()
|
|
.Enrich.WithProperty("Application", applicationName)
|
|
.Filter.ByIncludingOnly(GetSourceContextFilter(configuration))
|
|
|
|
// Log to console.
|
|
.WriteTo.Logger(lc => lc
|
|
.WriteTo.Console(outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] {SourceContext} {Message:lj} {Properties:j}{NewLine}{Exception}"))
|
|
|
|
// Log everything to a file.
|
|
.WriteTo.Logger(lc => lc
|
|
.WriteTo.File(
|
|
path: $"{logFolder}/{applicationName}-log-.txt",
|
|
rollingInterval: RollingInterval.Day,
|
|
outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] {SourceContext} {Message:lj} {Properties:j}{NewLine}{Exception}"))
|
|
|
|
// Log all errors and above to a separate file.
|
|
.WriteTo.Logger(lc => lc
|
|
.Filter.ByIncludingOnly(evt => evt.Level >= LogEventLevel.Error)
|
|
.WriteTo.File(
|
|
path: $"{logFolder}/{applicationName}-error-.txt",
|
|
rollingInterval: RollingInterval.Day,
|
|
outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] {SourceContext} {Message:lj} {Properties:j}{NewLine}{Exception}"))
|
|
|
|
// Log to database:
|
|
// - All warnings and above
|
|
// - Specific Information logs from allowed sources
|
|
// Exclude Microsoft.EntityFrameworkCore logs to prevent loops
|
|
.WriteTo.Logger(lc => lc
|
|
.Filter.ByIncludingOnly(evt => ShouldLogToDatabase(evt))
|
|
.Filter.ByExcluding(Matching.FromSource("Microsoft.EntityFrameworkCore"))
|
|
.WriteTo.Sink(new DatabaseSink(CultureInfo.InvariantCulture, () => services.BuildServiceProvider().GetRequiredService<IDbContextFactory<AliasServerDbContext>>(), applicationName)))
|
|
.CreateLogger());
|
|
|
|
return services;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Determines if a log event should be written to the database.
|
|
/// </summary>
|
|
/// <param name="evt">The log event to check.</param>
|
|
/// <returns>True if the event should be logged to database, false otherwise.</returns>
|
|
private static bool ShouldLogToDatabase(LogEvent evt)
|
|
{
|
|
// Always log warnings and above
|
|
if (evt.Level >= LogEventLevel.Warning)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
// For Information level, only log from allowed sources
|
|
if (evt.Level == LogEventLevel.Information && evt.Properties.ContainsKey(SourceContextKey))
|
|
{
|
|
var sourceContext = evt.Properties[SourceContextKey].ToString().Trim('"');
|
|
if (AllowedInformationSourcesForDatabase.Contains(sourceContext))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Helper method to create the source context filter.
|
|
/// </summary>
|
|
/// <param name="configuration">IConfiguration instance.</param>
|
|
/// <returns>Source context filter for serilog builder.</returns>
|
|
private static Func<LogEvent, bool> GetSourceContextFilter(IConfiguration configuration)
|
|
{
|
|
return evt =>
|
|
{
|
|
var sourceContext = evt.Properties.ContainsKey(SourceContextKey)
|
|
? evt.Properties[SourceContextKey].ToString()
|
|
: string.Empty;
|
|
var configuredLevel = GetLogEventLevel(sourceContext, configuration);
|
|
return evt.Level >= configuredLevel;
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the log event level for the source context based on IConfiguration (appsettings.json).
|
|
/// </summary>
|
|
/// <param name="sourceContext">The source context to look for in the configuration.</param>
|
|
/// <param name="configuration">IConfiguration instance.</param>
|
|
/// <returns>LogEventLevel enum.</returns>
|
|
private static LogEventLevel GetLogEventLevel(string sourceContext, IConfiguration configuration)
|
|
{
|
|
var logLevels = configuration.GetSection("Logging:LogLevel");
|
|
|
|
// If this specific source context has an override level defined in appsettings.json, return that.
|
|
var logLevel = logLevels.GetChildren()
|
|
.FirstOrDefault(ll => sourceContext.Contains(ll.Key, StringComparison.OrdinalIgnoreCase));
|
|
if (logLevel != null)
|
|
{
|
|
return Enum.Parse<LogEventLevel>(logLevel.Value ?? "Information", true);
|
|
}
|
|
|
|
// If there is no specific override, use the default.
|
|
var defaultLevel = logLevels["Default"] ?? "Information";
|
|
return Enum.Parse<LogEventLevel>(defaultLevel, true);
|
|
}
|
|
}
|