remote logging

This commit is contained in:
iTob 2026-03-01 16:13:49 +01:00
parent 9b39de7b76
commit 28ae78cea7
8 changed files with 310 additions and 49 deletions

View File

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="Encoding" addBOMForNewFiles="with BOM under Windows, with no BOM otherwise" /> <component name="Encoding" defaultCharsetForPropertiesFiles="UTF-8" />
</project> </project>

View File

@ -1,5 +1,5 @@
using System.IO; using System.IO;
using System.Windows; using System.Windows;
using CamBooth.App.Core.AppSettings; using CamBooth.App.Core.AppSettings;
using CamBooth.App.Core.Logging; using CamBooth.App.Core.Logging;
@ -11,6 +11,7 @@ using EDSDKLib.API.Base;
using EOSDigital.API; using EOSDigital.API;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
namespace CamBooth.App; namespace CamBooth.App;
@ -26,8 +27,25 @@ public partial class App : Application
{ {
base.OnStartup(e); base.OnStartup(e);
// Konfiguration laden
var environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Production";
var configBuilder = new ConfigurationBuilder()
.SetBasePath(AppContext.BaseDirectory)
.AddJsonFile("Core/AppSettings/app.settings.json", optional: false, reloadOnChange: true);
if (environment == "Development")
{
configBuilder.AddJsonFile("Core/AppSettings/app.settings.dev.json", optional: true, reloadOnChange: true);
}
var configuration = configBuilder.Build();
var services = new ServiceCollection(); var services = new ServiceCollection();
// Register Configuration
services.AddSingleton<IConfiguration>(configuration);
// Register base services // Register base services
services.AddSingleton<Logger>(); services.AddSingleton<Logger>();
services.AddSingleton<AppSettingsService>(); services.AddSingleton<AppSettingsService>();

View File

@ -25,6 +25,13 @@
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="9.0.1" /> <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="9.0.1" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.1" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.1" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="9.0.1" /> <PackageReference Include="Microsoft.Extensions.Logging.Console" Version="9.0.1" />
<PackageReference Include="Serilog" Version="4.3.1" />
<PackageReference Include="Serilog.Formatting.Compact" Version="3.0.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
<PackageReference Include="Serilog.Sinks.Http" Version="9.0.0" />
<PackageReference Include="Serilog.Settings.Configuration" Version="8.0.4" />
<PackageReference Include="Serilog.Sinks.Seq" Version="9.0.0" />
<PackageReference Include="WPF-UI" Version="4.0.0-rc.3" /> <PackageReference Include="WPF-UI" Version="4.0.0-rc.3" />
</ItemGroup> </ItemGroup>

View File

@ -1,4 +1,4 @@
using CamBooth.App.Core.Logging; using CamBooth.App.Core.Logging;
namespace CamBooth.App.Core.AppSettings; namespace CamBooth.App.Core.AppSettings;
@ -62,6 +62,15 @@ public class AppSettingsService
public string ConfigFileName => loadedConfigFile; public string ConfigFileName => loadedConfigFile;
// Logging Settings
public string LogLevel => configuration["LoggingSettings:LogLevel"] ?? "Information";
public string LogDirectory => configuration["LoggingSettings:LogDirectory"] ?? "Logs";
public string? RemoteServerUrl => configuration["LoggingSettings:RemoteServerUrl"];
public string? RemoteServerApiKey => configuration["LoggingSettings:RemoteServerApiKey"];
// Lychee Upload Settings // Lychee Upload Settings
public string? LycheeApiUrl => configuration["LycheeSettings:ApiUrl"]; public string? LycheeApiUrl => configuration["LycheeSettings:ApiUrl"];

View File

@ -11,8 +11,37 @@
"IsShutdownEnabled": false, "IsShutdownEnabled": false,
"UseMockCamera": true "UseMockCamera": true
}, },
"Serilog": {
"MinimumLevel": {
"Default": "Debug",
"Override": {
"Microsoft": "Warning",
"System": "Warning"
}
},
"WriteTo": [
{
"Name": "Console"
},
{
"Name": "File",
"Args": {
"path": "Logs/cambooth-dev-.log",
"rollingInterval": "Day",
"retainedFileCountLimit": 7,
"outputTemplate": "{Timestamp:yyyy-MM-dd HH:mm:ss.fff} [{Level:u3}] {Message:lj}{NewLine}{Exception}"
}
}
]
},
"LoggingSettings": {
"LogLevel": "Debug",
"LogDirectory": "Logs",
"RemoteServerUrl": "https://log.grimma-fotobox.de",
"RemoteServerApiKey": "nhnVql3QNgoAxvDWmNyU"
},
"LycheeSettings": { "LycheeSettings": {
"ApiUrl": "https://cambooth-pics.rblnews.de", "ApiUrl": "https://gallery.grimma-fotobox.de",
"Username": "itob", "Username": "itob",
"Password": "VfVyqal&Nv8U&P", "Password": "VfVyqal&Nv8U&P",
"DefaultAlbumId": "gMM3W-Pk9mr8k57k-WU2Jz8t", "DefaultAlbumId": "gMM3W-Pk9mr8k57k-WU2Jz8t",

View File

@ -8,11 +8,40 @@
"PhotoCountdownSeconds": 5, "PhotoCountdownSeconds": 5,
"FocusDelaySeconds": 2, "FocusDelaySeconds": 2,
"FocusTimeoutMs": 3000, "FocusTimeoutMs": 3000,
"IsShutdownEnabled": true, "IsShutdownEnabled": false,
"UseMockCamera": true "UseMockCamera": false
},
"Serilog": {
"MinimumLevel": {
"Default": "Information",
"Override": {
"Microsoft": "Warning",
"System": "Warning"
}
},
"WriteTo": [
{
"Name": "Console"
},
{
"Name": "File",
"Args": {
"path": "Logs/cambooth-.log",
"rollingInterval": "Day",
"retainedFileCountLimit": 120,
"outputTemplate": "{Timestamp:yyyy-MM-dd HH:mm:ss.fff} [{Level:u3}] {Message:lj}{NewLine}{Exception}"
}
}
]
},
"LoggingSettings": {
"LogLevel": "Information",
"LogDirectory": "Logs",
"RemoteServerUrl": "https://log.grimma-fotobox.de",
"RemoteServerApiKey": "8rjvr0zZmceuFZMYydKU"
}, },
"LycheeSettings": { "LycheeSettings": {
"ApiUrl": "https://cambooth-pics.rblnews.de", "ApiUrl": "https://gallery.grimma-fotobox.de",
"Username": "itob", "Username": "itob",
"Password": "VfVyqal&Nv8U&P", "Password": "VfVyqal&Nv8U&P",
"DefaultAlbumId": "gMM3W-Pk9mr8k57k-WU2Jz8t", "DefaultAlbumId": "gMM3W-Pk9mr8k57k-WU2Jz8t",

View File

@ -1,13 +1,63 @@
using System.Globalization; using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO; using System.IO;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using System.Windows; using System.Windows;
using Serilog;
using Serilog.Core;
using Serilog.Events;
using Serilog.Formatting.Json;
using Serilog.Sinks.Http;
using Microsoft.Extensions.Configuration;
namespace CamBooth.App.Core.Logging; namespace CamBooth.App.Core.Logging;
public class Logger /// <summary>
/// Custom HTTP Client für Serilog HTTP Sink mit API-Key Support
/// </summary>
public class SeqHttpClient : IHttpClient
{ {
private readonly string _logsDirectory = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Logs"); private readonly HttpClient _httpClient;
private readonly string _errorLogPath; private readonly string _apiKey;
public SeqHttpClient(string apiKey = "")
{
_httpClient = new HttpClient();
_apiKey = apiKey;
// Setze API-Key Header, falls vorhanden
if (!string.IsNullOrWhiteSpace(_apiKey))
{
_httpClient.DefaultRequestHeaders.Add("X-Seq-Api-Key", _apiKey);
}
}
public void Configure(IConfiguration configuration)
{
// Konfiguration vom HTTP Sink - nicht nötig für unseren Use-Case
}
public async Task<HttpResponseMessage> PostAsync(string requestUri, Stream contentStream, CancellationToken cancellationToken)
{
using (var content = new StreamContent(contentStream))
{
content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/json");
return await _httpClient.PostAsync(requestUri, content, cancellationToken);
}
}
public void Dispose()
{
_httpClient?.Dispose();
}
}
public class Logger : IDisposable
{
private readonly Serilog.Core.Logger _serilogLogger;
public event LoggingEventHandler? InfoLog; public event LoggingEventHandler? InfoLog;
public event LoggingEventHandler? ErrorLog; public event LoggingEventHandler? ErrorLog;
@ -15,68 +65,185 @@ public class Logger
public event LoggingEventHandler? DebugLog; public event LoggingEventHandler? DebugLog;
public delegate void LoggingEventHandler(string text); public delegate void LoggingEventHandler(string text);
public Logger() public Logger(IConfiguration configuration)
{ {
// Logs-Ordner erstellen, falls nicht vorhanden var logLevel = configuration["LoggingSettings:LogLevel"] ?? "Information";
if (!Directory.Exists(_logsDirectory)) var logDirectory = configuration["LoggingSettings:LogDirectory"] ?? "Logs";
var remoteServerUrl = configuration["LoggingSettings:RemoteServerUrl"];
var remoteServerApiKey = configuration["LoggingSettings:RemoteServerApiKey"];
if (!Directory.Exists(logDirectory))
{ {
Directory.CreateDirectory(_logsDirectory); Directory.CreateDirectory(logDirectory);
} }
_errorLogPath = Path.Combine(_logsDirectory, "error.txt"); var minimumLevel = ParseLogLevel(logLevel);
var loggerConfig = new LoggerConfiguration()
.MinimumLevel.Is(minimumLevel)
.MinimumLevel.Override("Microsoft", LogEventLevel.Warning)
.MinimumLevel.Override("System", LogEventLevel.Warning)
.Enrich.FromLogContext()
.Enrich.WithProperty("Application", "CamBooth")
.WriteTo.Console(
outputTemplate: "{Timestamp:dd.MM.yyyy HH:mm:ss} [{Level:u3}] {Message:lj}{NewLine}{Exception}")
.WriteTo.File(
Path.Combine(logDirectory, "cambooth-.log"),
rollingInterval: RollingInterval.Day,
retainedFileCountLimit: 30,
outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff} [{Level:u3}] {Message:lj}{NewLine}{Exception}");
if (!string.IsNullOrWhiteSpace(remoteServerUrl))
{
try
{
loggerConfig.WriteTo.Seq(
serverUrl: remoteServerUrl,
apiKey: remoteServerApiKey,
restrictedToMinimumLevel: minimumLevel
);
Console.WriteLine($"Seq Sink konfiguriert: {remoteServerUrl}");
}
catch (Exception ex)
{
Console.WriteLine($"Fehler beim Konfigurieren des Seq-Sinks: {ex.Message}");
}
}
_serilogLogger = loggerConfig.CreateLogger();
Log.Logger = _serilogLogger;
Info($"Serilog Logger initialisiert - Level: {logLevel}, Directory: {logDirectory}");
if (!string.IsNullOrWhiteSpace(remoteServerUrl))
{
Info($"Remote Server konfiguriert: {remoteServerUrl}");
}
} }
private void WriteToErrorLog(string message) private LogEventLevel ParseLogLevel(string level)
{ {
try return level.ToLowerInvariant() switch
{ {
File.AppendAllText(_errorLogPath, message + Environment.NewLine); "verbose" => LogEventLevel.Verbose,
} "debug" => LogEventLevel.Debug,
catch (Exception ex) "information" => LogEventLevel.Information,
{ "warning" => LogEventLevel.Warning,
Console.WriteLine($"Fehler beim Schreiben in Fehlerlog: {ex.Message}"); "error" => LogEventLevel.Error,
} "fatal" => LogEventLevel.Fatal,
_ => LogEventLevel.Information
};
} }
public void Info(string message) public void Info(string message)
{ {
Application.Current.Dispatcher.Invoke(() => try
{ {
message = DateTime.Now.ToString("dd.MM.yyyy HH:mm:ss", CultureInfo.InvariantCulture) + ": [INFO] " + message; if (Application.Current?.Dispatcher != null)
InfoLog?.Invoke(message); {
Console.WriteLine(message); Application.Current.Dispatcher.Invoke(() =>
}); {
var formattedMessage = DateTime.Now.ToString("dd.MM.yyyy HH:mm:ss", CultureInfo.InvariantCulture) + ": [INFO] " + message;
InfoLog?.Invoke(formattedMessage);
});
}
_serilogLogger.Information(message);
}
catch (Exception ex)
{
Console.WriteLine($"Fehler beim Info-Logging: {ex.Message}");
}
} }
public void Warning(string message) public void Warning(string message)
{ {
Application.Current.Dispatcher.Invoke(() => try
{ {
message = DateTime.Now.ToString("dd.MM.yyyy HH:mm:ss", CultureInfo.InvariantCulture) + ": [WARNING] " + message; if (Application.Current?.Dispatcher != null)
WarningLog?.Invoke(message); {
Console.WriteLine(message); Application.Current.Dispatcher.Invoke(() =>
WriteToErrorLog(message); {
}); var formattedMessage = DateTime.Now.ToString("dd.MM.yyyy HH:mm:ss", CultureInfo.InvariantCulture) + ": [WARNING] " + message;
WarningLog?.Invoke(formattedMessage);
});
}
_serilogLogger.Warning(message);
}
catch (Exception ex)
{
Console.WriteLine($"Fehler beim Warning-Logging: {ex.Message}");
}
} }
public void Error(string message) public void Error(string message)
{ {
Application.Current.Dispatcher.Invoke(() => try
{ {
message = DateTime.Now.ToString("dd.MM.yyyy HH:mm:ss", CultureInfo.InvariantCulture) + ": [ERROR] " + message; if (Application.Current?.Dispatcher != null)
ErrorLog?.Invoke(message); {
Console.WriteLine(message); Application.Current.Dispatcher.Invoke(() =>
WriteToErrorLog(message); {
}); var formattedMessage = DateTime.Now.ToString("dd.MM.yyyy HH:mm:ss", CultureInfo.InvariantCulture) + ": [ERROR] " + message;
ErrorLog?.Invoke(formattedMessage);
});
}
_serilogLogger.Error(message);
}
catch (Exception ex)
{
Console.WriteLine($"Fehler beim Error-Logging: {ex.Message}");
}
}
public void Error(string message, Exception exception)
{
try
{
if (Application.Current?.Dispatcher != null)
{
Application.Current.Dispatcher.Invoke(() =>
{
var formattedMessage = DateTime.Now.ToString("dd.MM.yyyy HH:mm:ss", CultureInfo.InvariantCulture) + ": [ERROR] " + message;
ErrorLog?.Invoke(formattedMessage);
});
}
_serilogLogger.Error(exception, message);
}
catch (Exception ex)
{
Console.WriteLine($"Fehler beim Error-Logging: {ex.Message}");
}
} }
public void Debug(string message) public void Debug(string message)
{ {
Application.Current.Dispatcher.Invoke(() => try
{ {
message = DateTime.Now.ToString("dd.MM.yyyy HH:mm:ss", CultureInfo.InvariantCulture) + ": [DEBUG] " + message; if (Application.Current?.Dispatcher != null)
DebugLog?.Invoke(message); {
Console.WriteLine(message); Application.Current.Dispatcher.Invoke(() =>
}); {
var formattedMessage = DateTime.Now.ToString("dd.MM.yyyy HH:mm:ss", CultureInfo.InvariantCulture) + ": [DEBUG] " + message;
DebugLog?.Invoke(formattedMessage);
});
}
_serilogLogger.Debug(message);
}
catch (Exception ex)
{
Console.WriteLine($"Fehler beim Debug-Logging: {ex.Message}");
}
}
public void Dispose()
{
_serilogLogger?.Dispose();
Log.CloseAndFlush();
} }
} }

View File

@ -6,6 +6,8 @@
- Energiesparmodus abschalten - Energiesparmodus abschalten
- Starbildschirm mit freundlicher Begrüßung, kurzer Erklärung, und viel Spaß wünschen mit der FotoCam - Starbildschirm mit freundlicher Begrüßung, kurzer Erklärung, und viel Spaß wünschen mit der FotoCam
- Verschiedene Hinweise anzeigen beim Fotografieren (lächeln, Hasensohren, Zunge raus, Grimasse, usw.) - Verschiedene Hinweise anzeigen beim Fotografieren (lächeln, Hasensohren, Zunge raus, Grimasse, usw.)
- Bild über QR Code runterladen - Bild über QR Code runterladen (QR Code anzeigen, sowie ausdrucken und anklebene)
- Windows updates deaktivieren - Windows updates deaktivieren
- logging einbinden (Elastic order ähnliches) - logging einbinden (Elastic order ähnliches)
- Router anschließen für Upload
- Configs kontrollieren auf Fotobox