Progress belajar
Modul 67 dari 73
0% 0/73 modul selesai
Setelah selesai, tandai modul ini agar progres kursus tetap rapi.
Progress disimpan lokal di browser ini.
Observability:
Memantau Backend di Production
Setelah backend skincare berjalan di ECS, kita butuh sinyal yang jelas dari log, metrik, alarm, dan notifikasi, bukan menatap layar dan menebak.
Kenapa Observability?
Production bukan tempat menebak, ia harus memberi sinyal sebelum pelanggan melihat error.
Di local development kamu menatap terminal, menaruh fmt.Println, lalu restart server. Di production hal itu tidak mungkin, dan justru di sanalah uang pelanggan berpindah.
Di mesin lokal, satu request adalah dunia yang utuh. Kamu lihat log-nya, set breakpoint, ulangi sampai paham. Di production backend skincare, API berjalan di beberapa ECS task sekaligus, worker memproses event pembayaran di task lain, database ada di RDS, dan ribuan request datang dari banyak pelanggan secara bersamaan. Tidak ada satu terminal yang bisa kamu tatap. Kalau checkout melambat pukul dua pagi, kamu butuh sistem yang sudah merekam jawaban sebelum pertanyaannya muncul.
Di React atau Laravel, console.log dan storage/logs/laravel.log cukup untuk debugging awal di satu mesin. Di AWS, log harus terpusat (semua task menulis ke satu tempat), bisa dicari (query, bukan scroll), punya metrik (angka agregat lintas waktu), dan bisa memicu alarm saat pola buruk mulai muncul. Idenya sama, skalanya beda.
Kemampuan sistem menjelaskan apa yang sedang terjadi dari luar, lewat log, metrics, dan alarm, tanpa kamu harus SSH ke server atau menambah kode baru tiap kali ada pertanyaan baru.
Tiga pilar yang kita pakai di modul ini punya peran berbeda dan saling melengkapi. Memahami beda perannya penting agar kamu tidak memaksa satu pilar menjawab pertanyaan yang bukan bidangnya.
Logs → “apa yang terjadi”
Catatan per-kejadian yang kaya konteks. Bagus untuk investigasi satu order atau satu request yang gagal.
Metrics → “seberapa parah”
Angka agregat lintas waktu. Bagus untuk melihat tren, p95 latency, dan kesehatan sistem secara kuantitatif.
Alarms → “kapan bertindak”
Aturan di atas metrik yang memicu notifikasi. Bagus agar manusia atau autoscaling bereaksi tepat waktu.
Fokus modul ini adalah observability minimal yang realistis untuk backend online shop skincare: log dari ECS masuk ke CloudWatch Logs, investigasi memakai Logs Insights, metrik infrastruktur datang dari ECS, ALB, RDS, dan SQS, metrik bisnis datang dari aplikasi Go, lalu alarm dikirim ke SNS dan diteruskan ke channel yang dipantau tim.
Rujukan utama: ECS awslogs, CloudWatch Logs Insights, ECS metrics, dan Embedded Metric Format.
Peta Observability Production
Observability bukan satu fitur, melainkan alur sinyal dari aplikasi ke manusia yang perlu bertindak.
Sebelum menyentuh kode, pegang dulu peta besarnya: dari mana sinyal lahir, di mana ia disimpan, dan bagaimana ia sampai ke mata manusia.
flowchart LR
subgraph App["Aplikasi di ECS Fargate"]
API["Go API<br/>stdout JSON"]
WRK["Go Worker<br/>stdout JSON"]
end
subgraph AWS["Layanan AWS"]
ALB["Application<br/>Load Balancer"]
RDS["RDS<br/>PostgreSQL"]
SQS["SQS<br/>Queue + DLQ"]
end
API -->|awslogs| LOGS["CloudWatch Logs"]
WRK -->|awslogs| LOGS
API -->|PutMetricData / EMF| METRICS["CloudWatch Metrics"]
ALB --> METRICS
RDS --> METRICS
SQS --> METRICS
LOGS --> INSIGHTS["Logs Insights<br/>query investigasi"]
METRICS --> ALARM["CloudWatch Alarm"]
ALARM --> SNS["SNS Topic"]
SNS --> EMAIL["Email"]
SNS --> CHAT["Slack / Chatbot"]Gambar 1. Alur observability minimal: aplikasi dan layanan AWS mengirim log dan metrik ke CloudWatch, alarm menilai kondisi, lalu SNS meneruskan notifikasi ke manusia.
Perhatikan dua jalur yang berpisah. Jalur kiri (log) bersifat naratif dan kaya detail, dipakai saat kamu sudah tahu ada masalah dan ingin tahu kenapa. Jalur kanan (metric ke alarm) bersifat agregat dan otomatis, dipakai agar kamu tahu ada masalah sejak awal. Sistem observability yang sehat memakai keduanya: alarm menunjuk ke jam dan service yang bermasalah, Logs Insights menjawab detail per request di jam itu.
- UI dashboard matang, korelasi otomatis, dan pengalaman lintas cloud lebih nyaman.
- Biasanya butuh agent atau SDK, integrasi, dan biaya tambahan per host atau per usage.
- Terintegrasi langsung dengan ECS, ALB, RDS, SQS, dan SNS tanpa agent pihak ketiga.
- Cocok untuk fase awal: bisa dimulai hari ini, dekat dengan infrastruktur, dan kamu hanya bayar yang kamu pakai.
Kalau di frontend kamu terbiasa Sentry mengumpulkan error dan New Relic memantau APM, CloudWatch adalah versi AWS-native yang menggabungkan tempat log, metrik, query, alarm, dashboard, dan notifikasi jadi satu, dengan trade-off UI yang lebih AWS-centric dan korelasi yang lebih manual.
CloudWatch Logs dari ECS
Di Fargate, log container dikirim ke CloudWatch Logs lewat driver awslogs di task definition.
ECS tidak otomatis tahu ke mana stdout dan stderr container harus mengalir. Kamu harus menyatakannya secara eksplisit di task definition.
Pada Fargate, kamu tidak punya akses ke node EC2 di bawah container, jadi tidak ada cara membaca file log lokal. Solusinya adalah logConfiguration dengan driver awslogs: driver ini meneruskan apa pun yang container tulis ke stdout dan stderr langsung ke CloudWatch Logs. Konsekuensinya jelas: aplikasi Go cukup menulis log ke stdout, dan biarkan AWS yang mengangkutnya.
Perlakukan log sebagai aliran event ke stdout, bukan sebagai file yang kamu kelola. File log lokal di dalam container akan hilang saat task diganti, dan tidak terlihat oleh siapa pun. Tulis ke stdout, biarkan platform yang merutekan.
Sebelum bicara struktur log, kenali dulu kosakata CloudWatch Logs karena tiga istilah ini sering tertukar.
Log group
Wadah seperti /ecs/skincare-api. Di level inilah retention, izin akses, dan metric filter diatur.
Log stream
Urutan event dari satu sumber, biasanya satu task atau container. Banyak stream di dalam satu group.
Log event
Satu baris: timestamp plus message UTF-8. Inilah satu entri log JSON yang ditulis aplikasimu.
Berikut potongan task definition Fargate yang relevan. Bagian logConfiguration adalah yang membuat log mengalir; sisanya konteks dari modul ECS sebelumnya.
infra/aws/task-definition.json{ "family": "skincare-api", "networkMode": "awsvpc", "requiresCompatibilities": ["FARGATE"], "cpu": "512", "memory": "1024", "executionRoleArn": "arn:aws:iam::123456789012:role/ecsTaskExecutionRole", "taskRoleArn": "arn:aws:iam::123456789012:role/skincare-api-task-role", "containerDefinitions": [ { "name": "api", "image": "123456789012.dkr.ecr.ap-southeast-1.amazonaws.com/skincare-api:latest", "essential": true, "portMappings": [ { "containerPort": 8080, "protocol": "tcp" } ], "logConfiguration": { "logDriver": "awslogs", "options": { "awslogs-group": "/ecs/skincare-api", "awslogs-region": "ap-southeast-1", "awslogs-stream-prefix": "api" } } } ] }
awslogs-group adalah tempat log dikumpulkan. awslogs-stream-prefix membantu membedakan stream per container dan task, sehingga nama stream menjadi api/api/<task-id>. Untuk production yang rapi, buat log group lebih dulu lewat IaC dengan retention eksplisit (misalnya 30 hari), supaya kamu tidak diam-diam membayar penyimpanan log selamanya.
Yang mengirim log ke CloudWatch lewat awslogs adalah agen ECS, memakai task execution role (ecsTaskExecutionRole), bukan kode Go-mu. Izin logs:CreateLogStream dan logs:PutLogEvents sudah ada di managed policy AmazonECSTaskExecutionRolePolicy. Ini role yang sama yang menarik image dari ECR dan mengambil secret. Beda dengan task role yang nanti dipakai kode Go-mu untuk memanggil cloudwatch:PutMetricData.
Log bisa tersimpan lama dan banyak orang operasional bisa mengaksesnya. Jangan pernah me-log password, refresh token, JWT mentah, payment key, nomor kartu, atau payload pribadi pelanggan. Sekali tertulis ke CloudWatch, ia ikut retention dan audit.
- cmd/
- api/
- main.go setup logger, router, server
- internal/
- observability/
- logger.go konfigurasi slog JSON
- metrics.go publish custom metrics (PutMetricData)
- emf.go emit Embedded Metric Format ke stdout
- httpmw/
- request_logger.go middleware log tiap request
- infra/
- aws/
- task-definition.json awslogs driver untuk ECS
- task-role-policy.json izin PutMetricData
- go.mod module github.com/kamu/skincare-backend
Log JSON dan Logs Insights
Log production harus mudah dicari oleh mesin, bukan hanya nyaman dibaca manusia.
Selisih antara “punya log” dan “bisa men-debug dari log” terletak pada struktur. Log teks bebas tidak bisa di-filter; log JSON bisa.
Go modern punya log/slog di standard library sejak Go 1.21, jadi kamu tidak perlu dependency eksternal. Untuk production, pakai JSON handler. Dengan begitu CloudWatch Logs Insights bisa memperlakukan tiap field (level, path, status, latency_ms, order_id, request_id) sebagai kolom yang bisa difilter dan diagregasi, bukan sekadar string.
Di Laravel kamu memberi context array ke logger: Log::info(‘checkout’, [‘order_id’ => $id]). Di Go dengan slog, context itu menjadi pasangan key-value variadic setelah pesan: setiap pasangan jadi field JSON yang bisa di-query. Sama seperti Winston atau Pino di Node yang menulis JSON line, lalu di-tail oleh pengangkut log.
Mulai dari logger dasar yang menulis JSON ke stdout dan menempelkan field tetap (service, env) ke setiap baris. Field tetap ini yang nanti membedakan log API dari log worker saat keduanya ada di satu pencarian.
internal/observability/logger.gopackage observability import ( "log/slog" "os" ) // NewLogger membuat logger JSON untuk production. Semua log menempel // field service dan env, jadi mudah difilter di Logs Insights. func NewLogger(service string) *slog.Logger { handler := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{ Level: slog.LevelInfo, }) return slog.New(handler).With( "service", service, "env", getenv("APP_ENV", "development"), ) } func getenv(key, fallback string) string { if v := os.Getenv(key); v != "" { return v } return fallback }
Berikutnya, middleware yang mencatat setiap request HTTP dengan field stabil. Trik statusRecorder membungkus http.ResponseWriter agar kita bisa membaca status code yang akhirnya ditulis handler, sesuatu yang tidak terekspos oleh interface standar.
internal/httpmw/request_logger.gopackage httpmw import ( "log/slog" "net/http" "time" ) type statusRecorder struct { http.ResponseWriter status int } func (r *statusRecorder) WriteHeader(code int) { r.status = code r.ResponseWriter.WriteHeader(code) } // RequestLogger mencatat satu baris JSON per request dengan field stabil. func RequestLogger(log *slog.Logger) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { started := time.Now() rec := &statusRecorder{ResponseWriter: w, status: http.StatusOK} next.ServeHTTP(rec, r) log.InfoContext(r.Context(), "http_request", "method", r.Method, "path", r.URL.Path, "status", rec.status, "latency_ms", time.Since(started).Milliseconds(), "request_id", r.Header.Get("X-Request-Id"), ) }) } }
Satu baris log yang dihasilkan akan terlihat seperti ini di CloudWatch. Inilah bentuk yang bisa di-query field demi field.
Contoh satu log event di CloudWatch{"time":"2026-06-09T03:14:07Z","level":"INFO","msg":"http_request","service":"skincare-api","env":"production","method":"POST","path":"/v1/checkout","status":201,"latency_ms":284,"request_id":"req_01JZK7"}
Logs Insights adalah bahasa query di atas log group. Ia membaca field JSON-mu sebagai kolom. Tiga query berikut menutupi mayoritas kebutuhan investigasi awal.
Pertama, cari error 5xx dalam rentang waktu terpilih, untuk menjawab “ada apa barusan”.
Logs Insights: error 5xx terbarufields @timestamp, level, msg, path, status, latency_ms, request_id | filter level = "ERROR" or status >= 500 | sort @timestamp desc | limit 50
Kedua, cari endpoint paling lambat dengan percentile, untuk menjawab “apa yang bikin lambat”. Percentile jauh lebih jujur daripada rata-rata saat melihat latency.
Logs Insights: endpoint paling lambat (p95)fields path, latency_ms | filter msg = "http_request" | stats pct(latency_ms, 95) as p95_ms, avg(latency_ms) as avg_ms, count(*) as n by path | sort p95_ms desc | limit 20
Ketiga, ikuti jejak satu order lintas service, untuk menjawab “kenapa order ini gagal”. Karena order_id konsisten di API dan worker, satu query menyatukan ceritanya.
Logs Insights: lacak satu orderfields @timestamp, service, msg, order_id, payment_id, status, request_id | filter order_id = "ord_01JZSKINCARE" | sort @timestamp asc
Sepakati kosakata field sekali, lalu pakai di mana-mana: order_id, payment_id, user_id, request_id, latency_ms, status. Field yang sama di API dan worker membuat satu query bisa menelusuri perjalanan order dari checkout sampai pembayaran selesai.
Jebakan umum dari kebiasaan fmt.Sprintf: jangan menempel data ke dalam pesan seperti log.Info("order " + id + " gagal"). Itu mengembalikanmu ke teks bebas yang tak bisa difilter. Selalu pisahkan: pesan tetap konstan ("checkout_failed"), data jadi field ("order_id", id).
CloudWatch Metrics yang Wajib Dilihat
Metrik menjawab kesehatan sistem secara kuantitatif, log menjawab detail per kejadian.
ECS, ALB, RDS, dan SQS sudah otomatis mengirim banyak metrik ke CloudWatch. Tugasmu bukan menyalakan semua grafik, melainkan memilih sinyal yang benar.
Seri data numerik berbasis waktu di sebuah namespace, dengan dimensi sebagai label. Contoh: CPUUtilization 72 persen pada pukul 10:15 untuk service skincare-api, atau ApproximateNumberOfMessagesVisible 128 untuk queue order.
Tiap layanan AWS punya namespace sendiri (AWS/ECS, AWS/ApplicationELB, AWS/RDS, AWS/SQS) dan metrik bawaan. Berikut metrik yang benar-benar dipakai untuk backend skincare, dikelompokkan per sumber.
ECS API dan worker (AWS/ECS)
Pantau CPUUtilization dan MemoryUtilization plus jumlah task running. CPU tinggi bisa berarti traffic naik atau query berat. ECS mempublikasikan metrik tiap 1 menit.
ALB (AWS/ApplicationELB)
Pantau RequestCount, TargetResponseTime, HTTPCode_Target_5XX_Count, dan HTTPCode_ELB_5XX_Count. Inilah pengalaman request dari pintu masuk.
RDS PostgreSQL (AWS/RDS)
Pantau DatabaseConnections, CPUUtilization, FreeableMemory, dan read/write latency. Koneksi penuh sering berasal dari pool yang terlalu besar.
SQS worker (AWS/SQS)
Pantau ApproximateNumberOfMessagesVisible (backlog) dan ApproximateAgeOfOldestMessage. Queue yang tumbuh berarti worker tertinggal dari produksi job.
Average bisa menutupi masalah tajam. Untuk latency endpoint, lihat percentile (p95, p99): rata-rata 120 ms terdengar sehat padahal p99 bisa 4 detik bagi sebagian pelanggan. Untuk CPU ECS, average berguna untuk keputusan scaling, tapi maximum membantu melihat satu task yang timpang.
Khusus error rate ALB, jangan ber-alarm pada jumlah 5xx absolut. Saat traffic naik, 10 error per menit bisa wajar; saat traffic sepi, 10 error berarti gawat. Hitung rasio dengan metric math, sehingga thresholdnya bermakna di segala kondisi traffic.
ALB error rate sebagai persentase (metric math)m1 = HTTPCode_ELB_5XX_Count (AWS/ApplicationELB) m2 = HTTPCode_Target_5XX_Count (AWS/ApplicationELB) m3 = RequestCount (AWS/ApplicationELB) expression e1 = 100 * (m1 + m2) / m3 -> persen 5xx terhadap total request
Untuk fase awal, satu dashboard CloudWatch sudah cukup. Susun panel berikut dalam satu layar: ALB request count, ALB 5xx rate (metric math di atas), ALB p95 TargetResponseTime, ECS API CPU dan memory, ECS worker CPU, SQS ApproximateNumberOfMessagesVisible, RDS DatabaseConnections dan CPU, lalu custom metric order dan payment.
flowchart TD
Q["Pertanyaan operasional"] --> Q1{"Apakah pelanggan terdampak?"}
Q1 -->|ya| ALB["ALB: 5xx rate, p95 latency"]
Q1 -->|tidak yakin| INFRA{"Sumber daya jenuh?"}
INFRA -->|CPU/mem tinggi| ECS["ECS: CPUUtilization, MemoryUtilization"]
INFRA -->|koneksi penuh| RDS["RDS: DatabaseConnections"]
Q --> ASYNC{"Job async tertinggal?"}
ASYNC -->|backlog naik| SQS["SQS: MessagesVisible, AgeOfOldest"]
ALB --> DETAIL["Logs Insights:<br/>cari request bermasalah"]
ECS --> DETAIL
RDS --> DETAIL
SQS --> DETAILGambar 2. Dari pertanyaan operasional ke metrik yang tepat, lalu turun ke Logs Insights untuk detail per kejadian.
Custom Metrics dari Aplikasi Go
Metrik AWS memberi tahu infrastruktur sehat, custom metric memberi tahu bisnis sehat.
CloudWatch tidak tahu bahwa POST /v1/checkout berarti order baru, atau bahwa payment success rate di bawah 95 persen langsung memukul revenue. Sinyal bisnis seperti itu harus dikirim dari aplikasi.
Orders created
Jumlah order berhasil per periode. Drop mendadak menandai gangguan conversion meski infrastruktur tampak sehat.
Payment success rate
Persentase payment sukses terhadap total attempt. Penurunan menandai gangguan gateway pembayaran.
Checkout failed
Jumlah checkout gagal karena stok, voucher, atau error DB. Memisahkan masalah bisnis dari masalah teknis.
Cara paling langsung adalah memanggil PutMetricData lewat aws-sdk-go-v2. Bungkus dalam tipe kecil agar service layer tidak perlu tahu detail SDK. Perhatikan context.Context sebagai parameter pertama, idiom Go yang konsisten kita pakai sejak modul awal.
Terminalgo get github.com/aws/aws-sdk-go-v2/config \ github.com/aws/aws-sdk-go-v2/service/cloudwatch
internal/observability/metrics.gopackage observability import ( "context" "time" "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/service/cloudwatch" "github.com/aws/aws-sdk-go-v2/service/cloudwatch/types" ) type MetricPublisher struct { client *cloudwatch.Client namespace string service string } func NewMetricPublisher(ctx context.Context, namespace, service string) (*MetricPublisher, error) { cfg, err := config.LoadDefaultConfig(ctx) if err != nil { return nil, err } return &MetricPublisher{ client: cloudwatch.NewFromConfig(cfg), namespace: namespace, service: service, }, nil } // PutCount mengirim satu datapoint count, mis. order dibuat. func (p *MetricPublisher) PutCount(ctx context.Context, name string, value float64) error { _, err := p.client.PutMetricData(ctx, &cloudwatch.PutMetricDataInput{ Namespace: aws.String(p.namespace), MetricData: []types.MetricDatum{{ MetricName: aws.String(name), Timestamp: aws.Time(time.Now()), Value: aws.Float64(value), Unit: types.StandardUnitCount, Dimensions: []types.Dimension{{ Name: aws.String("Service"), Value: aws.String(p.service), }}, }}, }) return err }
Di service layer, jangan jadikan publikasi metric sebagai dependency beton. Definisikan interface kecil milik domain (Metrics) lalu suntikkan implementasinya. Ini idiom “accept interfaces, return structs” dan memudahkan pengujian tanpa memanggil AWS.
internal/order/service.gopackage order import "context" // Metrics adalah port yang dibutuhkan order, bukan tipe AWS konkret. type Metrics interface { PutCount(ctx context.Context, name string, value float64) error } type Service struct { repo Repository metrics Metrics } func (s *Service) Checkout(ctx context.Context, in CheckoutInput) (*Order, error) { order, err := s.repo.CreateOrderFromCart(ctx, in.UserID, in.CartID) if err != nil { // Metric gagal kirim tidak boleh menggagalkan checkout: cukup catat. _ = s.metrics.PutCount(ctx, "CheckoutFailed", 1) return nil, err } _ = s.metrics.PutCount(ctx, "OrdersCreated", 1) return order, nil }
Jika PutMetricData gagal (jaringan, throttle), checkout tidak boleh ikut gagal. Abaikan error metric secara sengaja, log ringan kalau perlu, lalu lanjutkan response ke pelanggan. Observability adalah pengamat, bukan penghalang transaksi.
Beda dengan console.count() di browser yang lokal dan instan, tiap PutMetricData adalah HTTP request ke AWS yang menambah latency dan biaya. Memanggilnya satu per satu untuk tiap order berisiko menjadi bottleneck. Untuk volume tinggi, agregasi dulu (hitung di memori, kirim periodik), atau pakai EMF di section berikutnya yang menumpang jalur log.
Karena custom metric dikirim oleh kode aplikasi, izinnya harus ada di task role (yang dipakai container), bukan task execution role. Batasi ke namespace milikmu agar tidak terlalu lebar.
infra/aws/task-role-policy.json{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": ["cloudwatch:PutMetricData"], "Resource": "*", "Condition": { "StringEquals": { "cloudwatch:namespace": "SkincareShop/Production" } } } ] }
Pakai namespace milik aplikasi seperti SkincareShop/Production. Hindari awalan AWS/ karena itu dipesan untuk layanan AWS dan akan ditolak. Namespace memisahkan metrik bisnismu dari metrik infrastruktur di konsol.
Embedded Metric Format (EMF)
Satu tulisan ke stdout yang sekaligus menjadi log terstruktur dan custom metric.
PutMetricData baik untuk metric jarang, tapi boros untuk volume tinggi. EMF memberi jalan lebih hemat: kamu menulis JSON ke stdout, CloudWatch mengekstrak metriknya otomatis.
Spesifikasi JSON terstruktur dengan blok metadata _aws. Saat log seperti ini sampai ke CloudWatch Logs (lewat awslogs), CloudWatch otomatis mengekstrak field bertanda menjadi custom metric yang bisa di-alarm, sambil log mentahnya tetap bisa di-query Logs Insights.
Kenapa ini menarik untuk worker dan API di Fargate? Karena container itu ephemeral dan bisa banyak. Memanggil PutMetricData dari ribuan event akan throttle dan mahal. EMF menumpang jalur log yang sudah ada (stdout ke awslogs), jadi tidak ada panggilan API tambahan, dan izinnya cukup logs:PutLogEvents yang sudah dimiliki execution role.
sequenceDiagram participant App as Go Worker participant CWL as CloudWatch Logs participant CWM as CloudWatch Metrics participant Ins as Logs Insights App->>CWL: stdout JSON dengan blok _aws (via awslogs) CWL->>CWM: ekstrak metrik otomatis (bisa di-alarm) CWL->>Ins: log mentah tetap bisa di-query
Gambar 3. Satu tulisan EMF ke stdout bercabang menjadi metric (kanan) dan log yang bisa di-query (bawah) tanpa panggilan API tambahan.
- Satu HTTP request per kirim, menambah latency dan biaya request.
- Butuh izin
cloudwatch:PutMetricDatadi task role. - Cocok untuk metric jarang dan terkontrol.
- Hanya menulis ke stdout, ikut jalur log yang sudah ada.
- Cukup
logs:PutLogEventsdi execution role. - Cocok untuk volume tinggi dan resource ephemeral.
Berikut emitter EMF manual sesuai spesifikasi. Inti formatnya: objek _aws berisi Timestamp dan CloudWatchMetrics yang mendeklarasikan namespace, dimensi, dan daftar metric; nilai metric dan dimensi ada sebagai field biasa di level atas objek yang sama.
internal/observability/emf.gopackage observability import ( "encoding/json" "os" "time" ) // EmitEMF menulis satu log EMF ke stdout. CloudWatch akan mengekstrak // "PaymentProcessed" sebagai custom metric, sekaligus log tetap bisa di-query. func EmitEMF(service, metricName string, value float64, unit string) error { now := time.Now().UnixMilli() doc := map[string]any{ "_aws": map[string]any{ "Timestamp": now, "CloudWatchMetrics": []map[string]any{{ "Namespace": "SkincareShop/Production", "Dimensions": [][]string{{"Service"}}, "Metrics": []map[string]string{{"Name": metricName, "Unit": unit}}, }}, }, "Service": service, metricName: value, } line, err := json.Marshal(doc) if err != nil { return err } _, err = os.Stdout.Write(append(line, '\n')) return err }
Memakainya dari worker pembayaran terlihat seperti menulis log biasa, padahal sekaligus menghasilkan metric PaymentProcessed dan EmailSendLatencyMs.
internal/payment/worker.gofunc (w *Worker) handlePaymentEvent(ctx context.Context, ev PaymentEvent) error { if err := w.processor.Apply(ctx, ev); err != nil { _ = observability.EmitEMF("skincare-worker", "PaymentFailed", 1, "Count") return err } _ = observability.EmitEMF("skincare-worker", "PaymentProcessed", 1, "Count") return nil }
Tiap kombinasi unik nilai dimensi menjadi satu custom metric tersendiri dengan biaya sendiri. Memakai order_id atau request_id sebagai dimensi akan meledakkan jumlah metric dan tagihan. Jadikan field high-cardinality sebagai properti log biasa (untuk Logs Insights), bukan dimensi metric. Dimensi cukup rendah: Service, Env, atau Gateway.
Untuk produksi kamu bisa memakai aws-embedded-metrics-go yang menyembunyikan detail format dan buffering. Format manual di atas berguna untuk memahami apa yang sebenarnya terjadi di balik library itu.
Alarm yang Benar-benar Dipakai
Alarm yang baik tidak sekadar berisik, ia menunjuk kondisi yang perlu tindakan.
Alarm production awal untuk backend skincare tidak perlu puluhan. Mulai dari empat sinyal yang jelas, lalu tambah saat kamu paham pola normal aplikasimu.
Aturan di atas satu metrik: statistic (Average, Sum, p95) plus threshold plus jumlah evaluation period. Misalnya “Average CPUUtilization > 80 selama 5 periode 1 menit berturut-turut” memindahkan alarm ke state ALARM lalu menjalankan action.
ECS CPU > 80%
API jenuh: scale out atau investigasi endpoint berat. Untuk worker, CPU tinggi bisa berarti job CPU-bound yang tertahan.
ALB 5xx > 1%
Error rate naik: pakai metric math (total 5xx dibagi request count), bukan jumlah absolut, agar adil di segala traffic.
RDS connections > 80%
Pool mendekati penuh: DatabaseConnections melewati 80 persen dari max_connections menandai pool exhaustion yang akan datang.
SQS backlog > 100
Worker tertinggal: ApproximateNumberOfMessagesVisible terus tinggi berarti produksi job mengalahkan konsumsi.
State sebuah alarm bukan cuma OK dan ALARM. Memahami ketiganya mencegah kebingungan saat sebuah alarm “diam” padahal metriknya belum cukup data.
stateDiagram-v2 [*] --> OK OK --> ALARM: threshold terlampaui<br/>selama N periode ALARM --> OK: metrik kembali normal OK --> INSUFFICIENT_DATA: metrik belum ada<br/>(mis. task baru) INSUFFICIENT_DATA --> OK: data masuk, normal INSUFFICIENT_DATA --> ALARM: data masuk, di atas threshold ALARM --> [*]
Gambar 4. Tiga state CloudWatch Alarm. INSUFFICIENT_DATA muncul saat metrik belum punya datapoint, misalnya tepat setelah deploy task baru.
Spesifikasi keempat alarm awal, ditulis sebagai catatan target yang bisa kamu terjemahkan ke konsol, CLI, atau IaC.
observability/alarm-targets.txtECS API CPU: Namespace: AWS/ECS Metric: CPUUtilization Dimension: ClusterName=skincare-prod, ServiceName=skincare-api Condition: Average > 80 selama 5 x 1 menit Action: notifikasi + pertimbangkan scale out ALB 5xx rate: Namespace: AWS/ApplicationELB Metrics: HTTPCode_ELB_5XX_Count, HTTPCode_Target_5XX_Count, RequestCount Math: 100 * (elb_5xx + target_5xx) / request_count Condition: > 1 selama 5 x 1 menit Action: notifikasi engineer RDS connections: Namespace: AWS/RDS Metric: DatabaseConnections Dimension: DBInstanceIdentifier=skincare-prod Condition: Average > floor(max_connections * 0.8) selama 5 x 1 menit Action: notifikasi + review pool sizing SQS backlog: Namespace: AWS/SQS Metric: ApproximateNumberOfMessagesVisible Dimension: QueueName=skincare-order-worker-prod Condition: Average > 100 selama 10 x 1 menit Action: notifikasi + scale worker
Satu alarm lagi yang wajib untuk worker async: kedalaman DLQ. Jika ada satu pesan saja di Dead-Letter Queue, berarti ada payload yang gagal diproses berulang kali (poison message), dan itu selalu butuh mata manusia.
observability/dlq-alarm.txtDLQ depth: Namespace: AWS/SQS Metric: ApproximateNumberOfMessagesVisible Dimension: QueueName=skincare-order-worker-dlq Condition: Sum > 0 selama 1 x 1 menit Action: notifikasi (ada pesan gagal yang perlu diinspeksi)
Angka 80 persen, 1 persen, dan 100 pesan adalah baseline awal yang masuk akal, bukan kebenaran abadi. Setelah punya traffic nyata, revisi threshold berdasarkan pola normal: alarm yang terlalu sensitif sama buruknya dengan yang terlalu longgar.
Kondisi ketika terlalu banyak alarm tak penting membuat tim mengabaikan alarm penting. Obatnya: setiap alarm harus actionable (ada tindakan jelas yang diharapkan), bukan sekadar informatif.
Notifikasi: SNS ke Email atau Slack
Alarm tanpa notifikasi hanya lampu merah yang tidak dilihat siapa pun.
CloudWatch Alarm bisa menjalankan action saat state berubah. Action paling umum adalah publish ke SNS topic, dan dari sana sinyal bercabang ke manusia.
SNS (Simple Notification Service) adalah hub pub/sub: alarm mem-publish satu pesan ke topic, lalu setiap subscriber menerimanya. Satu topic bisa punya banyak subscriber sekaligus, jadi satu alarm bisa mengirim email ke tim ops dan notifikasi ke Slack secara bersamaan tanpa konfigurasi ganda di alarm.
flowchart LR A["CloudWatch Alarm<br/>state: ALARM"] --> T["SNS Topic<br/>skincare-prod-alerts"] T --> E["Email subscriber<br/>ops@example.com"] T --> C["AWS Chatbot<br/>Slack channel"] T --> L["Lambda<br/>webhook internal"] C --> ENG["Engineer<br/>investigasi"]
Gambar 5. Satu SNS topic menyebar ke banyak subscriber. Tambah atau cabut channel tanpa menyentuh konfigurasi alarm.
Kalau di Node kamu kenal pola pub/sub dengan EventEmitter atau message bus, SNS adalah versi terkelola dan terdistribusinya. Publisher (alarm) tidak tahu siapa subscriber-nya; menambah pendengar baru (Slack, paging) tidak mengubah publisher. Fan-out, decoupled, persis seperti event bus.
Buat topic dan langganan email lewat CLI. Perhatikan: langganan email butuh konfirmasi manual sebelum aktif.
Terminalaws sns create-topic \ --name skincare-prod-alerts \ --region ap-southeast-1 aws sns subscribe \ --topic-arn arn:aws:sns:ap-southeast-1:123456789012:skincare-prod-alerts \ --protocol email \ --notification-endpoint ops@example.com \ --region ap-southeast-1
Setelah sns subscribe, AWS mengirim email konfirmasi. Notifikasi tidak akan terkirim sampai penerima menekan tautan konfirmasi di email itu. Banyak orang lupa langkah ini lalu mengira alarm rusak.
Untuk Slack, jalur resmi adalah AWS Chatbot yang menjembatani SNS ke Slack channel tanpa kamu menulis kode. Bila tim lebih suka webhook internal, taruh sebuah Lambda sebagai subscriber yang memformat pesan lalu mem-post ke Incoming Webhook.
Slack Incoming Webhook URL adalah kredensial: siapa pun yang memilikinya bisa mengirim pesan atas nama channelmu. Simpan di Secrets Manager dan baca lewat task role, jangan hardcode di task definition, environment plain, atau repository.
Hands-on: Observability Minimal
Target hands-on ini adalah observability pertama yang berguna, bukan dashboard paling cantik.
Ikuti lima langkah ini secara berurutan. Setelah selesai, backend skincare-mu sudah punya log terstruktur, dashboard, dan alarm yang benar-benar memberi tahu saat ada masalah.
Pastikan container api dan worker punya logConfiguration driver awslogs dengan log group berbeda (/ecs/skincare-api, /ecs/skincare-worker) dan stream prefix yang jelas.
Ganti log ad-hoc menjadi slog JSON dengan field stabil: service, env, path, status, latency_ms, request_id, dan order_id di jalur order.
Simpan query error 5xx, endpoint lambat (p95), dan lacak satu order, agar investigasi incident tidak dimulai dari layar kosong saat panik.
Panel ALB request count, ALB 5xx rate, ECS CPU dan memory, RDS connections, SQS backlog, dan custom metric OrdersCreated serta PaymentProcessed.
Hubungkan alarm ECS CPU, ALB 5xx rate, RDS connections, dan SQS backlog (plus DLQ depth) ke SNS topic yang dipantau tim.
Verifikasi cepat bahwa log benar-benar mengalir, dengan men-tail log group dari terminal.
Terminalaws logs tail /ecs/skincare-api \ --since 15m \ --follow \ --region ap-southeast-1
Buat alarm CPU pertama lewat CLI. Threshold dan period di sini cocok untuk memulai, sesuaikan setelah ada data nyata.
Terminalaws cloudwatch put-metric-alarm \ --alarm-name skincare-api-cpu-high \ --namespace AWS/ECS \ --metric-name CPUUtilization \ --dimensions Name=ClusterName,Value=skincare-prod Name=ServiceName,Value=skincare-api \ --statistic Average \ --period 60 \ --evaluation-periods 5 \ --threshold 80 \ --comparison-operator GreaterThanThreshold \ --alarm-actions arn:aws:sns:ap-southeast-1:123456789012:skincare-prod-alerts \ --region ap-southeast-1
Endpoint yang relevan dengan observability di API skincare, agar dashboard dan alarm punya konteks bisnis yang jelas.
/healthz Health check yang dipanggil ALB target group, harus murah dan cepat /v1/checkout Sumber metric OrdersCreated dan CheckoutFailed /v1/payments/webhook Sumber metric PaymentProcessed dan PaymentFailed di worker Buktikan jalur notifikasi berfungsi dengan alarm non-kritis berthreshold rendah di staging, atau set alarm sementara dengan threshold yang pasti terlampaui. Jangan pernah sengaja membebani production hanya untuk membuktikan alarm menyala.
Jebakan Umum
Banyak sistem punya CloudWatch tapi tetap sulit di-debug, karena sinyalnya tidak didesain.
Punya log dan metrik tidak otomatis membuatmu bisa men-debug. Empat jebakan ini yang paling sering membuat observability terasa percuma.
Log berupa kalimat bebas
"payment gagal bro" mustahil difilter. Pakai JSON dengan field payment_id, order_id, reason, gateway_status agar bisa di-query dan diagregasi.
Alarm masuk inbox yang jarang dibuka
Email cocok untuk laporan, bukan incident. Alarm kritis harus masuk channel yang benar-benar dipantau (Slack, paging), agar tidak tenggelam di antara newsletter.
RDS pool tidak dihitung lintas task
6 ECS task masing-masing pool 30 koneksi berarti potensi 180 koneksi. Ini bisa menyentuh max_connections RDS jauh lebih cepat dari yang terlihat dari satu task.
Custom metric terlalu granular
Dimensi user_id atau order_id meledakkan cardinality dan biaya. Jadikan field high-cardinality sebagai properti log; dimensi metric cukup Service, Env, Gateway.
Jebakan kelima, paling halus dan paling mahal kalau terjadi: idempotency worker. Karena SQS Standard bersifat at-least-once, satu pesan bisa terkirim dua kali. Jika observability menunjukkan PaymentProcessed naik tapi jumlah order tidak, kamu mungkin sedang double-charge tanpa alarm yang menangkapnya.
SQS Standard bisa mengirim pesan yang sama lebih dari sekali. Worker pembayaran wajib idempoten: simpan kunci unik (payment_id) di tabel processed_events dengan unique constraint, lalu skip jika sudah ada. Tanpa ini, retry yang sehat justru menyebabkan double email atau double charge, dan metric PaymentProcessed akan menipu.
Payment webhook, alamat pelanggan, dan token auth bisa mengandung data sensitif. Log metadata secukupnya untuk investigasi (payment_id, order_id, gateway_status), bukan body mentah. Sekali data pribadi masuk CloudWatch, ia ikut retention dan jadi beban kepatuhan.
Di local kamu mencari bug dari satu request yang kamu ulang. Di production kamu mencari pola dari ribuan request yang tidak bisa diulang. Karena itu log harus terstruktur (bisa difilter), metric harus agregat (bisa di-tren), dan alarm harus actionable (memicu tindakan). Mindset berubah dari “menatap” menjadi “mendesain sinyal”.
Ringkasan & Poin Penting
Observability membuat backend skincare aman dioperasikan setelah masuk production.
Setelah modul ini, API, worker, RDS, SQS, dan ALB tidak lagi menjadi kotak hitam. Saat sesuatu melambat atau gagal, kamu punya jalur investigasi yang jelas.
Yang Wajib Menempel
- CloudWatch Logs menerima log ECS lewat driver
awslogsdi task definition; aplikasi Go cukup menulis log JSON kestdout, dan agen ECS (task execution role) yang mengangkutnya. log/slogdengan JSON handler membuat field (level,path,status,latency_ms,order_id,request_id) bisa difilter dan diagregasi di Logs Insights; pesan tetap konstan, data jadi field.- Metric AWS bawaan (ECS, ALB, RDS, SQS) memberi sinyal infrastruktur; custom metric lewat
PutMetricDataatau EMF memberi sinyal bisnis sepertiOrdersCreateddanPaymentProcessed. - EMF menumpang jalur log (
stdoutkeawslogs) sehingga hemat untuk volume tinggi dan resource ephemeral; hati-hati dimensi high-cardinality. - Empat alarm awal yang praktis: ECS CPU > 80 persen, ALB 5xx > 1 persen (metric math), RDS
DatabaseConnections> 80 persen batas, SQS backlog > 100, plus DLQ depth > 0. - SNS menyebar alarm ke email dan Slack secara fan-out; alarm harus actionable agar terhindar dari alarm fatigue.
Di proyek online shop skincare, modul ini menutup gap penting setelah deploy. Custom metric OrdersCreated dan PaymentProcessed memetakan langsung ke domain checkout dan payment yang kamu bangun di Roadmap 5; alarm SQS backlog dan DLQ depth menjaga worker async dari Roadmap 6 (ECS) agar tidak diam-diam tertinggal; dan alarm RDS connections menahan pool dari modul database agar tidak menghabiskan koneksi RDS. Saat checkout melambat, payment webhook gagal, atau worker tertinggal, kamu sudah punya log terstruktur, dashboard, dan alarm yang menunjuk jam dan service yang tepat.
Langkah berikutnya adalah menguatkan praktik operasional dari sisi biaya dan reliability untuk menutup Roadmap 8. Setelah itu, Roadmap 9 masuk ke advanced scaling: profiling dengan pprof, caching, optimasi search, dan event-driven pattern, di mana metric dan alarm yang kamu pasang di sini menjadi alat ukur sebelum dan sesudah setiap optimasi.
Progress disimpan lokal di browser ini.