feat(overview): show MXAccess session counts on the gateway card
The gateway publishes its live MXAccess session counts, and the dashboard could not reach them. Four things stood between the data and the card, and only the last one is cosmetic: The active tier was probed only for instances declared HasActiveRole. A standalone gateway has no active/standby role, so its active tier was never requested at all. The tempting fix is to declare mxgw HasActiveRole, and it is wrong: that flag also paints an Active/Standby badge, so it would buy the data by asserting a role that does not exist. The probe is now unconditional for any reachable instance while ObserveActive stays role-gated, separating "what does this tier report" from "am I the active half". The active probe's parsed body was then discarded - only ready.Report was kept - so InstanceSnapshot gains ActiveReport. The two tiers stay separate rather than merged, because a check name is only unique within a tier. The card's one data-rendering path, ClusterDataLine, is located by the Akka leader/activeNode keys and would ignore session data even once present. The new row follows that helper's own precedent and locates the check by the data it carries - the total/ready/faulted triple - not by name, requiring all three so a future check publishing a lone "total" cannot be mistaken for it. Rendered on the card FACE, not inside the collapsed check list: a count behind a disclosure triangle is one nobody reads. Zero is shown rather than hidden, since a gateway holding no sessions is the interesting case and suppressing it would make the row appear only when the news is good. Cost, stated plainly: every reachable instance now makes one extra HTTP request per poll. The unreachable-instance skip is untouched, so the worst-case sweep duration - which is set by timeouts, not by healthy round-trips - is unchanged. Four cadence tests were counting raw requests as a proxy for "instances polled"; they now count ready-tier probes, which is what they always meant and is robust to tier changes. The Wonder registry comment claiming the active tier ran an empty check set was true when written and stopped being true when the gateway added the check; it now warns against the HasActiveRole shortcut instead. 183 tests pass (177 + 6), including a positive control on the absence assertion so "row not rendered" cannot pass vacuously.
This commit is contained in:
@@ -42,6 +42,16 @@
|
||||
<span class="v @ChecksClass">@ChecksText</span>
|
||||
</div>
|
||||
|
||||
@if (SessionText is { Length: > 0 } sessionText)
|
||||
{
|
||||
@* Only the gateway publishes these counts, so the row is absent everywhere else
|
||||
rather than showing an empty or zero value on apps the concept does not apply to. *@
|
||||
<div class="kv" data-testid="mxaccess-sessions">
|
||||
<span class="k">MXAccess</span>
|
||||
<span class="v @SessionClass">@sessionText</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (Instance.Error is { Length: > 0 } error)
|
||||
{
|
||||
<div class="kv">
|
||||
@@ -260,6 +270,66 @@
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The MXAccess session check's entry from the <b>active</b> tier, or null when this instance
|
||||
/// publishes none.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Located by the data it carries rather than by check name, for the same reason
|
||||
/// <see cref="ClusterDataLine"/> is: a name is a rename away from silently dropping the
|
||||
/// line, whereas the <c>total</c>/<c>ready</c>/<c>faulted</c> triple is the check's
|
||||
/// actual contract. All three are required so a future check that happens to publish a
|
||||
/// lone <c>total</c> cannot be mistaken for this one.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// It comes from <see cref="InstanceSnapshot.ActiveReport"/>, never the ready tier —
|
||||
/// the gateway registers this check on <c>active</c> because a gateway with no sessions
|
||||
/// is still ready to serve.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
private ZbHealthEntry? SessionEntry =>
|
||||
Instance.ActiveReport?.Entries.Values.FirstOrDefault(e =>
|
||||
e.DataInt("total") is not null
|
||||
&& e.DataInt("ready") is not null
|
||||
&& e.DataInt("faulted") is not null);
|
||||
|
||||
/// <summary>The session counts as a short face-level line, or null when there are none to show.</summary>
|
||||
/// <remarks>
|
||||
/// Rendered on the card face rather than inside the collapsed check list: "how many MXAccess
|
||||
/// connections are up" is a number an operator scans for, and a value behind a disclosure
|
||||
/// triangle is one nobody sees. Zero is shown, not hidden — a gateway holding no sessions at
|
||||
/// all is precisely the interesting case, and suppressing it would make the row appear only
|
||||
/// when the news is good.
|
||||
/// </remarks>
|
||||
private string? SessionText
|
||||
{
|
||||
get
|
||||
{
|
||||
var entry = SessionEntry;
|
||||
if (entry is null)
|
||||
return null;
|
||||
|
||||
int total = entry.DataInt("total") ?? 0;
|
||||
if (total == 0)
|
||||
return "0";
|
||||
|
||||
var parts = new List<string> { total.ToString(System.Globalization.CultureInfo.InvariantCulture) };
|
||||
parts.Add($"{entry.DataInt("ready") ?? 0} ready");
|
||||
|
||||
foreach (var (key, label) in new[] { ("faulted", "faulted"), ("starting", "starting") })
|
||||
{
|
||||
if (entry.DataInt(key) is > 0 and var n)
|
||||
parts.Add($"{n} {label}");
|
||||
}
|
||||
|
||||
return string.Join(" · ", parts);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Highlights the session row only when a session is actually broken.</summary>
|
||||
private string? SessionClass => SessionEntry?.DataInt("faulted") is > 0 ? "bad" : null;
|
||||
|
||||
private static string DotClass(string? status) => status switch
|
||||
{
|
||||
"Healthy" => "ok",
|
||||
|
||||
@@ -158,8 +158,16 @@ public sealed class OverviewPollerService : BackgroundService
|
||||
// timeout to learn what we already know, and ObserveActive maps a missing probe to Unknown
|
||||
// anyway. A 503 from ready is NOT a skip — that instance is talking, and whether it is the
|
||||
// active half is exactly what an operator wants to know about a sick node.
|
||||
//
|
||||
// Deliberately NOT gated on HasActiveRole. The active tier answers two different questions
|
||||
// and they have to be separated: (1) am I the active half of a pair, and (2) what does this
|
||||
// tier report about me. A standalone gateway has no answer to (1) but can have plenty to say
|
||||
// about (2) — MxAccessGateway publishes its live MXAccess session counts there. Gating the
|
||||
// probe on the role made that data unreachable, and the tempting fix (declare the gateway
|
||||
// HasActiveRole) is wrong: it would paint an Active/Standby badge asserting a role that does
|
||||
// not exist. So probe unconditionally and let ObserveActive stay role-gated below.
|
||||
HealthProbeResult? active = null;
|
||||
if (target.Instance.HasActiveRole && ready.Reachable)
|
||||
if (ready.Reachable)
|
||||
active = await probe.ProbeStatusAsync(target.ActiveUrl, timeout, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var observed = StatusDerivation.Observe(ready);
|
||||
@@ -178,6 +186,7 @@ public sealed class OverviewPollerService : BackgroundService
|
||||
Latency = ready.Latency,
|
||||
Error = ready.Error,
|
||||
Report = ready.Report,
|
||||
ActiveReport = active?.Report,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -67,6 +67,17 @@ public sealed record InstanceSnapshot
|
||||
/// <summary>The last parsed ready-tier body, which drives the per-check detail list.</summary>
|
||||
public ZbHealthReport? Report { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The last parsed <b>active</b>-tier body, when that tier answered with one.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Kept separate from <see cref="Report"/> rather than merged into it: the two tiers answer
|
||||
/// different questions, and a check name is only unique within a tier. It is null whenever the
|
||||
/// active tier was not probed, did not answer, or answered without a canonical body — every one
|
||||
/// of which is normal, so nothing driven by it may be required for the card to render.
|
||||
/// </remarks>
|
||||
public ZbHealthReport? ActiveReport { get; init; }
|
||||
|
||||
/// <summary>This instance's effective staleness window.</summary>
|
||||
public TimeSpan StaleAfter { get; init; }
|
||||
|
||||
|
||||
@@ -118,9 +118,17 @@
|
||||
"ManagementLabel": "Dashboard",
|
||||
"Instances": [
|
||||
{
|
||||
// HasActiveRole false: mxgw is a standalone gateway with no cluster. Its
|
||||
// /health/active does answer 200, but only because the active tier runs an EMPTY
|
||||
// check set — an Active badge here would assert a role that does not exist.
|
||||
// HasActiveRole false: mxgw is a standalone gateway with no cluster, so an Active
|
||||
// badge here would assert a role that does not exist.
|
||||
//
|
||||
// This flag no longer decides whether the active tier is PROBED — since 2026-08-12 it
|
||||
// always is, for any reachable instance. That tier is where mxgw publishes its live
|
||||
// MXAccess session counts (the "MXAccess" row on the card), and gating the probe on the
|
||||
// role made those counts unreachable. Do NOT flip this to true to get them back: the
|
||||
// probe and the badge are deliberately separate, and true would paint a false badge.
|
||||
//
|
||||
// (The previous note here said the active tier ran an EMPTY check set. That was true
|
||||
// when written and stopped being true when the gateway added its session check.)
|
||||
"Name": "wonder",
|
||||
"BaseUrl": "http://127.0.0.1:5130",
|
||||
"ManagementUrl": "http://10.220.157.247:5130/",
|
||||
|
||||
Reference in New Issue
Block a user