Skip to main content

SLA Health Check

ข้อมูลควบคุมเอกสาร (Document Control)​

ฟิลด์ค่า
ชื่อเอกสารSLA Health Check Guideline
Version1.0
สถานะApproved
วันที่ออกเอกสาร2026-05-05
ผู้จัดทำ (Author)SLA Team
ผู้อนุมัติ (Approver)DIIS Development Guideline Team
มาตรฐานอ้างอิงISO/IEC 29110

ประวัติการแก้ไข (Change History)​

VersionDateAuthorDescription
1.02026-05-05SLA TeamInitial 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.HealthChecksLibrary หลักของ ASP.NET Core สำหรับลงทะเบียนและ map endpoint
AspNetCore.HealthChecks.UI.Clientจัดรูป JSON response ตาม format ของ HealthChecks UI
AspNetCore.HealthChecks.RedisHealth check สำหรับ Redis (ส่งคำสั่ง PING)
NpgsqlDriver สำหรับเชื่อมต่อ PostgreSQL (ใช้ภายใน custom check)
Neo4j.DriverDriver สำหรับเชื่อมต่อ 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:RedisConnectionStrings__Redis
Authentik:BaseUrlAuthentik__BaseUrl
Authentik:TimeoutAuthentik__Timeout
Neo4j:UriNeo4j__Uri
Neo4j:UserNeo4j__User
Neo4j:PasswordNeo4j__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):

  1. หยุด Redis ด้วย docker stop redis จากนั้นเรียก /sla/health ต้องได้ HTTP 503 และ entry redis มี status: "Unhealthy"
  2. ในสถานะเดียวกัน เรียก /sla/health/live ต้องยังได้ HTTP 200 Healthy (เพราะ liveness ไม่เช็ค dependency)

ภาคผนวก (Appendix)​

ตารางสรุป mapping ระหว่าง dependency, package และ tag

DependencyNuGet PackageProbeTag
PostgreSQL (multi)AspNetCore.HealthChecks.NpgSql + custom MultiDatabaseHealthCheckSELECT 1database, postgresql, ready
Neo4jNeo4j.Driver + custom Neo4jHealthCheckRETURN 1neo4j, ready
RedisAspNetCore.HealthChecks.RedisPINGredis, ready
AuthentikHttpClient + custom AuthentikHealthCheckGET /-/health/live/authentik, ready

หมายเหตุการตั้งค่าเพิ่มเติม:

  • ทุก HTTP probe ควรกำหนด timeout ไม่เกิน 5 วินาที
  • ทุก health check ควรติด tag ready ไว้ เพื่อให้สามารถสร้าง endpoint แยก (เช่น /sla/health/ready) ในอนาคตได้โดยไม่ต้องแก้ registration เดิม
  • บน production ควรปกป้อง endpoint ไม่ให้ถูกเรียกจากภายนอก ด้วย IP allowlist หรือ rule ของ reverse proxy