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;
}
}