Skip to main content

Health Checks

Vali-Blob.HealthChecks integrates with ASP.NET Core's health check framework, allowing you to verify storage provider availability as part of your application's readiness and liveness probes.


Installation

dotnet add package Vali-Blob.Core
dotnet add package Vali-Blob.HealthChecks

How the Check Works

The Vali-Blob health check calls ExistsAsync on a predefined canary path in your storage bucket (default: ".valiblob-health") on each probe. If the call completes without throwing, the check reports Healthy — even if ExistsAsync returns false (the canary file does not need to exist). What matters is that the provider is reachable and credentials are valid.

If the call throws or times out, the check reports the configured failureStatus.


Basic Setup

using Vali-Blob.HealthChecks;

// Register Vali-Blob providers
builder.Services
.AddVali-Blob(o => o.DefaultProvider = "aws")
.AddProvider<AWSS3Provider>("aws", opts =>
{
opts.BucketName = config["AWS:BucketName"]!;
opts.Region = config["AWS:Region"]!;
});

// Register health checks
builder.Services
.AddHealthChecks()
.AddVali-Blob(
name: "storage-aws",
providerName: "aws",
failureStatus: HealthStatus.Unhealthy,
tags: ["ready", "storage"],
timeout: TimeSpan.FromSeconds(5));

// Map health endpoint
app.MapHealthChecks("/health");

Multiple Providers

Register a check for each provider your application depends on:

builder.Services
.AddHealthChecks()
.AddVali-Blob(
name: "storage-aws-primary",
providerName: "aws-us",
failureStatus: HealthStatus.Unhealthy,
tags: ["ready", "storage"])
.AddVali-Blob(
name: "storage-aws-eu",
providerName: "aws-eu",
failureStatus: HealthStatus.Degraded, // secondary region — degraded, not unhealthy
tags: ["storage"])
.AddVali-Blob(
name: "storage-local-cache",
providerName: "local",
failureStatus: HealthStatus.Degraded,
tags: ["storage"]);

HealthStatus Behavior

StatusHTTP ResponseUse When
Unhealthy503 Service UnavailableProvider is critical — the application cannot function without it
Degraded200 OK (with warning)Provider is secondary or has a fallback path
Healthy200 OKOnly monitoring; never alert on failure

Health Check Endpoints

Basic Endpoint

app.MapHealthChecks("/health");
// Returns: 200 (Healthy/Degraded) or 503 (Unhealthy)

JSON Response

using Microsoft.AspNetCore.Diagnostics.HealthChecks;
using System.Text.Json;

app.MapHealthChecks("/health", new HealthCheckOptions
{
ResponseWriter = async (context, report) =>
{
context.Response.ContentType = "application/json";

var result = new
{
status = report.Status.ToString(),
totalDuration = report.TotalDuration.TotalMilliseconds,
checks = report.Entries.Select(e => new
{
name = e.Key,
status = e.Value.Status.ToString(),
description = e.Value.Description,
duration = e.Value.Duration.TotalMilliseconds,
error = e.Value.Exception?.Message,
tags = e.Value.Tags
})
};

await context.Response.WriteAsync(
JsonSerializer.Serialize(result,
new JsonSerializerOptions { WriteIndented = true }));
}
});

Example response when all checks pass:

{
"status": "Healthy",
"totalDuration": 78.3,
"checks": [
{
"name": "storage-aws-primary",
"status": "Healthy",
"description": "Vali-Blob provider 'aws-us' is reachable.",
"duration": 45.2,
"error": null,
"tags": ["ready", "storage"]
},
{
"name": "storage-aws-eu",
"status": "Healthy",
"description": "Vali-Blob provider 'aws-eu' is reachable.",
"duration": 33.1,
"error": null,
"tags": ["storage"]
}
]
}

Separate Liveness and Readiness Endpoints

Kubernetes and other orchestrators use separate liveness (is the process alive?) and readiness (is the app ready to serve traffic?) probes. Separate them by tag:

// Liveness: process is alive — no external checks needed
app.MapHealthChecks("/health/live", new HealthCheckOptions
{
Predicate = _ => false // exclude all registered checks
});

// Readiness: storage must be reachable to accept traffic
app.MapHealthChecks("/health/ready", new HealthCheckOptions
{
Predicate = check => check.Tags.Contains("ready")
});

// Full report: all checks (for monitoring dashboards)
app.MapHealthChecks("/health/all");

Kubernetes Probe Configuration

# kubernetes/deployment.yaml
apiVersion: apps/v1
kind: Deployment
spec:
template:
spec:
containers:
- name: my-app
image: my-app:latest
ports:
- containerPort: 8080
livenessProbe:
httpGet:
path: /health/live
port: 8080
initialDelaySeconds: 5
periodSeconds: 10
failureThreshold: 3
readinessProbe:
httpGet:
path: /health/ready
port: 8080
initialDelaySeconds: 10
periodSeconds: 15
failureThreshold: 2
successThreshold: 1

When the readiness probe fails (storage is unreachable), Kubernetes stops routing new traffic to the pod until the check recovers.


Custom Canary Path

Override the default canary path:

builder.Services
.AddHealthChecks()
.AddVali-Blob(
name: "storage-aws",
providerName: "aws",
options: new ValiBloBHealthCheckOptions
{
CanaryPath = ".health/canary"
});

Timeout Per Check

Add a per-check timeout to prevent health probes from hanging indefinitely:

builder.Services
.AddHealthChecks()
.AddVali-Blob(
name: "storage-aws",
providerName: "aws",
timeout: TimeSpan.FromSeconds(5)); // report Unhealthy if > 5s

Redis Session Store Check

When using the Redis session store for resumable uploads, register a separate Redis check:

builder.Services
.AddHealthChecks()
.AddVali-Blob(
name: "storage-aws",
providerName: "aws",
tags: ["ready", "storage"])
.AddRedis(
config["Redis:ConnectionString"]!,
name: "redis-session-store",
tags: ["ready", "session-store"]);

If Redis is down while S3 is healthy, resumable uploads will fail even though ExistsAsync on S3 succeeds. Separate checks help pinpoint which component is unhealthy.


Complete Health Check Setup

using Microsoft.AspNetCore.Diagnostics.HealthChecks;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Vali-Blob.HealthChecks;

// Providers
builder.Services
.AddVali-Blob(o => o.DefaultProvider = "aws")
.AddProvider<AWSS3Provider>("aws", opts =>
{
opts.BucketName = config["AWS:BucketName"]!;
opts.Region = config["AWS:Region"]!;
})
.AddRedisSessionStore(opts =>
{
opts.ConnectionString = config["Redis:ConnectionString"]!;
});

// Health checks
builder.Services
.AddHealthChecks()
.AddVali-Blob(
name: "storage",
providerName: "aws",
failureStatus: HealthStatus.Unhealthy,
tags: ["ready"],
timeout: TimeSpan.FromSeconds(5))
.AddRedis(
config["Redis:ConnectionString"]!,
name: "redis",
failureStatus: HealthStatus.Unhealthy,
tags: ["ready"],
timeout: TimeSpan.FromSeconds(3));

var app = builder.Build();

// Liveness (no external checks)
app.MapHealthChecks("/health/live", new HealthCheckOptions
{
Predicate = _ => false
});

// Readiness (storage + redis)
app.MapHealthChecks("/health/ready", new HealthCheckOptions
{
Predicate = check => check.Tags.Contains("ready"),
ResponseWriter = WriteJsonResponse
});

// Full report
app.MapHealthChecks("/health/all", new HealthCheckOptions
{
ResponseWriter = WriteJsonResponse
});

static Task WriteJsonResponse(HttpContext ctx, HealthReport report)
{
ctx.Response.ContentType = "application/json";
return ctx.Response.WriteAsync(JsonSerializer.Serialize(new
{
status = report.Status.ToString(),
checks = report.Entries.Select(e => new
{
name = e.Key,
status = e.Value.Status.ToString(),
duration = e.Value.Duration.TotalMilliseconds,
error = e.Value.Exception?.Message
})
}));
}

Grafana Alerting

Set up an alert when the health endpoint returns non-200 (via Blackbox Exporter):

# Prometheus alert rule
- alert: Vali-BlobStorageUnhealthy
expr: probe_success{job="valiblob-health"} == 0
for: 2m
labels:
severity: critical
annotations:
summary: "Vali-Blob storage health check failing"
description: "Storage provider has been unreachable for 2+ minutes. Uploads and downloads may be failing."