Skip to content
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

Merged
merged 44 commits into from
Apr 9, 2021
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
328b762
feat(stats): Emit outcomes for applied rate limits
RyanSkonnord Mar 16, 2021
79ccc02
Attempt quick-and-dirty Debug for CheckEnvelope
RyanSkonnord Mar 16, 2021
307f931
Account for RateLimit with empty categories
RyanSkonnord Mar 16, 2021
3c556b6
Extract rate limit outcomes into struct
RyanSkonnord Mar 17, 2021
9b36fc0
Move OutcomeProducer from CheckEnvelope to ProjectCache
RyanSkonnord Mar 18, 2021
f76e797
Add new envelope summary update in common.rs
RyanSkonnord Mar 18, 2021
be73b2b
Add new envelope summary update in events.rs
RyanSkonnord Mar 23, 2021
531bf2b
Empty the envelope summary if no items are left after rate limiting
RyanSkonnord Mar 24, 2021
168b6e4
Some more places to mpty the envelope summary after rate limiting
RyanSkonnord Mar 26, 2021
4c87e85
Update test_outcome assertions
RyanSkonnord Mar 26, 2021
4acba6d
Merge branch 'master' into emit-rate-limit-outcomes
RyanSkonnord Mar 26, 2021
3de6c5b
Move attrs from RateLimitEnvelope to EnvelopeSummary
RyanSkonnord Mar 31, 2021
c43bd1a
Rename RateLimitEnvelope and move to rate_limits.rs
RyanSkonnord Mar 31, 2021
e3dc62f
Refactor with RateLimitEnforcement - wip
RyanSkonnord Mar 31, 2021
6dcf4de
Refactor with RateLimitEnforcement - wip 2
RyanSkonnord Mar 31, 2021
4e37a7a
Refactor with RateLimitEnforcement - make Envelope optional
RyanSkonnord Mar 31, 2021
697381b
Attempt to emit outcomes correctly
RyanSkonnord Mar 31, 2021
e76b6e5
Small clarification on name
RyanSkonnord Mar 31, 2021
44e892b
Merge branch 'master' into emit-rate-limit-outcomes
RyanSkonnord Mar 31, 2021
afc85a7
oops
RyanSkonnord Mar 31, 2021
9648aaa
Restructure to capture applied RateLimit from retain_item
RyanSkonnord Mar 31, 2021
96198aa
Take a pass at linking rate limits directly to emitted outcomes
RyanSkonnord Mar 31, 2021
cba3a0d
Style fix
RyanSkonnord Mar 31, 2021
33bee5b
Style fix
RyanSkonnord Mar 31, 2021
686fdd0
Move item retention from envelope.rs to rate_limits.rs; make things p…
RyanSkonnord Mar 31, 2021
ae00b96
Do category inference earlier to avoid Item clone
RyanSkonnord Mar 31, 2021
d4af165
Refactor RateLimitEnforcement struct into a method
RyanSkonnord Mar 31, 2021
c8394e6
Revert "Refactor RateLimitEnforcement struct into a method"
RyanSkonnord Apr 1, 2021
d971c52
No longer need an Envelope::empty_copy method after refactoring
RyanSkonnord Apr 1, 2021
bd6071f
ref: Replace `get_active_limit` with `RateLimits::longest`
jan-auer Apr 1, 2021
d1d2ff3
ref: Reorder project cache dependencies
jan-auer Apr 1, 2021
f045d7b
fix: Do not double-track rate limited outcomes
jan-auer Apr 1, 2021
4719355
ref: Avoid redundant envelope_summary updates.
jan-auer Apr 1, 2021
c226d61
ref: Move outcome meta closer to enforcement
jan-auer Apr 1, 2021
6845720
fix: Lints and test compilation
jan-auer Apr 2, 2021
a126f91
doc: Add a description to EnvelopeLimiter::enforce
jan-auer Apr 2, 2021
760dac5
Merge branch 'master' into emit-rate-limit-outcomes
jan-auer Apr 2, 2021
8faf98c
meta: Changelog
jan-auer Apr 2, 2021
1a042b1
doc: Update doc comment
jan-auer Apr 2, 2021
22f9328
ref: More docs
jan-auer Apr 2, 2021
504a8bd
ref: Apply review suggestions
jan-auer Apr 6, 2021
56cd51f
fix: Do not emit outcomes for sessions
jan-auer Apr 9, 2021
ef105a5
Merge branch 'master' into emit-rate-limit-outcomes
jan-auer Apr 9, 2021
2f65537
meta: Changelog
jan-auer Apr 9, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion relay-server/src/actors/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1533,7 +1533,7 @@ impl Handler<HandleEnvelope> for EventManager {
let envelope_summary = Rc::new(RefCell::new(EnvelopeSummary::compute(&envelope)));

let future = project
.send(CheckEnvelope::fetched(envelope))
.send(CheckEnvelope::fetched(envelope, &outcome_producer))
.map_err(ProcessingError::ScheduleFailed)
.and_then(|result| result.map_err(ProcessingError::ProjectFailed))
.and_then(clone!(scoping, envelope_summary, |response| {
Expand Down
98 changes: 87 additions & 11 deletions relay-server/src/actors/project.rs
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};
Expand All @@ -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)]
Expand Down Expand Up @@ -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)?;
}
Expand All @@ -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)?;
Expand All @@ -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 }
}
}
Expand All @@ -595,6 +605,60 @@ impl Actor for Project {
}
}

struct RateLimitEnvelope {
event_id: Option<EventId>,
remote_addr: Option<IpAddr>,
Copy link
Member

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 into utils::rate_limits

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.
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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.

Copy link
Member

@jan-auer jan-auer Mar 30, 2021

Choose a reason for hiding this comment

The 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 RateLimits structure, unfortunately. This is an inherent flaw of RateLimits and we can consider changing that, too.

To illustrate, consider the following example:

  • There is a single quota category:error limit:0. It drops all error events.
  • EnvelopeLimiter will also drop all attachments in the same envelope as a result of that.
  • When you check RateLimits here, you won't find the attachment category, even though you just dropped them.

From the top of my head, I have two ideas to solve that:

  1. Move emitting outcomes into EnvelopeLimiter, because that's where you can keep track of the dropped quantities.
  2. From EnvelopeLimiter::enforce, return an instance of EnvelopeSummary that contains the dropped quantities. Then, use that summary (+ scoping) to emit outcomes. This approach is preferable as it decouples concerns.

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
Expand Down Expand Up @@ -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(),
}
}
}
Expand Down
Loading