diff --git a/cmd/client/main.go b/cmd/client/main.go index 0f9003d..daa5ce5 100644 --- a/cmd/client/main.go +++ b/cmd/client/main.go @@ -5,7 +5,6 @@ import ( "crypto/tls" "crypto/x509" "flag" - "log" "net" "os" "strings" @@ -14,21 +13,15 @@ import ( "github.com/dalbodeule/hop-gate/internal/dtls" "github.com/dalbodeule/hop-gate/internal/logging" "github.com/dalbodeule/hop-gate/internal/proxy" - - "github.com/joho/godotenv" ) -func init() { - // .env 파일 로드 - if err := godotenv.Overload(); err != nil { - log.Fatalf(".env 파일을 로드할 수 없습니다: %v", err) - } -} - -func getEnvOrPanic(key string) string { +func getEnvOrPanic(logger logging.Logger, key string) string { value, exists := os.LookupEnv(key) - if !exists || value == "" { - log.Fatalf("필수 환경 변수 %s가 설정되지 않았습니다.", key) + if !exists || strings.TrimSpace(value) == "" { + logger.Error("missing required environment variable", logging.Fields{ + "env": key, + }) + os.Exit(1) } return value } @@ -54,29 +47,40 @@ func firstNonEmpty(values ...string) string { func main() { logger := logging.NewStdJSONLogger("client") - // .env 파일 로드 - if err := godotenv.Load(); err != nil { - log.Println("Failed to load .env file. Using default environment variables.") + // 1. 환경변수(.env 포함)에서 클라이언트 설정 로드 + // internal/config 패키지가 .env 를 먼저 읽고, 이미 설정된 OS 환경변수를 우선시합니다. + envCfg, err := config.LoadClientConfigFromEnv() + if err != nil { + logger.Error("failed to load client config from env", logging.Fields{ + "error": err.Error(), + }) + os.Exit(1) } - // 필수 환경 변수 유효성 검사 - serverAddr := getEnvOrPanic("HOP_CLIENT_SERVER_ADDR") - clientDomain := getEnvOrPanic("HOP_CLIENT_DOMAIN") - apiKey := getEnvOrPanic("HOP_CLIENT_API_KEY") - localTarget := getEnvOrPanic("HOP_CLIENT_LOCAL_TARGET") - debug := getEnvOrPanic("HOP_CLIENT_DEBUG") + // 2. 필수 환경 변수 유효성 검사 (.env 포함; OS 환경변수가 우선) + serverAddrEnv := getEnvOrPanic(logger, "HOP_CLIENT_SERVER_ADDR") + clientDomainEnv := getEnvOrPanic(logger, "HOP_CLIENT_DOMAIN") + apiKeyEnv := getEnvOrPanic(logger, "HOP_CLIENT_API_KEY") + localTargetEnv := getEnvOrPanic(logger, "HOP_CLIENT_LOCAL_TARGET") + debugEnv := getEnvOrPanic(logger, "HOP_CLIENT_DEBUG") - // 디버깅 플래그 확인 - if debug != "true" && debug != "false" { - log.Fatalf("Invalid value for HOP_CLIENT_DEBUG. It must be set to 'true' or 'false'.") + // 디버깅 플래그 형식 확인 + if debugEnv != "true" && debugEnv != "false" { + logger.Error("invalid value for HOP_CLIENT_DEBUG; must be 'true' or 'false'", logging.Fields{ + "env": "HOP_CLIENT_DEBUG", + "value": debugEnv, + }) + os.Exit(1) } - // 유효성 검사 결과 출력 - log.Printf("SERVER_ADDR: %s", serverAddr) - log.Printf("CLIENT_DOMAIN: %s", clientDomain) - log.Printf("API_KEY: %s", maskAPIKey(apiKey)) - log.Printf("LOCAL_TARGET: %s", localTarget) - log.Printf("DEBUG: %s", debug) + // 유효성 검사 결과를 구조화 로그로 출력 + logger.Info("validated client env vars", logging.Fields{ + "HOP_CLIENT_SERVER_ADDR": serverAddrEnv, + "HOP_CLIENT_DOMAIN": clientDomainEnv, + "HOP_CLIENT_API_KEY_MASK": maskAPIKey(apiKeyEnv), + "HOP_CLIENT_LOCAL_TARGET": localTargetEnv, + "HOP_CLIENT_DEBUG": debugEnv, + }) // CLI 인자 정의 (env 보다 우선 적용됨) serverAddrFlag := flag.String("server-addr", "", "DTLS server address (host:port)") @@ -86,15 +90,6 @@ func main() { flag.Parse() - // 1. 환경변수(.env 포함)에서 클라이언트 설정 로드 - envCfg, err := config.LoadClientConfigFromEnv() - if err != nil { - logger.Error("failed to load client config from env", logging.Fields{ - "error": err.Error(), - }) - os.Exit(1) - } - // 2. CLI 인자 우선, env 후순위로 최종 설정 구성 finalCfg := &config.ClientConfig{ ServerAddr: firstNonEmpty(strings.TrimSpace(*serverAddrFlag), strings.TrimSpace(envCfg.ServerAddr)), diff --git a/cmd/server/main.go b/cmd/server/main.go index 0357fec..e43af50 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -8,7 +8,6 @@ import ( "fmt" "io" stdfs "io/fs" - "log" "net" "net/http" "os" @@ -29,8 +28,6 @@ import ( "github.com/dalbodeule/hop-gate/internal/observability" "github.com/dalbodeule/hop-gate/internal/protocol" "github.com/dalbodeule/hop-gate/internal/store" - - "github.com/joho/godotenv" ) type dtlsSessionWrapper struct { @@ -38,17 +35,13 @@ type dtlsSessionWrapper struct { mu sync.Mutex } -func init() { - // .env 파일 로드 - if err := godotenv.Overload(); err != nil { - log.Fatalf(".env 파일을 로드할 수 없습니다: %v", err) - } -} - -func getEnvOrPanic(key string) string { +func getEnvOrPanic(logger logging.Logger, key string) string { value, exists := os.LookupEnv(key) - if !exists || value == "" { - log.Fatalf("필수 환경 변수 %s가 설정되지 않았습니다.", key) + if !exists || strings.TrimSpace(value) == "" { + logger.Error("missing required environment variable", logging.Fields{ + "env": key, + }) + os.Exit(1) } return value } @@ -648,34 +641,8 @@ func newHTTPHandler(logger logging.Logger, proxyTimeout time.Duration) http.Hand func main() { logger := logging.NewStdJSONLogger("server") - // .env 파일 로드 - if err := godotenv.Load(); err != nil { - log.Println(".env 파일을 로드할 수 없습니다. 기본 환경 변수를 사용합니다.") - } - - // 필수 환경 변수 유효성 검사 - httpListen := getEnvOrPanic("HOP_SERVER_HTTP_LISTEN") - httpsListen := getEnvOrPanic("HOP_SERVER_HTTPS_LISTEN") - dtlsListen := getEnvOrPanic("HOP_SERVER_DTLS_LISTEN") - domain := getEnvOrPanic("HOP_SERVER_DOMAIN") - debug := getEnvOrPanic("HOP_SERVER_DEBUG") - - // 디버깅 플래그 확인 - if debug != "true" && debug != "false" { - log.Fatalf("HOP_SERVER_DEBUG 값이 잘못되었습니다. true 또는 false로 설정해야 합니다.") - } - - // 유효성 검사 결과 출력 - log.Printf("HTTP_LISTEN: %s", httpListen) - log.Printf("HTTPS_LISTEN: %s", httpsListen) - log.Printf("DTLS_LISTEN: %s", dtlsListen) - log.Printf("DOMAIN: %s", domain) - log.Printf("DEBUG: %s", debug) - - // Prometheus 메트릭 등록 - observability.MustRegister() - // 1. 서버 설정 로드 (.env + 환경변수) + // internal/config 패키지가 .env 를 먼저 읽고, 이미 설정된 OS 환경변수를 우선시합니다. cfg, err := config.LoadServerConfigFromEnv() if err != nil { logger.Error("failed to load server config from env", logging.Fields{ @@ -684,6 +651,34 @@ func main() { os.Exit(1) } + // 2. 필수 환경 변수 유효성 검사 (.env 포함; OS 환경변수가 우선) + httpListenEnv := getEnvOrPanic(logger, "HOP_SERVER_HTTP_LISTEN") + httpsListenEnv := getEnvOrPanic(logger, "HOP_SERVER_HTTPS_LISTEN") + dtlsListenEnv := getEnvOrPanic(logger, "HOP_SERVER_DTLS_LISTEN") + domainEnv := getEnvOrPanic(logger, "HOP_SERVER_DOMAIN") + debugEnv := getEnvOrPanic(logger, "HOP_SERVER_DEBUG") + + // 디버깅 플래그 형식 확인 + if debugEnv != "true" && debugEnv != "false" { + logger.Error("invalid value for HOP_SERVER_DEBUG; must be 'true' or 'false'", logging.Fields{ + "env": "HOP_SERVER_DEBUG", + "value": debugEnv, + }) + os.Exit(1) + } + + // 유효성 검사 결과를 구조화 로그로 출력 + logger.Info("validated server env vars", logging.Fields{ + "HOP_SERVER_HTTP_LISTEN": httpListenEnv, + "HOP_SERVER_HTTPS_LISTEN": httpsListenEnv, + "HOP_SERVER_DTLS_LISTEN": dtlsListenEnv, + "HOP_SERVER_DOMAIN": domainEnv, + "HOP_SERVER_DEBUG": debugEnv, + }) + + // Prometheus 메트릭 등록 + observability.MustRegister() + logger.Info("hop-gate server starting", logging.Fields{ "stack": "prometheus-loki-grafana", "http_listen": cfg.HTTPListen, diff --git a/internal/config/config.go b/internal/config/config.go index bba8ec9..7cc776e 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -107,7 +107,11 @@ func loadDotEnvOnce() { val = strings.Trim(val, `"'`) if key != "" { - _ = os.Setenv(key, val) + // 이미 OS 환경변수에 설정된 값이 있는 경우 이를 우선시하고, + // 비어 있는 키에 대해서만 .env 값을 주입합니다. + if _, exists := os.LookupEnv(key); !exists { + _ = os.Setenv(key, val) + } } } if err := scanner.Err(); err != nil { @@ -209,7 +213,8 @@ func loadLoggingFromEnv() LoggingConfig { } } -// LoadServerConfigFromEnv 는 .env 를 우선 읽고, 이후 환경 변수를 기반으로 서버 설정을 구성합니다. +// LoadServerConfigFromEnv 는 .env 를 한 번 읽어 현재 환경변수를 보완한 뒤 +// "환경변수 > .env" 우선순위로 서버 설정을 구성합니다. func LoadServerConfigFromEnv() (*ServerConfig, error) { loadDotEnvOnce() if dotenvErr != nil { @@ -228,7 +233,8 @@ func LoadServerConfigFromEnv() (*ServerConfig, error) { return cfg, nil } -// LoadClientConfigFromEnv 는 .env 를 우선 읽고, 이후 환경 변수를 기반으로 클라이언트 설정을 구성합니다. +// LoadClientConfigFromEnv 는 .env 를 한 번 읽어 현재 환경변수를 보완한 뒤 +// "환경변수 > .env" 우선순위로 클라이언트 설정을 구성합니다. // 실제 런타임에서 사용되는 필드는 ServerAddr, Domain, ClientAPIKey, LocalTarget 입니다. func LoadClientConfigFromEnv() (*ClientConfig, error) { loadDotEnvOnce()