-
Notifications
You must be signed in to change notification settings - Fork 1
feat: Logger service (MAPCO-8870) #30
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 16 commits
5996c7d
3417968
cf87f3f
5dd91a3
778d0ed
8a7ee5e
5844c82
c5c4f9a
00b97cb
43b46ab
8d8f712
4228218
e1388f6
247378c
e3e6a36
ac92cbe
f33aa65
626003d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| { | ||
| "Enabled": true, | ||
| "ConsoleEnabled": true, | ||
| "Log4NetConfigXml": "Logger/log4net.xml", | ||
| "MinLogLevel": "DEBUG", | ||
| "StackTraceRowLimit": 0 | ||
| } |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| <?xml version="1.0" encoding="utf-8"?> | ||
| <log4net> | ||
| <appender name="RollingInfoFileAppender" type="log4net.Appender.RollingFileAppender"> | ||
| <file type="log4net.Util.PatternString" value="%property{LogFilePath}info.log"/> | ||
| <appendToFile value="true"/> | ||
| <maximumFileSize value="10MB"/> | ||
| <maxSizeRollBackups value="5"/> | ||
| <staticLogFileName value="false"/> | ||
| <preserveLogFileNameExtension value="true"/> | ||
| <rollingStyle value="Size"/> | ||
| <layout type="log4net.Layout.PatternLayout"> | ||
| <conversionPattern value="%date %-5level %logger - %message%newline"/> | ||
| </layout> | ||
| <threshold value="DEBUG"/> | ||
| </appender> | ||
| <appender name="RollingErrorFileAppender" type="log4net.Appender.RollingFileAppender"> | ||
| <file type="log4net.Util.PatternString" value="%property{LogFilePath}error.log"/> | ||
| <appendToFile value="true"/> | ||
| <maximumFileSize value="10MB"/> | ||
| <maxSizeRollBackups value="5"/> | ||
| <staticLogFileName value="false"/> | ||
| <preserveLogFileNameExtension value="true"/> | ||
| <rollingStyle value="Size"/> | ||
| <layout type="log4net.Layout.PatternLayout"> | ||
| <conversionPattern value="%date %-5level %logger - %message%newline"/> | ||
| </layout> | ||
| <threshold value="WARN"/> | ||
| </appender> | ||
|
|
||
| <appender name="HttpAppender" | ||
| type="com.mapcolonies.core.Services.LoggerService.CustomAppenders.HttpAppender, com.mapcolonies.core"> | ||
| <EndpointUrl value="https://TODO/logs"/> | ||
| <threshold value="WARN"/> | ||
| <MaxPayloadSize value="5242880"/> | ||
| <TimeThresholdMs value="30000"/> | ||
| <PersistenceDirectory value="C:\Yahalom\logs\offline"/> | ||
geneg marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| </appender> | ||
|
|
||
| <root> | ||
| <level value="ALL"/> | ||
| <appender-ref ref="RollingInfoFileAppender"/> | ||
| <appender-ref ref="RollingErrorFileAppender"/> | ||
| <appender-ref ref="HttpAppender"/> | ||
| </root> | ||
| </log4net> | ||
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| namespace com.mapcolonies.core.Services.LoggerService | ||
| { | ||
| [System.Serializable] | ||
| public class Config | ||
| { | ||
| public string Log4NetConfigXml; | ||
| public int StackTraceRowLimit; | ||
| public bool Enabled; | ||
| public bool ConsoleEnabled; | ||
| public string MinLogLevel; | ||
| } | ||
| } |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,53 @@ | ||
| using System; | ||
| using log4net.Appender; | ||
| using log4net.Core; | ||
| using UnityEngine; | ||
| using IUnityLogHandler = UnityEngine.ILogHandler; | ||
| using Object = UnityEngine.Object; | ||
|
|
||
| namespace com.mapcolonies.core.Services.LoggerService.CustomAppenders | ||
| { | ||
| public class ConsoleAppender : AppenderSkeleton | ||
| { | ||
| public const string UnityContext = "unity:context"; | ||
| private readonly IUnityLogHandler _handler; | ||
| private static readonly int ErrorLevel = Level.Error.Value; | ||
| private static readonly int WarningLevel = Level.Warn.Value; | ||
|
|
||
| public ConsoleAppender(IUnityLogHandler handler) | ||
| { | ||
| _handler = handler; | ||
| } | ||
|
|
||
| protected override void Append(LoggingEvent loggingEvent) | ||
| { | ||
| if (_handler == null) return; | ||
|
|
||
| Level level = loggingEvent.Level; | ||
| if (level == null) return; | ||
|
|
||
| string message; | ||
| try | ||
| { | ||
| message = RenderLoggingEvent(loggingEvent); | ||
| } | ||
| catch (Exception e) | ||
| { | ||
| _handler.LogFormat(LogType.Exception, null, "{0}", e); | ||
| return; | ||
| } | ||
|
|
||
| Object ctx = loggingEvent.LookupProperty(UnityContext) as Object; | ||
|
|
||
| LogType logType = level.Value switch | ||
| { | ||
| int value when value < WarningLevel => LogType.Log, | ||
| int value when value >= WarningLevel && value < ErrorLevel => LogType.Warning, | ||
| int value when value >= ErrorLevel => LogType.Error, | ||
| _ => LogType.Log | ||
| }; | ||
|
|
||
| _handler.LogFormat(logType, ctx, "{0}", message); | ||
| } | ||
| } | ||
| } |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,224 @@ | ||
| using System; | ||
| using System.Collections.Concurrent; | ||
| using System.Collections.Generic; | ||
| using System.IO; | ||
| using System.Net.Http; | ||
| using System.Text; | ||
| using System.Threading; | ||
| using System.Threading.Tasks; | ||
| using log4net.Appender; | ||
| using log4net.Core; | ||
|
|
||
| namespace com.mapcolonies.core.Services.LoggerService.CustomAppenders | ||
| { | ||
| public class HttpAppender : AppenderSkeleton, IDisposable | ||
| { | ||
| private static readonly HttpClient _httpClient = new HttpClient(); | ||
| private readonly ConcurrentQueue<LoggingEvent> _queue = new ConcurrentQueue<LoggingEvent>(); | ||
| private Timer _flushTimer; | ||
| private volatile bool _isSending; | ||
|
|
||
| private long _currentPayloadSize; | ||
|
|
||
| private const string OfflineFilePrefix = "offline_"; | ||
| private const string OfflineFileExtension = ".log"; | ||
|
|
||
| public string EndpointUrl | ||
| { | ||
| get; | ||
| set; | ||
| } | ||
|
|
||
| public int MaxPayloadSize | ||
| { | ||
| get; | ||
| set; | ||
| } = 5 * 1024 * 1024; | ||
asafMasa marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| public int TimeThresholdMs | ||
| { | ||
| get; | ||
| set; | ||
| } = 30 * 1000; | ||
asafMasa marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| public string PersistenceDirectory | ||
| { | ||
| get; | ||
| set; | ||
| } | ||
|
|
||
| public HttpAppender() | ||
| { | ||
| _flushTimer = new Timer(OnTimerFlush, null, TimeThresholdMs, TimeThresholdMs); | ||
| } | ||
|
|
||
| protected override void Append(LoggingEvent loggingEvent) | ||
| { | ||
| string renderedMessage = RenderLoggingEvent(loggingEvent); | ||
| long messageSize = Encoding.UTF8.GetByteCount(renderedMessage); | ||
|
|
||
| loggingEvent.Fix = FixFlags.All; | ||
| _queue.Enqueue(loggingEvent); | ||
| Interlocked.Add(ref _currentPayloadSize, messageSize); | ||
|
|
||
| if (Interlocked.Read(ref _currentPayloadSize) >= MaxPayloadSize) | ||
| { | ||
| SendBatch(); | ||
| } | ||
| } | ||
|
|
||
| private void OnTimerFlush(object state) | ||
| { | ||
| if (!_queue.IsEmpty) | ||
| { | ||
| SendBatch(); | ||
| } | ||
| } | ||
|
|
||
| private async void SendBatch() | ||
| { | ||
| if (_isSending) return; | ||
|
|
||
| string batchContent = null; | ||
|
|
||
| try | ||
| { | ||
| _isSending = true; | ||
|
|
||
| _flushTimer?.Change(Timeout.Infinite, Timeout.Infinite); | ||
|
|
||
| if (_queue.IsEmpty) return; | ||
|
|
||
| List<string> logMessages = new List<string>(); | ||
|
|
||
| while (_queue.TryDequeue(out LoggingEvent loggingEvent)) | ||
| { | ||
| logMessages.Add(RenderLoggingEvent(loggingEvent)); | ||
| } | ||
|
|
||
| Interlocked.Exchange(ref _currentPayloadSize, 0); | ||
|
|
||
| if (logMessages.Count == 0) return; | ||
|
|
||
| batchContent = string.Join("\n", logMessages); | ||
| StringContent content = new StringContent(batchContent, Encoding.UTF8, "application/x-ndjson"); | ||
|
|
||
| var response = await _httpClient.PostAsync(EndpointUrl, content); | ||
|
|
||
| if (response.IsSuccessStatusCode) | ||
| { | ||
| await SendPersistedBatchesAsync(); | ||
| } | ||
| else | ||
| { | ||
| PersistBatchSafely(batchContent); | ||
| } | ||
| } | ||
| catch (Exception ex) | ||
| { | ||
| ErrorHandler.Error("Error occurred in HttpLogAppender.", ex); | ||
|
|
||
| if (!string.IsNullOrEmpty(batchContent)) | ||
| { | ||
| PersistBatchSafely(batchContent); | ||
| } | ||
| } | ||
| finally | ||
| { | ||
| _isSending = false; | ||
| _flushTimer?.Change(TimeThresholdMs, TimeThresholdMs); | ||
| } | ||
| } | ||
|
|
||
| private void PersistBatchSafely(string batchContent) | ||
| { | ||
| try | ||
| { | ||
| if (string.IsNullOrWhiteSpace(PersistenceDirectory)) | ||
| { | ||
| return; | ||
| } | ||
|
|
||
| Directory.CreateDirectory(PersistenceDirectory); | ||
|
|
||
| string fileName = Path.Combine( | ||
| PersistenceDirectory, | ||
| $"{OfflineFilePrefix}{DateTime.UtcNow:yyyyMMdd_HHmmss_fff}{OfflineFileExtension}" | ||
| ); | ||
|
|
||
| File.WriteAllText(fileName, batchContent, Encoding.UTF8); | ||
| } | ||
| catch (Exception fileEx) | ||
| { | ||
| ErrorHandler.Error("Failed to persist offline log batch.", fileEx); | ||
| } | ||
| } | ||
|
|
||
| private async Task SendPersistedBatchesAsync() | ||
| { | ||
| try | ||
| { | ||
| if (string.IsNullOrWhiteSpace(PersistenceDirectory)) | ||
| { | ||
| return; | ||
| } | ||
|
|
||
| if (!Directory.Exists(PersistenceDirectory)) | ||
| { | ||
| return; | ||
| } | ||
|
|
||
| string[] files = Directory.GetFiles( | ||
| PersistenceDirectory, | ||
| $"{OfflineFilePrefix}*{OfflineFileExtension}" | ||
| ); | ||
|
|
||
| foreach (string file in files) | ||
| { | ||
| string content = File.ReadAllText(file, Encoding.UTF8); | ||
|
|
||
| try | ||
| { | ||
| var response = await _httpClient.PostAsync( | ||
| EndpointUrl, | ||
| new StringContent(content, Encoding.UTF8, "application/x-ndjson") | ||
| ); | ||
|
|
||
| if (response.IsSuccessStatusCode) | ||
| { | ||
| File.Delete(file); | ||
| } | ||
| else | ||
| { | ||
| break; | ||
| } | ||
| } | ||
| catch (Exception ex) | ||
| { | ||
| ErrorHandler.Error($"Failed to send persisted log batch: {file}", ex); | ||
| break; | ||
| } | ||
| } | ||
| } | ||
| catch (Exception ex) | ||
| { | ||
| ErrorHandler.Error("Error while sending persisted log batches.", ex); | ||
| } | ||
| } | ||
|
|
||
| protected override void OnClose() | ||
| { | ||
| _flushTimer?.Dispose(); | ||
| _flushTimer = null; | ||
|
|
||
| SendBatch(); | ||
|
|
||
| base.OnClose(); | ||
| } | ||
|
|
||
| public void Dispose() | ||
| { | ||
| _flushTimer?.Dispose(); | ||
| } | ||
| } | ||
| } | ||
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Uh oh!
There was an error while loading. Please reload this page.