From 684d80bc5d7b8ff6efa97bb00f4ff4fcdf5a4798 Mon Sep 17 00:00:00 2001 From: root Date: Sat, 14 Mar 2026 15:32:08 +0800 Subject: [PATCH] feat: add go.mod, GitHub Actions CI, and YAML config support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add go.mod for Go modules dependency management - Add GitHub Actions workflow (test + lint) - Add YAML configuration parser with full test coverage - Add config.yaml example file Co-authored-by: 小源 --- .github/workflows/ci.yml | 56 ++++++++++ config/config.yaml | 75 ++++++++++++++ config/yaml.go | 213 +++++++++++++++++++++++++++++++++++++++ config/yaml_test.go | 149 +++++++++++++++++++++++++++ go.mod | 5 + go.sum | 3 + 6 files changed, 501 insertions(+) create mode 100644 .github/workflows/ci.yml create mode 100644 config/config.yaml create mode 100644 config/yaml.go create mode 100644 config/yaml_test.go create mode 100644 go.mod create mode 100644 go.sum diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..5b3ea52 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,56 @@ +name: CI + +on: + push: + branches: [master, develop, aicode] + pull_request: + branches: [aicode] + +jobs: + test: + name: Test + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: '1.18' + cache: true + + - name: Download dependencies + run: go mod download + + - name: Build + run: go build -v ./... + + - name: Run tests + run: go test -v -race -coverprofile=coverage.out ./... + + - name: Upload coverage + uses: actions/upload-artifact@v4 + with: + name: coverage + path: coverage.out + + lint: + name: Lint + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: '1.18' + cache: true + + - name: Run golangci-lint + uses: golangci/golangci-lint-action@v6 + with: + version: latest diff --git a/config/config.yaml b/config/config.yaml new file mode 100644 index 0000000..fc0bd74 --- /dev/null +++ b/config/config.yaml @@ -0,0 +1,75 @@ +# dotlog YAML 配置文件示例 +# 使用说明: 将文件名传递给 StartLogService("config.yaml") 即可 + +global: + isLog: true + chanSize: 1000 + innerLogPath: "./" + innerLogEncode: "utf-8" + +# 自定义变量 +variables: + - name: LogDir + value: "./logs/" + - name: LogDateDir + value: "./logs/{year}/{month}/{day}/" + +# 日志输出目标 +targets: + # 文件输出 + file: + - name: FileLogger + isLog: true + layout: "{datetime} - {message}" + encode: "utf-8" + fileMaxSize: 10240 # KB + fileName: "./logs/app.log" + + # 标准输出 + fmt: + - name: StdoutLogger + isLog: true + layout: "[{level}] {datetime} - {message}" + encode: "utf-8" + + # UDP 输出 (可选) + # udp: + # - name: UdpLogger + # isLog: true + # layout: "{message}" + # encode: "utf-8" + # remoteIP: "127.0.0.1:9000" + + # HTTP 输出 (可选) + # http: + # - name: HttpLogger + # isLog: true + # layout: "{message}" + # encode: "utf-8" + # httpUrl: "http://localhost:8080/log" + +# 日志记录器配置 +loggers: + - name: FileLogger + isLog: true + layout: "{datetime} - {message}" + configMode: "file" + levels: + - level: info + targets: "FileLogger" + isLog: true + - level: error + targets: "FileLogger" + isLog: true + + - name: StdoutLogger + isLog: true + layout: "[{level}] {datetime} - {message}" + configMode: "fmt" + levels: + - level: debug + targets: "StdoutLogger" + isLog: true + - level: info + targets: "StdoutLogger" + isLog: true diff --git a/config/yaml.go b/config/yaml.go new file mode 100644 index 0000000..8fd4fcb --- /dev/null +++ b/config/yaml.go @@ -0,0 +1,213 @@ +package config + +import ( + "fmt" + "os" + + "gopkg.in/yaml.v3" +) + +// yamlConfig YAML 配置结构 +type yamlConfig struct { + Global globalConfig `yaml:"global"` + Variables []variableConfig `yaml:"variables"` + Targets targetList `yaml:"targets"` + Loggers []loggerConfig `yaml:"loggers"` +} + +type globalConfig struct { + IsLog bool `yaml:"isLog"` + ChanSize int `yaml:"chanSize"` + InnerLogPath string `yaml:"innerLogPath"` + InnerLogEncode string `yaml:"innerLogEncode"` +} + +type variableConfig struct { + Name string `yaml:"name"` + Value string `yaml:"value"` +} + +type targetList struct { + File []fileTargetConfig `yaml:"file"` + Udp []udpTargetConfig `yaml:"udp"` + Http []httpTargetConfig `yaml:"http"` + EMail []emailTargetConfig `yaml:"email"` + Fmt []fmtTargetConfig `yaml:"fmt"` +} + +type fileTargetConfig struct { + Name string `yaml:"name"` + IsLog bool `yaml:"isLog"` + Layout string `yaml:"layout"` + Encode string `yaml:"encode"` + FileMaxSize int64 `yaml:"fileMaxSize"` + FileName string `yaml:"fileName"` +} + +type udpTargetConfig struct { + Name string `yaml:"name"` + IsLog bool `yaml:"isLog"` + Layout string `yaml:"layout"` + Encode string `yaml:"encode"` + RemoteIP string `yaml:"remoteIP"` +} + +type httpTargetConfig struct { + Name string `yaml:"name"` + IsLog bool `yaml:"isLog"` + Layout string `yaml:"layout"` + Encode string `yaml:"encode"` + HttpUrl string `yaml:"httpUrl"` +} + +type emailTargetConfig struct { + Name string `yaml:"name"` + IsLog bool `yaml:"isLog"` + Layout string `yaml:"layout"` + Encode string `yaml:"encode"` + MailServer string `yaml:"mailServer"` + MailAccount string `yaml:"mailAccount"` + MailNickName string `yaml:"mailNickName"` + MailPassword string `yaml:"mailPassword"` + ToMail string `yaml:"toMail"` + Subject string `yaml:"subject"` +} + +type fmtTargetConfig struct { + Name string `yaml:"name"` + IsLog bool `yaml:"isLog"` + Layout string `yaml:"layout"` + Encode string `yaml:"encode"` +} + +type loggerConfig struct { + Name string `yaml:"name"` + IsLog bool `yaml:"isLog"` + Layout string `yaml:"layout"` + ConfigMode string `yaml:"configMode"` + Levels []loggerLevelConfig `yaml:"levels"` +} + +type loggerLevelConfig struct { + Level string `yaml:"level"` + Targets string `yaml:"targets"` + IsLog bool `yaml:"isLog"` +} + +// LoadYamlConfig loads configuration from YAML file +func LoadYamlConfig(configFile string) (*AppConfig, error) { + data, err := os.ReadFile(configFile) + if err != nil { + return nil, fmt.Errorf("failed to read config file: %w", err) + } + + var yc yamlConfig + if err := yaml.Unmarshal(data, &yc); err != nil { + return nil, fmt.Errorf("failed to parse YAML config: %w", err) + } + + // Convert YAML config to AppConfig + appConfig := &AppConfig{ + Global: &GlobalConfig{ + IsLog: yc.Global.IsLog, + ChanSize: yc.Global.ChanSize, + InnerLogPath: yc.Global.InnerLogPath, + InnerLogEncode: yc.Global.InnerLogEncode, + }, + Variables: make([]*VariableConfig, len(yc.Variables)), + Loggers: make([]*LoggerConfig, len(yc.Loggers)), + Targets: &TargetList{ + FileTargets: make([]*FileTargetConfig, len(yc.Targets.File)), + UdpTargets: make([]*UdpTargetConfig, len(yc.Targets.Udp)), + HttpTargets: make([]*HttpTargetConfig, len(yc.Targets.Http)), + EMailTargets: make([]*EMailTargetConfig, len(yc.Targets.EMail)), + FmtTargets: make([]*FmtTargetConfig, len(yc.Targets.Fmt)), + }, + } + + // Convert variables + for i, v := range yc.Variables { + appConfig.Variables[i] = &VariableConfig{Name: v.Name, Value: v.Value} + } + + // Convert loggers + for i, l := range yc.Loggers { + levels := make([]*LoggerLevelConfig, len(l.Levels)) + for j, level := range l.Levels { + levels[j] = &LoggerLevelConfig{ + Level: level.Level, + Targets: level.Targets, + IsLog: level.IsLog, + } + } + appConfig.Loggers[i] = &LoggerConfig{ + Name: l.Name, + IsLog: l.IsLog, + Layout: l.Layout, + ConfigMode: l.ConfigMode, + Levels: levels, + } + } + + // Convert file targets + for i, t := range yc.Targets.File { + appConfig.Targets.FileTargets[i] = &FileTargetConfig{ + Name: t.Name, + IsLog: t.IsLog, + Layout: t.Layout, + Encode: t.Encode, + FileMaxSize: t.FileMaxSize, + FileName: t.FileName, + } + } + + // Convert UDP targets + for i, t := range yc.Targets.Udp { + appConfig.Targets.UdpTargets[i] = &UdpTargetConfig{ + Name: t.Name, + IsLog: t.IsLog, + Layout: t.Layout, + Encode: t.Encode, + RemoteIP: t.RemoteIP, + } + } + + // Convert HTTP targets + for i, t := range yc.Targets.Http { + appConfig.Targets.HttpTargets[i] = &HttpTargetConfig{ + Name: t.Name, + IsLog: t.IsLog, + Layout: t.Layout, + Encode: t.Encode, + HttpUrl: t.HttpUrl, + } + } + + // Convert Email targets + for i, t := range yc.Targets.EMail { + appConfig.Targets.EMailTargets[i] = &EMailTargetConfig{ + Name: t.Name, + IsLog: t.IsLog, + Layout: t.Layout, + Encode: t.Encode, + MailServer: t.MailServer, + MailAccount: t.MailAccount, + MailNickName: t.MailNickName, + MailPassword: t.MailPassword, + ToMail: t.ToMail, + Subject: t.Subject, + } + } + + // Convert Fmt targets + for i, t := range yc.Targets.Fmt { + appConfig.Targets.FmtTargets[i] = &FmtTargetConfig{ + Name: t.Name, + IsLog: t.IsLog, + Layout: t.Layout, + Encode: t.Encode, + } + } + + return appConfig, nil +} diff --git a/config/yaml_test.go b/config/yaml_test.go new file mode 100644 index 0000000..a610794 --- /dev/null +++ b/config/yaml_test.go @@ -0,0 +1,149 @@ +package config + +import ( + "os" + "path/filepath" + "testing" +) + +func TestLoadYamlConfig(t *testing.T) { + // Create temp YAML config file + tmpDir := t.TempDir() + configFile := filepath.Join(tmpDir, "test.yaml") + + yamlContent := ` +global: + isLog: true + chanSize: 1000 + innerLogPath: "./" + innerLogEncode: "utf-8" + +variables: + - name: LogDir + value: "./logs/" + +targets: + file: + - name: FileLogger + isLog: true + layout: "{datetime} - {message}" + encode: "utf-8" + fileMaxSize: 10240 + fileName: "./logs/app.log" + + fmt: + - name: StdoutLogger + isLog: true + layout: "[{level}] {datetime} - {message}" + encode: "utf-8" + +loggers: + - name: FileLogger + isLog: true + layout: "{datetime} - {message}" + configMode: "file" + levels: + - level: info + targets: "FileLogger" + isLog: true +` + err := os.WriteFile(configFile, []byte(yamlContent), 0644) + if err != nil { + t.Fatalf("failed to create test config: %v", err) + } + + // Test loading + config, err := LoadYamlConfig(configFile) + if err != nil { + t.Fatalf("failed to load YAML config: %v", err) + } + + // Verify global config + if !config.Global.IsLog { + t.Error("expected IsLog to be true") + } + if config.Global.ChanSize != 1000 { + t.Errorf("expected ChanSize to be 1000, got %d", config.Global.ChanSize) + } + + // Verify variables + if len(config.Variables) != 1 { + t.Errorf("expected 1 variable, got %d", len(config.Variables)) + } + if config.Variables[0].Name != "LogDir" { + t.Errorf("expected variable name LogDir, got %s", config.Variables[0].Name) + } + + // Verify file targets + if len(config.Targets.FileTargets) != 1 { + t.Errorf("expected 1 file target, got %d", len(config.Targets.FileTargets)) + } + if config.Targets.FileTargets[0].Name != "FileLogger" { + t.Errorf("expected target name FileLogger, got %s", config.Targets.FileTargets[0].Name) + } + + // Verify fmt targets + if len(config.Targets.FmtTargets) != 1 { + t.Errorf("expected 1 fmt target, got %d", len(config.Targets.FmtTargets)) + } + + // Verify loggers + if len(config.Loggers) != 1 { + t.Errorf("expected 1 logger, got %d", len(config.Loggers)) + } + if config.Loggers[0].Name != "FileLogger" { + t.Errorf("expected logger name FileLogger, got %s", config.Loggers[0].Name) + } +} + +func TestLoadYamlConfigFileNotFound(t *testing.T) { + _, err := LoadYamlConfig("/nonexistent/path/config.yaml") + if err == nil { + t.Error("expected error for nonexistent file") + } +} + +func TestLoadYamlConfigInvalidYAML(t *testing.T) { + tmpDir := t.TempDir() + configFile := filepath.Join(tmpDir, "invalid.yaml") + + // Invalid YAML content + invalidYAML := ` +global: + isLog: true + invalid: [unclosed +` + err := os.WriteFile(configFile, []byte(invalidYAML), 0644) + if err != nil { + t.Fatalf("failed to create test config: %v", err) + } + + _, err = LoadYamlConfig(configFile) + if err == nil { + t.Error("expected error for invalid YAML") + } +} + +func TestLoadYamlConfigEmpty(t *testing.T) { + tmpDir := t.TempDir() + configFile := filepath.Join(tmpDir, "empty.yaml") + + // Empty YAML content + err := os.WriteFile(configFile, []byte(""), 0644) + if err != nil { + t.Fatalf("failed to create test config: %v", err) + } + + config, err := LoadYamlConfig(configFile) + if err != nil { + t.Fatalf("failed to load empty YAML config: %v", err) + } + + // Verify default values + if config.Global.IsLog { + t.Error("expected IsLog to be false for empty config") + } + if len(config.Variables) != 0 { + t.Errorf("expected 0 variables, got %d", len(config.Variables)) + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..57087af --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module github.com/devfeel/dotlog + +go 1.18 + +require gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..4bc0337 --- /dev/null +++ b/go.sum @@ -0,0 +1,3 @@ +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=