Rene Nochebuena a7fa7fc51a docs(core): fix logz.Options -> logz.Config in doc examples; bump to v1.1.0
Coordinated framework release. Documentation-only code change plus the shared
version bump.

- core/launcher/doc.go, core/logz/doc.go: logz.New(logz.Options{...}) ->
  logz.Config (Options was retired per ADR-003; the constructor takes Config).
- go.mod: contracts -> v1.1.0 (via go get + go mod tidy).

Verified by compiling the example patterns against the real API. Badge -> v1.1.0.
2026-08-07 18:41:49 -06:00

einherjar/core

version license go

The chosen warriors do not choose their weapons. They forge them.

code.nochebuena.dev/einherjar/core is the foundational implementation module of the Einherjar framework. It sits directly above contracts in the dependency graph and provides the concrete tools every service needs before anything else can start: a lifecycle runner, a structured logger, typed errors, and struct validation.


What Is Einherjar?

In Norse mythology, the Einherjar are the chosen warriors of Valhalla — selected not for glory, but to be ready for what comes after. They train. They prepare. They build the capability that others will rely on.

This framework is named for that purpose. Every module is a piece of that preparation: built carefully, documented for those who were never in the room, and designed to hold under pressure.


Sub-packages

Package Import path Purpose
launcher .../core/launcher Application lifecycle — init, start, shutdown
logz .../core/logz Structured, leveled logging via log/slog
xerrors .../core/xerrors Typed error codes with context enrichment
valid .../core/valid Struct validation with pluggable i18n messages

All four are in one module because they ship together in every Einherjar service. See ADR-001.


Usage

Launcher

import (
    "code.nochebuena.dev/einherjar/core/launcher"
    "code.nochebuena.dev/einherjar/core/logz"
)

logger := logz.New(logz.Config{JSON: true, StaticArgs: []any{"service", "api"}})

lc := launcher.New(logger)
lc.Append(db, cache, server)
lc.BeforeStart(func() error {
    return server.RegisterRoutes(db, cache)
})

if err := lc.Run(); err != nil {
    logger.Error("launcher failed", err)
    os.Exit(1)
}

Environment variables (uses caarlos0/env tag syntax — application supplies the loader):

Variable Default Effect
EINHERJAR_BANNER (on) Set to off or false to suppress the startup banner
EINHERJAR_COMPONENT_STOP_TIMEOUT 15s Maximum time per component OnStop

Logger

import "code.nochebuena.dev/einherjar/core/logz"

logger := logz.New(logz.Config{Level: slog.LevelDebug, JSON: true})

// Attach request context in middleware
ctx = logz.WithRequestID(ctx, requestID)
ctx = logz.WithField(ctx, "user_id", userID)

// Enrich logger from context in handlers
reqLogger := logger.WithContext(ctx)
reqLogger.Info("handling request", "path", r.URL.Path)

// Error enrichment is automatic — no extra code needed
reqLogger.Error("query failed", err) // appends error_code and context fields

Environment variables:

Variable Default Effect
EINHERJAR_LOG_LEVEL INFO Minimum log level (DEBUG, INFO, WARN, ERROR)
EINHERJAR_LOG_JSON false JSON output when true

Errors

import "code.nochebuena.dev/einherjar/core/xerrors"

// Named constructors for common cases
err := xerrors.NotFound("user %s not found", userID)
err := xerrors.InvalidInput("email is required")
err := xerrors.Aborted("order modified by another session")

// Builder pattern for structured context
err := xerrors.New(xerrors.ErrInvalidInput, "validation failed").
    WithContext("field", "email").
    WithContext("rule", "required").
    WithError(cause)

// Inspecting errors
var e *xerrors.Err
if errors.As(err, &e) {
    switch e.Code() {
    case xerrors.ErrNotFound:       // HTTP 404
    case xerrors.ErrUnauthorized:   // HTTP 401
    case xerrors.ErrInvalidInput:   // HTTP 400
    }
}

Validation

import "code.nochebuena.dev/einherjar/core/valid"

type CreateUserReq struct {
    Email string `json:"email" validate:"required,email"`
    Age   int    `json:"age"   validate:"gte=18"`
}

v := valid.New() // English messages
// v := valid.New(valid.WithMessageProvider(valid.SpanishMessages))

if err := v.Struct(req); err != nil {
    var xe *xerrors.Err
    errors.As(err, &xe)
    // xe.Code()    == xerrors.ErrInvalidInput
    // xe.Fields()  == {"field": "email", "tag": "required"}
    // xe.Message() == "field 'email' is required"
}

All go-playground/validator built-in tags have specific messages in both DefaultMessages and SpanishMessages. The generic fallback only fires for unknown tags.

Custom validators

import "strings"

v := valid.New(
    valid.WithCustomValidator("nohttp", func(fl valid.FieldLevel) bool {
        return !strings.HasPrefix(fl.Field().String(), "http://")
    }),
    valid.WithMessageProvider(valid.OverrideProvider(
        map[string]func(field, param string) string{
            "nohttp": func(field, _ string) string {
                return fmt.Sprintf("field '%s' must not use http://", field)
            },
        },
        valid.DefaultMessages,
    )),
)

OverrideProvider chains a tag→message map with a fallback provider, so custom tag messages are handled without re-implementing all built-ins.


Error Codes

xerrors provides the full gRPC canonical error code set plus HTTP 410 (ErrGone):

Constant Wire value HTTP When to use
ErrInvalidInput INVALID_ARGUMENT 400 Malformed or invalid request data
ErrOutOfRange OUT_OF_RANGE 400 Valid value but outside accepted bounds
ErrUnauthorized UNAUTHENTICATED 401 Missing or invalid credentials
ErrPermissionDenied PERMISSION_DENIED 403 Authenticated but not authorised
ErrNotFound NOT_FOUND 404 Resource does not exist
ErrAlreadyExists ALREADY_EXISTS 409 Creation conflict (duplicate)
ErrAborted ABORTED 409 Concurrent modification; retry may succeed
ErrGone GONE 410 Resource permanently deleted
ErrPreconditionFailed FAILED_PRECONDITION 412 Business rule blocks the operation
ErrRateLimited RESOURCE_EXHAUSTED 429 Rate limit or quota exceeded
ErrCancelled CANCELLED 499 Request cancelled by the caller
ErrInternal INTERNAL 500 Unexpected server-side failure
ErrDataLoss DATA_LOSS 500 Unrecoverable data corruption
ErrNotImplemented UNIMPLEMENTED 501 Operation not implemented
ErrUnavailable UNAVAILABLE 503 Service temporarily unavailable
ErrDeadlineExceeded DEADLINE_EXCEEDED 504 Operation timed out

Wire values are stable across versions and safe to persist, send over the network, or switch on in client code.


Dependency Rules

contracts  (zero dependencies)
    ↑
  core     (depends on contracts only)
    ↑
 starters  (depend on core + contracts)
    ↑
  your app

core imports contracts. Nothing above core in this chain may import core directly — they depend on starters, which compose core's sub-packages behind framework-specific APIs.


Verification

cd core/
go build ./...     # must compile clean
go vet ./...       # no warnings
go test ./...      # structural + behavioural compliance passes
gofmt -l .         # no output

Architecture Decisions

ADR Title
ADR-001 Four sub-packages in one Go module
ADR-002 logz adopts contracts/errs instead of private duck typing
ADR-003 Config naming convention and caarlos0/env tag standard

They were not chosen because they were the strongest. They were chosen because they understood what they were building toward.

S
Description
Foundation: launcher, structured errors, validation, and structured logging
Readme AGPL-3.0
210 KiB
2026-08-12 15:49:52 -06:00
Languages
Go 100%