# Health Checks and OpenTelemetry (/integrations/aspire/how-to/health-checks-observability)



When you call `AddKubeMQClient` (or `AddKeyedKubeMQClient`), the client package wires observability into your service for you. It registers two health checks — readiness and liveness — and enrolls the KubeMQ SDK's OpenTelemetry tracing source and meter into the Aspire telemetry pipeline. All of this happens automatically, and each piece can be turned off independently through `KubeMQClientSettings`.

The registration lives in the `ConfigureObservability` step of `KubeMQClientExtensions`: it only registers health checks when `DisableHealthChecks` is false, and only adds the tracing source or meter when `DisableTracing` / `DisableMetrics` are false.

## Prerequisites [#prerequisites]

* `builder.AddKubeMQClient(...)` (or `AddKeyedKubeMQClient`) already registered in the service's `Program.cs` (see [Getting Started](/integrations/aspire/tutorials/getting-started))
* The Aspire `ServiceDefaults` project's `MapDefaultEndpoints()` wired up, if you want the checks exposed over `/health` and `/alive`

## Readiness Check (`ready` tag) [#readiness-check-ready-tag]

The readiness check answers "can this service currently reach KubeMQ and handle traffic?" It first inspects `IKubeMQClient.State`, and only when the client is `Ready` does it make a network call — `PingAsync` — to confirm the broker is actually reachable.

```csharp title="KubeMQReadinessHealthCheck.cs"
var state = _client.State;

switch (state)
{
    case ConnectionState.Ready:
        try
        {
            using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
            cts.CancelAfter(_timeout);

            await _client.PingAsync(cts.Token).ConfigureAwait(false);

            return HealthCheckResult.Healthy("KubeMQ connection is healthy");
        }
        catch (OperationCanceledException)
        {
            return HealthCheckResult.Unhealthy("KubeMQ health check timed out");
        }
        catch (Exception)
        {
            return HealthCheckResult.Unhealthy(
                "KubeMQ health check failed: unable to reach server");
        }

    case ConnectionState.Reconnecting:
        return HealthCheckResult.Degraded("KubeMQ client is reconnecting");

    case ConnectionState.Connecting:
        return HealthCheckResult.Unhealthy("KubeMQ client is connecting");

    case ConnectionState.Idle:
        return HealthCheckResult.Unhealthy("KubeMQ client is not connected");

    case ConnectionState.Closed:
        return HealthCheckResult.Unhealthy("KubeMQ client has been disposed");
}
```

The `PingAsync` call is bounded by `HealthCheckTimeout`: the check creates a linked cancellation token that cancels after the configured timeout, so a hung broker produces an `Unhealthy` result ("KubeMQ health check timed out") rather than blocking the probe. The default timeout is 5 seconds.

<Callout type="info">
  The ping fires **only** in the `Ready` state. In every other state the readiness check returns a status from the connection state alone, without a network round trip — so a disconnected client fails fast instead of waiting for a ping to time out.
</Callout>

## Liveness Check (`live` tag) [#liveness-check-live-tag]

The liveness check answers a different question: "is this process still in a state worth keeping alive?" It is state-only — it never touches the network — and is therefore cheap enough to call frequently.

```csharp title="KubeMQLivenessHealthCheck.cs"
var state = _client.State;
var result = state switch
{
    ConnectionState.Ready => HealthCheckResult.Healthy("KubeMQ client is ready"),
    ConnectionState.Reconnecting => HealthCheckResult.Degraded("KubeMQ client is reconnecting"),
    _ => HealthCheckResult.Unhealthy($"KubeMQ client state: {state}"),
};
return Task.FromResult(result);
```

A failed liveness check signals that the process should be restarted — for example by a Kubernetes liveness probe. `Ready` maps to `Healthy`, `Reconnecting` maps to `Degraded` (the SDK is still trying to recover on its own, so a restart would be premature), and any other state — `Connecting`, `Idle`, or `Closed` — maps to `Unhealthy`.

## Health Check Naming [#health-check-naming]

For a connection named `messaging`, the client registers two checks: `kubemq-messaging-ready` (tagged `ready`) and `kubemq-messaging-live` (tagged `live`). The general form is `kubemq-{connectionName}-ready` and `kubemq-{connectionName}-live`, so multiple keyed clients each get their own distinctly named pair.

```csharp title="KubeMQClientExtensions.cs"
var healthCheckName = $"kubemq-{name}";
var readyName = $"{healthCheckName}-ready";
var liveName = $"{healthCheckName}-live";

builder.Services.AddHealthChecks()
    .Add(new HealthCheckRegistration(
        readyName,
        sp => sp.GetRequiredKeyedService<IHealthCheck>(readyName),
        failureStatus: null,
        tags: ["ready"]))
    .Add(new HealthCheckRegistration(
        liveName,
        sp => sp.GetRequiredKeyedService<IHealthCheck>(liveName),
        failureStatus: null,
        tags: ["live"]));
```

Both checks map the SDK connection state to a health status as follows:

| Connection State | Health Status                                   |
| ---------------- | ----------------------------------------------- |
| Ready            | Healthy (readiness includes a live `PingAsync`) |
| Reconnecting     | Degraded                                        |
| Connecting       | Unhealthy                                       |
| Idle             | Unhealthy                                       |
| Closed           | Unhealthy                                       |

## Mapping Checks to Endpoints [#mapping-checks-to-endpoints]

The health checks are registered in the DI container, but they are not exposed over HTTP until you map them to endpoints. In the standard Aspire `ServiceDefaults` project, `MapDefaultEndpoints` filters checks by tag: the `/health` endpoint serves the readiness set, and `/alive` serves the liveness set.

```csharp title="ServiceDefaults/Extensions.cs"
public static WebApplication MapDefaultEndpoints(this WebApplication app)
{
    if (app.Environment.IsDevelopment())
    {
        app.MapHealthChecks("/health", new HealthCheckOptions
        {
            Predicate = check => check.Tags.Contains("ready")
        });
        app.MapHealthChecks("/alive", new HealthCheckOptions
        {
            Predicate = check => check.Tags.Contains("live")
        });
    }
    return app;
}
```

Because the predicate filters on the `ready` and `live` tags, the KubeMQ checks are picked up automatically alongside any other tagged checks in your app (for example the default `self` check, which `ServiceDefaults` tags `live`). Point a Kubernetes readiness probe at `/health` and a liveness probe at `/alive`.

<Callout type="info">
  The sample maps these endpoints only when `app.Environment.IsDevelopment()`. In production you typically expose them unconditionally (and secure them) — adjust the guard to match your deployment.
</Callout>

## Disabling Health Checks [#disabling-health-checks]

Set `DisableHealthChecks` to `true` to skip registering both checks. When health checks are enabled, `HealthCheckTimeout` must be a positive value; otherwise `BindAndResolveSettings` throws an `ArgumentOutOfRangeException` before the client is built.

```csharp title="KubeMQClientExtensions.cs"
if (!settings.DisableHealthChecks && settings.HealthCheckTimeout <= TimeSpan.Zero)
{
    throw new ArgumentOutOfRangeException(
        nameof(settings.HealthCheckTimeout),
        settings.HealthCheckTimeout,
        "Health check timeout must be a positive value.");
}
```

`HealthCheckTimeout` defaults to 5 seconds, so you only need to set it explicitly when you want a different bound on the readiness ping:

```csharp title="KubeMQClientSettings.cs"
/// <summary>Gets or sets whether to disable health check registration. Default: false.</summary>
public bool DisableHealthChecks { get; set; }
/// <summary>Gets or sets the health check timeout. Default: 5 seconds.</summary>
public TimeSpan HealthCheckTimeout { get; set; } = TimeSpan.FromSeconds(5);
```

## OpenTelemetry Tracing [#opentelemetry-tracing]

Tracing is enabled by default. The client registers the SDK's activity source — `KubeMQ.Sdk` — with the OpenTelemetry tracer provider, so spans produced by KubeMQ operations flow into the same trace pipeline configured by your `ServiceDefaults`.

```csharp title="KubeMQClientExtensions.cs"
if (!settings.DisableTracing)
{
    otel.WithTracing(t => t.AddSource("KubeMQ.Sdk"));
}
```

Set `DisableTracing` to `true` to opt out of registering the source.

## OpenTelemetry Metrics [#opentelemetry-metrics]

Metrics work the same way: by default the client registers the SDK meter — `KubeMQ.Sdk` — with the OpenTelemetry meter provider.

```csharp title="KubeMQClientExtensions.cs"
if (!settings.DisableMetrics)
{
    otel.WithMetrics(m => m.AddMeter("KubeMQ.Sdk"));
}
```

Set `DisableMetrics` to `true` to opt out. Both the source and the meter use the same name, `KubeMQ.Sdk`, matching the instrumentation built into the KubeMQ .NET SDK.

## Configuration via appsettings.json [#configuration-via-appsettingsjson]

All four flags bind from the `Aspire:KubeMQ:Client` configuration section, so you can control observability without changing code:

```json title="appsettings.json"
{
  "Aspire": {
    "KubeMQ": {
      "Client": {
        "DisableHealthChecks": false,
        "DisableTracing": false,
        "DisableMetrics": false,
        "HealthCheckTimeout": "00:00:05"
      }
    }
  }
}
```

`HealthCheckTimeout` is a duration string in `hh:mm:ss` form — `00:00:05` is five seconds. For a keyed client, nest the same keys under the keyed section (for example `Aspire:KubeMQ:Client:orders`).

<Callout type="info">
  These flags are independent. You can, for example, keep tracing on while turning off metrics — set `DisableMetrics` to `true` and leave `DisableTracing` and `DisableHealthChecks` at their defaults. Disabling both `DisableTracing` and `DisableMetrics` skips the OpenTelemetry registration entirely.
</Callout>

## Try It Locally [#try-it-locally]

To exercise the health checks against a real broker, run KubeMQ in Docker. Port `50000` is the gRPC port the client connects to — it is always on, so the native gRPC client needs no connector enable flag.

<RunKubeMQ ports="[50000]" />

With the broker up and your service running, the readiness endpoint reports `Healthy` once the client reaches the `Ready` state:

```bash
curl http://localhost:5000/health   # readiness (ready tag)
curl http://localhost:5000/alive    # liveness  (live tag)
```

Replace `5000` with your service's actual HTTP port. Stop the broker container to watch the readiness check transition to `Unhealthy` while the liveness check follows the SDK's `Reconnecting` (Degraded) state.

## Related [#related]

* [Getting Started](/integrations/aspire/tutorials/getting-started) to provision a broker and register the client
* [Keyed multi-instance clients](/integrations/aspire/how-to/keyed-multi-instance) for per-broker health checks and observability
* [.NET Aspire overview](/integrations/aspire) for the two-package model and architecture
