endpoint

package
v1.3.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 15, 2026 License: MIT Imports: 12 Imported by: 0

README

endpoint

The endpoint package is the core runtime abstraction of go-kit.

It is where business operations are wrapped with reusable runtime policy such as:

  • timeout
  • logging
  • metrics
  • tracing
  • backpressure
  • circuit breaking
  • rate limiting

If service is the business layer and transport is the protocol layer, endpoint is the runtime governance layer between them.

Core Abstractions

Endpoint

The central type is:

type Endpoint func(ctx context.Context, request any) (response any, err error)

This is the callable unit shared by:

  • transports
  • middleware
  • service wrappers
  • service discovery and client-side execution flows
Middleware

The standard middleware shape is:

type Middleware func(Endpoint) Endpoint

This keeps runtime policies composable and transport-agnostic.

Failer

Failer allows a response type to carry a business error without using the Go error return value.

Use it only when the transport requires a successful wire-level response even on business failure.

Most business logic should still prefer normal Go errors.

Recommended Entry Points

For most services, these are the main entry points:

  • Endpoint
  • Middleware
  • NewBuilder
  • NewTypedBuilder
  • Chain
  • TimeoutMiddleware
  • MetricsMiddleware
  • ErrorHandlingMiddleware
  • LoggingMiddleware
  • Unwrap

Related extension packages:

  • endpoint/circuitbreaker
  • endpoint/ratelimit

Builder API

The builder API is the recommended default for composing endpoint behavior.

Example:

var metrics endpoint.Metrics

ep := endpoint.NewBuilder(base).
    WithMetrics(&metrics).
    WithErrorHandling("CreateUser").
    WithTimeout(5 * time.Second).
    Use(circuitbreaker.Gobreaker(cb)).
    Use(ratelimit.NewErroringLimiter(limiter)).
    Build()

Why prefer the builder:

  • clearer than hand-wrapping multiple middleware layers
  • expresses runtime policy in one place
  • stays aligned with the framework's preferred composition style

Builder contract note:

  • NewBuilder requires a non-nil base endpoint
  • Use(...) requires non-nil middleware values
  • invalid composition input fails fast instead of deferring the problem to request time

Chain

Chain is the lower-level middleware composition helper.

Example:

ep = endpoint.Chain(
    loggingMiddleware,
    metricsMiddleware,
    authMiddleware,
)(base)

Middleware order remains important:

  • the first middleware passed to Chain is the outermost one

Typed Endpoints

TypedEndpoint[Req, Resp] provides compile-time request and response typing while preserving the same runtime model.

Example:

var ep endpoint.TypedEndpoint[HelloReq, HelloResp] =
    func(ctx context.Context, req HelloReq) (HelloResp, error) {
        return HelloResp{Message: "Hello, " + req.Name}, nil
    }

typed := endpoint.Unwrap[HelloReq, HelloResp](
    endpoint.NewTypedBuilder(ep).
        WithTimeout(5 * time.Second).
        Use(circuitbreaker.Gobreaker(cb)).
        Build(),
)

Use typed endpoints when:

  • type safety matters at call sites
  • you want to reduce runtime type assertions

You can adopt typed endpoints incrementally.

Type assertion note:

  • Wrap() and Unwrap() return typed assertion errors when request or response values do not match the expected types, instead of panicking on mismatch.

Built-In Middleware

Core middleware in endpoint:

  • MetricsMiddleware
  • ErrorHandlingMiddleware
  • TimeoutMiddleware
  • LoggingMiddleware

Logging note:

  • LoggingMiddleware(nil, ...) degrades to a nop logger so composition remains safe even when callers do not supply a logger instance.

Specialized middleware packages:

  • endpoint/circuitbreaker
    • Gobreaker
    • HandyBreaker
    • Hystrix integration
  • endpoint/ratelimit
    • NewErroringLimiter
    • NewDelayingLimiter

What Belongs In endpoint

Good responsibilities for this layer:

  • runtime timeout policy
  • request accounting and metrics
  • structured error wrapping
  • tracing and observability
  • resilience wrappers
  • reusable invocation policy

What Does Not Belong In endpoint

Avoid putting these concerns here:

  • protocol-specific encode/decode logic
  • HTTP or gRPC request mapping
  • database access logic
  • product-specific workflow orchestration
  • one-off application behavior that cannot be generalized

If a concern is protocol-specific, it likely belongs in transport. If it is pure domain behavior, it likely belongs in service.

Extension Points

The primary supported extension surface is custom middleware.

Recommended extension patterns:

  • compose custom Middleware
  • use Builder.Use(...)
  • wrap typed endpoints through NewTypedBuilder
  • plug circuit breaker or rate limiter adapters into the middleware chain

Avoid:

  • creating parallel middleware models that bypass Endpoint
  • encoding transport-specific concerns into middleware unless unavoidable

Stability Notes

endpoint core is part of the stable framework surface.

That includes:

  • Endpoint
  • Middleware
  • builder-style composition
  • the framework's central middleware model

More specialized subpackages such as endpoint/circuitbreaker and endpoint/ratelimit are public and supported, but still somewhat more evolvable.

Best Practices

  1. Keep endpoint middleware reusable across services.
  2. Prefer endpoint middleware over transport-specific policy code.
  3. Keep business logic out of endpoint wrappers unless it is truly policy-adjacent.
  4. Use typed endpoints where they improve safety and readability.
  5. Treat endpoint composition as the default place for runtime governance.

Documentation

Overview

Package endpoint defines the core Endpoint type and related helpers.

An Endpoint is the fundamental building block of the framework: a function that accepts a context and an arbitrary request value, and returns an arbitrary response value or an error. Business logic, middleware, and transport layers all communicate through this single interface.

Index

Constants

This section is empty.

Variables

View Source
var ErrBackpressure = errors.New("too many concurrent requests")

ErrBackpressure is returned when the concurrency limit is exceeded.

Functions

func Nop

func Nop(context.Context, any) (any, error)

Nop is a no-op Endpoint that always succeeds and returns an empty struct. Useful as a placeholder in tests or when an endpoint is not yet implemented.

func RequestIDFromContext added in v1.2.0

func RequestIDFromContext(ctx context.Context) string

RequestIDFromContext extracts the request ID from the context.

func WithRequestID added in v1.2.0

func WithRequestID(ctx context.Context, id string) context.Context

WithRequestID injects a request ID into the context.

func WithTraceID added in v1.2.0

func WithTraceID(ctx context.Context, id TraceID) context.Context

WithTraceID injects a trace ID into the context.

Types

type Builder added in v1.2.0

type Builder struct {
	// contains filtered or unexported fields
}

Builder provides a fluent API for assembling an Endpoint with a middleware chain. Middlewares are applied in the order they are added (outermost first), matching the behaviour of Chain().

Example:

ep := endpoint.NewBuilder(myEndpoint).
    Use(loggingMiddleware).
    Use(ratelimit.NewErroringLimiter(limiter)).
    Use(circuitbreaker.Gobreaker(cb)).
    Build()

func NewBuilder added in v1.2.0

func NewBuilder(base Endpoint) *Builder

NewBuilder creates a Builder wrapping the given base Endpoint.

func NewTypedBuilder added in v1.2.0

func NewTypedBuilder[Req, Resp any](te TypedEndpoint[Req, Resp]) *Builder

NewTypedBuilder creates a Builder from a TypedEndpoint. This is a convenience shorthand for endpoint.NewBuilder(te.Wrap()).

Example:

ep := endpoint.NewTypedBuilder(myTypedEndpoint).
    WithTimeout(5 * time.Second).
    Use(circuitbreaker.Gobreaker(cb)).
    Build()

func (*Builder) Build added in v1.2.0

func (b *Builder) Build() Endpoint

Build applies all middlewares and returns the final Endpoint. The Builder can be reused after calling Build.

func (*Builder) Use added in v1.2.0

func (b *Builder) Use(m Middleware) *Builder

Use appends a Middleware to the chain. Returns the same Builder for method chaining.

func (*Builder) WithBackpressure added in v1.2.0

func (b *Builder) WithBackpressure(max int64) *Builder

WithBackpressure appends BackpressureMiddleware with the given concurrency limit.

func (*Builder) WithErrorHandling added in v1.2.0

func (b *Builder) WithErrorHandling(operation string) *Builder

WithErrorHandling appends an ErrorHandlingMiddleware for the named operation.

func (*Builder) WithLogging added in v1.2.0

func (b *Builder) WithLogging(logger *Logger, operation string) *Builder

WithLogging appends LoggingMiddleware for the named operation. This is a shorthand for Use(LoggingMiddleware(logger, operation)).

func (*Builder) WithMetrics added in v1.2.0

func (b *Builder) WithMetrics(m *Metrics) *Builder

WithMetrics appends a MetricsMiddleware and returns the Metrics pointer so the caller can inspect counters later.

var m endpoint.Metrics
ep := endpoint.NewBuilder(base).WithMetrics(&m).Build()

func (*Builder) WithTimeout added in v1.2.0

func (b *Builder) WithTimeout(d time.Duration) *Builder

WithTimeout appends a TimeoutMiddleware that cancels the context after d. This is a shorthand for Use(TimeoutMiddleware(d)).

func (*Builder) WithTracing added in v1.2.0

func (b *Builder) WithTracing() *Builder

WithTracing appends TracingMiddleware to the Builder.

type Endpoint

type Endpoint func(ctx context.Context, request any) (response any, err error)

Endpoint is a function that handles a single RPC-style request. It is the primary abstraction in the framework — every service method, middleware, and transport adapter is expressed in terms of Endpoint.

type EndpointCache

type EndpointCache struct {
	// contains filtered or unexported fields
}

EndpointCache maps service instance addresses to Endpoints. It is updated by Endpointer as service-discovery events arrive and optionally invalidates stale entries after a configurable grace period.

func NewEndpointCache

func NewEndpointCache(factory Factory, logger *log.Logger, options EndpointerOptions) *EndpointCache

func (*EndpointCache) Endpoints

func (c *EndpointCache) Endpoints() ([]Endpoint, error)

func (*EndpointCache) Update

func (c *EndpointCache) Update(event events.Event)

type EndpointCloser

type EndpointCloser struct {
	Endpoint
	io.Closer
}

type EndpointerOption

type EndpointerOption func(*EndpointerOptions)

EndpointerOption is a functional option for EndpointerOptions.

func InvalidateOnError

func InvalidateOnError(timeout time.Duration) EndpointerOption

InvalidateOnError returns an EndpointerOption that enables cache invalidation after a service-discovery error. The cache is cleared once timeout has elapsed, causing subsequent Endpoints() calls to return an error until healthy instances are re-discovered.

type EndpointerOptions

type EndpointerOptions struct {
	// InvalidateOnError, when true, causes the cache to be cleared after
	// InvalidateTimeout has elapsed following a discovery error.
	InvalidateOnError bool

	// InvalidateTimeout is the grace period before the cache is cleared.
	// Only meaningful when InvalidateOnError is true.
	InvalidateTimeout time.Duration
}

EndpointerOptions configures the behaviour of an EndpointCache when a service-discovery error is received.

type ErrorWrapper

type ErrorWrapper struct {
	Operation string
	Err       error
}

ErrorWrapper wraps an endpoint error with the name of the operation that produced it, enabling callers to distinguish errors from different endpoints using errors.As.

func (*ErrorWrapper) Error

func (e *ErrorWrapper) Error() string

func (*ErrorWrapper) Unwrap

func (e *ErrorWrapper) Unwrap() error

type Factory

type Factory func(instance string) (Endpoint, io.Closer, error)

Factory creates an Endpoint for a given service instance address. It is used by EndpointCache and Endpointer to build endpoints on demand as service-discovery events arrive.

The returned io.Closer (if non-nil) is called when the instance is removed from the cache, allowing the caller to release resources (e.g. close a gRPC connection).

type Failer

type Failer interface {
	Failed() error
}

Failer may be implemented by a response type to signal a business-logic error without using the Go error return value. If the response implements Failer and Failed() returns non-nil, the transport layer treats the call as failed.

When to use Failer

Most services should return errors normally:

func (s *svc) CreateUser(ctx context.Context, req CreateUserRequest) (CreateUserResponse, error) {
    if req.Name == "" {
        return CreateUserResponse{}, errors.New("name required") // normal Go error
    }
    ...
}

Use Failer only when the transport protocol requires a successful wire-level response even on business failure — for example, when a gRPC method must return a proto message (not a gRPC status error) to carry structured error details:

type CreateUserResponse struct {
    User  *User
    Error string `json:"error,omitempty"`
    err   error  // unexported, set by business logic
}

func (r CreateUserResponse) Failed() error { return r.err }

type Logger added in v1.2.0

type Logger = log.Logger

Logger is an alias kept for convenience within this package.

type Metrics

type Metrics struct {
	RequestCount    int64
	ErrorCount      int64
	SuccessCount    int64
	TotalDuration   time.Duration
	LastRequestTime time.Time
	// contains filtered or unexported fields
}

Metrics holds counters and timing data collected by MetricsMiddleware. All fields are protected by an internal mutex; use Snapshot to read them safely from any goroutine.

func (*Metrics) Snapshot added in v1.3.0

func (m *Metrics) Snapshot() Metrics

Snapshot returns a point-in-time copy of the metrics that is safe to read without holding any lock.

type Middleware

type Middleware func(Endpoint) Endpoint

Middleware is a function that wraps an Endpoint to add cross-cutting concerns such as logging, metrics, rate limiting, or circuit breaking.

The recommended way to compose middlewares is via Builder:

ep := endpoint.NewBuilder(base).
    Use(m1).Use(m2).Use(m3).
    Build()

Chain is available for cases where a Middleware value itself is needed.

func BackpressureMiddleware added in v1.2.0

func BackpressureMiddleware(max int64) Middleware

BackpressureMiddleware returns a Middleware that limits the number of concurrent in-flight requests to max. When the limit is reached, new requests are rejected immediately with ErrBackpressure.

This is essential for large-scale systems to prevent cascading failures when a downstream service slows down.

Example:

// Allow at most 100 concurrent requests
ep = endpoint.BackpressureMiddleware(100)(ep)

func Chain

func Chain(outer Middleware, others ...Middleware) Middleware

Chain composes multiple Middlewares into a single Middleware. Prefer Builder.Use() for most use cases — it is more readable and supports named shortcuts (WithTimeout, WithMetrics, etc.).

The first argument is the outermost wrapper; subsequent arguments are applied inward:

outer → others[0] → others[1] → … → Endpoint

func ErrorHandlingMiddleware

func ErrorHandlingMiddleware(operation string) Middleware

ErrorHandlingMiddleware returns a Middleware that wraps any error returned by the next Endpoint in an ErrorWrapper tagged with operation. Use errors.As to unwrap it downstream:

var ew *endpoint.ErrorWrapper
if errors.As(err, &ew) { ... }

func InFlightMiddleware added in v1.2.0

func InFlightMiddleware(max int64, counter *int64) Middleware

InFlightMiddleware is like BackpressureMiddleware but also exposes the current in-flight count via the provided pointer. Useful for metrics.

Example:

var inflight int64
ep = endpoint.InFlightMiddleware(100, &inflight)(ep)
// inflight is updated atomically on every call

func LoggingMiddleware added in v1.2.0

func LoggingMiddleware(logger *log.Logger, operation string) Middleware

LoggingMiddleware returns a Middleware that logs each call to the wrapped Endpoint using the provided zap logger. It records:

  • the operation name
  • whether the call succeeded or failed
  • the elapsed duration

Example:

logger, _ := log.NewDevelopment()
ep = endpoint.LoggingMiddleware(logger, "CreateUser")(ep)

func MetricsMiddleware

func MetricsMiddleware(metrics *Metrics) Middleware

MetricsMiddleware returns a Middleware that records per-endpoint metrics into the provided Metrics struct. It increments RequestCount on every call, SuccessCount when the next Endpoint returns nil error, and ErrorCount otherwise. All operations are goroutine-safe.

func TimeoutMiddleware added in v1.2.0

func TimeoutMiddleware(d time.Duration) Middleware

TimeoutMiddleware returns a Middleware that cancels the context after d. The wrapped endpoint receives a context that will be cancelled when the deadline is exceeded.

Example:

ep = endpoint.TimeoutMiddleware(5 * time.Second)(ep)

func TracingMiddleware added in v1.2.0

func TracingMiddleware() Middleware

TracingMiddleware returns a Middleware that propagates or generates a trace ID and request ID in the context.

If the context already contains a trace ID (injected by the transport layer from an incoming header), it is preserved. Otherwise a new one is generated.

This enables end-to-end request correlation across service boundaries without requiring an external tracing system.

Example:

ep = endpoint.TracingMiddleware()(ep)

// In a handler, read the IDs:
traceID := endpoint.TraceIDFromContext(ctx)
reqID   := endpoint.RequestIDFromContext(ctx)

type SpanID added in v1.2.0

type SpanID string

SpanID is a unique identifier for a single operation within a trace.

type TraceID added in v1.2.0

type TraceID string

TraceID is a unique identifier for a distributed trace.

func TraceIDFromContext added in v1.2.0

func TraceIDFromContext(ctx context.Context) TraceID

TraceIDFromContext extracts the trace ID from the context. Returns an empty string if not set.

type TypeAssertError added in v1.2.0

type TypeAssertError struct {
	Got  any
	Want any
}

TypeAssertError is returned when a wrapped endpoint request or unwrapped endpoint response cannot be asserted to the expected type.

func (*TypeAssertError) Error added in v1.2.0

func (e *TypeAssertError) Error() string

type TypedEndpoint added in v1.2.0

type TypedEndpoint[Req, Resp any] func(ctx context.Context, req Req) (Resp, error)

TypedEndpoint is a type-safe variant of Endpoint that eliminates runtime type assertions. Req and Resp are the concrete request and response types.

Use Wrap to convert a TypedEndpoint into a plain Endpoint for use with middleware and transport layers. Use Unwrap to go the other direction.

Example:

type HelloReq  struct { Name string }
type HelloResp struct { Message string }

var ep endpoint.TypedEndpoint[HelloReq, HelloResp] =
    func(ctx context.Context, req HelloReq) (HelloResp, error) {
        return HelloResp{Message: "Hello, " + req.Name}, nil
    }

// Convert to plain Endpoint for middleware / transport
plain := ep.Wrap()

// Build with middleware, then call type-safely
typed := endpoint.Unwrap[HelloReq, HelloResp](
    endpoint.NewBuilder(plain).
        WithTimeout(5 * time.Second).
        Build(),
)
resp, err := typed(ctx, HelloReq{Name: "world"})

func Unwrap added in v1.2.0

func Unwrap[Req, Resp any](ep Endpoint) TypedEndpoint[Req, Resp]

Unwrap wraps a plain Endpoint in a TypedEndpoint[Req, Resp]. The returned function type-asserts the response; it returns an error if the response cannot be asserted to Resp.

Use this after applying middleware to recover type safety:

typed := endpoint.Unwrap[HelloReq, HelloResp](
    endpoint.NewBuilder(base).Use(myMiddleware).Build(),
)
resp, err := typed(ctx, HelloReq{Name: "world"})

func (TypedEndpoint[Req, Resp]) Wrap added in v1.2.0

func (te TypedEndpoint[Req, Resp]) Wrap() Endpoint

Wrap converts a TypedEndpoint into a plain Endpoint. The returned Endpoint performs a type assertion on the request value; it returns a TypeAssertError if the request is not of type Req.

Directories

Path Synopsis

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL