-
Notifications
You must be signed in to change notification settings - Fork 94
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(stats): Emit outcomes for applied rate limits #951
Changes from 4 commits
328b762
79ccc02
307f931
3c556b6
9b36fc0
f76e797
be73b2b
531bf2b
168b6e4
4c87e85
4acba6d
3de6c5b
c43bd1a
e3dc62f
6dcf4de
4e37a7a
697381b
e76b6e5
44e892b
afc85a7
9648aaa
96198aa
cba3a0d
33bee5b
686fdd0
ae00b96
d4af165
c8394e6
d971c52
bd6071f
d1d2ff3
f045d7b
4719355
c226d61
6845720
a126f91
760dac5
8faf98c
1a042b1
22f9328
504a8bd
56cd51f
ef105a5
2f65537
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,5 @@ | ||
use std::sync::Arc; | ||
use std::time::{Duration, Instant}; | ||
use std::{net::IpAddr, sync::Arc}; | ||
|
||
use actix::prelude::*; | ||
use chrono::{DateTime, Utc}; | ||
|
@@ -10,19 +10,24 @@ use smallvec::SmallVec; | |
use url::Url; | ||
|
||
use relay_auth::PublicKey; | ||
use relay_common::{metric, ProjectId, ProjectKey}; | ||
use relay_common::{metric, DataCategory, ProjectId, ProjectKey}; | ||
use relay_config::Config; | ||
use relay_filter::{matches_any_origin, FiltersConfig}; | ||
use relay_general::pii::{DataScrubbingConfig, PiiConfig}; | ||
use relay_quotas::{Quota, RateLimits, Scoping}; | ||
use relay_general::{ | ||
pii::{DataScrubbingConfig, PiiConfig}, | ||
protocol::EventId, | ||
}; | ||
use relay_quotas::{Quota, RateLimits, ReasonCode, Scoping}; | ||
use relay_sampling::SamplingConfig; | ||
|
||
use crate::actors::outcome::DiscardReason; | ||
use crate::actors::project_cache::{FetchProjectState, ProjectCache, ProjectError}; | ||
use crate::envelope::Envelope; | ||
use crate::extractors::RequestMeta; | ||
use crate::metrics::RelayCounters; | ||
use crate::utils::{ActorResponse, EnvelopeLimiter, Response}; | ||
use crate::utils::{ActorResponse, EnvelopeLimiter, EnvelopeSummary, Response}; | ||
|
||
use super::outcome::{Outcome, OutcomeProducer, TrackOutcome}; | ||
|
||
/// The current status of a project state. Return value of `ProjectState::outdated`. | ||
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)] | ||
|
@@ -549,9 +554,12 @@ impl Project { | |
|
||
fn check_envelope( | ||
&mut self, | ||
mut envelope: Envelope, | ||
message: CheckEnvelope, | ||
scoping: &Scoping, | ||
) -> Result<CheckedEnvelope, DiscardReason> { | ||
let rate_limit_envelope = RateLimitEnvelope::new(&message, scoping); | ||
let mut envelope = message.envelope; | ||
|
||
if let Some(state) = self.state() { | ||
state.check_request(envelope.meta(), &self.config)?; | ||
} | ||
|
@@ -560,7 +568,9 @@ impl Project { | |
|
||
let quotas = self.state().map(|s| s.get_quotas()).unwrap_or(&[]); | ||
let envelope_limiter = EnvelopeLimiter::new(|item_scoping, _| { | ||
Ok(self.rate_limits.check_with_quotas(quotas, item_scoping)) | ||
let applied_limits = self.rate_limits.check_with_quotas(quotas, item_scoping); | ||
rate_limit_envelope.emit_rate_limit_outcomes(&applied_limits); | ||
Ok(applied_limits) | ||
}); | ||
|
||
let rate_limits = envelope_limiter.enforce(&mut envelope, scoping)?; | ||
|
@@ -578,7 +588,7 @@ impl Project { | |
|
||
fn check_envelope_scoped(&mut self, message: CheckEnvelope) -> CheckEnvelopeResponse { | ||
let scoping = self.get_scoping(message.envelope.meta()); | ||
let result = self.check_envelope(message.envelope, &scoping); | ||
let result = self.check_envelope(message, &scoping); | ||
CheckEnvelopeResponse { result, scoping } | ||
} | ||
} | ||
|
@@ -595,6 +605,60 @@ impl Actor for Project { | |
} | ||
} | ||
|
||
struct RateLimitEnvelope { | ||
event_id: Option<EventId>, | ||
remote_addr: Option<IpAddr>, | ||
envelope_summary: EnvelopeSummary, | ||
outcome_producer: Addr<OutcomeProducer>, | ||
scoping: Scoping, | ||
} | ||
|
||
impl RateLimitEnvelope { | ||
fn new(message: &CheckEnvelope, scoping: &Scoping) -> Self { | ||
let envelope = &message.envelope; | ||
Self { | ||
event_id: envelope.event_id(), | ||
remote_addr: envelope.meta().client_addr(), | ||
envelope_summary: EnvelopeSummary::compute(&envelope), | ||
outcome_producer: message.outcome_producer.clone(), | ||
scoping: *scoping, | ||
} | ||
} | ||
|
||
fn emit_rate_limit_outcomes(&self, applied_limits: &RateLimits) { | ||
for applied_limit in applied_limits.iter() { | ||
if applied_limit.categories.is_empty() { | ||
// Empty categories value indicates that the rate limit applies to all data. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Per https://github.com/getsentry/relay/blob/master/relay-quotas/src/rate_limit.rs#L140-L141. So far I have only blind faith in that comment to support that this is correct/necessary behavior, but maybe the integration tests will clarify things as I continue digging. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I believe this approach is problematic since you cannot reconstruct the accurate drop reason from the To illustrate, consider the following example:
From the top of my head, I have two ideas to solve that:
|
||
if let Some(event_category) = self.envelope_summary.event_category { | ||
self.emit_outcome(&event_category, &applied_limit.reason_code); | ||
} | ||
if self.envelope_summary.attachment_quantity > 0 { | ||
self.emit_outcome(&DataCategory::Attachment, &applied_limit.reason_code); | ||
} | ||
} else { | ||
for category in applied_limit.categories.iter() { | ||
self.emit_outcome(category, &applied_limit.reason_code); | ||
} | ||
} | ||
} | ||
} | ||
|
||
fn emit_outcome(&self, category: &DataCategory, reason_code: &Option<ReasonCode>) { | ||
self.outcome_producer.do_send(TrackOutcome { | ||
timestamp: Instant::now(), | ||
scoping: self.scoping, | ||
outcome: Outcome::RateLimited(reason_code.clone()), | ||
event_id: self.event_id, | ||
remote_addr: self.remote_addr, | ||
category: *category, | ||
quantity: match category { | ||
DataCategory::Attachment => self.envelope_summary.attachment_quantity, | ||
_ => 1, | ||
}, | ||
}) | ||
} | ||
} | ||
|
||
/// Returns the project state if it is already cached. | ||
/// | ||
/// This is used for cases when we only want to perform operations that do | ||
|
@@ -660,26 +724,38 @@ impl Handler<GetProjectState> for Project { | |
/// - Validate origins and public keys | ||
/// - Quotas with a limit of `0` | ||
/// - Cached rate limits | ||
#[derive(Debug)] | ||
pub struct CheckEnvelope { | ||
envelope: Envelope, | ||
fetch: bool, | ||
outcome_producer: Addr<OutcomeProducer>, | ||
} | ||
|
||
impl std::fmt::Debug for CheckEnvelope { | ||
// TODO: Is this good enough? Can we avoid needing it? | ||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||
f.debug_struct("CheckEnvelope") | ||
.field("envelope", &self.envelope) | ||
.field("fetch", &self.fetch) | ||
.finish() | ||
} | ||
} | ||
|
||
impl CheckEnvelope { | ||
/// Fetches the project state and checks the envelope. | ||
pub fn fetched(envelope: Envelope) -> Self { | ||
pub fn fetched(envelope: Envelope, outcome_producer: &Addr<OutcomeProducer>) -> Self { | ||
Self { | ||
envelope, | ||
fetch: true, | ||
outcome_producer: outcome_producer.clone(), | ||
} | ||
} | ||
|
||
/// Uses a cached project state and checks the envelope. | ||
pub fn cached(envelope: Envelope) -> Self { | ||
pub fn cached(envelope: Envelope, outcome_producer: &Addr<OutcomeProducer>) -> Self { | ||
Self { | ||
envelope, | ||
fetch: false, | ||
outcome_producer: outcome_producer.clone(), | ||
} | ||
} | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You could move these two into
EnvelopeSummary
. It looks like this type is a utility that would fit well intoutils::rate_limits