using LiteDB; using System; using System.Collections.Concurrent; using System.IO; using System.Linq; using System.Threading.Tasks; public enum LogLevel { INFO, WARN, DEBUG, ERROR } public class LogEntry { public int Id { get; set; } // LiteDB 전용 public DateTime Timestamp { get; set; } public LogLevel Level { get; set; } public string Message { get; set; } } public sealed class AsyncLog : IDisposable { private static AsyncLog _instance; private static readonly object _lock = new object(); private readonly BlockingCollection _logQueue; private readonly BlockingCollection _errorQueue; private readonly Task _logTask; private readonly Task _errorTask; private StreamWriter _writer; private StreamWriter _errorWriter; private string _currentDateFolder; private LiteDatabase _db; public string BaseLogFolder { get; private set; } public string LogFileName { get; private set; } = "app.log"; public string ErrorFileName { get; private set; } = "error.log"; public bool EnableConsole { get; private set; } private static AsyncLog SafeInstance => _instance; #region Singleton Init public static void Init(string baseLogFolder, bool enableConsole = true) { if (_instance != null) return; lock (_lock) { if (_instance == null) { _instance = new AsyncLog(baseLogFolder, enableConsole); AppDomain.CurrentDomain.ProcessExit += (s, e) => _instance.Dispose(); } } } private AsyncLog(string baseLogFolder, bool enableConsole) { BaseLogFolder = baseLogFolder; EnableConsole = enableConsole; _logQueue = new BlockingCollection(new ConcurrentQueue()); _errorQueue = new BlockingCollection(new ConcurrentQueue()); _logTask = Task.Run(ProcessLogQueue); _errorTask = Task.Run(ProcessErrorQueue); Directory.CreateDirectory(BaseLogFolder); string dbPath = Path.Combine(BaseLogFolder, "Logs.db"); _db = new LiteDatabase(dbPath); } #endregion #region Public Logging API (다중 인자 지원) public static void I(string message, params object[] args) => SafeInstance?.EnqueueLog(LogLevel.INFO, Format(message, args)); public static void W(string message, params object[] args) => SafeInstance?.EnqueueLog(LogLevel.WARN, Format(message, args)); public static void D(string message, params object[] args) => SafeInstance?.EnqueueLog(LogLevel.DEBUG, Format(message, args)); public static void E(string message, params object[] args) => SafeInstance?.EnqueueError(Format(message, args)); private static string Format(string message, object[] args) { if (args == null || args.Length == 0) return message; try { return string.Format(message, args); } catch { return message + " (format error)"; } } #endregion #region Queue Processing private void EnqueueLog(LogLevel level, string message) { var entry = new LogEntry { Timestamp = DateTime.Now, Level = level, Message = message }; if (EnableConsole) Console.WriteLine($"[{entry.Timestamp:yyyy-MM-dd HH:mm:ss}] [{level}] {message}"); _logQueue.Add(entry); } private void EnqueueError(string message) { var entry = new LogEntry { Timestamp = DateTime.Now, Level = LogLevel.ERROR, Message = message }; if (EnableConsole) Console.WriteLine($"[{entry.Timestamp:yyyy-MM-dd HH:mm:ss}] [ERROR] {message}"); _errorQueue.Add(entry); } private void ProcessLogQueue() { foreach (var entry in _logQueue.GetConsumingEnumerable()) { try { string today = DateTime.Now.ToString("yyyy-MM-dd"); if (_writer == null || _currentDateFolder != today) { _writer?.Dispose(); _currentDateFolder = today; string todayFolder = Path.Combine(BaseLogFolder, today); Directory.CreateDirectory(todayFolder); string logFilePath = Path.Combine(todayFolder, LogFileName); _writer = new StreamWriter(logFilePath, true) { AutoFlush = true }; } _writer.WriteLine($"[{entry.Timestamp:HH:mm:ss}] [{entry.Level}] {entry.Message}"); var col = _db.GetCollection("logs"); col.Insert(entry); } catch { } } } private void ProcessErrorQueue() { foreach (var entry in _errorQueue.GetConsumingEnumerable()) { try { string today = DateTime.Now.ToString("yyyy-MM-dd"); if (_errorWriter == null || _currentDateFolder != today) { _errorWriter?.Dispose(); _currentDateFolder = today; string todayFolder = Path.Combine(BaseLogFolder, today); Directory.CreateDirectory(todayFolder); string errorFilePath = Path.Combine(todayFolder, ErrorFileName); _errorWriter = new StreamWriter(errorFilePath, true) { AutoFlush = true }; } _errorWriter.WriteLine($"[{entry.Timestamp:HH:mm:ss}] [ERROR] {entry.Message}"); var col = _db.GetCollection("errors"); col.Insert(entry); } catch { } } } #endregion #region Dispose public void Dispose() { _logQueue.CompleteAdding(); _errorQueue.CompleteAdding(); try { _logTask.Wait(); } catch { } try { _errorTask.Wait(); } catch { } _writer?.Dispose(); _errorWriter?.Dispose(); _logQueue.Dispose(); _errorQueue.Dispose(); _db?.Dispose(); } #endregion #region Query API public static LogEntry[] QueryLogs(DateTime? from = null, DateTime? to = null, LogLevel[] levels = null, bool errorLogs = false) { if (_instance == null) return Array.Empty(); var col = _instance._db.GetCollection(errorLogs ? "errors" : "logs"); var query = col.Query(); if (from.HasValue) query = query.Where(x => x.Timestamp >= from.Value); if (to.HasValue) query = query.Where(x => x.Timestamp <= to.Value); if (levels != null && levels.Length > 0) query = query.Where(x => levels.Contains(x.Level)); return query.ToArray(); } #endregion } /* ✅ 사용 예제 class Program { static void Main() { AsyncLog.Init("C:\\Logs", enableConsole: true); AsyncLog.I("애플리케이션 시작됨"); AsyncLog.D("디버그 메시지: 값={0}, 상태={1}", 123, "OK"); AsyncLog.W("경고 발생: 사용자 {0}, 시도 횟수 {1}", "Alice", 5); AsyncLog.E("에러 발생: 코드={0}, 메시지={1}", 500, "Internal Server Error"); // 로그 조회 (오늘, INFO 레벨만) var logs = AsyncLog.QueryLogs(DateTime.Today, DateTime.Today.AddDays(1), new[] { LogLevel.INFO }); foreach (var log in logs) { Console.WriteLine($"[조회] {log.Timestamp} {log.Level} {log.Message}"); } } } */