diff --git a/src/ScadaLink.CentralUI/Components/Pages/Monitoring/Health.razor b/src/ScadaLink.CentralUI/Components/Pages/Monitoring/Health.razor
index 347fd65..81a2476 100644
--- a/src/ScadaLink.CentralUI/Components/Pages/Monitoring/Health.razor
+++ b/src/ScadaLink.CentralUI/Components/Pages/Monitoring/Health.razor
@@ -100,6 +100,23 @@
}
+ @* Instances *@
+
+
Instances
+
+ Deployed
+ @report.DeployedInstanceCount
+
+
+ Enabled
+ @report.EnabledInstanceCount
+
+
+ Disabled
+ @report.DisabledInstanceCount
+
+
+
@* Error Counts *@
Error Counts
diff --git a/src/ScadaLink.Commons/Messages/Health/SiteHealthReport.cs b/src/ScadaLink.Commons/Messages/Health/SiteHealthReport.cs
index bdd8af1..ed9af6d 100644
--- a/src/ScadaLink.Commons/Messages/Health/SiteHealthReport.cs
+++ b/src/ScadaLink.Commons/Messages/Health/SiteHealthReport.cs
@@ -11,4 +11,7 @@ public record SiteHealthReport(
int ScriptErrorCount,
int AlarmEvaluationErrorCount,
IReadOnlyDictionary
StoreAndForwardBufferDepths,
- int DeadLetterCount);
+ int DeadLetterCount,
+ int DeployedInstanceCount,
+ int EnabledInstanceCount,
+ int DisabledInstanceCount);
diff --git a/src/ScadaLink.HealthMonitoring/HealthReportSender.cs b/src/ScadaLink.HealthMonitoring/HealthReportSender.cs
index 303934e..4eda01b 100644
--- a/src/ScadaLink.HealthMonitoring/HealthReportSender.cs
+++ b/src/ScadaLink.HealthMonitoring/HealthReportSender.cs
@@ -49,6 +49,10 @@ public class HealthReportSender : BackgroundService
{
try
{
+ // TODO: Wire S&F buffer depths when StoreAndForward service is available in DI
+ // e.g., var depths = await _bufferDepthProvider.GetDepthsAsync();
+ // _collector.SetStoreAndForwardDepths(depths);
+
var seq = Interlocked.Increment(ref _sequenceNumber);
var report = _collector.CollectReport(_siteId);
diff --git a/src/ScadaLink.HealthMonitoring/ISiteHealthCollector.cs b/src/ScadaLink.HealthMonitoring/ISiteHealthCollector.cs
index 95b9128..c64ee97 100644
--- a/src/ScadaLink.HealthMonitoring/ISiteHealthCollector.cs
+++ b/src/ScadaLink.HealthMonitoring/ISiteHealthCollector.cs
@@ -15,5 +15,7 @@ public interface ISiteHealthCollector
void UpdateConnectionHealth(string connectionName, ConnectionHealth health);
void RemoveConnection(string connectionName);
void UpdateTagResolution(string connectionName, int totalSubscribed, int successfullyResolved);
+ void SetStoreAndForwardDepths(IReadOnlyDictionary depths);
+ void SetInstanceCounts(int deployed, int enabled, int disabled);
SiteHealthReport CollectReport(string siteId);
}
diff --git a/src/ScadaLink.HealthMonitoring/SiteHealthCollector.cs b/src/ScadaLink.HealthMonitoring/SiteHealthCollector.cs
index d245b08..b676d20 100644
--- a/src/ScadaLink.HealthMonitoring/SiteHealthCollector.cs
+++ b/src/ScadaLink.HealthMonitoring/SiteHealthCollector.cs
@@ -15,6 +15,8 @@ public class SiteHealthCollector : ISiteHealthCollector
private int _deadLetterCount;
private readonly ConcurrentDictionary _connectionStatuses = new();
private readonly ConcurrentDictionary _tagResolutionCounts = new();
+ private IReadOnlyDictionary _sfBufferDepths = new Dictionary();
+ private int _deployedInstanceCount, _enabledInstanceCount, _disabledInstanceCount;
///
/// Increment the script error counter. Covers unhandled exceptions,
@@ -68,6 +70,26 @@ public class SiteHealthCollector : ISiteHealthCollector
_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);
+ }
+
///
/// 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).
@@ -84,8 +106,8 @@ public class SiteHealthCollector : ISiteHealthCollector
var connectionStatuses = new Dictionary(_connectionStatuses);
var tagResolution = new Dictionary(_tagResolutionCounts);
- // S&F buffer depth: placeholder (Phase 3C)
- var sfBufferDepths = new Dictionary();
+ // Snapshot current S&F buffer depths
+ var sfBufferDepths = new Dictionary(_sfBufferDepths);
return new SiteHealthReport(
SiteId: siteId,
@@ -96,6 +118,9 @@ public class SiteHealthCollector : ISiteHealthCollector
ScriptErrorCount: scriptErrors,
AlarmEvaluationErrorCount: alarmErrors,
StoreAndForwardBufferDepths: sfBufferDepths,
- DeadLetterCount: deadLetters);
+ DeadLetterCount: deadLetters,
+ DeployedInstanceCount: _deployedInstanceCount,
+ EnabledInstanceCount: _enabledInstanceCount,
+ DisabledInstanceCount: _disabledInstanceCount);
}
}
diff --git a/src/ScadaLink.Host/Actors/AkkaHostedService.cs b/src/ScadaLink.Host/Actors/AkkaHostedService.cs
index 93fa8bb..e53aa86 100644
--- a/src/ScadaLink.Host/Actors/AkkaHostedService.cs
+++ b/src/ScadaLink.Host/Actors/AkkaHostedService.cs
@@ -118,8 +118,9 @@ akka {{
// Register the dead letter monitor actor
var loggerFactory = _serviceProvider.GetRequiredService();
var dlmLogger = loggerFactory.CreateLogger();
+ var dlmHealthCollector = _serviceProvider.GetService();
_actorSystem.ActorOf(
- Props.Create(() => new DeadLetterMonitorActor(dlmLogger)),
+ Props.Create(() => new DeadLetterMonitorActor(dlmLogger, dlmHealthCollector)),
"dead-letter-monitor");
// Register role-specific actors
@@ -227,6 +228,9 @@ akka {{
_logger.LogInformation("Data Connection Layer manager actor created");
}
+ // Resolve the health collector for the Deployment Manager
+ var siteHealthCollector = _serviceProvider.GetService();
+
// Create the Deployment Manager as a cluster singleton
var singletonProps = ClusterSingletonManager.Props(
singletonProps: Props.Create(() => new DeploymentManagerActor(
@@ -236,7 +240,8 @@ akka {{
streamManager,
siteRuntimeOptionsValue,
dmLogger,
- dclManager)),
+ dclManager,
+ siteHealthCollector)),
terminationMessage: PoisonPill.Instance,
settings: ClusterSingletonManagerSettings.Create(_actorSystem!)
.WithRole(siteRole)
diff --git a/src/ScadaLink.Host/Actors/DeadLetterMonitorActor.cs b/src/ScadaLink.Host/Actors/DeadLetterMonitorActor.cs
index 1301ee5..9470bcf 100644
--- a/src/ScadaLink.Host/Actors/DeadLetterMonitorActor.cs
+++ b/src/ScadaLink.Host/Actors/DeadLetterMonitorActor.cs
@@ -1,6 +1,7 @@
using Akka.Actor;
using Akka.Event;
using Microsoft.Extensions.Logging;
+using ScadaLink.HealthMonitoring;
namespace ScadaLink.Host.Actors;
@@ -11,12 +12,16 @@ namespace ScadaLink.Host.Actors;
public class DeadLetterMonitorActor : ReceiveActor
{
private long _deadLetterCount;
+ private readonly ISiteHealthCollector? _healthCollector;
- public DeadLetterMonitorActor(ILogger logger)
+ public DeadLetterMonitorActor(ILogger logger, ISiteHealthCollector? healthCollector = null)
{
+ _healthCollector = healthCollector;
+
Receive(dl =>
{
_deadLetterCount++;
+ _healthCollector?.IncrementDeadLetter();
logger.LogWarning(
"Dead letter: {MessageType} from {Sender} to {Recipient}",
dl.Message.GetType().Name,
diff --git a/src/ScadaLink.SiteRuntime/Actors/AlarmActor.cs b/src/ScadaLink.SiteRuntime/Actors/AlarmActor.cs
index 8901956..e994f4d 100644
--- a/src/ScadaLink.SiteRuntime/Actors/AlarmActor.cs
+++ b/src/ScadaLink.SiteRuntime/Actors/AlarmActor.cs
@@ -4,6 +4,7 @@ using Microsoft.Extensions.Logging;
using ScadaLink.Commons.Messages.Streaming;
using ScadaLink.Commons.Types.Enums;
using ScadaLink.Commons.Types.Flattening;
+using ScadaLink.HealthMonitoring;
using ScadaLink.SiteRuntime.Scripts;
using System.Text.Json;
@@ -34,6 +35,7 @@ public class AlarmActor : ReceiveActor
private readonly SharedScriptLibrary _sharedScriptLibrary;
private readonly SiteRuntimeOptions _options;
private readonly ILogger _logger;
+ private readonly ISiteHealthCollector? _healthCollector;
private AlarmState _currentState = AlarmState.Normal;
private readonly AlarmTriggerType _triggerType;
@@ -56,7 +58,8 @@ public class AlarmActor : ReceiveActor
Script
internal int InstanceActorCount => _instanceActors.Count;
+ ///
+ /// Updates the health collector with current instance counts.
+ /// Total deployed = _totalDeployedCount, enabled = running actors, disabled = difference.
+ ///
+ private void UpdateInstanceCounts()
+ {
+ _healthCollector?.SetInstanceCounts(
+ deployed: _totalDeployedCount,
+ enabled: _instanceActors.Count,
+ disabled: _totalDeployedCount - _instanceActors.Count);
+ }
+
// ── Internal messages ──
internal record StartupConfigsLoaded(List Configs, string? Error);
diff --git a/src/ScadaLink.SiteRuntime/Actors/InstanceActor.cs b/src/ScadaLink.SiteRuntime/Actors/InstanceActor.cs
index 89be901..742b346 100644
--- a/src/ScadaLink.SiteRuntime/Actors/InstanceActor.cs
+++ b/src/ScadaLink.SiteRuntime/Actors/InstanceActor.cs
@@ -8,6 +8,7 @@ using ScadaLink.Commons.Messages.ScriptExecution;
using ScadaLink.Commons.Messages.Streaming;
using ScadaLink.Commons.Types.Enums;
using ScadaLink.Commons.Types.Flattening;
+using ScadaLink.HealthMonitoring;
using ScadaLink.SiteRuntime.Persistence;
using ScadaLink.SiteRuntime.Scripts;
using ScadaLink.SiteRuntime.Streaming;
@@ -37,6 +38,7 @@ public class InstanceActor : ReceiveActor
private readonly SiteStreamManager? _streamManager;
private readonly SiteRuntimeOptions _options;
private readonly ILogger _logger;
+ private readonly ISiteHealthCollector? _healthCollector;
private readonly Dictionary _attributes = new();
private readonly Dictionary _attributeQualities = new();
private readonly Dictionary _alarmStates = new();
@@ -61,7 +63,8 @@ public class InstanceActor : ReceiveActor
SiteStreamManager? streamManager,
SiteRuntimeOptions options,
ILogger logger,
- IActorRef? dclManager = null)
+ IActorRef? dclManager = null,
+ ISiteHealthCollector? healthCollector = null)
{
_instanceUniqueName = instanceUniqueName;
_storage = storage;
@@ -71,6 +74,7 @@ public class InstanceActor : ReceiveActor
_options = options;
_logger = logger;
_dclManager = dclManager;
+ _healthCollector = healthCollector;
// Deserialize the flattened configuration
_configuration = JsonSerializer.Deserialize(configJson);
@@ -474,7 +478,8 @@ public class InstanceActor : ReceiveActor
script,
_sharedScriptLibrary,
_options,
- _logger));
+ _logger,
+ _healthCollector));
var actorRef = Context.ActorOf(props, $"script-{script.CanonicalName}");
_scriptActors[script.CanonicalName] = actorRef;
@@ -516,7 +521,8 @@ public class InstanceActor : ReceiveActor
onTriggerScript,
_sharedScriptLibrary,
_options,
- _logger));
+ _logger,
+ _healthCollector));
var actorRef = Context.ActorOf(props, $"alarm-{alarm.CanonicalName}");
_alarmActors[alarm.CanonicalName] = actorRef;
diff --git a/src/ScadaLink.SiteRuntime/Actors/ScriptActor.cs b/src/ScadaLink.SiteRuntime/Actors/ScriptActor.cs
index ecec82f..71b5e92 100644
--- a/src/ScadaLink.SiteRuntime/Actors/ScriptActor.cs
+++ b/src/ScadaLink.SiteRuntime/Actors/ScriptActor.cs
@@ -4,6 +4,7 @@ using Microsoft.Extensions.Logging;
using ScadaLink.Commons.Messages.ScriptExecution;
using ScadaLink.Commons.Messages.Streaming;
using ScadaLink.Commons.Types.Flattening;
+using ScadaLink.HealthMonitoring;
using ScadaLink.SiteRuntime.Scripts;
using System.Text.Json;
@@ -29,6 +30,7 @@ public class ScriptActor : ReceiveActor, IWithTimers
private readonly SharedScriptLibrary _sharedScriptLibrary;
private readonly SiteRuntimeOptions _options;
private readonly ILogger _logger;
+ private readonly ISiteHealthCollector? _healthCollector;
private Script