feat(siteeventlog): emit alarm-category events on alarm transitions (M1.5)
AlarmActor (computed) and NativeAlarmActor (native mirror) now fire-and-forget an 'alarm' site operational event on every state transition: - raise/activate: Error (priority/severity >= 700) or Warning - clear/return-to-normal, ack, inter-band transition: Info Both actors take a new optional IServiceProvider? ctor param (default null so existing direct-construction tests still compile); InstanceActor passes its _serviceProvider at the two Props.Create sites. Resolution is optional and the LogEventAsync call is fire-and-forget, so a logging failure never affects alarm evaluation. Rehydration replays are not re-logged. Adds a capturing FakeSiteEventLogger test helper + SingleServiceProvider.
This commit is contained in:
@@ -1,10 +1,12 @@
|
||||
using Akka.Actor;
|
||||
using Microsoft.CodeAnalysis.Scripting;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Streaming;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Types.Flattening;
|
||||
using ZB.MOM.WW.ScadaBridge.HealthMonitoring;
|
||||
using ZB.MOM.WW.ScadaBridge.SiteEventLogging;
|
||||
using ZB.MOM.WW.ScadaBridge.SiteRuntime.Scripts;
|
||||
using System.Globalization;
|
||||
using System.Text.Json;
|
||||
@@ -37,6 +39,14 @@ public class AlarmActor : ReceiveActor
|
||||
private readonly SiteRuntimeOptions _options;
|
||||
private readonly ILogger _logger;
|
||||
private readonly ISiteHealthCollector? _healthCollector;
|
||||
private readonly IServiceProvider? _serviceProvider;
|
||||
|
||||
/// <summary>
|
||||
/// M1.5: priority at or above which a computed-alarm raise is logged as
|
||||
/// <c>Error</c> to the site event log; below it, raises log as <c>Warning</c>.
|
||||
/// Mirrors the 0–1000 alarm-severity scale.
|
||||
/// </summary>
|
||||
private const int ErrorPriorityThreshold = 700;
|
||||
|
||||
private AlarmState _currentState = AlarmState.Normal;
|
||||
/// <summary>
|
||||
@@ -83,6 +93,9 @@ public class AlarmActor : ReceiveActor
|
||||
/// <param name="compiledTriggerExpression">Pre-compiled trigger expression, or <c>null</c> for non-expression triggers.</param>
|
||||
/// <param name="initialAttributes">Seed attribute snapshot so static attributes evaluate correctly at startup.</param>
|
||||
/// <param name="healthCollector">Optional health collector for surfacing alarm execution metrics.</param>
|
||||
/// <param name="serviceProvider">Optional DI service provider used to resolve the optional
|
||||
/// <see cref="ISiteEventLogger"/> for M1.5 <c>alarm</c> operational events. Fire-and-forget;
|
||||
/// a logging failure never affects alarm evaluation.</param>
|
||||
public AlarmActor(
|
||||
string alarmName,
|
||||
string instanceName,
|
||||
@@ -94,7 +107,8 @@ public class AlarmActor : ReceiveActor
|
||||
ILogger logger,
|
||||
Script<object?>? compiledTriggerExpression = null,
|
||||
IReadOnlyDictionary<string, object?>? initialAttributes = null,
|
||||
ISiteHealthCollector? healthCollector = null)
|
||||
ISiteHealthCollector? healthCollector = null,
|
||||
IServiceProvider? serviceProvider = null)
|
||||
{
|
||||
_alarmName = alarmName;
|
||||
_instanceName = instanceName;
|
||||
@@ -103,6 +117,7 @@ public class AlarmActor : ReceiveActor
|
||||
_options = options;
|
||||
_logger = logger;
|
||||
_healthCollector = healthCollector;
|
||||
_serviceProvider = serviceProvider;
|
||||
_priority = alarmConfig.PriorityLevel;
|
||||
_onTriggerScriptName = alarmConfig.OnTriggerScriptCanonicalName;
|
||||
_onTriggerCompiledScript = onTriggerCompiledScript;
|
||||
@@ -208,6 +223,9 @@ public class AlarmActor : ReceiveActor
|
||||
_instanceName, _alarmName, AlarmState.Active, _priority, DateTimeOffset.UtcNow);
|
||||
_instanceActor.Tell(alarmChanged);
|
||||
|
||||
// M1.5: operational `alarm` event — raise. Severity by priority.
|
||||
LogAlarmEvent(RaiseSeverity(_priority), $"Alarm {_alarmName} activated (priority {_priority})");
|
||||
|
||||
// Spawn AlarmExecutionActor if on-trigger script defined
|
||||
if (_onTriggerCompiledScript != null)
|
||||
{
|
||||
@@ -225,6 +243,9 @@ public class AlarmActor : ReceiveActor
|
||||
var alarmChanged = new AlarmStateChanged(
|
||||
_instanceName, _alarmName, AlarmState.Normal, _priority, DateTimeOffset.UtcNow);
|
||||
_instanceActor.Tell(alarmChanged);
|
||||
|
||||
// M1.5: operational `alarm` event — return to normal.
|
||||
LogAlarmEvent("Info", $"Alarm {_alarmName} cleared");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -265,6 +286,24 @@ public class AlarmActor : ReceiveActor
|
||||
};
|
||||
_instanceActor.Tell(alarmChanged);
|
||||
|
||||
// M1.5: operational `alarm` event. Entering a band from Normal is a raise
|
||||
// (severity by the band's priority); returning to None is a clear; a
|
||||
// level-to-level escalation/de-escalation is an informational transition.
|
||||
if (newLevel == AlarmLevel.None)
|
||||
{
|
||||
LogAlarmEvent("Info", $"Alarm {_alarmName} cleared ({previousLevel} → Normal)");
|
||||
}
|
||||
else if (previousLevel == AlarmLevel.None)
|
||||
{
|
||||
LogAlarmEvent(RaiseSeverity(priority),
|
||||
$"Alarm {_alarmName} activated at {newLevel} (priority {priority})");
|
||||
}
|
||||
else
|
||||
{
|
||||
LogAlarmEvent("Info",
|
||||
$"Alarm {_alarmName} transitioned {previousLevel} → {newLevel} (priority {priority})");
|
||||
}
|
||||
|
||||
if (previousLevel == AlarmLevel.None
|
||||
&& newLevel != AlarmLevel.None
|
||||
&& _onTriggerCompiledScript != null)
|
||||
@@ -273,6 +312,27 @@ public class AlarmActor : ReceiveActor
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// M1.5: maps an alarm priority (0–1000) to a site-event severity for a
|
||||
/// <i>raise</i> transition — <c>Error</c> at or above
|
||||
/// <see cref="ErrorPriorityThreshold"/>, otherwise <c>Warning</c>. Clears and
|
||||
/// inter-band transitions always log as <c>Info</c>.
|
||||
/// </summary>
|
||||
private static string RaiseSeverity(int priority) =>
|
||||
priority >= ErrorPriorityThreshold ? "Error" : "Warning";
|
||||
|
||||
/// <summary>
|
||||
/// M1.5: fire-and-forget an <c>alarm</c> operational event to the optional
|
||||
/// <see cref="ISiteEventLogger"/>. Resolved optionally and never awaited so a
|
||||
/// logging failure cannot affect alarm evaluation (matching the established
|
||||
/// ScriptActor/ScriptExecutionActor pattern).
|
||||
/// </summary>
|
||||
private void LogAlarmEvent(string severity, string message)
|
||||
{
|
||||
_ = _serviceProvider?.GetService<ISiteEventLogger>()?.LogEventAsync(
|
||||
"alarm", severity, _instanceName, $"AlarmActor:{_alarmName}", message);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the per-setpoint priority for the given level. Falls back to
|
||||
/// the alarm-level <see cref="_priority"/> when the HiLo config did not
|
||||
|
||||
Reference in New Issue
Block a user