b028322568
M1: a subscription that dies while the session lives (outage longer than the subscription lifetime but shorter than the session timeout) used to end Healthy with fresh keep-alive stamps and frozen values. PublishError BadNoSubscription / BadSubscriptionIdInvalid naming a tracked subscription, or a tracked subscription whose SDK PublishingStopped persists one more keep-alive window + 5 s across good keep-alives, now declares it lost: Degraded, outage published once, and nothing advances LastSuccessfulRead until the session is released, so the #527 watchdog re-initialises and re-subscribes. Both detectors wire-tested. M2: in-place recovery is bound to the session whose keep-alive recovered; one overtaken by a re-initialise no longer re-arms the new session's outage latch or degrades its health (ReadCoreAsync expectedSession). L1 CAS health updates; L2 re-check before each recovery publish; L3 skip CloseSession on a lost session; L4 SDK log forwarding rate-limited (20/min); L5 per-driver SDK LoggerFactory disposed with the driver. Wire test asserts the read stamp stays frozen while the server is down.
102 lines
4.3 KiB
C#
102 lines
4.3 KiB
C#
using Microsoft.Extensions.Logging;
|
|
using Opc.Ua;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient;
|
|
|
|
/// <summary>
|
|
/// The <see cref="ITelemetryContext" /> the driver hands the SDK's non-obsolete (telemetry-taking)
|
|
/// overloads. <see cref="Context"/> has no logging provider (static helpers and tests);
|
|
/// <see cref="ForLogger"/> 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).
|
|
/// </summary>
|
|
internal static class SdkTelemetry
|
|
{
|
|
private static readonly Lazy<ITelemetryContext> Instance = new(() => DefaultTelemetry.Create(_ => { }));
|
|
|
|
/// <summary>Gets the shared, provider-less telemetry context.</summary>
|
|
public static ITelemetryContext Context => Instance.Value;
|
|
|
|
/// <summary>
|
|
/// Creates a telemetry context whose SDK log output at Warning and above goes to <paramref name="logger"/>,
|
|
/// 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 <see cref="ILoggerFactory"/>: release it with <see cref="Release"/>
|
|
/// when the driver is disposed — <see cref="DefaultTelemetry"/> is not itself disposable.
|
|
/// </summary>
|
|
/// <param name="logger">The driver instance's logger.</param>
|
|
/// <returns>The telemetry context.</returns>
|
|
public static ITelemetryContext ForLogger(ILogger logger) =>
|
|
DefaultTelemetry.Create(builder => builder
|
|
.SetMinimumLevel(LogLevel.Warning)
|
|
.AddProvider(new ForwardingLoggerProvider(new RateLimitedLogger(logger, MaxEntriesPerWindow, Window))));
|
|
|
|
/// <summary>Disposes the logger factory a <see cref="ForLogger"/> context owns. Never the shared one.</summary>
|
|
/// <param name="context">The context to release.</param>
|
|
public static void Release(ITelemetryContext context)
|
|
{
|
|
if (ReferenceEquals(context, Context)) return;
|
|
try { (context as TelemetryContextBase)?.LoggerFactory?.Dispose(); }
|
|
catch { /* best-effort */ }
|
|
}
|
|
|
|
/// <summary>SDK log entries forwarded 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>Hands every SDK logger category the one (rate-limited) driver logger.</summary>
|
|
private sealed class ForwardingLoggerProvider(ILogger target) : ILoggerProvider
|
|
{
|
|
public ILogger CreateLogger(string categoryName) => target;
|
|
|
|
public void Dispose() { }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Forwards at most <c>max</c> entries per window; the first entry of the next window carries a note of how
|
|
/// many were suppressed.
|
|
/// </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);
|
|
}
|
|
}
|
|
}
|