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
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 #[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#[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 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 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 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 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#[derive(Debug)]
298pub struct ProcessEnvelope {
299 pub envelope: ManagedEnvelope,
301 pub project_info: Arc<ProjectInfo>,
303 pub rate_limits: Arc<RateLimits>,
305 pub sampling_project_info: Option<Arc<ProjectInfo>>,
307}
308
309#[derive(Debug)]
321pub struct ProcessMetrics {
322 pub data: MetricData,
324 pub project_key: ProjectKey,
326 pub source: BucketSource,
328 pub received_at: DateTime<Utc>,
330 pub sent_at: Option<DateTime<Utc>>,
333}
334
335#[derive(Debug)]
337pub enum MetricData {
338 Raw(Vec<Item>),
340 Parsed(Vec<Bucket>),
342}
343
344impl MetricData {
345 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 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 pub payload: Bytes,
401 pub source: BucketSource,
403 pub received_at: DateTime<Utc>,
405 pub sent_at: Option<DateTime<Utc>>,
407}
408
409#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
411pub enum BucketSource {
412 Internal,
418 External,
423}
424
425impl BucketSource {
426 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#[derive(Debug)]
437pub struct SubmitClientReports {
438 pub client_reports: Vec<ClientReport>,
440 pub scoping: Scoping,
442}
443
444#[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 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
509pub type EnvelopeProcessorServicePool = AsyncPool<BoxFuture<'static, ()>>;
511
512#[derive(Clone)]
516pub struct EnvelopeProcessorService {
517 inner: Arc<InnerProcessor>,
518}
519
520pub 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 #[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 "a_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 if let Some(sampling_state) = ctx.sampling_project_info {
636 envelope
639 .envelope_mut()
640 .parametrize_dsc_transaction(&sampling_state.config.tx_name_rules);
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 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 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 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 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 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 upstream: Option<UpstreamDescriptor>,
900 ) {
901 if envelope.envelope_mut().is_empty() {
902 envelope.accept();
903 return;
904 }
905
906 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 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 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 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 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 #[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 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 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 #[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 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 store_forwarder.send(StoreMetrics {
1235 buckets,
1236 scoping,
1237 retention,
1238 });
1239 }
1240 }
1241
1242 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 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 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 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 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 if config.emit_outcomes() == EmitOutcomes::AsClientReports {
1441 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 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 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 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 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 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#[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 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#[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 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 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 fn is_empty(&self) -> bool {
1696 self.views.is_empty()
1697 }
1698
1699 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#[derive(Debug)]
1724struct SendMetricsRequest {
1725 upstream: Option<UpstreamDescriptor>,
1727 partition_key: String,
1729 unencoded: Bytes,
1731 encoded: Bytes,
1733 project_info: HashMap<ProjectKey, Scoping>,
1737 http_encoding: HttpEncoding,
1739 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 error.is_received() {
1826 return;
1827 }
1828
1829 self.create_error_outcomes()
1830 }
1831 }
1832 })
1833 }
1834}
1835
1836#[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 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 #[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 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 datascrubbing_settings.scrub_data = true;
2091 datascrubbing_settings.scrub_defaults = true;
2092 datascrubbing_settings.scrub_ip_addresses = true;
2093
2094 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 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 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}