Skip to content

Commit c27d686

Browse files
authored
feat: support adaptive sampling rate (#221)
1 parent 523ec80 commit c27d686

3 files changed

Lines changed: 217 additions & 5 deletions

File tree

docs/drift/configuration.md

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -317,11 +317,29 @@ For authentication in cloud mode, either use:
317317
</tr>
318318
</thead>
319319
<tbody>
320+
<tr>
321+
<td><code>recording.sampling.mode</code></td>
322+
<td>string</td>
323+
<td><code>fixed</code></td>
324+
<td>Sampling strategy for Drift SDK recording. Supported values: <code>fixed</code> and <code>adaptive</code>.</td>
325+
</tr>
326+
<tr>
327+
<td><code>recording.sampling.base_rate</code></td>
328+
<td>number</td>
329+
<td>0.1</td>
330+
<td>Base sampling fraction when recording traces. In <code>fixed</code> mode this is the effective rate. In <code>adaptive</code> mode the SDK may temporarily reduce below this base rate under pressure.</td>
331+
</tr>
332+
<tr>
333+
<td><code>recording.sampling.min_rate</code></td>
334+
<td>number</td>
335+
<td><code>0.001</code> in <code>adaptive</code> mode; unset in <code>fixed</code> mode</td>
336+
<td>Lower bound for adaptive sampling after load shedding is applied. This is only defaulted in <code>adaptive</code> mode and remains unset in <code>fixed</code> mode.</td>
337+
</tr>
320338
<tr>
321339
<td><code>recording.sampling_rate</code></td>
322340
<td>number</td>
323341
<td>0.1</td>
324-
<td>Target sampling fraction when recording traces.</td>
342+
<td><i>Deprecated.</i> Legacy alias for <code>recording.sampling.base_rate</code>. Still accepted for backwards compatibility.</td>
325343
</tr>
326344
<tr>
327345
<td><code>recording.export_spans</code></td>

internal/config/config.go

Lines changed: 54 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -89,10 +89,17 @@ type ComparisonConfig struct {
8989
IgnoreEpochTimestamps *bool `koanf:"ignore_epoch_timestamps"`
9090
}
9191

92+
type RecordingSamplingConfig struct {
93+
Mode string `koanf:"mode"`
94+
BaseRate *float64 `koanf:"base_rate"`
95+
MinRate *float64 `koanf:"min_rate"`
96+
}
97+
9298
type RecordingConfig struct {
93-
SamplingRate float64 `koanf:"sampling_rate"`
94-
ExportSpans *bool `koanf:"export_spans"`
95-
EnableEnvVarRecording *bool `koanf:"enable_env_var_recording"`
99+
SamplingRate float64 `koanf:"sampling_rate"`
100+
Sampling RecordingSamplingConfig `koanf:"sampling"`
101+
ExportSpans *bool `koanf:"export_spans"`
102+
EnableEnvVarRecording *bool `koanf:"enable_env_var_recording"`
96103
}
97104

98105
type ReplayConfig struct {
@@ -165,6 +172,12 @@ func Load(configFile string) error {
165172
if err := k.Set(configKey, val); err != nil {
166173
return fmt.Errorf("error setting %s from env: %w", envKey, err)
167174
}
175+
176+
if envKey == "TUSK_RECORDING_SAMPLING_RATE" {
177+
if err := k.Set("recording.sampling.base_rate", val); err != nil {
178+
return fmt.Errorf("error setting %s nested base rate from env: %w", envKey, err)
179+
}
180+
}
168181
}
169182
}
170183

@@ -215,9 +228,23 @@ func parseAndValidate() (*Config, error) {
215228
if cfg.TestExecution.Timeout == "" {
216229
cfg.TestExecution.Timeout = "30s"
217230
}
218-
if cfg.Recording.SamplingRate == 0 {
231+
if cfg.Recording.Sampling.BaseRate != nil {
232+
cfg.Recording.SamplingRate = *cfg.Recording.Sampling.BaseRate
233+
}
234+
if cfg.Recording.SamplingRate == 0 && cfg.Recording.Sampling.BaseRate == nil {
219235
cfg.Recording.SamplingRate = 0.1
220236
}
237+
if cfg.Recording.Sampling.Mode == "" {
238+
cfg.Recording.Sampling.Mode = "fixed"
239+
}
240+
if cfg.Recording.Sampling.BaseRate == nil {
241+
baseRate := cfg.Recording.SamplingRate
242+
cfg.Recording.Sampling.BaseRate = &baseRate
243+
}
244+
if cfg.Recording.Sampling.MinRate == nil && cfg.Recording.Sampling.Mode == "adaptive" {
245+
minRate := 0.001
246+
cfg.Recording.Sampling.MinRate = &minRate
247+
}
221248
if cfg.Recording.ExportSpans == nil {
222249
defaultExportSpans := false
223250
cfg.Recording.ExportSpans = &defaultExportSpans
@@ -298,6 +325,29 @@ func (cfg *Config) Validate() error {
298325
errs = append(errs, fmt.Errorf("replay.sandbox.mode must be 'auto', 'strict', or 'off', got %s", cfg.Replay.Sandbox.Mode))
299326
}
300327

328+
validSamplingModes := map[string]bool{"fixed": true, "adaptive": true}
329+
if cfg.Recording.Sampling.Mode != "" && !validSamplingModes[cfg.Recording.Sampling.Mode] {
330+
errs = append(errs, fmt.Errorf("recording.sampling.mode must be 'fixed' or 'adaptive', got %s", cfg.Recording.Sampling.Mode))
331+
}
332+
333+
if cfg.Recording.Sampling.BaseRate != nil {
334+
if *cfg.Recording.Sampling.BaseRate < 0 || *cfg.Recording.Sampling.BaseRate > 1 {
335+
errs = append(errs, fmt.Errorf("recording.sampling.base_rate must be between 0.0 and 1.0, got %v", *cfg.Recording.Sampling.BaseRate))
336+
}
337+
}
338+
339+
if cfg.Recording.Sampling.MinRate != nil {
340+
if *cfg.Recording.Sampling.MinRate < 0 || *cfg.Recording.Sampling.MinRate > 1 {
341+
errs = append(errs, fmt.Errorf("recording.sampling.min_rate must be between 0.0 and 1.0, got %v", *cfg.Recording.Sampling.MinRate))
342+
}
343+
}
344+
345+
if cfg.Recording.Sampling.BaseRate != nil && cfg.Recording.Sampling.MinRate != nil {
346+
if *cfg.Recording.Sampling.MinRate > *cfg.Recording.Sampling.BaseRate {
347+
errs = append(errs, fmt.Errorf("recording.sampling.min_rate must be less than or equal to recording.sampling.base_rate (got min_rate=%v, base_rate=%v)", *cfg.Recording.Sampling.MinRate, *cfg.Recording.Sampling.BaseRate))
348+
}
349+
}
350+
301351
if len(errs) > 0 {
302352
return errors.Join(errs...)
303353
}

internal/config/config_test.go

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,150 @@ func evalSymlinks(path string) string {
2020
return resolved
2121
}
2222

23+
func TestNestedRecordingSamplingConfig(t *testing.T) {
24+
defer Invalidate()
25+
26+
tmpDir := t.TempDir()
27+
configPath := filepath.Join(tmpDir, "config.yaml")
28+
require.NoError(t, os.WriteFile(configPath, []byte(`
29+
recording:
30+
sampling:
31+
mode: adaptive
32+
base_rate: 0.25
33+
min_rate: 0.05
34+
`), 0o600))
35+
36+
require.NoError(t, Load(configPath))
37+
38+
cfg, err := Get()
39+
require.NoError(t, err)
40+
assert.Equal(t, "adaptive", cfg.Recording.Sampling.Mode)
41+
require.NotNil(t, cfg.Recording.Sampling.BaseRate)
42+
assert.Equal(t, 0.25, *cfg.Recording.Sampling.BaseRate)
43+
require.NotNil(t, cfg.Recording.Sampling.MinRate)
44+
assert.Equal(t, 0.05, *cfg.Recording.Sampling.MinRate)
45+
assert.Equal(t, 0.25, cfg.Recording.SamplingRate)
46+
}
47+
48+
func TestLegacyRecordingSamplingRateBackfillsNestedSamplingConfig(t *testing.T) {
49+
defer Invalidate()
50+
51+
tmpDir := t.TempDir()
52+
configPath := filepath.Join(tmpDir, "config.yaml")
53+
require.NoError(t, os.WriteFile(configPath, []byte(`
54+
recording:
55+
sampling_rate: 0.25
56+
`), 0o600))
57+
58+
require.NoError(t, Load(configPath))
59+
60+
cfg, err := Get()
61+
require.NoError(t, err)
62+
assert.Equal(t, 0.25, cfg.Recording.SamplingRate)
63+
assert.Equal(t, "fixed", cfg.Recording.Sampling.Mode)
64+
require.NotNil(t, cfg.Recording.Sampling.BaseRate)
65+
assert.Equal(t, 0.25, *cfg.Recording.Sampling.BaseRate)
66+
assert.Nil(t, cfg.Recording.Sampling.MinRate)
67+
}
68+
69+
func TestRecordingSamplingRateEnvOverrideBeatsNestedBaseRate(t *testing.T) {
70+
defer Invalidate()
71+
t.Setenv("TUSK_RECORDING_SAMPLING_RATE", "0.5")
72+
73+
tmpDir := t.TempDir()
74+
configPath := filepath.Join(tmpDir, "config.yaml")
75+
require.NoError(t, os.WriteFile(configPath, []byte(`
76+
recording:
77+
sampling:
78+
mode: adaptive
79+
base_rate: 0.25
80+
min_rate: 0.05
81+
`), 0o600))
82+
83+
require.NoError(t, Load(configPath))
84+
85+
cfg, err := Get()
86+
require.NoError(t, err)
87+
require.NotNil(t, cfg.Recording.Sampling.BaseRate)
88+
assert.Equal(t, 0.5, *cfg.Recording.Sampling.BaseRate)
89+
assert.Equal(t, 0.5, cfg.Recording.SamplingRate)
90+
assert.Equal(t, "adaptive", cfg.Recording.Sampling.Mode)
91+
require.NotNil(t, cfg.Recording.Sampling.MinRate)
92+
assert.Equal(t, 0.05, *cfg.Recording.Sampling.MinRate)
93+
}
94+
95+
func TestValidateRejectsInvalidRecordingSamplingMode(t *testing.T) {
96+
cfg := &Config{
97+
Service: ServiceConfig{
98+
Port: 3000,
99+
Communication: CommunicationConfig{
100+
Type: "auto",
101+
TCPPort: 9001,
102+
},
103+
},
104+
Recording: RecordingConfig{
105+
Sampling: RecordingSamplingConfig{
106+
Mode: "adapttive",
107+
},
108+
},
109+
}
110+
111+
err := cfg.Validate()
112+
require.Error(t, err)
113+
assert.ErrorContains(t, err, "recording.sampling.mode must be 'fixed' or 'adaptive'")
114+
}
115+
116+
func TestValidateRejectsOutOfRangeRecordingSamplingRates(t *testing.T) {
117+
baseRate := 5.0
118+
minRate := -0.1
119+
cfg := &Config{
120+
Service: ServiceConfig{
121+
Port: 3000,
122+
Communication: CommunicationConfig{
123+
Type: "auto",
124+
TCPPort: 9001,
125+
},
126+
},
127+
Recording: RecordingConfig{
128+
Sampling: RecordingSamplingConfig{
129+
Mode: "adaptive",
130+
BaseRate: &baseRate,
131+
MinRate: &minRate,
132+
},
133+
},
134+
}
135+
136+
err := cfg.Validate()
137+
require.Error(t, err)
138+
assert.ErrorContains(t, err, "recording.sampling.base_rate must be between 0.0 and 1.0")
139+
assert.ErrorContains(t, err, "recording.sampling.min_rate must be between 0.0 and 1.0")
140+
}
141+
142+
func TestValidateRejectsRecordingMinRateGreaterThanBaseRate(t *testing.T) {
143+
baseRate := 0.1
144+
minRate := 0.9
145+
cfg := &Config{
146+
Service: ServiceConfig{
147+
Port: 3000,
148+
Communication: CommunicationConfig{
149+
Type: "auto",
150+
TCPPort: 9001,
151+
},
152+
},
153+
Recording: RecordingConfig{
154+
Sampling: RecordingSamplingConfig{
155+
Mode: "adaptive",
156+
BaseRate: &baseRate,
157+
MinRate: &minRate,
158+
},
159+
},
160+
}
161+
162+
err := cfg.Validate()
163+
require.Error(t, err)
164+
assert.ErrorContains(t, err, "recording.sampling.min_rate must be less than or equal to recording.sampling.base_rate")
165+
}
166+
23167
func TestFindConfigFile_ParentTraversal(t *testing.T) {
24168
wd, _ := os.Getwd()
25169
defer func() { _ = os.Chdir(wd) }()

0 commit comments

Comments
 (0)