246 lines
8.7 KiB
Go
246 lines
8.7 KiB
Go
package config
|
|
|
|
import (
|
|
"strings"
|
|
"time"
|
|
|
|
"git.yuntuo.hk/k2safe/go-omni/contract"
|
|
apierrors "git.yuntuo.hk/k2safe/go-omni/errors"
|
|
"git.yuntuo.hk/k2safe/go-omni/httpx"
|
|
"git.yuntuo.hk/k2safe/go-omni/lifecycle"
|
|
"git.yuntuo.hk/k2safe/go-omni/logx"
|
|
"git.yuntuo.hk/k2safe/go-omni/response"
|
|
)
|
|
|
|
const defaultRequestIDHeader = "X-Request-ID"
|
|
|
|
const (
|
|
TransportNetHTTP = "nethttp"
|
|
TransportNative = "native"
|
|
TransportHTTP2 = "http2"
|
|
)
|
|
|
|
// TLSConfig enables HTTPS when both certificate paths are provided.
|
|
type TLSConfig struct {
|
|
CertFile string `json:"certFile"`
|
|
KeyFile string `json:"keyFile"`
|
|
}
|
|
|
|
// Config controls the default runtime behavior of an App.
|
|
type Config struct {
|
|
Request contract.RequestPolicy `json:"request"`
|
|
Address string `json:"address"`
|
|
Transport string `json:"transport"`
|
|
TLS TLSConfig `json:"tls"`
|
|
HTTP HTTPConfig `json:"http"`
|
|
Endpoints EndpointsConfig `json:"endpoints"`
|
|
RequestIDHeader string `json:"requestIdHeader"`
|
|
EnableLogger bool `json:"enableLogger"`
|
|
SlowRequestThreshold time.Duration `json:"slowRequestThreshold"`
|
|
TrustedProxies []string `json:"trustedProxies"`
|
|
ShutdownTimeout time.Duration `json:"shutdownTimeout"`
|
|
Lifecycle lifecycle.ShutdownConfig `json:"lifecycle"`
|
|
Log logx.Config `json:"log"`
|
|
Errors apierrors.Catalog `json:"errors"`
|
|
Response response.Config `json:"response"`
|
|
ResponseEncoder response.Encoder `json:"-"`
|
|
LogInstance *logx.Logger `json:"-"`
|
|
}
|
|
|
|
func Default() Config {
|
|
return Config{
|
|
Address: ":8080",
|
|
Transport: TransportNetHTTP,
|
|
RequestIDHeader: defaultRequestIDHeader,
|
|
ShutdownTimeout: 10 * time.Second,
|
|
Lifecycle: lifecycle.DefaultShutdownConfig(),
|
|
Endpoints: DefaultEndpoints(),
|
|
Log: logx.Default(),
|
|
}
|
|
}
|
|
|
|
// Validate checks configuration values before an application starts.
|
|
func (c Config) Validate() error {
|
|
if err := c.Lifecycle.Validate(); err != nil {
|
|
return err
|
|
}
|
|
if err := c.Request.Validate(); err != nil {
|
|
return err
|
|
}
|
|
if err := c.Log.Validate(); err != nil {
|
|
return err
|
|
}
|
|
if err := c.Endpoints.Validate(); err != nil {
|
|
return err
|
|
}
|
|
if err := c.Errors.Validate(); err != nil {
|
|
return err
|
|
}
|
|
encoder := c.ResponseEncoder
|
|
if encoder == nil {
|
|
f, err := response.New(c.Response)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
encoder = f
|
|
}
|
|
// Check the same composition that App.New installs, including custom
|
|
// encoders and their existing catalogs, before App construction.
|
|
if _, err := response.WithErrors(c.Errors, encoder); err != nil {
|
|
return err
|
|
}
|
|
if err := c.Response.Validate(); err != nil {
|
|
return err
|
|
}
|
|
if c.SlowRequestThreshold < 0 {
|
|
return &ConfigError{Field: "slowRequestThreshold", Message: "must be nonnegative"}
|
|
}
|
|
if strings.TrimSpace(c.Address) == "" {
|
|
return &ConfigError{Field: "address", Message: "is required"}
|
|
}
|
|
if strings.TrimSpace(c.RequestIDHeader) == "" {
|
|
return &ConfigError{Field: "request_id_header", Message: "is required"}
|
|
}
|
|
if c.ShutdownTimeout <= 0 {
|
|
return &ConfigError{Field: "shutdown_timeout", Message: "must be positive"}
|
|
}
|
|
if _, err := httpx.NewClientIPResolver(c.TrustedProxies...); err != nil {
|
|
return &ConfigError{Field: "trusted_proxies", Message: err.Error()}
|
|
}
|
|
return c.ValidateTransport()
|
|
}
|
|
|
|
// ValidateTransport checks built-in transport selection without opening files
|
|
// or sockets. Empty Transport preserves the standard net/http default.
|
|
func (c Config) ValidateTransport() error {
|
|
if err := c.HTTP.Validate(); err != nil {
|
|
return err
|
|
}
|
|
switch c.Transport {
|
|
case "", TransportNetHTTP, TransportNative, TransportHTTP2:
|
|
default:
|
|
return &ConfigError{Field: "transport", Message: "must be nethttp, native or http2"}
|
|
}
|
|
if c.Transport == TransportHTTP2 && (c.HTTP.ReadHeaderTimeout > 0 || c.HTTP.ReadTimeout > 0 || c.HTTP.WriteTimeout > 0) {
|
|
return &ConfigError{Field: "http", Message: "readHeaderTimeout, readTimeout and writeTimeout are not supported by the experimental http2 transport"}
|
|
}
|
|
if c.HTTP.ErrorLog != nil && c.Transport != "" && c.Transport != TransportNetHTTP {
|
|
return &ConfigError{Field: "http.errorLog", Message: "is supported only by the nethttp transport"}
|
|
}
|
|
cert, key := strings.TrimSpace(c.TLS.CertFile), strings.TrimSpace(c.TLS.KeyFile)
|
|
if (cert == "") != (key == "") {
|
|
return &ConfigError{Field: "tls", Message: "requires both certFile and keyFile"}
|
|
}
|
|
if c.Transport == TransportHTTP2 && cert == "" {
|
|
return &ConfigError{Field: "tls", Message: "is required for the experimental http2 transport"}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ConfigError describes an invalid application configuration field.
|
|
type ConfigError struct {
|
|
Field string
|
|
Message string
|
|
}
|
|
|
|
func (e *ConfigError) Error() string { return "config " + e.Field + " " + e.Message }
|
|
|
|
// Option changes an App's configuration.
|
|
type Option func(*Config)
|
|
|
|
// WithTransport selects a built-in server. HTTP/2 is experimental and requires TLS.
|
|
func WithTransport(name string) Option {
|
|
return func(cfg *Config) { cfg.Transport = name }
|
|
}
|
|
|
|
// WithTLS enables HTTPS. Certificates are loaded before Run opens its listener.
|
|
func WithTLS(certFile, keyFile string) Option {
|
|
return func(cfg *Config) { cfg.TLS = TLSConfig{CertFile: certFile, KeyFile: keyFile} }
|
|
}
|
|
|
|
// WithRequestIDHeader changes the header used to read and return request IDs.
|
|
func WithRequestIDHeader(header string) Option {
|
|
return func(cfg *Config) {
|
|
if value := strings.TrimSpace(header); value != "" {
|
|
cfg.RequestIDHeader = value
|
|
}
|
|
}
|
|
}
|
|
|
|
// WithLogger enables GoOmni's structured access logger.
|
|
func WithLogger(enabled bool) Option {
|
|
return func(cfg *Config) { cfg.EnableLogger = enabled }
|
|
}
|
|
|
|
// WithSlowRequestThreshold enables access logs and warns after this duration.
|
|
func WithSlowRequestThreshold(threshold time.Duration) Option {
|
|
return func(cfg *Config) { cfg.SlowRequestThreshold = threshold; cfg.EnableLogger = true }
|
|
}
|
|
|
|
// WithTrustedProxies allows only these IPs/CIDRs to supply X-Forwarded-For.
|
|
// An empty list disables forwarding-header trust. Invalid entries are rejected
|
|
// by Config.Validate and panic when passed directly to App.New.
|
|
func WithTrustedProxies(proxies ...string) Option {
|
|
proxies = append([]string(nil), proxies...)
|
|
return func(cfg *Config) { cfg.TrustedProxies = append([]string(nil), proxies...) }
|
|
}
|
|
|
|
// WithConfig applies a complete configuration loaded from a file or
|
|
// environment. Later options can still override individual fields.
|
|
func WithConfig(settings Config) Option {
|
|
settings.Response = settings.Response.Clone()
|
|
settings.Errors = settings.Errors.Clone()
|
|
settings.TrustedProxies = append([]string(nil), settings.TrustedProxies...)
|
|
settings.Log.RedactKeys = append([]string(nil), settings.Log.RedactKeys...)
|
|
return func(cfg *Config) {
|
|
*cfg = settings
|
|
cfg.Response = settings.Response.Clone()
|
|
cfg.Errors = settings.Errors.Clone()
|
|
cfg.TrustedProxies = append([]string(nil), settings.TrustedProxies...)
|
|
cfg.Log.RedactKeys = append([]string(nil), settings.Log.RedactKeys...)
|
|
}
|
|
}
|
|
|
|
// WithResponse configures this application's default JSON envelope.
|
|
func WithResponse(settings response.Config) Option {
|
|
settings = settings.Clone()
|
|
return func(cfg *Config) { cfg.Response = settings.Clone(); cfg.ResponseEncoder = nil }
|
|
}
|
|
|
|
// WithResponseEncoder replaces the default encoder with application code.
|
|
// OpenAPI for custom encoders must describe their actual wire contract explicitly.
|
|
func WithResponseEncoder(encoder response.Encoder) Option {
|
|
return func(cfg *Config) { cfg.ResponseEncoder = encoder }
|
|
}
|
|
|
|
// WithLogConfig configures the logger used by the application runtime.
|
|
func WithLogConfig(settings logx.Config) Option {
|
|
settings.RedactKeys = append([]string(nil), settings.RedactKeys...)
|
|
return func(cfg *Config) {
|
|
cfg.Log = settings
|
|
cfg.Log.RedactKeys = append([]string(nil), settings.RedactKeys...)
|
|
}
|
|
}
|
|
|
|
// WithShutdownTimeout controls how long RunContext waits for active requests.
|
|
func WithShutdownTimeout(timeout time.Duration) Option {
|
|
return func(cfg *Config) {
|
|
if timeout > 0 {
|
|
cfg.ShutdownTimeout = timeout
|
|
}
|
|
}
|
|
}
|
|
|
|
// WithErrors installs a project-owned, app-local catalog.
|
|
func WithErrors(catalog apierrors.Catalog) Option {
|
|
catalog = catalog.Clone()
|
|
return func(cfg *Config) { cfg.Errors = catalog.Clone() }
|
|
}
|
|
|
|
// WithLogInstance borrows a logger for this App without changing process logging.
|
|
func WithLogInstance(logger *logx.Logger) Option { return func(c *Config) { c.LogInstance = logger } }
|
|
|
|
func WithRequestPolicy(policy contract.RequestPolicy) Option {
|
|
return func(c *Config) { c.Request = policy }
|
|
}
|