using LiteDB; using MailSharp.MailClient.Models; using Microsoft.Extensions.Logging; namespace MailSharp.MailClient.Services.Logging; public class LogQuery { // Exact match, not a minimum threshold - the Maintenance log viewer's dropdown is meant to // isolate one level at a time (e.g. "just show me the errors"), not "this level and worse". public LogLevel? Level { get; set; } public string? Category { get; set; } public int? AccountId { get; set; } public DateTime? From { get; set; } public DateTime? To { get; set; } public string? Search { get; set; } public int Page { get; set; } = 1; public int PageSize { get; set; } = 50; } public class LogQueryResult { public List Items { get; set; } = []; public int TotalCount { get; set; } } public interface ILogStore { void Add(LogEntry entry); LogQueryResult Query(LogQuery query); void PruneOlderThan(DateTime cutoffUtc); void Clear(); } // Holds a single long-lived LiteDatabase connection for the app's lifetime (registered as a // singleton), same pattern as the other *Store classes - a fresh connection per call would race on // the OS file lock under concurrent requests. public class LiteDbLogStore : ILogStore, IDisposable { private readonly LiteDatabase _db; public LiteDbLogStore(IWebHostEnvironment env) { var dir = Path.Combine(env.ContentRootPath, "App_Data"); Directory.CreateDirectory(dir); _db = new LiteDatabase(Path.Combine(dir, "logs.db")); Logs.EnsureIndex(x => x.Timestamp); Logs.EnsureIndex(x => x.Level); Logs.EnsureIndex(x => x.Category); Logs.EnsureIndex(x => x.AccountId); } private ILiteCollection Logs => _db.GetCollection("logs"); public void Add(LogEntry entry) => Logs.Insert(entry); public LogQueryResult Query(LogQuery query) { var q = Logs.Query(); if (query.Level != null) q = q.Where(x => x.Level == query.Level.Value); if (!string.IsNullOrWhiteSpace(query.Category)) q = q.Where(x => x.Category.Contains(query.Category)); if (query.AccountId != null) q = q.Where(x => x.AccountId == query.AccountId.Value); if (query.From != null) q = q.Where(x => x.Timestamp >= query.From.Value); if (query.To != null) q = q.Where(x => x.Timestamp <= query.To.Value); if (!string.IsNullOrWhiteSpace(query.Search)) q = q.Where(x => x.Message.Contains(query.Search)); var total = q.Count(); var page = Math.Max(1, query.Page); var pageSize = Math.Clamp(query.PageSize, 1, 500); var items = q.OrderByDescending(x => x.Timestamp) .Offset((page - 1) * pageSize) .Limit(pageSize) .ToList(); return new LogQueryResult { Items = items, TotalCount = total }; } public void PruneOlderThan(DateTime cutoffUtc) => Logs.DeleteMany(x => x.Timestamp < cutoffUtc); public void Clear() => Logs.DeleteAll(); public void Dispose() => _db.Dispose(); }