Skip to content
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions Assets/StreamingAssets/Logger.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions Assets/StreamingAssets/Logger/LoggerConfig.json
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
}
7 changes: 7 additions & 0 deletions Assets/StreamingAssets/Logger/LoggerConfig.json.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

45 changes: 45 additions & 0 deletions Assets/StreamingAssets/Logger/log4net.xml
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"/>
</appender>

<root>
<level value="ALL"/>
<appender-ref ref="RollingInfoFileAppender"/>
<appender-ref ref="RollingErrorFileAppender"/>
<appender-ref ref="HttpAppender"/>
</root>
</log4net>
7 changes: 7 additions & 0 deletions Assets/StreamingAssets/Logger/log4net.xml.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions Assets/com.mapcolonies.core/Services/LoggerService.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 12 additions & 0 deletions Assets/com.mapcolonies.core/Services/LoggerService/Config.cs
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;

public int TimeThresholdMs
{
get;
set;
} = 30 * 1000;

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.

Loading
Loading