summaryrefslogtreecommitdiff
path: root/src/Agent.App
diff options
context:
space:
mode:
Diffstat (limited to 'src/Agent.App')
-rw-r--r--src/Agent.App/Logging/McpHostLogObserver.cs74
-rw-r--r--src/Agent.App/Logging/RollingFileLog.cs6
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);
}