feat(surf): serve startup summary, shutdown report and http request log

- serve prints a startup summary on a terminal (version, environment, URL,
  admin, database host and name, pending migrations, plugins, drivers) and
  logs it as a 'startup summary' record plus 'listening on <addr>'
- on SIGINT/SIGTERM serve drains, prints and logs a shutdown report and
  returns nil (exit 0), or the drain errors (exit 1)
- the request log is on by default under diary component http with
  request_id; the route pattern, never the path, is logged
- lagoon.PendingMigrations, conga.Worker.Stats, diary.HideOnTTY,
  bonfire.IsTerminal and bonfire.Details back the summary and report
This commit is contained in:
Jakub Zych
2026-10-21 12:59:07 +02:00
parent f0ee26f39f
commit c38dc1fe48
23 changed files with 1333 additions and 94 deletions

View File

@@ -17,6 +17,7 @@ import (
"charm.land/lipgloss/v2"
"git.golem15.com/golem15/summercms/modules/bonfire"
"github.com/charmbracelet/colorprofile"
"golang.org/x/term"
)
// componentWidth is the column width of the component prefix.
@@ -113,7 +114,12 @@ func (h *consoleHandler) WithGroup(name string) slog.Handler {
return c
}
func (h *consoleHandler) Handle(_ context.Context, r slog.Record) error {
func (h *consoleHandler) Handle(ctx context.Context, r slog.Record) error {
// The styled console is read by a person; facts already shown as a
// block (HideOnTTY) are not repeated as log lines.
if hiddenOnTTY(ctx) {
return nil
}
component := h.component
prefix := groupPrefix(h.groups)
var recAttrs []consoleAttr
@@ -168,6 +174,37 @@ func (h *consoleHandler) Handle(_ context.Context, r slog.Record) error {
return err
}
// ttyFilter drops records marked with HideOnTTY; it wraps a plain (JSON
// or logfmt) console channel whose writer is a terminal.
type ttyFilter struct {
slog.Handler
}
func (f ttyFilter) Enabled(ctx context.Context, l slog.Level) bool {
return !hiddenOnTTY(ctx) && f.Handler.Enabled(ctx, l)
}
func (f ttyFilter) Handle(ctx context.Context, r slog.Record) error {
if hiddenOnTTY(ctx) {
return nil
}
return f.Handler.Handle(ctx, r)
}
func (f ttyFilter) WithAttrs(attrs []slog.Attr) slog.Handler {
return ttyFilter{f.Handler.WithAttrs(attrs)}
}
func (f ttyFilter) WithGroup(name string) slog.Handler {
return ttyFilter{f.Handler.WithGroup(name)}
}
// isTerminal reports whether w is a terminal.
func isTerminal(w io.Writer) bool {
f, ok := w.(interface{ Fd() uintptr })
return ok && term.IsTerminal(int(f.Fd()))
}
func levelLabel(l slog.Level, p bonfire.Palette) (string, lipgloss.Style) {
switch {
case l < slog.LevelInfo: