실험·앱 계층 설계서 3.2의 legacy-api(VM 계층). /healthz(프로세스 생존),
/v1/price/{item}(db-primary app.price 조회, DB 불가 시 503), /metrics(Prometheus).
Go 1.27.1, pgx v5.11.0, client_golang v1.24.1. 이미지는 Cloud Build로 Artifact Registry에
올린다(MIG 인스턴스가 VM SA로 pull).
113 lines
3.5 KiB
Go
113 lines
3.5 KiB
Go
// legacy-api: the "VM layer" service of the self-healing lab (experiment design 3.2).
|
|
// GET /healthz process liveness only (MIG autohealing + ILB health check)
|
|
// GET /v1/price/{item} reads app.price on db-primary (503 when the DB is unreachable)
|
|
// GET /metrics Prometheus exposition
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"encoding/json"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
_ "github.com/jackc/pgx/v5/stdlib"
|
|
"github.com/prometheus/client_golang/prometheus"
|
|
"github.com/prometheus/client_golang/prometheus/promhttp"
|
|
)
|
|
|
|
var (
|
|
reqTotal = prometheus.NewCounterVec(prometheus.CounterOpts{
|
|
Name: "legacy_http_requests_total", Help: "HTTP requests by route and status",
|
|
}, []string{"route", "status"})
|
|
reqDur = prometheus.NewHistogramVec(prometheus.HistogramOpts{
|
|
Name: "legacy_http_request_duration_seconds", Help: "HTTP request latency",
|
|
Buckets: []float64{.005, .01, .025, .05, .1, .25, .5, 1, 2.5, 5},
|
|
}, []string{"route"})
|
|
)
|
|
|
|
type statusWriter struct {
|
|
http.ResponseWriter
|
|
code int
|
|
}
|
|
|
|
func (w *statusWriter) WriteHeader(c int) { w.code = c; w.ResponseWriter.WriteHeader(c) }
|
|
|
|
func instrument(route string, h http.HandlerFunc) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
sw := &statusWriter{ResponseWriter: w, code: 200}
|
|
start := time.Now()
|
|
h(sw, r)
|
|
reqDur.WithLabelValues(route).Observe(time.Since(start).Seconds())
|
|
reqTotal.WithLabelValues(route, strconv.Itoa(sw.code)).Inc()
|
|
}
|
|
}
|
|
|
|
func writeJSON(w http.ResponseWriter, code int, v any) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(code)
|
|
_ = json.NewEncoder(w).Encode(v)
|
|
}
|
|
|
|
func main() {
|
|
prometheus.MustRegister(reqTotal, reqDur)
|
|
port := os.Getenv("PORT")
|
|
if port == "" {
|
|
port = "8080"
|
|
}
|
|
dsn := os.Getenv("DB_DSN")
|
|
var db *sql.DB
|
|
if dsn != "" {
|
|
var err error
|
|
if db, err = sql.Open("pgx", dsn); err != nil {
|
|
log.Printf("db open: %v", err)
|
|
db = nil
|
|
} else {
|
|
db.SetMaxOpenConns(8)
|
|
db.SetConnMaxLifetime(5 * time.Minute)
|
|
}
|
|
} else {
|
|
log.Printf("DB_DSN not set: /v1/price will answer 503")
|
|
}
|
|
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc("/healthz", instrument("healthz", func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "text/plain")
|
|
_, _ = w.Write([]byte("ok\n"))
|
|
}))
|
|
mux.HandleFunc("/v1/price/", instrument("price", func(w http.ResponseWriter, r *http.Request) {
|
|
item := strings.TrimPrefix(r.URL.Path, "/v1/price/")
|
|
if item == "" || strings.Contains(item, "/") {
|
|
writeJSON(w, 404, map[string]string{"error": "item required"})
|
|
return
|
|
}
|
|
if db == nil {
|
|
writeJSON(w, 503, map[string]string{"error": "db unavailable"})
|
|
return
|
|
}
|
|
ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second)
|
|
defer cancel()
|
|
var price float64
|
|
var updated time.Time
|
|
err := db.QueryRowContext(ctx, "SELECT price, updated_at FROM app.price WHERE item = $1", item).Scan(&price, &updated)
|
|
switch {
|
|
case err == sql.ErrNoRows:
|
|
writeJSON(w, 404, map[string]string{"error": "unknown item", "item": item})
|
|
case err != nil:
|
|
log.Printf("price query: %v", err)
|
|
writeJSON(w, 503, map[string]string{"error": "db unavailable"})
|
|
default:
|
|
writeJSON(w, 200, map[string]any{"item": item, "price": price, "updated_at": updated.UTC().Format(time.RFC3339)})
|
|
}
|
|
}))
|
|
mux.Handle("/metrics", promhttp.Handler())
|
|
|
|
srv := &http.Server{Addr: ":" + port, Handler: mux, ReadHeaderTimeout: 5 * time.Second}
|
|
log.Printf("legacy-api listening on :%s", port)
|
|
log.Fatal(srv.ListenAndServe())
|
|
}
|