Observability #
Ada perbedaan mendasar antara sistem yang bisa dimonitor dan sistem yang bisa di-observe. Sistem yang bisa dimonitor memberitahu kamu ketika sesuatu sudah rusak — CPU 95%, response time naik, error rate melonjak. Sistem yang bisa di-observe memungkinkan kamu menjawab pertanyaan apapun tentang kondisi internalnya, termasuk pertanyaan yang belum pernah kamu pikirkan sebelumnya, hanya dari output yang dihasilkan sistem.
Perbedaan ini bukan semantik. Ketika terjadi insiden di production pada tengah malam, perbedaan antara tim yang bisa mengidentifikasi root cause dalam 10 menit dan tim yang menghabiskan 2 jam untuk debugging seringkali adalah seberapa baik sistem mereka di-observe. Sistem yang bisa di-observe memungkinkan kamu bertanya: “Kenapa request dari user X lambat tepat pada pukul 02:47?” dan mendapatkan jawaban yang konkret.
Observability dibangun di atas tiga pilar yang saling melengkapi: metrics (angka yang berubah seiring waktu), logs (catatan event yang terjadi), dan traces (perjalanan satu request melalui sistem). Ketiga pilar ini harus ada bersama — tidak ada satu pun yang cukup sendirian.
Tiga Pilar Observability #
graph TD
A[Observability] --> B["Metrics\nApa yang terjadi?"]
A --> C["Logs\nMengapa terjadi?"]
A --> D["Traces\nDi mana terjadi?"]
B --> B1["Prometheus\nGrafana"]
C --> C1["Structured Logs\nElasticsearch / Loki"]
D --> D1["OpenTelemetry\nJaeger / Zipkin"]
B --> B2["Alerting:\nSomething is wrong"]
C --> C2["Debugging:\nWhat went wrong"]
D --> D2["Profiling:\nWhere it went wrong"]Kapan menggunakan setiap pilar:
Metrics — "Ada yang salah dengan error rate"
→ Angka agregat yang berubah seiring waktu
→ Ideal untuk alerting, dashboard, SLO tracking
→ Contoh: requests/second, p99 latency, error rate, CPU usage
Logs — "Apa yang sebenarnya terjadi untuk request yang gagal ini?"
→ Event terstruktur dengan konteks lengkap
→ Ideal untuk debugging individual request/event
→ Contoh: "User 42 gagal login karena akun dikunci"
Traces — "Request ini lambat — di service/function mana waktunya habis?"
→ Perjalanan satu request end-to-end melalui semua komponen
→ Ideal untuk debugging performa dan dependency
→ Contoh: span menunjukkan 400ms dihabiskan di database query
Metrics: Prometheus dan Grafana #
Prometheus adalah sistem monitoring berbasis time series. Ia mengumpulkan (scrape) metrics dari aplikasi secara berkala dan menyimpannya untuk query dan alerting.
Empat Jenis Metric di Prometheus #
// 1. Counter — hanya naik, tidak pernah turun
// Cocok untuk: jumlah request, jumlah error, jumlah event
httpRequestsTotal := prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "http_requests_total",
Help: "Total HTTP requests",
},
[]string{"method", "endpoint", "status_code"}, // labels untuk dimensi
)
// Penggunaan:
httpRequestsTotal.WithLabelValues("GET", "/api/products", "200").Inc()
// 2. Gauge — bisa naik dan turun
// Cocok untuk: koneksi aktif, ukuran queue, penggunaan memory
activeConnections := prometheus.NewGauge(prometheus.GaugeOpts{
Name: "active_connections",
Help: "Number of active connections",
})
queueSize := prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: "queue_size",
Help: "Current number of items in queue",
},
[]string{"queue_name"},
)
activeConnections.Inc() // naik
activeConnections.Dec() // turun
activeConnections.Set(42) // set langsung
// 3. Histogram — distribusi nilai (untuk latency)
// Cocok untuk: latency, ukuran request/response
requestDurationSeconds := prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Name: "http_request_duration_seconds",
Help: "HTTP request duration",
Buckets: []float64{0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5},
// buckets = batas waktu dalam detik (5ms, 10ms, 25ms, ...)
},
[]string{"method", "endpoint"},
)
// Penggunaan — otomatis hitung distribusi
timer := prometheus.NewTimer(
requestDurationSeconds.WithLabelValues("GET", "/api/products"),
)
result := fetchProducts()
timer.ObserveDuration()
// 4. Summary — seperti Histogram tapi hitung quantile di client
// Jarang digunakan karena tidak bisa di-aggregate antar instance
requestProcessingSeconds := prometheus.NewSummary(prometheus.SummaryOpts{
Name: "request_processing_seconds",
Help: "Time spent processing request",
})
Instrumentasi Aplikasi Flask #
// Go: padanan middleware net/http
// Metrics
var (
requestCount = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "flask_request_count",
Help: "Flask Request Count",
},
[]string{"method", "endpoint", "status"},
)
requestLatency = prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Name: "flask_request_latency_seconds",
Help: "Flask Request Latency",
Buckets: []float64{0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0},
},
[]string{"method", "endpoint"},
)
)
// statusRecorder menangkap kode status response
type statusRecorder struct {
http.ResponseWriter
status int
}
// Middleware — berjalan sebelum dan sesudah setiap request
func metricsMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
rec := &statusRecorder{ResponseWriter: w, status: http.StatusOK}
next.ServeHTTP(rec, r)
requestCount.WithLabelValues(
r.Method, r.URL.Path, strconv.Itoa(rec.status),
).Inc()
requestLatency.WithLabelValues(r.Method, r.URL.Path).
Observe(time.Since(start).Seconds())
})
}
// Endpoint untuk Prometheus scrape
func metricsHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
promhttp.Handler().ServeHTTP(w, r)
}
# prometheus.yml — konfigurasi scraping
global:
scrape_interval: 15s # scrape setiap 15 detik
evaluation_interval: 15s # evaluasi alert setiap 15 detik
scrape_configs:
- job_name: 'myapp'
static_configs:
- targets: ['app:8000'] # host:port endpoint /metrics
metrics_path: '/metrics'
- job_name: 'node-exporter' # sistem metrics (CPU, memory, disk)
static_configs:
- targets: ['node-exporter:9100']
Logs: Structured Logging #
Log yang tidak terstruktur sulit di-query dan di-parse. Structured logging menggunakan JSON sehingga setiap field bisa di-filter dan di-aggregate.
// Go: padanan log/slog dari standard library
// requestID membawa request ID melalui context request
type ctxKey string
const requestIDKey ctxKey = "request_id"
// structuredFormatter mengeluarkan satu objek JSON per baris log
func structuredFormatter(r slog.Record) ([]byte, error) {
attrs := make(map[string]any)
attrs["timestamp"] = r.Time.Format("2006-01-02T15:04:05.000")
attrs["level"] = r.Level.String()
attrs["logger"] = "myapp"
attrs["message"] = r.Message
attrs["request_id"] = ""
attrs["service"] = "myapp"
attrs["environment"] = os.Getenv("APP_ENV")
r.Attrs(func(a slog.Attr) bool {
attrs[a.Key] = a.Value.Any()
return true
})
return json.Marshal(attrs)
}
// Middleware untuk inject request ID
func injectRequestID(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
requestID := r.Header.Get("X-Request-ID")
if requestID == "" {
requestID = uuid.New().String()
}
// Set juga di response header untuk client tracing
w.Header().Set("X-Request-ID", requestID)
next.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), requestIDKey, requestID)))
})
}
// logRequest mencatat baris terstruktur setelah setiap request
func logRequest(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
next.ServeHTTP(w, r)
logger.Info("HTTP request completed",
"method", r.Method,
"path", r.URL.Path,
"duration_ms", float64(time.Since(start).Microseconds())/1000,
"request_id", r.Context().Value(requestIDKey),
)
})
}
Contoh log terstruktur vs tidak terstruktur:
Tidak terstruktur (sulit di-parse):
[2025-06-01 14:23:45] ERROR User 42 gagal login dari IP 1.2.3.4 setelah 3 percobaan
Terstruktur (mudah di-filter dan di-aggregate):
{
"timestamp": "2025-06-01T14:23:45.123",
"level": "ERROR",
"message": "Login failed",
"user_id": 42,
"ip": "1.2.3.4",
"attempt_count": 3,
"reason": "account_locked",
"request_id": "req-abc123",
"service": "auth-service"
}
Dengan structured log, di Elasticsearch bisa query:
level:ERROR AND reason:account_locked AND @timestamp:[now-1h TO now]
Di Loki dengan LogQL:
{service="auth-service"} | json | reason="account_locked"
Distributed Tracing dengan OpenTelemetry #
Distributed tracing menghubungkan log dan metrics dari berbagai service menjadi satu narasi tentang perjalanan sebuah request.
// Go: padanan OpenTelemetry Go SDK
func setupTracing(serviceName string, otlpEndpoint string) {
// serviceName dan otlpEndpoint mengidentifikasi service dan collector ini
ctx := context.Background()
exporter, err := otlptracegrpc.New(ctx,
otlptracegrpc.WithEndpoint(otlpEndpoint),
otlptracegrpc.WithInsecure(),
)
if err != nil {
log.Fatal(err)
}
tp := sdktrace.NewTracerProvider(
sdktrace.WithBatcher(exporter),
sdktrace.WithResource(resource.NewWithAttributes(
semconv.SchemaURL,
semconv.ServiceName(serviceName),
attribute.String("service.version", os.Getenv("APP_VERSION")),
attribute.String("deployment.environment", os.Getenv("APP_ENV")),
)),
)
otel.SetTracerProvider(tp)
}
// Tracer untuk manual instrumentation
var tracer = otel.Tracer("myapp")
// Contoh manual span
func processOrder(ctx context.Context, orderData map[string]any) (map[string]any, error) {
ctx, span := tracer.Start(ctx, "process_order")
defer span.End()
// Tambahkan attribute yang berguna untuk debugging
span.SetAttributes(
attribute.String("order.id", orderData["id"].(string)),
attribute.Float64("order.total", orderData["total"].(float64)),
attribute.Int("order.item_count", len(orderData["items"].([]any))),
)
ctx, inventorySpan := tracer.Start(ctx, "validate_inventory")
inventoryOK := checkInventory(orderData["items"])
if !inventoryOK {
inventorySpan.RecordError(errors.New("Insufficient inventory"))
inventorySpan.SetStatus(codes.Error, "Insufficient inventory")
inventorySpan.End()
return nil, errors.New("insufficient inventory")
}
inventorySpan.End()
_, paymentSpan := tracer.Start(ctx, "process_payment")
paymentSpan.SetAttributes(attribute.Float64("payment.amount", orderData["total"].(float64)))
result := chargePayment(orderData)
paymentSpan.SetAttributes(attribute.String("payment.transaction_id", result["transaction_id"].(string)))
paymentSpan.End()
return finalizeOrder(orderData, result)
}
SLI, SLO, dan SLA: Bahasa Reliability #
Observability tidak hanya tentang teknis — ia juga tentang mendefinisikan dan mengukur keandalan dari perspektif pengguna.
Definisi:
SLI (Service Level Indicator):
→ Metrik konkret yang mengukur aspek keandalan
→ Angka yang bisa diukur: availability, latency, error rate, throughput
→ Contoh: "Persentase HTTP request yang selesai < 200ms"
SLO (Service Level Objective):
→ Target nilai untuk SLI yang disepakati secara internal
→ Biasanya dinyatakan sebagai persentase dalam periode waktu
→ Contoh: "99.9% request harus selesai < 200ms dalam 30 hari"
SLA (Service Level Agreement):
→ Kontrak resmi dengan konsekuensi jika SLO tidak terpenuhi
→ Biasanya antara provider dan customer
→ Contoh: "Jika availability < 99.9%, customer mendapat kredit"
Error Budget:
→ Jumlah "kegagalan yang diizinkan" sebelum melanggar SLO
→ SLO 99.9% dalam 30 hari = 0.1% × 30 × 24 × 60 = 43.2 menit downtime
→ Digunakan untuk keseimbangan: reliability vs kecepatan development
// Go: menghitung dan tracking SLI/SLO
// SLI 1: Availability (persentase request yang berhasil)
httpRequestsTotal := prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "http_requests_total",
Help: "Total HTTP requests",
},
[]string{"status_code"},
)
// Availability = successful requests / total requests
// Query Prometheus:
// sum(rate(http_requests_total{status_code!~"5.."}[30d]))
// /
// sum(rate(http_requests_total[30d]))
func calculateAvailabilitySLI() float64 {
// Implementasi via Prometheus API
return 0.0
}
// SLI 2: Latency (persentase request di bawah threshold)
requestDuration := prometheus.NewHistogram(prometheus.HistogramOpts{
Name: "http_request_duration_seconds",
Help: "Request duration",
Buckets: []float64{0.1, 0.2, 0.5, 1.0, 2.0, 5.0},
})
// Query untuk latency SLI:
// sum(rate(http_request_duration_seconds_bucket{le="0.2"}[30d]))
// /
// sum(rate(http_request_duration_seconds_count[30d]))
// → Persentase request yang selesai dalam 200ms
// Alert untuk SLO breach dan error budget burn rate
# Prometheus alerting rules untuk SLO
groups:
- name: slo_alerts
rules:
# Alert jika error rate melebihi threshold
- alert: HighErrorRate
expr: |
sum(rate(http_requests_total{status_code=~"5.."}[5m]))
/
sum(rate(http_requests_total[5m])) > 0.05
for: 5m
labels:
severity: warning
annotations:
summary: "Error rate > 5% selama 5 menit"
description: "Error rate: {{ $value | humanizePercentage }}"
# Alert untuk latency SLO
- alert: HighLatency
expr: |
histogram_quantile(0.99,
sum(rate(http_request_duration_seconds_bucket[5m])) by (le)
) > 1.0
for: 5m
labels:
severity: warning
annotations:
summary: "P99 latency > 1 detik"
description: "P99 latency: {{ $value }}s"
# Alert untuk error budget burn rate yang terlalu cepat
- alert: ErrorBudgetBurnRateFast
expr: |
(
sum(rate(http_requests_total{status_code=~"5.."}[1h]))
/
sum(rate(http_requests_total[1h]))
) > 0.001 * 14.4
# 14.4x normal burn rate = akan habis dalam 5 hari
for: 2m
labels:
severity: critical
annotations:
summary: "Error budget burn rate terlalu cepat"
Alerting yang Efektif #
Alert yang baik membuat engineer mengambil tindakan. Alert yang buruk menciptakan alert fatigue — banyak notifikasi yang pada akhirnya diabaikan.
Prinsip alerting yang efektif:
Alert hanya untuk hal yang butuh tindakan manusia SEKARANG
✗ Alert yang tidak butuh tindakan segera → notifikasi, bukan alert
✗ Alert yang selalu muncul tapi tidak pernah kritis → hilangkan
✓ Alert yang berarti: ada yang tidak beres dan butuh dilihat sekarang
Alert berdasarkan symptom, bukan cause
✗ "CPU 90%" — mungkin OK jika latency masih baik
✓ "P99 latency > 2 detik" — ini yang user rasakan
✗ "Disk 80% penuh" — belum tentu masalah sekarang
✓ "Disk akan penuh dalam 4 jam berdasarkan rate saat ini"
Severity yang jelas
P1/Critical: Butuh tindakan SEGERA, bangunkan on-call
→ Service down, data corruption, security breach
P2/Warning: Butuh tindakan dalam jam ini
→ Degraded performance, approaching limit
P3/Info: Perlu diketahui, tidak urgent
→ Scheduled tasks selesai, config changes
Setiap alert harus punya runbook
→ Link ke dokumentasi: apa yang harus dilakukan
→ Konteks: kenapa alert ini ada
→ Eskalasi: siapa yang harus dihubungi jika tidak terselesaikan
# Alertmanager routing — kirim alert ke channel yang tepat
global:
resolve_timeout: 5m
route:
group_by: ['alertname', 'cluster']
group_wait: 30s
group_interval: 5m
repeat_interval: 4h
receiver: 'slack-default'
routes:
# Critical alerts → PagerDuty (bangunkan on-call)
- match:
severity: critical
receiver: 'pagerduty-oncall'
continue: false
# Warning alerts → Slack
- match:
severity: warning
receiver: 'slack-alerts'
receivers:
- name: 'pagerduty-oncall'
pagerduty_configs:
- service_key: ${PAGERDUTY_KEY}
description: '{{ range .Alerts }}{{ .Annotations.summary }}{{ end }}'
- name: 'slack-alerts'
slack_configs:
- api_url: ${SLACK_WEBHOOK}
channel: '#alerts'
text: |
*Alert:* {{ .GroupLabels.alertname }}
*Summary:* {{ range .Alerts }}{{ .Annotations.summary }}{{ end }}
*Runbook:* {{ range .Alerts }}{{ .Annotations.runbook_url }}{{ end }}
Grafana Dashboard: Visualisasi yang Berguna #
Dashboard yang wajib ada untuk setiap service:
Overview Dashboard (untuk on-call):
→ Request rate (requests per second)
→ Error rate (%)
→ P50, P95, P99 latency
→ Active instances / uptime
Resource Dashboard:
→ CPU usage per instance
→ Memory usage (RSS, available)
→ Disk I/O dan utilization
→ Network throughput
Business Metrics Dashboard:
→ Transaksi per menit/jam
→ Revenue (jika relevan)
→ Conversion rate
→ Active users
Dependency Dashboard:
→ Database query latency
→ Cache hit rate
→ External API latency dan error rate
→ Circuit breaker state
Tips membuat dashboard yang berguna:
✓ Urutkan panel dari yang paling penting (kiri atas = paling kritis)
✓ Selalu tampilkan context: grafik saat ini vs kemarin vs minggu lalu
✓ Gunakan warna yang konsisten: merah = buruk, hijau = baik
✓ Tambahkan annotation untuk deployment dan insiden
✓ Buat dashboard yang bisa di-filter per environment dan instance
Anti-Pattern yang Harus Dihindari #
// Go: anti-pattern
// ✗ Anti-pattern 1: Log sebagai satu-satunya observability
// Tidak ada metrics, tidak ada tracing
// Debugging butuh grep manual di ribuan baris log
// ✓ Solusi: implementasi ketiga pilar — metrics, logs, traces
// ✗ Anti-pattern 2: Unstructured log
logger.Printf("User %d failed to login from %s after %d attempts", userID, ip, n)
// Tidak bisa di-query, tidak bisa di-aggregate, tidak bisa di-filter
// ✓ Solusi: structured JSON log dengan field yang konsisten
// ✗ Anti-pattern 3: Alert pada metrik yang tidak actionable
// alert: CPU > 80%
// Apa yang harus dilakukan? Siapa yang harus dihubungi?
// ✓ Solusi: alert pada symptom (latency, error rate) bukan cause
// ✗ Anti-pattern 4: Alert fatigue — terlalu banyak alert
// Tim mendapat 200 alert per hari, semua diabaikan
// ✓ Solusi: audit alert secara berkala, hapus yang tidak pernah actionable
// ✗ Anti-pattern 5: Tidak ada runbook untuk alert
// Alert berbunyi, on-call tidak tahu harus apa
// ✓ Solusi: setiap alert punya link runbook yang jelas
// ✗ Anti-pattern 6: Trace ID tidak di-propagate
// Trace mulai di API gateway tapi tidak diteruskan ke service lain
// Trace terfragmentasi, tidak bisa dilihat end-to-end
// ✓ Solusi: OpenTelemetry auto-instrumentation yang propagate trace context
// ✗ Anti-pattern 7: Dashboard yang terlalu banyak grafik
// 50+ panel di satu dashboard, sulit fokus saat insiden
// ✓ Solusi: dashboard ringkas untuk on-call, detail dashboard untuk debugging
Checklist Observability #
METRICS:
□ Empat golden signals dimonitor: latency, traffic, errors, saturation
□ Request rate, error rate, dan latency (P50/P95/P99) ada di setiap service
□ Business metrics kritis dimonitor (transaksi, conversion rate)
□ Dependency metrics: database query time, cache hit rate, external API latency
□ Resource metrics: CPU, memory, disk, network
LOGGING:
□ Structured logging (JSON) di semua service
□ Log level yang konsisten (DEBUG, INFO, WARN, ERROR, FATAL)
□ Request ID/Correlation ID di setiap log entry
□ Tidak ada data sensitif (password, token, PII) di log
□ Log di-aggregate ke central system (ELK, Loki, CloudWatch)
TRACING:
□ OpenTelemetry dikonfigurasi di semua service
□ Trace ID di-propagate ke semua downstream call
□ Span yang meaningful dengan attribute yang berguna
□ Traces dikirim ke backend (Jaeger, Zipkin, atau managed service)
SLO:
□ SLI didefinisikan untuk setiap service (availability, latency, error rate)
□ SLO target disepakati dengan stakeholder
□ Error budget dihitung dan di-track
□ Alert untuk error budget burn rate yang terlalu cepat
ALERTING:
□ Alert hanya untuk kondisi yang butuh tindakan manusia
□ Severity yang jelas (critical vs warning)
□ Setiap alert punya runbook
□ Alert routing yang tepat (critical → on-call, warning → Slack)
□ Alert di-review secara berkala untuk menghapus yang tidak actionable
DASHBOARD:
□ Overview dashboard untuk on-call (ringkas, informasi paling penting)
□ Detail dashboard untuk debugging
□ Dashboard bisa di-filter per environment
□ Annotations untuk deployment dan insiden
Ringkasan #
- Observability bukan monitoring — monitoring memberitahu ketika sesuatu rusak. Observability memungkinkan kamu menjawab pertanyaan apapun tentang sistem dari outputnya, bahkan pertanyaan yang belum pernah terpikirkan sebelumnya.
- Tiga pilar yang saling melengkapi — metrics untuk alerting dan trend, logs untuk debugging event spesifik, traces untuk menelusuri request end-to-end. Tidak ada satu pun yang cukup sendirian.
- Four Golden Signals — latency, traffic, errors, dan saturation adalah minimum yang harus dimonitor untuk setiap service. Ini yang paling langsung berkorelasi dengan pengalaman user.
- Structured logging adalah investasi — log JSON yang bisa di-query per field jauh lebih berguna dari log teks bebas saat insiden. Implementasi di awal, bukan refactor saat sudah ada masalah.
- Distributed tracing bergantung pada propagasi trace context — OpenTelemetry harus di-configure di semua service dan trace ID harus diteruskan ke setiap downstream call. Trace yang terfragmentasi tidak berguna.
- SLO mendefinisikan target keandalan yang nyata — tanpa SLO, tidak ada yang tahu seberapa andal sistem “seharusnya”. Error budget memungkinkan keseimbangan antara reliability dan kecepatan delivery.
- Alert pada symptom, bukan cause — P99 latency > 2 detik lebih actionable dari CPU > 80%. User merasakan latency, bukan CPU usage.
- Alert fatigue adalah pembunuh on-call — 200 alert per hari yang diabaikan lebih berbahaya dari tidak ada alert sama sekali. Audit alert secara berkala dan hapus yang tidak pernah actionable.
- Setiap alert butuh runbook — on-call yang menerima alert pada tengah malam harus tahu persis apa yang harus dilakukan, siapa yang harus dihubungi, dan bagaimana mengeskalasi.
- Dashboard dirancang untuk skenario penggunaan — dashboard on-call (ringkas, paling penting) berbeda dari dashboard debugging (detail). Jangan buat satu dashboard untuk semua.