diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.Host/OpcUa/OtOpcUaServerHostedService.cs b/src/Server/ZB.MOM.WW.OtOpcUa.Host/OpcUa/OtOpcUaServerHostedService.cs index 95a5251a..bf6d045e 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.Host/OpcUa/OtOpcUaServerHostedService.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.Host/OpcUa/OtOpcUaServerHostedService.cs @@ -141,7 +141,7 @@ public sealed class OtOpcUaServerHostedService : IHostedService, IAsyncDisposabl /// A task that represents the asynchronous operation. public async Task StartAsync(CancellationToken cancellationToken) { - _server = new OtOpcUaSdkServer(); + _server = new OtOpcUaSdkServer(_loggerFactory.CreateLogger()); _appHost = new OpcUaApplicationHost( _options, _loggerFactory.CreateLogger(), diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/OtOpcUaNodeManager.cs b/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/OtOpcUaNodeManager.cs index b078231a..986790e2 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/OtOpcUaNodeManager.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/OtOpcUaNodeManager.cs @@ -1,4 +1,6 @@ using System.Collections.Concurrent; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; using Opc.Ua; using Opc.Ua.Server; using ZB.MOM.WW.OtOpcUa.Commons.OpcUa; @@ -92,13 +94,18 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2 /// . private readonly ConcurrentDictionary _eventNotifierSources = new(); private FolderState? _root; + private readonly ILogger _logger; /// Initializes a new instance of the class with the OPC UA server and configuration. /// The OPC UA server instance. /// The application configuration. - public OtOpcUaNodeManager(IServerInternal server, ApplicationConfiguration configuration) + /// Logger for the manager's swallowed-fault and skipped-wiring diagnostics; + /// null (tests) falls back to . + public OtOpcUaNodeManager( + IServerInternal server, ApplicationConfiguration configuration, ILogger? logger = null) : base(server, configuration, RegisteredNamespaceUris) { + _logger = logger ?? NullLogger.Instance; // SystemContext is initialised by the base ctor; both namespace URIs are now registered and their // indexes captured (in the order passed) in NamespaceIndexes. } @@ -153,11 +160,9 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2 // Unreachable by construction — every call site passes a node this manager minted with a string // `s=` identifier. If it ever fires, the derived key resolves to nothing and the caller sees an // unexplained miss, so name the seam rather than collapsing quietly. -#pragma warning disable CS0618 // Utils.LogWarning is [Obsolete] in favour of an ITelemetryContext this manager doesn't carry. - Utils.LogWarning( - "OtOpcUaNodeManager.MapKey: non-string NodeId identifier reached the map-key seam (node {0}, identifier type {1}) — the derived key will not resolve", + _logger.LogWarning( + "OtOpcUaNodeManager.MapKey: non-string NodeId identifier reached the map-key seam (node {NodeId}, identifier type {IdentifierType}) — the derived key will not resolve", nodeId, nodeId.Identifier?.GetType().Name ?? "null"); -#pragma warning restore CS0618 return new RealmKey(RealmOf(nodeId), nodeId.Identifier?.ToString() ?? string.Empty); } @@ -742,14 +747,11 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2 if (skipped > 0) { // Debug, not Warning: the caller's index racing a surgical remove is expected, and a driver - // whose nodes were all just removed would otherwise log once per ref. This manager carries no - // ILogger, so log via the SDK's static trace (see ServeNodeAsync). -#pragma warning disable CS0618 // Type or member is obsolete - Utils.LogDebug( - "OtOpcUaNodeManager: WriteStatuses skipped {0} of {1} item(s) — node not materialised.", + // whose nodes were all just removed would otherwise log once per ref. + _logger.LogDebug( + "OtOpcUaNodeManager: WriteStatuses skipped {Skipped} of {Total} item(s) — node not materialised.", skipped, statuses.Count); -#pragma warning restore CS0618 } if (failed > 0) @@ -1055,17 +1057,10 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2 { // A failed event report must NOT break the state projection or the calling actor: the node's // state has already been applied + ClearChangeMasks'd, so attribute subscribers still see the - // change; only the event delivery is lost. This CustomNodeManager2 carries no ILogger, so log - // through the SDK's static trace (Utils.LogError) instead of swallowing silently — a recurring - // failure here is then visible in the server log rather than invisible. T19's live Client.CLI - // run is the integration proof that the happy path delivers. - // Utils.LogError routes to the SDK's trace sink. It's [Obsolete] in 1.5.378 in favour of an - // ITelemetryContext/ILogger this CustomNodeManager2 doesn't have wired — suppress the - // deprecation here (wiring the telemetry logger through is a separate follow-up); the point is - // that a recurring failure is visible in the server trace rather than silently swallowed. -#pragma warning disable CS0618 // Type or member is obsolete - Utils.LogError(ex, "OtOpcUaNodeManager: failed to report Part 9 condition event for {0}", alarm.NodeId); -#pragma warning restore CS0618 + // change; only the event delivery is lost. Log through the injected ILogger instead of + // swallowing silently — a recurring failure here is then visible in the server log rather than + // invisible. T19's live Client.CLI run is the integration proof that the happy path delivers. + _logger.LogError(ex, "OtOpcUaNodeManager: failed to report Part 9 condition event for {NodeId}", alarm.NodeId); } } @@ -1479,15 +1474,10 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2 // not fault on the notifier wiring. if (!_alarmConditions.TryGetValue(alarmKey, out var alarm)) { - // This CustomNodeManager2 carries no ILogger; log through the SDK's static trace (see the - // ReportEvent catch below for the same pattern). Utils.LogInfo is [Obsolete] in 1.5.378 in - // favour of an ITelemetryContext this manager doesn't wire — suppress the deprecation. -#pragma warning disable CS0618 // Type or member is obsolete // Interpolate the key's PARTS, not the RealmKey struct — a struct dump would render a shape // no log-grep pattern in this file matches. - Utils.LogInfo("OtOpcUaNodeManager.WireAlarmNotifiers: condition {0} (realm {1}) not materialised — skipping notifier wiring", + _logger.LogInformation("OtOpcUaNodeManager.WireAlarmNotifiers: condition {ConditionId} (realm {Realm}) not materialised — skipping notifier wiring", alarmKey.Id, alarmKey.Realm); -#pragma warning restore CS0618 return; } @@ -1520,10 +1510,8 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2 if (!_folders.TryGetValue(MapKey(notifierFolderRealm, folderNodeId), out var folder)) { // Referencing-equipment folder not (yet) materialised — skip this one (logged, never thrown). -#pragma warning disable CS0618 // Type or member is obsolete - Utils.LogInfo("OtOpcUaNodeManager.WireAlarmNotifiers: notifier folder {0} for condition {1} (realm {2}) not present — skipped", + _logger.LogInformation("OtOpcUaNodeManager.WireAlarmNotifiers: notifier folder {FolderId} for condition {ConditionId} (realm {Realm}) not present — skipped", folderNodeId, alarmKey.Id, alarmKey.Realm); -#pragma warning restore CS0618 continue; } @@ -2212,9 +2200,7 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2 } catch (Exception ex) { -#pragma warning disable CS0618 // Utils.LogError is [Obsolete] in favour of an ITelemetryContext this manager doesn't carry. - Utils.LogError(ex, "OtOpcUaNodeManager: failed to build AuditWriteUpdateEvent for {0}", nodeId); -#pragma warning restore CS0618 + _logger.LogError(ex, "OtOpcUaNodeManager: failed to build AuditWriteUpdateEvent for {NodeId}", nodeId); auditEvent = null; } } @@ -2297,9 +2283,7 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2 { // Auditing disabled / no monitored items / server shutting down ⇒ ReportEvent may no-op or throw; // either way the revert already stands. Surface a recurring failure in the SDK trace, don't rethrow. -#pragma warning disable CS0618 // Utils.LogError is [Obsolete] in favour of an ITelemetryContext this manager doesn't carry. - Utils.LogError(ex, "OtOpcUaNodeManager: failed to report AuditWriteUpdateEvent for {0}", auditEvent.SourceNode?.Value); -#pragma warning restore CS0618 + _logger.LogError(ex, "OtOpcUaNodeManager: failed to report AuditWriteUpdateEvent for {NodeId}", auditEvent.SourceNode?.Value); } } @@ -2660,9 +2644,7 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2 { // Model-change reporting disabled / no monitored items / server shutting down ⇒ ReportEvent may // no-op or throw; either way the in-place update already stands. Log to the SDK trace, don't rethrow. -#pragma warning disable CS0618 // Utils.LogError is [Obsolete] in favour of an ITelemetryContext this manager doesn't carry. - Utils.LogError(ex, "OtOpcUaNodeManager: failed to report GeneralModelChangeEvent for {0}", variable.NodeId); -#pragma warning restore CS0618 + _logger.LogError(ex, "OtOpcUaNodeManager: failed to report GeneralModelChangeEvent for {NodeId}", variable.NodeId); } } @@ -2704,9 +2686,7 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2 { // Model-change reporting disabled / no monitored items / server shutting down ⇒ ReportEvent may // no-op or throw; either way the node add already stands. Log to the SDK trace, don't rethrow. -#pragma warning disable CS0618 // Utils.LogError is [Obsolete] in favour of an ITelemetryContext this manager doesn't carry. - Utils.LogError(ex, "OtOpcUaNodeManager: failed to report GeneralModelChangeEvent(NodeAdded) for {0}", affectedNodeId); -#pragma warning restore CS0618 + _logger.LogError(ex, "OtOpcUaNodeManager: failed to report GeneralModelChangeEvent(NodeAdded) for {NodeId}", affectedNodeId); } } @@ -2730,11 +2710,9 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2 // concurrent rebuild has torn one side down; never throw out of a deploy-time reference wire. if (source is null || target is null) { -#pragma warning disable CS0618 // Utils.LogWarning is [Obsolete] in favour of an ITelemetryContext this manager doesn't carry. - Utils.LogWarning( - "OtOpcUaNodeManager: AddReference({0}) skipped — {1} node missing (source '{2}' present={3}, target '{4}' present={5}).", + _logger.LogWarning( + "OtOpcUaNodeManager: AddReference({RefType}) skipped — {Side} node missing (source '{SourceKey}' present={SourcePresent}, target '{TargetKey}' present={TargetPresent}).", refType, source is null ? "source" : "target", sourceKey, source is not null, targetKey, target is not null); -#pragma warning restore CS0618 return; } @@ -2888,9 +2866,7 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2 } catch (Exception ex) { -#pragma warning disable CS0618 // Utils.LogError is [Obsolete] in favour of an ITelemetryContext this manager doesn't carry. - Utils.LogError(ex, "OtOpcUaNodeManager: failed to report GeneralModelChangeEvent for {0}", affectedNodeId); -#pragma warning restore CS0618 + _logger.LogError(ex, "OtOpcUaNodeManager: failed to report GeneralModelChangeEvent for {NodeId}", affectedNodeId); } } @@ -4719,10 +4695,8 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2 catch (Exception ex) { // One node's backend failure must not throw out of the batch — surface Bad for THIS node - // only. This manager carries no ILogger, so log via the SDK's static trace (see ServeNodeAsync). -#pragma warning disable CS0618 // Type or member is obsolete - Utils.LogError(ex, "OtOpcUaNodeManager: HistoryReadEvents failed for node {0}", handle.NodeId); -#pragma warning restore CS0618 + // only. Logged via the injected ILogger. + _logger.LogError(ex, "OtOpcUaNodeManager: HistoryReadEvents failed for node {NodeId}", handle.NodeId); errors[handle.Index] = StatusCodes.BadHistoryOperationUnsupported; results[handle.Index] = new SdkHistoryReadResult { StatusCode = StatusCodes.BadHistoryOperationUnsupported }; } @@ -4994,13 +4968,9 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2 catch (Exception ex) { // One node's backend failure (throw / timeout) must not throw out of the batch — surface a - // Bad status for THIS node only. This CustomNodeManager2 carries no ILogger (see - // ReportConditionEvent), so log through the SDK's static trace rather than swallowing silently. - // Utils.LogError is [Obsolete] in 1.5.378 (favours an ITelemetryContext this manager doesn't - // wire) — suppress the deprecation, matching the existing pattern. -#pragma warning disable CS0618 // Type or member is obsolete - Utils.LogError(ex, "OtOpcUaNodeManager: HistoryRead failed for node {0}", handle.NodeId); -#pragma warning restore CS0618 + // Bad status for THIS node only. Logged through the injected ILogger + // rather than swallowed silently. + _logger.LogError(ex, "OtOpcUaNodeManager: HistoryRead failed for node {NodeId}", handle.NodeId); errors[handle.Index] = StatusCodes.BadHistoryOperationUnsupported; } } @@ -5127,12 +5097,10 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2 // larger ServerHistorian:MaxTieClusterOverfetch (or NumValuesPerNode). if (cluster.Count > MaxTieClusterOverfetch) { -#pragma warning disable CS0618 // Type or member is obsolete - Utils.LogError( - "OtOpcUaNodeManager: HistoryReadRaw tie cluster at {0:O} for tag '{1}' has {2} samples, " + - "exceeding MaxTieClusterOverfetch={3}; cannot page within it. Increase MaxTieClusterOverfetch.", + _logger.LogError( + "OtOpcUaNodeManager: HistoryReadRaw tie cluster at {StartUtc:O} for tag '{Tagname}' has {Count} samples, " + + "exceeding MaxTieClusterOverfetch={Max}; cannot page within it. Increase MaxTieClusterOverfetch.", startUtc, tagname, cluster.Count, MaxTieClusterOverfetch); -#pragma warning restore CS0618 errors[handle.Index] = StatusCodes.BadHistoryOperationUnsupported; results[handle.Index] = new SdkHistoryReadResult { StatusCode = StatusCodes.BadHistoryOperationUnsupported }; return; @@ -5201,9 +5169,7 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2 catch (Exception ex) { // One node's backend failure must not throw out of the batch — Bad for THIS node only. -#pragma warning disable CS0618 // Type or member is obsolete - Utils.LogError(ex, "OtOpcUaNodeManager: HistoryReadRaw (paged) failed for node {0}", handle.NodeId); -#pragma warning restore CS0618 + _logger.LogError(ex, "OtOpcUaNodeManager: HistoryReadRaw (paged) failed for node {NodeId}", handle.NodeId); errors[handle.Index] = StatusCodes.BadHistoryOperationUnsupported; } } diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/OtOpcUaSdkServer.cs b/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/OtOpcUaSdkServer.cs index 9cdd91ce..18da840c 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/OtOpcUaSdkServer.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/OtOpcUaSdkServer.cs @@ -1,3 +1,4 @@ +using Microsoft.Extensions.Logging; using Opc.Ua; using Opc.Ua.Server; using ZB.MOM.WW.OtOpcUa.Commons.OpcUa; @@ -15,8 +16,14 @@ namespace ZB.MOM.WW.OtOpcUa.OpcUaServer; /// public sealed class OtOpcUaSdkServer : StandardServer { + private readonly ILogger? _nodeManagerLogger; private OtOpcUaNodeManager? _otOpcUaNodeManager; + /// Initializes a new instance of the class. + /// Logger handed to the created ; + /// null leaves it on a no-op logger. + public OtOpcUaSdkServer(ILogger? nodeManagerLogger = null) => _nodeManagerLogger = nodeManagerLogger; + /// The custom node manager once StartAsync has called /// . Null until the SDK has bootstrapped. public OtOpcUaNodeManager? NodeManager => _otOpcUaNodeManager; @@ -163,7 +170,7 @@ public sealed class OtOpcUaSdkServer : StandardServer protected override MasterNodeManager CreateMasterNodeManager( IServerInternal server, ApplicationConfiguration configuration) { - _otOpcUaNodeManager = new OtOpcUaNodeManager(server, configuration); + _otOpcUaNodeManager = new OtOpcUaNodeManager(server, configuration, _nodeManagerLogger); return new MasterNodeManager(server, configuration, dynamicNamespaceUri: null, _otOpcUaNodeManager); } } diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests/ServerEventRig.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests/ServerEventRig.cs index 1fdaff44..f5322bba 100644 --- a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests/ServerEventRig.cs +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests/ServerEventRig.cs @@ -229,9 +229,7 @@ internal sealed class ServerEventRig : IAsyncDisposable if (encrypted) { -#pragma warning disable CS0618 // the telemetry-context overload is not needed for a test client - var app = new ApplicationInstance { ApplicationName = appConfig.ApplicationName, ApplicationType = ApplicationType.Client }; -#pragma warning restore CS0618 + var app = new ApplicationInstance(DefaultTelemetry.Create(_ => { })) { ApplicationName = appConfig.ApplicationName, ApplicationType = ApplicationType.Client }; app.ApplicationConfiguration = appConfig; await app.CheckApplicationInstanceCertificatesAsync(silent: true, lifeTimeInMonths: null, ct); }