diff --git a/EonaCat.Logger/EonaCat.Logger.csproj b/EonaCat.Logger/EonaCat.Logger.csproj index ed3d15e..e22a245 100644 --- a/EonaCat.Logger/EonaCat.Logger.csproj +++ b/EonaCat.Logger/EonaCat.Logger.csproj @@ -3,7 +3,7 @@ .netstandard2.1; net6.0; net7.0; net8.0; net4.8; icon.ico latest - 1.4.5 + 1.4.6 EonaCat (Jeroen Saey) true EonaCat (Jeroen Saey) @@ -24,7 +24,7 @@ - 1.4.5+{chash:10}.{c:ymd} + 1.4.6+{chash:10}.{c:ymd} true true v[0-9]* @@ -51,15 +51,16 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive all - - + + + diff --git a/EonaCat.Logger/EonaCatCoreLogger/ElasticSearchLogger.cs b/EonaCat.Logger/EonaCatCoreLogger/ElasticSearchLogger.cs new file mode 100644 index 0000000..5ba6d1b --- /dev/null +++ b/EonaCat.Logger/EonaCatCoreLogger/ElasticSearchLogger.cs @@ -0,0 +1,182 @@ +using System.Linq; + +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +namespace EonaCat.Logger.EonaCatCoreLogger +{ + using Microsoft.Extensions.Logging; + using System; + using System.Collections.Generic; + using System.Net.Http; + using System.Text; + using System.Text.Json; + using System.Threading.Tasks; + + public class ElasticSearchLogger : ILogger + { + private readonly string _categoryName; + private readonly ElasticSearchLoggerOptions _options; + private static readonly HttpClient _httpClient = new HttpClient(); + private static readonly List _buffer = new List(); + private static readonly object _lock = new object(); + private static bool _flushLoopStarted = false; + public event EventHandler OnException; + public event EventHandler OnInvalidStatusCode; + private readonly LoggerScopedContext _context = new(); + public bool IncludeCorrelationId { get; set; } + + public ElasticSearchLogger(string categoryName, ElasticSearchLoggerOptions options) + { + _categoryName = categoryName; + _options = options; + IncludeCorrelationId = options.IncludeCorrelationId; + + if (!_flushLoopStarted) + { + _flushLoopStarted = true; + Task.Run(FlushLoopAsync); + } + } + + public void SetContext(string key, string value) => _context.Set(key, value); + public void ClearContext() => _context.Clear(); + public string GetContext(string key) => _context.Get(key); + + public IDisposable BeginScope(TState state) => null; + public bool IsEnabled(LogLevel logLevel) => _options.IsEnabled; + + public void Log(LogLevel logLevel, EventId eventId, TState state, + Exception exception, Func formatter) + { + if (!_options.IsEnabled || formatter == null) + { + return; + } + + // Get correlation ID from context or generate new one + if (IncludeCorrelationId) + { + var correlationId = _context.Get("CorrelationId") ?? Guid.NewGuid().ToString(); + _context.Set("CorrelationId", correlationId); + } + + // Prepare the log entry + var logDoc = new + { + timestamp = DateTime.UtcNow, + level = logLevel.ToString(), + category = _categoryName, + message = formatter(state, exception), + exception = exception?.ToString(), + eventId = eventId.Id, + customContext = _context.GetAll() + }; + + // Serialize log to JSON + string json = JsonSerializer.Serialize(logDoc); + + // Add to the buffer + lock (_lock) + { + _buffer.Add(json); + if (_buffer.Count >= _options.RetryBufferSize) + { + _ = FlushBufferAsync(); + } + } + } + + private async Task FlushLoopAsync() + { + while (true) + { + await Task.Delay(TimeSpan.FromSeconds(_options.FlushIntervalSeconds)); + await FlushBufferAsync(); + } + } + + private async Task FlushBufferAsync() + { + List toSend; + lock (_lock) + { + if (_buffer.Count == 0) return; + toSend = new List(_buffer); + _buffer.Clear(); + } + + // Elasticsearch URL with dynamic index + string indexName = $"{_options.IndexName}-{DateTime.UtcNow:yyyy.MM.dd}"; + string url = $"{_options.Uri.TrimEnd('/')}/{(_options.UseBulkInsert ? "_bulk" : indexName + "/_doc")}"; + + var request = new HttpRequestMessage(HttpMethod.Post, url); + request.Headers.Accept.ParseAdd("application/json"); + + // Add authentication headers if configured + if (!string.IsNullOrWhiteSpace(_options.Username)) + { + var authToken = Convert.ToBase64String(Encoding.UTF8.GetBytes($"{_options.Username}:{_options.Password}")); + request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Basic", authToken); + } + + // Add custom headers (static ones) + foreach (var header in _options.CustomHeaders) + { + request.Headers.TryAddWithoutValidation(header.Key, header.Value); + } + + // Add template headers (dynamic ones based on log data) + var dynamicHeaders = new Dictionary + { + { "index", _options.IndexName }, + { "date", DateTime.UtcNow.ToString("yyyy-MM-dd") }, + { "timestamp", DateTime.UtcNow.ToString("o") }, + }; + + foreach (var header in _options.TemplateHeaders) + { + var value = ReplaceTemplate(header.Value, dynamicHeaders); + request.Headers.TryAddWithoutValidation(header.Key, value); + } + + // Add context headers (correlationId, custom context) + foreach (var kv in _context.GetAll()) + { + request.Headers.TryAddWithoutValidation($"X-Context-{kv.Key}", kv.Value); + } + + // Prepare the content for the request + request.Content = new StringContent( + _options.UseBulkInsert + ? string.Join("\n", toSend.Select(d => $"{{\"index\":{{}}}}\n{d}")) + "\n" + : string.Join("\n", toSend), + Encoding.UTF8, + "application/json" + ); + + try + { + var response = await _httpClient.SendAsync(request); + if (!response.IsSuccessStatusCode) + { + var errorContent = await response.Content.ReadAsStringAsync(); + OnInvalidStatusCode?.Invoke(this, $"ElasticSearch request failed: {response.StatusCode}, {errorContent}"); + } + } + catch (Exception exception) + { + OnException?.Invoke(this, exception); + } + } + + private static string ReplaceTemplate(string template, Dictionary values) + { + foreach (var kv in values) + { + template = template.Replace($"{{{kv.Key}}}", kv.Value); + } + return template; + } + } +} diff --git a/EonaCat.Logger/EonaCatCoreLogger/ElasticSearchLoggerOptions.cs b/EonaCat.Logger/EonaCatCoreLogger/ElasticSearchLoggerOptions.cs new file mode 100644 index 0000000..7f35120 --- /dev/null +++ b/EonaCat.Logger/EonaCatCoreLogger/ElasticSearchLoggerOptions.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Text; + +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +namespace EonaCat.Logger.EonaCatCoreLogger +{ + public class ElasticSearchLoggerOptions + { + public string Uri { get; set; } = "http://localhost:9200"; + public string IndexName { get; set; } = "eonacat-logs"; + public bool IsEnabled { get; set; } = true; + public string Username { get; set; } + public string Password { get; set; } + public Dictionary CustomHeaders { get; set; } = new(); + public Dictionary TemplateHeaders { get; set; } = new(); + public int RetryBufferSize { get; set; } = 100; + public int FlushIntervalSeconds { get; set; } = 5; + public bool UseBulkInsert { get; set; } = true; + public bool IncludeCorrelationId { get; set; } = true; + } +} diff --git a/EonaCat.Logger/EonaCatCoreLogger/ElasticSearchLoggerProvider.cs b/EonaCat.Logger/EonaCatCoreLogger/ElasticSearchLoggerProvider.cs new file mode 100644 index 0000000..0798f91 --- /dev/null +++ b/EonaCat.Logger/EonaCatCoreLogger/ElasticSearchLoggerProvider.cs @@ -0,0 +1,32 @@ +using Microsoft.Extensions.Logging; +using System.Collections.Generic; + +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +namespace EonaCat.Logger.EonaCatCoreLogger +{ + public class ElasticSearchLoggerProvider : ILoggerProvider + { + private readonly ElasticSearchLoggerOptions _options; + + public ElasticSearchLoggerProvider(ElasticSearchLoggerOptions options = null) + { + if (options == null) + { + options = new ElasticSearchLoggerOptions(); + } + _options = options; + } + + public ILogger CreateLogger(string categoryName) + { + return new ElasticSearchLogger(categoryName, _options); + } + + public void Dispose() + { + // No unmanaged resources, nothing to dispose here + } + } +} \ No newline at end of file diff --git a/EonaCat.Logger/EonaCatCoreLogger/Extensions/ConsoleLoggerFactoryExtensions.cs b/EonaCat.Logger/EonaCatCoreLogger/Extensions/ConsoleLoggerFactoryExtensions.cs new file mode 100644 index 0000000..81a5866 --- /dev/null +++ b/EonaCat.Logger/EonaCatCoreLogger/Extensions/ConsoleLoggerFactoryExtensions.cs @@ -0,0 +1,34 @@ +using System; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Console; + +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +namespace EonaCat.Logger.EonaCatCoreLogger.Extensions +{ + /// + /// Extensions for adding the built-in Console Logger to the + /// + public static class ConsoleLoggerFactoryExtensions + { + /// + /// Adds the built-in Console Logger to the logging builder. + /// + /// The to use. + /// Optional configuration for + public static ILoggingBuilder AddEonaCatConsoleLogger(this ILoggingBuilder builder, Action configure = null) + { + if (configure != null) + { + builder.AddConsole(configure); + } + else + { + builder.AddConsole(); + } + + return builder; + } + } +} diff --git a/EonaCat.Logger/EonaCatCoreLogger/Extensions/ElasticSearchLoggerFactoryExtensions.cs b/EonaCat.Logger/EonaCatCoreLogger/Extensions/ElasticSearchLoggerFactoryExtensions.cs new file mode 100644 index 0000000..90d2094 --- /dev/null +++ b/EonaCat.Logger/EonaCatCoreLogger/Extensions/ElasticSearchLoggerFactoryExtensions.cs @@ -0,0 +1,20 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using System; + +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +namespace EonaCat.Logger.EonaCatCoreLogger.Extensions +{ + public static class ElasticSearchLoggerFactoryExtensions + { + public static ILoggingBuilder AddEonaCatElasticSearchLogger(this ILoggingBuilder builder, Action configure) + { + var options = new ElasticSearchLoggerOptions(); + configure?.Invoke(options); + builder.Services.AddSingleton(new ElasticSearchLoggerProvider(options)); + return builder; + } + } +} diff --git a/EonaCat.Logger/EonaCatCoreLogger/Extensions/HttpLoggerFactoryExtensions.cs b/EonaCat.Logger/EonaCatCoreLogger/Extensions/HttpLoggerFactoryExtensions.cs new file mode 100644 index 0000000..d5929ca --- /dev/null +++ b/EonaCat.Logger/EonaCatCoreLogger/Extensions/HttpLoggerFactoryExtensions.cs @@ -0,0 +1,21 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using System; + +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +namespace EonaCat.Logger.EonaCatCoreLogger.Extensions +{ + public static class HttpLoggerFactoryExtensions + { + public static ILoggingBuilder AddEonaCatHttpLogger(this ILoggingBuilder builder, Action configure) + { + var options = new HttpLoggerOptions(); + configure?.Invoke(options); + builder.Services.AddSingleton(new HttpLoggerProvider(options)); + return builder; + } + } + +} diff --git a/EonaCat.Logger/EonaCatCoreLogger/Extensions/JsonFIleLoggerFactoryExtensions.cs b/EonaCat.Logger/EonaCatCoreLogger/Extensions/JsonFIleLoggerFactoryExtensions.cs new file mode 100644 index 0000000..4c0d9b6 --- /dev/null +++ b/EonaCat.Logger/EonaCatCoreLogger/Extensions/JsonFIleLoggerFactoryExtensions.cs @@ -0,0 +1,21 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using System; + +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +namespace EonaCat.Logger.EonaCatCoreLogger.Extensions +{ + public static class JsonFileLoggerFactoryExtensions + { + public static ILoggingBuilder AddEonaCatJsonFileLogger(this ILoggingBuilder builder, Action configure) + { + var options = new JsonFileLoggerOptions(); + configure?.Invoke(options); + + builder.Services.AddSingleton(new JsonFileLoggerProvider(options)); + return builder; + } + } +} diff --git a/EonaCat.Logger/EonaCatCoreLogger/Extensions/TcpLoggerFactoryExtensions.cs b/EonaCat.Logger/EonaCatCoreLogger/Extensions/TcpLoggerFactoryExtensions.cs new file mode 100644 index 0000000..172856e --- /dev/null +++ b/EonaCat.Logger/EonaCatCoreLogger/Extensions/TcpLoggerFactoryExtensions.cs @@ -0,0 +1,22 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using System; + +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +namespace EonaCat.Logger.EonaCatCoreLogger.Extensions +{ + public static class TcpLoggerFactoryExtensions + { + public static ILoggingBuilder AddEonaCatTcpLogger(this ILoggingBuilder builder, Action configure) + { + var options = new TcpLoggerOptions(); + configure?.Invoke(options); + + builder.Services.AddSingleton(new TcpLoggerProvider(options)); + return builder; + } + } + +} diff --git a/EonaCat.Logger/EonaCatCoreLogger/Extensions/UdpLoggerFactoryExtensions.cs b/EonaCat.Logger/EonaCatCoreLogger/Extensions/UdpLoggerFactoryExtensions.cs new file mode 100644 index 0000000..4bd783d --- /dev/null +++ b/EonaCat.Logger/EonaCatCoreLogger/Extensions/UdpLoggerFactoryExtensions.cs @@ -0,0 +1,21 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using System; + +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +namespace EonaCat.Logger.EonaCatCoreLogger.Extensions +{ + public static class UdpLoggerFactoryExtensions + { + public static ILoggingBuilder AddEonaCatUdpLogger(this ILoggingBuilder builder, Action configure) + { + var options = new UdpLoggerOptions(); + configure?.Invoke(options); + + builder.Services.AddSingleton(new UdpLoggerProvider(options)); + return builder; + } + } +} diff --git a/EonaCat.Logger/EonaCatCoreLogger/FileLoggerOptions.cs b/EonaCat.Logger/EonaCatCoreLogger/FileLoggerOptions.cs index a156261..04bac3c 100644 --- a/EonaCat.Logger/EonaCatCoreLogger/FileLoggerOptions.cs +++ b/EonaCat.Logger/EonaCatCoreLogger/FileLoggerOptions.cs @@ -139,4 +139,9 @@ public class FileLoggerOptions : BatchingLoggerOptions /// Custom keywords specified in LoggerSettings /// public bool UseDefaultMasking { get; set; } = true; + + /// + /// Determines if we need to include the correlation ID in the log + /// + public bool IncludeCorrelationId { get; set; } = true; } \ No newline at end of file diff --git a/EonaCat.Logger/EonaCatCoreLogger/FileLoggerProvider.cs b/EonaCat.Logger/EonaCatCoreLogger/FileLoggerProvider.cs index cfa66a7..225e2f2 100644 --- a/EonaCat.Logger/EonaCatCoreLogger/FileLoggerProvider.cs +++ b/EonaCat.Logger/EonaCatCoreLogger/FileLoggerProvider.cs @@ -30,6 +30,7 @@ public class FileLoggerProvider : BatchingLoggerProvider private bool _rollingOver; private int _rollOverCount; private ConcurrentDictionary _buffer = new ConcurrentDictionary(); + private readonly LoggerScopedContext _context = new(); public event EventHandler OnError; @@ -46,6 +47,7 @@ public class FileLoggerProvider : BatchingLoggerProvider _maxRetainedFiles = loggerOptions.RetainedFileCountLimit; _maxRolloverFiles = loggerOptions.MaxRolloverFiles; _maxTries = loggerOptions.MaxWriteTries; + IncludeCorrelationId = loggerOptions.IncludeCorrelationId; } /// @@ -72,6 +74,10 @@ public class FileLoggerProvider : BatchingLoggerProvider } } + public void SetContext(string key, string value) => _context.Set(key, value); + public void ClearContext() => _context.Clear(); + public string GetContext(string key) => _context.Get(key); + /// protected override async Task WriteMessagesAsync(IEnumerable messages, CancellationToken cancellationToken) @@ -90,7 +96,22 @@ public class FileLoggerProvider : BatchingLoggerProvider foreach (var group in messages.GroupBy(GetGrouping)) { LogFile = GetFullName(group.Key); - var currentMessages = string.Join(string.Empty, group.Select(item => item.Message)); + + var currentMessages = string.Join(string.Empty, group.Select(item => + { + if (IncludeCorrelationId) + { + var correlationId = _context.Get("CorrelationId") ?? Guid.NewGuid().ToString(); + _context.Set("CorrelationId", correlationId); + } + + var contextData = _context.GetAll(); + var contextInfo = contextData.Count > 0 + ? string.Join(" ", contextData.Select(kvp => $"{kvp.Key}={kvp.Value}")) + : string.Empty; + return $"[Timestamp: {item.Timestamp:u}] {item.Message} {contextInfo}{Environment.NewLine}"; + })); + if (!_buffer.TryAdd(LogFile, currentMessages)) { _buffer[LogFile] += currentMessages; @@ -128,6 +149,7 @@ public class FileLoggerProvider : BatchingLoggerProvider } public bool IsWriting { get; set; } + public bool IncludeCorrelationId { get; set; } private async Task TryWriteToFileAsync(CancellationToken cancellationToken) { diff --git a/EonaCat.Logger/EonaCatCoreLogger/HttpLogger.cs b/EonaCat.Logger/EonaCatCoreLogger/HttpLogger.cs new file mode 100644 index 0000000..27f84f0 --- /dev/null +++ b/EonaCat.Logger/EonaCatCoreLogger/HttpLogger.cs @@ -0,0 +1,132 @@ +using EonaCat.Json; +using Microsoft.Extensions.Logging; +using System; +using System.Net.Http; +using System.Text; +using System.Collections.Generic; + +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +namespace EonaCat.Logger.EonaCatCoreLogger +{ + public class HttpLogger : ILogger + { + private readonly string _categoryName; + private readonly HttpLoggerOptions _options; + private readonly LoggerScopedContext _context = new(); + + public bool IncludeCorrelationId { get; set; } + + private static readonly HttpClient _client = new(); + public event EventHandler OnException; + public event EventHandler OnInvalidStatusCode; + + public HttpLogger(string categoryName, HttpLoggerOptions options) + { + _categoryName = categoryName; + _options = options; + IncludeCorrelationId = options.IncludeCorrelationId; + } + + public void SetContext(string key, string value) => _context.Set(key, value); + public void ClearContext() => _context.Clear(); + public string GetContext(string key) => _context.Get(key); + + public IDisposable BeginScope(TState state) => null; + public bool IsEnabled(LogLevel logLevel) => _options.IsEnabled; + + public void Log(LogLevel logLevel, EventId eventId, TState state, + Exception exception, Func formatter) + { + if (!IsEnabled(logLevel)) + { + return; + } + + string message = formatter(state, exception); + + if (IncludeCorrelationId) + { + var correlationId = _context.Get("CorrelationId") ?? Guid.NewGuid().ToString(); + _context.Set("CorrelationId", correlationId); + } + + var payload = new Dictionary + { + { "timestamp", DateTime.UtcNow }, + { "level", logLevel.ToString() }, + { "category", _categoryName }, + { "message", message }, + { "eventId", eventId.Id } + }; + + // Add full log context as a nested dictionary + var contextData = _context.GetAll(); + if (contextData.Count > 0) + { + payload["context"] = contextData; + } + + try + { + var content = _options.SendAsJson + ? new StringContent(JsonHelper.ToJson(payload), Encoding.UTF8, "application/json") + : new StringContent(message); + + var result = _client.PostAsync(_options.Endpoint, content); + if (result.Result.IsSuccessStatusCode) + { + return; + } + + // Handle non-success status codes + var statusCode = result.Result.StatusCode; + var statusCodeMessage = statusCode switch + { + System.Net.HttpStatusCode.BadRequest => "400 Bad Request", + System.Net.HttpStatusCode.Unauthorized => "401 Unauthorized", + System.Net.HttpStatusCode.Forbidden => "403 Forbidden", + System.Net.HttpStatusCode.NotFound => "404 Not Found", + System.Net.HttpStatusCode.MethodNotAllowed => "405 Method Not Allowed", + System.Net.HttpStatusCode.NotAcceptable => "406 Not Acceptable", + System.Net.HttpStatusCode.ProxyAuthenticationRequired => "407 Proxy Authentication Required", + System.Net.HttpStatusCode.RequestTimeout => "408 Request Timeout", + System.Net.HttpStatusCode.Conflict => "409 Conflict", + System.Net.HttpStatusCode.Gone => "410 Gone", + System.Net.HttpStatusCode.LengthRequired => "411 Length Required", + System.Net.HttpStatusCode.PreconditionFailed => "412 Precondition Failed", + System.Net.HttpStatusCode.RequestEntityTooLarge => "413 Request Entity Too Large", + System.Net.HttpStatusCode.RequestUriTooLong => "414 Request URI Too Long", + System.Net.HttpStatusCode.UnsupportedMediaType => "415 Unsupported Media Type", + System.Net.HttpStatusCode.RequestedRangeNotSatisfiable => "416 Requested Range Not Satisfiable", + System.Net.HttpStatusCode.ExpectationFailed => "417 Expectation Failed", + (System.Net.HttpStatusCode)418 => "418 I'm a teapot", + (System.Net.HttpStatusCode)421 => "421 Misdirected Request", + (System.Net.HttpStatusCode)422 => "422 Unprocessable Entity", + (System.Net.HttpStatusCode)423 => "423 Locked", + (System.Net.HttpStatusCode)424 => "424 Failed Dependency", + (System.Net.HttpStatusCode)425 => "425 Too Early", + (System.Net.HttpStatusCode)426 => "426 Upgrade Required", + (System.Net.HttpStatusCode)428 => "428 Precondition Required", + (System.Net.HttpStatusCode)429 => "429 Too Many Requests", + (System.Net.HttpStatusCode)431 => "431 Request Header Fields Too Large", + (System.Net.HttpStatusCode)451 => "451 Unavailable For Legal Reasons", + System.Net.HttpStatusCode.InternalServerError => "500 Internal Server Error", + System.Net.HttpStatusCode.NotImplemented => "501 Not Implemented", + System.Net.HttpStatusCode.BadGateway => "502 Bad Gateway", + System.Net.HttpStatusCode.ServiceUnavailable => "503 Service Unavailable", + System.Net.HttpStatusCode.GatewayTimeout => "504 Gateway Timeout", + System.Net.HttpStatusCode.HttpVersionNotSupported => "505 HTTP Version Not Supported", + _ => statusCode.ToString() + }; + + OnInvalidStatusCode?.Invoke(this, statusCodeMessage); + } + catch (Exception e) + { + OnException?.Invoke(this, e); + } + } + } +} diff --git a/EonaCat.Logger/EonaCatCoreLogger/HttpLoggerOptions.cs b/EonaCat.Logger/EonaCatCoreLogger/HttpLoggerOptions.cs new file mode 100644 index 0000000..d505b00 --- /dev/null +++ b/EonaCat.Logger/EonaCatCoreLogger/HttpLoggerOptions.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Text; + +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +namespace EonaCat.Logger.EonaCatCoreLogger +{ + public class HttpLoggerOptions + { + public string Endpoint { get; set; } = "http://localhost:5000/logs"; + public bool IsEnabled { get; set; } = true; + public bool SendAsJson { get; set; } = true; + public bool IncludeCorrelationId { get; set; } = true; + } + +} diff --git a/EonaCat.Logger/EonaCatCoreLogger/HttpLoggerProvider.cs b/EonaCat.Logger/EonaCatCoreLogger/HttpLoggerProvider.cs new file mode 100644 index 0000000..18f1f9c --- /dev/null +++ b/EonaCat.Logger/EonaCatCoreLogger/HttpLoggerProvider.cs @@ -0,0 +1,31 @@ +using Microsoft.Extensions.Logging; + +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +namespace EonaCat.Logger.EonaCatCoreLogger +{ + public class HttpLoggerProvider : ILoggerProvider + { + private readonly HttpLoggerOptions _options; + + public HttpLoggerProvider(HttpLoggerOptions options = null) + { + if (options == null) + { + options = new HttpLoggerOptions(); + } + _options = options; + } + + public ILogger CreateLogger(string categoryName) + { + return new HttpLogger(categoryName, _options); + } + + public void Dispose() + { + // No unmanaged resources, nothing to dispose here + } + } +} diff --git a/EonaCat.Logger/EonaCatCoreLogger/JsonFIleLoggerProvider.cs b/EonaCat.Logger/EonaCatCoreLogger/JsonFIleLoggerProvider.cs new file mode 100644 index 0000000..5a2788f --- /dev/null +++ b/EonaCat.Logger/EonaCatCoreLogger/JsonFIleLoggerProvider.cs @@ -0,0 +1,35 @@ +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Text; + +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +namespace EonaCat.Logger.EonaCatCoreLogger +{ + public class JsonFileLoggerProvider : ILoggerProvider + { + private readonly JsonFileLoggerOptions _options; + + public JsonFileLoggerProvider(JsonFileLoggerOptions options = null) + { + if (options == null) + { + options = new JsonFileLoggerOptions(); + } + _options = options; + } + + public ILogger CreateLogger(string categoryName) + { + return new JsonFileLogger(categoryName, _options); + } + + public void Dispose() + { + + } + } + +} diff --git a/EonaCat.Logger/EonaCatCoreLogger/JsonFileLogger.cs b/EonaCat.Logger/EonaCatCoreLogger/JsonFileLogger.cs new file mode 100644 index 0000000..b39b3cf --- /dev/null +++ b/EonaCat.Logger/EonaCatCoreLogger/JsonFileLogger.cs @@ -0,0 +1,80 @@ +using EonaCat.Json; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; + +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +namespace EonaCat.Logger.EonaCatCoreLogger +{ + public class JsonFileLogger : ILogger + { + private readonly string _categoryName; + private readonly JsonFileLoggerOptions _options; + private readonly string _filePath; + private readonly LoggerScopedContext _context = new(); + public bool IncludeCorrelationId { get; set; } + public event EventHandler OnException; + + public JsonFileLogger(string categoryName, JsonFileLoggerOptions options) + { + _categoryName = categoryName; + _options = options; + _filePath = Path.Combine(_options.LogDirectory, _options.FileName); + Directory.CreateDirectory(_options.LogDirectory); + IncludeCorrelationId = options.IncludeCorrelationId; + } + + public void SetContext(string key, string value) => _context.Set(key, value); + public void ClearContext() => _context.Clear(); + public string GetContext(string key) => _context.Get(key); + + public IDisposable BeginScope(TState state) => null; + public bool IsEnabled(LogLevel logLevel) => _options.IsEnabled; + + public void Log(LogLevel logLevel, EventId eventId, TState state, + Exception exception, Func formatter) + { + if (!IsEnabled(logLevel)) + { + return; + } + + string message = formatter(state, exception); + if (IncludeCorrelationId) + { + var correlationId = _context.Get("CorrelationId") ?? Guid.NewGuid().ToString(); + _context.Set("CorrelationId", correlationId); + } + + var logObject = new Dictionary + { + { "timestamp", DateTime.UtcNow }, + { "level", logLevel.ToString() }, + { "category", _categoryName }, + { "message", message }, + { "exception", exception?.ToString() }, + { "eventId", eventId.Id } + }; + + var contextData = _context.GetAll(); + if (contextData.Count > 0) + { + logObject["context"] = contextData; + } + + try + { + string json = JsonHelper.ToJson(logObject); + File.AppendAllText(_filePath, json + Environment.NewLine, Encoding.UTF8); + } + catch (Exception e) + { + OnException?.Invoke(this, e); + } + } + } +} diff --git a/EonaCat.Logger/EonaCatCoreLogger/JsonFileLoggerOptions.cs b/EonaCat.Logger/EonaCatCoreLogger/JsonFileLoggerOptions.cs new file mode 100644 index 0000000..a162304 --- /dev/null +++ b/EonaCat.Logger/EonaCatCoreLogger/JsonFileLoggerOptions.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Text; + +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +namespace EonaCat.Logger.EonaCatCoreLogger +{ + public class JsonFileLoggerOptions + { + public string LogDirectory { get; set; } = "Logs"; + public string FileName { get; set; } = "log.json"; + public bool IsEnabled { get; set; } = true; + public bool IncludeCorrelationId { get; set; } = true; + } + +} diff --git a/EonaCat.Logger/EonaCatCoreLogger/LogContext.cs b/EonaCat.Logger/EonaCatCoreLogger/LogContext.cs new file mode 100644 index 0000000..3acf2c1 --- /dev/null +++ b/EonaCat.Logger/EonaCatCoreLogger/LogContext.cs @@ -0,0 +1,30 @@ +using System.Collections.Concurrent; +using System.Collections.Generic; + +namespace EonaCat.Logger.EonaCatCoreLogger +{ + public class LoggerScopedContext + { + private readonly ConcurrentDictionary _context = new(); + + public void Set(string key, string value) + { + _context[key] = value; + } + + public string Get(string key) + { + return _context.TryGetValue(key, out var value) ? value : null; + } + + public IReadOnlyDictionary GetAll() + { + return new Dictionary(_context); + } + + public void Clear() + { + _context.Clear(); + } + } +} diff --git a/EonaCat.Logger/EonaCatCoreLogger/TcpLogger.cs b/EonaCat.Logger/EonaCatCoreLogger/TcpLogger.cs new file mode 100644 index 0000000..326108f --- /dev/null +++ b/EonaCat.Logger/EonaCatCoreLogger/TcpLogger.cs @@ -0,0 +1,90 @@ +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.IO; +using System.Net.Sockets; + +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +namespace EonaCat.Logger.EonaCatCoreLogger +{ + public class TcpLogger : ILogger + { + private readonly string _categoryName; + private readonly TcpLoggerOptions _options; + private readonly LoggerScopedContext _context = new(); + public bool IncludeCorrelationId { get; set; } + public event EventHandler OnException; + + public TcpLogger(string categoryName, TcpLoggerOptions options) + { + _categoryName = categoryName; + _options = options; + IncludeCorrelationId = options.IncludeCorrelationId; + } + + public void SetContext(string key, string value) => _context.Set(key, value); + public void ClearContext() => _context.Clear(); + public string GetContext(string key) => _context.Get(key); + + public IDisposable BeginScope(TState state) => null; + public bool IsEnabled(LogLevel logLevel) => _options.IsEnabled; + + public void Log(LogLevel logLevel, EventId eventId, TState state, + Exception exception, Func formatter) + { + if (!IsEnabled(logLevel)) + { + return; + } + + try + { + string message = formatter(state, exception); + if (IncludeCorrelationId) + { + var correlationId = _context.Get("CorrelationId") ?? Guid.NewGuid().ToString(); + _context.Set("CorrelationId", correlationId); + } + + var logParts = new List + { + $"[{DateTime.UtcNow:u}]", + $"[{logLevel}]", + $"[{_categoryName}]", + $"Message: {message}", + }; + + var contextData = _context.GetAll(); + if (contextData.Count > 0) + { + foreach (var kvp in contextData) + { + logParts.Add($"{kvp.Key}: {kvp.Value}"); + } + } + + if (exception != null) + { + logParts.Add($"Exception: {exception}"); + } + + string fullLog = string.Join(" | ", logParts); + + using var client = new TcpClient(); + client.Connect(_options.Host, _options.Port); + using var stream = client.GetStream(); + using var writer = new StreamWriter(stream, System.Text.Encoding.UTF8) + { + AutoFlush = true + }; + writer.WriteLine(fullLog); + } + catch (Exception e) + { + OnException?.Invoke(this, e); + } + } + } +} diff --git a/EonaCat.Logger/EonaCatCoreLogger/TcpLoggerOptions.cs b/EonaCat.Logger/EonaCatCoreLogger/TcpLoggerOptions.cs new file mode 100644 index 0000000..bc436ee --- /dev/null +++ b/EonaCat.Logger/EonaCatCoreLogger/TcpLoggerOptions.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Text; + +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +namespace EonaCat.Logger.EonaCatCoreLogger +{ + public class TcpLoggerOptions + { + public string Host { get; set; } + public int Port { get; set; } + public bool IsEnabled { get; set; } = true; + public bool IncludeCorrelationId { get; set; } = true; + } +} diff --git a/EonaCat.Logger/EonaCatCoreLogger/TcpLoggerProvider.cs b/EonaCat.Logger/EonaCatCoreLogger/TcpLoggerProvider.cs new file mode 100644 index 0000000..bad5c80 --- /dev/null +++ b/EonaCat.Logger/EonaCatCoreLogger/TcpLoggerProvider.cs @@ -0,0 +1,31 @@ +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Text; + +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +namespace EonaCat.Logger.EonaCatCoreLogger +{ + public class TcpLoggerProvider : ILoggerProvider + { + private readonly TcpLoggerOptions _options; + + public TcpLoggerProvider(TcpLoggerOptions options) + { + _options = options; + } + + public ILogger CreateLogger(string categoryName) + { + return new TcpLogger(categoryName, _options); + } + + public void Dispose() + { + + } + } + +} diff --git a/EonaCat.Logger/EonaCatCoreLogger/UdpLogger.cs b/EonaCat.Logger/EonaCatCoreLogger/UdpLogger.cs new file mode 100644 index 0000000..8d12b2c --- /dev/null +++ b/EonaCat.Logger/EonaCatCoreLogger/UdpLogger.cs @@ -0,0 +1,88 @@ +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Net.Sockets; +using System.Text; + +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +namespace EonaCat.Logger.EonaCatCoreLogger +{ + public class UdpLogger : ILogger + { + private readonly string _categoryName; + private readonly UdpLoggerOptions _options; + private readonly LoggerScopedContext _context = new(); + public bool IncludeCorrelationId { get; set; } + public event EventHandler OnException; + + public UdpLogger(string categoryName, UdpLoggerOptions options) + { + _categoryName = categoryName; + _options = options; + IncludeCorrelationId = options.IncludeCorrelationId; + } + + public void SetContext(string key, string value) => _context.Set(key, value); + public void ClearContext() => _context.Clear(); + public string GetContext(string key) => _context.Get(key); + + public IDisposable BeginScope(TState state) => null; + + public bool IsEnabled(LogLevel logLevel) => _options.IsEnabled; + + public void Log(LogLevel logLevel, EventId eventId, TState state, + Exception exception, Func formatter) + { + if (!IsEnabled(logLevel) || formatter == null) + { + return; + } + + string message = formatter(state, exception); + + // Get correlation ID from context or generate new one + if (IncludeCorrelationId) + { + var correlationId = _context.Get("CorrelationId") ?? Guid.NewGuid().ToString(); + _context.Set("CorrelationId", correlationId); + } + + var logParts = new List + { + $"[{DateTime.UtcNow:u}]", + $"[{logLevel}]", + $"[{_categoryName}]", + $"Message: {message}", + }; + + var contextData = _context.GetAll(); + if (contextData.Count > 0) + { + foreach (var kvp in contextData) + { + logParts.Add($"{kvp.Key}: {kvp.Value}"); + } + } + + if (exception != null) + { + logParts.Add($"Exception: {exception}"); + } + + string fullMessage = string.Join(" | ", logParts); + + try + { + using var client = new UdpClient(); + byte[] bytes = Encoding.UTF8.GetBytes(fullMessage); + client.Send(bytes, bytes.Length, _options.Host, _options.Port); + } + catch (Exception e) + { + OnException?.Invoke(this, e); + } + } + } +} diff --git a/EonaCat.Logger/EonaCatCoreLogger/UdpLoggerOptions.cs b/EonaCat.Logger/EonaCatCoreLogger/UdpLoggerOptions.cs new file mode 100644 index 0000000..29491fb --- /dev/null +++ b/EonaCat.Logger/EonaCatCoreLogger/UdpLoggerOptions.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Text; + +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +namespace EonaCat.Logger.EonaCatCoreLogger +{ + public class UdpLoggerOptions + { + public string Host { get; set; } = "127.0.0.1"; + + public int Port { get; set; } = 514; + + public bool IsEnabled { get; set; } = true; + public bool IncludeCorrelationId { get; set; } = true; + } + +} diff --git a/EonaCat.Logger/EonaCatCoreLogger/UdpLoggerProvider.cs b/EonaCat.Logger/EonaCatCoreLogger/UdpLoggerProvider.cs new file mode 100644 index 0000000..5b0006c --- /dev/null +++ b/EonaCat.Logger/EonaCatCoreLogger/UdpLoggerProvider.cs @@ -0,0 +1,32 @@ +using Microsoft.Extensions.Logging; + +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +namespace EonaCat.Logger.EonaCatCoreLogger +{ + public class UdpLoggerProvider : ILoggerProvider + { + private readonly UdpLoggerOptions _options; + + public UdpLoggerProvider(UdpLoggerOptions options = null) + { + if (options == null) + { + options = new UdpLoggerOptions(); + } + _options = options; + } + + public ILogger CreateLogger(string categoryName) + { + return new UdpLogger(categoryName, _options); + } + + public void Dispose() + { + // No unmanaged resources, nothing to dispose here + } + } + +} diff --git a/EonaCat.Logger/EonaCatCoreLogger/XmlFIleLoggerProvider.cs b/EonaCat.Logger/EonaCatCoreLogger/XmlFIleLoggerProvider.cs new file mode 100644 index 0000000..8e1575e --- /dev/null +++ b/EonaCat.Logger/EonaCatCoreLogger/XmlFIleLoggerProvider.cs @@ -0,0 +1,35 @@ +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Text; + +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +namespace EonaCat.Logger.EonaCatCoreLogger +{ + public class XmlFileLoggerProvider : ILoggerProvider + { + private readonly XmlFileLoggerOptions _options; + + public XmlFileLoggerProvider(XmlFileLoggerOptions options = null) + { + if (options == null) + { + options = new XmlFileLoggerOptions(); + } + _options = options; + } + + public ILogger CreateLogger(string categoryName) + { + return new XmlFileLogger(categoryName, _options); + } + + public void Dispose() + { + + } + } + +} diff --git a/EonaCat.Logger/EonaCatCoreLogger/XmlFileLogger.cs b/EonaCat.Logger/EonaCatCoreLogger/XmlFileLogger.cs new file mode 100644 index 0000000..0af6283 --- /dev/null +++ b/EonaCat.Logger/EonaCatCoreLogger/XmlFileLogger.cs @@ -0,0 +1,87 @@ +using Microsoft.Extensions.Logging; +using System; +using System.IO; +using System.Xml.Linq; + +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +namespace EonaCat.Logger.EonaCatCoreLogger +{ + public class XmlFileLogger : ILogger + { + private readonly string _categoryName; + private readonly XmlFileLoggerOptions _options; + private readonly string _filePath; + private readonly LoggerScopedContext _context = new(); + + public bool IncludeCorrelationId { get; set; } + + public event EventHandler OnException; + + + public XmlFileLogger(string categoryName, XmlFileLoggerOptions options) + { + _categoryName = categoryName; + _options = options; + _filePath = Path.Combine(_options.LogDirectory, _options.FileName); + Directory.CreateDirectory(_options.LogDirectory); + IncludeCorrelationId = options.IncludeCorrelationId; + } + + public void SetContext(string key, string value) => _context.Set(key, value); + public void ClearContext() => _context.Clear(); + public string GetContext(string key) => _context.Get(key); + + public IDisposable BeginScope(TState state) => null; + + public bool IsEnabled(LogLevel logLevel) => _options.IsEnabled; + + public void Log(LogLevel logLevel, EventId eventId, TState state, + Exception exception, Func formatter) + { + if (!IsEnabled(logLevel) || formatter == null) + { + return; + } + + try + { + if (IncludeCorrelationId) + { + var correlationId = _context.Get("CorrelationId") ?? Guid.NewGuid().ToString(); + _context.Set("CorrelationId", correlationId); + } + + var logElement = new XElement("log", + new XElement("timestamp", DateTime.UtcNow.ToString("o")), + new XElement("level", logLevel.ToString()), + new XElement("category", _categoryName), + new XElement("message", formatter(state, exception)) + ); + + var context = _context.GetAll(); + if (context.Count > 0) + { + var contextElement = new XElement("context"); + foreach (var item in context) + { + contextElement.Add(new XElement(item.Key, item.Value)); + } + logElement.Add(contextElement); + } + + if (exception != null) + { + logElement.Add(new XElement("exception", exception.ToString())); + } + + File.AppendAllText(_filePath, logElement.ToString() + Environment.NewLine); + } + catch (Exception e) + { + OnException?.Invoke(this, e); + } + } + } +} diff --git a/EonaCat.Logger/EonaCatCoreLogger/XmlFileLoggerOptions.cs b/EonaCat.Logger/EonaCatCoreLogger/XmlFileLoggerOptions.cs new file mode 100644 index 0000000..6cb2940 --- /dev/null +++ b/EonaCat.Logger/EonaCatCoreLogger/XmlFileLoggerOptions.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Text; + +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +namespace EonaCat.Logger.EonaCatCoreLogger +{ + public class XmlFileLoggerOptions + { + public string LogDirectory { get; set; } = "Logs"; + public string FileName { get; set; } = "log.xml"; + public bool IsEnabled { get; set; } = true; + public bool IncludeCorrelationId { get; set; } = true; + } + +} diff --git a/README.md b/README.md index 6e58a1a..dcf8584 100644 --- a/README.md +++ b/README.md @@ -378,4 +378,15 @@ private void LogHelper_OnException(object sender, ErrorMessage e) Console.WriteLine("Error message from logger: " + e.Message) } } -``` \ No newline at end of file +``` + +Example of adding custom context to the log messages: + +```csharp +var jsonLogger = new JsonFileLoggerProvider().CreateLogger("MyCategory") as JsonFileLogger; +jsonLogger?.SetContext("CorrelationId", "abc-123"); +jsonLogger?.SetContext("UserId", "john.doe"); +jsonLogger?.LogInformation("User logged in"); +```` +// Output: +// [2025-04-25 17:01:00Z] [Information] MyCategory: User logged in | CorrelationId=abc-123 UserId=john.doe diff --git a/Testers/EonaCat.Logger.Test.Web/Logger.cs b/Testers/EonaCat.Logger.Test.Web/Logger.cs index 58788c0..702209b 100644 --- a/Testers/EonaCat.Logger.Test.Web/Logger.cs +++ b/Testers/EonaCat.Logger.Test.Web/Logger.cs @@ -37,9 +37,11 @@ public class Logger UseLocalTime = UseLocalTime, }, }; + _logManager = new LogManager(LoggerSettings); _logManager.Settings.TypesToLog.Clear(); _logManager.Settings.LogInfo(); + while (true) { _logManager.WriteAsync("2222", ELogType.INFO, writeToConsole: false); diff --git a/Testers/EonaCat.Logger.Test.Web/Program.cs b/Testers/EonaCat.Logger.Test.Web/Program.cs index 2f2127a..78a062f 100644 --- a/Testers/EonaCat.Logger.Test.Web/Program.cs +++ b/Testers/EonaCat.Logger.Test.Web/Program.cs @@ -9,6 +9,7 @@ using EonaCat.Web.RateLimiter; using EonaCat.Web.RateLimiter.Endpoints.Extensions; using EonaCat.Web.Tracer.Extensions; using EonaCat.Web.Tracer.Models; +using Microsoft.Extensions.Logging; using System.Runtime.Versioning; var builder = WebApplication.CreateBuilder(args); @@ -32,6 +33,11 @@ logger.LoggerSettings.UseMask = true; Console.WriteLine(DllInfo.EonaCatVersion); Console.WriteLine(VersionHelper.GetInformationalVersion()); +var jsonLogger = new JsonFileLoggerProvider().CreateLogger("MyCategory") as JsonFileLogger; +jsonLogger?.SetContext("CorrelationId", "abc-123"); +jsonLogger?.SetContext("UserId", "john.doe"); +jsonLogger?.LogInformation("User logged in"); + void LoggerSettings_OnLog(EonaCatLogMessage message) { Console.ForegroundColor = ConsoleColor.Yellow; @@ -45,6 +51,7 @@ options.MaxRolloverFiles = 5; options.UseLocalTime = true; options.UseMask = true; builder.Logging.AddEonaCatFileLogger(fileLoggerOptions: options, filenamePrefix: "web"); +builder.Logging.AddEonaCatConsoleLogger(); builder.Services.AddRazorPages();