Initial version

This commit is contained in:
2026-02-27 21:12:55 +01:00
parent d2bbbd8bc7
commit a73beb6ed5
184 changed files with 90370 additions and 63 deletions

View File

@@ -0,0 +1,39 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<PackageId>EonaCat.LogStack.LogClient</PackageId>
<Version>0.0.1</Version>
<Authors>EonaCat (Jeroen Saey)</Authors>
<Description>Logging client for the EonaCat Logger LogServer LogStack</Description>
<PackageTags>logging;monitoring;analytics;diagnostics</PackageTags>
<Copyright>EonaCat (Jeroen Saey)</Copyright>
<PackageIcon>icon.png</PackageIcon>
<PackageReadmeFile>readme.md</PackageReadmeFile>
<RepositoryUrl>https://git.saey.me/EonaCat/EonaCat.LogStack.LogClient</RepositoryUrl>
<RepositoryType>git</RepositoryType>
<PackageLicenseFile>LICENSE</PackageLicenseFile>
</PropertyGroup>
<ItemGroup>
<None Include="..\EonaCat.LogStack\icon.png">
<Pack>True</Pack>
<PackagePath>\</PackagePath>
</None>
<None Include="..\LICENSE">
<Pack>True</Pack>
<PackagePath>\</PackagePath>
</None>
</ItemGroup>
<ItemGroup>
<PackageReference Include="System.Net.Http.Json" Version="10.0.3" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\EonaCat.LogStack\EonaCat.LogStack.csproj" />
</ItemGroup>
<ItemGroup>
<None Update="readme.md">
<Pack>True</Pack>
<PackagePath>\</PackagePath>
</None>
</ItemGroup>
</Project>

View File

@@ -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<LogEntry> _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<LogEntry>();
_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<string, object>? 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<string, object>? properties = null)
{
await LogAsync(new LogEntry
{
Level = (int)LogLevel.Security,
Category = "Security",
Message = $"[{eventType}] {message}",
Properties = properties
});
}
public async Task LogAnalyticsAsync(string eventName,
Dictionary<string, object>? 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<LogEntry>();
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<LogEntry> 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<string, object>? Properties { get; set; }
public string? UserId { get; set; }
public string? SessionId { get; set; }
public string? RequestId { get; set; }
public string? CorrelationId { get; set; }
}
}

View File

@@ -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<string, object>
{
{ "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);
}
}
}

View File

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

View File

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

View File

@@ -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<string, object>? Properties
{
get => string.IsNullOrEmpty(PropertiesJson)
? null
: JsonHelper.ToObject<Dictionary<string, object>>(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
};
}
}

View File

@@ -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<string, object>
{
["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<string, object>
{
["OrderId"] = "12345",
["CustomerId"] = "cust-789"
});
}
```
### Security Event Logging
```csharp
await logClient.LogSecurityEventAsync(
"LoginAttempt",
"Failed login attempt detected",
new Dictionary<string, object>
{
["Username"] = "admin",
["IPAddress"] = "192.168.1.100",
["Attempts"] = 5
}
);
await logClient.LogSecurityEventAsync(
"UnauthorizedAccess",
"Unauthorized API access attempt",
new Dictionary<string, object>
{
["Endpoint"] = "/api/admin/users",
["Method"] = "DELETE",
["UserId"] = "user456"
}
);
```
### Analytics Logging
```csharp
// Track user events
await logClient.LogAnalyticsAsync("PageView",
new Dictionary<string, object>
{
["Page"] = "/products/electronics",
["Duration"] = 45.2,
["Source"] = "Google"
}
);
await logClient.LogAnalyticsAsync("Purchase",
new Dictionary<string, object>
{
["ProductId"] = "prod-123",
["Price"] = 299.99,
["Category"] = "Electronics",
["PaymentMethod"] = "CreditCard"
}
);
await logClient.LogAnalyticsAsync("FeatureUsage",
new Dictionary<string, object>
{
["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<string, object>
{
["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<string, object> { ["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<string, object> { ["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<string, object>
{
["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