README
¶
go-kit
Small, framework-agnostic Go helpers for service infrastructure and application plumbing.
This repository provides reusable building blocks for:
- API-friendly error payloads
- user-safe structured context
- request correlation helpers
net/httpmiddleware helpers, including CORS and access logginglog/slogsetup and request-scoped logger enrichment
The module is intentionally lightweight and stays close to the standard library.
Requirements
- Go
1.25+
Installation
go get github.com/pumpingbytes/go-kit
Packages
| Package | Purpose |
|---|---|
apperror |
Transport-agnostic application/service error type with stable code, safe message, suggested status, and wrapped cause |
apierror |
Standardized JSON error envelope with stable codes, HTTP status, client-safe context, and internal debug payloads |
context |
User-safe serializable metadata payloads plus small generic helpers for typed context.Context values |
dberror |
Small interfaces for database error classification |
dberror/postgres |
Postgres/pgx classifier implementation for dberror.Classifier |
httpmw |
net/http helpers for request correlation, CORS support, and access log field definitions |
httpmw/slogx |
slog adapters for request context enrichment and HTTP access logging |
log |
Output configuration and writer setup for application logging |
log/slogx |
Shared slog.Logger loader plus request-scoped logger context helpers |
streams |
User-facing input/output stream abstractions backed by stdio, writers, discard sinks, and buffers |
streams/slogx |
slog adapter for streams.IOStreams |
Quick start
API errors with request correlation
package main
import (
"context"
"fmt"
"github.com/pumpingbytes/go-kit/apierror"
"github.com/pumpingbytes/go-kit/httpmw"
)
func main() {
ctx := context.Background()
ctx = httpmw.PutRequestID(ctx, "req-123")
ctx = httpmw.PutTraceIDs(ctx, "trace-abc", "span-def")
err := apierror.ErrInternalServerError.WithContext(httpmw.ErrorContext(ctx))
fmt.Println(err.Error())
// Output: INTERNAL: internal error
}
The serialized error shape is:
{
"code": "INTERNAL",
"message": "internal error",
"context": {
"request.id": "req-123",
"trace.id": "trace-abc",
"span.id": "span-def"
}
}
Use Debug only for internal diagnostics and call Cleanup() before returning an error to clients if needed.
Error sink examples
For transport layers, a good pattern is to normalize everything into apierror.APIError before writing the response.
Plain net/http
package main
import (
"encoding/json"
"log/slog"
"net/http"
"github.com/pumpingbytes/go-kit/apierror"
"github.com/pumpingbytes/go-kit/httpmw"
)
func writeAPIError(w http.ResponseWriter, r *http.Request, logger *slog.Logger, err error) {
if err == nil {
return
}
var out *apierror.APIError
if ae, ok := apierror.As(err); ok {
out = ae
if out.Status == 0 {
out = out.WithStatus(http.StatusBadRequest)
}
} else if ae, ok := apierror.FromAppError(err); ok {
out = ae
if out.Status == 0 {
out = out.WithStatus(http.StatusInternalServerError)
}
} else {
out = apierror.ErrInternalServerError
}
if ctx := httpmw.ErrorContext(r.Context()); ctx != nil {
out = out.WithContext(ctx)
}
logger.Error("request failed",
slog.String("path", r.URL.Path),
slog.String("error", err.Error()),
)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(out.Status)
_ = json.NewEncoder(w).Encode(out.Cleanup())
}
Gin
package api
import (
"log/slog"
"net/http"
"github.com/gin-gonic/gin"
"github.com/pumpingbytes/go-kit/apierror"
"github.com/pumpingbytes/go-kit/httpmw"
)
func handleError(ctx *gin.Context, logger *slog.Logger, err error) {
if err == nil {
return
}
var out *apierror.APIError
if ae, ok := apierror.As(err); ok {
out = ae
if out.Status == 0 {
out = out.WithStatus(http.StatusBadRequest)
}
} else if ae, ok := apierror.FromAppError(err); ok {
out = ae
if out.Status == 0 {
out = out.WithStatus(http.StatusInternalServerError)
}
} else {
out = apierror.ErrInternalServerError
}
if reqCtx := httpmw.ErrorContext(ctx.Request.Context()); reqCtx != nil {
out = out.WithContext(reqCtx)
}
logger.Error("request failed",
slog.String("path", ctx.FullPath()),
slog.String("error", err.Error()),
)
ctx.AbortWithStatusJSON(out.Status, out.Cleanup())
}
These examples intentionally keep policy small and explicit:
apierror.As(err)wins when the error is already transport-readyapierror.FromAppError(err)adaptsapperror.Error- unknown errors fall back to
apierror.ErrInternalServerError
Request ID propagation
package main
import (
"net/http"
"github.com/pumpingbytes/go-kit/httpmw"
)
func middleware(next http.Handler) http.Handler {
return httpmw.WithRequestID(httpmw.DefaultRequestIDHeader)(next)
}
If the incoming request does not include X-Request-Id, a new random hex ID is generated.
Typed context.Context values
The context package also provides small generic helpers for storing typed values in
stdlib context.Context. Package-level wrappers like httpmw.PutRequestID(...),
logslogx.PutLogger(...), and streams.PutIOStreams(...) build on these helpers.
package main
import (
"context"
"fmt"
kitcontext "github.com/pumpingbytes/go-kit/context"
)
type tenantKey struct{}
func main() {
ctx := kitcontext.PutValue(context.Background(), tenantKey{}, "tenant-42")
tenantID := kitcontext.GetValue(ctx, tenantKey{}, "")
fmt.Println(tenantID)
// Output: tenant-42
}
slog logger setup
package main
import (
"log/slog"
kitlog "github.com/pumpingbytes/go-kit/log"
logslogx "github.com/pumpingbytes/go-kit/log/slogx"
)
func main() {
logger, closer, err := logslogx.Load(kitlog.Options{
Path: "/var/log/my-service/service.log",
Format: kitlog.FormatJSON,
Level: "info",
AddSource: false,
}, "my-service")
if err != nil {
// Load falls back to stderr and still returns a usable logger.
logger.Warn("log output fallback", slog.String("error", err.Error()))
}
defer closer.Close()
logger.Info("service started")
}
Behavior summary:
- if
Options.Pathis set, logs go to that exact file path - else if
Options.Diris empty, logs go tostderr - otherwise logs go to
<Dir>/log/<File> - if
Fileis empty, the default file is<appName>.log - log level can be overridden with
<APPNAME>_LOG_LEVEL
CLI streams with slog
package main
import (
"fmt"
"log/slog"
streamsslogx "github.com/pumpingbytes/go-kit/streams/slogx"
)
func main() {
logger := slog.Default()
streams := streamsslogx.New(logger, slog.LevelInfo, slog.LevelError)
_, _ = fmt.Fprintln(streams.Out(), "operation started")
_, _ = fmt.Fprintln(streams.ErrOut(), "validation failed")
}
Request-scoped logging
package main
import (
"context"
"log/slog"
"net/http"
"github.com/pumpingbytes/go-kit/httpmw"
httpslogx "github.com/pumpingbytes/go-kit/httpmw/slogx"
logslogx "github.com/pumpingbytes/go-kit/log/slogx"
)
func main() {
base := slog.Default()
appCtx := logslogx.PutLogger(context.Background(), base)
accessLogger := httpslogx.AccessLoggerCtxWithOptions(appCtx, httpslogx.AccessLoggerOptions{
LevelPolicy: func(f httpmw.AccessLogFields) slog.Level {
switch {
case f.Status >= http.StatusInternalServerError:
return slog.LevelError
case f.Status >= http.StatusBadRequest:
return slog.LevelWarn
default:
return slog.LevelInfo
}
},
})
handler := httpmw.WithRequestID(httpmw.DefaultRequestIDHeader)(
httpmw.WithAccessLogOptions(httpmw.AccessLogOptions{
Logger: accessLogger,
Skip: httpmw.SkipPaths("/healthz", "/readyz"),
})(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := logslogx.PutLogger(r.Context(), httpslogx.EnrichLogger(r.Context(), base))
r = r.WithContext(ctx)
logslogx.GetLogger(r.Context()).Info("handling request")
_, _ = w.Write([]byte("ok\n"))
})),
)
_ = handler
}
httpmw/slogx.AccessLogger and AccessLoggerWithOptions emit a single access log entry using the standard access-log field names from httpmw.
The primary filter API is Skip func(*http.Request) bool, which keeps the middleware flexible across transports and routing setups. httpmw.SkipPaths(...) is a convenience helper for exact path matches.
If you want custom slog levels for access logs, use httpslogx.AccessLoggerWithOptions(...) with a LevelPolicy function as shown above.
Rule of thumb:
- for plain
net/http, useWithRequestID(...)andWithAccessLogOptions(...)directly - for Gin and similar frameworks, use
EnsureRequestID(...), then populatehttpmw.AccessLogFieldsmanually from framework-specific request/response data
Gin integration
package middleware
import (
"log/slog"
"time"
"github.com/gin-gonic/gin"
"github.com/pumpingbytes/go-kit/httpmw"
httpslogx "github.com/pumpingbytes/go-kit/httpmw/slogx"
logslogx "github.com/pumpingbytes/go-kit/log/slogx"
)
const RequestIDHeader = httpmw.DefaultRequestIDHeader
func RequestLogging(base *slog.Logger) gin.HandlerFunc {
logAccess := httpslogx.AccessLoggerWithOptions(base, httpslogx.AccessLoggerOptions{
LevelPolicy: func(f httpmw.AccessLogFields) slog.Level {
switch {
case f.Status >= 500:
return slog.LevelError
case f.Status >= 400:
return slog.LevelWarn
default:
return slog.LevelInfo
}
},
})
return func(c *gin.Context) {
start := time.Now()
ctx, rid := httpmw.EnsureRequestID(c.Request.Context(), c.Request, RequestIDHeader)
c.Header(RequestIDHeader, rid)
ctx = logslogx.PutLogger(ctx, httpslogx.EnrichLogger(ctx, base))
c.Request = c.Request.WithContext(ctx)
c.Next()
logAccess(c.Request.Context(), httpmw.AccessLogFields{
Method: c.Request.Method,
Path: c.FullPath(),
Status: c.Writer.Status(),
Latency: time.Since(start),
ClientIP: c.ClientIP(),
ResponseBytes: int64(c.Writer.Size()),
UserAgent: c.Request.UserAgent(),
})
}
}
For Gin and other frameworks, the current httpmw building blocks are usually the best fit: use EnsureRequestID(...), populate httpmw.AccessLogFields, and emit through httpmw/slogx.
CORS helper
package main
import (
"net/http"
"github.com/pumpingbytes/go-kit/httpmw"
)
func withCORS(next http.Handler) http.Handler {
opts := httpmw.CORSOptions{
AllowedOrigins: []string{"https://example.com"},
AllowHeaders: []string{"Authorization", "Content-Type"},
AllowMethods: []string{"GET", "POST", "OPTIONS"},
}
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
preflight, status := httpmw.ApplyCORS(w, r, opts)
if preflight {
w.WriteHeader(status)
return
}
next.ServeHTTP(w, r)
})
}
ApplyCORS is framework-agnostic and works directly with net/http.
API notes
apierror
New(code, message)creates a baseAPIErrorWithStatus(status)clones with HTTP statusWithContext(ctx)attaches client-safe serialized contextWithDebug(ctx)attaches internal-only serialized debug payloadCleanup()removes the debug payloadAs(err)unwraps anerrorinto*APIErrorFromAppError(err)conservatively adaptsapperror.Errorwithout exposing wrapped causes or internal debug data
apperror
New(code, message, status)creates a transport-agnostic application errorWrap(err, code, message, status)preserves an upstream cause while attaching application-level metadataAs(err)unwraps anerrorinto*apperror.Error
Common predeclared errors include:
ErrInternalServerErrorErrServiceUnavailableErrMissingAuthorizationHeaderErrAuthorizationRequiredErrInvalidTokenErrMissingRequiredScopeErrTooManyRequestsErrEntitlementRequired
context
The context package serves two related purposes:
Contextis a small serializable map intended for client-safe metadataPutValue(...)/GetValue(...)are tiny generic helpers for typed values in stdlibcontext.Context
For examples that import both packages, alias this package to avoid confusion with the stdlib package name.
import (
stdcontext "context"
kitcontext "github.com/pumpingbytes/go-kit/context"
)
payload := kitcontext.Ctx(httpmw.RequestID, "req-123", httpmw.TraceID, "trace-abc")
ctx := kitcontext.PutValue(stdcontext.Background(), requestKey{}, "req-123")
Keys use github.com/ygrebnov/keys for consistency.
dberror
Classifierdefines a tiny interface for database-specific error classificationdberror/postgres.Classifierprovides a pgx/Postgres implementation that can be reused from repositories without coupling service code to transport-layer errors
httpmw
WithRequestID(headerName)is the plainnet/httprequest ID middleware; an empty header name usesDefaultRequestIDHeaderPutRequestID(...),GetRequestID(...),PutTraceIDs(...),GetTraceID(...), andGetSpanID(...)expose request-correlation values directly throughcontext.ContextWithAccessLogOptions(...)wrapsnet/httphandlers and emitsAccessLogFieldsafter the response is writtenSkipPaths(...)is an exact-path helper; for anything more advanced, preferSkip func(*http.Request) bool
httpmw/slogx
AccessLogger(...)keeps the default access-log level policy:ERRORfor>= 500,INFOotherwiseAccessLoggerWithOptions(...)lets you customize slog level selection withLevelPolicyEnrichLogger(...)adds request correlation fields from context to a base logger
log
Options.Pathwrites to an exact file path and takes precedence overDirandFile- when
Pathis empty andDiris set, logs go to<Dir>/log/<File-or-app.log> - when both
PathandDirare empty, logging falls back tostderr
log/slogx
Load(...)builds a*slog.Loggerfromlog.OptionsPutLogger(...)andGetLogger(...)propagate request-scoped loggers viacontext.ContextGetLogger(...)falls back to a default stderr JSON logger when no logger is stored in context
streams
NewBasic(in, out, errOut)builds a basic stream set explicitlyNewDefault()usesos.Stdin,os.Stdout, andos.StderrNewSilent()disables both output streams while keeping stdinNewBuffers()andNewThreadSafeBuffers()cover common CLI/test scenariosPutIOStreams(...)andGetIOStreams(...)propagate stream sets viacontext.Context
streams/slogx
New(logger, outLevel, errLevel)creates slog-backed streamsNewCtx(ctx, outLevel, errLevel)uses the logger fromlog/slogx.GetLogger(ctx)
Development
Run the test suite:
go test ./...
Status
This repository currently focuses on low-level helpers rather than a full application framework. The packages are small and composable so they can be embedded into HTTP services, background workers, and other Go applications without adopting a larger runtime stack.
License
This project is licensed under the BSD-3-Clause License. See LICENSE.
Documentation
¶
Overview ¶
Package gokit provides small, framework-agnostic helpers for service infrastructure and application plumbing.
The module is organized into focused subpackages for application errors, API error envelopes, user-safe context payloads, typed context value helpers, HTTP middleware, slog setup, database error classification, and CLI streams.
Directories
¶
| Path | Synopsis |
|---|---|
|
Package context provides two small, related building blocks:
|
Package context provides two small, related building blocks: |
|
Package streams provides small user-facing input/output stream adapters.
|
Package streams provides small user-facing input/output stream adapters. |