f40409a688
Tracking IDs are review bookkeeping that rots in shipped source; the rationale they tagged is kept. Comment-only change (CommentChecker 458 -> 16 false positives).
183 lines
8.2 KiB
C#
183 lines
8.2 KiB
C#
using ZB.MOM.WW.GalaxyRepository;
|
|
using ZB.MOM.WW.MxGateway.Contracts.Proto;
|
|
using ZB.MOM.WW.MxGateway.Server.Configuration;
|
|
|
|
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>
|
|
public sealed class AlarmWatchListResolver : IAlarmWatchListResolver
|
|
{
|
|
private const string ProviderLiteral = "Galaxy";
|
|
private const string DefaultActiveSubtag = "InAlarm";
|
|
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<AlarmWatchListResolution> ResolveAsync(
|
|
AlarmsOptions options,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(options);
|
|
|
|
AlarmDiscoveryOptions discovery = options.Fallback.Discovery;
|
|
|
|
// The caller cannot grade an empty watch-list without knowing whether discovery
|
|
// was asked to contribute and whether it failed while doing so, because the failure is
|
|
// deliberately swallowed below.
|
|
string? discoveryFailureMessage = null;
|
|
|
|
// Config fallback area used only for explicit IncludeAttributes entries (which
|
|
// carry no discovered area): discovery area, else the default area (may be empty).
|
|
string configFallbackArea = string.IsNullOrEmpty(discovery.Area) ? options.DefaultArea : discovery.Area;
|
|
|
|
// 1. Build the ordered, de-duplicated attribute reference set.
|
|
// Each entry carries the reference, the source-object reference, and the
|
|
// per-entry area used to compose the canonical reference. GR rows contribute
|
|
// the object's real Galaxy area; config includes contribute the config
|
|
// fallback area (Discovery.Area else DefaultArea).
|
|
List<(string Reference, string SourceObject, string Area)> ordered = [];
|
|
HashSet<string> seen = new(StringComparer.OrdinalIgnoreCase);
|
|
|
|
if (discovery.UseGalaxyRepository)
|
|
{
|
|
List<GalaxyAlarmAttributeRow> rows;
|
|
try
|
|
{
|
|
rows = await _repository.GetAlarmAttributesAsync(cancellationToken).ConfigureAwait(false);
|
|
}
|
|
catch (Exception ex) when (ex is not OperationCanceledException)
|
|
{
|
|
// 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. Cancellation is the one
|
|
// exception — an OperationCanceledException propagates per the
|
|
// IAlarmWatchListResolver contract so the caller unwinds promptly.
|
|
_logger.LogWarning(
|
|
ex,
|
|
"Galaxy Repository alarm-attribute discovery failed; continuing with configuration-only watch-list.");
|
|
discoveryFailureMessage = ex.Message;
|
|
rows = [];
|
|
}
|
|
|
|
foreach (GalaxyAlarmAttributeRow row in rows)
|
|
{
|
|
if (string.IsNullOrEmpty(row.FullTagReference) || !seen.Add(row.FullTagReference))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
ordered.Add((row.FullTagReference, row.SourceObjectReference, row.Area));
|
|
}
|
|
}
|
|
|
|
foreach (string include in discovery.IncludeAttributes)
|
|
{
|
|
if (string.IsNullOrEmpty(include) || !seen.Add(include))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
ordered.Add((include, DeriveSourceObject(include), configFallbackArea));
|
|
}
|
|
|
|
// Remove excluded references (case-insensitive), but only when GR discovery
|
|
// is active. ExcludeAttributes is documented as "Ignored when
|
|
// UseGalaxyRepository is false" (AlarmDiscoveryOptions.ExcludeAttributes).
|
|
// Whitespace-only entries are skipped, consistent with the include guard above.
|
|
if (discovery.UseGalaxyRepository)
|
|
{
|
|
HashSet<string> excluded = new(
|
|
discovery.ExcludeAttributes.Where(e => !string.IsNullOrWhiteSpace(e)),
|
|
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. Compose one target per reference, using the PER-ENTRY area: the GR row's
|
|
// real Galaxy area (matching the alarmmgr group), or the config fallback for
|
|
// explicit includes.
|
|
List<AlarmSubtagTarget> targets = new(ordered.Count);
|
|
foreach ((string reference, string sourceObject, string area) 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}",
|
|
});
|
|
}
|
|
|
|
// 4. Report the resolved count; warn when subtag mode was expected to cover
|
|
// something (GR enabled, or explicit includes were configured) but resolved
|
|
// to nothing. Only emit the Debug line when there is at least one target,
|
|
// to avoid a confusing "0 target(s)" noise line.
|
|
if (targets.Count == 0 && (discovery.UseGalaxyRepository || discovery.IncludeAttributes.Length > 0))
|
|
{
|
|
_logger.LogWarning(
|
|
"Alarm subtag watch-list resolved to zero targets; subtag-polling fallback will cover no alarms.");
|
|
}
|
|
else if (targets.Count > 0)
|
|
{
|
|
_logger.LogDebug("Resolved alarm subtag watch-list with {TargetCount} target(s).", targets.Count);
|
|
}
|
|
|
|
return new AlarmWatchListResolution(
|
|
targets,
|
|
DiscoveryAttempted: discovery.UseGalaxyRepository,
|
|
DiscoveryFailed: discoveryFailureMessage is not null,
|
|
discoveryFailureMessage);
|
|
}
|
|
|
|
/// <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}";
|
|
}
|