diff --git a/EonaCat.Logger/EonaCat.Logger.csproj b/EonaCat.Logger/EonaCat.Logger.csproj
index e22a245..9a87a35 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.6
+ 1.4.7
EonaCat (Jeroen Saey)
true
EonaCat (Jeroen Saey)
@@ -24,7 +24,7 @@
- 1.4.6+{chash:10}.{c:ymd}
+ 1.4.7+{chash:10}.{c:ymd}
true
true
v[0-9]*
diff --git a/EonaCat.Logger/EonaCatCoreLogger/BatchingDatabaseLogger.cs b/EonaCat.Logger/EonaCatCoreLogger/BatchingDatabaseLogger.cs
new file mode 100644
index 0000000..e3477e4
--- /dev/null
+++ b/EonaCat.Logger/EonaCatCoreLogger/BatchingDatabaseLogger.cs
@@ -0,0 +1,187 @@
+using Microsoft.Extensions.Logging;
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Data.Common;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace EonaCat.Logger.EonaCatCoreLogger
+{
+ public class BatchingDatabaseLogger : ILogger, IDisposable
+ {
+ private readonly string _categoryName;
+ private readonly BatchingDatabaseLoggerOptions _options;
+ private readonly LoggerScopedContext _context = new();
+ private readonly BlockingCollection _queue;
+ private readonly CancellationTokenSource _cts;
+ private readonly Task _processingTask;
+
+ public bool IncludeCorrelationId { get; set; }
+ public event EventHandler OnException;
+
+ public BatchingDatabaseLogger(string categoryName, BatchingDatabaseLoggerOptions options)
+ {
+ _categoryName = categoryName;
+ _options = options;
+ IncludeCorrelationId = options.IncludeCorrelationId;
+
+ _queue = new BlockingCollection(new ConcurrentQueue());
+ _cts = new CancellationTokenSource();
+ _processingTask = Task.Run(ProcessQueueAsync);
+ }
+
+ public IDisposable BeginScope(TState state) => null;
+ public bool IsEnabled(LogLevel logLevel) => _options.IsEnabled;
+
+ 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 void Log(LogLevel logLevel, EventId eventId, TState state,
+ Exception exception, Func formatter)
+ {
+ if (!IsEnabled(logLevel) || formatter == null) return;
+
+ var message = formatter(state, exception);
+
+ var correlationId = IncludeCorrelationId
+ ? _context.Get("CorrelationId") ?? Guid.NewGuid().ToString()
+ : null;
+
+ if (correlationId != null)
+ {
+ _context.Set("CorrelationId", correlationId);
+ }
+
+ _queue.Add(new LogEntry
+ {
+ Timestamp = DateTime.UtcNow,
+ LogLevel = logLevel.ToString(),
+ Category = _categoryName,
+ Message = message,
+ Exception = exception?.ToString(),
+ CorrelationId = correlationId
+ });
+ }
+
+ private async Task ProcessQueueAsync()
+ {
+ var batch = new List();
+ var timeoutMs = (int)Math.Min(_options.BatchInterval.TotalMilliseconds, int.MaxValue);
+
+ while (!_cts.Token.IsCancellationRequested)
+ {
+ try
+ {
+ if (_queue.TryTake(out var logEntry, timeoutMs, _cts.Token))
+ {
+ batch.Add(logEntry);
+
+ // Drain the queue quickly without waiting
+ while (_queue.TryTake(out var additionalEntry))
+ {
+ batch.Add(additionalEntry);
+ }
+ }
+
+ if (batch.Count > 0)
+ {
+ await InsertBatchSafelyAsync(batch);
+ }
+ }
+ catch (OperationCanceledException)
+ {
+ break;
+ }
+ catch (Exception ex)
+ {
+ OnException?.Invoke(this, ex);
+ }
+ }
+
+ // Final flush outside the loop
+ if (batch.Count > 0)
+ {
+ await InsertBatchSafelyAsync(batch);
+ }
+ }
+
+ private async Task InsertBatchSafelyAsync(List batch)
+ {
+ try
+ {
+ await InsertBatchAsync(batch);
+ }
+ catch (Exception ex)
+ {
+ OnException?.Invoke(this, ex);
+ }
+ finally
+ {
+ batch.Clear();
+ }
+ }
+
+ private async Task InsertBatchAsync(List batch)
+ {
+ using var connection = _options.DbProviderFactory.CreateConnection();
+ if (connection == null)
+ {
+ throw new InvalidOperationException("Failed to create database connection.");
+ }
+
+ connection.ConnectionString = _options.ConnectionString;
+ await connection.OpenAsync();
+
+ foreach (var entry in batch)
+ {
+ using var command = connection.CreateCommand();
+ command.CommandText = _options.InsertCommand;
+ command.Parameters.Clear();
+
+ command.Parameters.Add(CreateParameter(command, "Timestamp", entry.Timestamp));
+ command.Parameters.Add(CreateParameter(command, "LogLevel", entry.LogLevel));
+ command.Parameters.Add(CreateParameter(command, "Category", entry.Category));
+ command.Parameters.Add(CreateParameter(command, "Message", entry.Message));
+ command.Parameters.Add(CreateParameter(command, "Exception", entry.Exception));
+ command.Parameters.Add(CreateParameter(command, "CorrelationId", entry.CorrelationId));
+
+ await command.ExecuteNonQueryAsync();
+ }
+ }
+
+ private DbParameter CreateParameter(DbCommand command, string name, object value)
+ {
+ var param = command.CreateParameter();
+ param.ParameterName = $"@{name}";
+ param.Value = value ?? DBNull.Value;
+ return param;
+ }
+
+ public void Dispose()
+ {
+ _cts.Cancel();
+ _queue.CompleteAdding();
+ try
+ {
+ _processingTask.Wait(_options.ShutdownTimeout);
+ }
+ catch (Exception ex)
+ {
+ OnException?.Invoke(this, ex);
+ }
+ }
+
+ private class LogEntry
+ {
+ public DateTime Timestamp { get; set; }
+ public string LogLevel { get; set; }
+ public string Category { get; set; }
+ public string Message { get; set; }
+ public string Exception { get; set; }
+ public string CorrelationId { get; set; }
+ }
+ }
+}
diff --git a/EonaCat.Logger/EonaCatCoreLogger/BatchingDatabaseLoggerOptions.cs b/EonaCat.Logger/EonaCatCoreLogger/BatchingDatabaseLoggerOptions.cs
new file mode 100644
index 0000000..3647d79
--- /dev/null
+++ b/EonaCat.Logger/EonaCatCoreLogger/BatchingDatabaseLoggerOptions.cs
@@ -0,0 +1,20 @@
+using System;
+using System.Data.Common;
+
+namespace EonaCat.Logger.EonaCatCoreLogger
+{
+ public class BatchingDatabaseLoggerOptions
+ {
+ public DbProviderFactory DbProviderFactory { get; set; }
+ public string ConnectionString { get; set; }
+ public string InsertCommand { get; set; } =
+ @"INSERT INTO Logs (Timestamp, LogLevel, Category, Message, Exception, CorrelationId)
+ VALUES (@Timestamp, @LogLevel, @Category, @Message, @Exception, @CorrelationId)";
+
+ public bool IsEnabled { get; set; } = true;
+ public bool IncludeCorrelationId { get; set; } = true;
+
+ public TimeSpan BatchInterval { get; set; } = TimeSpan.FromSeconds(5); // 5 sec batch
+ public TimeSpan ShutdownTimeout { get; set; } = TimeSpan.FromSeconds(10); // wait on shutdown
+ }
+}
diff --git a/EonaCat.Logger/EonaCatCoreLogger/BatchingDatabaseLoggerProvider.cs b/EonaCat.Logger/EonaCatCoreLogger/BatchingDatabaseLoggerProvider.cs
new file mode 100644
index 0000000..4258db1
--- /dev/null
+++ b/EonaCat.Logger/EonaCatCoreLogger/BatchingDatabaseLoggerProvider.cs
@@ -0,0 +1,24 @@
+using Microsoft.Extensions.Logging;
+
+namespace EonaCat.Logger.EonaCatCoreLogger
+{
+ public class BatchingDatabaseLoggerProvider : ILoggerProvider
+ {
+ private readonly BatchingDatabaseLoggerOptions _options;
+
+ public BatchingDatabaseLoggerProvider(BatchingDatabaseLoggerOptions options)
+ {
+ _options = options;
+ }
+
+ public ILogger CreateLogger(string categoryName)
+ {
+ return new BatchingDatabaseLogger(categoryName, _options);
+ }
+
+ public void Dispose()
+ {
+ // Will be disposed in BatchingDatabaseLogger itself
+ }
+ }
+}
diff --git a/EonaCat.Logger/EonaCatCoreLogger/DatabaseLogger.cs b/EonaCat.Logger/EonaCatCoreLogger/DatabaseLogger.cs
new file mode 100644
index 0000000..2abf87b
--- /dev/null
+++ b/EonaCat.Logger/EonaCatCoreLogger/DatabaseLogger.cs
@@ -0,0 +1,83 @@
+using Microsoft.Extensions.Logging;
+using System;
+using System.Collections.Generic;
+using System.Data.Common;
+using System.Text;
+
+namespace EonaCat.Logger.EonaCatCoreLogger
+{
+ public class DatabaseLogger : ILogger
+ {
+ private readonly string _categoryName;
+ private readonly DatabaseLoggerOptions _options;
+ private readonly LoggerScopedContext _context = new();
+
+ public bool IncludeCorrelationId { get; set; }
+ public event EventHandler OnException;
+
+ public DatabaseLogger(string categoryName, DatabaseLoggerOptions options)
+ {
+ _categoryName = categoryName;
+ _options = options;
+ IncludeCorrelationId = options.IncludeCorrelationId;
+ }
+
+ public IDisposable BeginScope(TState state) => null;
+ public bool IsEnabled(LogLevel logLevel) => _options.IsEnabled;
+
+ 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 void Log(LogLevel logLevel, EventId eventId, TState state,
+ Exception exception, Func formatter)
+ {
+ if (!IsEnabled(logLevel) || formatter == null) return;
+
+ var message = formatter(state, exception);
+
+ if (IncludeCorrelationId)
+ {
+ var correlationId = _context.Get("CorrelationId") ?? Guid.NewGuid().ToString();
+ _context.Set("CorrelationId", correlationId);
+ }
+
+ try
+ {
+ using var connection = _options.DbProviderFactory.CreateConnection();
+ if (connection == null)
+ {
+ throw new InvalidOperationException("Failed to create database connection.");
+ }
+
+ connection.ConnectionString = _options.ConnectionString;
+ connection.Open();
+
+ using var command = connection.CreateCommand();
+ command.CommandText = _options.InsertCommand;
+ command.Parameters.Clear();
+
+ command.Parameters.Add(CreateParameter(command, "Timestamp", DateTime.UtcNow));
+ command.Parameters.Add(CreateParameter(command, "LogLevel", logLevel.ToString()));
+ command.Parameters.Add(CreateParameter(command, "Category", _categoryName));
+ command.Parameters.Add(CreateParameter(command, "Message", message));
+ command.Parameters.Add(CreateParameter(command, "Exception", exception?.ToString()));
+ command.Parameters.Add(CreateParameter(command, "CorrelationId", _context.Get("CorrelationId")));
+
+ command.ExecuteNonQuery();
+ }
+ catch (Exception e)
+ {
+ OnException?.Invoke(this, e);
+ }
+ }
+
+ private DbParameter CreateParameter(DbCommand command, string name, object value)
+ {
+ var param = command.CreateParameter();
+ param.ParameterName = $"@{name}";
+ param.Value = value ?? DBNull.Value;
+ return param;
+ }
+ }
+}
diff --git a/EonaCat.Logger/EonaCatCoreLogger/DatabaseLoggerOptions.cs b/EonaCat.Logger/EonaCatCoreLogger/DatabaseLoggerOptions.cs
new file mode 100644
index 0000000..5a03fe4
--- /dev/null
+++ b/EonaCat.Logger/EonaCatCoreLogger/DatabaseLoggerOptions.cs
@@ -0,0 +1,17 @@
+using System.Data.Common;
+
+namespace EonaCat.Logger.EonaCatCoreLogger
+{
+ public class DatabaseLoggerOptions
+ {
+ public DbProviderFactory DbProviderFactory { get; set; }
+ public string ConnectionString { get; set; }
+
+ public string InsertCommand { get; set; } =
+ @"INSERT INTO Logs (Timestamp, LogLevel, Category, Message, Exception, CorrelationId)
+ VALUES (@Timestamp, @LogLevel, @Category, @Message, @Exception, @CorrelationId)";
+
+ public bool IsEnabled { get; set; } = true;
+ public bool IncludeCorrelationId { get; set; } = true;
+ }
+}
diff --git a/EonaCat.Logger/EonaCatCoreLogger/DatabaseLoggerProvider.cs b/EonaCat.Logger/EonaCatCoreLogger/DatabaseLoggerProvider.cs
new file mode 100644
index 0000000..07bedcb
--- /dev/null
+++ b/EonaCat.Logger/EonaCatCoreLogger/DatabaseLoggerProvider.cs
@@ -0,0 +1,24 @@
+using Microsoft.Extensions.Logging;
+
+namespace EonaCat.Logger.EonaCatCoreLogger
+{
+ public class DatabaseLoggerProvider : ILoggerProvider
+ {
+ private readonly DatabaseLoggerOptions _options;
+
+ public DatabaseLoggerProvider(DatabaseLoggerOptions options)
+ {
+ _options = options;
+ }
+
+ public ILogger CreateLogger(string categoryName)
+ {
+ return new DatabaseLogger(categoryName, _options);
+ }
+
+ public void Dispose()
+ {
+ // Nothing to dispose
+ }
+ }
+}
diff --git a/EonaCat.Logger/EonaCatCoreLogger/DiscordLogger.cs b/EonaCat.Logger/EonaCatCoreLogger/DiscordLogger.cs
new file mode 100644
index 0000000..9ad3c1d
--- /dev/null
+++ b/EonaCat.Logger/EonaCatCoreLogger/DiscordLogger.cs
@@ -0,0 +1,80 @@
+using Microsoft.Extensions.Logging;
+using System;
+using System.Collections.Generic;
+using System.Net.Http;
+using System.Text;
+using System.Text.Json;
+
+namespace EonaCat.Logger.EonaCatCoreLogger
+{
+ public class DiscordLogger : ILogger
+ {
+ private readonly string _categoryName;
+ private readonly DiscordLoggerOptions _options;
+ private readonly LoggerScopedContext _context = new();
+
+ public bool IncludeCorrelationId { get; set; }
+ public event EventHandler OnException;
+
+ public DiscordLogger(string categoryName, DiscordLoggerOptions options)
+ {
+ _categoryName = categoryName;
+ _options = options;
+ IncludeCorrelationId = options.IncludeCorrelationId;
+ }
+
+ public IDisposable BeginScope(TState state) => null;
+ public bool IsEnabled(LogLevel logLevel) => _options.IsEnabled;
+
+ 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 void Log(LogLevel logLevel, EventId eventId, TState state,
+ Exception exception, Func formatter)
+ {
+ if (!IsEnabled(logLevel) || formatter == null) return;
+
+ var 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}",
+ };
+
+ foreach (var kvp in _context.GetAll())
+ {
+ logParts.Add($"`{kvp.Key}`: {kvp.Value}");
+ }
+
+ if (exception != null)
+ {
+ logParts.Add($"Exception: {exception}");
+ }
+
+ string fullMessage = string.Join("\n", logParts);
+
+ try
+ {
+ using var client = new HttpClient();
+ var payload = new { content = fullMessage };
+ var content = new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json");
+
+ client.PostAsync(_options.WebhookUrl, content).Wait();
+ }
+ catch (Exception e)
+ {
+ OnException?.Invoke(this, e);
+ }
+ }
+ }
+}
diff --git a/EonaCat.Logger/EonaCatCoreLogger/DiscordLoggerOptions.cs b/EonaCat.Logger/EonaCatCoreLogger/DiscordLoggerOptions.cs
new file mode 100644
index 0000000..f6077cf
--- /dev/null
+++ b/EonaCat.Logger/EonaCatCoreLogger/DiscordLoggerOptions.cs
@@ -0,0 +1,9 @@
+namespace EonaCat.Logger.EonaCatCoreLogger
+{
+ public class DiscordLoggerOptions
+ {
+ public string WebhookUrl { get; set; }
+ public bool IsEnabled { get; set; } = true;
+ public bool IncludeCorrelationId { get; set; } = true;
+ }
+}
diff --git a/EonaCat.Logger/EonaCatCoreLogger/DiscordLoggerProvider.cs b/EonaCat.Logger/EonaCatCoreLogger/DiscordLoggerProvider.cs
new file mode 100644
index 0000000..53e72d4
--- /dev/null
+++ b/EonaCat.Logger/EonaCatCoreLogger/DiscordLoggerProvider.cs
@@ -0,0 +1,24 @@
+using Microsoft.Extensions.Logging;
+
+namespace EonaCat.Logger.EonaCatCoreLogger
+{
+ public class DiscordLoggerProvider : ILoggerProvider
+ {
+ private readonly DiscordLoggerOptions _options;
+
+ public DiscordLoggerProvider(DiscordLoggerOptions options)
+ {
+ _options = options;
+ }
+
+ public ILogger CreateLogger(string categoryName)
+ {
+ return new DiscordLogger(categoryName, _options);
+ }
+
+ public void Dispose()
+ {
+ // Nothing to dispose
+ }
+ }
+}
diff --git a/EonaCat.Logger/EonaCatCoreLogger/Extensions/BatchingDatabaseLoggerFactoryExtensions.cs b/EonaCat.Logger/EonaCatCoreLogger/Extensions/BatchingDatabaseLoggerFactoryExtensions.cs
new file mode 100644
index 0000000..aec8400
--- /dev/null
+++ b/EonaCat.Logger/EonaCatCoreLogger/Extensions/BatchingDatabaseLoggerFactoryExtensions.cs
@@ -0,0 +1,18 @@
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using System;
+
+namespace EonaCat.Logger.EonaCatCoreLogger.Extensions
+{
+ public static class BatchingDatabaseLoggerFactoryExtensions
+ {
+ public static ILoggingBuilder AddEonaCatBatchingDatabaseLogger(this ILoggingBuilder builder, Action configure)
+ {
+ var options = new BatchingDatabaseLoggerOptions();
+ configure?.Invoke(options);
+
+ builder.Services.AddSingleton(new BatchingDatabaseLoggerProvider(options));
+ return builder;
+ }
+ }
+}
diff --git a/EonaCat.Logger/EonaCatCoreLogger/Extensions/DatabaseLoggerFactoryExtensions.cs b/EonaCat.Logger/EonaCatCoreLogger/Extensions/DatabaseLoggerFactoryExtensions.cs
new file mode 100644
index 0000000..db2b3c6
--- /dev/null
+++ b/EonaCat.Logger/EonaCatCoreLogger/Extensions/DatabaseLoggerFactoryExtensions.cs
@@ -0,0 +1,18 @@
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using System;
+
+namespace EonaCat.Logger.EonaCatCoreLogger.Extensions
+{
+ public static class DatabaseLoggerFactoryExtensions
+ {
+ public static ILoggingBuilder AddEonaCatDatabaseLogger(this ILoggingBuilder builder, Action configure)
+ {
+ var options = new DatabaseLoggerOptions();
+ configure?.Invoke(options);
+
+ builder.Services.AddSingleton(new DatabaseLoggerProvider(options));
+ return builder;
+ }
+ }
+}
diff --git a/EonaCat.Logger/EonaCatCoreLogger/Extensions/DiscordLoggerFactoryExtensions.cs b/EonaCat.Logger/EonaCatCoreLogger/Extensions/DiscordLoggerFactoryExtensions.cs
new file mode 100644
index 0000000..9be0dc4
--- /dev/null
+++ b/EonaCat.Logger/EonaCatCoreLogger/Extensions/DiscordLoggerFactoryExtensions.cs
@@ -0,0 +1,18 @@
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using System;
+
+namespace EonaCat.Logger.EonaCatCoreLogger.Extensions
+{
+ public static class DiscordLoggerFactoryExtensions
+ {
+ public static ILoggingBuilder AddEonaCatDiscordLogger(this ILoggingBuilder builder, Action configure)
+ {
+ var options = new DiscordLoggerOptions();
+ configure?.Invoke(options);
+
+ builder.Services.AddSingleton(new DiscordLoggerProvider(options));
+ return builder;
+ }
+ }
+}
diff --git a/EonaCat.Logger/EonaCatCoreLogger/Extensions/SlackLoggerFactoryExtensions.cs b/EonaCat.Logger/EonaCatCoreLogger/Extensions/SlackLoggerFactoryExtensions.cs
new file mode 100644
index 0000000..9d2a3d0
--- /dev/null
+++ b/EonaCat.Logger/EonaCatCoreLogger/Extensions/SlackLoggerFactoryExtensions.cs
@@ -0,0 +1,18 @@
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using System;
+
+namespace EonaCat.Logger.EonaCatCoreLogger.Extensions
+{
+ public static class SlackLoggerFactoryExtensions
+ {
+ public static ILoggingBuilder AddEonaCatSlackLogger(this ILoggingBuilder builder, Action configure)
+ {
+ var options = new SlackLoggerOptions();
+ configure?.Invoke(options);
+
+ builder.Services.AddSingleton(new SlackLoggerProvider(options));
+ return builder;
+ }
+ }
+}
diff --git a/EonaCat.Logger/EonaCatCoreLogger/Extensions/TelegramLoggerFactoryExtensions.cs b/EonaCat.Logger/EonaCatCoreLogger/Extensions/TelegramLoggerFactoryExtensions.cs
new file mode 100644
index 0000000..5aed83e
--- /dev/null
+++ b/EonaCat.Logger/EonaCatCoreLogger/Extensions/TelegramLoggerFactoryExtensions.cs
@@ -0,0 +1,18 @@
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using System;
+
+namespace EonaCat.Logger.EonaCatCoreLogger.Extensions
+{
+ public static class TelegramLoggerFactoryExtensions
+ {
+ public static ILoggingBuilder AddEonaCatTelegramLogger(this ILoggingBuilder builder, Action configure)
+ {
+ var options = new TelegramLoggerOptions();
+ configure?.Invoke(options);
+
+ builder.Services.AddSingleton(new TelegramLoggerProvider(options));
+ return builder;
+ }
+ }
+}
diff --git a/EonaCat.Logger/EonaCatCoreLogger/FileLoggerProvider.cs b/EonaCat.Logger/EonaCatCoreLogger/FileLoggerProvider.cs
index e6eb455..17aa586 100644
--- a/EonaCat.Logger/EonaCatCoreLogger/FileLoggerProvider.cs
+++ b/EonaCat.Logger/EonaCatCoreLogger/FileLoggerProvider.cs
@@ -27,8 +27,6 @@ public class FileLoggerProvider : BatchingLoggerProvider
private readonly int _maxTries;
private readonly string _path;
private string _logFile;
- private bool _rollingOver;
- private int _rollOverCount;
private ConcurrentDictionary _buffer = new ConcurrentDictionary();
private readonly LoggerScopedContext _context = new();
diff --git a/EonaCat.Logger/EonaCatCoreLogger/SlackLogger.cs b/EonaCat.Logger/EonaCatCoreLogger/SlackLogger.cs
new file mode 100644
index 0000000..cfb1d36
--- /dev/null
+++ b/EonaCat.Logger/EonaCatCoreLogger/SlackLogger.cs
@@ -0,0 +1,83 @@
+using Microsoft.Extensions.Logging;
+using System;
+using System.Collections.Generic;
+using System.Net.Http;
+using System.Text;
+using System.Text.Json;
+
+// 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 SlackLogger : ILogger
+ {
+ private readonly string _categoryName;
+ private readonly SlackLoggerOptions _options;
+ private readonly LoggerScopedContext _context = new();
+
+ public bool IncludeCorrelationId { get; set; }
+ public event EventHandler OnException;
+
+ public SlackLogger(string categoryName, SlackLoggerOptions options)
+ {
+ _categoryName = categoryName;
+ _options = options;
+ IncludeCorrelationId = options.IncludeCorrelationId;
+ }
+
+ public IDisposable BeginScope(TState state) => null;
+ public bool IsEnabled(LogLevel logLevel) => _options.IsEnabled;
+
+ 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 void Log(LogLevel logLevel, EventId eventId, TState state,
+ Exception exception, Func formatter)
+ {
+ if (!IsEnabled(logLevel) || formatter == null) return;
+
+ var 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}",
+ };
+
+ foreach (var kvp in _context.GetAll())
+ {
+ logParts.Add($"_{kvp.Key}_: {kvp.Value}");
+ }
+
+ if (exception != null)
+ {
+ logParts.Add($"Exception: {exception}");
+ }
+
+ string fullMessage = string.Join("\n", logParts);
+
+ try
+ {
+ using var client = new HttpClient();
+ var payload = new { text = fullMessage };
+ var content = new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json");
+
+ client.PostAsync(_options.WebhookUrl, content).Wait();
+ }
+ catch (Exception e)
+ {
+ OnException?.Invoke(this, e);
+ }
+ }
+ }
+}
diff --git a/EonaCat.Logger/EonaCatCoreLogger/SlackLoggerOptions.cs b/EonaCat.Logger/EonaCatCoreLogger/SlackLoggerOptions.cs
new file mode 100644
index 0000000..5525b8a
--- /dev/null
+++ b/EonaCat.Logger/EonaCatCoreLogger/SlackLoggerOptions.cs
@@ -0,0 +1,13 @@
+namespace EonaCat.Logger.EonaCatCoreLogger
+{
+
+ // 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.
+
+ public class SlackLoggerOptions
+ {
+ public string WebhookUrl { get; set; }
+ public bool IsEnabled { get; set; } = true;
+ public bool IncludeCorrelationId { get; set; } = true;
+ }
+}
diff --git a/EonaCat.Logger/EonaCatCoreLogger/SlackLoggerProvider.cs b/EonaCat.Logger/EonaCatCoreLogger/SlackLoggerProvider.cs
new file mode 100644
index 0000000..ec1e09b
--- /dev/null
+++ b/EonaCat.Logger/EonaCatCoreLogger/SlackLoggerProvider.cs
@@ -0,0 +1,27 @@
+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 SlackLoggerProvider : ILoggerProvider
+ {
+ private readonly SlackLoggerOptions _options;
+
+ public SlackLoggerProvider(SlackLoggerOptions options)
+ {
+ _options = options;
+ }
+
+ public ILogger CreateLogger(string categoryName)
+ {
+ return new SlackLogger(categoryName, _options);
+ }
+
+ public void Dispose()
+ {
+ // Nothing to dispose
+ }
+ }
+}
diff --git a/EonaCat.Logger/EonaCatCoreLogger/TelegramLogger.cs b/EonaCat.Logger/EonaCatCoreLogger/TelegramLogger.cs
new file mode 100644
index 0000000..eb50a3b
--- /dev/null
+++ b/EonaCat.Logger/EonaCatCoreLogger/TelegramLogger.cs
@@ -0,0 +1,57 @@
+using Microsoft.Extensions.Logging;
+using System;
+using System.Net.Http;
+using System.Text;
+using System.Text.Json;
+
+// 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 TelegramLogger : ILogger
+ {
+ private readonly string _categoryName;
+ private readonly TelegramLoggerOptions _options;
+ private readonly HttpClient _httpClient = new HttpClient();
+ public event EventHandler OnException;
+
+ public TelegramLogger(string categoryName, TelegramLoggerOptions options)
+ {
+ _categoryName = categoryName;
+ _options = options;
+ }
+
+ public IDisposable BeginScope(TState state) => null;
+ public bool IsEnabled(LogLevel logLevel) => _options.IsEnabled;
+
+ public async void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter)
+ {
+ if (!IsEnabled(logLevel)) return;
+
+ var message = $"[{DateTime.UtcNow:u}] [{logLevel}] {_categoryName}: {formatter(state, exception)}";
+ if (exception != null)
+ {
+ message += $"\nException: {exception}";
+ }
+
+ var url = $"https://api.telegram.org/bot{_options.BotToken}/sendMessage";
+ var payload = new
+ {
+ chat_id = _options.ChatId,
+ text = message
+ };
+
+ var content = new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json");
+
+ try
+ {
+ await _httpClient.PostAsync(url, content);
+ }
+ catch (Exception ex)
+ {
+ OnException?.Invoke(this, ex);
+ }
+ }
+ }
+}
diff --git a/EonaCat.Logger/EonaCatCoreLogger/TelegramLoggerOptions.cs b/EonaCat.Logger/EonaCatCoreLogger/TelegramLoggerOptions.cs
new file mode 100644
index 0000000..eb682ba
--- /dev/null
+++ b/EonaCat.Logger/EonaCatCoreLogger/TelegramLoggerOptions.cs
@@ -0,0 +1,9 @@
+namespace EonaCat.Logger.EonaCatCoreLogger
+{
+ public class TelegramLoggerOptions
+ {
+ public string BotToken { get; set; }
+ public string ChatId { get; set; }
+ public bool IsEnabled { get; set; } = true;
+ }
+}
diff --git a/EonaCat.Logger/EonaCatCoreLogger/TelegramLoggerProvider.cs b/EonaCat.Logger/EonaCatCoreLogger/TelegramLoggerProvider.cs
new file mode 100644
index 0000000..09e4216
--- /dev/null
+++ b/EonaCat.Logger/EonaCatCoreLogger/TelegramLoggerProvider.cs
@@ -0,0 +1,24 @@
+using Microsoft.Extensions.Logging;
+
+namespace EonaCat.Logger.EonaCatCoreLogger
+{
+ public class TelegramLoggerProvider : ILoggerProvider
+ {
+ private readonly TelegramLoggerOptions _options;
+
+ public TelegramLoggerProvider(TelegramLoggerOptions options)
+ {
+ _options = options;
+ }
+
+ public ILogger CreateLogger(string categoryName)
+ {
+ return new TelegramLogger(categoryName, _options);
+ }
+
+ public void Dispose()
+ {
+ // Nothing to dispose
+ }
+ }
+}
diff --git a/README.md b/README.md
index dcf8584..65b5fd4 100644
--- a/README.md
+++ b/README.md
@@ -380,6 +380,81 @@ private void LogHelper_OnException(object sender, ErrorMessage e)
}
```
+Example of Discord Logging:
+```csharp
+loggingBuilder.AddEonaCatDiscordLogger(options =>
+{
+ options.WebhookUrl = "https://discord.com/api/webhooks/your_webhook_here";
+});
+```
+
+Example of Slack Logging:
+```csharp
+loggingBuilder.AddEonaCatSlackLogger(options =>
+{
+ options.WebhookUrl = "https://hooks.slack.com/services/your_webhook_here";
+});
+
+```
+
+Example of DatabaseLogging:
+
+Create the table:
+```csharp
+CREATE TABLE Logs (
+ Id INT IDENTITY(1,1) PRIMARY KEY, -- SERIAL for PostgreSQL, AUTO_INCREMENT for MySQL
+ Timestamp DATETIME NOT NULL,
+ LogLevel NVARCHAR(50),
+ Category NVARCHAR(255),
+ Message NVARCHAR(MAX),
+ Exception NVARCHAR(MAX),
+ CorrelationId NVARCHAR(255)
+);
+```
+
+MySQL:
+```csharp
+using MySql.Data.MySqlClient;
+
+loggingBuilder.AddEonaCatDatabaseLogger(options =>
+{
+ options.DbProviderFactory = MySqlClientFactory.Instance;
+ options.ConnectionString = "Server=localhost;Database=EonaCatLogs;User=root;Password=yourpassword;";
+});
+```
+
+SQL Server:
+```csharp
+using Microsoft.Data.SqlClient;
+
+loggingBuilder.AddEonaCatDatabaseLogger(options =>
+{
+ options.DbProviderFactory = SqlClientFactory.Instance;
+ options.ConnectionString = "Server=localhost;Database=EonaCatLogs;User Id=sa;Password=yourpassword;";
+});
+```
+
+PostgreSQL:
+```csharp
+using Npgsql;
+
+loggingBuilder.AddEonaCatDatabaseLogger(options =>
+{
+ options.DbProviderFactory = NpgsqlFactory.Instance;
+ options.ConnectionString = "Host=localhost;Database=EonaCatLogs;Username=postgres;Password=yourpassword;";
+});
+```
+
+Example of batching database logging:
+```csharp
+loggingBuilder.AddEonaCatBatchingDatabaseLogger(options =>
+{
+ options.DbProviderFactory = MySql.Data.MySqlClient.MySqlClientFactory.Instance;
+ options.ConnectionString = "Server=localhost;Database=EonaCatLogs;User=root;Password=yourpassword;";
+ options.BatchInterval = TimeSpan.FromSeconds(10); // Flush every 10 seconds
+});
+```
+
Example of adding custom context to the log messages:
```csharp