feat: bootstrap reusable GoOmni framework

This commit is contained in:
2026-09-20 22:06:24 +08:00
commit 8b19aa79ee
60 changed files with 4923 additions and 0 deletions

93
config/config.go Normal file
View File

@@ -0,0 +1,93 @@
package config
import (
"strings"
"time"
"git.yuntuo.hk/k2safe/go-omni/logx"
)
const defaultRequestIDHeader = "X-Request-ID"
// Config controls the default runtime behavior of an App.
type Config struct {
Mode string
Address string
RequestIDHeader string
EnableLogger bool
ShutdownTimeout time.Duration
Log logx.Config
}
func Default() Config {
return Config{
Address: ":8080",
RequestIDHeader: defaultRequestIDHeader,
ShutdownTimeout: 10 * time.Second,
Log: logx.Default(),
}
}
// Validate checks configuration values before an application starts.
func (c Config) Validate() error {
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"}
}
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)
// WithMode sets the default transport's running mode.
func WithMode(mode string) Option {
return func(cfg *Config) { cfg.Mode = mode }
}
// 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 }
}
// WithConfig applies a complete configuration loaded from a file or
// environment. Later options can still override individual fields.
func WithConfig(settings Config) Option {
return func(cfg *Config) { *cfg = settings }
}
// WithLogConfig configures the logger used by the application runtime.
func WithLogConfig(settings logx.Config) Option {
return func(cfg *Config) { cfg.Log = settings }
}
// 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
}
}
}

95
config/config_test.go Normal file
View File

@@ -0,0 +1,95 @@
package config
import (
"strings"
"testing"
"time"
)
func TestLoadFromEnvUsesDefaults(t *testing.T) {
for _, key := range []string{
"GO_OMNI_MODE", "GO_OMNI_HTTP_ADDR", "GO_OMNI_REQUEST_ID_HEADER", "GO_OMNI_ENABLE_LOGGER",
"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",
} {
t.Setenv(key, "")
}
cfg, err := LoadFromEnv("")
if err != nil {
t.Fatal(err)
}
if cfg.Address != ":8080" || 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" {
t.Fatalf("unexpected logger defaults: %+v", cfg.Log)
}
}
func TestLoadFromEnvOverridesSettings(t *testing.T) {
t.Setenv("GO_OMNI_MODE", "release")
t.Setenv("GO_OMNI_HTTP_ADDR", "127.0.0.1:9090")
t.Setenv("GO_OMNI_REQUEST_ID_HEADER", "X-Correlation-ID")
t.Setenv("GO_OMNI_ENABLE_LOGGER", "true")
t.Setenv("GO_OMNI_LOG_LEVEL", "debug")
t.Setenv("GO_OMNI_LOG_FORMAT", "text")
t.Setenv("GO_OMNI_LOG_OUTPUT", "stdout")
t.Setenv("GO_OMNI_SERVICE", "billing")
t.Setenv("GO_OMNI_ENVIRONMENT", "test")
t.Setenv("GO_OMNI_LOG_ADD_SOURCE", "true")
t.Setenv("GO_OMNI_SHUTDOWN_TIMEOUT", "3s")
cfg, err := LoadFromEnv("")
if err != nil {
t.Fatal(err)
}
if cfg.Mode != "release" || cfg.Address != "127.0.0.1:9090" || cfg.RequestIDHeader != "X-Correlation-ID" || !cfg.EnableLogger {
t.Fatalf("unexpected application settings: %+v", cfg)
}
if cfg.ShutdownTimeout != 3*time.Second || cfg.Log.Level != "debug" || cfg.Log.Format != "text" || cfg.Log.Output != "stdout" || cfg.Log.Service != "billing" || cfg.Log.Environment != "test" || !cfg.Log.AddSource {
t.Fatalf("unexpected logger settings: %+v", cfg.Log)
}
}
func TestLoadFromEnvRejectsInvalidValues(t *testing.T) {
t.Setenv("GO_OMNI_ENABLE_LOGGER", "sometimes")
if _, err := LoadFromEnv(""); err == nil || !strings.Contains(err.Error(), "ENABLE_LOGGER") {
t.Fatalf("expected invalid logger flag error, got %v", err)
}
t.Setenv("GO_OMNI_ENABLE_LOGGER", "")
t.Setenv("GO_OMNI_SHUTDOWN_TIMEOUT", "later")
if _, err := LoadFromEnv(""); err == nil || !strings.Contains(err.Error(), "SHUTDOWN_TIMEOUT") {
t.Fatalf("expected invalid timeout error, got %v", err)
}
t.Setenv("GO_OMNI_SHUTDOWN_TIMEOUT", "1s")
cfg := Default()
cfg.Address = ""
if err := cfg.Validate(); err == nil || !strings.Contains(err.Error(), "address") {
t.Fatalf("expected invalid address error, got %v", err)
}
cfg = Default()
cfg.RequestIDHeader = " "
if err := cfg.Validate(); err == nil || !strings.Contains(err.Error(), "request_id_header") {
t.Fatalf("expected invalid request header error, got %v", err)
}
}
func TestWithConfigAndWithLogConfig(t *testing.T) {
settings := Default()
settings.Mode = "release"
settings.Log.Service = "orders"
cfg := Default()
WithConfig(settings)(&cfg)
if cfg.Mode != "release" || cfg.Log.Service != "orders" {
t.Fatalf("complete config was not applied: %+v", cfg)
}
WithLogConfig(Default().Log)(&cfg)
if cfg.Log.Service != "" {
t.Fatalf("log config was not replaced: %+v", cfg.Log)
}
}

67
config/env.go Normal file
View File

@@ -0,0 +1,67 @@
package config
import (
"fmt"
"os"
"strconv"
"strings"
"time"
)
const defaultEnvPrefix = "GO_OMNI_"
// LoadFromEnv loads application settings from GO_OMNI_* variables. The
// database settings remain owned by database.ConfigFromEnv.
func LoadFromEnv(prefix string) (Config, error) {
prefix = strings.TrimSpace(prefix)
if prefix == "" {
prefix = defaultEnvPrefix
}
cfg := Default()
cfg.Mode = envString(prefix+"MODE", cfg.Mode)
cfg.Address = envString(prefix+"HTTP_ADDR", cfg.Address)
cfg.RequestIDHeader = envString(prefix+"REQUEST_ID_HEADER", cfg.RequestIDHeader)
cfg.Log.Level = envString(prefix+"LOG_LEVEL", cfg.Log.Level)
cfg.Log.Format = envString(prefix+"LOG_FORMAT", cfg.Log.Format)
cfg.Log.Output = envString(prefix+"LOG_OUTPUT", cfg.Log.Output)
cfg.Log.File = envString(prefix+"LOG_FILE", cfg.Log.File)
cfg.Log.Service = envString(prefix+"SERVICE", cfg.Log.Service)
cfg.Log.Environment = envString(prefix+"ENVIRONMENT", cfg.Log.Environment)
var err error
if cfg.EnableLogger, err = envBool(prefix+"ENABLE_LOGGER", cfg.EnableLogger); err != nil {
return Config{}, err
}
if cfg.Log.AddSource, err = envBool(prefix+"LOG_ADD_SOURCE", cfg.Log.AddSource); err != nil {
return Config{}, err
}
if value := strings.TrimSpace(os.Getenv(prefix + "SHUTDOWN_TIMEOUT")); value != "" {
cfg.ShutdownTimeout, err = time.ParseDuration(value)
if err != nil {
return Config{}, fmt.Errorf("%sSHUTDOWN_TIMEOUT must be a duration: %w", prefix, err)
}
}
if err := cfg.Validate(); err != nil {
return Config{}, err
}
return cfg, nil
}
func envString(key, fallback string) string {
if value := strings.TrimSpace(os.Getenv(key)); value != "" {
return value
}
return fallback
}
func envBool(key string, fallback bool) (bool, error) {
value := strings.TrimSpace(os.Getenv(key))
if value == "" {
return fallback, nil
}
parsed, err := strconv.ParseBool(value)
if err != nil {
return false, fmt.Errorf("%s must be a boolean: %w", key, err)
}
return parsed, nil
}