1+ using System . Collections . Concurrent ;
2+ using System . IO . Compression ;
3+ using System . Runtime . CompilerServices ;
4+ using System . Text ;
5+
6+
7+ namespace Trion . Core . Logging ;
8+
9+ public sealed class TrionLogger : IAsyncDisposable
10+ {
11+ private readonly LoggerOptions _opts ;
12+ private readonly BlockingCollection < LogEntry > _queue = new ( ) ;
13+ private readonly CancellationTokenSource _cts = new ( ) ;
14+ private readonly Task _writerTask ;
15+ private readonly SemaphoreSlim _fileGate = new ( 1 , 1 ) ;
16+
17+ private StreamWriter ? _todayWriter ;
18+ private DateTime _todayDate ;
19+
20+ public TrionLogger ( LoggerOptions ? opts = null )
21+ {
22+ _opts = opts ?? new LoggerOptions ( ) ;
23+ Directory . CreateDirectory ( _opts . Folder ) ;
24+ OpenTodayFile ( ) ;
25+ _writerTask = Task . Run ( WritePump ) ;
26+ _ = Task . Run ( HouseKeepingLoop ) ;
27+ }
28+
29+ /* ------------- public API ------------- */
30+ public void Info ( string message , string category = "" ,
31+ [ CallerMemberName ] string member = "" ,
32+ [ CallerFilePath ] string file = "" ,
33+ [ CallerLineNumber ] int line = 0 )
34+ => Write ( LogLevel . Info , message , category , member , file , line ) ;
35+
36+ public void Success ( string message , string category = "" ,
37+ [ CallerMemberName ] string member = "" ,
38+ [ CallerFilePath ] string file = "" ,
39+ [ CallerLineNumber ] int line = 0 )
40+ => Write ( LogLevel . Success , message , category , member , file , line ) ;
41+
42+ public void Warning ( string message , string category = "" ,
43+ [ CallerMemberName ] string member = "" ,
44+ [ CallerFilePath ] string file = "" ,
45+ [ CallerLineNumber ] int line = 0 )
46+ => Write ( LogLevel . Warning , message , category , member , file , line ) ;
47+
48+ public void Error ( string message , string category = "" ,
49+ [ CallerMemberName ] string member = "" ,
50+ [ CallerFilePath ] string file = "" ,
51+ [ CallerLineNumber ] int line = 0 )
52+ => Write ( LogLevel . Error , message , category , member , file , line ) ;
53+
54+ public void Error ( Exception ex , string category = "" ,
55+ [ CallerMemberName ] string member = "" ,
56+ [ CallerFilePath ] string file = "" ,
57+ [ CallerLineNumber ] int line = 0 )
58+ => Error ( ex . ToString ( ) , category , member , file , line ) ;
59+
60+ /* ------------- private ------------- */
61+ private void Write ( LogLevel lvl , string msg , string cat ,
62+ string member , string file , int line )
63+ {
64+ bool allowed = lvl switch
65+ {
66+ LogLevel . Info => _opts . ShowInfo ,
67+ LogLevel . Success => _opts . ShowSuccess ,
68+ LogLevel . Warning => _opts . ShowWarning ,
69+ LogLevel . Error => _opts . ShowError ,
70+ _ => false
71+ } ;
72+
73+ if ( ! allowed ) return ;
74+
75+ var entry = new LogEntry (
76+ Timestamp : DateTimeOffset . UtcNow ,
77+ Level : lvl ,
78+ Message : msg ,
79+ Category : cat ,
80+ Member : member ,
81+ FilePath : Path . GetFileName ( file ) ,
82+ Line : line ) ;
83+
84+ _queue . Add ( entry ) ;
85+ }
86+
87+ private async Task WritePump ( )
88+ {
89+ await Task . Run ( ( ) =>
90+ {
91+ foreach ( var entry in _queue . GetConsumingEnumerable ( _cts . Token ) )
92+ {
93+ var line = Format ( entry ) ;
94+ if ( _opts . WriteToConsole ) WriteConsole ( line , entry . Level ) ;
95+ if ( _opts . WriteToFile ) WriteFileAsync ( line ) . GetAwaiter ( ) . GetResult ( ) ;
96+ }
97+ } , _cts . Token ) ;
98+ }
99+
100+ private static string Format ( LogEntry e ) =>
101+ $ "{ e . Timestamp : O} |{ e . Level , - 7 } |{ e . Category , - 15 } |{ e . Member } ({ e . FilePath } :{ e . Line } )|{ e . Message } ";
102+
103+ private static void WriteConsole ( string text , LogLevel lvl )
104+ {
105+ var color = lvl switch
106+ {
107+ LogLevel . Info => ConsoleColor . Cyan ,
108+ LogLevel . Success => ConsoleColor . Green ,
109+ LogLevel . Warning => ConsoleColor . Yellow ,
110+ LogLevel . Error => ConsoleColor . Red ,
111+ _ => ConsoleColor . Gray
112+ } ;
113+ lock ( Console . Out )
114+ {
115+ var prev = Console . ForegroundColor ;
116+ Console . ForegroundColor = color ;
117+ Console . WriteLine ( text ) ;
118+ Console . ForegroundColor = prev ;
119+ }
120+ }
121+
122+ private async ValueTask WriteFileAsync ( string line )
123+ {
124+ await _fileGate . WaitAsync ( ) ;
125+ try
126+ {
127+ var now = DateTime . UtcNow ;
128+ if ( now . Date != _todayDate )
129+ {
130+ _todayWriter ? . Dispose ( ) ;
131+ OpenTodayFile ( ) ;
132+ }
133+ await _todayWriter ! . WriteLineAsync ( line ) ;
134+ await _todayWriter . FlushAsync ( ) ;
135+ }
136+ finally { _fileGate . Release ( ) ; }
137+ }
138+
139+ private void OpenTodayFile ( )
140+ {
141+ _todayDate = DateTime . UtcNow . Date ;
142+ var path = Path . Combine ( _opts . Folder , $ "trion-{ _todayDate : yyyyMMdd} .log") ;
143+ _todayWriter = new StreamWriter ( path , append : true , Encoding . UTF8 ) { AutoFlush = false } ;
144+ }
145+
146+ private async Task HouseKeepingLoop ( )
147+ {
148+ while ( ! _cts . Token . IsCancellationRequested )
149+ {
150+ try
151+ {
152+ await Task . Delay ( TimeSpan . FromHours ( 6 ) , _cts . Token ) ;
153+ HouseKeeping ( ) ;
154+ }
155+ catch { /* best effort */ }
156+ }
157+ }
158+
159+ private void HouseKeeping ( )
160+ {
161+ var now = DateTime . UtcNow ;
162+ var compressDate = now . AddDays ( - _opts . DaysToCompress ) ;
163+ var deleteDate = now . AddDays ( - _opts . DaysToKeep ) ;
164+
165+ foreach ( var f in Directory . GetFiles ( _opts . Folder , "trion-*.log" ) )
166+ {
167+ var fi = new FileInfo ( f ) ;
168+ if ( fi . LastWriteTimeUtc < deleteDate )
169+ {
170+ fi . Delete ( ) ;
171+ }
172+ else if ( fi . LastWriteTimeUtc < compressDate &&
173+ ! f . EndsWith ( ".gz" , StringComparison . OrdinalIgnoreCase ) )
174+ {
175+ var gz = f + ".gz" ;
176+ if ( ! File . Exists ( gz ) )
177+ {
178+ using var original = fi . OpenRead ( ) ;
179+ using var compressed = File . Create ( gz ) ;
180+ using var gzip = new GZipStream ( compressed , CompressionMode . Compress ) ;
181+ original . CopyTo ( gzip ) ;
182+ }
183+ fi . Delete ( ) ;
184+ }
185+ }
186+ }
187+
188+ public async ValueTask DisposeAsync ( )
189+ {
190+ _queue . CompleteAdding ( ) ;
191+ _cts . Cancel ( ) ;
192+ await _writerTask ;
193+ _cts . Dispose ( ) ;
194+ _queue . Dispose ( ) ;
195+ await _fileGate . WaitAsync ( ) ;
196+ _todayWriter ? . Dispose ( ) ;
197+ _fileGate . Dispose ( ) ;
198+ }
199+ }
0 commit comments