fix(scripting): name the script on the mirrored quarantine line; keep BadInternalError (lmxopcua#784)
v2-ci / unit-tests (push) Has been cancelled

(a) ScriptRunGuard logs its quarantine Error through the seam's script root
logger, which carries no ScriptName, so ScriptLogCompanionSink mirrored it to
the main log as "[Script] unknown emitted Error ..." and rate-limited every
quarantine on the node under one shared "unknown" key. The guard now binds the
script instance key as ScriptName (ForContext) on the quarantine Error and the
subscriber-threw Warning, so the mirror names the script and limits each one
separately. Test drives two real quarantines through the companion sink with a
1-per-window limiter: both mirrored, each named, none "unknown". Mutation-
checked: dropping the ForContext fails it.

(b) Evaluated a quarantine-specific OPC UA status (BadOutOfService) and kept
BadInternalError: it is the documented code for any script-attributable fault
on virtual tags, Calculation tags and scripted-alarm quality (and the
out-of-process runner design keeps it), BadOutOfService reads as a deliberately
disabled point, a new code is a ScadaBridge-visible contract change, and the
once-per-Good->Bad dedup means a timeout-driven quarantine would rarely show a
different code anyway. Documented in docs/VirtualTags.md ("Quarantine: what a
client sees"), with pointers from Calculation.md, ScriptedAlarms.md and the
OpcUaStatus.BadInternalError doc comment, plus where a quarantine IS visible.

Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
This commit is contained in:
Joseph Doherty
2026-11-09 21:20:39 -05:00
parent 80118a01c5
commit 850c5405d0
7 changed files with 90 additions and 3 deletions
@@ -358,11 +358,17 @@ public sealed class ScriptRunGuard
QuarantinedCounter.Add(1,
new KeyValuePair<string, object?>("script", key),
new KeyValuePair<string, object?>("reason", metricReason));
_logger.Error("Script {ScriptKey} quarantined: {Reason}", key, why);
// lmxopcua#784: bind the script's identity onto the event. The guard's logger is the seam's script ROOT
// logger, which carries no ScriptName, so ScriptLogCompanionSink mirrored this line into the main log as
// "[Script] unknown emitted Error ..." and rate-limited every quarantine on the node under one shared
// "unknown" key. The instance key (virtual-tag id, calc-tag RawPath, alarm id) is the identity the guard
// has, so the mirror now names the script and rate-limits each one separately.
var scoped = _logger.ForContext(ScriptLoggerFactory.ScriptNameProperty, key);
scoped.Error("Script {ScriptKey} quarantined: {Reason}", key, why);
try { Quarantined?.Invoke(this, new ScriptQuarantinedEventArgs(key, reason)); }
catch (Exception ex)
{
_logger.Warning(ex, "ScriptRunGuard Quarantined subscriber threw for {ScriptKey}", key);
scoped.Warning(ex, "ScriptRunGuard Quarantined subscriber threw for {ScriptKey}", key);
}
}