Added additional providers and correlationId (by using a LogContext)

This commit is contained in:
2025-04-25 21:23:41 +02:00
parent d7944065a6
commit 60777caaa5
31 changed files with 1194 additions and 7 deletions

View File

@@ -3,7 +3,7 @@
<TargetFrameworks>.netstandard2.1; net6.0; net7.0; net8.0; net4.8;</TargetFrameworks> <TargetFrameworks>.netstandard2.1; net6.0; net7.0; net8.0; net4.8;</TargetFrameworks>
<ApplicationIcon>icon.ico</ApplicationIcon> <ApplicationIcon>icon.ico</ApplicationIcon>
<LangVersion>latest</LangVersion> <LangVersion>latest</LangVersion>
<FileVersion>1.4.5</FileVersion> <FileVersion>1.4.6</FileVersion>
<Authors>EonaCat (Jeroen Saey)</Authors> <Authors>EonaCat (Jeroen Saey)</Authors>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild> <GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<Company>EonaCat (Jeroen Saey)</Company> <Company>EonaCat (Jeroen Saey)</Company>
@@ -24,7 +24,7 @@
</PropertyGroup> </PropertyGroup>
<PropertyGroup> <PropertyGroup>
<EVRevisionFormat>1.4.5+{chash:10}.{c:ymd}</EVRevisionFormat> <EVRevisionFormat>1.4.6+{chash:10}.{c:ymd}</EVRevisionFormat>
<EVDefault>true</EVDefault> <EVDefault>true</EVDefault>
<EVInfo>true</EVInfo> <EVInfo>true</EVInfo>
<EVTagMatch>v[0-9]*</EVTagMatch> <EVTagMatch>v[0-9]*</EVTagMatch>
@@ -51,15 +51,16 @@
</None> </None>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="EonaCat.Json" Version="1.1.4" /> <PackageReference Include="EonaCat.Json" Version="1.1.9" />
<PackageReference Include="EonaCat.Versioning" Version="1.2.6"> <PackageReference Include="EonaCat.Versioning" Version="1.2.6">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
</PackageReference> </PackageReference>
<PackageReference Include="EonaCat.Versioning.Helpers" Version="1.0.2" /> <PackageReference Include="EonaCat.Versioning.Helpers" Version="1.0.2" />
<PackageReference Include="Microsoft.CSharp" Version="4.7.0" /> <PackageReference Include="Microsoft.CSharp" Version="4.7.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.2" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.4" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.2" /> <PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.4" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="9.0.4" />
<PackageReference Include="System.Net.Http" Version="4.3.4" /> <PackageReference Include="System.Net.Http" Version="4.3.4" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@@ -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<string> _buffer = new List<string>();
private static readonly object _lock = new object();
private static bool _flushLoopStarted = false;
public event EventHandler<Exception> OnException;
public event EventHandler<string> 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>(TState state) => null;
public bool IsEnabled(LogLevel logLevel) => _options.IsEnabled;
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state,
Exception exception, Func<TState, Exception, string> 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<string> toSend;
lock (_lock)
{
if (_buffer.Count == 0) return;
toSend = new List<string>(_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<string, string>
{
{ "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<string, string> values)
{
foreach (var kv in values)
{
template = template.Replace($"{{{kv.Key}}}", kv.Value);
}
return template;
}
}
}

View File

@@ -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<string, string> CustomHeaders { get; set; } = new();
public Dictionary<string, string> 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;
}
}

View File

@@ -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
}
}
}

View File

@@ -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
{
/// <summary>
/// Extensions for adding the built-in Console Logger to the <see cref="ILoggingBuilder" />
/// </summary>
public static class ConsoleLoggerFactoryExtensions
{
/// <summary>
/// Adds the built-in Console Logger to the logging builder.
/// </summary>
/// <param name="builder">The <see cref="ILoggingBuilder" /> to use.</param>
/// <param name="configure">Optional configuration for <see cref="ConsoleLoggerOptions" /></param>
public static ILoggingBuilder AddEonaCatConsoleLogger(this ILoggingBuilder builder, Action<ConsoleLoggerOptions> configure = null)
{
if (configure != null)
{
builder.AddConsole(configure);
}
else
{
builder.AddConsole();
}
return builder;
}
}
}

View File

@@ -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<ElasticSearchLoggerOptions> configure)
{
var options = new ElasticSearchLoggerOptions();
configure?.Invoke(options);
builder.Services.AddSingleton<ILoggerProvider>(new ElasticSearchLoggerProvider(options));
return builder;
}
}
}

View File

@@ -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<HttpLoggerOptions> configure)
{
var options = new HttpLoggerOptions();
configure?.Invoke(options);
builder.Services.AddSingleton<ILoggerProvider>(new HttpLoggerProvider(options));
return builder;
}
}
}

View File

@@ -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<JsonFileLoggerOptions> configure)
{
var options = new JsonFileLoggerOptions();
configure?.Invoke(options);
builder.Services.AddSingleton<ILoggerProvider>(new JsonFileLoggerProvider(options));
return builder;
}
}
}

View File

@@ -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<TcpLoggerOptions> configure)
{
var options = new TcpLoggerOptions();
configure?.Invoke(options);
builder.Services.AddSingleton<ILoggerProvider>(new TcpLoggerProvider(options));
return builder;
}
}
}

View File

@@ -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<UdpLoggerOptions> configure)
{
var options = new UdpLoggerOptions();
configure?.Invoke(options);
builder.Services.AddSingleton<ILoggerProvider>(new UdpLoggerProvider(options));
return builder;
}
}
}

View File

@@ -139,4 +139,9 @@ public class FileLoggerOptions : BatchingLoggerOptions
/// Custom keywords specified in LoggerSettings /// Custom keywords specified in LoggerSettings
/// </summary> /// </summary>
public bool UseDefaultMasking { get; set; } = true; public bool UseDefaultMasking { get; set; } = true;
/// <summary>
/// Determines if we need to include the correlation ID in the log
/// </summary>
public bool IncludeCorrelationId { get; set; } = true;
} }

View File

@@ -30,6 +30,7 @@ public class FileLoggerProvider : BatchingLoggerProvider
private bool _rollingOver; private bool _rollingOver;
private int _rollOverCount; private int _rollOverCount;
private ConcurrentDictionary<string,string> _buffer = new ConcurrentDictionary<string, string>(); private ConcurrentDictionary<string,string> _buffer = new ConcurrentDictionary<string, string>();
private readonly LoggerScopedContext _context = new();
public event EventHandler<ErrorMessage> OnError; public event EventHandler<ErrorMessage> OnError;
@@ -46,6 +47,7 @@ public class FileLoggerProvider : BatchingLoggerProvider
_maxRetainedFiles = loggerOptions.RetainedFileCountLimit; _maxRetainedFiles = loggerOptions.RetainedFileCountLimit;
_maxRolloverFiles = loggerOptions.MaxRolloverFiles; _maxRolloverFiles = loggerOptions.MaxRolloverFiles;
_maxTries = loggerOptions.MaxWriteTries; _maxTries = loggerOptions.MaxWriteTries;
IncludeCorrelationId = loggerOptions.IncludeCorrelationId;
} }
/// <summary> /// <summary>
@@ -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);
/// <inheritdoc /> /// <inheritdoc />
protected override async Task WriteMessagesAsync(IEnumerable<LogMessage> messages, protected override async Task WriteMessagesAsync(IEnumerable<LogMessage> messages,
CancellationToken cancellationToken) CancellationToken cancellationToken)
@@ -90,7 +96,22 @@ public class FileLoggerProvider : BatchingLoggerProvider
foreach (var group in messages.GroupBy(GetGrouping)) foreach (var group in messages.GroupBy(GetGrouping))
{ {
LogFile = GetFullName(group.Key); 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)) if (!_buffer.TryAdd(LogFile, currentMessages))
{ {
_buffer[LogFile] += currentMessages; _buffer[LogFile] += currentMessages;
@@ -128,6 +149,7 @@ public class FileLoggerProvider : BatchingLoggerProvider
} }
public bool IsWriting { get; set; } public bool IsWriting { get; set; }
public bool IncludeCorrelationId { get; set; }
private async Task<bool> TryWriteToFileAsync(CancellationToken cancellationToken) private async Task<bool> TryWriteToFileAsync(CancellationToken cancellationToken)
{ {

View File

@@ -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<Exception> OnException;
public event EventHandler<string> 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>(TState state) => null;
public bool IsEnabled(LogLevel logLevel) => _options.IsEnabled;
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state,
Exception exception, Func<TState, Exception, string> 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<string, object>
{
{ "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);
}
}
}
}

View File

@@ -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;
}
}

View File

@@ -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
}
}
}

View File

@@ -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()
{
}
}
}

View File

@@ -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<Exception> 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>(TState state) => null;
public bool IsEnabled(LogLevel logLevel) => _options.IsEnabled;
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state,
Exception exception, Func<TState, Exception, string> 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<string, object>
{
{ "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);
}
}
}
}

View File

@@ -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;
}
}

View File

@@ -0,0 +1,30 @@
using System.Collections.Concurrent;
using System.Collections.Generic;
namespace EonaCat.Logger.EonaCatCoreLogger
{
public class LoggerScopedContext
{
private readonly ConcurrentDictionary<string, string> _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<string, string> GetAll()
{
return new Dictionary<string, string>(_context);
}
public void Clear()
{
_context.Clear();
}
}
}

View File

@@ -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<Exception> 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>(TState state) => null;
public bool IsEnabled(LogLevel logLevel) => _options.IsEnabled;
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state,
Exception exception, Func<TState, Exception, string> 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<string>
{
$"[{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);
}
}
}
}

View File

@@ -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;
}
}

View File

@@ -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()
{
}
}
}

View File

@@ -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<Exception> 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>(TState state) => null;
public bool IsEnabled(LogLevel logLevel) => _options.IsEnabled;
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state,
Exception exception, Func<TState, Exception, string> 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<string>
{
$"[{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);
}
}
}
}

View File

@@ -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;
}
}

View File

@@ -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
}
}
}

View File

@@ -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()
{
}
}
}

View File

@@ -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<Exception> 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>(TState state) => null;
public bool IsEnabled(LogLevel logLevel) => _options.IsEnabled;
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state,
Exception exception, Func<TState, Exception, string> 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);
}
}
}
}

View File

@@ -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;
}
}

View File

@@ -379,3 +379,14 @@ private void LogHelper_OnException(object sender, ErrorMessage e)
} }
} }
``` ```
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

View File

@@ -37,9 +37,11 @@ public class Logger
UseLocalTime = UseLocalTime, UseLocalTime = UseLocalTime,
}, },
}; };
_logManager = new LogManager(LoggerSettings); _logManager = new LogManager(LoggerSettings);
_logManager.Settings.TypesToLog.Clear(); _logManager.Settings.TypesToLog.Clear();
_logManager.Settings.LogInfo(); _logManager.Settings.LogInfo();
while (true) while (true)
{ {
_logManager.WriteAsync("2222", ELogType.INFO, writeToConsole: false); _logManager.WriteAsync("2222", ELogType.INFO, writeToConsole: false);

View File

@@ -9,6 +9,7 @@ using EonaCat.Web.RateLimiter;
using EonaCat.Web.RateLimiter.Endpoints.Extensions; using EonaCat.Web.RateLimiter.Endpoints.Extensions;
using EonaCat.Web.Tracer.Extensions; using EonaCat.Web.Tracer.Extensions;
using EonaCat.Web.Tracer.Models; using EonaCat.Web.Tracer.Models;
using Microsoft.Extensions.Logging;
using System.Runtime.Versioning; using System.Runtime.Versioning;
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
@@ -32,6 +33,11 @@ logger.LoggerSettings.UseMask = true;
Console.WriteLine(DllInfo.EonaCatVersion); Console.WriteLine(DllInfo.EonaCatVersion);
Console.WriteLine(VersionHelper.GetInformationalVersion()); 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) void LoggerSettings_OnLog(EonaCatLogMessage message)
{ {
Console.ForegroundColor = ConsoleColor.Yellow; Console.ForegroundColor = ConsoleColor.Yellow;
@@ -45,6 +51,7 @@ options.MaxRolloverFiles = 5;
options.UseLocalTime = true; options.UseLocalTime = true;
options.UseMask = true; options.UseMask = true;
builder.Logging.AddEonaCatFileLogger(fileLoggerOptions: options, filenamePrefix: "web"); builder.Logging.AddEonaCatFileLogger(fileLoggerOptions: options, filenamePrefix: "web");
builder.Logging.AddEonaCatConsoleLogger();
builder.Services.AddRazorPages(); builder.Services.AddRazorPages();