package config import ( "bufio" "errors" "os" "strconv" "strings" "sync" ) // LoggingConfig 는 공통 로그 설정을 담습니다. // Loki push 에 필요한 엔드포인트/인증/정적 라벨 등을 포함합니다. type LoggingConfig struct { Level string // 예: "debug", "info", "warn", "error" Loki LokiConfig // Loki 관련 설정 } // LokiConfig 는 Loki HTTP push 설정을 담습니다. type LokiConfig struct { Enable bool // true 인 경우 Loki 로도 push Endpoint string // 예: "http://loki:3100/loki/api/v1/push" TenantID string // multi-tenant Loki 사용 시 X-Scope-OrgID 등에 사용 Username string // basic auth 사용자명(선택) Password string // basic auth 비밀번호(선택) StaticLabels map[string]string // 모든 로그에 공통으로 붙일 라벨 (app=hop-gate,env=dev 등) } // ServerConfig 는 서버 프로세스 설정을 담습니다. type ServerConfig struct { HTTPListen string // 예: ":80" HTTPSListen string // 예: ":443" DTLSListen string // 예: ":443" Domain string // 메인 도메인 ProxyDomains []string // 프록시 서브도메인 또는 별도 도메인 Logging LoggingConfig // 서버용 로그 설정 } // ClientConfig 는 클라이언트 프로세스 설정을 담습니다. type ClientConfig struct { ServerAddr string // DTLS 서버 주소 (host:port) ClientID string // 클라이언트 식별자 AuthToken string // 선택적 인증 토큰 ServicePorts map[string]string // service name -> "127.0.0.1:PORT" Logging LoggingConfig // 클라이언트용 로그 설정 } var ( dotenvOnce sync.Once dotenvErr error ) // loadDotEnvOnce 는 현재 작업 디렉터리의 .env 파일을 한 번만 읽어서 os.Environ 에 주입합니다. // - KEY=VALUE, export KEY=VALUE 형식을 지원 // - # 으로 시작하는 줄은 주석으로 간주합니다. func loadDotEnvOnce() { dotenvOnce.Do(func() { fi, err := os.Stat(".env") if err != nil { if errors.Is(err, os.ErrNotExist) { // .env 가 없으면 조용히 무시 return } dotenvErr = err return } if fi.IsDir() { // 디렉터리이면 무시 return } f, err := os.Open(".env") if err != nil { dotenvErr = err return } defer f.Close() scanner := bufio.NewScanner(f) for scanner.Scan() { line := strings.TrimSpace(scanner.Text()) if line == "" || strings.HasPrefix(line, "#") { continue } if strings.HasPrefix(line, "export ") { line = strings.TrimSpace(strings.TrimPrefix(line, "export ")) } parts := strings.SplitN(line, "=", 2) if len(parts) != 2 { continue } key := strings.TrimSpace(parts[0]) val := strings.TrimSpace(parts[1]) // 양 끝의 작은/큰따옴표 제거 val = strings.Trim(val, `"'`) if key != "" { _ = os.Setenv(key, val) } } if err := scanner.Err(); err != nil { dotenvErr = err return } }) } func getEnvOrDefault(key, def string) string { if v := os.Getenv(key); v != "" { return v } return def } func getEnvBool(key string, def bool) bool { v := strings.ToLower(strings.TrimSpace(os.Getenv(key))) if v == "" { return def } switch v { case "1", "true", "yes", "y", "on": return true case "0", "false", "no", "n", "off": return false default: return def } } func parseCSVEnv(key string) []string { raw := os.Getenv(key) if raw == "" { return nil } parts := strings.Split(raw, ",") out := make([]string, 0, len(parts)) for _, p := range parts { p = strings.TrimSpace(p) if p != "" { out = append(out, p) } } return out } // parseKeyValueCSV 는 "k1=v1,k2=v2" 형태의 문자열을 map 으로 변환합니다. func parseKeyValueCSV(raw string) map[string]string { if raw == "" { return nil } m := make(map[string]string) for _, part := range strings.Split(raw, ",") { part = strings.TrimSpace(part) if part == "" { continue } kv := strings.SplitN(part, "=", 2) if len(kv) != 2 { continue } k := strings.TrimSpace(kv[0]) v := strings.TrimSpace(kv[1]) if k != "" { m[k] = v } } return m } // parseServicePortsEnv 는 "name1=127.0.0.1:8080,name2=127.0.0.1:9000" 형식을 파싱합니다. func parseServicePortsEnv(key string) map[string]string { raw := os.Getenv(key) return parseKeyValueCSV(raw) } // loadLoggingFromEnv 는 공통 로그 설정을 .env/환경변수에서 읽어옵니다. func loadLoggingFromEnv() LoggingConfig { level := getEnvOrDefault("HOP_LOG_LEVEL", "info") lokiEnable := getEnvBool("HOP_LOKI_ENABLE", false) lokiEndpoint := os.Getenv("HOP_LOKI_ENDPOINT") lokiTenantID := os.Getenv("HOP_LOKI_TENANT_ID") lokiUsername := os.Getenv("HOP_LOKI_USERNAME") lokiPassword := os.Getenv("HOP_LOKI_PASSWORD") lokiStaticLabels := parseKeyValueCSV(os.Getenv("HOP_LOKI_STATIC_LABELS")) return LoggingConfig{ Level: level, Loki: LokiConfig{ Enable: lokiEnable, Endpoint: lokiEndpoint, TenantID: lokiTenantID, Username: lokiUsername, Password: lokiPassword, StaticLabels: lokiStaticLabels, }, } } // LoadServerConfigFromEnv 는 .env 를 우선 읽고, 이후 환경 변수를 기반으로 서버 설정을 구성합니다. func LoadServerConfigFromEnv() (*ServerConfig, error) { loadDotEnvOnce() if dotenvErr != nil { return nil, dotenvErr } cfg := &ServerConfig{ HTTPListen: getEnvOrDefault("HOP_SERVER_HTTP_LISTEN", ":80"), HTTPSListen: getEnvOrDefault("HOP_SERVER_HTTPS_LISTEN", ":443"), DTLSListen: getEnvOrDefault("HOP_SERVER_DTLS_LISTEN", ":443"), Domain: os.Getenv("HOP_SERVER_DOMAIN"), ProxyDomains: parseCSVEnv("HOP_SERVER_PROXY_DOMAINS"), Logging: loadLoggingFromEnv(), } return cfg, nil } // LoadClientConfigFromEnv 는 .env 를 우선 읽고, 이후 환경 변수를 기반으로 클라이언트 설정을 구성합니다. func LoadClientConfigFromEnv() (*ClientConfig, error) { loadDotEnvOnce() if dotenvErr != nil { return nil, dotenvErr } cfg := &ClientConfig{ ServerAddr: os.Getenv("HOP_CLIENT_SERVER_ADDR"), ClientID: os.Getenv("HOP_CLIENT_ID"), AuthToken: os.Getenv("HOP_CLIENT_AUTH_TOKEN"), ServicePorts: parseServicePortsEnv("HOP_CLIENT_SERVICE_PORTS"), Logging: loadLoggingFromEnv(), } return cfg, nil } // Optional: 숫자 포트만 지정하고 싶을 경우를 위한 헬퍼 (예: "80" -> ":80"). // 현재는 사용하지 않지만, 향후 유효성 검사/정규화에 사용할 수 있습니다. func normalizePort(p string, def string) string { p = strings.TrimSpace(p) if p == "" { return def } if strings.HasPrefix(p, ":") { return p } // 숫자로만 구성된 경우 ":" prefix 를 붙입니다. if _, err := strconv.Atoi(p); err == nil { return ":" + p } return p }