refactor: framework modules log through diary components

- Each module logs through diary.For(<package>): cabana, lagoon, attach, cooler, postcard, beachcomber, lighthouse, centrifugo, flare, phrasebook, wristband, bouncer
- App-published *slog.Logger overrides stay first; messages and attributes unchanged
- sunscreen: InstallDefault documented as the compatibility entry point; diary wraps every channel
- READMEs list diary as a dependency; logging docs: plugins use diary.For
This commit is contained in:
Jakub Zych
2026-10-21 11:58:22 +02:00
parent 71b99317ad
commit 44291adcca
53 changed files with 185 additions and 130 deletions

View File

@@ -37,7 +37,7 @@ Use cases:
- Column types: `lagoon.Encrypted` stores AES-256-GCM ciphertext under a key derived from `app.key`, decrypts with previous keys during rotation, and always redacts itself in JSON and string output; `lagoon.Jsonable` stores JSON as TEXT and keeps SQL NULL distinct from an empty value.
- Lifecycle and relations: hook interfaces matching GORM's native method names (`lagoon.HasBeforeCreate`, `lagoon.HasBeforeSave`, `lagoon.HasBeforeDelete`, `lagoon.HasAfterDelete`) plus `lagoon.HasBeforeValidate`; `lagoon.WithSoftDeleteCascade` runs a cascade inside the parent delete; `lagoon.RegisterJoinTable` wires pivot models with business columns.
- Deferred binding: WinterCMS's `deferred_bindings` table holds the uploads and related-record changes of a form whose record is not saved yet. Every operation takes a `lagoon.DeferredKey` (the form's session key, the owning backend admin's id and the master record's morph type from `lagoon.MorphType`) and never reads or changes another admin's rows, since each row stores `backend_user_id`. `lagoon.DeferredBind` and `lagoon.DeferredUnbind` port WinterCMS's duplicate and cancel rules: a repeated bind writes nothing, and an unbind of a slave with a pending bind deletes that bind and returns it so the caller can remove what it created. `lagoon.DeferredBindings` reads and locks a session's bindings for the save that commits them, `lagoon.DeferredForget` deletes them once applied, and `lagoon.DeferredSlaves` is the subquery a list uses to include pending rows. A child created under deferral carries the `lagoon.DeferredEnvelope` (`{"created":true,"pivot":{...},"upload_id":"..."}`) in `pivot_data`. `lagoon.PurgeDeferred` removes expired bindings: it deletes an unattached `system_files` row a bind points at, and its blobs only after the commit, deletes a child only when its binding carries the created envelope, keeps records that were only linked, and locks each batch with `FOR UPDATE SKIP LOCKED`.
- Logging: GORM's lines no longer go to stdout through GORM's own logger. They arrive through [diary](../diary/README.md) as component `db`: slow queries (over 200 ms) and errors at warn, while a missing record (`gorm.ErrRecordNotFound`) is no longer logged. At verbosity 3 (`-vvv`) every query is traced at info, with placeholders instead of the bound values.
- Logging: GORM's lines no longer go to stdout through GORM's own logger. They arrive through [diary](../diary/README.md) as component `db`: slow queries (over 200 ms) and errors at warn, while a missing record (`gorm.ErrRecordNotFound`) is no longer logged. At verbosity 3 (`-vvv`) every query is traced at info, with placeholders instead of the bound values. lagoon's own warnings log as component `lagoon`, and `attach` as component `attach`.
- Imports from Laravel: `lagoon.DecryptLaravelPayload` decrypts Laravel `encrypted` payloads with the old application key, for one-off data imports.
- Attachments (`attach`): the `attach.File` model for `system_files` rows, WinterCMS-compatible partitioned storage keys (`attach.BlobKey`, `attach.PartitionDirectory`), an S3 bucket option behind the `attach.S3BuildTag` build tag (an untagged binary refuses an `s3://` bucket URL at boot with an error naming the tag), public URLs (`attach.PublicURL` for any key, under `storage.uploads.public_url` when a public bucket or CDN serves the files (`attach.PublicURLBase`), `attach.File.URL` for an original, matching WinterCMS's `File::getPath()` under the WinterCMS layout), on-demand thumbnails through `attach.File.Thumb` for JPEG, PNG, GIF and WebP originals (a WebP original's thumbnail is JPEG bytes under its `.webp` name, since WebP cannot be encoded; a missing, undecodable or oversized original gets WinterCMS's broken-image picture, `attach.BrokenImagePNG`, as its thumbnail, as `File::makeThumb` does), storing uploads through `attach.Store` (a server-generated disk name whose extension follows the sniffed content through `attach.DiskExtension`, so a PNG named `x.html` is stored as `.png`, an extension allow-list with `attach.DefaultImageExtensions` and `attach.DefaultFileExtensions` as defaults, a MIME filter, a size limit enforced while streaming, in image mode the `attach.IsAllowedImage` content guard, and the refusal of protected uploads while `storage.uploads.public_url` serves the bucket publicly, `attach.ErrProtectedNeedsPrivateBucket`), attachment relation declarations (`attach.Relation`, `attach.HasRelations`), static serving with an optional `is_public` gate (`attach.StaticHandlerPublic`), and a two-phase delete that removes blobs only after the database transaction commits (`attach.DeleteForOwner`, `attach.DeleteKeys`).

View File

@@ -10,11 +10,16 @@ import (
"git.golem15.com/golem15/summercms/modules/backpack"
"git.golem15.com/golem15/summercms/modules/compass"
"git.golem15.com/golem15/summercms/modules/diary"
"gocloud.dev/blob"
_ "gocloud.dev/blob/fileblob"
_ "gocloud.dev/blob/memblob"
)
// moduleLog is attach's logger: records carry component=attach and follow
// log.components.attach (diary).
var moduleLog = diary.For("attach")
const defaultPublicPathPrefix = "/storage/uploads"
// S3BuildTag is the Go build tag that links the gocloud S3 driver
@@ -150,7 +155,7 @@ func OpenBucket(ctx context.Context, cfg *compass.Config) (*blob.Bucket, error)
return bucket, nil
}
setPublicURLBase(publicURL.String())
slog.Default().WarnContext(ctx, "attach: storage.uploads.public_url is set; every file and thumbnail URL points at this host, "+
moduleLog.WarnContext(ctx, "attach: storage.uploads.public_url is set; every file and thumbnail URL points at this host, "+
"which serves any key without the is_public check, so the bucket behind it must hold only public files: "+
"protected (is_public false) uploads are refused",
slog.String("host", publicURL.Host))

View File

@@ -259,7 +259,7 @@ var BrokenImagePNG = func() []byte {
// brokenThumb is the catch branch of WinterCMS's File::makeThumb: log the
// reason, store BrokenImagePNG under the thumbnail key and return that key.
func brokenThumb(ctx context.Context, bucket *blob.Bucket, f *File, thumbKey string, reason error) (string, error) {
slog.Default().WarnContext(ctx, "attach: thumbnail original is unusable, storing the broken-image picture",
moduleLog.WarnContext(ctx, "attach: thumbnail original is unusable, storing the broken-image picture",
slog.Uint64("file_id", uint64(f.ID)), slog.String("error", reason.Error()))
if err := bucket.WriteAll(ctx, thumbKey, BrokenImagePNG, &blob.WriterOptions{ContentType: "image/png"}); err != nil {
return "", fmt.Errorf("attach: broken-image thumb: %w", err)

View File

@@ -16,6 +16,10 @@ import (
"gorm.io/gorm/logger"
)
// moduleLog is lagoon's logger: records carry component=lagoon and follow
// log.components.lagoon (diary).
var moduleLog = diary.For("lagoon")
// Open pings dsn through pgx stdlib and returns that exact *sql.DB plus a
// GORM handle opened on it.
//

View File

@@ -5,7 +5,6 @@ import (
"encoding"
"encoding/json"
"fmt"
"log/slog"
"reflect"
"strconv"
"strings"
@@ -84,7 +83,7 @@ func logDroppedKeyOnce(production bool, typeName, key string) {
if _, loaded := droppedKeys.LoadOrStore(k, struct{}{}); loaded {
return
}
slog.Warn("lagoon: dropped non-fillable key", "type", typeName, "key", key)
moduleLog.Warn("lagoon: dropped non-fillable key", "type", typeName, "key", key)
}
func fieldByColumn(rv reflect.Value, rt reflect.Type, column string) (reflect.Value, bool) {

View File

@@ -125,7 +125,7 @@ func PurgeDeferred(ctx context.Context, db *gorm.DB, bucket *blob.Bucket, opts P
if len(keys) > 0 {
AfterCommit(ctx, tx, func(ctx context.Context, _ *gorm.DB) {
if err := attach.DeleteKeys(context.WithoutCancel(ctx), bucket, keys); err != nil {
slog.Default().WarnContext(ctx, "lagoon: purge could not delete file blobs", slog.String("error", err.Error()))
moduleLog.WarnContext(ctx, "lagoon: purge could not delete file blobs", slog.String("error", err.Error()))
}
})
}
@@ -178,7 +178,7 @@ func purgeBinding(ctx context.Context, tx *gorm.DB, bucket *blob.Bucket, opts Pu
}
env, err := row.Envelope()
if err != nil {
slog.Default().WarnContext(ctx, "lagoon: purge treats an unreadable deferred binding as a plain link",
moduleLog.WarnContext(ctx, "lagoon: purge treats an unreadable deferred binding as a plain link",
slog.Uint64("binding_id", uint64(row.ID)), slog.String("error", err.Error()))
return true, nil
}
@@ -193,7 +193,7 @@ func purgeBinding(ctx context.Context, tx *gorm.DB, bucket *blob.Bucket, opts Pu
if !ok || model == nil {
if !warned[row.SlaveType] {
warned[row.SlaveType] = true
slog.Default().WarnContext(ctx, "lagoon: purge cannot resolve the model of a deferred child; its bindings are kept",
moduleLog.WarnContext(ctx, "lagoon: purge cannot resolve the model of a deferred child; its bindings are kept",
slog.String("slave_type", row.SlaveType))
}
res.Skipped++

View File

@@ -3,7 +3,6 @@ package lagoon
import (
"context"
"fmt"
"log/slog"
"sync"
"gorm.io/gorm"
@@ -127,7 +126,7 @@ func AfterCommit(ctx context.Context, db *gorm.DB, fn func(ctx context.Context,
return
}
if transactionalHandle(db) {
slog.Default().Warn("lagoon: after-commit callback skipped inside unmanaged transaction")
moduleLog.Warn("lagoon: after-commit callback skipped inside unmanaged transaction")
return
}
}
@@ -196,7 +195,7 @@ func runAfterCommit(ctx context.Context, db *gorm.DB, fns []func(context.Context
func() {
defer func() {
if r := recover(); r != nil {
slog.Default().Warn("lagoon: after-commit callback panicked", "panic", fmt.Sprint(r))
moduleLog.Warn("lagoon: after-commit callback panicked", "panic", fmt.Sprint(r))
}
}()
fn(ctx, db)