clientkit

package module
v0.3.0 Latest Latest
Warning

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

Go to latest
Published: Aug 16, 2026 License: MIT Imports: 11 Imported by: 0

README

clientkit

Release CI Go Support License

Clientkit is the outbound-client shell for Go services. It keeps HTTP and TCP usage recognizable while adding immutable client identity, bounded execution policy, safe retries, timeouts, propagation, observability, cached health, and readiness integration.

Clientkit is independently useful. It integrates with the rest of the Kit Series through Opskit, but it does not require Servekit, Workerkit, Configkit, or Dependkit.

What Clientkit owns

Clientkit owns The application owns
Stable outbound-client identity Endpoint and credential configuration
HTTP execution, retry, and timeout policy Whether repeating an operation is semantically safe
TCP connection establishment and optional TLS Protocol exchanges over a returned net.Conn
Trace propagation and observer events OpenTelemetry SDK, exporter, and provider lifecycle
Cached client health and readiness projection Scheduling active checks
Safe bounded outcome and failure classifications Closing returned HTTP bodies and TCP connections

Clientkit is not service discovery, client-side load balancing, a circuit breaker, an authentication framework, a generated SDK system, or a raw TCP connection pool. Dependkit remains the generic external-dependency health package; Clientkit does not require it.

Installation

go get github.com/jaredjakacky/clientkit@latest

Import only the packages needed by the application. The root package depends only on Opskit outside the standard library. OpenTelemetry API dependencies are kept in the relevant protocol and adapter packages; Clientkit never initializes an SDK or exporter.

HTTP quick start

package main

import (
	"context"
	"io"
	"log"
	"net/http"

	"github.com/jaredjakacky/clientkit"
	"github.com/jaredjakacky/clientkit/httpclient"
)

func main() {
	client, err := httpclient.New(httpclient.Config{
		Config:  clientkit.Config{Name: "payments"},
		BaseURL: "https://payments.example/api/",
	})
	if err != nil {
		log.Fatal(err)
	}

	request, err := client.NewRequest(context.Background(), http.MethodGet, "status", nil)
	if err != nil {
		log.Fatal(err)
	}

	response, err := client.Do(request)
	if err != nil {
		log.Fatal(err)
	}
	defer response.Body.Close()

	_, _ = io.Copy(io.Discard, response.Body)
}

httpclient.New validates configuration without performing network I/O. Do preserves ordinary net/http response/error semantics: a response rejected by Clientkit's classifier is still returned with a nil error. The caller owns every returned response body and must close it.

TCP/TLS quick start

client, err := tcpclient.New(tcpclient.Config{
	Config:  clientkit.Config{Name: "events"},
	Address: "events.example:443",
	TLS:     tcpclient.TLSConfig{Enabled: true},
})
if err != nil {
	log.Fatal(err)
}

conn, err := client.Dial(context.Background())
if err != nil {
	log.Fatal(err)
}
defer conn.Close()

With TLS enabled and no custom tls.Config, Clientkit verifies certificates, infers the verification name from the configured address, and requires TLS 1.2 or newer. A successful connection is an ordinary caller-owned net.Conn. Clientkit does not retain it or place it in a hidden pool.

Production defaults

Area Default
Readiness policy Optional
Health checks Disabled; cached health begins unknown
HTTP response policy Accept 2xx
HTTP total timeout 30 seconds
HTTP attempt timeout 10 seconds
HTTP retries Up to 3 attempts for selected idempotent methods and retryable failures
HTTP origin policy Cross-origin requests and host overrides rejected
HTTP transport Bounded production transport with HTTP/2 attempts enabled
TCP dial timeout 5 seconds
TCP keepalive 30 seconds with the built-in dialer
TCP security Plaintext unless TLS is explicitly enabled
TLS handshake timeout 10 seconds
Default TLS minimum TLS 1.2 when Clientkit creates the TLS policy

Zero often selects a documented production default. Explicit disable fields remove the corresponding Clientkit layer, while parent contexts and caller-owned client behavior remain authoritative. Custom retry and TLS configurations are complete policies rather than partial merges. Consult the API map and Go documentation before overriding defaults.

Packages

Package Responsibility
clientkit Transport-neutral identity, health, failure classification, observers, registry, and Opskit contracts
httpclient HTTP request construction, execution, retries, timeouts, health checks, and results
tcpclient Raw TCP connection establishment, optional TLS, probes, health checks, and results
clientkit/otel Logical-operation, direct-remote, retry, and health OpenTelemetry adapter
httpclient/otel Per-RoundTrip CLIENT spans, propagation, and optional standard HTTP metrics
slogobserver Safe structured logging adapter

Packages under internal/ are implementation details and must not be imported.

HTTP execution model

Do, Execute, and ExecuteWithOptions
Method Use it when
Do Ordinary net/http response/error semantics are enough
Execute The caller needs Clientkit Result, Outcome, attempts, and FailureClass
ExecuteWithOptions One operation needs an explicit name, classifier, retry policy, retry-safety assertion, or timeout override

Outcome answers what happened to the logical operation. FailureClass provides a stable, bounded classification suitable for policy and telemetry. Result.Err remains the original caller-visible Go error. Response rejection is policy information and does not manufacture a transport error.

Automatic repetition requires authorization

An HTTP retry occurs only when all three independent gates allow it:

  1. The selected retry policy permits the method and failure or status.
  2. RetrySafety says repeating the operation is semantically safe.
  3. A request body is absent or mechanically replayable through Request.GetBody.

The default policy does not blindly retry POST, PATCH, CONNECT, or custom methods. Authorizing a POST with RetrySafetyIdempotent is an application assertion. Clientkit does not create or validate idempotency keys, and a retry after a timeout can duplicate a side effect.

For eligible operations, the default retries connection refused, reset, closed, temporary or unknown DNS, and otherwise unclassified transport failures. It fails immediately for recognized TLS failures, DNS not-found, and a RoundTripper that returns neither a response nor an error. TransportRetryNone and TransportRetryAll provide explicit narrower and broader behavior; timeouts remain controlled separately.

RetrySafety also governs method-preserving 307 and 308 redirects. The default follows them only for Clientkit's built-in idempotent methods, RetrySafetyNever rejects them, and RetrySafetyIdempotent permits them. Ordinary 301, 302, and 303 redirect behavior remains unchanged. A non-empty body still requires Request.GetBody before net/http can follow a 307 or 308.

http.NewRequest and http.NewRequestWithContext populate GetBody for common in-memory readers such as bytes.Buffer, bytes.Reader, and strings.Reader. ExecuteWithOptions takes ownership of a non-nil request body and closes it, including when validation prevents the first attempt.

See Usage for complete retry and body rules.

Timeouts and response bodies

The total timeout spans attempts, retry delays, and final response-body use. The attempt timeout restarts for each Clientkit attempt and also remains active for the final body. A parent context, http.Client.Timeout, or transport limit may end work earlier.

Logical and physical observations finish when final response headers or a terminal error are available; they do not measure body consumption. Timeout cleanup remains attached to the final body until EOF, body error, close, or context completion. Always read or close the body promptly. If every timeout is disabled and the parent has no deadline, abandoning the body can retain resources indefinitely.

URL and origin safety

NewRequest uses normal RFC 3986 reference resolution. The BaseURL path is a convenient base, not a confinement boundary: root-relative and parent references can replace or escape it. Absolute references and fragments are rejected.

Execution rejects changes to scheme, host, or effective port by default, as well as Request.Host overrides. Enabling cross-origin execution may forward caller-supplied headers or permit an HTTPS downgrade and should be paired with a restrictive redirect policy.

A caller-supplied *http.Client remains caller-owned and is never mutated or retained. Clientkit shallow-copies its top-level value during construction, so later field assignments do not change Clientkit behavior. Referenced transports, jars, and callback state remain shared. Calling CloseIdleConnections may affect other users when the construction-time transport is shared.

Health and cached readiness

Protocol checks are disabled by default. Check and Registry.CheckAll are the active operations that may contact dependencies. Health, Snapshot, Status, Readiness, and Inspect only project cached state and never perform synchronous dependency I/O.

Registered clients own ordinary cached health. When Registry.CheckAll must synthesize a client-specific failure that the client cannot cache, Registry passive projections retain that exceptional result until a later client-owned assessment supersedes it.

Clientkit creates no scheduler or background goroutine. Applications may run checks directly or use Workerkit to periodically execute the Registry's Opskit CheckGroup. Servekit can then present the same cached state through Opskit. See Health and readiness and Composition.

Observability

When Clientkit owns the default HTTP client and no observer is supplied, it installs the complete default HTTP model:

  • One logical Clientkit HTTP operation represented by an INTERNAL span.
  • One CLIENT span for every instrumented RoundTrip invocation, including retries and redirects.
  • Trace-context injection from the corresponding RoundTrip span.
  • Low-cardinality Clientkit operation, attempt, retry, and health metrics.

A caller-owned HTTP client or a non-nil custom observer replaces part of that automatic boundary. Use httpclient/otel.NewTransport and clientkit/otel.New explicitly when those cases still need complete tracing.

Standard HTTP duration metrics and request-target span attributes are opt-in because they introduce server identity. Raw errors are excluded by default from OpenTelemetry and slog because they may contain URLs, hosts, certificate text, or application data. Applications own SDK/exporter lifecycle and must configure global providers and propagation before constructing Clientkit adapters.

See Observability and Operational safety.

Kit Series composition

HTTP and TCP clients
        │
Clientkit Registry
        │
      Opskit
      ├── Workerkit periodically refreshes checks
      └── Servekit presents passive status/readiness/inspection

Clientkit's per-client readiness policy is separate from the policy used to register the Clientkit Registry with Opskit. Register the Registry as required when its client policies should gate service readiness. If Workerkit only schedules those checks, disable that worker's independent readiness contribution to avoid two gates for the same state.

Documentation

Examples

Runnable examples live under examples/. They use local servers and listeners and require no external service or credentials.

go run ./examples/http-basic
go -C examples/kit-series-composition run .

Development

make help
make verify
make test-race
make govulncheck

make verify checks formatting, the root dependency boundary, vet, tests, runnable examples, and tidy module state.

Maintenance and releasing

Report security issues using SECURITY.md. Use the repository's issue tracker for ordinary defects and proposals.

Releases are created through the manual GitHub Actions release workflow. The workflow validates the requested semantic version and target commit, runs the release verification gate, and creates the tag and GitHub Release only after those checks pass. Do not manually push a release tag around that gate.

Clientkit intentionally keeps its scope narrow. New transports or policy systems should be added only when a concrete outbound-client requirement cannot be expressed through ordinary Go and the existing package boundaries.

License

Clientkit is licensed under the terms in LICENSE.

Documentation

Overview

Package clientkit defines the protocol-neutral contracts shared by production-oriented outbound clients.

The package provides stable client identity, including a bounded protocol category, readiness policy, cached health, failure classification, backend-neutral observation, and registry integration. Protocol packages such as httpclient and tcpclient build on these contracts to perform network operations.

Registry status and readiness evaluation are passive: they read cached health and never contact dependencies. Registry.CheckAll is the explicit boundary for active health checks. Registration captures names, protocol categories, readiness policies, and health-check enablement once so later operation and inspection do not depend on mutable implementations.

A nil Observer passed directly to New becomes a no-op observer. Protocol constructors may install their documented default observer before creating the shared Client. Applications can replace or compose observers without changing protocol execution.

Index

Constants

View Source
const (
	// DefaultMaxConcurrentChecks is the production concurrency bound used by
	// Registry.CheckAll.
	DefaultMaxConcurrentChecks = 4
)
View Source
const DefaultMaxHealthMessageBytes = 256

DefaultMaxHealthMessageBytes is the byte limit applied to health text by DefaultHealthSanitizer.

View Source
const MaxClientNameBytes = 64

MaxClientNameBytes is the maximum byte length accepted by ValidateClientName.

View Source
const MaxClientProtocolBytes = 32

MaxClientProtocolBytes is the maximum byte length accepted by ValidateClientProtocol.

Variables

This section is empty.

Functions

func ValidateClientName

func ValidateClientName(name string) error

ValidateClientName verifies a stable, path-safe, telemetry-safe logical client identifier. Names must contain 1 through MaxClientNameBytes lowercase ASCII letters, digits, periods, underscores, or hyphens; must begin and end with a letter or digit; and must not contain consecutive periods.

func ValidateClientProtocol

func ValidateClientProtocol(protocol string) error

ValidateClientProtocol verifies a stable, low-cardinality, telemetry-safe client-family category. Protocols must contain 1 through MaxClientProtocolBytes lowercase ASCII letters, digits, periods, underscores, or hyphens; must begin and end with a letter or digit; and must not contain consecutive periods.

A protocol identifies the concrete client family, such as "http" or "tcp". It must not contain an endpoint, URL, address, connection string, tenant, or other sensitive or high-cardinality configuration.

Types

type AttemptEvent

type AttemptEvent struct {
	// Client is the stable configured client name.
	Client string
	// Protocol identifies the client protocol.
	Protocol string
	// Operation identifies the bounded operation type.
	Operation string
	// Number is the one-based attempt number.
	Number int
	// StartedAt is the UTC attempt start time.
	StartedAt time.Time
	// EndedAt is the UTC attempt completion time.
	EndedAt time.Time
	// Duration is the attempt duration.
	Duration time.Duration
	// Outcome is the protocol implementation's bounded attempt outcome.
	Outcome string
	// Succeeded is the protocol implementation's authoritative decision that the
	// attempt met its configured acceptance criteria. A true value requires a nil
	// Err and FailureNone; adapters treat contradictory events as failures.
	Succeeded bool
	// FailureClass is the protocol implementation's stable classified failure.
	// It supplements Outcome and Err and is empty on success.
	FailureClass FailureClass
	// Err is the original attempt error and must not be used as a metric label.
	Err error
	// Attributes contains bounded, production-safe attempt details.
	Attributes []opskit.Attribute
}

AttemptEvent describes one actual protocol execution attempt. Err may be recorded by error-aware telemetry but must never be used directly as a metric label.

type Client

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

Client stores immutable shared client policy and concurrency-safe cached health. It must be constructed with New. Protocol implementations may compose it to implement RegisteredClient and manage health-recording lifecycle. It is protocol-neutral and therefore is not itself a complete RegisteredClient.

func New

func New(cfg Config) (*Client, error)

New validates and constructs a protocol-neutral Client without performing network I/O. Protocol users will normally call a protocol package's New function instead.

func (*Client) Health

func (c *Client) Health() Health

Health returns the most recently cached health value without performing I/O.

func (*Client) Name

func (c *Client) Name() string

Name returns the immutable logical client name.

func (*Client) Observer

func (c *Client) Observer() Observer

Observer returns the client's safe backend-neutral observer.

func (*Client) ReadinessPolicy

func (c *Client) ReadinessPolicy() ReadinessPolicy

ReadinessPolicy returns the immutable normalized readiness policy.

func (*Client) UpdateHealth

func (c *Client) UpdateHealth(health Health) Health

UpdateHealth applies the configured sanitization policy, caches the resulting health, and returns the cached value. Callers emitting health through telemetry should use the returned value.

type ClientSnapshot

type ClientSnapshot struct {
	// Name is the client's stable registered identity.
	Name string `json:"name"`
	// Protocol is the client-family category captured during registration.
	Protocol string `json:"protocol"`
	// ReadinessPolicy is the policy captured during registration.
	ReadinessPolicy ReadinessPolicy `json:"readiness_policy"`
	// Health is the producer's effective health at snapshot time. Registry
	// snapshots may project a newer Registry-synthesized check failure over the
	// client-owned cache.
	Health Health `json:"health"`
}

ClientSnapshot is one immutable registry-facing view of a client's identity, readiness policy, and effective cached health.

type Config

type Config struct {
	// Name is the stable, telemetry-safe logical name of the outbound client.
	Name string
	// ReadinessPolicy controls how the client contributes to aggregate
	// readiness. Its zero value is ReadinessOptional.
	ReadinessPolicy ReadinessPolicy
	// Observer completely replaces any protocol-client default observer when it
	// is non-nil. A nil observer lets built-in protocol clients install their
	// default OpenTelemetry observer; clientkit.New itself uses a no-op observer.
	// Use NopObserver to disable observation or MultiObserver to compose observers
	// explicitly.
	Observer Observer
	// HealthSanitizer completely replaces DefaultHealthSanitizer for cached
	// health and health telemetry emitted by built-in protocol clients.
	HealthSanitizer HealthSanitizer
	// DisableHealthSanitizer disables client-level health sanitization. It cannot
	// be combined with HealthSanitizer. The caller then owns validation,
	// cardinality, redaction, and message-size safety.
	DisableHealthSanitizer bool
}

Config defines the protocol-neutral identity, readiness, observation, and health-sanitization policy included by Clientkit protocol configurations.

func (Config) Validate

func (cfg Config) Validate() error

Validate checks shared client configuration without constructing observers or performing protocol-specific work.

type FailureClass

type FailureClass string

FailureClass is a stable, low-cardinality classification that supplements protocol outcomes and original errors. Consumers may use it for metrics, policy decisions, inspection, and alert grouping. Platform error wrapping can prevent some failures from receiving their most specific class; FailureTransport is the safe fallback. Raw errors must never be used as telemetry labels.

const (
	// FailureNone indicates that no classified failure occurred. It is the empty
	// string so successful and unclassified zero values require no special setup.
	FailureNone FailureClass = ""
	// FailureConfiguration indicates missing or unusable client configuration.
	FailureConfiguration FailureClass = "configuration"
	// FailurePolicy indicates rejection by a Clientkit execution policy.
	FailurePolicy FailureClass = "policy"
	// FailureRequest indicates invalid request or operation preparation.
	FailureRequest FailureClass = "request"
	// FailureCanceled indicates caller or parent-context cancellation.
	FailureCanceled FailureClass = "canceled"
	// FailureTimeout indicates deadline expiry or another recognized timeout.
	FailureTimeout FailureClass = "timeout"
	// FailureNameResolution indicates a DNS or name-resolution failure.
	FailureNameResolution FailureClass = "name_resolution"
	// FailureConnectionRefused indicates that the destination refused a connection.
	FailureConnectionRefused FailureClass = "connection_refused"
	// FailureConnectionReset indicates that a connection was reset.
	FailureConnectionReset FailureClass = "connection_reset"
	// FailureConnectionClosed indicates a recognized closed-connection condition.
	FailureConnectionClosed FailureClass = "connection_closed"
	// FailureTLS indicates a non-timeout TLS or certificate failure.
	FailureTLS FailureClass = "tls"
	// FailureRemoteResponse indicates an unacceptable completed remote response.
	FailureRemoteResponse FailureClass = "remote_response"
	// FailureTransport indicates another network or transport failure.
	FailureTransport FailureClass = "transport"
)

type Health

type Health struct {
	// State is the bounded health decision.
	State HealthState `json:"state"`
	// FailureClass is the stable classified cause of a non-healthy result. It
	// supplements health state and never contains a raw error.
	FailureClass FailureClass `json:"failure_class,omitempty"`
	// CheckedAt is the UTC completion time of the active assessment.
	CheckedAt time.Time `json:"checked_at,omitempty"`
	// Duration is the complete assessment duration.
	Duration time.Duration `json:"duration,omitempty"`
	// Message is bounded operational context and must not contain secrets.
	Message string `json:"message,omitempty"`
}

Health is one dependency-health observation suitable for caching and operational inspection.

func DefaultHealthSanitizer

func DefaultHealthSanitizer(_ string, health Health) Health

DefaultHealthSanitizer enforces valid states, UTC timestamps, non-negative durations, and bounded single-line operational text. Control and Unicode formatting characters are removed. It cannot detect secrets; custom clients remain responsible for not returning sensitive text.

func (Health) IsHealthy

func (h Health) IsHealthy() bool

IsHealthy reports whether the state is exactly HealthHealthy.

func (Health) IsReady

func (h Health) IsReady(policy ReadinessPolicy) bool

IsReady reports whether this health state satisfies policy. Informational policy is always satisfied; Registry omits informational clients from readiness entirely.

type HealthAssessment

type HealthAssessment struct {
	// State is the protocol-level health decision.
	State HealthState `json:"state"`
	// FailureClass is the stable cause when the assessment represents failure.
	FailureClass FailureClass `json:"failure_class,omitempty"`
	// Message is protocol-supplied operational context and must not contain
	// secrets.
	Message string `json:"message,omitempty"`
}

HealthAssessment is a protocol-level health decision without lifecycle metadata. Clientkit owns check timestamps, durations, sanitization, caching, and telemetry around the assessment.

type HealthCheckConfigurable

type HealthCheckConfigurable interface {
	// HealthCheckEnabled reports whether Check is configured for active use.
	HealthCheckEnabled() bool
}

HealthCheckConfigurable optionally reports whether a registered client's active health check is enabled. Registry captures this immutable configuration once during registration. The method must return quickly without performing I/O. HealthChecker implementations that do not implement this interface are treated as enabled.

type HealthChecker

type HealthChecker interface {
	RegisteredClient
	// Check actively assesses the dependency and updates cached health.
	Check(context.Context) Health
}

HealthChecker actively refreshes client health. Implementations must be safe for concurrent calls and honor context cancellation cooperatively. For an enabled check, Check must make its returned assessment subsequently visible through Health before it returns. Registry may retain a newer synthetic failure for its own passive projections when Check panics or Registry rejects a result; it does not mutate the client-owned cache.

type HealthEvent

type HealthEvent struct {
	// Client is the stable configured client name.
	Client string
	// Protocol identifies the client protocol.
	Protocol string
	// State is the final health state.
	State HealthState
	// FailureClass is the stable classified health-check failure and is empty
	// when no execution failure caused the health state.
	FailureClass FailureClass
	// CheckedAt is the UTC health-check completion time.
	CheckedAt time.Time
	// Duration is the complete health-check duration.
	Duration time.Duration
	// Message is the bounded health result message.
	Message string
	// Attributes contains bounded, production-safe health details.
	Attributes []opskit.Attribute
}

HealthEvent describes a completed client health check.

type HealthSanitizer

type HealthSanitizer func(clientName string, health Health) Health

HealthSanitizer transforms client health before it reaches telemetry, registry checks, readiness, status, or inspection surfaces. Custom implementations are synchronous: they must be concurrency-safe, return quickly without performing I/O, and produce bounded, non-sensitive output. Clientkit contains sanitizer panics as unknown policy failures.

type HealthState

type HealthState string

HealthState is the bounded operational state of an outbound dependency.

const (
	// HealthUnknown means no current, trustworthy assessment is available.
	HealthUnknown HealthState = "unknown"
	// HealthHealthy means the dependency satisfies its configured health policy.
	HealthHealthy HealthState = "healthy"
	// HealthDegraded means the dependency is usable with reduced capability.
	HealthDegraded HealthState = "degraded"
	// HealthUnhealthy means the dependency does not satisfy its health policy.
	HealthUnhealthy HealthState = "unhealthy"
)

type IdleConnectionCloser

type IdleConnectionCloser interface {
	// CloseIdleConnections releases idle resources without closing the client.
	CloseIdleConnections()
}

IdleConnectionCloser releases currently idle reusable connections without canceling or waiting for active operations. It does not permanently close a client: implementations may remain usable, and future operations may create new connections. Calls are explicit, synchronous, and may be made concurrently.

Applications using this capability during shutdown remain responsible for stopping new work and draining active work first. The capability is optional and pool-oriented; tcpclient does not implement it because successful raw TCP connections are caller-owned rather than tracked by Clientkit.

type NopObserver

type NopObserver struct{}

NopObserver is an Observer that performs no work. Supplying it in a protocol client configuration explicitly disables the protocol's default observer.

func (NopObserver) ObserveAttempt

func (NopObserver) ObserveAttempt(context.Context, AttemptEvent)

ObserveAttempt performs no work.

func (NopObserver) ObserveHealth

func (NopObserver) ObserveHealth(context.Context, HealthEvent)

ObserveHealth performs no work.

func (NopObserver) ObserveRetry

func (NopObserver) ObserveRetry(context.Context, RetryEvent)

ObserveRetry performs no work.

func (NopObserver) StartOperation

StartOperation returns the incoming context and a no-op observation.

type NopOperationObservation

type NopOperationObservation struct{}

NopOperationObservation is an OperationObservation that performs no work.

func (NopOperationObservation) End

End performs no work.

type Observer

type Observer interface {
	// StartOperation observes an operation start and may return a derived context
	// used by the operation and its later observer callbacks.
	StartOperation(context.Context, OperationStartEvent) (context.Context, OperationObservation)
	// ObserveAttempt observes one completed execution attempt.
	ObserveAttempt(context.Context, AttemptEvent)
	// ObserveRetry observes one retry that has been scheduled.
	ObserveRetry(context.Context, RetryEvent)
	// ObserveHealth observes one completed health check.
	ObserveHealth(context.Context, HealthEvent)
}

Observer receives backend-neutral client lifecycle events. Implementations must be safe for concurrent use. Callbacks are synchronous and should return quickly. Event attributes must remain bounded and safe for production telemetry.

func MultiObserver

func MultiObserver(observers ...Observer) Observer

MultiObserver explicitly composes non-nil observers in registration order. Derived operation contexts are chained in the same order, callback panics are contained independently, and operation observations end in reverse order to support stacked spans and cleanup.

func SafeObserver

func SafeObserver(observer Observer) Observer

SafeObserver wraps an observer so telemetry panics cannot affect client execution. Event attribute slices are cloned before callbacks, a panic from StartOperation preserves the incoming context, and a nil observer becomes NopObserver.

type OperationEndEvent

type OperationEndEvent struct {
	// Client is the stable configured client name.
	Client string
	// Protocol identifies the client protocol.
	Protocol string
	// Operation identifies the bounded operation type.
	Operation string
	// StartedAt is the UTC operation start time.
	StartedAt time.Time
	// EndedAt is the UTC operation completion time.
	EndedAt time.Time
	// Duration is the complete operation duration.
	Duration time.Duration
	// Attempts is the number of actual execution attempts.
	Attempts int
	// Outcome is the protocol implementation's bounded final outcome.
	Outcome string
	// Succeeded is the protocol implementation's authoritative decision that the
	// operation met its configured acceptance criteria. A true value requires a
	// nil Err and FailureNone; adapters treat contradictory events as failures.
	Succeeded bool
	// FailureClass is the protocol implementation's stable classified failure.
	// It supplements Outcome and Err and is empty on success.
	FailureClass FailureClass
	// Err is the original terminal error and must not be used as a metric label.
	Err error
	// Attributes contains bounded, production-safe operation details.
	Attributes []opskit.Attribute
}

OperationEndEvent describes the completion of a client operation. Err may be recorded by error-aware telemetry but must never be used directly as a metric label.

type OperationKind

type OperationKind uint8

OperationKind describes whether an observed operation coordinates logical client policy or directly represents one remote interaction.

const (
	// OperationKindLogical identifies an operation that may coordinate retries,
	// backoff, classification, or multiple remote interactions.
	OperationKindLogical OperationKind = iota
	// OperationKindRemote identifies an operation that directly represents one
	// remote interaction.
	OperationKindRemote
)

type OperationObservation

type OperationObservation interface {
	// End observes the final operation outcome exactly once.
	End(context.Context, OperationEndEvent)
}

OperationObservation completes an observation started by Observer.

type OperationObservationFunc

type OperationObservationFunc func(context.Context, OperationEndEvent)

OperationObservationFunc adapts a function into an OperationObservation.

func (OperationObservationFunc) End

End invokes fn when it is non-nil.

type OperationStartEvent

type OperationStartEvent struct {
	// Kind identifies the operation boundary for tracing adapters.
	Kind OperationKind
	// Client is the stable configured client name.
	Client string
	// Protocol identifies the client protocol.
	Protocol string
	// Operation identifies the bounded operation type.
	Operation string
	// StartedAt is the UTC operation start time.
	StartedAt time.Time
	// Attributes contains bounded, production-safe operation details.
	Attributes []opskit.Attribute
}

OperationStartEvent describes the beginning of a client operation. The zero Kind is OperationKindLogical for compatibility with ordinary event literals.

type ReadinessPolicy

type ReadinessPolicy string

ReadinessPolicy describes how one outbound client affects the Clientkit registry's aggregate readiness. It is deliberately separate from Opskit's component-registration policy: this policy belongs to the client domain.

const (
	// ReadinessRequired requires healthy client state.
	ReadinessRequired ReadinessPolicy = "required"
	// ReadinessOptional includes the client as an optional readiness component
	// without allowing it to block aggregate readiness. It is the production-safe
	// zero-value default.
	ReadinessOptional ReadinessPolicy = "optional"
	// ReadinessDegradedAllowed accepts healthy or degraded client state, but not
	// unknown or unhealthy state.
	ReadinessDegradedAllowed ReadinessPolicy = "degraded_allowed"
	// ReadinessInformational keeps client health visible through status, checks,
	// snapshots, and inspection, but omits the client from readiness items
	// and aggregate readiness decisions.
	ReadinessInformational ReadinessPolicy = "informational"
)

func (ReadinessPolicy) BlocksReadiness

func (policy ReadinessPolicy) BlocksReadiness() bool

BlocksReadiness reports whether unsatisfied client health prevents aggregate readiness and therefore requires an active health strategy when used by a built-in protocol client.

type RegisteredClient

type RegisteredClient interface {
	// Name returns a stable name accepted by ValidateClientName.
	Name() string
	// Protocol returns a stable, low-cardinality client-family category accepted
	// by ValidateClientProtocol. It must not contain an endpoint or other
	// sensitive configuration.
	Protocol() string
	// ReadinessPolicy returns stable policy metadata.
	ReadinessPolicy() ReadinessPolicy
	// Health returns cached health without performing dependency I/O.
	Health() Health
}

RegisteredClient exposes the passive operational state required by Registry.

RegisteredClient should normally be implemented by a pointer or value type. Registry registration rejects nil interfaces and typed-nil pointers. The registry captures Name, Protocol, and ReadinessPolicy once during registration, and implementations must keep that metadata stable. After registration, implementations must remain safe for concurrent operational use.

type Registry

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

Registry stores clients and executes enabled health checks with an immutable concurrency bound. Its zero value is ready to use and applies the production default bound.

func NewRegistry

func NewRegistry() *Registry

NewRegistry constructs a registry using DefaultRegistryConfig.

func NewRegistryWithConfig

func NewRegistryWithConfig(cfg RegistryConfig) (*Registry, error)

NewRegistryWithConfig validates cfg and constructs a registry with immutable bounded health-check concurrency. Zero uses the production default; one restores sequential checking.

func (*Registry) CheckAll

func (r *Registry) CheckAll(ctx context.Context) opskit.CheckSummary

CheckAll concurrently executes enabled client checks with bounded concurrency and returns results in deterministic name order. The configured concurrency bound applies across overlapping CheckAll calls, and checks for the same registered client are serialized. Checkers must honor context cancellation cooperatively; a checker panic becomes a stable unhealthy result rather than escaping the worker goroutine. After checker and sanitizer callbacks return, results that crossed a cancellation or deadline boundary are rejected with the corresponding stable failure classification. Client-specific failures synthesized by Registry remain visible to passive registry projections until a later client-owned assessment supersedes them.

func (*Registry) CloseIdleConnections

func (r *Registry) CloseIdleConnections()

CloseIdleConnections synchronously asks every capable registered client to release its currently idle reusable connections in deterministic registered name order. It snapshots membership under the registry read lock, releases the lock before invoking clients, and contains panics from external implementations so later clients are still invoked. Active operations are not canceled or awaited, clients remain registered and reusable, and future requests may establish new connections.

Applications remain responsible for stopping new work and draining active operations before cleanup when required. Typical shutdown composition is:

clients := clientkit.NewRegistry()
clients.MustRegister(payments)
clients.MustRegister(catalog)

// Stop accepting new work and wait for active operations first.
clients.CloseIdleConnections()

func (*Registry) ComponentInfo

func (r *Registry) ComponentInfo() opskit.ComponentInfo

ComponentInfo returns the registry's immutable Opskit identity. The returned value is cloned so callers cannot mutate registry configuration.

func (*Registry) Get

func (r *Registry) Get(name string) (RegisteredClient, bool)

Get returns the registered client identified by name. The returned client remains owned by its creator and may be used concurrently according to its contract.

func (*Registry) Inspect

func (r *Registry) Inspect(ctx context.Context) (opskit.Inspection, error)

Inspect returns a passive registry snapshot and honors cancellation before and after collecting external client health.

func (*Registry) MustRegister

func (r *Registry) MustRegister(client RegisteredClient)

MustRegister registers one client or panics with the exact registration error returned by Register. It is intended for deterministic application composition during startup.

func (*Registry) MustRegisterAll

func (r *Registry) MustRegisterAll(clients ...RegisteredClient)

MustRegisterAll atomically registers clients or panics with the exact registration error returned by RegisterAll. It is intended for static application composition during startup.

func (*Registry) Readiness

func (r *Registry) Readiness(context.Context) opskit.Readiness

Readiness projects passive cached health and Clientkit readiness policies into Opskit readiness items. Informational clients are omitted.

func (*Registry) Register

func (r *Registry) Register(client RegisteredClient) error

Register validates and adds one client to the registry. Nil interfaces and typed-nil pointers are rejected. Registration captures the client's stable name, protocol, readiness policy, and health-check enablement once and returns any validation or duplicate error. The registry supports static composition; clients cannot be replaced or unregistered.

func (*Registry) RegisterAll

func (r *Registry) RegisterAll(clients ...RegisteredClient) error

RegisterAll validates and atomically registers clients in argument order. Nil interfaces and typed-nil pointers are rejected. Registration metadata is captured once per client, including health-check enablement, and validation or duplicate errors register none of the batch. The registry supports static composition; clients cannot be replaced or unregistered.

func (*Registry) Snapshot

func (r *Registry) Snapshot() RegistrySnapshot

Snapshot returns registered clients in deterministic name order using passive health reads and the registry's sanitization policy. A newer client-specific failure synthesized by CheckAll is projected over older client-owned health until a later assessment supersedes it. Snapshot never executes health checks.

func (*Registry) Status

func (r *Registry) Status(context.Context) opskit.Status

Status projects passive cached client health into aggregate Opskit status. It performs no network I/O.

type RegistryConfig

type RegistryConfig struct {
	// MaxConcurrentChecks bounds simultaneous enabled health checks. Zero uses
	// DefaultMaxConcurrentChecks, and one restores sequential execution.
	MaxConcurrentChecks int
	// ComponentInfo overrides the registry's Opskit identity. Empty fields use
	// Clientkit defaults. The stable kit=clientkit label is always preserved.
	ComponentInfo opskit.ComponentInfo
	// HealthSanitizer completely replaces DefaultHealthSanitizer for registry
	// checks, snapshots, status, readiness, and inspection output.
	HealthSanitizer HealthSanitizer
	// DisableHealthSanitizer disables registry-level health sanitization. It
	// cannot be combined with HealthSanitizer. Registered clients must then
	// provide valid, bounded, non-sensitive health values.
	DisableHealthSanitizer bool
}

RegistryConfig configures immutable registry execution behavior.

func DefaultRegistryConfig

func DefaultRegistryConfig() RegistryConfig

DefaultRegistryConfig returns the production registry configuration.

type RegistrySnapshot

type RegistrySnapshot struct {
	// Clients is sorted by client name.
	Clients []ClientSnapshot `json:"clients"`
}

RegistrySnapshot is a deterministic point-in-time view of registered clients.

type RetryEvent

type RetryEvent struct {
	// Client is the stable configured client name.
	Client string
	// Protocol identifies the client protocol.
	Protocol string
	// Operation identifies the bounded operation type.
	Operation string
	// AfterAttempt is the completed attempt that caused the retry.
	AfterAttempt int
	// At is the UTC time at which the retry was scheduled.
	At time.Time
	// Delay is the exact selected retry delay.
	Delay time.Duration
	// Cause is the bounded outcome that caused the retry.
	Cause string
	// FailureClass is the stable classified failure that caused the retry.
	FailureClass FailureClass
	// Attributes contains bounded, production-safe retry details.
	Attributes []opskit.Attribute
}

RetryEvent describes one retry that has been scheduled.

Directories

Path Synopsis
examples
http-basic command
tcp-tls command
Package httpclient provides production-oriented outbound HTTP clients with bounded execution, retries, propagation, observability, and optional cached dependency health.
Package httpclient provides production-oriented outbound HTTP clients with bounded execution, retries, propagation, observability, and optional cached dependency health.
otel
Package otel provides Clientkit's OpenTelemetry HTTP propagation and per-RoundTrip transport instrumentation.
Package otel provides Clientkit's OpenTelemetry HTTP propagation and per-RoundTrip transport instrumentation.
internal
configvalue
Package configvalue centralizes shared configuration-value normalization.
Package configvalue centralizes shared configuration-value normalization.
failure
Package failure provides internal standard-library network failure classification shared by protocol implementations.
Package failure provides internal standard-library network failure classification shared by protocol implementations.
healthrecord
Package healthrecord owns the shared protocol health-recording lifecycle.
Package healthrecord owns the shared protocol health-recording lifecycle.
Package otel adapts Clientkit observer events to OpenTelemetry traces and metrics.
Package otel adapts Clientkit observer events to OpenTelemetry traces and metrics.
Package slogobserver adapts Clientkit observer events to synchronous structured log/slog records.
Package slogobserver adapts Clientkit observer events to synchronous structured log/slog records.
Package tcpclient establishes ordinary plaintext or TLS-over-TCP connections while preserving Clientkit identity, readiness, cached health, and observability.
Package tcpclient establishes ordinary plaintext or TLS-over-TCP connections while preserving Clientkit identity, readiness, cached health, and observability.

Jump to

Keyboard shortcuts

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