1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
|
using Agent.App.Logging;
using Agent.Core.Localization;
using Agent.Mcp;
using System.Diagnostics;
using System.Text;
namespace Agent.Core.Tests;
public sealed class ObservabilityTests
{
[Fact]
public void Ready_is_always_visible_even_when_log_threshold_is_fatal()
{
using var directory = new TestDirectory();
LocaleCatalog locale = LocaleCatalog.Load(Path.GetFullPath("config/locales"), "en");
using (var application = new RollingFileLog(directory.Path, 1024 * 1024, 2, "fatal"))
using (var observer = new McpHostLogObserver(application, locale, directory.Path, false, 1024 * 1024, 2))
observer.Ready(new McpReadyInfo("http", "http://127.0.0.1:8080/mcp", 42, "2026-07-28", true, true));
Assert.Contains("Agent is ready", File.ReadAllText(Path.Combine(directory.Path, "application.log")), StringComparison.Ordinal);
}
[Fact]
public void Ready_and_http_request_are_logged_without_sensitive_payloads()
{
using var directory = new TestDirectory();
LocaleCatalog locale = LocaleCatalog.Load(Path.GetFullPath("config/locales"), "en");
using (var application = new RollingFileLog(directory.Path, 1024 * 1024, 2))
{
using var observer = new McpHostLogObserver(application, locale, directory.Path, true, 1024 * 1024, 2);
observer.Ready(new McpReadyInfo("http", "http://127.0.0.1:8080/mcp", 42, "2026-07-28", true, true));
observer.HttpRequestStarted(new McpHttpRequestStartInfo("connection", "trace", "127.0.0.1", 50123, "POST", "/mcp"));
observer.HttpRequest(new McpHttpRequestInfo(DateTimeOffset.UtcNow, "connection", "trace", "127.0.0.1", 50123,
"127.0.0.1", 8080, "POST", "/mcp", 200, 12.5, "application/json", 100, "test-client",
"2026-07-28", "tools/list", "agent_run"));
}
string applicationText = File.ReadAllText(Path.Combine(directory.Path, "application.log"));
Assert.Contains("Agent is ready", applicationText, StringComparison.Ordinal);
Assert.Contains("remote=127.0.0.1:50123", applicationText, StringComparison.Ordinal);
string httpText = File.ReadAllText(Path.Combine(directory.Path, "http.jsonl"));
Assert.Contains("\"method\":\"POST\"", httpText, StringComparison.Ordinal);
Assert.Contains("\"remote_ip\":\"127.0.0.1\"", httpText, StringComparison.Ordinal);
Assert.DoesNotContain("Authorization", httpText, StringComparison.OrdinalIgnoreCase);
Assert.DoesNotContain("Cookie", httpText, StringComparison.OrdinalIgnoreCase);
Assert.DoesNotContain("prompt", httpText, StringComparison.OrdinalIgnoreCase);
}
[Fact]
public void Http_request_callbacks_do_not_wait_for_a_blocked_console_writer()
{
using var directory = new TestDirectory();
using var console = new BlockingTextWriter();
LocaleCatalog locale = LocaleCatalog.Load(Path.GetFullPath("config/locales"), "en");
using var application = new RollingFileLog(directory.Path, 1024 * 1024, 2, consoleWriter: console);
using var observer = new McpHostLogObserver(application, locale, directory.Path, true, 1024 * 1024, 2);
observer.HttpRequestStarted(new McpHttpRequestStartInfo("connection", "trace-1", "127.0.0.1", 50123, "POST", "/mcp"));
Assert.True(console.WaitUntilBlocked(TimeSpan.FromSeconds(5)), "Background writer did not reach the blocking console.");
var stopwatch = Stopwatch.StartNew();
observer.HttpRequest(new McpHttpRequestInfo(DateTimeOffset.UtcNow, "connection", "trace-2", "127.0.0.1", 50124,
"127.0.0.1", 8080, "POST", "/mcp", 200, 12.5, "application/json", 100, "test-client",
"2026-07-28", "tools/list", null));
stopwatch.Stop();
Assert.True(stopwatch.Elapsed < TimeSpan.FromMilliseconds(250), $"HTTP callback blocked for {stopwatch.Elapsed}.");
console.Release();
}
private sealed class BlockingTextWriter : TextWriter
{
private readonly ManualResetEventSlim _entered = new(false);
private readonly ManualResetEventSlim _release = new(false);
public override Encoding Encoding => Encoding.UTF8;
public override void WriteLine(string? value)
{
_entered.Set();
_release.Wait(TimeSpan.FromSeconds(30));
}
public bool WaitUntilBlocked(TimeSpan timeout) => _entered.Wait(timeout);
public void Release() => _release.Set();
protected override void Dispose(bool disposing)
{
if (disposing)
{
_release.Set();
_entered.Dispose();
_release.Dispose();
}
base.Dispose(disposing);
}
}
}
|