feat: configure application transports and unify graceful shutdown

This commit is contained in:
2026-09-23 12:49:56 +08:00
parent 59bf04a461
commit 8e18528fc2
18 changed files with 842 additions and 96 deletions

View File

@@ -10,19 +10,34 @@ import (
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 {
Address string
RequestIDHeader string
EnableLogger bool
TrustedProxies []string
ShutdownTimeout time.Duration
Log logx.Config
Address string `json:"address"`
Transport string `json:"transport"`
TLS TLSConfig `json:"tls"`
RequestIDHeader string `json:"requestIdHeader"`
EnableLogger bool `json:"enableLogger"`
TrustedProxies []string `json:"trustedProxies"`
ShutdownTimeout time.Duration `json:"shutdownTimeout"`
Log logx.Config `json:"log"`
}
func Default() Config {
return Config{
Address: ":8080",
Transport: TransportNetHTTP,
RequestIDHeader: defaultRequestIDHeader,
ShutdownTimeout: 10 * time.Second,
Log: logx.Default(),
@@ -43,6 +58,24 @@ func (c Config) Validate() error {
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 {
switch c.Transport {
case "", TransportNetHTTP, TransportNative, TransportHTTP2:
default:
return &ConfigError{Field: "transport", Message: "must be nethttp, native or http2"}
}
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
}
@@ -57,6 +90,16 @@ func (e *ConfigError) Error() string { return "config " + e.Field + " " + e.Mess
// 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) {

View File

@@ -10,6 +10,7 @@ func TestLoadFromEnvUsesDefaults(t *testing.T) {
for _, key := range []string{
"GO_OMNI_HTTP_ADDR", "GO_OMNI_REQUEST_ID_HEADER", "GO_OMNI_ENABLE_LOGGER",
"GO_OMNI_TRUSTED_PROXIES",
"GO_OMNI_TRANSPORT", "GO_OMNI_TLS_CERT_FILE", "GO_OMNI_TLS_KEY_FILE",
"GO_OMNI_LOG_LEVEL", "GO_OMNI_LOG_FORMAT", "GO_OMNI_LOG_OUTPUT",
"GO_OMNI_LOG_FILE", "GO_OMNI_SERVICE", "GO_OMNI_ENVIRONMENT",
"GO_OMNI_LOG_ADD_SOURCE", "GO_OMNI_SHUTDOWN_TIMEOUT",
@@ -21,7 +22,7 @@ func TestLoadFromEnvUsesDefaults(t *testing.T) {
if err != nil {
t.Fatal(err)
}
if cfg.Address != ":8080" || cfg.RequestIDHeader != "X-Request-ID" || cfg.ShutdownTimeout != 10*time.Second {
if cfg.Address != ":8080" || cfg.Transport != TransportNetHTTP || cfg.TLS != (TLSConfig{}) || cfg.RequestIDHeader != "X-Request-ID" || cfg.ShutdownTimeout != 10*time.Second {
t.Fatalf("unexpected defaults: %+v", cfg)
}
if cfg.EnableLogger || cfg.Log.Level != "info" || cfg.Log.Format != "json" || cfg.Log.Output != "stderr" {
@@ -29,6 +30,44 @@ func TestLoadFromEnvUsesDefaults(t *testing.T) {
}
}
func TestTransportEnvironment(t *testing.T) {
for _, test := range []struct {
name, transport, cert, key, errorField string
}{
{"default", "", "", "", ""},
{"native", "native", "", "", ""},
{"nativeTLS", "native", "server.crt", "server.key", ""},
{"http2", "http2", "server.crt", "server.key", ""},
{"unknown", "typo", "", "", "transport"},
{"certOnly", "nethttp", "server.crt", "", "tls"},
{"keyOnly", "native", "", "server.key", "tls"},
{"http2WithoutTLS", "http2", "", "", "tls"},
} {
t.Run(test.name, func(t *testing.T) {
t.Setenv("TRANSPORT_TEST_TRANSPORT", test.transport)
t.Setenv("TRANSPORT_TEST_TLS_CERT_FILE", test.cert)
t.Setenv("TRANSPORT_TEST_TLS_KEY_FILE", test.key)
cfg, err := LoadFromEnv("TRANSPORT_TEST_")
if test.errorField != "" {
if err == nil || !strings.Contains(err.Error(), test.errorField) {
t.Fatalf("unexpected error: %v", err)
}
return
}
if err != nil {
t.Fatal(err)
}
want := test.transport
if want == "" {
want = TransportNetHTTP
}
if cfg.Transport != want || cfg.TLS.CertFile != test.cert || cfg.TLS.KeyFile != test.key {
t.Fatalf("settings: %+v", cfg)
}
})
}
}
func TestLoadFromEnvOverridesSettings(t *testing.T) {
t.Setenv("GO_OMNI_HTTP_ADDR", "127.0.0.1:9090")
t.Setenv("GO_OMNI_REQUEST_ID_HEADER", "X-Correlation-ID")

View File

@@ -19,6 +19,9 @@ func LoadFromEnv(prefix string) (Config, error) {
}
cfg := Default()
cfg.Address = envString(prefix+"HTTP_ADDR", cfg.Address)
cfg.Transport = envString(prefix+"TRANSPORT", cfg.Transport)
cfg.TLS.CertFile = envString(prefix+"TLS_CERT_FILE", "")
cfg.TLS.KeyFile = envString(prefix+"TLS_KEY_FILE", "")
cfg.RequestIDHeader = envString(prefix+"REQUEST_ID_HEADER", cfg.RequestIDHeader)
if value := strings.TrimSpace(os.Getenv(prefix + "TRUSTED_PROXIES")); value != "" {
cfg.TrustedProxies = strings.Split(value, ",")