Files
lmxopcua/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Historian/ContinuousHistorizationMetricsTests.cs
T
Joseph Doherty 96ec751a0b fix(historian): latch the recorder's dropped-kind Debug log per run; pin the outbox.unreadable gauge
The DroppedUnsupportedKind counter still counts every value; only the Debug
line is latched (cleared by the next captured value), so a Gateway-backend
comms loss no longer logs one line per tag.

Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
2026-10-09 14:23:42 -04:00

59 lines
2.6 KiB
C#

using System.Diagnostics.Metrics;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Commons.Observability;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
using ZB.MOM.WW.OtOpcUa.Runtime.Historian;
namespace ZB.MOM.WW.OtOpcUa.Runtime.Tests.Historian;
/// <summary>
/// The <c>otopcua.historian.outbox.unreadable</c> gauge reports the bound outbox's
/// <see cref="IHistorizationOutbox.UnreadableCount"/>. The listener is filtered by the instrument INSTANCE (the
/// shared <see cref="OtOpcUaTelemetry.Meter"/> object plus the instrument name), never by meter name alone, so a
/// parallel test's measurements on another meter of the same name cannot reach it; and the bound outbox reports a
/// distinctive count, so the asserted value can only have come from this test's outbox.
/// </summary>
public sealed class ContinuousHistorizationMetricsTests
{
private const string GaugeName = "otopcua.historian.outbox.unreadable";
[Fact]
public void The_unreadable_gauge_reports_the_bound_outboxs_unreadable_count()
{
const long distinctive = 7_331_919L;
ContinuousHistorizationMetrics.BindOutbox(new UnreadableOutbox(distinctive));
var observed = new List<long>();
using var listener = new MeterListener
{
InstrumentPublished = (instrument, l) =>
{
if (ReferenceEquals(instrument.Meter, OtOpcUaTelemetry.Meter) && instrument.Name == GaugeName)
l.EnableMeasurementEvents(instrument);
},
};
listener.SetMeasurementEventCallback<long>((instrument, value, _, _) =>
{
if (instrument.Name == GaugeName) observed.Add(value);
});
listener.Start();
listener.RecordObservableInstruments();
observed.ShouldBe([distinctive]);
}
private sealed class UnreadableOutbox(long unreadable) : IHistorizationOutbox
{
public long DroppedCount => 0;
public long UnreadableCount => unreadable;
public ValueTask AppendAsync(HistorizationOutboxEntry entry, CancellationToken ct) => ValueTask.CompletedTask;
public ValueTask AppendBatchAsync(IReadOnlyList<HistorizationOutboxEntry> entries, CancellationToken ct) => ValueTask.CompletedTask;
public ValueTask<IReadOnlyList<HistorizationOutboxEntry>> PeekBatchAsync(int max, CancellationToken ct) =>
ValueTask.FromResult<IReadOnlyList<HistorizationOutboxEntry>>([]);
public ValueTask RemoveAsync(Guid id, CancellationToken ct) => ValueTask.CompletedTask;
public ValueTask<int> CountAsync(CancellationToken ct) => ValueTask.FromResult(0);
public void Dispose() { }
}
}