SLA Health Check
ข้อมูลควบคุมเอกสาร (Document Control)
| ฟิลด์ | ค่า |
|---|---|
| ชื่อเอกสาร | SLA Health Check Guideline |
| Version | 1.0 |
| สถานะ | Approved |
| วันที่ออกเอกสาร | 2026-05-05 |
| ผู้จัดทำ (Author) | SLA Team |
| ผู้อนุมัติ (Approver) | DIIS Development Guideline Team |
| มาตรฐานอ้างอิง | ISO/IEC 29110 |
ประวัติการแก้ไข (Change History)
| Version | Date | Author | Description |
|---|---|---|---|
| 1.0 | 2026-05-05 | SLA Team | Initial release |
แนวทางการสร้าง endpoint สำหรับตรวจสอบสถานะ (health) ของ service ทุกตัวที่ทีมพัฒนา เพื่อให้ระบบ monitoring, load balancer และ Kubernetes probe สามารถตรวจสอบความพร้อมและการทำงานของ service ได้ตามมาตรฐานเดียวกัน
Objective
ทุก service ที่พัฒนาภายใต้ DIIS Guideline ต้องเปิด SLA health check endpoint จำนวน 2 ตัว ดังนี้
| Endpoint | วัตถุประสงค์ | ใครเรียก |
|---|---|---|
GET /sla/health | ตรวจสอบทุก dependency แบบละเอียด | Monitoring system, readiness probe |
GET /sla/health/live | ตรวจสอบเฉพาะว่า process ของ service ยังทำงานอยู่ | Liveness probe ของ Kubernetes / orchestrator |
แนวทางนี้ใช้ library มาตรฐานของ ASP.NET Core (Microsoft.Extensions.Diagnostics.HealthChecks) ซึ่งรองรับทั้งการเขียน custom health check และการรวมผลลัพธ์ (aggregate) เป็น JSON ตาม format ที่กำหนด
Endpoints
GET /sla/health (Readiness)
ตรวจสอบทุก dependency ที่ service พึ่งพา ได้แก่ Relational Database, Graph Database, NoSQL และ Authentication Service หาก dependency ตัวใดตัวหนึ่งล้มเหลว endpoint จะตอบกลับด้วย HTTP 503 Service Unavailable พร้อมรายละเอียดสถานะของแต่ละ dependency
ตัวอย่าง response:
{
"status": "Healthy",
"totalDuration": "00:00:00.0029013",
"entries": {
"database": {
"data": {
"database_01": "Healthy",
"database_02": "Healthy",
"database_03": "Healthy",
"database_04": "Healthy",
"database_05": "Healthy",
"total_databases": 5,
"healthy_databases": 5,
"unhealthy_databases": 0,
"healthy_database_list": "01, 02, 03, 04, 05"
},
"description": "All 5 database(s) are healthy",
"duration": "00:00:00.0028300",
"status": "Healthy",
"tags": [
"database",
"postgresql",
"ready"
]
}
}
}
GET /sla/health/live (Liveness)
ตรวจสอบเฉพาะว่า process ของ service ยังทำงานอยู่ โดยไม่เช็ค dependency ใด ๆ เพื่อป้องกันไม่ให้ orchestrator สั่ง restart container เมื่อ dependency ภายนอก (database, cache) ล่มชั่วคราว
ตัวอย่าง response:
{
"status": "Healthy",
"totalDuration": 0.0062,
"entries": []
}
การติดตั้ง (Installation)
ติดตั้ง NuGet packages ดังต่อไปนี้
dotnet add package Microsoft.Extensions.Diagnostics.HealthChecks
dotnet add package AspNetCore.HealthChecks.UI.Client
dotnet add package AspNetCore.HealthChecks.Redis
dotnet add package Npgsql
dotnet add package Neo4j.Driver
| Package | หน้าที่ |
|---|---|
Microsoft.Extensions.Diagnostics.HealthChecks | Library หลักของ ASP.NET Core สำหรับลงทะเบียนและ map endpoint |
AspNetCore.HealthChecks.UI.Client | จัดรูป JSON response ตาม format ของ HealthChecks UI |
AspNetCore.HealthChecks.Redis | Health check สำหรับ Redis (ส่งคำสั่ง PING) |
Npgsql | Driver สำหรับเชื่อมต่อ PostgreSQL (ใช้ภายใน custom check) |
Neo4j.Driver | Driver สำหรับเชื่อมต่อ Neo4j (ใช้ภายใน custom check) |
การตั้งค่า (Configuration)
ASP.NET Core เก็บค่าตั้งค่าผ่าน IConfiguration ซึ่งสามารถอ่านค่าจากหลายแหล่งพร้อมกันได้ ที่ใช้บ่อย ได้แก่
appsettings.json— ไฟล์ตั้งค่าหลักของโปรเจกต์appsettings.{Environment}.json— ไฟล์ตั้งค่าเฉพาะ environment เช่นappsettings.Production.json- Environment variable ของระบบ
- ไฟล์
.env(ต้องติดตั้ง package เสริม) - User Secrets ของ .NET (ใช้ตอน develop ในเครื่องตัวเอง)
หากค่าตัวเดียวกันถูกตั้งจากหลายแหล่ง ASP.NET Core จะเอาค่าจากแหล่งที่อ่านทีหลังมา แทนที่ (override) ค่าจากแหล่งก่อนหน้า ตามลำดับ ค่าที่อยู่ใน environment variable จึงมีความสำคัญสูงกว่าค่าใน appsettings.json เสมอ และเนื่องจากค่าจากไฟล์ .env ถูกโหลดเป็น environment variable จึงแทนที่ค่าใน appsettings.json ได้อัตโนมัติเช่นเดียวกัน
แนวทางการเลือกที่เก็บตามลักษณะของค่า
| ลักษณะของค่า | ที่เก็บที่แนะนำ |
|---|---|
| ค่าที่มีโครงสร้างเป็น array หรือ object ซ้อนหลายชั้น | appsettings.json |
| ค่าที่เปลี่ยนตาม environment (dev / staging / prod) | appsettings.{Environment}.json |
| ค่าที่เป็นความลับ เช่น password, connection string จริง | .env หรือ secret manager |
การโหลดไฟล์ .env
ASP.NET Core ไม่ได้รองรับไฟล์ .env ในตัวโดยตรง ต้องติดตั้ง package เสริมชื่อ DotNetEnv ก่อน
dotnet add package DotNetEnv
จากนั้นในไฟล์ Program.cs ให้เรียก Env.Load() ก่อน WebApplication.CreateBuilder() ทุกครั้ง เพราะตอนสร้าง builder ASP.NET Core จะไปอ่าน environment variable ของระบบทันที ถ้าโหลด .env ทีหลัง ค่าที่อยู่ในไฟล์จะไม่ถูกนำมาใช้
using DotNetEnv;
Env.Load(); // อ่านค่าจากไฟล์ .env แล้วตั้งเป็น environment variable ของ process
var builder = WebApplication.CreateBuilder(args);
// ลงทะเบียน health check ตามปกติ
⚠️ อย่า commit ไฟล์
.envเข้า git เพราะมีค่าที่เป็นความลับอยู่ ให้เพิ่มชื่อไฟล์ใน.gitignoreแล้ว commit เฉพาะไฟล์ตัวอย่าง.env.exampleที่ใส่เพียงรายชื่อ key พร้อมค่า dummy เพื่อให้สมาชิกในทีมรู้ว่าต้องตั้งค่าอะไรบ้าง
กฎการตั้งชื่อ key
ค่าใน appsettings.json มักเขียนเป็นโครงสร้างซ้อนหลายชั้น แต่ใน .env หรือ environment variable เขียนเป็นบรรทัดเดียวไม่มีโครงสร้าง ASP.NET Core จึงมีกฎสำหรับแปลงชื่อ key ระหว่างสองรูปแบบ ดังนี้
- ใช้
__(underscore สองตัว) แทนเครื่องหมาย:ที่คั่นระหว่างชั้นของ key - หาก key เป็น array ให้ใช้เลข index (
0,1,2, …) ระบุตำแหน่งของแต่ละ element
ตัวอย่างการแปลงชื่อ key
โครงสร้างใน appsettings.json | ชื่อใน .env / environment variable |
|---|---|
ConnectionStrings:Redis | ConnectionStrings__Redis |
Authentik:BaseUrl | Authentik__BaseUrl |
Authentik:Timeout | Authentik__Timeout |
Neo4j:Uri | Neo4j__Uri |
Neo4j:User | Neo4j__User |
Neo4j:Password | Neo4j__Password |
Databases:0:Number (สมาชิกตัวที่ 1 ของ array) | Databases__0__Number |
Databases:0:ConnectionString (สมาชิกตัวที่ 1 ของ array) | Databases__0__ConnectionString |
ตัวอย่างไฟล์ .env ที่ครอบคลุมทุก dependency ของ guideline นี้
# Redis
ConnectionStrings__Redis=localhost:6379,abortConnect=false
# Neo4j
Neo4j__Uri=bolt://localhost:7687
Neo4j__User=neo4j
Neo4j__Password=change-me
# Authentik
Authentik__BaseUrl=https://auth.example.com
Authentik__Timeout=00:00:05
# PostgreSQL (multi-DB) — ใช้ index ของ array (เริ่มจาก 0)
Databases__0__Number=01
Databases__0__ConnectionString=Host=db1;Database=app01;Username=appuser;Password=pwd1
Databases__1__Number=02
Databases__1__ConnectionString=Host=db2;Database=app02;Username=appuser;Password=pwd2
แนวทางใช้งานร่วมกันที่แนะนำ คือ ใส่ค่า default และโครงสร้างเริ่มต้นที่ไม่ใช่ความลับไว้ใน appsettings.json ส่วนค่าที่เป็นความลับเช่น password ให้ใส่ใน .env แยกออกมา ระบบจะนำค่าจากทั้งสองที่มารวมกันให้อัตโนมัติเมื่อ service เริ่มทำงาน
การลงทะเบียน Health Check (Registration)
ใน Program.cs ลงทะเบียน health check ทุกตัว และ map endpoint ทั้งสอง
using HealthChecks.UI.Client;
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHttpClient();
builder.Services.AddHealthChecks()
.AddCheck<MultiDatabaseHealthCheck>(
name: "database",
tags: new[] { "database", "postgresql", "ready" },
timeout: TimeSpan.FromSeconds(5))
.AddRedis(
builder.Configuration.GetConnectionString("Redis")!,
name: "redis",
tags: new[] { "redis", "ready" },
timeout: TimeSpan.FromSeconds(5))
.AddCheck<Neo4jHealthCheck>(
name: "neo4j",
tags: new[] { "neo4j", "ready" },
timeout: TimeSpan.FromSeconds(5))
.AddCheck<AuthentikHealthCheck>(
name: "authentik",
tags: new[] { "authentik", "ready" },
timeout: TimeSpan.FromSeconds(5));
var app = builder.Build();
app.MapHealthChecks("/sla/health", new HealthCheckOptions
{
Predicate = _ => true,
ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse,
});
app.MapHealthChecks("/sla/health/live", new HealthCheckOptions
{
Predicate = _ => false,
ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse,
});
app.Run();
จุดที่ควรระวัง:
Predicate = _ => trueคือเลือก ทุก health check (สำหรับ readiness)Predicate = _ => falseคือ ไม่เลือก health check ใด ๆ (สำหรับ liveness — ตอบ Healthy ถ้า process ยังตอบสนองอยู่)- ทุก check ต้องกำหนด
timeout(ตามตัวอย่างด้านบน) เพื่อกัน hang กรณี dependency ไม่ตอบ ค่าแนะนำคือไม่เกิน 5 วินาที - ถ้าใช้
.envในการเก็บ config ต้องเรียกDotNetEnv.Env.Load()ก่อนWebApplication.CreateBuilder()ดูรายละเอียดที่หัวข้อ การตั้งค่า (Configuration)
Health Check ราย Dependency
Relational Database (PostgreSQL หลายตัว)
กรณีที่ service เชื่อมต่อ PostgreSQL หลาย database พร้อมกัน ให้เขียน custom IHealthCheck ตัวเดียวที่วน SELECT 1 กับทุก connection แล้วรวมผลลัพธ์เป็น JSON ตาม format ที่กำหนด
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Npgsql;
public sealed record DatabaseConnection(string Number, string ConnectionString);
public sealed class MultiDatabaseHealthCheck : IHealthCheck
{
private readonly IReadOnlyList<DatabaseConnection> _databases;
public MultiDatabaseHealthCheck(IEnumerable<DatabaseConnection> databases)
{
_databases = databases.ToList();
}
public async Task<HealthCheckResult> CheckHealthAsync(
HealthCheckContext context,
CancellationToken cancellationToken = default)
{
var data = new Dictionary<string, object>();
var healthyNumbers = new List<string>();
var unhealthyCount = 0;
foreach (var db in _databases)
{
var key = $"database_{db.Number}";
try
{
await using var conn = new NpgsqlConnection(db.ConnectionString);
await conn.OpenAsync(cancellationToken);
await using var cmd = conn.CreateCommand();
cmd.CommandText = "SELECT 1";
await cmd.ExecuteScalarAsync(cancellationToken);
data[key] = "Healthy";
healthyNumbers.Add(db.Number);
}
catch (Exception ex)
{
data[key] = $"Unhealthy: {ex.Message}";
unhealthyCount++;
}
}
data["total_databases"] = _databases.Count;
data["healthy_databases"] = healthyNumbers.Count;
data["unhealthy_databases"] = unhealthyCount;
data["healthy_database_list"] = string.Join(", ", healthyNumbers);
var description = unhealthyCount == 0
? $"All {_databases.Count} database(s) are healthy"
: $"{unhealthyCount} of {_databases.Count} database(s) are unhealthy";
return unhealthyCount == 0
? HealthCheckResult.Healthy(description, data)
: HealthCheckResult.Unhealthy(description, data: data);
}
}
ลงทะเบียน connection list ผ่าน DI:
builder.Services.AddSingleton<IEnumerable<DatabaseConnection>>(sp =>
builder.Configuration
.GetSection("Databases")
.Get<List<DatabaseConnection>>() ?? new List<DatabaseConnection>());
builder.Services.AddSingleton<MultiDatabaseHealthCheck>();
ตัวอย่าง appsettings.json:
{
"Databases": [
{ "Number": "01", "ConnectionString": "Host=db1;Database=app01;Username=...;Password=..." },
{ "Number": "02", "ConnectionString": "Host=db2;Database=app02;Username=...;Password=..." }
]
}
Graph Database (Neo4j)
ใช้ Neo4j.Driver เปิด session แล้วรัน Cypher RETURN 1 เพื่อทดสอบทั้งการเชื่อมต่อและการประมวลผล query
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Neo4j.Driver;
public sealed class Neo4jHealthCheck : IHealthCheck
{
private readonly IDriver _driver;
public Neo4jHealthCheck(IDriver driver)
{
_driver = driver;
}
public async Task<HealthCheckResult> CheckHealthAsync(
HealthCheckContext context,
CancellationToken cancellationToken = default)
{
try
{
await using var session = _driver.AsyncSession();
var result = await session.RunAsync("RETURN 1");
await result.ConsumeAsync();
return HealthCheckResult.Healthy("Neo4j is reachable");
}
catch (Exception ex)
{
return HealthCheckResult.Unhealthy("Neo4j is unreachable", ex);
}
}
}
ลงทะเบียน driver ผ่าน DI (singleton):
builder.Services.AddSingleton<IDriver>(sp =>
GraphDatabase.Driver(
builder.Configuration["Neo4j:Uri"]!,
AuthTokens.Basic(
builder.Configuration["Neo4j:User"]!,
builder.Configuration["Neo4j:Password"]!)));
builder.Services.AddSingleton<Neo4jHealthCheck>();
NoSQL (Redis)
Redis ใช้ extension AddRedis ของ AspNetCore.HealthChecks.Redis (ลงทะเบียนแล้วในตัวอย่าง Registration ด้านบน) extension นี้จะส่งคำสั่ง PING ไปยัง Redis server เมื่อ endpoint ถูกเรียก
ตั้ง connection string ใน appsettings.json:
{
"ConnectionStrings": {
"Redis": "localhost:6379,abortConnect=false"
}
}
Authentik (HTTP Ping + Timeout)
Authentik ยังไม่มี client library สำหรับ .NET โดยเฉพาะ จึงใช้วิธีเรียก HTTP endpoint /-/health/live/ ของ Authentik โดยตรง พร้อมกำหนด timeout เพื่อป้องกันการ hang เมื่อ Authentik ไม่ตอบสนอง
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.Extensions.Options;
public sealed class AuthentikHealthCheckOptions
{
public string BaseUrl { get; set; } = string.Empty;
public TimeSpan Timeout { get; set; } = TimeSpan.FromSeconds(5);
}
public sealed class AuthentikHealthCheck : IHealthCheck
{
private readonly IHttpClientFactory _httpClientFactory;
private readonly AuthentikHealthCheckOptions _options;
public AuthentikHealthCheck(
IHttpClientFactory httpClientFactory,
IOptions<AuthentikHealthCheckOptions> options)
{
_httpClientFactory = httpClientFactory;
_options = options.Value;
}
public async Task<HealthCheckResult> CheckHealthAsync(
HealthCheckContext context,
CancellationToken cancellationToken = default)
{
var url = _options.BaseUrl.TrimEnd('/') + "/-/health/live/";
using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
cts.CancelAfter(_options.Timeout);
try
{
var client = _httpClientFactory.CreateClient();
var response = await client.GetAsync(url, cts.Token);
return response.IsSuccessStatusCode
? HealthCheckResult.Healthy($"Authentik responded {(int)response.StatusCode}")
: HealthCheckResult.Unhealthy($"Authentik returned {(int)response.StatusCode}");
}
catch (OperationCanceledException) when (cts.IsCancellationRequested)
{
return HealthCheckResult.Unhealthy($"Authentik timeout after {_options.Timeout.TotalSeconds}s");
}
catch (Exception ex)
{
return HealthCheckResult.Unhealthy("Authentik unreachable", ex);
}
}
}
ลงทะเบียนผ่าน DI:
builder.Services.Configure<AuthentikHealthCheckOptions>(
builder.Configuration.GetSection("Authentik"));
builder.Services.AddSingleton<AuthentikHealthCheck>();
ตัวอย่าง appsettings.json:
{
"Authentik": {
"BaseUrl": "https://auth.example.com",
"Timeout": "00:00:05"
}
}
การทดสอบ (Verification)
หลังจากรัน service ให้ทดสอบทั้งสอง endpoint ด้วย curl
# Readiness — ควรได้ 200 OK พร้อมรายละเอียดของทุก dependency
curl -i http://localhost:5000/sla/health
# Liveness — ควรได้ 200 OK พร้อม entries: []
curl -i http://localhost:5000/sla/health/live
ทดสอบกรณี dependency ล้ม (failure case):
- หยุด Redis ด้วย
docker stop redisจากนั้นเรียก/sla/healthต้องได้ HTTP503และ entryredisมีstatus: "Unhealthy" - ในสถานะเดียวกัน เรียก
/sla/health/liveต้องยังได้ HTTP200 Healthy(เพราะ liveness ไม่เช็ค dependency)
ภาคผนวก (Appendix)
ตารางสรุป mapping ระหว่าง dependency, package และ tag
| Dependency | NuGet Package | Probe | Tag |
|---|---|---|---|
| PostgreSQL (multi) | AspNetCore.HealthChecks.NpgSql + custom MultiDatabaseHealthCheck | SELECT 1 | database, postgresql, ready |
| Neo4j | Neo4j.Driver + custom Neo4jHealthCheck | RETURN 1 | neo4j, ready |
| Redis | AspNetCore.HealthChecks.Redis | PING | redis, ready |
| Authentik | HttpClient + custom AuthentikHealthCheck | GET /-/health/live/ | authentik, ready |
หมายเหตุการตั้งค่าเพิ่มเติม:
- ทุก HTTP probe ควรกำหนด timeout ไม่เกิน 5 วินาที
- ทุก health check ควรติด tag
readyไว้ เพื่อให้สามารถสร้าง endpoint แยก (เช่น/sla/health/ready) ในอนาคตได้โดยไม่ต้องแก้ registration เดิม - บน production ควรปกป้อง endpoint ไม่ให้ถูกเรียกจากภายนอก ด้วย IP allowlist หรือ rule ของ reverse proxy