feat: add source/mirror info reporting for monitoring (Gap 4.10)

Add GetMirrorInfo/GetSourceInfo methods to MirrorCoordinator and
SourceCoordinator returning structured MirrorInfoResponse/SourceInfoResponse
records (Name, Lag, Active ms, FilterSubject, Error). Wire both into
StreamManager.GetMirrorInfo and GetSourceInfos for the monitoring API path.
This commit is contained in:
Joseph Doherty
2026-02-25 11:26:34 -05:00
parent 5e49006cfa
commit 0e1a39df96
3 changed files with 261 additions and 0 deletions

View File

@@ -297,6 +297,22 @@ public sealed class MirrorCoordinator : IAsyncDisposable
};
}
/// <summary>
/// Returns a structured monitoring response for this mirror.
/// Go reference: server/stream.go:2739-2743 (mirrorInfo building StreamSourceInfo)
/// </summary>
public MirrorInfoResponse GetMirrorInfo(string streamName)
{
var report = GetHealthReport();
return new MirrorInfoResponse
{
Name = streamName,
Lag = report.Lag,
Active = report.LastSyncUtc != default ? (long)(DateTime.UtcNow - report.LastSyncUtc).TotalMilliseconds : -1,
Error = ErrorMessage,
};
}
public async ValueTask DisposeAsync()
{
await StopAsync();
@@ -462,3 +478,25 @@ public sealed record MirrorHealthReport
public bool IsRunning { get; init; }
public bool IsStalled { get; init; }
}
/// <summary>
/// Structured monitoring response for a mirror, as returned by the JetStream monitoring API.
/// Go reference: server/stream.go:2739-2743 (mirrorInfo / StreamSourceInfo)
/// </summary>
public sealed record MirrorInfoResponse
{
/// <summary>Name of the stream being mirrored.</summary>
public string Name { get; init; } = string.Empty;
/// <summary>Number of messages behind the origin stream.</summary>
public ulong Lag { get; init; }
/// <summary>
/// Milliseconds since the last successful sync.
/// -1 when the mirror has never synced.
/// </summary>
public long Active { get; init; } = -1;
/// <summary>Current error message, or null if no error.</summary>
public string? Error { get; init; }
}

View File

@@ -295,6 +295,23 @@ public sealed class SourceCoordinator : IAsyncDisposable
};
}
/// <summary>
/// Returns a structured monitoring response for this source.
/// Go reference: server/stream.go:2687-2736 (sourcesInfo / StreamSourceInfo)
/// </summary>
public SourceInfoResponse GetSourceInfo()
{
var report = GetHealthReport();
return new SourceInfoResponse
{
Name = _sourceConfig.Name,
Lag = report.Lag,
Active = report.LastSyncUtc != default ? (long)(DateTime.UtcNow - report.LastSyncUtc).TotalMilliseconds : -1,
FilterSubject = _sourceConfig.FilterSubject,
Error = null, // SourceCoordinator doesn't have error state tracking yet
};
}
public async ValueTask DisposeAsync()
{
await StopAsync();
@@ -539,3 +556,28 @@ public sealed record SourceHealthReport
public long FilteredOutCount { get; init; }
public long DeduplicatedCount { get; init; }
}
/// <summary>
/// Structured monitoring response for a source, as returned by the JetStream monitoring API.
/// Go reference: server/stream.go:2687-2736 (sourcesInfo / StreamSourceInfo)
/// </summary>
public sealed record SourceInfoResponse
{
/// <summary>Name of the origin stream.</summary>
public string Name { get; init; } = string.Empty;
/// <summary>Number of messages behind the origin stream.</summary>
public ulong Lag { get; init; }
/// <summary>
/// Milliseconds since the last successful sync.
/// -1 when the source has never synced.
/// </summary>
public long Active { get; init; } = -1;
/// <summary>Optional subject filter applied by this source coordinator.</summary>
public string? FilterSubject { get; init; }
/// <summary>Current error message, or null if no error.</summary>
public string? Error { get; init; }
}