diff --git a/src/NexusReader.Maui/App.xaml.cs b/src/NexusReader.Maui/App.xaml.cs index 28db06e..365ccf5 100644 --- a/src/NexusReader.Maui/App.xaml.cs +++ b/src/NexusReader.Maui/App.xaml.cs @@ -8,4 +8,22 @@ public partial class App : Microsoft.Maui.Controls.Application MainPage = new MainPage(); } + + protected override Window CreateWindow(IActivationState? activationState) + { + var window = base.CreateWindow(activationState); + + // Hook into native MAUI lifecycle events to cleanly flush and close Serilog buffers + window.Stopped += (s, e) => + { + Serilog.Log.CloseAndFlush(); + }; + + window.Destroying += (s, e) => + { + Serilog.Log.CloseAndFlush(); + }; + + return window; + } } diff --git a/src/NexusReader.Maui/Infrastructure/Logging/BlazorLoggingBridge.cs b/src/NexusReader.Maui/Infrastructure/Logging/BlazorLoggingBridge.cs new file mode 100644 index 0000000..b667064 --- /dev/null +++ b/src/NexusReader.Maui/Infrastructure/Logging/BlazorLoggingBridge.cs @@ -0,0 +1,53 @@ +using Microsoft.Extensions.Logging; +using Microsoft.JSInterop; + +namespace NexusReader.Maui.Infrastructure.Logging; + +/// +/// Lightweight bridge service that intercepts logs, console outputs, and uncaught exceptions +/// from the Blazor WebView/JS side, and routes them directly to Serilog under "BlazorWebView" context. +/// +public sealed class BlazorLoggingBridge +{ + private readonly ILogger _logger; + + public BlazorLoggingBridge(ILoggerFactory loggerFactory) + { + _logger = loggerFactory.CreateLogger("BlazorWebView"); + } + + [JSInvokable("LogJsMessage")] + public void LogJsMessage(string level, string message, string? stackTrace = null) + { + if (string.IsNullOrWhiteSpace(message)) + { + return; + } + + switch (level.ToLowerInvariant()) + { + case "error": + case "exception": + if (!string.IsNullOrWhiteSpace(stackTrace)) + { + _logger.LogError("JS Unhandled Exception: {Message}\nStack Trace:\n{StackTrace}", message, stackTrace); + } + else + { + _logger.LogError("JS Error: {Message}", message); + } + break; + + case "warning": + case "warn": + _logger.LogWarning("JS Warning: {Message}", message); + break; + + case "info": + case "log": + default: + _logger.LogInformation("JS Log: {Message}", message); + break; + } + } +} diff --git a/src/NexusReader.Maui/Infrastructure/Logging/SerilogConfiguration.cs b/src/NexusReader.Maui/Infrastructure/Logging/SerilogConfiguration.cs new file mode 100644 index 0000000..7a94779 --- /dev/null +++ b/src/NexusReader.Maui/Infrastructure/Logging/SerilogConfiguration.cs @@ -0,0 +1,106 @@ +using Microsoft.Extensions.Logging; +using Serilog; +using Serilog.Core; +using Serilog.Events; +using Serilog.Formatting; +using Serilog.Formatting.Display; + +namespace NexusReader.Maui.Infrastructure.Logging; + +public static class SerilogConfiguration +{ + private const string OutputTemplate = + "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz}] [{Level:u3}] [ThreadId: {ThreadId}] [{SourceContext}] {Message:lj}{NewLine}{Exception}"; + + public static MauiAppBuilder RegisterLogging(this MauiAppBuilder builder) + { + // 1. Ensure logs directory exists in secure sandbox + var logDir = Path.Combine(Microsoft.Maui.Storage.FileSystem.AppDataDirectory, "logs"); + if (!Directory.Exists(logDir)) + { + Directory.CreateDirectory(logDir); + } + var logPath = Path.Combine(logDir, "log-.txt"); + + // 2. Inject sandboxed log path dynamically into configuration provider + builder.Configuration["Serilog:WriteTo:0:Args:configure:0:Args:path"] = logPath; + + // 3. Configure Serilog Logger Configuration using App Configuration settings + var loggerConfig = new LoggerConfiguration() + .ReadFrom.Configuration(builder.Configuration) + .Enrich.With(new ThreadIdEnricher()); + + // 4. Platform-specific and environment-specific sinks +#if ANDROID + // Direct Native Android Logcat Sink (JNI bindings for native diagnostics) + loggerConfig.WriteTo.Sink( + new AndroidLogcatSink(new MessageTemplateTextFormatter(OutputTemplate, null)), + restrictedToMinimumLevel: LogEventLevel.Debug); +#endif + + // 5. Initialize the static Serilog Log + Log.Logger = loggerConfig.CreateLogger(); + + // 6. Connect Serilog to Microsoft.Extensions.Logging + builder.Logging.ClearProviders(); + builder.Logging.AddSerilog(dispose: true); + + return builder; + } +} + +/// +/// A custom self-contained thread enricher to avoid unnecessary NuGet packages. +/// +internal sealed class ThreadIdEnricher : ILogEventEnricher +{ + public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory) + { + logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty("ThreadId", Environment.CurrentManagedThreadId)); + } +} + +#if ANDROID +/// +/// A high-performance, direct Android Logcat Sink utilizing native Android APIs. +/// +internal sealed class AndroidLogcatSink : ILogEventSink +{ + private readonly ITextFormatter _formatter; + private const string Tag = "NexusReader"; + + public AndroidLogcatSink(ITextFormatter formatter) + { + _formatter = formatter ?? throw new ArgumentNullException(nameof(formatter)); + } + + public void Emit(LogEvent logEvent) + { + using var writer = new StringWriter(); + _formatter.Format(logEvent, writer); + var message = writer.ToString().Trim(); + + switch (logEvent.Level) + { + case LogEventLevel.Verbose: + Android.Util.Log.Verbose(Tag, message); + break; + case LogEventLevel.Debug: + Android.Util.Log.Debug(Tag, message); + break; + case LogEventLevel.Information: + Android.Util.Log.Info(Tag, message); + break; + case LogEventLevel.Warning: + Android.Util.Log.Warn(Tag, message); + break; + case LogEventLevel.Error: + Android.Util.Log.Error(Tag, message); + break; + case LogEventLevel.Fatal: + Android.Util.Log.Wtf(Tag, message); + break; + } + } +} +#endif diff --git a/src/NexusReader.Maui/Main.razor b/src/NexusReader.Maui/Main.razor index 29c775f..95b82eb 100644 --- a/src/NexusReader.Maui/Main.razor +++ b/src/NexusReader.Maui/Main.razor @@ -1,5 +1,8 @@ @using Microsoft.AspNetCore.Components.Routing @using NexusReader.UI.Shared +@using NexusReader.Maui.Infrastructure.Logging +@inject IJSRuntime JSRuntime +@inject BlazorLoggingBridge LoggingBridge @@ -16,3 +19,21 @@ + +@code { + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + { + try + { + var dotNetRef = DotNetObjectReference.Create(LoggingBridge); + await JSRuntime.InvokeVoidAsync("NexusLogging.initializeBridge", dotNetRef); + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"[SerilogBridge] Failed to initialize Blazor/JS Bridge: {ex}"); + } + } + } +} diff --git a/src/NexusReader.Maui/MauiProgram.cs b/src/NexusReader.Maui/MauiProgram.cs index 8768784..6024cc6 100644 --- a/src/NexusReader.Maui/MauiProgram.cs +++ b/src/NexusReader.Maui/MauiProgram.cs @@ -1,9 +1,11 @@ using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Configuration; using NexusReader.Application.Abstractions.Services; using NexusReader.Infrastructure.Mobile.Services; using NexusReader.UI.Shared.Services; using NexusReader.Application; using MediatR; +using NexusReader.Maui.Infrastructure.Logging; namespace NexusReader.Maui; @@ -14,16 +16,30 @@ public static class MauiProgram try { var builder = MauiApp.CreateBuilder(); + + // Load embedded appsettings.json configuration + var assembly = typeof(App).Assembly; + using (var stream = assembly.GetManifestResourceStream("NexusReader.Maui.appsettings.json")) + { + if (stream != null) + { + ((IConfigurationBuilder)builder.Configuration).AddJsonStream(stream); + } + } + builder - .UseMauiApp(); + .UseMauiApp() + .RegisterLogging(); builder.Services.AddMauiBlazorWebView(); #if DEBUG builder.Services.AddBlazorWebViewDeveloperTools(); - builder.Logging.AddDebug(); #endif + // Interception bridge for JS/Blazor WebView logs + builder.Services.AddSingleton(); + // Minimal Infrastructure builder.Services.AddSingleton(); builder.Services.AddSingleton(); diff --git a/src/NexusReader.Maui/NexusReader.Maui.csproj b/src/NexusReader.Maui/NexusReader.Maui.csproj index b92e219..32f874c 100644 --- a/src/NexusReader.Maui/NexusReader.Maui.csproj +++ b/src/NexusReader.Maui/NexusReader.Maui.csproj @@ -27,6 +27,13 @@ + + + + + + + @@ -34,4 +41,8 @@ + + + + diff --git a/src/NexusReader.Maui/appsettings.json b/src/NexusReader.Maui/appsettings.json new file mode 100644 index 0000000..14e9ab2 --- /dev/null +++ b/src/NexusReader.Maui/appsettings.json @@ -0,0 +1,45 @@ +{ + "Serilog": { + "Using": [ + "Serilog.Sinks.File", + "Serilog.Sinks.Debug", + "Serilog.Sinks.Async" + ], + "MinimumLevel": { + "Default": "Debug", + "Override": { + "Microsoft": "Warning", + "Microsoft.AspNetCore": "Warning", + "System": "Warning" + } + }, + "WriteTo": [ + { + "Name": "Async", + "Args": { + "configure": [ + { + "Name": "File", + "Args": { + "path": "LOG_PATH_PLACEHOLDER", + "rollingInterval": "Day", + "retainedFileCountLimit": 7, + "outputTemplate": "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz}] [{Level:u3}] [ThreadId: {ThreadId}] [{SourceContext}] {Message:lj}{NewLine}{Exception}", + "shared": true + } + } + ] + } + }, + { + "Name": "Debug", + "Args": { + "outputTemplate": "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz}] [{Level:u3}] [ThreadId: {ThreadId}] [{SourceContext}] {Message:lj}{NewLine}{Exception}" + } + } + ], + "Enrich": [ + "FromLogContext" + ] + } +} diff --git a/src/NexusReader.Maui/wwwroot/index.html b/src/NexusReader.Maui/wwwroot/index.html index e0a77ce..8570c5a 100644 --- a/src/NexusReader.Maui/wwwroot/index.html +++ b/src/NexusReader.Maui/wwwroot/index.html @@ -26,7 +26,53 @@ + diff --git a/src/NexusReader.UI.Shared/Pages/SerilogDemo.razor b/src/NexusReader.UI.Shared/Pages/SerilogDemo.razor new file mode 100644 index 0000000..08c8fde --- /dev/null +++ b/src/NexusReader.UI.Shared/Pages/SerilogDemo.razor @@ -0,0 +1,370 @@ +@page "/serilog-demo" +@inject ILogger Logger +@inject IJSRuntime JSRuntime + +
+
+
+ +
+

Serilog Logging Infrastructure

+

Production-grade diagnostic pipeline for unified native & web logs

+
+
+
+ + Pipeline Active +
+
+ +
+ +
+
+ +

Native .NET Logs (C#)

+
+

Trigger structured C# logs using Dependency Injected ILogger.

+
+ + + +
+
+ + +
+
+ +

Blazor / JS WebView Logs

+
+

Trigger logs from JavaScript to verify the interop error capture bridge.

+
+ + +
+
+
+ + +
+
+ +

Pipeline Diagnostics

+
+
+
+ Rolling Daily File Sandbox Path + AppDataDirectory/logs/log-*.txt +
+
+ Active Configuration Provider + Serilog.Settings.Configuration (appsettings.json) +
+
+ Native Apple Console Sink + Serilog.Sinks.Debug (conditional compilation) +
+
+ Native Android Logcat Sink + AndroidLogcatSink (direct JNI bindings) +
+
+
+
+ + + +@code { + private void LogInfo() + { + Logger.LogInformation("Structured native log triggered by user from SerilogDemo. Button: LogInfo"); + } + + private void LogWarning() + { + Logger.LogWarning("Potential warning log triggered from Blazor razor component at {Time}", DateTime.UtcNow); + } + + private void LogError() + { + try + { + throw new InvalidOperationException("Simulated native C# operation exception triggered in Diagnostic dashboard."); + } + catch (Exception ex) + { + Logger.LogError(ex, "Captured exception successfully in native Serilog pipeline!"); + } + } + + private async Task TriggerJsLog() + { + await JSRuntime.InvokeVoidAsync("console.log", "Intercepted JS console statement from Blazor WebView interop trigger!"); + } + + private async Task TriggerJsException() + { + await JSRuntime.InvokeVoidAsync("eval", "throw new Error('Simulated runtime JS Exception triggered from Blazor UI button click!');"); + } +} diff --git a/src/NexusReader.UI.Shared/Pages/Settings.razor b/src/NexusReader.UI.Shared/Pages/Settings.razor index 227a733..f19734b 100644 --- a/src/NexusReader.UI.Shared/Pages/Settings.razor +++ b/src/NexusReader.UI.Shared/Pages/Settings.razor @@ -2,16 +2,63 @@ @attribute [Authorize]
-

Ustawienia

-

Konfiguracja Twojego konta i preferencji czytania.

+

Settings

+

Configure your account and application preferences.

+ +
+

Diagnostics & System Logs

+

Inspect native logging infrastructure, trigger custom logs, and trace WebView errors.

+ + + Open Serilog Diagnostics Dashboard + +