// 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()) }