using System.Diagnostics.Metrics; namespace ZB.MOM.WW.LocalDb.Replication; /// /// The replication engine's OpenTelemetry instruments, under the single /// ZB.MOM.WW.LocalDb.Replication meter (family METRIC-CONVENTIONS §1: meters are named /// after their root namespace). Optional throughout: the sessions and adapters hold a nullable /// reference and every call is null-guarded, so replication runs identically whether or not this /// is registered. Instrument names follow the family metric conventions (lower-case, /// dot-separated, seconds for durations). /// public sealed class LocalDbMetrics : IDisposable { /// The single meter name every replication instrument is published under. public const string MeterName = "ZB.MOM.WW.LocalDb.Replication"; private readonly Meter _meter; private readonly Counter _applied; private readonly Counter _conflictsDiscarded; private readonly Counter _deadLettered; private readonly Counter _reconnects; private readonly Counter _snapshots; private readonly Func _utcNow; private bool _disposed; /// Creates the meter and its instruments. is a test seam for the lag gauge's clock; null uses . public LocalDbMetrics(Func? utcNow = null) { _utcNow = utcNow ?? (() => DateTimeOffset.UtcNow); _meter = new Meter(MeterName, typeof(LocalDbMetrics).Assembly.GetName().Version?.ToString(3)); _applied = _meter.CreateCounter("localdb.sync.applied"); _conflictsDiscarded = _meter.CreateCounter("localdb.sync.conflicts_discarded"); _deadLettered = _meter.CreateCounter("localdb.sync.dead_lettered"); _reconnects = _meter.CreateCounter("localdb.sync.reconnects"); _snapshots = _meter.CreateCounter("localdb.sync.snapshots"); _meter.CreateObservableGauge("localdb.oplog.depth", ObserveOplogDepth, "1"); _meter.CreateObservableGauge("localdb.sync.lag.seconds", ObserveSyncLag, "s"); } /// /// Supplies the current unacked oplog backlog for the localdb.oplog.depth gauge. Invoked /// only at collection time; a cheap synchronous SELECT is acceptable at that frequency. /// public Func? OplogDepthProvider { get; internal set; } /// Supplies the last successful sync time for the localdb.sync.lag.seconds gauge; null means no sync yet, and the gauge then reports no measurement. public Func? LastSyncUtcProvider { get; internal set; } /// The backing meter — exposed so a test can filter by instance reference rather than the process-shared meter name. internal Meter Meter => _meter; /// Adds rows applied from inbound delta batches to localdb.sync.applied (no-op when not positive). public void RecordApplied(long count) { if (count > 0) _applied.Add(count); } /// Adds inbound rows that lost LWW to localdb.sync.conflicts_discarded (no-op when not positive). public void RecordConflictsDiscarded(long count) { if (count > 0) _conflictsDiscarded.Add(count); } /// Adds poison / over-drift rows routed to the dead-letter table to localdb.sync.dead_lettered (no-op when not positive). public void RecordDeadLettered(long count) { if (count > 0) _deadLettered.Add(count); } /// Increments localdb.sync.reconnects for one initiator reconnect attempt. public void RecordReconnect() => _reconnects.Add(1); /// Increments localdb.sync.snapshots with direction=sent for one completed outbound snapshot. public void RecordSnapshotSent() => _snapshots.Add(1, new KeyValuePair("direction", "sent")); /// Increments localdb.sync.snapshots with direction=received for one completed inbound snapshot. public void RecordSnapshotReceived() => _snapshots.Add(1, new KeyValuePair("direction", "received")); private IEnumerable> ObserveOplogDepth() { var provider = OplogDepthProvider; if (provider is null) return []; try { return [new Measurement(provider())]; } catch { // The observation SELECT can race a disposed db / closing connection; a scrape must // never throw, so drop this sample rather than surface a partial value. return []; } } private IEnumerable> ObserveSyncLag() { var last = LastSyncUtcProvider?.Invoke(); if (last is null) return []; var lag = (_utcNow() - last.Value).TotalSeconds; if (lag < 0) lag = 0; return [new Measurement(lag)]; } /// Disposes the backing meter; idempotent. public void Dispose() { if (_disposed) return; _meter.Dispose(); _disposed = true; } }