2026-01-26 14:51:28 +09:00

213 lines
7.3 KiB
C#

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<LogEntry> _logQueue;
private readonly BlockingCollection<LogEntry> _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<LogEntry>(new ConcurrentQueue<LogEntry>());
_errorQueue = new BlockingCollection<LogEntry>(new ConcurrentQueue<LogEntry>());
_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<LogEntry>("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<LogEntry>("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<LogEntry>();
var col = _instance._db.GetCollection<LogEntry>(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}");
}
}
}
*/