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.

text Health check response
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.

kotlin HealthIndicator
@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:

TipeContohPeran
Counterhttp_requests_totalJumlah kumulatif event
Gaugejvm_memory_used_mbNilai saat ini
Timerhttp_request_durationDistribusi durasi
Distribution Summaryorder_amountDistribusi nilai
kotlin Micrometer usage
@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:

yaml application.yml
server:
  shutdown: graceful
spring:
  lifecycle:
    timeout-per-shutdown-phase: 30s

Saat 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.

json Structured log JSON
{"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.

kotlin
// 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:

text
=== 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=3

Mari kita bedah bagian penting kode di atas:

  1. interface HealthIndicator { val name; fun check(): HealthInfo }: kontrak indicator. Setiap komponen (database, redis, disk) memiliki indicator sendiri yang tahu cara mengecek kesehatannya.
  2. 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.
  3. class MeterRegistry { incrementCounter, setGauge, recordTimer }: tiga tipe metric dasar. Counter kumulatif (hanya naik), gauge nilai saat ini, timer distribusi durasi.
  4. @Volatile private var shuttingDown = false: @Volatile memastikan perubahan shuttingDown terlihat oleh semua thread. Tanpa ini, thread handler request mungkin masih menerima request baru setelah shutdown dimulai.
  5. 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.
  6. val ts = "2026-01-15T09:00:00Z": timestamp deterministik untuk demo. Pada produksi, ambil Instant.now() sehingga log menunjukkan waktu aktual.

Endpoint Actuator #

Spring Boot Actuator menyediakan beberapa endpoint produksi:

EndpointPeran
/actuator/healthStatus kesehatan aplikasi
/actuator/metricsDaftar metric tersedia
/actuator/metrics/{name}Nilai metric tertentu
/actuator/prometheusFormat Prometheus untuk scraping
/actuator/infoInfo build (versi, git commit)
/actuator/loggersLihat 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.