server(alarms): watch-list resolver merging GR discovery + config override
This commit is contained in:
@@ -0,0 +1,158 @@
|
||||
using ZB.MOM.WW.MxGateway.Contracts.Proto;
|
||||
using ZB.MOM.WW.MxGateway.Server.Configuration;
|
||||
using ZB.MOM.WW.MxGateway.Server.Galaxy;
|
||||
|
||||
namespace ZB.MOM.WW.MxGateway.Server.Alarms;
|
||||
|
||||
/// <summary>
|
||||
/// Default <see cref="IAlarmWatchListResolver"/>. Merges Galaxy Repository
|
||||
/// alarm-attribute discovery with the configured include/exclude overrides
|
||||
/// and composes the per-attribute subtag item addresses from the configured
|
||||
/// subtag names.
|
||||
/// </summary>
|
||||
// NOTE: The exact subtag names and the canonical AlarmFullReference shape
|
||||
// ("Galaxy!{area}.{reference}") are validated against a live Galaxy in the
|
||||
// Task 17 live smoke test. The config Subtags block exists precisely so these
|
||||
// names are not hard-coded here.
|
||||
public sealed class AlarmWatchListResolver : IAlarmWatchListResolver
|
||||
{
|
||||
private const string ProviderLiteral = "Galaxy";
|
||||
private const string DefaultActiveSubtag = "active";
|
||||
private const string DefaultAckedSubtag = "acked";
|
||||
|
||||
private readonly IGalaxyRepository _repository;
|
||||
private readonly ILogger<AlarmWatchListResolver> _logger;
|
||||
|
||||
/// <summary>Initializes the watch-list resolver.</summary>
|
||||
/// <param name="repository">Galaxy Repository used for alarm-attribute discovery.</param>
|
||||
/// <param name="logger">Diagnostic logger.</param>
|
||||
public AlarmWatchListResolver(
|
||||
IGalaxyRepository repository,
|
||||
ILogger<AlarmWatchListResolver> logger)
|
||||
{
|
||||
_repository = repository ?? throw new ArgumentNullException(nameof(repository));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<AlarmSubtagTarget>> ResolveAsync(
|
||||
AlarmsOptions options,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(options);
|
||||
|
||||
AlarmDiscoveryOptions discovery = options.Fallback.Discovery;
|
||||
|
||||
// 1. Build the ordered, de-duplicated attribute reference set.
|
||||
// Each entry carries the reference plus the source-object reference.
|
||||
List<(string Reference, string SourceObject)> ordered = [];
|
||||
HashSet<string> seen = new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
if (discovery.UseGalaxyRepository)
|
||||
{
|
||||
List<GalaxyAlarmAttributeRow> rows;
|
||||
try
|
||||
{
|
||||
rows = await _repository.GetAlarmAttributesAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Discovery being unavailable must not crash the resolver: log and
|
||||
// continue with an empty discovery set. The caller decides what to
|
||||
// do with the (possibly config-only) result.
|
||||
_logger.LogWarning(
|
||||
ex,
|
||||
"Galaxy Repository alarm-attribute discovery failed; continuing with configuration-only watch-list.");
|
||||
rows = [];
|
||||
}
|
||||
|
||||
foreach (GalaxyAlarmAttributeRow row in rows)
|
||||
{
|
||||
if (string.IsNullOrEmpty(row.FullTagReference) || !seen.Add(row.FullTagReference))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
ordered.Add((row.FullTagReference, row.SourceObjectReference));
|
||||
}
|
||||
}
|
||||
|
||||
foreach (string include in discovery.IncludeAttributes)
|
||||
{
|
||||
if (string.IsNullOrEmpty(include) || !seen.Add(include))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
ordered.Add((include, DeriveSourceObject(include)));
|
||||
}
|
||||
|
||||
// Remove excluded references (case-insensitive). Excludes are rare, so a
|
||||
// per-entry lookup against a small set is fine.
|
||||
HashSet<string> excluded = new(discovery.ExcludeAttributes, StringComparer.OrdinalIgnoreCase);
|
||||
if (excluded.Count > 0)
|
||||
{
|
||||
ordered.RemoveAll(e => excluded.Contains(e.Reference));
|
||||
}
|
||||
|
||||
// 2. Resolve subtag names with safe fallbacks.
|
||||
string active = string.IsNullOrEmpty(options.Fallback.Subtags.Active)
|
||||
? DefaultActiveSubtag
|
||||
: options.Fallback.Subtags.Active;
|
||||
string acked = string.IsNullOrEmpty(options.Fallback.Subtags.Acked)
|
||||
? DefaultAckedSubtag
|
||||
: options.Fallback.Subtags.Acked;
|
||||
string priority = options.Fallback.Subtags.Priority;
|
||||
string ackComment = options.Fallback.Subtags.AckComment;
|
||||
|
||||
// 3. Resolve the area: discovery area, else the default area (may be empty).
|
||||
string area = string.IsNullOrEmpty(discovery.Area) ? options.DefaultArea : discovery.Area;
|
||||
|
||||
// 4. Compose one target per reference.
|
||||
List<AlarmSubtagTarget> targets = new(ordered.Count);
|
||||
foreach ((string reference, string sourceObject) in ordered)
|
||||
{
|
||||
targets.Add(new AlarmSubtagTarget
|
||||
{
|
||||
AlarmFullReference = ComposeFullReference(area, reference),
|
||||
SourceObjectReference = sourceObject,
|
||||
ActiveSubtag = $"{reference}.{active}",
|
||||
AckedSubtag = $"{reference}.{acked}",
|
||||
PrioritySubtag = string.IsNullOrEmpty(priority) ? string.Empty : $"{reference}.{priority}",
|
||||
AckCommentSubtag = string.IsNullOrEmpty(ackComment) ? string.Empty : $"{reference}.{ackComment}",
|
||||
});
|
||||
}
|
||||
|
||||
// 5. Report the resolved count; warn if subtag mode would cover nothing.
|
||||
if (targets.Count == 0 && discovery.UseGalaxyRepository)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Alarm subtag watch-list resolved to zero targets; subtag-polling fallback will cover no alarms.");
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogDebug("Resolved alarm subtag watch-list with {TargetCount} target(s).", targets.Count);
|
||||
}
|
||||
|
||||
return targets;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Derives the source-object reference for a configuration entry: the
|
||||
/// substring before the first '.', or the whole string when there is no dot.
|
||||
/// </summary>
|
||||
private static string DeriveSourceObject(string reference)
|
||||
{
|
||||
int dot = reference.IndexOf('.', StringComparison.Ordinal);
|
||||
return dot < 0 ? reference : reference[..dot];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Composes the canonical alarm full reference: <c>Galaxy!{area}.{reference}</c>
|
||||
/// when an area is set, otherwise <c>Galaxy!{reference}</c>.
|
||||
/// </summary>
|
||||
private static string ComposeFullReference(string area, string reference) =>
|
||||
string.IsNullOrEmpty(area)
|
||||
? $"{ProviderLiteral}!{reference}"
|
||||
: $"{ProviderLiteral}!{area}.{reference}";
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using ZB.MOM.WW.MxGateway.Contracts.Proto;
|
||||
using ZB.MOM.WW.MxGateway.Server.Configuration;
|
||||
|
||||
namespace ZB.MOM.WW.MxGateway.Server.Alarms;
|
||||
|
||||
/// <summary>
|
||||
/// Resolves the subtag watch-list the gateway sends to the worker when the
|
||||
/// central alarm monitor operates in subtag-polling fallback mode. Merges
|
||||
/// Galaxy Repository alarm-attribute discovery with the configured
|
||||
/// include/exclude overrides and composes the per-attribute subtag item
|
||||
/// addresses from the configured subtag names.
|
||||
/// </summary>
|
||||
public interface IAlarmWatchListResolver
|
||||
{
|
||||
/// <summary>
|
||||
/// Builds the subtag watch-list for the supplied alarm configuration.
|
||||
/// </summary>
|
||||
/// <param name="options">Alarm configuration carrying discovery and subtag-name settings.</param>
|
||||
/// <param name="cancellationToken">Token to cancel the asynchronous operation.</param>
|
||||
/// <returns>
|
||||
/// The resolved <see cref="AlarmSubtagTarget"/> watch-list, possibly empty.
|
||||
/// Discovery being unavailable never throws; the caller decides what to do
|
||||
/// with an empty list.
|
||||
/// </returns>
|
||||
Task<IReadOnlyList<AlarmSubtagTarget>> ResolveAsync(
|
||||
AlarmsOptions options,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
Reference in New Issue
Block a user