Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions src/MauiDevFlow.Agent.Core/AgentOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,13 @@ public class AgentOptions
/// </summary>
public bool EnableFileLogging { get; set; } = true;

/// <summary>
/// Whether to register the FileLogProvider as an ILoggerProvider so that
/// ILogger output is written to the rotating log files. Default: true.
/// Requires <see cref="EnableFileLogging"/> to be true.
/// </summary>
public bool CaptureILogger { get; set; } = true;

/// <summary>
/// Maximum size of each log file in bytes before rotation. Default: 1MB.
/// </summary>
Expand All @@ -39,6 +46,19 @@ public class AgentOptions
/// </summary>
public int MaxLogFiles { get; set; } = 5;

/// <summary>
/// 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 <see cref="EnableFileLogging"/> to be true.
/// </summary>
public bool CaptureConsole { get; set; } = true;

/// <summary>
/// Whether to capture Trace/Debug output into the file log pipeline. Default: true.
/// Requires <see cref="EnableFileLogging"/> to be true.
/// </summary>
public bool CaptureTrace { get; set; } = true;

/// <summary>
/// Whether to intercept HttpClient requests for network monitoring. Default: true.
/// When enabled, all IHttpClientFactory-created HttpClients are automatically monitored.
Expand Down
10 changes: 9 additions & 1 deletion src/MauiDevFlow.Agent.Gtk/GtkAgentServiceExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
10 changes: 9 additions & 1 deletion src/MauiDevFlow.Agent/AgentServiceExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
232 changes: 232 additions & 0 deletions src/MauiDevFlow.Logging/ConsoleLogCapture.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
using System.Diagnostics;
using System.Text;

namespace MauiDevFlow.Logging;

/// <summary>
/// Captures Console.Write/WriteLine and Trace/Debug output into the FileLogWriter pipeline.
/// Output is tee'd — the original console stream still receives everything.
/// Call <see cref="Install"/> once after the FileLogWriter is available.
/// Call <see cref="Uninstall"/> (or Dispose) to restore original streams and listeners.
/// </summary>
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);
}

/// <summary>
/// Redirects Console.Out, Console.Error, and/or adds a TraceListener based on flags.
/// Safe to call multiple times — only installs once.
/// </summary>
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);
}
}

/// <summary>
/// Restores original Console streams and removes the TraceListener.
/// </summary>
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();
}

/// <summary>
/// A TextWriter that tees output to both the original stream and the FileLogWriter.
/// Buffers partial writes and flushes complete lines as log entries.
/// </summary>
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
));
}
}

/// <summary>
/// TraceListener that writes Trace/Debug output to the FileLogWriter.
/// </summary>
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"
));
}
}
}
Loading