feat: add legacy-api service
실험·앱 계층 설계서 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).
This commit is contained in:
commit
7186ef0c49
5 files changed
+221
No files matched your search
@@ -0,0 +1,112 @@
|
||||
// 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())
|
||||
}
|
||||
Reference in new issue
Block a user