feat(replicate): add --log-level CLI flag for runtime debugging (#986)

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Cory LaNou
2026-01-13 09:22:20 -06:00
committed by GitHub
parent ecb34a0f43
commit bd57a0fcb6
2 changed files with 128 additions and 1 deletions

View File

@@ -58,6 +58,7 @@ func NewReplicateCommand() *ReplicateCommand {
func (c *ReplicateCommand) ParseFlags(_ context.Context, args []string) (err error) {
fs := flag.NewFlagSet("litestream-replicate", flag.ContinueOnError)
execFlag := fs.String("exec", "", "execute subcommand")
logLevelFlag := fs.String("log-level", "", "log level (trace, debug, info, warn, error)")
restoreIfDBNotExists := fs.Bool("restore-if-db-not-exists", false, "restore from replica if database doesn't exist")
onceFlag := fs.Bool("once", false, "replicate once and exit")
forceSnapshotFlag := fs.Bool("force-snapshot", false, "force snapshot when replicating once")
@@ -78,6 +79,17 @@ func (c *ReplicateCommand) ParseFlags(_ context.Context, args []string) (err err
if c.Config, err = ReadConfigFile(*configPath, !*noExpandEnv); err != nil {
return err
}
// Override log level if CLI flag provided (takes precedence over env var)
if *logLevelFlag != "" {
c.Config.Logging.Level = *logLevelFlag
// Set env var so initLog sees CLI flag as highest priority
os.Setenv("LOG_LEVEL", *logLevelFlag)
logOutput := os.Stdout
if c.Config.Logging.Stderr {
logOutput = os.Stderr
}
initLog(logOutput, c.Config.Logging.Level, c.Config.Logging.Type)
}
case 1:
// Only database path provided, missing replica URL
@@ -91,7 +103,14 @@ func (c *ReplicateCommand) ParseFlags(_ context.Context, args []string) (err err
// Initialize config with defaults when using command-line arguments
c.Config = DefaultConfig()
initLog(os.Stdout, "INFO", "text")
logLevel := "INFO"
if *logLevelFlag != "" {
logLevel = *logLevelFlag
// Set env var so initLog sees CLI flag as highest priority
os.Setenv("LOG_LEVEL", *logLevelFlag)
}
c.Config.Logging.Level = logLevel
initLog(os.Stdout, logLevel, "text")
dbConfig := &DBConfig{
Path: fs.Arg(0),
@@ -482,6 +501,10 @@ Arguments:
This removes snapshots that are older than the configured
snapshot retention period.
-log-level LEVEL
Sets the log level. Overrides the config file setting.
Valid values: trace, debug, info, warn, error
-no-expand-env
Disables environment variable expansion in configuration file.

View File

@@ -2,6 +2,7 @@ package main_test
import (
"context"
"os"
"strings"
"testing"
@@ -172,3 +173,106 @@ func TestReplicateCommand_ParseFlags_FlagPositioning(t *testing.T) {
}
})
}
func TestReplicateCommand_ParseFlags_LogLevel(t *testing.T) {
t.Run("LogLevelWithCLIArgs", func(t *testing.T) {
cmd := main.NewReplicateCommand()
args := []string{"-log-level", "debug", "test.db", "file:///tmp/replica"}
err := cmd.ParseFlags(context.Background(), args)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if cmd.Config.Logging.Level != "debug" {
t.Errorf("expected log level to be %q, got %q", "debug", cmd.Config.Logging.Level)
}
})
t.Run("LogLevelTrace", func(t *testing.T) {
cmd := main.NewReplicateCommand()
args := []string{"-log-level", "trace", "test.db", "file:///tmp/replica"}
err := cmd.ParseFlags(context.Background(), args)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if cmd.Config.Logging.Level != "trace" {
t.Errorf("expected log level to be %q, got %q", "trace", cmd.Config.Logging.Level)
}
})
t.Run("LogLevelError", func(t *testing.T) {
cmd := main.NewReplicateCommand()
args := []string{"-log-level", "error", "test.db", "file:///tmp/replica"}
err := cmd.ParseFlags(context.Background(), args)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if cmd.Config.Logging.Level != "error" {
t.Errorf("expected log level to be %q, got %q", "error", cmd.Config.Logging.Level)
}
})
t.Run("LogLevelDefaultsToInfo", func(t *testing.T) {
cmd := main.NewReplicateCommand()
args := []string{"test.db", "file:///tmp/replica"}
err := cmd.ParseFlags(context.Background(), args)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if cmd.Config.Logging.Level != "INFO" {
t.Errorf("expected log level to default to %q, got %q", "INFO", cmd.Config.Logging.Level)
}
})
t.Run("LogLevelWithOtherFlags", func(t *testing.T) {
cmd := main.NewReplicateCommand()
args := []string{"-log-level", "warn", "-once", "test.db", "file:///tmp/replica"}
err := cmd.ParseFlags(context.Background(), args)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if cmd.Config.Logging.Level != "warn" {
t.Errorf("expected log level to be %q, got %q", "warn", cmd.Config.Logging.Level)
}
})
t.Run("LogLevelAfterPositionalArgs", func(t *testing.T) {
cmd := main.NewReplicateCommand()
args := []string{"test.db", "file:///tmp/replica", "-log-level", "debug"}
err := cmd.ParseFlags(context.Background(), args)
if err == nil {
t.Fatal("expected error when -log-level flag is positioned after positional arguments")
}
expectedError := `flag "-log-level" must be positioned before DB_PATH and REPLICA_URL arguments`
if !strings.Contains(err.Error(), expectedError) {
t.Errorf("expected error message to contain %q, got %q", expectedError, err.Error())
}
})
t.Run("LogLevelFlagOverridesEnvVar", func(t *testing.T) {
// Set LOG_LEVEL env var to a different value
oldEnv := os.Getenv("LOG_LEVEL")
os.Setenv("LOG_LEVEL", "error")
defer func() {
if oldEnv == "" {
os.Unsetenv("LOG_LEVEL")
} else {
os.Setenv("LOG_LEVEL", oldEnv)
}
}()
cmd := main.NewReplicateCommand()
args := []string{"-log-level", "debug", "test.db", "file:///tmp/replica"}
err := cmd.ParseFlags(context.Background(), args)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
// CLI flag should take precedence, setting LOG_LEVEL env var to "debug"
if got := os.Getenv("LOG_LEVEL"); got != "debug" {
t.Errorf("expected LOG_LEVEL env var to be %q (CLI flag), got %q", "debug", got)
}
if cmd.Config.Logging.Level != "debug" {
t.Errorf("expected config log level to be %q, got %q", "debug", cmd.Config.Logging.Level)
}
})
}