using System.Collections.Concurrent; using ScadaLink.Commons.Messages.Health; using ScadaLink.Commons.Types.Enums; namespace ScadaLink.HealthMonitoring; /// /// Collects health metrics from all site subsystems. /// Thread-safe: counters use Interlocked operations, connection/tag data uses ConcurrentDictionary. /// public class SiteHealthCollector : ISiteHealthCollector { private int _scriptErrorCount; private int _alarmErrorCount; private int _deadLetterCount; private readonly ConcurrentDictionary _connectionStatuses = new(); private readonly ConcurrentDictionary _tagResolutionCounts = new(); private IReadOnlyDictionary _sfBufferDepths = new Dictionary(); private int _deployedInstanceCount, _enabledInstanceCount, _disabledInstanceCount; private volatile bool _isActiveNode; /// /// Increment the script error counter. Covers unhandled exceptions, /// timeouts, and recursion limit violations. /// public void IncrementScriptError() { Interlocked.Increment(ref _scriptErrorCount); } /// /// Increment the alarm evaluation error counter. /// public void IncrementAlarmError() { Interlocked.Increment(ref _alarmErrorCount); } /// /// Increment the dead letter counter for this reporting interval. /// public void IncrementDeadLetter() { Interlocked.Increment(ref _deadLetterCount); } /// /// Update the health status for a named data connection. /// Called by DCL when connection state changes. /// public void UpdateConnectionHealth(string connectionName, ConnectionHealth health) { _connectionStatuses[connectionName] = health; } /// /// Remove a connection from tracking (e.g., on connection disposal). /// public void RemoveConnection(string connectionName) { _connectionStatuses.TryRemove(connectionName, out _); _tagResolutionCounts.TryRemove(connectionName, out _); } /// /// Update tag resolution counts for a named data connection. /// Called by DCL after tag resolution attempts. /// public void UpdateTagResolution(string connectionName, int totalSubscribed, int successfullyResolved) { _tagResolutionCounts[connectionName] = new TagResolutionStatus(totalSubscribed, successfullyResolved); } /// /// Set the current store-and-forward buffer depths snapshot. /// Called before report collection with data from the S&F service. /// public void SetStoreAndForwardDepths(IReadOnlyDictionary depths) { _sfBufferDepths = depths; } /// /// Set the current instance counts. /// Called by the Deployment Manager after instance state changes. /// public void SetInstanceCounts(int deployed, int enabled, int disabled) { Interlocked.Exchange(ref _deployedInstanceCount, deployed); Interlocked.Exchange(ref _enabledInstanceCount, enabled); Interlocked.Exchange(ref _disabledInstanceCount, disabled); } public void SetActiveNode(bool isActive) => _isActiveNode = isActive; public bool IsActiveNode => _isActiveNode; /// /// Collect the current health report for the site and reset interval counters. /// Connection statuses and tag resolution counts are NOT reset (they reflect current state). /// Script errors, alarm errors, and dead letters ARE reset (they are per-interval counts). /// public SiteHealthReport CollectReport(string siteId) { // Atomically read and reset the counters var scriptErrors = Interlocked.Exchange(ref _scriptErrorCount, 0); var alarmErrors = Interlocked.Exchange(ref _alarmErrorCount, 0); var deadLetters = Interlocked.Exchange(ref _deadLetterCount, 0); // Snapshot current connection and tag resolution state var connectionStatuses = new Dictionary(_connectionStatuses); var tagResolution = new Dictionary(_tagResolutionCounts); // Snapshot current S&F buffer depths var sfBufferDepths = new Dictionary(_sfBufferDepths); // Determine node role from active/standby state var nodeRole = _isActiveNode ? "Active" : "Standby"; return new SiteHealthReport( SiteId: siteId, SequenceNumber: 0, // Caller (HealthReportSender) assigns the sequence number ReportTimestamp: DateTimeOffset.UtcNow, DataConnectionStatuses: connectionStatuses, TagResolutionCounts: tagResolution, ScriptErrorCount: scriptErrors, AlarmEvaluationErrorCount: alarmErrors, StoreAndForwardBufferDepths: sfBufferDepths, DeadLetterCount: deadLetters, DeployedInstanceCount: _deployedInstanceCount, EnabledInstanceCount: _enabledInstanceCount, DisabledInstanceCount: _disabledInstanceCount, NodeRole: nodeRole); } }