diff --git a/config.yaml.example b/config.yaml.example index 424d1a2..0ca06b7 100644 --- a/config.yaml.example +++ b/config.yaml.example @@ -33,4 +33,8 @@ telegram: # paths: # worktree: "./worktrees" -# db: "./ratatoskr.db" # или через env RATATOSKR_DB \ No newline at end of file +# db: "./ratatoskr.db" # или через env RATATOSKR_DB + +# log (уровень логирования) +# level: "info" # "info" (по умолчанию) или "debug" — debug включает +# # отладочные логи (напр. все API-вызовы к opencode serve) diff --git a/internal/app/app.go b/internal/app/app.go index 1b89079..fd436ae 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -120,6 +120,7 @@ func New(configPath, version, updateToken string) (*App, error) { IdleTimeout: cfg.OpenCode.IdleTimeout.Duration(), HardTimeout: cfg.OpenCode.HardTimeout.Duration(), PollInterval: cfg.OpenCode.PollMs.Duration(), + Debug: cfg.Log.Debug(), Stdout: os.Stderr, } diff --git a/internal/config/config_test.go b/internal/config/config_test.go index ba6d883..e77c7d4 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -1,6 +1,7 @@ package config import ( + "errors" "os" "path/filepath" "strings" @@ -90,6 +91,61 @@ telegram: } } +func TestLoad_LogLevelDefault(t *testing.T) { + t.Setenv("TG_TOKEN", "tok") + t.Setenv("TG_CHAT_ID", "42") + + yaml := `telegram: + token: "${TG_TOKEN}" + chat_id: "${TG_CHAT_ID}" +` + cfg, err := Load(writeCfg(t, yaml)) + if err != nil { + t.Fatalf("Load: %v", err) + } + if cfg.Log.Level != "info" { + t.Errorf("log.level = %q, want info (дефолт)", cfg.Log.Level) + } + if cfg.Log.Debug() { + t.Errorf("Debug() = true при уровне info, want false") + } +} + +func TestLoad_LogLevelDebug(t *testing.T) { + t.Setenv("TG_TOKEN", "tok") + t.Setenv("TG_CHAT_ID", "42") + + yaml := `telegram: + token: "${TG_TOKEN}" + chat_id: "${TG_CHAT_ID}" +log: + level: debug +` + cfg, err := Load(writeCfg(t, yaml)) + if err != nil { + t.Fatalf("Load: %v", err) + } + if !cfg.Log.Debug() { + t.Errorf("Debug() = false при level=debug, want true") + } +} + +func TestLoad_LogLevelInvalid(t *testing.T) { + t.Setenv("TG_TOKEN", "tok") + t.Setenv("TG_CHAT_ID", "42") + + yaml := `telegram: + token: "${TG_TOKEN}" + chat_id: "${TG_CHAT_ID}" +log: + level: warn +` + _, err := Load(writeCfg(t, yaml)) + if !errors.Is(err, ErrInvalidFormat) { + t.Fatalf("Load: err = %v, want ErrInvalidFormat", err) + } +} + func TestLoad_OpenCodeConfigDir(t *testing.T) { t.Setenv("TG_TOKEN", "tok") t.Setenv("TG_CHAT_ID", "42") diff --git a/internal/config/types.go b/internal/config/types.go index 7b2e0b6..f4918ca 100644 --- a/internal/config/types.go +++ b/internal/config/types.go @@ -10,6 +10,7 @@ package config import ( "errors" "fmt" + "strings" "time" ) @@ -40,8 +41,18 @@ type Config struct { Chat ChatCfg `yaml:"chat"` Paths PathsCfg `yaml:"paths"` Update UpdateCfg `yaml:"update"` + Log LogCfg `yaml:"log"` } +// LogCfg — уровень логирования. Level: "info" (по умолчанию) или "debug". +// debug включает отладочные логи (напр. все API-вызовы к opencode serve). +type LogCfg struct { + Level string `yaml:"level" default:"info"` +} + +// Debug возвращает true, если включён отладочный уровень логирования. +func (l LogCfg) Debug() bool { return strings.EqualFold(l.Level, "debug") } + // GitCfg — источник репозиториев (для git clone). type GitCfg struct { BaseURL string `yaml:"base_url" env:"GIT_BASE_URL"` @@ -116,5 +127,8 @@ func (c *Config) Validate() error { if c.Telegram.ChatID == "" { errs = append(errs, fmt.Errorf("%w: telegram.chat_id", ErrMissingField)) } + if !c.Log.Debug() && !strings.EqualFold(c.Log.Level, "info") { + errs = append(errs, fmt.Errorf("%w: log.level (ожидается \"info\" или \"debug\")", ErrInvalidFormat)) + } return errors.Join(errs...) } \ No newline at end of file diff --git a/internal/opencode/client.go b/internal/opencode/client.go index 51c202e..79bb6b4 100644 --- a/internal/opencode/client.go +++ b/internal/opencode/client.go @@ -26,6 +26,7 @@ import ( type Client struct { BaseURL string // http://host:port (без завершающего слеша) Password string // basic auth (username "opencode") + Debug bool // включать отладочные логи API-вызовов (log.level=debug) http *http.Client } @@ -62,10 +63,11 @@ func (c *Client) do(ctx context.Context, method, path, op string, body []byte) ( if body != nil { req.Header.Set("Content-Type", "application/json") } - log.Printf("opencode api %s -> %s %s%s", - op, method, c.BaseURL, path) - if len(body) > 0 { - log.Printf("opencode api %s request body: %s", op, truncateStr(string(body), 5000)) + if c.Debug { + log.Printf("opencode api %s -> %s %s%s", op, method, c.BaseURL, path) + if len(body) > 0 { + log.Printf("opencode api %s request body: %s", op, truncateStr(string(body), 5000)) + } } resp, err := c.http.Do(req) if err != nil { @@ -77,10 +79,14 @@ func (c *Client) do(ctx context.Context, method, path, op string, body []byte) ( return nil, &ClientErr{Op: "connect", Err: err} } if resp.StatusCode < 200 || resp.StatusCode > 299 { - log.Printf("opencode api %s response: status %d: %s", op, resp.StatusCode, truncateStr(string(b), 1000)) + if c.Debug { + log.Printf("opencode api %s response: status %d: %s", op, resp.StatusCode, truncateStr(string(b), 1000)) + } return nil, &ClientErr{Op: op, Err: fmt.Errorf("status %d: %s", resp.StatusCode, truncateStr(string(b), 300))} } - log.Printf("opencode api %s response (%d bytes): %s", op, len(b), truncateStr(string(b), 5000)) + if c.Debug { + log.Printf("opencode api %s response (%d bytes): %s", op, len(b), truncateStr(string(b), 5000)) + } return b, nil } diff --git a/internal/opencode/runner.go b/internal/opencode/runner.go index 3aa7409..efb5854 100644 --- a/internal/opencode/runner.go +++ b/internal/opencode/runner.go @@ -31,6 +31,7 @@ type Runner struct { IdleTimeout time.Duration HardTimeout time.Duration PollInterval time.Duration + Debug bool // отладочные логи API-вызовов (из log.level=debug) // Заменяемый для тестов: Stdout io.Writer // диагностика (лог), по умолчанию os.Stderr @@ -71,7 +72,7 @@ func (r *Runner) Run(ctx context.Context, prompt, cwd, agent, sessionID string) if err != nil { return nil, err } - c := &Client{BaseURL: srv.Addr(), Password: srv.Password} + c := &Client{BaseURL: srv.Addr(), Password: srv.Password, Debug: r.Debug} // Сессия: заданная (resume) или новая. sid := sessionID