Documentation
¶
Overview ¶
Package app defines Warren's central abstraction: a transport-agnostic use case, and the core-ring middleware shape that decorates it.
A Handler is written once and exposed over HTTP, gRPC, and message consumers by adapters this package knows nothing about. A Middleware wraps the handler rather than the protocol, so a transaction decorator or retry policy is written once and applies everywhere — the core ring of the two-ring model in warren.md §1.4; transport-shaped concerns are the edge ring, owned by each adapter.
Index ¶
- func Claim[T any](id Identity, name string) (T, bool)
- func HandlerName(ctx context.Context) string
- func InstrumentsHandlers(t Telemetry) bool
- func IsNilPolicy(p AuthorizationPolicy) bool
- func Stamp(name string, t Telemetry) func(context.Context) context.Context
- func StampHandlerName(name string) func(context.Context) context.Context
- func WithHandlerName(ctx context.Context, name string) context.Context
- func WithIdentity(ctx context.Context, id Identity) context.Context
- func WithTelemetry(ctx context.Context, t Telemetry) context.Context
- func WithoutIdentity(ctx context.Context) context.Context
- type AuthorizationPolicy
- type Handler
- type HandlerFunc
- type HandlerInstrumentation
- type Identity
- type Middleware
- func Authorized[Req, Res any](policy AuthorizationPolicy) Middleware[Req, Res]
- func Metered[Req, Res any]() Middleware[Req, Res]
- func Retrying[Req, Res any](policy RetryPolicy) Middleware[Req, Res]
- func RetryingOn[Req, Res any](policy RetryPolicy, codes ...errors.Code) Middleware[Req, Res]
- func Timeout[Req, Res any](d time.Duration) Middleware[Req, Res]
- func Traced[Req, Res any]() Middleware[Req, Res]
- func Transactional[Req, Res any](uow UnitOfWork) Middleware[Req, Res]
- type RequestInfo
- type RequestSpan
- type RetryPolicy
- type Telemetry
- type UnitOfWork
- type Unwrapper
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func Claim ¶ added in v0.2.0
Claim reads a typed claim out of an identity's Claims map.
It is a generic FREE FUNCTION rather than a method because Go 1.26 has no generic methods, and a generic Identity[T] would give every instantiation its own context key. It never panics: a missing key, a wrong type and a nil map all return the zero value and false, because this runs on the request path over a map some verifier produced.
JSON NUMBERS DECODE AS float64. This is the wrong type every JWT user reaches for first, and it fails silently:
Claim[int](id, "exp") // (0, false) — always, for every token Claim[float64](id, "exp") // (1735689600, true)
A JSON array is []any, not []string, for the same reason — use Scopes for scopes, which is already split. And if your verifier is configured to decode numbers as json.Number instead, every Claim[float64] in your codebase flips to false at once; pick one and keep it.
An explicit JSON null is indistinguishable from an absent claim: both are (zero, false). That is the safe direction — a null claim carries no information a handler should act on.
func HandlerName ¶
HandlerName returns the qualified handler name carried by ctx, or "".
func InstrumentsHandlers ¶ added in v0.2.0
InstrumentsHandlers reports whether t wants boot to compose Traced and Metered around each route. A nil t does not.
func IsNilPolicy ¶ added in v0.2.0
func IsNilPolicy(p AuthorizationPolicy) bool
IsNilPolicy reports whether a policy is nil, including a non-nil interface holding a nil pointer — which would allow every request it guards.
It is exported because transport.Guard must make the same refusal at the same moment, and a second copy of the probe is a second thing to get wrong.
func Stamp ¶ added in v0.2.0
Stamp returns the per-request context stamp for one route: the handler's qualified name and the Telemetry, both fixed at boot, stored under ONE key.
One key, not two, because both values are known at boot and an instrumented request would otherwise pay two context.WithValue allocations to carry a pair that never varies. A nil t stamps the name alone, through StampHandlerName, so an uninstrumented service's request path is unchanged.
func StampHandlerName ¶ added in v0.2.0
StampHandlerName returns a function that does what WithHandlerName does, with the boxing paid once here instead of on every call.
context.WithValue takes an `any`, so passing a string boxes it — an allocation, per request, on every HTTP, gRPC and event route. A route's handler name is fixed at boot, so that allocation belongs at boot too. Measured on an M3 Pro: 25.4 ns and 2 allocs (64 B) becomes 14.2 ns and 1 alloc (48 B).
Use WithHandlerName anywhere the name is not fixed in advance; this exists for the request path, and the request path alone.
func WithHandlerName ¶
WithHandlerName returns a copy of ctx carrying the handler's qualified name, "<module>.<handler>". The transport adapter seeds it in the route table's pre-built closure at boot step 5 — it is the one party that knows both names — and Traced and Metered read it back.
func WithIdentity ¶ added in v0.2.0
WithIdentity returns a copy of ctx carrying id. An authentication middleware calls it at the edge; a test calls it directly.
An Identity whose Subject is empty is NOT carried, and the context is returned unchanged — the same refusal WithTelemetry makes for a nil Telemetry. It is the second of the three mechanisms that stop "no identity" reading as "identity with an empty subject": a guard bug that produces an empty subject makes the request read as unauthenticated, which is fail-closed, instead of authenticated-as-nobody, which files rows owned by "".
It costs 2 allocations — the interface box and the context node, 112 B measured on go1.26.3/darwin-arm64 — charged to the application's own edge middleware and only on requests that actually carry a credential. Warren installs no identity middleware of its own, which is what keeps transport/http's committed request budget where it is.
func WithTelemetry ¶
WithTelemetry returns a copy of ctx carrying t. It is the seeding side of the instrumentation seam, called by observability's edge integration; a nil t — a typed-nil pointer inside the interface included — is not carried, so the never-instrumented path stays a pass-through instead of a request-time panic in Span.
The typed-nil probe uses one constant-time reflect nil check. Invariant 7 forbids type-driven dispatch and container consultation on the request path; a nil probe is neither, and the alternative is exactly the production panic the invariant exists to prevent.
func WithoutIdentity ¶ added in v0.2.0
WithoutIdentity returns a copy of ctx carrying NO identity, whatever it carried before.
Seeding a zero Identity does not do this — WithIdentity refuses a blank subject and returns the context unchanged, so the PREVIOUS identity stays. A second authentication stage that failed and "reset" identity to the zero value was therefore fail-OPEN: the first stage's caller survived. This is the explicit way to say no-one, and it is the direction that matters.
Types ¶
type AuthorizationPolicy ¶
AuthorizationPolicy decides whether the identity on the context may proceed. The edge ring authenticates and puts identity on the context; this policy authorizes.
It is the port an authentication adapter implements — warren/auth, in v0.2 — and it is fully usable without one today: write the policy yourself and attach it per route with transport.Guard, which runs before decode so an unauthorized caller's malformed body is a 403 and not a 400.
The contract: return nil to allow. Return an error from the warren/errors vocabulary to deny — errors.PermissionDenied(action) for a known caller that may not act, errors.Unauthenticated(reason) when identity is absent — and the middleware returns it unchanged, so the adapter maps the right status. An error outside the vocabulary maps downstream to INTERNAL, the safe default for the unknown.
func RequireAuthenticated ¶ added in v0.2.0
func RequireAuthenticated() AuthorizationPolicy
RequireAuthenticated returns the policy that demands any identity at all.
Attach it per route with transport.Guard, which runs BEFORE decode — so an unauthenticated caller's malformed body is a 401, not a 400.
func RequireScope ¶ added in v0.2.0
func RequireScope(scopes ...string) AuthorizationPolicy
RequireScope returns the policy that demands an identity carrying every one of scopes.
It NEVER merges the two denial codes, because they answer different questions and map to different statuses:
no identity at all → UNAUTHENTICATED (401): prove who you are an identity, wrong scope → PERMISSION_DENIED (403): you may not do this
Called with no scopes it is exactly RequireAuthenticated — an empty list demands nothing extra, and must not silently allow an anonymous caller, which is the shape a variadic helper like this usually fails in.
It is written entirely in core types with no dependency, which is what makes it the proof that AuthorizationPolicy is usable before warren/auth exists.
type Handler ¶
Handler is a use case: one request in, one response out, plus an error drawn from the warren/errors vocabulary. It is the unit every transport adapter wraps and every core middleware decorates.
A Handler imports no transport package. That is the framework's whole point.
func Chain ¶
func Chain[Req, Res any](h Handler[Req, Res], mw ...Middleware[Req, Res]) Handler[Req, Res]
Chain composes middleware around a handler and returns the composed handler. mw[0] is the outermost: the first to see the request and the last to see the response, so the argument order reads in execution order.
Chain runs at boot, not per request — the result is stored in the route table as a pre-built closure, and invoking it allocates nothing.
A nil handler, a nil middleware, or a middleware that returns nil panics here, at composition time, with a message naming the position — a startup crash instead of the request-time nil dereference each of them would otherwise become. This is the boot-time panic AGENT.md § General names as sanctioned alongside di.MustResolve: Chain cannot return an error (the §3.2 signature is fixed), and deferring the failure to request 1 is the exact outcome the boot-ordering rule exists to prevent. Note a conditional middleware belongs in the slice only when enabled — append it, don't leave a nil hole. THE FIRST MIDDLEWARE IS THE OUTERMOST ONE, and the loop below is why: it wraps from the end, so mw[0] is applied last and therefore sees the request first. Chain(h, A, B) is A(B(h)).
That ordering is load-bearing for exactly one pair, and both spellings compile: Retrying BEFORE Transactional gives each attempt its own transaction, and the reverse wraps one transaction around every attempt. A field test wrote the reverse and measured zero retries across eight concurrent requests, with two callers refused for stock that existed.
The reverse is REFUSED, at boot, and the walk that finds it sees through every middleware Warren ships and through any of yours whose handler implements Unwrapper — including across a nested Chain. A middleware of yours that does not implement it is a wall the walk stops at, so a Retrying beneath it is invisible here; one method lifts that. There is deliberately no unchecked variant of this function.
type HandlerFunc ¶
HandlerFunc adapts a bare function to Handler — how middleware wrap handlers without declaring a struct each time. Like net/http.HandlerFunc, a nil HandlerFunc panics when called; Chain refuses nil handlers at composition time, so a nil can only be called by bypassing Chain.
type HandlerInstrumentation ¶ added in v0.2.0
type HandlerInstrumentation interface {
InstrumentHandlers() bool
}
HandlerInstrumentation is the optional interface a Telemetry implements to decline boot-time composition of Traced and Metered around every route.
It is optional because the common case has no opinion: a Telemetry that does not implement it is instrumented. An implementation returning false still carries trace context — the transport edge and the broker still use Inject and Extract — it just leaves handler middleware to the user.
type Identity ¶ added in v0.2.0
type Identity struct {
// Subject is the principal: "sub", a service account name, a user ID.
// An Identity without one is not an identity — see WithIdentity.
Subject string
// Issuer is who vouched for it — "iss", or "" when nobody external did.
Issuer string
// Scopes is the OAuth2 "scope" claim, already split. It is a field rather
// than a Claims entry because it is the one claim OAuth2 and OIDC spell
// identically, and it is what lets RequireScope live in core without core
// parsing a token.
Scopes []string
// Claims is everything else, verbatim, and may be nil.
//
// It is READ-ONLY once seeded, and carried by reference rather than
// copied: copying per request would cost an allocation proportional to
// the token, and this map is the verifier's output, not the handler's
// working state.
//
// It is also the reason there is no Audience, ExpiresAt or Tenant field.
// Expiry and audience are the VERIFIER's business — an identity on the
// context is by definition already verified, and a field for expiry
// invites handlers to re-check it badly. Anything v0.2 proves it needs
// gets promoted then, and promotion is additive.
Claims map[string]any
}
Identity is the authenticated caller, as core can express it: no token, no header, no driver type. The edge ring produces it — an authentication middleware, or warren/auth in v0.2 — and AuthorizationPolicy and the handler consume it.
It is a STRUCT and not an interface, and that is the same decision as the ok-bool on IdentityFromContext: absence must not be able to look like presence. An interface would reintroduce the typed-nil trap WithTelemetry already defends against with a reflect probe — a non-nil interface holding a nil pointer reads as identity PRESENT. It would also put a hand-written fake in every user's test package, forever, to express "the caller is u-1". Identity is DATA, like RequestInfo; Telemetry and AuthorizationPolicy are BEHAVIOUR, and that is where interfaces belong.
Nor is it Identity[T]. Each instantiation would get its own context key, so a guard seeding one T and a handler reading another would silently see no identity at all — and AuthorizationPolicy.Authorize(ctx) error is not generic, so a generic identity could only be read by type-switching at request time, which is reflective dispatch on the request path. Claims and the generic free function Claim[T] serve app-defined claims instead.
A test writes:
ctx := app.WithIdentity(context.Background(),
app.Identity{Subject: "u-1", Scopes: []string{"orders:write"}})
func IdentityFromContext ¶ added in v0.2.0
IdentityFromContext returns the caller's identity and whether there is one.
The ok-bool is the first and strongest of the three anti-ambiguity mechanisms, because it is enforced by the compiler rather than by review:
app.IdentityFromContext(ctx).Subject // does not compile
A single-return accessor would make that line legal, and it writes rows owned by "". This is a deliberate asymmetry with log.CorrelationID, which does return a bare string — an absent correlation ID is cosmetic, one log line harder to join, while an absent identity is a security decision. transport.Params.Path already returns an ok-bool for the same reason.
Reading costs 0 allocations, present or absent.
func (Identity) LogValue ¶ added in v0.2.0
LogValue renders the identity for slog WITHOUT its claims.
It is not decoration. Without it, one
log.FromContext(ctx).InfoContext(ctx, "handled", "identity", id)
dumps the claims map — emails, phone numbers, sometimes a nested token — into every log line that touches it. slog.LogValuer is the standard library's redaction seam, and six lines here prevent a data-protection incident that no amount of documentation prevents.
The scope COUNT is included rather than the scopes: it is the part useful in an audit line, and a scope name can carry a tenant or a resource id.
func (Identity) String ¶ added in v0.2.0
String renders the identity for fmt WITHOUT its claims.
LogValue covers slog's top-level attr value and nothing else: slog does not call LogValuer on a NESTED value, and fmt never calls it at all. So an identity logged through any of these leaked the whole claims map —
fmt.Errorf("denied for %v", id) // the 2 a.m. line
slog.Any("w", struct{ ID Identity }{id})
fmt.Sprintf("%+v", []Identity{id})
— and the first of those can wrap into errors.Internal, which reaches a log AND a response body. One String method closes the entire fmt family, including %v, %s, %+v and every container that formats its elements.
It still names the caller, deliberately: a redaction that says nothing gets replaced by someone printing the fields, which leaks again.
type Middleware ¶
Middleware decorates a Handler with a cross-cutting concern. Because it wraps the handler rather than the protocol, one middleware applies identically to HTTP, gRPC, and consumers.
A middleware must return the handler's error with its warren/errors code intact — unchanged or wrapped with %w — because the adapter downstream reads the code to pick a status. Flattening CodeConflict into CodeInternal silently turns a 409 into a 500 and a DLQ message into a nack.
func Authorized ¶
func Authorized[Req, Res any](policy AuthorizationPolicy) Middleware[Req, Res]
Authorized runs the policy before invoking the handler; a denial short-circuits — the handler is never called — and the policy's error returns unchanged, code intact. Because it is core-ring, the same check applies to HTTP, gRPC, and consumers. A nil policy panics at composition time, like Chain's guards.
func Metered ¶
func Metered[Req, Res any]() Middleware[Req, Res]
Metered records a duration histogram per handler and an error counter keyed by the warren/errors code, through the context-carried Telemetry — once per invocation, the error path included. On a context carrying no Telemetry it is an exact pass-through.
func Retrying ¶
func Retrying[Req, Res any](policy RetryPolicy) Middleware[Req, Res]
Retrying re-invokes the handler when the error's OUTERMOST code is CodeUnavailable or CodeContention — the two retryable codes. It is what optimistic concurrency needs: a stale write is CONTENTION, and re-invoking the handler re-reads the aggregate, which is the whole point. The outermost code decides, exactly as an adapter's status mapping does, because wrapping is recategorization: a handler that wraps an Unavailable inside Internal has declared the failure non-retryable, and this middleware agrees. A plain %w wrap (fmt.Errorf) leaves the outermost Warren error untouched and stays retryable. Every other code returns unretried.
When retries exhaust, or the context is cancelled during a wait, the handler's LAST error is returned: it is the freshest and still carries the code the adapter maps. A context that is already cancelled still reaches the handler once — the handler owns its own context checks; this middleware observes cancellation between attempts and during waits. A nil policy panics at composition time, like Chain's guards.
func RetryingOn ¶ added in v0.2.0
func RetryingOn[Req, Res any](policy RetryPolicy, codes ...errors.Code) Middleware[Req, Res]
RetryingOn is Retrying with the retryable set given explicitly. The legal set is CONTENTION, UNAVAILABLE and INTERNAL — the three codes for which the same request may succeed later. The other five are terminal, and composing on one panics at boot.
Prefer Retrying. It already covers CONTENTION and UNAVAILABLE, which is what almost every handler wants and cannot spell wrongly. Two things are reachable only through this function.
FIRST, CodeInternal. Retrying never retries it — an unanticipated failure carries no promise that a second attempt helps — and no other middleware offers it. On a consumer-shaped handler, where a dead-letter queue is already behind the retry, spending a bounded budget before giving up costs one message's latency and saves the operator a redrive:
app.RetryingOn(policy, errors.CodeInternal, errors.CodeUnavailable)
SECOND, deliberate NARROWING — which is the shape of the argument, not widening. A handler safe to re-run on CONTENTION is not automatically safe to re-run on UNAVAILABLE: if it makes an outbound call, an UNAVAILABLE can mean the call arrived and only the reply was lost, so re-invoking the handler charges the card twice. A lost conditional write cannot say that — nothing was written — so name CONTENTION alone and leave UNAVAILABLE to the caller:
app.Chain(h, app.RetryingOn(policy, errors.CodeContention), app.Transactional(uow))
Under plain Retrying that same handler retries UNAVAILABLE too, and doubles the side effect. That is the trade this function exists to let you make.
The codes you name are the WHOLE set; nothing is inherited. Migrating Retrying(p) to RetryingOn(p, errors.CodeContention) STOPS retrying a dependency that was briefly away. Pass both when you want both —
app.RetryingOn(policy, errors.CodeContention, errors.CodeUnavailable)
— which is Retrying(policy) spelled out, and the reason optimistic concurrency needs no code list here at all. A stale write is CONTENTION, not CONFLICT, and Retrying covers it.
Everything else is Retrying's behaviour, including the rule that matters most: the OUTERMOST code decides, so a handler that wraps a CONTENTION inside an INTERNAL has declared the failure final and this agrees. The handler must RE-READ its aggregate on each attempt — this re-invokes the HANDLER, not the transaction, so one that closed over a stale version contends for ever. Retries are only safe on an idempotent handler; see Retrying.
At least one code is required. An empty set would retry nothing, silently, which is the failure mode a variadic helper like this usually has.
This comment said the opposite until 2026-08-09, and the correction is worth stating rather than hiding: it taught that a stale write was CONFLICT and that RetryingOn was the fix for the undersell a field test measured. The CONTENTION split (4a1d152) reassigned that case and gave it to Retrying, and the examples the old comment gave now panic at boot.
func Timeout ¶ added in v0.2.0
func Timeout[Req, Res any](d time.Duration) Middleware[Req, Res]
Timeout bounds the handler with a derived deadline.
Where you put it decides what it bounds ¶
Composed INSIDE Retrying it bounds each ATTEMPT; composed OUTSIDE it bounds the whole retried SEQUENCE. Chain applies its middleware so that the last argument is nearest the handler:
app.Chain(h, app.Retrying(p), app.Timeout(3*time.Second)) // per attempt app.Chain(h, app.Timeout(3*time.Second), app.Retrying(p)) // per sequence
Those are materially different systems — three attempts of up to three seconds each, against three attempts sharing three seconds — and that is exactly why there is no Policy(Timeout(...), Retry(...)) combinator. A combinator would hide, inside a DSL, a choice Chain already makes visible on one line.
It signals; it does not interrupt ¶
A handler that ignores its context runs to completion, exactly as transport/http's ShutdownTimeout documents for the drain. Go has no way to kill a goroutine. What the deadline does is reach every adapter that respects a context — postgres, kafka, any http.Client you build — so the work a handler delegates is bounded even when the handler itself is not.
A caller's SHORTER deadline always wins: context.WithTimeout never extends one, so a request already bounded at 100ms stays bounded at 100ms.
The expiry surfaces as whatever the handler returns. An adapter that respects the context typically returns CodeUnavailable — postgres does — which app.Retrying then retries and every transport maps to 503.
func Traced ¶
func Traced[Req, Res any]() Middleware[Req, Res]
Traced opens one span per handler invocation, named with the "<module>.<handler>" the adapter seeded via WithHandlerName. The Telemetry rides the context (WithTelemetry); on a context carrying none, Traced is an exact pass-through.
func Transactional ¶
func Transactional[Req, Res any](uow UnitOfWork) Middleware[Req, Res]
Transactional wraps Handle in a unit of work, so the aggregate state the handler wrote and the outbox rows for the events it raised commit in one transaction — or neither does.
A handler that calls uow.Do itself under this middleware is joined, not nested: warren.md §10 shows both patterns, and the unit of work's own contract makes the inner call join the transaction in scope. The transaction's error passes through with its code intact, so a serialization failure arrives as CONTENTION (it was UNAVAILABLE until 4a1d152) and app.Retrying — composed OUTSIDE this middleware — re-runs the whole transaction rather than retrying inside a doomed one.
Composing it the other way round is REFUSED by Chain, with a panic naming the fix: one transaction around every attempt has no correct reading, and on Postgres it commits a failed attempt's staged writes alongside the next one's. A nil unit of work panics at composition time, like Chain's guards.
type RequestInfo ¶ added in v0.2.0
type RequestInfo struct {
Protocol string // "http", "grpc"
Method string // "POST", or the gRPC full method
Route string // the matched pattern, NOT the concrete path
Path string // the concrete path, for a span attribute only
Scheme string // "http", "https"
Host string
}
RequestInfo describes one inbound request to the telemetry seam, in terms core can express: no net/http type, no gRPC type, no OTel type.
Route is the PATTERN — "/users/{id}", not "/users/42". It is the field a dashboard groups by and an alert fires on, and the only one whose cardinality is bounded by the size of the route table rather than by traffic.
type RequestSpan ¶ added in v0.2.0
type RequestSpan interface {
// ServerSpan opens the span and returns the derived context and the
// function that ends it, recording the response status and the error.
ServerSpan(ctx context.Context, info RequestInfo) (context.Context, func(status int, err error))
}
RequestSpan is the optional interface a Telemetry implements to open a transport-level SERVER span around a whole request — decode, validation, the handler, and encode.
It is what makes a trace answer "which route", "which status" and "how much of the latency was transport" rather than only "which handler was slow". The handler span nested inside it is where the business time is; this one is where the request is.
Optional because the seam must work for a Telemetry that only implements the two required methods: an adapter checks and falls back to no span.
type RetryPolicy ¶
type RetryPolicy interface {
// Next reports whether a retry should follow the given completed attempt
// (1-based) and how long to wait before it. Returning retry == false
// ends the loop; the handler's last error is returned as-is.
//
// Termination is the policy's contract: the middleware imposes no
// attempt ceiling of its own, so a policy that never returns
// retry == false retries a persistent failure forever — with a zero
// delay, as a hot loop. Every real policy bounds its attempts.
Next(attempt int) (delay time.Duration, retry bool)
}
RetryPolicy decides whether a failed attempt is retried and how long to wait first. The kernel never sees a backoff library.
A concrete one already ships in the core module: broker.ExponentialBackoff. It is the ONLY vocabulary — there is no resilience module and there will not be one (warren.md §7.3). A circuit breaker guards an OUTBOUND dependency and belongs in the adapter that makes the call, behind the port your domain declares; returning errors.Unavailable from there is the whole integration, because this middleware then retries it and every transport maps it.
type Telemetry ¶
type Telemetry interface {
// Span opens a span named name and returns the derived context and the
// function that ends the span, recording err (nil on success). The
// returned context MUST derive from ctx, preserving its values — Metered
// and HandlerName downstream read through it, and a Span that returns a
// fresh context silently disables them.
Span(ctx context.Context, name string) (context.Context, func(err error))
// Record records one handler invocation: its name, its duration, and its
// error — nil on success. Implementations key error counters by the
// warren/errors code.
Record(name string, d time.Duration, err error)
// Inject writes the trace context on ctx through set, one key/value pair
// at a time.
//
// The carrier is a FUNCTION rather than a map so an HTTP header, a
// broker.Message.Headers and a gRPC metadata block are all reachable
// without an intermediate allocation — and, more importantly, so that
// core never names a header key. Which keys carry trace context is the
// W3C spec's business and the implementation's; the kernel's business is
// only that they travel.
Inject(ctx context.Context, set func(key, value string))
// Extract returns a context continuing the trace the key/value pairs get
// answers describe, or ctx unchanged when there is none. get returns ""
// for an absent key.
Extract(ctx context.Context, get func(key string) string) context.Context
}
Telemetry is the core-ring instrumentation seam Traced and Metered speak to. warren/observability implements it over OpenTelemetry and seeds it on the context at the edge; the kernel never imports a telemetry SDK. When no Telemetry is on the context, Traced and Metered are exact pass-throughs.
func TelemetryFromContext ¶
TelemetryFromContext returns the Telemetry carried by ctx, or nil.
type UnitOfWork ¶
UnitOfWork is the transaction seam Transactional speaks to — the shape warren/persistence's UnitOfWork already has. It is declared here rather than imported so app stays free of every sibling contract: the middleware needs one method, and a port with one method costs less than a coupling.
type Unwrapper ¶ added in v0.2.0
Unwrapper is the interface a middleware's handler implements to make itself TRANSPARENT to Chain's composition checks.
EVERY middleware Warren ships implements it, so Chain can walk a stack it was handed rather than only the one it is building — Chain(Chain(h, Retrying(p)), Transactional(uow)) is the same mistake spelled in two calls, and the walk catches it.
It is EXPORTED so yours can implement it too, and that is the point rather than a courtesy. A field test found four compositions the transaction refusal missed, all one cause: this interface was unexported, so a user's middleware could not declare itself transparent even when it wanted to. A middleware whose handler does not implement Unwrapper is a wall the walk stops at, and a Retrying beneath it is invisible to the refusal in an enclosing Chain.
One method, and it costs nothing at request time — the walk runs at boot step 5, never per request:
type logging[Req, Res any] struct{ next app.Handler[Req, Res] }
func (m logging[Req, Res]) Handle(ctx context.Context, r Req) (Res, error) { … }
func (m logging[Req, Res]) Unwrap() app.Handler[Req, Res] { return m.next }
"Every" is load-bearing for Warren's own, and was not always true. Traced, Metered, Authorized and Timeout each returned an anonymous HandlerFunc, so a Retrying beneath one of them was invisible and Chain(Chain(h, Traced(), Retrying(p)), Transactional(uow)) built the corrupting composition without a word. A new middleware here MUST be a named type with an Unwrap, not a closure.
Directories
¶
| Path | Synopsis |
|---|---|
|
internal
|
|
|
exampledomain
Package domain is the user-side domain of the §10 example, so the app tests can compile warren.md's handler verbatim.
|
Package domain is the user-side domain of the §10 example, so the app tests can compile warren.md's handler verbatim. |