fintual-backend-devops-go / go-service / operations.go
operations.go
Raw
package main

import (
	"context"
	"fmt"
	"log/slog"
	"net"
	"net/http"
	"strings"
	"sync/atomic"
	"time"
)

type operationalHandler struct {
	next     http.Handler
	ready    func(context.Context) error
	hosts    map[string]bool
	slots    chan struct{}
	sequence atomic.Uint64
	logger   *slog.Logger
}

type operationalWriter struct {
	http.ResponseWriter
	status int
}

func (w *operationalWriter) WriteHeader(status int) {
	if w.status == 0 {
		w.status = status
		w.ResponseWriter.WriteHeader(status)
	}
}
func (w *operationalWriter) Write(p []byte) (int, error) {
	if w.status == 0 {
		w.WriteHeader(200)
	}
	return w.ResponseWriter.Write(p)
}

func (h *operationalHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	start := time.Now()
	w.Header().Set("X-Request-ID", fmt.Sprintf("%x-%x", start.UnixNano(), h.sequence.Add(1)))
	out := &operationalWriter{ResponseWriter: w}
	defer func() {
		if recover() != nil {
			if out.status == 0 {
				writeJSON(out, 500, map[string]string{"detail": "Internal Server Error"})
			}
			h.logger.Error("handler panic", "request_id", w.Header().Get("X-Request-ID"))
		}
		status := out.status
		if status == 0 {
			status = 200
		}
		h.logger.Info("request", "request_id", w.Header().Get("X-Request-ID"), "method", r.Method, "status", status, "duration_ms", float64(time.Since(start))/1e6)
	}()
	host := r.Host
	if name, _, err := net.SplitHostPort(host); err == nil {
		host = name
	}
	if !h.hosts[strings.ToLower(host)] {
		writeJSON(out, 400, map[string]string{"detail": "Invalid host"})
		return
	}
	// Forwarded and X-Forwarded-* headers are deliberately not trusted.
	if r.URL.Path == "/healthz" || r.URL.Path == "/readyz" {
		if r.Method != "GET" {
			out.Header().Set("Allow", "GET")
			out.WriteHeader(405)
			return
		}
		if r.URL.Path == "/readyz" {
			ctx, cancel := context.WithTimeout(r.Context(), time.Second)
			defer cancel()
			if err := h.ready(ctx); err != nil {
				writeJSON(out, 503, map[string]string{"status": "unavailable"})
				return
			}
		}
		writeJSON(out, 200, map[string]string{"status": "ok"})
		return
	}
	select {
	case h.slots <- struct{}{}:
		defer func() { <-h.slots }()
	default:
		writeJSON(out, 503, map[string]string{"detail": "Busy"})
		return
	}
	const maxBody = 1 << 20
	if r.ContentLength > maxBody {
		writeJSON(out, 413, map[string]string{"detail": "Request body too large"})
		return
	}
	r.Body = http.MaxBytesReader(out, r.Body, maxBody)
	ctx, cancel := context.WithTimeout(r.Context(), 15*time.Second)
	defer cancel()
	h.next.ServeHTTP(out, r.WithContext(ctx))
}