feat: user service

This commit is contained in:
Ilia Denisov
2026-04-10 19:05:02 +02:00
committed by GitHub
parent 710bad712e
commit 23ffcb7535
140 changed files with 33418 additions and 952 deletions
+551
View File
@@ -0,0 +1,551 @@
// Package config loads the user-service process configuration from environment
// variables.
package config
import (
"crypto/tls"
"fmt"
"net"
"os"
"strconv"
"strings"
"time"
)
const (
shutdownTimeoutEnvVar = "USERSERVICE_SHUTDOWN_TIMEOUT"
logLevelEnvVar = "USERSERVICE_LOG_LEVEL"
internalHTTPAddrEnvVar = "USERSERVICE_INTERNAL_HTTP_ADDR"
internalHTTPReadHeaderTimeoutEnvVar = "USERSERVICE_INTERNAL_HTTP_READ_HEADER_TIMEOUT"
internalHTTPReadTimeoutEnvVar = "USERSERVICE_INTERNAL_HTTP_READ_TIMEOUT"
internalHTTPIdleTimeoutEnvVar = "USERSERVICE_INTERNAL_HTTP_IDLE_TIMEOUT"
internalHTTPRequestTimeoutEnvVar = "USERSERVICE_INTERNAL_HTTP_REQUEST_TIMEOUT"
adminHTTPAddrEnvVar = "USERSERVICE_ADMIN_HTTP_ADDR"
adminHTTPReadHeaderTimeoutEnvVar = "USERSERVICE_ADMIN_HTTP_READ_HEADER_TIMEOUT"
adminHTTPReadTimeoutEnvVar = "USERSERVICE_ADMIN_HTTP_READ_TIMEOUT"
adminHTTPIdleTimeoutEnvVar = "USERSERVICE_ADMIN_HTTP_IDLE_TIMEOUT"
redisAddrEnvVar = "USERSERVICE_REDIS_ADDR"
redisUsernameEnvVar = "USERSERVICE_REDIS_USERNAME"
redisPasswordEnvVar = "USERSERVICE_REDIS_PASSWORD"
redisDBEnvVar = "USERSERVICE_REDIS_DB"
redisTLSEnabledEnvVar = "USERSERVICE_REDIS_TLS_ENABLED"
redisOperationTimeoutEnvVar = "USERSERVICE_REDIS_OPERATION_TIMEOUT"
redisKeyspacePrefixEnvVar = "USERSERVICE_REDIS_KEYSPACE_PREFIX"
redisDomainEventsStreamEnvVar = "USERSERVICE_REDIS_DOMAIN_EVENTS_STREAM"
redisDomainEventsStreamMaxLenEnvVar = "USERSERVICE_REDIS_DOMAIN_EVENTS_STREAM_MAX_LEN"
otelServiceNameEnvVar = "OTEL_SERVICE_NAME"
otelTracesExporterEnvVar = "OTEL_TRACES_EXPORTER"
otelMetricsExporterEnvVar = "OTEL_METRICS_EXPORTER"
otelExporterOTLPProtocolEnvVar = "OTEL_EXPORTER_OTLP_PROTOCOL"
otelExporterOTLPTracesProtocolEnvVar = "OTEL_EXPORTER_OTLP_TRACES_PROTOCOL"
otelExporterOTLPMetricsProtocolEnvVar = "OTEL_EXPORTER_OTLP_METRICS_PROTOCOL"
otelStdoutTracesEnabledEnvVar = "USERSERVICE_OTEL_STDOUT_TRACES_ENABLED"
otelStdoutMetricsEnabledEnvVar = "USERSERVICE_OTEL_STDOUT_METRICS_ENABLED"
defaultShutdownTimeout = 5 * time.Second
defaultLogLevel = "info"
defaultInternalHTTPAddr = ":8091"
defaultAdminHTTPAddr = ""
defaultReadHeaderTimeout = 2 * time.Second
defaultReadTimeout = 10 * time.Second
defaultIdleTimeout = time.Minute
defaultRequestTimeout = 3 * time.Second
defaultRedisDB = 0
defaultRedisOperationTimeout = 250 * time.Millisecond
defaultRedisKeyspacePrefix = "user:"
defaultDomainEventsStream = "user:domain_events"
defaultDomainEventsStreamMaxLen = 1024
defaultOTelServiceName = "galaxy-user"
otelExporterNone = "none"
otelExporterOTLP = "otlp"
otelProtocolHTTPProtobuf = "http/protobuf"
otelProtocolGRPC = "grpc"
)
// Config stores the full user-service process configuration.
type Config struct {
// ShutdownTimeout bounds graceful shutdown of the long-lived listeners and
// runtime resources.
ShutdownTimeout time.Duration
// Logging configures the process-wide logger.
Logging LoggingConfig
// InternalHTTP configures the trusted internal HTTP listener.
InternalHTTP InternalHTTPConfig
// AdminHTTP configures the optional private admin HTTP listener.
AdminHTTP AdminHTTPConfig
// Redis configures the Redis-backed user store and domain-event publisher.
Redis RedisConfig
// Telemetry configures the process-wide OpenTelemetry runtime.
Telemetry TelemetryConfig
}
// LoggingConfig configures the process-wide logger.
type LoggingConfig struct {
// Level stores the process log level.
Level string
}
// InternalHTTPConfig configures the internal HTTP listener.
type InternalHTTPConfig struct {
// Addr stores the TCP listen address.
Addr string
// ReadHeaderTimeout bounds request-header reading.
ReadHeaderTimeout time.Duration
// ReadTimeout bounds reading one request.
ReadTimeout time.Duration
// IdleTimeout bounds how long keep-alive connections stay open.
IdleTimeout time.Duration
// RequestTimeout bounds one application-layer request execution.
RequestTimeout time.Duration
}
// Validate reports whether cfg stores a usable internal HTTP listener
// configuration.
func (cfg InternalHTTPConfig) Validate() error {
switch {
case strings.TrimSpace(cfg.Addr) == "":
return fmt.Errorf("internal HTTP addr must not be empty")
case cfg.ReadHeaderTimeout <= 0:
return fmt.Errorf("internal HTTP read header timeout must be positive")
case cfg.ReadTimeout <= 0:
return fmt.Errorf("internal HTTP read timeout must be positive")
case cfg.IdleTimeout <= 0:
return fmt.Errorf("internal HTTP idle timeout must be positive")
case cfg.RequestTimeout <= 0:
return fmt.Errorf("internal HTTP request timeout must be positive")
default:
return nil
}
}
// AdminHTTPConfig describes the private operational HTTP listener used for
// Prometheus metrics exposure. The listener remains disabled when Addr is
// empty.
type AdminHTTPConfig struct {
// Addr stores the TCP listen address used by the admin HTTP server.
Addr string
// ReadHeaderTimeout bounds request-header reading.
ReadHeaderTimeout time.Duration
// ReadTimeout bounds reading one request.
ReadTimeout time.Duration
// IdleTimeout bounds how long keep-alive connections stay open.
IdleTimeout time.Duration
}
// Validate reports whether cfg stores a usable optional admin HTTP listener
// configuration.
func (cfg AdminHTTPConfig) Validate() error {
if strings.TrimSpace(cfg.Addr) == "" {
return nil
}
switch {
case cfg.ReadHeaderTimeout <= 0:
return fmt.Errorf("admin HTTP read header timeout must be positive")
case cfg.ReadTimeout <= 0:
return fmt.Errorf("admin HTTP read timeout must be positive")
case cfg.IdleTimeout <= 0:
return fmt.Errorf("admin HTTP idle timeout must be positive")
default:
return nil
}
}
// RedisConfig configures the Redis-backed store and domain-event publisher.
type RedisConfig struct {
// Addr stores the Redis network address.
Addr string
// Username stores the optional Redis ACL username.
Username string
// Password stores the optional Redis ACL password.
Password string
// DB stores the Redis logical database index.
DB int
// TLSEnabled reports whether TLS must be used for Redis connections.
TLSEnabled bool
// OperationTimeout bounds one Redis round trip.
OperationTimeout time.Duration
// KeyspacePrefix stores the root prefix of the service-owned Redis keyspace.
KeyspacePrefix string
// DomainEventsStream stores the Redis Stream key used for auxiliary
// post-commit domain events.
DomainEventsStream string
// DomainEventsStreamMaxLen bounds the domain-events Redis Stream with
// approximate trimming.
DomainEventsStreamMaxLen int64
}
// TLSConfig returns the conservative TLS configuration used by Redis adapters
// when TLSEnabled is true.
func (cfg RedisConfig) TLSConfig() *tls.Config {
if !cfg.TLSEnabled {
return nil
}
return &tls.Config{MinVersion: tls.VersionTLS12}
}
// Validate reports whether cfg stores a usable Redis configuration.
func (cfg RedisConfig) Validate() error {
switch {
case strings.TrimSpace(cfg.Addr) == "":
return fmt.Errorf("redis addr must not be empty")
case cfg.DB < 0:
return fmt.Errorf("redis db must not be negative")
case cfg.OperationTimeout <= 0:
return fmt.Errorf("redis operation timeout must be positive")
case strings.TrimSpace(cfg.KeyspacePrefix) == "":
return fmt.Errorf("redis keyspace prefix must not be empty")
case strings.TrimSpace(cfg.DomainEventsStream) == "":
return fmt.Errorf("redis domain events stream must not be empty")
case cfg.DomainEventsStreamMaxLen <= 0:
return fmt.Errorf("redis domain events stream max len must be positive")
default:
return nil
}
}
// TelemetryConfig configures the user-service OpenTelemetry runtime.
type TelemetryConfig struct {
// ServiceName overrides the default OpenTelemetry service name.
ServiceName string
// TracesExporter selects the external traces exporter. Supported values are
// `none` and `otlp`.
TracesExporter string
// MetricsExporter selects the external metrics exporter. Supported values
// are `none` and `otlp`.
MetricsExporter string
// TracesProtocol selects the OTLP traces protocol when TracesExporter is
// `otlp`.
TracesProtocol string
// MetricsProtocol selects the OTLP metrics protocol when MetricsExporter is
// `otlp`.
MetricsProtocol string
// StdoutTracesEnabled enables the additional stdout trace exporter used for
// local development and debugging.
StdoutTracesEnabled bool
// StdoutMetricsEnabled enables the additional stdout metric exporter used
// for local development and debugging.
StdoutMetricsEnabled bool
}
// Validate reports whether cfg contains a supported OpenTelemetry exporter
// configuration.
func (cfg TelemetryConfig) Validate() error {
switch cfg.TracesExporter {
case otelExporterNone, otelExporterOTLP:
default:
return fmt.Errorf("%s %q is unsupported", otelTracesExporterEnvVar, cfg.TracesExporter)
}
switch cfg.MetricsExporter {
case otelExporterNone, otelExporterOTLP:
default:
return fmt.Errorf("%s %q is unsupported", otelMetricsExporterEnvVar, cfg.MetricsExporter)
}
if cfg.TracesProtocol != "" && cfg.TracesProtocol != otelProtocolHTTPProtobuf && cfg.TracesProtocol != otelProtocolGRPC {
return fmt.Errorf("%s %q is unsupported", otelExporterOTLPTracesProtocolEnvVar, cfg.TracesProtocol)
}
if cfg.MetricsProtocol != "" && cfg.MetricsProtocol != otelProtocolHTTPProtobuf && cfg.MetricsProtocol != otelProtocolGRPC {
return fmt.Errorf("%s %q is unsupported", otelExporterOTLPMetricsProtocolEnvVar, cfg.MetricsProtocol)
}
return nil
}
// DefaultAdminHTTPConfig returns the default settings for the optional private
// admin HTTP listener.
func DefaultAdminHTTPConfig() AdminHTTPConfig {
return AdminHTTPConfig{
Addr: defaultAdminHTTPAddr,
ReadHeaderTimeout: defaultReadHeaderTimeout,
ReadTimeout: defaultReadTimeout,
IdleTimeout: defaultIdleTimeout,
}
}
// DefaultConfig returns the default process configuration with all optional
// values filled.
func DefaultConfig() Config {
return Config{
ShutdownTimeout: defaultShutdownTimeout,
Logging: LoggingConfig{
Level: defaultLogLevel,
},
InternalHTTP: InternalHTTPConfig{
Addr: defaultInternalHTTPAddr,
ReadHeaderTimeout: defaultReadHeaderTimeout,
ReadTimeout: defaultReadTimeout,
IdleTimeout: defaultIdleTimeout,
RequestTimeout: defaultRequestTimeout,
},
AdminHTTP: DefaultAdminHTTPConfig(),
Redis: RedisConfig{
DB: defaultRedisDB,
OperationTimeout: defaultRedisOperationTimeout,
KeyspacePrefix: defaultRedisKeyspacePrefix,
DomainEventsStream: defaultDomainEventsStream,
DomainEventsStreamMaxLen: defaultDomainEventsStreamMaxLen,
},
Telemetry: TelemetryConfig{
ServiceName: defaultOTelServiceName,
TracesExporter: otelExporterNone,
MetricsExporter: otelExporterNone,
},
}
}
// Validate reports whether cfg is process-ready.
func (cfg Config) Validate() error {
switch {
case cfg.ShutdownTimeout <= 0:
return fmt.Errorf("shutdown timeout must be positive")
}
if err := cfg.InternalHTTP.Validate(); err != nil {
return fmt.Errorf("internal HTTP config: %w", err)
}
if err := cfg.AdminHTTP.Validate(); err != nil {
return fmt.Errorf("admin HTTP config: %w", err)
}
if err := cfg.Redis.Validate(); err != nil {
return fmt.Errorf("redis config: %w", err)
}
if _, err := parseLogLevel(cfg.Logging.Level); err != nil {
return fmt.Errorf("logging config: %w", err)
}
if err := cfg.Telemetry.Validate(); err != nil {
return fmt.Errorf("telemetry config: %w", err)
}
return nil
}
// LoadFromEnv loads Config from the process environment.
func LoadFromEnv() (Config, error) {
cfg := DefaultConfig()
var err error
cfg.ShutdownTimeout, err = loadDuration(shutdownTimeoutEnvVar, cfg.ShutdownTimeout)
if err != nil {
return Config{}, err
}
cfg.Logging.Level = loadString(logLevelEnvVar, cfg.Logging.Level)
cfg.InternalHTTP.Addr = loadString(internalHTTPAddrEnvVar, cfg.InternalHTTP.Addr)
cfg.InternalHTTP.ReadHeaderTimeout, err = loadDuration(internalHTTPReadHeaderTimeoutEnvVar, cfg.InternalHTTP.ReadHeaderTimeout)
if err != nil {
return Config{}, err
}
cfg.InternalHTTP.ReadTimeout, err = loadDuration(internalHTTPReadTimeoutEnvVar, cfg.InternalHTTP.ReadTimeout)
if err != nil {
return Config{}, err
}
cfg.InternalHTTP.IdleTimeout, err = loadDuration(internalHTTPIdleTimeoutEnvVar, cfg.InternalHTTP.IdleTimeout)
if err != nil {
return Config{}, err
}
cfg.InternalHTTP.RequestTimeout, err = loadDuration(internalHTTPRequestTimeoutEnvVar, cfg.InternalHTTP.RequestTimeout)
if err != nil {
return Config{}, err
}
cfg.AdminHTTP.Addr = loadString(adminHTTPAddrEnvVar, cfg.AdminHTTP.Addr)
cfg.AdminHTTP.ReadHeaderTimeout, err = loadDuration(adminHTTPReadHeaderTimeoutEnvVar, cfg.AdminHTTP.ReadHeaderTimeout)
if err != nil {
return Config{}, err
}
cfg.AdminHTTP.ReadTimeout, err = loadDuration(adminHTTPReadTimeoutEnvVar, cfg.AdminHTTP.ReadTimeout)
if err != nil {
return Config{}, err
}
cfg.AdminHTTP.IdleTimeout, err = loadDuration(adminHTTPIdleTimeoutEnvVar, cfg.AdminHTTP.IdleTimeout)
if err != nil {
return Config{}, err
}
cfg.Redis.Addr = loadString(redisAddrEnvVar, cfg.Redis.Addr)
cfg.Redis.Username = loadString(redisUsernameEnvVar, cfg.Redis.Username)
cfg.Redis.Password = loadString(redisPasswordEnvVar, cfg.Redis.Password)
cfg.Redis.DB, err = loadInt(redisDBEnvVar, cfg.Redis.DB)
if err != nil {
return Config{}, err
}
cfg.Redis.TLSEnabled, err = loadBool(redisTLSEnabledEnvVar, cfg.Redis.TLSEnabled)
if err != nil {
return Config{}, err
}
cfg.Redis.OperationTimeout, err = loadDuration(redisOperationTimeoutEnvVar, cfg.Redis.OperationTimeout)
if err != nil {
return Config{}, err
}
cfg.Redis.KeyspacePrefix = loadString(redisKeyspacePrefixEnvVar, cfg.Redis.KeyspacePrefix)
cfg.Redis.DomainEventsStream = loadString(redisDomainEventsStreamEnvVar, cfg.Redis.DomainEventsStream)
cfg.Redis.DomainEventsStreamMaxLen, err = loadInt64(redisDomainEventsStreamMaxLenEnvVar, cfg.Redis.DomainEventsStreamMaxLen)
if err != nil {
return Config{}, err
}
cfg.Telemetry.ServiceName = loadString(otelServiceNameEnvVar, cfg.Telemetry.ServiceName)
cfg.Telemetry.TracesExporter = normalizeExporterValue(loadString(otelTracesExporterEnvVar, cfg.Telemetry.TracesExporter))
cfg.Telemetry.MetricsExporter = normalizeExporterValue(loadString(otelMetricsExporterEnvVar, cfg.Telemetry.MetricsExporter))
cfg.Telemetry.TracesProtocol = loadOTLPProtocol(
os.Getenv(otelExporterOTLPTracesProtocolEnvVar),
os.Getenv(otelExporterOTLPProtocolEnvVar),
cfg.Telemetry.TracesExporter,
)
cfg.Telemetry.MetricsProtocol = loadOTLPProtocol(
os.Getenv(otelExporterOTLPMetricsProtocolEnvVar),
os.Getenv(otelExporterOTLPProtocolEnvVar),
cfg.Telemetry.MetricsExporter,
)
cfg.Telemetry.StdoutTracesEnabled, err = loadBool(otelStdoutTracesEnabledEnvVar, cfg.Telemetry.StdoutTracesEnabled)
if err != nil {
return Config{}, err
}
cfg.Telemetry.StdoutMetricsEnabled, err = loadBool(otelStdoutMetricsEnabledEnvVar, cfg.Telemetry.StdoutMetricsEnabled)
if err != nil {
return Config{}, err
}
if err := cfg.Validate(); err != nil {
return Config{}, err
}
return cfg, nil
}
func loadString(envName string, defaultValue string) string {
value, ok := os.LookupEnv(envName)
if !ok {
return defaultValue
}
return strings.TrimSpace(value)
}
func loadDuration(envName string, defaultValue time.Duration) (time.Duration, error) {
value, ok := os.LookupEnv(envName)
if !ok {
return defaultValue, nil
}
duration, err := time.ParseDuration(strings.TrimSpace(value))
if err != nil {
return 0, fmt.Errorf("%s: parse duration: %w", envName, err)
}
return duration, nil
}
func loadInt(envName string, defaultValue int) (int, error) {
value, ok := os.LookupEnv(envName)
if !ok {
return defaultValue, nil
}
parsedValue, err := strconv.Atoi(strings.TrimSpace(value))
if err != nil {
return 0, fmt.Errorf("%s: parse int: %w", envName, err)
}
return parsedValue, nil
}
func loadInt64(envName string, defaultValue int64) (int64, error) {
value, ok := os.LookupEnv(envName)
if !ok {
return defaultValue, nil
}
parsedValue, err := strconv.ParseInt(strings.TrimSpace(value), 10, 64)
if err != nil {
return 0, fmt.Errorf("%s: parse int64: %w", envName, err)
}
return parsedValue, nil
}
func loadBool(envName string, defaultValue bool) (bool, error) {
value, ok := os.LookupEnv(envName)
if !ok {
return defaultValue, nil
}
parsedValue, err := strconv.ParseBool(strings.TrimSpace(value))
if err != nil {
return false, fmt.Errorf("%s: parse bool: %w", envName, err)
}
return parsedValue, nil
}
func parseLogLevel(value string) (string, error) {
switch strings.ToLower(strings.TrimSpace(value)) {
case "debug", "info", "warn", "error":
return value, nil
default:
return "", fmt.Errorf("unsupported log level %q", value)
}
}
func normalizeExporterValue(value string) string {
switch strings.TrimSpace(value) {
case "", otelExporterNone:
return otelExporterNone
default:
return strings.TrimSpace(value)
}
}
func loadOTLPProtocol(primary string, fallback string, exporter string) string {
protocol := strings.TrimSpace(primary)
if protocol == "" {
protocol = strings.TrimSpace(fallback)
}
if protocol == "" && exporter == otelExporterOTLP {
return otelProtocolHTTPProtobuf
}
return protocol
}
// ListenAddress returns the resolved listen address used by tests and process
// startup.
func (cfg InternalHTTPConfig) ListenAddress() string {
if strings.HasPrefix(cfg.Addr, ":") {
return net.JoinHostPort("", strings.TrimPrefix(cfg.Addr, ":"))
}
return cfg.Addr
}
+106
View File
@@ -0,0 +1,106 @@
package config
import (
"testing"
"time"
"github.com/stretchr/testify/require"
)
func TestLoadFromEnvUsesDefaults(t *testing.T) {
t.Setenv(redisAddrEnvVar, "127.0.0.1:6379")
cfg, err := LoadFromEnv()
require.NoError(t, err)
defaults := DefaultConfig()
require.Equal(t, defaults.ShutdownTimeout, cfg.ShutdownTimeout)
require.Equal(t, defaults.Logging.Level, cfg.Logging.Level)
require.Equal(t, defaults.InternalHTTP, cfg.InternalHTTP)
require.Equal(t, defaults.AdminHTTP, cfg.AdminHTTP)
require.Equal(t, "127.0.0.1:6379", cfg.Redis.Addr)
require.Equal(t, defaults.Redis.DB, cfg.Redis.DB)
require.Equal(t, defaults.Redis.DomainEventsStream, cfg.Redis.DomainEventsStream)
require.Equal(t, defaults.Redis.DomainEventsStreamMaxLen, cfg.Redis.DomainEventsStreamMaxLen)
require.Equal(t, defaults.Telemetry, cfg.Telemetry)
}
func TestLoadFromEnvAppliesOverrides(t *testing.T) {
t.Setenv(shutdownTimeoutEnvVar, "9s")
t.Setenv(logLevelEnvVar, "debug")
t.Setenv(internalHTTPAddrEnvVar, "127.0.0.1:18091")
t.Setenv(internalHTTPReadHeaderTimeoutEnvVar, "3s")
t.Setenv(internalHTTPRequestTimeoutEnvVar, "750ms")
t.Setenv(adminHTTPAddrEnvVar, "127.0.0.1:19091")
t.Setenv(adminHTTPIdleTimeoutEnvVar, "90s")
t.Setenv(redisAddrEnvVar, "127.0.0.1:6380")
t.Setenv(redisUsernameEnvVar, "alice")
t.Setenv(redisPasswordEnvVar, "secret")
t.Setenv(redisDBEnvVar, "3")
t.Setenv(redisTLSEnabledEnvVar, "true")
t.Setenv(redisOperationTimeoutEnvVar, "900ms")
t.Setenv(redisKeyspacePrefixEnvVar, "user:custom:")
t.Setenv(redisDomainEventsStreamEnvVar, "user:test_events")
t.Setenv(redisDomainEventsStreamMaxLenEnvVar, "2048")
t.Setenv(otelServiceNameEnvVar, "galaxy-user-stage12")
t.Setenv(otelTracesExporterEnvVar, "otlp")
t.Setenv(otelMetricsExporterEnvVar, "otlp")
t.Setenv(otelExporterOTLPTracesProtocolEnvVar, "grpc")
t.Setenv(otelExporterOTLPMetricsProtocolEnvVar, "http/protobuf")
t.Setenv(otelStdoutTracesEnabledEnvVar, "true")
t.Setenv(otelStdoutMetricsEnabledEnvVar, "true")
cfg, err := LoadFromEnv()
require.NoError(t, err)
require.Equal(t, 9*time.Second, cfg.ShutdownTimeout)
require.Equal(t, "debug", cfg.Logging.Level)
require.Equal(t, "127.0.0.1:18091", cfg.InternalHTTP.Addr)
require.Equal(t, 3*time.Second, cfg.InternalHTTP.ReadHeaderTimeout)
require.Equal(t, 750*time.Millisecond, cfg.InternalHTTP.RequestTimeout)
require.Equal(t, "127.0.0.1:19091", cfg.AdminHTTP.Addr)
require.Equal(t, 90*time.Second, cfg.AdminHTTP.IdleTimeout)
require.Equal(t, "127.0.0.1:6380", cfg.Redis.Addr)
require.Equal(t, "alice", cfg.Redis.Username)
require.Equal(t, "secret", cfg.Redis.Password)
require.Equal(t, 3, cfg.Redis.DB)
require.True(t, cfg.Redis.TLSEnabled)
require.Equal(t, 900*time.Millisecond, cfg.Redis.OperationTimeout)
require.Equal(t, "user:custom:", cfg.Redis.KeyspacePrefix)
require.Equal(t, "user:test_events", cfg.Redis.DomainEventsStream)
require.Equal(t, int64(2048), cfg.Redis.DomainEventsStreamMaxLen)
require.Equal(t, "galaxy-user-stage12", cfg.Telemetry.ServiceName)
require.Equal(t, "otlp", cfg.Telemetry.TracesExporter)
require.Equal(t, "otlp", cfg.Telemetry.MetricsExporter)
require.Equal(t, "grpc", cfg.Telemetry.TracesProtocol)
require.Equal(t, "http/protobuf", cfg.Telemetry.MetricsProtocol)
require.True(t, cfg.Telemetry.StdoutTracesEnabled)
require.True(t, cfg.Telemetry.StdoutMetricsEnabled)
}
func TestLoadFromEnvRejectsInvalidValues(t *testing.T) {
tests := []struct {
name string
envName string
envVal string
}{
{name: "invalid duration", envName: shutdownTimeoutEnvVar, envVal: "later"},
{name: "invalid bool", envName: redisTLSEnabledEnvVar, envVal: "sometimes"},
{name: "invalid log level", envName: logLevelEnvVar, envVal: "verbose"},
{name: "invalid int", envName: redisDBEnvVar, envVal: "db-three"},
{name: "invalid stream max len", envName: redisDomainEventsStreamMaxLenEnvVar, envVal: "many"},
{name: "invalid traces exporter", envName: otelTracesExporterEnvVar, envVal: "zipkin"},
{name: "invalid metrics protocol", envName: otelExporterOTLPMetricsProtocolEnvVar, envVal: "udp"},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Setenv(redisAddrEnvVar, "127.0.0.1:6379")
t.Setenv(tt.envName, tt.envVal)
_, err := LoadFromEnv()
require.Error(t, err)
})
}
}