Aplikasi siap produksi bukan hanya kode yang berjalan benar — aplikasi tersebut harus dapat dimonitor, dijinakkan saat load balancer memberi sinyal stop, dan log-nya dapat diparse otomatis. Empat pilar production readiness adalah health check, metrics, graceful shutdown, dan structured logging. Pada chapter ini kita akan memodelkan keempatnya sebagai class Kotlin yang dapat dijalankan tanpa dependensi Spring Boot Actuator.
Health Check #
Health check adalah endpoint yang melaporkan status aplikasi dan komponennya. Load balancer (AWS ALB, Kubernetes ingress) memeriksa endpoint ini secara berkala — bila status DOWN, instance dikeluarkan dari rotasi. Spring Boot Actuator menyediakan /actuator/health otomatis.
GET /actuator/health
Response:
{
"status": "UP",
"components": {
"database": {"status": "UP", "detail": "HikariPool-1: 10 connections"},
"redis": {"status": "UP", "detail": "Ping OK, latency=2ms"},
"diskSpace": {"status": "UP", "detail": "Free: 500MB"}
}
}Status keseluruhan adalah UP bila semua komponen UP, DEGRADED bila ada komponen DEGRADED, dan DOWN bila ada komponen DOWN. Komponen yang umum: database, redis, disk space, message queue.
HealthIndicator #
HealthIndicator adalah interface Spring Boot Actuator. Setiap komponen memiliki indicator sendiri yang melakukan pengecekan ringan — mis. SELECT 1 untuk database, PING untuk Redis.
@Component
class DatabaseHealthIndicator(private val ds: DataSource) : HealthIndicator {
override fun health(): Health {
ds.connection.use { conn ->
conn.createStatement().executeQuery("SELECT 1").use { rs ->
if (rs.next()) return Health.up().withDetail("connection", "OK").build()
}
}
return Health.down().withDetail("error", "no connection").build()
}
}Indicator dipanggil saat /actuator/health diakses. Agar tidak membebani sistem, endpoint ini di-cache selama beberapa detik.
Metrics Micrometer #
Micrometer adalah facade metrics yang mendukung backend berbeda (Prometheus, Datadog, CloudWatch). Metric utama yang dilacak:
| Tipe | Contoh | Peran |
|---|---|---|
| Counter | http_requests_total | Jumlah kumulatif event |
| Gauge | jvm_memory_used_mb | Nilai saat ini |
| Timer | http_request_duration | Distribusi durasi |
| Distribution Summary | order_amount | Distribusi nilai |
@Autowired private lateinit var meters: MeterRegistry
meters.counter("http_requests_total", "method", "GET", "path", "/users").increment()
meters.gauge("jvm_memory_used_mb", runtime.totalMemory() / 1_000_000.0)
val timer = meters.timer("http_request_duration", "path", "/users")
timer.record(150, TimeUnit.MILLISECONDS)Metric diekspos di /actuator/metrics dan /actuator/prometheus. Prometheus mengambil metric secara berkala dan menyimpannya sebagai time series.
Graceful Shutdown #
Graceful shutdown memastikan instance tidak langsung mati saat menerima sinyal SIGTERM. Spring Boot 2.3+ mendukung graceful shutdown dengan konfigurasi:
server:
shutdown: graceful
spring:
lifecycle:
timeout-per-shutdown-phase: 30sSaat SIGTERM diterima, Spring menolak request baru, menunggu request aktif selesai (hingga 30 detik), lalu menutup koneksi database, flush metric, dan akhirnya exit 0. Hal ini mencegah request terpotong di tengah proses.
Structured Logging #
Log tradisional berbentuk teks bebas — sulit diparse otomatis. Structured logging mengeluarkan log sebagai JSON dengan field konsisten — memudahkan agregasi di ELK, Loki, atau Datadog.
{"ts":"2026-01-15T09:00:00Z","level":"INFO","msg":"Server started","port":8080}
{"ts":"2026-01-15T09:01:23Z","level":"WARN","msg":"Redis latency tinggi","latency_ms":250}
{"ts":"2026-01-15T09:02:45Z","level":"ERROR","msg":"Database gagal connect","host":"db.example.com","retry":3}Field trace_id dan span_id dapat ditambahkan untuk distributed tracing — satu request yang melalui multiple service memiliki trace_id yang sama, sehingga dapat ditelusuri end-to-end.
Demo Production Readiness #
Kita akan memodelkan keempat pilar sebagai class Kotlin: HealthEndpoint dengan daftar HealthIndicator, MeterRegistry dengan counter/gauge/timer, GracefulShutdownManager, dan StructuredLogger. Program ini mendemonstrasikan keempat pilar dalam satu eksekusi.
// backend_production_readiness.kt
// Mendemonstrasikan production readiness: health check, metrics (Micrometer),
// graceful shutdown, dan structured logging. Pada kode produksi, Spring Boot
// Actuator menyediakan endpoint /actuator/health, /actuator/metrics, dll.
// Di sini kita memodelkannya.
// Health check: status komponen
enum class HealthStatus { UP, DOWN, DEGRADED }
data class HealthInfo(val name: String, val status: HealthStatus, val detail: String)
data class HealthResponse(val status: HealthStatus, val components: List<HealthInfo>) {
fun toJson(): String {
val comps = components.joinToString(",") { c ->
""""${c.name}":{"status":"${c.status}","detail":"${c.detail}"}"""
}
return """{"status":"$status","components":{$comps}}"""
}
}
// Component health checker
interface HealthIndicator {
val name: String
fun check(): HealthInfo
}
class DatabaseHealthIndicator : HealthIndicator {
override val name = "database"
override fun check(): HealthInfo {
// Simulasi: cek koneksi DB
return HealthInfo("database", HealthStatus.UP, "HikariPool-1: 10 connections, 0 active")
}
}
class RedisHealthIndicator(private val available: Boolean) : HealthIndicator {
override val name = "redis"
override fun check(): HealthInfo {
return if (available) {
HealthInfo("redis", HealthStatus.UP, "Ping OK, latency=2ms")
} else {
HealthInfo("redis", HealthStatus.DOWN, "Connection refused")
}
}
}
class DiskSpaceHealthIndicator(private val freeMb: Int) : HealthIndicator {
override val name = "diskSpace"
override fun check(): HealthInfo {
val status = if (freeMb > 100) HealthStatus.UP else HealthStatus.DEGRADED
return HealthInfo("diskSpace", status, "Free: ${freeMb}MB (threshold: 100MB)")
}
}
// HealthEndpoint: /actuator/health
class HealthEndpoint(private val indicators: List<HealthIndicator>) {
fun health(): HealthResponse {
val infos = indicators.map { it.check() }
val overall = when {
infos.any { it.status == HealthStatus.DOWN } -> HealthStatus.DOWN
infos.any { it.status == HealthStatus.DEGRADED } -> HealthStatus.DEGRADED
else -> HealthStatus.UP
}
return HealthResponse(overall, infos)
}
}
// Micrometer-style metrics registry
class MeterRegistry {
private val counters = mutableMapOf<String, Long>()
private val gauges = mutableMapOf<String, Double>()
private val timers = mutableMapOf<String, MutableList<Long>>() // in nanos
fun incrementCounter(name: String, amount: Long = 1) {
counters[name] = (counters[name] ?: 0L) + amount
}
fun setGauge(name: String, value: Double) { gauges[name] = value }
fun recordTimer(name: String, durationNanos: Long) {
timers.getOrPut(name) { mutableListOf() }.add(durationNanos)
}
fun snapshot(): String {
val sb = StringBuilder()
sb.append("=== Counters ===\n")
counters.forEach { (n, v) -> sb.append(" $n = $v\n") }
sb.append("=== Gauges ===\n")
gauges.forEach { (n, v) -> sb.append(" $n = $v\n") }
sb.append("=== Timers ===\n")
timers.forEach { (n, list) ->
val avgNanos = list.average().toLong()
sb.append(" $n: count=${list.size} avg=${avgNanos}ns\n")
}
return sb.toString().trimEnd()
}
}
// Graceful shutdown manager
class GracefulShutdownManager(
private val timeoutMs: Long = 30_000,
private val activeRequests: () -> Int
) {
@Volatile private var shuttingDown = false
fun isShuttingDown(): Boolean = shuttingDown
fun initiateShutdown(): String {
shuttingDown = true
val steps = mutableListOf<String>()
steps.add("SIGTERM diterima, shuttingDown=true (request baru ditolak)")
val startMs = System.currentTimeMillis()
var waited = 0L
while (activeRequests() > 0 && waited < timeoutMs) {
Thread.sleep(100)
waited = System.currentTimeMillis() - startMs
}
steps.add("Aktif request selesai dalam ${waited}ms (sisa: ${activeRequests()})")
steps.add("Closing HikariPool, flushing metrics, closing log appender")
steps.add("Shutdown selesai, exit 0")
return steps.joinToString("\n ")
}
}
// Structured logger
class StructuredLogger {
enum class Level { INFO, WARN, ERROR }
private val entries = mutableListOf<String>()
fun log(level: Level, message: String, fields: Map<String, Any> = emptyMap()) {
val ts = "2026-01-15T09:00:00Z" // deterministik
val fieldStr = if (fields.isEmpty()) "" else " " + fields.entries.joinToString(" ") { (k, v) -> "$k=$v" }
val line = "$ts [$level] $message$fieldStr"
entries.add(line)
}
fun entries(): List<String> = entries.toList()
}
fun main() {
println("=== Health check (semua sehat) ===")
val healthAllUp = HealthEndpoint(listOf(
DatabaseHealthIndicator(),
RedisHealthIndicator(available = true),
DiskSpaceHealthIndicator(freeMb = 500)
))
val r1 = healthAllUp.health()
println("Status: ${r1.status}")
println(r1.toJson())
println("\n=== Health check (redis DOWN) ===")
val healthRedisDown = HealthEndpoint(listOf(
DatabaseHealthIndicator(),
RedisHealthIndicator(available = false),
DiskSpaceHealthIndicator(freeMb = 50) // DEGRADED
))
val r2 = healthRedisDown.health()
println("Status: ${r2.status}")
r2.components.forEach { println(" ${it.name}: ${it.status} - ${it.detail}") }
println("\n=== Metrics (Micrometer style) ===")
val meters = MeterRegistry()
meters.incrementCounter("http_requests_total", 1)
meters.incrementCounter("http_requests_total", 1)
meters.incrementCounter("http_requests_total", 1)
meters.incrementCounter("orders_created_total", 5)
meters.setGauge("jvm_memory_used_mb", 128.5)
meters.setGauge("hikari_active_connections", 3.0)
meters.recordTimer("http_request_duration", 1_200_000)
meters.recordTimer("http_request_duration", 3_400_000)
meters.recordTimer("http_request_duration", 2_100_000)
println(meters.snapshot())
println("\n=== Graceful shutdown ===")
var active = 3
val shutdownMgr = GracefulShutdownManager(timeoutMs = 5000) { active }
// Simulasi request aktif yang selesai satu per satu
Thread {
Thread.sleep(150); active = 2
Thread.sleep(150); active = 1
Thread.sleep(150); active = 0
}.start()
println(" ${shutdownMgr.initiateShutdown()}")
println("\n=== Structured logging ===")
val logger = StructuredLogger()
logger.log(StructuredLogger.Level.INFO, "Server started", mapOf("port" to 8080))
logger.log(StructuredLogger.Level.WARN, "Redis latency tinggi", mapOf("latency_ms" to 250))
logger.log(StructuredLogger.Level.ERROR, "Database gagal connect",
mapOf("host" to "db.example.com", "retry" to 3))
logger.entries().forEach { println(" $it") }
}Output yang Dihasilkan:
=== Health check (semua sehat) ===
Status: UP
{"status":"UP","components":{"database":{"status":"UP","detail":"HikariPool-1: 10 connections, 0 active"},"redis":{"status":"UP","detail":"Ping OK, latency=2ms"},"diskSpace":{"status":"UP","detail":"Free: 500MB (threshold: 100MB)"}}}
=== Health check (redis DOWN) ===
Status: DOWN
database: UP - HikariPool-1: 10 connections, 0 active
redis: DOWN - Connection refused
diskSpace: DEGRADED - Free: 50MB (threshold: 100MB)
=== Metrics (Micrometer style) ===
=== Counters ===
http_requests_total = 3
orders_created_total = 5
=== Gauges ===
jvm_memory_used_mb = 128.5
hikari_active_connections = 3.0
=== Timers ===
http_request_duration: count=3 avg=2233333ns
=== Graceful shutdown ===
SIGTERM diterima, shuttingDown=true (request baru ditolak)
Aktif request selesai dalam 500ms (sisa: 0)
Closing HikariPool, flushing metrics, closing log appender
Shutdown selesai, exit 0
=== Structured logging ===
2026-01-15T09:00:00Z [INFO] Server started port=8080
2026-01-15T09:00:00Z [WARN] Redis latency tinggi latency_ms=250
2026-01-15T09:00:00Z [ERROR] Database gagal connect host=db.example.com retry=3Mari kita bedah bagian penting kode di atas:
interface HealthIndicator { val name; fun check(): HealthInfo }: kontrak indicator. Setiap komponen (database, redis, disk) memiliki indicator sendiri yang tahu cara mengecek kesehatannya.val overall = when { infos.any { it.status == DOWN } -> DOWN; infos.any { it.status == DEGRADED } -> DEGRADED; else -> UP }: status agregat — DOWN menang atas DEGRADED, DEGRADED menang atas UP. Ini memastikan load balancer segera mengeluarkan instance yang bermasalah.class MeterRegistry { incrementCounter, setGauge, recordTimer }: tiga tipe metric dasar. Counter kumulatif (hanya naik), gauge nilai saat ini, timer distribusi durasi.@Volatile private var shuttingDown = false:@Volatilememastikan perubahanshuttingDownterlihat oleh semua thread. Tanpa ini, thread handler request mungkin masih menerima request baru setelah shutdown dimulai.while (activeRequests() > 0 && waited < timeoutMs): poll aktif request setiap 100ms. Bila semua selesai sebelum timeout, shutdown cepat. Bila tidak, paksa keluar setelah timeout untuk menghindari hang.val ts = "2026-01-15T09:00:00Z": timestamp deterministik untuk demo. Pada produksi, ambilInstant.now()sehingga log menunjukkan waktu aktual.
Endpoint Actuator #
Spring Boot Actuator menyediakan beberapa endpoint produksi:
| Endpoint | Peran |
|---|---|
/actuator/health | Status kesehatan aplikasi |
/actuator/metrics | Daftar metric tersedia |
/actuator/metrics/{name} | Nilai metric tertentu |
/actuator/prometheus | Format Prometheus untuk scraping |
/actuator/info | Info build (versi, git commit) |
/actuator/loggers | Lihat dan ubah log level runtime |
Keamanan endpoint ini penting — /actuator/env dapat membocorkan kredensial. Batasi akses ke internal network atau tambahkan autentikasi.
Referensi Lanjutan #
- Spring Boot reference — rujukan resmi Spring Boot untuk produksi: health check, metrics, dan konfigurasi profil
- Ktor welcome — halaman Ktor untuk merancang deployment produksi yang ringan dan coroutine-friendly
Selanjutnya #
Empat pilar production readiness — health check, metrics, graceful shutdown, structured logging — menutup section Backend Development. Dengan pengetahuan Ktor dan Spring Boot yang sudah dipelajari, pembaca siap membangun layanan backend Kotlin yang dapat diandalkan di produksi.