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);
}
}
}