feat(config): уровень логирования log.level (info|debug), по умолчанию info
All checks were successful
CI / test (push) Successful in 41s
CI / build-and-package (amd64, linux) (push) Successful in 38s
CI / build-and-package (amd64, windows) (push) Successful in 35s

Отладочные логи API-вызовов к opencode serve теперь включаются только при
log.level=debug (гейт через Client.Debug <- Runner.Debug <- cfg.Log.Debug()).
По умолчанию 'info' — отладочных логов нет.

- config: добавлен LogCfg{Level}, дефолт 'info', валидация (info|debug)
- opencode: Client.Debug и Runner.Debug — гейттят логи do()
- app: Runner.Debug из cfg.Log.Debug()
- config.yaml.example: задокументирован log.level
- тесты: дефолт=info, level=debug, невалидный уровень
This commit is contained in:
Hermes
2026-08-18 18:44:37 +05:00
parent f77a6000c7
commit ad9c2dd522
6 changed files with 90 additions and 8 deletions

View File

@@ -34,3 +34,7 @@ telegram:
# paths:
# worktree: "./worktrees"
# db: "./ratatoskr.db" # или через env RATATOSKR_DB
# log (уровень логирования)
# level: "info" # "info" (по умолчанию) или "debug" — debug включает
# # отладочные логи (напр. все API-вызовы к opencode serve)

View File

@@ -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,
}

View File

@@ -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")

View File

@@ -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...)
}

View File

@@ -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,11 +63,12 @@ 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 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 {
return nil, &ClientErr{Op: "connect", Err: err}
@@ -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 {
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))}
}
if c.Debug {
log.Printf("opencode api %s response (%d bytes): %s", op, len(b), truncateStr(string(b), 5000))
}
return b, nil
}

View File

@@ -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