From b50ab3784c84373086edd2a4fcc8c01cf10a663f Mon Sep 17 00:00:00 2001 From: EonaCat Date: Sat, 26 Apr 2025 10:56:32 +0200 Subject: [PATCH] Added multiple providers --- EonaCat.Logger/EonaCat.Logger.csproj | 4 +- .../BatchingDatabaseLogger.cs | 187 ++++++++++++++++++ .../BatchingDatabaseLoggerOptions.cs | 20 ++ .../BatchingDatabaseLoggerProvider.cs | 24 +++ .../EonaCatCoreLogger/DatabaseLogger.cs | 83 ++++++++ .../DatabaseLoggerOptions.cs | 17 ++ .../DatabaseLoggerProvider.cs | 24 +++ .../EonaCatCoreLogger/DiscordLogger.cs | 80 ++++++++ .../EonaCatCoreLogger/DiscordLoggerOptions.cs | 9 + .../DiscordLoggerProvider.cs | 24 +++ ...BatchingDatabaseLoggerFactoryExtensions.cs | 18 ++ .../DatabaseLoggerFactoryExtensions.cs | 18 ++ .../DiscordLoggerFactoryExtensions.cs | 18 ++ .../SlackLoggerFactoryExtensions.cs | 18 ++ .../TelegramLoggerFactoryExtensions.cs | 18 ++ .../EonaCatCoreLogger/FileLoggerProvider.cs | 2 - .../EonaCatCoreLogger/SlackLogger.cs | 83 ++++++++ .../EonaCatCoreLogger/SlackLoggerOptions.cs | 13 ++ .../EonaCatCoreLogger/SlackLoggerProvider.cs | 27 +++ .../EonaCatCoreLogger/TelegramLogger.cs | 57 ++++++ .../TelegramLoggerOptions.cs | 9 + .../TelegramLoggerProvider.cs | 24 +++ README.md | 75 +++++++ 23 files changed, 848 insertions(+), 4 deletions(-) create mode 100644 EonaCat.Logger/EonaCatCoreLogger/BatchingDatabaseLogger.cs create mode 100644 EonaCat.Logger/EonaCatCoreLogger/BatchingDatabaseLoggerOptions.cs create mode 100644 EonaCat.Logger/EonaCatCoreLogger/BatchingDatabaseLoggerProvider.cs create mode 100644 EonaCat.Logger/EonaCatCoreLogger/DatabaseLogger.cs create mode 100644 EonaCat.Logger/EonaCatCoreLogger/DatabaseLoggerOptions.cs create mode 100644 EonaCat.Logger/EonaCatCoreLogger/DatabaseLoggerProvider.cs create mode 100644 EonaCat.Logger/EonaCatCoreLogger/DiscordLogger.cs create mode 100644 EonaCat.Logger/EonaCatCoreLogger/DiscordLoggerOptions.cs create mode 100644 EonaCat.Logger/EonaCatCoreLogger/DiscordLoggerProvider.cs create mode 100644 EonaCat.Logger/EonaCatCoreLogger/Extensions/BatchingDatabaseLoggerFactoryExtensions.cs create mode 100644 EonaCat.Logger/EonaCatCoreLogger/Extensions/DatabaseLoggerFactoryExtensions.cs create mode 100644 EonaCat.Logger/EonaCatCoreLogger/Extensions/DiscordLoggerFactoryExtensions.cs create mode 100644 EonaCat.Logger/EonaCatCoreLogger/Extensions/SlackLoggerFactoryExtensions.cs create mode 100644 EonaCat.Logger/EonaCatCoreLogger/Extensions/TelegramLoggerFactoryExtensions.cs create mode 100644 EonaCat.Logger/EonaCatCoreLogger/SlackLogger.cs create mode 100644 EonaCat.Logger/EonaCatCoreLogger/SlackLoggerOptions.cs create mode 100644 EonaCat.Logger/EonaCatCoreLogger/SlackLoggerProvider.cs create mode 100644 EonaCat.Logger/EonaCatCoreLogger/TelegramLogger.cs create mode 100644 EonaCat.Logger/EonaCatCoreLogger/TelegramLoggerOptions.cs create mode 100644 EonaCat.Logger/EonaCatCoreLogger/TelegramLoggerProvider.cs 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