using System.Diagnostics.Metrics; using System.Linq; using FluentAssertions; using StellaOps.Findings.Ledger.Observability; using Xunit; using StellaOps.TestKit; namespace StellaOps.Findings.Ledger.Tests; public class LedgerMetricsTests { [Trait("Category", TestCategories.Unit)] [Fact] public void ProjectionLagGauge_RecordsLatestPerTenant() { using var listener = CreateListener(); var measurements = new List<(double Value, KeyValuePair[] Tags)>(); listener.SetMeasurementEventCallback((instrument, measurement, tags, state) => { if (instrument.Name == "ledger_projection_lag_seconds") { measurements.Add((measurement, tags.ToArray())); } }); LedgerMetrics.RecordProjectionLag(TimeSpan.FromSeconds(42), "tenant-a"); listener.RecordObservableInstruments(); var measurement = measurements.Should().ContainSingle().Subject; measurement.Value.Should().BeApproximately(42, precision: 0.001); measurement.Tags.ToDictionary(kvp => kvp.Key, kvp => kvp.Value) .Should().Contain(new KeyValuePair("tenant", "tenant-a")); } [Trait("Category", TestCategories.Unit)] [Fact] public void MerkleAnchorDuration_EmitsHistogramMeasurement() { using var listener = CreateListener(); var measurements = new List<(double Value, KeyValuePair[] Tags)>(); listener.SetMeasurementEventCallback((instrument, measurement, tags, state) => { if (instrument.Name == "ledger_merkle_anchor_duration_seconds") { measurements.Add((measurement, tags.ToArray())); } }); LedgerMetrics.RecordMerkleAnchorDuration(TimeSpan.FromSeconds(1.5), "tenant-b", 10); var measurement = measurements.Should().ContainSingle().Subject; measurement.Value.Should().BeApproximately(1.5, precision: 0.001); measurement.Tags.ToDictionary(kvp => kvp.Key, kvp => kvp.Value) .Should().Contain(new KeyValuePair("tenant", "tenant-b")); } [Trait("Category", TestCategories.Unit)] [Fact] public void MerkleAnchorFailure_IncrementsCounter() { using var listener = CreateListener(); var measurements = new List<(long Value, KeyValuePair[] Tags)>(); listener.SetMeasurementEventCallback((instrument, measurement, tags, state) => { if (instrument.Name == "ledger_merkle_anchor_failures_total") { measurements.Add((measurement, tags.ToArray())); } }); LedgerMetrics.RecordMerkleAnchorFailure("tenant-c", "persist_failure"); var measurement = measurements.Should().ContainSingle().Subject; measurement.Value.Should().Be(1); var tags = measurement.Tags.ToDictionary(kvp => kvp.Key, kvp => kvp.Value); tags.Should().Contain(new KeyValuePair("tenant", "tenant-c")); tags.Should().Contain(new KeyValuePair("reason", "persist_failure")); } [Trait("Category", TestCategories.Unit)] [Fact] public void AttachmentFailure_IncrementsCounter() { using var listener = CreateListener(); var measurements = new List<(long Value, KeyValuePair[] Tags)>(); listener.SetMeasurementEventCallback((instrument, measurement, tags, state) => { if (instrument.Name == "ledger_attachments_encryption_failures_total") { measurements.Add((measurement, tags.ToArray())); } }); LedgerMetrics.RecordAttachmentFailure("tenant-d", "encrypt"); var measurement = measurements.Should().ContainSingle().Subject; measurement.Value.Should().Be(1); var tags = measurement.Tags.ToDictionary(kvp => kvp.Key, kvp => kvp.Value); tags.Should().Contain(new KeyValuePair("tenant", "tenant-d")); tags.Should().Contain(new KeyValuePair("stage", "encrypt")); } [Trait("Category", TestCategories.Unit)] [Fact] public void BacklogGauge_ReflectsOutstandingQueue() { using var listener = CreateListener(); var measurements = new List<(long Value, KeyValuePair[] Tags)>(); // Reset LedgerMetrics.DecrementBacklog("tenant-q"); LedgerMetrics.IncrementBacklog("tenant-q"); LedgerMetrics.IncrementBacklog("tenant-q"); LedgerMetrics.DecrementBacklog("tenant-q"); listener.SetMeasurementEventCallback((instrument, measurement, tags, state) => { if (instrument.Name == "ledger_ingest_backlog_events") { measurements.Add((measurement, tags.ToArray())); } }); listener.RecordObservableInstruments(); var measurement = measurements.Should().ContainSingle().Subject; measurement.Value.Should().Be(1); measurement.Tags.ToDictionary(kvp => kvp.Key, kvp => kvp.Value) .Should().Contain(new KeyValuePair("tenant", "tenant-q")); } [Trait("Category", TestCategories.Unit)] [Fact] public void ProjectionRebuildHistogram_RecordsScenarioTags() { using var listener = CreateListener(); var measurements = new List<(double Value, KeyValuePair[] Tags)>(); listener.SetMeasurementEventCallback((instrument, measurement, tags, state) => { if (instrument.Name == "ledger_projection_rebuild_seconds") { measurements.Add((measurement, tags.ToArray())); } }); LedgerMetrics.RecordProjectionRebuild(TimeSpan.FromSeconds(3.2), "tenant-r", "replay"); var measurement = measurements.Should().ContainSingle().Subject; measurement.Value.Should().BeApproximately(3.2, 0.001); var tags = measurement.Tags.ToDictionary(kvp => kvp.Key, kvp => kvp.Value); tags.Should().Contain(new KeyValuePair("tenant", "tenant-r")); tags.Should().Contain(new KeyValuePair("scenario", "replay")); } [Trait("Category", TestCategories.Unit)] [Fact] public void DbConnectionsGauge_TracksRoleCounts() { using var listener = CreateListener(); var measurements = new List<(long Value, KeyValuePair[] Tags)>(); // Reset LedgerMetrics.DecrementDbConnection("writer"); LedgerMetrics.IncrementDbConnection("writer"); listener.SetMeasurementEventCallback((instrument, measurement, tags, state) => { if (instrument.Name == "ledger_db_connections_active") { measurements.Add((measurement, tags.ToArray())); } }); listener.RecordObservableInstruments(); var measurement = measurements.Should().ContainSingle().Subject; measurement.Value.Should().Be(1); measurement.Tags.ToDictionary(kvp => kvp.Key, kvp => kvp.Value) .Should().Contain(new KeyValuePair("role", "writer")); LedgerMetrics.DecrementDbConnection("writer"); } [Trait("Category", TestCategories.Unit)] [Fact] public void VersionInfoGauge_EmitsConstantOne() { using var listener = CreateListener(); var measurements = new List<(long Value, KeyValuePair[] Tags)>(); listener.SetMeasurementEventCallback((instrument, measurement, tags, state) => { if (instrument.Name == "ledger_app_version_info") { measurements.Add((measurement, tags.ToArray())); } }); listener.RecordObservableInstruments(); var measurement = measurements.Should().ContainSingle().Subject; measurement.Value.Should().Be(1); var tags = measurement.Tags.ToDictionary(kvp => kvp.Key, kvp => kvp.Value); tags.Should().ContainKey("version"); tags.Should().ContainKey("git_sha"); } private static MeterListener CreateListener() { var listener = new MeterListener { InstrumentPublished = (instrument, l) => { if (instrument.Meter.Name == "StellaOps.Findings.Ledger") { l.EnableMeasurementEvents(instrument); } } }; listener.Start(); return listener; } }