{"id":50362365,"url":"https://github.com/adrielcodeco/go-fiber-gorm","last_synced_at":"2026-05-30T02:30:28.617Z","repository":{"id":357688722,"uuid":"1238075831","full_name":"adrielcodeco/go-fiber-gorm","owner":"adrielcodeco","description":"Fiber + GORM toolbox: request-scoped DB transactions (lazy BEGIN, OnCommit/OnRollback) and Kubernetes-aware graceful shutdown (phases, hooks, drain, readiness, force-kill). Supports Fiber v2 and v3.","archived":false,"fork":false,"pushed_at":"2026-05-13T20:58:47.000Z","size":47,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-13T22:38:02.102Z","etag":null,"topics":["compensating-transactions","context-cancellation","database","fiber","fiber-v2","fiber-v3","go","gofiber","golang","gorm","graceful-shutdown","k8s","kubernetes","middleware","outbox-pattern","production-ready","readiness-probe","shutdown","transaction","transactions"],"latest_commit_sha":null,"homepage":"","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/adrielcodeco.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null},"funding":{"github":"adrielcodeco"}},"created_at":"2026-05-13T19:41:03.000Z","updated_at":"2026-05-13T21:01:18.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/adrielcodeco/go-fiber-gorm","commit_stats":null,"previous_names":["adrielcodeco/go-fiber-gorm"],"tags_count":5,"template":false,"template_full_name":null,"purl":"pkg:github/adrielcodeco/go-fiber-gorm","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/adrielcodeco%2Fgo-fiber-gorm","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/adrielcodeco%2Fgo-fiber-gorm/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/adrielcodeco%2Fgo-fiber-gorm/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/adrielcodeco%2Fgo-fiber-gorm/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/adrielcodeco","download_url":"https://codeload.github.com/adrielcodeco/go-fiber-gorm/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/adrielcodeco%2Fgo-fiber-gorm/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33678270,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-05-30T02:00:06.278Z","response_time":92,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["compensating-transactions","context-cancellation","database","fiber","fiber-v2","fiber-v3","go","gofiber","golang","gorm","graceful-shutdown","k8s","kubernetes","middleware","outbox-pattern","production-ready","readiness-probe","shutdown","transaction","transactions"],"created_at":"2026-05-30T02:30:28.055Z","updated_at":"2026-05-30T02:30:28.610Z","avatar_url":"https://github.com/adrielcodeco.png","language":"Go","funding_links":["https://github.com/sponsors/adrielcodeco"],"categories":[],"sub_categories":[],"readme":"# go-fiber-gorm\n\nA toolbox for production [Fiber](https://github.com/gofiber/fiber) + [GORM](https://gorm.io) services. It ships three complementary primitives that share the same design philosophy (framework-agnostic core + thin Fiber adapters for v2 and v3):\n\n1. **`txctx` / `txctxv3`** — request-scoped database transactions with lazy `BEGIN`, automatic rollback on error/timeout/panic, and commit/rollback callbacks.\n2. **`gsfiber` / `gsfiberv3`** — Kubernetes-aware graceful shutdown for Fiber + GORM + outbound calls, with ordered phases, hooks, readiness probe, and force-kill ceiling.\n3. **`apmfiber` / `apmfiberv3`** — Elastic APM tracing for Fiber + GORM + outgoing HTTP + Redis: foldable DB spans, log↔trace correlation, transaction labels, error capture, and DB-pool metrics.\n\n**Module:** `github.com/adrielcodeco/go-fiber-gorm`\n\n| Feature | Fiber v2 | Fiber v3 | Go min |\n|---|---|---|---|\n| Request-scoped transactions | `…/txctx` | `…/txctxv3` | 1.22 / 1.25 |\n| Graceful shutdown | `…/gsfiber` | `…/gsfiberv3` | 1.22 / 1.25 |\n| Elastic APM instrumentation | `…/apmfiber` | `…/apmfiberv3` | 1.22 / 1.25 |\n\nEach trio shares a framework-agnostic engine (`txcore`, `gscore`, `apmcore`) so both Fiber versions have identical semantics.\n\n---\n\n## Table of Contents\n\n- [Transactions (`txctx` / `txctxv3`)](#transactions-txctx--txctxv3)\n  - [Features](#features)\n  - [Installation](#installation)\n  - [Public API](#public-api)\n  - [Usage](#usage)\n  - [Commit / Rollback Decision Table](#commit--rollback-decision-table)\n  - [Propagating cancellation to outbound calls](#propagating-cancellation-to-outbound-calls)\n- [Graceful Shutdown (`gsfiber` / `gsfiberv3`)](#graceful-shutdown-gsfiber--gsfiberv3)\n  - [Features](#features-1)\n  - [Installation](#installation-1)\n  - [Public API](#public-api-1)\n  - [Phases](#phases)\n  - [Usage](#usage-1)\n  - [Kubernetes integration](#kubernetes-integration)\n- [Elastic APM (`apmfiber` / `apmfiberv3`)](#elastic-apm-apmfiber--apmfiberv3)\n  - [Packages](#packages)\n  - [Features](#features-2)\n  - [Installation](#installation-2)\n  - [Quick start (Fiber v2)](#quick-start-fiber-v2)\n  - [Local stack](#local-stack)\n  - [Pitfall index](#pitfall-index)\n\n---\n\n## Transactions (`txctx` / `txctxv3`)\n\n---\n\n## Features\n\n- **Lazy transactions** — a DB transaction is only opened when the first write (`Create`/`Update`/`Delete`) occurs in a request. Pure read requests never touch a transaction.\n- **Timeout-triggered rollback** — each request gets a configurable context timeout. If it expires before the handler finishes, the transaction is rolled back automatically.\n- **`Outside(c)`** — returns a `*gorm.DB` connected to `context.Background()`, completely outside the request transaction. Writes via `Outside` persist even if the main transaction rolls back.\n- **`OnRollback(c, fn)`** — registers a compensating callback that runs if the transaction rolls back (timeout, error, or panic). Runs with a fresh context (`CompensationCtx` duration) because the request context is already cancelled.\n- **`OnCommit(c, fn)`** — registers a callback that runs only after a successful commit. Useful for the outbox pattern (publish events only after the DB write is confirmed).\n- **Panic recovery** — the middleware recovers panics, rolls back the transaction, runs `OnRollback` callbacks, then re-panics so Fiber's `ErrorHandler` can still handle it.\n- **Context propagation** — all public functions have both `*fiber.Ctx` and `context.Context` variants, so repository and service layers stay framework-agnostic.\n\n---\n\n## Installation\n\nFor Fiber v2:\n```bash\ngo get github.com/adrielcodeco/go-fiber-gorm/txctx\n```\n\nFor Fiber v3:\n```bash\ngo get github.com/adrielcodeco/go-fiber-gorm/txctxv3\n```\n\nThe v3 adapter has the same surface as v2; just swap `txctx` → `txctxv3` and replace `*fiber.Ctx` with `fiber.Ctx` in your handler signatures.\n\n---\n\n## Public API\n\n```go\n// Middleware\ntxctx.Middleware(db *gorm.DB, cfg txctx.Config) fiber.Handler\n\n// Config\ntype Config struct {\n    Timeout         time.Duration   // request deadline (default: 30s)\n    LazyTx          *bool           // open tx only on first write (default: true; use txctx.BoolPtr to set)\n    CompensationCtx time.Duration   // timeout for OnRollback callbacks (default: 5s)\n    OnCallbackError func(error)     // optional sink for errors from OnCommit/OnRollback callbacks and from rollback/commit\n}\n\n// DB access\ntxctx.DB(c *fiber.Ctx) *gorm.DB\ntxctx.DBFromCtx(ctx context.Context) *gorm.DB\n\n// Outside-tx access\ntxctx.Outside(c *fiber.Ctx) *gorm.DB\ntxctx.OutsideCtx(ctx context.Context) *gorm.DB\n\n// Callbacks\ntxctx.OnRollback(c *fiber.Ctx, fn func(*gorm.DB) error)\ntxctx.OnRollbackCtx(ctx context.Context, fn func(*gorm.DB) error)\ntxctx.OnCommit(c *fiber.Ctx, fn func(*gorm.DB) error)\ntxctx.OnCommitCtx(ctx context.Context, fn func(*gorm.DB) error)\n```\n\n---\n\n## Usage\n\n### 1. Setup\n\nRegister the middleware once, globally or on a route group:\n\n```go\napp.Use(txctx.Middleware(db, txctx.Config{\n    Timeout:         5 * time.Second,\n    LazyTx:          txctx.BoolPtr(true),\n    CompensationCtx: 3 * time.Second,\n}))\n```\n\n- `Timeout` — maximum duration allowed for a single request before the context is cancelled and any open transaction is rolled back.\n- `LazyTx` — when `true`, `BEGIN` is deferred until the first write operation. Read-only requests skip transactions entirely.\n- `CompensationCtx` — timeout granted to `OnRollback` callbacks. Because the original request context is already cancelled at rollback time, each callback receives a fresh context with this duration.\n\n---\n\n### 2. Read-only handler\n\nWhen `LazyTx` is `true` and no write happens, `DB(c)` returns a plain `*gorm.DB` without ever opening a transaction.\n\n```go\nfunc getUser(c *fiber.Ctx) error {\n    var u User\n    if err := txctx.DB(c).First(\u0026u, c.Params(\"id\")).Error; err != nil {\n        return err\n    }\n    return c.JSON(u)\n}\n```\n\n---\n\n### 3. Simple write (lazy tx)\n\nThe first call to a write operation (`Create`, `Save`, `Update`, `Delete`) transparently triggers `BEGIN`.\n\n```go\nfunc createUser(c *fiber.Ctx) error {\n    var u User\n    c.BodyParser(\u0026u)\n    // First write: middleware transparently opens BEGIN here\n    if err := txctx.DB(c).Create(\u0026u).Error; err != nil {\n        return err\n    }\n    return c.JSON(u) // handler returns nil → COMMIT\n}\n```\n\n---\n\n### 4. Multiple writes in the same transaction\n\nAll calls to `DB(c)` within the same request share the same underlying transaction.\n\n```go\nfunc createOrder(c *fiber.Ctx) error {\n    db := txctx.DB(c)\n    user := User{Email: \"a@b.com\"}\n    db.Create(\u0026user)                                        // opens tx\n    db.Create(\u0026Order{UserID: user.ID, Total: 100})          // same tx\n    db.Model(\u0026user).Update(\"Name\", \"updated\")               // same tx\n    return c.JSON(user)                                     // COMMIT — all three writes atomic\n}\n```\n\n---\n\n### 5. `Outside` — write that survives rollback\n\n`Outside(c)` returns a `*gorm.DB` backed by `context.Background()`, completely independent of the request transaction. Writes via `Outside` are committed immediately and are not affected by a subsequent rollback of the main transaction.\n\n```go\nfunc signupWithAudit(c *fiber.Ctx) error {\n    var u User\n    c.BodyParser(\u0026u)\n\n    // Persists regardless of what happens to the main tx\n    txctx.Outside(c).Create(\u0026AuditLog{Action: \"signup_attempt\", Payload: u.Email})\n\n    if err := txctx.DB(c).Create(\u0026u).Error; err != nil {\n        return err // rollback of User, but AuditLog stays\n    }\n    return c.JSON(u)\n}\n```\n\n---\n\n### 6. `OnRollback` — compensating transaction\n\n`OnRollback` registers a function that runs only if the transaction is rolled back (due to a handler error, timeout, or panic). The callback receives a `*gorm.DB` with a fresh context whose deadline is `CompensationCtx`.\n\n```go\nfunc paymentHandler(c *fiber.Ctx) error {\n    var u User\n    c.BodyParser(\u0026u)\n    txctx.DB(c).Create(\u0026u)\n\n    txctx.OnRollback(c, func(bg *gorm.DB) error {\n        return bg.Create(\u0026FailedSignup{Email: u.Email, Error: \"rolled back\"}).Error\n    })\n\n    if err := chargeExternal(c.UserContext(), u.ID); err != nil {\n        return err // triggers rollback → OnRollback callback fires\n    }\n    return c.JSON(u)\n}\n```\n\n---\n\n### 7. `OnCommit` — outbox / post-commit event\n\n`OnCommit` registers a function that runs only after a successful commit. This is the recommended pattern for publishing domain events (outbox pattern): the event is only dispatched once the DB write is durably confirmed.\n\n```go\nfunc createOrder(c *fiber.Ctx) error {\n    var o Order\n    c.BodyParser(\u0026o)\n    txctx.DB(c).Create(\u0026o)\n\n    txctx.OnCommit(c, func(bg *gorm.DB) error {\n        return publishEvent(\"order.created\", o.ID)\n    })\n    return c.JSON(o)\n}\n```\n\n---\n\n### 8. Handler returns error → rollback\n\nAny non-nil error returned by the handler causes the middleware to roll back the active transaction before passing the error to Fiber's error handler.\n\n```go\nfunc manualRollback(c *fiber.Ctx) error {\n    var u User\n    txctx.DB(c).Create(\u0026u)\n    if u.Email == \"\" {\n        return errors.New(\"email required\") // rollback triggered\n    }\n    return c.JSON(u)\n}\n```\n\n---\n\n### 9. Panic → rollback + re-panic\n\nThe middleware recovers from panics, rolls back the transaction (running any registered `OnRollback` callbacks), and then re-panics so that Fiber's `ErrorHandler` or `RecoverHandler` can process it normally.\n\n```go\nfunc panicHandler(c *fiber.Ctx) error {\n    txctx.DB(c).Create(\u0026User{Email: \"boom\"})\n    txctx.OnRollback(c, func(bg *gorm.DB) error {\n        return bg.Create(\u0026AuditLog{Action: \"panicked\"}).Error\n    })\n    panic(\"something went very wrong\") // middleware: recover → rollback → re-panic\n}\n```\n\n---\n\n### 10. Layered architecture\n\nThe `*Ctx` variants (`DBFromCtx`, `OutsideCtx`, `OnRollbackCtx`, `OnCommitCtx`) accept a `context.Context` instead of a `*fiber.Ctx`. This allows repository and service layers to remain completely framework-agnostic while still participating in the request-scoped transaction.\n\n```go\n// handler — Fiber layer\nfunc createUserHandler(c *fiber.Ctx) error {\n    var u User\n    c.BodyParser(\u0026u)\n    if err := userService.Create(c.UserContext(), \u0026u); err != nil {\n        return err\n    }\n    return c.JSON(u)\n}\n\n// service — no Fiber dependency\nfunc (s *UserService) Create(ctx context.Context, u *User) error {\n    if err := s.repo.Insert(ctx, u); err != nil {\n        return err\n    }\n    txctx.OnCommitCtx(ctx, func(db *gorm.DB) error {\n        return s.events.Publish(\"user.created\", u.ID)\n    })\n    return nil\n}\n\n// repository — no Fiber dependency\nfunc (r *UserRepository) Insert(ctx context.Context, u *User) error {\n    return txctx.DBFromCtx(ctx).Create(u).Error\n}\n```\n\n---\n\n## Commit / Rollback Decision Table\n\n| Situation | Result |\n|---|---|\n| Handler returns `nil` | COMMIT → `OnCommit` callbacks run |\n| Handler returns `error` | ROLLBACK → `OnRollback` callbacks run |\n| Request context timeout | ROLLBACK → `OnRollback` callbacks run |\n| Panic in handler | ROLLBACK → `OnRollback` callbacks run → re-panic |\n| `tx.Commit()` itself fails | ROLLBACK → `OnRollback` callbacks run → commit error returned |\n| `OnCommit` callback fails (after successful commit) | Tx stays committed; error surfaced via `OnCallbackError` + returned to Fiber. `OnRollback` does **not** fire. |\n| Write via `Outside` | Always persists, independent of tx. Context cancellation is decoupled but values (request-id, tracing) are preserved. |\n\n### Concurrency notes\n\nThe request-scoped `*gorm.DB` is safe for sequential use within the handler.\nIf you spawn goroutines from the handler, do **not** use `DB(c)` from them\nafter the handler returns — the middleware will commit/rollback as soon as\nthe handler returns, and the underlying `*sql.Tx` becomes invalid. Use\n`Outside(c)` for fire-and-forget work, or wait for the goroutine before\nreturning from the handler.\n\n---\n\n## Propagating cancellation to outbound calls\n\nThe middleware wraps `c.UserContext()` with the configured `Timeout`. **Any\noutbound call (HTTP, gRPC, Redis, message broker, etc.) that receives this\ncontext will be cancelled automatically when the request times out, errors,\nor the client disconnects** — Go's standard libraries already implement this:\n`net/http` aborts the in-flight TCP request, `database/sql` interrupts the\nquery, gRPC closes the stream, and so on.\n\nFor this to work you must **thread the context through every outbound call**.\nThe package can't do this for you — it would require wrapping every client\ntype in the ecosystem. The discipline is:\n\n```go\nfunc chargeExternal(c *fiber.Ctx, userID uint) error {\n    // ✅ Pass the request context — cancels on Fiber timeout/error/panic.\n    req, err := http.NewRequestWithContext(c.UserContext(),\n        http.MethodPost, \"https://payments.example/charge\", body)\n    if err != nil {\n        return err\n    }\n    resp, err := http.DefaultClient.Do(req)\n    // ...\n}\n\nfunc chargeExternalBAD(userID uint) error {\n    // ❌ No context: the call will keep running after the request times out,\n    //    burning a goroutine and a connection until the remote replies.\n    resp, err := http.Post(\"https://payments.example/charge\", \"...\", body)\n    // ...\n}\n```\n\nThe same applies to gRPC (`grpc.Invoke(ctx, ...)`), Redis\n(`rdb.Get(ctx, ...)`), AWS SDK v2 (`client.GetItem(ctx, ...)`), and any other\nclient that accepts a `context.Context` as its first argument.\n\n**Service / repository layers:** use the `*Ctx` variants\n(`DBFromCtx`, `OutsideCtx`, `OnRollbackCtx`, `OnCommitCtx`) so the same\n`context.Context` flows through the whole call chain — DB, HTTP, gRPC, queue\npublishes, etc. — and a single cancellation point unwinds everything.\n\n**When you need to escape cancellation** (e.g. publishing a \"request-failed\"\nevent to a queue from `OnRollback` callbacks), `Outside(c)` already gives you\na context decoupled from the request cancellation while preserving values\nlike request-id and trace headers — use the same pattern for outbound HTTP\nin that scenario:\n\n```go\ntxctx.OnRollback(c, func(_ *gorm.DB) error {\n    // Need a fresh ctx because c.UserContext() is already cancelled here.\n    ctx, cancel := context.WithTimeout(\n        context.WithoutCancel(c.UserContext()), 3*time.Second)\n    defer cancel()\n    req, _ := http.NewRequestWithContext(ctx, http.MethodPost, alertURL, body)\n    _, _ = http.DefaultClient.Do(req)\n    return nil\n})\n```\n\n---\n\n## Graceful Shutdown (`gsfiber` / `gsfiberv3`)\n\nA coordinator for the full shutdown sequence of a Fiber + GORM service:\n**drain in-flight HTTP requests**, **cancel outbound calls**, **flush\napplication state**, **close the database pool**, all bounded by per-phase\nand global timeouts. Designed around the Kubernetes pod lifecycle.\n\n### Features\n\n- **Phased sequence** — `PreStop → Drain → PostDrain → DB → PostDB`, so each\n  resource is cleaned up at the right moment (e.g. flush outbox *before*\n  closing the DB; close Redis *after*).\n- **Ordered hooks** — each phase runs registered hooks sorted by `Priority`;\n  a failing hook is logged but does not stop the sequence.\n- **`RootContext()`** — a `context.Context` that is cancelled the moment\n  shutdown begins. Derive outbound HTTP/gRPC/queue calls from it and they\n  abort cleanly on SIGTERM.\n- **Readiness flip** — `IsReady()` (and the provided `ReadinessHandler`)\n  returns `200` while serving and `503` once shutdown begins, so kube-proxy\n  can remove the pod from service endpoints before any request is dropped.\n- **Configurable timeouts** — independent `PreStopDelay`, `DrainTimeout`,\n  `HookTimeout`, `DBCloseTimeout`, plus a global `ForceKillAfter` that\n  `os.Exit(1)`s if the whole sequence overshoots\n  `terminationGracePeriodSeconds`.\n- **Configurable signals** — defaults to `SIGINT` + `SIGTERM`, override via\n  `Config.Signals`.\n- **Structured logging** — every phase logs begin/end with duration; plug\n  any logger that implements the 3-method `Logger` interface.\n- **GORM-aware** — closes the underlying `*sql.DB` of each registered\n  `*gorm.DB` with a deadline (avoids hanging on a stuck pool).\n- **Concurrent drain** — multiple `*fiber.App` instances (or anything\n  implementing `Shutdowner`) are drained in parallel under a shared\n  deadline.\n\n### Installation\n\nFor Fiber v2:\n```bash\ngo get github.com/adrielcodeco/go-fiber-gorm/gsfiber\n```\n\nFor Fiber v3:\n```bash\ngo get github.com/adrielcodeco/go-fiber-gorm/gsfiberv3\n```\n\nThe two adapters share an engine (`gscore`); the public surface is\nidentical apart from `*fiber.App` vs `fiber.App` and `*fiber.Ctx` vs\n`fiber.Ctx` in the readiness handler.\n\n### Public API\n\n```go\n// Manager\ngsfiber.New(cfg gsfiber.Config) *gsfiber.Manager\n\n// Registration\ngsfiber.RegisterApp(m *Manager, app *fiber.App)    // one or more\nmgr.RegisterDB(db *gorm.DB)                        // one or more\nmgr.AddHook(gsfiber.Hook{Name, Phase, Priority, Run})\n\n// Lifecycle\nmgr.RootContext() context.Context                  // cancelled on shutdown\nmgr.IsReady() bool                                 // false once shutdown began\nmgr.Trigger()                                      // start sequence programmatically\nmgr.ListenAndWait() error                          // block on signals + run\nmgr.Wait() error                                   // block until sequence done\n\n// Readiness probe\ngsfiber.ReadinessHandler(mgr) fiber.Handler\n\n// Phases (re-exported on the adapter package)\ngsfiber.PhasePreStop\ngsfiber.PhaseDrain\ngsfiber.PhasePostDrain\ngsfiber.PhaseDB\ngsfiber.PhasePostDB\n\n// Config\ntype Config struct {\n    Signals        []os.Signal     // default: SIGINT, SIGTERM\n    PreStopDelay   time.Duration   // wait before any phase runs (default: 0)\n    DrainTimeout   time.Duration   // bound on HTTP drain (default: 25s)\n    HookTimeout    time.Duration   // bound per phase (default: 10s)\n    DBCloseTimeout time.Duration   // bound on each gorm.DB close (default: 5s)\n    ForceKillAfter time.Duration   // global ceiling, os.Exit(1) (default: 60s)\n    Logger         gscore.Logger   // structured logger; nil = silent\n    OnHookError    func(name string, phase gscore.Phase, err error)\n}\n```\n\n### Phases\n\n| Phase | Purpose |\n|---|---|\n| `PhasePreStop` | Runs first, while the server is still serving. Use for actions that need the HTTP layer alive (signal in-flight workers, flush in-memory queue). |\n| `PhaseDrain` | Drains all registered Fiber apps concurrently with `DrainTimeout`. |\n| `PhasePostDrain` | Runs after HTTP is fully drained, before DB close. Best place for outbound-call cleanups, worker pool waits, etc. |\n| `PhaseDB` | Closes each registered `*gorm.DB`'s underlying `*sql.DB` with `DBCloseTimeout`. |\n| `PhasePostDB` | Last phase. Use for resources that do not depend on the DB: Kafka producers, log flushers, metric exporters. |\n\n### Usage\n\n#### 1. Minimum setup\n\n```go\nfunc main() {\n    db := openGORM()\n    app := fiber.New()\n    app.Use(txctx.Middleware(db, txctx.Config{Timeout: 5 * time.Second}))\n    registerRoutes(app)\n\n    mgr := gsfiber.New(gsfiber.Config{\n        PreStopDelay:   5 * time.Second,  // give kube-proxy time to drop the endpoint\n        DrainTimeout:   25 * time.Second,\n        DBCloseTimeout: 5 * time.Second,\n        ForceKillAfter: 55 * time.Second, // \u003c terminationGracePeriodSeconds\n    })\n    gsfiber.RegisterApp(mgr, app)\n    mgr.RegisterDB(db)\n\n    // Readiness probe flips to 503 the instant SIGTERM arrives.\n    app.Get(\"/healthz/ready\", gsfiber.ReadinessHandler(mgr))\n\n    go func() {\n        if err := app.Listen(\":8080\"); err != nil {\n            mgr.Trigger() // server failed → start shutdown\n        }\n    }()\n\n    if err := mgr.ListenAndWait(); err != nil {\n        log.Fatal(err)\n    }\n}\n```\n\n#### 2. Cancel outbound calls on shutdown\n\nDerive any long-running outbound call from `mgr.RootContext()`. It is\ncancelled the moment SIGTERM is observed, so the call aborts cleanly\nduring the drain phase.\n\n```go\ngo func() {\n    ticker := time.NewTicker(30 * time.Second)\n    defer ticker.Stop()\n    for {\n        select {\n        case \u003c-mgr.RootContext().Done():\n            return\n        case \u003c-ticker.C:\n            req, _ := http.NewRequestWithContext(mgr.RootContext(),\n                http.MethodGet, \"https://api.example/poll\", nil)\n            _, _ = http.DefaultClient.Do(req)\n        }\n    }\n}()\n```\n\nFor per-request outbound calls inside a handler, keep using\n`c.UserContext()` — `txctx` already wires its cancellation.\n\n#### 3. Ordered hooks across phases\n\n```go\nmgr.AddHook(gsfiber.Hook{\n    Name:     \"outbox-flush\",\n    Phase:    gsfiber.PhasePreStop, // before we stop accepting requests\n    Priority: 0,\n    Run: func(ctx context.Context) error {\n        return outbox.FlushAll(ctx)\n    },\n})\n\nmgr.AddHook(gsfiber.Hook{\n    Name:     \"kafka-close\",\n    Phase:    gsfiber.PhasePostDB,  // after DB is closed\n    Priority: 10,\n    Run: func(ctx context.Context) error {\n        return kafkaProducer.Close()\n    },\n})\n\nmgr.AddHook(gsfiber.Hook{\n    Name:     \"redis-close\",\n    Phase:    gsfiber.PhasePostDB,\n    Priority: 0, // runs before kafka-close (lower priority first)\n    Run: func(ctx context.Context) error {\n        return redisClient.Close()\n    },\n})\n```\n\nLower `Priority` runs first within the same phase; equal priorities run in\nregistration order.\n\n#### 4. Custom logger\n\nAny type that satisfies the three-method `gscore.Logger` interface works\n(slog, zap, zerolog, logrus, etc.).\n\n```go\ntype slogAdapter struct{ l *slog.Logger }\n\nfunc (s slogAdapter) Info(msg string, kv ...any)  { s.l.Info(msg, kv...) }\nfunc (s slogAdapter) Warn(msg string, kv ...any)  { s.l.Warn(msg, kv...) }\nfunc (s slogAdapter) Error(msg string, kv ...any) { s.l.Error(msg, kv...) }\n\nmgr := gsfiber.New(gsfiber.Config{\n    Logger: slogAdapter{l: slog.Default()},\n})\n```\n\n#### 5. Triggering shutdown programmatically\n\n`mgr.Trigger()` starts the sequence from anywhere — useful for fatal\nerrors caught outside the HTTP layer (e.g. a background worker losing a\ncritical connection).\n\n```go\nif err := kafkaConsumer.Run(mgr.RootContext()); err != nil \u0026\u0026 !errors.Is(err, context.Canceled) {\n    log.Printf(\"consumer fatal: %v\", err)\n    mgr.Trigger()\n}\n```\n\n`Trigger` is idempotent — the sequence runs exactly once regardless of\nhow many times it is called or whether a signal also arrives.\n\n### Kubernetes integration\n\nA typical deployment lines up cleanly with the Manager's phases:\n\n```yaml\nspec:\n  terminationGracePeriodSeconds: 60   # \u003e ForceKillAfter (55s in example above)\n  containers:\n  - name: api\n    readinessProbe:\n      httpGet:\n        path: /healthz/ready          # gsfiber.ReadinessHandler\n        port: 8080\n      periodSeconds: 2\n      failureThreshold: 1\n    lifecycle:\n      preStop:\n        exec:\n          # Optional: belt-and-suspenders if PreStopDelay isn't enough.\n          # The Manager already handles SIGTERM directly.\n          command: [\"sleep\", \"5\"]\n```\n\nThe sequence on `kubectl delete pod`:\n\n1. Kubernetes sends `SIGTERM` and starts the `preStop` hook (in parallel).\n2. The Manager observes the signal → flips readiness to `503` → starts\n   `PreStopDelay`.\n3. kube-proxy sees the failing readiness probe and removes the pod from\n   service endpoints → no new requests arrive.\n4. `PreStopDelay` elapses → hooks run → HTTP drain → DB close → post-DB\n   hooks.\n5. Process exits cleanly, well before\n   `terminationGracePeriodSeconds`.\n\nKeep `ForceKillAfter` strictly **less than** `terminationGracePeriodSeconds`\nso the Manager's own ceiling fires first, with logs you can read, instead\nof an abrupt `SIGKILL` from the kubelet.\n\n---\n\n## Elastic APM (`apmfiber` / `apmfiberv3`)\n\nWraps the [Elastic APM Go agent](https://www.elastic.co/guide/en/apm/agent/go/current/index.html)\ninto the same core-plus-adapter shape used by the rest of this toolbox.\n\n### Packages\n\n| Submodule | Import | Purpose |\n|---|---|---|\n| `apmcore` | `github.com/adrielcodeco/go-fiber-gorm/apmcore` | Bootstrap + OTel bridge, DB driver wrapper, GORM plugin, pool metrics, zap helpers |\n| `apmfiber` | `github.com/adrielcodeco/go-fiber-gorm/apmfiber` | Fiber v2 middleware + labels + error capture |\n| `apmfiberv3` | `github.com/adrielcodeco/go-fiber-gorm/apmfiberv3` | Fiber v3 middleware + labels + error capture |\n\nEach submodule has its own `go.mod` so projects that don't need APM aren't\nforced to pull the Elastic + OTel dependency tree.\n\n### Features\n\n- **One-call bootstrap** — `apmcore.SetupOTelSDK(ctx)` wires the APM agent\n  into the OTel global providers and registers an `apm.MetricsGatherer`.\n  Returns a shutdown func to call after the server drains.\n- **Fiber middleware** — `apmfiber.Middleware()` / `apmfiberv3.Middleware()`\n  starts an APM transaction per request, names it `\u003cMETHOD\u003e \u003croute\u003e`, and\n  attaches it to the underlying `*fasthttp.RequestCtx`. Fiber v3 has no\n  upstream adapter — this package ships one.\n- **Transaction labels** — `Labels(LabelsConfig{...})` decodes a typed\n  struct from the request body once and publishes business identifiers\n  (`wallet_id`, `external_id`, …) as `labels.\u003ckey\u003e` filters in Kibana.\n- **Inline error capture** — `CaptureError(c, err)` records an error\n  against the active transaction so handler-mapped errors (that never\n  bubble to Fiber's `ErrorHandler`) still appear in Kibana → APM → Errors.\n- **Foldable DB spans** — `apmcore.NewGormPlugin()` + a driver wrap via\n  `apmcore.RegisterDriver(name, baseDriver)` produce a parent gorm span\n  per logical operation, with prepare/exec/query/close spans nested\n  inside. Works with any `database/sql` driver — pgx, mysql, sqlite —\n  because the base driver is passed in.\n- **DB-pool metrics** — `apmcore.RegisterDBPoolMetrics(sqlDB)` emits\n  `db.pool.*` on the agent's metrics tick (chartable in Metrics Explorer).\n- **HTTP / Redis / zap helpers** — `WrapHTTPTransport`, `InstrumentRedis`,\n  `WrapZapCore`, `LogCtxFields` cover the surrounding instrumentation\n  surface without forcing a particular client style.\n\n### Installation\n\n```bash\ngo get github.com/adrielcodeco/go-fiber-gorm/apmcore\ngo get github.com/adrielcodeco/go-fiber-gorm/apmfiber       # Fiber v2\n# or\ngo get github.com/adrielcodeco/go-fiber-gorm/apmfiberv3     # Fiber v3\n```\n\nConfigure the agent via the standard `ELASTIC_APM_*` environment\nvariables (`ELASTIC_APM_SERVER_URL`, `ELASTIC_APM_SERVICE_NAME`, …). The\nagent ignores `OTEL_*` variables — set both if you need the same value\nin both subsystems.\n\n### Quick start (Fiber v2)\n\n```go\nfunc main() {\n    shutdown, err := apmcore.SetupOTelSDK(context.Background())\n    if err != nil { panic(err) }\n    http.DefaultTransport = apmcore.WrapHTTPTransport(http.DefaultTransport)\n\n    app := fiber.New()\n    app.Use(apmfiber.Middleware())          // must be first\n    app.Use(apmfiber.Labels(apmfiber.LabelsConfig{\n        Headers: map[string]string{\"X-Origin\": \"origin\"},\n    }))\n\n    go app.Listen(\":8080\")\n    // ... wait for shutdown signal, then drain ...\n    _ = shutdown(context.Background())\n}\n```\n\n### Local stack\n\nA reference `docker-compose.apm.yml` lives in `examples/`. It bootstraps\nElasticsearch + Kibana + APM Server (8.13.4) with security enabled, the\nAPM integration pre-installed, and recommended agent central\nconfiguration applied — no manual Kibana clicks required.\n\n```bash\ndocker compose -f examples/docker-compose.apm.yml up -d\nopen http://localhost:5601    # elastic / changeme\n```\n\n### Pitfall index\n\n- Read the active transaction via `c.Context()` (Fiber v2) or\n  `c.RequestCtx()` (Fiber v3), **not** `c.UserContext()` — the agent\n  stores the transaction on the underlying `*fasthttp.RequestCtx`.\n- Set `ELASTIC_APM_*` env vars; the agent ignores `OTEL_*`.\n- Call `apmcore.SetupOTelSDK` **before** opening DB/Redis clients so\n  they pick up the global TracerProvider.\n- `span, _ := apm.StartSpan(ctx, …)` discards the new ctx and breaks\n  span nesting — use `span, ctx := …`.\n- For foldable DB spans, the gorm plugin reassigns\n  `tx.Statement.Context`; preserve it through your repositories with\n  `db.WithContext(ctx)`.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fadrielcodeco%2Fgo-fiber-gorm","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fadrielcodeco%2Fgo-fiber-gorm","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fadrielcodeco%2Fgo-fiber-gorm/lists"}