Skip to content

Commit fb3700d

Browse files
feat(config): add local config override and env var support for recording fields (#231)
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: Sohil Kshirsagar <sohil@usetusk.ai>
1 parent 06c827b commit fb3700d

9 files changed

Lines changed: 472 additions & 11 deletions

File tree

cmd/drift.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@ import (
77
"github.com/spf13/cobra"
88
)
99

10-
const configFlagUsage = "config file (default is .tusk/config.yaml)"
10+
const (
11+
configFlagUsage = "config file (default is .tusk/config.yaml)"
12+
configOverrideFlagUsage = "config override file (merges on top of the base config)"
13+
)
1114

1215
//go:embed short_docs/drift/drift_overview.md
1316
var driftOverviewContent string
@@ -21,6 +24,7 @@ var driftCmd = &cobra.Command{
2124
func init() {
2225
rootCmd.AddCommand(driftCmd)
2326
driftCmd.PersistentFlags().StringVar(&cfgFile, "config", "", configFlagUsage)
27+
driftCmd.PersistentFlags().StringVar(&cfgOverrideFile, "config-override", "", configOverrideFlagUsage)
2428
}
2529

2630
func bindLegacyDriftAliasConfigFlag(cmd *cobra.Command) {

cmd/drift_query.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,13 @@ func init() {
2828

2929
// setupDriftQueryCloud sets up the API client and resolves the service ID.
3030
func setupDriftQueryCloud(serviceIDFlag string) (*api.TuskClient, api.AuthOptions, string, error) {
31+
// Ensure config is loaded with override support before Get() is called by SetupCloud
32+
if err := config.Load(cfgFile, cfgOverrideFile); err != nil {
33+
if cfgOverrideFile != "" || os.Getenv("TUSK_CONFIG_OVERRIDE") != "" {
34+
return nil, api.AuthOptions{}, "", fmt.Errorf("failed to load config: %w", err)
35+
}
36+
}
37+
3138
client, authOptions, cfg, err := api.SetupCloud(context.Background(), false)
3239
if err != nil {
3340
return nil, api.AuthOptions{}, "", err

cmd/drift_query_services.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,11 @@ package cmd
22

33
import (
44
"context"
5+
"fmt"
6+
"os"
57

68
"github.com/Use-Tusk/tusk-cli/internal/api"
9+
"github.com/Use-Tusk/tusk-cli/internal/config"
710
"github.com/spf13/cobra"
811
)
912

@@ -12,6 +15,13 @@ var driftQueryServicesCmd = &cobra.Command{
1215
Short: "List available Tusk Drift Cloud services",
1316
SilenceUsage: true,
1417
RunE: func(cmd *cobra.Command, args []string) error {
18+
// Ensure config is loaded with override support before Get() is called by SetupCloud
19+
if err := config.Load(cfgFile, cfgOverrideFile); err != nil {
20+
if cfgOverrideFile != "" || os.Getenv("TUSK_CONFIG_OVERRIDE") != "" {
21+
return fmt.Errorf("failed to load config: %w", err)
22+
}
23+
}
24+
1525
client, authOptions, _, err := api.SetupCloud(context.Background(), false)
1626
if err != nil {
1727
return formatApiError(err)

cmd/list.go

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,11 @@ func bindListFlags(cmd *cobra.Command) {
6969
func listTests(cmd *cobra.Command, args []string) error {
7070
setupSignalHandling()
7171

72-
_ = config.Load(cfgFile)
72+
if err := config.Load(cfgFile, cfgOverrideFile); err != nil {
73+
if cfgOverrideFile != "" || os.Getenv("TUSK_CONFIG_OVERRIDE") != "" {
74+
return fmt.Errorf("failed to load config: %w", err)
75+
}
76+
}
7377
cfg, getConfigErr := config.Get()
7478

7579
executor := runner.NewExecutor()
@@ -124,7 +128,11 @@ func listTests(cmd *cobra.Command, args []string) error {
124128
}
125129
tests = runner.ConvertTraceTestsToRunnerTests(all)
126130
} else {
127-
_ = config.Load("")
131+
if err := config.Load("", cfgOverrideFile); err != nil {
132+
if cfgOverrideFile != "" || os.Getenv("TUSK_CONFIG_OVERRIDE") != "" {
133+
return fmt.Errorf("failed to load config: %w", err)
134+
}
135+
}
128136
cfg, getConfigErr := config.Get()
129137

130138
selected := traceDir

cmd/root.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,10 @@ import (
1919
)
2020

2121
var (
22-
cfgFile string
23-
debug bool
24-
showVersion bool
22+
cfgFile string
23+
cfgOverrideFile string
24+
debug bool
25+
showVersion bool
2526

2627
// Cleanup infrastructure
2728
cleanupFuncs []func()

cmd/run.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,11 @@ func runTests(cmd *cobra.Command, args []string) error {
157157
executor := runner.NewExecutor()
158158
executor.SetDebug(debug)
159159

160-
_ = config.Load(cfgFile)
160+
if err := config.Load(cfgFile, cfgOverrideFile); err != nil {
161+
if cfgOverrideFile != "" || os.Getenv("TUSK_CONFIG_OVERRIDE") != "" {
162+
return fmt.Errorf("failed to load config: %w", err)
163+
}
164+
}
161165
cfg, getConfigErr := config.Get()
162166
if getConfigErr == nil && cfg.TestExecution.Concurrency > 0 {
163167
executor.SetConcurrency(cfg.TestExecution.Concurrency)

docs/drift/configuration.md

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,57 @@
22

33
This document lists all configuration options, defaults, environment overrides, and guidance. See [`docs/architecture.md`](architecture.md) for the end‑to‑end flow.
44

5-
Where the CLI reads config from:
5+
Where the CLI reads config from (highest precedence first):
66

77
1. CLI flags (e.g., `--concurrency`, `--results-dir`, `--enable-service-logs`). See `--help` for each command for more details.
88
2. Environment variables (prefix `TUSK_`)
9-
3. Config file (auto-discovered): `.tusk/config.yaml`, `.tusk/config.yml`, `tusk.yaml`, `tusk.yml`, or `~/.tusk/config.yaml`
9+
3. Config override file via `--config-override` flag or `TUSK_CONFIG_OVERRIDE` env var (flag takes precedence)
10+
4. Base config file (auto-discovered): `.tusk/config.yaml`, `.tusk/config.yml`, `tusk.yaml`, `tusk.yml`, or `~/.tusk/config.yaml`
1011

1112
**✨ Run `tusk drift setup` in your service root directory to start an agent automatically create a config file based on your service.**
1213

14+
## Config Overrides
15+
16+
You can provide a config override file that is merged on top of the base config. Only the fields you specify in the override file are changed; everything else is preserved from the base config.
17+
18+
This is useful for local-only settings like disabling span export or changing the sampling mode without modifying the shared config file.
19+
20+
**Using the `--config-override` flag:**
21+
```bash
22+
tusk drift run --config-override .tusk/local-config.yaml
23+
tusk drift list --config-override .tusk/local-config.yaml
24+
```
25+
26+
**Using the `TUSK_CONFIG_OVERRIDE` env var:**
27+
```bash
28+
TUSK_CONFIG_OVERRIDE=.tusk/local-config.yaml tusk drift run
29+
```
30+
31+
The `--config-override` flag takes precedence over the `TUSK_CONFIG_OVERRIDE` env var. Both are overridden by individual env vars (e.g., `TUSK_RECORDING_SAMPLING_MODE`).
32+
33+
Example override file:
34+
35+
```yaml
36+
recording:
37+
sampling:
38+
mode: fixed
39+
base_rate: 1.0
40+
export_spans: false
41+
enable_env_var_recording: false
42+
```
43+
44+
### Recording Environment Variable Overrides
45+
46+
These env vars override the corresponding config keys regardless of what's in the config files:
47+
48+
| Env var | Config key |
49+
|---------|-----------|
50+
| `TUSK_RECORDING_SAMPLING_MODE` | `recording.sampling.mode` |
51+
| `TUSK_RECORDING_SAMPLING_RATE` | `recording.sampling.base_rate` (+ legacy `recording.sampling_rate`) |
52+
| `TUSK_RECORDING_SAMPLING_LOG_TRANSITIONS` | `recording.sampling.log_transitions` |
53+
| `TUSK_RECORDING_EXPORT_SPANS` | `recording.export_spans` |
54+
| `TUSK_ENABLE_ENV_VAR_RECORDING` | `recording.enable_env_var_recording` |
55+
1356
## Service
1457

1558
<table>

internal/config/config.go

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -131,9 +131,14 @@ type CoverageConfig struct {
131131
StripPathPrefix string `koanf:"strip_path_prefix"`
132132
}
133133

134-
// Load loads the config file and applies environment overrides.
134+
// Load loads the config file, applies overrides, and applies environment overrides.
135135
// This function is idempotent - calling it multiple times will only load once.
136-
func Load(configFile string) error {
136+
//
137+
// Config precedence (highest wins):
138+
// 1. Environment variables (TUSK_*)
139+
// 2. Override file (from --config-override flag or TUSK_CONFIG_OVERRIDE env var)
140+
// 3. Base config file (.tusk/config.yaml)
141+
func Load(configFile string, overrideFiles ...string) error {
137142
loadMutex.Lock()
138143
defer loadMutex.Unlock()
139144

@@ -158,6 +163,30 @@ func Load(configFile string) error {
158163
log.Debug("No config file found, using defaults and environment variables")
159164
}
160165

166+
// Determine override file: --config-override flag takes precedence over TUSK_CONFIG_OVERRIDE env var.
167+
// The env var fallback is only checked when the caller explicitly passes the overrideFiles arg
168+
// (even if empty). Callers like ValidateConfigFile that pass no variadic arg won't trigger env
169+
// var lookup, preventing TUSK_CONFIG_OVERRIDE from interfering with validation.
170+
var overridePath string
171+
if len(overrideFiles) > 0 {
172+
overridePath = overrideFiles[0]
173+
if overridePath == "" {
174+
overridePath = os.Getenv("TUSK_CONFIG_OVERRIDE")
175+
}
176+
}
177+
178+
if overridePath != "" {
179+
if _, err := os.Stat(overridePath); err == nil { // #nosec G703 -- path from trusted flag/env var
180+
if err := k.Load(file.Provider(overridePath), yaml.Parser()); err != nil {
181+
return fmt.Errorf("error loading config override file %s: %w", overridePath, err)
182+
}
183+
log.Debug("Config override file loaded", "file", overridePath)
184+
configFileFound = true
185+
} else {
186+
return fmt.Errorf("config override file not found: %s", overridePath)
187+
}
188+
}
189+
161190
// Support environment variable overrides for specific config keys
162191
envOverrides := map[string]string{
163192
"TUSK_TRACES_DIR": "traces.dir",
@@ -167,6 +196,9 @@ func Load(configFile string) error {
167196
"TUSK_RESULTS_DIR": "results.dir",
168197
"TUSK_RECORDING_SAMPLING_RATE": "recording.sampling_rate",
169198
"TUSK_RECORDING_SAMPLING_LOG_TRANSITIONS": "recording.sampling.log_transitions",
199+
"TUSK_RECORDING_SAMPLING_MODE": "recording.sampling.mode",
200+
"TUSK_RECORDING_EXPORT_SPANS": "recording.export_spans",
201+
"TUSK_ENABLE_ENV_VAR_RECORDING": "recording.enable_env_var_recording",
170202
}
171203

172204
for envKey, configKey := range envOverrides {

0 commit comments

Comments
 (0)