elephantine

package module
v0.29.1 Latest Latest
Warning

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

Go to latest
Published: Sep 7, 2026 License: MIT Imports: 44 Imported by: 11

README

Elephantine

Go Reference

Shared functionality for Elephant systems. It's most likely not something anyone outside of Elephant would be interested in.

What's in the box

Root package
  • HTTP/API server — production-ready server with graceful shutdown, TLS, CORS (with public, CDN-friendly path prefixes), request body limits, health/readiness probes, and pprof
  • JWT & OIDC — JWT claims parsing, OIDC discovery, and OAuth2 client credentials
  • RPC — protocol-neutral authentication middleware, and the Twirp hooks and Connect interceptors that give a service the same logging, metrics and error behaviour on both stacks. See Serving Connect and Twirp below, and docs/connect.md for the fleet-wide reference
  • HTTP client — configurable client with timeouts, connection limits, oauth2 token injection, and Prometheus instrumentation
  • Graceful shutdown — signal-based (SIGINT/SIGTERM) shutdown coordination
  • Error groups — panic-recovering error groups with retry and backoff support, restarts are counted in the task_restarts_total metric
  • Prometheus helpersMetricsHelper for registering counters, gauges, and histograms, and RegisterOrReuse for metrics that are shared between components. Metric conventions for elephant services are documented in docs/metrics.md
  • Feature flags — context-based feature flag propagation
  • Vault — HashiCorp Vault client with Kubernetes auth
rpc/ — RPC errors and interceptors
  • The error vocabulary every elephant service returns: rpc.NotFound, rpc.InvalidArgument, rpc.FailedPreconditionf and the rest, producing *connect.Error with the metadata Twirp carried in its meta map
  • rpc.ToTwirp/rpc.FromTwirp and the interceptors that install them, so one implementation answers both protocols identically
  • rpc.RequireAnyScope, rpc.LoggingInterceptor, rpc.MetricsInterceptor, rpc.WithOutgoingHeaders/rpc.PropagateHeaders
pg/ — PostgreSQL
  • Type conversion helpers for pgtype (Text, Int32, UUID, Time, and nullable pointer variants)
  • Transaction helpers (WithTX, Rollback)
  • NOTIFY/LISTEN pub/sub with ping-based health checking, reconnection, and generic fan-out
  • PoolStatCollector for exposing pgxpool connection pool statistics (saturation, acquire waits, connection churn) as Prometheus metrics
pg/joblock/ — Job locks
  • Distributed job locking via a job_lock table row, instrumented with the pg_job_lock_held, pg_job_lock_transitions_total and pg_job_lock_restarts_total metrics. joblock.Run supervises a worker that must run on one instance at a time; see docs/joblock-restart-semantics.md
  • The table is created by the tern migration in pg/joblock/schema, which a service vendors into its own ./schema (see below). pg/joblock/schema.sql is generated from it for sqlc and must not be edited
test/ — Test utilities
  • Must/MustNot assertions and generic equality checks with diff output
  • Golden file testing for JSON and protobuf
  • Test helpers for JWT auth, RPC errors (IsRPCError, ErrorParity) and structured logging
cmd/protoc-gen-elephant-rpc — Connect adapters
  • A protobuf compiler plugin that generates the adapters that let a service keep the plain interface Twirp gives it while serving Connect, and the interface itself once Twirp generation stops. See Generating the RPC adapters

Generating the RPC adapters

protoc-gen-elephant-rpc keeps the plain service interface — Get(ctx, *GetRequest) (*GetResponse, error), the one protoc-gen-twirp generates — as the contract a service implements and a client is handed, with Connect underneath. It is run through buf, at a version github.com/ttab/mage pins, alongside protoc-gen-go and protoc-gen-connect-go:

{
  "version": "v2",
  "plugins": [
    {"local": ["go", "run", "github.com/ttab/elephantine/cmd/protoc-gen-elephant-rpc@<version>"], "out": "."}
  ]
}

<version> is not written out by hand: github.com/ttab/mage/rpc pins it and passes the template to buf, so a bump of ttab/mage is what moves the generator, exactly as an image tag was before. The template above is what that bump produces.

For each service in a file it writes <proto base>.elephant.go into the <pkg>connect package protoc-gen-connect-go generates, next to <proto base>.connect.go, holding two constructors:

  • New<Service>ServiceHandler(svc <pkg>.<Service>, opts ...connect.HandlerOption) (string, http.Handler) serves an implementation of the plain interface over Connect, and returns the path to mount it on together with the handler, like New<Service>Handler does
  • New<Service>ServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) <pkg>.<Service> is a client with that same plain interface, so it is a drop-in for New<Service>ProtobufClient

Errors pass through both adapters untouched, so an implementation that wants to control the response code returns a *connect.Error.

The options, given as opt entries in the buf template:

Option Default Effect
package_suffix connect The suffix protoc-gen-connect-go generates with. The adapters go into the same package, so the two have to agree
interface false Also emit the plain interface itself into the message package, as <proto base>.rpc.go. Turn it on the day protoc-gen-twirp stops generating it: the name, method set, signatures and doc comments are the ones Twirp emits, so implementations compile unchanged

Only unary RPCs are supported — a streaming method fails generation with an error naming it, since a stream has no place in the plain interface.

The generated code imports connectrpc.com/connect, context, net/http and the message package, and nothing else. It never imports elephantine, so a declarations module like elephant-api can generate with this plugin without taking on elephantine's dependencies; header propagation on the client side comes from an interceptor the caller passes in rather than from the generated client.

cmd/protoc-gen-elephant-rpc/testdata holds a fixture proto generated with both settings of interface, committed and compared by go test ./cmd/protoc-gen-elephant-rpc. Run that test with REGENERATE=true after changing the plugin, and note that the generated fixture packages are compiled by the test rather than by go build ./..., since the go command skips testdata.

Serving Connect and Twirp

This section is the API tour. docs/connect.md is the fleet reference for how Connect is served, called, tested and generated, and the migration playbooks are docs/migration-service.md (moving a service, step by step) and docs/migration-client.md (moving a Go, TypeScript or raw HTTP client).

A service implements the plain protobuf interface — Get(ctx, *GetRequest) (*GetResponse, error) — once, and mounts it on both protocols. Twirp serves /twirp/<pkg>.<Service>/<Method>, Connect serves /<pkg>.<Service>/<Method> plus gRPC and gRPC-Web on the same path, so the two mounts never collide. gRPC and gRPC-Web are reachable inside the cluster only: the fleet's ingress speaks HTTP/1.1 to its targets and external gRPC access is deliberately not provided.

opt, err := elephantine.NewDefaultServiceOptions(
    logger, authParser, reg, elephantine.ServiceAuthRequired)
if err != nil {
    return fmt.Errorf("set up service options: %w", err)
}

server.RegisterAPI(repository.NewDocumentsServer(
    svc, opt.ServerOptions()), opt)

path, handler := repositoryconnect.NewDocumentsServiceHandler(
    svc, opt.HandlerOptions()...)

server.RegisterConnect(path, handler, opt)

The plaintext listener serves HTTP/1.1 and HTTP/2 side by side, told apart by the HTTP/2 connection preface, which is what makes gRPC reachable without TLS from inside the cluster: Go negotiates HTTP/2 through the TLS ALPN handshake and nowhere else, so a listener that does not say so answers HTTP/1.1 only and a gRPC client cannot connect to it at all. It does not make gRPC reachable from outside; the ingress speaks HTTP/1.1 to its targets and no gRPC target group is provided.

A service that serves its RPCs from an http.Server of its own says the same thing with Protocols: elephantine.PlaintextProtocols().

NewDefaultServiceOptions fills in both stacks, so a service gets logging, metrics and authentication parity by construction. A service whose handlers still return Twirp errors adds rpc.LegacyTwirpErrors() to opt.Interceptors, and removes it in the change that moves the handlers to the helpers below.

Errors

*connect.Error is the neutral error type, and the Twirp mount translates on the way out. Error metadata travels as an elephantine.rpc.ErrorMeta detail, declared in rpc/errormeta.proto, because Connect has no free-form meta map in the response body; the Twirp translation flattens it back into one.

Twirp rpc
twirp.NewErrorf(code, ...) rpc.Errorf(code, ...)
twirp.InvalidArgumentError(arg, msg) rpc.InvalidArgument(arg, msg)
elephantine.InvalidArgumentf(arg, ...) rpc.InvalidArgumentf(arg, ...)
twirp.RequiredArgumentError(arg) rpc.RequiredArgument(arg)
twirp.NotFoundError(msg) rpc.NotFound(msg)
twirp.InternalErrorf(...) rpc.Internalf(...)
twirp.FailedPrecondition.Errorf(...) rpc.FailedPreconditionf(...)
twirp.PermissionDenied.Errorf(...) rpc.PermissionDeniedf(...)
twirp.Unauthenticated.Error(msg) rpc.Unauthenticated(msg)
twirp.AlreadyExists.Error(msg) rpc.AlreadyExists(msg)
err.WithMeta(k, v) rpc.WithMeta(err, k, v)
err.MetaMap() rpc.Meta(err)
elephantine.IsTwirpErrorCode(err, code) rpc.IsCode(err, code)
elephantine.RequireAnyScope(ctx, ...) rpc.RequireAnyScope(ctx, ...)
test.IsTwirpError(t, err, code) test.IsRPCError(t, err, code)

rpc.IsCode accepts both error types, so a caller can move its checks before it moves its client constructor. test.ErrorParity(t, twirpErr, connectErr) asserts that the same call answered the two stacks with the same code, message and metadata, which is what makes a service's move checkable.

The codes are identical, and so are the HTTP statuses except three: canceled is 499 rather than 408, deadline_exceeded is 504 rather than 408, and failed_precondition is 400 rather than 412. Anything keyed on 412 for lock conflicts reads rpc_protocol_responses_total{code=...} instead.

Authentication

Authentication is protocol-neutral HTTP middleware, and it fails closed. SetAuthInfoValidation parses the Authorization header and puts the AuthInfo on the request context, where both stacks read it with GetAuthInfo, and answers a request it could not authenticate itself, before the request reaches a handler: connect.NewErrorWriter renders the Connect, gRPC and gRPC-Web error bodies and twirp.WriteError the Twirp one. A missing authorization and an invalid one are both unauthenticated (401); permission_denied is for a caller we identified and that is not allowed to make the call. ServiceAuthOptional lets a missing authorization through as an anonymous caller; an invalid one always fails.

The Twirp hook and the rpc.AuthInfoInterceptor the same call installs are the safety net for a mount that does not run the middleware: they refuse a call that reaches a handler with no authenticated caller on its context. Every interceptor in rpc wraps streaming handlers and streaming clients as well as unary calls, since connect.UnaryInterceptorFunc would pass a stream through unauthenticated and uncounted.

Client headers

A caller that needs per-call headers, which twirp.WithHTTPRequestHeaders gave it, sets them on the context and adds one client interceptor:

client := repositoryconnect.NewDocumentsServiceClient(
    httpClient, endpoint,
    connect.WithInterceptors(rpc.PropagateHeaders()))

ctx = rpc.WithOutgoingHeaders(ctx, http.Header{
    "X-Forwarded-For": []string{addr},
})
Generating the protobuf in this repository

mage proto:generate compiles rpc/errormeta.proto and the fixture service in internal/testservice, with buf and the plugin versions github.com/ttab/mage/rpc pins. rpc:generate in ttab/mage cannot be used here, since it discovers services as <proto root>/*/service.proto and neither source is laid out that way.

CORS and request bodies

APIServer wraps the request mux in the CORS middleware and a request body limit before handing it to the listeners, so both the plain and the TLS server get the same treatment.

CORS

Origins are checked against an allowlist: Hosts entries match a hostname exactly or as a parent domain, HostPatterns entries are globs, and the scheme must be https unless the host is localhost. The defaults allow localhost and tt.se; APIServerCORSHosts(...) replaces the host list. The default allowed headers are Authorization, Content-Type, Connect-Protocol-Version and Connect-Timeout-Ms, the last two because a browser Connect client sends them on every call. An allowed origin is echoed back in Access-Control-Allow-Origin together with Vary: Origin.

An anonymous read surface served through a CDN wants the opposite of that: the response is the same for everyone, and varying on Origin gives the CDN one cache entry per embedding site. Mark such paths public:

srv := elephantine.NewAPIServer(logger, addr, profileAddr,
    elephantine.APIServerPublicCORS("/public/"),
)

A request whose path starts with a public prefix is answered with Access-Control-Allow-Origin: * and no Vary header, and its preflight is answered with a 204 whatever the Origin header says. Everything outside the prefixes keeps the allowlist behaviour unchanged. Prefixes are matched literally, so pass "/public/" rather than "/public".

Do not mark a path that reads the caller's Authorization header or cookies as public. The browser will not send credentials to a wildcard origin, but a shared cache in front of the service is still free to hand one caller's response to another.

Request body limit

Request bodies are capped at DefaultMaxBodyBytes (8 MiB). A request that declares a larger Content-Length is refused with 413 before it reaches a handler; a body of unknown length fails when the handler reads past the limit. The cap matters because Twirp buffers the whole body in memory before unmarshalling it, so the limit is what a single caller can make a replica hold.

Override it per service, and only turn it off (0 or less) for a listener that has to take genuinely large uploads:

srv := elephantine.NewAPIServer(logger, addr, profileAddr,
    elephantine.APIServerMaxBodyBytes(64<<20),
)

Reporting the application version

APIServer exposes two build-info endpoints:

  • GET /version on the public API server — JSON summary with the application name, version, VCS stamp, and a curated module list (defaults: github.com/ttab/elephantine, github.com/ttab/elephant-api, github.com/ttab/elephant-tt-api). Pass APIServerModules(...) to report additional modules.
  • GET /debug/bom on the health/metrics server — the full debug.BuildInfo in the canonical go version -m format, for SBOM/forensic use. The health server must stay internal.
Setting the application version

Our services are built as Docker images triggered by git tags. Wire the tag through the pipeline in three places.

1. Service main package — declare a package-level version variable and pass it to NewAPIServer:

package main

var version string // set via -ldflags at build time

func main() {
    // ...
    srv := elephantine.NewAPIServer(logger, addr, profileAddr,
        elephantine.APIServerVersion(version),
    )
}

2. Dockerfile — accept a VERSION build-arg and pass it to go build via -ldflags:

ARG TARGETOS TARGETARCH
ARG VERSION=v0.0.0-dev
RUN GOOS=$TARGETOS GOARCH=$TARGETARCH \
    go build \
      -ldflags "-X main.version=$VERSION" \
      -o /build/myservice ./cmd/myservice

3. .github/workflows/build.yaml — forward the git tag (github.ref_name) to the build-arg:

- name: Build and push release image
  uses: docker/build-push-action@v7
  with:
    context: .
    platforms: linux/amd64,linux/arm64
    push: true
    tags: ghcr.io/${{ github.repository }}:${{ github.ref_name }}
    build-args: |
      VERSION=${{ github.ref_name }}
    cache-from: type=gha
    cache-to: type=gha,mode=max

The workflow already triggers on v* tags, so github.ref_name is the tag (v1.2.3).

If APIServerVersion is not set, or if the binary is built locally without the ldflag, the endpoint reports v0.0.0-dev.

VCS revision, timestamp, and dirty state are stamped automatically by the Go toolchain (-buildvcs=auto, default) as long as .git is present in the build context — which it is with the standard ADD . ./ step. Dependency versions come from the build graph with no extra flags.

Vendoring the job lock migration

Neither mage sql:migrate nor elephant-platform's setup db migrate looks inside a dependency for a migration, so a service that uses pg/joblock has to carry the job_lock table in its own ./schema. Declare the library once and let mage copy the migration in:

mage sql:vendorAdd github.com/ttab/elephantine pg/joblock/schema
mage sql:vendor
mage sql:migrate

mage sql:vendorCheck fails when elephantine ships a migration the service has not vendored yet, so wire it into lint or a test. A service that created the table by hand before the migration existed asserts that in the migration that did the work instead of vendoring:

-- covers: github.com/ttab/elephantine pg/joblock/schema/001_job_lock.sql

Each feature that needs a table of its own gets its own schema directory under its package, so a service only ever vendors the migrations for the features it uses. The path is part of the contract: a service that vendored the job lock from the old pg/schema location updates the dir in schema/vendor.json and the -- vendored-from: or -- covers: line in its migration to pg/joblock/schema/001_job_lock.sql. The file itself is unchanged, so its checksum and applied state are unaffected.

Adding a migration to the library:

mage sql:librarySchema pg/joblock/schema pg/joblock/schema.sql
mage sql:generate

The first regenerates the flat schema sqlc reads, the second the query code in pg/joblock/internal/postgres. pg/joblock/schema_test.go fails if the flat schema is left behind.

Documentation

Index

Constants

View Source
const (
	DialTimeoutInternal = 1 * time.Second
	DialTimeoutExternal = 5 * time.Second
	DialTimeoutSlow     = 10 * time.Second
)
View Source
const (
	// LogKeyLogLevel is the log level that an application was configured
	// with.
	LogKeyLogLevel = "log_level"
	// LogKeyError is an error message.
	LogKeyError = "err"
	// LogKeyErrorCode is an error code.
	LogKeyErrorCode = "err_code"
	// LogKeyErrorMeta is a JSON object with error metadata.
	LogKeyErrorMeta = "err_meta"
	// LogKeyCountMetric was planned to be used to increment a given metric
	// when used. TODO: not implemented yet, should it be removed?
	LogKeyCountMetric = "count_metric"
	// LogKeyDocumentUUID is the UUID of a document.
	LogKeyDocumentUUID = "document_uuid"
	// LogKeyDocumentType is the type of a document.
	LogKeyDocumentType = "document_type"
	// LogKeyDocumenTitle is the title of a document.
	LogKeyDocumentTitle = "document_title"
	// LogKeyDocumentVersion is the version of a document.
	LogKeyDocumentVersion = "document_version"
	// LogKeyDocumentStatus is the status of a document.
	LogKeyDocumentStatus = "document_status"
	// LogKeyDocumentStatusID is the id of a document status.
	LogKeyDocumentStatusID = "document_status_id"
	// LogKeyTransaction is the name of a transaction, usually used to
	// identify a transaction that has failed.
	LogKeyTransaction = "transaction"
	// LogKeyOCSource is used to identify a source document from OC by UUID.
	LogKeyOCSource = "oc_source"
	// LogKeyOCVersion is the version of the OC document.
	LogKeyOCVersion = "oc_version"
	// LogKeyOCEvent is the type of an OC event- or content-log event.
	LogKeyOCEvent = "oc_event"
	// LogKeyChannel identifies a notification channel.
	LogKeyChannel = "channel"
	// LogKeyMessage can be used to log a unexpected message.
	LogKeyMessage = "message"
	// LogKeyDelay can be used to communicate the delay when logging
	// information about retry attempts and backoff delays.
	LogKeyDelay = "delay"
	// LogKeyAttempts can be used to communicate a retry attempt counter.
	LogKeyAttempts = "attempts"
	// LogKeyBucket is used to log a S3 bucket name.
	LogKeyBucket = "bucket"
	// LogKeyObjectKey is used to log a S3 object key.
	LogKeyObjectKey = "object_key"
	// LogKeyComponent is used to communicate what application subcomponent
	// the log entry is from.
	LogKeyComponent = "component"
	// LogKeyCount is used to communicate a count.
	LogKeyCount = "count"
	// LogKeyEventID is the ID of an event.
	LogKeyEventID = "event_id"
	// LogKeyEventType is the type of an event.
	LogKeyEventType = "event_type"
	// LogKeyJobLock is the name of a job lock.
	LogKeyJobLock = "job_lock"
	// LogKeyJobLockID is the ID of a job lock.
	LogKeyJobLockID = "job_lock_id"
	// LogKeyState is the name of a state, like "held", "lost" or "accepted".
	LogKeyState = "state"
	// LogKeyIndex is the name of a search index, like an Open Search index.
	LogKeyIndex = "index"
	// LogKeyRoute is used to name a route or path.
	LogKeyRoute = "route"
	// LogKeyService is used to specify an RPC service.
	LogKeyService = "service"
	// LogKeyMethod is used to specify an RPC method.
	LogKeyMethod = "method"
	// LogKeySubject is the sub of an authenticated client.
	LogKeySubject = "sub"
	// LogKeyScopes are the scopes of the authenticated client.
	LogKeyScopes = "scopes"
	// LogKeyStatusCode is the HTTP status code used for a response.
	LogKeyStatusCode = "status_code"
	// LogKeyName is the name of a resource.
	LogKeyName = "name"
	// LogKeyAlertCode is a code used to flag that something needs the
	// attention of a human operator.
	LogKeyAlertCode = "alert_code"
)

Log attribute keys used throughout the application.

View Source
const (
	EnvServiceAccountToken = "SERVICE_ACCOUNT_TOKEN"
	// A well-known file path, not a credential.
	DefaultServiceAccountTokenPath = "/var/run/secrets/kubernetes.io/serviceaccount/token" //nolint:gosec // file path
	EnvVaultAuthRole               = "VAULT_AUTH_ROLE"
	DefaultAuthRole                = "deploy"
)
View Source
const DefaultMaxBodyBytes int64 = 8 << 20

DefaultMaxBodyBytes is the request body limit an APIServer applies when APIServerMaxBodyBytes isn't used.

The number is picked from what our services actually send. The routine Twirp body is a document write: a news document with its blocks, metadata and links serialises to tens of kilobytes of JSON, and the largest we see stay well under a megabyte. The outliers are the RPCs that carry file content inline as a protobuf bytes field — elephant-hub's PublishVersion and BulkPublishVersion ship manifests and assets that way — where the JSON encoding adds a third on top for base64. Eight mebibytes leaves an order of magnitude of headroom over the first case and room for a multi-megabyte bundle in the second.

It matters because Twirp buffers the whole request body in memory and then unmarshals it, so an unbounded body is an unbounded allocation per in-flight request: the limit is what one caller can make a replica hold.

A service that serves real file uploads on the API listener — elephant-hub's CI publish endpoint takes multipart bodies up to 64 MiB — has to raise this with APIServerMaxBodyBytes.

Variables

View Source
var (
	ErrGracefulStop = errors.New("stop requested")
	ErrGracefulQuit = errors.New("quit requested")
)
View Source
var ErrNoAuthorization = auth.ErrNoAuthorization

ErrNoAuthorization is used to communicate that authorization was completely missing, rather than being invalid, expired, or malformed.

View Source
var ErrTaskDisabled = errors.New("task disabled")

ErrTaskDisabled can be returned by a Required task to signal that it is disabled (typically by configuration) and should not run. The group treats it as if the task was never registered: it does not cancel the group, and Wait does not report it as an error. This lets callers register a task unconditionally and opt out from inside it, instead of wrapping the registration in a conditional.

Functions

func AuthenticationCLIFlags added in v0.13.5

func AuthenticationCLIFlags() []cli.Flag

AuthenticationCLIFlags returns all the CLI flags that are needed to later call AuthenticationConfigFromCLI with the resulting cli.Context.

func CORSMiddleware added in v0.11.0

func CORSMiddleware(opts CORSOptions, handler http.Handler) http.Handler

func CallWithRecover added in v0.22.0

func CallWithRecover(ctx context.Context, fn func(ctx context.Context) error) (outErr error)

func Close added in v0.17.4

func Close(name string, c io.Closer, outErr *error)

Close a resource and joins the error to the outError if the close fails. Will ignore os.ErrClosed so it's safe to use together with "manual" closing of files.

func ContextWithFeatureFlags added in v0.23.5

func ContextWithFeatureFlags(ctx context.Context, flags map[string]bool) context.Context

ContextWithFeatureFlags creates a context with the specified context flags set. If the context already has feature flags set they will be preserved as is unless overridden by the new flags.

func FeatureIsEnabled added in v0.23.5

func FeatureIsEnabled(ctx context.Context, flag string, defaultState bool) bool

FeatureIsEnabled checks the state of a feature flag.

func GetLogMetadata added in v0.5.0

func GetLogMetadata(ctx context.Context) map[string]any

GetLogMetadata returns the log metatada map for the context.

func HTTPErrorFromResponse

func HTTPErrorFromResponse(res *http.Response) error

HTTPErrorFromResponse creates a HTTPError from a response struct. This will consume and create a copy of the response body, so don't use it in a scenario where you expect really large error response bodies.

If we fail to copy the response body the error will be joined with the HTTPError.

func HTTPErrorHandlerFunc added in v0.14.0

func HTTPErrorHandlerFunc(
	fn func(http.ResponseWriter, *http.Request) error,
) http.HandlerFunc

HTTPErrorHandlerFunc creates a http.HandlerFunc from a function that can return an error. If the error is a HTTPError the information it carries will be used for the error response. Otherwise it will be treated as a internal server error and the error message will be sent as the response.

func InvalidArgumentf deprecated added in v0.17.10

func InvalidArgumentf(argument string, format string, a ...any) error

InvalidArgumentf creates an invalid argument error with a formatted message.

Deprecated: use github.com/ttab/elephantine/rpc.InvalidArgumentf, which creates the same error as a *connect.Error. The Twirp error helpers go away with the last Twirp mount in the fleet.

func IsHTTPErrorWithStatus

func IsHTTPErrorWithStatus(err error, status int) bool

IsHTTPErrorWithStatus checks if the error (or any error in its tree) is a HTTP error with the given status code.

func IsTwirpErrorCode deprecated added in v0.4.0

func IsTwirpErrorCode(err error, code twirp.ErrorCode) bool

IsTwirpErrorCode checks if any error in the tree is a twirp.Error with the given error code.

Deprecated: use github.com/ttab/elephantine/rpc.IsCode, which takes a connect.Code and recognises both error types, so a check can be moved before the client constructor that produces the errors is.

func ListenAndServeContext

func ListenAndServeContext(
	ctx context.Context, server *http.Server,
	shutdownTimeout time.Duration,
	opts ...ListenAndServeOption,
) error

ListenAndServeContext will call ListenAndServe() for the provided server and then Shutdown() if the context is cancelled.

Check `errors.Is(err, http.ErrServerClosed)` to differentiate between a graceful server close and other errors.

func LogMetadataMiddleware added in v0.9.6

func LogMetadataMiddleware(next http.Handler) http.Handler

LogMetadataMiddleware wraps an http.Handler with a middleware that adds a log metadata map to the request context.

func LoggingHooks deprecated added in v0.5.0

func LoggingHooks(
	logger *slog.Logger,
) *twirp.ServerHooks

LoggingHooks creaes a twirp.ServerHooks that will set log metadata for the twirp service and method name, and log error responses.

Deprecated: a service that also serves Connect gets the same behaviour on that stack from github.com/ttab/elephantine/rpc.LoggingInterceptor, and NewDefaultServiceOptions installs both. The hooks go away with the last Twirp mount in the fleet.

func MarshalFile added in v0.8.2

func MarshalFile(path string, o any) (outErr error)

MarshalFile is a utility function for marshalling a data structure to JSON and writing it to a file. The JSON will be pretty printed.

func MaxBodyBytesMiddleware added in v0.28.0

func MaxBodyBytesMiddleware(n int64, handler http.Handler) http.Handler

MaxBodyBytesMiddleware caps the request bodies passed to the wrapped handler at n bytes. A request that declares a larger Content-Length is refused with 413 without being read; anything else gets a http.MaxBytesReader body, so a chunked or lying request fails when the handler reads past the limit. A limit of zero or less is no limit.

func NewHTTPClient added in v0.21.0

func NewHTTPClient(
	timeout time.Duration,
	opts ...HTTPClientOption,
) *http.Client

NewHTTPClient returns a http.Client configured with timeouts and connection limits. The default request timeout, including time for response read is 10 seconds. Use the option functions to customise.

func NewTwirpMetricsHooks deprecated added in v0.4.0

func NewTwirpMetricsHooks(opts ...TwirpMetricOptionFunc) (*twirp.ServerHooks, error)

NewTwirpMetricsHooks creates new twirp hooks enabling prometheus metrics.

Deprecated: a service that also serves Connect gets the same series on that stack from github.com/ttab/elephantine/rpc.MetricsInterceptor, and NewDefaultServiceOptions installs both. The hooks go away with the last Twirp mount in the fleet.

func PlaintextProtocols added in v0.29.0

func PlaintextProtocols() *http.Protocols

PlaintextProtocols is the protocol set the plaintext listener serves: HTTP/1.1 and HTTP/2 without TLS. Go only negotiates HTTP/2 through the TLS ALPN handshake, so a listener that does not say this serves HTTP/1.1 only, and gRPC — which Connect serves on the same path as everything else, and which requires HTTP/2 — cannot be spoken to it at all. The two are told apart by the HTTP/2 connection preface, so Twirp, SSE, the websocket upgrade and every other HTTP/1.1 caller are unaffected.

APIServer sets it on its own listener. It is exported for the services that serve their RPCs from an http.Server of their own, which have to say the same thing or lose gRPC without any error to say so.

func RHandleFunc deprecated

RHandleFunc creates a httprouter.Handle from a function that can return an error. If the error is a HTTPError the information it carries will be used for the error response. Otherwise it will be treated as an internal server error and the error message will be sent as the response.

Deprecated: use the standard library muxer and HTTPErrorHandlerFunc instead.

func RegisterOrReuse added in v0.27.3

func RegisterOrReuse[C prometheus.Collector](
	reg prometheus.Registerer, c C,
) (C, error)

RegisterOrReuse registers the collector with the registerer. If a collector with an identical descriptor set already has been registered, that collector is returned instead. This allows shared metric vectors to be declared by every component that uses them, instead of requiring coordination around a single registration point.

func SafeClose deprecated added in v0.6.2

func SafeClose(logger *slog.Logger, name string, c io.Closer)

SafeClose can be used with defer to defer the Close of a resource without ignoring the error.

Deprecated: use Close() instead.

func ScopePrefixRegexp added in v0.12.0

func ScopePrefixRegexp(prefix string) *regexp.Regexp

func SetAuthInfo added in v0.6.0

func SetAuthInfo(ctx context.Context, info *AuthInfo) context.Context

SetAuthInfo creates a child context with the given authentication information.

func SetLogMetadata added in v0.5.0

func SetLogMetadata(ctx context.Context, key string, value any)

SetLogMetadata sets a log metadata value on the context if it has a log metadata map.

func SetUpLogger

func SetUpLogger(logLevel string, w io.Writer) *slog.Logger

SetUpLogger creates a default JSON logger and sets it as the global logger.

func TwirpErrorToHTTPStatusCode deprecated added in v0.8.4

func TwirpErrorToHTTPStatusCode(err error) int

TwirpErrorToHTTPStatusCode returns the HTTP status code for the given error. If the error is nil 200 will be returned, if the error isn't a twirp.Error 500 will be returned.

Deprecated: use github.com/ttab/elephantine/rpc.HTTPStatus with the code of a *connect.Error. Note that Connect answers canceled, deadline_exceeded and failed_precondition with a different status than Twirp does.

func UnmarshalFile

func UnmarshalFile(path string, o any) (outErr error)

UnmarshalFile is a utility function for reading and unmarshalling a file containing JSON. The parsing will be strict and disallow unknown fields.

func UnmarshalHTTPResource

func UnmarshalHTTPResource(resURL string, o any) (outErr error)

UnmarshalHTTPResource is a utility function for reading and unmarshalling a HTTP resource. Uses the default HTTP client.

func WithLogMetadata added in v0.5.0

func WithLogMetadata(ctx context.Context) context.Context

WithLogMetadata creates a child context with a log metadata map.

Types

type APIServer added in v0.14.0

type APIServer struct {
	Mux    *http.ServeMux
	Health *HealthServer
	CORS   *CORSOptions
	// contains filtered or unexported fields
}

APIServer is a HTTP server for our APIs that bundles a request mux, a health server, and CORS handling. Construct it with NewAPIServer (or NewTestAPIServer for tests), register services with RegisterAPI(s), and start it with ListenAndServe.

func NewAPIServer added in v0.14.0

func NewAPIServer(
	logger *slog.Logger,
	addr string, profileAddr string,
	opts ...APIServerOption,
) *APIServer

func NewTestAPIServer added in v0.17.0

func NewTestAPIServer(
	t Cleaner,
	logger *slog.Logger,
	opts ...APIServerOption,
) (*APIServer, *http.Client)

func (*APIServer) Addr added in v0.17.0

func (s *APIServer) Addr() string

func (*APIServer) AliveEndpoint added in v0.14.0

func (s *APIServer) AliveEndpoint() string

func (*APIServer) ListenAndServe added in v0.14.0

func (s *APIServer) ListenAndServe(ctx context.Context) error

func (*APIServer) RegisterAPI added in v0.14.0

func (s *APIServer) RegisterAPI(
	api APIServiceHandler, opt ServiceOptions,
)

func (*APIServer) RegisterAPIs added in v0.21.4

func (s *APIServer) RegisterAPIs(
	opt ServiceOptions, apis ...APIServiceHandler,
)

func (*APIServer) RegisterConnect added in v0.29.0

func (s *APIServer) RegisterConnect(
	path string, h http.Handler, opt ServiceOptions,
)

RegisterConnect mounts a Connect handler behind the same authentication middleware as the Twirp services. Pass it the path and handler a generated New<Service>ServiceHandler returns:

server.RegisterConnect(documentsv1connect.NewDocumentsServiceHandler(
	svc, opt.HandlerOptions()...))

The path is a subtree, and no method is bound, since Connect serves gRPC and gRPC-Web on the same path and may answer GET for the RPCs that declare themselves free of side effects.

type APIServerOption added in v0.17.13

type APIServerOption func(s *APIServer)

APIServerOption configures an APIServer when passed to NewAPIServer or NewTestAPIServer.

func APIServerCORSHosts added in v0.17.13

func APIServerCORSHosts(hosts ...string) APIServerOption

func APIServerMaxBodyBytes added in v0.28.0

func APIServerMaxBodyBytes(n int64) APIServerOption

APIServerMaxBodyBytes limits the size of a request body accepted by the API listener, overriding DefaultMaxBodyBytes. A request that declares a larger Content-Length is refused with 413 before it reaches a handler, and a request that streams past the limit fails on read.

Pass a value of zero or less to turn the limit off. Do that only for a listener that has to accept genuinely large uploads, and prefer raising the limit to removing it.

func APIServerModules added in v0.26.0

func APIServerModules(modules ...string) APIServerOption

APIServerModules adds module paths to the /version endpoint's module list. The defaults (github.com/ttab/elephantine, github.com/ttab/elephant-api, github.com/ttab/elephant-tt-api) are always included; modules passed here are appended.

func APIServerPublicCORS added in v0.28.0

func APIServerPublicCORS(prefixes ...string) APIServerOption

APIServerPublicCORS marks request path prefixes as open to any origin: requests under them are answered with "Access-Control-Allow-Origin: *" and no "Vary: Origin", and their preflights succeed whatever the Origin header is. Paths outside the prefixes keep the origin allowlist from APIServerCORSHosts.

Use it for anonymous read surfaces, typically served through a CDN, where the per-origin response and the Vary header only fragment the cache. See CORSOptions.PublicPrefixes for the full semantics and the caveat about paths that read credentials. Prefixes are matched literally, so pass "/public/" rather than "/public".

func APIServerTLS added in v0.23.6

func APIServerTLS(addr string, certFile string, keyFile string) APIServerOption

func APIServerVersion added in v0.26.0

func APIServerVersion(version string) APIServerOption

APIServerVersion sets the application version string reported by the /version endpoint. If not provided, the version falls back to debug.BuildInfo.Main.Version (which is "(devel)" for plain `go build`).

type APIServiceHandler added in v0.14.0

type APIServiceHandler interface {
	http.Handler

	PathPrefix() string
}

APIServiceHandler is implemented by the generated Twirp service handlers. It is a http.Handler that also reports the path prefix the service should be mounted on.

type ApplicationInfo added in v0.26.0

type ApplicationInfo struct {
	Name        string `json:"name"`
	Version     string `json:"version"`
	VCSRevision string `json:"vcs_revision,omitempty"`
	VCSTime     string `json:"vcs_time,omitempty"`
	VCSModified bool   `json:"vcs_modified,omitempty"`
}

ApplicationInfo describes the running application binary.

type AuthInfo added in v0.6.0

type AuthInfo = auth.Info

AuthInfo is used to add authentication information to a request context. It is the same type as rpc.AuthInfo.

func GetAuthInfo added in v0.6.0

func GetAuthInfo(ctx context.Context) (*AuthInfo, bool)

GetAuthInfo returns the authentication information for the given context.

func RequireAnyScope deprecated added in v0.14.0

func RequireAnyScope(ctx context.Context, scopes ...string) (*AuthInfo, error)

RequireAnyScope checks that the authenticated caller carries one of the named scopes. On success it returns the AuthInfo from the context; on failure it returns a twirp error suitable for direct return from an RPC handler. An anonymous caller (no AuthInfo, or an empty subject) yields Unauthenticated; an authenticated caller without any of the required scopes yields PermissionDenied with the accepted scope list in the error meta under "required_any_of_scopes".

Scopes are OR-ed: passing more than one means the caller may hold any of them.

Deprecated: use github.com/ttab/elephantine/rpc.RequireAnyScope, which returns the same check's result as a *connect.Error. This function is that one with rpc.ToTwirp applied to the error, and goes away with the last Twirp mount in the fleet.

type AuthInfoParser added in v0.12.0

type AuthInfoParser = auth.Parser

AuthInfoParser validates bearer tokens and turns them into AuthInfo. See JWTAuthInfoParser for the standard JWT-based implementation.

type AuthenticationConfig added in v0.13.0

type AuthenticationConfig struct {
	OIDCConfig  *OpenIDConnectConfig
	TokenSource oauth2.TokenSource
	AuthParser  *JWTAuthInfoParser
	// contains filtered or unexported fields
}

AuthenticationConfig bundles the resolved OIDC configuration, an optional client-credentials token source, and a JWT auth info parser. Create it with AuthenticationConfigFromCLI or AuthenticationConfigFromSettings.

func AuthenticationConfigFromCLI added in v0.13.0

func AuthenticationConfigFromCLI(
	ctx context.Context, cmd *cli.Command, scopes []string,
) (*AuthenticationConfig, error)

func AuthenticationConfigFromSettings added in v0.20.4

func AuthenticationConfigFromSettings(
	ctx context.Context, settings AuthenticationSettings, scopes []string,
) (*AuthenticationConfig, error)

func (*AuthenticationConfig) NewTokenSource added in v0.13.6

func (conf *AuthenticationConfig) NewTokenSource(
	ctx context.Context, scopes []string,
) (oauth2.TokenSource, error)

type AuthenticationSettings added in v0.20.4

type AuthenticationSettings struct {
	OIDCConfig   string
	Audience     string
	ScopePrefix  string
	ClientID     string
	ClientSecret string
}

AuthenticationSettings holds the raw inputs used to build an AuthenticationConfig: the OIDC configuration URL, the expected JWT audience and scope prefix, and the client credentials used to mint tokens.

type BackoffFunction added in v0.14.0

type BackoffFunction func(retry int) time.Duration

BackoffFunction returns how long to wait before the given retry attempt. It is used by ErrGroup.GoWithRetries; see StaticBackoff for a constant-delay implementation.

func StaticBackoff added in v0.14.0

func StaticBackoff(wait time.Duration) BackoffFunction

type BuildInfo added in v0.26.0

type BuildInfo struct {
	Application ApplicationInfo   `json:"application"`
	Modules     map[string]string `json:"modules"`
}

BuildInfo is the payload returned by the /version endpoint.

type CORSOptions added in v0.11.0

type CORSOptions struct {
	AllowInsecure          bool
	AllowInsecureLocalhost bool
	Hosts                  []string
	HostPatterns           []string
	AllowedMethods         []string
	AllowedHeaders         []string
	MaxAgeSeconds          int

	// PublicPrefixes are request path prefixes that are open to any origin.
	// A request whose path starts with one of them is answered with
	// "Access-Control-Allow-Origin: *" and no "Vary: Origin", and a
	// preflight for it is allowed whatever the Origin header says. Requests
	// to any other path are unaffected and go through the Hosts and
	// HostPatterns checks as before.
	//
	// This is for anonymous read surfaces that sit behind a CDN: a response
	// that varies on Origin is cached once per embedding site, and an
	// origin allowlist is meaningless for content anyone may fetch without
	// credentials anyway. Never mark a path that reads the caller's
	// authorization or cookies as public — the browser will not send
	// credentials to a wildcard origin, but a shared cache in front of the
	// service would still be free to serve one caller's response to
	// another.
	//
	// Prefixes are matched literally with strings.HasPrefix, so include the
	// trailing slash ("/public/") unless you mean to cover every path that
	// merely starts with the string.
	PublicPrefixes []string
}

CORSOptions configures the CORS middleware and the AllowOrigin check: which origins are allowed, which methods and headers are permitted, and how long preflight responses may be cached.

func (CORSOptions) AllowOrigin added in v0.26.3

func (opts CORSOptions) AllowOrigin(origin string) bool

AllowOrigin reports whether the given Origin header value is accepted under these options. Exposed so that non-CORS code paths (notably WebSocket upgrades) can validate Origin with the same rules as the CORS middleware:

  • Origin is parsed and only its hostname is considered (port stripped).
  • The scheme must be https unless AllowInsecure is set, or the hostname is "localhost" and AllowInsecureLocalhost is set.
  • Hosts entries match the hostname exactly or as a parent domain (entry "tt.se" matches "tt.se" and "foo.tt.se").
  • HostPatterns entries are go-glob patterns matched against the hostname.

type CertificateSource added in v0.25.0

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

CertificateSource manages a TLS certificate that is automatically reloaded when the underlying files change on disk. It polls the certificate and key files for modification time changes and reloads after a settle delay to avoid loading partially written files.

func NewCertificateSource added in v0.25.0

func NewCertificateSource(
	logger *slog.Logger, certFile string, keyFile string,
	opts ...CertificateSourceOption,
) (*CertificateSource, error)

NewCertificateSource creates a CertificateSource that loads the initial certificate and key pair from the given files. It returns an error if the initial load fails.

func (*CertificateSource) GetCertificate added in v0.25.0

func (cs *CertificateSource) GetCertificate(
	_ *tls.ClientHelloInfo,
) (*tls.Certificate, error)

GetCertificate returns the current TLS certificate. It is intended to be used as the tls.Config.GetCertificate callback.

func (*CertificateSource) Run added in v0.25.0

func (cs *CertificateSource) Run(ctx context.Context) error

Run polls the certificate and key files for changes and reloads the certificate after the settle delay. It returns nil when the context is cancelled.

type CertificateSourceOption added in v0.25.0

type CertificateSourceOption func(*CertificateSource)

CertificateSourceOption configures a CertificateSource.

func CertSourcePollInterval added in v0.25.0

func CertSourcePollInterval(d time.Duration) CertificateSourceOption

CertSourcePollInterval overrides the default poll interval (5s).

func CertSourceSettleDelay added in v0.25.0

func CertSourceSettleDelay(d time.Duration) CertificateSourceOption

CertSourceSettleDelay overrides the default settle delay (10s).

type Cleaner added in v0.17.0

type Cleaner interface {
	Cleanup(fn func())
}

Cleaner is the subset of testing.TB used to register cleanup callbacks, satisfied by *testing.T. NewTestAPIServer uses it to tear down the test server when the test finishes.

type ErrGroup added in v0.14.0

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

ErrGroup is meant to be used when we run "top level" subsystems in a service. If a task panics it will be handled as a ErrPanicRecovered error.

func NewErrGroup added in v0.14.0

func NewErrGroup(
	ctx context.Context, logger *slog.Logger, opts ...ErrGroupOption,
) *ErrGroup

func (*ErrGroup) Go added in v0.14.0

func (eg *ErrGroup) Go(task string, fn func(ctx context.Context) error)

func (*ErrGroup) GoWithRetries added in v0.14.0

func (eg *ErrGroup) GoWithRetries(
	task string,
	maxRetries int,
	backoff BackoffFunction,
	resetAfter time.Duration,
	fn func(ctx context.Context) error,
)

GoWithRetries runs a task in a retry loop. The retry counter will reset to zero if more time than `resetAfter` has passed since the last error. This is used to avoid creeping up on a retry limit over long periods of time.

func (*ErrGroup) Required added in v0.27.0

func (eg *ErrGroup) Required(task string, fn func(ctx context.Context) error)

Required runs a task that the rest of the group depends on. Unlike Go, the group context is cancelled as soon as the task returns — even if it returns a nil error — which stops the sibling tasks and unblocks Wait.

Use it for subsystems that must run for the entire lifetime of the service: if one exits for any reason we want the whole service to stop and be restarted, rather than linger with only a subset of its subsystems running. A nil return still yields a nil Wait result unless a sibling reports an error, so a clean shutdown stays clean.

A task that is disabled by configuration can return ErrTaskDisabled to opt out: the group is then left untouched, as if the task had never been registered.

func (*ErrGroup) Wait added in v0.14.0

func (eg *ErrGroup) Wait() error

type ErrGroupOption added in v0.27.3

type ErrGroupOption func(o *errGroupOptions)

ErrGroupOption customises the behaviour of an ErrGroup.

func WithErrGroupMetricsRegisterer added in v0.27.3

func WithErrGroupMetricsRegisterer(reg prometheus.Registerer) ErrGroupOption

WithErrGroupMetricsRegisterer overrides the registerer used for the task metrics. Defaults to prometheus.DefaultRegisterer.

type ErrPanicRecovered added in v0.22.0

type ErrPanicRecovered struct {
	PanicValue any
}

ErrPanicRecovered is the error a task fails with when CallWithRecover recovers a panic. PanicValue is the value that was passed to panic().

func (ErrPanicRecovered) Error added in v0.22.0

func (err ErrPanicRecovered) Error() string

type GracefulShutdown added in v0.4.0

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

GracefulShutdown is a helper that can be used to listen for SIGINT and SIGTERM to gracefully shut down your application.

SIGTERM will trigger a stop, followed by quit after the specified timeout. SIGINT will trigger a immediate quit.

func NewGracefulShutdown added in v0.4.0

func NewGracefulShutdown(logger *slog.Logger, timeout time.Duration) *GracefulShutdown

NewGracefulShutdown creates a new GracefulShutdown that will wait for `timeout` between "stop" and "quit".

func NewManualGracefulShutdown added in v0.9.1

func NewManualGracefulShutdown(logger *slog.Logger, timeout time.Duration) *GracefulShutdown

NewManualGracefulShutdown creates a GracefulShutdown instance that doesn't listen to OS signals.

func (*GracefulShutdown) CancelOnQuit added in v0.4.0

func (gs *GracefulShutdown) CancelOnQuit(ctx context.Context) context.Context

CancelOnQuit returns a child context that will be cancelled when quit is triggered.

func (*GracefulShutdown) CancelOnStop added in v0.4.0

func (gs *GracefulShutdown) CancelOnStop(ctx context.Context) context.Context

CancelOnStop returns a child context that will be cancelled when stop is triggered.

func (*GracefulShutdown) ShouldQuit added in v0.4.0

func (gs *GracefulShutdown) ShouldQuit() <-chan struct{}

ShouldQuit returns a channel that will be closed when quit is triggered.

func (*GracefulShutdown) ShouldStop added in v0.4.0

func (gs *GracefulShutdown) ShouldStop() <-chan struct{}

ShouldStop returns a channel that will be closed when stop is triggered.

func (*GracefulShutdown) Stop added in v0.4.0

func (gs *GracefulShutdown) Stop()

Stop triggers a stop, which will trigger quit after the configured timeout.

type HTTPClientInstrumentation added in v0.4.0

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

HTTPClientInstrumentation provides a way to instrument HTTP clients.

func NewHTTPClientIntrumentation added in v0.4.0

func NewHTTPClientIntrumentation(
	registerer prometheus.Registerer,
) (*HTTPClientInstrumentation, error)

NewHTTPClientIntrumentation registers a set of HTTP client metrics with the provided registerer.

func (*HTTPClientInstrumentation) Client added in v0.4.0

func (ci *HTTPClientInstrumentation) Client(name string, client *http.Client) error

Client instruments the HTTP client transport with the standard promhttp metrics. The client_requests_total, client_in_flight_requests, and client_request_duration_seconds metrics will be labelled with the client name.

type HTTPClientOption added in v0.21.0

type HTTPClientOption func(opts *HTTPClientOptions)

HTTPClientOption customises a http.Client built by NewHTTPClient.

func DialTimeout added in v0.21.0

func DialTimeout(d time.Duration) HTTPClientOption

func IdleConnections added in v0.21.0

func IdleConnections(
	maxIdle int,
	maxIdlePerHost int,
	idleConnTimeout time.Duration,
) HTTPClientOption

func LongpollClient added in v0.21.0

func LongpollClient() HTTPClientOption

LongpollClient is syntactic sugar for setting the response header timeout to 0 (no timeout), can be used to communicate intent.

func MaxConnectionsPerHost added in v0.21.0

func MaxConnectionsPerHost(n int) HTTPClientOption

func ResponseHeaderTimeout added in v0.21.0

func ResponseHeaderTimeout(d time.Duration) HTTPClientOption

func TLSHandshakeTimeout added in v0.21.0

func TLSHandshakeTimeout(d time.Duration) HTTPClientOption

func WithTokenSource added in v0.21.0

func WithTokenSource(source oauth2.TokenSource) HTTPClientOption

Wraps the client transport with an oauth2.Transport.

type HTTPClientOptions added in v0.21.0

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

HTTPClientOptions holds the client, transport, and dialer that HTTPClientOption functions modify when building a client with NewHTTPClient.

type HTTPError

type HTTPError struct {
	Status     string
	StatusCode int
	Header     http.Header
	Body       io.Reader
}

HTTPError can be used to describe a non-OK response. Either as an error value in a client that got an error response from a server, or in a server implementation to communicate what the error response to a client should be.

func HTTPErrorf

func HTTPErrorf(statusCode int, format string, a ...any) *HTTPError

HTTPErrorf creates a HTTPError using a format string.

func NewHTTPError

func NewHTTPError(statusCode int, message string) *HTTPError

NewHTTPError creates a new HTTPError with the given status code and response message.

func (*HTTPError) Error

func (e *HTTPError) Error() string

Error implements the error interface.

type HealthServer

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

HealthServer exposes health endpoints, metrics, and PPROF endpoints.

A HealthServer should never be publicly exposed, as that both could expose sensitive information and could be used to DDOS your application.

Example output for a request to `GET /health/ready`:

{
  "api_liveness": {
    "ok": false,
    "error": "api liveness endpoint returned non-ok status: 404 Not Found"
  },
  "postgres": {
    "ok": true
  },
  "s3": {
    "ok": true
  }
}

func NewHealthServer

func NewHealthServer(
	logger *slog.Logger, addr string, opts ...HealthServerOption,
) *HealthServer

NewHealthServer creates a new health server that will listen to the provided address. Pass an empty addr to construct a no-op server: the readiness machinery still works (useful for tests and for processes that share a health endpoint with another listener), but ListenAndServe does not bind any socket.

func NewTestHealthServer added in v0.9.3

func NewTestHealthServer(
	logger *slog.Logger, opts ...HealthServerOption,
) *HealthServer

func (*HealthServer) AddOptionalReadyFunction added in v0.26.2

func (s *HealthServer) AddOptionalReadyFunction(name string, fn ReadyFunc)

AddOptionalReadyFunction adds a function that will be called when a client requests "/health/ready". A non-nil error from the function will be reported in the response body with "ok": false but will not cause "/health/ready" to respond with 500.

func (*HealthServer) AddReadyFunction

func (s *HealthServer) AddReadyFunction(name string, fn ReadyFunc)

AddReadyFunction adds a function that will be called when a client requests "/health/ready". A non-nil error from the function will cause "/health/ready" to respond with 500.

func (*HealthServer) Addr added in v0.17.0

func (s *HealthServer) Addr() string

func (*HealthServer) Close

func (s *HealthServer) Close() error

Close stops the health server.

func (*HealthServer) ListenAndServe

func (s *HealthServer) ListenAndServe(ctx context.Context) error

ListenAndServe starts the health server, shutting it down if the context gets cancelled.

type HealthServerOption added in v0.26.2

type HealthServerOption func(*healthServerOptions)

HealthServerOption configures a HealthServer.

func WithHealthServerRegisterer added in v0.26.2

func WithHealthServerRegisterer(reg prometheus.Registerer) HealthServerOption

WithHealthServerRegisterer sets the prometheus registerer used for the readiness check gauge. Pass nil to disable metric registration.

type JWTAuthInfoParser added in v0.16.0

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

JWTAuthInfoParser is the standard AuthInfoParser implementation. It validates JWTs using the configured key function, caches successful results until the token expires, and optionally strips a scope prefix.

func NewJWKSAuthInfoParser added in v0.12.0

func NewJWKSAuthInfoParser(
	ctx context.Context, jwksURL string, opts JWTAuthInfoParserOptions,
) (*JWTAuthInfoParser, error)

func NewJWTAuthInfoParser added in v0.20.2

func NewJWTAuthInfoParser(
	ctx context.Context,
	keyfunc jwt.Keyfunc,
	opts JWTAuthInfoParserOptions,
) *JWTAuthInfoParser

func NewStaticAuthInfoParser added in v0.12.0

func NewStaticAuthInfoParser(
	ctx context.Context, key ecdsa.PublicKey, opts JWTAuthInfoParserOptions,
) *JWTAuthInfoParser

func (*JWTAuthInfoParser) AuthInfoFromHeader added in v0.16.0

func (p *JWTAuthInfoParser) AuthInfoFromHeader(authorization string) (*AuthInfo, error)

func (*JWTAuthInfoParser) AuthInfoFromToken added in v0.17.9

func (p *JWTAuthInfoParser) AuthInfoFromToken(token string) (*AuthInfo, error)

func (*JWTAuthInfoParser) Valid added in v0.16.0

func (p *JWTAuthInfoParser) Valid(c jwt.Claims) error

Valid validates the jwt.RegisteredClaims.

func (*JWTAuthInfoParser) ValidateTokenWithClaims added in v0.17.9

func (p *JWTAuthInfoParser) ValidateTokenWithClaims(token string, claims jwt.Claims) (*jwt.Token, error)

type JWTAuthInfoParserOptions added in v0.16.0

type JWTAuthInfoParserOptions struct {
	Audience    string
	Issuer      string
	ScopePrefix string
}

JWTAuthInfoParserOptions configures a JWTAuthInfoParser: the expected audience and issuer to validate against, and an optional scope prefix to strip from token scopes.

type JWTClaims added in v0.6.0

type JWTClaims = auth.JWTClaims

JWTClaims defines the claims that the elephant services understand.

It is an alias of the type in the internal auth package, which is where it has to live for the rpc package to be able to use it without importing this one. It is the same type as rpc.JWTClaims.

type ListenAndServeOption added in v0.23.6

type ListenAndServeOption func(s *http.Server, o *ListenAndServeOptions)

ListenAndServeOption customises the behaviour of ListenAndServeContext, for example to enable TLS through ListenAndServeTLS.

func ListenAndServeTLS added in v0.23.6

func ListenAndServeTLS(
	logger *slog.Logger, certFile string, keyFile string,
	opts ...CertificateSourceOption,
) ListenAndServeOption

ListenAndServeTLS configures the server to use TLS with automatic certificate reloading. The certificate and key files are polled for changes, and the TLS certificate is reloaded after a settle delay.

type ListenAndServeOptions added in v0.23.6

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

ListenAndServeOptions holds the internal configuration that ListenAndServeOption functions modify.

type MetricsHelper added in v0.23.2

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

MetricsHelper reduces the boilerplate of registering Prometheus collectors. Each method creates a collector, registers it, and assigns it through the provided pointer. The first registration error is captured and short circuits later calls, so a batch of registrations can be followed by a single Err check.

func NewMetricsHelper added in v0.23.2

func NewMetricsHelper(reg prometheus.Registerer) *MetricsHelper

func (*MetricsHelper) Collector added in v0.27.3

func (h *MetricsHelper) Collector(name string, c prometheus.Collector)

Collector registers a custom collector. The name is only used for error reporting.

func (*MetricsHelper) Counter added in v0.23.2

func (h *MetricsHelper) Counter(
	o *prometheus.Counter,
	opts prometheus.CounterOpts,
)

func (*MetricsHelper) CounterVec added in v0.23.2

func (h *MetricsHelper) CounterVec(
	o **prometheus.CounterVec,
	opts prometheus.CounterOpts,
	labels []string,
)

func (*MetricsHelper) Err added in v0.23.2

func (h *MetricsHelper) Err() error

func (*MetricsHelper) Gauge added in v0.23.2

func (h *MetricsHelper) Gauge(
	o *prometheus.Gauge,
	opts prometheus.GaugeOpts,
)

func (*MetricsHelper) GaugeVec added in v0.23.2

func (h *MetricsHelper) GaugeVec(
	o **prometheus.GaugeVec,
	opts prometheus.GaugeOpts,
	labels []string,
)

func (*MetricsHelper) Histogram added in v0.23.3

func (h *MetricsHelper) Histogram(
	o *prometheus.Histogram,
	opts prometheus.HistogramOpts,
)

func (*MetricsHelper) HistogramVec added in v0.23.3

func (h *MetricsHelper) HistogramVec(
	o **prometheus.HistogramVec,
	opts prometheus.HistogramOpts,
	labels []string,
)

type OpenIDConnectConfig added in v0.13.0

type OpenIDConnectConfig struct {
	Issuer                                                    string            `json:"issuer"`
	AuthorizationEndpoint                                     string            `json:"authorization_endpoint"`
	TokenEndpoint                                             string            `json:"token_endpoint"`
	IntrospectionEndpoint                                     string            `json:"introspection_endpoint"`
	UserinfoEndpoint                                          string            `json:"userinfo_endpoint"`
	EndSessionEndpoint                                        string            `json:"end_session_endpoint"`
	FrontchannelLogoutSessionSupported                        bool              `json:"frontchannel_logout_session_supported"`
	FrontchannelLogoutSupported                               bool              `json:"frontchannel_logout_supported"`
	JwksURI                                                   string            `json:"jwks_uri"`
	CheckSessionIframe                                        string            `json:"check_session_iframe"`
	GrantTypesSupported                                       []string          `json:"grant_types_supported"`
	AcrValuesSupported                                        []string          `json:"acr_values_supported"`
	ResponseTypesSupported                                    []string          `json:"response_types_supported"`
	SubjectTypesSupported                                     []string          `json:"subject_types_supported"`
	IDTokenSigningAlgValuesSupported                          []string          `json:"id_token_signing_alg_values_supported"`
	IDTokenEncryptionAlgValuesSupported                       []string          `json:"id_token_encryption_alg_values_supported"`
	IDTokenEncryptionEncValuesSupported                       []string          `json:"id_token_encryption_enc_values_supported"`
	UserinfoSigningAlgValuesSupported                         []string          `json:"userinfo_signing_alg_values_supported"`
	UserinfoEncryptionAlgValuesSupported                      []string          `json:"userinfo_encryption_alg_values_supported"`
	UserinfoEncryptionEncValuesSupported                      []string          `json:"userinfo_encryption_enc_values_supported"`
	RequestObjectSigningAlgValuesSupported                    []string          `json:"request_object_signing_alg_values_supported"`
	RequestObjectEncryptionAlgValuesSupported                 []string          `json:"request_object_encryption_alg_values_supported"`
	RequestObjectEncryptionEncValuesSupported                 []string          `json:"request_object_encryption_enc_values_supported"`
	ResponseModesSupported                                    []string          `json:"response_modes_supported"`
	RegistrationEndpoint                                      string            `json:"registration_endpoint"`
	TokenEndpointAuthMethodsSupported                         []string          `json:"token_endpoint_auth_methods_supported"`
	TokenEndpointAuthSigningAlgValuesSupported                []string          `json:"token_endpoint_auth_signing_alg_values_supported"`
	IntrospectionEndpointAuthMethodsSupported                 []string          `json:"introspection_endpoint_auth_methods_supported"`
	IntrospectionEndpointAuthSigningAlgValuesSupported        []string          `json:"introspection_endpoint_auth_signing_alg_values_supported"`
	AuthorizationSigningAlgValuesSupported                    []string          `json:"authorization_signing_alg_values_supported"`
	AuthorizationEncryptionAlgValuesSupported                 []string          `json:"authorization_encryption_alg_values_supported"`
	AuthorizationEncryptionEncValuesSupported                 []string          `json:"authorization_encryption_enc_values_supported"`
	ClaimsSupported                                           []string          `json:"claims_supported"`
	ClaimTypesSupported                                       []string          `json:"claim_types_supported"`
	ClaimsParameterSupported                                  bool              `json:"claims_parameter_supported"`
	ScopesSupported                                           []string          `json:"scopes_supported"`
	RequestParameterSupported                                 bool              `json:"request_parameter_supported"`
	RequestURIParameterSupported                              bool              `json:"request_uri_parameter_supported"`
	RequireRequestURIRegistration                             bool              `json:"require_request_uri_registration"`
	CodeChallengeMethodsSupported                             []string          `json:"code_challenge_methods_supported"`
	TLSClientCertificateBoundAccessTokens                     bool              `json:"tls_client_certificate_bound_access_tokens"`
	RevocationEndpoint                                        string            `json:"revocation_endpoint"`
	RevocationEndpointAuthMethodsSupported                    []string          `json:"revocation_endpoint_auth_methods_supported"`
	RevocationEndpointAuthSigningAlgValuesSupported           []string          `json:"revocation_endpoint_auth_signing_alg_values_supported"`
	BackchannelLogoutSupported                                bool              `json:"backchannel_logout_supported"`
	BackchannelLogoutSessionSupported                         bool              `json:"backchannel_logout_session_supported"`
	DeviceAuthorizationEndpoint                               string            `json:"device_authorization_endpoint"`
	BackchannelTokenDeliveryModesSupported                    []string          `json:"backchannel_token_delivery_modes_supported"`
	BackchannelAuthenticationEndpoint                         string            `json:"backchannel_authentication_endpoint"`
	BackchannelAuthenticationRequestSigningAlgValuesSupported []string          `json:"backchannel_authentication_request_signing_alg_values_supported"`
	RequirePushedAuthorizationRequests                        bool              `json:"require_pushed_authorization_requests"`
	PushedAuthorizationRequestEndpoint                        string            `json:"pushed_authorization_request_endpoint"`
	MtlsEndpointAliases                                       map[string]string `json:"mtls_endpoint_aliases"`
	AuthorizationResponseIssParameterSupported                bool              `json:"authorization_response_iss_parameter_supported"`
}

OpenIDConnectConfig mirrors the OpenID Connect provider metadata document (the response from the issuer's ".well-known/openid-configuration" endpoint). Only a subset of these fields is used by elephantine; the rest are decoded for completeness.

func OpenIDConnectConfigFromURL added in v0.13.0

func OpenIDConnectConfigFromURL(
	wellKnown string,
) (*OpenIDConnectConfig, error)

type ReadyFunc

type ReadyFunc func(ctx context.Context) error

ReadyFunc is a function that will be called to determine if a service is ready to receive traffic. It should return a descriptive error that helps with debugging if the underlying check fails.

func LivenessReadyCheck added in v0.6.3

func LivenessReadyCheck(endpoint string) ReadyFunc

LivenessReadyCheck returns a ReadyFunc that verifies that an endpoint answers to GET requests with 200 OK.

type ServiceAuth added in v0.16.0

type ServiceAuth bool

ServiceAuth is used to control behaviour when an unauthorized client makes a call to the service.

const (
	// ServiceAuthRequired respond with an unauthenticated error for
	// unauthorized calls.
	ServiceAuthRequired ServiceAuth = true
	// ServiceAuthOptional allow unauthorized calls, invalid authorizations
	// will still result in an error, but calls missing authorization will
	// be let through to the service implementation.
	ServiceAuthOptional ServiceAuth = false
)

type ServiceOptions added in v0.14.0

type ServiceOptions struct {
	Hooks          *twirp.ServerHooks
	Interceptors   []connect.Interceptor
	AuthMiddleware func(
		w http.ResponseWriter, r *http.Request, next http.Handler,
	) error

	// JSONSkipDefaults configures JSON serialization to skip unpopulated or
	// default values in JSON responses, which results in smaller responses
	// that are easier to read if your messages contain lots of fields that
	// may have their default/zero value.
	//
	// It only affects the Twirp mount. Connect's JSON codec is protojson
	// with its default options, which also omits unpopulated fields.
	//
	// Note that the two stacks do not spell field names the same way:
	// Twirp's JSON uses the proto names (document_uuid) and Connect's the
	// protojson default of lowerCamelCase (documentUuid). Both accept
	// either spelling in a request.
	JSONSkipDefaults bool
	// contains filtered or unexported fields
}

ServiceOptions carries the Twirp server hooks, the Connect interceptors and the authentication middleware applied to the API services registered with an APIServer. Use NewDefaultServiceOptions for the standard setup, or compose it manually with the Add*/Set* methods, and apply it to a Twirp server with ServerOptions and to a Connect handler with HandlerOptions.

func NewDefaultServiceOptions added in v0.14.0

func NewDefaultServiceOptions(
	logger *slog.Logger,
	parser AuthInfoParser,
	reg prometheus.Registerer,
	requireAuth ServiceAuth,
) (ServiceOptions, error)

NewDefaultServiceOptions sets up the standard options for our RPC services. This sets up authentication, logging and metrics for both stacks: apply the options to a Twirp server with the ServerOptions() method, and to a Connect handler with HandlerOptions().

func (*ServiceOptions) AddLoggingHooks added in v0.14.0

func (so *ServiceOptions) AddLoggingHooks(
	logger *slog.Logger,
)

func (*ServiceOptions) AddMetricsHooks added in v0.15.0

func (so *ServiceOptions) AddMetricsHooks(
	reg prometheus.Registerer, opts ...TwirpMetricOptionFunc,
) error

AddMetricsHooks adds the RPC metrics to both stacks. The options are the ones that configure the Twirp hooks; the customer function and the test latency are passed on to the Connect interceptor so that the two report the same label values, and WithTwirpMetricsRegisterer overrides the registerer for both.

func (*ServiceOptions) HandlerOptions added in v0.29.0

func (so *ServiceOptions) HandlerOptions() []connect.HandlerOption

HandlerOptions returns the options that configure a Connect handler according to the set service options. It is the Connect counterpart of ServerOptions, and is passed to the generated New<Service>ServiceHandler constructor.

func (*ServiceOptions) ServerOptions added in v0.16.0

func (so *ServiceOptions) ServerOptions() twirp.ServerOption

ServerOptions returns a ServerOptions function that configures the twirp server according to the set service options.

The Twirp error interceptor is always installed, so that a handler that has been moved to the Connect error vocabulary answers a Twirp caller with the code, message and meta it always has. It is a no-op for a handler that still returns Twirp errors.

func (*ServiceOptions) SetAuthInfoValidation added in v0.16.0

func (so *ServiceOptions) SetAuthInfoValidation(
	parser AuthInfoParser, requireAuth ServiceAuth,
)

SetAuthInfoValidation makes the service authenticate its callers with the parser.

Authentication is protocol neutral HTTP middleware: it parses the Authorization header, puts the resulting AuthInfo on the request context, where both stacks read it with GetAuthInfo, and answers a request it could not authenticate itself, with an unauthenticated error rendered in the protocol the caller is speaking. Connect, gRPC and gRPC-Web errors are rendered by connect.ErrorWriter and Twirp errors by twirp.WriteError, so the caller sees the error body its own client parses.

Both a missing and an invalid authorization are unauthenticated: the caller could not be identified either way. ServiceAuthOptional lets a request without an Authorization header through as an anonymous caller; an authorization the parser rejects always fails.

The Twirp hook and the Connect interceptor installed here are the safety net for a mount that does not run the middleware: they refuse a call that reaches a handler with no authenticated caller on its context when the service requires authentication, rather than letting it run unauthenticated.

type TwirpMetricOptionFunc added in v0.4.0

type TwirpMetricOptionFunc func(opts *TwirpMetricsOptions)

TwirpMetricOptionFunc configures the metrics hooks created by NewTwirpMetricsHooks.

func WithTwirpMetricsCustomerFunc added in v0.9.5

func WithTwirpMetricsCustomerFunc(fn func(ctx context.Context) string) TwirpMetricOptionFunc

WithTwirpMetricsCustomerFunc sets a function that can be used to return the customer label value for a context.

func WithTwirpMetricsRegisterer added in v0.4.0

func WithTwirpMetricsRegisterer(reg prometheus.Registerer) TwirpMetricOptionFunc

WithTwirpMetricsRegisterer uses a custom registerer for Twirp metrics.

func WithTwirpMetricsStaticTestLatency added in v0.4.0

func WithTwirpMetricsStaticTestLatency(latency time.Duration) TwirpMetricOptionFunc

WithTwirpMetricsStaticTestLatency configures the RPC metrics to report a static duration.

type TwirpMetricsOptions added in v0.4.0

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

TwirpMetricsOptions holds the configuration for the Twirp metrics hooks created by NewTwirpMetricsHooks. Set it through TwirpMetricOptionFunc options.

type Vault added in v0.9.0

type Vault struct {
	Client *vault.Client
	// contains filtered or unexported fields
}

Vault is a helper for setting up a Vault client, also implements ParameterSource.

func NewVault added in v0.9.0

func NewVault() (*Vault, error)

NewVault creates a vault client that can be used as a ParameterSource.

func (*Vault) GetParameterValue added in v0.9.0

func (v *Vault) GetParameterValue(ctx context.Context, name string) (string, error)

GetParameterValue implements ParameterSource.

func (*Vault) KeepAlive added in v0.9.0

func (v *Vault) KeepAlive() error

KeepAlive is used to keep the lease on the vault login active, not necessary if you're just reading secrets on startup. Returns an error if the lease is lost or fails to renew. Returns immediately without an error if a token was used to authenticate directly with vault.

func (*Vault) Stop added in v0.9.0

func (v *Vault) Stop()

Stop the keepalive loop.

Directories

Path Synopsis
cmd
listen-test command
protoc-gen-elephant-rpc command
protoc-gen-elephant-rpc is a protobuf compiler plugin that keeps the plain service interface alive on top of Connect.
protoc-gen-elephant-rpc is a protobuf compiler plugin that keeps the plain service interface alive on top of Connect.
internal
auth
Package auth holds the authentication types and the request-scoped authentication state that both the elephantine root package and the rpc package need.
Package auth holds the authentication types and the request-scoped authentication state that both the elephantine root package and the rpc package need.
logmeta
Package logmeta holds the request-scoped log metadata map.
Package logmeta holds the request-scoped log metadata map.
protogen
Package protogen holds the parts of this repository's protobuf generation that are code rather than magefile: the module protoc-gen-twirp is run out of, and the environment every generator is invoked with.
Package protogen holds the parts of this repository's protobuf generation that are code rather than magefile: the module protoc-gen-twirp is run out of, and the environment every generator is invoked with.
rpcmetrics
Package rpcmetrics declares the RPC server metrics.
Package rpcmetrics declares the RPC server metrics.
pg
joblock
Package joblock coordinates which instance of a service runs a background task, using a row in the job_lock table as the lock.
Package joblock coordinates which instance of a service runs a background task, using a row in the job_lock table as the lock.
Package rpc is the protocol-neutral RPC vocabulary for elephant services: error construction and inspection, the translation between Connect and Twirp errors, and the server and client interceptors that give a Connect mount the same authorization, logging and metrics behaviour the Twirp hooks give a Twirp mount.
Package rpc is the protocol-neutral RPC vocabulary for elephant services: error construction and inspection, the translation between Connect and Twirp errors, and the server and client interceptors that give a Connect mount the same authorization, logging and metrics behaviour the Twirp hooks give a Twirp mount.

Jump to

Keyboard shortcuts

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