Logging #

Dalam arsitektur monolitik tradisional, melacak kesalahan sistem biasanya sesederhana menghubungkan sesi terminal SSH ke server tunggal dan membaca file log teks seperti /var/log/nginx/access.log atau /var/log/app.log menggunakan perintah tail. Namun, di dalam ekosistem Kubernetes yang bersifat dinamis dan terdistribusi, pendekatan ini sama sekali tidak dapat digunakan. Pod bersifat efemer (ephemeral) — mereka dapat dibuat, di-schedule ulang ke node lain, atau dihancurkan kapan saja oleh orkestrator. Ketika sebuah Pod mati atau mengalami restart, seluruh file log lokal di dalam kontainer akan ikut hilang selamanya. Oleh karena itu, kita harus merancang sistem pengumpulan log (logging pipeline) terpusat yang mampu mengumpulkan, memperkaya dengan metadata kluster, menyaring, dan menyimpan log dari ratusan kontainer secara real-time dan persisten.


Arsitektur Logging Tiga Lapis di Kubernetes #

Untuk memahami bagaimana log diproses di Kubernetes, kita harus melihatnya sebagai aliran data tiga lapis yang mengalir dari kontainer aplikasi hingga ke database log terpusat di tingkat kluster.

flowchart TD
    AppPod["Application Pod (Container)"] -->|"stdout / stderr"| CRI["Container Runtime (containerd)"]
    CRI -->|"Tulis Log ke Host Path"| HostLog["Host Directory (/var/log/pods/)"]
    
    subgraph AgentEnrichment["Node-Level Logging Agent"]
        direction TB
        DaemonSet["Logging Agent DaemonSet (Fluent Bit / Promtail)"] -->|"Mount Host Path"| HostLog
        DaemonSet -->|"Query metadata (pod_name, namespace)"| K8sAPI["Kube-API Server"]
        DaemonSet -->|"Enrich & Parse JSON"| ProcessLog["Log Parser & Filter Engine"]
    end
    
    ProcessLog -->|"Kirim Batch via Webhook / API"| Aggregators["Central Storage & UI Backend"]
    
    subgraph Aggregators
        direction LR
        Loki["Grafana Loki (Index Labels, Chunks in S3)"]
        Elasticsearch["Elasticsearch (Full-Text Indexing)"]
    end

1. Lapisan Pertama: Aplikasi (Stdout/Stderr) #

Prinsip fundamental dari aplikasi cloud-native (sesuai metodologi Twelve-Factor App) adalah memperlakukan log sebagai aliran data peristiwa (event streams). Aplikasi kita tidak boleh mengelola file log secara mandiri ke disk. Sebaliknya, aplikasi harus menulis seluruh log secara langsung ke saluran keluaran standar (stdout) dan saluran kesalahan standar (stderr). Container Runtime (seperti containerd atau CRI-O) akan menangkap aliran data ini secara otomatis.

2. Lapisan Kedua: Node-Level Logging #

Di tingkat mesin host (node), container runtime menulis aliran log yang ditangkap dari kontainer ke sebuah file log lokal di disk node.

  • Path default penyimpanan log kontainer berada di /var/log/pods/<namespace>_<pod-name>_<pod-uid>/<container-name>/<restart-count>.log.
  • Sistem juga membuat tautan simbolik (symlink) ke direktori /var/log/containers/ agar mempermudah pengumpulan.
  • Log-log ini disimpan dalam format terstruktur sederhana (misalnya JSON log format oleh Docker daemon, atau format CRI standar).

3. Lapisan Ketiga: Cluster-Level Logging #

Karena file log di tingkat node akan dirotasi atau dihapus saat disk penuh, kita memerlukan agen pengumpul log tingkat kluster (cluster-level logging agent).

  • Agen ini dijalankan sebagai DaemonSet (satu Pod di setiap node worker) seperti Fluent Bit atau Promtail.
  • Agen tersebut melakukan mounting direktori host /var/log/pods secara read-only.
  • Agen membaca aliran log baru, menghubungi Kube-API Server untuk menanyakan metadata Pod (seperti label Pod, nama namespace, alamat IP node), menyisipkan metadata tersebut ke entri log, dan mengirimkannya dalam batch ke database log terpusat.

Standarisasi Structured Logging (JSON) #

Menulis log dalam bentuk baris teks polos (plain text) adalah sebuah kesalahan besar (anti-pattern) untuk kluster tingkat produksi. Log seperti 2026-06-17 08:00:10 INFO User login success for ID 4891 membutuhkan ekspresi reguler (regex) yang rumit untuk di-parse di tingkat SIEM, yang memakan banyak waktu CPU ketika memproses jutaan baris log per detik.

Kita harus mewajibkan seluruh aplikasi menulis log dalam format JSON terstruktur. Dengan format JSON, setiap atribut log (seperti level log, nama fungsi, durasi eksekusi) didefinisikan sebagai pasangan key-value yang dapat diindeks dan dicari secara instan tanpa proses parsing ekspresi reguler.

Perbandingan Format Logging di Kode #

# ANTI-PATTERN: Menulis log plaintext. Sulit diekstrak parameternya oleh parser otomatis.
import logging
logging.basicConfig(level=logging.INFO)
logging.info("Koneksi database ke host %s sukses dalam waktu %d ms", "db-01.internal", 145)

# ==============================================================================
# BENAR: Menulis log JSON terstruktur dengan metadata kontekstual.
import sys
import json
from datetime import datetime

def log_json(level, msg, **kwargs):
    log_entry = {
        "time": datetime.utcnow().isoformat() + "Z", # ISO 8601 UTC
        "level": level,
        "msg": msg
    }
    log_entry.update(kwargs)
    sys.stdout.write(json.dumps(log_entry) + "\n")
    sys.stdout.flush()

log_json("INFO", "database_connection_established", host="db-01.internal", elapsed_ms=145)

JSON Schema Standar untuk Log Kluster #

Untuk mempermudah korelasi log antar layanan yang berbeda bahasa, kita harus menetapkan skema JSON standar yang harus dipatuhi oleh seluruh tim developer:

{
  "time": "2026-06-17T08:14:23.901Z",     // Format ISO 8601 UTC wajib
  "level": "ERROR",                       // DEBUG, INFO, WARN, ERROR, FATAL
  "msg": "database_query_timeout",        // Ringkas, deskriptif, snake_case
  "service": "billing-service",           // Nama layanan aplikasi
  "version": "v1.2.4",                   // Versi build aplikasi
  "trace_id": "4bf92f3577b34da6a3ce929d0e0e4736", // Untuk korelasi distributed tracing
  "span_id": "00f067aa0ba902b7",          // ID sub-operasi
  "elapsed_ms": 5000,                     // Metrik performa jika ada
  "error_details": {                      // Objek khusus jika level ERROR
    "class": "TimeoutException",
    "stacktrace": "at Billing.Query(db.go:45)..."
  }
}

Menggunakan Downward API untuk Menyisipkan Metadata Pod #

Kita tidak perlu memaksa aplikasi menulis nama Pod atau alamat IP node secara manual ke dalam file konfigurasi. Kita dapat memanfaatkan Downward API Kubernetes untuk menyuntikkan metadata infrastruktur secara otomatis ke dalam variabel lingkungan kontainer, yang kemudian dibaca oleh library logger aplikasi.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: billing-app
  namespace: production
spec:
  replicas: 3
  selector:
    matchLabels:
      app: billing-app
  template:
    metadata:
      labels:
        app: billing-app
    spec:
      containers:
      - name: app
        image: billing:v1.2.4
        env:
        # Suntikkan nama Pod aktual ke env var
        - name: K8S_POD_NAME
          valueFrom:
            fieldRef:
              fieldPath: metadata.name
        # Suntikkan nama namespace
        - name: K8S_NAMESPACE
          valueFrom:
            fieldRef:
              fieldPath: metadata.namespace
        # Suntikkan nama node worker host
        - name: K8S_NODE_NAME
          valueFrom:
            fieldRef:
              fieldPath: spec.nodeName

Mengelola Log Aplikasi Warisan (Legacy Sidecar Streamer) #

Tantangan muncul ketika kita harus memigrasikan aplikasi warisan (legacy applications) yang tidak dapat dimodifikasi kode sumbernya dan secara keras (hardcoded) menulis log ke file di dalam disk lokal (misalnya menulis ke /var/log/my-app/out.log).

Jika kita membiarkan aplikasi menulis ke disk kontainer:

  • File log akan terus tumbuh hingga menghabiskan ruang disk kontainer, memicu pengosongan node (node eviction) karena tekanan disk.
  • Log tersebut tidak ditangkap oleh Kubelet karena tidak dikirim ke stdout/stderr.

Solusi: Sidecar Log Streamer Pattern #

Kita menerapkan pola kontainer pendamping (sidecar) yang bertugas membaca file log tersebut dan mengalirkannya ke stdout kontainer sidecar menggunakan utilitas tail.

# Implementasi Sidecar Log Streaming
apiVersion: apps/v1
kind: Deployment
metadata:
  name: legacy-app
  namespace: production
spec:
  replicas: 2
  selector:
    matchLabels:
      app: legacy-app
  template:
    metadata:
      labels:
        app: legacy-app
    spec:
      containers:
      # 1. Kontainer Utama: Menulis log ke file lokal di direktori bersama
      - name: legacy-main
        image: legacy-service:v4.1
        volumeMounts:
        - name: shared-log-dir
          mountPath: /var/log/app
      
      # 2. Kontainer Sidecar: Membaca file log dan mengalirkannya ke stdout
      - name: log-shipper-sidecar
        image: busybox:1.36
        command: ["/bin/sh", "-c", "touch /var/log/app/out.log && tail -F /var/log/app/out.log"]
        volumeMounts:
        - name: shared-log-dir
          mountPath: /var/log/app
          
      volumes:
      # Volume memory-backed ephemeral (emptyDir) sebagai jembatan penyimpanan log
      - name: shared-log-dir
        emptyDir:
          medium: Memory
          sizeLimit: 100Mi # Batasi kapasitas agar tidak membanjiri RAM node

Dengan pola ini, agen logging di tingkat node dapat menangkap log kontainer sidecar (log-shipper-sidecar) karena dialirkan ke stdout, sementara aplikasi utama tetap berjalan tanpa modifikasi kode.


Perbandingan Stack Pengumpul Log: EFK vs Grafana Loki #

Ada dua stack arsitektur pengumpulan log yang mendominasi ekosistem Kubernetes. Memilih teknologi yang tepat sangat bergantung pada anggaran operasional dan kebutuhan fitur pencarian.

Tabel Perbandingan EFK dan Loki #

Karakteristik Stack EFK (Elasticsearch / Fluent Bit / Kibana) Stack Grafana Loki (Loki / Promtail / Grafana)
Metode Indeks Mengindeks seluruh teks log (Full-Text Indexing). Hanya mengindeks label metadata (Metadata-Only Indexing).
Penyimpanan (Storage) Sangat Besar ( raw log size + index size yang gemuk). Sangat Hemat (plaintext log dikompresi menjadi chunks).
Resource CPU/RAM Tinggi (Elasticsearch membutuhkan RAM minimum 4GB - 8GB per node). Sangat Ringan (Loki dapat berjalan dengan RAM 500MB).
Bahasa Kueri KQL (Kibana Query Language) / Elasticsearch DSL. LogQL (terinspirasi dari PromQL milik Prometheus).
Kecepatan Kueri Sangat Cepat untuk pencarian teks acak (ad-hoc string search). Lambat untuk pencarian teks acak pada data skala besar.
Skenario Terbaik Audit forensik teks, investigasi pelanggaran keamanan tingkat tinggi. Kluster Kubernetes dengan ribuan microservices dan budget minim.

Contoh Kueri LogQL Grafana Loki Tingkat Lanjut #

Loki memanfaatkan bahasa LogQL untuk menganalisis log secara terstruktur. Berikut adalah contoh kueri untuk memecahkan masalah di produksi:

# 1. Tampilkan semua log ERROR dari layanan billing di namespace production
{namespace="production", app="billing-service"} |= "ERROR"

# 2. Parse payload JSON log secara dinamis dan filter request yang memakan waktu > 2 detik
{namespace="production", app="billing-service"} 
  | json 
  | elapsed_ms > 2000 
  | line_format "Layanan {{.service}} lambat pada path {{.path}} dengan durasi {{.elapsed_ms}}ms"

# 3. Hitung jumlah error per menit (Rate of Errors) untuk membuat grafik visual di dasbor
sum(rate({namespace="production", app="billing-service"} |= "ERROR" [1m]))

Proteksi Disk Node terhadap Kebocoran Log #

Jika sebuah aplikasi mengalami kegagalan sistem loop (restart loop atau deadlock loop) dan terus-menerus menulis jutaan baris log kegagalan ke stdout, hard disk pada host node worker dapat terisi penuh dalam hitungan menit. Kondisi Disk Pressure ini akan memaksa Kubelet mengosongkan node (evicting pods) dan dapat merusak kestabilan kluster.

Kita harus mengonfigurasi rotasi log secara ketat pada tingkat Container Runtime (containerd).

Konfigurasi Rotasi Log containerd #

Buat parameter konfigurasi rotasi log berikut pada file /etc/containerd/config.toml di setiap node worker:

# Konfigurasi containerd log rotation
[plugins."io.containerd.grpc.v1.cri".containerd]
  # Batasi ukuran maksimum file log kontainer sebelum dirotasi
  # Default sering kali tidak dibatasi
  max_container_log_line_size = 16384 # 16KB per baris log maksimum

[plugins."io.containerd.grpc.v1.cri"]
  # Rotasi log otomatis
  # Mengaktifkan rotasi log local file
  [plugins."io.containerd.grpc.v1.cri".registry]
    # ...

Catatan: Pada sistem operasi modern yang menggunakan kubelet, rotasi log juga dikelola secara mandiri oleh parameter kubelet config di /var/lib/kubelet/config.yaml.

# Tambahkan parameter limit log di kubelet-config
containerLogMaxSize: "10Mi"       # Rotasi file jika ukuran mencapai 10 Megabyte
containerLogMaxFiles: 5           # Simpan maksimal 5 file rotasi cadangan

Dengan konfigurasi ini, file log lokal kontainer di disk node worker tidak akan pernah melebihi 50MB (10MB x 5 file), melindungi node dari bahaya pemadaman akibat kehabisan ruang disk (disk exhaustion).


Anti-Pattern dalam Praktik Logging #

Berikut adalah kebiasaan buruk yang sering dilakukan tim operasional dan developer dalam mengelola logging di Kubernetes:

1. Mengirim Log Sinkron Langsung ke Database Log (Synchronous Remoting) #

// ANTI-PATTERN: Aplikasi memanggil API Elasticsearch secara langsung (sinkron) di dalam request path.
// Jika Elasticsearch mengalami kelambatan atau down, aplikasi utama akan ikut macet/down.
func HandlePayment(w http.ResponseWriter, r *http.Request) {
    status := ProcessPayment()
    // JANGAN: Panggilan HTTP memblokir request thread aplikasi
    http.Post("http://elasticsearch:9200/logs", "application/json", jsonLog)
    w.WriteHeader(http.StatusOK)
}

// ==============================================================================
// BENAR: Aplikasi menulis ke stdout secara asinkron. Biarkan agen eksternal yang mengirimnya.
func HandlePayment(w http.ResponseWriter, r *http.Request) {
    status := ProcessPayment()
    // Cukup tulis ke stdout; containerd dan Fluent Bit yang mengurus pengiriman ke backend
    log.Printf("{\"event\":\"payment_processed\",\"status\":\"%s\"}", status)
    w.WriteHeader(http.StatusOK)
}

2. Kebocoran Rahasia plain-text ke stdout #

Mencetak token autentikasi API atau kata sandi database secara polos ke output terminal kontainer. Hal ini melanggar kepatuhan industri (PCI-DSS/GDPR) karena log shipper akan mendistribusikannya ke database indeks umum.

3. Tidak Melakukan Throttling/Rate-Limiting Log Debug #

Membiarkan level log aplikasi diatur ke tingkat DEBUG di lingkungan produksi. Ini membebani performa CPU kontainer, memadati bandwidth jaringan internal kluster, dan menghabiskan ruang penyimpanan SIEM secara tidak perlu. Setel level log produksi minimum ke INFO atau WARN.


Checklist Audit Praktik Logging Kluster #

Gunakan checklist berikut untuk memverifikasi apakah alur kerja logging Anda telah memenuhi standar praktik produksi yang baik:

RANCANGAN LOGGER APLIKASI:
  □ Aplikasi menulis seluruh log ke stdout/stderr secara asinkron.
  □ Format log menggunakan JSON terstruktur yang konsisten.
  □ Atribut log menyertakan stempel waktu ISO 8601 UTC dengan presisi milidetik.
  □ Atribut log menyertakan trace_id dan span_id untuk distributed tracing.
  □ Menggunakan Downward API untuk menyisipkan metadata nama Pod, namespace, dan node ke log.
  □ Dilakukan redaction (sensor) otomatis terhadap kata kunci sensitif (password, credit_card).

PROTEKSI DISK & NODE LEVEL:
  □ Kubelet dikonfigurasi dengan limit 'containerLogMaxSize' (misal: 10Mi).
  □ Kubelet dikonfigurasi dengan batas maksimum rotasi file 'containerLogMaxFiles' (misal: 5).
  □ Aplikasi legacy yang menulis ke file disk dipasangkan dengan sidecar tail streamer.
  □ Volume sidecar emptyDir log dibatasi kapasitasnya menggunakan 'sizeLimit' memory-backed.

ARSITEKTUR PIPELINE KELAS KLUSTER:
  □ Agen pengumpul log (Fluent Bit / Promtail) berjalan sebagai DaemonSet.
  □ Log parser dikonfigurasi untuk mem-parse JSON log secara otomatis (merge log on).
  □ Penyimpanan log utama dipisahkan antara hot storage (7-14 hari) dan cold archive (S3/GCS).
  □ Tingkat kebisingan log dikurangi dengan menyaring status ping health check dari system component.

Ringkasan #

  • Stdout/Stderr adalah Jalur Log Resmi — Tulis seluruh log aplikasi ke stdout/stderr; jangan kelola file log secara lokal di dalam disk kontainer karena akan hilang saat restart.
  • Log JSON Mempermudah Otomatisasi Kueri — Gunakan structured logging format JSON agar SIEM dan sistem visualisasi dapat melakukan pengindeksan data secara efisien tanpa parsing regex lambat.
  • Batasi Log Size untuk Proteksi Node — Konfigurasikan parameter rotasi log Kubelet untuk menghindari kepenuhan disk node worker akibat log loop tak terkontrol.
  • Gunakan Sidecar Streamer untuk Legacy App — Pasangkan aplikasi warisan yang menulis log ke file fisik dengan busybox sidecar untuk mem-bypass pembatasan I/O disk kontainer.
  • Loki untuk Efisiensi, EFK untuk Kecepatan — Evaluasi trade-off arsitektur log index; Loki menghemat storage secara signifikan, sedangkan Elasticsearch unggul pada kueri teks acak.
  • Downward API Memberikan Konteks Infrastruktur — Suntikkan nama namespace, nama Pod, dan node worker langsung ke variabel lingkungan aplikasi untuk memperkaya log runtime.

← Sebelumnya: Anti-Pattern Security   Berikutnya: Metrics dan Prometheus →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact