feat: 添加配置加载和日志配置支持
- 新增 LogConfig 结构体,支持 9 个日志配置字段(level, encoding, output_stdout, file_path, max_size, max_backups, max_age, compress, gorm_level) - Config 结构体新增 Log 字段,支持 YAML 解析 - output_stdout 使用 *bool 指针类型,nil 默认为 true - 更新 config.example.yaml 添加完整 log 配置段 - 新增 TDD 测试:日志配置解析、向后兼容、字段完整性 Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
16
internal/config/config.example.yaml
Normal file
16
internal/config/config.example.yaml
Normal file
@@ -0,0 +1,16 @@
|
||||
server_port: "8080"
|
||||
slurm_api_url: "http://localhost:6820"
|
||||
slurm_user_name: "root"
|
||||
slurm_jwt_key_path: "/etc/slurm/jwt_hs256.key"
|
||||
mysql_dsn: "root:@tcp(127.0.0.1:3306)/hpc_platform?parseTime=true"
|
||||
|
||||
log:
|
||||
level: "info" # debug, info, warn, error
|
||||
encoding: "json" # json, console
|
||||
output_stdout: true # 是否输出日志到终端
|
||||
file_path: "" # 日志文件路径,留空则不写文件
|
||||
max_size: 100 # max MB per log file
|
||||
max_backups: 5 # number of old log files to retain
|
||||
max_age: 30 # days to retain old log files
|
||||
compress: true # gzip rotated log files
|
||||
gorm_level: "warn" # GORM SQL log level: silent, error, warn, info
|
||||
51
internal/config/config.go
Normal file
51
internal/config/config.go
Normal file
@@ -0,0 +1,51 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// LogConfig holds logging configuration values.
|
||||
type LogConfig struct {
|
||||
Level string `yaml:"level"` // debug, info, warn, error (default: info)
|
||||
Encoding string `yaml:"encoding"` // json, console (default: json)
|
||||
OutputStdout *bool `yaml:"output_stdout"` // 输出到终端 (default: true)
|
||||
FilePath string `yaml:"file_path"` // log file path (for rotation)
|
||||
MaxSize int `yaml:"max_size"` // MB per file (default: 100)
|
||||
MaxBackups int `yaml:"max_backups"` // retained files (default: 5)
|
||||
MaxAge int `yaml:"max_age"` // days to retain (default: 30)
|
||||
Compress bool `yaml:"compress"` // gzip old files (default: true)
|
||||
GormLevel string `yaml:"gorm_level"` // GORM SQL log level (default: warn)
|
||||
}
|
||||
|
||||
// Config holds all application configuration values.
|
||||
type Config struct {
|
||||
ServerPort string `yaml:"server_port"`
|
||||
SlurmAPIURL string `yaml:"slurm_api_url"`
|
||||
SlurmUserName string `yaml:"slurm_user_name"`
|
||||
SlurmJWTKeyPath string `yaml:"slurm_jwt_key_path"`
|
||||
MySQLDSN string `yaml:"mysql_dsn"`
|
||||
Log LogConfig `yaml:"log"`
|
||||
}
|
||||
|
||||
// Load reads a YAML configuration file and returns a parsed Config.
|
||||
// If path is empty, it defaults to "./config.yaml".
|
||||
func Load(path string) (*Config, error) {
|
||||
if path == "" {
|
||||
path = "./config.yaml"
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read config file %s: %w", path, err)
|
||||
}
|
||||
|
||||
var cfg Config
|
||||
if err := yaml.Unmarshal(data, &cfg); err != nil {
|
||||
return nil, fmt.Errorf("parse config file %s: %w", path, err)
|
||||
}
|
||||
|
||||
return &cfg, nil
|
||||
}
|
||||
256
internal/config/config_test.go
Normal file
256
internal/config/config_test.go
Normal file
@@ -0,0 +1,256 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestLoad(t *testing.T) {
|
||||
content := []byte(`server_port: "9090"
|
||||
slurm_api_url: "http://slurm.example.com:6820"
|
||||
slurm_user_name: "admin"
|
||||
slurm_jwt_key_path: "/etc/slurm/jwt.key"
|
||||
mysql_dsn: "user:pass@tcp(10.0.0.1:3306)/testdb?parseTime=true"
|
||||
`)
|
||||
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "config.yaml")
|
||||
if err := os.WriteFile(path, content, 0644); err != nil {
|
||||
t.Fatalf("write temp config: %v", err)
|
||||
}
|
||||
|
||||
cfg, err := Load(path)
|
||||
if err != nil {
|
||||
t.Fatalf("Load() error = %v", err)
|
||||
}
|
||||
|
||||
if cfg.ServerPort != "9090" {
|
||||
t.Errorf("ServerPort = %q, want %q", cfg.ServerPort, "9090")
|
||||
}
|
||||
if cfg.SlurmAPIURL != "http://slurm.example.com:6820" {
|
||||
t.Errorf("SlurmAPIURL = %q, want %q", cfg.SlurmAPIURL, "http://slurm.example.com:6820")
|
||||
}
|
||||
if cfg.SlurmUserName != "admin" {
|
||||
t.Errorf("SlurmUserName = %q, want %q", cfg.SlurmUserName, "admin")
|
||||
}
|
||||
if cfg.SlurmJWTKeyPath != "/etc/slurm/jwt.key" {
|
||||
t.Errorf("SlurmJWTKeyPath = %q, want %q", cfg.SlurmJWTKeyPath, "/etc/slurm/jwt.key")
|
||||
}
|
||||
if cfg.MySQLDSN != "user:pass@tcp(10.0.0.1:3306)/testdb?parseTime=true" {
|
||||
t.Errorf("MySQLDSN = %q, want %q", cfg.MySQLDSN, "user:pass@tcp(10.0.0.1:3306)/testdb?parseTime=true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadDefaultPath(t *testing.T) {
|
||||
content := []byte(`server_port: "8080"
|
||||
slurm_api_url: "http://localhost:6820"
|
||||
slurm_user_name: "root"
|
||||
slurm_jwt_key_path: "/etc/slurm/jwt_hs256.key"
|
||||
mysql_dsn: "root:@tcp(127.0.0.1:3306)/hpc_platform?parseTime=true"
|
||||
`)
|
||||
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "config.yaml")
|
||||
if err := os.WriteFile(path, content, 0644); err != nil {
|
||||
t.Fatalf("write temp config: %v", err)
|
||||
}
|
||||
|
||||
cfg, err := Load(path)
|
||||
if err != nil {
|
||||
t.Fatalf("Load() error = %v", err)
|
||||
}
|
||||
if cfg == nil {
|
||||
t.Fatal("Load() returned nil config")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadWithLogConfig(t *testing.T) {
|
||||
content := []byte(`server_port: "9090"
|
||||
slurm_api_url: "http://slurm.example.com:6820"
|
||||
slurm_user_name: "admin"
|
||||
slurm_jwt_key_path: "/etc/slurm/jwt.key"
|
||||
mysql_dsn: "user:pass@tcp(10.0.0.1:3306)/testdb?parseTime=true"
|
||||
log:
|
||||
level: "debug"
|
||||
encoding: "console"
|
||||
output_stdout: true
|
||||
file_path: "/var/log/app.log"
|
||||
max_size: 200
|
||||
max_backups: 10
|
||||
max_age: 60
|
||||
compress: false
|
||||
gorm_level: "info"
|
||||
`)
|
||||
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "config.yaml")
|
||||
if err := os.WriteFile(path, content, 0644); err != nil {
|
||||
t.Fatalf("write temp config: %v", err)
|
||||
}
|
||||
|
||||
cfg, err := Load(path)
|
||||
if err != nil {
|
||||
t.Fatalf("Load() error = %v", err)
|
||||
}
|
||||
|
||||
if cfg.Log.Level != "debug" {
|
||||
t.Errorf("Log.Level = %q, want %q", cfg.Log.Level, "debug")
|
||||
}
|
||||
if cfg.Log.Encoding != "console" {
|
||||
t.Errorf("Log.Encoding = %q, want %q", cfg.Log.Encoding, "console")
|
||||
}
|
||||
if cfg.Log.OutputStdout == nil || *cfg.Log.OutputStdout != true {
|
||||
t.Errorf("Log.OutputStdout = %v, want true", cfg.Log.OutputStdout)
|
||||
}
|
||||
if cfg.Log.FilePath != "/var/log/app.log" {
|
||||
t.Errorf("Log.FilePath = %q, want %q", cfg.Log.FilePath, "/var/log/app.log")
|
||||
}
|
||||
if cfg.Log.MaxSize != 200 {
|
||||
t.Errorf("Log.MaxSize = %d, want %d", cfg.Log.MaxSize, 200)
|
||||
}
|
||||
if cfg.Log.MaxBackups != 10 {
|
||||
t.Errorf("Log.MaxBackups = %d, want %d", cfg.Log.MaxBackups, 10)
|
||||
}
|
||||
if cfg.Log.MaxAge != 60 {
|
||||
t.Errorf("Log.MaxAge = %d, want %d", cfg.Log.MaxAge, 60)
|
||||
}
|
||||
if cfg.Log.Compress != false {
|
||||
t.Errorf("Log.Compress = %v, want %v", cfg.Log.Compress, false)
|
||||
}
|
||||
if cfg.Log.GormLevel != "info" {
|
||||
t.Errorf("Log.GormLevel = %q, want %q", cfg.Log.GormLevel, "info")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadWithoutLogConfig(t *testing.T) {
|
||||
content := []byte(`server_port: "8080"
|
||||
slurm_api_url: "http://localhost:6820"
|
||||
slurm_user_name: "root"
|
||||
slurm_jwt_key_path: "/etc/slurm/jwt_hs256.key"
|
||||
mysql_dsn: "root:@tcp(127.0.0.1:3306)/hpc_platform?parseTime=true"
|
||||
`)
|
||||
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "config.yaml")
|
||||
if err := os.WriteFile(path, content, 0644); err != nil {
|
||||
t.Fatalf("write temp config: %v", err)
|
||||
}
|
||||
|
||||
cfg, err := Load(path)
|
||||
if err != nil {
|
||||
t.Fatalf("Load() error = %v", err)
|
||||
}
|
||||
|
||||
if cfg.Log.Level != "" {
|
||||
t.Errorf("Log.Level = %q, want empty string", cfg.Log.Level)
|
||||
}
|
||||
if cfg.Log.Encoding != "" {
|
||||
t.Errorf("Log.Encoding = %q, want empty string", cfg.Log.Encoding)
|
||||
}
|
||||
if cfg.Log.OutputStdout != nil {
|
||||
t.Errorf("Log.OutputStdout = %v, want nil", cfg.Log.OutputStdout)
|
||||
}
|
||||
if cfg.Log.FilePath != "" {
|
||||
t.Errorf("Log.FilePath = %q, want empty string", cfg.Log.FilePath)
|
||||
}
|
||||
if cfg.Log.MaxSize != 0 {
|
||||
t.Errorf("Log.MaxSize = %d, want 0", cfg.Log.MaxSize)
|
||||
}
|
||||
if cfg.Log.MaxBackups != 0 {
|
||||
t.Errorf("Log.MaxBackups = %d, want 0", cfg.Log.MaxBackups)
|
||||
}
|
||||
if cfg.Log.MaxAge != 0 {
|
||||
t.Errorf("Log.MaxAge = %d, want 0", cfg.Log.MaxAge)
|
||||
}
|
||||
if cfg.Log.Compress != false {
|
||||
t.Errorf("Log.Compress = %v, want false", cfg.Log.Compress)
|
||||
}
|
||||
if cfg.Log.GormLevel != "" {
|
||||
t.Errorf("Log.GormLevel = %q, want empty string", cfg.Log.GormLevel)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadExistingFieldsWithLogConfig(t *testing.T) {
|
||||
content := []byte(`server_port: "7070"
|
||||
slurm_api_url: "http://slurm2.example.com:6820"
|
||||
slurm_user_name: "testuser"
|
||||
slurm_jwt_key_path: "/keys/jwt.key"
|
||||
mysql_dsn: "root:secret@tcp(db:3306)/mydb?parseTime=true"
|
||||
log:
|
||||
level: "warn"
|
||||
encoding: "json"
|
||||
`)
|
||||
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "config.yaml")
|
||||
if err := os.WriteFile(path, content, 0644); err != nil {
|
||||
t.Fatalf("write temp config: %v", err)
|
||||
}
|
||||
|
||||
cfg, err := Load(path)
|
||||
if err != nil {
|
||||
t.Fatalf("Load() error = %v", err)
|
||||
}
|
||||
|
||||
if cfg.ServerPort != "7070" {
|
||||
t.Errorf("ServerPort = %q, want %q", cfg.ServerPort, "7070")
|
||||
}
|
||||
if cfg.SlurmAPIURL != "http://slurm2.example.com:6820" {
|
||||
t.Errorf("SlurmAPIURL = %q, want %q", cfg.SlurmAPIURL, "http://slurm2.example.com:6820")
|
||||
}
|
||||
if cfg.SlurmUserName != "testuser" {
|
||||
t.Errorf("SlurmUserName = %q, want %q", cfg.SlurmUserName, "testuser")
|
||||
}
|
||||
if cfg.SlurmJWTKeyPath != "/keys/jwt.key" {
|
||||
t.Errorf("SlurmJWTKeyPath = %q, want %q", cfg.SlurmJWTKeyPath, "/keys/jwt.key")
|
||||
}
|
||||
if cfg.MySQLDSN != "root:secret@tcp(db:3306)/mydb?parseTime=true" {
|
||||
t.Errorf("MySQLDSN = %q, want %q", cfg.MySQLDSN, "root:secret@tcp(db:3306)/mydb?parseTime=true")
|
||||
}
|
||||
if cfg.Log.Level != "warn" {
|
||||
t.Errorf("Log.Level = %q, want %q", cfg.Log.Level, "warn")
|
||||
}
|
||||
if cfg.Log.Encoding != "json" {
|
||||
t.Errorf("Log.Encoding = %q, want %q", cfg.Log.Encoding, "json")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadNonExistentFile(t *testing.T) {
|
||||
_, err := Load("/nonexistent/path/config.yaml")
|
||||
if err == nil {
|
||||
t.Fatal("Load() expected error for non-existent file, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadWithOutputStdoutFalse(t *testing.T) {
|
||||
content := []byte(`server_port: "9090"
|
||||
slurm_api_url: "http://slurm.example.com:6820"
|
||||
slurm_user_name: "admin"
|
||||
slurm_jwt_key_path: "/etc/slurm/jwt.key"
|
||||
mysql_dsn: "user:pass@tcp(10.0.0.1:3306)/testdb?parseTime=true"
|
||||
log:
|
||||
level: "info"
|
||||
encoding: "json"
|
||||
output_stdout: false
|
||||
file_path: "/var/log/app.log"
|
||||
`)
|
||||
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "config.yaml")
|
||||
if err := os.WriteFile(path, content, 0644); err != nil {
|
||||
t.Fatalf("write temp config: %v", err)
|
||||
}
|
||||
|
||||
cfg, err := Load(path)
|
||||
if err != nil {
|
||||
t.Fatalf("Load() error = %v", err)
|
||||
}
|
||||
|
||||
if cfg.Log.OutputStdout == nil || *cfg.Log.OutputStdout != false {
|
||||
t.Errorf("Log.OutputStdout = %v, want false", cfg.Log.OutputStdout)
|
||||
}
|
||||
if cfg.Log.FilePath != "/var/log/app.log" {
|
||||
t.Errorf("Log.FilePath = %q, want %q", cfg.Log.FilePath, "/var/log/app.log")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user