2e948d3139
The host handed the SDK a provider-less telemetry context, so SDK-side refusals — identity-token decryption (the SDK 2 RsaEncryptedSecret clock window, #873), certificate validation, secure-channel faults — left no server log line (found on windev during the #800 Windows validation). The hosted service now creates one ServerSdkTelemetry context from its ILoggerFactory (Warning and above, SDK categories, rate-limited 20/min per category) and shares it between OtOpcUaSdkServer and the ApplicationInstance; released on dispose. Test: an untrusted client's rejection reaches the captured log. Claude-Session: https://claude.ai/code/session_01GT67AD99zQQqpLCVm3p7xk
93 lines
4.0 KiB
C#
93 lines
4.0 KiB
C#
using Microsoft.Extensions.Logging;
|
|
using Opc.Ua;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.OpcUaServer;
|
|
|
|
/// <summary>
|
|
/// The <see cref="ITelemetryContext"/> the server hands the OPC Foundation SDK. The host used to pass a
|
|
/// provider-less context, so every SDK-side refusal — identity-token decryption (e.g. the SDK 2
|
|
/// <c>RsaEncryptedSecret</c> signing-time window, #873), certificate validation, secure-channel faults — left
|
|
/// no server log line at all (#874). <see cref="Create"/> forwards the SDK's Warning-and-above output to the
|
|
/// host's <see cref="ILoggerFactory"/> under the SDK's own categories, rate-limited per category because an
|
|
/// unauthenticated client retrying in a loop would otherwise flood the log.
|
|
/// </summary>
|
|
public static class ServerSdkTelemetry
|
|
{
|
|
/// <summary>SDK log entries forwarded per category per <see cref="Window"/> before the rest are dropped and counted.</summary>
|
|
internal const int MaxEntriesPerWindow = 20;
|
|
|
|
/// <summary>The rate-limit window.</summary>
|
|
internal static readonly TimeSpan Window = TimeSpan.FromMinutes(1);
|
|
|
|
/// <summary>Creates a context whose SDK Warning-and-above output goes to <paramref name="loggerFactory"/>.</summary>
|
|
/// <param name="loggerFactory">The host's logger factory.</param>
|
|
/// <returns>The telemetry context; release it with <see cref="Release"/>.</returns>
|
|
public static ITelemetryContext Create(ILoggerFactory loggerFactory)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(loggerFactory);
|
|
return DefaultTelemetry.Create(builder => builder
|
|
.SetMinimumLevel(LogLevel.Warning)
|
|
.AddProvider(new ForwardingProvider(loggerFactory)));
|
|
}
|
|
|
|
/// <summary>Disposes the logger factory a <see cref="Create"/> context owns (not the host's factory).</summary>
|
|
/// <param name="context">The context to release.</param>
|
|
public static void Release(ITelemetryContext? context)
|
|
{
|
|
try { (context as TelemetryContextBase)?.LoggerFactory?.Dispose(); }
|
|
catch { /* best-effort */ }
|
|
}
|
|
|
|
private sealed class ForwardingProvider(ILoggerFactory target) : ILoggerProvider
|
|
{
|
|
public ILogger CreateLogger(string categoryName) =>
|
|
new RateLimitedLogger(target.CreateLogger(categoryName), MaxEntriesPerWindow, Window);
|
|
|
|
// The host owns the target factory; disposing the SDK's factory must not dispose it.
|
|
public void Dispose() { }
|
|
}
|
|
|
|
/// <summary>Forwards at most <c>max</c> entries per window; the next window's first entry notes how many were dropped.</summary>
|
|
internal sealed class RateLimitedLogger(ILogger target, int max, TimeSpan window) : ILogger
|
|
{
|
|
private readonly object _lock = new();
|
|
private DateTime _windowStart = DateTime.MinValue;
|
|
private int _count;
|
|
private int _suppressed;
|
|
|
|
public IDisposable? BeginScope<TState>(TState state) where TState : notnull => target.BeginScope(state);
|
|
|
|
public bool IsEnabled(LogLevel logLevel) => target.IsEnabled(logLevel);
|
|
|
|
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception,
|
|
Func<TState, Exception?, string> formatter)
|
|
{
|
|
int suppressedNote;
|
|
lock (_lock)
|
|
{
|
|
var now = DateTime.UtcNow;
|
|
if (now - _windowStart >= window)
|
|
{
|
|
suppressedNote = _suppressed;
|
|
_windowStart = now;
|
|
_count = 0;
|
|
_suppressed = 0;
|
|
}
|
|
else suppressedNote = 0;
|
|
|
|
if (_count >= max)
|
|
{
|
|
_suppressed++;
|
|
return;
|
|
}
|
|
_count++;
|
|
}
|
|
|
|
if (suppressedNote > 0)
|
|
target.LogWarning("OPC UA SDK: {Suppressed} further log entries were suppressed in the last {Window}.",
|
|
suppressedNote, window);
|
|
target.Log(logLevel, eventId, state, exception, formatter);
|
|
}
|
|
}
|
|
}
|