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, 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;
56use symbolic_unreal::{Unreal4Error, Unreal4ErrorKind};
57#[cfg(feature = "processing")]
58use {
59 crate::services::objectstore::Objectstore,
60 crate::services::store::Store,
61 itertools::Itertools,
62 relay_dynamic_config::GlobalConfig,
63 relay_quotas::{Quota, RateLimitingError, RedisRateLimiter},
64 relay_redis::RedisClients,
65 std::time::Instant,
66};
67
68mod metrics;
69
70pub const MINIMUM_CLOCK_DRIFT: Duration = Duration::from_secs(55 * 60);
72
73#[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 #[error("invalid unreal crash report")]
86 InvalidUnrealReport(#[source] Unreal4Error),
87
88 #[error("event payload too large")]
89 PayloadTooLarge(DiscardItemType),
90
91 #[error("invalid transaction event")]
92 InvalidTransaction,
93
94 #[error("the item is not allowed/supported in this envelope")]
95 UnsupportedItem,
96
97 #[error("envelope processor failed")]
98 ProcessingFailed(#[from] ProcessingAction),
99
100 #[error("duplicate {0} in event")]
101 DuplicateItem(ItemType),
102
103 #[error("failed to extract event payload")]
104 NoEventPayload,
105
106 #[error("invalid security report type: {0:?}")]
107 InvalidSecurityType(Bytes),
108
109 #[error("unsupported security report type")]
110 UnsupportedSecurityType,
111
112 #[error("invalid security report")]
113 InvalidSecurityReport(#[source] serde_json::Error),
114
115 #[error("event filtered with reason: {0:?}")]
116 EventFiltered(FilterStatKey),
117
118 #[error("could not serialize event payload")]
119 SerializeFailed(#[source] serde_json::Error),
120
121 #[cfg(feature = "processing")]
122 #[error("failed to apply quotas")]
123 QuotasFailed(#[from] RateLimitingError),
124
125 #[error("nintendo switch dying message processing failed {0:?}")]
126 InvalidNintendoDyingMessage(#[source] SwitchProcessingError),
127
128 #[cfg(all(sentry, feature = "processing"))]
129 #[error("playstation dump processing failed: {0}")]
130 InvalidPlaystationDump(String),
131
132 #[cfg(feature = "processing")]
133 #[error("invalid attachment reference")]
134 InvalidAttachmentRef,
135}
136
137impl ProcessingError {
138 pub fn to_outcome(&self) -> Option<Outcome> {
139 match self {
140 Self::PayloadTooLarge(payload_type) => {
141 Some(Outcome::Invalid(DiscardReason::ItemTooLarge(*payload_type)))
142 }
143 Self::InvalidJson(_) => Some(Outcome::Invalid(DiscardReason::InvalidJson)),
144 Self::InvalidMsgpack(_) => Some(Outcome::Invalid(DiscardReason::InvalidMsgpack)),
145 Self::NestingTooDeep => Some(Outcome::Invalid(DiscardReason::NestingTooDeep)),
146 Self::InvalidSecurityType(_) => {
147 Some(Outcome::Invalid(DiscardReason::SecurityReportType))
148 }
149 Self::UnsupportedItem => Some(Outcome::Invalid(DiscardReason::InvalidEnvelope)),
150 Self::InvalidSecurityReport(_) => Some(Outcome::Invalid(DiscardReason::SecurityReport)),
151 Self::UnsupportedSecurityType => Some(Outcome::Filtered(FilterStatKey::InvalidCsp)),
152 Self::InvalidTransaction => Some(Outcome::Invalid(DiscardReason::InvalidTransaction)),
153 Self::DuplicateItem(_) => Some(Outcome::Invalid(DiscardReason::DuplicateItem)),
154 Self::NoEventPayload => Some(Outcome::Invalid(DiscardReason::NoEventPayload)),
155 Self::InvalidNintendoDyingMessage(_) => Some(Outcome::Invalid(DiscardReason::Payload)),
156 #[cfg(all(sentry, feature = "processing"))]
157 Self::InvalidPlaystationDump(_) => Some(Outcome::Invalid(DiscardReason::Payload)),
158 Self::InvalidUnrealReport(err) if err.kind() == Unreal4ErrorKind::BadCompression => {
159 Some(Outcome::Invalid(DiscardReason::InvalidCompression))
160 }
161 Self::InvalidUnrealReport(_) => Some(Outcome::Invalid(DiscardReason::ProcessUnreal)),
162 Self::SerializeFailed(_) | Self::ProcessingFailed(_) => {
163 Some(Outcome::Invalid(DiscardReason::Internal))
164 }
165 #[cfg(feature = "processing")]
166 Self::QuotasFailed(_) => Some(Outcome::Invalid(DiscardReason::Internal)),
167 Self::EventFiltered(key) => Some(Outcome::Filtered(key.clone())),
168
169 #[cfg(feature = "processing")]
170 Self::InvalidAttachmentRef => {
171 Some(Outcome::Invalid(DiscardReason::InvalidAttachmentRef))
172 }
173 }
174 }
175}
176
177impl From<Unreal4Error> for ProcessingError {
178 fn from(err: Unreal4Error) -> Self {
179 match err.kind() {
180 Unreal4ErrorKind::TooLarge => Self::PayloadTooLarge(ItemType::UnrealReport.into()),
181 _ => ProcessingError::InvalidUnrealReport(err),
182 }
183 }
184}
185
186#[derive(Debug)]
191pub struct ProcessingExtractedMetrics {
192 metrics: ExtractedMetrics,
193}
194
195impl ProcessingExtractedMetrics {
196 pub fn new() -> Self {
197 Self {
198 metrics: ExtractedMetrics::default(),
199 }
200 }
201
202 pub fn into_inner(self) -> ExtractedMetrics {
203 self.metrics
204 }
205
206 pub fn extend(
208 &mut self,
209 extracted: ExtractedMetrics,
210 sampling_decision: Option<SamplingDecision>,
211 ) {
212 self.extend_project_metrics(extracted.project_metrics, sampling_decision);
213 self.extend_sampling_metrics(extracted.sampling_metrics, sampling_decision);
214 }
215
216 pub fn extend_project_metrics<I>(
218 &mut self,
219 buckets: I,
220 sampling_decision: Option<SamplingDecision>,
221 ) where
222 I: IntoIterator<Item = Bucket>,
223 {
224 self.metrics
225 .project_metrics
226 .extend(buckets.into_iter().map(|mut bucket| {
227 bucket.metadata.extracted_from_indexed =
228 sampling_decision == Some(SamplingDecision::Keep);
229 bucket
230 }));
231 }
232
233 pub fn extend_sampling_metrics<I>(
235 &mut self,
236 buckets: I,
237 sampling_decision: Option<SamplingDecision>,
238 ) where
239 I: IntoIterator<Item = Bucket>,
240 {
241 self.metrics
242 .sampling_metrics
243 .extend(buckets.into_iter().map(|mut bucket| {
244 bucket.metadata.extracted_from_indexed =
245 sampling_decision == Some(SamplingDecision::Keep);
246 bucket
247 }));
248 }
249}
250
251fn send_metrics(
252 metrics: ExtractedMetrics,
253 project_key: ProjectKey,
254 sampling_key: Option<ProjectKey>,
255 aggregator: &Addr<Aggregator>,
256) {
257 let ExtractedMetrics {
258 project_metrics,
259 sampling_metrics,
260 } = metrics;
261
262 if !project_metrics.is_empty() {
263 aggregator.send(MergeBuckets {
264 project_key,
265 buckets: project_metrics,
266 });
267 }
268
269 if !sampling_metrics.is_empty() {
270 let sampling_project_key = sampling_key.unwrap_or(project_key);
277 aggregator.send(MergeBuckets {
278 project_key: sampling_project_key,
279 buckets: sampling_metrics,
280 });
281 }
282}
283
284#[derive(Debug)]
294pub struct ProcessEnvelope {
295 pub envelope: ManagedEnvelope,
297 pub project_info: Arc<ProjectInfo>,
299 pub rate_limits: Arc<RateLimits>,
301 pub sampling_project_info: Option<Arc<ProjectInfo>>,
303}
304
305#[derive(Debug)]
317pub struct ProcessMetrics {
318 pub data: MetricData,
320 pub project_key: ProjectKey,
322 pub source: BucketSource,
324 pub received_at: DateTime<Utc>,
326 pub sent_at: Option<DateTime<Utc>>,
329}
330
331#[derive(Debug)]
333pub enum MetricData {
334 Raw(Vec<Item>),
336 Parsed(Vec<Bucket>),
338}
339
340impl MetricData {
341 fn into_buckets(self, timestamp: UnixTimestamp) -> Vec<Bucket> {
346 let items = match self {
347 Self::Parsed(buckets) => return buckets,
348 Self::Raw(items) => items,
349 };
350
351 let mut buckets = Vec::new();
352 for item in items {
353 let payload = item.payload();
354 if item.ty() == &ItemType::Statsd {
355 for bucket_result in Bucket::parse_all(&payload, timestamp) {
356 match bucket_result {
357 Ok(bucket) => buckets.push(bucket),
358 Err(error) => relay_log::debug!(
359 error = &error as &dyn Error,
360 "failed to parse metric bucket from statsd format",
361 ),
362 }
363 }
364 } else if item.ty() == &ItemType::MetricBuckets {
365 match serde_json::from_slice::<Vec<Bucket>>(&payload) {
366 Ok(parsed_buckets) => {
367 if buckets.is_empty() {
369 buckets = parsed_buckets;
370 } else {
371 buckets.extend(parsed_buckets);
372 }
373 }
374 Err(error) => {
375 relay_log::debug!(
376 error = &error as &dyn Error,
377 "failed to parse metric bucket",
378 );
379 metric!(counter(RelayCounters::MetricBucketsParsingFailed) += 1);
380 }
381 }
382 } else {
383 relay_log::error!(
384 "invalid item of type {} passed to ProcessMetrics",
385 item.ty()
386 );
387 }
388 }
389 buckets
390 }
391}
392
393#[derive(Debug)]
394pub struct ProcessBatchedMetrics {
395 pub payload: Bytes,
397 pub source: BucketSource,
399 pub received_at: DateTime<Utc>,
401 pub sent_at: Option<DateTime<Utc>>,
403}
404
405#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
407pub enum BucketSource {
408 Internal,
414 External,
419}
420
421impl BucketSource {
422 pub fn from_meta(meta: &RequestMeta) -> Self {
424 match meta.request_trust() {
425 RequestTrust::Trusted => Self::Internal,
426 RequestTrust::Untrusted => Self::External,
427 }
428 }
429}
430
431#[derive(Debug)]
433pub struct SubmitClientReports {
434 pub client_reports: Vec<ClientReport>,
436 pub scoping: Scoping,
438}
439
440#[derive(Debug)]
442pub enum EnvelopeProcessor {
443 ProcessEnvelope(Box<ProcessEnvelope>),
444 ProcessProjectMetrics(Box<ProcessMetrics>),
445 ProcessBatchedMetrics(Box<ProcessBatchedMetrics>),
446 FlushBuckets(Box<FlushBuckets>),
447 SubmitClientReports(Box<SubmitClientReports>),
448}
449
450impl EnvelopeProcessor {
451 pub fn variant(&self) -> &'static str {
453 match self {
454 EnvelopeProcessor::ProcessEnvelope(_) => "ProcessEnvelope",
455 EnvelopeProcessor::ProcessProjectMetrics(_) => "ProcessProjectMetrics",
456 EnvelopeProcessor::ProcessBatchedMetrics(_) => "ProcessBatchedMetrics",
457 EnvelopeProcessor::FlushBuckets(_) => "FlushBuckets",
458 EnvelopeProcessor::SubmitClientReports(_) => "SubmitClientReports",
459 }
460 }
461}
462
463impl relay_system::Interface for EnvelopeProcessor {}
464
465impl FromMessage<ProcessEnvelope> for EnvelopeProcessor {
466 type Response = relay_system::NoResponse;
467
468 fn from_message(message: ProcessEnvelope, _sender: ()) -> Self {
469 Self::ProcessEnvelope(Box::new(message))
470 }
471}
472
473impl FromMessage<ProcessMetrics> for EnvelopeProcessor {
474 type Response = NoResponse;
475
476 fn from_message(message: ProcessMetrics, _: ()) -> Self {
477 Self::ProcessProjectMetrics(Box::new(message))
478 }
479}
480
481impl FromMessage<ProcessBatchedMetrics> for EnvelopeProcessor {
482 type Response = NoResponse;
483
484 fn from_message(message: ProcessBatchedMetrics, _: ()) -> Self {
485 Self::ProcessBatchedMetrics(Box::new(message))
486 }
487}
488
489impl FromMessage<FlushBuckets> for EnvelopeProcessor {
490 type Response = NoResponse;
491
492 fn from_message(message: FlushBuckets, _: ()) -> Self {
493 Self::FlushBuckets(Box::new(message))
494 }
495}
496
497impl FromMessage<SubmitClientReports> for EnvelopeProcessor {
498 type Response = NoResponse;
499
500 fn from_message(message: SubmitClientReports, _: ()) -> Self {
501 Self::SubmitClientReports(Box::new(message))
502 }
503}
504
505pub type EnvelopeProcessorServicePool = AsyncPool<BoxFuture<'static, ()>>;
507
508#[derive(Clone)]
512pub struct EnvelopeProcessorService {
513 inner: Arc<InnerProcessor>,
514}
515
516pub struct Addrs {
518 pub outcome_aggregator: Addr<TrackOutcome>,
519 pub upstream_relay: Addr<UpstreamRelay>,
520 #[cfg(feature = "processing")]
521 pub objectstore: Option<Addr<Objectstore>>,
522 #[cfg(feature = "processing")]
523 pub store_forwarder: Option<Addr<Store>>,
524 pub aggregator: Addr<Aggregator>,
525}
526
527impl Default for Addrs {
528 fn default() -> Self {
529 Addrs {
530 outcome_aggregator: Addr::dummy(),
531 upstream_relay: Addr::dummy(),
532 #[cfg(feature = "processing")]
533 objectstore: None,
534 #[cfg(feature = "processing")]
535 store_forwarder: None,
536 aggregator: Addr::dummy(),
537 }
538 }
539}
540
541struct InnerProcessor {
542 pool: EnvelopeProcessorServicePool,
543 config: Arc<Config>,
544 global_config: GlobalConfigHandle,
545 project_cache: ProjectCacheHandle,
546 cogs: Cogs,
547 addrs: Addrs,
548 #[cfg(feature = "processing")]
549 rate_limiter: Option<Arc<RedisRateLimiter>>,
550 metric_outcomes: MetricOutcomes,
551 processor: RelayProcessor,
552}
553
554impl EnvelopeProcessorService {
555 #[cfg_attr(feature = "processing", expect(clippy::too_many_arguments))]
557 pub fn new(
558 pool: EnvelopeProcessorServicePool,
559 config: Arc<Config>,
560 global_config: GlobalConfigHandle,
561 project_cache: ProjectCacheHandle,
562 cogs: Cogs,
563 #[cfg(feature = "processing")] redis: Option<RedisClients>,
564 addrs: Addrs,
565 metric_outcomes: MetricOutcomes,
566 ) -> Self {
567 let geoip_lookup = config
568 .geoip_path()
569 .and_then(
570 |p| match GeoIpLookup::open(p).context(ServiceError::GeoIp) {
571 Ok(geoip) => Some(geoip),
572 Err(err) => {
573 relay_log::error!("failed to open GeoIP db {p:?}: {err:?}");
574 None
575 }
576 },
577 )
578 .unwrap_or_else(GeoIpLookup::empty);
579
580 if let Some(build_epoch) = geoip_lookup.build_epoch() {
581 relay_log::info!("Loaded GeoIP database (build: {build_epoch})");
582 }
583
584 #[cfg(feature = "processing")]
585 let rate_limiter = redis.map(|redis| {
586 RedisRateLimiter::new(redis.quotas)
587 .max_limit(config.max_rate_limit())
588 .cache(config.quota_cache_ratio(), config.quota_cache_max())
589 });
590
591 let quota_limiter = Arc::new(QuotaRateLimiter::new(
592 #[cfg(feature = "processing")]
593 project_cache.clone(),
594 #[cfg(feature = "processing")]
595 rate_limiter.clone(),
596 ));
597 #[cfg(feature = "processing")]
598 let rate_limiter = rate_limiter.map(Arc::new);
599 let inner = InnerProcessor {
600 pool,
601 global_config,
602 project_cache,
603 #[cfg(feature = "processing")]
604 rate_limiter,
605 processor: RelayProcessor::new(
606 cogs.clone(),
607 "a_limiter,
608 &geoip_lookup,
609 addrs.outcome_aggregator.clone(),
610 ),
611 cogs,
612 addrs,
613 metric_outcomes,
614 config,
615 };
616
617 Self {
618 inner: Arc::new(inner),
619 }
620 }
621
622 async fn process_envelope(
623 &self,
624 project_id: ProjectId,
625 mut envelope: ManagedEnvelope,
626 ctx: processing::Context<'_>,
627 ) -> Vec<Output<Outputs>> {
628 if let Some(sampling_state) = ctx.sampling_project_info {
630 envelope
633 .envelope_mut()
634 .parametrize_dsc_transaction(&sampling_state.config.tx_name_rules);
635 }
636
637 if let Some(retention) = ctx.project_info.config.event_retention {
640 envelope.envelope_mut().set_retention(retention);
641 }
642
643 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 async fn process<'a>(
661 &self,
662 mut envelope: ManagedEnvelope,
663 ctx: processing::Context<'a>,
664 ) -> Vec<Output<Outputs>> {
665 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 cogs.cancel();
698
699 let global_config = self.inner.global_config.current().unwrap_or_default();
700
701 let ctx = processing::Context {
702 config: &self.inner.config,
703 global_config: &global_config,
704 project_info: &message.project_info,
705 sampling_project_info: message.sampling_project_info.as_deref(),
706 rate_limits: &message.rate_limits,
707 };
708
709 let project_key = message.envelope.meta().public_key();
710 let sampling_key = ctx
714 .sampling_project_info
715 .and_then(|p| p.get_public_key_config())
716 .map(|pkc| pkc.public_key);
717
718 relay_log::configure_scope(|scope| {
719 scope.set_tag("project_key", project_key);
720 if let Some(sampling_key) = sampling_key {
721 scope.set_tag("sampling_key", sampling_key);
722 }
723 let meta = message.envelope.envelope().meta();
724 scope.set_tag("sdk_name", meta.client_name());
725 if let Some(client) = meta.client() {
726 scope.set_tag("sdk", client);
727 }
728 if let Some(user_agent) = meta.user_agent() {
729 scope.set_extra("user_agent", user_agent.into());
730 }
731 });
732
733 let outputs = metric!(timer(RelayTimers::EnvelopeProcessingTime), {
734 self.process(message.envelope, ctx).await
735 });
736
737 let ctx = ctx.to_forward();
738 for Output { main, metrics } in outputs {
739 if let Some(metrics) = metrics {
740 let agg = &self.inner.addrs.aggregator;
741 metrics.accept(|metrics| {
742 send_metrics(metrics, project_key, sampling_key, agg);
743 });
744 }
745
746 if let Some(output) = main {
747 self.submit_upstream(&mut Token::noop(), output, ctx);
749 }
750 }
751 }
752
753 fn handle_process_metrics(&self, cogs: &mut Token, message: ProcessMetrics) {
754 let ProcessMetrics {
755 data,
756 project_key,
757 received_at,
758 sent_at,
759 source,
760 } = message;
761
762 let received_timestamp =
763 UnixTimestamp::from_datetime(received_at).unwrap_or(UnixTimestamp::now());
764
765 let mut buckets = data.into_buckets(received_timestamp);
766 if buckets.is_empty() {
767 return;
768 };
769 cogs.update(relay_metrics::cogs::BySize(&buckets));
770
771 let clock_drift_processor =
772 ClockDriftProcessor::new(sent_at, received_at).at_least(MINIMUM_CLOCK_DRIFT);
773
774 buckets.retain_mut(|bucket| {
775 if let Err(error) = relay_metrics::normalize_bucket(bucket) {
776 relay_log::debug!(error = &error as &dyn Error, "dropping bucket {bucket:?}");
777 return false;
778 }
779
780 if !self::metrics::is_valid_namespace(bucket, source) {
781 relay_log::debug!("dropping bucket in invalid namespace {bucket:?}");
782 return false;
783 }
784
785 clock_drift_processor.process_timestamp(&mut bucket.timestamp);
786
787 if !matches!(source, BucketSource::Internal) {
788 bucket.metadata = BucketMetadata::new(received_timestamp);
789 }
790
791 true
792 });
793
794 let project = self.inner.project_cache.get(project_key);
795
796 let buckets = match project.state() {
799 ProjectState::Enabled(project_info) => {
800 let rate_limits = project.rate_limits().current_limits();
801 self.check_buckets(project_key, project_info, &rate_limits, buckets)
802 }
803 _ => buckets,
804 };
805
806 relay_log::trace!("merging metric buckets into the aggregator");
807 self.inner
808 .addrs
809 .aggregator
810 .send(MergeBuckets::new(project_key, buckets));
811 }
812
813 fn handle_process_batched_metrics(&self, cogs: &mut Token, message: ProcessBatchedMetrics) {
814 let ProcessBatchedMetrics {
815 payload,
816 source,
817 received_at,
818 sent_at,
819 } = message;
820
821 #[derive(serde::Deserialize)]
822 struct Wrapper {
823 buckets: HashMap<ProjectKey, Vec<Bucket>>,
824 }
825
826 let buckets = match serde_json::from_slice(&payload) {
827 Ok(Wrapper { buckets }) => buckets,
828 Err(error) => {
829 relay_log::debug!(
830 error = &error as &dyn Error,
831 "failed to parse batched metrics",
832 );
833 metric!(counter(RelayCounters::MetricBucketsParsingFailed) += 1);
834 return;
835 }
836 };
837
838 for (project_key, buckets) in buckets {
839 self.handle_process_metrics(
840 cogs,
841 ProcessMetrics {
842 data: MetricData::Parsed(buckets),
843 project_key,
844 source,
845 received_at,
846 sent_at,
847 },
848 )
849 }
850 }
851
852 fn submit_upstream(
856 &self,
857 cogs: &mut Token,
858 output: Outputs,
859 ctx: processing::ForwardContext<'_>,
860 ) {
861 let _submit = cogs.start_category("submit");
862
863 #[cfg(feature = "processing")]
864 if ctx.config.processing_enabled()
865 && let Some(store_forwarder) = &self.inner.addrs.store_forwarder
866 {
867 use crate::processing::StoreHandle;
868
869 let objectstore = self.inner.addrs.objectstore.as_ref();
870 let handle = StoreHandle::new(store_forwarder, objectstore, ctx.global_config);
871
872 output
873 .forward_store(handle, ctx)
874 .unwrap_or_else(|err| err.into_inner());
875
876 return;
877 }
878
879 match output.serialize_envelope(ctx) {
880 Ok(envelope) => {
881 let envelope = ManagedEnvelope::from(envelope);
882 self.submit_envelope_upstream(envelope, ctx.project_info.upstream.clone());
883 }
884 Err(_) => relay_log::error!("failed to serialize output to an envelope"),
885 };
886 }
887
888 fn submit_envelope_upstream(
889 &self,
890 mut envelope: ManagedEnvelope,
891 upstream: Option<UpstreamDescriptor>,
894 ) {
895 if envelope.envelope_mut().is_empty() {
896 envelope.accept();
897 return;
898 }
899
900 if self.inner.config.processing_enabled() {
906 relay_log::error!(
907 "attempt to forward envelope to http upstream when processing is enabled"
908 );
909 return;
910 }
911
912 envelope.envelope_mut().set_sent_at(Utc::now());
918
919 relay_log::trace!("sending envelope to sentry endpoint");
920 let http_encoding = self.inner.config.http_encoding();
921 let result = envelope.envelope().to_vec().and_then(|v| {
922 encode_payload(&v.into(), http_encoding).map_err(EnvelopeError::PayloadIoFailed)
923 });
924
925 match result {
926 Ok(body) => {
927 self.inner
928 .addrs
929 .upstream_relay
930 .send(SendRequest(SendEnvelope {
931 upstream,
932 envelope,
933 body,
934 http_encoding,
935 project_cache: self.inner.project_cache.clone(),
936 }));
937 }
938 Err(error) => {
939 relay_log::error!(
942 error = &error as &dyn Error,
943 tags.project_key = %envelope.scoping().project_key,
944 "failed to serialize envelope payload"
945 );
946
947 envelope.reject(Outcome::Invalid(DiscardReason::Internal));
948 }
949 }
950 }
951
952 fn handle_submit_client_reports(&self, message: SubmitClientReports) {
953 let SubmitClientReports {
954 client_reports,
955 scoping,
956 } = message;
957
958 relay_log::trace!(
959 "sending {} client report(s) to project id {}",
960 client_reports.len(),
961 scoping.project_id
962 );
963
964 if client_reports.is_empty() {
965 return;
966 }
967
968 let upstream = self.inner.config.upstream();
969 let dsn = PartialDsn::outbound(&scoping, upstream);
970
971 let mut envelope = Envelope::from_request(None, RequestMeta::outbound(dsn));
972 for client_report in client_reports {
973 match client_report.serialize() {
974 Ok(payload) => {
975 let mut item = Item::new(ItemType::ClientReport);
976 item.set_payload(ContentType::Json, payload);
977 envelope.add_item(item);
978 }
979 Err(error) => {
980 relay_log::error!(
981 error = &error as &dyn std::error::Error,
982 "failed to serialize client report"
983 );
984 }
985 }
986 }
987
988 let envelope = ManagedEnvelope::new(envelope, self.inner.addrs.outcome_aggregator.clone());
989 self.submit_envelope_upstream(envelope, None);
990 }
991
992 fn check_buckets(
993 &self,
994 project_key: ProjectKey,
995 project_info: &ProjectInfo,
996 rate_limits: &RateLimits,
997 buckets: Vec<Bucket>,
998 ) -> Vec<Bucket> {
999 let Some(scoping) = project_info.scoping(project_key) else {
1000 relay_log::error!(
1001 tags.project_key = project_key.as_str(),
1002 "there is no scoping: dropping {} buckets",
1003 buckets.len(),
1004 );
1005 return Vec::new();
1006 };
1007
1008 let mut buckets =
1009 self::metrics::remove_invalid_namespaces(buckets, &self.inner.metric_outcomes, scoping);
1010
1011 let mut namespaces: BTreeSet<MetricNamespace> = buckets
1012 .iter()
1013 .filter_map(|bucket| bucket.name.try_namespace())
1014 .collect();
1015
1016 namespaces.remove(&MetricNamespace::Outcomes);
1018
1019 for namespace in namespaces {
1020 let limits = rate_limits
1021 .check_with_quotas(project_info.get_quotas(), scoping.metric_bucket(namespace));
1022
1023 if limits.is_limited() {
1024 let rejected;
1025 (buckets, rejected) = utils::split_off(buckets, |bucket| {
1026 bucket.name.try_namespace() == Some(namespace)
1027 });
1028
1029 let reason_code = limits.longest().and_then(|limit| limit.reason_code.clone());
1030 self.inner.metric_outcomes.track(
1031 scoping,
1032 &rejected,
1033 Outcome::RateLimited(reason_code),
1034 );
1035 }
1036 }
1037
1038 let quotas = project_info.config.quotas.clone();
1039 match MetricsLimiter::create(buckets, quotas, scoping) {
1040 Ok(mut bucket_limiter) => {
1041 bucket_limiter.enforce_limits(rate_limits, &self.inner.metric_outcomes);
1042 bucket_limiter.into_buckets()
1043 }
1044 Err(buckets) => buckets,
1045 }
1046 }
1047
1048 #[cfg(feature = "processing")]
1049 async fn rate_limit_buckets(
1050 &self,
1051 scoping: Scoping,
1052 project_info: &ProjectInfo,
1053 mut buckets: Vec<Bucket>,
1054 ) -> Vec<Bucket> {
1055 let Some(rate_limiter) = &self.inner.rate_limiter else {
1056 return buckets;
1057 };
1058
1059 let global_config = self.inner.global_config.current().unwrap_or_default();
1060 let mut namespaces = buckets
1061 .iter()
1062 .filter_map(|bucket| bucket.name.try_namespace())
1063 .counts();
1064
1065 namespaces.remove(&MetricNamespace::Outcomes);
1067
1068 let quotas = CombinedQuotas::new(&global_config, project_info.get_quotas());
1069
1070 for (namespace, quantity) in namespaces {
1071 let item_scoping = scoping.metric_bucket(namespace);
1072
1073 let limits = match rate_limiter
1074 .is_rate_limited(quotas, item_scoping, quantity, false)
1075 .await
1076 {
1077 Ok(limits) => limits,
1078 Err(err) => {
1079 relay_log::error!(
1080 error = &err as &dyn std::error::Error,
1081 "failed to check redis rate limits"
1082 );
1083 break;
1084 }
1085 };
1086
1087 if limits.is_limited() {
1088 let rejected;
1089 (buckets, rejected) = utils::split_off(buckets, |bucket| {
1090 bucket.name.try_namespace() == Some(namespace)
1091 });
1092
1093 let reason_code = limits.longest().and_then(|limit| limit.reason_code.clone());
1094 self.inner.metric_outcomes.track(
1095 scoping,
1096 &rejected,
1097 Outcome::RateLimited(reason_code),
1098 );
1099
1100 self.inner
1101 .project_cache
1102 .get(item_scoping.scoping.project_key)
1103 .rate_limits()
1104 .merge(limits);
1105 }
1106 }
1107
1108 match MetricsLimiter::create(buckets, project_info.config.quotas.clone(), scoping) {
1109 Err(buckets) => buckets,
1110 Ok(bucket_limiter) => self.apply_other_rate_limits(bucket_limiter).await,
1111 }
1112 }
1113
1114 #[cfg(feature = "processing")]
1116 async fn apply_other_rate_limits(&self, mut bucket_limiter: MetricsLimiter) -> Vec<Bucket> {
1117 relay_log::trace!("handle_rate_limit_buckets");
1118
1119 let scoping = *bucket_limiter.scoping();
1120
1121 if let Some(rate_limiter) = self.inner.rate_limiter.as_ref() {
1122 let global_config = self.inner.global_config.current().unwrap_or_default();
1123 let quotas = CombinedQuotas::new(&global_config, bucket_limiter.quotas());
1124
1125 let over_accept_once = true;
1128 let mut rate_limits = RateLimits::new();
1129
1130 let (category, count) = bucket_limiter.count();
1131
1132 let timer = Instant::now();
1133 let mut is_limited = false;
1134
1135 if let Some(count) = count {
1136 match rate_limiter
1137 .is_rate_limited(quotas, scoping.item(category), count, over_accept_once)
1138 .await
1139 {
1140 Ok(limits) => {
1141 is_limited = limits.is_limited();
1142 rate_limits.merge(limits)
1143 }
1144 Err(e) => {
1145 relay_log::error!(error = &e as &dyn Error, "rate limiting error")
1146 }
1147 }
1148 }
1149
1150 relay_statsd::metric!(
1151 timer(RelayTimers::RateLimitBucketsDuration) = timer.elapsed(),
1152 category = category.name(),
1153 limited = if is_limited { "true" } else { "false" },
1154 count = match count {
1155 None => "none",
1156 Some(0) => "0",
1157 Some(1) => "1",
1158 Some(1..=10) => "10",
1159 Some(1..=25) => "25",
1160 Some(1..=50) => "50",
1161 Some(51..=100) => "100",
1162 Some(101..=500) => "500",
1163 _ => "> 500",
1164 },
1165 );
1166
1167 if rate_limits.is_limited() {
1168 let was_enforced =
1169 bucket_limiter.enforce_limits(&rate_limits, &self.inner.metric_outcomes);
1170
1171 if was_enforced {
1172 self.inner
1174 .project_cache
1175 .get(scoping.project_key)
1176 .rate_limits()
1177 .merge(rate_limits);
1178 }
1179 }
1180 }
1181
1182 bucket_limiter.into_buckets()
1183 }
1184
1185 #[cfg(feature = "processing")]
1192 async fn encode_metrics_processing(
1193 &self,
1194 message: FlushBuckets,
1195 store_forwarder: &Addr<Store>,
1196 ) {
1197 use crate::constants::DEFAULT_EVENT_RETENTION;
1198 use crate::services::store::StoreMetrics;
1199
1200 for ProjectBuckets {
1201 buckets,
1202 scoping,
1203 project_info,
1204 ..
1205 } in message.buckets.into_values()
1206 {
1207 let mut buckets = self
1208 .rate_limit_buckets(scoping, &project_info, buckets)
1209 .await;
1210
1211 if buckets.is_empty() {
1212 continue;
1213 }
1214
1215 self.inner
1217 .metric_outcomes
1218 .track_accepted_outcome(scoping, &mut buckets);
1219
1220 let retention = project_info
1221 .config
1222 .event_retention
1223 .unwrap_or(DEFAULT_EVENT_RETENTION);
1224
1225 store_forwarder.send(StoreMetrics {
1228 buckets,
1229 scoping,
1230 retention,
1231 });
1232 }
1233 }
1234
1235 fn encode_metrics_envelope(&self, message: FlushBuckets) {
1245 let FlushBuckets {
1246 partition_key,
1247 buckets,
1248 } = message;
1249
1250 let batch_size = self.inner.config.metrics_max_batch_size_bytes();
1251 let upstream = self.inner.config.upstream();
1252
1253 for ProjectBuckets {
1254 buckets,
1255 scoping,
1256 project_info,
1257 ..
1258 } in buckets.values()
1259 {
1260 let dsn = PartialDsn::outbound(scoping, upstream);
1261
1262 relay_statsd::metric!(
1263 distribution(RelayDistributions::PartitionKeys) = u64::from(partition_key)
1264 );
1265
1266 let mut num_batches = 0;
1267 for batch in BucketsView::from(buckets).by_size(batch_size) {
1268 let mut envelope = Envelope::from_request(None, RequestMeta::outbound(dsn.clone()));
1269
1270 let mut item = Item::new(ItemType::MetricBuckets);
1271 item.set_source_quantities(crate::metrics::extract_quantities(batch));
1272 item.set_payload(ContentType::Json, serde_json::to_vec(&buckets).unwrap());
1273 envelope.add_item(item);
1274
1275 let mut envelope =
1276 ManagedEnvelope::new(envelope, self.inner.addrs.outcome_aggregator.clone());
1277 envelope
1278 .set_partition_key(Some(partition_key))
1279 .scope(*scoping);
1280
1281 relay_statsd::metric!(
1282 distribution(RelayDistributions::BucketsPerBatch) = batch.len() as u64
1283 );
1284
1285 self.submit_envelope_upstream(envelope, project_info.upstream.clone());
1286 num_batches += 1;
1287 }
1288
1289 relay_statsd::metric!(
1290 distribution(RelayDistributions::BatchesPerPartition) = num_batches
1291 );
1292 }
1293 }
1294
1295 fn send_global_partition(
1297 &self,
1298 upstream: Option<UpstreamDescriptor>,
1299 partition_key: u32,
1300 partition: &mut Partition<'_>,
1301 ) {
1302 if partition.is_empty() {
1303 return;
1304 }
1305
1306 let (unencoded, project_info) = partition.take();
1307 let http_encoding = self.inner.config.http_encoding();
1308 let encoded = match encode_payload(&unencoded, http_encoding) {
1309 Ok(payload) => payload,
1310 Err(error) => {
1311 let error = &error as &dyn std::error::Error;
1312 relay_log::error!(error, "failed to encode metrics payload");
1313 return;
1314 }
1315 };
1316
1317 let request = SendMetricsRequest {
1318 upstream,
1319 partition_key: partition_key.to_string(),
1320 unencoded,
1321 encoded,
1322 project_info,
1323 http_encoding,
1324 metric_outcomes: self.inner.metric_outcomes.clone(),
1325 };
1326
1327 self.inner.addrs.upstream_relay.send(SendRequest(request));
1328 }
1329
1330 fn encode_metrics_global(&self, message: FlushBuckets) {
1341 let FlushBuckets {
1342 partition_key,
1343 buckets,
1344 } = message;
1345
1346 let batch_size = self.inner.config.metrics_max_batch_size_bytes();
1347 let mut partitions = BTreeMap::new();
1348 let mut partition_splits = 0;
1349
1350 for ProjectBuckets {
1351 buckets,
1352 scoping,
1353 project_info,
1354 ..
1355 } in buckets.values()
1356 {
1357 let partition = match partitions.get_mut(&project_info.upstream) {
1358 Some(partition) => partition,
1359 None => partitions
1360 .entry(project_info.upstream.clone())
1361 .or_insert_with(|| Partition::new(batch_size)),
1362 };
1363
1364 for bucket in buckets {
1365 let mut remaining = Some(BucketView::new(bucket));
1366
1367 while let Some(bucket) = remaining.take() {
1368 if let Some(next) = partition.insert(bucket, *scoping) {
1369 self.send_global_partition(
1373 project_info.upstream.clone(),
1374 partition_key,
1375 partition,
1376 );
1377 remaining = Some(next);
1378 partition_splits += 1;
1379 }
1380 }
1381 }
1382 }
1383
1384 if partition_splits > 0 {
1385 metric!(distribution(RelayDistributions::PartitionSplits) = partition_splits);
1386 }
1387
1388 for (upstream, mut partition) in partitions {
1389 self.send_global_partition(upstream, partition_key, &mut partition);
1390 }
1391 }
1392
1393 fn encode_metrics_client_reports(&self, mut message: FlushBuckets) -> FlushBuckets {
1397 for ProjectBuckets {
1398 buckets, scoping, ..
1399 } in message.buckets.values_mut()
1400 {
1401 let client_reports = outcome::metric::extract_client_reports(buckets).collect();
1402
1403 self.handle_submit_client_reports(SubmitClientReports {
1404 client_reports,
1405 scoping: *scoping,
1406 });
1407 }
1408
1409 message
1410 }
1411
1412 async fn handle_flush_buckets(&self, mut message: FlushBuckets) {
1413 for (project_key, pb) in message.buckets.iter_mut() {
1414 let buckets = std::mem::take(&mut pb.buckets);
1415 pb.buckets =
1416 self.check_buckets(*project_key, &pb.project_info, &pb.rate_limits, buckets);
1417 }
1418
1419 #[cfg(feature = "processing")]
1420 if self.inner.config.processing_enabled()
1421 && let Some(ref store_forwarder) = self.inner.addrs.store_forwarder
1422 {
1423 return self
1424 .encode_metrics_processing(message, store_forwarder)
1425 .await;
1426 }
1427
1428 if self.inner.config.emit_outcomes() == EmitOutcomes::AsClientReports {
1431 message = self.encode_metrics_client_reports(message);
1434 }
1435
1436 if self.inner.config.http_global_metrics() {
1437 self.encode_metrics_global(message)
1438 } else {
1439 self.encode_metrics_envelope(message)
1440 }
1441 }
1442
1443 #[cfg(all(test, feature = "processing"))]
1444 fn redis_rate_limiter_enabled(&self) -> bool {
1445 self.inner.rate_limiter.is_some()
1446 }
1447
1448 async fn handle_message(self, message: EnvelopeProcessor) {
1449 let ty = message.variant();
1450 let feature_weights = self.feature_weights(&message);
1451
1452 metric!(timer(RelayTimers::ProcessMessageDuration), message = ty, {
1453 let mut cogs = self.inner.cogs.timed(ResourceId::Relay, feature_weights);
1454
1455 match message {
1456 EnvelopeProcessor::ProcessEnvelope(m) => {
1457 self.handle_process_envelope(&mut cogs, *m).await
1458 }
1459 EnvelopeProcessor::ProcessProjectMetrics(m) => {
1460 self.handle_process_metrics(&mut cogs, *m)
1461 }
1462 EnvelopeProcessor::ProcessBatchedMetrics(m) => {
1463 self.handle_process_batched_metrics(&mut cogs, *m)
1464 }
1465 EnvelopeProcessor::FlushBuckets(m) => self.handle_flush_buckets(*m).await,
1466 EnvelopeProcessor::SubmitClientReports(m) => self.handle_submit_client_reports(*m),
1467 }
1468 });
1469 }
1470
1471 fn feature_weights(&self, message: &EnvelopeProcessor) -> FeatureWeights {
1472 match message {
1473 EnvelopeProcessor::ProcessEnvelope(_) => AppFeature::Unattributed.into(),
1475 EnvelopeProcessor::ProcessProjectMetrics(_) => AppFeature::Unattributed.into(),
1476 EnvelopeProcessor::ProcessBatchedMetrics(_) => AppFeature::Unattributed.into(),
1477 EnvelopeProcessor::FlushBuckets(v) => v
1478 .buckets
1479 .values()
1480 .map(|s| {
1481 if self.inner.config.processing_enabled() {
1482 relay_metrics::cogs::ByCount(&s.buckets).into()
1485 } else {
1486 relay_metrics::cogs::BySize(&s.buckets).into()
1487 }
1488 })
1489 .fold(FeatureWeights::none(), FeatureWeights::merge),
1490 EnvelopeProcessor::SubmitClientReports(_) => AppFeature::ClientReports.into(),
1491 }
1492 }
1493}
1494
1495impl Service for EnvelopeProcessorService {
1496 type Interface = EnvelopeProcessor;
1497
1498 async fn run(self, mut rx: relay_system::Receiver<Self::Interface>) {
1499 while let Some(message) = rx.recv().await {
1500 let service = self.clone();
1501 let hub = relay_log::Hub::new_from_top(relay_log::Hub::current());
1503
1504 self.inner
1505 .pool
1506 .spawn_async(Box::pin(service.handle_message(message).bind_hub(hub)))
1507 .await;
1508 }
1509 }
1510}
1511
1512pub fn encode_payload(body: &Bytes, http_encoding: HttpEncoding) -> Result<Bytes, std::io::Error> {
1513 let envelope_body: Vec<u8> = match http_encoding {
1514 HttpEncoding::Identity => return Ok(body.clone()),
1515 HttpEncoding::Deflate => {
1516 let mut encoder = ZlibEncoder::new(Vec::new(), Compression::default());
1517 encoder.write_all(body.as_ref())?;
1518 encoder.finish()?
1519 }
1520 HttpEncoding::Gzip => {
1521 let mut encoder = GzEncoder::new(Vec::new(), Compression::default());
1522 encoder.write_all(body.as_ref())?;
1523 encoder.finish()?
1524 }
1525 HttpEncoding::Br => {
1526 let mut encoder = BrotliEncoder::new(Vec::new(), 0, 5, 22);
1528 encoder.write_all(body.as_ref())?;
1529 encoder.into_inner()
1530 }
1531 HttpEncoding::Zstd => {
1532 let mut encoder = ZstdEncoder::new(Vec::new(), 1)?;
1535 encoder.write_all(body.as_ref())?;
1536 encoder.finish()?
1537 }
1538 };
1539
1540 Ok(envelope_body.into())
1541}
1542
1543#[derive(Debug)]
1545pub struct SendEnvelope {
1546 pub upstream: Option<UpstreamDescriptor>,
1547 pub envelope: ManagedEnvelope,
1548 pub body: Bytes,
1549 pub http_encoding: HttpEncoding,
1550 pub project_cache: ProjectCacheHandle,
1551}
1552
1553impl UpstreamRequest for SendEnvelope {
1554 fn upstream(&self) -> Option<&UpstreamDescriptor> {
1555 self.upstream.as_ref()
1556 }
1557
1558 fn method(&self) -> reqwest::Method {
1559 reqwest::Method::POST
1560 }
1561
1562 fn path(&self) -> Cow<'_, str> {
1563 format!("/api/{}/envelope/", self.envelope.scoping().project_id).into()
1564 }
1565
1566 fn route(&self) -> &'static str {
1567 "envelope"
1568 }
1569
1570 fn build(&mut self, builder: &mut http::RequestBuilder) -> Result<(), http::HttpError> {
1571 let envelope_body = self.body.clone();
1572
1573 let meta = &self.envelope.meta();
1574 let shard = self.envelope.partition_key().map(|p| p.to_string());
1575 builder
1576 .content_encoding(self.http_encoding)
1577 .header_opt("Origin", meta.origin().map(|url| url.as_str()))
1578 .header_opt("User-Agent", meta.user_agent())
1579 .header("X-Sentry-Auth", meta.auth_header())
1580 .header("X-Forwarded-For", meta.forwarded_for())
1581 .header("Content-Type", envelope::CONTENT_TYPE)
1582 .header_opt("X-Sentry-Relay-Shard", shard)
1583 .body(envelope_body);
1584
1585 Ok(())
1586 }
1587
1588 fn sign(&mut self) -> Option<Sign> {
1589 Some(Sign::Optional(SignatureType::RequestSign))
1590 }
1591
1592 fn respond(
1593 self: Box<Self>,
1594 result: Result<http::Response, UpstreamRequestError>,
1595 ) -> Pin<Box<dyn Future<Output = ()> + Send + Sync>> {
1596 Box::pin(async move {
1597 let result = match result {
1598 Ok(mut response) => response.consume().await.map_err(UpstreamRequestError::Http),
1599 Err(error) => Err(error),
1600 };
1601
1602 match result {
1603 Ok(()) => self.envelope.accept(),
1604 Err(error) if error.is_received() => {
1605 let scoping = self.envelope.scoping();
1606 self.envelope.accept();
1607
1608 if let UpstreamRequestError::RateLimited(limits) = error {
1609 self.project_cache
1610 .get(scoping.project_key)
1611 .rate_limits()
1612 .merge(limits.scope(&scoping));
1613 }
1614 }
1615 Err(error) => {
1616 let mut envelope = self.envelope;
1619 envelope.reject(Outcome::Invalid(DiscardReason::Internal));
1620 relay_log::error!(
1621 error = &error as &dyn Error,
1622 tags.project_key = %envelope.scoping().project_key,
1623 "error sending envelope"
1624 );
1625 }
1626 }
1627 })
1628 }
1629}
1630
1631#[derive(Debug)]
1638struct Partition<'a> {
1639 max_size: usize,
1640 remaining: usize,
1641 views: HashMap<ProjectKey, Vec<BucketView<'a>>>,
1642 project_info: HashMap<ProjectKey, Scoping>,
1643}
1644
1645impl<'a> Partition<'a> {
1646 pub fn new(size: usize) -> Self {
1648 Self {
1649 max_size: size,
1650 remaining: size,
1651 views: HashMap::new(),
1652 project_info: HashMap::new(),
1653 }
1654 }
1655
1656 pub fn insert(&mut self, bucket: BucketView<'a>, scoping: Scoping) -> Option<BucketView<'a>> {
1667 let (current, next) = bucket.split(self.remaining, Some(self.max_size));
1668
1669 if let Some(current) = current {
1670 self.remaining = self.remaining.saturating_sub(current.estimated_size());
1671 self.views
1672 .entry(scoping.project_key)
1673 .or_default()
1674 .push(current);
1675
1676 self.project_info
1677 .entry(scoping.project_key)
1678 .or_insert(scoping);
1679 }
1680
1681 next
1682 }
1683
1684 fn is_empty(&self) -> bool {
1686 self.views.is_empty()
1687 }
1688
1689 fn take(&mut self) -> (Bytes, HashMap<ProjectKey, Scoping>) {
1693 #[derive(serde::Serialize)]
1694 struct Wrapper<'a> {
1695 buckets: &'a HashMap<ProjectKey, Vec<BucketView<'a>>>,
1696 }
1697
1698 let buckets = &self.views;
1699 let payload = serde_json::to_vec(&Wrapper { buckets }).unwrap().into();
1700
1701 let scopings = std::mem::take(&mut self.project_info);
1702
1703 self.views.clear();
1704 self.remaining = self.max_size;
1705
1706 (payload, scopings)
1707 }
1708}
1709
1710#[derive(Debug)]
1714struct SendMetricsRequest {
1715 upstream: Option<UpstreamDescriptor>,
1717 partition_key: String,
1719 unencoded: Bytes,
1721 encoded: Bytes,
1723 project_info: HashMap<ProjectKey, Scoping>,
1727 http_encoding: HttpEncoding,
1729 metric_outcomes: MetricOutcomes,
1731}
1732
1733impl SendMetricsRequest {
1734 fn create_error_outcomes(self) {
1735 #[derive(serde::Deserialize)]
1736 struct Wrapper {
1737 buckets: HashMap<ProjectKey, Vec<MinimalTrackableBucket>>,
1738 }
1739
1740 let buckets = match serde_json::from_slice(&self.unencoded) {
1741 Ok(Wrapper { buckets }) => buckets,
1742 Err(err) => {
1743 relay_log::error!(
1744 error = &err as &dyn std::error::Error,
1745 "failed to parse buckets from failed transmission"
1746 );
1747 return;
1748 }
1749 };
1750
1751 for (key, buckets) in buckets {
1752 let Some(&scoping) = self.project_info.get(&key) else {
1753 relay_log::error!("missing scoping for project key");
1754 continue;
1755 };
1756
1757 self.metric_outcomes.track(
1758 scoping,
1759 &buckets,
1760 Outcome::Invalid(DiscardReason::Internal),
1761 );
1762 }
1763 }
1764}
1765
1766impl UpstreamRequest for SendMetricsRequest {
1767 fn upstream(&self) -> Option<&UpstreamDescriptor> {
1768 self.upstream.as_ref()
1769 }
1770
1771 fn set_relay_id(&self) -> bool {
1772 true
1773 }
1774
1775 fn sign(&mut self) -> Option<Sign> {
1776 Some(Sign::Required(SignatureType::Body(self.unencoded.clone())))
1777 }
1778
1779 fn method(&self) -> reqwest::Method {
1780 reqwest::Method::POST
1781 }
1782
1783 fn path(&self) -> Cow<'_, str> {
1784 "/api/0/relays/metrics/".into()
1785 }
1786
1787 fn route(&self) -> &'static str {
1788 "global_metrics"
1789 }
1790
1791 fn build(&mut self, builder: &mut http::RequestBuilder) -> Result<(), http::HttpError> {
1792 builder
1793 .content_encoding(self.http_encoding)
1794 .header("X-Sentry-Relay-Shard", self.partition_key.as_bytes())
1795 .header(header::CONTENT_TYPE, b"application/json")
1796 .body(self.encoded.clone());
1797
1798 Ok(())
1799 }
1800
1801 fn respond(
1802 self: Box<Self>,
1803 result: Result<http::Response, UpstreamRequestError>,
1804 ) -> Pin<Box<dyn Future<Output = ()> + Send + Sync>> {
1805 Box::pin(async {
1806 match result {
1807 Ok(mut response) => {
1808 response.consume().await.ok();
1809 }
1810 Err(error) => {
1811 relay_log::error!(error = &error as &dyn Error, "Failed to send metrics batch");
1812
1813 if error.is_received() {
1816 return;
1817 }
1818
1819 self.create_error_outcomes()
1820 }
1821 }
1822 })
1823 }
1824}
1825
1826#[derive(Copy, Clone, Debug)]
1828#[cfg(feature = "processing")]
1829struct CombinedQuotas<'a> {
1830 global_quotas: &'a [Quota],
1831 project_quotas: &'a [Quota],
1832}
1833
1834#[cfg(feature = "processing")]
1835impl<'a> CombinedQuotas<'a> {
1836 pub fn new(global_config: &'a GlobalConfig, project_quotas: &'a [Quota]) -> Self {
1838 Self {
1839 global_quotas: &global_config.quotas,
1840 project_quotas,
1841 }
1842 }
1843}
1844
1845#[cfg(feature = "processing")]
1846impl<'a> IntoIterator for CombinedQuotas<'a> {
1847 type Item = &'a Quota;
1848 type IntoIter = std::iter::Chain<std::slice::Iter<'a, Quota>, std::slice::Iter<'a, Quota>>;
1849
1850 fn into_iter(self) -> Self::IntoIter {
1851 self.global_quotas.iter().chain(self.project_quotas.iter())
1852 }
1853}
1854
1855#[cfg(test)]
1856mod tests {
1857 use insta::assert_debug_snapshot;
1858 use relay_common::glob2::LazyGlob;
1859 use relay_dynamic_config::ProjectConfig;
1860 use relay_event_normalization::{
1861 NormalizationConfig, RedactionRule, TransactionNameConfig, TransactionNameRule,
1862 };
1863 use relay_event_schema::protocol::{Event, EventId, TransactionSource};
1864 use relay_pii::DataScrubbingConfig;
1865 use relay_protocol::Annotated;
1866 #[cfg(feature = "processing")]
1867 use relay_quotas::DataCategory;
1868 use similar_asserts::assert_eq;
1869
1870 use crate::testutils::{create_test_processor, create_test_processor_with_addrs};
1871
1872 #[cfg(feature = "processing")]
1873 use {
1874 relay_metrics::BucketValue,
1875 relay_quotas::{QuotaScope, ReasonCode},
1876 relay_test::mock_service,
1877 };
1878
1879 use super::*;
1880
1881 async fn process_to_single_envelope<'a>(
1882 processor: &EnvelopeProcessorService,
1883 envelope: ManagedEnvelope,
1884 ctx: processing::Context<'a>,
1885 ) -> Box<Envelope> {
1886 let mut outputs = processor.process(envelope, ctx).await;
1887 assert_eq!(outputs.len(), 1);
1888
1889 let Output { main, metrics } = outputs.pop().unwrap();
1890
1891 if let Some(metrics) = metrics {
1892 metrics.accept(drop);
1893 }
1894
1895 main.unwrap()
1896 .serialize_envelope(ctx.to_forward())
1897 .unwrap()
1898 .accept(|envelope| envelope)
1899 }
1900
1901 #[cfg(feature = "processing")]
1902 fn mock_quota(id: &str) -> Quota {
1903 Quota {
1904 id: Some(id.into()),
1905 categories: [DataCategory::MetricBucket].into(),
1906 scope: QuotaScope::Organization,
1907 scope_id: None,
1908 limit: Some(0),
1909 window: None,
1910 reason_code: None,
1911 namespace: None,
1912 }
1913 }
1914
1915 #[cfg(feature = "processing")]
1916 #[test]
1917 fn test_dynamic_quotas() {
1918 let global_config = relay_dynamic_config::GlobalConfig {
1919 quotas: vec![mock_quota("foo"), mock_quota("bar")],
1920 ..Default::default()
1921 };
1922
1923 let project_quotas = vec![mock_quota("baz"), mock_quota("qux")];
1924
1925 let dynamic_quotas = CombinedQuotas::new(&global_config, &project_quotas);
1926
1927 let quota_ids = dynamic_quotas.into_iter().filter_map(|q| q.id.as_deref());
1928 assert!(quota_ids.eq(["foo", "bar", "baz", "qux"]));
1929 }
1930
1931 #[cfg(feature = "processing")]
1934 #[tokio::test]
1935 async fn test_ratelimit_per_batch() {
1936 use relay_base_schema::organization::OrganizationId;
1937 use relay_protocol::FiniteF64;
1938
1939 let rate_limited_org = Scoping {
1940 organization_id: OrganizationId::new(1),
1941 project_id: ProjectId::new(21),
1942 project_key: ProjectKey::parse("00000000000000000000000000000000").unwrap(),
1943 key_id: Some(17),
1944 };
1945
1946 let not_rate_limited_org = Scoping {
1947 organization_id: OrganizationId::new(2),
1948 project_id: ProjectId::new(21),
1949 project_key: ProjectKey::parse("11111111111111111111111111111111").unwrap(),
1950 key_id: Some(17),
1951 };
1952
1953 let message = {
1954 let project_info = {
1955 let quota = Quota {
1956 id: Some("testing".into()),
1957 categories: [DataCategory::MetricBucket].into(),
1958 scope: relay_quotas::QuotaScope::Organization,
1959 scope_id: Some(rate_limited_org.organization_id.to_string().into()),
1960 limit: Some(0),
1961 window: None,
1962 reason_code: Some(ReasonCode::new("test")),
1963 namespace: None,
1964 };
1965
1966 let mut config = ProjectConfig::default();
1967 config.quotas.push(quota);
1968
1969 Arc::new(ProjectInfo {
1970 config,
1971 ..Default::default()
1972 })
1973 };
1974
1975 let project_metrics = |scoping| ProjectBuckets {
1976 buckets: vec![Bucket {
1977 name: "d:spans/bar".into(),
1978 value: BucketValue::Counter(FiniteF64::new(1.0).unwrap()),
1979 timestamp: UnixTimestamp::now(),
1980 tags: Default::default(),
1981 width: 10,
1982 metadata: BucketMetadata::default(),
1983 }],
1984 rate_limits: Default::default(),
1985 project_info: project_info.clone(),
1986 scoping,
1987 };
1988
1989 let buckets = hashbrown::HashMap::from([
1990 (
1991 rate_limited_org.project_key,
1992 project_metrics(rate_limited_org),
1993 ),
1994 (
1995 not_rate_limited_org.project_key,
1996 project_metrics(not_rate_limited_org),
1997 ),
1998 ]);
1999
2000 FlushBuckets {
2001 partition_key: 0,
2002 buckets,
2003 }
2004 };
2005
2006 assert_eq!(message.buckets.keys().count(), 2);
2008
2009 let config = {
2010 let config_json = serde_json::json!({
2011 "processing": {
2012 "enabled": true,
2013 "kafka_config": [],
2014 "redis": {
2015 "server": std::env::var("RELAY_REDIS_URL").unwrap_or_else(|_| "redis://127.0.0.1:6379".to_owned()),
2016 }
2017 }
2018 });
2019 Config::from_json_value(config_json).unwrap()
2020 };
2021
2022 let (store, handle) = {
2023 let f = |org_ids: &mut Vec<OrganizationId>, msg: Store| {
2024 let org_id = match msg {
2025 Store::Metrics(x) => x.scoping.organization_id,
2026 _ => panic!("received envelope when expecting only metrics"),
2027 };
2028 org_ids.push(org_id);
2029 };
2030
2031 mock_service("store_forwarder", vec![], f)
2032 };
2033
2034 let processor = create_test_processor(config).await;
2035 assert!(processor.redis_rate_limiter_enabled());
2036
2037 processor.encode_metrics_processing(message, &store).await;
2038
2039 drop(store);
2040 let orgs_not_ratelimited = handle.await.unwrap();
2041
2042 assert_eq!(
2043 orgs_not_ratelimited,
2044 vec![not_rate_limited_org.organization_id]
2045 );
2046 }
2047
2048 #[tokio::test]
2049 async fn test_browser_version_extraction_with_pii_like_data() {
2050 let processor = create_test_processor(Default::default()).await;
2051 let outcome_aggregator = Addr::dummy();
2052 let event_id = EventId::new();
2053
2054 let dsn = "https://e12d836b15bb49d7bbf99e64295d995b:@sentry.io/42"
2055 .parse()
2056 .unwrap();
2057
2058 let request_meta = RequestMeta::new(dsn);
2059 let mut envelope = Envelope::from_request(Some(event_id), request_meta);
2060
2061 envelope.add_item({
2062 let mut item = Item::new(ItemType::Event);
2063 item.set_payload(
2064 ContentType::Json,
2065 r#"
2066 {
2067 "request": {
2068 "headers": [
2069 ["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"]
2070 ]
2071 }
2072 }
2073 "#,
2074 );
2075 item
2076 });
2077
2078 let mut datascrubbing_settings = DataScrubbingConfig::default();
2079 datascrubbing_settings.scrub_data = true;
2081 datascrubbing_settings.scrub_defaults = true;
2082 datascrubbing_settings.scrub_ip_addresses = true;
2083
2084 let pii_config = serde_json::from_str(r#"{"applications": {"**": ["@ip:mask"]}}"#).unwrap();
2086
2087 let config = ProjectConfig {
2088 datascrubbing_settings,
2089 pii_config: Some(pii_config),
2090 ..Default::default()
2091 };
2092
2093 let project_info = ProjectInfo {
2094 config,
2095 ..Default::default()
2096 };
2097
2098 let envelope = ManagedEnvelope::new(envelope, outcome_aggregator);
2099
2100 let ctx = processing::Context {
2101 project_info: &project_info,
2102 ..processing::Context::for_test()
2103 };
2104
2105 let new_envelope = process_to_single_envelope(&processor, envelope, ctx).await;
2106
2107 let event_item = new_envelope.items().last().unwrap();
2108 let annotated_event: Annotated<Event> =
2109 Annotated::from_json_bytes(&event_item.payload()).unwrap();
2110 let event = annotated_event.into_value().unwrap();
2111 let headers = event
2112 .request
2113 .into_value()
2114 .unwrap()
2115 .headers
2116 .into_value()
2117 .unwrap();
2118
2119 assert_eq!(
2121 Some(
2122 "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/********* Safari/537.36"
2123 ),
2124 headers.get_header("User-Agent")
2125 );
2126 let contexts = event.contexts.into_value().unwrap();
2128 let browser = contexts.0.get("browser").unwrap();
2129 assert_eq!(
2130 r#"{"browser":"Chrome 103.0.0","name":"Chrome","version":"103.0.0","type":"browser"}"#,
2131 browser.to_json().unwrap()
2132 );
2133 }
2134
2135 #[tokio::test]
2136 #[cfg(feature = "processing")]
2137 async fn test_materialize_dsc() {
2138 use crate::services::projects::project::PublicKeyConfig;
2139
2140 let dsn = "https://e12d836b15bb49d7bbf99e64295d995b:@sentry.io/42"
2141 .parse()
2142 .unwrap();
2143 let request_meta = RequestMeta::new(dsn);
2144 let mut envelope = Envelope::from_request(None, request_meta);
2145
2146 let dsc = r#"{
2147 "trace_id": "00000000-0000-0000-0000-000000000001",
2148 "public_key": "e12d836b15bb49d7bbf99e64295d995b",
2149 "sample_rate": "0.2"
2150 }"#;
2151 envelope.set_dsc(serde_json::from_str(dsc).unwrap());
2152
2153 let mut item = Item::new(ItemType::Event);
2154 item.set_payload(ContentType::Json, r#"{}"#);
2155 envelope.add_item(item);
2156
2157 let outcome_aggregator = Addr::dummy();
2158 let managed_envelope = ManagedEnvelope::new(envelope, outcome_aggregator);
2159
2160 let mut project_info = ProjectInfo::default();
2161 project_info.public_keys.push(PublicKeyConfig {
2162 public_key: ProjectKey::parse("e12d836b15bb49d7bbf99e64295d995b").unwrap(),
2163 numeric_id: Some(1),
2164 });
2165
2166 let config = serde_json::json!({
2167 "processing": {
2168 "enabled": true,
2169 "kafka_config": [],
2170 }
2171 });
2172
2173 let processor =
2174 create_test_processor(Config::from_json_value(config.clone()).unwrap()).await;
2175 let config = Config::from_json_value(config).unwrap();
2176 let ctx = processing::Context {
2177 config: &config,
2178 project_info: &project_info,
2179 sampling_project_info: Some(&project_info),
2180 ..processing::Context::for_test()
2181 };
2182
2183 let envelope = process_to_single_envelope(&processor, managed_envelope, ctx).await;
2184 let event = envelope
2185 .get_item_by(|item| item.ty() == &ItemType::Event)
2186 .unwrap();
2187
2188 let event = Annotated::<Event>::from_json_bytes(&event.payload()).unwrap();
2189 insta::assert_debug_snapshot!(event.value().unwrap()._dsc, @r###"
2190 Object(
2191 {
2192 "environment": ~,
2193 "public_key": String(
2194 "e12d836b15bb49d7bbf99e64295d995b",
2195 ),
2196 "release": ~,
2197 "replay_id": ~,
2198 "sample_rate": String(
2199 "0.2",
2200 ),
2201 "trace_id": String(
2202 "00000000000000000000000000000001",
2203 ),
2204 "transaction": ~,
2205 },
2206 )
2207 "###);
2208 }
2209
2210 fn capture_test_event(transaction_name: &str, source: TransactionSource) -> Vec<String> {
2211 let mut event = Annotated::<Event>::from_json(
2212 r#"
2213 {
2214 "type": "transaction",
2215 "transaction": "/foo/",
2216 "timestamp": 946684810.0,
2217 "start_timestamp": 946684800.0,
2218 "contexts": {
2219 "trace": {
2220 "trace_id": "4c79f60c11214eb38604f4ae0781bfb2",
2221 "span_id": "fa90fdead5f74053",
2222 "op": "http.server",
2223 "type": "trace"
2224 }
2225 },
2226 "transaction_info": {
2227 "source": "url"
2228 }
2229 }
2230 "#,
2231 )
2232 .unwrap();
2233 let e = event.value_mut().as_mut().unwrap();
2234 e.transaction.set_value(Some(transaction_name.into()));
2235
2236 e.transaction_info
2237 .value_mut()
2238 .as_mut()
2239 .unwrap()
2240 .source
2241 .set_value(Some(source));
2242
2243 relay_statsd::with_capturing_test_client(|| {
2244 utils::log_transaction_name_metrics(&mut event, |event| {
2245 let config = NormalizationConfig {
2246 transaction_name_config: TransactionNameConfig {
2247 rules: &[TransactionNameRule {
2248 pattern: LazyGlob::new("/foo/*/**".to_owned()),
2249 expiry: DateTime::<Utc>::MAX_UTC,
2250 redaction: RedactionRule::Replace {
2251 substitution: "*".to_owned(),
2252 },
2253 }],
2254 },
2255 ..Default::default()
2256 };
2257 relay_event_normalization::normalize_event(event, &config)
2258 });
2259 })
2260 }
2261
2262 #[test]
2263 fn test_log_transaction_metrics_none() {
2264 let captures = capture_test_event("/nothing", TransactionSource::Url);
2265 insta::assert_debug_snapshot!(captures, @r###"
2266 [
2267 "event.transaction_name_changes:1|c|#source_in:url,changes:none,source_out:sanitized,is_404:false",
2268 ]
2269 "###);
2270 }
2271
2272 #[test]
2273 fn test_log_transaction_metrics_rule() {
2274 let captures = capture_test_event("/foo/john/denver", TransactionSource::Url);
2275 insta::assert_debug_snapshot!(captures, @r###"
2276 [
2277 "event.transaction_name_changes:1|c|#source_in:url,changes:rule,source_out:sanitized,is_404:false",
2278 ]
2279 "###);
2280 }
2281
2282 #[test]
2283 fn test_log_transaction_metrics_pattern() {
2284 let captures = capture_test_event("/something/12345", TransactionSource::Url);
2285 insta::assert_debug_snapshot!(captures, @r###"
2286 [
2287 "event.transaction_name_changes:1|c|#source_in:url,changes:pattern,source_out:sanitized,is_404:false",
2288 ]
2289 "###);
2290 }
2291
2292 #[test]
2293 fn test_log_transaction_metrics_both() {
2294 let captures = capture_test_event("/foo/john/12345", TransactionSource::Url);
2295 insta::assert_debug_snapshot!(captures, @r###"
2296 [
2297 "event.transaction_name_changes:1|c|#source_in:url,changes:both,source_out:sanitized,is_404:false",
2298 ]
2299 "###);
2300 }
2301
2302 #[test]
2303 fn test_log_transaction_metrics_no_match() {
2304 let captures = capture_test_event("/foo/john/12345", TransactionSource::Route);
2305 insta::assert_debug_snapshot!(captures, @r###"
2306 [
2307 "event.transaction_name_changes:1|c|#source_in:route,changes:none,source_out:route,is_404:false",
2308 ]
2309 "###);
2310 }
2311
2312 #[tokio::test]
2313 async fn test_process_metrics_bucket_metadata() {
2314 let mut token = Cogs::noop().timed(ResourceId::Relay, AppFeature::Unattributed);
2315 let project_key = ProjectKey::parse("a94ae32be2584e0bbd7a4cbb95971fee").unwrap();
2316 let received_at = Utc::now();
2317 let config = Config::default();
2318
2319 let (aggregator, mut aggregator_rx) = Addr::custom();
2320 let processor = create_test_processor_with_addrs(
2321 config,
2322 Addrs {
2323 aggregator,
2324 ..Default::default()
2325 },
2326 )
2327 .await;
2328
2329 let mut item = Item::new(ItemType::Statsd);
2330 item.set_payload(ContentType::Text, "spans/foo:3182887624:4267882815|s");
2331 for (source, expected_received_at) in [
2332 (
2333 BucketSource::External,
2334 Some(UnixTimestamp::from_datetime(received_at).unwrap()),
2335 ),
2336 (BucketSource::Internal, None),
2337 ] {
2338 let message = ProcessMetrics {
2339 data: MetricData::Raw(vec![item.clone()]),
2340 project_key,
2341 source,
2342 received_at,
2343 sent_at: Some(Utc::now()),
2344 };
2345 processor.handle_process_metrics(&mut token, message);
2346
2347 let Aggregator::MergeBuckets(merge_buckets) = aggregator_rx.recv().await.unwrap();
2348 let buckets = merge_buckets.buckets;
2349 assert_eq!(buckets.len(), 1);
2350 assert_eq!(buckets[0].metadata.received_at, expected_received_at);
2351 }
2352 }
2353
2354 #[tokio::test]
2355 async fn test_process_batched_metrics() {
2356 let mut token = Cogs::noop().timed(ResourceId::Relay, AppFeature::Unattributed);
2357 let received_at = Utc::now();
2358 let config = Config::default();
2359
2360 let (aggregator, mut aggregator_rx) = Addr::custom();
2361 let processor = create_test_processor_with_addrs(
2362 config,
2363 Addrs {
2364 aggregator,
2365 ..Default::default()
2366 },
2367 )
2368 .await;
2369
2370 let payload = r#"{
2371 "buckets": {
2372 "11111111111111111111111111111111": [
2373 {
2374 "timestamp": 1615889440,
2375 "width": 0,
2376 "name": "d:transactions/endpoint.response_time@millisecond",
2377 "type": "d",
2378 "value": [
2379 68.0
2380 ],
2381 "tags": {
2382 "route": "user_index"
2383 }
2384 }
2385 ],
2386 "22222222222222222222222222222222": [
2387 {
2388 "timestamp": 1615889440,
2389 "width": 0,
2390 "name": "d:transactions/endpoint.cache_rate@none",
2391 "type": "d",
2392 "value": [
2393 36.0
2394 ]
2395 }
2396 ]
2397 }
2398}
2399"#;
2400 let message = ProcessBatchedMetrics {
2401 payload: Bytes::from(payload),
2402 source: BucketSource::Internal,
2403 received_at,
2404 sent_at: Some(Utc::now()),
2405 };
2406 processor.handle_process_batched_metrics(&mut token, message);
2407
2408 let Aggregator::MergeBuckets(mb1) = aggregator_rx.recv().await.unwrap();
2409 let Aggregator::MergeBuckets(mb2) = aggregator_rx.recv().await.unwrap();
2410
2411 let mut messages = vec![mb1, mb2];
2412 messages.sort_by_key(|mb| mb.project_key);
2413
2414 let actual = messages
2415 .into_iter()
2416 .map(|mb| (mb.project_key, mb.buckets))
2417 .collect::<Vec<_>>();
2418
2419 assert_debug_snapshot!(actual, @r###"
2420 [
2421 (
2422 ProjectKey("11111111111111111111111111111111"),
2423 [
2424 Bucket {
2425 timestamp: UnixTimestamp(1615889440),
2426 width: 0,
2427 name: MetricName(
2428 "d:transactions/endpoint.response_time@millisecond",
2429 ),
2430 value: Distribution(
2431 [
2432 68.0,
2433 ],
2434 ),
2435 tags: {
2436 "route": "user_index",
2437 },
2438 metadata: BucketMetadata {
2439 merges: 1,
2440 received_at: None,
2441 extracted_from_indexed: false,
2442 },
2443 },
2444 ],
2445 ),
2446 (
2447 ProjectKey("22222222222222222222222222222222"),
2448 [
2449 Bucket {
2450 timestamp: UnixTimestamp(1615889440),
2451 width: 0,
2452 name: MetricName(
2453 "d:transactions/endpoint.cache_rate@none",
2454 ),
2455 value: Distribution(
2456 [
2457 36.0,
2458 ],
2459 ),
2460 tags: {},
2461 metadata: BucketMetadata {
2462 merges: 1,
2463 received_at: None,
2464 extracted_from_indexed: false,
2465 },
2466 },
2467 ],
2468 ),
2469 ]
2470 "###);
2471 }
2472}