#![cfg_attr(
not(test),
deny(
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
clippy::unreachable,
clippy::todo,
clippy::unimplemented,
clippy::indexing_slicing,
clippy::string_slice,
clippy::arithmetic_side_effects,
)
)]
use axum::http::{
HeaderMap, Method,
header::{CONTENT_LENGTH, TRANSFER_ENCODING},
};
use crate::entropy::Entropy;
pub const MIRRORABLE_METHODS: [Method; 2] = [Method::GET, Method::HEAD];
pub const SHADOW_HEADER: &str = "x-autumn-shadow";
pub const SHADOW_HEADER_VALUE: &str = "1";
const ALL_ROUTES_LABEL: &str = "*";
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SkipReason {
Method,
LoopGuard,
HasRequestBody,
Conditional,
ExemptPath,
RouteNotOptedIn,
NotSampled,
}
impl SkipReason {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Method => "method",
Self::LoopGuard => "loop_guard",
Self::HasRequestBody => "has_request_body",
Self::Conditional => "conditional",
Self::ExemptPath => "exempt_path",
Self::RouteNotOptedIn => "route_not_opted_in",
Self::NotSampled => "not_sampled",
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum MirrorDecision {
Mirror,
Skip(SkipReason),
}
#[derive(Clone, Debug)]
enum RoutePattern {
Prefix { pattern: String, prefix: String },
Exact(String),
}
impl RoutePattern {
fn parse(raw: &str) -> Self {
raw.strip_suffix('*').map_or_else(
|| Self::Exact(raw.to_owned()),
|prefix| Self::Prefix {
pattern: raw.to_owned(),
prefix: prefix.to_owned(),
},
)
}
fn matches(&self, path: &str) -> bool {
match self {
Self::Prefix { prefix, .. } => path.starts_with(prefix.as_str()),
Self::Exact(exact) => path == exact,
}
}
fn label(&self) -> &str {
match self {
Self::Prefix { pattern, .. } => pattern,
Self::Exact(exact) => exact,
}
}
}
#[derive(Clone, Debug)]
pub struct MirrorSelector {
sample_rate: f64,
routes: std::sync::Arc<Vec<RoutePattern>>,
actuator_prefix: String,
actuator_prefix_slash: String,
probe_paths: std::sync::Arc<Vec<String>>,
}
impl MirrorSelector {
#[must_use]
pub fn new(
sample_rate: f64,
routes: &[String],
actuator_prefix: &str,
probe_paths: &[String],
) -> Self {
let actuator_prefix = crate::actuator::normalize_actuator_prefix(actuator_prefix);
Self {
sample_rate,
routes: std::sync::Arc::new(routes.iter().map(|r| RoutePattern::parse(r)).collect()),
actuator_prefix_slash: format!("{actuator_prefix}/"),
actuator_prefix,
probe_paths: std::sync::Arc::new(probe_paths.to_vec()),
}
}
#[must_use]
pub fn decide(
&self,
method: &Method,
target: &str,
headers: &HeaderMap,
body_known_empty: bool,
roll: impl FnOnce() -> f64,
) -> MirrorDecision {
if !MIRRORABLE_METHODS.contains(method) {
return MirrorDecision::Skip(SkipReason::Method);
}
if headers.contains_key(SHADOW_HEADER) {
return MirrorDecision::Skip(SkipReason::LoopGuard);
}
if !body_known_empty || request_carries_body(headers) {
return MirrorDecision::Skip(SkipReason::HasRequestBody);
}
if is_conditional(headers) {
return MirrorDecision::Skip(SkipReason::Conditional);
}
let path = path_of(target);
if self.is_exempt(path) {
return MirrorDecision::Skip(SkipReason::ExemptPath);
}
if !self.routes.is_empty() && !self.routes.iter().any(|r| r.matches(path)) {
return MirrorDecision::Skip(SkipReason::RouteNotOptedIn);
}
if self.sample_rate <= 0.0 {
return MirrorDecision::Skip(SkipReason::NotSampled);
}
if roll() < self.sample_rate {
MirrorDecision::Mirror
} else {
MirrorDecision::Skip(SkipReason::NotSampled)
}
}
#[must_use]
pub fn route_label(&self, target: &str) -> &str {
let path = path_of(target);
self.routes
.iter()
.find(|r| r.matches(path))
.map_or(ALL_ROUTES_LABEL, RoutePattern::label)
}
fn is_exempt(&self, path: &str) -> bool {
if !self.actuator_prefix.is_empty()
&& (path == self.actuator_prefix || path.starts_with(&self.actuator_prefix_slash))
{
return true;
}
self.probe_paths.iter().any(|probe| probe == path)
}
}
fn path_of(target: &str) -> &str {
target.split('?').next().unwrap_or(target)
}
#[must_use]
fn request_carries_body(headers: &HeaderMap) -> bool {
if headers.contains_key(TRANSFER_ENCODING) {
return true;
}
headers.get(CONTENT_LENGTH).is_some_and(|value| {
!matches!(
value
.to_str()
.ok()
.and_then(|text| text.trim().parse::<u64>().ok()),
Some(0)
)
})
}
fn is_conditional(headers: &HeaderMap) -> bool {
use axum::http::header::{
IF_MATCH, IF_MODIFIED_SINCE, IF_NONE_MATCH, IF_RANGE, IF_UNMODIFIED_SINCE,
};
headers.contains_key(IF_NONE_MATCH)
|| headers.contains_key(IF_MODIFIED_SINCE)
|| headers.contains_key(IF_RANGE)
|| headers.contains_key(IF_MATCH)
|| headers.contains_key(IF_UNMODIFIED_SINCE)
}
#[must_use]
#[allow(
clippy::cast_precision_loss,
reason = "the shift keeps the value in 53 bits, which an f64 represents exactly — \
that is the point of taking the top bits rather than the whole u64"
)]
pub fn roll_from(entropy: &dyn Entropy) -> f64 {
const SCALE: f64 = 1.0 / (1u64 << 53) as f64;
((entropy.next_u64() >> 11) as f64) * SCALE
}
#[cfg(test)]
mod tests {
use super::*;
use axum::http::{HeaderMap, HeaderValue, Method};
fn selector() -> MirrorSelector {
MirrorSelector::new(1.0, &[], "/actuator", &["/healthz".to_owned()])
}
#[test]
fn get_and_head_are_mirrored() {
let selector = selector();
for method in [Method::GET, Method::HEAD] {
assert_eq!(
selector.decide(&method, "/api/orders", &HeaderMap::new(), true, || 0.0),
MirrorDecision::Mirror
);
}
}
#[test]
fn mutating_methods_are_never_mirrored() {
let selector = selector();
for method in [
Method::POST,
Method::PUT,
Method::PATCH,
Method::DELETE,
Method::OPTIONS,
] {
assert_eq!(
selector.decide(&method, "/api/orders", &HeaderMap::new(), true, || 0.0),
MirrorDecision::Skip(SkipReason::Method),
"{method} must not be mirrored"
);
}
}
#[test]
fn a_mirrored_request_is_never_mirrored_again() {
let selector = selector();
let mut headers = HeaderMap::new();
headers.insert(SHADOW_HEADER, HeaderValue::from_static(SHADOW_HEADER_VALUE));
assert_eq!(
selector.decide(&Method::GET, "/api/orders", &headers, true, || 0.0),
MirrorDecision::Skip(SkipReason::LoopGuard)
);
}
#[test]
fn a_get_with_a_nonzero_content_length_is_never_mirrored() {
let selector = selector();
for length in ["1", "42", " 17 "] {
let mut headers = HeaderMap::new();
headers.insert(CONTENT_LENGTH, HeaderValue::from_static(length));
assert_eq!(
selector.decide(&Method::GET, "/api/orders", &headers, true, || 0.0),
MirrorDecision::Skip(SkipReason::HasRequestBody),
"a GET with Content-Length {length} must not be mirrored"
);
}
}
#[test]
fn a_head_with_a_nonzero_content_length_is_never_mirrored() {
let selector = selector();
let mut headers = HeaderMap::new();
headers.insert(CONTENT_LENGTH, HeaderValue::from_static("9"));
assert_eq!(
selector.decide(&Method::HEAD, "/api/orders", &headers, true, || 0.0),
MirrorDecision::Skip(SkipReason::HasRequestBody)
);
}
#[test]
fn conditional_requests_are_never_mirrored() {
use axum::http::header::{
HeaderName, IF_MATCH, IF_MODIFIED_SINCE, IF_NONE_MATCH, IF_RANGE, IF_UNMODIFIED_SINCE,
};
let selector = selector();
let conditionals: [(&HeaderName, &str); 5] = [
(&IF_NONE_MATCH, "\"abc123\""),
(&IF_MODIFIED_SINCE, "Wed, 21 Oct 2015 07:28:00 GMT"),
(&IF_RANGE, "\"abc123\""),
(&IF_MATCH, "\"abc123\""),
(&IF_UNMODIFIED_SINCE, "Wed, 21 Oct 2015 07:28:00 GMT"),
];
for (name, value) in conditionals {
for method in [Method::GET, Method::HEAD] {
let mut headers = HeaderMap::new();
headers.insert(name, HeaderValue::from_str(value).expect("header value"));
assert_eq!(
selector.decide(&method, "/api/orders", &headers, true, || 0.0),
MirrorDecision::Skip(SkipReason::Conditional),
"{method} with {name} must not be mirrored"
);
}
}
let mut headers = HeaderMap::new();
headers.insert(
axum::http::header::ACCEPT,
HeaderValue::from_static("application/json"),
);
assert_eq!(
selector.decide(&Method::GET, "/api/orders", &headers, true, || 0.0),
MirrorDecision::Mirror
);
}
#[test]
fn any_transfer_encoding_marks_a_get_as_body_carrying() {
let selector = selector();
for encoding in ["chunked", "gzip, chunked"] {
let mut headers = HeaderMap::new();
headers.insert(TRANSFER_ENCODING, HeaderValue::from_static(encoding));
assert_eq!(
selector.decide(&Method::GET, "/api/orders", &headers, true, || 0.0),
MirrorDecision::Skip(SkipReason::HasRequestBody),
"Transfer-Encoding: {encoding} means a framed body follows"
);
}
}
#[test]
fn a_get_with_a_zero_or_absent_content_length_is_still_mirrored() {
let selector = selector();
let mut headers = HeaderMap::new();
headers.insert(CONTENT_LENGTH, HeaderValue::from_static("0"));
assert_eq!(
selector.decide(&Method::GET, "/api/orders", &headers, true, || 0.0),
MirrorDecision::Mirror,
"Content-Length: 0 promises no body"
);
assert_eq!(
selector.decide(&Method::GET, "/api/orders", &HeaderMap::new(), true, || 0.0),
MirrorDecision::Mirror,
"a GET with no framing headers is the normal mirrorable case"
);
}
#[test]
fn an_unparseable_content_length_fails_closed_to_skipping() {
let selector = selector();
let mut headers = HeaderMap::new();
headers.insert(CONTENT_LENGTH, HeaderValue::from_static("seventeen"));
assert_eq!(
selector.decide(&Method::GET, "/api/orders", &headers, true, || 0.0),
MirrorDecision::Skip(SkipReason::HasRequestBody),
"a malformed Content-Length cannot be proven empty"
);
}
#[test]
fn an_undeclared_body_on_a_get_is_never_mirrored() {
let selector = selector();
let drew = std::cell::Cell::new(false);
assert_eq!(
selector.decide(
&Method::GET,
"/api/orders",
&HeaderMap::new(),
false,
|| {
drew.set(true);
0.0
}
),
MirrorDecision::Skip(SkipReason::HasRequestBody),
"an undeclared body must not be mirrored"
);
assert!(!drew.get(), "the body gate must decide before the roll");
}
#[test]
fn an_undeclared_body_beats_a_zero_content_length() {
let selector = selector();
let mut headers = HeaderMap::new();
headers.insert(CONTENT_LENGTH, HeaderValue::from_static("0"));
assert_eq!(
selector.decide(&Method::GET, "/api/orders", &headers, false, || 0.0),
MirrorDecision::Skip(SkipReason::HasRequestBody)
);
}
#[test]
fn a_declared_body_skips_even_when_the_stream_reports_empty() {
let selector = selector();
let mut headers = HeaderMap::new();
headers.insert(CONTENT_LENGTH, HeaderValue::from_static("42"));
assert_eq!(
selector.decide(&Method::GET, "/api/orders", &headers, true, || 0.0),
MirrorDecision::Skip(SkipReason::HasRequestBody)
);
}
#[test]
fn the_body_gate_runs_before_the_sampling_roll() {
let selector = selector();
let mut headers = HeaderMap::new();
headers.insert(CONTENT_LENGTH, HeaderValue::from_static("5"));
let drew = std::cell::Cell::new(false);
let decision = selector.decide(&Method::GET, "/api/orders", &headers, true, || {
drew.set(true);
0.0
});
assert_eq!(decision, MirrorDecision::Skip(SkipReason::HasRequestBody));
assert!(!drew.get(), "the body gate must decide before the roll");
}
#[test]
fn actuator_and_probe_paths_are_exempt() {
let selector = selector();
for path in ["/actuator", "/actuator/health", "/healthz"] {
assert_eq!(
selector.decide(&Method::GET, path, &HeaderMap::new(), true, || 0.0),
MirrorDecision::Skip(SkipReason::ExemptPath),
"{path} must be exempt"
);
}
assert_eq!(
selector.decide(
&Method::GET,
"/actuatorsomething",
&HeaderMap::new(),
true,
|| 0.0
),
MirrorDecision::Mirror
);
}
#[test]
fn an_empty_route_list_opts_every_route_in() {
let selector = selector();
assert_eq!(
selector.decide(
&Method::GET,
"/anything/at/all",
&HeaderMap::new(),
true,
|| 0.0
),
MirrorDecision::Mirror
);
}
#[test]
fn route_patterns_gate_which_paths_are_mirrored() {
let selector = MirrorSelector::new(
1.0,
&["/api/*".to_owned(), "/status".to_owned()],
"/actuator",
&[],
);
for path in ["/api/orders", "/api/", "/status"] {
assert_eq!(
selector.decide(&Method::GET, path, &HeaderMap::new(), true, || 0.0),
MirrorDecision::Mirror,
"{path} must match"
);
}
for path in ["/apiary", "/status/detail", "/"] {
assert_eq!(
selector.decide(&Method::GET, path, &HeaderMap::new(), true, || 0.0),
MirrorDecision::Skip(SkipReason::RouteNotOptedIn),
"{path} must not match"
);
}
}
#[test]
fn a_query_string_does_not_defeat_route_matching() {
let selector = MirrorSelector::new(1.0, &["/api/*".to_owned()], "/actuator", &[]);
assert_eq!(
selector.decide(
&Method::GET,
"/api/orders?page=2",
&HeaderMap::new(),
true,
|| 0.0
),
MirrorDecision::Mirror
);
}
#[test]
fn sample_rate_zero_mirrors_nothing() {
let selector = MirrorSelector::new(0.0, &[], "/actuator", &[]);
for roll in [0.0, 0.5, 0.999] {
assert_eq!(
selector.decide(&Method::GET, "/api/orders", &HeaderMap::new(), true, || {
roll
}),
MirrorDecision::Skip(SkipReason::NotSampled)
);
}
}
#[test]
fn sample_rate_is_a_deterministic_function_of_the_roll() {
let selector = MirrorSelector::new(0.25, &[], "/actuator", &[]);
assert_eq!(
selector.decide(&Method::GET, "/api/orders", &HeaderMap::new(), true, || 0.1),
MirrorDecision::Mirror
);
assert_eq!(
selector.decide(&Method::GET, "/api/orders", &HeaderMap::new(), true, || 0.9),
MirrorDecision::Skip(SkipReason::NotSampled)
);
}
#[test]
fn route_label_is_the_configured_pattern_not_the_raw_path() {
let selector = MirrorSelector::new(1.0, &["/api/*".to_owned()], "/actuator", &[]);
assert_eq!(selector.route_label("/api/orders/42"), "/api/*");
let all = MirrorSelector::new(1.0, &[], "/actuator", &[]);
assert_eq!(all.route_label("/api/orders/42"), "*");
}
#[test]
fn the_sampling_roll_is_only_drawn_once_the_cheap_gates_pass() {
let selector = MirrorSelector::new(1.0, &["/api/*".to_owned()], "/actuator", &[]);
let draws = std::cell::Cell::new(0_u32);
let roll = || {
draws.set(draws.get() + 1);
0.0
};
let _ = selector.decide(&Method::POST, "/api/orders", &HeaderMap::new(), true, roll);
assert_eq!(draws.get(), 0);
let mut conditional = HeaderMap::new();
conditional.insert(
axum::http::header::IF_NONE_MATCH,
HeaderValue::from_static("\"x\""),
);
let _ = selector.decide(&Method::GET, "/api/orders", &conditional, true, roll);
assert_eq!(draws.get(), 0);
let _ = selector.decide(
&Method::GET,
"/actuator/health",
&HeaderMap::new(),
true,
roll,
);
assert_eq!(draws.get(), 0);
let _ = selector.decide(&Method::GET, "/elsewhere", &HeaderMap::new(), true, roll);
assert_eq!(draws.get(), 0);
let _ = selector.decide(&Method::GET, "/api/orders", &HeaderMap::new(), true, roll);
assert_eq!(draws.get(), 1);
}
#[test]
fn a_zero_sample_rate_never_draws_at_all() {
let selector = MirrorSelector::new(0.0, &[], "/actuator", &[]);
let drew = std::cell::Cell::new(false);
let decision =
selector.decide(&Method::GET, "/api/orders", &HeaderMap::new(), true, || {
drew.set(true);
0.0
});
assert_eq!(decision, MirrorDecision::Skip(SkipReason::NotSampled));
assert!(!drew.get(), "a disabled sample rate must not draw entropy");
}
#[test]
fn skip_reasons_have_stable_metric_labels() {
assert_eq!(SkipReason::Method.as_str(), "method");
assert_eq!(SkipReason::LoopGuard.as_str(), "loop_guard");
assert_eq!(SkipReason::HasRequestBody.as_str(), "has_request_body");
assert_eq!(SkipReason::Conditional.as_str(), "conditional");
assert_eq!(SkipReason::ExemptPath.as_str(), "exempt_path");
assert_eq!(SkipReason::RouteNotOptedIn.as_str(), "route_not_opted_in");
assert_eq!(SkipReason::NotSampled.as_str(), "not_sampled");
}
#[test]
fn roll_from_entropy_stays_in_range_and_is_reproducible() {
let seeded = crate::entropy::SeededEntropy::new(42);
let first: Vec<f64> = (0..8).map(|_| roll_from(&seeded)).collect();
assert!(first.iter().all(|r| (0.0..1.0).contains(r)), "{first:?}");
let replay = crate::entropy::SeededEntropy::new(42);
let second: Vec<f64> = (0..8).map(|_| roll_from(&replay)).collect();
assert_eq!(first, second, "the same seed must reproduce the same rolls");
}
}