Health checks

AddAceMq registers one IHealthCheck, named acemq, tagged acemq and ready. There is nothing to add.

app.MapHealthChecks("/readyz", new HealthCheckOptions
{
    Predicate = r => r.Tags.Contains("ready"),
});

What it reports

status why
connected, nothing wrong Healthy
connected, broker has blocked the connection Healthy, with the reason see below
a registered contributor is degraded Degraded an ordered queue with a halted partition, say
a registered contributor is down Unhealthy
the connection is not open Unhealthy
not connected yet, or a connect that failed Unhealthy
shutting down Unhealthy, "draining"
acemq:enabled is false Healthy nothing was asked of it

The data dictionary carries transport, open, blocked, inFlight, held, consumers, a blockedReason when there is one, consuming and publishing when either is paused, and a check.{name} entry per contributor. Those are the facts worth having in an incident.

Every value is a string. open is "true", not true; inFlight is "0", not 0. Most of them come straight out of the library's own connection.Health() report, and that report is a IReadOnlyDictionary<string, string>; consumers is this package's own count and is rendered the same way so that a reader needs one rule rather than two. A rule written against a boolean or a number silently stops matching — see the changelog if you wrote one before AceMq.Amqp 0.7.0.

held is new in AceMq.Amqp 0.7.0 and is worth an eye during a shutdown: it counts deliveries the broker has already sent that are waiting at the pause gate, fetched and not handled. A finished drain means every handler finished, not that held was zero. See the lifecycle page.

A blocked connection is not a reason to restart

RabbitMQ sends connection.blocked when it is low on memory or disk, and stops reading from the connection until the pressure clears. An application that fails its own health check for that is an application an orchestrator restarts — into the same blocked broker, having thrown away whatever it was holding. A fleet doing that in unison is a queue that stops being drained at the moment it most needs draining.

So a blocked connection is Healthy, with the reason in the description and in the data. The Spring Boot starter reports it the same way, for the same reason.

HealthStatus.Degraded was the obvious alternative and was rejected. It maps to 200 in the framework's own defaults, so it would have been harmless there — but HealthCheckOptions.ResultStatusCodes is routinely changed to map Degraded to 503, and a choice whose safety depends on a setting in somebody else's file is not a choice.

If you want an alert on it, alert on blocked == "true" in the data, or on the broker's own metrics. Blocked is an operational fact about the broker, not a verdict on this instance.

The data key is the contract here, not the sentence. The Go, Python and Ruby libraries all write one identical sentence for a blocked connection so that a single alert rule reads a blocked broker whatever a service is written in; .NET's framework gives a structured dictionary alongside the description, so the dictionary is where the fact lives and blocked is the key to match. The description — the broker has blocked this connection: followed by the broker's reason — is this package's own wording and is meant for a human reading a dashboard.

Draining is unhealthy, and that is the opposite decision

Once shutdown begins the check reports Unhealthy with the description draining. That is the right answer to "should traffic still come here", which is the question a readiness probe is asking. An instance that has been told to stop should leave the rotation before it starts refusing work.

Do not wire this to a liveness probe

Liveness is "this process has stopped being a process" — deadlocked, out of file descriptors, a leak that ate the heap. Nothing about the broker belongs there. A process that cannot reach its broker is not a process that restarting fixes; restarting throws away every message it was holding and reconnects to the same unreachable broker.

// Liveness: this process is running. Nothing about the broker.
app.MapHealthChecks("/livez", new HealthCheckOptions { Predicate = _ => false });

// Readiness: this process can do the work.
app.MapHealthChecks("/readyz", new HealthCheckOptions
{
    Predicate = r => r.Tags.Contains("ready"),
});

Excluding it

The acemq tag exists for that:

Predicate = r => r.Tags.Contains("ready") && !r.Tags.Contains("acemq")

Contributors

Anything registered with the library — connection.RegisterHealth(contributor), or an OrderedQueue, which registers itself — is folded in, and the worst of them wins. Each appears as check.{name} in the data, with Up, Degraded or Down for a value.

IHealthContributor is two members, Name and Report(), and Report() is synchronous — which is the awkward part, because most things worth reporting on are not. Worked examples of contributors for the things this package does not know about: an outbox backlog and a stopped stream reader.

Note that AceMq.Amqp.HealthStatus is not Microsoft.Extensions.Diagnostics.HealthChecks.HealthStatus. A contributor returns the library's; inside a namespace under AceMq.Amqp the enclosing namespace beats a file-level using alias, so write it out or declare the alias inside the namespace.

The connection's own report is folded in with them, and its details are copied into the data as they are.

That is only true from AceMq.Amqp 0.7.0. Up to 0.6.0 the library reported a blocked connection as Degraded, and since the worst report wins, folding the connection's own report in would have let that one reading overrule the careful answer above with a plain one. So this package left the connection's report out of the fold and rebuilt the facts from IsOpen, IsBlocked and BlockedReason instead. 0.7.0 reports a blocked connection as Up, with blocked and blockedReason among its details, and the workaround went with the reason for it.