Skip to content

Commit

Permalink
feat(dynamic-sampling): Add possibility to run dynamic sampling from …
Browse files Browse the repository at this point in the history
…`sentry-relay` (#2091)
  • Loading branch information
iambriccardo authored May 8, 2023
1 parent 7cf8b68 commit 6e5bab1
Show file tree
Hide file tree
Showing 6 changed files with 303 additions and 4 deletions.
1 change: 1 addition & 0 deletions py/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
## Unreleased

- Store `geo.subdivision` of the end user location. ([#2058](https://github.com/getsentry/relay/pull/2058))
- Add new FFI function for running dynamic sampling. ([#2091](https://github.com/getsentry/relay/pull/2091))

## 0.8.21

Expand Down
21 changes: 21 additions & 0 deletions py/sentry_relay/processing.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"validate_sampling_condition",
"validate_sampling_configuration",
"validate_project_config",
"run_dynamic_sampling",
]


Expand Down Expand Up @@ -262,3 +263,23 @@ def validate_project_config(config, strict: bool):
error = decode_str(raw_error, free=True)
if error:
raise ValueError(error)


def run_dynamic_sampling(sampling_config, root_sampling_config, dsc, event):
"""
Runs dynamic sampling on an event and returns the merged rules together with the sample rate.
"""
assert isinstance(sampling_config, string_types)
assert isinstance(root_sampling_config, string_types)
assert isinstance(dsc, string_types)
assert isinstance(event, string_types)

result_json = rustcall(
lib.run_dynamic_sampling,
encode_str(sampling_config),
encode_str(root_sampling_config),
encode_str(dsc),
encode_str(event),
)

return json.loads(decode_str(result_json, free=True))
212 changes: 212 additions & 0 deletions py/tests/test_processing.py
Original file line number Diff line number Diff line change
Expand Up @@ -283,3 +283,215 @@ def test_validate_project_config():
with pytest.raises(ValueError) as e:
sentry_relay.validate_project_config(json.dumps(config), strict=True)
assert str(e.value) == 'json atom at path ".foobar" is missing from rhs'


def test_run_dynamic_sampling_with_valid_params_and_match():
sampling_config = """{
"rules": [],
"rulesV2": [
{
"samplingValue":{
"type": "factor",
"value": 2.0
},
"type": "transaction",
"active": true,
"condition": {
"op": "and",
"inner": [
{
"op": "eq",
"name": "event.transaction",
"value": [
"/world"
],
"options": {
"ignoreCase": true
}
}
]
},
"id": 1000
}
],
"mode": "received"
}"""

root_sampling_config = """{
"rules": [],
"rulesV2": [
{
"samplingValue":{
"type": "sampleRate",
"value": 0.5
},
"type": "trace",
"active": true,
"condition": {
"op": "and",
"inner": []
},
"id": 1001
}
],
"mode": "received"
}"""

dsc = """{
"trace_id": "d0303a19-909a-4b0b-a639-b17a74c3533b",
"public_key": "abd0f232775f45feab79864e580d160b",
"release": "1.0",
"environment": "dev",
"transaction": "/hello",
"replay_id": "d0303a19-909a-4b0b-a639-b17a73c3533b"
}"""

event = """{
"type": "transaction",
"transaction": "/world"
}"""

result = sentry_relay.run_dynamic_sampling(
sampling_config,
root_sampling_config,
dsc,
event,
)
assert "merged_sampling_configs" in result
assert result["sampling_match"] == {
"sample_rate": 1.0,
"seed": "d0303a19-909a-4b0b-a639-b17a74c3533b",
"matched_rule_ids": [1000, 1001],
}


def test_run_dynamic_sampling_with_valid_params_and_no_match():
sampling_config = """{
"rules": [],
"rulesV2": [],
"mode": "received"
}"""

root_sampling_config = """{
"rules": [],
"rulesV2": [
{
"samplingValue":{
"type": "sampleRate",
"value": 0.5
},
"type": "trace",
"active": true,
"condition": {
"op": "and",
"inner": [
{
"op": "eq",
"name": "trace.transaction",
"value": [
"/foo"
],
"options": {
"ignoreCase": true
}
}
]
},
"id": 1001
}
],
"mode": "received"
}"""

dsc = """{
"trace_id": "d0303a19-909a-4b0b-a639-b17a74c3533b",
"public_key": "abd0f232775f45feab79864e580d160b",
"release": "1.0",
"environment": "dev",
"transaction": "/hello",
"replay_id": "d0303a19-909a-4b0b-a639-b17a73c3533b"
}"""

event = """{
"type": "transaction",
"transaction": "/world"
}"""

result = sentry_relay.run_dynamic_sampling(
sampling_config,
root_sampling_config,
dsc,
event,
)
assert "merged_sampling_configs" in result
assert result["sampling_match"] is None


def test_run_dynamic_sampling_with_valid_params_and_no_dsc_and_no_event():
sampling_config = """{
"rules": [],
"rulesV2": [],
"mode": "received"
}"""

root_sampling_config = """{
"rules": [],
"rulesV2": [
{
"samplingValue":{
"type": "sampleRate",
"value": 0.5
},
"type": "trace",
"active": true,
"condition": {
"op": "and",
"inner": []
},
"id": 1001
}
],
"mode": "received"
}"""

dsc = "{}"

event = "{}"

result = sentry_relay.run_dynamic_sampling(
sampling_config,
root_sampling_config,
dsc,
event,
)
assert "merged_sampling_configs" in result
assert result["sampling_match"] is None


def test_run_dynamic_sampling_with_invalid_params():
sampling_config = """{
"rules": [],
"mode": "received"
}"""

root_sampling_config = """{
"rules": [],
"mode": "received"
}"""

dsc = """{
"trace_id": "d0303a19-909a-4b0b-a639-b17a74c3533b",
}"""

event = """{
"type": "transaction",
"test": "/test"
}"""

with pytest.raises(sentry_relay.InvalidJsonError):
sentry_relay.run_dynamic_sampling(
sampling_config,
root_sampling_config,
dsc,
event,
)
10 changes: 10 additions & 0 deletions relay-cabi/include/relay.h
Original file line number Diff line number Diff line change
Expand Up @@ -597,4 +597,14 @@ struct RelayStr relay_validate_sampling_configuration(const struct RelayStr *val
struct RelayStr relay_validate_project_config(const struct RelayStr *value,
bool strict);

/**
* Runs dynamic sampling given the sampling config, root sampling config, dsc and event.
*
* Returns the sampling decision containing the sample_rate and the list of matched rule ids.
*/
struct RelayStr run_dynamic_sampling(const struct RelayStr *sampling_config,
const struct RelayStr *root_sampling_config,
const struct RelayStr *dsc,
const struct RelayStr *event);

#endif /* RELAY_H_INCLUDED */
57 changes: 56 additions & 1 deletion relay-cabi/src/processing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ use std::ffi::CStr;
use std::os::raw::c_char;
use std::slice;

use anyhow::Context;
use chrono::Utc;
use once_cell::sync::OnceCell;
use relay_common::{codeowners_match_bytes, glob_match_bytes, GlobOptions};
use relay_dynamic_config::{validate_json, ProjectConfig};
Expand All @@ -21,7 +23,11 @@ use relay_general::store::{
};
use relay_general::types::{Annotated, Remark};
use relay_general::user_agent::RawUserAgentInfo;
use relay_sampling::{RuleCondition, SamplingConfig};
use relay_sampling::{
merge_rules_from_configs, DynamicSamplingContext, RuleCondition, SamplingConfig, SamplingMatch,
SamplingRule,
};
use serde::Serialize;

use crate::core::{RelayBuf, RelayStr};

Expand Down Expand Up @@ -321,3 +327,52 @@ pub unsafe extern "C" fn relay_validate_project_config(
Err(e) => RelayStr::from_string(e.to_string()),
}
}

#[derive(Debug, Serialize)]
struct EphemeralSamplingResult {
merged_sampling_configs: Vec<SamplingRule>,
sampling_match: Option<SamplingMatch>,
}

/// Runs dynamic sampling given the sampling config, root sampling config, dsc and event.
///
/// Returns the sampling decision containing the sample_rate and the list of matched rule ids.
#[no_mangle]
#[relay_ffi::catch_unwind]
pub unsafe extern "C" fn run_dynamic_sampling(
sampling_config: &RelayStr,
root_sampling_config: &RelayStr,
dsc: &RelayStr,
event: &RelayStr,
) -> RelayStr {
let sampling_config = serde_json::from_str::<SamplingConfig>(sampling_config.as_str())?;
let root_sampling_config =
serde_json::from_str::<SamplingConfig>(root_sampling_config.as_str())?;
// We can optionally accept a dsc and event.
let dsc = serde_json::from_str::<DynamicSamplingContext>(dsc.as_str());
let event = Annotated::<Event>::from_json(event.as_str())?;
let event = event.value().context("the event can't be serialized")?;

// Instead of creating a new function, we decided to reuse the existing code here. This will have
// the only downside of not having the possibility to set the sample rate to a different value
// based on the `SamplingMode` but for this simulation it is not that relevant.
let rules: Vec<SamplingRule> =
merge_rules_from_configs(&sampling_config, Some(&root_sampling_config))
.cloned()
.collect();

// Only if we have both dsc and event we want to run dynamic sampling, otherwise we just return
// the merged sampling configs.
let match_result = if let Ok(dsc) = dsc {
SamplingMatch::match_against_rules(rules.iter(), event, Some(&dsc), None, Utc::now())
} else {
None
};

let result = EphemeralSamplingResult {
merged_sampling_configs: rules,
sampling_match: match_result,
};

RelayStr::from(serde_json::to_string(&result).unwrap())
}
6 changes: 3 additions & 3 deletions relay-sampling/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -913,7 +913,7 @@ impl Default for SamplingMode {
}

/// Represents a list of rule ids which is used for outcomes.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]
pub struct MatchedRuleIds(pub Vec<RuleId>);

impl MatchedRuleIds {
Expand Down Expand Up @@ -970,7 +970,7 @@ fn check_unsupported_rules(
///
/// The chaining logic will take all the non-trace rules from the project and all the trace/unsupported
/// rules from the root project and concatenate them.
fn merge_rules_from_configs<'a>(
pub fn merge_rules_from_configs<'a>(
sampling_config: &'a SamplingConfig,
root_sampling_config: Option<&'a SamplingConfig>,
) -> impl Iterator<Item = &'a SamplingRule> {
Expand Down Expand Up @@ -1032,7 +1032,7 @@ pub fn merge_configs_and_match(
}

/// Represents the specification for sampling an incoming event.
#[derive(Clone, Debug, PartialEq)]
#[derive(Clone, Debug, PartialEq, Serialize)]
pub struct SamplingMatch {
/// The sample rate to use for the incoming event.
pub sample_rate: f64,
Expand Down

0 comments on commit 6e5bab1

Please sign in to comment.