From ffa30a95ee6cfe23cab006913eafdea273bcf422 Mon Sep 17 00:00:00 2001 From: redth Date: Thu, 5 Mar 2026 10:15:16 -0500 Subject: [PATCH 1/2] feat: capture Console and Trace/Debug output into file log pipeline Add ConsoleLogCapture that tees Console.Out, Console.Error, and Trace/Debug output through to the FileLogWriter while preserving original stream output. Entries are tagged with source 'console.out', 'console.error', or 'trace' for filtering. Enabled by default via AgentOptions.CaptureConsoleOutput. Wired into both MAUI and GTK agent registration paths. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/MauiDevFlow.Agent.Core/AgentOptions.cs | 7 + .../GtkAgentServiceExtensions.cs | 6 + .../AgentServiceExtensions.cs | 6 + src/MauiDevFlow.Logging/ConsoleLogCapture.cs | 225 ++++++++++++++++++ 4 files changed, 244 insertions(+) create mode 100644 src/MauiDevFlow.Logging/ConsoleLogCapture.cs diff --git a/src/MauiDevFlow.Agent.Core/AgentOptions.cs b/src/MauiDevFlow.Agent.Core/AgentOptions.cs index 854787e..1bba3be 100644 --- a/src/MauiDevFlow.Agent.Core/AgentOptions.cs +++ b/src/MauiDevFlow.Agent.Core/AgentOptions.cs @@ -39,6 +39,13 @@ public class AgentOptions /// public int MaxLogFiles { get; set; } = 5; + /// + /// Whether to capture Console.Out, Console.Error, and Trace/Debug output + /// into the file log pipeline. Output is tee'd — original streams still receive everything. + /// Default: true. + /// + public bool CaptureConsoleOutput { get; set; } = true; + /// /// Whether to intercept HttpClient requests for network monitoring. Default: true. /// When enabled, all IHttpClientFactory-created HttpClients are automatically monitored. diff --git a/src/MauiDevFlow.Agent.Gtk/GtkAgentServiceExtensions.cs b/src/MauiDevFlow.Agent.Gtk/GtkAgentServiceExtensions.cs index 3326e6c..a3b13e9 100644 --- a/src/MauiDevFlow.Agent.Gtk/GtkAgentServiceExtensions.cs +++ b/src/MauiDevFlow.Agent.Gtk/GtkAgentServiceExtensions.cs @@ -73,6 +73,12 @@ public static MauiAppBuilder AddMauiDevFlowAgent(this MauiAppBuilder builder, Ac var logProvider = new FileLogProvider(logDir, options.MaxLogFileSize, options.MaxLogFiles); service.SetLogProvider(logProvider); builder.Logging.AddProvider(logProvider); + + if (options.CaptureConsoleOutput) + { + var capture = new ConsoleLogCapture(logProvider.Writer); + capture.Install(); + } } if (options.EnableNetworkMonitoring) diff --git a/src/MauiDevFlow.Agent/AgentServiceExtensions.cs b/src/MauiDevFlow.Agent/AgentServiceExtensions.cs index 68349c7..8015ae3 100644 --- a/src/MauiDevFlow.Agent/AgentServiceExtensions.cs +++ b/src/MauiDevFlow.Agent/AgentServiceExtensions.cs @@ -96,6 +96,12 @@ public static MauiAppBuilder AddMauiDevFlowAgent(this MauiAppBuilder builder, Ac var logProvider = new FileLogProvider(logDir, options.MaxLogFileSize, options.MaxLogFiles); service.SetLogProvider(logProvider); builder.Logging.AddProvider(logProvider); + + if (options.CaptureConsoleOutput) + { + var capture = new ConsoleLogCapture(logProvider.Writer); + capture.Install(); + } } // Auto-inject network monitoring handler into all IHttpClientFactory-created clients diff --git a/src/MauiDevFlow.Logging/ConsoleLogCapture.cs b/src/MauiDevFlow.Logging/ConsoleLogCapture.cs new file mode 100644 index 0000000..6cae484 --- /dev/null +++ b/src/MauiDevFlow.Logging/ConsoleLogCapture.cs @@ -0,0 +1,225 @@ +using System.Diagnostics; +using System.Text; + +namespace MauiDevFlow.Logging; + +/// +/// Captures Console.Write/WriteLine and Trace/Debug output into the FileLogWriter pipeline. +/// Output is tee'd — the original console stream still receives everything. +/// Call once after the FileLogWriter is available. +/// Call (or Dispose) to restore original streams and listeners. +/// +public sealed class ConsoleLogCapture : IDisposable +{ + private readonly FileLogWriter _writer; + private readonly TextWriter _originalOut; + private readonly TextWriter _originalError; + private readonly LogTextWriter _outWriter; + private readonly LogTextWriter _errorWriter; + private readonly LogTraceListener _traceListener; + private volatile bool _installed; + private volatile bool _disposed; + + public ConsoleLogCapture(FileLogWriter writer) + { + _writer = writer; + _originalOut = Console.Out; + _originalError = Console.Error; + _outWriter = new LogTextWriter(_originalOut, writer, "console.out"); + _errorWriter = new LogTextWriter(_originalError, writer, "console.error"); + _traceListener = new LogTraceListener(writer); + } + + /// + /// Redirects Console.Out, Console.Error, and adds a TraceListener. + /// Safe to call multiple times — only installs once. + /// + public void Install() + { + if (_installed || _disposed) return; + _installed = true; + + Console.SetOut(_outWriter); + Console.SetError(_errorWriter); + Trace.Listeners.Add(_traceListener); + } + + /// + /// Restores original Console streams and removes the TraceListener. + /// + public void Uninstall() + { + if (!_installed) return; + _installed = false; + + Console.SetOut(_originalOut); + Console.SetError(_originalError); + Trace.Listeners.Remove(_traceListener); + } + + public void Dispose() + { + if (_disposed) return; + _disposed = true; + Uninstall(); + } + + /// + /// A TextWriter that tees output to both the original stream and the FileLogWriter. + /// Buffers partial writes and flushes complete lines as log entries. + /// + private sealed class LogTextWriter : TextWriter + { + private readonly TextWriter _inner; + private readonly FileLogWriter _logWriter; + private readonly string _source; + private readonly StringBuilder _lineBuffer = new(); + private readonly object _lock = new(); + + public LogTextWriter(TextWriter inner, FileLogWriter logWriter, string source) + { + _inner = inner; + _logWriter = logWriter; + _source = source; + } + + public override Encoding Encoding => _inner.Encoding; + + public override void Write(char value) + { + _inner.Write(value); + + lock (_lock) + { + if (value == '\n') + FlushLine(); + else + _lineBuffer.Append(value); + } + } + + public override void Write(string? value) + { + if (value == null) return; + _inner.Write(value); + + lock (_lock) + { + foreach (var ch in value) + { + if (ch == '\n') + FlushLine(); + else + _lineBuffer.Append(ch); + } + } + } + + public override void WriteLine(string? value) + { + _inner.WriteLine(value); + + lock (_lock) + { + _lineBuffer.Append(value); + FlushLine(); + } + } + + public override void WriteLine() + { + _inner.WriteLine(); + + lock (_lock) + { + FlushLine(); + } + } + + public override void Flush() + { + _inner.Flush(); + + lock (_lock) + { + if (_lineBuffer.Length > 0) + FlushLine(); + } + } + + private void FlushLine() + { + var line = _lineBuffer.ToString().TrimEnd('\r'); + _lineBuffer.Clear(); + + if (string.IsNullOrEmpty(line)) return; + + _logWriter.Write(new FileLogEntry( + Timestamp: DateTime.UtcNow, + Level: _source == "console.error" ? "Warning" : "Information", + Category: _source, + Message: line, + Source: _source + )); + } + } + + /// + /// TraceListener that writes Trace/Debug output to the FileLogWriter. + /// + private sealed class LogTraceListener : TraceListener + { + private readonly FileLogWriter _logWriter; + private readonly StringBuilder _lineBuffer = new(); + private readonly object _lock = new(); + + public LogTraceListener(FileLogWriter logWriter) : base("MauiDevFlowTrace") + { + _logWriter = logWriter; + } + + public override void Write(string? message) + { + if (message == null) return; + + lock (_lock) + { + _lineBuffer.Append(message); + } + } + + public override void WriteLine(string? message) + { + lock (_lock) + { + _lineBuffer.Append(message); + FlushLine(); + } + } + + public override void Flush() + { + lock (_lock) + { + if (_lineBuffer.Length > 0) + FlushLine(); + } + } + + private void FlushLine() + { + var line = _lineBuffer.ToString(); + _lineBuffer.Clear(); + + if (string.IsNullOrEmpty(line)) return; + + _logWriter.Write(new FileLogEntry( + Timestamp: DateTime.UtcNow, + Level: "Debug", + Category: "trace", + Message: line, + Source: "trace" + )); + } + } +} From 5ae0e10fa4c4bbce24b3ddd968ef6be64fe2de9c Mon Sep 17 00:00:00 2001 From: redth Date: Thu, 5 Mar 2026 10:28:48 -0500 Subject: [PATCH 2/2] feat: granular capture options for ILogger, Console, and Trace Replace single CaptureConsoleOutput bool with three independent options: - CaptureILogger: opt out of registering FileLogProvider as ILoggerProvider - CaptureConsole: opt out of Console.Out/Error redirection - CaptureTrace: opt out of Trace/Debug listener All default to true. EnableFileLogging remains the master switch. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/MauiDevFlow.Agent.Core/AgentOptions.cs | 21 +++++++++++++++---- .../GtkAgentServiceExtensions.cs | 8 ++++--- .../AgentServiceExtensions.cs | 8 ++++--- src/MauiDevFlow.Logging/ConsoleLogCapture.cs | 17 ++++++++++----- 4 files changed, 39 insertions(+), 15 deletions(-) diff --git a/src/MauiDevFlow.Agent.Core/AgentOptions.cs b/src/MauiDevFlow.Agent.Core/AgentOptions.cs index 1bba3be..0a3aa1c 100644 --- a/src/MauiDevFlow.Agent.Core/AgentOptions.cs +++ b/src/MauiDevFlow.Agent.Core/AgentOptions.cs @@ -29,6 +29,13 @@ public class AgentOptions /// public bool EnableFileLogging { get; set; } = true; + /// + /// Whether to register the FileLogProvider as an ILoggerProvider so that + /// ILogger output is written to the rotating log files. Default: true. + /// Requires to be true. + /// + public bool CaptureILogger { get; set; } = true; + /// /// Maximum size of each log file in bytes before rotation. Default: 1MB. /// @@ -40,11 +47,17 @@ public class AgentOptions public int MaxLogFiles { get; set; } = 5; /// - /// Whether to capture Console.Out, Console.Error, and Trace/Debug output - /// into the file log pipeline. Output is tee'd — original streams still receive everything. - /// Default: true. + /// Whether to capture Console.Out and Console.Error output into the file log pipeline. + /// Output is tee'd — original streams still receive everything. Default: true. + /// Requires to be true. + /// + public bool CaptureConsole { get; set; } = true; + + /// + /// Whether to capture Trace/Debug output into the file log pipeline. Default: true. + /// Requires to be true. /// - public bool CaptureConsoleOutput { get; set; } = true; + public bool CaptureTrace { get; set; } = true; /// /// Whether to intercept HttpClient requests for network monitoring. Default: true. diff --git a/src/MauiDevFlow.Agent.Gtk/GtkAgentServiceExtensions.cs b/src/MauiDevFlow.Agent.Gtk/GtkAgentServiceExtensions.cs index a3b13e9..991125e 100644 --- a/src/MauiDevFlow.Agent.Gtk/GtkAgentServiceExtensions.cs +++ b/src/MauiDevFlow.Agent.Gtk/GtkAgentServiceExtensions.cs @@ -72,12 +72,14 @@ public static MauiAppBuilder AddMauiDevFlowAgent(this MauiAppBuilder builder, Ac "mauidevflow-logs"); var logProvider = new FileLogProvider(logDir, options.MaxLogFileSize, options.MaxLogFiles); service.SetLogProvider(logProvider); - builder.Logging.AddProvider(logProvider); - if (options.CaptureConsoleOutput) + if (options.CaptureILogger) + builder.Logging.AddProvider(logProvider); + + if (options.CaptureConsole || options.CaptureTrace) { var capture = new ConsoleLogCapture(logProvider.Writer); - capture.Install(); + capture.Install(captureConsole: options.CaptureConsole, captureTrace: options.CaptureTrace); } } diff --git a/src/MauiDevFlow.Agent/AgentServiceExtensions.cs b/src/MauiDevFlow.Agent/AgentServiceExtensions.cs index 8015ae3..8bb094d 100644 --- a/src/MauiDevFlow.Agent/AgentServiceExtensions.cs +++ b/src/MauiDevFlow.Agent/AgentServiceExtensions.cs @@ -95,12 +95,14 @@ public static MauiAppBuilder AddMauiDevFlowAgent(this MauiAppBuilder builder, Ac var logDir = Path.Combine(FileSystem.CacheDirectory, "mauidevflow-logs"); var logProvider = new FileLogProvider(logDir, options.MaxLogFileSize, options.MaxLogFiles); service.SetLogProvider(logProvider); - builder.Logging.AddProvider(logProvider); - if (options.CaptureConsoleOutput) + if (options.CaptureILogger) + builder.Logging.AddProvider(logProvider); + + if (options.CaptureConsole || options.CaptureTrace) { var capture = new ConsoleLogCapture(logProvider.Writer); - capture.Install(); + capture.Install(captureConsole: options.CaptureConsole, captureTrace: options.CaptureTrace); } } diff --git a/src/MauiDevFlow.Logging/ConsoleLogCapture.cs b/src/MauiDevFlow.Logging/ConsoleLogCapture.cs index 6cae484..3bc634b 100644 --- a/src/MauiDevFlow.Logging/ConsoleLogCapture.cs +++ b/src/MauiDevFlow.Logging/ConsoleLogCapture.cs @@ -31,17 +31,24 @@ public ConsoleLogCapture(FileLogWriter writer) } /// - /// Redirects Console.Out, Console.Error, and adds a TraceListener. + /// Redirects Console.Out, Console.Error, and/or adds a TraceListener based on flags. /// Safe to call multiple times — only installs once. /// - public void Install() + public void Install(bool captureConsole = true, bool captureTrace = true) { if (_installed || _disposed) return; _installed = true; - Console.SetOut(_outWriter); - Console.SetError(_errorWriter); - Trace.Listeners.Add(_traceListener); + if (captureConsole) + { + Console.SetOut(_outWriter); + Console.SetError(_errorWriter); + } + + if (captureTrace) + { + Trace.Listeners.Add(_traceListener); + } } ///