Skip to main content

relay_server/services/
processor.rs

1use std::borrow::Cow;
2use std::collections::{BTreeMap, BTreeSet, HashMap};
3use std::error::Error;
4use std::fmt::Debug;
5use std::future::Future;
6use std::io::Write;
7use std::pin::Pin;
8use std::sync::Arc;
9use std::time::Duration;
10
11use anyhow::Context;
12use brotli::CompressorWriter as BrotliEncoder;
13use bytes::Bytes;
14use chrono::{DateTime, Utc};
15use flate2::Compression;
16use flate2::write::{GzEncoder, ZlibEncoder};
17use futures::future::BoxFuture;
18use relay_base_schema::project::{ProjectId, ProjectKey};
19use relay_cogs::{AppFeature, Cogs, FeatureWeights, ResourceId, Token};
20use relay_common::time::UnixTimestamp;
21use relay_config::{Config, ConfigSnapshot, EmitOutcomes, HttpEncoding, UpstreamDescriptor};
22use relay_event_normalization::{ClockDriftProcessor, GeoIpLookup};
23use relay_event_schema::processor::ProcessingAction;
24use relay_event_schema::protocol::ClientReport;
25use relay_filter::FilterStatKey;
26use relay_log::sentry::SentryFutureExt;
27use relay_metrics::{Bucket, BucketMetadata, BucketView, BucketsView, MetricNamespace};
28use relay_quotas::{RateLimits, Scoping};
29use relay_sampling::evaluation::SamplingDecision;
30use relay_statsd::metric;
31use relay_system::{Addr, FromMessage, NoResponse, Service};
32use reqwest::header;
33use zstd::stream::Encoder as ZstdEncoder;
34
35use crate::envelope::{self, ContentType, Envelope, EnvelopeError, Item, ItemType};
36use crate::extractors::{PartialDsn, RequestMeta, RequestTrust};
37use crate::managed::ManagedEnvelope;
38use crate::metrics::{MetricOutcomes, MetricsLimiter, MinimalTrackableBucket};
39use crate::metrics_extraction::ExtractedMetrics;
40use crate::processing::errors::SwitchProcessingError;
41use crate::processing::relay::RelayProcessor;
42use crate::processing::{Forward as _, Output, Outputs, QuotaRateLimiter};
43use crate::service::ServiceError;
44use crate::services::global_config::GlobalConfigHandle;
45use crate::services::metrics::{Aggregator, FlushBuckets, MergeBuckets, ProjectBuckets};
46use crate::services::outcome::{self, DiscardItemType, DiscardReason, Outcome, TrackOutcome};
47use crate::services::projects::cache::ProjectCacheHandle;
48use crate::services::projects::project::{ProjectInfo, ProjectState};
49use crate::services::upstream::{
50    SendRequest, Sign, SignatureType, UpstreamRelay, UpstreamRequest, UpstreamRequestError,
51};
52use crate::statsd::{RelayCounters, RelayDistributions, RelayTimers};
53use crate::utils;
54use crate::{http, processing};
55use relay_threading::AsyncPool;
56#[cfg(feature = "processing")]
57use {
58    crate::services::objectstore::Objectstore,
59    crate::services::store::Store,
60    itertools::Itertools,
61    relay_dynamic_config::GlobalConfig,
62    relay_quotas::{Quota, RateLimitingError, RedisRateLimiter},
63    relay_redis::RedisClients,
64    std::time::Instant,
65    symbolic_unreal::{Unreal4Error, Unreal4ErrorKind},
66};
67
68mod metrics;
69
70/// The minimum clock drift for correction to apply.
71pub const MINIMUM_CLOCK_DRIFT: Duration = Duration::from_secs(55 * 60);
72
73/// An error returned when handling [`ProcessEnvelope`].
74#[derive(Debug, thiserror::Error)]
75pub enum ProcessingError {
76    #[error("invalid json in event")]
77    InvalidJson(#[source] serde_json::Error),
78
79    #[error("invalid message pack event payload")]
80    InvalidMsgpack(#[from] rmp_serde::decode::Error),
81
82    #[error("event data too deeply nested")]
83    NestingTooDeep,
84
85    #[cfg(feature = "processing")]
86    #[error("invalid unreal crash report")]
87    InvalidUnrealReport(#[source] Unreal4Error),
88
89    #[error("event payload too large")]
90    PayloadTooLarge(DiscardItemType),
91
92    #[error("invalid transaction event")]
93    InvalidTransaction,
94
95    #[error("the item is not allowed/supported in this envelope")]
96    UnsupportedItem,
97
98    #[error("envelope processor failed")]
99    ProcessingFailed(#[from] ProcessingAction),
100
101    #[error("duplicate {0} in event")]
102    DuplicateItem(ItemType),
103
104    #[error("failed to extract event payload")]
105    NoEventPayload,
106
107    #[error("invalid security report type: {0:?}")]
108    InvalidSecurityType(Bytes),
109
110    #[error("unsupported security report type")]
111    UnsupportedSecurityType,
112
113    #[error("invalid security report")]
114    InvalidSecurityReport(#[source] serde_json::Error),
115
116    #[error("event filtered with reason: {0:?}")]
117    EventFiltered(FilterStatKey),
118
119    #[error("could not serialize event payload")]
120    SerializeFailed(#[source] serde_json::Error),
121
122    #[cfg(feature = "processing")]
123    #[error("failed to apply quotas")]
124    QuotasFailed(#[from] RateLimitingError),
125
126    #[error("nintendo switch dying message processing failed {0:?}")]
127    InvalidNintendoDyingMessage(#[source] SwitchProcessingError),
128
129    #[cfg(all(sentry, feature = "processing"))]
130    #[error("playstation dump processing failed: {0}")]
131    InvalidPlaystationDump(String),
132
133    #[cfg(feature = "processing")]
134    #[error("invalid attachment reference")]
135    InvalidAttachmentRef,
136}
137
138impl ProcessingError {
139    pub fn to_outcome(&self) -> Option<Outcome> {
140        match self {
141            Self::PayloadTooLarge(payload_type) => {
142                Some(Outcome::Invalid(DiscardReason::ItemTooLarge(*payload_type)))
143            }
144            Self::InvalidJson(_) => Some(Outcome::Invalid(DiscardReason::InvalidJson)),
145            Self::InvalidMsgpack(_) => Some(Outcome::Invalid(DiscardReason::InvalidMsgpack)),
146            Self::NestingTooDeep => Some(Outcome::Invalid(DiscardReason::NestingTooDeep)),
147            Self::InvalidSecurityType(_) => {
148                Some(Outcome::Invalid(DiscardReason::SecurityReportType))
149            }
150            Self::UnsupportedItem => Some(Outcome::Invalid(DiscardReason::InvalidEnvelope)),
151            Self::InvalidSecurityReport(_) => Some(Outcome::Invalid(DiscardReason::SecurityReport)),
152            Self::UnsupportedSecurityType => Some(Outcome::Filtered(FilterStatKey::InvalidCsp)),
153            Self::InvalidTransaction => Some(Outcome::Invalid(DiscardReason::InvalidTransaction)),
154            Self::DuplicateItem(_) => Some(Outcome::Invalid(DiscardReason::DuplicateItem)),
155            Self::NoEventPayload => Some(Outcome::Invalid(DiscardReason::NoEventPayload)),
156            Self::InvalidNintendoDyingMessage(_) => Some(Outcome::Invalid(DiscardReason::Payload)),
157            #[cfg(all(sentry, feature = "processing"))]
158            Self::InvalidPlaystationDump(_) => Some(Outcome::Invalid(DiscardReason::Payload)),
159            #[cfg(feature = "processing")]
160            Self::InvalidUnrealReport(err) if err.kind() == Unreal4ErrorKind::BadCompression => {
161                Some(Outcome::Invalid(DiscardReason::InvalidCompression))
162            }
163            #[cfg(feature = "processing")]
164            Self::InvalidUnrealReport(_) => Some(Outcome::Invalid(DiscardReason::ProcessUnreal)),
165            Self::SerializeFailed(_) | Self::ProcessingFailed(_) => {
166                Some(Outcome::Invalid(DiscardReason::Internal))
167            }
168            #[cfg(feature = "processing")]
169            Self::QuotasFailed(_) => Some(Outcome::Invalid(DiscardReason::Internal)),
170            Self::EventFiltered(key) => Some(Outcome::Filtered(key.clone())),
171
172            #[cfg(feature = "processing")]
173            Self::InvalidAttachmentRef => {
174                Some(Outcome::Invalid(DiscardReason::InvalidAttachmentRef))
175            }
176        }
177    }
178}
179
180#[cfg(feature = "processing")]
181impl From<Unreal4Error> for ProcessingError {
182    fn from(err: Unreal4Error) -> Self {
183        match err.kind() {
184            Unreal4ErrorKind::TooLarge => Self::PayloadTooLarge(ItemType::UnrealReport.into()),
185            _ => ProcessingError::InvalidUnrealReport(err),
186        }
187    }
188}
189
190/// A container for extracted metrics during processing.
191///
192/// The container enforces that the extracted metrics are correctly tagged
193/// with the dynamic sampling decision.
194#[derive(Debug)]
195pub struct ProcessingExtractedMetrics {
196    metrics: ExtractedMetrics,
197}
198
199impl ProcessingExtractedMetrics {
200    pub fn new() -> Self {
201        Self {
202            metrics: ExtractedMetrics::default(),
203        }
204    }
205
206    pub fn into_inner(self) -> ExtractedMetrics {
207        self.metrics
208    }
209
210    /// Extends the contained metrics with [`ExtractedMetrics`].
211    pub fn extend(
212        &mut self,
213        extracted: ExtractedMetrics,
214        sampling_decision: Option<SamplingDecision>,
215    ) {
216        self.extend_project_metrics(extracted.project_metrics, sampling_decision);
217        self.extend_sampling_metrics(extracted.sampling_metrics, sampling_decision);
218    }
219
220    /// Extends the contained project metrics.
221    pub fn extend_project_metrics<I>(
222        &mut self,
223        buckets: I,
224        sampling_decision: Option<SamplingDecision>,
225    ) where
226        I: IntoIterator<Item = Bucket>,
227    {
228        self.metrics
229            .project_metrics
230            .extend(buckets.into_iter().map(|mut bucket| {
231                bucket.metadata.extracted_from_indexed =
232                    sampling_decision == Some(SamplingDecision::Keep);
233                bucket
234            }));
235    }
236
237    /// Extends the contained sampling metrics.
238    pub fn extend_sampling_metrics<I>(
239        &mut self,
240        buckets: I,
241        sampling_decision: Option<SamplingDecision>,
242    ) where
243        I: IntoIterator<Item = Bucket>,
244    {
245        self.metrics
246            .sampling_metrics
247            .extend(buckets.into_iter().map(|mut bucket| {
248                bucket.metadata.extracted_from_indexed =
249                    sampling_decision == Some(SamplingDecision::Keep);
250                bucket
251            }));
252    }
253}
254
255fn send_metrics(
256    metrics: ExtractedMetrics,
257    project_key: ProjectKey,
258    sampling_key: Option<ProjectKey>,
259    aggregator: &Addr<Aggregator>,
260) {
261    let ExtractedMetrics {
262        project_metrics,
263        sampling_metrics,
264    } = metrics;
265
266    if !project_metrics.is_empty() {
267        aggregator.send(MergeBuckets {
268            project_key,
269            buckets: project_metrics,
270        });
271    }
272
273    if !sampling_metrics.is_empty() {
274        // If no sampling project state is available, we associate the sampling
275        // metrics with the current project.
276        //
277        // project_without_tracing         -> metrics goes to self
278        // dependent_project_with_tracing  -> metrics goes to root
279        // root_project_with_tracing       -> metrics goes to root == self
280        let sampling_project_key = sampling_key.unwrap_or(project_key);
281        aggregator.send(MergeBuckets {
282            project_key: sampling_project_key,
283            buckets: sampling_metrics,
284        });
285    }
286}
287
288/// Applies processing to all contents of the given envelope.
289///
290/// Depending on the contents of the envelope and Relay's mode, this includes:
291///
292///  - Basic normalization and validation for all item types.
293///  - Clock drift correction if the required `sent_at` header is present.
294///  - Expansion of certain item types (e.g. unreal).
295///  - Store normalization for event payloads in processing mode.
296///  - Rate limiters and inbound filters on events in processing mode.
297#[derive(Debug)]
298pub struct ProcessEnvelope {
299    /// Envelope to process.
300    pub envelope: ManagedEnvelope,
301    /// The project info.
302    pub project_info: Arc<ProjectInfo>,
303    /// Currently active cached rate limits for this project.
304    pub rate_limits: Arc<RateLimits>,
305    /// Root sampling project info.
306    pub sampling_project_info: Option<Arc<ProjectInfo>>,
307}
308
309/// Parses a list of metrics or metric buckets and pushes them to the project's aggregator.
310///
311/// This parses and validates the metrics:
312///  - For [`Metrics`](ItemType::Statsd), each metric is parsed separately, and invalid metrics are
313///    ignored independently.
314///  - For [`MetricBuckets`](ItemType::MetricBuckets), the entire list of buckets is parsed and
315///    dropped together on parsing failure.
316///  - Other envelope items will be ignored with an error message.
317///
318/// Additionally, processing applies clock drift correction using the system clock of this Relay, if
319/// the Envelope specifies the [`sent_at`](Envelope::sent_at) header.
320#[derive(Debug)]
321pub struct ProcessMetrics {
322    /// A list of metric items.
323    pub data: MetricData,
324    /// The target project.
325    pub project_key: ProjectKey,
326    /// Whether to keep or reset the metric metadata.
327    pub source: BucketSource,
328    /// The wall clock time at which the request was received.
329    pub received_at: DateTime<Utc>,
330    /// The value of the Envelope's [`sent_at`](Envelope::sent_at) header for clock drift
331    /// correction.
332    pub sent_at: Option<DateTime<Utc>>,
333}
334
335/// Raw unparsed metric data.
336#[derive(Debug)]
337pub enum MetricData {
338    /// Raw data, unparsed envelope items.
339    Raw(Vec<Item>),
340    /// Already parsed buckets but unprocessed.
341    Parsed(Vec<Bucket>),
342}
343
344impl MetricData {
345    /// Consumes the metric data and parses the contained buckets.
346    ///
347    /// If the contained data is already parsed the buckets are returned unchanged.
348    /// Raw buckets are parsed and created with the passed `timestamp`.
349    fn into_buckets(self, timestamp: UnixTimestamp) -> Vec<Bucket> {
350        let items = match self {
351            Self::Parsed(buckets) => return buckets,
352            Self::Raw(items) => items,
353        };
354
355        let mut buckets = Vec::new();
356        for item in items {
357            let payload = item.payload();
358            if item.ty() == &ItemType::Statsd {
359                for bucket_result in Bucket::parse_all(&payload, timestamp) {
360                    match bucket_result {
361                        Ok(bucket) => buckets.push(bucket),
362                        Err(error) => relay_log::debug!(
363                            error = &error as &dyn Error,
364                            "failed to parse metric bucket from statsd format",
365                        ),
366                    }
367                }
368            } else if item.ty() == &ItemType::MetricBuckets {
369                match serde_json::from_slice::<Vec<Bucket>>(&payload) {
370                    Ok(parsed_buckets) => {
371                        // Re-use the allocation of `b` if possible.
372                        if buckets.is_empty() {
373                            buckets = parsed_buckets;
374                        } else {
375                            buckets.extend(parsed_buckets);
376                        }
377                    }
378                    Err(error) => {
379                        relay_log::debug!(
380                            error = &error as &dyn Error,
381                            "failed to parse metric bucket",
382                        );
383                        metric!(counter(RelayCounters::MetricBucketsParsingFailed) += 1);
384                    }
385                }
386            } else {
387                relay_log::error!(
388                    "invalid item of type {} passed to ProcessMetrics",
389                    item.ty()
390                );
391            }
392        }
393        buckets
394    }
395}
396
397#[derive(Debug)]
398pub struct ProcessBatchedMetrics {
399    /// Metrics payload in JSON format.
400    pub payload: Bytes,
401    /// Whether to keep or reset the metric metadata.
402    pub source: BucketSource,
403    /// The wall clock time at which the request was received.
404    pub received_at: DateTime<Utc>,
405    /// The wall clock time at which the request was received.
406    pub sent_at: Option<DateTime<Utc>>,
407}
408
409/// Source information where a metric bucket originates from.
410#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
411pub enum BucketSource {
412    /// The metric bucket originated from an internal Relay use case.
413    ///
414    /// The metric bucket originates either from within the same Relay
415    /// or was accepted coming from another Relay which is registered as
416    /// an internal Relay via Relay's configuration.
417    Internal,
418    /// The bucket source originated from an untrusted source.
419    ///
420    /// Managed Relays sending extracted metrics are considered external,
421    /// it's a project use case but it comes from an untrusted source.
422    External,
423}
424
425impl BucketSource {
426    /// Infers the bucket source from [`RequestMeta::request_trust`].
427    pub fn from_meta(meta: &RequestMeta) -> Self {
428        match meta.request_trust() {
429            RequestTrust::Trusted => Self::Internal,
430            RequestTrust::Untrusted => Self::External,
431        }
432    }
433}
434
435/// Sends a client report to the upstream.
436#[derive(Debug)]
437pub struct SubmitClientReports {
438    /// The client report to be sent.
439    pub client_reports: Vec<ClientReport>,
440    /// Scoping information for the client report.
441    pub scoping: Scoping,
442}
443
444/// CPU-intensive processing tasks for envelopes.
445#[derive(Debug)]
446pub enum EnvelopeProcessor {
447    ProcessEnvelope(Box<ProcessEnvelope>),
448    ProcessProjectMetrics(Box<ProcessMetrics>),
449    ProcessBatchedMetrics(Box<ProcessBatchedMetrics>),
450    FlushBuckets(Box<FlushBuckets>),
451    SubmitClientReports(Box<SubmitClientReports>),
452}
453
454impl EnvelopeProcessor {
455    /// Returns the name of the message variant.
456    pub fn variant(&self) -> &'static str {
457        match self {
458            EnvelopeProcessor::ProcessEnvelope(_) => "ProcessEnvelope",
459            EnvelopeProcessor::ProcessProjectMetrics(_) => "ProcessProjectMetrics",
460            EnvelopeProcessor::ProcessBatchedMetrics(_) => "ProcessBatchedMetrics",
461            EnvelopeProcessor::FlushBuckets(_) => "FlushBuckets",
462            EnvelopeProcessor::SubmitClientReports(_) => "SubmitClientReports",
463        }
464    }
465}
466
467impl relay_system::Interface for EnvelopeProcessor {}
468
469impl FromMessage<ProcessEnvelope> for EnvelopeProcessor {
470    type Response = relay_system::NoResponse;
471
472    fn from_message(message: ProcessEnvelope, _sender: ()) -> Self {
473        Self::ProcessEnvelope(Box::new(message))
474    }
475}
476
477impl FromMessage<ProcessMetrics> for EnvelopeProcessor {
478    type Response = NoResponse;
479
480    fn from_message(message: ProcessMetrics, _: ()) -> Self {
481        Self::ProcessProjectMetrics(Box::new(message))
482    }
483}
484
485impl FromMessage<ProcessBatchedMetrics> for EnvelopeProcessor {
486    type Response = NoResponse;
487
488    fn from_message(message: ProcessBatchedMetrics, _: ()) -> Self {
489        Self::ProcessBatchedMetrics(Box::new(message))
490    }
491}
492
493impl FromMessage<FlushBuckets> for EnvelopeProcessor {
494    type Response = NoResponse;
495
496    fn from_message(message: FlushBuckets, _: ()) -> Self {
497        Self::FlushBuckets(Box::new(message))
498    }
499}
500
501impl FromMessage<SubmitClientReports> for EnvelopeProcessor {
502    type Response = NoResponse;
503
504    fn from_message(message: SubmitClientReports, _: ()) -> Self {
505        Self::SubmitClientReports(Box::new(message))
506    }
507}
508
509/// The asynchronous thread pool used for scheduling processing tasks in the processor.
510pub type EnvelopeProcessorServicePool = AsyncPool<BoxFuture<'static, ()>>;
511
512/// Service implementing the [`EnvelopeProcessor`] interface.
513///
514/// This service handles messages in a worker pool with configurable concurrency.
515#[derive(Clone)]
516pub struct EnvelopeProcessorService {
517    inner: Arc<InnerProcessor>,
518}
519
520/// Contains the addresses of services that the processor publishes to.
521pub struct Addrs {
522    pub outcome_aggregator: Addr<TrackOutcome>,
523    pub upstream_relay: Addr<UpstreamRelay>,
524    #[cfg(feature = "processing")]
525    pub objectstore: Option<Addr<Objectstore>>,
526    #[cfg(feature = "processing")]
527    pub store_forwarder: Option<Addr<Store>>,
528    pub aggregator: Addr<Aggregator>,
529}
530
531impl Default for Addrs {
532    fn default() -> Self {
533        Addrs {
534            outcome_aggregator: Addr::dummy(),
535            upstream_relay: Addr::dummy(),
536            #[cfg(feature = "processing")]
537            objectstore: None,
538            #[cfg(feature = "processing")]
539            store_forwarder: None,
540            aggregator: Addr::dummy(),
541        }
542    }
543}
544
545struct InnerProcessor {
546    pool: EnvelopeProcessorServicePool,
547    config: Arc<Config>,
548    global_config: GlobalConfigHandle,
549    project_cache: ProjectCacheHandle,
550    cogs: Cogs,
551    addrs: Addrs,
552    #[cfg(feature = "processing")]
553    rate_limiter: Option<Arc<RedisRateLimiter>>,
554    metric_outcomes: MetricOutcomes,
555    processor: RelayProcessor,
556}
557
558impl EnvelopeProcessorService {
559    /// Creates a multi-threaded envelope processor.
560    #[cfg_attr(feature = "processing", expect(clippy::too_many_arguments))]
561    pub fn new(
562        pool: EnvelopeProcessorServicePool,
563        config: Arc<Config>,
564        global_config: GlobalConfigHandle,
565        project_cache: ProjectCacheHandle,
566        cogs: Cogs,
567        #[cfg(feature = "processing")] redis: Option<RedisClients>,
568        addrs: Addrs,
569        metric_outcomes: MetricOutcomes,
570    ) -> Self {
571        let c = config.current();
572
573        let geoip_lookup = c
574            .geoip_path()
575            .and_then(
576                |p| match GeoIpLookup::open(p).context(ServiceError::GeoIp) {
577                    Ok(geoip) => Some(geoip),
578                    Err(err) => {
579                        relay_log::error!("failed to open GeoIP db {p:?}: {err:?}");
580                        None
581                    }
582                },
583            )
584            .unwrap_or_else(GeoIpLookup::empty);
585
586        if let Some(build_epoch) = geoip_lookup.build_epoch() {
587            relay_log::info!("Loaded GeoIP database (build: {build_epoch})");
588        }
589
590        #[cfg(feature = "processing")]
591        let rate_limiter = redis.map(|redis| {
592            RedisRateLimiter::new(redis.quotas)
593                .max_limit(c.max_rate_limit())
594                .cache(c.quota_cache_ratio(), c.quota_cache_max())
595        });
596
597        let quota_limiter = Arc::new(QuotaRateLimiter::new(
598            #[cfg(feature = "processing")]
599            project_cache.clone(),
600            #[cfg(feature = "processing")]
601            rate_limiter.clone(),
602        ));
603        #[cfg(feature = "processing")]
604        let rate_limiter = rate_limiter.map(Arc::new);
605        let inner = InnerProcessor {
606            pool,
607            global_config,
608            project_cache,
609            #[cfg(feature = "processing")]
610            rate_limiter,
611            processor: RelayProcessor::new(
612                cogs.clone(),
613                &quota_limiter,
614                &geoip_lookup,
615                addrs.outcome_aggregator.clone(),
616            ),
617            cogs,
618            addrs,
619            metric_outcomes,
620            config,
621        };
622
623        Self {
624            inner: Arc::new(inner),
625        }
626    }
627
628    async fn process_envelope(
629        &self,
630        project_id: ProjectId,
631        mut envelope: ManagedEnvelope,
632        ctx: processing::Context<'_>,
633    ) -> Vec<Output<Outputs>> {
634        // Pre-process the envelope headers.
635        if let Some(sampling_state) = ctx.sampling_project_info {
636            // Both transactions and standalone span envelopes need a normalized DSC header
637            // to make sampling rules based on the segment/transaction name work correctly.
638            envelope
639                .envelope_mut()
640                .parametrize_dsc_transaction(&sampling_state.config.tx_name_rules);
641        }
642
643        // Ensure the project ID is updated to the stored instance for this project cache. This can
644        // differ in two cases:
645        //  1. The envelope was sent to the legacy `/store/` endpoint without a project ID.
646        //  2. The DSN was moved and the envelope sent to the old project ID.
647        envelope
648            .envelope_mut()
649            .meta_mut()
650            .set_project_id(project_id);
651
652        self.inner.processor.run(envelope, ctx).await
653    }
654
655    /// Processes the envelope and returns the processed envelope back.
656    ///
657    /// Returns `Some` if the envelope passed inbound filtering and rate limiting. Invalid items are
658    /// removed from the envelope. Otherwise, if the envelope is empty or the entire envelope needs
659    /// to be dropped, this is `None`.
660    async fn process<'a>(
661        &self,
662        mut envelope: ManagedEnvelope,
663        ctx: processing::Context<'a>,
664    ) -> Vec<Output<Outputs>> {
665        // Prefer the project's project ID, and fall back to the stated project id from the
666        // envelope. The project ID is available in all modes, other than in proxy mode, where
667        // envelopes for unknown projects are forwarded blindly.
668        //
669        // Neither ID can be available in proxy mode on the /store/ endpoint. This is not supported,
670        // since we cannot process an envelope without project ID, so drop it.
671        let Some(project_id) = ctx
672            .project_info
673            .project_id
674            .or_else(|| envelope.envelope().meta().project_id())
675        else {
676            relay_log::error!(
677                tags.project_key = %envelope.envelope().meta().public_key(),
678                "project info does not contain project id"
679            );
680            envelope.reject(Outcome::Invalid(DiscardReason::Internal));
681            return Vec::new();
682        };
683
684        relay_log::configure_scope(|scope| {
685            scope.set_tag("project_id", project_id);
686        });
687
688        self.process_envelope(project_id, envelope, ctx).await
689    }
690
691    async fn handle_process_envelope(&self, cogs: &mut Token, message: ProcessEnvelope) {
692        let wait_time = message.envelope.age();
693        metric!(timer(RelayTimers::EnvelopeWaitTime) = wait_time);
694
695        // This COGS handling may need an overhaul in the future:
696        // Cancel the passed in token, to start individual measurements per processor instead.
697        cogs.cancel();
698
699        let global_config = self.inner.global_config.current().unwrap_or_default();
700        let config = self.inner.config.current();
701
702        let ctx = processing::Context {
703            config: &config,
704            global_config: &global_config,
705            project_info: &message.project_info,
706            sampling_project_info: message.sampling_project_info.as_deref(),
707            rate_limits: &message.rate_limits,
708        };
709
710        let project_key = message.envelope.meta().public_key();
711        // Only allow sending to the sampling key, if we successfully loaded a sampling project
712        // info relating to it. This filters out unknown/invalid project keys as well as project
713        // keys from different organizations.
714        let sampling_key = ctx
715            .sampling_project_info
716            .and_then(|p| p.get_public_key_config())
717            .map(|pkc| pkc.public_key);
718
719        relay_log::configure_scope(|scope| {
720            scope.set_tag("project_key", project_key);
721            if let Some(sampling_key) = sampling_key {
722                scope.set_tag("sampling_key", sampling_key);
723            }
724            let meta = message.envelope.envelope().meta();
725            scope.set_tag("sdk_name", meta.client_name());
726            if let Some(client) = meta.client() {
727                scope.set_tag("sdk", client);
728            }
729            if let Some(user_agent) = meta.user_agent() {
730                scope.set_extra("user_agent", user_agent.into());
731            }
732        });
733
734        let outputs = metric!(timer(RelayTimers::EnvelopeProcessingTime), {
735            self.process(message.envelope, ctx).await
736        });
737
738        let ctx = ctx.to_forward();
739        for Output { main, metrics } in outputs {
740            if let Some(metrics) = metrics {
741                let agg = &self.inner.addrs.aggregator;
742                metrics.accept(|metrics| {
743                    send_metrics(metrics, project_key, sampling_key, agg);
744                });
745            }
746
747            if let Some(output) = main {
748                // Only counting processing time for COGS at the moment.
749                self.submit_upstream(&mut Token::noop(), output, ctx);
750            }
751        }
752    }
753
754    fn handle_process_metrics(&self, cogs: &mut Token, message: ProcessMetrics) {
755        let ProcessMetrics {
756            data,
757            project_key,
758            received_at,
759            sent_at,
760            source,
761        } = message;
762
763        let received_timestamp =
764            UnixTimestamp::from_datetime(received_at).unwrap_or(UnixTimestamp::now());
765
766        let mut buckets = data.into_buckets(received_timestamp);
767        if buckets.is_empty() {
768            return;
769        };
770        cogs.update(relay_metrics::cogs::BySize(&buckets));
771
772        let clock_drift_processor =
773            ClockDriftProcessor::new(sent_at, received_at).at_least(MINIMUM_CLOCK_DRIFT);
774
775        buckets.retain_mut(|bucket| {
776            if let Err(error) = relay_metrics::normalize_bucket(bucket) {
777                relay_log::debug!(error = &error as &dyn Error, "dropping bucket {bucket:?}");
778                return false;
779            }
780
781            if !self::metrics::is_valid_namespace(bucket, source) {
782                relay_log::debug!("dropping bucket in invalid namespace {bucket:?}");
783                return false;
784            }
785
786            clock_drift_processor.process_timestamp(&mut bucket.timestamp);
787
788            if !matches!(source, BucketSource::Internal) {
789                bucket.metadata = BucketMetadata::new(received_timestamp);
790            }
791
792            true
793        });
794
795        let project = self.inner.project_cache.get(project_key);
796
797        // Best effort check to filter and rate limit buckets, if there is no project state
798        // available at the current time, we will check again after flushing.
799        let buckets = match project.state() {
800            ProjectState::Enabled(project_info) => {
801                let rate_limits = project.rate_limits().current_limits();
802                self.check_buckets(project_key, project_info, &rate_limits, buckets)
803            }
804            _ => buckets,
805        };
806
807        relay_log::trace!("merging metric buckets into the aggregator");
808        self.inner
809            .addrs
810            .aggregator
811            .send(MergeBuckets::new(project_key, buckets));
812    }
813
814    fn handle_process_batched_metrics(&self, cogs: &mut Token, message: ProcessBatchedMetrics) {
815        let ProcessBatchedMetrics {
816            payload,
817            source,
818            received_at,
819            sent_at,
820        } = message;
821
822        #[derive(serde::Deserialize)]
823        struct Wrapper {
824            buckets: HashMap<ProjectKey, Vec<Bucket>>,
825        }
826
827        let buckets = match serde_json::from_slice(&payload) {
828            Ok(Wrapper { buckets }) => buckets,
829            Err(error) => {
830                relay_log::debug!(
831                    error = &error as &dyn Error,
832                    "failed to parse batched metrics",
833                );
834                metric!(counter(RelayCounters::MetricBucketsParsingFailed) += 1);
835                return;
836            }
837        };
838
839        for (project_key, buckets) in buckets {
840            self.handle_process_metrics(
841                cogs,
842                ProcessMetrics {
843                    data: MetricData::Parsed(buckets),
844                    project_key,
845                    source,
846                    received_at,
847                    sent_at,
848                },
849            )
850        }
851    }
852
853    /// Submits a processor [`Output`] to the appropriate upstream.
854    ///
855    /// If processing is enabled, the upstream is Kafka.
856    fn submit_upstream(
857        &self,
858        cogs: &mut Token,
859        output: Outputs,
860        ctx: processing::ForwardContext<'_>,
861    ) {
862        let _submit = cogs.start_category("submit");
863
864        #[cfg(feature = "processing")]
865        if ctx.config.processing_enabled()
866            && let Some(store_forwarder) = &self.inner.addrs.store_forwarder
867        {
868            use crate::processing::StoreHandle;
869
870            let objectstore = self.inner.addrs.objectstore.as_ref();
871            let handle = StoreHandle::new(store_forwarder, objectstore, ctx.global_config);
872
873            output
874                .forward_store(handle, ctx)
875                .unwrap_or_else(|err| err.into_inner());
876
877            return;
878        }
879
880        match output.serialize_envelope(ctx) {
881            Ok(envelope) => {
882                let envelope = ManagedEnvelope::from(envelope);
883                self.submit_envelope_upstream(
884                    envelope,
885                    ctx.config,
886                    ctx.project_info.upstream.clone(),
887                );
888            }
889            Err(_) => relay_log::error!("failed to serialize output to an envelope"),
890        };
891    }
892
893    fn submit_envelope_upstream(
894        &self,
895        mut envelope: ManagedEnvelope,
896        config: &ConfigSnapshot,
897        // Currently allowed to be optional as code is migrated to respect the upstream override
898        // provided from the project config. Eventually must be available and is required.
899        upstream: Option<UpstreamDescriptor>,
900    ) {
901        if envelope.envelope_mut().is_empty() {
902            envelope.accept();
903            return;
904        }
905
906        // No code path should hit this.
907        //
908        // Any item which is produced by processing is handled in `submit_upstream`,
909        // metrics are sent to the store directly and outcomes must be produced to Kafka
910        // instead of being sent onward as client report.
911        if config.processing_enabled() {
912            relay_log::error!(
913                "attempt to forward envelope to http upstream when processing is enabled"
914            );
915            return;
916        }
917
918        // Override the `sent_at` timestamp. Since the envelope went through basic
919        // normalization, all timestamps have been corrected. We propagate the new
920        // `sent_at` to allow the next Relay to double-check this timestamp and
921        // potentially apply correction again. This is done as close to sending as
922        // possible so that we avoid internal delays.
923        envelope.envelope_mut().set_sent_at(Utc::now());
924
925        relay_log::trace!("sending envelope to sentry endpoint");
926        let http_encoding = config.http_encoding();
927        let result = envelope.envelope().to_vec().and_then(|v| {
928            encode_payload(&v.into(), http_encoding).map_err(EnvelopeError::PayloadIoFailed)
929        });
930
931        match result {
932            Ok(body) => {
933                self.inner
934                    .addrs
935                    .upstream_relay
936                    .send(SendRequest(SendEnvelope {
937                        upstream,
938                        envelope,
939                        body,
940                        http_encoding,
941                        project_cache: self.inner.project_cache.clone(),
942                    }));
943            }
944            Err(error) => {
945                // Errors are only logged for what we consider an internal discard reason. These
946                // indicate errors in the infrastructure or implementation bugs.
947                relay_log::error!(
948                    error = &error as &dyn Error,
949                    tags.project_key = %envelope.scoping().project_key,
950                    "failed to serialize envelope payload"
951                );
952
953                envelope.reject(Outcome::Invalid(DiscardReason::Internal));
954            }
955        }
956    }
957
958    fn handle_submit_client_reports(&self, message: SubmitClientReports) {
959        let SubmitClientReports {
960            client_reports,
961            scoping,
962        } = message;
963
964        relay_log::trace!(
965            "sending {} client report(s) to project id {}",
966            client_reports.len(),
967            scoping.project_id
968        );
969
970        if client_reports.is_empty() {
971            return;
972        }
973
974        let config = self.inner.config.current();
975        let upstream = config.upstream();
976        let dsn = PartialDsn::outbound(&scoping, upstream);
977
978        let mut envelope = Envelope::from_request(None, RequestMeta::outbound(dsn));
979        for client_report in client_reports {
980            match client_report.serialize() {
981                Ok(payload) => {
982                    let mut item = Item::new(ItemType::ClientReport);
983                    item.set_payload(ContentType::Json, payload);
984                    envelope.add_item(item);
985                }
986                Err(error) => {
987                    relay_log::error!(
988                        error = &error as &dyn std::error::Error,
989                        "failed to serialize client report"
990                    );
991                }
992            }
993        }
994
995        let envelope = ManagedEnvelope::new(envelope, self.inner.addrs.outcome_aggregator.clone());
996        self.submit_envelope_upstream(envelope, &self.inner.config.current(), None);
997    }
998
999    fn check_buckets(
1000        &self,
1001        project_key: ProjectKey,
1002        project_info: &ProjectInfo,
1003        rate_limits: &RateLimits,
1004        buckets: Vec<Bucket>,
1005    ) -> Vec<Bucket> {
1006        let Some(scoping) = project_info.scoping(project_key) else {
1007            relay_log::error!(
1008                tags.project_key = project_key.as_str(),
1009                "there is no scoping: dropping {} buckets",
1010                buckets.len(),
1011            );
1012            return Vec::new();
1013        };
1014
1015        let mut buckets =
1016            self::metrics::remove_invalid_namespaces(buckets, &self.inner.metric_outcomes, scoping);
1017
1018        let mut namespaces: BTreeSet<MetricNamespace> = buckets
1019            .iter()
1020            .filter_map(|bucket| bucket.name.try_namespace())
1021            .collect();
1022
1023        // Never rate limit outcomes.
1024        namespaces.remove(&MetricNamespace::Outcomes);
1025
1026        for namespace in namespaces {
1027            let limits = rate_limits
1028                .check_with_quotas(project_info.get_quotas(), scoping.metric_bucket(namespace));
1029
1030            if limits.is_limited() {
1031                let rejected;
1032                (buckets, rejected) = utils::split_off(buckets, |bucket| {
1033                    bucket.name.try_namespace() == Some(namespace)
1034                });
1035
1036                let reason_code = limits.longest().and_then(|limit| limit.reason_code.clone());
1037                self.inner.metric_outcomes.track(
1038                    scoping,
1039                    &rejected,
1040                    Outcome::RateLimited(reason_code),
1041                );
1042            }
1043        }
1044
1045        let quotas = project_info.config.quotas.clone();
1046        match MetricsLimiter::create(buckets, quotas, scoping) {
1047            Ok(mut bucket_limiter) => {
1048                bucket_limiter.enforce_limits(rate_limits, &self.inner.metric_outcomes);
1049                bucket_limiter.into_buckets()
1050            }
1051            Err(buckets) => buckets,
1052        }
1053    }
1054
1055    #[cfg(feature = "processing")]
1056    async fn rate_limit_buckets(
1057        &self,
1058        scoping: Scoping,
1059        project_info: &ProjectInfo,
1060        mut buckets: Vec<Bucket>,
1061    ) -> Vec<Bucket> {
1062        let Some(rate_limiter) = &self.inner.rate_limiter else {
1063            return buckets;
1064        };
1065
1066        let global_config = self.inner.global_config.current().unwrap_or_default();
1067        let mut namespaces = buckets
1068            .iter()
1069            .filter_map(|bucket| bucket.name.try_namespace())
1070            .counts();
1071
1072        // Never rate limit outcomes.
1073        namespaces.remove(&MetricNamespace::Outcomes);
1074
1075        let quotas = CombinedQuotas::new(&global_config, project_info.get_quotas());
1076
1077        for (namespace, quantity) in namespaces {
1078            let item_scoping = scoping.metric_bucket(namespace);
1079
1080            let limits = match rate_limiter
1081                .is_rate_limited(quotas, item_scoping, quantity, false)
1082                .await
1083            {
1084                Ok(limits) => limits,
1085                Err(err) => {
1086                    relay_log::error!(
1087                        error = &err as &dyn std::error::Error,
1088                        "failed to check redis rate limits"
1089                    );
1090                    break;
1091                }
1092            };
1093
1094            if limits.is_limited() {
1095                let rejected;
1096                (buckets, rejected) = utils::split_off(buckets, |bucket| {
1097                    bucket.name.try_namespace() == Some(namespace)
1098                });
1099
1100                let reason_code = limits.longest().and_then(|limit| limit.reason_code.clone());
1101                self.inner.metric_outcomes.track(
1102                    scoping,
1103                    &rejected,
1104                    Outcome::RateLimited(reason_code),
1105                );
1106
1107                self.inner
1108                    .project_cache
1109                    .get(item_scoping.scoping.project_key)
1110                    .rate_limits()
1111                    .merge(limits);
1112            }
1113        }
1114
1115        match MetricsLimiter::create(buckets, project_info.config.quotas.clone(), scoping) {
1116            Err(buckets) => buckets,
1117            Ok(bucket_limiter) => self.apply_other_rate_limits(bucket_limiter).await,
1118        }
1119    }
1120
1121    /// Check and apply rate limits to metrics buckets for transactions and spans.
1122    #[cfg(feature = "processing")]
1123    async fn apply_other_rate_limits(&self, mut bucket_limiter: MetricsLimiter) -> Vec<Bucket> {
1124        relay_log::trace!("handle_rate_limit_buckets");
1125
1126        let scoping = *bucket_limiter.scoping();
1127
1128        if let Some(rate_limiter) = self.inner.rate_limiter.as_ref() {
1129            let global_config = self.inner.global_config.current().unwrap_or_default();
1130            let quotas = CombinedQuotas::new(&global_config, bucket_limiter.quotas());
1131
1132            // We set over_accept_once such that the limit is actually reached, which allows subsequent
1133            // calls with quantity=0 to be rate limited.
1134            let over_accept_once = true;
1135            let mut rate_limits = RateLimits::new();
1136
1137            let (category, count) = bucket_limiter.count();
1138
1139            let timer = Instant::now();
1140            let mut is_limited = false;
1141
1142            if let Some(count) = count {
1143                match rate_limiter
1144                    .is_rate_limited(quotas, scoping.item(category), count, over_accept_once)
1145                    .await
1146                {
1147                    Ok(limits) => {
1148                        is_limited = limits.is_limited();
1149                        rate_limits.merge(limits)
1150                    }
1151                    Err(e) => {
1152                        relay_log::error!(error = &e as &dyn Error, "rate limiting error")
1153                    }
1154                }
1155            }
1156
1157            relay_statsd::metric!(
1158                timer(RelayTimers::RateLimitBucketsDuration) = timer.elapsed(),
1159                category = category.name(),
1160                limited = if is_limited { "true" } else { "false" },
1161                count = match count {
1162                    None => "none",
1163                    Some(0) => "0",
1164                    Some(1) => "1",
1165                    Some(1..=10) => "10",
1166                    Some(1..=25) => "25",
1167                    Some(1..=50) => "50",
1168                    Some(51..=100) => "100",
1169                    Some(101..=500) => "500",
1170                    _ => "> 500",
1171                },
1172            );
1173
1174            if rate_limits.is_limited() {
1175                let was_enforced =
1176                    bucket_limiter.enforce_limits(&rate_limits, &self.inner.metric_outcomes);
1177
1178                if was_enforced {
1179                    // Update the rate limits in the project cache.
1180                    self.inner
1181                        .project_cache
1182                        .get(scoping.project_key)
1183                        .rate_limits()
1184                        .merge(rate_limits);
1185                }
1186            }
1187        }
1188
1189        bucket_limiter.into_buckets()
1190    }
1191
1192    /// Processes metric buckets and sends them to Kafka.
1193    ///
1194    /// This function runs the following steps:
1195    ///  - rate limiting
1196    ///  - emit billing outcomes
1197    ///  - submit to `StoreForwarder`
1198    #[cfg(feature = "processing")]
1199    async fn encode_metrics_processing(
1200        &self,
1201        message: FlushBuckets,
1202        store_forwarder: &Addr<Store>,
1203    ) {
1204        use crate::constants::DEFAULT_EVENT_RETENTION;
1205        use crate::services::store::StoreMetrics;
1206
1207        for ProjectBuckets {
1208            buckets,
1209            scoping,
1210            project_info,
1211            ..
1212        } in message.buckets.into_values()
1213        {
1214            let mut buckets = self
1215                .rate_limit_buckets(scoping, &project_info, buckets)
1216                .await;
1217
1218            if buckets.is_empty() {
1219                continue;
1220            }
1221
1222            // Emit metric billing outcomes.
1223            self.inner
1224                .metric_outcomes
1225                .track_accepted_outcome(scoping, &mut buckets);
1226
1227            let retention = project_info
1228                .config
1229                .event_retention
1230                .unwrap_or(DEFAULT_EVENT_RETENTION);
1231
1232            // The store forwarder takes care of bucket splitting internally, so we can submit the
1233            // entire list of buckets. There is no batching needed here.
1234            store_forwarder.send(StoreMetrics {
1235                buckets,
1236                scoping,
1237                retention,
1238            });
1239        }
1240    }
1241
1242    /// Serializes metric buckets to JSON and sends them to the upstream.
1243    ///
1244    /// This function runs the following steps:
1245    ///  - partitioning
1246    ///  - batching by configured size limit
1247    ///  - serialize to JSON and pack in an envelope
1248    ///
1249    /// Rate limiting runs only in processing Relays as it requires access to the central Redis instance.
1250    /// Cached rate limits are applied in the project cache already.
1251    fn encode_metrics_envelope(&self, message: FlushBuckets) {
1252        let FlushBuckets {
1253            partition_key,
1254            buckets,
1255        } = message;
1256
1257        let config = self.inner.config.current();
1258        let batch_size = config.metrics_max_batch_size_bytes();
1259        let upstream = config.upstream();
1260
1261        for ProjectBuckets {
1262            buckets,
1263            scoping,
1264            project_info,
1265            ..
1266        } in buckets.values()
1267        {
1268            let dsn = PartialDsn::outbound(scoping, upstream);
1269
1270            relay_statsd::metric!(
1271                distribution(RelayDistributions::PartitionKeys) = u64::from(partition_key)
1272            );
1273
1274            let mut num_batches = 0;
1275            for batch in BucketsView::from(buckets).by_size(batch_size) {
1276                let mut envelope = Envelope::from_request(None, RequestMeta::outbound(dsn.clone()));
1277
1278                let mut item = Item::new(ItemType::MetricBuckets);
1279                item.set_source_quantities(crate::metrics::extract_quantities(batch));
1280                item.set_payload(ContentType::Json, serde_json::to_vec(&buckets).unwrap());
1281                envelope.add_item(item);
1282
1283                let mut envelope =
1284                    ManagedEnvelope::new(envelope, self.inner.addrs.outcome_aggregator.clone());
1285                envelope
1286                    .set_partition_key(Some(partition_key))
1287                    .scope(*scoping);
1288
1289                relay_statsd::metric!(
1290                    distribution(RelayDistributions::BucketsPerBatch) = batch.len() as u64
1291                );
1292
1293                self.submit_envelope_upstream(envelope, &config, project_info.upstream.clone());
1294                num_batches += 1;
1295            }
1296
1297            relay_statsd::metric!(
1298                distribution(RelayDistributions::BatchesPerPartition) = num_batches
1299            );
1300        }
1301    }
1302
1303    /// Creates a [`SendMetricsRequest`] and sends it to the upstream relay.
1304    fn send_global_partition(
1305        &self,
1306        upstream: Option<UpstreamDescriptor>,
1307        partition_key: u32,
1308        partition: &mut Partition<'_>,
1309    ) {
1310        if partition.is_empty() {
1311            return;
1312        }
1313
1314        let (unencoded, project_info) = partition.take();
1315        let http_encoding = self.inner.config.current().http_encoding();
1316        let encoded = match encode_payload(&unencoded, http_encoding) {
1317            Ok(payload) => payload,
1318            Err(error) => {
1319                let error = &error as &dyn std::error::Error;
1320                relay_log::error!(error, "failed to encode metrics payload");
1321                return;
1322            }
1323        };
1324
1325        let request = SendMetricsRequest {
1326            upstream,
1327            partition_key: partition_key.to_string(),
1328            unencoded,
1329            encoded,
1330            project_info,
1331            http_encoding,
1332            metric_outcomes: self.inner.metric_outcomes.clone(),
1333        };
1334
1335        self.inner.addrs.upstream_relay.send(SendRequest(request));
1336    }
1337
1338    /// Serializes metric buckets to JSON and sends them to the upstream via the global endpoint.
1339    ///
1340    /// This function is similar to [`Self::encode_metrics_envelope`], but sends a global batched
1341    /// payload directly instead of per-project Envelopes.
1342    ///
1343    /// This function runs the following steps:
1344    ///  - partitioning
1345    ///  - batching by configured size limit
1346    ///  - serialize to JSON
1347    ///  - submit directly to the upstream
1348    fn encode_metrics_global(&self, message: FlushBuckets) {
1349        let FlushBuckets {
1350            partition_key,
1351            buckets,
1352        } = message;
1353
1354        let batch_size = self.inner.config.current().metrics_max_batch_size_bytes();
1355        let mut partitions = BTreeMap::new();
1356        let mut partition_splits = 0;
1357
1358        for ProjectBuckets {
1359            buckets,
1360            scoping,
1361            project_info,
1362            ..
1363        } in buckets.values()
1364        {
1365            let partition = match partitions.get_mut(&project_info.upstream) {
1366                Some(partition) => partition,
1367                None => partitions
1368                    .entry(project_info.upstream.clone())
1369                    .or_insert_with(|| Partition::new(batch_size)),
1370            };
1371
1372            for bucket in buckets {
1373                let mut remaining = Some(BucketView::new(bucket));
1374
1375                while let Some(bucket) = remaining.take() {
1376                    if let Some(next) = partition.insert(bucket, *scoping) {
1377                        // A part of the bucket could not be inserted. Take the partition and submit
1378                        // it immediately. Repeat until the final part was inserted. This should
1379                        // always result in a request, otherwise we would enter an endless loop.
1380                        self.send_global_partition(
1381                            project_info.upstream.clone(),
1382                            partition_key,
1383                            partition,
1384                        );
1385                        remaining = Some(next);
1386                        partition_splits += 1;
1387                    }
1388                }
1389            }
1390        }
1391
1392        if partition_splits > 0 {
1393            metric!(distribution(RelayDistributions::PartitionSplits) = partition_splits);
1394        }
1395
1396        for (upstream, mut partition) in partitions {
1397            self.send_global_partition(upstream, partition_key, &mut partition);
1398        }
1399    }
1400
1401    /// Removes all outcome metrics from `message` and sends them as client reports.
1402    ///
1403    /// Returns a new [`FlushBuckets`] message, without any outcome metrics remaining.
1404    fn encode_metrics_client_reports(&self, mut message: FlushBuckets) -> FlushBuckets {
1405        for ProjectBuckets {
1406            buckets, scoping, ..
1407        } in message.buckets.values_mut()
1408        {
1409            let client_reports = outcome::metric::extract_client_reports(buckets).collect();
1410
1411            self.handle_submit_client_reports(SubmitClientReports {
1412                client_reports,
1413                scoping: *scoping,
1414            });
1415        }
1416
1417        message
1418    }
1419
1420    async fn handle_flush_buckets(&self, mut message: FlushBuckets) {
1421        for (project_key, pb) in message.buckets.iter_mut() {
1422            let buckets = std::mem::take(&mut pb.buckets);
1423            pb.buckets =
1424                self.check_buckets(*project_key, &pb.project_info, &pb.rate_limits, buckets);
1425        }
1426
1427        let config = self.inner.config.current();
1428
1429        #[cfg(feature = "processing")]
1430        if config.processing_enabled()
1431            && let Some(ref store_forwarder) = self.inner.addrs.store_forwarder
1432        {
1433            return self
1434                .encode_metrics_processing(message, store_forwarder)
1435                .await;
1436        }
1437
1438        // Processing Relays never send outcomes as client reports, which is why this check is after
1439        // the processing check.
1440        if config.emit_outcomes() == EmitOutcomes::AsClientReports {
1441            // Remove client reports from metrics to be sent, if configured as client reports
1442            // and send them separately.
1443            message = self.encode_metrics_client_reports(message);
1444        }
1445
1446        if config.http_global_metrics() {
1447            self.encode_metrics_global(message)
1448        } else {
1449            self.encode_metrics_envelope(message)
1450        }
1451    }
1452
1453    #[cfg(all(test, feature = "processing"))]
1454    fn redis_rate_limiter_enabled(&self) -> bool {
1455        self.inner.rate_limiter.is_some()
1456    }
1457
1458    async fn handle_message(self, message: EnvelopeProcessor) {
1459        let ty = message.variant();
1460        let feature_weights = self.feature_weights(&message);
1461
1462        metric!(timer(RelayTimers::ProcessMessageDuration), message = ty, {
1463            let mut cogs = self.inner.cogs.timed(ResourceId::Relay, feature_weights);
1464
1465            match message {
1466                EnvelopeProcessor::ProcessEnvelope(m) => {
1467                    self.handle_process_envelope(&mut cogs, *m).await
1468                }
1469                EnvelopeProcessor::ProcessProjectMetrics(m) => {
1470                    self.handle_process_metrics(&mut cogs, *m)
1471                }
1472                EnvelopeProcessor::ProcessBatchedMetrics(m) => {
1473                    self.handle_process_batched_metrics(&mut cogs, *m)
1474                }
1475                EnvelopeProcessor::FlushBuckets(m) => self.handle_flush_buckets(*m).await,
1476                EnvelopeProcessor::SubmitClientReports(m) => self.handle_submit_client_reports(*m),
1477            }
1478        });
1479    }
1480
1481    fn feature_weights(&self, message: &EnvelopeProcessor) -> FeatureWeights {
1482        match message {
1483            // Envelope is split later and tokens are attributed then.
1484            EnvelopeProcessor::ProcessEnvelope(_) => AppFeature::Unattributed.into(),
1485            EnvelopeProcessor::ProcessProjectMetrics(_) => AppFeature::Unattributed.into(),
1486            EnvelopeProcessor::ProcessBatchedMetrics(_) => AppFeature::Unattributed.into(),
1487            EnvelopeProcessor::FlushBuckets(v) => v
1488                .buckets
1489                .values()
1490                .map(|s| {
1491                    if self.inner.config.current().processing_enabled() {
1492                        // Processing does not encode the metrics but instead rate limit the metrics,
1493                        // which scales by count and not size.
1494                        relay_metrics::cogs::ByCount(&s.buckets).into()
1495                    } else {
1496                        relay_metrics::cogs::BySize(&s.buckets).into()
1497                    }
1498                })
1499                .fold(FeatureWeights::none(), FeatureWeights::merge),
1500            EnvelopeProcessor::SubmitClientReports(_) => AppFeature::ClientReports.into(),
1501        }
1502    }
1503}
1504
1505impl Service for EnvelopeProcessorService {
1506    type Interface = EnvelopeProcessor;
1507
1508    async fn run(self, mut rx: relay_system::Receiver<Self::Interface>) {
1509        while let Some(message) = rx.recv().await {
1510            let service = self.clone();
1511            // Create a new hub to prevent sentry scopes from bleeding to other tasks.
1512            let hub = relay_log::Hub::new_from_top(relay_log::Hub::current());
1513
1514            self.inner
1515                .pool
1516                .spawn_async(Box::pin(service.handle_message(message).bind_hub(hub)))
1517                .await;
1518        }
1519    }
1520}
1521
1522pub fn encode_payload(body: &Bytes, http_encoding: HttpEncoding) -> Result<Bytes, std::io::Error> {
1523    let envelope_body: Vec<u8> = match http_encoding {
1524        HttpEncoding::Identity => return Ok(body.clone()),
1525        HttpEncoding::Deflate => {
1526            let mut encoder = ZlibEncoder::new(Vec::new(), Compression::default());
1527            encoder.write_all(body.as_ref())?;
1528            encoder.finish()?
1529        }
1530        HttpEncoding::Gzip => {
1531            let mut encoder = GzEncoder::new(Vec::new(), Compression::default());
1532            encoder.write_all(body.as_ref())?;
1533            encoder.finish()?
1534        }
1535        HttpEncoding::Br => {
1536            // Use default buffer size (via 0), medium quality (5), and the default lgwin (22).
1537            let mut encoder = BrotliEncoder::new(Vec::new(), 0, 5, 22);
1538            encoder.write_all(body.as_ref())?;
1539            encoder.into_inner()
1540        }
1541        HttpEncoding::Zstd => {
1542            // Use the fastest compression level, our main objective here is to get the best
1543            // compression ratio for least amount of time spent.
1544            let mut encoder = ZstdEncoder::new(Vec::new(), 1)?;
1545            encoder.write_all(body.as_ref())?;
1546            encoder.finish()?
1547        }
1548    };
1549
1550    Ok(envelope_body.into())
1551}
1552
1553/// An upstream request that submits an envelope via HTTP.
1554#[derive(Debug)]
1555pub struct SendEnvelope {
1556    pub upstream: Option<UpstreamDescriptor>,
1557    pub envelope: ManagedEnvelope,
1558    pub body: Bytes,
1559    pub http_encoding: HttpEncoding,
1560    pub project_cache: ProjectCacheHandle,
1561}
1562
1563impl UpstreamRequest for SendEnvelope {
1564    fn upstream(&self) -> Option<&UpstreamDescriptor> {
1565        self.upstream.as_ref()
1566    }
1567
1568    fn method(&self) -> reqwest::Method {
1569        reqwest::Method::POST
1570    }
1571
1572    fn path(&self) -> Cow<'_, str> {
1573        format!("/api/{}/envelope/", self.envelope.scoping().project_id).into()
1574    }
1575
1576    fn route(&self) -> &'static str {
1577        "envelope"
1578    }
1579
1580    fn build(&mut self, builder: &mut http::RequestBuilder) -> Result<(), http::HttpError> {
1581        let envelope_body = self.body.clone();
1582
1583        let meta = &self.envelope.meta();
1584        let shard = self.envelope.partition_key().map(|p| p.to_string());
1585        builder
1586            .content_encoding(self.http_encoding)
1587            .header_opt("Origin", meta.origin().map(|url| url.as_str()))
1588            .header_opt("User-Agent", meta.user_agent())
1589            .header("X-Sentry-Auth", meta.auth_header())
1590            .header("X-Forwarded-For", meta.forwarded_for())
1591            .header("Content-Type", envelope::CONTENT_TYPE)
1592            .header_opt("X-Sentry-Relay-Shard", shard)
1593            .body(envelope_body);
1594
1595        Ok(())
1596    }
1597
1598    fn sign(&mut self) -> Option<Sign> {
1599        Some(Sign::Optional(SignatureType::RequestSign))
1600    }
1601
1602    fn respond(
1603        self: Box<Self>,
1604        result: Result<http::Response, UpstreamRequestError>,
1605    ) -> Pin<Box<dyn Future<Output = ()> + Send + Sync>> {
1606        Box::pin(async move {
1607            let result = match result {
1608                Ok(mut response) => response.consume().await.map_err(UpstreamRequestError::Http),
1609                Err(error) => Err(error),
1610            };
1611
1612            match result {
1613                Ok(()) => self.envelope.accept(),
1614                Err(error) if error.is_received() => {
1615                    let scoping = self.envelope.scoping();
1616                    self.envelope.accept();
1617
1618                    if let UpstreamRequestError::RateLimited(limits) = error {
1619                        self.project_cache
1620                            .get(scoping.project_key)
1621                            .rate_limits()
1622                            .merge(limits.scope(&scoping));
1623                    }
1624                }
1625                Err(error) => {
1626                    // Errors are only logged for what we consider an internal discard reason. These
1627                    // indicate errors in the infrastructure or implementation bugs.
1628                    let mut envelope = self.envelope;
1629                    envelope.reject(Outcome::Invalid(DiscardReason::Internal));
1630                    relay_log::error!(
1631                        error = &error as &dyn Error,
1632                        tags.project_key = %envelope.scoping().project_key,
1633                        "error sending envelope"
1634                    );
1635                }
1636            }
1637        })
1638    }
1639}
1640
1641/// A container for metric buckets from multiple projects.
1642///
1643/// This container is used to send metrics to the upstream in global batches as part of the
1644/// [`FlushBuckets`] message if the `http.global_metrics` option is enabled. The container monitors
1645/// the size of all metrics and allows to split them into multiple batches. See
1646/// [`insert`](Self::insert) for more information.
1647#[derive(Debug)]
1648struct Partition<'a> {
1649    max_size: usize,
1650    remaining: usize,
1651    views: HashMap<ProjectKey, Vec<BucketView<'a>>>,
1652    project_info: HashMap<ProjectKey, Scoping>,
1653}
1654
1655impl<'a> Partition<'a> {
1656    /// Creates a new partition with the given maximum size in bytes.
1657    pub fn new(size: usize) -> Self {
1658        Self {
1659            max_size: size,
1660            remaining: size,
1661            views: HashMap::new(),
1662            project_info: HashMap::new(),
1663        }
1664    }
1665
1666    /// Inserts a bucket into the partition, splitting it if necessary.
1667    ///
1668    /// This function attempts to add the bucket to this partition. If the bucket does not fit
1669    /// entirely into the partition given its maximum size, the remaining part of the bucket is
1670    /// returned from this function call.
1671    ///
1672    /// If this function returns `Some(_)`, the partition is full and should be submitted to the
1673    /// upstream immediately. Use [`Self::take`] to retrieve the contents of the
1674    /// partition. Afterwards, the caller is responsible to call this function again with the
1675    /// remaining bucket until it is fully inserted.
1676    pub fn insert(&mut self, bucket: BucketView<'a>, scoping: Scoping) -> Option<BucketView<'a>> {
1677        let (current, next) = bucket.split(self.remaining, Some(self.max_size));
1678
1679        if let Some(current) = current {
1680            self.remaining = self.remaining.saturating_sub(current.estimated_size());
1681            self.views
1682                .entry(scoping.project_key)
1683                .or_default()
1684                .push(current);
1685
1686            self.project_info
1687                .entry(scoping.project_key)
1688                .or_insert(scoping);
1689        }
1690
1691        next
1692    }
1693
1694    /// Returns `true` if the partition does not hold any data.
1695    fn is_empty(&self) -> bool {
1696        self.views.is_empty()
1697    }
1698
1699    /// Returns the serialized buckets for this partition.
1700    ///
1701    /// This empties the partition, so that it can be reused.
1702    fn take(&mut self) -> (Bytes, HashMap<ProjectKey, Scoping>) {
1703        #[derive(serde::Serialize)]
1704        struct Wrapper<'a> {
1705            buckets: &'a HashMap<ProjectKey, Vec<BucketView<'a>>>,
1706        }
1707
1708        let buckets = &self.views;
1709        let payload = serde_json::to_vec(&Wrapper { buckets }).unwrap().into();
1710
1711        let scopings = std::mem::take(&mut self.project_info);
1712
1713        self.views.clear();
1714        self.remaining = self.max_size;
1715
1716        (payload, scopings)
1717    }
1718}
1719
1720/// An upstream request that submits metric buckets via HTTP.
1721///
1722/// This request is not awaited. It automatically tracks outcomes if the request is not received.
1723#[derive(Debug)]
1724struct SendMetricsRequest {
1725    /// Optional upstream override where the request will be sent to.
1726    upstream: Option<UpstreamDescriptor>,
1727    /// If the partition key is set, the request is marked with `X-Sentry-Relay-Shard`.
1728    partition_key: String,
1729    /// Serialized metric buckets without encoding applied, used for signing.
1730    unencoded: Bytes,
1731    /// Serialized metric buckets with the stated HTTP encoding applied.
1732    encoded: Bytes,
1733    /// Mapping of all contained project keys to their scoping and extraction mode.
1734    ///
1735    /// Used to track outcomes for transmission failures.
1736    project_info: HashMap<ProjectKey, Scoping>,
1737    /// Encoding (compression) of the payload.
1738    http_encoding: HttpEncoding,
1739    /// Metric outcomes instance to send outcomes on error.
1740    metric_outcomes: MetricOutcomes,
1741}
1742
1743impl SendMetricsRequest {
1744    fn create_error_outcomes(self) {
1745        #[derive(serde::Deserialize)]
1746        struct Wrapper {
1747            buckets: HashMap<ProjectKey, Vec<MinimalTrackableBucket>>,
1748        }
1749
1750        let buckets = match serde_json::from_slice(&self.unencoded) {
1751            Ok(Wrapper { buckets }) => buckets,
1752            Err(err) => {
1753                relay_log::error!(
1754                    error = &err as &dyn std::error::Error,
1755                    "failed to parse buckets from failed transmission"
1756                );
1757                return;
1758            }
1759        };
1760
1761        for (key, buckets) in buckets {
1762            let Some(&scoping) = self.project_info.get(&key) else {
1763                relay_log::error!("missing scoping for project key");
1764                continue;
1765            };
1766
1767            self.metric_outcomes.track(
1768                scoping,
1769                &buckets,
1770                Outcome::Invalid(DiscardReason::Internal),
1771            );
1772        }
1773    }
1774}
1775
1776impl UpstreamRequest for SendMetricsRequest {
1777    fn upstream(&self) -> Option<&UpstreamDescriptor> {
1778        self.upstream.as_ref()
1779    }
1780
1781    fn set_relay_id(&self) -> bool {
1782        true
1783    }
1784
1785    fn sign(&mut self) -> Option<Sign> {
1786        Some(Sign::Required(SignatureType::Body(self.unencoded.clone())))
1787    }
1788
1789    fn method(&self) -> reqwest::Method {
1790        reqwest::Method::POST
1791    }
1792
1793    fn path(&self) -> Cow<'_, str> {
1794        "/api/0/relays/metrics/".into()
1795    }
1796
1797    fn route(&self) -> &'static str {
1798        "global_metrics"
1799    }
1800
1801    fn build(&mut self, builder: &mut http::RequestBuilder) -> Result<(), http::HttpError> {
1802        builder
1803            .content_encoding(self.http_encoding)
1804            .header("X-Sentry-Relay-Shard", self.partition_key.as_bytes())
1805            .header(header::CONTENT_TYPE, b"application/json")
1806            .body(self.encoded.clone());
1807
1808        Ok(())
1809    }
1810
1811    fn respond(
1812        self: Box<Self>,
1813        result: Result<http::Response, UpstreamRequestError>,
1814    ) -> Pin<Box<dyn Future<Output = ()> + Send + Sync>> {
1815        Box::pin(async {
1816            match result {
1817                Ok(mut response) => {
1818                    response.consume().await.ok();
1819                }
1820                Err(error) => {
1821                    relay_log::error!(error = &error as &dyn Error, "Failed to send metrics batch");
1822
1823                    // If the request did not arrive at the upstream, we are responsible for outcomes.
1824                    // Otherwise, the upstream is responsible to log outcomes.
1825                    if error.is_received() {
1826                        return;
1827                    }
1828
1829                    self.create_error_outcomes()
1830                }
1831            }
1832        })
1833    }
1834}
1835
1836/// Container for global and project level [`Quota`].
1837#[derive(Copy, Clone, Debug)]
1838#[cfg(feature = "processing")]
1839struct CombinedQuotas<'a> {
1840    global_quotas: &'a [Quota],
1841    project_quotas: &'a [Quota],
1842}
1843
1844#[cfg(feature = "processing")]
1845impl<'a> CombinedQuotas<'a> {
1846    /// Returns a new [`CombinedQuotas`].
1847    pub fn new(global_config: &'a GlobalConfig, project_quotas: &'a [Quota]) -> Self {
1848        Self {
1849            global_quotas: &global_config.quotas,
1850            project_quotas,
1851        }
1852    }
1853}
1854
1855#[cfg(feature = "processing")]
1856impl<'a> IntoIterator for CombinedQuotas<'a> {
1857    type Item = &'a Quota;
1858    type IntoIter = std::iter::Chain<std::slice::Iter<'a, Quota>, std::slice::Iter<'a, Quota>>;
1859
1860    fn into_iter(self) -> Self::IntoIter {
1861        self.global_quotas.iter().chain(self.project_quotas.iter())
1862    }
1863}
1864
1865#[cfg(test)]
1866mod tests {
1867    use insta::assert_debug_snapshot;
1868    use relay_common::glob2::LazyGlob;
1869    use relay_dynamic_config::ProjectConfig;
1870    use relay_event_normalization::{
1871        NormalizationConfig, RedactionRule, TransactionNameConfig, TransactionNameRule,
1872    };
1873    use relay_event_schema::protocol::{Event, EventId, TransactionSource};
1874    use relay_pii::DataScrubbingConfig;
1875    use relay_protocol::Annotated;
1876    #[cfg(feature = "processing")]
1877    use relay_quotas::DataCategory;
1878    use similar_asserts::assert_eq;
1879
1880    use crate::testutils::{create_test_processor, create_test_processor_with_addrs};
1881
1882    #[cfg(feature = "processing")]
1883    use {
1884        relay_metrics::BucketValue,
1885        relay_quotas::{QuotaScope, ReasonCode},
1886        relay_test::mock_service,
1887    };
1888
1889    use super::*;
1890
1891    async fn process_to_single_envelope<'a>(
1892        processor: &EnvelopeProcessorService,
1893        envelope: ManagedEnvelope,
1894        ctx: processing::Context<'a>,
1895    ) -> Box<Envelope> {
1896        let mut outputs = processor.process(envelope, ctx).await;
1897        assert_eq!(outputs.len(), 1);
1898
1899        let Output { main, metrics } = outputs.pop().unwrap();
1900
1901        if let Some(metrics) = metrics {
1902            metrics.accept(drop);
1903        }
1904
1905        main.unwrap()
1906            .serialize_envelope(ctx.to_forward())
1907            .unwrap()
1908            .accept(|envelope| envelope)
1909    }
1910
1911    #[cfg(feature = "processing")]
1912    fn mock_quota(id: &str) -> Quota {
1913        Quota {
1914            id: Some(id.into()),
1915            categories: [DataCategory::MetricBucket].into(),
1916            scope: QuotaScope::Organization,
1917            scope_id: None,
1918            limit: Some(0),
1919            window: None,
1920            reason_code: None,
1921            namespace: None,
1922        }
1923    }
1924
1925    #[cfg(feature = "processing")]
1926    #[test]
1927    fn test_dynamic_quotas() {
1928        let global_config = relay_dynamic_config::GlobalConfig {
1929            quotas: vec![mock_quota("foo"), mock_quota("bar")],
1930            ..Default::default()
1931        };
1932
1933        let project_quotas = vec![mock_quota("baz"), mock_quota("qux")];
1934
1935        let dynamic_quotas = CombinedQuotas::new(&global_config, &project_quotas);
1936
1937        let quota_ids = dynamic_quotas.into_iter().filter_map(|q| q.id.as_deref());
1938        assert!(quota_ids.eq(["foo", "bar", "baz", "qux"]));
1939    }
1940
1941    /// Ensures that if we ratelimit one batch of buckets in [`FlushBuckets`] message, it won't
1942    /// also ratelimit the next batches in the same message automatically.
1943    #[cfg(feature = "processing")]
1944    #[tokio::test]
1945    async fn test_ratelimit_per_batch() {
1946        use relay_base_schema::organization::OrganizationId;
1947        use relay_protocol::FiniteF64;
1948
1949        let rate_limited_org = Scoping {
1950            organization_id: OrganizationId::new(1),
1951            project_id: ProjectId::new(21),
1952            project_key: ProjectKey::parse("00000000000000000000000000000000").unwrap(),
1953            key_id: Some(17),
1954        };
1955
1956        let not_rate_limited_org = Scoping {
1957            organization_id: OrganizationId::new(2),
1958            project_id: ProjectId::new(21),
1959            project_key: ProjectKey::parse("11111111111111111111111111111111").unwrap(),
1960            key_id: Some(17),
1961        };
1962
1963        let message = {
1964            let project_info = {
1965                let quota = Quota {
1966                    id: Some("testing".into()),
1967                    categories: [DataCategory::MetricBucket].into(),
1968                    scope: relay_quotas::QuotaScope::Organization,
1969                    scope_id: Some(rate_limited_org.organization_id.to_string().into()),
1970                    limit: Some(0),
1971                    window: None,
1972                    reason_code: Some(ReasonCode::new("test")),
1973                    namespace: None,
1974                };
1975
1976                let mut config = ProjectConfig::default();
1977                config.quotas.push(quota);
1978
1979                Arc::new(ProjectInfo {
1980                    config,
1981                    ..Default::default()
1982                })
1983            };
1984
1985            let project_metrics = |scoping| ProjectBuckets {
1986                buckets: vec![Bucket {
1987                    name: "d:spans/bar".into(),
1988                    value: BucketValue::Counter(FiniteF64::new(1.0).unwrap()),
1989                    timestamp: UnixTimestamp::now(),
1990                    tags: Default::default(),
1991                    width: 10,
1992                    metadata: BucketMetadata::default(),
1993                }],
1994                rate_limits: Default::default(),
1995                project_info: project_info.clone(),
1996                scoping,
1997            };
1998
1999            let buckets = hashbrown::HashMap::from([
2000                (
2001                    rate_limited_org.project_key,
2002                    project_metrics(rate_limited_org),
2003                ),
2004                (
2005                    not_rate_limited_org.project_key,
2006                    project_metrics(not_rate_limited_org),
2007                ),
2008            ]);
2009
2010            FlushBuckets {
2011                partition_key: 0,
2012                buckets,
2013            }
2014        };
2015
2016        // ensure the order of the map while iterating is as expected.
2017        assert_eq!(message.buckets.keys().count(), 2);
2018
2019        let config = {
2020            let config_json = serde_json::json!({
2021                "processing": {
2022                    "enabled": true,
2023                    "kafka_config": [],
2024                    "redis": {
2025                        "server": std::env::var("RELAY_REDIS_URL").unwrap_or_else(|_| "redis://127.0.0.1:6379".to_owned()),
2026                    }
2027                }
2028            });
2029            Config::from_json_value(config_json).unwrap()
2030        };
2031
2032        let (store, handle) = {
2033            let f = |org_ids: &mut Vec<OrganizationId>, msg: Store| {
2034                let org_id = match msg {
2035                    Store::Metrics(x) => x.scoping.organization_id,
2036                    _ => panic!("received envelope when expecting only metrics"),
2037                };
2038                org_ids.push(org_id);
2039            };
2040
2041            mock_service("store_forwarder", vec![], f)
2042        };
2043
2044        let processor = create_test_processor(config).await;
2045        assert!(processor.redis_rate_limiter_enabled());
2046
2047        processor.encode_metrics_processing(message, &store).await;
2048
2049        drop(store);
2050        let orgs_not_ratelimited = handle.await.unwrap();
2051
2052        assert_eq!(
2053            orgs_not_ratelimited,
2054            vec![not_rate_limited_org.organization_id]
2055        );
2056    }
2057
2058    #[tokio::test]
2059    async fn test_browser_version_extraction_with_pii_like_data() {
2060        let processor = create_test_processor(Default::default()).await;
2061        let outcome_aggregator = Addr::dummy();
2062        let event_id = EventId::new();
2063
2064        let dsn = "https://e12d836b15bb49d7bbf99e64295d995b:@sentry.io/42"
2065            .parse()
2066            .unwrap();
2067
2068        let request_meta = RequestMeta::new(dsn);
2069        let mut envelope = Envelope::from_request(Some(event_id), request_meta);
2070
2071        envelope.add_item({
2072                let mut item = Item::new(ItemType::Event);
2073                item.set_payload(
2074                    ContentType::Json,
2075                    r#"
2076                    {
2077                        "request": {
2078                            "headers": [
2079                                ["User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36"]
2080                            ]
2081                        }
2082                    }
2083                "#,
2084                );
2085                item
2086            });
2087
2088        let mut datascrubbing_settings = DataScrubbingConfig::default();
2089        // enable all the default scrubbing
2090        datascrubbing_settings.scrub_data = true;
2091        datascrubbing_settings.scrub_defaults = true;
2092        datascrubbing_settings.scrub_ip_addresses = true;
2093
2094        // Make sure to mask any IP-like looking data
2095        let pii_config = serde_json::from_str(r#"{"applications": {"**": ["@ip:mask"]}}"#).unwrap();
2096
2097        let config = ProjectConfig {
2098            datascrubbing_settings,
2099            pii_config: Some(pii_config),
2100            ..Default::default()
2101        };
2102
2103        let project_info = ProjectInfo {
2104            config,
2105            ..Default::default()
2106        };
2107
2108        let envelope = ManagedEnvelope::new(envelope, outcome_aggregator);
2109
2110        let ctx = processing::Context {
2111            project_info: &project_info,
2112            ..processing::Context::for_test()
2113        };
2114
2115        let new_envelope = process_to_single_envelope(&processor, envelope, ctx).await;
2116
2117        let event_item = new_envelope.items().last().unwrap();
2118        let annotated_event: Annotated<Event> =
2119            Annotated::from_json_bytes(&event_item.payload()).unwrap();
2120        let event = annotated_event.into_value().unwrap();
2121        let headers = event
2122            .request
2123            .into_value()
2124            .unwrap()
2125            .headers
2126            .into_value()
2127            .unwrap();
2128
2129        // IP-like data must be masked
2130        assert_eq!(
2131            Some(
2132                "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/********* Safari/537.36"
2133            ),
2134            headers.get_header("User-Agent")
2135        );
2136        // But we still get correct browser and version number
2137        let contexts = event.contexts.into_value().unwrap();
2138        let browser = contexts.0.get("browser").unwrap();
2139        assert_eq!(
2140            r#"{"browser":"Chrome 103.0.0","name":"Chrome","version":"103.0.0","type":"browser"}"#,
2141            browser.to_json().unwrap()
2142        );
2143    }
2144
2145    #[tokio::test]
2146    #[cfg(feature = "processing")]
2147    async fn test_materialize_dsc() {
2148        use crate::services::projects::project::PublicKeyConfig;
2149
2150        let dsn = "https://e12d836b15bb49d7bbf99e64295d995b:@sentry.io/42"
2151            .parse()
2152            .unwrap();
2153        let request_meta = RequestMeta::new(dsn);
2154        let mut envelope = Envelope::from_request(None, request_meta);
2155
2156        let dsc = r#"{
2157            "trace_id": "00000000-0000-0000-0000-000000000001",
2158            "public_key": "e12d836b15bb49d7bbf99e64295d995b",
2159            "sample_rate": "0.2"
2160        }"#;
2161        envelope.set_dsc(serde_json::from_str(dsc).unwrap());
2162
2163        let mut item = Item::new(ItemType::Event);
2164        item.set_payload(ContentType::Json, r#"{}"#);
2165        envelope.add_item(item);
2166
2167        let outcome_aggregator = Addr::dummy();
2168        let managed_envelope = ManagedEnvelope::new(envelope, outcome_aggregator);
2169
2170        let mut project_info = ProjectInfo::default();
2171        project_info.public_keys.push(PublicKeyConfig {
2172            public_key: ProjectKey::parse("e12d836b15bb49d7bbf99e64295d995b").unwrap(),
2173            numeric_id: Some(1),
2174        });
2175
2176        let config = serde_json::json!({
2177            "processing": {
2178                "enabled": true,
2179                "kafka_config": [],
2180            }
2181        });
2182
2183        let processor =
2184            create_test_processor(Config::from_json_value(config.clone()).unwrap()).await;
2185        let config = Config::from_json_value(config).unwrap().current();
2186        let ctx = processing::Context {
2187            config: &config,
2188            project_info: &project_info,
2189            sampling_project_info: Some(&project_info),
2190            ..processing::Context::for_test()
2191        };
2192
2193        let envelope = process_to_single_envelope(&processor, managed_envelope, ctx).await;
2194        let event = envelope
2195            .get_item_by(|item| item.ty() == &ItemType::Event)
2196            .unwrap();
2197
2198        let event = Annotated::<Event>::from_json_bytes(&event.payload()).unwrap();
2199        insta::assert_debug_snapshot!(event.value().unwrap()._dsc, @r###"
2200        Object(
2201            {
2202                "environment": ~,
2203                "public_key": String(
2204                    "e12d836b15bb49d7bbf99e64295d995b",
2205                ),
2206                "release": ~,
2207                "replay_id": ~,
2208                "sample_rate": String(
2209                    "0.2",
2210                ),
2211                "trace_id": String(
2212                    "00000000000000000000000000000001",
2213                ),
2214                "transaction": ~,
2215            },
2216        )
2217        "###);
2218    }
2219
2220    fn capture_test_event(transaction_name: &str, source: TransactionSource) -> Vec<String> {
2221        let mut event = Annotated::<Event>::from_json(
2222            r#"
2223            {
2224                "type": "transaction",
2225                "transaction": "/foo/",
2226                "timestamp": 946684810.0,
2227                "start_timestamp": 946684800.0,
2228                "contexts": {
2229                    "trace": {
2230                        "trace_id": "4c79f60c11214eb38604f4ae0781bfb2",
2231                        "span_id": "fa90fdead5f74053",
2232                        "op": "http.server",
2233                        "type": "trace"
2234                    }
2235                },
2236                "transaction_info": {
2237                    "source": "url"
2238                }
2239            }
2240            "#,
2241        )
2242        .unwrap();
2243        let e = event.value_mut().as_mut().unwrap();
2244        e.transaction.set_value(Some(transaction_name.into()));
2245
2246        e.transaction_info
2247            .value_mut()
2248            .as_mut()
2249            .unwrap()
2250            .source
2251            .set_value(Some(source));
2252
2253        relay_statsd::with_capturing_test_client(|| {
2254            utils::log_transaction_name_metrics(&mut event, |event| {
2255                let config = NormalizationConfig {
2256                    transaction_name_config: TransactionNameConfig {
2257                        rules: &[TransactionNameRule {
2258                            pattern: LazyGlob::new("/foo/*/**".to_owned()),
2259                            expiry: DateTime::<Utc>::MAX_UTC,
2260                            redaction: RedactionRule::Replace {
2261                                substitution: "*".to_owned(),
2262                            },
2263                        }],
2264                    },
2265                    ..Default::default()
2266                };
2267                relay_event_normalization::normalize_event(event, &config)
2268            });
2269        })
2270    }
2271
2272    #[test]
2273    fn test_log_transaction_metrics_none() {
2274        let captures = capture_test_event("/nothing", TransactionSource::Url);
2275        insta::assert_debug_snapshot!(captures, @r###"
2276        [
2277            "event.transaction_name_changes:1|c|#source_in:url,changes:none,source_out:sanitized,is_404:false",
2278        ]
2279        "###);
2280    }
2281
2282    #[test]
2283    fn test_log_transaction_metrics_rule() {
2284        let captures = capture_test_event("/foo/john/denver", TransactionSource::Url);
2285        insta::assert_debug_snapshot!(captures, @r###"
2286        [
2287            "event.transaction_name_changes:1|c|#source_in:url,changes:rule,source_out:sanitized,is_404:false",
2288        ]
2289        "###);
2290    }
2291
2292    #[test]
2293    fn test_log_transaction_metrics_pattern() {
2294        let captures = capture_test_event("/something/12345", TransactionSource::Url);
2295        insta::assert_debug_snapshot!(captures, @r###"
2296        [
2297            "event.transaction_name_changes:1|c|#source_in:url,changes:pattern,source_out:sanitized,is_404:false",
2298        ]
2299        "###);
2300    }
2301
2302    #[test]
2303    fn test_log_transaction_metrics_both() {
2304        let captures = capture_test_event("/foo/john/12345", TransactionSource::Url);
2305        insta::assert_debug_snapshot!(captures, @r###"
2306        [
2307            "event.transaction_name_changes:1|c|#source_in:url,changes:both,source_out:sanitized,is_404:false",
2308        ]
2309        "###);
2310    }
2311
2312    #[test]
2313    fn test_log_transaction_metrics_no_match() {
2314        let captures = capture_test_event("/foo/john/12345", TransactionSource::Route);
2315        insta::assert_debug_snapshot!(captures, @r###"
2316        [
2317            "event.transaction_name_changes:1|c|#source_in:route,changes:none,source_out:route,is_404:false",
2318        ]
2319        "###);
2320    }
2321
2322    #[tokio::test]
2323    async fn test_process_metrics_bucket_metadata() {
2324        let mut token = Cogs::noop().timed(ResourceId::Relay, AppFeature::Unattributed);
2325        let project_key = ProjectKey::parse("a94ae32be2584e0bbd7a4cbb95971fee").unwrap();
2326        let received_at = Utc::now();
2327        let config = Config::default();
2328
2329        let (aggregator, mut aggregator_rx) = Addr::custom();
2330        let processor = create_test_processor_with_addrs(
2331            config,
2332            Addrs {
2333                aggregator,
2334                ..Default::default()
2335            },
2336        )
2337        .await;
2338
2339        let mut item = Item::new(ItemType::Statsd);
2340        item.set_payload(ContentType::Text, "spans/foo:3182887624:4267882815|s");
2341        for (source, expected_received_at) in [
2342            (
2343                BucketSource::External,
2344                Some(UnixTimestamp::from_datetime(received_at).unwrap()),
2345            ),
2346            (BucketSource::Internal, None),
2347        ] {
2348            let message = ProcessMetrics {
2349                data: MetricData::Raw(vec![item.clone()]),
2350                project_key,
2351                source,
2352                received_at,
2353                sent_at: Some(Utc::now()),
2354            };
2355            processor.handle_process_metrics(&mut token, message);
2356
2357            let Aggregator::MergeBuckets(merge_buckets) = aggregator_rx.recv().await.unwrap();
2358            let buckets = merge_buckets.buckets;
2359            assert_eq!(buckets.len(), 1);
2360            assert_eq!(buckets[0].metadata.received_at, expected_received_at);
2361        }
2362    }
2363
2364    #[tokio::test]
2365    async fn test_process_batched_metrics() {
2366        let mut token = Cogs::noop().timed(ResourceId::Relay, AppFeature::Unattributed);
2367        let received_at = Utc::now();
2368        let config = Config::default();
2369
2370        let (aggregator, mut aggregator_rx) = Addr::custom();
2371        let processor = create_test_processor_with_addrs(
2372            config,
2373            Addrs {
2374                aggregator,
2375                ..Default::default()
2376            },
2377        )
2378        .await;
2379
2380        let payload = r#"{
2381    "buckets": {
2382        "11111111111111111111111111111111": [
2383            {
2384                "timestamp": 1615889440,
2385                "width": 0,
2386                "name": "d:transactions/endpoint.response_time@millisecond",
2387                "type": "d",
2388                "value": [
2389                  68.0
2390                ],
2391                "tags": {
2392                  "route": "user_index"
2393                }
2394            }
2395        ],
2396        "22222222222222222222222222222222": [
2397            {
2398                "timestamp": 1615889440,
2399                "width": 0,
2400                "name": "d:transactions/endpoint.cache_rate@none",
2401                "type": "d",
2402                "value": [
2403                  36.0
2404                ]
2405            }
2406        ]
2407    }
2408}
2409"#;
2410        let message = ProcessBatchedMetrics {
2411            payload: Bytes::from(payload),
2412            source: BucketSource::Internal,
2413            received_at,
2414            sent_at: Some(Utc::now()),
2415        };
2416        processor.handle_process_batched_metrics(&mut token, message);
2417
2418        let Aggregator::MergeBuckets(mb1) = aggregator_rx.recv().await.unwrap();
2419        let Aggregator::MergeBuckets(mb2) = aggregator_rx.recv().await.unwrap();
2420
2421        let mut messages = vec![mb1, mb2];
2422        messages.sort_by_key(|mb| mb.project_key);
2423
2424        let actual = messages
2425            .into_iter()
2426            .map(|mb| (mb.project_key, mb.buckets))
2427            .collect::<Vec<_>>();
2428
2429        assert_debug_snapshot!(actual, @r###"
2430        [
2431            (
2432                ProjectKey("11111111111111111111111111111111"),
2433                [
2434                    Bucket {
2435                        timestamp: UnixTimestamp(1615889440),
2436                        width: 0,
2437                        name: MetricName(
2438                            "d:transactions/endpoint.response_time@millisecond",
2439                        ),
2440                        value: Distribution(
2441                            [
2442                                68.0,
2443                            ],
2444                        ),
2445                        tags: {
2446                            "route": "user_index",
2447                        },
2448                        metadata: BucketMetadata {
2449                            merges: 1,
2450                            received_at: None,
2451                            extracted_from_indexed: false,
2452                        },
2453                    },
2454                ],
2455            ),
2456            (
2457                ProjectKey("22222222222222222222222222222222"),
2458                [
2459                    Bucket {
2460                        timestamp: UnixTimestamp(1615889440),
2461                        width: 0,
2462                        name: MetricName(
2463                            "d:transactions/endpoint.cache_rate@none",
2464                        ),
2465                        value: Distribution(
2466                            [
2467                                36.0,
2468                            ],
2469                        ),
2470                        tags: {},
2471                        metadata: BucketMetadata {
2472                            merges: 1,
2473                            received_at: None,
2474                            extracted_from_indexed: false,
2475                        },
2476                    },
2477                ],
2478            ),
2479        ]
2480        "###);
2481    }
2482}