Shared Go SDK for the duynhlab microservices platform — common gRPC, auth, observability, database, logging, migration and protobuf code so services don't reimplement it.
This is a multi-module monorepo: there is no top-level go.mod (the
single-module line is frozen at v0.35.0). Each
module below has its own go.mod and is versioned and tagged independently
(<module>/vX.Y.Z), so a service pulls in only what it imports:
go get github.com/duynhlab/pkg/httpx@v0.36.0 # tag: httpx/v0.36.0
go get github.com/duynhlab/pkg/logger/zapx@v0.36.0 # tag: logger/zapx/v0.36.0Module tags continue the pre-split numbering (the last single-module tag was
v0.35.0). Migrating a service from the single-module pkg? See
docs/MIGRATION.md — import paths don't change, only
go.mod does.
Modules are layered; lower layers never import higher ones (see AGENTS.md for the full dependency rules).
| Module | Layer | What it provides |
|---|---|---|
proto |
0 | Versioned gRPC contracts for all services (<svc>/v1/*.proto) with committed generated stubs. |
logger/zapx |
0 | zap logger construction with trace-ID injection — the production default, pairs with obsx.ZapCore. |
logger/clog |
0 | log/slog + chainguard-dev/clog logger with trace-context correlation. |
logger/zerolog |
0 | rs/zerolog logger with trace-ID injection. |
flagx |
0 | Startup-validated environment flags (Enum, Percent + Must*) — fail fast, bounded values safe for metric labels. |
httpx |
1 | HTTP helpers on gin: consistent error responses and pagination. |
grpcx |
1 | gRPC server/client for east-west calls: otelgrpc, health, reflection, panic recovery, access logs, error reasons. |
authmw |
1 | Fail-closed gin JWT middleware (RS256 + cached JWKS, issuer/audience pinned). |
idempotency |
1 | Stripe-style idempotency keys: Record, sentinel errors, Postgres Repository over *pgxpool.Pool. |
obsx |
2 | OpenTelemetry SDK bootstrap — traces + metrics + logs over OTLP, zap bridge, Pyroscope profiling. The only module linking the OTel SDK. |
dbx |
2 | Postgres pgxpool builder with otelpgx tracing and pool metrics, pooler-safe settings, no PII in telemetry. |
migratex |
2 | Embedded SQL migrations runner (golang-migrate). |
temporalx |
2 | Temporal client/worker bootstrap with OTel tracing and Worker Deployment Versioning. |
Authoritative per-module detail and contribution rules live in AGENTS.md.
Typical main() wiring (observability, DB pool, gRPC):
import (
"context"
"github.com/duynhlab/pkg/dbx"
"github.com/duynhlab/pkg/grpcx"
"github.com/duynhlab/pkg/logger/zapx"
"github.com/duynhlab/pkg/obsx"
)
ctx := context.Background()
log, _ := zapx.New("info")
// One-call OTel SDK wiring — traces + metrics + logs over OTLP.
obs, _ := obsx.SetupObservability(ctx, obsx.ConfigFromEnv())
defer obs.Shutdown(ctx)
// Postgres pool with query tracing + pool-stat metrics baked in.
pool, _ := dbx.NewPool(ctx, "postgres://user:pass@localhost/db")
defer pool.Close()
// gRPC server (otel + health + reflection + access logs) and client.
srv, health := grpcx.NewServer(log)
conn, _ := grpcx.Dial("dns:///shipping.shipping.svc.cluster.local:9090")
_, _, _ = srv, health, connAll workflows go through the root Makefile — go test ./... at the repo
root checks nothing, because there is no root module:
make modules # list discovered modules
make test # tidy+fmt+vet+lint+test for every module
make test-obsx # one module ("/" becomes ":" — make test-logger:zapx)
make test TAGS=integration # include testcontainers tests (needs Docker)
make generate-proto # buf generate + buf lint after editing a .proto
make release-obsx VER=0.36.0 # tag and push obsx/v0.36.0CI (.github/workflows/check.yml) gates on the same make test, buf
lint/breaking, and SonarCloud; CodeQL analyzes Go and workflow files, and
repo labels are declaratively synced from .github/labels.yaml.
Each module is tagged separately, <module>/vX.Y.Z. A pushed tag is cached
by the Go module proxy immediately and cannot be fixed, only superseded by a
new patch version. make release-<module> guards the common mistakes: it
refuses a dirty tree, a HEAD not on origin/main, a non-semver VER, and an
unknown module.
Step 0 — prepare (release always cuts from pushed main):
git checkout main
git pull --ff-only origin main # HEAD must already be on origin/main
git status # working tree must be clean
make modules # confirm all modules are discoveredStep 1 — tag each module you're releasing. Every command tags
<module>/v<VER> and pushes it immediately; nested modules use : instead
of /. VER carries no v prefix. Releasing everything at once:
# Layer 0
make release-proto VER=0.36.0
make release-flagx VER=0.36.0
make release-logger:zapx VER=0.36.0
make release-logger:zerolog VER=0.36.0
make release-logger:clog VER=0.36.0
# Layer 1
make release-httpx VER=0.36.0
make release-grpcx VER=0.36.0
make release-authmw VER=0.36.0
make release-idempotency VER=0.36.0
# Layer 2
make release-obsx VER=0.36.0
make release-dbx VER=0.36.0
make release-migratex VER=0.36.0
make release-temporalx VER=0.36.0Order does not matter today (no cross-module dependencies); the layer
grouping is habit-forming — the moment one module requires another, its
dependency must be tagged first (Layer 0 → 1 → 2). Each pushed tag triggers
the release workflow, which publishes a GitHub Release with generated
notes.
Step 2 — verify:
git tag --sort=-creatordate | head -13
# The proxy must resolve the new versions (spot-check a few):
GOPROXY=https://proxy.golang.org go list -m github.com/duynhlab/pkg/httpx@v0.36.0
GOPROXY=https://proxy.golang.org go list -m github.com/duynhlab/pkg/logger/zapx@v0.36.0
GOPROXY=https://proxy.golang.org go list -m github.com/duynhlab/pkg/obsx@v0.36.0If the repo is private, go through git instead of the public proxy:
GOPRIVATE=github.com/duynhlab go list -m ....
MIT