feat(telemetry): deploy-failure, S&F activity, script-run and DCL-transition instruments (ScadaBridge#81)

Adds four instruments on the already-allow-listed ZB.MOM.WW.ScadaBridge meter:
scadabridge.deployments.failed{reason}, scadabridge.store_and_forward.activity{action,category},
scadabridge.site.script.runs{outcome} + scadabridge.site.script.duration{outcome} (ms),
scadabridge.site.data_connections.transitions{state}. Queue depth, DCL connection state and
deploy-applied already existed. No new Meter, so ZbTelemetryOptions.Meters needs no change.

Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
This commit is contained in:
Joseph Doherty
2026-11-09 14:17:53 -05:00
parent 7892c622cf
commit 5ef27fd9a3
8 changed files with 301 additions and 0 deletions
@@ -97,6 +97,43 @@ public class QueueDepthGaugeTests : IAsyncLifetime, IDisposable
return observed;
}
/// <summary>
/// ScadaBridge#81: the service emits <c>scadabridge.store_and_forward.activity</c> for a buffered
/// message (Queued) and for a message that exhausts its retries (Parked), through the real service.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task ActivityCounter_IsEmitted_ForQueuedAndParked()
{
var seen = new System.Collections.Concurrent.ConcurrentQueue<string>();
using var listener = new MeterListener
{
InstrumentPublished = (i, l) =>
{
if (i.Meter.Name == ScadaBridgeTelemetry.MeterName
&& i.Name == "scadabridge.store_and_forward.activity")
l.EnableMeasurementEvents(i);
}
};
listener.SetMeasurementEventCallback<long>((_, _, tags, _) =>
{
foreach (var t in tags)
if (t.Key == "action" && t.Value is string a) seen.Enqueue(a);
});
listener.Start();
_service.RegisterDeliveryHandler(StoreAndForwardCategory.ExternalSystem,
_ => throw new HttpRequestException("transient"));
var r = await _service.EnqueueAsync(
StoreAndForwardCategory.ExternalSystem, "api-activity", """{}""", maxRetries: 1);
Assert.True(r.WasBuffered);
await _service.RetryPendingMessagesAsync();
Assert.Contains("Queued", seen);
Assert.Contains("Parked", seen);
}
/// <summary>
/// Review 2026-10-21 02/C3: rows that arrive without passing through this node's counter
/// paths (LocalDb CDC on a standby) are picked up by the sweep's periodic re-seed.