diff --git a/.gitignore b/.gitignore
index 7e2e97c..1998960 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,7 +2,7 @@
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
##
-## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore
+## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
# User-specific files
*.rsuser
@@ -83,8 +83,6 @@ StyleCopReport.xml
*.pgc
*.pgd
*.rsp
-# but not Directory.Build.rsp, as it configures directory-level build defaults
-!Directory.Build.rsp
*.sbr
*.tlb
*.tli
@@ -209,6 +207,9 @@ PublishScripts/
*.nuget.props
*.nuget.targets
+# Nuget personal access tokens and Credentials
+nuget.config
+
# Microsoft Azure Build Output
csx/
*.build.csdef
@@ -297,17 +298,6 @@ node_modules/
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
*.vbw
-# Visual Studio 6 auto-generated project file (contains which files were open etc.)
-*.vbp
-
-# Visual Studio 6 workspace and project file (working project files containing files to include in project)
-*.dsw
-*.dsp
-
-# Visual Studio 6 technical files
-*.ncb
-*.aps
-
# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
@@ -364,9 +354,6 @@ ASALocalRun/
# Local History for Visual Studio
.localhistory/
-# Visual Studio History (VSHistory) files
-.vshistory/
-
# BeatPulse healthcheck temp database
healthchecksdb
@@ -398,6 +385,7 @@ FodyWeavers.xsd
*.msp
# JetBrains Rider
+.idea/
*.sln.iml
# ---> VisualStudioCode
@@ -406,11 +394,8 @@ FodyWeavers.xsd
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
-!.vscode/*.code-snippets
+*.code-workspace
# Local History for Visual Studio Code
.history/
-# Built Visual Studio Code Extensions
-*.vsix
-
diff --git a/ConsoleApp1/ConsoleApp1.csproj b/ConsoleApp1/ConsoleApp1.csproj
new file mode 100644
index 0000000..22bf901
--- /dev/null
+++ b/ConsoleApp1/ConsoleApp1.csproj
@@ -0,0 +1,14 @@
+
+
+
+ Exe
+ net9
+ enable
+ enable
+
+
+
+
+
+
+
diff --git a/ConsoleApp1/Program.cs b/ConsoleApp1/Program.cs
new file mode 100644
index 0000000..6d6fc0c
--- /dev/null
+++ b/ConsoleApp1/Program.cs
@@ -0,0 +1,31 @@
+using EonaCat.LogStack.Configuration;
+using EonaCat.LogStack.Core;
+
+var logger = new LogBuilder("MyApp")
+ .WithMinimumLevel(LogLevel.Information)
+ .WriteToConsole()
+ .WriteToFile("C:\\tesss", maxFileSize: 50 * 1024 * 1024)
+ //.WriteToJsonFile("./logs", maxFileSize: 50 * 1024 * 1024)
+ //.WriteToHttp("https://127.0.0.1")
+ //.WriteToUdp("127.0.0.1", 514)
+ //.WriteToTcp("127.0.0.1", 514)
+ //.WriteToDatabase(null)
+ //.WriteToDiscord("https://discord.com/api/webhooks/...")
+ //.WriteToMicrosoftTeams("https://outlook.office.com/webhook/...")
+ //.WriteToElasticSearch("http://localhost:9200/logs")
+ //.WriteToGraylogFlow(null)
+ //.WriteToZabbixFlow(null)
+ .BoostWithCorrelationId()
+ .BoostWithProcessId()
+ .Build();
+
+
+
+while (true)
+{
+ logger.Information("Application started");
+ logger.Error(new Exception("Nerd!"), "Something went wrong");
+ await Task.Delay(1);
+}
+
+await logger.DisposeAsync(); // Flushes all logs
\ No newline at end of file
diff --git a/EonaCat.LogStack.LogClient/EonaCat.LogStack.LogClient.csproj b/EonaCat.LogStack.LogClient/EonaCat.LogStack.LogClient.csproj
new file mode 100644
index 0000000..3bfd282
--- /dev/null
+++ b/EonaCat.LogStack.LogClient/EonaCat.LogStack.LogClient.csproj
@@ -0,0 +1,39 @@
+
+
+ netstandard2.1
+ true
+ EonaCat.LogStack.LogClient
+ 0.0.1
+ EonaCat (Jeroen Saey)
+ Logging client for the EonaCat Logger LogServer LogStack
+ logging;monitoring;analytics;diagnostics
+ EonaCat (Jeroen Saey)
+ icon.png
+ readme.md
+ https://git.saey.me/EonaCat/EonaCat.LogStack.LogClient
+ git
+ LICENSE
+
+
+
+ True
+ \
+
+
+ True
+ \
+
+
+
+
+
+
+
+
+
+
+ True
+ \
+
+
+
\ No newline at end of file
diff --git a/EonaCat.LogStack.LogClient/LogCentralClient.cs b/EonaCat.LogStack.LogClient/LogCentralClient.cs
new file mode 100644
index 0000000..7847037
--- /dev/null
+++ b/EonaCat.LogStack.LogClient/LogCentralClient.cs
@@ -0,0 +1,204 @@
+using EonaCat.LogStack.LogClient.Models;
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net.Http;
+using System.Net.Http.Json;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace EonaCat.LogStack.LogClient
+{
+ public class LogCentralClient : IDisposable
+ {
+ private readonly HttpClient _httpClient;
+ private readonly LogCentralOptions _options;
+ private readonly ConcurrentQueue _logQueue;
+ private readonly Timer _flushTimer;
+ private readonly SemaphoreSlim _flushSemaphore;
+ private bool _disposed;
+
+ public LogCentralClient(LogCentralOptions options)
+ {
+ _options = options ?? throw new ArgumentNullException(nameof(options));
+ _httpClient = new HttpClient { BaseAddress = new Uri(_options.ServerUrl) };
+ _httpClient.DefaultRequestHeaders.Add("X-API-Key", _options.ApiKey);
+ _logQueue = new ConcurrentQueue();
+ _flushSemaphore = new SemaphoreSlim(1, 1);
+ _flushTimer = new Timer(async _ => await FlushAsync(), null,
+ TimeSpan.FromSeconds(_options.FlushIntervalSeconds),
+ TimeSpan.FromSeconds(_options.FlushIntervalSeconds));
+ }
+
+ public async Task LogAsync(LogEntry entry)
+ {
+ entry.ApplicationName = _options.ApplicationName;
+ entry.ApplicationVersion = _options.ApplicationVersion;
+ entry.Environment = _options.Environment;
+ entry.Timestamp = DateTime.UtcNow;
+
+ entry.MachineName ??= Environment.MachineName;
+ entry.Category ??= entry.Category ?? "Default";
+ entry.Message ??= entry.Message ?? "";
+
+ _logQueue.Enqueue(entry);
+
+ if (_logQueue.Count >= _options.BatchSize)
+ {
+ await FlushAsync();
+ }
+ }
+
+ public async Task LogExceptionAsync(Exception ex, string message = "",
+ Dictionary? properties = null)
+ {
+ await LogAsync(new LogEntry
+ {
+ Level = (int)LogLevel.Error,
+ Category = "Exception",
+ Message = message,
+ Exception = ex.ToString(),
+ StackTrace = ex.StackTrace,
+ Properties = properties
+ });
+ }
+
+ public async Task LogSecurityEventAsync(string eventType, string message,
+ Dictionary? properties = null)
+ {
+ await LogAsync(new LogEntry
+ {
+ Level = (int)LogLevel.Security,
+ Category = "Security",
+ Message = $"[{eventType}] {message}",
+ Properties = properties
+ });
+ }
+
+ public async Task LogAnalyticsAsync(string eventName,
+ Dictionary? properties = null)
+ {
+ await LogAsync(new LogEntry
+ {
+ Level = (int)LogLevel.Analytics,
+ Category = "Analytics",
+ Message = eventName,
+ Properties = properties
+ });
+ }
+
+ private async Task FlushAsync()
+ {
+ if (_logQueue.IsEmpty)
+ {
+ return;
+ }
+
+ await _flushSemaphore.WaitAsync();
+ try
+ {
+ var batch = new List();
+ while (batch.Count < _options.BatchSize && _logQueue.TryDequeue(out var entry))
+ {
+ batch.Add(entry);
+ }
+
+ if (batch.Count > 0)
+ {
+ await SendBatchAsync(batch);
+ }
+ }
+ finally
+ {
+ _flushSemaphore.Release();
+ }
+ }
+
+ private async Task SendBatchAsync(List entries)
+ {
+ try
+ {
+ // Map EF entities to DTOs for API
+ var dtos = entries.Select(e => new LogEntryDto
+ {
+ Id = e.Id,
+ Timestamp = e.Timestamp,
+ ApplicationName = e.ApplicationName,
+ ApplicationVersion = e.ApplicationVersion,
+ Environment = e.Environment,
+ MachineName = e.MachineName,
+ Level = e.Level,
+ Category = e.Category,
+ Message = e.Message,
+ Exception = e.Exception,
+ StackTrace = e.StackTrace,
+ Properties = e.Properties,
+ UserId = e.UserId,
+ SessionId = e.SessionId,
+ RequestId = e.RequestId,
+ CorrelationId = e.CorrelationId
+ }).ToList();
+
+ var response = await _httpClient.PostAsJsonAsync("/api/logs/batch", dtos);
+ response.EnsureSuccessStatusCode();
+ }
+ catch (Exception ex)
+ {
+ if (_options.EnableFallbackLogging)
+ {
+ Console.WriteLine($"[LogCentral] Failed to send logs: {ex.Message}");
+ }
+
+ foreach (var entry in entries)
+ {
+ _logQueue.Enqueue(entry);
+ }
+ }
+ }
+
+
+ public async Task FlushAndDisposeAsync()
+ {
+ await FlushAsync();
+ Dispose();
+ }
+
+ public void Dispose()
+ {
+ if (_disposed)
+ {
+ return;
+ }
+
+ _flushTimer?.Dispose();
+ FlushAsync().GetAwaiter().GetResult();
+ _httpClient?.Dispose();
+ _flushSemaphore?.Dispose();
+ _disposed = true;
+ GC.SuppressFinalize(this);
+ }
+ }
+
+ public class LogEntryDto
+ {
+ public string Id { get; set; } = Guid.NewGuid().ToString();
+ public DateTime Timestamp { get; set; }
+ public string ApplicationName { get; set; } = default!;
+ public string ApplicationVersion { get; set; } = default!;
+ public string Environment { get; set; } = default!;
+ public string MachineName { get; set; } = default!;
+ public int Level { get; set; }
+ public string Category { get; set; } = default!;
+ public string Message { get; set; } = default!;
+ public string? Exception { get; set; }
+ public string? StackTrace { get; set; }
+ public Dictionary? Properties { get; set; }
+ public string? UserId { get; set; }
+ public string? SessionId { get; set; }
+ public string? RequestId { get; set; }
+ public string? CorrelationId { get; set; }
+ }
+
+}
diff --git a/EonaCat.LogStack.LogClient/LogCentralEonaCatAdapter.cs b/EonaCat.LogStack.LogClient/LogCentralEonaCatAdapter.cs
new file mode 100644
index 0000000..622866d
--- /dev/null
+++ b/EonaCat.LogStack.LogClient/LogCentralEonaCatAdapter.cs
@@ -0,0 +1,60 @@
+using EonaCat.LogStack.Configuration;
+using EonaCat.LogStack.LogClient.Models;
+using System;
+using System.Collections.Generic;
+
+namespace EonaCat.LogStack.LogClient
+{
+ public class LogCentralEonaCatAdapter : IDisposable
+ {
+ private readonly LogCentralClient _client;
+ private LogBuilder _logBuilder;
+
+ public LogCentralEonaCatAdapter(LogBuilder logBuilder, LogCentralClient client)
+ {
+ _client = client;
+ _logBuilder.OnLog += LogSettings_OnLog;
+ }
+
+ private void LogSettings_OnLog(object sender, LogMessage e)
+ {
+ var entry = new LogEntry
+ {
+ Level = (int)MapLogLevel(e.Level),
+ Category = e.Category ?? "General",
+ Message = e.Message,
+ Properties = new Dictionary
+ {
+ { "Source", e.Origin ?? "Unknown" }
+ }
+ };
+
+ if (e.Exception != null)
+ {
+ entry.Exception = e.Exception.ToString();
+ entry.StackTrace = e.Exception.StackTrace;
+ }
+
+ _client.LogAsync(entry).ConfigureAwait(false);
+ }
+ private static LogLevel MapLogLevel(Core.LogLevel logType)
+ {
+ return logType switch
+ {
+ Core.LogLevel.Trace => LogLevel.Trace,
+ Core.LogLevel.Debug => LogLevel.Debug,
+ Core.LogLevel.Information => LogLevel.Information,
+ Core.LogLevel.Warning => LogLevel.Warning,
+ Core.LogLevel.Error => LogLevel.Error,
+ Core.LogLevel.Critical => LogLevel.Critical,
+ _ => LogLevel.Information
+ };
+ }
+
+ public void Dispose()
+ {
+ _logBuilder.OnLog -= LogSettings_OnLog;
+ GC.SuppressFinalize(this);
+ }
+ }
+}
diff --git a/EonaCat.LogStack.LogClient/LogCentralOptions.cs b/EonaCat.LogStack.LogClient/LogCentralOptions.cs
new file mode 100644
index 0000000..26f5a0e
--- /dev/null
+++ b/EonaCat.LogStack.LogClient/LogCentralOptions.cs
@@ -0,0 +1,18 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace EonaCat.LogStack.LogClient
+{
+ public class LogCentralOptions
+ {
+ public string ServerUrl { get; set; } = "http://localhost:5000";
+ public string ApiKey { get; set; } = string.Empty;
+ public string ApplicationName { get; set; } = string.Empty;
+ public string ApplicationVersion { get; set; } = "1.0.0";
+ public string Environment { get; set; } = "Production";
+ public int BatchSize { get; set; } = 50;
+ public int FlushIntervalSeconds { get; set; } = 5;
+ public bool EnableFallbackLogging { get; set; } = true;
+ }
+}
diff --git a/EonaCat.LogStack.LogClient/LogLevel.cs b/EonaCat.LogStack.LogClient/LogLevel.cs
new file mode 100644
index 0000000..dabe0b8
--- /dev/null
+++ b/EonaCat.LogStack.LogClient/LogLevel.cs
@@ -0,0 +1,15 @@
+namespace EonaCat.LogStack.LogClient
+{
+ public enum LogLevel
+ {
+ Trace = 0,
+ Debug = 1,
+ Information = 2,
+ Warning = 3,
+ Error = 4,
+ Critical = 5,
+ Traffic = 6,
+ Security = 7,
+ Analytics = 8
+ }
+}
diff --git a/EonaCat.LogStack.LogClient/Models/LogEntry.cs b/EonaCat.LogStack.LogClient/Models/LogEntry.cs
new file mode 100644
index 0000000..5a21db2
--- /dev/null
+++ b/EonaCat.LogStack.LogClient/Models/LogEntry.cs
@@ -0,0 +1,64 @@
+using EonaCat.Json;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations.Schema;
+using System.Text;
+
+namespace EonaCat.LogStack.LogClient.Models
+{
+ public class LogEntry
+ {
+ public string Id { get; set; } = Guid.NewGuid().ToString();
+
+ public DateTime Timestamp { get; set; }
+
+ public string ApplicationName { get; set; } = default!;
+ public string ApplicationVersion { get; set; } = default!;
+ public string Environment { get; set; } = default!;
+ public string MachineName { get; set; } = default!;
+ public int Level { get; set; }
+ public string Category { get; set; } = default!;
+ public string Message { get; set; } = default!;
+
+ public string? Exception { get; set; }
+ public string? StackTrace { get; set; }
+
+ [Column(TypeName = "TEXT")]
+ public string? PropertiesJson { get; set; }
+
+ [NotMapped]
+ public Dictionary? Properties
+ {
+ get => string.IsNullOrEmpty(PropertiesJson)
+ ? null
+ : JsonHelper.ToObject>(PropertiesJson);
+ set => PropertiesJson = value == null ? null : JsonHelper.ToJson(value);
+ }
+
+ public string? UserId { get; set; }
+ public string? SessionId { get; set; }
+ public string? RequestId { get; set; }
+ public string? CorrelationId { get; set; }
+
+
+ public static LogEntryDto ToDto(LogEntry entry) => new LogEntryDto()
+ {
+ Id = entry.Id,
+ Timestamp = entry.Timestamp,
+ ApplicationName = entry.ApplicationName,
+ ApplicationVersion = entry.ApplicationVersion,
+ Environment = entry.Environment,
+ MachineName = entry.MachineName,
+ Level = entry.Level,
+ Category = entry.Category,
+ Message = entry.Message,
+ Exception = entry.Exception,
+ StackTrace = entry.StackTrace,
+ Properties = entry.Properties,
+ UserId = entry.UserId,
+ SessionId = entry.SessionId,
+ RequestId = entry.RequestId,
+ CorrelationId = entry.CorrelationId
+ };
+ }
+}
diff --git a/EonaCat.LogStack.LogClient/readme.md b/EonaCat.LogStack.LogClient/readme.md
new file mode 100644
index 0000000..24d3a5d
--- /dev/null
+++ b/EonaCat.LogStack.LogClient/readme.md
@@ -0,0 +1,389 @@
+# EonaCat.LogStack.LogClient
+
+### Client Installation
+
+#### Via NuGet Package Manager:
+```bash
+dotnet add package EonaCat.LogStack.LogClient
+```
+
+#### Via Package Manager Console:
+```powershell
+Install-Package EonaCat.LogStack.LogClient
+```
+
+## 📖 Usage Examples
+
+### Basic Setup
+
+```csharp
+using EonaCat.LogStack.LogClient;
+using EonaCat.LogStack.LogClient.Models;
+
+// Configure the client
+var options = new LogCentralOptions
+{
+ ServerUrl = "https://your-logcentral-server.com",
+ ApiKey = "your-api-key-here",
+ ApplicationName = "MyAwesomeApp",
+ ApplicationVersion = "1.0.0",
+ Environment = "Production",
+ BatchSize = 50,
+ FlushIntervalSeconds = 5
+};
+
+var logClient = new LogCentralClient(options);
+```
+
+### Integration with EonaCat.LogStack
+
+```csharp
+using EonaCat.LogStack;
+using EonaCat.LogStack.LogClient.Integration;
+
+var loggerSettings = new LoggerSettings();
+loggerSettings.UseLocalTime = true;
+loggerSettings.Id = "TEST";
+var logger = new LogManager(loggerSettings);
+
+// Create the adapter
+var adapter = new LogCentralEonaCatAdapter(loggerSettings, logClient);
+
+// Now all EonaCat.LogStack logs will be sent to LogCentral automatically
+logger.Log("Application started", LogLevel.Info);
+logger.Log("User logged in", LogLevel.Info, "Authentication");
+```
+
+### Manual Logging
+
+```csharp
+// Simple log
+await logClient.LogAsync(new LogEntry
+{
+ Level = LogLevel.Information,
+ Category = "Startup",
+ Message = "Application started successfully"
+});
+
+// Log with properties
+await logClient.LogAsync(new LogEntry
+{
+ Level = LogLevel.Information,
+ Category = "UserAction",
+ Message = "User performed action",
+ UserId = "user123",
+ Properties = new Dictionary
+ {
+ ["Action"] = "Purchase",
+ ["Amount"] = 99.99,
+ ["ProductId"] = "prod-456"
+ }
+});
+
+// Log exception
+try
+{
+ // Your code
+ throw new Exception("Something went wrong");
+}
+catch (Exception ex)
+{
+ await logClient.LogExceptionAsync(ex, "Error processing order",
+ new Dictionary
+ {
+ ["OrderId"] = "12345",
+ ["CustomerId"] = "cust-789"
+ });
+}
+```
+
+### Security Event Logging
+
+```csharp
+await logClient.LogSecurityEventAsync(
+ "LoginAttempt",
+ "Failed login attempt detected",
+ new Dictionary
+ {
+ ["Username"] = "admin",
+ ["IPAddress"] = "192.168.1.100",
+ ["Attempts"] = 5
+ }
+);
+
+await logClient.LogSecurityEventAsync(
+ "UnauthorizedAccess",
+ "Unauthorized API access attempt",
+ new Dictionary
+ {
+ ["Endpoint"] = "/api/admin/users",
+ ["Method"] = "DELETE",
+ ["UserId"] = "user456"
+ }
+);
+```
+
+### Analytics Logging
+
+```csharp
+// Track user events
+await logClient.LogAnalyticsAsync("PageView",
+ new Dictionary
+ {
+ ["Page"] = "/products/electronics",
+ ["Duration"] = 45.2,
+ ["Source"] = "Google"
+ }
+);
+
+await logClient.LogAnalyticsAsync("Purchase",
+ new Dictionary
+ {
+ ["ProductId"] = "prod-123",
+ ["Price"] = 299.99,
+ ["Category"] = "Electronics",
+ ["PaymentMethod"] = "CreditCard"
+ }
+);
+
+await logClient.LogAnalyticsAsync("FeatureUsage",
+ new Dictionary
+ {
+ ["Feature"] = "DarkMode",
+ ["Enabled"] = true,
+ ["Platform"] = "iOS"
+ }
+);
+```
+
+### ASP.NET Core Integration
+
+```csharp
+// Program.cs or Startup.cs
+public class Program
+{
+ public static void Main(string[] args)
+ {
+ var builder = WebApplication.CreateBuilder(args);
+
+ // Register LogCentral
+ var logCentralOptions = new LogCentralOptions
+ {
+ ServerUrl = builder.Configuration["LogCentral:ServerUrl"],
+ ApiKey = builder.Configuration["LogCentral:ApiKey"],
+ ApplicationName = "MyWebApp",
+ ApplicationVersion = "1.0.0",
+ Environment = builder.Environment.EnvironmentName
+ };
+
+ var logClient = new LogCentralClient(logCentralOptions);
+ builder.Services.AddSingleton(logClient);
+
+ var app = builder.Build();
+
+ // Use middleware to log requests
+ app.Use(async (context, next) =>
+ {
+ var requestId = Guid.NewGuid().ToString();
+
+ await logClient.LogAsync(new LogEntry
+ {
+ Level = LogLevel.Information,
+ Category = "HTTP",
+ Message = $"{context.Request.Method} {context.Request.Path}",
+ RequestId = requestId,
+ Properties = new Dictionary
+ {
+ ["Method"] = context.Request.Method,
+ ["Path"] = context.Request.Path.Value,
+ ["QueryString"] = context.Request.QueryString.Value
+ }
+ });
+
+ await next();
+ });
+
+ app.Run();
+ }
+}
+```
+
+### Windows Service / Console App
+
+```csharp
+using EonaCat.LogStack.LogClient;
+using Microsoft.Extensions.Hosting;
+
+public class Worker : BackgroundService
+{
+ private readonly LogCentralClient _logClient;
+
+ public Worker(LogCentralClient logClient)
+ {
+ _logClient = logClient;
+ }
+
+ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
+ {
+ await _logClient.LogAsync(new LogEntry
+ {
+ Level = LogLevel.Information,
+ Category = "Service",
+ Message = "Worker service started"
+ });
+
+ while (!stoppingToken.IsCancellationRequested)
+ {
+ try
+ {
+ // Your work here
+ await Task.Delay(1000, stoppingToken);
+ }
+ catch (Exception ex)
+ {
+ await _logClient.LogExceptionAsync(ex, "Error in worker");
+ }
+ }
+
+ await _logClient.FlushAndDisposeAsync();
+ }
+}
+```
+
+### WPF / WinForms Application
+
+```csharp
+public partial class MainWindow : Window
+{
+ private readonly LogCentralClient _logClient;
+
+ public MainWindow()
+ {
+ InitializeComponent();
+
+ _logClient = new LogCentralClient(new LogCentralOptions
+ {
+ ServerUrl = "https://logs.mycompany.com",
+ ApiKey = "your-api-key",
+ ApplicationName = "MyDesktopApp",
+ ApplicationVersion = Assembly.GetExecutingAssembly().GetName().Version.ToString(),
+ Environment = "Production"
+ });
+
+ Application.Current.DispatcherUnhandledException += OnUnhandledException;
+ }
+
+ private async void OnUnhandledException(object sender, DispatcherUnhandledExceptionEventArgs e)
+ {
+ await _logClient.LogExceptionAsync(e.Exception, "Unhandled exception in UI");
+ e.Handled = true;
+ }
+
+ protected override async void OnClosing(CancelEventArgs e)
+ {
+ await _logClient.FlushAndDisposeAsync();
+ base.OnClosing(e);
+ }
+}
+```
+
+## 🎯 Advanced Features
+
+### Correlation IDs for Distributed Tracing
+
+```csharp
+var correlationId = Guid.NewGuid().ToString();
+
+await logClient.LogAsync(new LogEntry
+{
+ Level = LogLevel.Information,
+ Category = "OrderProcessing",
+ Message = "Order created",
+ CorrelationId = correlationId,
+ Properties = new Dictionary { ["OrderId"] = "12345" }
+});
+
+// In another service
+await logClient.LogAsync(new LogEntry
+{
+ Level = LogLevel.Information,
+ Category = "PaymentProcessing",
+ Message = "Payment processed",
+ CorrelationId = correlationId, // Same ID
+ Properties = new Dictionary { ["Amount"] = 99.99 }
+});
+```
+
+### Performance Monitoring
+
+```csharp
+var stopwatch = Stopwatch.StartNew();
+
+try
+{
+ // Your operation
+ await SomeSlowOperation();
+}
+finally
+{
+ stopwatch.Stop();
+
+ await logClient.LogAsync(new LogEntry
+ {
+ Level = LogLevel.Information,
+ Category = "Performance",
+ Message = "Operation completed",
+ Properties = new Dictionary
+ {
+ ["Operation"] = "DatabaseQuery",
+ ["DurationMs"] = stopwatch.ElapsedMilliseconds,
+ ["Status"] = "Success"
+ }
+ });
+}
+```
+
+## 📊 Dashboard Features
+
+- **Real-time monitoring**: Auto-refreshes every 30 seconds
+- **Advanced search**: Full-text search across all log fields
+- **Filtering**: By application, environment, level, date range
+- **Charts**: Visual representation of log levels and trends
+- **Export**: Download logs as CSV or JSON
+- **Alerts**: Configure notifications for critical events (planned)
+
+## 🔒 Security Best Practices
+
+1. **Use HTTPS** for production deployments
+2. **Rotate API keys** regularly
+3. **Limit API key permissions** by application
+4. **Store API keys** in secure configuration (Azure Key Vault, AWS Secrets Manager)
+5. **Enable authentication** for dashboard access (add authentication middleware)
+
+## 🚀 Deployment
+
+### Docker Deployment
+
+```dockerfile
+FROM mcr.microsoft.com/dotnet/aspnet:8.0
+WORKDIR /app
+COPY --from=build /app/publish .
+ENTRYPOINT ["dotnet", "EonaCat.LogStack.LogServer.dll"]
+```
+
+### Azure Deployment
+
+```bash
+az webapp create --resource-group MyResourceGroup --plan MyPlan --name logcentral --runtime "DOTNETCORE:8.0"
+az webapp deployment source config-zip --resource-group MyResourceGroup --name logcentral --src logcentral.zip
+```
+
+## 📈 Scalability
+
+For high-volume applications:
+
+1. Use **Redis** for caching
+2. Implement **Elasticsearch** for faster searches
+3. Use **message queues** (RabbitMQ, Azure Service Bus) for async processing
+4. Partition database by date ranges
+5. Implement log archival and retention policies
\ No newline at end of file
diff --git a/EonaCat.LogStack.OpenTelemetryFlow/EonaCat.LogStack.OpenTelemetryFlow.csproj b/EonaCat.LogStack.OpenTelemetryFlow/EonaCat.LogStack.OpenTelemetryFlow.csproj
new file mode 100644
index 0000000..0724594
--- /dev/null
+++ b/EonaCat.LogStack.OpenTelemetryFlow/EonaCat.LogStack.OpenTelemetryFlow.csproj
@@ -0,0 +1,19 @@
+
+
+
+ netstandard2.1
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/EonaCat.LogStack.OpenTelemetryFlow/LogBuilder.cs b/EonaCat.LogStack.OpenTelemetryFlow/LogBuilder.cs
new file mode 100644
index 0000000..0cbe295
--- /dev/null
+++ b/EonaCat.LogStack.OpenTelemetryFlow/LogBuilder.cs
@@ -0,0 +1,33 @@
+using EonaCat.LogStack.Configuration;
+using EonaCat.LogStack.Core;
+using EonaCat.LogStack.Flows;
+using OpenTelemetry.Exporter;
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace EonaCat.LogStack.Flows.WindowsEventLog
+{
+ // 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 static class EonaCatLogStackExtensions
+ {
+ ///
+ /// Write to OpenTelemetry
+ ///
+ public static LogBuilder WriteToOpenTelemetry(this LogBuilder logBuilder,
+ string serviceName,
+ Uri endpoint,
+ OtlpExportProtocol protocol = OtlpExportProtocol.Grpc,
+ LogLevel minimumLevel = LogLevel.Trace)
+ {
+ logBuilder.AddFlow(new OpenTelemetryFlow(
+ serviceName,
+ endpoint,
+ protocol,
+ minimumLevel));
+ return logBuilder;
+ }
+ }
+}
diff --git a/EonaCat.LogStack.OpenTelemetryFlow/OpenTelemetryFlow.cs b/EonaCat.LogStack.OpenTelemetryFlow/OpenTelemetryFlow.cs
new file mode 100644
index 0000000..b1519d9
--- /dev/null
+++ b/EonaCat.LogStack.OpenTelemetryFlow/OpenTelemetryFlow.cs
@@ -0,0 +1,162 @@
+using EonaCat.LogStack.Core;
+using Microsoft.Extensions.Logging;
+using OpenTelemetry.Exporter;
+using OpenTelemetry.Logs;
+using OpenTelemetry.Resources;
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Threading;
+using System.Threading.Tasks;
+using LogLevel = EonaCat.LogStack.Core.LogLevel;
+
+namespace EonaCat.LogStack.Flows
+{
+ public sealed class OpenTelemetryFlow : FlowBase
+ {
+ private readonly ILoggerFactory _loggerFactory;
+ private readonly ILogger _logger;
+
+ public OpenTelemetryFlow(string serviceName, Uri endpoint, OtlpExportProtocol protocol = OtlpExportProtocol.Grpc, LogLevel minimumLevel = LogLevel.Trace) : base("OpenTelemetry:" + serviceName, minimumLevel)
+ {
+ if (string.IsNullOrWhiteSpace(serviceName))
+ {
+ throw new ArgumentNullException(nameof(serviceName));
+ }
+
+ if (endpoint == null)
+ {
+ throw new ArgumentNullException(nameof(endpoint));
+ }
+
+ _loggerFactory = LoggerFactory.Create(builder =>
+ {
+ builder.ClearProviders();
+
+ builder.AddOpenTelemetry(options =>
+ {
+ options.SetResourceBuilder(
+ ResourceBuilder.CreateDefault()
+ .AddService(serviceName)
+ .AddAttributes(new Dictionary
+ {
+ ["host.name"] = Environment.MachineName,
+ ["process.id"] = Process.GetCurrentProcess().Id
+ }));
+
+ options.AddOtlpExporter(otlp =>
+ {
+ otlp.Endpoint = endpoint;
+ otlp.Protocol = protocol;
+ });
+
+ options.IncludeScopes = true;
+ options.IncludeFormattedMessage = true;
+ options.ParseStateValues = true;
+ });
+ });
+
+ _logger = _loggerFactory.CreateLogger(serviceName);
+ }
+
+ public override Task BlastAsync(
+ LogEvent logEvent,
+ CancellationToken cancellationToken = default)
+ {
+ if (!IsEnabled || !IsLogLevelEnabled(logEvent))
+ {
+ return Task.FromResult(WriteResult.LevelFiltered);
+ }
+
+ WriteLog(logEvent);
+ Interlocked.Increment(ref BlastedCount);
+
+ return Task.FromResult(WriteResult.Success);
+ }
+
+ public override Task BlastBatchAsync(
+ ReadOnlyMemory logEvents,
+ CancellationToken cancellationToken = default)
+ {
+ if (!IsEnabled)
+ {
+ return Task.FromResult(WriteResult.FlowDisabled);
+ }
+
+ foreach (var e in logEvents.Span)
+ {
+ if (e.Level < MinimumLevel)
+ {
+ continue;
+ }
+
+ WriteLog(e);
+ Interlocked.Increment(ref BlastedCount);
+ }
+
+ return Task.FromResult(WriteResult.Success);
+ }
+
+ private void WriteLog(LogEvent log)
+ {
+ var state = new List>();
+
+ if (!string.IsNullOrEmpty(log.Category))
+ {
+ state.Add(new KeyValuePair("category", log.Category));
+ }
+
+ foreach (var prop in log.Properties)
+ {
+ state.Add(new KeyValuePair(prop.Key, prop.Value ?? "null"));
+ }
+
+ if (log.Exception != null)
+ {
+ state.Add(new KeyValuePair("exception.type", log.Exception.GetType().FullName));
+ state.Add(new KeyValuePair("exception.message", log.Exception.Message));
+ state.Add(new KeyValuePair("exception.stacktrace", log.Exception.StackTrace));
+ }
+
+ _logger.Log(
+ MapLevel(log.Level),
+ new EventId(0, log.Category),
+ state,
+ log.Exception,
+ (s, e) => log.Message.ToString());
+ }
+
+ private static Microsoft.Extensions.Logging.LogLevel MapLevel(LogLevel level)
+ {
+ return level switch
+ {
+ LogLevel.Trace => Microsoft.Extensions.Logging.LogLevel.Trace,
+ LogLevel.Debug => Microsoft.Extensions.Logging.LogLevel.Debug,
+ LogLevel.Information => Microsoft.Extensions.Logging.LogLevel.Information,
+ LogLevel.Warning => Microsoft.Extensions.Logging.LogLevel.Warning,
+ LogLevel.Error => Microsoft.Extensions.Logging.LogLevel.Error,
+ LogLevel.Critical => Microsoft.Extensions.Logging.LogLevel.Critical,
+ _ => Microsoft.Extensions.Logging.LogLevel.Information
+ };
+ }
+
+ public override async ValueTask DisposeAsync()
+ {
+ if (!IsEnabled)
+ {
+ return;
+ }
+
+ IsEnabled = false;
+
+ _loggerFactory?.Dispose();
+
+ await base.DisposeAsync().ConfigureAwait(false);
+ }
+
+ public override Task FlushAsync(CancellationToken cancellationToken = default)
+ {
+ return Task.CompletedTask;
+ }
+ }
+}
\ No newline at end of file
diff --git a/EonaCat.LogStack.SerilogTest/EonaCat.LogStack.SerilogTest.csproj b/EonaCat.LogStack.SerilogTest/EonaCat.LogStack.SerilogTest.csproj
new file mode 100644
index 0000000..af014cb
--- /dev/null
+++ b/EonaCat.LogStack.SerilogTest/EonaCat.LogStack.SerilogTest.csproj
@@ -0,0 +1,18 @@
+
+
+
+ Exe
+ net10.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
diff --git a/EonaCat.LogStack.SerilogTest/Program.cs b/EonaCat.LogStack.SerilogTest/Program.cs
new file mode 100644
index 0000000..62b59c9
--- /dev/null
+++ b/EonaCat.LogStack.SerilogTest/Program.cs
@@ -0,0 +1,197 @@
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.DataProtection;
+using Microsoft.Extensions.Hosting;
+using Serilog;
+using Serilog.Events;
+using Serilog.Formatting.Json;
+using System.Net.Sockets;
+using System.Runtime.InteropServices;
+using System.Text;
+
+var builder = WebApplication.CreateBuilder(args);
+
+//
+// LOGGER CONFIGURATION (Equivalent to LoggerSettings)
+//
+Log.Logger = new LoggerConfiguration()
+ .MinimumLevel.Verbose()
+ .Enrich.WithProperty("Id", "TEST")
+ .Enrich.WithProperty("AppName", "[JIJ BENT EEN BRASSER!]")
+ .WriteTo.Async(a => a.Console())
+ .WriteTo.Async(a => a.File(
+ path: "logs/web-.log",
+ rollingInterval: RollingInterval.Day,
+ fileSizeLimitBytes: 1_000_000,
+ rollOnFileSizeLimit: true,
+ retainedFileCountLimit: 5,
+ shared: true))
+ .WriteTo.Async(a => a.File(
+ new JsonFormatter(),
+ path: "logs/test.json",
+ rollingInterval: RollingInterval.Day))
+ //.WriteTo.Seq("http://localhost:5341") // central logging
+ .CreateLogger();
+
+builder.Services.AddDataProtection()
+ .PersistKeysToFileSystem(new DirectoryInfo(Path.Combine(Directory.GetCurrentDirectory(), "keys")))
+ .SetApplicationName("SerilogStressTest");
+
+builder.Services.AddRazorPages();
+
+builder.WebHost.ConfigureKestrel(options =>
+{
+ options.ListenAnyIP(6000);
+});
+
+var app = builder.Build();
+
+app.UseHttpsRedirection();
+app.UseStaticFiles();
+app.UseRouting();
+app.UseAuthorization();
+app.MapRazorPages();
+
+//
+// ==============================
+// 🔥 TESTS START HERE
+// ==============================
+//
+
+_ = Task.Run(RunLoggingTestsAsync);
+_ = Task.Run(RunWebLoggingTestsAsync);
+_ = Task.Run(RunLoggingExceptionTests);
+_ = Task.Run(RunWebLoggingExceptionTests);
+//_ = Task.Run(RunMemoryLeakTest);
+_ = Task.Run(RunTcpLoggerTest);
+
+app.Run();
+
+
+// =======================================================
+// 1️⃣ EXACT HIGH-SPEED FILE LOGGING LOOP
+// =======================================================
+async Task RunLoggingTestsAsync()
+{
+ for (var i = 0; i < 9_000_000; i++)
+ {
+ Log.Information("test to file {i} INFO", i);
+ Log.Fatal("test to file {i} CRITICAL", i);
+ Log.Debug("test to file {i} DEBUG", i);
+ Log.Error("test to file {i} ERROR", i);
+ Log.Verbose("test to file {i} TRACE", i);
+ Log.Warning("test to file {i} WARNING", i);
+
+ Console.WriteLine($"Logged: {i}");
+ await Task.Delay(1);
+ }
+}
+
+
+// =======================================================
+// 2️⃣ WEB LOGGER STRESS TEST
+// =======================================================
+async Task RunWebLoggingTestsAsync()
+{
+ int i = 0;
+
+ while (true)
+ {
+ i++;
+
+ Log.Information("web-test {i}", i);
+ Log.Debug("web-test {i}", i);
+ Log.Warning("web-test {i}", i);
+ Log.Error("web-test {i}", i);
+ Log.Verbose("web-test {i}", i);
+
+ await Task.Delay(1);
+ }
+}
+
+
+// =======================================================
+// 3️⃣ EXCEPTION TEST (FILE LOGGER)
+// =======================================================
+void RunLoggingExceptionTests()
+{
+ for (int i = 0; i < 10; i++)
+ {
+ try
+ {
+ throw new Exception($"Normal Exception {i}");
+ }
+ catch (Exception ex)
+ {
+ Log.Error(ex, "Exception {Index}", i);
+ Console.WriteLine($"Normal ExceptionLogged: {i}");
+ }
+ }
+}
+
+
+// =======================================================
+// 4️⃣ WEB EXCEPTION TEST
+// =======================================================
+void RunWebLoggingExceptionTests()
+{
+ for (int i = 0; i < 10; i++)
+ {
+ try
+ {
+ throw new Exception($"WebException {i}");
+ }
+ catch (Exception ex)
+ {
+ Log.Fatal(ex, "CRITICAL");
+ Log.Debug(ex, "DEBUG");
+ Log.Error(ex, "ERROR");
+ Log.Verbose(ex, "TRACE");
+ Log.Warning(ex, "WARNING");
+ Log.Information(ex, "INFORMATION");
+
+ Console.WriteLine($"WebExceptionLogged: {i}");
+ }
+ }
+}
+
+// =======================================================
+// 6️⃣ MEMORY LEAK TEST (IDENTICAL BEHAVIOR)
+// =======================================================
+async Task RunMemoryLeakTest()
+{
+ var managedLeak = new List();
+
+ while (true)
+ {
+ managedLeak.Add(new byte[5_000_000]); // 5MB
+ Marshal.AllocHGlobal(10_000_000); // 10MB unmanaged
+
+ await Task.Delay(500);
+ }
+}
+
+
+// =======================================================
+// 7️⃣ TCP LOGGER TEST
+// =======================================================
+async Task RunTcpLoggerTest()
+{
+ using var client = new TcpClient();
+
+ try
+ {
+ await client.ConnectAsync("192.168.1.1", 12345);
+
+ int i = 0;
+ while (true)
+ {
+ var message = Encoding.UTF8.GetBytes($"TCP log {++i}\n");
+ await client.GetStream().WriteAsync(message);
+ await Task.Delay(1000);
+ }
+ }
+ catch
+ {
+ Log.Warning("TCP server not reachable");
+ }
+}
diff --git a/EonaCat.LogStack.SerilogTest/Properties/launchSettings.json b/EonaCat.LogStack.SerilogTest/Properties/launchSettings.json
new file mode 100644
index 0000000..4837432
--- /dev/null
+++ b/EonaCat.LogStack.SerilogTest/Properties/launchSettings.json
@@ -0,0 +1,12 @@
+{
+ "profiles": {
+ "EonaCat.LogStack.SerilogTest": {
+ "commandName": "Project",
+ "launchBrowser": true,
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ },
+ "applicationUrl": "https://localhost:56815;http://localhost:56816"
+ }
+ }
+}
\ No newline at end of file
diff --git a/EonaCat.LogStack.SerilogTest/keys/key-ce36e671-3994-4686-93db-52c792888079.xml b/EonaCat.LogStack.SerilogTest/keys/key-ce36e671-3994-4686-93db-52c792888079.xml
new file mode 100644
index 0000000..3957f02
--- /dev/null
+++ b/EonaCat.LogStack.SerilogTest/keys/key-ce36e671-3994-4686-93db-52c792888079.xml
@@ -0,0 +1,16 @@
+
+
+ 2026-02-13T12:58:52.6395786Z
+ 2026-02-13T12:58:52.6395786Z
+ 2026-05-14T12:58:52.6395786Z
+
+
+
+
+
+
+ /V8LCH65h4jnYN0CNj+b+f/KcWTcYS7HEFlmIS8h/ryyTH5YEXlxLIWHxoZYbu6+vY7JXF3O+iDkdNnuW8BtFg==
+
+
+
+
\ No newline at end of file
diff --git a/EonaCat.LogStack.Server/EonaCat.Logger.Server.csproj b/EonaCat.LogStack.Server/EonaCat.Logger.Server.csproj
new file mode 100644
index 0000000..a347705
--- /dev/null
+++ b/EonaCat.LogStack.Server/EonaCat.Logger.Server.csproj
@@ -0,0 +1,28 @@
+
+
+
+ netstandard2.1
+ enable
+ True
+ EonaCat.LogStack.Server
+ EonaCat (Jeroen Saey)
+ EonaCat.LogStack.Server is a server for the logging library
+ EonaCat (Jeroen Saey)
+ https://www.nuget.org/packages/EonaCat.LogStack.Server/
+ icon.png
+ README.md
+ EonaCat;Logger;EonaCatLogStack;server;Log;Writer;Jeroen;Saey
+
+
+
+
+ True
+ \
+
+
+ True
+ \
+
+
+
+
diff --git a/EonaCat.LogStack.Server/Server.cs b/EonaCat.LogStack.Server/Server.cs
new file mode 100644
index 0000000..c84aa32
--- /dev/null
+++ b/EonaCat.LogStack.Server/Server.cs
@@ -0,0 +1,287 @@
+using System.IO;
+using System.Net.Sockets;
+using System.Net;
+using System.Text;
+using System.Threading.Tasks;
+using System.Threading;
+using System;
+using System.Linq;
+
+namespace EonaCat.LogStack.Server
+{
+ // 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 Server
+ {
+ private TcpListener _tcpListener;
+ private UdpClient _udpListener;
+ private CancellationTokenSource _cts;
+ private bool _isRunning;
+ private readonly bool _useUdp;
+ private const long MaxLogFileSize = 200 * 1024 * 1024; // 200MB log rollover limit
+ private readonly int _logRetentionDays; // Number of days to retain logs
+ private readonly long _maxLogDirectorySize; // Maximum allowed size of the logs directory
+ private const int UdpBufferSize = 65507; // Maximum UDP packet size (65507 bytes for UDP payload)
+
+ ///
+ /// EonaCat Log Server
+ ///
+ /// Determine if we need to start a udp server (default: true)
+ /// Max log retention days (default: 30)
+ /// Max log directory size (default: 10GB)
+ public Server(bool useUdp = true, int logRetentionDays = 30, long maxLogDirectorySize = 10L * 1024 * 1024 * 1024) // Default 10GB max directory size
+ {
+ _useUdp = useUdp;
+ _logRetentionDays = logRetentionDays;
+ _maxLogDirectorySize = maxLogDirectorySize;
+ }
+
+ protected virtual Task ProcessLogAsync(string logData)
+ {
+ string logsRootDirectory = "logs";
+
+ // Create root log directory if it doesn't exist
+ if (!Directory.Exists(logsRootDirectory))
+ {
+ Directory.CreateDirectory(logsRootDirectory);
+ }
+
+ // Create a daily directory for logs
+ string dailyLogsDirectory = Path.Combine(logsRootDirectory, DateTime.Now.ToString("yyyyMMdd"));
+ if (!Directory.Exists(dailyLogsDirectory))
+ {
+ Directory.CreateDirectory(dailyLogsDirectory);
+ }
+
+ // Base log file name
+ string baseLogFilePath = Path.Combine(dailyLogsDirectory, "EonaCatLogs");
+ string logFilePath = baseLogFilePath + ".log";
+
+ int fileIndex = 1;
+ while (File.Exists(logFilePath) && new FileInfo(logFilePath).Length > MaxLogFileSize)
+ {
+ logFilePath = baseLogFilePath + $"_{fileIndex}.log";
+ fileIndex++;
+ }
+
+ // After processing log, check directory size and clean up if needed
+ CleanUpOldLogs();
+
+ return File.AppendAllTextAsync(logFilePath, logData + Environment.NewLine);
+ }
+
+ private void CleanUpOldLogs()
+ {
+ string logsRootDirectory = "logs";
+ if (!Directory.Exists(logsRootDirectory))
+ {
+ return;
+ }
+
+ // Delete old directories
+ foreach (var directory in Directory.GetDirectories(logsRootDirectory))
+ {
+ try
+ {
+ DirectoryInfo dirInfo = new DirectoryInfo(directory);
+ if (dirInfo.CreationTime < DateTime.Now.AddDays(-_logRetentionDays))
+ {
+ Console.WriteLine($"Deleting old log directory: {directory}");
+ Directory.Delete(directory, true); // Delete directory and its contents
+ }
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"Error deleting old directory {directory}: {ex.Message}");
+ }
+ }
+
+ // Ensure total size of log directory doesn't exceed max limit
+ long totalDirectorySize = GetDirectorySize(logsRootDirectory);
+ if (totalDirectorySize > _maxLogDirectorySize)
+ {
+ Console.WriteLine("Log directory size exceeded limit, cleaning up...");
+
+ // Delete the oldest directories until the size limit is met
+ foreach (var directory in Directory.GetDirectories(logsRootDirectory).OrderBy(d => new DirectoryInfo(d).CreationTime))
+ {
+ try
+ {
+ DirectoryInfo dirInfo = new DirectoryInfo(directory);
+ long dirSize = GetDirectorySize(directory);
+ totalDirectorySize -= dirSize;
+
+ // Delete the directory if the total size exceeds the limit
+ Directory.Delete(directory, true);
+ Console.WriteLine($"Deleted directory: {directory}");
+
+ // Stop deleting if we are under the size limit
+ if (totalDirectorySize <= _maxLogDirectorySize)
+ {
+ break;
+ }
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"Error deleting directory {directory}: {ex.Message}");
+ }
+ }
+ }
+ }
+
+ private long GetDirectorySize(string directory)
+ {
+ long size = 0;
+ try
+ {
+ // Add size of files in the directory
+ size += Directory.GetFiles(directory).Sum(file => new FileInfo(file).Length);
+
+ // Add size of files in subdirectories
+ foreach (var subdirectory in Directory.GetDirectories(directory))
+ {
+ size += GetDirectorySize(subdirectory);
+ }
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"Error calculating size for directory {directory}: {ex.Message}");
+ }
+ return size;
+ }
+
+ public async Task Start(IPAddress ipAddress = null, int port = 5555)
+ {
+ if (ipAddress == null)
+ {
+ ipAddress = IPAddress.Any;
+ }
+
+ _cts = new CancellationTokenSource();
+ _isRunning = true;
+
+ if (_useUdp)
+ {
+ _udpListener = new UdpClient(port);
+ Console.WriteLine($"EonaCat UDP Log Server started on port {port}...");
+ await ListenUdpAsync();
+ }
+ else
+ {
+ _tcpListener = new TcpListener(ipAddress, port);
+ _tcpListener.Start();
+ Console.WriteLine($"EonaCat TCP Log Server started on port {port}...");
+ await ListenTcpAsync();
+ }
+ }
+
+ private async Task ListenTcpAsync()
+ {
+ try
+ {
+ while (!_cts.Token.IsCancellationRequested)
+ {
+ TcpClient client = await _tcpListener.AcceptTcpClientAsync();
+ _ = Task.Run(() => HandleTcpClient(client));
+ }
+ }
+ catch (OperationCanceledException)
+ {
+ Console.WriteLine("TCP Server stopping...");
+ }
+ }
+
+ private async Task ListenUdpAsync()
+ {
+ try
+ {
+ while (!_cts.Token.IsCancellationRequested)
+ {
+ // Increased buffer size for UDP
+ UdpReceiveResult result = await _udpListener.ReceiveAsync();
+ string logData = Encoding.UTF8.GetString(result.Buffer);
+
+ // If the received data is too large, process it in chunks
+ if (result.Buffer.Length > UdpBufferSize)
+ {
+ // Handle fragmentation and reassembly (this is a basic placeholder logic)
+ Console.WriteLine("Received large UDP data. Handling fragmentation.");
+ await ProcessLargeDataAsync(result.Buffer);
+ }
+ else
+ {
+ Console.WriteLine($"Received UDP Log: {logData}");
+ await ProcessLogAsync(logData);
+ }
+ }
+ }
+ catch (OperationCanceledException)
+ {
+ Console.WriteLine("UDP Server stopping...");
+ }
+ }
+
+ private async Task ProcessLargeDataAsync(byte[] data)
+ {
+ // You can implement your own logic here for processing large UDP data, such as fragmentation handling
+ string largeDataString = Encoding.UTF8.GetString(data);
+ await ProcessLogAsync(largeDataString);
+ }
+
+ public void Stop()
+ {
+ if (_isRunning)
+ {
+ _cts.Cancel();
+
+ // Proper cleanup of resources
+ _cts.Dispose();
+ if (_useUdp)
+ {
+ _udpListener?.Close();
+ _udpListener?.Dispose();
+ }
+ else
+ {
+ _tcpListener?.Stop();
+ _tcpListener?.Server?.Dispose(); // Dispose of the socket (if any)
+ }
+
+ _isRunning = false;
+ Console.WriteLine("EonaCat Log Server stopped.");
+ }
+ }
+
+ private async Task HandleTcpClient(TcpClient client)
+ {
+ try
+ {
+ using (NetworkStream stream = client.GetStream())
+ using (StreamReader reader = new StreamReader(stream, Encoding.UTF8))
+ {
+ char[] buffer = new char[8192]; // 8KB buffer size for large data
+ int bytesRead;
+ StringBuilder logData = new StringBuilder();
+
+ while ((bytesRead = await reader.ReadAsync(buffer, 0, buffer.Length)) > 0)
+ {
+ logData.Append(new string(buffer, 0, bytesRead));
+ }
+
+ Console.WriteLine($"Received TCP Log: {logData.ToString()}");
+ await ProcessLogAsync(logData.ToString());
+ }
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"Error: {ex.Message}");
+ }
+ finally
+ {
+ // Ensure client is properly disposed
+ client.Close();
+ client.Dispose();
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/EonaCat.LogStack.WindowsEventLogFlow/EonaCat.LogStack.Flows.WindowsEventLog.csproj b/EonaCat.LogStack.WindowsEventLogFlow/EonaCat.LogStack.Flows.WindowsEventLog.csproj
new file mode 100644
index 0000000..0689817
--- /dev/null
+++ b/EonaCat.LogStack.WindowsEventLogFlow/EonaCat.LogStack.Flows.WindowsEventLog.csproj
@@ -0,0 +1,17 @@
+
+
+
+ netstandard2.1
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/EonaCat.LogStack.WindowsEventLogFlow/LogBuilder.cs b/EonaCat.LogStack.WindowsEventLogFlow/LogBuilder.cs
new file mode 100644
index 0000000..f7f8181
--- /dev/null
+++ b/EonaCat.LogStack.WindowsEventLogFlow/LogBuilder.cs
@@ -0,0 +1,32 @@
+using EonaCat.LogStack.Configuration;
+using EonaCat.LogStack.Flows;
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace EonaCat.LogStack.Flows.WindowsEventLog
+{
+ // 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 static class EonaCatLogStackExtensions
+ {
+ ///
+ /// Write to Windows Event log
+ ///
+ public static LogBuilder WriteToWindowsEventLog(this LogBuilder logBuilder,
+ string sourceName = "EonaCatLogStack",
+ string logName = "Application",
+ int maxMessageLength = 30000,
+ Core.LogLevel minimumLevel = Core.LogLevel.Warning)
+ {
+ logBuilder.AddFlow(new WindowsEventLogFlow(
+ sourceName,
+ logName,
+ maxMessageLength,
+ minimumLevel));
+ WindowsEventLogFlow.EnsureSourceExists();
+ return logBuilder;
+ }
+ }
+}
diff --git a/EonaCat.LogStack.WindowsEventLogFlow/WindowsEventLogFlow.cs b/EonaCat.LogStack.WindowsEventLogFlow/WindowsEventLogFlow.cs
new file mode 100644
index 0000000..fe77d63
--- /dev/null
+++ b/EonaCat.LogStack.WindowsEventLogFlow/WindowsEventLogFlow.cs
@@ -0,0 +1,258 @@
+using EonaCat.LogStack.Core;
+using System;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace EonaCat.LogStack.Flows
+{
+ // 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.
+
+ ///
+ /// Writes log events to the Windows Event Log.
+ ///
+ /// Requires the source to be registered before first use.
+ /// Call once during application setup
+ /// (requires elevated privileges the first time).
+ ///
+ /// .NET 4.8.1 compatible. Silently no-ops on non-Windows platforms.
+ ///
+ public sealed class WindowsEventLogFlow : FlowBase
+ {
+ private readonly string _sourceName;
+ private readonly string _logName;
+ private readonly int _maxMessageLength;
+ private System.Diagnostics.EventLog _eventLog;
+ private readonly object _initLock = new object();
+ private volatile bool _initialized;
+
+ public WindowsEventLogFlow(
+ string sourceName = "EonaCatLogStack",
+ string logName = "Application",
+ int maxMessageLength = 30000,
+ LogLevel minimumLevel = LogLevel.Warning)
+ : base("WindowsEventLog:" + sourceName, minimumLevel)
+ {
+ if (sourceName == null)
+ {
+ throw new ArgumentNullException("sourceName");
+ }
+
+ if (logName == null)
+ {
+ throw new ArgumentNullException("logName");
+ }
+
+ _sourceName = sourceName;
+ _logName = logName;
+ _maxMessageLength = maxMessageLength;
+ }
+
+ ///
+ /// Registers the event source with the OS. Must be called with admin rights
+ /// the first time on each machine. Safe to call repeatedly.
+ ///
+ public static void EnsureSourceExists(string sourceName = "EonaCatLogStack",
+ string logName = "Application")
+ {
+ if (!IsWindows())
+ {
+ return;
+ }
+
+ try
+ {
+ if (!System.Diagnostics.EventLog.SourceExists(sourceName))
+ {
+ System.Diagnostics.EventLog.CreateEventSource(sourceName, logName);
+ }
+ }
+ catch (Exception ex)
+ {
+ Console.Error.WriteLine("[WindowsEventLogFlow] Cannot create source: " + ex.Message);
+ }
+ }
+
+ public override Task BlastAsync(
+ LogEvent logEvent,
+ CancellationToken cancellationToken = default(CancellationToken))
+ {
+ if (!IsEnabled || !IsLogLevelEnabled(logEvent))
+ {
+ return Task.FromResult(WriteResult.LevelFiltered);
+ }
+
+ if (!IsWindows())
+ {
+ return Task.FromResult(WriteResult.Success);
+ }
+
+ EnsureInitialized();
+ if (_eventLog == null)
+ {
+ return Task.FromResult(WriteResult.Dropped);
+ }
+
+ try
+ {
+ string msg = BuildMessage(logEvent);
+ if (msg.Length > _maxMessageLength)
+ {
+ msg = msg.Substring(0, _maxMessageLength) + "... [truncated]";
+ }
+
+ _eventLog.WriteEntry(msg, ToEventType(logEvent.Level), ToEventId(logEvent.Level));
+ Interlocked.Increment(ref BlastedCount);
+ return Task.FromResult(WriteResult.Success);
+ }
+ catch (Exception ex)
+ {
+ Console.Error.WriteLine("[WindowsEventLogFlow] Write error: " + ex.Message);
+ Interlocked.Increment(ref DroppedCount);
+ return Task.FromResult(WriteResult.Dropped);
+ }
+ }
+
+ public override Task BlastBatchAsync(
+ ReadOnlyMemory logEvents,
+ CancellationToken cancellationToken = default(CancellationToken))
+ {
+ if (!IsEnabled)
+ {
+ return Task.FromResult(WriteResult.FlowDisabled);
+ }
+
+ foreach (LogEvent e in logEvents.ToArray())
+ {
+ if (IsLogLevelEnabled(e))
+ {
+ BlastAsync(e, cancellationToken);
+ }
+ }
+
+ return Task.FromResult(WriteResult.Success);
+ }
+
+ public override Task FlushAsync(CancellationToken cancellationToken = default(CancellationToken))
+ => Task.FromResult(0);
+
+ public override async ValueTask DisposeAsync()
+ {
+ IsEnabled = false;
+ if (_eventLog != null) { try { _eventLog.Dispose(); } catch { } }
+ await base.DisposeAsync().ConfigureAwait(false);
+ }
+
+ // ----------------------------------------------------------------- helpers
+
+ private void EnsureInitialized()
+ {
+ if (_initialized)
+ {
+ return;
+ }
+
+ lock (_initLock)
+ {
+ if (_initialized)
+ {
+ return;
+ }
+
+ try
+ {
+ if (System.Diagnostics.EventLog.SourceExists(_sourceName))
+ {
+ _eventLog = new System.Diagnostics.EventLog(_logName) { Source = _sourceName };
+ }
+ else
+ {
+ Console.Error.WriteLine(
+ "[WindowsEventLogFlow] Source '" + _sourceName +
+ "' not registered. Call EnsureSourceExists() with admin rights.");
+ }
+ }
+ catch (Exception ex)
+ {
+ Console.Error.WriteLine("[WindowsEventLogFlow] Init error: " + ex.Message);
+ }
+ _initialized = true;
+ }
+ }
+
+ private static string BuildMessage(LogEvent log)
+ {
+ var sb = new System.Text.StringBuilder(512);
+ sb.Append("Level: ").AppendLine(LevelString(log.Level));
+ sb.Append("Category: ").AppendLine(log.Category ?? string.Empty);
+ sb.Append("Time: ").AppendLine(LogEvent.GetDateTime(log.Timestamp).ToString("O"));
+ sb.Append("Message: ").AppendLine(log.Message.Length > 0 ? log.Message.ToString() : string.Empty);
+
+ if (log.Exception != null)
+ {
+ sb.Append("Exception: ").AppendLine(log.Exception.ToString());
+ }
+
+ if (log.Properties.Count > 0)
+ {
+ sb.AppendLine("Properties:");
+ foreach (var kv in log.Properties.ToArray())
+ {
+ sb.Append(" ").Append(kv.Key).Append(" = ")
+ .AppendLine(kv.Value != null ? kv.Value.ToString() : "null");
+ }
+ }
+ return sb.ToString();
+ }
+
+ private static System.Diagnostics.EventLogEntryType ToEventType(LogLevel level)
+ {
+ switch (level)
+ {
+ case LogLevel.Warning: return System.Diagnostics.EventLogEntryType.Warning;
+ case LogLevel.Error:
+ case LogLevel.Critical: return System.Diagnostics.EventLogEntryType.Error;
+ default: return System.Diagnostics.EventLogEntryType.Information;
+ }
+ }
+
+ private static int ToEventId(LogLevel level)
+ {
+ // Stable event IDs per level for easy filtering in Event Viewer
+ switch (level)
+ {
+ case LogLevel.Trace: return 1000;
+ case LogLevel.Debug: return 1001;
+ case LogLevel.Information: return 1002;
+ case LogLevel.Warning: return 1003;
+ case LogLevel.Error: return 1004;
+ case LogLevel.Critical: return 1005;
+ default: return 1999;
+ }
+ }
+
+ private static string LevelString(LogLevel level)
+ {
+ switch (level)
+ {
+ case LogLevel.Trace: return "TRACE";
+ case LogLevel.Debug: return "DEBUG";
+ case LogLevel.Information: return "INFO";
+ case LogLevel.Warning: return "WARN";
+ case LogLevel.Error: return "ERROR";
+ case LogLevel.Critical: return "CRITICAL";
+ default: return level.ToString().ToUpperInvariant();
+ }
+ }
+
+ private static bool IsWindows()
+ {
+#if NET48 || NET45 || NET451 || NET452 || NET46 || NET461 || NET462 || NET47 || NET471 || NET472 || NET481
+ return true;
+#else
+ return System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.Windows);
+#endif
+ }
+ }
+}
\ No newline at end of file
diff --git a/EonaCat.LogStack.sln b/EonaCat.LogStack.sln
new file mode 100644
index 0000000..b7adf93
--- /dev/null
+++ b/EonaCat.LogStack.sln
@@ -0,0 +1,126 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 18
+VisualStudioVersion = 18.1.11312.151
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EonaCat.LogStack", "EonaCat.LogStack\EonaCat.LogStack.csproj", "{DCD1D32E-0F24-4D0F-A6B6-59941C0F9BB7}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EonaCat.LogStack.LogClient", "EonaCat.LogStack.LogClient\EonaCat.LogStack.LogClient.csproj", "{D1025803-9588-46EB-8771-88E25209B780}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConsoleApp1", "ConsoleApp1\ConsoleApp1.csproj", "{C9F66B51-6661-467A-9E22-E0E578EB76A1}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EonaCat.LogStack.Flows.WindowsEventLog", "EonaCat.LogStack.WindowsEventLogFlow\EonaCat.LogStack.Flows.WindowsEventLog.csproj", "{F5EFDDEA-C4A4-4AE7-B853-DF91062D4558}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EonaCat.LogStack.OpenTelemetryFlow", "EonaCat.LogStack.OpenTelemetryFlow\EonaCat.LogStack.OpenTelemetryFlow.csproj", "{CBF0AF0C-CF27-7D45-BCC2-DA7B7A40539C}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EonaCat.LogStack.SerilogTest", "EonaCat.LogStack.SerilogTest\EonaCat.LogStack.SerilogTest.csproj", "{F360998D-46E0-5A88-BA3E-47A4162C8EB4}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EonaCat.LogStack.Test.Web", "Testers\EonaCat.LogStack.Test.Web\EonaCat.LogStack.Test.Web.csproj", "{9240A706-1852-C232-FB58-E54A5A528135}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{85A2505C-8976-4046-963B-D7B63EF81E47}"
+ ProjectSection(SolutionItems) = preProject
+ README.md = README.md
+ EndProjectSection
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Debug|x64 = Debug|x64
+ Debug|x86 = Debug|x86
+ Release|Any CPU = Release|Any CPU
+ Release|x64 = Release|x64
+ Release|x86 = Release|x86
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {DCD1D32E-0F24-4D0F-A6B6-59941C0F9BB7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {DCD1D32E-0F24-4D0F-A6B6-59941C0F9BB7}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {DCD1D32E-0F24-4D0F-A6B6-59941C0F9BB7}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {DCD1D32E-0F24-4D0F-A6B6-59941C0F9BB7}.Debug|x64.Build.0 = Debug|Any CPU
+ {DCD1D32E-0F24-4D0F-A6B6-59941C0F9BB7}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {DCD1D32E-0F24-4D0F-A6B6-59941C0F9BB7}.Debug|x86.Build.0 = Debug|Any CPU
+ {DCD1D32E-0F24-4D0F-A6B6-59941C0F9BB7}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {DCD1D32E-0F24-4D0F-A6B6-59941C0F9BB7}.Release|Any CPU.Build.0 = Release|Any CPU
+ {DCD1D32E-0F24-4D0F-A6B6-59941C0F9BB7}.Release|x64.ActiveCfg = Release|Any CPU
+ {DCD1D32E-0F24-4D0F-A6B6-59941C0F9BB7}.Release|x64.Build.0 = Release|Any CPU
+ {DCD1D32E-0F24-4D0F-A6B6-59941C0F9BB7}.Release|x86.ActiveCfg = Release|Any CPU
+ {DCD1D32E-0F24-4D0F-A6B6-59941C0F9BB7}.Release|x86.Build.0 = Release|Any CPU
+ {D1025803-9588-46EB-8771-88E25209B780}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {D1025803-9588-46EB-8771-88E25209B780}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {D1025803-9588-46EB-8771-88E25209B780}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {D1025803-9588-46EB-8771-88E25209B780}.Debug|x64.Build.0 = Debug|Any CPU
+ {D1025803-9588-46EB-8771-88E25209B780}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {D1025803-9588-46EB-8771-88E25209B780}.Debug|x86.Build.0 = Debug|Any CPU
+ {D1025803-9588-46EB-8771-88E25209B780}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {D1025803-9588-46EB-8771-88E25209B780}.Release|Any CPU.Build.0 = Release|Any CPU
+ {D1025803-9588-46EB-8771-88E25209B780}.Release|x64.ActiveCfg = Release|Any CPU
+ {D1025803-9588-46EB-8771-88E25209B780}.Release|x64.Build.0 = Release|Any CPU
+ {D1025803-9588-46EB-8771-88E25209B780}.Release|x86.ActiveCfg = Release|Any CPU
+ {D1025803-9588-46EB-8771-88E25209B780}.Release|x86.Build.0 = Release|Any CPU
+ {C9F66B51-6661-467A-9E22-E0E578EB76A1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {C9F66B51-6661-467A-9E22-E0E578EB76A1}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {C9F66B51-6661-467A-9E22-E0E578EB76A1}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {C9F66B51-6661-467A-9E22-E0E578EB76A1}.Debug|x64.Build.0 = Debug|Any CPU
+ {C9F66B51-6661-467A-9E22-E0E578EB76A1}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {C9F66B51-6661-467A-9E22-E0E578EB76A1}.Debug|x86.Build.0 = Debug|Any CPU
+ {C9F66B51-6661-467A-9E22-E0E578EB76A1}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {C9F66B51-6661-467A-9E22-E0E578EB76A1}.Release|Any CPU.Build.0 = Release|Any CPU
+ {C9F66B51-6661-467A-9E22-E0E578EB76A1}.Release|x64.ActiveCfg = Release|Any CPU
+ {C9F66B51-6661-467A-9E22-E0E578EB76A1}.Release|x64.Build.0 = Release|Any CPU
+ {C9F66B51-6661-467A-9E22-E0E578EB76A1}.Release|x86.ActiveCfg = Release|Any CPU
+ {C9F66B51-6661-467A-9E22-E0E578EB76A1}.Release|x86.Build.0 = Release|Any CPU
+ {F5EFDDEA-C4A4-4AE7-B853-DF91062D4558}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {F5EFDDEA-C4A4-4AE7-B853-DF91062D4558}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {F5EFDDEA-C4A4-4AE7-B853-DF91062D4558}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {F5EFDDEA-C4A4-4AE7-B853-DF91062D4558}.Debug|x64.Build.0 = Debug|Any CPU
+ {F5EFDDEA-C4A4-4AE7-B853-DF91062D4558}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {F5EFDDEA-C4A4-4AE7-B853-DF91062D4558}.Debug|x86.Build.0 = Debug|Any CPU
+ {F5EFDDEA-C4A4-4AE7-B853-DF91062D4558}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {F5EFDDEA-C4A4-4AE7-B853-DF91062D4558}.Release|Any CPU.Build.0 = Release|Any CPU
+ {F5EFDDEA-C4A4-4AE7-B853-DF91062D4558}.Release|x64.ActiveCfg = Release|Any CPU
+ {F5EFDDEA-C4A4-4AE7-B853-DF91062D4558}.Release|x64.Build.0 = Release|Any CPU
+ {F5EFDDEA-C4A4-4AE7-B853-DF91062D4558}.Release|x86.ActiveCfg = Release|Any CPU
+ {F5EFDDEA-C4A4-4AE7-B853-DF91062D4558}.Release|x86.Build.0 = Release|Any CPU
+ {CBF0AF0C-CF27-7D45-BCC2-DA7B7A40539C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {CBF0AF0C-CF27-7D45-BCC2-DA7B7A40539C}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {CBF0AF0C-CF27-7D45-BCC2-DA7B7A40539C}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {CBF0AF0C-CF27-7D45-BCC2-DA7B7A40539C}.Debug|x64.Build.0 = Debug|Any CPU
+ {CBF0AF0C-CF27-7D45-BCC2-DA7B7A40539C}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {CBF0AF0C-CF27-7D45-BCC2-DA7B7A40539C}.Debug|x86.Build.0 = Debug|Any CPU
+ {CBF0AF0C-CF27-7D45-BCC2-DA7B7A40539C}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {CBF0AF0C-CF27-7D45-BCC2-DA7B7A40539C}.Release|Any CPU.Build.0 = Release|Any CPU
+ {CBF0AF0C-CF27-7D45-BCC2-DA7B7A40539C}.Release|x64.ActiveCfg = Release|Any CPU
+ {CBF0AF0C-CF27-7D45-BCC2-DA7B7A40539C}.Release|x64.Build.0 = Release|Any CPU
+ {CBF0AF0C-CF27-7D45-BCC2-DA7B7A40539C}.Release|x86.ActiveCfg = Release|Any CPU
+ {CBF0AF0C-CF27-7D45-BCC2-DA7B7A40539C}.Release|x86.Build.0 = Release|Any CPU
+ {F360998D-46E0-5A88-BA3E-47A4162C8EB4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {F360998D-46E0-5A88-BA3E-47A4162C8EB4}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {F360998D-46E0-5A88-BA3E-47A4162C8EB4}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {F360998D-46E0-5A88-BA3E-47A4162C8EB4}.Debug|x64.Build.0 = Debug|Any CPU
+ {F360998D-46E0-5A88-BA3E-47A4162C8EB4}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {F360998D-46E0-5A88-BA3E-47A4162C8EB4}.Debug|x86.Build.0 = Debug|Any CPU
+ {F360998D-46E0-5A88-BA3E-47A4162C8EB4}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {F360998D-46E0-5A88-BA3E-47A4162C8EB4}.Release|Any CPU.Build.0 = Release|Any CPU
+ {F360998D-46E0-5A88-BA3E-47A4162C8EB4}.Release|x64.ActiveCfg = Release|Any CPU
+ {F360998D-46E0-5A88-BA3E-47A4162C8EB4}.Release|x64.Build.0 = Release|Any CPU
+ {F360998D-46E0-5A88-BA3E-47A4162C8EB4}.Release|x86.ActiveCfg = Release|Any CPU
+ {F360998D-46E0-5A88-BA3E-47A4162C8EB4}.Release|x86.Build.0 = Release|Any CPU
+ {9240A706-1852-C232-FB58-E54A5A528135}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {9240A706-1852-C232-FB58-E54A5A528135}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {9240A706-1852-C232-FB58-E54A5A528135}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {9240A706-1852-C232-FB58-E54A5A528135}.Debug|x64.Build.0 = Debug|Any CPU
+ {9240A706-1852-C232-FB58-E54A5A528135}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {9240A706-1852-C232-FB58-E54A5A528135}.Debug|x86.Build.0 = Debug|Any CPU
+ {9240A706-1852-C232-FB58-E54A5A528135}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {9240A706-1852-C232-FB58-E54A5A528135}.Release|Any CPU.Build.0 = Release|Any CPU
+ {9240A706-1852-C232-FB58-E54A5A528135}.Release|x64.ActiveCfg = Release|Any CPU
+ {9240A706-1852-C232-FB58-E54A5A528135}.Release|x64.Build.0 = Release|Any CPU
+ {9240A706-1852-C232-FB58-E54A5A528135}.Release|x86.ActiveCfg = Release|Any CPU
+ {9240A706-1852-C232-FB58-E54A5A528135}.Release|x86.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {B01183F3-D85E-45FB-9749-DA281F465A0F}
+ EndGlobalSection
+EndGlobal
diff --git a/EonaCat.LogStack/Constants.cs b/EonaCat.LogStack/Constants.cs
new file mode 100644
index 0000000..e98bb36
--- /dev/null
+++ b/EonaCat.LogStack/Constants.cs
@@ -0,0 +1,11 @@
+namespace EonaCat.LogStack;
+// 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 static class Constants
+{
+ public static class DateTimeFormats
+ {
+ public static string LOGGING { get; set; } = "yyyy-MM-dd HH:mm:ss.fff";
+ }
+}
\ No newline at end of file
diff --git a/EonaCat.LogStack/DllInfo.cs b/EonaCat.LogStack/DllInfo.cs
new file mode 100644
index 0000000..35e42f2
--- /dev/null
+++ b/EonaCat.LogStack/DllInfo.cs
@@ -0,0 +1,27 @@
+using EonaCat.Versioning.Helpers;
+using System.Reflection;
+namespace EonaCat.LogStack;
+
+// 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 static class DllInfo
+{
+ public const string NAME = "EonaCatLogStack";
+ public const string VERSION = "0.0.1";
+
+ static DllInfo()
+ {
+ var isDebug = false;
+#if DEBUG
+ isDebug = true;
+#endif
+ VersionName = isDebug ? "DEBUG" : "RELEASE";
+ }
+
+ internal static string VersionName { get; }
+
+ public static string ApplicationName { get; set; } = "EonaCatLogStack";
+
+ public static string EonaCatVersion => VersionHelper.GetEonaCatVersion(Assembly.GetExecutingAssembly());
+}
\ No newline at end of file
diff --git a/EonaCat.LogStack/Enums.cs b/EonaCat.LogStack/Enums.cs
new file mode 100644
index 0000000..7e4b835
--- /dev/null
+++ b/EonaCat.LogStack/Enums.cs
@@ -0,0 +1,199 @@
+using Microsoft.Extensions.Logging;
+
+namespace EonaCat.LogStack;
+// 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 static class LogTypeConverter
+{
+ public static Core.LogLevel FromLogLevel(this LogLevel logLevel)
+ {
+ switch (logLevel)
+ {
+ case LogLevel.None:
+ return Core.LogLevel.None;
+ case LogLevel.Error:
+ return Core.LogLevel.Error;
+ case LogLevel.Debug:
+ return Core.LogLevel.Debug;
+ case LogLevel.Critical:
+ return Core.LogLevel.Critical;
+ case LogLevel.Warning:
+ return Core.LogLevel.Warning;
+ case LogLevel.Trace:
+ return Core.LogLevel.Trace;
+ case LogLevel.Information:
+ return Core.LogLevel.Information;
+ default:
+ return Core.LogLevel.Trace;
+ }
+ }
+
+ public static LogLevel ToLogLevel(this Core.LogLevel logLevel)
+ {
+ switch (logLevel)
+ {
+ case Core.LogLevel.None:
+ return LogLevel.None;
+ case Core.LogLevel.Error:
+ return LogLevel.Error;
+ case Core.LogLevel.Debug:
+ return LogLevel.Debug;
+ case Core.LogLevel.Critical:
+ return LogLevel.Critical;
+ case Core.LogLevel.Warning:
+ return LogLevel.Warning;
+ case Core.LogLevel.Trace:
+ return LogLevel.Trace;
+ case Core.LogLevel.Information:
+ return LogLevel.Information;
+ default:
+ return LogLevel.Information;
+ }
+ }
+
+ public static string ToString(this Core.LogLevel logLevel)
+ {
+ switch (logLevel)
+ {
+ case Core.LogLevel.None:
+ return "NONE";
+ case Core.LogLevel.Error:
+ return "ERROR";
+ case Core.LogLevel.Debug:
+ return "DEBUG";
+ case Core.LogLevel.Critical:
+ return "CRITICAL";
+ case Core.LogLevel.Warning:
+ return "WARNING";
+ case Core.LogLevel.Trace:
+ return "TRACE";
+ case Core.LogLevel.Information:
+ return "INFO";
+ default:
+ return "INFO";
+ }
+ }
+
+ public static Core.LogLevel FromSeverity(this ESeverity logLevel)
+ {
+ switch (logLevel)
+ {
+ case ESeverity.Debug:
+ return Core.LogLevel.Debug;
+ case ESeverity.Warn:
+ return Core.LogLevel.Warning;
+ case ESeverity.Emergency:
+ return Core.LogLevel.Trace;
+ case ESeverity.Critical:
+ return Core.LogLevel.Critical;
+ case ESeverity.Error:
+ return Core.LogLevel.Error;
+ default:
+ return Core.LogLevel.Information;
+ }
+ }
+
+ public static int ToGrayLogLevel(this Core.LogLevel logLevel)
+ {
+ // Loglevel to GELF format
+ switch (logLevel.ToString())
+ {
+ case "TRACE": return 7;
+ case "DEBUG": return 7;
+ case "INFO": return 6;
+ case "WARNING": return 4;
+ case "ERROR": return 3;
+ case "CRITICAL": return 2;
+ default: return 6; // Default to INFO
+ }
+ }
+
+ public static ESeverity ToSeverity(this Core.LogLevel logLevel)
+ {
+ switch (logLevel)
+ {
+ case Core.LogLevel.Debug:
+ return ESeverity.Debug;
+ case Core.LogLevel.Warning:
+ return ESeverity.Warn;
+ case Core.LogLevel.Critical:
+ return ESeverity.Critical;
+ case Core.LogLevel.Trace:
+ return ESeverity.Emergency;
+ case Core.LogLevel.Error:
+ return ESeverity.Error;
+ default:
+ return ESeverity.Info;
+ }
+ }
+}
+
+public enum SyslogFacility
+{
+ Kernel = 0, // 0 - Kernel messages
+ UserLevel = 1, // 1 - User-level messages
+ MailSystem = 2, // 2 - Mail system
+ Daemon = 3, // 3 - Daemon messages
+ Auth = 4, // 4 - Security/authorization messages
+ Syslog = 5, // 5 - Messages generated by syslogd
+ Lpr = 6, // 6 - Line printer subsystem
+ News = 7, // 7 - Network news subsystem
+ UUCP = 8, // 8 - UUCP subsystem
+ Clock = 9, // 9 - Clock daemon
+ AuthPriv = 10, // 10 - Security/authorization messages (privileged)
+ Ftp = 11, // 11 - FTP daemon
+ Ntp = 12, // 12 - NTP subsystem
+ Audit = 13, // 13 - Audit messages
+ Alert = 14, // 14 - Log alert messages
+ Cron = 15, // 15 - Cron daemon
+ Local0 = 16, // 16 - Local use 0 (custom usage)
+ Local1 = 17, // 17 - Local use 1 (custom usage)
+ Local2 = 18, // 18 - Local use 2 (custom usage)
+ Local3 = 19, // 19 - Local use 3 (custom usage)
+ Local4 = 20, // 20 - Local use 4 (custom usage)
+ Local5 = 21, // 21 - Local use 5 (custom usage)
+ Local6 = 22, // 22 - Local use 6 (custom usage)
+ Local7 = 23 // 23 - Local use 7 (custom usage)
+}
+
+///
+/// Message severity.
+///
+public enum ESeverity
+{
+ ///
+ /// Debug messages.
+ ///
+ Debug = 0,
+
+ ///
+ /// Informational messages.
+ ///
+ Info = 1,
+
+ ///
+ /// Warning messages.
+ ///
+ Warn = 2,
+
+ ///
+ /// Error messages.
+ ///
+ Error = 3,
+
+ ///
+ /// Alert messages.
+ ///
+ Alert = 4,
+
+ ///
+ /// Critical messages.
+ ///
+ Critical = 5,
+
+ ///
+ /// Emergency messages.
+ ///
+ Emergency = 6
+}
\ No newline at end of file
diff --git a/EonaCat.LogStack/EonaCat.LogStack.csproj b/EonaCat.LogStack/EonaCat.LogStack.csproj
new file mode 100644
index 0000000..f3dcc5e
--- /dev/null
+++ b/EonaCat.LogStack/EonaCat.LogStack.csproj
@@ -0,0 +1,85 @@
+
+
+ .netstandard2.1; net8.0; net4.8;
+ icon.ico
+ latest
+ EonaCat (Jeroen Saey)
+ true
+ EonaCat (Jeroen Saey)
+ icon.png
+ https://www.nuget.org/packages/EonaCat.LogStack/
+ EonaCat.LogStack is a logging library
+ Public release version
+ EonaCat (Jeroen Saey)
+ EonaCat;Logger;EonaCatLogStack;Log;Writer;Jeroen;Saey
+
+ 1.7.9
+ 1.7.9
+ README.md
+ True
+ LICENSE
+
+ True
+ EonaCat.LogStack
+ git
+
+
+
+ 1.7.6+{chash:10}.{c:ymd}
+ true
+ true
+ v[0-9]*
+ true
+ git
+ true
+ true
+
+
+
+ $(GeneratedVersion)
+
+
+
+
+
+ $(GeneratedVersion)
+
+
+
+
+
+
+ True
+ \
+
+
+ True
+
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
+
+
+
+ True
+ \
+
+
+ True
+ \
+
+
+
\ No newline at end of file
diff --git a/EonaCat.LogStack/EonaCatLogger.cs b/EonaCat.LogStack/EonaCatLogger.cs
new file mode 100644
index 0000000..f6f2065
--- /dev/null
+++ b/EonaCat.LogStack/EonaCatLogger.cs
@@ -0,0 +1,291 @@
+using EonaCat.LogStack.Boosters;
+using EonaCat.LogStack.Core;
+using EonaCat.LogStack.Flows;
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Linq;
+using System.Runtime.CompilerServices;
+using System.Threading;
+using System.Threading.Tasks;
+
+// 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.LogStack
+{
+ ///
+ /// EonaCat logger with flow-based architecture, booster, and pre-build modifier hook.
+ /// Designed for zero-allocation logging paths and superior memory efficiency.
+ ///
+ public sealed class EonaCatLogStack : IAsyncDisposable
+ {
+ private readonly string _category;
+ private readonly List _flows = new List();
+ private readonly List _boosters = new List();
+ private readonly ConcurrentBag _concurrentFlows = new ConcurrentBag();
+ private readonly LogLevel _minimumLevel;
+ private readonly TimestampMode _timestampMode;
+
+ private volatile bool _isDisposed;
+ private long _totalLoggedCount;
+ private long _totalDroppedCount;
+
+ private readonly List> _modifiers = new List>();
+ public delegate void ActionRef(ref T item);
+
+ private readonly object _modifiersLock = new object();
+
+ public event EventHandler OnLog;
+
+ ///
+ /// Creates a new logger instance
+ ///
+ public EonaCatLogStack(string category = "Application",
+ LogLevel minimumLevel = LogLevel.Trace,
+ TimestampMode timestampMode = TimestampMode.Utc)
+ {
+ _category = category ?? throw new ArgumentNullException(nameof(category));
+ _minimumLevel = minimumLevel;
+ _timestampMode = timestampMode;
+ }
+
+ ///
+ /// Adds a flow (output destination) to this logger
+ ///
+ public EonaCatLogStack AddFlow(IFlow flow)
+ {
+ if (flow == null)
+ {
+ throw new ArgumentNullException(nameof(flow));
+ }
+
+ lock (_flows) { _flows.Add(flow); }
+ _concurrentFlows.Add(flow);
+ return this;
+ }
+
+ ///
+ /// Adds a booster to this logger
+ ///
+ public EonaCatLogStack AddBooster(IBooster booster)
+ {
+ if (booster == null)
+ {
+ throw new ArgumentNullException(nameof(booster));
+ }
+
+ lock (_boosters) { _boosters.Add(booster); }
+ return this;
+ }
+
+ ///
+ /// Removes a flow by name
+ ///
+ public EonaCatLogStack RemoveFlow(string name)
+ {
+ lock (_flows) { _flows.RemoveAll(f => f.Name == name); }
+ return this;
+ }
+
+ ///
+ /// Removes a booster by name
+ ///
+ public EonaCatLogStack RemoveBooster(string name)
+ {
+ lock (_boosters) { _boosters.RemoveAll(b => b.Name == name); }
+ return this;
+ }
+
+ ///
+ /// Adds a modifier to run before building the LogEvent.
+ /// Return false to cancel logging.
+ ///
+ public EonaCatLogStack AddModifier(ActionRef modifier)
+ {
+ if (modifier == null)
+ {
+ throw new ArgumentNullException(nameof(modifier));
+ }
+
+ lock (_modifiersLock) { _modifiers.Add(modifier); }
+ return this;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void Log(string message, LogLevel level = LogLevel.Information)
+ {
+ if (_isDisposed || level < _minimumLevel)
+ {
+ return;
+ }
+
+ var builder = new LogEventBuilder()
+ .WithLevel(level)
+ .WithCategory(_category)
+ .WithMessage(message)
+ .WithTimestamp(GetTimestamp());
+
+ ProcessLogEvent(ref builder);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void Log(LogLevel level, Exception exception, string message)
+ {
+ if (_isDisposed || level < _minimumLevel)
+ {
+ return;
+ }
+
+ var builder = new LogEventBuilder()
+ .WithLevel(level)
+ .WithCategory(_category)
+ .WithMessage(message)
+ .WithException(exception)
+ .WithTimestamp(GetTimestamp());
+
+ ProcessLogEvent(ref builder);
+
+ OnLog?.Invoke(this, new LogMessage
+ {
+ Level = level,
+ Exception = exception,
+ Message = message,
+ Category = _category,
+ Origin = null
+ });
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void Log(LogLevel level, string message, params (string Key, object Value)[] properties)
+ {
+ if (_isDisposed || level < _minimumLevel)
+ {
+ return;
+ }
+
+ var builder = new LogEventBuilder()
+ .WithLevel(level)
+ .WithCategory(_category)
+ .WithMessage(message)
+ .WithTimestamp(GetTimestamp());
+
+ foreach (var (key, value) in properties)
+ {
+ builder.WithProperty(key, value);
+ }
+
+ ProcessLogEvent(ref builder);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Trace(string message) => Log(LogLevel.Trace, message);
+ [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Debug(string message) => Log(LogLevel.Debug, message);
+ [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Information(string message) => Log(LogLevel.Information, message);
+ [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Warning(string message) => Log(LogLevel.Warning, message);
+ [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Warning(Exception ex, string message) => Log(LogLevel.Warning, ex, message);
+ [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Error(string message) => Log(LogLevel.Error, message);
+ [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Error(Exception ex, string message) => Log(LogLevel.Error, ex, message);
+ [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Critical(string message) => Log(LogLevel.Critical, message);
+ [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Critical(Exception ex, string message) => Log(LogLevel.Critical, ex, message);
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private void ProcessLogEvent(ref LogEventBuilder builder)
+ {
+ // Apply boosters
+ lock (_boosters)
+ {
+ foreach (var booster in _boosters)
+ {
+ try
+ {
+ if (!booster.Boost(ref builder))
+ {
+ return; // filtered out
+ }
+ }
+ catch { }
+ }
+ }
+
+ // Apply modifiers
+ foreach (var mod in _modifiers)
+ {
+ try
+ {
+ mod(ref builder);
+ }
+ catch { }
+ }
+
+ var logEvent = builder.Build();
+ Interlocked.Increment(ref _totalLoggedCount);
+
+ // Blast to flows
+ foreach (var flow in _concurrentFlows)
+ {
+ try
+ {
+ var result = flow.BlastAsync(logEvent).GetAwaiter().GetResult();
+ if (result == WriteResult.Dropped)
+ {
+ Interlocked.Increment(ref _totalDroppedCount);
+ }
+ }
+ catch { }
+ }
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private long GetTimestamp()
+ {
+ switch (_timestampMode)
+ {
+ case TimestampMode.Local: return DateTime.Now.Ticks;
+ case TimestampMode.HighPrecision: return System.Diagnostics.Stopwatch.GetTimestamp();
+ default: return DateTime.UtcNow.Ticks;
+ }
+ }
+
+ public async Task FlushAsync(CancellationToken cancellationToken = default)
+ {
+ var tasks = _concurrentFlows.Select(f => f.FlushAsync(cancellationToken));
+ await Task.WhenAll(tasks).ConfigureAwait(false);
+ }
+
+ public LoggerDiagnostics GetDiagnostics()
+ {
+ var flowDiagnostics = _concurrentFlows
+ .Select(f => f is FlowBase fb ? fb.GetDiagnostics() : null)
+ .Where(d => d != null)
+ .ToList();
+
+ return new LoggerDiagnostics
+ {
+ Category = _category,
+ MinimumLevel = _minimumLevel,
+ TotalLogged = Interlocked.Read(ref _totalLoggedCount),
+ TotalDropped = Interlocked.Read(ref _totalDroppedCount),
+ FlowCount = _flows.Count,
+ BoosterCount = _boosters.Count,
+ Flows = flowDiagnostics
+ };
+ }
+
+ public async ValueTask DisposeAsync()
+ {
+ if (_isDisposed)
+ {
+ return;
+ }
+
+ _isDisposed = true;
+
+ await FlushAsync().ConfigureAwait(false);
+
+ var disposeTasks = _concurrentFlows.Select(f => f.DisposeAsync().AsTask());
+ await Task.WhenAll(disposeTasks).ConfigureAwait(false);
+
+ GC.SuppressFinalize(this);
+ }
+ }
+}
diff --git a/EonaCat.LogStack/EonaCatLoggerCore/BoosterBase.cs b/EonaCat.LogStack/EonaCatLoggerCore/BoosterBase.cs
new file mode 100644
index 0000000..0d4b569
--- /dev/null
+++ b/EonaCat.LogStack/EonaCatLoggerCore/BoosterBase.cs
@@ -0,0 +1,22 @@
+using EonaCat.LogStack.Core;
+using System;
+
+namespace EonaCat.LogStack.Boosters;
+
+// 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.
+
+///
+/// Base class for boosters that need configuration
+///
+public abstract class BoosterBase : IBooster
+{
+ protected BoosterBase(string name)
+ {
+ Name = name ?? throw new ArgumentNullException(nameof(name));
+ }
+
+ public string Name { get; }
+
+ public abstract bool Boost(ref LogEventBuilder builder);
+}
\ No newline at end of file
diff --git a/EonaCat.LogStack/EonaCatLoggerCore/Boosters/AppBooster.cs b/EonaCat.LogStack/EonaCatLoggerCore/Boosters/AppBooster.cs
new file mode 100644
index 0000000..19c978c
--- /dev/null
+++ b/EonaCat.LogStack/EonaCatLoggerCore/Boosters/AppBooster.cs
@@ -0,0 +1,25 @@
+using EonaCat.LogStack.Core;
+using System;
+using System.Runtime.CompilerServices;
+
+// 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.LogStack.Boosters
+{
+ public sealed class AppBooster : BoosterBase
+ {
+ private static readonly string AppName = AppDomain.CurrentDomain.FriendlyName;
+ private static readonly string AppBase = AppDomain.CurrentDomain.BaseDirectory;
+
+ public AppBooster() : base("App") { }
+
+ [System.Runtime.CompilerServices.MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public override bool Boost(ref LogEventBuilder builder)
+ {
+ builder.WithProperty("App", AppName);
+ builder.WithProperty("AppBase", AppBase);
+ return true;
+ }
+ }
+}
diff --git a/EonaCat.LogStack/EonaCatLoggerCore/Boosters/ApplicationBooster.cs b/EonaCat.LogStack/EonaCatLoggerCore/Boosters/ApplicationBooster.cs
new file mode 100644
index 0000000..078a2dc
--- /dev/null
+++ b/EonaCat.LogStack/EonaCatLoggerCore/Boosters/ApplicationBooster.cs
@@ -0,0 +1,34 @@
+using EonaCat.LogStack.Core;
+using System;
+using System.Runtime.CompilerServices;
+
+namespace EonaCat.LogStack.Boosters;
+
+// 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.
+
+///
+/// Adds application name and version to log events
+///
+public sealed class ApplicationBooster : BoosterBase
+{
+ private readonly string _applicationName;
+ private readonly string? _version;
+
+ public ApplicationBooster(string applicationName, string? version = null) : base("Application")
+ {
+ _applicationName = applicationName ?? throw new ArgumentNullException(nameof(applicationName));
+ _version = version;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public override bool Boost(ref LogEventBuilder builder)
+ {
+ builder.WithProperty("Application", _applicationName);
+ if (_version != null)
+ {
+ builder.WithProperty("Version", _version);
+ }
+ return true;
+ }
+}
diff --git a/EonaCat.LogStack/EonaCatLoggerCore/Boosters/CallbackBooster.cs b/EonaCat.LogStack/EonaCatLoggerCore/Boosters/CallbackBooster.cs
new file mode 100644
index 0000000..75248d8
--- /dev/null
+++ b/EonaCat.LogStack/EonaCatLoggerCore/Boosters/CallbackBooster.cs
@@ -0,0 +1,41 @@
+using EonaCat.LogStack.Core;
+using System;
+using System.Collections.Generic;
+
+namespace EonaCat.LogStack.Boosters;
+
+// 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.
+
+///
+/// Adds custom properties from a callback
+///
+public sealed class CallbackBooster : BoosterBase
+{
+ private readonly Func> _propertiesCallback;
+
+ public CallbackBooster(string name, Func> propertiesCallback) : base(name)
+ {
+ _propertiesCallback = propertiesCallback ?? throw new ArgumentNullException(nameof(propertiesCallback));
+ }
+
+ public override bool Boost(ref LogEventBuilder builder)
+ {
+ try
+ {
+ var properties = _propertiesCallback();
+ if (properties != null)
+ {
+ foreach (var kvp in properties)
+ {
+ builder.WithProperty(kvp.Key, kvp.Value);
+ }
+ }
+ }
+ catch
+ {
+ // Swallow exceptions in boosters to prevent logging failures
+ }
+ return true;
+ }
+}
diff --git a/EonaCat.LogStack/EonaCatLoggerCore/Boosters/CorrelationIdBooster.cs b/EonaCat.LogStack/EonaCatLoggerCore/Boosters/CorrelationIdBooster.cs
new file mode 100644
index 0000000..ac4d1b2
--- /dev/null
+++ b/EonaCat.LogStack/EonaCatLoggerCore/Boosters/CorrelationIdBooster.cs
@@ -0,0 +1,27 @@
+using EonaCat.LogStack.Core;
+using System.Diagnostics;
+using System.Runtime.CompilerServices;
+
+namespace EonaCat.LogStack.Boosters;
+
+// 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.
+
+///
+/// Adds correlation ID from Activity or custom source
+///
+public sealed class CorrelationIdBooster : BoosterBase
+{
+ public CorrelationIdBooster() : base("CorrelationId") { }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public override bool Boost(ref LogEventBuilder builder)
+ {
+ var activity = Activity.Current;
+ if (activity != null)
+ {
+ builder.WithProperty("CorrelationId", activity.Id ?? activity.TraceId.ToString());
+ }
+ return true;
+ }
+}
diff --git a/EonaCat.LogStack/EonaCatLoggerCore/Boosters/CustomTextBooster.cs b/EonaCat.LogStack/EonaCatLoggerCore/Boosters/CustomTextBooster.cs
new file mode 100644
index 0000000..a0d22e0
--- /dev/null
+++ b/EonaCat.LogStack/EonaCatLoggerCore/Boosters/CustomTextBooster.cs
@@ -0,0 +1,37 @@
+using EonaCat.LogStack.Core;
+using System;
+using System.Runtime.CompilerServices;
+
+namespace EonaCat.LogStack.Boosters
+{
+ // 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.
+
+ ///
+ /// Adds a custom text property to log events
+ ///
+ public sealed class CustomTextBooster : BoosterBase
+ {
+ private readonly string _propertyName;
+ private readonly string _text;
+
+ ///
+ /// Creates a new booster that adds a custom text property to logs
+ ///
+ /// The name of the property to add
+ /// The text value to set
+ public CustomTextBooster(string propertyName, string text)
+ : base("CustomText")
+ {
+ _propertyName = propertyName ?? throw new ArgumentNullException(nameof(propertyName));
+ _text = text ?? throw new ArgumentNullException(nameof(text));
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public override bool Boost(ref LogEventBuilder builder)
+ {
+ builder.WithProperty(_propertyName, _text);
+ return true;
+ }
+ }
+}
diff --git a/EonaCat.LogStack/EonaCatLoggerCore/Boosters/DateBooster.cs b/EonaCat.LogStack/EonaCatLoggerCore/Boosters/DateBooster.cs
new file mode 100644
index 0000000..b9654b7
--- /dev/null
+++ b/EonaCat.LogStack/EonaCatLoggerCore/Boosters/DateBooster.cs
@@ -0,0 +1,21 @@
+using EonaCat.LogStack.Core;
+using System;
+using System.Runtime.CompilerServices;
+
+namespace EonaCat.LogStack.Boosters
+{
+ // 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 sealed class DateBooster : BoosterBase
+ {
+ public DateBooster() : base("Date") { }
+
+ [System.Runtime.CompilerServices.MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public override bool Boost(ref LogEventBuilder builder)
+ {
+ builder.WithProperty("Date", DateTime.UtcNow.ToString("yyyy-MM-dd"));
+ return true;
+ }
+ }
+}
diff --git a/EonaCat.LogStack/EonaCatLoggerCore/Boosters/EnvironmentBooster.cs b/EonaCat.LogStack/EonaCatLoggerCore/Boosters/EnvironmentBooster.cs
new file mode 100644
index 0000000..f2d28ec
--- /dev/null
+++ b/EonaCat.LogStack/EonaCatLoggerCore/Boosters/EnvironmentBooster.cs
@@ -0,0 +1,27 @@
+using EonaCat.LogStack.Core;
+using System.Runtime.CompilerServices;
+
+namespace EonaCat.LogStack.Boosters;
+
+// 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.
+
+///
+/// Adds environment name to log events
+///
+public sealed class EnvironmentBooster : BoosterBase
+{
+ private readonly string _environmentName;
+
+ public EnvironmentBooster(string environmentName) : base("Environment")
+ {
+ _environmentName = environmentName ?? "Production";
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public override bool Boost(ref LogEventBuilder builder)
+ {
+ builder.WithProperty("Environment", _environmentName);
+ return true;
+ }
+}
diff --git a/EonaCat.LogStack/EonaCatLoggerCore/Boosters/FrameworkBooster.cs b/EonaCat.LogStack/EonaCatLoggerCore/Boosters/FrameworkBooster.cs
new file mode 100644
index 0000000..0c21759
--- /dev/null
+++ b/EonaCat.LogStack/EonaCatLoggerCore/Boosters/FrameworkBooster.cs
@@ -0,0 +1,23 @@
+using EonaCat.LogStack.Core;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+namespace EonaCat.LogStack.Boosters
+{
+ // 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 sealed class FrameworkBooster : BoosterBase
+ {
+ private static readonly string FrameworkDesc = RuntimeInformation.FrameworkDescription;
+
+ public FrameworkBooster() : base("Framework") { }
+
+ [System.Runtime.CompilerServices.MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public override bool Boost(ref LogEventBuilder builder)
+ {
+ builder.WithProperty("Framework", FrameworkDesc);
+ return true;
+ }
+ }
+}
diff --git a/EonaCat.LogStack/EonaCatLoggerCore/Boosters/LevelFilterBooster.cs b/EonaCat.LogStack/EonaCatLoggerCore/Boosters/LevelFilterBooster.cs
new file mode 100644
index 0000000..b41b111
--- /dev/null
+++ b/EonaCat.LogStack/EonaCatLoggerCore/Boosters/LevelFilterBooster.cs
@@ -0,0 +1,28 @@
+using EonaCat.LogStack.Core;
+using System.Runtime.CompilerServices;
+
+namespace EonaCat.LogStack.Boosters;
+
+// 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.
+
+///
+/// Filters log events based on level
+///
+public sealed class LevelFilterBooster : BoosterBase
+{
+ private readonly LogLevel _minimumLevel;
+
+ public LevelFilterBooster(LogLevel minimumLevel) : base("LevelFilter")
+ {
+ _minimumLevel = minimumLevel;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public override bool Boost(ref LogEventBuilder builder)
+ {
+ // Filter will be handled by the pipeline, this is a no-op booster
+ // Actual filtering happens in the logger pipeline based on configuration
+ return true;
+ }
+}
diff --git a/EonaCat.LogStack/EonaCatLoggerCore/Boosters/MachineNameBooster.cs b/EonaCat.LogStack/EonaCatLoggerCore/Boosters/MachineNameBooster.cs
new file mode 100644
index 0000000..da255ab
--- /dev/null
+++ b/EonaCat.LogStack/EonaCatLoggerCore/Boosters/MachineNameBooster.cs
@@ -0,0 +1,25 @@
+using EonaCat.LogStack.Core;
+using System;
+using System.Runtime.CompilerServices;
+
+namespace EonaCat.LogStack.Boosters;
+
+// 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.
+
+///
+/// Adds machine name to log events
+///
+public sealed class MachineNameBooster : BoosterBase
+{
+ private static readonly string MachineName = Environment.MachineName;
+
+ public MachineNameBooster() : base("MachineName") { }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public override bool Boost(ref LogEventBuilder builder)
+ {
+ builder.WithProperty("MachineName", MachineName);
+ return true;
+ }
+}
diff --git a/EonaCat.LogStack/EonaCatLoggerCore/Boosters/MemoryBooster.cs b/EonaCat.LogStack/EonaCatLoggerCore/Boosters/MemoryBooster.cs
new file mode 100644
index 0000000..39657bb
--- /dev/null
+++ b/EonaCat.LogStack/EonaCatLoggerCore/Boosters/MemoryBooster.cs
@@ -0,0 +1,22 @@
+using EonaCat.LogStack.Core;
+using System;
+using System.Runtime.CompilerServices;
+
+namespace EonaCat.LogStack.Boosters
+{
+ // 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 sealed class MemoryBooster : BoosterBase
+ {
+ public MemoryBooster() : base("Memory") { }
+
+ [System.Runtime.CompilerServices.MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public override bool Boost(ref LogEventBuilder builder)
+ {
+ var memoryMB = GC.GetTotalMemory(false) / 1024 / 1024;
+ builder.WithProperty("Memory", memoryMB);
+ return true;
+ }
+ }
+}
diff --git a/EonaCat.LogStack/EonaCatLoggerCore/Boosters/OSBooster.cs b/EonaCat.LogStack/EonaCatLoggerCore/Boosters/OSBooster.cs
new file mode 100644
index 0000000..1f01969
--- /dev/null
+++ b/EonaCat.LogStack/EonaCatLoggerCore/Boosters/OSBooster.cs
@@ -0,0 +1,23 @@
+using EonaCat.LogStack.Core;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+namespace EonaCat.LogStack.Boosters
+{
+ // 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 sealed class OSBooster : BoosterBase
+ {
+ private static readonly string OSDesc = RuntimeInformation.OSDescription;
+
+ public OSBooster() : base("OS") { }
+
+ [System.Runtime.CompilerServices.MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public override bool Boost(ref LogEventBuilder builder)
+ {
+ builder.WithProperty("OS", OSDesc);
+ return true;
+ }
+ }
+}
diff --git a/EonaCat.LogStack/EonaCatLoggerCore/Boosters/ProcStartBooster.cs b/EonaCat.LogStack/EonaCatLoggerCore/Boosters/ProcStartBooster.cs
new file mode 100644
index 0000000..311131a
--- /dev/null
+++ b/EonaCat.LogStack/EonaCatLoggerCore/Boosters/ProcStartBooster.cs
@@ -0,0 +1,24 @@
+using EonaCat.LogStack.Core;
+using System;
+using System.Diagnostics;
+using System.Runtime.CompilerServices;
+
+namespace EonaCat.LogStack.Boosters
+{
+ // 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 sealed class ProcStartBooster : BoosterBase
+ {
+ private static readonly DateTime ProcessStart = Process.GetCurrentProcess().StartTime;
+
+ public ProcStartBooster() : base("ProcStart") { }
+
+ [System.Runtime.CompilerServices.MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public override bool Boost(ref LogEventBuilder builder)
+ {
+ builder.WithProperty("ProcStart", ProcessStart);
+ return true;
+ }
+ }
+}
diff --git a/EonaCat.LogStack/EonaCatLoggerCore/Boosters/ProcessIdBooster.cs b/EonaCat.LogStack/EonaCatLoggerCore/Boosters/ProcessIdBooster.cs
new file mode 100644
index 0000000..55b1c22
--- /dev/null
+++ b/EonaCat.LogStack/EonaCatLoggerCore/Boosters/ProcessIdBooster.cs
@@ -0,0 +1,25 @@
+using EonaCat.LogStack.Core;
+using System.Diagnostics;
+using System.Runtime.CompilerServices;
+
+namespace EonaCat.LogStack.Boosters;
+
+// 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.
+
+///
+/// Adds process ID to log events
+///
+public sealed class ProcessIdBooster : BoosterBase
+{
+ private static readonly int ProcessId = Process.GetCurrentProcess().Id;
+
+ public ProcessIdBooster() : base("ProcessId") { }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public override bool Boost(ref LogEventBuilder builder)
+ {
+ builder.WithProperty("ProcessId", ProcessId);
+ return true;
+ }
+}
diff --git a/EonaCat.LogStack/EonaCatLoggerCore/Boosters/ThreadIdBooster.cs b/EonaCat.LogStack/EonaCatLoggerCore/Boosters/ThreadIdBooster.cs
new file mode 100644
index 0000000..78b7930
--- /dev/null
+++ b/EonaCat.LogStack/EonaCatLoggerCore/Boosters/ThreadIdBooster.cs
@@ -0,0 +1,21 @@
+using EonaCat.LogStack.Core;
+using System;
+using System.Runtime.CompilerServices;
+
+namespace EonaCat.LogStack.Boosters
+{
+ // 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 sealed class ThreadIdBooster : BoosterBase
+ {
+ public ThreadIdBooster() : base("ThreadId") { }
+
+ [System.Runtime.CompilerServices.MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public override bool Boost(ref LogEventBuilder builder)
+ {
+ builder.WithProperty("ThreadId", Environment.CurrentManagedThreadId);
+ return true;
+ }
+ }
+}
diff --git a/EonaCat.LogStack/EonaCatLoggerCore/Boosters/ThreadNameBooster.cs b/EonaCat.LogStack/EonaCatLoggerCore/Boosters/ThreadNameBooster.cs
new file mode 100644
index 0000000..b53009f
--- /dev/null
+++ b/EonaCat.LogStack/EonaCatLoggerCore/Boosters/ThreadNameBooster.cs
@@ -0,0 +1,21 @@
+using EonaCat.LogStack.Core;
+using System.Runtime.CompilerServices;
+using System.Threading;
+
+namespace EonaCat.LogStack.Boosters
+{
+ // 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 sealed class ThreadNameBooster : BoosterBase
+ {
+ public ThreadNameBooster() : base("ThreadName") { }
+
+ [System.Runtime.CompilerServices.MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public override bool Boost(ref LogEventBuilder builder)
+ {
+ builder.WithProperty("ThreadName", Thread.CurrentThread.Name ?? "n/a");
+ return true;
+ }
+ }
+}
diff --git a/EonaCat.LogStack/EonaCatLoggerCore/Boosters/TicksBooster.cs b/EonaCat.LogStack/EonaCatLoggerCore/Boosters/TicksBooster.cs
new file mode 100644
index 0000000..007a07d
--- /dev/null
+++ b/EonaCat.LogStack/EonaCatLoggerCore/Boosters/TicksBooster.cs
@@ -0,0 +1,21 @@
+using EonaCat.LogStack.Core;
+using System;
+using System.Runtime.CompilerServices;
+
+namespace EonaCat.LogStack.Boosters
+{
+ // 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 sealed class TicksBooster : BoosterBase
+ {
+ public TicksBooster() : base("Ticks") { }
+
+ [System.Runtime.CompilerServices.MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public override bool Boost(ref LogEventBuilder builder)
+ {
+ builder.WithProperty("Ticks", DateTime.UtcNow.Ticks);
+ return true;
+ }
+ }
+}
diff --git a/EonaCat.LogStack/EonaCatLoggerCore/Boosters/TimeBooster.cs b/EonaCat.LogStack/EonaCatLoggerCore/Boosters/TimeBooster.cs
new file mode 100644
index 0000000..ab65971
--- /dev/null
+++ b/EonaCat.LogStack/EonaCatLoggerCore/Boosters/TimeBooster.cs
@@ -0,0 +1,21 @@
+using EonaCat.LogStack.Core;
+using System;
+using System.Runtime.CompilerServices;
+
+namespace EonaCat.LogStack.Boosters
+{
+ // 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 sealed class TimeBooster : BoosterBase
+ {
+ public TimeBooster() : base("Time") { }
+
+ [System.Runtime.CompilerServices.MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public override bool Boost(ref LogEventBuilder builder)
+ {
+ builder.WithProperty("Time", DateTime.UtcNow.ToString("HH:mm:ss.fff"));
+ return true;
+ }
+ }
+}
diff --git a/EonaCat.LogStack/EonaCatLoggerCore/Boosters/TimestampBooster.cs b/EonaCat.LogStack/EonaCatLoggerCore/Boosters/TimestampBooster.cs
new file mode 100644
index 0000000..ab39b39
--- /dev/null
+++ b/EonaCat.LogStack/EonaCatLoggerCore/Boosters/TimestampBooster.cs
@@ -0,0 +1,36 @@
+using EonaCat.LogStack.Core;
+using System;
+using System.Diagnostics;
+using System.Runtime.CompilerServices;
+
+namespace EonaCat.LogStack.Boosters;
+
+// 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.
+
+///
+/// Adds timestamp in multiple formats
+///
+public sealed class TimestampBooster : BoosterBase
+{
+ private readonly TimestampMode _mode;
+
+ public TimestampBooster(TimestampMode mode = TimestampMode.Utc) : base("Timestamp")
+ {
+ _mode = mode;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public override bool Boost(ref LogEventBuilder builder)
+ {
+ var timestamp = _mode switch
+ {
+ TimestampMode.Local => DateTime.Now.Ticks,
+ TimestampMode.HighPrecision => Stopwatch.GetTimestamp(),
+ _ => DateTime.UtcNow.Ticks
+ };
+
+ builder.WithTimestamp(timestamp);
+ return true;
+ }
+}
\ No newline at end of file
diff --git a/EonaCat.LogStack/EonaCatLoggerCore/Boosters/UptimeBooster.cs b/EonaCat.LogStack/EonaCatLoggerCore/Boosters/UptimeBooster.cs
new file mode 100644
index 0000000..77f78dc
--- /dev/null
+++ b/EonaCat.LogStack/EonaCatLoggerCore/Boosters/UptimeBooster.cs
@@ -0,0 +1,25 @@
+using EonaCat.LogStack.Core;
+using System;
+using System.Diagnostics;
+using System.Runtime.CompilerServices;
+
+namespace EonaCat.LogStack.Boosters
+{
+ // 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 sealed class UptimeBooster : BoosterBase
+ {
+ private static readonly DateTime ProcessStart = Process.GetCurrentProcess().StartTime;
+
+ public UptimeBooster() : base("Uptime") { }
+
+ [System.Runtime.CompilerServices.MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public override bool Boost(ref LogEventBuilder builder)
+ {
+ var uptime = (DateTime.Now - ProcessStart).TotalSeconds;
+ builder.WithProperty("Uptime", uptime);
+ return true;
+ }
+ }
+}
diff --git a/EonaCat.LogStack/EonaCatLoggerCore/Boosters/UserBooster.cs b/EonaCat.LogStack/EonaCatLoggerCore/Boosters/UserBooster.cs
new file mode 100644
index 0000000..9a492cc
--- /dev/null
+++ b/EonaCat.LogStack/EonaCatLoggerCore/Boosters/UserBooster.cs
@@ -0,0 +1,23 @@
+using EonaCat.LogStack.Core;
+using System;
+using System.Runtime.CompilerServices;
+
+namespace EonaCat.LogStack.Boosters
+{
+ // 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 sealed class UserBooster : BoosterBase
+ {
+ private static readonly string UserName = Environment.UserName;
+
+ public UserBooster() : base("User") { }
+
+ [System.Runtime.CompilerServices.MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public override bool Boost(ref LogEventBuilder builder)
+ {
+ builder.WithProperty("User", UserName);
+ return true;
+ }
+ }
+}
diff --git a/EonaCat.LogStack/EonaCatLoggerCore/ColorSchema.cs b/EonaCat.LogStack/EonaCatLoggerCore/ColorSchema.cs
new file mode 100644
index 0000000..ebbeffc
--- /dev/null
+++ b/EonaCat.LogStack/EonaCatLoggerCore/ColorSchema.cs
@@ -0,0 +1,74 @@
+using System;
+
+namespace EonaCat.LogStack;
+
+// 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.
+
+///
+/// Colors to use when writing to the console.
+///
+public class ColorSchema
+{
+ ///
+ /// The color to use for critical messages.
+ ///
+ public ColorScheme Critical = new(ConsoleColor.DarkRed, ConsoleColor.Black);
+
+ ///
+ /// The color to use for debug messages.
+ ///
+ public ColorScheme Debug = new(ConsoleColor.Green, ConsoleColor.Black);
+
+ ///
+ /// The color to use for error messages.
+ ///
+ public ColorScheme Error = new(ConsoleColor.Red, ConsoleColor.Black);
+
+ ///
+ /// The color to use for informational messages.
+ ///
+ public ColorScheme Info = new(ConsoleColor.Blue, ConsoleColor.Black);
+
+ ///
+ /// The color to use for emergency messages.
+ ///
+ public ColorScheme Trace = new(ConsoleColor.Cyan, ConsoleColor.Black);
+
+ ///
+ /// The color to use for alert messages.
+ ///
+ public ColorScheme Traffic = new(ConsoleColor.DarkMagenta, ConsoleColor.Black);
+
+ ///
+ /// The color to use for warning messages.
+ ///
+ public ColorScheme Warning = new(ConsoleColor.DarkYellow, ConsoleColor.Black);
+}
+
+///
+/// Color scheme for logging messages.
+///
+public class ColorScheme
+{
+ ///
+ /// Background color.
+ ///
+ public ConsoleColor Background = Console.BackgroundColor;
+
+ ///
+ /// Foreground color.
+ ///
+ public ConsoleColor Foreground = Console.ForegroundColor;
+
+ ///
+ /// Instantiates a new color scheme.
+ ///
+ /// Foreground color.
+ /// Background color.
+ public ColorScheme(ConsoleColor foreground, ConsoleColor background)
+ {
+ Foreground = foreground;
+ Background = background;
+ }
+}
\ No newline at end of file
diff --git a/EonaCat.LogStack/EonaCatLoggerCore/CompressionFormat.cs b/EonaCat.LogStack/EonaCatLoggerCore/CompressionFormat.cs
new file mode 100644
index 0000000..5989004
--- /dev/null
+++ b/EonaCat.LogStack/EonaCatLoggerCore/CompressionFormat.cs
@@ -0,0 +1,11 @@
+// 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.LogStack.EonaCatLogStackCore
+{
+ public enum CompressionFormat
+ {
+ None,
+ GZip,
+ }
+}
\ No newline at end of file
diff --git a/EonaCat.LogStack/EonaCatLoggerCore/Enums.cs b/EonaCat.LogStack/EonaCatLoggerCore/Enums.cs
new file mode 100644
index 0000000..ce21e4a
--- /dev/null
+++ b/EonaCat.LogStack/EonaCatLoggerCore/Enums.cs
@@ -0,0 +1,59 @@
+namespace EonaCat.LogStack.Core;
+
+// 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.
+
+///
+/// Defines the severity level of log entries
+///
+public enum LogLevel : byte
+{
+ None = 0,
+ Trace = 1,
+ Debug = 2,
+ Information = 3,
+ Warning = 4,
+ Error = 5,
+ Critical = 6,
+}
+
+///
+/// Result of a log write operation
+///
+public enum WriteResult : byte
+{
+ Success = 0,
+ Dropped = 1,
+ Failed = 2,
+ FlowDisabled = 3,
+ LevelFiltered = 4,
+ NoBlastZone = 5
+}
+
+///
+/// Strategy for handling backpressure in flows
+///
+public enum BackpressureStrategy : byte
+{
+ /// Wait for capacity to become available
+ Wait = 0,
+
+ /// Drop the newest incoming message
+ DropNewest = 1,
+
+ /// Drop the oldest message in the queue
+ DropOldest = 2,
+
+ /// Block until space is available (may impact performance)
+ Block = 3
+}
+
+///
+/// Options for timestamp generation
+///
+public enum TimestampMode : byte
+{
+ Utc = 0,
+ Local = 1,
+ HighPrecision = 2
+}
\ No newline at end of file
diff --git a/EonaCat.LogStack/EonaCatLoggerCore/FileOutputFormat.cs b/EonaCat.LogStack/EonaCatLoggerCore/FileOutputFormat.cs
new file mode 100644
index 0000000..e66a7e5
--- /dev/null
+++ b/EonaCat.LogStack/EonaCatLoggerCore/FileOutputFormat.cs
@@ -0,0 +1,14 @@
+// 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.LogStack.EonaCatLogStackCore
+{
+ public enum FileOutputFormat
+ {
+ Text,
+ Json,
+ Xml,
+ Csv, // RFC-4180 CSV
+ StructuredJson, // Machine-readable JSON with correlation IDs
+ }
+}
\ No newline at end of file
diff --git a/EonaCat.LogStack/EonaCatLoggerCore/Flows/AuditFlow.cs b/EonaCat.LogStack/EonaCatLoggerCore/Flows/AuditFlow.cs
new file mode 100644
index 0000000..b6f347b
--- /dev/null
+++ b/EonaCat.LogStack/EonaCatLoggerCore/Flows/AuditFlow.cs
@@ -0,0 +1,400 @@
+using EonaCat.LogStack.Core;
+using EonaCat.LogStack.EonaCatLogStackCore;
+using EonaCat.LogStack.Flows;
+using System;
+using System.Collections.Concurrent;
+using System.IO;
+using System.Linq;
+using System.Security.Cryptography;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace EonaCat.LogStack.Flows
+{
+ // 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.
+
+ ///
+ /// Audit log severity filter — only these levels are written to the audit trail.
+ ///
+ public enum AuditLevel
+ {
+ All,
+ WarningAndAbove,
+ ErrorAndAbove,
+ CriticalOnly,
+ }
+
+ ///
+ /// A tamper-evident, append-only audit flow.
+ ///
+ /// Each entry is written as:
+ /// SEQ|ISO-TIMESTAMP|LEVEL|CATEGORY|MESSAGE|PROPS|HASH
+ ///
+ /// Where HASH = SHA-256( previousHash + currentLineWithoutHash ).
+ /// This creates a hash-chain so any deletion or modification of a past
+ /// entry invalidates all subsequent hashes, making tampering detectable.
+ ///
+ /// The file is opened with FileShare.Read only (no concurrent writers).
+ /// The flow is synchronous-by-design: audit entries must land on disk
+ /// before the method returns, so blocks until
+ /// the entry is flushed.
+ ///
+ public sealed class AuditFlow : FlowBase
+ {
+ private const string Delimiter = "|";
+ private const int HashLength = 64; // hex SHA-256
+
+ private readonly string _filePath;
+ private readonly AuditLevel _auditLevel;
+ private readonly bool _includeProperties;
+
+ private readonly object _writeLock = new object();
+ private readonly FileStream _stream;
+ private readonly StreamWriter _writer;
+
+ private long _sequence;
+ private string _previousHash;
+
+ private long _totalEntries;
+
+ public AuditFlow(
+ string directory,
+ string filePrefix = "audit",
+ AuditLevel auditLevel = AuditLevel.All,
+ LogLevel minimumLevel = LogLevel.Trace,
+ bool includeProperties = true)
+ : base("Audit:" + directory, minimumLevel)
+ {
+ if (directory == null)
+ {
+ throw new ArgumentNullException(nameof(directory));
+ }
+
+ if (filePrefix == null)
+ {
+ throw new ArgumentNullException(nameof(filePrefix));
+ }
+
+ _auditLevel = auditLevel;
+ _includeProperties = includeProperties;
+
+ // Resolve relative path
+ if (directory.StartsWith("./", StringComparison.Ordinal))
+ {
+ directory = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, directory.Substring(2));
+ }
+
+ Directory.CreateDirectory(directory);
+
+ // One file per day, named with date stamp
+ string date = DateTime.UtcNow.ToString("yyyyMMdd");
+ _filePath = Path.Combine(directory, $"{filePrefix}_{Environment.MachineName}_{date}.audit");
+
+ // Exclusive write access
+ _stream = new FileStream(
+ _filePath,
+ FileMode.Append,
+ FileAccess.Write,
+ FileShare.Read, // allow external readers, but no other writers
+ bufferSize: 4096,
+ FileOptions.WriteThrough); // WriteThrough = no OS cache, hits disk immediately
+
+ _writer = new StreamWriter(_stream, Encoding.UTF8) { AutoFlush = true };
+
+ // Derive starting hash from the last line already in the file (for continuity)
+ _previousHash = ReadLastHash(directory, filePrefix, date);
+ _sequence = CountExistingLines(_filePath);
+ }
+
+ /// Path to the current audit file.
+ public string FilePath => _filePath;
+
+ /// Total entries written in this session.
+ public long TotalEntries => Interlocked.Read(ref _totalEntries);
+
+ ///
+ /// Verify the integrity of the audit file by replaying the hash chain.
+ /// Returns (true, null) if intact, (false, reason) if tampered.
+ ///
+ public static (bool ok, string reason) Verify(string filePath)
+ {
+ if (!File.Exists(filePath))
+ {
+ return (false, "File not found.");
+ }
+
+ string previousHash = new string('0', HashLength);
+ long expectedSeq = 1;
+
+ foreach (string raw in File.ReadLines(filePath, Encoding.UTF8))
+ {
+ if (string.IsNullOrWhiteSpace(raw) || raw.StartsWith("#"))
+ {
+ continue;
+ }
+
+ int lastPipe = raw.LastIndexOf(Delimiter, StringComparison.Ordinal);
+ if (lastPipe < 0)
+ {
+ return (false, $"Malformed line (no delimiter): {Truncate(raw, 120)}");
+ }
+
+ string body = raw.Substring(0, lastPipe);
+ string storedHash = raw.Substring(lastPipe + 1).Trim();
+
+ if (storedHash.Length != HashLength)
+ {
+ return (false, $"Bad hash length on line {expectedSeq}: '{storedHash}'");
+ }
+
+ string computedHash = ComputeHash(previousHash, body);
+
+ if (!string.Equals(storedHash, computedHash, StringComparison.OrdinalIgnoreCase))
+ {
+ return (false, $"Hash mismatch on sequence {expectedSeq}. " +
+ $"Expected {computedHash}, found {storedHash}. " +
+ $"Entry may have been tampered with.");
+ }
+
+ // Verify sequence number (first field)
+ int firstPipe = body.IndexOf(Delimiter, StringComparison.Ordinal);
+ if (firstPipe > 0)
+ {
+ string seqStr = body.Substring(0, firstPipe);
+ if (long.TryParse(seqStr, out long seq) && seq != expectedSeq)
+ {
+ return (false, $"Sequence gap: expected {expectedSeq}, found {seq}.");
+ }
+ }
+
+ previousHash = computedHash;
+ expectedSeq++;
+ }
+
+ return (true, null);
+ }
+
+ public override Task BlastAsync(
+ LogEvent logEvent,
+ CancellationToken cancellationToken = default)
+ {
+ if (!IsEnabled || !IsLogLevelEnabled(logEvent))
+ {
+ return Task.FromResult(WriteResult.LevelFiltered);
+ }
+
+ if (!PassesAuditLevel(logEvent.Level))
+ {
+ return Task.FromResult(WriteResult.LevelFiltered);
+ }
+
+ WriteEntry(logEvent);
+ return Task.FromResult(WriteResult.Success);
+ }
+
+ public override Task BlastBatchAsync(
+ ReadOnlyMemory logEvents,
+ CancellationToken cancellationToken = default)
+ {
+ if (!IsEnabled)
+ {
+ return Task.FromResult(WriteResult.FlowDisabled);
+ }
+
+ foreach (var e in logEvents.ToArray())
+ {
+ if (IsLogLevelEnabled(e) && PassesAuditLevel(e.Level))
+ {
+ WriteEntry(e);
+ }
+ }
+
+ return Task.FromResult(WriteResult.Success);
+ }
+
+ public override Task FlushAsync(CancellationToken cancellationToken = default)
+ {
+ lock (_writeLock)
+ {
+ _writer.Flush();
+ _stream.Flush(flushToDisk: true);
+ }
+ return Task.CompletedTask;
+ }
+
+ public override async ValueTask DisposeAsync()
+ {
+ IsEnabled = false;
+ lock (_writeLock)
+ {
+ try { _writer.Flush(); } catch { }
+ try { _stream.Flush(true); } catch { }
+ try { _writer.Dispose(); } catch { }
+ try { _stream.Dispose(); } catch { }
+ }
+ await base.DisposeAsync().ConfigureAwait(false);
+ }
+
+ private void WriteEntry(LogEvent log)
+ {
+ lock (_writeLock)
+ {
+ long seq = Interlocked.Increment(ref _sequence);
+
+ var sb = new StringBuilder(256);
+ sb.Append(seq);
+ sb.Append(Delimiter);
+ sb.Append(LogEvent.GetDateTime(log.Timestamp).ToString("O"));
+ sb.Append(Delimiter);
+ sb.Append(LevelString(log.Level));
+ sb.Append(Delimiter);
+ sb.Append(Escape(log.Category));
+ sb.Append(Delimiter);
+ sb.Append(Escape(log.Message.Length > 0 ? log.Message.ToString() : string.Empty));
+
+ if (log.Exception != null)
+ {
+ sb.Append(Delimiter);
+ sb.Append("EX=");
+ sb.Append(Escape(log.Exception.GetType().Name + ": " + log.Exception.Message));
+ }
+
+ if (_includeProperties && log.Properties.Count > 0)
+ {
+ sb.Append(Delimiter);
+ bool first = true;
+ foreach (var kv in log.Properties.ToArray())
+ {
+ if (!first)
+ {
+ sb.Append(';');
+ }
+
+ first = false;
+ sb.Append(Escape(kv.Key)).Append('=').Append(Escape(kv.Value?.ToString() ?? "null"));
+ }
+ }
+
+ string body = sb.ToString();
+ string hash = ComputeHash(_previousHash, body);
+ string line = body + Delimiter + hash;
+
+ _writer.WriteLine(line);
+ // AutoFlush=true + WriteThrough stream = immediate disk write
+
+ _previousHash = hash;
+ Interlocked.Increment(ref _totalEntries);
+ Interlocked.Increment(ref BlastedCount);
+ }
+ }
+
+ private bool PassesAuditLevel(LogLevel level) => _auditLevel switch
+ {
+ AuditLevel.All => true,
+ AuditLevel.WarningAndAbove => level >= LogLevel.Warning,
+ AuditLevel.ErrorAndAbove => level >= LogLevel.Error,
+ AuditLevel.CriticalOnly => level >= LogLevel.Critical,
+ _ => true
+ };
+
+ private static string LevelString(LogLevel level) => level switch
+ {
+ LogLevel.Trace => "TRACE",
+ LogLevel.Debug => "DEBUG",
+ LogLevel.Information => "INFO",
+ LogLevel.Warning => "WARN",
+ LogLevel.Error => "ERROR",
+ LogLevel.Critical => "CRITICAL",
+ _ => level.ToString().ToUpperInvariant()
+ };
+
+ /// Replace pipe characters inside field values so the delimiter stays unique.
+ private static string Escape(string value)
+ => string.IsNullOrEmpty(value) ? string.Empty : value.Replace("|", "\\|").Replace("\r", "\\r").Replace("\n", "\\n");
+
+ public static string ComputeHash(string previousHash, string body)
+ {
+ if (string.IsNullOrEmpty(previousHash) || string.IsNullOrEmpty(body))
+ {
+ throw new ArgumentException("Input values cannot be null or empty.");
+ }
+
+ string inputString = previousHash + "|" + body;
+ byte[] input = Encoding.UTF8.GetBytes(inputString);
+
+ using (SHA256 sha = SHA256.Create())
+ {
+ byte[] digest = sha.ComputeHash(input);
+ return BitConverter.ToString(digest).Replace("-", "").ToLowerInvariant();
+ }
+ }
+
+ private static string ReadLastHash(string directory, string prefix, string date)
+ {
+ string path = Path.Combine(directory, $"{prefix}_{Environment.MachineName}_{date}.audit");
+
+ if (!File.Exists(path))
+ {
+ return new string('0', HashLength);
+ }
+
+ string lastLine = null;
+
+ // Open file with FileShare.ReadWrite to allow reading while it's being written to
+ using (var fileStream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
+ using (var reader = new StreamReader(fileStream, Encoding.UTF8))
+ {
+ // Read lines from the file
+ while (!reader.EndOfStream)
+ {
+ var line = reader.ReadLine();
+ if (!string.IsNullOrWhiteSpace(line) && !line.StartsWith("#"))
+ {
+ lastLine = line;
+ }
+ }
+ }
+
+ if (lastLine == null)
+ {
+ return new string('0', HashLength);
+ }
+
+ int lastPipe = lastLine.LastIndexOf(Delimiter, StringComparison.Ordinal);
+ return lastPipe >= 0 ? lastLine.Substring(lastPipe + 1).Trim() : new string('0', HashLength);
+ }
+
+
+ private static long CountExistingLines(string path)
+ {
+ if (!File.Exists(path))
+ {
+ return 0;
+ }
+
+ long count = 0;
+
+ // Open the file with FileShare.ReadWrite to allow concurrent read/write access
+ using (var fileStream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
+ using (var reader = new StreamReader(fileStream, Encoding.UTF8))
+ {
+ while (!reader.EndOfStream)
+ {
+ var line = reader.ReadLine();
+ if (!string.IsNullOrWhiteSpace(line) && !line.StartsWith("#"))
+ {
+ count++;
+ }
+ }
+ }
+
+ return count;
+ }
+
+
+ private static string Truncate(string s, int max)
+ => s.Length <= max ? s : s.Substring(0, max) + "...";
+ }
+}
\ No newline at end of file
diff --git a/EonaCat.LogStack/EonaCatLoggerCore/Flows/ConsoleFlow.cs b/EonaCat.LogStack/EonaCatLoggerCore/Flows/ConsoleFlow.cs
new file mode 100644
index 0000000..2c8bd9b
--- /dev/null
+++ b/EonaCat.LogStack/EonaCatLoggerCore/Flows/ConsoleFlow.cs
@@ -0,0 +1,285 @@
+using EonaCat.LogStack.Core;
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Runtime.CompilerServices;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace EonaCat.LogStack.Flows
+{
+ // 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.
+
+ ///
+ /// console flow with color support and minimal allocations
+ /// Uses a ColorSchema for configurable colors
+ ///
+ public sealed class ConsoleFlow : FlowBase
+ {
+ private readonly bool _useColors;
+ private readonly TimestampMode _timestampMode;
+ private readonly StringBuilder _buffer = new(1024);
+ private readonly object _consoleLock = new();
+ private readonly ColorSchema _colors;
+
+ private readonly string _template;
+ private List> _compiledTemplate;
+
+ public ConsoleFlow(
+ LogLevel minimumLevel = LogLevel.Trace,
+ bool useColors = true,
+ TimestampMode timestampMode = TimestampMode.Local,
+ ColorSchema? colorSchema = null,
+ string template = "[{ts}] [{tz}] [Host: {host}] [Category: {category}] [Thread: {thread}] [{logtype}] {message}{props}")
+ : base("Console", minimumLevel)
+ {
+ _useColors = useColors;
+ _timestampMode = timestampMode;
+ _colors = colorSchema ?? new ColorSchema();
+ _template = template ?? throw new ArgumentNullException(nameof(template));
+
+ CompileTemplate(_template);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public override Task BlastAsync(LogEvent logEvent, CancellationToken cancellationToken = default)
+ {
+ if (!IsEnabled || !IsLogLevelEnabled(logEvent))
+ {
+ return Task.FromResult(WriteResult.LevelFiltered);
+ }
+
+ WriteToConsole(logEvent);
+ Interlocked.Increment(ref BlastedCount);
+ return Task.FromResult(WriteResult.Success);
+ }
+
+ public override Task BlastBatchAsync(ReadOnlyMemory logEvents, CancellationToken cancellationToken = default)
+ {
+ if (!IsEnabled)
+ {
+ return Task.FromResult(WriteResult.FlowDisabled);
+ }
+
+ foreach (var logEvent in logEvents.Span)
+ {
+ if (logEvent.Level >= MinimumLevel)
+ {
+ WriteToConsole(logEvent);
+ Interlocked.Increment(ref BlastedCount);
+ }
+ }
+
+ return Task.FromResult(WriteResult.Success);
+ }
+
+ private void WriteToConsole(LogEvent logEvent)
+ {
+ lock (_consoleLock)
+ {
+ _buffer.Clear();
+
+ foreach (var action in _compiledTemplate)
+ {
+ action(logEvent, _buffer);
+ }
+
+ if (_useColors && TryGetColor(logEvent.Level, out var color))
+ {
+ Console.ForegroundColor = color.Foreground;
+ }
+
+ Console.WriteLine(_buffer.ToString());
+
+ if (logEvent.Exception != null)
+ {
+ if (_useColors)
+ {
+ Console.ForegroundColor = ConsoleColor.DarkRed;
+ }
+
+ Console.WriteLine(logEvent.Exception.ToString());
+
+ if (_useColors)
+ {
+ Console.ResetColor();
+ }
+ }
+
+ if (_useColors)
+ {
+ Console.ResetColor();
+ }
+ }
+ }
+
+ private void CompileTemplate(string template)
+ {
+ _compiledTemplate = new List>();
+ int pos = 0;
+
+ while (pos < template.Length)
+ {
+ int open = template.IndexOf('{', pos);
+ if (open < 0)
+ {
+ string lit = template.Substring(pos);
+ _compiledTemplate.Add((_, sb) => sb.Append(lit));
+ break;
+ }
+
+ if (open > pos)
+ {
+ string lit = template.Substring(pos, open - pos);
+ _compiledTemplate.Add((_, sb) => sb.Append(lit));
+ }
+
+ int close = template.IndexOf('}', open);
+ if (close < 0)
+ {
+ string lit = template.Substring(open);
+ _compiledTemplate.Add((_, sb) => sb.Append(lit));
+ break;
+ }
+
+ string token = template.Substring(open + 1, close - open - 1);
+ _compiledTemplate.Add(ResolveToken(token));
+ pos = close + 1;
+ }
+ }
+
+ private Action ResolveToken(string token)
+ {
+ switch (token.ToLowerInvariant())
+ {
+ case "ts":
+ return (log, sb) =>
+ sb.Append(LogEvent.GetDateTime(log.Timestamp)
+ .ToString("yyyy-MM-dd HH:mm:ss.fff"));
+
+ case "tz":
+ return (_, sb) =>
+ sb.Append(_timestampMode == TimestampMode.Local
+ ? TimeZoneInfo.Local.StandardName
+ : "UTC");
+
+ case "host":
+ return (_, sb) => sb.Append(Environment.MachineName);
+
+ case "category":
+ return (log, sb) =>
+ {
+ if (!string.IsNullOrEmpty(log.Category))
+ {
+ sb.Append(log.Category);
+ }
+ };
+
+ case "thread":
+ return (_, sb) => sb.Append(Thread.CurrentThread.ManagedThreadId);
+
+ case "pid":
+ return (_, sb) => sb.Append(Process.GetCurrentProcess().Id);
+
+ case "message":
+ return (log, sb) => sb.Append(log.Message);
+
+ case "props":
+ return AppendProperties;
+
+ case "newline":
+ return (_, sb) => sb.AppendLine();
+
+ case "logtype":
+ return (log, sb) =>
+ {
+ var levelText = GetLevelText(log.Level);
+
+ if (_useColors && TryGetColor(log.Level, out var color))
+ {
+ Console.ForegroundColor = color.Foreground;
+ Console.BackgroundColor = color.Background;
+
+ Console.Write(sb.ToString());
+ Console.Write(levelText);
+
+ Console.ResetColor();
+ sb.Clear();
+ }
+ else
+ {
+ sb.Append(levelText);
+ }
+ };
+
+ default:
+ return (_, sb) => sb.Append('{').Append(token).Append('}');
+ }
+ }
+
+ private void AppendProperties(LogEvent log, StringBuilder sb)
+ {
+ if (log.Properties.Count == 0)
+ {
+ return;
+ }
+
+ sb.Append(" {");
+
+ bool first = true;
+ foreach (var prop in log.Properties)
+ {
+ if (!first)
+ {
+ sb.Append(", ");
+ }
+
+ sb.Append(prop.Key);
+ sb.Append('=');
+ sb.Append(prop.Value?.ToString() ?? "null");
+
+ first = false;
+ }
+
+ sb.Append('}');
+ }
+
+ private bool TryGetColor(LogLevel level, out ColorScheme color)
+ {
+ color = level switch
+ {
+ LogLevel.Trace => _colors.Trace,
+ LogLevel.Debug => _colors.Debug,
+ LogLevel.Information => _colors.Info,
+ LogLevel.Warning => _colors.Warning,
+ LogLevel.Error => _colors.Error,
+ LogLevel.Critical => _colors.Critical,
+ _ => _colors.Info
+ };
+ return color != null;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static string GetLevelText(LogLevel level)
+ {
+ return level switch
+ {
+ LogLevel.Trace => "TRACE",
+ LogLevel.Debug => "DEBUG",
+ LogLevel.Information => "INFO",
+ LogLevel.Warning => "WARN",
+ LogLevel.Error => "ERROR",
+ LogLevel.Critical => "CRITICAL",
+ _ => "???"
+ };
+ }
+
+ public override Task FlushAsync(CancellationToken cancellationToken = default)
+ {
+ // Console auto-flushes
+ return Task.CompletedTask;
+ }
+ }
+}
diff --git a/EonaCat.LogStack/EonaCatLoggerCore/Flows/DatabaseFlow.cs b/EonaCat.LogStack/EonaCatLoggerCore/Flows/DatabaseFlow.cs
new file mode 100644
index 0000000..e10f3ce
--- /dev/null
+++ b/EonaCat.LogStack/EonaCatLoggerCore/Flows/DatabaseFlow.cs
@@ -0,0 +1,231 @@
+using EonaCat.Json;
+using EonaCat.LogStack.Core;
+using System;
+using System.Collections.Generic;
+using System.Data.Common;
+using System.Text;
+using System.Threading;
+using System.Threading.Channels;
+using System.Threading.Tasks;
+
+namespace EonaCat.LogStack.Flows
+{
+ // 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.
+
+ ///
+ /// database flow with batched inserts for any ADO.NET database
+ ///
+ public sealed class DatabaseFlow : FlowBase
+ {
+ private const int ChannelCapacity = 4096;
+ private const int DefaultBatchSize = 128;
+
+ private readonly Channel _channel;
+ private readonly Task _writerTask;
+ private readonly CancellationTokenSource _cts;
+
+ private readonly Func _connectionFactory;
+ private readonly string _tableName;
+
+ public DatabaseFlow(
+ Func connectionFactory,
+ string tableName = "Logs",
+ LogLevel minimumLevel = LogLevel.Trace)
+ : base($"Database:{tableName}", minimumLevel)
+ {
+ _connectionFactory = connectionFactory ?? throw new ArgumentNullException(nameof(connectionFactory));
+ _tableName = tableName;
+
+ var channelOptions = new BoundedChannelOptions(ChannelCapacity)
+ {
+ FullMode = BoundedChannelFullMode.DropOldest,
+ SingleReader = true,
+ SingleWriter = false
+ };
+
+ _channel = Channel.CreateBounded(channelOptions);
+ _cts = new CancellationTokenSource();
+ _writerTask = Task.Run(() => ProcessLogEventsAsync(_cts.Token));
+ }
+
+ public override Task BlastAsync(LogEvent logEvent, CancellationToken cancellationToken = default)
+ {
+ if (!IsEnabled || !IsLogLevelEnabled(logEvent))
+ {
+ return Task.FromResult(WriteResult.LevelFiltered);
+ }
+
+ if (_channel.Writer.TryWrite(logEvent))
+ {
+ Interlocked.Increment(ref BlastedCount);
+ return Task.FromResult(WriteResult.Success);
+ }
+
+ Interlocked.Increment(ref DroppedCount);
+ return Task.FromResult(WriteResult.Dropped);
+ }
+
+ public override async Task FlushAsync(CancellationToken cancellationToken = default)
+ {
+ _channel.Writer.Complete();
+ try
+ {
+ await _writerTask.ConfigureAwait(false);
+ }
+ catch (OperationCanceledException) { }
+ }
+
+ private async Task ProcessLogEventsAsync(CancellationToken cancellationToken)
+ {
+ var batch = new List(DefaultBatchSize);
+
+ try
+ {
+ while (await _channel.Reader.WaitToReadAsync(cancellationToken))
+ {
+ while (_channel.Reader.TryRead(out var logEvent))
+ {
+ batch.Add(logEvent);
+
+ if (batch.Count >= DefaultBatchSize)
+ {
+ await WriteBatchAsync(batch, cancellationToken).ConfigureAwait(false);
+ batch.Clear();
+ }
+ }
+
+ if (batch.Count > 0)
+ {
+ await WriteBatchAsync(batch, cancellationToken).ConfigureAwait(false);
+ batch.Clear();
+ }
+ }
+
+ if (batch.Count > 0)
+ {
+ await WriteBatchAsync(batch, cancellationToken).ConfigureAwait(false);
+ batch.Clear();
+ }
+ }
+ catch (OperationCanceledException) { }
+ catch (Exception ex)
+ {
+ Console.Error.WriteLine($"DatabaseFlow error: {ex.Message}");
+ }
+ }
+
+ private async Task WriteBatchAsync(List batch, CancellationToken cancellationToken)
+ {
+ using var connection = _connectionFactory();
+ await connection.OpenAsync(cancellationToken);
+
+ using var transaction = connection.BeginTransaction();
+
+ // Build a single SQL command with multiple inserts
+ var sb = new StringBuilder();
+ var parameters = new List();
+ int paramIndex = 0;
+
+ foreach (var logEvent in batch)
+ {
+ sb.Append($"INSERT INTO {_tableName} (Timestamp, Level, Category, Message, ThreadId, Exception, Properties) VALUES (");
+
+ // Timestamp
+ var timestampParam = CreateParameter(connection, $"@p{paramIndex++}", LogEvent.GetDateTime(logEvent.Timestamp).ToString("O"));
+ parameters.Add(timestampParam);
+ sb.Append(timestampParam.ParameterName).Append(", ");
+
+ // Level
+ var levelParam = CreateParameter(connection, $"@p{paramIndex++}", logEvent.Level.ToString());
+ parameters.Add(levelParam);
+ sb.Append(levelParam.ParameterName).Append(", ");
+
+ // Category
+ var categoryParam = CreateParameter(connection, $"@p{paramIndex++}", logEvent.Category ?? string.Empty);
+ parameters.Add(categoryParam);
+ sb.Append(categoryParam.ParameterName).Append(", ");
+
+ // Message
+ var messageParam = CreateParameter(connection, $"@p{paramIndex++}", logEvent.Message.ToString());
+ parameters.Add(messageParam);
+ sb.Append(messageParam.ParameterName).Append(", ");
+
+ // ThreadId
+ var threadParam = CreateParameter(connection, $"@p{paramIndex++}", logEvent.ThreadId);
+ parameters.Add(threadParam);
+ sb.Append(threadParam.ParameterName).Append(", ");
+
+ // Exception
+ object exValue = logEvent.Exception != null
+ ? JsonHelper.ToJson(new
+ {
+ type = logEvent.Exception.GetType().FullName,
+ message = logEvent.Exception.Message,
+ stackTrace = logEvent.Exception.StackTrace
+ })
+ : DBNull.Value;
+
+ var exParam = CreateParameter(connection, $"@p{paramIndex++}", exValue);
+ parameters.Add(exParam);
+ sb.Append(exParam.ParameterName).Append(", ");
+
+ // Properties
+ object propsValue = logEvent.Properties.Count > 0
+ ? JsonHelper.ToJson(logEvent.Properties)
+ : DBNull.Value;
+
+ var propsParam = CreateParameter(connection, $"@p{paramIndex++}", propsValue);
+ parameters.Add(propsParam);
+ sb.Append(propsParam.ParameterName).Append(");");
+ }
+
+ using var command = connection.CreateCommand();
+ command.Transaction = transaction;
+ command.CommandText = sb.ToString();
+
+ foreach (var p in parameters)
+ {
+ command.Parameters.Add(p);
+ }
+
+ await command.ExecuteNonQueryAsync(cancellationToken);
+ transaction.Commit();
+ }
+
+ private static Dictionary ToDictionary(ReadOnlyMemory> properties)
+ {
+ var dict = new Dictionary();
+ foreach (var prop in properties.Span)
+ {
+ dict[prop.Key] = prop.Value;
+ }
+
+ return dict;
+ }
+
+ private static DbParameter CreateParameter(DbConnection connection, string name, object value)
+ {
+ var p = connection.CreateCommand().CreateParameter();
+ p.ParameterName = name;
+ p.Value = value ?? DBNull.Value;
+ return p;
+ }
+
+ public override async ValueTask DisposeAsync()
+ {
+ IsEnabled = false;
+ _channel.Writer.Complete();
+ _cts.Cancel();
+
+ try
+ {
+ await _writerTask.ConfigureAwait(false);
+ }
+ catch { }
+
+ _cts.Dispose();
+ await base.DisposeAsync();
+ }
+ }
+}
diff --git a/EonaCat.LogStack/EonaCatLoggerCore/Flows/DiagnosticsFlow.cs b/EonaCat.LogStack/EonaCatLoggerCore/Flows/DiagnosticsFlow.cs
new file mode 100644
index 0000000..6f36364
--- /dev/null
+++ b/EonaCat.LogStack/EonaCatLoggerCore/Flows/DiagnosticsFlow.cs
@@ -0,0 +1,300 @@
+using EonaCat.LogStack.Core;
+using EonaCat.LogStack.EonaCatLogStackCore;
+using EonaCat.LogStack.Extensions;
+using Microsoft.Extensions.Primitives;
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace EonaCat.LogStack.Flows
+{
+ // 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.
+
+ ///
+ /// Diagnostic counters snapshot emitted on a regular interval.
+ ///
+ public sealed class DiagnosticsSnapshot
+ {
+ public DateTime CapturedAt { get; internal set; }
+ public double CpuPercent { get; internal set; }
+ public long WorkingSetBytes { get; internal set; }
+ public long GcGen0 { get; internal set; }
+ public long GcGen1 { get; internal set; }
+ public long GcGen2 { get; internal set; }
+ public long ThreadCount { get; internal set; }
+ public long HandleCount { get; internal set; }
+ public double UptimeSeconds { get; internal set; }
+ public Dictionary Custom { get; internal set; }
+ }
+
+ ///
+ /// A flow that periodically captures process diagnostics (CPU, memory, GC, threads)
+ /// and writes them as structured log events. Also acts as a pass-through: every
+ /// normal log event optionally gets runtime metrics injected as properties.
+ ///
+ /// Additionally exposes an in-process registry so application
+ /// code can record business metrics (request count, error rate, etc.) that are
+ /// flushed alongside diagnostic snapshots.
+ ///
+ public sealed class DiagnosticsFlow : FlowBase
+ {
+ /// Counter for business metrics.
+ public sealed class Counter
+ {
+ private long _value;
+ public string Name { get; }
+ public Counter(string name) { Name = name; }
+ public void Increment() { Interlocked.Increment(ref _value); }
+ public void IncrementBy(long delta) { Interlocked.Add(ref _value, delta); }
+ public void Reset() { Interlocked.Exchange(ref _value, 0); }
+ public long Value { get { return Interlocked.Read(ref _value); } }
+ }
+
+ private readonly ConcurrentDictionary _counters
+ = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase);
+
+ private readonly TimeSpan _snapshotInterval;
+ private readonly bool _injectIntoEvents;
+ private readonly bool _writeSnapshotEvents;
+ private readonly string _snapshotCategory;
+ private readonly IFlow _forwardTo;
+ private readonly Func> _customMetricsFactory;
+
+ private readonly CancellationTokenSource _cts = new CancellationTokenSource();
+ private readonly Thread _samplerThread;
+ private readonly Stopwatch _uptime = Stopwatch.StartNew();
+
+ private volatile DiagnosticsSnapshot _latest;
+
+ private TimeSpan _lastCpuTime;
+ private DateTime _lastCpuSample;
+ private readonly Process _proc;
+
+ public DiagnosticsSnapshot LatestSnapshot { get { return _latest; } }
+
+ public DiagnosticsFlow(
+ TimeSpan snapshotInterval = default(TimeSpan),
+ bool injectIntoEvents = false,
+ bool writeSnapshotEvents = true,
+ string snapshotCategory = "Diagnostics",
+ IFlow forwardTo = null,
+ LogLevel minimumLevel = LogLevel.Trace,
+ Func> customMetrics = null)
+ : base("Diagnostics", minimumLevel)
+ {
+ _snapshotInterval = snapshotInterval == default(TimeSpan)
+ ? TimeSpan.FromSeconds(60)
+ : snapshotInterval;
+ _injectIntoEvents = injectIntoEvents;
+ _writeSnapshotEvents = writeSnapshotEvents;
+ _snapshotCategory = snapshotCategory ?? "Diagnostics";
+ _forwardTo = forwardTo;
+ _customMetricsFactory = customMetrics;
+
+ _proc = Process.GetCurrentProcess();
+ _lastCpuTime = _proc.TotalProcessorTime;
+ _lastCpuSample = DateTime.UtcNow;
+
+ _samplerThread = new Thread(SamplerLoop)
+ {
+ IsBackground = true,
+ Name = "DiagnosticsFlow.Sampler",
+ Priority = ThreadPriority.BelowNormal
+ };
+ _samplerThread.Start();
+ }
+
+ /// Gets or creates a named counter.
+ public Counter GetCounter(string name)
+ {
+ if (name == null)
+ {
+ throw new ArgumentNullException("name");
+ }
+
+ return _counters.GetOrAdd(name, n => new Counter(n));
+ }
+
+ /// Current value of a named counter (0 if not yet created).
+ public long ReadCounter(string name)
+ {
+ Counter c;
+ return _counters.TryGetValue(name, out c) ? c.Value : 0;
+ }
+
+ public override Task BlastAsync(
+ LogEvent logEvent,
+ CancellationToken cancellationToken = default(CancellationToken))
+ {
+ if (!IsEnabled || !IsLogLevelEnabled(logEvent))
+ {
+ return Task.FromResult(WriteResult.LevelFiltered);
+ }
+
+ if (_injectIntoEvents)
+ {
+ DiagnosticsSnapshot snap = _latest;
+ if (snap != null)
+ {
+ logEvent.Properties.TryAdd("diag.mem_mb",(snap.WorkingSetBytes / 1024 / 1024).ToString());
+ logEvent.Properties.TryAdd("diag.cpu",snap.CpuPercent.ToString("F1"));
+ logEvent.Properties.TryAdd("diag.threads",snap.ThreadCount.ToString());
+ }
+ }
+
+ Interlocked.Increment(ref BlastedCount);
+ return Task.FromResult(WriteResult.Success);
+ }
+
+ public override Task BlastBatchAsync(
+ ReadOnlyMemory logEvents,
+ CancellationToken cancellationToken = default(CancellationToken))
+ {
+ if (!IsEnabled)
+ {
+ return Task.FromResult(WriteResult.FlowDisabled);
+ }
+
+ foreach (LogEvent e in logEvents.ToArray())
+ {
+ if (IsLogLevelEnabled(e))
+ {
+ BlastAsync(e, cancellationToken);
+ }
+ }
+ return Task.FromResult(WriteResult.Success);
+ }
+
+ public override Task FlushAsync(CancellationToken cancellationToken = default(CancellationToken))
+ => Task.FromResult(0);
+
+ public override async ValueTask DisposeAsync()
+ {
+ IsEnabled = false;
+ _cts.Cancel();
+ _samplerThread.Join(TimeSpan.FromSeconds(3));
+ _cts.Dispose();
+ await base.DisposeAsync().ConfigureAwait(false);
+ }
+
+
+ private void SamplerLoop()
+ {
+ while (!_cts.Token.IsCancellationRequested)
+ {
+ try
+ {
+ Thread.Sleep(_snapshotInterval);
+ DiagnosticsSnapshot snap = Capture();
+ _latest = snap;
+
+ if (_writeSnapshotEvents && _forwardTo != null)
+ {
+ LogEvent ev = BuildSnapshotEvent(snap);
+ _forwardTo.BlastAsync(ev).GetAwaiter().GetResult();
+ }
+ }
+ catch (ThreadInterruptedException) { break; }
+ catch (Exception ex)
+ {
+ Console.Error.WriteLine("[DiagnosticsFlow] Sampler error: " + ex.Message);
+ }
+ }
+ }
+
+ private DiagnosticsSnapshot Capture()
+ {
+ _proc.Refresh();
+
+ DateTime now = DateTime.UtcNow;
+ TimeSpan cpuNow = _proc.TotalProcessorTime;
+ double elapsed = (now - _lastCpuSample).TotalSeconds;
+ double cpu = elapsed > 0
+ ? (cpuNow - _lastCpuTime).TotalSeconds / elapsed / Environment.ProcessorCount * 100.0
+ : 0;
+
+ _lastCpuTime = cpuNow;
+ _lastCpuSample = now;
+
+ Dictionary custom = null;
+ if (_customMetricsFactory != null)
+ {
+ try { custom = _customMetricsFactory(); } catch { }
+ }
+
+ // Append counters to custom dict
+ if (_counters.Count > 0)
+ {
+ if (custom == null)
+ {
+ custom = new Dictionary(StringComparer.OrdinalIgnoreCase);
+ }
+
+ foreach (KeyValuePair kv in _counters)
+ {
+ custom["counter." + kv.Key] = kv.Value.Value;
+ }
+ }
+
+ return new DiagnosticsSnapshot
+ {
+ CapturedAt = now,
+ CpuPercent = Math.Round(cpu, 2),
+ WorkingSetBytes = _proc.WorkingSet64,
+ GcGen0 = GC.CollectionCount(0),
+ GcGen1 = GC.CollectionCount(1),
+ GcGen2 = GC.CollectionCount(2),
+ ThreadCount = _proc.Threads.Count,
+ HandleCount = _proc.HandleCount,
+ UptimeSeconds = _uptime.Elapsed.TotalSeconds,
+ Custom = custom
+ };
+ }
+
+ private LogEvent BuildSnapshotEvent(DiagnosticsSnapshot snap)
+ {
+ var sb = new StringBuilder(256);
+ sb.AppendFormat(
+ "Diagnostics | CPU={0:F1}% Mem={1}MB GC=[{2},{3},{4}] Threads={5} Handles={6} Uptime={7:F0}s",
+ snap.CpuPercent,
+ snap.WorkingSetBytes / 1024 / 1024,
+ snap.GcGen0, snap.GcGen1, snap.GcGen2,
+ snap.ThreadCount,
+ snap.HandleCount,
+ snap.UptimeSeconds);
+
+ var ev = new LogEvent
+ {
+ Level = LogLevel.Information,
+ Category = _snapshotCategory,
+ Message = new StringSegment(sb.ToString()),
+ Timestamp = snap.CapturedAt.Ticks
+ };
+
+ ev.Properties.TryAdd("cpu_pct", snap.CpuPercent.ToString("F2"));
+ ev.Properties.TryAdd("mem_bytes", snap.WorkingSetBytes.ToString());
+ ev.Properties.TryAdd("gc_gen0", snap.GcGen0.ToString());
+ ev.Properties.TryAdd("gc_gen1", snap.GcGen1.ToString());
+ ev.Properties.TryAdd("gc_gen2", snap.GcGen2.ToString());
+ ev.Properties.TryAdd("threads", snap.ThreadCount.ToString());
+ ev.Properties.TryAdd("handles", snap.HandleCount.ToString());
+ ev.Properties.TryAdd("uptime_s", snap.UptimeSeconds.ToString("F0"));
+
+ if (snap.Custom != null)
+ {
+ foreach (KeyValuePair kv in snap.Custom)
+ {
+ ev.Properties.TryAdd(kv.Key, kv.Value != null ? kv.Value.ToString() : "null");
+ }
+ }
+
+ return ev;
+ }
+ }
+}
\ No newline at end of file
diff --git a/EonaCat.LogStack/EonaCatLoggerCore/Flows/DiscordFlow.cs b/EonaCat.LogStack/EonaCatLoggerCore/Flows/DiscordFlow.cs
new file mode 100644
index 0000000..b437223
--- /dev/null
+++ b/EonaCat.LogStack/EonaCatLoggerCore/Flows/DiscordFlow.cs
@@ -0,0 +1,197 @@
+using EonaCat.Json;
+using EonaCat.LogStack.Core;
+using System;
+using System.Collections.Generic;
+using System.Net.Http;
+using System.Text;
+using System.Threading;
+using System.Threading.Channels;
+using System.Threading.Tasks;
+
+namespace EonaCat.LogStack.Flows
+{
+ // 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.
+
+ ///
+ /// logging flow that sends messages to a Discord channel via webhook.
+ ///
+ public sealed class DiscordFlow : FlowBase, IAsyncDisposable
+ {
+ private const int ChannelCapacity = 4096;
+ private const int DefaultBatchSize = 10;
+
+ private readonly Channel _channel;
+ private readonly Task _workerTask;
+ private readonly CancellationTokenSource _cts;
+ private readonly HttpClient _httpClient;
+ private readonly string _webhookUrl;
+
+ public DiscordFlow(
+ string webhookUrl,
+ string botName,
+ LogLevel minimumLevel = LogLevel.Information)
+ : base("Discord", minimumLevel)
+ {
+ _webhookUrl = webhookUrl ?? throw new ArgumentNullException(nameof(webhookUrl));
+ _httpClient = new HttpClient();
+
+ var channelOptions = new BoundedChannelOptions(ChannelCapacity)
+ {
+ FullMode = BoundedChannelFullMode.DropOldest,
+ SingleReader = true,
+ SingleWriter = false
+ };
+
+ _channel = Channel.CreateBounded(channelOptions);
+ _cts = new CancellationTokenSource();
+ _workerTask = Task.Run(() => ProcessQueueAsync(botName, _cts.Token));
+ }
+
+ public override Task BlastAsync(LogEvent logEvent, CancellationToken cancellationToken = default)
+ {
+ if (!IsEnabled || !IsLogLevelEnabled(logEvent))
+ {
+ return Task.FromResult(WriteResult.LevelFiltered);
+ }
+
+ if (_channel.Writer.TryWrite(logEvent))
+ {
+ Interlocked.Increment(ref BlastedCount);
+ return Task.FromResult(WriteResult.Success);
+ }
+
+ Interlocked.Increment(ref DroppedCount);
+ return Task.FromResult(WriteResult.Dropped);
+ }
+
+ private async Task ProcessQueueAsync(string botName, CancellationToken cancellationToken)
+ {
+ var batch = new List(DefaultBatchSize);
+
+ try
+ {
+ while (await _channel.Reader.WaitToReadAsync(cancellationToken))
+ {
+ while (_channel.Reader.TryRead(out var logEvent))
+ {
+ batch.Add(logEvent);
+
+ if (batch.Count >= DefaultBatchSize)
+ {
+ await SendBatchAsync(botName, batch, cancellationToken);
+ batch.Clear();
+ }
+ }
+
+ if (batch.Count > 0)
+ {
+ await SendBatchAsync(botName, batch, cancellationToken);
+ batch.Clear();
+ }
+ }
+ }
+ catch (OperationCanceledException) { }
+ catch (Exception ex)
+ {
+ Console.Error.WriteLine($"DiscordFlow error: {ex.Message}");
+ }
+ }
+
+ private async Task SendBatchAsync(string botName, List batch, CancellationToken cancellationToken)
+ {
+ foreach (var logEvent in batch)
+ {
+ var content = new
+ {
+ username = botName,
+ embeds = new[]
+ {
+ new
+ {
+ title = logEvent.Level.ToString(),
+ description = logEvent.Message,
+ color = GetDiscordColor(logEvent.Level),
+ timestamp = LogEvent.GetDateTime(logEvent.Timestamp).ToString("O"),
+ fields = logEvent.Properties.Count > 0
+ ? GetFields(logEvent)
+ : Array.Empty