136 lines
3.6 KiB
Go
136 lines
3.6 KiB
Go
package service
|
|
|
|
import (
|
|
"encoding/json"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// Config describes one internal Go service behind the Elysia gateway.
|
|
type Config struct {
|
|
Name string `json:"name"`
|
|
Domain string `json:"domain"`
|
|
DefaultPort string `json:"defaultPort"`
|
|
Capabilities []string `json:"capabilities"`
|
|
}
|
|
|
|
type response struct {
|
|
Status string `json:"status"`
|
|
Service string `json:"service"`
|
|
Domain string `json:"domain"`
|
|
Capabilities []string `json:"capabilities,omitempty"`
|
|
Time time.Time `json:"time"`
|
|
}
|
|
|
|
// EnvPort reads PORT with a safe fallback for local service runs.
|
|
func EnvPort(defaultPort string) string {
|
|
if port := strings.TrimSpace(os.Getenv("PORT")); port != "" {
|
|
return port
|
|
}
|
|
if strings.TrimSpace(defaultPort) != "" {
|
|
return defaultPort
|
|
}
|
|
return "8080"
|
|
}
|
|
|
|
// MustRun starts a service and terminates the process if startup fails.
|
|
func MustRun(cfg Config) {
|
|
if err := Run(cfg); err != nil {
|
|
log.Fatalf("%s stopped: %v", cfg.Name, err)
|
|
}
|
|
}
|
|
|
|
// Run starts an HTTP server for an internal service.
|
|
func Run(cfg Config) error {
|
|
port := EnvPort(cfg.DefaultPort)
|
|
server := &http.Server{
|
|
Addr: ":" + port,
|
|
Handler: requestIDMiddleware(loggingMiddleware(NewHandler(cfg))),
|
|
ReadHeaderTimeout: 5 * time.Second,
|
|
}
|
|
|
|
log.Printf("%s listening on :%s", cfg.Name, port)
|
|
return server.ListenAndServe()
|
|
}
|
|
|
|
// NewHandler returns the standard internal service HTTP API.
|
|
func NewHandler(cfg Config) http.Handler {
|
|
if cfg.Name == "" {
|
|
cfg.Name = "Fable Service"
|
|
}
|
|
if cfg.Domain == "" {
|
|
cfg.Domain = "unknown"
|
|
}
|
|
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
|
|
writeJSON(w, http.StatusOK, response{
|
|
Status: "ok",
|
|
Service: cfg.Name,
|
|
Domain: cfg.Domain,
|
|
Capabilities: cfg.Capabilities,
|
|
Time: time.Now().UTC(),
|
|
})
|
|
})
|
|
mux.HandleFunc("/ready", func(w http.ResponseWriter, r *http.Request) {
|
|
writeJSON(w, http.StatusOK, response{
|
|
Status: "ready",
|
|
Service: cfg.Name,
|
|
Domain: cfg.Domain,
|
|
Capabilities: cfg.Capabilities,
|
|
Time: time.Now().UTC(),
|
|
})
|
|
})
|
|
mux.HandleFunc("/metadata", func(w http.ResponseWriter, r *http.Request) {
|
|
writeJSON(w, http.StatusOK, cfg)
|
|
})
|
|
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
|
if routeAPI(w, r, cfg) {
|
|
return
|
|
}
|
|
if r.URL.Path != "/" {
|
|
writeJSON(w, http.StatusNotFound, map[string]string{"error": "route not found"})
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, response{
|
|
Status: "ok",
|
|
Service: cfg.Name,
|
|
Domain: cfg.Domain,
|
|
Capabilities: cfg.Capabilities,
|
|
Time: time.Now().UTC(),
|
|
})
|
|
})
|
|
|
|
return mux
|
|
}
|
|
|
|
func writeJSON(w http.ResponseWriter, status int, payload any) {
|
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
|
w.WriteHeader(status)
|
|
if err := json.NewEncoder(w).Encode(payload); err != nil {
|
|
log.Printf("write response: %v", err)
|
|
}
|
|
}
|
|
|
|
func requestIDMiddleware(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
requestID := strings.TrimSpace(r.Header.Get("X-Request-Id"))
|
|
if requestID == "" {
|
|
requestID = time.Now().UTC().Format("20060102150405.000000000")
|
|
}
|
|
w.Header().Set("X-Request-Id", requestID)
|
|
next.ServeHTTP(w, r)
|
|
})
|
|
}
|
|
|
|
func loggingMiddleware(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
started := time.Now()
|
|
next.ServeHTTP(w, r)
|
|
log.Printf("method=%s path=%s duration=%s", r.Method, r.URL.Path, time.Since(started))
|
|
})
|
|
}
|