CRUMB a card from devarno-cloud

shared-go Auth, Errors & Metrics

tektree beginner 4 min read

ELI5

shared-go is the toolbox every service grabs on the way out of the locker room: the same hammer (JWT validator), the same red error stickers, the same stopwatch (Prometheus metric). Every service measures and fails the same way so the monitoring wall reads consistently.

Technical Deep Dive

libs/shared-go/pkg/ ships five packages: auth, config, logging, errors, metrics. Every Go service imports the ones it needs; nothing in here is service-specific.

Package Class Diagram

classDiagram
class BaseConfig {
+string ServiceName
+string Environment
+int Port
+string MongoURI
+string MongoDatabase
+string RedisURL
+string JWTPublicKey
+string JWTPrivateKey
+string LogLevel
+bool DebugMode
}
class Claims {
+string UserID
+string Tier
+string Role
+RegisteredClaims
}
class TokenValidator {
+rsa.PublicKey Pub
+Validate(raw string) (*Claims, error)
}
class AppError {
+ErrorCode Code
+string Message
+int Status
+map Details
+string TraceID
+error Err
}
class Logger {
+zap.Logger inner
+WithTraceID(id) Logger
+WithUserID(id) Logger
+WithService(name) Logger
}
class ServiceMetrics {
+CounterVec RequestsTotal // method, endpoint, status
+HistogramVec RequestDuration // method, endpoint
+Gauge RequestsInFlight
+CounterVec ErrorsTotal // type
}
class EventMetrics {
+CounterVec EventsPublished // event_type
+CounterVec EventsConsumed // event_type
+HistogramVec EventLatency // event_type
+CounterVec EventErrors // event_type, error_type
}
BaseConfig <.. TokenValidator : reads JWTPublicKey
Claims <-- TokenValidator
AppError --> ErrorCode

Error Codes

libs/shared-go/pkg/errors/errors.go:

ErrorCodeDefault HTTP Status
INVALID_REQUEST400
UNAUTHORIZED401
FORBIDDEN403
NOT_FOUND404
CONFLICT409
VALIDATION_ERROR422
RATE_LIMIT_EXCEEDED429
INTERNAL_ERROR500
SERVICE_UNAVAILABLE503

AppError is wrapped via Err so errors.Is/As chains keep working. Handlers should return AppError and a single response writer translates it to JSON {error: {code, message, status, details, trace_id, documentation_url}} (the contract in docs/docs/api/API_CONTRACTS.md).

Logging

Logger is a Zap wrapper with three context helpers (WithTraceID, WithUserID, WithService) — the standard fields required by OBSERVABILITY_PLAN.md. Production uses JSON encoding with ISO8601 timestamps; dev can switch to console.

Metrics

Two metric structs are shipped as primitives:

  • ServiceMetrics — HTTP-shaped, for any service handling requests. Labels are method, endpoint, status on the counter; histograms exclude status.
  • EventMetrics — for services that publish or consume events. Labels are event_type (and additionally error_type on errors).

Names are prefixed by service in production scrape: gamification_requests_total{...} etc., per OBSERVABILITY_PLAN.md.

Why Centralised

A new service should import shared-go/pkg/auth for JWT verification rather than reading the env and parsing the key itself — this keeps the validator, claim shape, and error-on-failure consistent. Drift in any of those fields is the most common cause of “this service rejects a token the gateway accepted.”

Key Terms

  • BaseConfig → the env-driven config every service embeds.
  • Claims → the only canonical JWT shape; do not duplicate per service.
  • AppError → the only error type that should leave a handler.
  • ServiceMetrics / EventMetrics → standard Prometheus collectors a service registers and increments.

Q&A

Q: A handler wants to reject with “you don’t own this resource.” Which ErrorCode? A: FORBIDDEN. Use UNAUTHORIZED only when no valid identity was presented; the user is authenticated, just not allowed.

Q: A panic in a worker goroutine logs without a trace_id. How do you fix it? A: Use Logger.WithTraceID(traceID) at the top of the goroutine entry (or pass a context-bound logger). Naked Logger writes do not auto-attach trace context.

Q: Two services emit *_request_duration_seconds with different bucket bounds. Is that okay? A: Functionally yes (Prometheus per-metric-name buckets are independent), but it makes cross-service histograms incomparable. Use the constructor in metrics so all services land on the same bucket grid.

Examples

A new service bootstrap:

cfg := config.MustLoad[MyConfig](config.Defaults())
log := logging.New(cfg.LogLevel).WithService(cfg.ServiceName)
val := auth.NewTokenValidator(cfg.JWTPublicKey)
sm := metrics.NewServiceMetrics(cfg.ServiceName)
prometheus.MustRegister(sm.RequestsTotal, sm.RequestDuration, sm.RequestsInFlight, sm.ErrorsTotal)

neighbors on the map