Monitoring adalah bagian penting dari menjalankan perangkat lunak yang dapat diandalkan, namun banyak tim hanya menemukan gangguan setelah keluhan pengguna mulai muncul. Bayangkan Anda menerima pesan Slack pada jam 2 pagi, memberitahu Anda bahwa API Anda telah turun selama lebih dari satu jam dan tidak ada yang memperhatikan sampai pelanggan mulai mengeluh. layanan monitoring memecahkan masalah ini dengan membiarkan Anda dan tim Anda secara proaktif menanggapi insiden, sebelum masalah meningkat. Dalam tutorial ini, saya akan membawa Anda melalui langkah-langkah tentang bagaimana membangun aplikasi pemantauan status dari awal. Mencoba layanan Anda pada jadwal (HTTP, TCP, DNS, dan banyak lagi) Mendeteksi gangguan dan mengirimkan peringatan ke berbagai saluran komunikasi (Teams, Slack, dll) Melacak insiden dengan otomatis membuka / menutup Menampilkan metrik untuk dashboard Prometheus dan Grafana Menggunakan Docker Untuk aplikasi ini, saya akan menggunakan Go karena cepat, mengkompilasi ke biner tunggal untuk dukungan lintas platform, dan menangani concurrency, yang penting untuk aplikasi yang perlu memantau beberapa titik akhir secara bersamaan. Apa yang kita bangun Kami akan membangun aplikasi Go "StatusD". ia membaca file konfigurasi yang memiliki daftar layanan untuk memantau, menyelidiki mereka, dan menciptakan insiden, pemberitahuan kebakaran ketika sesuatu yang salah. Tech Stack Used: Golang Postgresifikasi Grafana (Prometheus untuk metrik) Docker Nginx Berikut adalah arsitektur tingkat tinggi: ┌─────────────────────────────────────────────────────────────────┐ │ Docker Compose │ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────────────┐ │ │ │ Postgres │ │Prometheus│ │ Grafana │ │ Nginx │ │ │ │ DB │ │ (metrics)│ │(dashboard)│ │ (reverse proxy) │ │ │ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────────┬─────────┘ │ │ │ │ │ │ │ │ └─────────────┴─────────────┴──────────────────┘ │ │ │ │ │ ┌─────────┴─────────┐ │ │ │ StatusD │ │ │ │ (our Go app) │ │ │ └─────────┬─────────┘ │ │ │ │ └──────────────────────────────┼──────────────────────────────────┘ │ ┌────────────────┼────────────────┐ ▼ ▼ ▼ ┌────────┐ ┌────────┐ ┌────────┐ │Service │ │Service │ │Service │ │ A │ │ B │ │ C │ └────────┘ └────────┘ └────────┘ Struktur Proyek Sebelum kita menulis kode, mari kita pahami bagaimana potongan-potongan itu cocok bersama. status-monitor/ ├── cmd/statusd/ │ └── main.go # Application entry point ├── internal/ │ ├── models/ │ │ └── models.go # Data structures (Asset, Incident, etc.) │ ├── probe/ │ │ ├── probe.go # Probe registry │ │ └── http.go # HTTP probe implementation │ ├── scheduler/ │ │ └── scheduler.go # Worker pool and scheduling │ ├── alert/ │ │ └── engine.go # State machine and notifications │ ├── notifier/ │ │ └── teams.go # Teams/Slack integration │ ├── store/ │ │ └── postgres.go # Database layer │ ├── api/ │ │ └── handlers.go # REST API │ └── config/ │ └── manifest.go # Config loading ├── config/ │ ├── manifest.json # Services to monitor │ └── notifiers.json # Notification channels ├── migrations/ │ └── 001_init_schema.up.sql ├── docker-compose.yml ├── Dockerfile └── entrypoint.sh Model data inti Di sini kita akan mendefinisikan 'tipe', yang pada dasarnya berarti kita akan mendefinisikan apa yang terlihat sebagai 'layanan yang dipantau'. Kami akan mendefinisikan empat “tipe”: Aset: Ini adalah layanan yang ingin kami monitor. ProbeResult: Apa yang terjadi ketika kita memeriksa aset; respons, latensi, dll. Insiden: Ini melacak ketika sesuatu salah, yaitu, ketika ProbeResult mengembalikan tanggapan yang tidak terduga (dan ketika layanan pulih). Pemberitahuan: Ini adalah peringatan atau pesan yang dikirim ke saluran komunikasi yang didefinisikan, misalnya Teams, Slack, email, dll. Untuk mendefinisikan tipe dalam kode: // internal/models/models.go package models import "time" // Asset represents a monitored service type Asset struct { ID string `json:"id"` AssetType string `json:"assetType"` // http, tcp, dns, etc. Name string `json:"name"` Address string `json:"address"` IntervalSeconds int `json:"intervalSeconds"` TimeoutSeconds int `json:"timeoutSeconds"` ExpectedStatusCodes []int `json:"expectedStatusCodes,omitempty"` Metadata map[string]string `json:"metadata,omitempty"` } // ProbeResult contains the outcome of a single health check type ProbeResult struct { AssetID string Timestamp time.Time Success bool LatencyMs int64 Code int // HTTP status code Message string // Error message if failed } // Incident tracks a service outage type Incident struct { ID string AssetID string StartedAt time.Time EndedAt *time.Time // nil if still open Severity string Summary string } // Notification is what we send to Slack/Teams type Notification struct { AssetID string AssetName string Event string // "DOWN", "RECOVERY", "UP" Timestamp time.Time Details string } Perhatikan yang Tidak semua titik akhir mengembalikan 200, beberapa mungkin mengembalikan 204 atau redirect. ExpectedStatusCodes Rencana Database Kami akan menggunakan PostgreSQL untuk ini dan inilah skema kami: -- migrations/001_init_schema.up.sql CREATE TABLE IF NOT EXISTS assets ( id TEXT PRIMARY KEY, name TEXT NOT NULL, address TEXT NOT NULL, asset_type TEXT NOT NULL DEFAULT 'http', interval_seconds INTEGER DEFAULT 300, timeout_seconds INTEGER DEFAULT 5, expected_status_codes TEXT, metadata JSONB, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS probe_events ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), asset_id TEXT NOT NULL REFERENCES assets(id), timestamp TIMESTAMP WITH TIME ZONE NOT NULL, success BOOLEAN NOT NULL, latency_ms BIGINT NOT NULL, code INTEGER, message TEXT ); CREATE TABLE IF NOT EXISTS incidents ( id SERIAL PRIMARY KEY, asset_id TEXT NOT NULL REFERENCES assets(id), severity TEXT DEFAULT 'INITIAL', summary TEXT, started_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, ended_at TIMESTAMP ); -- Indexes for common queries CREATE INDEX IF NOT EXISTS idx_probe_events_asset_id_timestamp ON probe_events(asset_id, timestamp DESC); CREATE INDEX IF NOT EXISTS idx_incidents_asset_id ON incidents(asset_id); CREATE INDEX IF NOT EXISTS idx_incidents_ended_at ON incidents(ended_at); Kunci wawasan adalah pada Di sini, kami mengindeks oleh aset dan timestamp (dalam urutan turun), yang memungkinkan kami untuk dengan cepat menanyakan hasil survei layanan. probe_events(asset_id, timestamp DESC) Membangun sistem uji coba Hal-hal mulai menjadi menarik di sini. kami ingin mendukung penyelidikan atas beberapa jenis protokol: HTTPS, TCP, DNS, dll. tanpa harus menulis pernyataan switch yang kompleks. Pertama, kita akan mendefinisikan seperti apa sebuah probe: // internal/probe/probe.go package probe import ( "context" "fmt" "github.com/yourname/status/internal/models" ) // Probe defines the interface for checking service health type Probe interface { Probe(ctx context.Context, asset models.Asset) (models.ProbeResult, error) } // registry holds all probe types var registry = make(map[string]func() Probe) // Register adds a probe type to the registry func Register(assetType string, factory func() Probe) { registry[assetType] = factory } // GetProbe returns a probe for the given asset type func GetProbe(assetType string) (Probe, error) { factory, ok := registry[assetType] if !ok { return nil, fmt.Errorf("unknown asset type: %s", assetType) } return factory(), nil } Langkah-langkah untuk menggunakan HTTP probe: // internal/probe/http.go package probe import ( "context" "io" "net/http" "time" "github.com/yourname/status/internal/models" ) func init() { Register("http", func() Probe { return &httpProbe{} }) } type httpProbe struct{} func (p *httpProbe) Probe(ctx context.Context, asset models.Asset) (models.ProbeResult, error) { result := models.ProbeResult{ AssetID: asset.ID, Timestamp: time.Now(), } client := &http.Client{ Timeout: time.Duration(asset.TimeoutSeconds) * time.Second, } req, err := http.NewRequestWithContext(ctx, http.MethodGet, asset.Address, nil) if err != nil { result.Success = false result.Message = err.Error() return result, err } start := time.Now() resp, err := client.Do(req) result.LatencyMs = time.Since(start).Milliseconds() if err != nil { result.Success = false result.Message = err.Error() return result, err } defer resp.Body.Close() // Read body (limit to 1MB) io.ReadAll(io.LimitReader(resp.Body, 1024*1024)) result.Code = resp.StatusCode // Check if status code is expected if len(asset.ExpectedStatusCodes) > 0 { for _, code := range asset.ExpectedStatusCodes { if code == resp.StatusCode { result.Success = true return result, nil } } result.Success = false result.Message = "unexpected status code" } else { result.Success = resp.StatusCode < 400 } return result, nil } Fungsi init() berjalan secara otomatis ketika aplikasi Go Anda dimulai. ini menambahkan probe HTTP ke registri tanpa perubahan kode. Ingin menambahkan probes TCP? , menerapkan antarmuka, dan mendaftarkannya di . tcp.go init() Jadwal dan kompetisi Kami perlu menyelidiki semua Aset kami pada jadwal dan untuk ini kami akan menggunakan kolam buruh. kolam buruh memungkinkan kami untuk menjalankan beberapa probe secara bersamaan tanpa menghasilkan goroutine untuk setiap layanan. // internal/scheduler/scheduler.go package scheduler import ( "context" "sync" "time" "github.com/yourname/status/internal/models" "github.com/yourname/status/internal/probe" ) type JobHandler func(result models.ProbeResult) type Scheduler struct { workers int jobs chan models.Asset tickers map[string]*time.Ticker handler JobHandler mu sync.Mutex done chan struct{} wg sync.WaitGroup } func NewScheduler(workerCount int, handler JobHandler) *Scheduler { return &Scheduler{ workers: workerCount, jobs: make(chan models.Asset, 100), tickers: make(map[string]*time.Ticker), handler: handler, done: make(chan struct{}), } } func (s *Scheduler) Start(ctx context.Context) { for i := 0; i < s.workers; i++ { s.wg.Add(1) go s.worker(ctx) } } func (s *Scheduler) ScheduleAssets(assets []models.Asset) error { s.mu.Lock() defer s.mu.Unlock() for _, asset := range assets { interval := time.Duration(asset.IntervalSeconds) * time.Second ticker := time.NewTicker(interval) s.tickers[asset.ID] = ticker s.wg.Add(1) go s.scheduleAsset(asset, ticker) } return nil } func (s *Scheduler) scheduleAsset(asset models.Asset, ticker *time.Ticker) { defer s.wg.Done() for { select { case <-s.done: ticker.Stop() return case <-ticker.C: s.jobs <- asset } } } func (s *Scheduler) worker(ctx context.Context) { defer s.wg.Done() for { select { case <-s.done: return case asset := <-s.jobs: p, err := probe.GetProbe(asset.AssetType) if err != nil { continue } result, _ := p.Probe(ctx, asset) s.handler(result) } } } func (s *Scheduler) Stop() { close(s.done) close(s.jobs) s.wg.Wait() } Setiap aset mendapatkan goroutine ticker sendiri yang hanya jadwal bekerja. Ketika waktunya untuk memeriksa aset, ticker mengirimkan pekerjaan probe ke dalam saluran. Ada sejumlah goroutine pekerja yang mendengarkan saluran dan melakukan probing aktual. Kami tidak menjalankan probes langsung di goroutines ticker karena probes dapat memblokir sementara menunggu respons jaringan atau timeout. Misalnya, dengan 4 karyawan dan 100 aset, hanya 4 probe akan berjalan setiap saat bahkan jika tiker menembak secara bersamaan. Pastikan semua karyawan tetap bersih. sync.WaitGroup Deteksi Insiden: Mesin Negara Ketika sebuah probe gagal, kita tidak secara otomatis mengasumsikan kegagalan. Ini bisa menjadi kegagalan jaringan. Namun, jika gagal lagi, kita menciptakan insiden. Ini adalah mesin negara: UP → DOWN → UP. Lakukan pembuatan mesin: // internal/alert/engine.go package alert import ( "context" "fmt" "sync" "time" "github.com/yourname/status/internal/models" "github.com/yourname/status/internal/store" ) type NotifierFunc func(ctx context.Context, notification models.Notification) error type AssetState struct { IsUp bool LastProbeTime time.Time OpenIncidentID string } type Engine struct { store store.Store notifiers map[string]NotifierFunc mu sync.RWMutex assetState map[string]AssetState } func NewEngine(store store.Store) *Engine { return &Engine{ store: store, notifiers: make(map[string]NotifierFunc), assetState: make(map[string]AssetState), } } func (e *Engine) RegisterNotifier(name string, fn NotifierFunc) { e.mu.Lock() defer e.mu.Unlock() e.notifiers[name] = fn } func (e *Engine) Process(ctx context.Context, result models.ProbeResult, asset models.Asset) error { e.mu.Lock() defer e.mu.Unlock() state := e.assetState[result.AssetID] state.LastProbeTime = result.Timestamp // State hasn't changed? Nothing to do. if state.IsUp == result.Success { e.assetState[result.AssetID] = state return nil } // Save probe event if err := e.store.SaveProbeEvent(ctx, result); err != nil { return err } if result.Success && !state.IsUp { // Recovery! return e.handleRecovery(ctx, asset, state) } else if !result.Success && state.IsUp { // Outage! return e.handleOutage(ctx, asset, state, result) } return nil } func (e *Engine) handleOutage(ctx context.Context, asset models.Asset, state AssetState, result models.ProbeResult) error { incidentID, err := e.store.CreateIncident(ctx, asset.ID, fmt.Sprintf("Service %s is down", asset.Name)) if err != nil { return err } state.IsUp = false state.OpenIncidentID = incidentID e.assetState[asset.ID] = state notification := models.Notification{ AssetID: asset.ID, AssetName: asset.Name, Event: "DOWN", Timestamp: result.Timestamp, Details: result.Message, } return e.sendNotifications(ctx, notification) } func (e *Engine) handleRecovery(ctx context.Context, asset models.Asset, state AssetState) error { if state.OpenIncidentID != "" { e.store.CloseIncident(ctx, state.OpenIncidentID) } state.IsUp = true state.OpenIncidentID = "" e.assetState[asset.ID] = state notification := models.Notification{ AssetID: asset.ID, AssetName: asset.Name, Event: "RECOVERY", Timestamp: time.Now(), Details: "Service has recovered", } return e.sendNotifications(ctx, notification) } func (e *Engine) sendNotifications(ctx context.Context, notification models.Notification) error { for name, notifier := range e.notifiers { if err := notifier(ctx, notification); err != nil { fmt.Printf("notifier %s failed: %v\n", name, err) } } return nil } Key insight: Kami melacak keadaan dalam memori untuk pencarian cepat, tetapi insiden berlanjut ke database untuk ketahanan.Jika proses dimulai kembali, kita dapat membangun kembali status dari insiden terbuka. assetState Mengirim Notifikasi Jika ada yang rusak, orang perlu tahu. kita perlu mengirimkan pemberitahuan ke berbagai saluran komunikasi. Mari kita mendefinisikan pemberitahuan tim kami: // internal/notifier/teams.go package notifier import ( "bytes" "context" "encoding/json" "fmt" "net/http" "time" "github.com/yourname/status/internal/models" ) type TeamsNotifier struct { webhookURL string client *http.Client } func NewTeamsNotifier(webhookURL string) *TeamsNotifier { return &TeamsNotifier{ webhookURL: webhookURL, client: &http.Client{Timeout: 10 * time.Second}, } } func (t *TeamsNotifier) Notify(ctx context.Context, n models.Notification) error { emoji := "🟢" if n.Event == "DOWN" { emoji = "🔴" } card := map[string]interface{}{ "type": "message", "attachments": []map[string]interface{}{ { "contentType": "application/vnd.microsoft.card.adaptive", "content": map[string]interface{}{ "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", "type": "AdaptiveCard", "version": "1.4", "body": []map[string]interface{}{ { "type": "TextBlock", "text": fmt.Sprintf("%s %s - %s", emoji, n.AssetName, n.Event), "weight": "Bolder", "size": "Large", }, { "type": "FactSet", "facts": []map[string]interface{}{ {"title": "Service", "value": n.AssetName}, {"title": "Status", "value": n.Event}, {"title": "Time", "value": n.Timestamp.Format(time.RFC1123)}, {"title": "Details", "value": n.Details}, }, }, }, }, }, }, } body, _ := json.Marshal(card) req, _ := http.NewRequestWithContext(ctx, "POST", t.webhookURL, bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") resp, err := t.client.Do(req) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode >= 300 { return fmt.Errorf("Teams webhook returned %d", resp.StatusCode) } return nil } Teams menggunakan Adaptive Cards untuk pemformatan yang kaya.Anda dapat mendefinisikan berbagai notifikasi untuk saluran komunikasi lainnya, misalnya Slack, Discord, dll. Sisa api Kami membutuhkan titik akhir untuk menanyakan status layanan yang kami monitor. untuk ini, kami akan menggunakan Chi, yang merupakan router ringan yang mendukung parameter rute seperti . /assets/{id} Mari kita definisikan apis: // internal/api/handlers.go package api import ( "encoding/json" "net/http" "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" "github.com/yourname/status/internal/store" ) type Server struct { store store.Store mux *chi.Mux } func NewServer(s store.Store) *Server { srv := &Server{store: s, mux: chi.NewRouter()} srv.mux.Use(middleware.Logger) srv.mux.Use(middleware.Recoverer) srv.mux.Route("/api", func(r chi.Router) { r.Get("/health", srv.health) r.Get("/assets", srv.listAssets) r.Get("/assets/{id}/events", srv.getAssetEvents) r.Get("/incidents", srv.listIncidents) }) return srv } func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { s.mux.ServeHTTP(w, r) } func (s *Server) health(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]string{"status": "healthy"}) } func (s *Server) listAssets(w http.ResponseWriter, r *http.Request) { assets, err := s.store.GetAssets(r.Context()) if err != nil { http.Error(w, err.Error(), 500) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(assets) } func (s *Server) getAssetEvents(w http.ResponseWriter, r *http.Request) { id := chi.URLParam(r, "id") events, _ := s.store.GetProbeEvents(r.Context(), id, 100) w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(events) } func (s *Server) listIncidents(w http.ResponseWriter, r *http.Request) { incidents, _ := s.store.GetOpenIncidents(r.Context()) w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(incidents) } Kode di atas mendefinisikan server API HTTP kecil, yang mengekspos 4 titik akhir yang hanya dapat dibaca: GET /api/health - Pemeriksaan kesehatan (apakah layanan berjalan?) GET /api/assets - Daftar semua layanan yang dipantau GET /api/assets/{id}/events - Dapatkan riwayat pencarian untuk layanan tertentu GET /api/incidents - Daftar insiden terbuka Dockering aplikasi Dockerizing aplikasi cukup lurus karena Go kompilasi ke biner tunggal. kita akan menggunakan multi-etape build untuk menjaga gambar akhir kecil: # Dockerfile FROM golang:1.24-alpine AS builder WORKDIR /app RUN apk add --no-cache git COPY go.mod go.sum ./ RUN go mod download COPY . . RUN CGO_ENABLED=0 GOOS=linux go build -o statusd ./cmd/statusd/ FROM alpine:latest WORKDIR /app RUN apk --no-cache add ca-certificates COPY --from=builder /app/statusd . COPY entrypoint.sh . RUN chmod +x /app/entrypoint.sh EXPOSE 8080 ENTRYPOINT ["/app/entrypoint.sh"] Tahap terakhir hanya Alpine ditambah biner kami - biasanya di bawah 20MB. Script entry point membangun string koneksi database dari variabel lingkungan: #!/bin/sh # entrypoint.sh DB_HOST=${DB_HOST:-localhost} DB_PORT=${DB_PORT:-5432} DB_USER=${DB_USER:-status} DB_PASSWORD=${DB_PASSWORD:-status} DB_NAME=${DB_NAME:-status_db} DB_CONN_STRING="postgres://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_NAME}" exec ./statusd \ -manifest /app/config/manifest.json \ -notifiers /app/config/notifiers.json \ -db "$DB_CONN_STRING" \ -workers 4 \ -api-port 8080 Spesifikasi Docker Compose: Putting It All Together Satu file untuk mengatur mereka semua: # docker-compose.yml version: "3.8" services: postgres: image: postgres:15-alpine container_name: status_postgres environment: POSTGRES_USER: status POSTGRES_PASSWORD: changeme POSTGRES_DB: status_db volumes: - postgres_data:/var/lib/postgresql/data - ./migrations:/docker-entrypoint-initdb.d healthcheck: test: ["CMD-SHELL", "pg_isready -U status"] interval: 10s timeout: 5s retries: 5 networks: - status_network statusd: build: . container_name: status_app environment: - DB_HOST=postgres - DB_PORT=5432 - DB_USER=status - DB_PASSWORD=changeme - DB_NAME=status_db volumes: - ./config:/app/config:ro depends_on: postgres: condition: service_healthy networks: - status_network prometheus: image: prom/prometheus:latest container_name: status_prometheus volumes: - ./docker/prometheus.yml:/etc/prometheus/prometheus.yml - prometheus_data:/prometheus networks: - status_network depends_on: - statusd grafana: image: grafana/grafana:latest container_name: status_grafana environment: GF_SECURITY_ADMIN_USER: admin GF_SECURITY_ADMIN_PASSWORD: admin volumes: - grafana_data:/var/lib/grafana networks: - status_network depends_on: - prometheus nginx: image: nginx:alpine container_name: status_nginx volumes: - ./docker/nginx/nginx.conf:/etc/nginx/nginx.conf:ro - ./docker/nginx/conf.d:/etc/nginx/conf.d:ro ports: - "80:80" depends_on: - statusd - grafana - prometheus networks: - status_network networks: status_network: driver: bridge volumes: postgres_data: prometheus_data: grafana_data: Beberapa hal yang harus diperhatikan: PostgreSQL healthcheck: Statusd service menunggu sampai Postgres benar-benar siap, bukan baru dimulai. Config mount: Kami mount ./config sebagai read-only. Edit manifest Anda secara lokal, dan kontainer yang berjalan melihat perubahan. Nginx: Mengarahkan lalu lintas eksternal ke Grafana dan Prometheus dashboard. File Konfigurasi Aplikasi ini membaca dua file: dan manifest.json notifiers.json The file lists the assets we want to monitor. Each asset needs an ID, a probe type, and an address. The controls how often we check (60 = once per minute). lets you define what "healthy" means. Some endpoints return 301 redirects or 204 No Content, and that's fine. manifest.json intervalSeconds expectedStatusCodes // config/manifest.json { "assets": [ { "id": "api-prod", "assetType": "http", "name": "Production API", "address": "https://api.example.com/health", "intervalSeconds": 60, "timeoutSeconds": 5, "expectedStatusCodes": [200], "metadata": { "env": "prod", "owner": "platform-team" } }, { "id": "web-prod", "assetType": "http", "name": "Production Website", "address": "https://www.example.com", "intervalSeconds": 120, "timeoutSeconds": 10, "expectedStatusCodes": [200, 301] } ] } The controls where to send alerts. You define notification channels (Teams, Slack), then set policies for which channels fire on which events. means you won't get spammed more than once every 5 minutes for the same issue. notifiers.json throttleSeconds: 300 // config/notifiers.json { "notifiers": { "teams": { "type": "teams", "webhookUrl": "https://outlook.office.com/webhook/your-webhook-url" } }, "notificationPolicy": { "onDown": ["teams"], "onRecovery": ["teams"], "throttleSeconds": 300, "repeatAlerts": false } } berlari itu docker-compose up -d Itu saja. lima layanan spin up: PostgreSQL menyimpan data Anda StatusD menguji layanan Anda Prometheus mengumpulkan metrik Perangkat lunak yang digunakan untuk menampilkan dashboard (http://localhost:80) Nginx mengarahkan segalanya Periksa semua log: docker logs -f status_app Anda harus melihat: Loading assets manifest... Loaded 2 assets Loading notifiers config... Loaded 1 notifiers Connecting to database... Starting scheduler... [✓] Production API (api-prod): 45ms [✓] Production Website (web-prod): 120ms Pendekatan Sekarang Anda memiliki sistem pemantauan yang: Membaca layanan dari konfigurasi JSON Cobalah mereka pada jadwal menggunakan kolam buruh Mengidentifikasi gangguan dan menciptakan insiden Mengirim pemberitahuan ke Teams/Slack Metrik untuk Prometheus Mengoperasikan Docker dengan satu perintah Tutorial ini akan membantu Anda mengimplementasikan sistem pemantauan yang bekerja. Namun, ada lebih banyak di bawah topi yang kami jelaskan. Pemblokir sirkuit mencegah kegagalan kaskad saat layanan meledak Multi-tier eskalasi peringatan manajer jika insinyur on-call tidak merespon Deduplikasi peringatan mencegah badai pemberitahuan Adaptive probe interval lebih sering memeriksa selama insiden Hot-reload konfigurasi tanpa restart layanan Perhitungan SLA dan pelacakan kepatuhan