github.com/alexfalkowski/go-service/v2 is an opinionated framework/library for building Go services with consistent wiring for configuration, DI, transports, telemetry, crypto, etc.
This repo is primarily a library of packages (no top-level cmd/ binary). Services built on top typically define their own main package elsewhere and import this module.
Long-running services are expected to start from go-service-template, while short-lived client commands start from go-client-template. Both compose the high-level module bundles from this repository. These are the primary supported paths. Lower-level package-by-package composition is still available, but it is an advanced mode and may require extra manual registration.
For a new long-running service, start from go-service-template so the application main, server command wiring, configuration fixtures, and standard module composition are generated together. For a short-lived control, migration, or batch command, start from go-client-template; it demonstrates cli.Application.AddClient, module.Client, and lifecycle OnStart command work.
For direct package use in an existing module, add the library dependency with the versioned module path:
go get github.com/alexfalkowski/go-service/v2Use the Go version declared in go.mod or newer when installing or building this module.
The framework is designed around dependency injection and uses Uber Fx (and Dig under the hood). Most subsystems expose Fx modules that you compose into your service.
If you are new to Fx, their docs/examples are worth reading first.
The module package exposes three top-level bundles:
module.Libraryfor shared foundations (env, compress, encoding, crypto, time, sync buffer-pool wiring, id)module.Serverfor server processes (Library + config, transports, telemetry, debug, health, etc.)module.Clientfor short-lived/batch/client processes (Library + config, telemetry, sql, hooks, etc.)
These bundles are the intended default for services generated from go-service-template. They handle the internal registration expected by the framework so most services do not need to wire lower-level transport or lifecycle helpers manually.
This repository is a library, so your binary is usually in another module. A typical main uses cli.Application and composes module bundles:
package main
import (
"github.com/alexfalkowski/go-service/v2/cli"
"github.com/alexfalkowski/go-service/v2/context"
"github.com/alexfalkowski/go-service/v2/module"
"github.com/alexfalkowski/go-service/v2/os"
)
func main() {
app := cli.NewApplication(func(commander cli.Commander) {
serve := commander.AddServer("serve", "Run the service", module.Server)
serve.AddConfig("file:./config.yaml") // adds the `-config` / `-c` config flag with this default
})
os.Exit(app.RunCode(context.Background()))
}The file:./config.yaml default above expects a non-empty config file. A minimal
server config can start with the environment plus one enabled transport:
environment: development
transport:
http:
address: tcp://localhost:8000
timeout: 10sUse app.RunCode(context.Background()) from main when exiting the process. It
returns os.ExitCodeSuccess on success, returns a requested non-zero shutdown
exit code such as os.ExitCodeServeFailure, and returns os.ExitCodeFailure
for other errors. Use app.Run(context.Background()) in tests or embedding code
that needs to inspect the returned error.
Services commonly expose two command shapes:
- Server: long-running daemon process
- Client: short-lived control/admin process
The framework uses acmd. Your serviceβs main typically wires Fx modules + commands.
This repo intentionally does not ship a ready-to-run
mainβ it provides the building blocks. In normal usage server applications consume them throughgo-service-templateplusmodule.Server, while short-lived commands usego-client-templateplusmodule.Client, rather than wiring every subsystem manually.
The repo is intentionally split between high-level service composition and lower-level reusable helpers:
module/exposes the opinionated Fx bundles (Library,Server,Client)config/defines the standard top-level config shape plus projections used by module wiring- feature packages such as
cache/,crypto/,database/sql/,feature/,telemetry/,time/, andid/provide config, constructors, and Fx modules for a subsystem net/...contains lower-level protocol helpers and reusable primitives (net/http,net/grpc, metadata/header helpers, gRPC health protocol aliases, andnet/server)transport/...contains the higher-level service transport layer: composed HTTP/gRPC stacks, policy middleware, operational endpoints, and transport-specific modulesinternal/test/contains the shared test world and fixtures used across packages
As a rule of thumb: if you want protocol primitives or shared helpers, start in net/...; if you want service wiring and middleware policy, start in transport/.... Shared metadata, header, and lifecycle helpers live under net/..., including net/http/meta, net/grpc/meta, net/header, and net/server.Register.
For most service authors, the right starting point is still the high-level module bundles rather than these lower-level packages directly.
The config decoder supports:
- JSON
- HJSON (
github.com/hjson/hjson-go/v4) - TOML (
github.com/BurntSushi/toml) - YAML (
go.yaml.in/yaml/v3)
Config input is routed by flags called -config and -c:
-
file:<path>Read config from a file at<path>; parser is selected from the file extension (.json,.hjson,.yaml,.toml). -
env:<ENV_VAR>Read config from env var<ENV_VAR>. The env var value must be formatted as:"<extension>:<base64-content>"Example format:
yaml:ZW52aXJvbm1lbnQ6IGRldmVsb3BtZW50Cg==Example commands:
# Linux (GNU base64) export SERVICE_CONFIG="yaml:$(base64 -w 0 < ./config.yaml)" ./your-service serve -config env:SERVICE_CONFIG
# macOS/BSD base64 export SERVICE_CONFIG="yaml:$(base64 < ./config.yaml | tr -d '\n')" ./your-service serve -c env:SERVICE_CONFIG
HJSON works the same way, for example
hjson:<base64-content>.The repository helper
make kind=configs/config encode-configuses GNUbase64 -w 0; on macOS/BSD, usebase64 | tr -d '\n'for the equivalent single-line payload. -
Unsupported explicit
kind:locationprefixes fail startup instead of falling back to another source. -
Unprefixed values, including an empty value, fall back to default lookup, searching for:
<serviceName>.{yaml,hjson,toml,json}Default lookup checks extensions first (
.yaml,.hjson,.toml,.json), and for each extension checks:- executable directory
$XDG_CONFIG_HOME/<serviceName>/(viaos.UserConfigDir())/etc/<serviceName>/
Important
Because the user config directory is part of that search, runtimes using default lookup are expected to provide HOME or XDG_CONFIG_HOME. Services that cannot rely on those environment variables should pass an explicit -config file:<path> or -config env:<ENV_VAR> source.
At runtime, services typically decode into a struct (often embedding config.Config) and validate it using go-playground/validator.
The library provides a helper config.NewConfig[T] which:
- decodes into
*T - rejects an βemptyβ decoded value (guards against starting with a zero-value config)
- validates the decoded config
Empty detection uses zero-value semantics and supports config types containing maps, slices, or other non-comparable fields.
Example:
type WorkerConfig struct {
Queue string `yaml:"queue" json:"queue" toml:"queue" validate:"required"`
}
type AppConfig struct {
Worker *WorkerConfig `yaml:"worker" json:"worker" toml:"worker" validate:"required"`
*config.Config `yaml:",inline" json:",inline" toml:",inline" validate:"required"`
}
func loadConfig(decoder config.Decoder, validator *config.Validator) (*AppConfig, error) {
return config.NewConfig[AppConfig](decoder, validator)
}
func sharedConfig(cfg *AppConfig) *config.Config {
return cfg.Config
}
func workerConfig(cfg *AppConfig) *WorkerConfig {
return cfg.Worker
}
var AppConfigModule = di.Module(
di.Constructor(config.NewConfig[AppConfig]),
di.Decorate(sharedConfig),
di.Constructor(workerConfig),
)Compose AppConfigModule alongside module.Server or module.Client. The
decorator projects the embedded shared *config.Config into the standard graph,
so the service-specific config is decoded once while existing transport, SQL,
and telemetry projections continue to work. Add constructors like
workerConfig for service-owned sub-configs.
The canonical top-level config type is config.Config (in config/config.go). It contains:
debug,cache,crypto,feature,hooks,id,sql,telemetry,time,transport,environment
Most sub-configs are optional pointers. Conventionally, nil means disabled.
Many fields accept a source string rather than only a literal:
env:NAMEβ read from environment variableNAME(fails ifNAMEis unset; resolves to an empty value ifNAMEis explicitly set to"")file:/path/to/thingβ read from filesystem after path cleaning; returned bytes are trimmed of leading and trailing whitespace- otherwise β treat as literal string
This is used for secrets and key material (TLS keys, HMAC keys, webhook secrets, SQL DSNs, etc).
env: values and literal values are returned exactly as provided; they are not
trimmed.
Example:
hooks:
key: current
secrets:
current: env:WEBHOOK_SECRETTop-level environment is:
environment: developmentThis is an env.Environment value used to drive environment-specific behavior in services.
Compression kinds used by subsystems that support compression:
nonezstds2snappy
Encoding kinds used by subsystems that support encoding. encoding.Map registers each encoder under
exactly one canonical kind (no aliases):
jsonhjsontomlyamlmsgpackprotobufprotojsonprototextgobbytes
Note
bytesis the passthrough encoder forio.ReaderFrom/io.WriterTopayloads.- HTTP media-type aliases such as
pb,proto,protobin,pbbin,pbtxt,prototxt,pbjson,octet-stream,plain, andymlare resolved to the canonical kinds above bynet/http/content/unarybefore they ever reach this registry. See HTTP content types. encoding/stream.Mapis a separate registry for streaming (multi-value) encoding βjson,msgpack,gob,yamlβ used by HTTP streaming (NDJSON), not by this single-value registry.- Not every kind in this registry is interchangeable for HTTP request-body decoding:
msgpackandgobremain valid response codecs but are rejected as a requestContent-Type. See HTTP content types.
Cache configuration is defined in cache/config.Config:
cache:
kind: redis
compressor: zstd
encoder: json
max_size: 4MB
max_entries: 1024
options:
url: env:CACHE_URLNote
- Built-in driver kinds in this repo are
redisandttlcache. - Unknown
kindvalues returncache/driver/errors.ErrNotFound. - Unknown or empty
compressorvalues fall back tonone. - For normal values, unknown or empty
encodervalues fall back tojson. - Configured
compressorandencodervalues are part of the cache driver key namespace, so changing either setting creates cache misses for values written with the previous format. - Cache operations use
bytesforio.WriterTo/io.ReaderFromstream values andprotobuffor protobuf messages, regardless of the configuredencoder. max_sizelimits encoded cache values before compression, after compression, and after decompression. A zero value uses the default4MB.max_entrieslimits entries retained by bounded in-memory cache drivers. A zero value uses the default1024; negative values are invalid.optionsis backend-specific and decoded asmap[string]any.- Redis-backed
GetOrPersistrequires Redis 7.0-compatibleSETsemantics because atomic publication usesSET ... NX GET. - Configure each cache backend for a specific service or purpose. For Redis, use a dedicated database, endpoint, or deployment-level key namespace in the connection/configuration instead of sharing one general cache for unrelated data.
Warning
Cache.Flush follows backend semantics; for Redis it clears the selected database.
The feature.Config embeds client-side config (config/client.Config), so it supports:
addresstimeoutretrybreakerlimitertlstokenoptions
Example:
feature:
address: localhost:9000
timeout: 10s
breaker:
max_requests: 2
interval: 15s
timeout: 5s
consecutive_failures: 4
retry:
backoff: 100ms
timeout: 1s
attempts: 3
tls:
cert: file:test/certs/client-cert.pem
key: file:test/certs/client-key.pem
ca: file:test/certs/rootCA.pem
server_name: localhostNote
feature.Configembeds client config;IsEnabledis true only when both the feature config and embedded client config are present. An emptyfeature:block is treated as disabled by feature config helpers.- This repository does not construct a built-in OpenFeature provider from this config.
- Services that need a remote or custom provider should use
feature.Configin their own provider constructor and provide the resultingopenfeature.FeatureProviderin DI;feature.Moduleregisters that supplied provider with the OpenFeature SDK lifecycle.
Configured via hooks.Config:
hooks:
key: current
secrets:
current: env:WEBHOOK_SECRET_CURRENT
previous: env:WEBHOOK_SECRET_PREVIOUS
leeway: 30sEach secrets value is a source string. The resolved value must be accepted by the
Standard Webhooks library, such as a secret generated by hooks.Generator with
or without the whsec_ prefix. Empty resolved secrets fail startup.
Signing uses the active key. Verification accepts signatures from every
configured secret, trying the active secret first. Standard Webhooks includes a
message id (Webhook-Id) but not a signing key id, so go-service does not extend
the protocol with a custom selector header.
leeway is optional clock-skew tolerance applied to the Webhook-Timestamp
freshness check during verification. A zero value (the default) keeps the
Standard Webhooks library's fixed 5-minute freshness window; a non-zero value
replaces that fixed window with this configured tolerance, matching the
clock-skew leeway already exposed by the JWT, PASETO, and SSH token
verifiers. Like those, leeway is a Go duration string and must be a positive
whole-second duration.
Inbound verification checks Standard Webhooks signatures and timestamps, but
does not store or reject previously seen webhook ids. Receivers that perform
non-idempotent work should deduplicate or process idempotently using
Webhook-Id or the event id, backed by durable shared storage when running more
than one receiver instance.
Important
Webhook-protected CloudEvents must use structured HTTP encoding. Binary-mode
CloudEvents with ce-* headers are rejected before signature verification.
Supported ID kinds:
uuidksuidnanoidulidxid
Config:
id:
kind: uuidNote
ID generators produce operational identifiers such as request ids, webhook ids, and token jti values. They are not a secret-material API and should not be used as passwords, bearer tokens, or other credentials. Omit id entirely to select the uuid default. If id is present, kind must be one of the supported registered kinds. Sortable kinds such as ksuid, ulid, and xid expose ordering characteristics.
Server commands created through cli.Application.AddServer include runtime.Module, which currently enables:
Note
This registration is best-effort and does not fail startup if a memory limit cannot be applied. Direct Fx compositions and client-style commands should include runtime.Module explicitly when they want this behavior.
SQL root config is database/sql.Config, with Postgres under sql.pg.
Postgres config embeds common pool + DSN config (database/sql/config.Config), including writer/reader pools. Each role pool owns its dsns and settings. Enabled SQL pool settings must set positive max_open_conns and max_idle_conns; max_idle_conns must not exceed max_open_conns.
module.Server and module.Client both include sql.Module, which currently wires PostgreSQL support via database/sql/pg.Module.
Enablement is presence-based: a nil sql block or a nil sql.pg block disables SQL wiring. When enabled, the pgx stdlib driver is registered under the name pg, and reader/writer DSNs are resolved using the source-string rules described above. Enabled PostgreSQL config must provide at least one non-empty reader.dsns[].url or writer.dsns[].url. Driver instrumentation is installed when tracing or metrics are enabled, OpenTelemetry database/sql stats metrics are registered when metrics are enabled, and the resulting pools are closed on lifecycle stop.
SQL wiring creates database/sql pool handles and applies pool settings, but it
does not ping PostgreSQL during construction. Call DBs.Ping, DBs.PingWriter,
or DBs.PingReader with an SLO-appropriate
deadline, or register health/checker.NewDBChecker, when startup or readiness
should verify database reachability.
Example (with source strings for DSNs):
sql:
pg:
reader:
dsns:
- url: env:PG_READER_DSN
settings:
max_open_conns: 20
max_idle_conns: 10
conn_max_idle_time: 30m
conn_max_lifetime: 1h
writer:
dsns:
- url: env:PG_WRITER_DSN
settings:
max_open_conns: 3
max_idle_conns: 2
conn_max_idle_time: 10m
conn_max_lifetime: 30mExample (literal DSN; not recommended for production secrets):
sql:
pg:
writer:
dsns:
- url: postgres://user:pass@localhost:5432/dbname?sslmode=disable
settings:
max_open_conns: 10
max_idle_conns: 5Health checks are based on go-health.
The framework provides Kubernetes-style endpoints:
/<name>/healthzβ general serving health status/<name>/livezβ liveness probe/<name>/readyzβ readiness probe
Successful health responses return HTTP 200 with the plain-text body SERVING.
Missing or failing observers return HTTP 503 with the standard go-service error response.
During server shutdown, /readyz also returns HTTP 503 after the lifecycle starts draining so
orchestrators can stop sending new traffic before the listener fully stops.
Built-in checker helpers under health/checker include DB connectivity checks and
cache connectivity checks for pingable cache drivers such as Redis and ttlcache.
module.Server installs the HTTP/gRPC health transports, but services own the
checks and observer mapping. Create go-health server.Registration values,
register them under the service or gRPC service name on *server.Server, and
map them to healthz, livez, readyz, or grpc with Observe. See the
executable Registrations example and the
go-service-template health module
for the standard DI pattern. A checker is not exposed by a probe until that
registration and observer mapping exists.
When gRPC transport is enabled, transport/grpc/health registers the standard
grpc.health.v1.Health service on the gRPC server. Named checks use the service
name as the request service; an empty service checks overall gRPC health:
grpcurl -plaintext -d '{"service":"<name>"}' localhost:9000 grpc.health.v1.Health/CheckCheck returns SERVING or NOT_SERVING for known services and NotFound for
unknown services. List returns the current statuses for registered services.
Watch streams status changes until the client cancels; unknown services stream
SERVICE_UNKNOWN. Health operation RPCs bypass token verification. Unary
Check and List also bypass unary server-side limiting, while health Watch
is a stream and still uses stream limiting.
These are modeled after Kubernetes API health endpoints.
Telemetry config root is telemetry.Config:
telemetry:
attributes:
k8s.namespace.name: payments
metadata:
max_value_size: 4KB
logger: ...
metrics: ...
propagation: ...
tracer: ...attributes are plain OpenTelemetry resource labels attached to logs, metrics,
and traces. They are not source strings. Fixed go-service identity attributes
such as host.id, service.instance.id, service.name, service.version,
and deployment.environment.name take precedence if the same key is
configured.
Request and service metadata is copied from the context to go-service logger records and trace attributes. Configure the maximum size of each exported value:
telemetry:
metadata:
max_value_size: 4KBWhen max_value_size is omitted or 0, each value defaults to 1,024 bytes.
Values are truncated on a UTF-8 boundary only in telemetry; the original value
remains in the request context and transport metadata. Choose a value that fits
the service's log and trace payload budgets: the limit applies to every
metadata-bearing log record and span.
OpenTelemetry context propagation defaults to W3C Trace Context plus W3C Baggage for extraction and injection:
telemetry:
propagation:
formats:
- tracecontext
- baggageMixed tracing estates can enable additional formats:
telemetry:
propagation:
formats:
- tracecontext
- baggage
- b3Supported propagators are tracecontext, baggage, b3, b3multi, and
none. Use none only as the sole value for formats.
B3 uses the upstream B3 propagator, which supports both single-header and multi-header B3 formats.
Logging uses log/slog.
Supported built-in logger kinds:
jsontexttintotlp
Supported logger levels are debug, info, warn, and error. When level
is unset, logging defaults to info; unknown values fail logger construction.
telemetry:
logger:
kind: json
level: infotelemetry:
logger:
kind: text
level: infotelemetry:
logger:
kind: otlp
level: info
protocol: http
url: http://localhost:4318/v1/logs
http_timeout: 10s
batch_timeout: 5s
export_timeout: 30s
max_queue_size: 2048
max_export_batch_size: 512
headers:
Authorization: env:OTLP_LOGS_AUTHNote
batch_timeout,export_timeout,max_queue_size, andmax_export_batch_sizetune the OTLP batch export pipeline and apply only whenkindisotlp. When a value is unset or zero, the OpenTelemetry SDK default is used (queue2048, batch512). A nonzerobatch_timeoutmust use whole-second precision. Explicit queue and batch limits may be at most8192and2048, respectively; the effective batch may not exceed the effective queue.http_timeoutbounds one OTLP/HTTP export request. It defaults to10swhen unset or zero and does not apply to OTLP/gRPC.headersvalues are source strings.- Telemetry header maps are resolved during config projection; unset
env:values and unreadablefile:values fail fast (panic during startup). - After resolution, go-service passes header names and values to the selected exporter without validating HTTP or gRPC syntax. Use headers valid for the selected protocol; an exporter may report invalid syntax only when it attempts an export, not during startup.
Warning
OTLP exporters reject non-loopback http:// endpoints when headers are configured. Use HTTPS for remote collectors that require authorization headers; cleartext with headers is accepted only for local loopback endpoints.
OTLP/HTTP exporters do not follow redirects; configure the final collector URL.
OTLP/gRPC exporters use protocol: grpc and a host:port endpoint such as localhost:4317. Header-bearing remote gRPC endpoints require the signal's tls config; loopback gRPC endpoints may still use cleartext.
OTLP exporter endpoints must be set in go-service config fields such as telemetry.logger.url, telemetry.metrics.url, and telemetry.tracer.url. Standard OpenTelemetry endpoint environment variables such as OTEL_EXPORTER_OTLP_ENDPOINT are not used as fallback sources.
Configure OTLP/HTTP request timeouts and TLS material with http_timeout and tls; corresponding OpenTelemetry timeout and certificate environment variables are not projected into go-service config.
OTLP exporters can use the same TLS source-string model as other go-service clients. HTTP exporters require an https:// URL for TLS, while gRPC exporters use protocol: grpc:
telemetry:
tracer:
kind: otlp
protocol: grpc
url: collector.example.com:4317
tls:
ca: file:/etc/otel/ca.pem
cert: file:/etc/otel/client.crt
key: file:/etc/otel/client.key
server_name: collector.example.com
headers:
Authorization: env:OTLP_TRACES_AUTHUse the same tls shape under telemetry.logger or telemetry.metrics for OTLP/HTTPS or OTLP/gRPC.
Supported metrics kinds:
prometheusotlp
telemetry:
metrics:
kind: prometheus
prometheus:
without_suffixes: true
without_target_info: true
without_scope_info: trueWhen Prometheus is enabled on HTTP transport, metrics are exposed at /<name>/metrics.
The optional prometheus block shapes exporter output for compatibility with an
existing Prometheus/Grafana/alerting stack. without_suffixes drops unit
(for example _seconds, _bytes) and _total counter suffixes from metric
names, without_target_info omits the target_info metric, and
without_scope_info omits the otel_scope_name/otel_scope_version labels. When
the prometheus block is omitted, the exporter keeps its default
OpenTelemetry-conventional output.
telemetry:
metrics:
kind: otlp
protocol: http
url: http://localhost:9009/otlp/v1/metrics
http_timeout: 10s
interval: 30s
timeout: 5s
headers:
Authorization: env:OTLP_METRICS_AUTHinterval and timeout apply only to OTLP push metrics. http_timeout bounds
an OTLP/HTTP request. When interval or timeout is unset or zero, the
OpenTelemetry SDK default is used; http_timeout defaults to 10s. A nonzero
interval must use whole-second precision.
Override the default histogram bucket boundaries per instrument with an ordered
telemetry.metrics.views list. Each pattern uses OpenTelemetry name matching,
including * wildcards:
telemetry:
metrics:
views:
- pattern: http.server.request.duration
boundaries: [0.005, 0.01, 0.05, 0.1, 0.5, 1, 5]
- pattern: "rpc.*.duration"
boundaries: [0.01, 0.1, 1]Boundaries are in the instrument's unit (seconds for duration histograms, bytes
for size histograms) and should be listed in increasing order. Views apply to
histogram instruments regardless of metrics kind; an unset or empty list keeps the
OpenTelemetry SDK default buckets. Views are evaluated in list order; the first
matching view is applied. Migrate the previous map form by making each map entry a
list item with pattern and boundaries fields.
go-service passes configured boundaries to OpenTelemetry unchanged and does not validate their order. Boundaries should be increasing: the supported OpenTelemetry SDK reports duplicate or decreasing boundaries through its global error handler and uses its default histogram aggregation for the matching instrument; configuration and startup do not fail.
Tracing supports OTLP exporter config:
telemetry:
tracer:
kind: otlp
protocol: http
url: http://localhost:4318/v1/traces
http_timeout: 10s
batch_timeout: 5s
export_timeout: 30s
max_queue_size: 2048
max_export_batch_size: 512
sampler:
kind: ratio
ratio: 0.25
headers:
Authorization: env:OTLP_TRACES_AUTHNote
batch_timeout, export_timeout, max_queue_size, and max_export_batch_size tune the OTLP batch span export pipeline. When a value is unset or zero, the OpenTelemetry SDK default is used (queue 2048, batch 512). A nonzero batch_timeout must use whole-second precision. Explicit queue and batch limits may be at most 8192 and 2048, respectively; the effective batch may not exceed the effective queue.
http_timeout bounds one OTLP/HTTP export request. It defaults to 10s when unset or zero and does not apply to OTLP/gRPC.
OTLP exporters default to protocol: http. Set protocol: grpc and use a
host:port url, such as localhost:4317, to export through OTLP/gRPC.
Supported sampler kinds:
always_on: record every trace.always_off: drop every trace.ratio: follow an incoming parent span's sampled decision when the request already has trace context; otherwise record the configured fraction of new root traces. Setratiobetween0and1, where0drops new root traces and1records all new root traces.
When sampler is omitted, go-service preserves the OpenTelemetry SDK default
sampler and SDK sampler environment handling.
- https://pkg.go.dev/go.opentelemetry.io/contrib/instrumentation/runtime
- https://pkg.go.dev/go.opentelemetry.io/contrib/instrumentation/host
- https://pkg.go.dev/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp
- https://pkg.go.dev/go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc
- https://github.com/redis/go-redis/tree/master/extra/redisotel
- https://github.com/XSAM/otelsql
Token configuration is rooted at token.Config, usually nested under transport config as transport.http.token and/or transport.grpc.token (via the shared server-side transport config).
Supported token kind values:
jwtpasetossh
Access control is configured once at the transport level and shared by all enabled HTTP and gRPC server stacks:
transport:
access:
model: file:./config/rbac.conf
policy: file:./config/rbac.csvWhen access is configured, the standard HTTP and gRPC server stacks enforce
the policy after token authentication and before application handlers run. Omit
access to leave transport authorization disabled.
The model is based on Casbin RBAC: https://github.com/casbin/casbin/blob/master/examples/rbac_model.conf
Example rbac.conf:
[request_definition]
r = sub, obj, act
[policy_definition]
p = sub, obj, act
[role_definition]
g = _, _
[policy_effect]
e = some(where (p.eft == allow))
[matchers]
m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.actPolicies use the verified user id as sub, meta.TransportServiceMethod as
obj, and invoke as act. Example rbac.csv:
p, reader, http:GET /users/{id}, invoke
p, writer, http:POST /users, invoke
p, greeter, grpc:/greet.v1.GreeterService/SayHello, invoke
g, frontend, reader
g, admin, reader
g, admin, writer
g, billing-service, greeterThe p rows define permissions and must match the model's p = sub, obj, act
shape, so they include invoke. The g rows define role membership and match
g = _, _, so they only contain subject, role.
Warning
Casbin's string policy adapter can skip malformed policy rows without failing startup. Validate policy files before deployment; a successful startup does not prove that every configured row was loaded.
For HTTP servers the object uses the matched route pattern when available, such
as http:GET /users/{id}. HTTP tokens are authenticated against the concrete
request method and path, such as GET /users/123; access policy enforcement
uses the canonical route pattern. gRPC tokens are authenticated against the full
method name, such as /greet.v1.GreeterService/SayHello; access policy
enforcement uses the transport service-method object, such as
grpc:/greet.v1.GreeterService/SayHello.
Note
access.model and access.policy are resolved through os.FS.ReadSource; use file: for files, env: for environment-provided content, or literal content.
Access config builds an injectable controller for authorization checks. The built-in HTTP and gRPC server stacks authenticate tokens, store the verified user id, and enforce the configured Casbin policy before application handlers run.
JWT config:
transport:
http:
token:
kind: jwt
jwt:
iss: my-service
exp: 1h
leeway: 30s
key: active
keys:
active:
public: file:/keys/ed25519.pub
private: file:/keys/ed25519
old:
public: file:/keys/ed25519-old.pubImportant behavior:
- JWT generation signs with
jwt.key; verification requires the tokenkidheader to select an entry injwt.keys. expis parsed as a Go duration string; invalid values can fail fast.leewayis optional clock-skew tolerance for verification; keep it small because it extends acceptance aroundiat/nbfandexp.
Important
JWT generation and verification use Ed25519 key material from jwt.keys. Keep private key material only on services that mint tokens; verifiers only need public keys.
All token exp and non-zero leeway values are Go duration strings and must be positive whole-second durations. Values such
as 1s, 15m, and 24h validate; sub-second values such as 500ms do not.
Paseto config:
transport:
http:
token:
kind: paseto
paseto:
iss: my-service
exp: 1h
leeway: 30s
key: active
keys:
active:
public: file:/keys/ed25519.pub
private: file:/keys/ed25519
old:
public: file:/keys/ed25519-old.pubNote
The PASETO implementation issues v4 public tokens. Generation signs with paseto.key, writes that id as footer kid, and verification selects the public key from paseto.keys. paseto.leeway is optional clock-skew tolerance for verification.
SSH token verification keys are id-addressable and support rotation.
Verification-only example:
transport:
http:
token:
kind: ssh
ssh:
exp: 5m
leeway: 30s
keys:
active:
public: file:/keys/active.pubSigning + verification example:
transport:
http:
token:
kind: ssh
ssh:
exp: 5m
leeway: 30s
key: active
keys:
active:
public: file:/keys/active.pub
private: file:/keys/active
old:
public: file:/keys/old.pubNote
ssh.keyis the active key id used for minting tokens (the matchingssh.keysentry requires private key material).ssh.keysis the trusted key map used for verification (public keys).ssh.expsets the token validity window; SSH keys remain long-lived, while generated tokens are short-lived.ssh.leewayis optional clock-skew tolerance for verification; keep it small because it extends acceptance aroundiatandexp.- SSH tokens carry
subequal tokid, so the verified subject is the trusted peer key id.
Limiter config is transport/limiter.Config and is typically applied at transport level.
Supported key kinds (built-in):
user-idtransport-service-methodservice-methodipuser-agent
Example:
transport:
http:
limiter:
kind: user-agent
tokens: 10
interval: 1s
max_keys: 4096Note
intervalis parsed as a Go duration string. Invalid values can fail fast.tokensandintervaluse the underlying in-memory store defaults when set to zero:1token per1s. Configure positive values for explicit quotas.max_keyscaps the number of caller-derived keys that receive independent in-memory buckets. A zero value uses the default4096; additional distinct keys share one overflow bucket.- The built-in limiter is an in-memory, per-process safeguard. Use it as a last resort and prefer an external edge, gateway, ingress, load balancer, or service-mesh limiter for production abuse protection.
- The
user-idkey uses the verified principal stored in metadata. For JWT/PASETO tokens this is the subject claim; for SSH tokens this is the verified key name. Prefer it when authenticated identity is available. - The
transport-service-methodkey prefixes the service-method value with the transport name, such ashttp:GET /users/{id}orgrpc:/users.v1.Users/Get, so HTTP and gRPC operations use separate buckets. - The
service-methodkey uses HTTP route/path metadata or the gRPC full method name. Prefertransport-service-methodunless cross-transport operations intentionally share quota. - Server-side HTTP and gRPC limiters run after metadata extraction and token verification, so missing, malformed, or invalid authorization is rejected before it reaches the limiter. This is intentional; enforce quotas for those attempts with an external edge, gateway, ingress, load balancer, or service-mesh limiter.
- Server-side HTTP limiters set
RateLimitandRateLimit-Policyheaders; denied HTTP requests also setRetry-Afterwhen reset timing is available. Server-side gRPC limiters setratelimitandratelimit-policyresponse metadata; denied gRPC requests also attach agoogle.rpc.RetryInfodetail when reset timing is available. - gRPC stream limiters consume one token when the stream opens and one token for each
RecvMsgandSendMsgoperation. Unary HTTP and gRPC requests consume one token per request/RPC.
Time config:
time:
kind: nts
address: time.cloudflare.com
timeout: 2sSupported kinds:
ntpnts
Omit the time block to disable network time. If the block is present, kind
must be ntp or nts; empty or unknown kinds fail startup with the time
provider not found error. address is provider-specific and is used when the
network time provider performs I/O. timeout bounds network operations for the
selected provider; a zero value uses the upstream client's default timeout, and
negative values are invalid.
The transport layer provides higher-level wiring and middleware policy for communication in/out of the service.
At a high level:
transport/...contains the opinionated service transport layer: Fx wiring, composed HTTP/gRPC server and client stacks, retries, breakers, token middleware, health wiring, and related policy.net/...contains lower-level protocol helpers and reusable primitives such asnet/http,net/grpc,net/http/meta,net/grpc/meta,net/grpc/health,net/header, andnet/server.
Supported stacks include:
- gRPC (https://grpc.io/)
- HTTP REST abstraction (
net/http/rest) using content negotiation - HTTP RPC abstraction (
net/http/rpc) using content negotiation - HTTP MVC helpers (
net/http/mvc) - CloudEvents (https://github.com/cloudevents/sdk-go)
CloudEvents HTTP wiring lives under transport/http/events: use
NewReceiver(...).Register(...) to receive events on a POST route and
NewSender(...).Send(...) with net/http/events.ContextWithTarget(...) to
send events. The sender uses structured HTTP encoding by default; configure
WithSenderEncoding(SenderEncodingBinary) for outbound integrations that
require binary-mode CloudEvents. Webhook-protected receivers require structured
encoding and reject binary-mode CloudEvents with ce-* headers before
signature verification. Receiver registration marks the event route as
unauthenticated for transport token/access middleware so webhook verification can
act as the event authentication boundary.
The HTTP REST and RPC helpers decode request bodies from the request Content-Type, falling back to JSON when Content-Type is absent. An unparseable, unregistered, or intentionally undecodable Content-Type is rejected with HTTP 415 rather than falling back to JSON. Response encoding uses the first Accept media type when present, falling back to the request Content-Type when Accept is absent. Client helpers can set ContentType for the request body and Accept for an independent response format.
Built-in text/object payload media types include:
application/jsonapplication/hjsonapplication/yamlapplication/tomlapplication/octet-stream,text/plain
Internal binary payload media types include:
application/vnd.msgpackapplication/gob
Built-in protobuf-oriented media type aliases include:
application/proto,application/pb,application/protobuf,application/protobin,application/pbbinapplication/protojson,application/pbjsonapplication/prototext,application/prototxt,application/pbtxt
Note
application/hjsonmaps to the built-inhjsonencoder kind.- Unknown or invalid media types fall back to JSON selection only for outbound (
Accept-driven) negotiation. An absent requestContent-Typestill defaults to JSON, but an unknown or invalid one is rejected with HTTP 415 rather than decoded as a different format than the caller declared. text/erroris reserved for error responses and should not be sent by clients as a request content type.
application/toml, application/vnd.msgpack, and application/gob can be resolved as media types and remain valid
response codecs, but REST/RPC request-body decoding β for both single-value and streaming
(NDJSON) requests β rejects them with HTTP 415. This follows the decoder-bounds rule documented in
net/http/content/unary's package documentation: a codec is admissible for decoding untrusted input only
when it is both ratio-bounded and depth-bounded, which TOML, msgpack, and gob are not.
REST and RPC support streaming routes alongside the single-value helpers above, for responses (and, over HTTP/2, requests) that arrive as a sequence of values instead of one buffered payload:
| single-value | streaming | direction | HTTP/2 required |
|---|---|---|---|
rest.Get/rest.Route |
rest.StreamGet/rest.StreamRoute |
send-only | no |
rest.Post/rest.Put/rest.Patch/rest.RouteRequest |
rest.StreamPost/rest.StreamPut/rest.StreamPatch/rest.StreamRouteRequest |
bidirectional | yes |
rpc.Route |
rpc.StreamRoute |
bidirectional | yes |
A send-only streaming handler gets a *stream.Stream[Res] with Send; a bidirectional streaming
handler gets a *stream.RequestStream[Req, Res] with both Send and Recv. Client calls use the
matching client.Stream/client.RequestStream functions, which take the same kind of callback.
See net/http/client's ExampleClient_RequestStream for a complete HTTP/2 bidirectional client call.
Register a raw HTTP handler with http.Router.HandleRoute and compose its policy in the same call:
router.HandleRoute(
"GET /feed",
handler,
http.WithRouteOperation(),
http.WithRouteUnauthenticated(),
)Warning
http.WithRouteOperation is for service-owned infrastructure paths, such as the health and metrics routes.
Operation matching is path-only, so it marks the pattern's path for every method β marking GET /feed
also marks a separately registered POST /feed β and supported middleware treats an operation route as
exempt from token verification, access control, and rate limiting, and omits its per-request outcome log
line. http.WithRouteUnauthenticated instead bypasses transport token verification and access control while
retaining rate limiting and normal outcome logging; use it only when another boundary protects the route.
REST and RPC route helpers accept HTTP route options. Streaming helpers add their inherent stream direction,
and supplied streaming options are additive. MVC accepts only mvc.WithRouteUnauthenticated for its view routes.
MVC static helpers keep their StaticOption signature; use
mvc.WithStaticUnauthenticated() to opt a static route out of authentication.
Important
Route policy registration now happens only through HandleRoute. This intentionally removes the previous
Router.Handle* and mutating RoutePolicy registration APIs from v2. Migrate Router.Handle to HandleRoute
without options, and migrate specialized registration to HandleRoute with the corresponding WithRoute* option.
Replace IsStreaming checks with the separate IsRequestStreaming and IsResponseStreaming checks.
Important
Single-value helpers live in github.com/alexfalkowski/go-service/v2/net/http/content/unary; streaming helpers
live in github.com/alexfalkowski/go-service/v2/net/http/content/stream. Import unary for Content, Media,
NewContent, NewHandler, and NewRequestHandler; import stream for incremental request/response helpers.
stream.NewHandler and stream.NewRequestHandler take *stream.Content, while unary handlers take
*unary.Content. rest.Register, rpc.Register, and client.NewClient take the unary and streaming content
owners separately.
Migrate root content imports and identifiers to content/unary and unary, respectively; use
net/http.ContentTypeKey and net/http.AcceptKey for the shared header names.
The initial wire format is NDJSON (application/x-ndjson), newline-delimited JSON values, resolved
through a separate streaming encoder/decoder registry (encoding/stream.Map) from the single-value one
above β an unregistered or unparseable streaming media type is rejected outright rather than falling
back to JSON, unlike single-value negotiation.
Note
- Bidirectional streaming routes require HTTP/2 (including h2c); a request over HTTP/1.x is rejected
with
505 HTTP Version Not Supportedbefore the handler runs. Send-only streaming routes have no such requirement and stay fully supported on HTTP/1.1 chunked responses. - Streaming responses are not gzip-compressed, regardless of the client's
Accept-Encoding. max_receive_sizeapplies per decoded value on a streaming request body, not as a cumulative total across the whole stream; overall stream volume is controlled by the configured rate limiter instead, which charges one token per streamed message in addition to the token charged when the stream opens.- A successful
Sendextends the HTTP server's configured write timeout, and on a bidirectional stream a successfulRecvextends both the read and write timeouts (andSendextends both too), so a slow-but-active stream is not severed by a whole-stream deadline in either direction; bound a client-side streaming call with the request context instead of the client's overall request timeout. - The per-message read/write timeouts follow the same
options.read_timeout/options.write_timeoutprecedence as the server's own timeouts (see Transport configuration (servers)), falling back to30swhen the corresponding option is unset. - Streaming requests are never retried by the client's retry middleware.
- A stream failure after the response has committed is recorded as a trace error and in the access log, then aborts the response so clients do not receive a clean but truncated stream. The upstream HTTP server RED metrics do not record aborted streams; use the access log to investigate that failure class.
- During standard server shutdown, stream handler contexts are canceled. Handlers must return after
ctx.Done()when waiting on an upstream source; an activeRecvends with the drain signal. A blockedSendremains subject to the configured write timeout. If the lifecycle shutdown deadline expires, the server force-closes remaining HTTP connections, so clients observe a transport error. A bidirectional HTTP/2 client may observe the forced request-body close as a stream reset and should reconnect to a non-draining server.
The HTTP transport wraps the mux with net/http.NewNotFoundHandler so generated 404 responses can be rendered consistently while preserving other mux responses such as 405 Method Not Allowed.
- REST/RPC-style missing routes use
net/http/status.NotFoundHandler, which writes the standardstatus.WriteErrorresponse. - MVC missing routes can use
net/http/mvc.NotFoundHandlerto render the registered MVC not-found view when the request accepts HTML (Accept: text/html) or is an HTMX request (Hx-Request: true). - Routes that match and write their own status are not replaced by this mux-level not-found handler.
When an MVC controller returns an error, net/http/mvc.Route renders the returned view with a client-safe mvc.Error model. The model contains the HTTP status Code and safe client-visible Message.
The raw error string remains available to templates as mvcModelError metadata for compatibility. Rendering that metadata can expose diagnostic details, so prefer .Model.Message for client-visible error pages.
Transport config root is transport.Config:
transport.httpandtransport.grpcembedconfig/server.Configand own their unarytimeoutfields.
Minimal example:
transport:
http:
address: tcp://localhost:8000
timeout: 10s
grpc:
address: tcp://localhost:9000
timeout: 10sNote
- Address may use
<network>://<address>(for exampletcp://:8000) or a raw listen address such as:8000, which defaults to thetcpnetwork. - If address is omitted, defaults are
tcp://:8080(HTTP) andtcp://:9090(gRPC). transport.http.timeoutbounds non-streaming handler contexts andtransport.grpc.timeoutbounds unary RPC handlers. Both default to30sand do not cap stream lifetime; long-lived HTTP and gRPC streams remain governed by client cancellation and their stream-specific controls.- HTTP socket deadlines and streaming read/write inactivity budgets are controlled by
transport.http.options.read_timeout,write_timeout,idle_timeout, andread_header_timeout, each of which defaults independently to30s. gRPC connection and keepalive lifetimes are controlled bytransport.grpc.optionsand retain their documented lower-level defaults when unset. - For gRPC keepalives,
keepalive_ping_timeis the interval between heartbeats andkeepalive_ping_timeoutis the maximum wait for a heartbeat acknowledgement. - gRPC limits each client connection to 64 concurrent streams by default. Set
transport.grpc.options.max_concurrent_streamsto a positive base-10 integer to override it, or to"0"to explicitly retain upstream's unbounded behavior. max_receive_sizelimits inbound payload size. A zero value uses the default4MB.- For HTTP,
max_receive_sizeapplies per request body, except for bidirectional streaming routes (see HTTP streaming (NDJSON)), where it applies per decoded value instead, with no cumulative total. For gRPC, it applies per inbound unary request and per inbound stream message. - MVC does not enforce its own body-size caps; supported HTTP server wiring applies
max_receive_sizebefore MVC handlers run, and go-service HTTP clients apply their configured response-size cap when reading responses.
Receive-limit example:
transport:
http:
max_receive_size: 2MB
grpc:
max_receive_size: 3MBWith low-level server options:
transport:
http:
address: tcp://localhost:8000
timeout: 10s
options:
read_timeout: 10s
write_timeout: 10s
idle_timeout: 10s
read_header_timeout: 10s
grpc:
address: tcp://localhost:9000
timeout: 10s
options:
keepalive_enforcement_policy_ping_min_time: 10s
keepalive_max_connection_idle: 10s
keepalive_max_connection_age: 10s
keepalive_max_connection_age_grace: 10s
keepalive_ping_time: 10s
keepalive_ping_timeout: 10sTLS config uses crypto/tls/config.Config and fields are source strings:
transport:
http:
tls:
cert: file:test/certs/cert.pem
key: file:test/certs/key.pem
ca: file:test/certs/rootCA.pem
grpc:
tls:
cert: file:test/certs/cert.pem
key: file:test/certs/key.pem
ca: file:test/certs/rootCA.pemSet ca on server TLS config to require and verify client certificates for mTLS. Set ca on client TLS
config to verify server certificates issued by the same local or private CA. server_name is only needed
on clients when the dial address differs from the certificate DNS name.
Server-side TLS requires a complete cert and key pair whenever TLS material is configured. ca enables
client-certificate verification for mTLS, but a CA-only server TLS config fails startup.
Runtime servers require TLS 1.3 or newer on inbound handshakes; clients keep a TLS 1.2 floor so outbound calls stay interoperable with TLS-1.2-only endpoints.
gRPC clients use insecure transport credentials when TLS is not configured. That default is intended for local or platform-secured traffic; configure client TLS for calls outside that trusted boundary.
Important
If you are using go-service-template or composing server transport bundles such as module.Server or transport.Module, the required transport registration is handled for you by DI.
module.Client does not wire transports by default. When a client process constructs HTTP or gRPC TLS config from source strings such as file:, call the relevant transport-level Register(...) functions, such as transport/http.Register(...) or transport/grpc.Register(...).
You only need to call transport-level Register(...) functions yourself when you intentionally wire transports manually or compose lower-level packages outside the transport module graph.
If you are wiring server lifecycle manually, use net/server.Register(...).
Warning
HTTP and gRPC metadata extraction intentionally trusts common forwarded IP headers/metadata such as X-Forwarded-For, X-Real-IP, CF-Connecting-IP, and True-Client-IP. Services that rely on extracted IPs for logging, policy, or rate limiting should only receive traffic through trusted edge infrastructure that strips or overwrites client-supplied forwarding headers.
Warning
gRPC server reflection is intentionally always registered by net/grpc.NewServer so internal tooling can discover services. Services that should not expose reflection publicly should restrict access with bind addresses, TLS/client authentication, ingress policy, firewall rules, or service-mesh authorization.
The transport client wrappers include optional circuit breakers:
-
HTTP breaker (
transport/http/breaker):- Scope is per
"<METHOD> <HOST>". - Default failure statuses are
>=500and429. - Requests with an already deadline-exceeded context bypass breaker accounting.
- Transport errors are counted as failures.
- Failure status responses are still returned to callers (while breaker accounting records a failure).
- Scope is per
-
gRPC breaker (
transport/grpc/breaker):- Scope is per
fullMethod. - Default failure codes are
Unavailable,DeadlineExceeded,ResourceExhausted, andInternal. - Errors with other gRPC codes are treated as successful for breaker accounting.
- Scope is per
Client config uses the shared transport/breaker.Config shape for breaker mechanics. Any config type that
embeds config/client.Config has its own breaker block under that client config. This example uses
feature.Config only because it is one such client config:
feature:
address: localhost:9000
breaker:
max_requests: 2
interval: 15s
timeout: 5s
consecutive_failures: 4When manually constructing HTTP or gRPC clients, pass a transport-specific breaker config to
transport/http.WithClientBreaker(...) or transport/grpc.WithClientBreaker(...). These configs
embed the shared breaker mechanics and add protocol-specific failure classification:
httpBreaker := httpbreaker.NewConfig(sharedBreaker, 429, 502, 503)
grpcBreaker := grpcbreaker.NewConfig(sharedBreaker, codes.Unavailable, codes.ResourceExhausted)NewConfig returns nil when the shared breaker config is nil, preserving client-option wiring that
disables breakers by omitting breaker config.
max_requests controls half-open probe concurrency. interval controls the
closed-state count reset window. timeout controls how long the breaker stays
open before allowing half-open probes. consecutive_failures controls when the
breaker opens. Zero values keep the package defaults.
Instead of (or alongside) consecutive_failures, failure_ratio and
min_requests open the breaker on a sustained error rate rather than an
unbroken run of failures:
feature:
address: localhost:9000
breaker:
failure_ratio: 0.5
min_requests: 10failure_ratio is the fraction of failed requests (0 < r <= 1) within the
current interval that opens the breaker, evaluated only once min_requests
requests have been observed. When failure_ratio is set, it takes precedence
over consecutive_failures.
HTTP StatusCodes and gRPC Codes are optional replacement lists for failure
classification. When omitted, the default lists above apply. When set, only the
configured values count as breaker failures, so include the defaults as well
when extending rather than replacing default behavior.
Client config uses the shared transport/retry.Config shape for retry mechanics. Any config type that embeds
config/client.Config has its own retry block under that client config. This example uses feature.Config
only because it is one such client config:
feature:
address: localhost:9000
retry:
timeout: 1s
backoff: 100ms
attempts: 3
strategy: exponentialWhen manually constructing HTTP or gRPC clients, pass a transport-specific retry config to
transport/http.WithClientRetry(...) or transport/grpc.WithClientRetry(...). These configs embed the
shared retry mechanics and add protocol-specific failure classification:
httpRetry := httpretry.NewConfig(sharedRetry, 429, 502, 503)
grpcRetry := grpcretry.NewConfig(sharedRetry, codes.Unavailable, codes.ResourceExhausted)NewConfig returns nil when the shared retry config is nil, preserving client-option wiring that
disables retries by omitting retry config.
attempts is the total number of attempts, including the initial call. A value
of 0 or 1 means no retry beyond the first attempt; values above 10 are
rejected during config validation. backoff is the base delay between retry
attempts.
strategy selects how backoff grows between attempts: constant (the
default) reuses the base delay for every wait, exponential doubles it on each
attempt, and fibonacci grows it along the Fibonacci sequence. An unset value
applies constant, jitter is applied on top of the chosen strategy, and any
other value is rejected during config validation.
timeout is transport-specific. gRPC unary retries apply it per attempt, so
total elapsed time can include multiple attempt timeouts plus backoff unless the
caller context ends first. HTTP retries do not create a retry-owned per-attempt
timeout; bound outbound HTTP calls with the request context or
http.Client.Timeout.
max_backoff caps the per-attempt backoff duration, applied before jitter. It
is most useful with exponential and fibonacci growth, which otherwise grow
unbounded across attempts. A zero value (the default) leaves backoff
uncapped:
feature:
address: localhost:9000
retry:
backoff: 1s
strategy: exponential
attempts: 10
max_backoff: 30sHTTP StatusCodes and gRPC Codes are optional replacement lists for failure
classification. When omitted, the default lists below apply. When set, only the
configured values are retryable, so include the defaults as well when extending
rather than replacing default behavior. HTTP values must be 4xx or 5xx status
codes. gRPC values must be non-OK codes.Code values.
Default retry policy is intentionally conservative:
- HTTP retries side-effect-safe methods (
GET,HEAD,OPTIONS) or requests with aRequest-Id. - HTTP retries response/status failures only for
429 Too Many Requestsand503 Service Unavailable, plus selected transport errors classified byretryablehttp.DefaultRetryPolicy. - gRPC retries AIP-style read methods named
Get*orList*, or calls with aRequest-Id. - gRPC retries only
Unavailableby default.
HTTP retryable responses with a valid Retry-After delay greater than the
minimum jittered backoff suppress another attempt and return the current
response. gRPC retryable status errors with google.rpc RetryInfo.retry_delay
use the same suppression policy.
Request-Id identifies the logical request, not an individual wire attempt.
Services that allow retried writes should treat it as the idempotency key and
deduplicate repeated attempts when duplicate processing would be unsafe.
The crypto root config is crypto.Config and supports multiple key types. Most fields are source strings.
Example:
crypto:
aes:
key: file:test/secrets/aes
ed25519:
public: file:test/secrets/ed25519_public
private: file:test/secrets/ed25519_private
hmac:
key: file:test/secrets/hmac
rsa:
public: file:test/secrets/rsa_public
private: file:test/secrets/rsa_private
ssh:
public: file:test/secrets/ssh_public
private: file:test/secrets/ssh_privateNote
- AES keys must be 16/24/32 bytes after resolving the source string.
- HMAC keys should be high-entropy secrets and must remain private.
- RSA keys expect PKCS#1 PEM blocks (
RSA PUBLIC KEY/RSA PRIVATE KEY) and must be at least 4096 bits. - Ed25519 expects PKIX
PUBLIC KEYand PKCS#8PRIVATE KEYPEM blocks. - SSH keys must be Ed25519 SSH keys: public keys use
authorized_keysformat and private keys use SSH private key format.
AES and RSA encryption APIs accept crypto.Message. Data is encrypted or
decrypted, while Meta is authenticated context that must match during
decryption. AES-GCM uses Meta as associated data; RSA-OAEP uses it as the
OAEP label.
Debug server config:
debug:
address: tcp://localhost:6060Enable TLS:
debug:
tls:
cert: file:test/certs/cert.pem
key: file:test/certs/key.pem
ca: file:test/certs/rootCA.pemDebug TLS uses the same server-side TLS contract as transports: cert and key
are required whenever TLS material is configured, and ca adds client-certificate
verification for mTLS.
All debug endpoints are namespaced by service name: /<name>/debug/....
Warning
If debug.address is omitted while debug is enabled, the debug server binds to tcp://:6060. Set an explicit address, TLS/mTLS, and network or policy controls appropriate for the deployment.
GET http://localhost:6060/<name>/debug/statsvizhttps://github.com/arl/statsviz
GET http://localhost:6060/<name>/debug/pprof/
GET http://localhost:6060/<name>/debug/pprof/cmdline
GET http://localhost:6060/<name>/debug/pprof/profile
GET http://localhost:6060/<name>/debug/pprof/symbol
GET http://localhost:6060/<name>/debug/pprof/tracehttps://pkg.go.dev/net/http/pprof
GET http://localhost:6060/<name>/debug/fgprof?seconds=10https://pkg.go.dev/github.com/felixge/fgprof
This repo generally follows the Uber Go Style Guide.
Exported Go identifiers should have GoDoc comments, and each comment should start with the identifier name or Deprecated:.
Common repository targets expect these tools on PATH:
makegotestsumformake specsfieldalignmentformake lintgolangci-lintfor fullmake lintcoverage (the wrapper no-ops when it is missing)govulncheckandtrivyformake secmkcertfor local TLS fixtures andmake create-certsbufformake generategodaand Graphvizdotformake diagrams
This repo uses a bin/ git submodule for make targets.
git submodule sync
git submodule update --init
mkcert -install
make create-certs
make depIf submodule fetch fails, ensure GitHub SSH access is configured (.gitmodules uses git@github.com:... URLs).
make helpmake depmake dep runs:
go mod downloadgo mod tidygo mod vendor
Tests are run with -mod vendor, so after dependency changes run make dep before make specs.
make start uses the shared Docker-based environment from the sibling
../docker repo. It requires Docker and may require GitHub SSH access if that
sibling repo must be fetched.
Start required services:
make startStop them:
make stopRun unit tests with race + coverage:
make specsArtifacts:
- JUnit XML:
test/reports/specs.xml - Coverage profile:
test/reports/profile.cov
make lint
make fix-lint
make formatmake secmake benchmarks
make http-benchmarks
make grpc-benchmarks
make limiter-benchmarks
make sql-benchmarks
make cache-benchmarks
make bytes-benchmarks
make strings-benchmarks
make id-benchmarks
make net-http-benchmarks
make http-content-benchmarksmake fuzzes
make bytes-fuzz
make time-fuzz
make encoding-fuzz
make compress-fuzz
make net-fuzz
make package=encoding/json name=FuzzUnmarshal fuzztime=10s fuzzmake coverage
make html-coverage
make func-coverageRoot generation targets are for the internal/test protobuf fixtures. After
changing those fixtures, regenerate them. To match the CI stale-output check,
run make generate-stale from a clean worktree, or after staging the intended
fixture and generated-file changes:
make generate
make generate-stalemake diagrams
make crypto-diagram
make database-diagram
make telemetry-diagram
make transport-diagram



