diff options
Diffstat (limited to 'src/Agent.App/Logging')
| -rw-r--r-- | src/Agent.App/Logging/McpHostLogObserver.cs | 74 | ||||
| -rw-r--r-- | src/Agent.App/Logging/RollingFileLog.cs | 6 |
2 files changed, 70 insertions, 10 deletions
diff --git a/src/Agent.App/Logging/McpHostLogObserver.cs b/src/Agent.App/Logging/McpHostLogObserver.cs index b1410e8..37f32ec 100644 --- a/src/Agent.App/Logging/McpHostLogObserver.cs +++ b/src/Agent.App/Logging/McpHostLogObserver.cs @@ -3,16 +3,22 @@ using System.Text.Json; using Agent.Core.Localization; using Agent.Mcp; using System.Collections.Concurrent; +using System.Threading.Channels; namespace Agent.App.Logging; public sealed class McpHostLogObserver : IMcpHostObserver, IDisposable { + private const int QueueCapacity = 4096; private readonly RollingFileLog _application; private readonly RollingFileWriter? _http; private readonly LocaleCatalog _locale; private readonly string _logDirectory; private readonly ConcurrentDictionary<string, byte> _observedConnections = new(StringComparer.Ordinal); + private readonly Channel<Action> _queue; + private readonly Task _writerTask; + private long _droppedEntries; + private int _disposed; public McpHostLogObserver(RollingFileLog application, LocaleCatalog locale, string logDirectory, bool httpRequests, long maxBytes, int retainedFiles) @@ -21,6 +27,14 @@ public sealed class McpHostLogObserver : IMcpHostObserver, IDisposable _locale = locale; _logDirectory = logDirectory; if (httpRequests) _http = new RollingFileWriter(Path.Combine(logDirectory, "http.jsonl"), maxBytes, retainedFiles); + _queue = Channel.CreateBounded<Action>(new BoundedChannelOptions(QueueCapacity) + { + SingleReader = true, + SingleWriter = false, + FullMode = BoundedChannelFullMode.Wait, + AllowSynchronousContinuations = false + }); + _writerTask = Task.Run(WriteLoopAsync); } public void Ready(McpReadyInfo info) @@ -35,21 +49,21 @@ public sealed class McpHostLogObserver : IMcpHostObserver, IDisposable { ["protocol"] = info.ProtocolVersion, ["pid"] = info.ProcessId, ["logs"] = _logDirectory }); - _application.Operational(message); + Enqueue(() => _application.Operational(message)); } public void HttpRequestStarted(McpHttpRequestStartInfo info) { if (_observedConnections.TryAdd(info.ConnectionId, 0)) - _application.Information($"HTTP connection observed connection={info.ConnectionId} remote={info.RemoteIp}:{info.RemotePort}"); - _application.Information($"HTTP request started connection={info.ConnectionId} trace={info.TraceId} remote={info.RemoteIp}:{info.RemotePort} method={info.Method} path={info.Path}"); + Enqueue(() => _application.Information($"HTTP connection observed connection={info.ConnectionId} remote={info.RemoteIp}:{info.RemotePort}")); + Enqueue(() => _application.Information($"HTTP request started connection={info.ConnectionId} trace={info.TraceId} remote={info.RemoteIp}:{info.RemotePort} method={info.Method} path={info.Path}")); } public void HttpRequest(McpHttpRequestInfo info) { - _application.Information(string.Create(CultureInfo.InvariantCulture, - $"HTTP request completed connection={info.ConnectionId} remote={info.RemoteIp}:{info.RemotePort} method={info.Method} path={info.Path} status={info.Status} duration_ms={info.DurationMs:F3}")); - _http?.WriteLine(JsonSerializer.Serialize(new + string applicationLine = string.Create(CultureInfo.InvariantCulture, + $"HTTP request completed connection={info.ConnectionId} remote={info.RemoteIp}:{info.RemotePort} method={info.Method} path={info.Path} status={info.Status} duration_ms={info.DurationMs:F3}"); + string? httpLine = _http is null ? null : JsonSerializer.Serialize(new { timestamp = info.Timestamp, connection_id = info.ConnectionId, @@ -68,9 +82,53 @@ public sealed class McpHostLogObserver : IMcpHostObserver, IDisposable mcp_protocol_version = NullIfEmpty(info.ProtocolVersion), mcp_method = NullIfEmpty(info.McpMethod), mcp_name = NullIfEmpty(info.McpName) - })); + }); + Enqueue(() => + { + if (httpLine is not null) _http!.WriteLine(httpLine); + _application.Information(applicationLine); + }); + } + + private void Enqueue(Action write) + { + if (Volatile.Read(ref _disposed) != 0) return; + if (!_queue.Writer.TryWrite(write)) Interlocked.Increment(ref _droppedEntries); + } + + private async Task WriteLoopAsync() + { + try + { + await foreach (Action write in _queue.Reader.ReadAllAsync().ConfigureAwait(false)) + { + try { write(); } + catch (Exception ex) when (ex is not (OutOfMemoryException or StackOverflowException or AccessViolationException)) + { + System.Diagnostics.Debug.WriteLine($"MCP observability write failed: {ex.Message}"); + } + + long dropped = Interlocked.Exchange(ref _droppedEntries, 0); + if (dropped > 0) + { + try { _application.Warning($"MCP observability queue dropped {dropped} log entries."); } + catch (Exception ex) when (ex is not (OutOfMemoryException or StackOverflowException or AccessViolationException)) + { + System.Diagnostics.Debug.WriteLine($"MCP observability drop report failed: {ex.Message}"); + } + } + } + } + finally { _http?.Dispose(); } } private static string? NullIfEmpty(string? value) => string.IsNullOrWhiteSpace(value) ? null : value; - public void Dispose() => _http?.Dispose(); + + public void Dispose() + { + if (Interlocked.Exchange(ref _disposed, 1) != 0) return; + _queue.Writer.TryComplete(); + try { _writerTask.Wait(TimeSpan.FromSeconds(5)); } + catch (AggregateException) { } + } } diff --git a/src/Agent.App/Logging/RollingFileLog.cs b/src/Agent.App/Logging/RollingFileLog.cs index 8bdbbb4..f471a9e 100644 --- a/src/Agent.App/Logging/RollingFileLog.cs +++ b/src/Agent.App/Logging/RollingFileLog.cs @@ -5,12 +5,14 @@ namespace Agent.App.Logging; public sealed class RollingFileLog : IDisposable { private readonly RollingFileWriter _writer; + private readonly TextWriter _consoleWriter; private readonly int _minimumLevel; - public RollingFileLog(string directory, long maxBytes, int retainedFiles, string minimumLevel = "information") + public RollingFileLog(string directory, long maxBytes, int retainedFiles, string minimumLevel = "information", TextWriter? consoleWriter = null) { Directory.CreateDirectory(directory); _writer = new RollingFileWriter(Path.Combine(directory, "application.log"), maxBytes, retainedFiles); + _consoleWriter = consoleWriter ?? Console.Error; _minimumLevel = ParseLevel(minimumLevel); } @@ -24,7 +26,7 @@ public sealed class RollingFileLog : IDisposable { if (!force && Level(level) < _minimumLevel) return; string line = string.Create(CultureInfo.InvariantCulture, $"{DateTimeOffset.UtcNow:O} [{level}] {message}"); - Console.Error.WriteLine(line); + _consoleWriter.WriteLine(line); _writer.WriteLine(line); } |
