repos
/ orchard main

orchard

mirror

Every site I host, in one repo, along with the Cloudflare Tunnel and Caddy that front them. It's all Go, Vite, and SQLite, and it runs on a desktop at home with nothing listening on an inbound port.

blogbuncaddycloudflare-tunneldockergogolanghomelabhtml-templatemonorepoself-hostedseosqlitestatic-sitetypstuptime-monitoringviteweb-analytics

2.4 KB · 89 lines · Go Raw History
 1package web
 2
 3import (
 4	"context"
 5	"errors"
 6	"fmt"
 7	"io"
 8	"log/slog"
 9	"net/http"
10	"os"
11	"os/signal"
12	"syscall"
13	"time"
14)
15
16// SetupLogging installs the process-wide structured logger. JSON, because these
17// logs are read by machine before they are read by a person.
18func SetupLogging() {
19	slog.SetDefault(slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
20		Level: slog.LevelInfo,
21		// UTC is not slog's default, and local time in a container
22		// silently differs from the host's.
23		ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
24			if a.Key == slog.TimeKey && len(groups) == 0 {
25				a.Value = slog.TimeValue(a.Value.Time().UTC())
26			}
27			return a
28		},
29	})))
30}
31
32// Serve runs h on addr until SIGINT or SIGTERM, then drains in-flight requests
33// before returning.
34func Serve(addr string, h http.Handler) error {
35	srv := &http.Server{
36		Addr:              addr,
37		Handler:           h,
38		ReadHeaderTimeout: 10 * time.Second,
39		// Whole-request bounds, so a slow body or a wedged handler cannot
40		// hold a connection open forever. The write bound is generous enough
41		// for a Typst report, which has its own 30 second ceiling.
42		ReadTimeout:  30 * time.Second,
43		WriteTimeout: 60 * time.Second,
44		IdleTimeout:  120 * time.Second,
45	}
46
47	stop := make(chan os.Signal, 1)
48	signal.Notify(stop, os.Interrupt, syscall.SIGTERM)
49
50	errs := make(chan error, 1)
51	go func() {
52		slog.Info("listening", slog.String("addr", addr))
53		if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
54			errs <- err
55		}
56	}()
57
58	select {
59	case err := <-errs:
60		return err
61	case sig := <-stop:
62		slog.Info("shutting down", slog.String("signal", sig.String()))
63	}
64
65	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
66	defer cancel()
67	return srv.Shutdown(ctx)
68}
69
70// HealthCheck probes a running server over the loopback and reports whether it
71// answered 200, so a container can check itself. Two of these images are FROM
72// scratch, with no shell and no curl for HEALTHCHECK to call.
73func HealthCheck(url string, timeout time.Duration) error {
74	client := &http.Client{Timeout: timeout}
75
76	resp, err := client.Get(url)
77	if err != nil {
78		return err
79	}
80	// Drained as well as closed, or the connection leaks out of the pool.
81	defer resp.Body.Close()
82	_, _ = io.Copy(io.Discard, resp.Body)
83
84	if resp.StatusCode != http.StatusOK {
85		return fmt.Errorf("%s: %s", url, resp.Status)
86	}
87	return nil
88}