Feature 01 task 1: Go module, fail-fast config and /health (#2)
Co-authored-by: dev_agent <dev_agent@cubecraftlabs.com> Co-committed-by: dev_agent <dev_agent@cubecraftlabs.com>
This commit was merged in pull request #2.
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
// Command server runs the MealQuest HTTP service.
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"code.cubecraftlabs.com/Maniacs_Mansion/Meal-Quest/internal/config"
|
||||
"code.cubecraftlabs.com/Maniacs_Mansion/Meal-Quest/internal/server"
|
||||
)
|
||||
|
||||
// version is set at build time with -ldflags "-X main.version=<version>".
|
||||
var version = "dev"
|
||||
|
||||
func main() {
|
||||
cfg, err := config.Load()
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
|
||||
if err := run(cfg, logger); err != nil {
|
||||
logger.Error("server stopped", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func run(cfg config.Config, logger *slog.Logger) error {
|
||||
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||||
defer stop()
|
||||
|
||||
srv := &http.Server{
|
||||
Addr: cfg.HTTPAddr,
|
||||
Handler: server.NewRouter(logger, version),
|
||||
ReadHeaderTimeout: 10 * time.Second,
|
||||
ErrorLog: slog.NewLogLogger(logger.Handler(), slog.LevelError),
|
||||
}
|
||||
|
||||
errc := make(chan error, 1)
|
||||
go func() {
|
||||
logListening(logger, cfg)
|
||||
errc <- srv.ListenAndServe()
|
||||
}()
|
||||
|
||||
select {
|
||||
case err := <-errc:
|
||||
return err
|
||||
case <-ctx.Done():
|
||||
}
|
||||
|
||||
logger.Info("shutting down", "timeout", cfg.ShutdownTimeout.String())
|
||||
shutdownCtx, cancel := context.WithTimeout(context.Background(), cfg.ShutdownTimeout)
|
||||
defer cancel()
|
||||
if err := srv.Shutdown(shutdownCtx); err != nil {
|
||||
return fmt.Errorf("shutdown: %w", err)
|
||||
}
|
||||
if err := <-errc; !errors.Is(err, http.ErrServerClosed) {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// logListening logs the startup line. Only the public URL's origin is logged
|
||||
// so no credential-bearing URL component can reach the log.
|
||||
func logListening(logger *slog.Logger, cfg config.Config) {
|
||||
logger.Info("listening",
|
||||
"addr", cfg.HTTPAddr,
|
||||
"public_origin", cfg.PublicOrigin(),
|
||||
"data_dir", cfg.DataDir,
|
||||
"version", version,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"log/slog"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"code.cubecraftlabs.com/Maniacs_Mansion/Meal-Quest/internal/config"
|
||||
)
|
||||
|
||||
func TestLogListeningOmitsURLCredentials(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Load rejects such URLs; this proves the log stays safe even if one
|
||||
// reached the Config some other way.
|
||||
u, err := url.Parse("https://alice:hunter2@meals.example.com/app?token=t0ken")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
logListening(slog.New(slog.NewJSONHandler(&buf, nil)), config.Config{HTTPAddr: ":8080", PublicURL: u})
|
||||
|
||||
out := buf.String()
|
||||
for _, leak := range []string{"alice", "hunter2", "t0ken"} {
|
||||
if strings.Contains(out, leak) {
|
||||
t.Errorf("startup log leaks %q: %s", leak, out)
|
||||
}
|
||||
}
|
||||
if !strings.Contains(out, `"public_origin":"https://meals.example.com"`) {
|
||||
t.Errorf("startup log missing public origin: %s", out)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user