REST API rendszer infóval: stackctl
Az envcheck után az volt a kérdés: hogyan lépek tovább a fájl-beolvasástól a hálózati kommunikációig? A stackctl erre ad választ – egy REST API ami a szerver állapotát teszi elérhetővé HTTP-n keresztül.
A gondolat onnan jött, hogy a szervereimet távolról szerettem volna lekérdezni: mennyi a memória, mekkora a lemezhasználat, milyen konténerek futnak. SSH-val persze meg lehet nézni, de egy curl paranccsal egyszerűbb – és scriptelhető.
Mit csinál?
Három végpont van. A /health egy egyszerű életjel – ha válaszol, a szerver fut. A /info visszaadja a rendszer információt: uname, uptime, memória, lemezhasználat. A /stacks pedig a futó konténereket listázza JSON formátumban.
Indítás után a stackctl egy porton figyel és vár a kérésekre:
stackctl -port 9090 -runtime docker
Egy másik terminálból:
curl localhost:9090/health
{"status":"ok","runtime":"docker"}
curl localhost:9090/info
{"system":"Linux srv 6.1.0 ...","uptime":"up 12 days",...}
Ami új volt a Go-ban
Az envcheck-ben fájlokat olvastam be. A stackctl-ben HTTP kéréseket szolgálok ki – és meglepően kevés kód kell hozzá.
A Go net/http csomagja három sorban ad egy működő webszervert. A http.HandleFunc regisztrál egy útvonalat, a http.ListenAndServe elindítja a szervert. Nincs framework, nincs konfiguráció, nincs middleware lánc. Csak a standard library.
A másik új elem az os/exec csomag. Ezzel futtatok shell parancsokat Go-ból – uname -a, free -h, df -h /. Lényegében ugyanaz mint bash-ben a $(uname -a), csak Go-ban a kimenetet explicit kell kezelni. Az exec.Command visszaad egy objektumot, az .Output() metódus futtatja és visszaadja az eredményt byte tömbként.
Handler függvények
Az eredeti verzióban az összes handler a main() függvényben volt anonim függvényként. A production ready verzióban ezeket kiszerveztem nevesített függvényekbe:
func healthHandler(cfg config) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
writeJSON(w, HealthResponse{
Status: "ok",
Runtime: cfg.runtime,
})
}
}
Ez a minta – egy függvény ami visszaad egy másik függvényt – először furcsa volt. De van értelme: a külső függvény megkapja a konfigurációt, a belső pedig a HTTP kérést. Így a handler ismeri a beállításokat anélkül hogy globális változókat használnánk.
JSON válaszok
Az eredeti verzióban kézzel raktam össze a JSON stringeket fmt.Sprintf-fel. Ez törékeny – egy elírt idézőjel és érvénytelen a JSON. A production ready verzióban struct-okat definiálok és a json.NewEncoder sorosítja őket:
type InfoResponse struct {
System string `json:"system"`
Uptime string `json:"uptime"`
Memory string `json:"memory"`
Disk string `json:"disk"`
}
A backtick-es tag-ek (json:"system") mondják meg a Go-nak hogy a struct mező milyen néven jelenjen meg a JSON kimenetben. Ez a Go módja a serializációnak – nincs annotation, nincs dekorátor, csak struct tag-ek.
Tesztelés HTTP nélkül
A Go net/http/httptest csomagja lehetővé teszi hogy HTTP handlereket tesztelj anélkül hogy ténylegesen elindítanád a szervert. Egy httptest.NewRecorder() szimulálja a HTTP választ, egy httptest.NewRequest() pedig a kérést:
req := httptest.NewRequest("GET", "/health", nil)
rec := httptest.NewRecorder()
handler(rec, req)
Ez után a rec.Code a HTTP státusz kód, a rec.Body pedig a válasz tartalma. Nincs port, nincs hálózat, nincs várakozás – a teszt milliszekundum alatt lefut.
Mit tanultam?
A stackctl három alapvető Go koncepciót tanított meg:
A HTTP szerver a net/http csomaggal – hogyan regisztrálj útvonalakat, hogyan írj JSON választ, hogyan kezeld a hibákat HTTP szinten.
A shell parancs futtatás az os/exec-kel – hogyan hívj meg külső programokat, hogyan kapd el a kimenetüket, és hogyan kezeld ha nem sikerül.
A handler minta – függvények amik függvényeket adnak vissza, és a httptest csomag amivel mindezt teszteled szerver indítás nélkül.
Hogyan próbáld ki?
git clone https://github.com/brtkcs/werkstatt-tools.git
cd werkstatt-tools/stackctl
go run main.go
Egy másik terminálból:
curl localhost:8080/health
curl localhost:8080/info
curl localhost:8080/stacks
A teljes forráskód a werkstatt-tools repóban.