A detailed backend reference

Logging, Monitoring & Observabilitas

Ini bukan fitur, ini praktik. Backend produksi tanpa keduanya adalah kotak hitam. Dengan keduanya, Anda bisa menentukan state internal seluruh sistem hanya dengan melihat apa yang ia pancarkan. Itulah definisi sistem yang observable.


Versi Sederhana

01

Kenapa Observabilitas Penting

Aplikasi backend modern berjalan di lingkungan terdistribusi: banyak service, banyak server, banyak region, pengguna tersebar di seluruh dunia. Sesuatu akan rusak. Pertanyaannya bukan jika, tapi kapan, dan yang lebih penting, seberapa cepat Anda bisa menemukannya dan memperbaikinya?

Tanpa logging, monitoring, dan observabilitas, saat insiden terjadi Anda buta total. Anda tahu sesuatu salah (pengguna mengeluh, konversi turun), tapi tidak tahu apa, di mana, atau kenapa. Setiap menit debugging dalam kegelapan adalah pendapatan hilang dan kepercayaan pengguna terkikis.

Important Framing

Semua ini diimplementasikan pada spektrum: tidak ada perusahaan yang jujur bisa bilang mereka mengikuti 100% semua praktik observabilitas. Tujuannya terus menaikkan cakupan Anda, bukan mencapai semacam state sempurna yang mitis. Jangan gentar oleh banyaknya tool dan praktik di ruang ini.

Praktik ini juga menuntut usaha kolektif. Sebagai developer, Anda meng-instrumentasi kode Anda. Tim DevOps / SRE mengonfigurasi infrastruktur yang mengumpulkan, menyimpan, dan menampilkan data itu. Tak satu sisi pun cukup sendirian.

INCIDENT something broke WITHOUT LMO blind debugging WITH LMO alert -> metrics -> logs -> trace hours of manual log grep / guessing exact function, line, user, timestamp same incident, different outcomes
02

Tiga Pilar Observabilitas

Teori observabilitas mendefinisikan tiga pilar fondasi. Sistem hanya sepenuhnya observable saat ketiganya ada. Setiap pilar menjawab pertanyaan berbeda tentang perilaku sistem Anda.

LOGS
What happened?
Timestamped records of discrete events in your application, a request came in, a database query ran, an error occurred. Think of logs as your backend's diary.
METRICS
What are the patterns?
Numerical measurements aggregated over time, request rate, error rate, latency p99, CPU usage. Metrics tell you trends, not individual events.
TRACES
Where did it go?
A trace follows a single request through every component it touched, handler -> service -> DB -> external API. Shows component interactions and latency per step.
Logs -> Answer
"User 42 tried to log in at 14:32:01 and failed, invalid password."
Metrics -> Answer
"In the last 5 minutes, 800 requests/sec, 12% error rate, p99 latency 340ms."
Traces -> Answer
"This request spent 240ms in the DB query layer and 80ms calling the email provider, that's where the latency is."
03

Monitoring vs Observabilitas: Perbedaannya

Istilah ini sering dipakai bergantian tapi mereka konsep berbeda di tingkat kemampuan yang berbeda.

DimensiMonitoringObservabilitas
Pertanyaan inti "Ada yang salah?" "Apa persisnya yang salah dan kenapa?"
Pendekatan Dashboard dan alert yang sudah didefinisikan pada metrik yang dikenal Investigasi ad-hoc memakai log + metrik + trace secara bersama
Cakupan Melacak metrik yang Anda putuskan diukur sebelumnya Bisa menjawab pertanyaan yang tidak Anda tahu untuk ditanyakan saat setup
Keluaran "CPU di 95%" -> alert menyala "CPU 95% karena fungsi X menjalankan query DB N+1 untuk segmen pengguna Y"
Analogi historis Dashboard mobil: kecepatan, bensin, suhu Sistem diagnostik penuh: memberi tahu komponen dan penyebab yang persis rusak
Yang dibutuhkan Koleksi metrik, aturan alert Log + Metrik + Trace semuanya terkorelasi dan tertaut
Historical Context

Satu dekade lalu, monitoring bentuk utama deteksi error. Anda set alert Nagios pada CPU dan memori lalu selesai. Gerakan observabilitas muncul karena di sistem microservice/terdistribusi, mengetahui bahwa sesuatu gagal hampir sia-sia tanpa mengetahui di mana di antara lusinan service kegagalan itu sebenarnya berasal.

Wawasan kunci: Monitoring memberi tahu Anda ada masalah. Observabilitas memberi tahu Anda persis apa masalahnya, fungsi spesifik, service spesifik, pengguna spesifik, query spesifik. Ini hanya mungkin jika Anda mengimplementasikan ketiga pilar dengan benar.

04

Cara Ketiganya Bekerja Bersama: Alur Kerja Debugging

Dalam praktik, saat insiden terjadi, Anda menavigasi dari sinyal berbutir kasar ke detail berbutir halus. Inilah alur kanonis yang dipakai sistem produksi:

  • 1. Alert menyala (Monitoring). Grafana / New Relic mendeteksi tingkat error > 80% dan menembakkan webhook Slack. Anda dapat pesan: "tingkat error service API melonjak di 14:32." Ini monitoring melakukan tugasnya.
  • 2. Pergi ke dashboard Metrik. Buka Grafana / New Relic. Anda melihat angka konkret: 840 request/menit, 82% mengembalikan 5xx, latensi p99 melonjak dari 120ms ke 4,2 detik di 14:31. Anda kini tahu skala masalahnya dan kapan ia mulai.
  • 3. Korelasikan ke Log. Dari tampilan metrik, klik ke log error terkait untuk jendela 2 menit itu. Anda melihat ratusan log: ERROR: connection pool exhausted after 4000ms. Kini Anda tahu apa yang gagal.
  • 4. Ikuti Trace. Klik salah satu log error itu. Ia menaut ke trace. Trace menampilkan perjalanan lengkap request: AuthMiddleware (2ms) -> UserHandler (5ms) -> UserService (3ms) -> DB.GetUser (3990ms <- DI SINI). Query DB-nya timeout. Akar masalah ditemukan.
  • 5. Perbaiki & Konfirmasi dengan Metrik. Anda menambah index yang hilang ke query. Dalam hitungan menit, dashboard Grafana menunjukkan tingkat error turun kembali ke 0,2%, latensi kembali ke 110ms. Monitoring mengonfirmasi perbaikan bekerja.
The Power of Correlation

Alert -> Metrik -> Log -> Trace hanya mungkin saat ketiga pilar terkorelasi. Tool seperti Grafana, New Relic, dan Datadog menautkannya memakai traceid / requestid bersama yang mengalir lewat setiap komponen. Inilah kenapa men-set request ID di middleware dan meneruskannya ke mana-mana itu kritis.

05

Logging: Selami Mendalam

Logging adalah praktik mencatat semua peristiwa penting di sepanjang siklus eksekusi aplikasi Anda. Anggap ia jurnal yang dipelihara backend Anda, entri ber-timestamp, terperinci, terstruktur untuk setiap hal signifikan yang terjadi.

Apa yang Harus Anda Log?

Aturan praktis yang baik: apa pun yang ingin diketahui developer saat men-debug masalah jam 2 pagi, tanpa akses ke sistem live, tanpa kemampuan mereproduksi bug, hanya bermodal log.

Business events
User created a to-do. Order was placed. Payment succeeded. Subscription cancelled. These are the events that matter to the business and to auditing.
Security events
Login attempt (success or failure). Password reset. Token revoked. Admin action. These are critical for security audits and intrusion detection.
System events
Server startup / shutdown. Database connection established / lost. Background job started / finished. Config loaded.
Errors & exceptions
Every unhandled error, every caught exception, every failed external API call. Always include: timestamp, user ID, request ID, error type, stack trace.
Performance signals
Slow queries (> 500ms), large payload responses, DB connection pool warnings. These are logs that become metrics over time.

Metadata Apa yang Disertakan di Setiap Log

Log tanpa konteks hampir sia-sia. Setiap entri log harus membawa metadata yang cukup untuk menjawab: siapa melakukan apa, kapan, di mana, dan dengan hasil apa?

// A well-structured log entry (JSON, production format)
{
  "timestamp":   "2024-01-15T14:32:01.234Z",
  "level":        "error",
  "message":      "database query failed",
  "service":      "todo-api",
  "environment":  "production",
  "request_id":   "req_01J2KXYZ",    // unique per request
  "trace_id":     "abc123def456",    // links to distributed trace
  "span_id":      "span_789",
  "user_id":      "usr_01J2K",
  "method":       "POST",
  "path":         "/todos",
  "error":        "pq: deadlock detected",
  "duration_ms":  4001,
  "host":         "worker-node-3"
}
JSON Log Entry
06

Log Level

Setiap entri log diberi tingkat keparahan. Ini membuat Anda bisa memfilter apa yang dilihat, di development Anda ingin semuanya, di produksi Anda peduli warning ke atas. Library logging Anda akan membiarkan Anda men-set level minimum per lingkungan.

DEBUG
Verbose diagnostic info for development only. Variable values, function entry/exit, loop iterations. Never enable in production, the volume would overwhelm your log pipeline and cost a fortune in storage. Used when troubleshooting a specific issue locally.
INFO
Normal, successful application operations. Server started. User logged in. To-do created. Background job completed. These record the normal flow of business events. This is the default level for production.
WARN
Something unexpected, but not a failure. A user typed the wrong password (not your fault). An API responded slowly but successfully. A deprecated config option is still in use. Something you should investigate eventually but the system is still working.
ERROR
Something failed and needs attention. Database query failed. External API returned 500. Validation failed in a way that should not have happened. A task handler threw an unhandled exception. This is the level you build alerts around.
FATAL
The application cannot continue and is shutting down. Cannot connect to database on startup. Config file missing. Critical dependency unavailable. After logging FATAL, the process terminates. Infrastructure will restart it. Use sparingly, only for truly unrecoverable states.
Level Hierarchy

Level membentuk hirarki: DEBUG < INFO < WARN < ERROR < FATAL. Men-set logger ke INFO berarti ia memancarkan INFO, WARN, ERROR, dan FATAL, tapi menekan DEBUG. Men-set ke ERROR berarti hanya ERROR dan FATAL yang dipancarkan. Begitulah Anda mengendalikan volume log per lingkungan.

Konfigurasi Level Khusus Lingkungan (Go)

// logger/logger.go, Environment-aware log level
package logger

import (
    "os"
    "go.uber.org/zap"
    "go.uber.org/zap/zapcore"
)

func getLogLevel() zapcore.Level {
    env := os.Getenv("APP_ENV")
    switch env {
    case "production":
        return zapcore.InfoLevel   // production: INFO and above
    case "staging":
        return zapcore.WarnLevel   // staging: WARN and above only
    default:
        return zapcore.DebugLevel  // local dev: everything
    }
}

func New() *zap.Logger {
    env := os.Getenv("APP_ENV")
    level := getLogLevel()

    var cfg zap.Config
    if env == "production" {
        // Production: JSON format, parseable by Loki, ELK, New Relic
        cfg = zap.NewProductionConfig()
        cfg.EncoderConfig.TimeKey = "timestamp"
        cfg.EncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
    } else {
        // Development: coloured console format, human-readable
        cfg = zap.NewDevelopmentConfig()
        cfg.EncoderConfig.EncodeLevel = zapcore.CapitalColorLevelEncoder
    }

    cfg.Level = zap.NewAtomicLevelAt(level)
    logger, _ := cfg.Build()
    return logger
}
Go / Zap
07

Logging Terstruktur vs Tidak Terstruktur

Ada dua cara fundamental menulis entri log. Pilihan yang mana tergantung lingkungan Anda.

SifatTidak Terstruktur (Teks Polos)Terstruktur (JSON)
Format 2024-01-15 14:32 ERROR: DB query failed for user 42 {"level":"error","user_id":"42","error":"pq: timeout",...}
Keterbacaan manusia Mudah dibaca di terminal Lebih sulit dibaca tanpa viewer
Keteruraian mesin Sulit, butuh regex untuk mengekstrak user_id, pesan error, dll. JSON asli, tool apa pun bisa mem-parse seketika
Ekstraksi field Pola regex rawan error Akses field langsung: log.user_id
Query di Loki/ELK Kompleks, pola grep rapuh {user_id="42"} |= "error", sepele
Dipakai di Pengembangan lokal Staging & Produksi

Seperti Apa Log Terstruktur Berdampingan

// UNSTRUCTURED, development console (human-friendly)
14:32:01 DEBUG  Connected to PostgreSQL on localhost:5432
14:32:01 INFO   Started background job worker
14:32:01 INFO   HTTP server listening on :8080
14:32:14 WARN   Slow query detected: 520ms  query=SELECT * FROM todos WHERE user_id=$1
14:32:15 ERROR  Failed to send email  user_id=usr_01J2K  error=connection refused
Console (Dev)
// STRUCTURED, production JSON (machine-friendly)
{"timestamp":"2024-01-15T14:32:14Z","level":"warn","msg":"slow query","duration_ms":520,"query":"SELECT * FROM todos WHERE user_id=$1","service":"todo-api","env":"production"}
{"timestamp":"2024-01-15T14:32:15Z","level":"error","msg":"failed to send email","user_id":"usr_01J2K","error":"connection refused","trace_id":"abc123","span_id":"span_001","service":"todo-api","env":"production"}
JSON (Production)
Why JSON in Production?

Sistem manajemen log (Loki, ELK, New Relic, Datadog) menelan ribuan baris log per detik. Jika log teks polos, tool ini harus memakai regex untuk mengekstrak field seperti userid, error, durationms, yang lambat, rawan error, dan rapuh. JSON membuat mereka mem-parse field secara natif dalam O(1). Selalu pakai JSON di produksi.

08

Metrik: Selami Mendalam

Metrik adalah pengukuran numerik perilaku sistem Anda, digabungkan seiring waktu. Tidak seperti log (satu entri per peristiwa), metrik adalah counter atau gauge yang merangkum banyak peristiwa menjadi satu angka, mis. "320 request dalam 15 detik terakhir". Ini membuatnya sangat efisien untuk dashboard dan alerting.

Empat Tipe Metrik Inti (Model Prometheus)

Counter
A value that only ever goes up (and resets on restart). Used for: total requests served, total errors, total tasks processed. Query: rate(http_requests_total[5m]) = requests/sec over last 5 min.
Gauge
A value that can go up or down. Used for: current queue depth, current active DB connections, current goroutine count, memory usage right now.
Histogram
Records observations in configurable buckets. Used for: request latency, response size. Lets you compute percentiles (p50, p95, p99), crucial for SLOs. "95% of requests complete under 200ms."
Summary
Similar to Histogram but calculates percentiles client-side. Less flexible for aggregation across multiple instances. Prefer Histograms in most cases.

Metrik Kunci yang Harus Diekspos Setiap Backend

MetrikTipeKenapa Penting
httprequeststotalCounterThroughput request. Label: method, path, status_code.
httprequestduration_secondsHistogramDistribusi latensi. p99 memberi tahu pengalaman pengguna kasus terburuk.
httperrorstotalCounterJumlah respons 4xx / 5xx. Lonjakan = ada yang rusak.
dbqueryduration_secondsHistogramQuery DB lambat. Lonjakan menandakan index hilang atau masalah N+1.
dbconnectionsopenGaugeRisiko kehabisan pool. Mendekati maks = Anda butuh koneksi lebih atau query lebih sedikit.
taskqueuedepthGaugeTumpukan kerja di sistem background task Anda. Naik = worker perlu scaling.
taskprocessingduration_secondsHistogramBerapa lama background task berjalan. Berguna untuk pelacakan SLA.
gogoroutines / processresidentmemorybytesGaugeKesehatan runtime. Kebocoran goroutine = ini terus naik.

Interval Scrape & Jeda

Prometheus men-scrape endpoint /metrics Anda pada interval terkonfigurasi (biasanya 15 detik). Ini berarti ada jeda bawaan ±10-15 detik antara kenyataan dan yang Anda lihat di dashboard. Ini bisa diterima untuk kebanyakan kasus. Untuk alerting yang mendekati real-time, pakai interval scrape di bawah 15 detik, tapi sadari ini menaikkan beban di service Anda dan Prometheus.

09

Distributed Tracing

Trace adalah catatan lengkap perjalanan satu request melewati sistem Anda. Setiap langkah perjalanan itu disebut span. Span ditautkan oleh trace_id bersama yang dibuat saat request pertama masuk ke sistem Anda dan disebarkan lewat setiap panggilan berikutnya.

TRACE: abc123def456, total: 340ms HTTP Handler 340ms AuthMiddleware 28ms TodoService 280ms DB.GetTodos 240ms <- slow! JSONSerialise 22ms <- time -> (each bar shows the span's start and duration relative to trace start)

Trace vs Span: Definisi

Trace
The entire end-to-end journey of a single request. Identified by a globally unique trace_id. Contains all spans for that request.
Span
A single unit of work within a trace, one function call, one DB query, one HTTP call. Has a start time, duration, parent span ID, and arbitrary attributes (user_id, query, etc.).
Root Span
The first span in a trace. Typically the HTTP handler or the entry point of the request. Has no parent.
Context Propagation
The mechanism by which trace_id and span_id are passed from function to function (via Go's context.Context), and from service to service (via HTTP headers like traceparent).
traceparent header
W3C standard header: traceparent: 00-abc123-span001-01. When your service calls another service, it includes this header so the receiving service can join its spans to the same trace.
Why Traces Are Different from Logs

Entri log berbunyi "error ini terjadi". Trace berbunyi "error ini terjadi di sini, di titik ini dalam perjalanan request, setelah langkah-langkah pendahulu ini, dan ia menyebabkan total request memakan 4,2 detik alih-alih 50ms". Trace memberi konteks kausal yang tidak bisa diberikan log terisolasi.

10

Instrumentasi & OpenTelemetry

Instrumentasi adalah tindakan menambah kode ke aplikasi Anda untuk mengukur perilakunya, mencatat span, memancarkan metrik, memperkaya log dengan konteks. Anda tidak bisa mengamati yang tidak Anda instrumentasi.

OpenTelemetry (OTel) adalah standar instrumentasi open source dan netral-vendor. Ia menyediakan API, SDK, dan agen (the Collector) untuk semua bahasa utama. Nilai kuncinya: instrumentasi sekali, kirim ke backend mana pun (Jaeger, Grafana Tempo, New Relic, Datadog, Honeycomb) cukup dengan mengubah konfigurasi, tanpa perubahan kode.

YOUR APP OTel SDK instrumented OTEL COLLECTOR receives, processes, exports Jaeger / Tempo Prometheus New Relic / DD Loki (logs) OTLP/gRPC instrument once -> send anywhere

Konsep Inti OTel

Tracer
The OTel object that creates spans. Each component (service, library) gets its own named tracer. otel.Tracer("todo-service")
Span
Created by the tracer. Has a name, start/end time, status, and key-value attributes. OTel spans automatically link to parent spans via context.
Context Propagation
OTel automatically injects/extracts traceparent headers in HTTP clients and servers (with the right libraries). Zero manual work for standard HTTP.
OTel Collector
A standalone agent/sidecar that receives telemetry from your app (via OTLP protocol), optionally processes/filters it, and exports to one or more backends. Decouples instrumentation from backend choice.
Auto-instrumentation
OTel provides libraries that auto-instrument popular frameworks (net/http, gin, gRPC, database/sql, redis, etc.) with zero code changes. You get traces for DB queries and HTTP calls for free.
11

Kode: Go: Implementasi LMO Lengkap

Contoh lengkap yang menunjukkan logging (Zap), metrik (Prometheus), dan tracing (OpenTelemetry) dirangkai ke handler service Go. Ini pola dari aplikasi to-do di kuliah.

Setup Logger (Zap)

// logger/logger.go
package logger

import (
    "os"
    "go.uber.org/zap"
    "go.uber.org/zap/zapcore"
)

var Log *zap.Logger

func Init() {
    env := os.Getenv("APP_ENV")
    var cfg zap.Config

    if env == "production" {
        cfg = zap.NewProductionConfig()          // JSON output
        cfg.Level = zap.NewAtomicLevelAt(zapcore.InfoLevel)
    } else {
        cfg = zap.NewDevelopmentConfig()         // coloured console
        cfg.Level = zap.NewAtomicLevelAt(zapcore.DebugLevel)
        cfg.EncoderConfig.EncodeLevel = zapcore.CapitalColorLevelEncoder
    }

    Log, _ = cfg.Build(
        // Always include these fields in every log entry
        zap.Fields(
            zap.String("service", "todo-api"),
            zap.String("env", env),
        ),
    )
}
Go / Zap

Tracing Middleware (OpenTelemetry)

// middleware/tracing.go, Creates a trace span per request
package middleware

import (
    "github.com/gin-gonic/gin"
    "go.opentelemetry.io/otel"
    "go.opentelemetry.io/otel/attribute"
    "go.opentelemetry.io/otel/codes"
    "go.uber.org/zap"
    "myapp/logger"
)

func EnhancedTracing() gin.HandlerFunc {
    tracer := otel.Tracer("todo-api")

    return func(c *gin.Context) {
        // Extract incoming trace context (from load balancer / front-end)
        ctx := otel.GetTextMapPropagator().Extract(
            c.Request.Context(),
            propagation.HeaderCarrier(c.Request.Header),
        )

        // Start root span for this request
        ctx, span := tracer.Start(ctx, c.FullPath())
        defer span.End()

        // Attach metadata to span (visible in trace UI)
        span.SetAttributes(
            attribute.String("http.method", c.Request.Method),
            attribute.String("http.path", c.FullPath()),
            attribute.String("http.user_agent", c.Request.UserAgent()),
            attribute.String("user.id", getUserID(c)),
        )

        // Inject context so downstream handlers can create child spans
        c.Request = c.Request.WithContext(ctx)
        c.Next()

        // After handler returns: record status code on span
        statusCode := c.Writer.Status()
        span.SetAttributes(attribute.Int("http.status_code", statusCode))
        if statusCode >= 500 {
            span.SetStatus(codes.Error, "server error")
        }

        // Structured log entry linking to this trace
        logger.Log.Info("request completed",
            zap.String("method", c.Request.Method),
            zap.String("path", c.FullPath()),
            zap.Int("status", statusCode),
            zap.String("trace_id", span.SpanContext().TraceID().String()),
        )
    }
}
Go / Gin + OTel

Service Layer: Logging + Tracing + Penanganan Error

// services/todo_service.go, The full LMO pattern per function
package services

import (
    "context"
    "fmt"
    "go.opentelemetry.io/otel"
    "go.opentelemetry.io/otel/attribute"
    "go.opentelemetry.io/otel/codes"
    "go.uber.org/zap"
    "myapp/logger"
    "myapp/models"
)

var tracer = otel.Tracer("todo-service")

func CreateTodo(ctx context.Context, req models.CreateTodoRequest, userID string) (*models.Todo, error) {
    // 1. Start a child span (parent span is in ctx from the middleware)
    ctx, span := tracer.Start(ctx, "TodoService.CreateTodo")
    defer span.End()

    // 2. Add business context to the span, visible in trace UI
    span.SetAttributes(
        attribute.String("user.id", userID),
        attribute.String("todo.title", req.Title),
        attribute.String("todo.priority", req.Priority),
    )

    // 3. INFO log, record business event start
    logger.Log.Info("creating todo",
        zap.String("user_id", userID),
        zap.String("title", req.Title),
        zap.String("trace_id", span.SpanContext().TraceID().String()),
    )

    // 4. Execute DB operation (inside its own child span, auto via otelgorm)
    todo, err := repo.CreateTodo(ctx, req, userID)
    if err != nil {
        // ERROR log with full context
        logger.Log.Error("failed to create todo",
            zap.String("user_id", userID),
            zap.String("error", err.Error()),
            zap.String("trace_id", span.SpanContext().TraceID().String()),
        )
        // Record error on span, shows as failed in Jaeger/Grafana Tempo
        span.RecordError(err)
        span.SetStatus(codes.Error, err.Error())
        return nil, fmt.Errorf("repo.CreateTodo: %w", err)
    }

    // 5. DEBUG log, only visible in dev, suppressed in production
    logger.Log.Debug("todo created",
        zap.String("todo_id", todo.ID),
    )

    // 6. INFO, business event completion log (for audit trail)
    logger.Log.Info("todo created successfully",
        zap.String("todo_id", todo.ID),
        zap.String("user_id", userID),
        zap.String("title", todo.Title),
        zap.String("priority", todo.Priority),
        zap.String("category_id", todo.CategoryID),
    )

    return todo, nil
}
Go / Zap + OTel

Prometheus Middleware (Metrik)

// middleware/metrics.go, HTTP metrics instrumentation
package middleware

import (
    "strconv"
    "time"
    "github.com/gin-gonic/gin"
    "github.com/prometheus/client_golang/prometheus"
    "github.com/prometheus/client_golang/prometheus/promauto"
)

var (
    httpRequestsTotal = promauto.NewCounterVec(prometheus.CounterOpts{
        Name: "http_requests_total",
        Help: "Total HTTP requests",
    }, []string{"method", "path", "status"})

    httpDuration = promauto.NewHistogramVec(prometheus.HistogramOpts{
        Name:    "http_request_duration_seconds",
        Help:    "HTTP request latency",
        Buckets: []float64{.005, .01, .025, .05, .1, .25, .5, 1, 2.5, 5},
    }, []string{"method", "path"})
)

func PrometheusMetrics() gin.HandlerFunc {
    return func(c *gin.Context) {
        start := time.Now()
        c.Next()
        duration := time.Since(start)

        status := strconv.Itoa(c.Writer.Status())
        httpRequestsTotal.WithLabelValues(c.Request.Method, c.FullPath(), status).Inc()
        httpDuration.WithLabelValues(c.Request.Method, c.FullPath()).Observe(duration.Seconds())
    }
}
Go / Gin + Prometheus
12

Kode: Python: Implementasi LMO Lengkap

Structured Logging dengan structlog

# logging_config.py, structlog setup for Python
import logging
import os
import structlog

def configure_logging():
    env = os.getenv("APP_ENV", "development")

    if env == "production":
        # JSON renderer for production, parseable by Loki / ELK
        renderer = structlog.processors.JSONRenderer()
        level = logging.INFO
    else:
        # ColourfulConsole for local dev, human-readable
        renderer = structlog.dev.ConsoleRenderer(colors=True)
        level = logging.DEBUG

    structlog.configure(
        processors=[
            structlog.contextvars.merge_contextvars,      # merge request-scoped context
            structlog.processors.TimeStamper(fmt="iso"),
            structlog.stdlib.add_log_level,
            structlog.stdlib.add_logger_name,
            structlog.processors.StackInfoRenderer(),
            structlog.processors.ExceptionRenderer(),     # auto-renders exceptions
            renderer,
        ],
        logger_factory=structlog.stdlib.LoggerFactory(),
        wrapper_class=structlog.stdlib.BoundLogger,
        cache_logger_on_first_use=True,
    )

    logging.basicConfig(level=level)

log = structlog.get_logger()
Python / structlog

OpenTelemetry Tracing di FastAPI

# tracing.py, OTel setup for FastAPI
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
from opentelemetry.instrumentation.sqlalchemy import SQLAlchemyInstrumentor

def setup_tracing(app):
    # Tracer provider, sends spans to OTel Collector via gRPC
    provider = TracerProvider()
    exporter = OTLPSpanExporter(endpoint="http://otel-collector:4317")
    provider.add_span_processor(BatchSpanProcessor(exporter))
    trace.set_tracer_provider(provider)

    # Auto-instrument FastAPI, all routes get spans automatically
    FastAPIInstrumentor.instrument_app(app)

    # Auto-instrument SQLAlchemy, all DB queries get spans automatically
    SQLAlchemyInstrumentor().instrument(engine=engine)

# main.py, wire it all up
from fastapi import FastAPI
from tracing import setup_tracing
from logging_config import configure_logging, log

configure_logging()
app = FastAPI()
setup_tracing(app)
Python / FastAPI + OTel

Service Layer dengan Pola LMO Lengkap

# services/todo_service.py
from opentelemetry import trace
import structlog

tracer = trace.get_tracer("todo-service")
log = structlog.get_logger()

async def create_todo(user_id: str, req: CreateTodoRequest) -> Todo:
    # 1. Start child span
    with tracer.start_as_current_span("TodoService.create_todo") as span:
        span.set_attribute("user.id", user_id)
        span.set_attribute("todo.title", req.title)

        # 2. INFO log, business event start
        log.info("creating_todo", user_id=user_id, title=req.title)

        try:
            todo = await repo.create_todo(user_id, req)

            # 3. Business event log, audit trail
            log.info("todo_created",
                todo_id=todo.id,
                user_id=user_id,
                title=todo.title,
                priority=todo.priority,
            )
            return todo

        except Exception as e:
            # 4. ERROR log + span error recording
            log.error("todo_creation_failed", user_id=user_id, error=str(e))
            span.record_exception(e)
            span.set_status(trace.StatusCode.ERROR, str(e))
            raise
Python / FastAPI + OTel + structlog
13

Tooling: Stack Open Source

Stack observabilitas open source adalah yang dijalankan kebanyakan perusahaan besar. Lebih banyak kendali, tanpa vendor lock-in, tanpa harga per-seat, tapi butuh keahlian operasional untuk memeliharanya.

ToolPilarPeran
Prometheus Metrik Men-scrape endpoint /metrics setiap 15 detik, menyimpan data time-series, mengevaluasi aturan alert. Backend metrik standar de facto.
Grafana Dashboard Lapisan visualisasi. Meng-query Prometheus, Loki, Tempo, dan lainnya. Bangun dashboard, set ambang alert, buat runbook on-call.
Loki Log Sistem agregasi log oleh Grafana Labs. Tidak seperti ELK, ia hanya meng-index metadata (label), bukan full-text, jauh lebih murah. Bisa di-query via LogQL. Push-based lewat agen Promtail.
Promtail Log (agen) Berjalan di tiap server, men-tail file log atau stream log Docker/Kubernetes, lalu mengirimkannya ke Loki. Tanpa perubahan kode sama sekali.
Grafana Tempo Trace Backend distributed tracing. Menerima span OTel, menyimpannya murah (object storage), bisa di-query dari Grafana. Menggantikan Jaeger untuk setup baru.
Jaeger Trace Sistem tracing terdistribusi yang lebih tua (CNCF). Masih banyak dipakai. UI untuk menavigasi trace, grafik dependensi service. Kini merekomendasikan migrasi ke SDK OTel.
Alertmanager Alerting Merutekan alert Prometheus ke Slack, PagerDuty, email, dll. Menangani deduplikasi, pengelompokan, dan peredaman alert.
OTel Collector Ketiganya Menerima semua telemetri dari aplikasi, memproses/memperkayanya, dan menyebar ke Prometheus + Loki + Tempo. Pipeline telemetri terpusat.

Arsitektur Stack Lengkap

API Service Worker Auth Service Promtail (log agent) OTEL COLLECTOR Prometheus Loki Grafana Tempo GRAFANA dashboards alerts The Grafana Stack: logs + metrics + traces in one unified dashboard
14

Tooling: Solusi Proprietary

Jika Anda tidak punya tim untuk mengelola stack open source, solusi all-in-one proprietary pilihan pragmatis. Mereka menggabungkan logging, metrik, tracing, dan alerting ke satu platform dengan penyimpanan terkelola dan UI yang rapi.

ToolKekuatanTerbaik untukModel Harga
New Relic Observabilitas full-stack, agen Go/Python/Node, APM, monitoring browser, mobile. Setup 10 menit yang mudah. Tim yang mau satu tool dengan overhead ops minimal Tier gratis (100GB/bulan), lalu per GB yang ditelan
Datadog Dashboard terdepan industri, 500+ integrasi, APM sangat baik, monitoring infra, keamanan. Sangat kuat. Perusahaan dengan infra kompleks, Kubernetes, multi-cloud Per host + per GB, bisa cepat mahal
Sentry Pelacakan error kelas terbaik. Mengelompokkan error identik, menampilkan stack trace, konteks pengguna, pelacakan rilis. Monitoring error secara khusus, berpasangan baik dengan Grafana untuk metrik Tier gratis (5k error/bulan), lalu per peristiwa
Honeycomb Query peristiwa high-cardinality. Dibangun untuk tim observability-first. Dukungan OTel natif. Tim yang melakukan distributed tracing lanjutan, banyak microservice Per peristiwa yang ditelan

New Relic Go Agent: Integrasi Cepat

// main.go, New Relic agent setup in Go
import (
    "github.com/newrelic/go-agent/v3/newrelic"
    "github.com/newrelic/go-agent/v3/integrations/nrgin"
)

func main() {
    // Initialize New Relic application agent
    nrApp, _ := newrelic.NewApplication(
        newrelic.ConfigAppName("todo-api"),
        newrelic.ConfigLicense(os.Getenv("NEW_RELIC_LICENSE_KEY")),
        newrelic.ConfigAppLogForwardingEnabled(true),  // forward logs to NR
        newrelic.ConfigDistributedTracerEnabled(true), // enable distributed tracing
    )

    r := gin.New()
    // nrgin middleware, auto-instruments every route with NR transactions
    r.Use(nrgin.Middleware(nrApp))

    r.Run(":8080")
}
Go / New Relic
What New Relic shows you

Setelah menambah middleware, New Relic otomatis menangkap: waktu transaksi per route, tingkat error, throughput (req/detik), skor Apdex, metrik runtime Go (jeda GC, jumlah goroutine, memori), dan trace terdistribusi. Dashboard menampilkan semua ini dalam hitungan menit setelah deploy.

15

ELK Stack: Elasticsearch, Logstash, Kibana

ELK Stack (kini disebut Elastic Stack) adalah platform logging enterprise klasik. Ia menyediakan full-text search lintas log, agregasi kuat, dan dashboard kaya. Lebih berat daripada Loki tapi menawarkan fleksibilitas query lebih banyak.

Elasticsearch
The storage and search engine. Indexes every field of every log document. Supports complex queries: full-text search, aggregations, range filters. Horizontally scalable.
Logstash
The log pipeline. Ingests logs from many sources (files, Kafka, Redis, Beats agents), transforms/filters them (parse JSON, add geo-IP, mask PII), and outputs to Elasticsearch. Powerful but resource-heavy.
Kibana
The visualization layer. Build dashboards, run queries against Elasticsearch (KQL language), create alerts. The UI equivalent of Grafana for Elastic data.
Filebeat / Fluentd
Lightweight log shipping agents that run on your servers and ship log files to Logstash or Elasticsearch directly. Lower resource overhead than Logstash as a local agent.

ELK vs Grafana Loki

SifatELK StackGrafana Loki
IndexingFull-text meng-index setiap field -> query kuatHanya meng-index label (metadata) -> penyimpanan lebih murah
Biaya penyimpananTinggi, meng-index semua field mahalRendah, seperti Prometheus untuk log
Daya querySangat tinggi, KQL, agregasi kompleksBagus untuk query berbasis label (LogQL)
IntegrasiUI Kibana yang mandiriGrafana, satu UI untuk metrik + log + trace
Terbaik untukKompleksitas query tinggi, kasus keamanan/kepatuhanSetup yang sensitif biaya dan sudah memakai stack Grafana
16

Desain Alert: Apa yang Di-alert

Tidak setiap metrik butuh alert. Alert fatigue masalah nyata, jika engineer menerima terlalu banyak notifikasi, mereka mulai mengabaikannya. Desain alert yang baik berarti alert pada gejala yang berdampak pada pengguna, bukan detail implementasi internal.

Empat Golden Signal (Buku Google SRE)

Latency

Waktu melayani sebuah request. Bedakan request sukses dan gagal, error yang cepat bukan metrik yang baik. Alert pada p99 > ambang.

Traffic

Permintaan pada sistem Anda, request/detik. Penurunan mendadak bisa menandakan pemadaman sama seperti lonjakan mendadak. Alert pada kedua ekstrem.

Errors

Tingkat request gagal (5xx). Alert pada tingkat error, bukan jumlah mentah, lonjakan lalu lintas secara alami menyebabkan lebih banyak error bahkan jika tingkat errornya stabil.

Saturation

Seberapa "penuh" service Anda, CPU, memori, koneksi DB, kedalaman antrean. Alert sebelum sistem terjungkal ke kegagalan. Alert di 80%, page di 95%.

Aturan Alert (format Grafana / Prometheus)

# alert_rules.yml, Production-grade alert rules
groups:
  - name: api_health
    rules:

      # P1, Error rate critical
      - alert: HighErrorRate
        expr: |
          rate(http_requests_total{status=~"5.."}[5m])
          / rate(http_requests_total[5m]) * 100 > 5
        for: 2m
        labels:
          severity: critical
        annotations:
          summary: "HTTP error rate above 5% ({{ $value | printf \"%.1f\" }}%)"
          runbook: "https://wiki.company.com/runbooks/high-error-rate"

      # P2, High latency
      - alert: HighP99Latency
        expr: |
          histogram_quantile(0.99,
            rate(http_request_duration_seconds_bucket[5m])
          ) > 2
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "P99 latency above 2s ({{ $value | printf \"%.2f\" }}s)"

      # P2, DB connection pool near exhaustion
      - alert: DBConnectionPoolExhaustion
        expr: db_connections_open / db_connections_max > 0.85
        for: 3m
        labels:
          severity: warning
        annotations:
          summary: "DB connection pool at {{ $value | humanizePercentage }}"

      # P1, Service is down
      - alert: ServiceDown
        expr: up{job="todo-api"} == 0
        for: 1m
        labels:
          severity: critical
        annotations:
          summary: "todo-api service is unreachable"
YAML / Prometheus Rules
Alert Severity Levels

Definisikan setidaknya dua tingkat: warning (selidiki saat nyaman, jam kerja) dan critical (page on-call seketika, jam apa pun). Membangunkan engineer jam 3 pagi untuk masalah tingkat warning adalah cara membakar tim Anda dan mengajari mereka mengabaikan alert.

17

Alur Kerja Debugging Produksi: Contoh Lengkap

Inilah alur kerja lengkap ujung-ke-ujung saat insiden terjadi di sistem dengan LMO penuh terpasang.

Skenario: API Tiba-tiba Lambat + Banyak Error

  • 1. Alert menyala di 14:31 UTC. Alertmanager mengirim pesan Slack: "[CRITICAL] HighErrorRate: HTTP error rate is 82.4% (was 0.3%). Service: todo-api. Runbook: wiki/..."
  • 2. Buka Grafana -> dashboard Metrik. Latensi P99 melonjak dari 120ms ke 4,3 detik di 14:31. Requests/detik tidak berubah (bukan lonjakan lalu lintas). Connection pool DB di 97%. Jenis error: 100% 500-an, bukan 4xx. Menandakan masalah DB sisi server.
  • 3. Korelasikan ke Log di Loki / New Relic. Query: {service="todo-api", level="error"} |= "connection". Ratusan entri: ERROR: pq: remaining connection slots are reserved for non-replication superuser connections. Connection pool DB habis.
  • 4. Lompat ke Trace dari entri log. Klik tautan trace di log. Trace menampilkan: HTTP Handler -> AuthMiddleware (3ms) -> TodoService (4290ms) -> DB.GetTodos (4285ms <- MACET). Span DB.GetTodos punya atribut: query="SELECT * FROM todos WHERE user_id=$1".
  • 5. Akar masalah teridentifikasi. Deployment 5 menit lalu menambah SELECT * yang melakukan full table scan (index hilang). Di bawah beban, query menumpuk, koneksi dipegang lebih lama, pool habis. Cek git log, mengonfirmasi deployment di 14:29 UTC.
  • 6. Perbaiki + verifikasi. Tambah migrasi index yang hilang. Deploy. Grafana menunjukkan tingkat error turun ke 0,1% dalam 2 menit setelah deployment. Connection pool DB kembali ke 12%. Insiden selesai. Total waktu: 11 menit.
Without LMO

Insiden yang sama tanpa observabilitas: pengguna mengeluh di Twitter, Anda SSH ke server menjalankan grep ERROR /var/log/app.log, mengkorelasikan timestamp manual, akhirnya menyadari ada yang salah dengan DB, menghabiskan 90 menit mempersempit query mana, lalu menebak soal index yang hilang. Total waktu: minimal 2-3 jam.

18

Praktik Terbaik

1. Selalu Sertakan Request ID

Buat requestid unik (UUID atau ULID) di titik masuk setiap request, entah di load balancer Anda atau di middleware pertama. Suntikkan ke konteks dan sertakan di setiap_ entri log dan span untuk request itu. Inilah yang membuat Anda bisa menarik semua log untuk satu request yang gagal dari jutaan entri.

2. Log di Batas, Bukan di Dalam Logika

Log di masuk dan keluar komponen utama (handler, service, repository), bukan tersebar di setiap loop dan kondisi. Ini memberi jejak bersih dan terbaca tanpa kebisingan. Di dalam fungsi, pakai span untuk timing granular, bukan spam log.Debug.

3. Jangan Pernah Log Data Sensitif

Kata sandi, nomor kartu kredit, SSN, token auth, dan API key tidak boleh pernah muncul di log. Pakai library logging terstruktur yang memungkinkan Anda menandai field sebagai ter-redact. Di pipeline CI Anda, pertimbangkan langkah pemindai log yang menggagalkan build jika pola tertentu ditemukan di pernyataan log.

4. Pakai Sampling untuk Trace Volume Tinggi

Di lalu lintas tinggi (1000+ req/detik), mencatat setiap trace mahal. Pakai head-based sampling (catat 10% semua request) atau tail-based sampling (catat 100% request error + 1% request sukses). OTel Collector mendukung keduanya. Jangan pernah sampling error, selalu catat trace untuk kegagalan.

5. Disiplin Log Level

  • Jangan pernah memakai DEBUG di produksi, ia menghasilkan volume raksasa dan biaya penyimpanan
  • INFO harus default produksi Anda, peristiwa bermakna, bukan cerewet
  • Perlakukan setiap log ERROR sebagai sesuatu yang butuh tiket atau aturan alert
  • FATAL harus berarti "aku butuh seseorang di-page sekarang juga"

6. Ini Spektrum, Bukan Kotak Centang

Mulai dengan logging terstruktur dasar dalam format JSON + satu counter Prometheus sederhana untuk tingkat error. Itu saja menghemat berjam-jam saat insiden produksi pertama terjadi. Lalu tambah secara iteratif: metrik histogram, tracing dasar, dashboard Grafana, aturan alert. Anda tidak butuh stack ELK + Jaeger + OTel Collector penuh di hari pertama. Bangun secara bertahap.


Recap

Log memberi tahu Anda apa yang terjadi. Metrik memberi tahu pola dan tren. Trace memberi tahu di mana persis hal-hal salah dan berapa lama tiap langkah. Sistem observable saat ketiganya terpasang dan terkorelasi. Ini butuh usaha developer (instrumentasi kode Anda) dan usaha infrastruktur (kumpulkan, simpan, tampilkan) bekerja bersama. Tooling-nya, entah stack Grafana atau New Relic, sekunder dari praktik meng-instrumentasi kode Anda secara konsisten dengan konteks yang tepat.