using Microsoft.Extensions.Logging; using Opc.Ua; namespace ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient; /// /// The the driver hands the SDK's non-obsolete (telemetry-taking) /// overloads. has no logging provider (static helpers and tests); /// routes the SDK's own Warning-and-above output (keep-alive, publish and /// channel errors) to a driver instance's logger — without it every SDK-side session error was /// invisible (Gitea #670). /// internal static class SdkTelemetry { private static readonly Lazy Instance = new(() => DefaultTelemetry.Create(_ => { })); /// Gets the shared, provider-less telemetry context. public static ITelemetryContext Context => Instance.Value; /// /// Creates a telemetry context whose SDK log output at Warning and above goes to , /// rate-limited (a dead upstream makes the SDK warn on every keep-alive and publish for as long as the /// outage lasts). The context owns a : release it with /// when the driver is disposed — is not itself disposable. /// /// The driver instance's logger. /// The telemetry context. public static ITelemetryContext ForLogger(ILogger logger) => DefaultTelemetry.Create(builder => builder .SetMinimumLevel(LogLevel.Warning) .AddProvider(new ForwardingLoggerProvider(new RateLimitedLogger(logger, MaxEntriesPerWindow, Window)))); /// Disposes the logger factory a context owns. Never the shared one. /// The context to release. public static void Release(ITelemetryContext context) { if (ReferenceEquals(context, Context)) return; try { (context as TelemetryContextBase)?.LoggerFactory?.Dispose(); } catch { /* best-effort */ } } /// SDK log entries forwarded 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); /// Hands every SDK logger category the one (rate-limited) driver logger. private sealed class ForwardingLoggerProvider(ILogger target) : ILoggerProvider { public ILogger CreateLogger(string categoryName) => target; public void Dispose() { } } /// /// Forwards at most max entries per window; the first entry of the next window carries a note of how /// many were suppressed. /// 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); } } }