diff --git a/src/MauiDevFlow.Agent.Core/AgentOptions.cs b/src/MauiDevFlow.Agent.Core/AgentOptions.cs index 854787e..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. /// @@ -39,6 +46,19 @@ public class AgentOptions /// public int MaxLogFiles { get; set; } = 5; + /// + /// 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 CaptureTrace { 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..991125e 100644 --- a/src/MauiDevFlow.Agent.Gtk/GtkAgentServiceExtensions.cs +++ b/src/MauiDevFlow.Agent.Gtk/GtkAgentServiceExtensions.cs @@ -72,7 +72,15 @@ 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.CaptureILogger) + builder.Logging.AddProvider(logProvider); + + if (options.CaptureConsole || options.CaptureTrace) + { + var capture = new ConsoleLogCapture(logProvider.Writer); + capture.Install(captureConsole: options.CaptureConsole, captureTrace: options.CaptureTrace); + } } if (options.EnableNetworkMonitoring) diff --git a/src/MauiDevFlow.Agent/AgentServiceExtensions.cs b/src/MauiDevFlow.Agent/AgentServiceExtensions.cs index 68349c7..8bb094d 100644 --- a/src/MauiDevFlow.Agent/AgentServiceExtensions.cs +++ b/src/MauiDevFlow.Agent/AgentServiceExtensions.cs @@ -95,7 +95,15 @@ 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.CaptureILogger) + builder.Logging.AddProvider(logProvider); + + if (options.CaptureConsole || options.CaptureTrace) + { + var capture = new ConsoleLogCapture(logProvider.Writer); + capture.Install(captureConsole: options.CaptureConsole, captureTrace: options.CaptureTrace); + } } // 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..3bc634b --- /dev/null +++ b/src/MauiDevFlow.Logging/ConsoleLogCapture.cs @@ -0,0 +1,232 @@ +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/or adds a TraceListener based on flags. + /// Safe to call multiple times — only installs once. + /// + public void Install(bool captureConsole = true, bool captureTrace = true) + { + if (_installed || _disposed) return; + _installed = true; + + if (captureConsole) + { + Console.SetOut(_outWriter); + Console.SetError(_errorWriter); + } + + if (captureTrace) + { + 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" + )); + } + } +}