using Microsoft.Extensions.Logging; using Opc.Ua; namespace ZB.MOM.WW.OtOpcUa.OpcUaServer; /// /// The 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 /// RsaEncryptedSecret signing-time window, #873), certificate validation, secure-channel faults — left /// no server log line at all (#874). forwards the SDK's Warning-and-above output to the /// host's under the SDK's own categories, rate-limited per category because an /// unauthenticated client retrying in a loop would otherwise flood the log. /// public static class ServerSdkTelemetry { /// SDK log entries forwarded per category per before the rest are dropped and counted. internal const int MaxEntriesPerWindow = 20; /// The rate-limit window. internal static readonly TimeSpan Window = TimeSpan.FromMinutes(1); /// Creates a context whose SDK Warning-and-above output goes to . /// The host's logger factory. /// The telemetry context; release it with . public static ITelemetryContext Create(ILoggerFactory loggerFactory) { ArgumentNullException.ThrowIfNull(loggerFactory); return DefaultTelemetry.Create(builder => builder .SetMinimumLevel(LogLevel.Warning) .AddProvider(new ForwardingProvider(loggerFactory))); } /// Disposes the logger factory a context owns (not the host's factory). /// The context to release. 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() { } } /// Forwards at most max entries per window; the next window's first entry notes how many were dropped. 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 state) where TState : notnull => target.BeginScope(state); public bool IsEnabled(LogLevel logLevel) => target.IsEnabled(logLevel); public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func 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); } } }