Files
lmxopcua/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect.Tests/RecordingDriverLogger.cs
T
Joseph Doherty d67634cc4f fix(mtconnect): redact the Agent URI everywhere it is shown (R06:T-8)
The driver logged and surfaced the raw AgentUri (which may carry user:pass@) as
HostName (the /hosts key), in ~20 log lines and in exception messages that reach
DriverHealth.LastError, and the client's request-URI messages (timeouts, stream
end, not-a-stream, framing) embedded it too - while the Test-Connect probe
deliberately redacted it. New MTConnectUriRedaction (user-info, and for the
configured string its query, removed; a clean URI is unchanged so host keys do
not move); the driver computes its display form once per options document.

Refs #743

Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
2026-10-22 16:00:40 -04:00

85 lines
2.6 KiB
C#

using Microsoft.Extensions.Logging;
namespace ZB.MOM.WW.OtOpcUa.Driver.MTConnect.Tests;
/// <summary>
/// Captures what the driver logs, so that "and it says so" can be asserted rather than assumed.
/// </summary>
/// <remarks>
/// Used where the log line <b>is</b> the behaviour: a browse scope that matches nothing, and a
/// subscription the driver silently refuses to serve. Both are cases whose only other symptom is
/// an operator staring at an empty panel, so a fix that emits nothing is not a fix.
/// </remarks>
internal sealed class RecordingDriverLogger : ILogger<MTConnectDriver>
{
/// <summary>Formatted <see cref="LogLevel.Warning"/> lines, in order.</summary>
public List<string> Warnings { get; } = [];
/// <summary>Formatted <see cref="LogLevel.Error"/> lines, in order.</summary>
public List<string> Errors { get; } = [];
/// <summary>Formatted <see cref="LogLevel.Information"/> lines, in order.</summary>
public List<string> Infos { get; } = [];
/// <summary>Every recorded line at Information and above, with its exception's message appended.</summary>
public IReadOnlyList<string> AllSnapshot()
{
var all = new List<string>();
foreach (var sink in new[] { Infos, Warnings, Errors })
{
lock (sink)
{
all.AddRange(sink);
}
}
return all;
}
/// <inheritdoc/>
public IDisposable? BeginScope<TState>(TState state)
where TState : notnull => null;
/// <inheritdoc/>
public bool IsEnabled(LogLevel logLevel) => true;
/// <inheritdoc/>
public void Log<TState>(
LogLevel logLevel,
EventId eventId,
TState state,
Exception? exception,
Func<TState, Exception?, string> formatter)
{
ArgumentNullException.ThrowIfNull(formatter);
var sink = logLevel switch
{
LogLevel.Warning => Warnings,
LogLevel.Error => Errors,
LogLevel.Information => Infos,
_ => null,
};
// Locked: the /sample pump logs from its own task while the test thread reads.
if (sink is null)
{
return;
}
lock (sink)
{
sink.Add(exception is null ? formatter(state, exception) : $"{formatter(state, exception)} | {exception.Message}");
}
}
/// <summary>A snapshot copy of <see cref="Warnings"/>, safe to enumerate while the pump runs.</summary>
public IReadOnlyList<string> WarningsSnapshot()
{
lock (Warnings)
{
return [.. Warnings];
}
}
}