vector/transforms/sample/
config.rs1use snafu::Snafu;
2use vector_lib::{
3 config::LegacyKey,
4 configurable::configurable_component,
5 lookup::{lookup_v2::OptionalValuePath, owned_value_path},
6};
7use vrl::value::Kind;
8
9use super::transform::{DynamicSampleFields, Sample, SampleMode};
10use crate::{
11 conditions::AnyCondition,
12 config::{
13 DataType, GenerateConfig, Input, OutputId, TransformConfig, TransformContext,
14 TransformOutput,
15 },
16 schema,
17 template::UnconfinedTemplate,
18 transforms::Transform,
19};
20
21#[derive(Debug, Snafu)]
22pub enum SampleError {
23 #[snafu(display(
25 "Only positive, non-zero numbers are allowed values for `ratio`, value: {ratio}"
26 ))]
27 InvalidRatio { ratio: f64 },
28
29 #[snafu(display("Only non-zero numbers are allowed values for `rate`"))]
30 InvalidRate,
31
32 #[snafu(display("Only one value can be provided for either 'rate' or 'ratio', but not both"))]
33 InvalidStaticConfiguration,
34
35 #[snafu(display(
36 "Only one value can be provided for either 'ratio_field' or 'rate_field', but not both"
37 ))]
38 InvalidDynamicConfiguration,
39
40 #[snafu(display(
41 "Exactly one value must be provided for either 'rate' or 'ratio' to configure static sampling"
42 ))]
43 MissingStaticConfiguration,
44
45 #[snafu(display(
46 "'key_field' cannot be combined with 'ratio_field' or 'rate_field' because dynamic values can vary per event and break key-based coherence"
47 ))]
48 InvalidKeyFieldDynamicCombination,
49}
50
51#[configurable_component(transform(
53 "sample",
54 "Sample events from an event stream based on supplied criteria and at a configurable rate."
55))]
56#[derive(Clone, Debug)]
57#[serde(deny_unknown_fields)]
58pub struct SampleConfig {
59 #[configurable(required_one_of = "sampling_strategy")]
65 pub rate: Option<u64>,
66
67 #[configurable(required_one_of = "sampling_strategy", metadata(docs::examples = 0.13))]
73 #[configurable(validation(range(min = 0.0, max = 1.0)))]
74 pub ratio: Option<f64>,
75
76 pub ratio_field: Option<String>,
83
84 pub rate_field: Option<String>,
91
92 #[configurable(metadata(docs::examples = "message"))]
106 pub key_field: Option<String>,
107
108 #[configurable(metadata(docs::examples = "sample_rate"))]
110 #[serde(default = "default_sample_rate_key")]
111 pub sample_rate_key: OptionalValuePath,
112
113 #[configurable(metadata(
121 docs::examples = "{{ service }}",
122 docs::examples = "{{ hostname }}-{{ service }}"
123 ))]
124 pub group_by: Option<UnconfinedTemplate>,
125
126 pub exclude: Option<AnyCondition>,
128}
129
130impl SampleConfig {
131 fn sample_rate(&self) -> Result<SampleMode, SampleError> {
132 if self.ratio_field.is_some() && self.rate_field.is_some() {
133 return Err(SampleError::InvalidDynamicConfiguration);
134 }
135
136 if self.key_field.is_some() && (self.ratio_field.is_some() || self.rate_field.is_some()) {
137 return Err(SampleError::InvalidKeyFieldDynamicCombination);
138 }
139
140 if self.rate.is_some() && self.ratio.is_some() {
141 return Err(SampleError::InvalidStaticConfiguration);
142 }
143
144 match (self.rate, self.ratio) {
145 (None, Some(ratio)) => {
146 if ratio <= 0.0 {
147 Err(SampleError::InvalidRatio { ratio })
148 } else {
149 Ok(SampleMode::new_ratio(ratio))
150 }
151 }
152 (Some(rate), None) => {
153 if rate == 0 {
154 Err(SampleError::InvalidRate)
155 } else {
156 Ok(SampleMode::new_rate(rate))
157 }
158 }
159 (None, None) => Err(SampleError::MissingStaticConfiguration),
160 _ => Err(SampleError::InvalidStaticConfiguration),
161 }
162 }
163}
164
165impl GenerateConfig for SampleConfig {
166 fn generate_config() -> serde_json::Value {
167 serde_json::to_value(Self {
168 rate: None,
169 ratio: Some(0.1),
170 ratio_field: None,
171 rate_field: None,
172 key_field: None,
173 group_by: None,
174 exclude: None::<AnyCondition>,
175 sample_rate_key: default_sample_rate_key(),
176 })
177 .unwrap()
178 }
179}
180
181#[async_trait::async_trait]
182#[typetag::serde(name = "sample")]
183impl TransformConfig for SampleConfig {
184 async fn build(&self, context: &TransformContext) -> crate::Result<Transform> {
185 let sample_mode = self.sample_rate()?;
186 let exclude = self
187 .exclude
188 .as_ref()
189 .map(|condition| condition.build(&context.enrichment_tables, &context.metrics_storage))
190 .transpose()?;
191
192 let sample = if self.ratio_field.is_some() || self.rate_field.is_some() {
193 Sample::new_with_dynamic(
194 Self::NAME.to_string(),
195 sample_mode,
196 DynamicSampleFields {
197 ratio_field: self.ratio_field.clone(),
198 rate_field: self.rate_field.clone(),
199 },
200 self.group_by.clone(),
201 exclude,
202 self.sample_rate_key.clone(),
203 )
204 } else {
205 Sample::new(
206 Self::NAME.to_string(),
207 sample_mode,
208 self.key_field.clone(),
209 self.group_by.clone(),
210 exclude,
211 self.sample_rate_key.clone(),
212 )
213 };
214
215 Ok(Transform::function(sample))
216 }
217
218 fn input(&self) -> Input {
219 Input::new(DataType::Log | DataType::Trace)
220 }
221
222 fn validate_structure(&self) -> Result<(), Vec<String>> {
223 self.sample_rate()
224 .map(|_| ())
225 .map_err(|e| vec![e.to_string()])
226 }
227
228 fn validate_with_context(&self, context: &TransformContext) -> Result<(), Vec<String>> {
229 if let Some(Err(e)) = self
230 .exclude
231 .as_ref()
232 .map(|c| c.validate(&context.enrichment_tables, &context.metrics_storage))
233 {
234 Err(vec![format!("exclude: {e}")])
235 } else {
236 Ok(())
237 }
238 }
239
240 fn outputs(
241 &self,
242 _: &TransformContext,
243 input_definitions: &[(OutputId, schema::Definition)],
244 ) -> Vec<TransformOutput> {
245 vec![TransformOutput::new(
246 DataType::Log | DataType::Trace,
247 input_definitions
248 .iter()
249 .map(|(output, definition)| {
250 (
251 output.clone(),
252 definition.clone().with_source_metadata(
253 SampleConfig::NAME,
254 Some(LegacyKey::Overwrite(owned_value_path!("sample_rate"))),
255 &owned_value_path!("sample_rate"),
256 Kind::bytes(),
257 None,
258 ),
259 )
260 })
261 .collect(),
262 )]
263 }
264}
265
266pub fn default_sample_rate_key() -> OptionalValuePath {
267 OptionalValuePath::from(owned_value_path!("sample_rate"))
268}
269
270#[cfg(test)]
271mod tests {
272 use crate::{
273 config::TransformConfig,
274 transforms::sample::config::{SampleConfig, SampleError},
275 };
276
277 #[test]
278 fn generate_config() {
279 crate::test_util::test_generate_config::<SampleConfig>();
280 }
281
282 #[test]
283 fn rejects_dynamic_ratio_only_configuration() {
284 let config = SampleConfig {
285 rate: None,
286 ratio: None,
287 ratio_field: Some("sample_rate".to_string()),
288 rate_field: None,
289 key_field: None,
290 sample_rate_key: super::default_sample_rate_key(),
291 group_by: None,
292 exclude: None,
293 };
294
295 let err = config.sample_rate().unwrap_err();
296 assert!(matches!(err, SampleError::MissingStaticConfiguration));
297 }
298
299 #[test]
300 fn rejects_dynamic_rate_only_configuration() {
301 let config = SampleConfig {
302 rate: None,
303 ratio: None,
304 ratio_field: None,
305 rate_field: Some("sample_rate_n".to_string()),
306 key_field: None,
307 sample_rate_key: super::default_sample_rate_key(),
308 group_by: None,
309 exclude: None,
310 };
311
312 let err = config.sample_rate().unwrap_err();
313 assert!(matches!(err, SampleError::MissingStaticConfiguration));
314 }
315
316 #[test]
317 fn validates_static_with_dynamic_configuration() {
318 let config = SampleConfig {
319 rate: Some(10),
320 ratio: None,
321 ratio_field: None,
322 rate_field: Some("sample_rate_n".to_string()),
323 key_field: None,
324 sample_rate_key: super::default_sample_rate_key(),
325 group_by: None,
326 exclude: None,
327 };
328
329 assert!(config.validate_structure().is_ok());
330 }
331
332 #[test]
333 fn rejects_both_dynamic_fields_configuration() {
334 let config = SampleConfig {
335 rate: Some(10),
336 ratio: None,
337 ratio_field: Some("sample_rate".to_string()),
338 rate_field: Some("sample_rate_n".to_string()),
339 key_field: None,
340 sample_rate_key: super::default_sample_rate_key(),
341 group_by: None,
342 exclude: None,
343 };
344
345 let err = config.sample_rate().unwrap_err();
346 assert!(matches!(err, SampleError::InvalidDynamicConfiguration));
347 }
348
349 #[test]
350 fn rejects_key_field_with_dynamic_configuration() {
351 let config = SampleConfig {
352 rate: Some(10),
353 ratio: None,
354 ratio_field: Some("sample_ratio".to_string()),
355 rate_field: None,
356 key_field: Some("trace_id".to_string()),
357 sample_rate_key: super::default_sample_rate_key(),
358 group_by: None,
359 exclude: None,
360 };
361
362 let err = config.sample_rate().unwrap_err();
363 assert!(matches!(
364 err,
365 SampleError::InvalidKeyFieldDynamicCombination
366 ));
367 }
368}