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

@@ -136,6 +136,7 @@ func (p *Plugin) Migrations() []*gormigrate.Migration {
| `lagoon.Migrate` | Runs framework and plugin migrations in order. |
| `lagoon.RollbackLast` | Rolls back the last migration of one plugin. |
| `lagoon.Status` | Lists applied migration IDs per plugin as `lagoon.StatusRow` values. |
| `lagoon.PendingMigrations` | Lists the migrations `lagoon.Migrate` would apply, as `lagoon.MigrationRef` values (`Plugin`, `ID`): the framework sets first, then each plugin in order, each in declaration order. A missing history table means the whole set is pending. Read-only; `serve` reports the count in its startup summary. |
| `lagoon.HistoryTableName` | Returns the gormigrate history table for a plugin ID. |
| `lagoon.BackendAdminMigrations` | Creates the backend user, role and admin token blacklist tables, the `backend_api_tokens` table for personal API tokens, seeds the system roles, adds a case-insensitive unique index on `backend_users.email` and adds WinterCMS's nullable jsonable `backend_users.metadata` column (TEXT; the admin subtitle lives under its `subtitle` key). The index migration refuses to run while emails differ only in case and names the clashing logins. Rolling back the metadata migration drops only that column. |
| `lagoon.QueueMigrations` | Migrates River's schema to `lagoon.RiverSchemaVersion` and creates the `summer_jobs` job record table. |

View File

@@ -201,3 +201,79 @@ func Status(gdb *gorm.DB, plugins []party.Plugin) ([]StatusRow, error) {
}
return rows, nil
}
// MigrationRef names one migration: the plugin (or framework set) that
// declares it and its ID.
type MigrationRef struct {
Plugin string
ID string
}
// migrationSet is one history table's declared migrations.
type migrationSet struct {
id string
migrations []*gormigrate.Migration
}
// migrationSets lists the sets Migrate applies, in its order: the framework
// sets (system files, backend admin, deferred bindings, job queue), then each
// plugin's HasMigrations set in plugin order.
func migrationSets(gdb *gorm.DB, plugins []party.Plugin) ([]migrationSet, error) {
sqlDB, err := gdb.DB()
if err != nil {
return nil, fmt.Errorf("lagoon: migration sets: %w", err)
}
sets := []migrationSet{
{"summercms.attach", attach.Migrations},
{"summercms.cabana", BackendAdminMigrations},
{DeferredHistoryID, DeferredBindingMigrations},
{QueueHistoryID, QueueMigrations(sqlDB)},
}
for _, p := range plugins {
hm, ok := p.(pact.HasMigrations)
if !ok {
continue
}
if ms := hm.Migrations(); len(ms) > 0 {
sets = append(sets, migrationSet{p.ID(), ms})
}
}
return sets, nil
}
// PendingMigrations returns the migrations Migrate would apply: each set's
// declared IDs that its history table does not hold, framework sets first,
// then plugins in order, each in declaration order. A missing history table
// means every migration of that set is pending. It only reads.
func PendingMigrations(gdb *gorm.DB, plugins []party.Plugin) ([]MigrationRef, error) {
if gdb == nil {
return nil, fmt.Errorf("lagoon: gorm db is nil")
}
sets, err := migrationSets(gdb, plugins)
if err != nil {
return nil, err
}
var pending []MigrationRef
for _, s := range sets {
table, err := HistoryTableName(s.id)
if err != nil {
return nil, err
}
applied := map[string]bool{}
if gdb.Migrator().HasTable(table) {
var ids []string
if err := gdb.Table(table).Pluck("id", &ids).Error; err != nil {
return nil, fmt.Errorf("lagoon: pending %s: %w", s.id, err)
}
for _, id := range ids {
applied[id] = true
}
}
for _, m := range s.migrations {
if m != nil && !applied[m.ID] {
pending = append(pending, MigrationRef{Plugin: s.id, ID: m.ID})
}
}
}
return pending, nil
}