Skip to content

Commit 3cb09d2

Browse files
authored
add a dedicated tester for drop processor configs (#92)
Fixes BIT-6161 Signed-off-by: Matt Klein <[email protected]>
1 parent 5134ddf commit 3cb09d2

File tree

14 files changed

+1119
-124
lines changed

14 files changed

+1119
-124
lines changed

Cargo.lock

Lines changed: 159 additions & 114 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
[workspace]
22
members = [
33
"pulse-common",
4+
"pulse-drop-tester",
45
"pulse-metrics",
56
"pulse-promcli",
67
"pulse-promloadgen",
@@ -17,7 +18,7 @@ assert_matches = "1.5.0"
1718
async-trait = "0.1"
1819
aws-config = "1.8.5"
1920
aws-credential-types = "1.2.5"
20-
aws-sdk-sqs = "1.81.0"
21+
aws-sdk-sqs = "1.82.1"
2122
aws-sigv4 = "1.3.4"
2223
aws-smithy-async = "1.2.5"
2324
aws-smithy-http = "0.62.3"
@@ -40,8 +41,8 @@ bd-test-helpers = { git = "https://github.com/bitdriftlabs/shared-core.gi
4041
bd-time = { git = "https://github.com/bitdriftlabs/shared-core.git" }
4142
built = { version = "0.8", features = ["git2"] }
4243
bytes = "1"
43-
cc = "1.2.33"
44-
clap = { version = "4.5.45", features = ["derive", "env"] }
44+
cc = "1.2.34"
45+
clap = { version = "4.5.46", features = ["derive", "env"] }
4546
comfy-table = "7.1.4"
4647
console-subscriber = "0.4.1"
4748
criterion = { version = "0.7", features = ["html_reports"] }
@@ -129,7 +130,7 @@ topk = "0.5.0"
129130
topological-sort = "0.2.2"
130131
tracing = "0.1.41"
131132
unwrap-infallible = "0.1.5"
132-
url = "2.5.6"
133+
url = "2.5.7"
133134
uuid = { version = "1.18.0", features = ["v4"] }
134135

135136
vrl = { git = "https://github.com/mattklein123/vrl.git", branch = "performance-20250625", default-features = false, features = [

pulse-drop-tester/Cargo.toml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
[package]
2+
edition = "2024"
3+
license-file = "../LICENSE"
4+
name = "pulse-drop-tester"
5+
publish = false
6+
version = "1.0.0"
7+
8+
[lib]
9+
doctest = false
10+
11+
[dependencies]
12+
anyhow.workspace = true
13+
bd-log.workspace = true
14+
bd-server-stats.workspace = true
15+
clap.workspace = true
16+
ctor.workspace = true
17+
log.workspace = true
18+
pretty_assertions.workspace = true
19+
protobuf.workspace = true
20+
pulse-common = { path = "../pulse-common" }
21+
pulse-metrics = { path = "../pulse-metrics" }
22+
pulse-protobuf = { path = "../pulse-protobuf" }
23+
vrl.workspace = true

pulse-drop-tester/README.md

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# Drop tester
2+
3+
The drop tester binary (build via `cargo build --bin pulse-drop-tester`) is used to run self
4+
contained tests.
5+
6+
The configuration for the tests is defined in Protobuf and passed to the tool as YAML. See here for
7+
the [configuration definition](../pulse-protobuf/proto/pulse/drop_tester/v1/drop_tester.proto). The
8+
binary is invoked with the following options:
9+
10+
```
11+
Usage: pulse-drop-tester [OPTIONS] --config <CONFIG>
12+
13+
Options:
14+
-c, --config <CONFIG>
15+
--proxy-config <PROXY_CONFIG>
16+
-h, --help Print help
17+
```
18+
19+
The `-c` option is based the test config. Optionally `--proxy-config` can be used to pass a proxy
20+
configuration to load drop processor configs from. This makes it easier to keep the real
21+
configuration and the test cases in sync.
22+
23+
An example test file look as follows:
24+
25+
```yaml
26+
test_cases:
27+
- config:
28+
rules:
29+
- name: foo
30+
conditions:
31+
- metric_name:
32+
exact: bar
33+
metrics:
34+
- input: bar:1|c
35+
dropped_by: foo
36+
- input: baz:1|g
37+
```
38+
39+
The `dropped_by` test case specifies which rule name should drop the metric. If the metric should
40+
not be dropped just leave it empty/missing.
41+
42+
Currently all input and output metrics are specified in DogStatsD format, even though internally
43+
Prometheus style metrics will work just fine. In the future we will support both formats in tests.

pulse-drop-tester/src/lib.rs

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
// pulse - bitdrift's observability proxy
2+
// Copyright Bitdrift, Inc. All rights reserved.
3+
//
4+
// Use of this source code is governed by a source available license that can be found in the
5+
// LICENSE file or at:
6+
// https://polyformproject.org/wp-content/uploads/2020/06/PolyForm-Shield-1.0.0.txt
7+
8+
#[cfg(test)]
9+
mod test;
10+
11+
use anyhow::{anyhow, bail};
12+
use bd_server_stats::stats::Collector;
13+
use config::bootstrap::v1::bootstrap::Config;
14+
use config::processor::v1::processor::processor_config::Processor_type;
15+
use drop::DropConfig;
16+
use drop::drop_processor_config::Config_source;
17+
use protobuf::Message;
18+
use pulse_common::proto::yaml_to_proto;
19+
use pulse_metrics::pipeline::processor::drop::TranslatedDropConfig;
20+
use pulse_metrics::protos::metric::{DownstreamId, MetricSource, ParsedMetric};
21+
use pulse_metrics::protos::statsd;
22+
use pulse_protobuf::protos::pulse::config;
23+
use pulse_protobuf::protos::pulse::config::common::v1::common::wire_protocol::StatsD;
24+
use pulse_protobuf::protos::pulse::config::processor::v1::drop;
25+
use pulse_protobuf::protos::pulse::config::processor::v1::processor::ProcessorConfig;
26+
use pulse_protobuf::protos::pulse::drop_tester::v1::drop_tester::drop_test_case::Config_type;
27+
use pulse_protobuf::protos::pulse::drop_tester::v1::drop_tester::{DropTestCase, DropTesterConfig};
28+
use std::time::Instant;
29+
30+
#[ctor::ctor]
31+
fn global_init() {
32+
bd_log::SwapLogger::initialize();
33+
}
34+
35+
fn run_test_case(test_case: DropTestCase, proxy_config: Option<&Config>) -> anyhow::Result<usize> {
36+
fn extract_from_config(
37+
field_name: &str,
38+
proxy_config: Option<&Config>,
39+
processor_name: &str,
40+
extract: impl Fn(&ProcessorConfig) -> Option<DropConfig>,
41+
) -> anyhow::Result<DropConfig> {
42+
let Some(config) = proxy_config else {
43+
bail!("{field_name} requires passing a proxy config via --proxy-config");
44+
};
45+
config
46+
.pipeline()
47+
.processors
48+
.iter()
49+
.find_map(|(name, value)| {
50+
if name.as_str() == processor_name {
51+
return extract(value);
52+
}
53+
None
54+
})
55+
.ok_or_else(|| anyhow!("no processor named '{processor_name} found in proxy config"))
56+
}
57+
58+
let drop_config: DropConfig = match test_case.config_type.as_ref().expect("pgv") {
59+
Config_type::Config(config) => Ok(config.clone()),
60+
Config_type::DropProcessorName(processor_name) => extract_from_config(
61+
"mutate_processor_name",
62+
proxy_config,
63+
processor_name,
64+
|value| {
65+
if let Some(Processor_type::Drop(drop)) = &value.processor_type {
66+
return Some(match drop.config_source.as_ref().expect("pgv") {
67+
Config_source::Inline(config) => config.clone(),
68+
Config_source::FileSource(_) => {
69+
// TODO(mattklein123): Support file source if needed.
70+
return None;
71+
},
72+
});
73+
}
74+
75+
None
76+
},
77+
),
78+
}?;
79+
80+
let drop_config = TranslatedDropConfig::new(&drop_config, &Collector::default().scope("test"))?;
81+
82+
let mut num_metrics = 0;
83+
for metric in test_case.metrics {
84+
num_metrics += 1;
85+
86+
// TODO(mattklein123): Support parsing other formats. Probably a limited PromQL query of the
87+
// metric?
88+
let mut input = statsd::parse(
89+
&metric.input.clone().into_bytes(),
90+
StatsD::default_instance(),
91+
)
92+
.map_err(|e| anyhow!("unable to parse input '{}' as statsd: {e}", metric.input))?;
93+
log::debug!("parsed input metric: {input}");
94+
input.timestamp = 0;
95+
let parsed_input = ParsedMetric::new(
96+
input,
97+
MetricSource::PromRemoteWrite,
98+
Instant::now(),
99+
DownstreamId::LocalOrigin,
100+
);
101+
102+
let dropped_by = drop_config.drop_sample(&parsed_input).unwrap_or("");
103+
if metric.dropped_by.as_str() != dropped_by {
104+
bail!(
105+
"expected metric '{}' to be dropped by '{}' but actually dropped by '{}'",
106+
metric.input,
107+
metric.dropped_by,
108+
dropped_by
109+
);
110+
}
111+
}
112+
113+
Ok(num_metrics)
114+
}
115+
116+
pub fn run(config: &str, proxy_config: Option<&str>) -> anyhow::Result<()> {
117+
let config: DropTesterConfig = yaml_to_proto(config)?;
118+
let proxy_config: Option<Config> = proxy_config.map(yaml_to_proto).transpose()?;
119+
120+
let num_test_cases = config.test_cases.len();
121+
let mut num_metrics = 0;
122+
for test_case in config.test_cases {
123+
num_metrics += run_test_case(test_case, proxy_config.as_ref())?;
124+
}
125+
log::info!("processed {num_test_cases} test case(s) and {num_metrics} test metrics(s)");
126+
127+
Ok(())
128+
}

pulse-drop-tester/src/main.rs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// pulse - bitdrift's observability proxy
2+
// Copyright Bitdrift, Inc. All rights reserved.
3+
//
4+
// Use of this source code is governed by a source available license that can be found in the
5+
// LICENSE file or at:
6+
// https://polyformproject.org/wp-content/uploads/2020/06/PolyForm-Shield-1.0.0.txt
7+
8+
use clap::Parser;
9+
use pulse_drop_tester::run;
10+
11+
#[derive(Parser)]
12+
struct Options {
13+
#[arg(short = 'c', long = "config")]
14+
pub config: String,
15+
16+
#[arg(long = "proxy-config")]
17+
pub proxy_config: Option<String>,
18+
}
19+
20+
fn main() -> anyhow::Result<()> {
21+
let options = Options::parse();
22+
log::info!("loading test config from: {}", options.config);
23+
let config = std::fs::read_to_string(options.config)?;
24+
let proxy_config = options
25+
.proxy_config
26+
.map(|proxy_config| {
27+
log::info!("loading proxy config from: {proxy_config}");
28+
std::fs::read_to_string(proxy_config)
29+
})
30+
.transpose()?;
31+
run(&config, proxy_config.as_deref())
32+
}

pulse-drop-tester/src/test/mod.rs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// pulse - bitdrift's observability proxy
2+
// Copyright Bitdrift, Inc. All rights reserved.
3+
//
4+
// Use of this source code is governed by a source available license that can be found in the
5+
// LICENSE file or at:
6+
// https://polyformproject.org/wp-content/uploads/2020/06/PolyForm-Shield-1.0.0.txt
7+
8+
use crate::run;
9+
10+
#[test]
11+
fn basic_case() {
12+
let config = r"
13+
test_cases:
14+
- config:
15+
rules:
16+
- name: foo
17+
conditions:
18+
- metric_name:
19+
exact: bar
20+
metrics:
21+
- input: bar:1|c
22+
dropped_by: foo
23+
- input: baz:1|g
24+
";
25+
26+
run(config, None).unwrap();
27+
}

pulse-metrics/src/pipeline/processor/drop/mod.rs

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -186,12 +186,12 @@ impl TranslatedDropRule {
186186
// TranslatedDropConfig
187187
//
188188

189-
struct TranslatedDropConfig {
189+
pub struct TranslatedDropConfig {
190190
rules: Vec<TranslatedDropRule>,
191191
}
192192

193193
impl TranslatedDropConfig {
194-
fn new(config: &DropConfig, scope: &Scope) -> anyhow::Result<Self> {
194+
pub fn new(config: &DropConfig, scope: &Scope) -> anyhow::Result<Self> {
195195
let rules = config
196196
.rules
197197
.iter()
@@ -201,8 +201,14 @@ impl TranslatedDropConfig {
201201
Ok(Self { rules })
202202
}
203203

204-
fn drop_sample(&self, sample: &ParsedMetric) -> bool {
205-
self.rules.iter().any(|rule| rule.drop_sample(sample))
204+
pub fn drop_sample(&self, sample: &ParsedMetric) -> Option<&str> {
205+
self.rules.iter().find_map(|rule| {
206+
if rule.drop_sample(sample) {
207+
Some(rule.name.as_str())
208+
} else {
209+
None
210+
}
211+
})
206212
}
207213
}
208214

@@ -279,7 +285,7 @@ impl PipelineProcessor for DropProcessor {
279285
let config = self.current_config.read();
280286
samples
281287
.into_iter()
282-
.filter(|sample| !config.drop_sample(sample))
288+
.filter(|sample| config.drop_sample(sample).is_none())
283289
.collect()
284290
};
285291
log::debug!("forwarding {} sample(s)", samples.len());

pulse-metrics/src/pipeline/processor/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ mod aggregation;
3737
mod buffer;
3838
mod cardinality_limiter;
3939
mod cardinality_tracker;
40-
mod drop;
40+
pub mod drop;
4141
pub mod elision;
4242
mod internode;
4343
mod mutate;
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// pulse - bitdrift's observability proxy
2+
// Copyright Bitdrift, Inc. All rights reserved.
3+
//
4+
// Use of this source code is governed by a source available license that can be found in the
5+
// LICENSE file or at:
6+
// https://polyformproject.org/wp-content/uploads/2020/06/PolyForm-Shield-1.0.0.txt
7+
8+
syntax = "proto3";
9+
package pulse.drop_tester.v1;
10+
11+
import "pulse/config/processor/v1/drop.proto";
12+
import "validate/validate.proto";
13+
14+
// A metric transform to test in a given test context.
15+
message MetricDropTest {
16+
// The input metric. Currently this string must be specified in statsd format. For example:
17+
// foo:1|c|#foo:bar
18+
string input = 1 [(validate.rules).string = {min_len: 1}];
19+
20+
// The expected rule name that drops the metric. If the metric is not meant to be dropped, this
21+
// field should be left empty.
22+
string dropped_by = 2;
23+
}
24+
25+
// An individual test case, composed of a drop config and a number of test transforms to perform.
26+
message DropTestCase {
27+
oneof config_type {
28+
option (validate.required) = true;
29+
30+
// The drop config to test against.
31+
config.processor.v1.DropConfig config = 1 [(validate.rules).string = {min_len: 1}];
32+
33+
// The name of the drop processor in the supplied proxy config to load the program from.
34+
string drop_processor_name = 2 [(validate.rules).string = {min_len: 1}];
35+
}
36+
37+
// 1 or more metrics that will be tested against the above parameters.
38+
repeated MetricDropTest metrics = 3 [(validate.rules).repeated .min_items = 1];
39+
}
40+
41+
// Root configuration for a test run. Each test run is composed of 1 or more test cases.
42+
message DropTesterConfig {
43+
// The test cases in the test run.
44+
repeated DropTestCase test_cases = 1 [(validate.rules).repeated .min_items = 1];
45+
}

0 commit comments

Comments
 (0)