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;
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 geoip_lookup = config
572 .geoip_path()
573 .and_then(
574 |p| match GeoIpLookup::open(p).context(ServiceError::GeoIp) {
575 Ok(geoip) => Some(geoip),
576 Err(err) => {
577 relay_log::error!("failed to open GeoIP db {p:?}: {err:?}");
578 None
579 }
580 },
581 )
582 .unwrap_or_else(GeoIpLookup::empty);
583
584 if let Some(build_epoch) = geoip_lookup.build_epoch() {
585 relay_log::info!("Loaded GeoIP database (build: {build_epoch})");
586 }
587
588 #[cfg(feature = "processing")]
589 let rate_limiter = redis.map(|redis| {
590 RedisRateLimiter::new(redis.quotas)
591 .max_limit(config.max_rate_limit())
592 .cache(config.quota_cache_ratio(), config.quota_cache_max())
593 });
594
595 let quota_limiter = Arc::new(QuotaRateLimiter::new(
596 #[cfg(feature = "processing")]
597 project_cache.clone(),
598 #[cfg(feature = "processing")]
599 rate_limiter.clone(),
600 ));
601 #[cfg(feature = "processing")]
602 let rate_limiter = rate_limiter.map(Arc::new);
603 let inner = InnerProcessor {
604 pool,
605 global_config,
606 project_cache,
607 #[cfg(feature = "processing")]
608 rate_limiter,
609 processor: RelayProcessor::new(
610 cogs.clone(),
611 "a_limiter,
612 &geoip_lookup,
613 addrs.outcome_aggregator.clone(),
614 ),
615 cogs,
616 addrs,
617 metric_outcomes,
618 config,
619 };
620
621 Self {
622 inner: Arc::new(inner),
623 }
624 }
625
626 async fn process_envelope(
627 &self,
628 project_id: ProjectId,
629 mut envelope: ManagedEnvelope,
630 ctx: processing::Context<'_>,
631 ) -> Vec<Output<Outputs>> {
632 if let Some(sampling_state) = ctx.sampling_project_info {
634 envelope
637 .envelope_mut()
638 .parametrize_dsc_transaction(&sampling_state.config.tx_name_rules);
639 }
640
641 envelope
646 .envelope_mut()
647 .meta_mut()
648 .set_project_id(project_id);
649
650 self.inner.processor.run(envelope, ctx).await
651 }
652
653 async fn process<'a>(
659 &self,
660 mut envelope: ManagedEnvelope,
661 ctx: processing::Context<'a>,
662 ) -> Vec<Output<Outputs>> {
663 let Some(project_id) = ctx
670 .project_info
671 .project_id
672 .or_else(|| envelope.envelope().meta().project_id())
673 else {
674 relay_log::error!(
675 tags.project_key = %envelope.envelope().meta().public_key(),
676 "project info does not contain project id"
677 );
678 envelope.reject(Outcome::Invalid(DiscardReason::Internal));
679 return Vec::new();
680 };
681
682 relay_log::configure_scope(|scope| {
683 scope.set_tag("project_id", project_id);
684 });
685
686 self.process_envelope(project_id, envelope, ctx).await
687 }
688
689 async fn handle_process_envelope(&self, cogs: &mut Token, message: ProcessEnvelope) {
690 let wait_time = message.envelope.age();
691 metric!(timer(RelayTimers::EnvelopeWaitTime) = wait_time);
692
693 cogs.cancel();
696
697 let global_config = self.inner.global_config.current().unwrap_or_default();
698
699 let ctx = processing::Context {
700 config: &self.inner.config,
701 global_config: &global_config,
702 project_info: &message.project_info,
703 sampling_project_info: message.sampling_project_info.as_deref(),
704 rate_limits: &message.rate_limits,
705 };
706
707 let project_key = message.envelope.meta().public_key();
708 let sampling_key = ctx
712 .sampling_project_info
713 .and_then(|p| p.get_public_key_config())
714 .map(|pkc| pkc.public_key);
715
716 relay_log::configure_scope(|scope| {
717 scope.set_tag("project_key", project_key);
718 if let Some(sampling_key) = sampling_key {
719 scope.set_tag("sampling_key", sampling_key);
720 }
721 let meta = message.envelope.envelope().meta();
722 scope.set_tag("sdk_name", meta.client_name());
723 if let Some(client) = meta.client() {
724 scope.set_tag("sdk", client);
725 }
726 if let Some(user_agent) = meta.user_agent() {
727 scope.set_extra("user_agent", user_agent.into());
728 }
729 });
730
731 let outputs = metric!(timer(RelayTimers::EnvelopeProcessingTime), {
732 self.process(message.envelope, ctx).await
733 });
734
735 let ctx = ctx.to_forward();
736 for Output { main, metrics } in outputs {
737 if let Some(metrics) = metrics {
738 let agg = &self.inner.addrs.aggregator;
739 metrics.accept(|metrics| {
740 send_metrics(metrics, project_key, sampling_key, agg);
741 });
742 }
743
744 if let Some(output) = main {
745 self.submit_upstream(&mut Token::noop(), output, ctx);
747 }
748 }
749 }
750
751 fn handle_process_metrics(&self, cogs: &mut Token, message: ProcessMetrics) {
752 let ProcessMetrics {
753 data,
754 project_key,
755 received_at,
756 sent_at,
757 source,
758 } = message;
759
760 let received_timestamp =
761 UnixTimestamp::from_datetime(received_at).unwrap_or(UnixTimestamp::now());
762
763 let mut buckets = data.into_buckets(received_timestamp);
764 if buckets.is_empty() {
765 return;
766 };
767 cogs.update(relay_metrics::cogs::BySize(&buckets));
768
769 let clock_drift_processor =
770 ClockDriftProcessor::new(sent_at, received_at).at_least(MINIMUM_CLOCK_DRIFT);
771
772 buckets.retain_mut(|bucket| {
773 if let Err(error) = relay_metrics::normalize_bucket(bucket) {
774 relay_log::debug!(error = &error as &dyn Error, "dropping bucket {bucket:?}");
775 return false;
776 }
777
778 if !self::metrics::is_valid_namespace(bucket, source) {
779 relay_log::debug!("dropping bucket in invalid namespace {bucket:?}");
780 return false;
781 }
782
783 clock_drift_processor.process_timestamp(&mut bucket.timestamp);
784
785 if !matches!(source, BucketSource::Internal) {
786 bucket.metadata = BucketMetadata::new(received_timestamp);
787 }
788
789 true
790 });
791
792 let project = self.inner.project_cache.get(project_key);
793
794 let buckets = match project.state() {
797 ProjectState::Enabled(project_info) => {
798 let rate_limits = project.rate_limits().current_limits();
799 self.check_buckets(project_key, project_info, &rate_limits, buckets)
800 }
801 _ => buckets,
802 };
803
804 relay_log::trace!("merging metric buckets into the aggregator");
805 self.inner
806 .addrs
807 .aggregator
808 .send(MergeBuckets::new(project_key, buckets));
809 }
810
811 fn handle_process_batched_metrics(&self, cogs: &mut Token, message: ProcessBatchedMetrics) {
812 let ProcessBatchedMetrics {
813 payload,
814 source,
815 received_at,
816 sent_at,
817 } = message;
818
819 #[derive(serde::Deserialize)]
820 struct Wrapper {
821 buckets: HashMap<ProjectKey, Vec<Bucket>>,
822 }
823
824 let buckets = match serde_json::from_slice(&payload) {
825 Ok(Wrapper { buckets }) => buckets,
826 Err(error) => {
827 relay_log::debug!(
828 error = &error as &dyn Error,
829 "failed to parse batched metrics",
830 );
831 metric!(counter(RelayCounters::MetricBucketsParsingFailed) += 1);
832 return;
833 }
834 };
835
836 for (project_key, buckets) in buckets {
837 self.handle_process_metrics(
838 cogs,
839 ProcessMetrics {
840 data: MetricData::Parsed(buckets),
841 project_key,
842 source,
843 received_at,
844 sent_at,
845 },
846 )
847 }
848 }
849
850 fn submit_upstream(
854 &self,
855 cogs: &mut Token,
856 output: Outputs,
857 ctx: processing::ForwardContext<'_>,
858 ) {
859 let _submit = cogs.start_category("submit");
860
861 #[cfg(feature = "processing")]
862 if ctx.config.processing_enabled()
863 && let Some(store_forwarder) = &self.inner.addrs.store_forwarder
864 {
865 use crate::processing::StoreHandle;
866
867 let objectstore = self.inner.addrs.objectstore.as_ref();
868 let handle = StoreHandle::new(store_forwarder, objectstore, ctx.global_config);
869
870 output
871 .forward_store(handle, ctx)
872 .unwrap_or_else(|err| err.into_inner());
873
874 return;
875 }
876
877 match output.serialize_envelope(ctx) {
878 Ok(envelope) => {
879 let envelope = ManagedEnvelope::from(envelope);
880 self.submit_envelope_upstream(envelope, ctx.project_info.upstream.clone());
881 }
882 Err(_) => relay_log::error!("failed to serialize output to an envelope"),
883 };
884 }
885
886 fn submit_envelope_upstream(
887 &self,
888 mut envelope: ManagedEnvelope,
889 upstream: Option<UpstreamDescriptor>,
892 ) {
893 if envelope.envelope_mut().is_empty() {
894 envelope.accept();
895 return;
896 }
897
898 if self.inner.config.processing_enabled() {
904 relay_log::error!(
905 "attempt to forward envelope to http upstream when processing is enabled"
906 );
907 return;
908 }
909
910 envelope.envelope_mut().set_sent_at(Utc::now());
916
917 relay_log::trace!("sending envelope to sentry endpoint");
918 let http_encoding = self.inner.config.http_encoding();
919 let result = envelope.envelope().to_vec().and_then(|v| {
920 encode_payload(&v.into(), http_encoding).map_err(EnvelopeError::PayloadIoFailed)
921 });
922
923 match result {
924 Ok(body) => {
925 self.inner
926 .addrs
927 .upstream_relay
928 .send(SendRequest(SendEnvelope {
929 upstream,
930 envelope,
931 body,
932 http_encoding,
933 project_cache: self.inner.project_cache.clone(),
934 }));
935 }
936 Err(error) => {
937 relay_log::error!(
940 error = &error as &dyn Error,
941 tags.project_key = %envelope.scoping().project_key,
942 "failed to serialize envelope payload"
943 );
944
945 envelope.reject(Outcome::Invalid(DiscardReason::Internal));
946 }
947 }
948 }
949
950 fn handle_submit_client_reports(&self, message: SubmitClientReports) {
951 let SubmitClientReports {
952 client_reports,
953 scoping,
954 } = message;
955
956 relay_log::trace!(
957 "sending {} client report(s) to project id {}",
958 client_reports.len(),
959 scoping.project_id
960 );
961
962 if client_reports.is_empty() {
963 return;
964 }
965
966 let upstream = self.inner.config.upstream();
967 let dsn = PartialDsn::outbound(&scoping, upstream);
968
969 let mut envelope = Envelope::from_request(None, RequestMeta::outbound(dsn));
970 for client_report in client_reports {
971 match client_report.serialize() {
972 Ok(payload) => {
973 let mut item = Item::new(ItemType::ClientReport);
974 item.set_payload(ContentType::Json, payload);
975 envelope.add_item(item);
976 }
977 Err(error) => {
978 relay_log::error!(
979 error = &error as &dyn std::error::Error,
980 "failed to serialize client report"
981 );
982 }
983 }
984 }
985
986 let envelope = ManagedEnvelope::new(envelope, self.inner.addrs.outcome_aggregator.clone());
987 self.submit_envelope_upstream(envelope, None);
988 }
989
990 fn check_buckets(
991 &self,
992 project_key: ProjectKey,
993 project_info: &ProjectInfo,
994 rate_limits: &RateLimits,
995 buckets: Vec<Bucket>,
996 ) -> Vec<Bucket> {
997 let Some(scoping) = project_info.scoping(project_key) else {
998 relay_log::error!(
999 tags.project_key = project_key.as_str(),
1000 "there is no scoping: dropping {} buckets",
1001 buckets.len(),
1002 );
1003 return Vec::new();
1004 };
1005
1006 let mut buckets =
1007 self::metrics::remove_invalid_namespaces(buckets, &self.inner.metric_outcomes, scoping);
1008
1009 let mut namespaces: BTreeSet<MetricNamespace> = buckets
1010 .iter()
1011 .filter_map(|bucket| bucket.name.try_namespace())
1012 .collect();
1013
1014 namespaces.remove(&MetricNamespace::Outcomes);
1016
1017 for namespace in namespaces {
1018 let limits = rate_limits
1019 .check_with_quotas(project_info.get_quotas(), scoping.metric_bucket(namespace));
1020
1021 if limits.is_limited() {
1022 let rejected;
1023 (buckets, rejected) = utils::split_off(buckets, |bucket| {
1024 bucket.name.try_namespace() == Some(namespace)
1025 });
1026
1027 let reason_code = limits.longest().and_then(|limit| limit.reason_code.clone());
1028 self.inner.metric_outcomes.track(
1029 scoping,
1030 &rejected,
1031 Outcome::RateLimited(reason_code),
1032 );
1033 }
1034 }
1035
1036 let quotas = project_info.config.quotas.clone();
1037 match MetricsLimiter::create(buckets, quotas, scoping) {
1038 Ok(mut bucket_limiter) => {
1039 bucket_limiter.enforce_limits(rate_limits, &self.inner.metric_outcomes);
1040 bucket_limiter.into_buckets()
1041 }
1042 Err(buckets) => buckets,
1043 }
1044 }
1045
1046 #[cfg(feature = "processing")]
1047 async fn rate_limit_buckets(
1048 &self,
1049 scoping: Scoping,
1050 project_info: &ProjectInfo,
1051 mut buckets: Vec<Bucket>,
1052 ) -> Vec<Bucket> {
1053 let Some(rate_limiter) = &self.inner.rate_limiter else {
1054 return buckets;
1055 };
1056
1057 let global_config = self.inner.global_config.current().unwrap_or_default();
1058 let mut namespaces = buckets
1059 .iter()
1060 .filter_map(|bucket| bucket.name.try_namespace())
1061 .counts();
1062
1063 namespaces.remove(&MetricNamespace::Outcomes);
1065
1066 let quotas = CombinedQuotas::new(&global_config, project_info.get_quotas());
1067
1068 for (namespace, quantity) in namespaces {
1069 let item_scoping = scoping.metric_bucket(namespace);
1070
1071 let limits = match rate_limiter
1072 .is_rate_limited(quotas, item_scoping, quantity, false)
1073 .await
1074 {
1075 Ok(limits) => limits,
1076 Err(err) => {
1077 relay_log::error!(
1078 error = &err as &dyn std::error::Error,
1079 "failed to check redis rate limits"
1080 );
1081 break;
1082 }
1083 };
1084
1085 if limits.is_limited() {
1086 let rejected;
1087 (buckets, rejected) = utils::split_off(buckets, |bucket| {
1088 bucket.name.try_namespace() == Some(namespace)
1089 });
1090
1091 let reason_code = limits.longest().and_then(|limit| limit.reason_code.clone());
1092 self.inner.metric_outcomes.track(
1093 scoping,
1094 &rejected,
1095 Outcome::RateLimited(reason_code),
1096 );
1097
1098 self.inner
1099 .project_cache
1100 .get(item_scoping.scoping.project_key)
1101 .rate_limits()
1102 .merge(limits);
1103 }
1104 }
1105
1106 match MetricsLimiter::create(buckets, project_info.config.quotas.clone(), scoping) {
1107 Err(buckets) => buckets,
1108 Ok(bucket_limiter) => self.apply_other_rate_limits(bucket_limiter).await,
1109 }
1110 }
1111
1112 #[cfg(feature = "processing")]
1114 async fn apply_other_rate_limits(&self, mut bucket_limiter: MetricsLimiter) -> Vec<Bucket> {
1115 relay_log::trace!("handle_rate_limit_buckets");
1116
1117 let scoping = *bucket_limiter.scoping();
1118
1119 if let Some(rate_limiter) = self.inner.rate_limiter.as_ref() {
1120 let global_config = self.inner.global_config.current().unwrap_or_default();
1121 let quotas = CombinedQuotas::new(&global_config, bucket_limiter.quotas());
1122
1123 let over_accept_once = true;
1126 let mut rate_limits = RateLimits::new();
1127
1128 let (category, count) = bucket_limiter.count();
1129
1130 let timer = Instant::now();
1131 let mut is_limited = false;
1132
1133 if let Some(count) = count {
1134 match rate_limiter
1135 .is_rate_limited(quotas, scoping.item(category), count, over_accept_once)
1136 .await
1137 {
1138 Ok(limits) => {
1139 is_limited = limits.is_limited();
1140 rate_limits.merge(limits)
1141 }
1142 Err(e) => {
1143 relay_log::error!(error = &e as &dyn Error, "rate limiting error")
1144 }
1145 }
1146 }
1147
1148 relay_statsd::metric!(
1149 timer(RelayTimers::RateLimitBucketsDuration) = timer.elapsed(),
1150 category = category.name(),
1151 limited = if is_limited { "true" } else { "false" },
1152 count = match count {
1153 None => "none",
1154 Some(0) => "0",
1155 Some(1) => "1",
1156 Some(1..=10) => "10",
1157 Some(1..=25) => "25",
1158 Some(1..=50) => "50",
1159 Some(51..=100) => "100",
1160 Some(101..=500) => "500",
1161 _ => "> 500",
1162 },
1163 );
1164
1165 if rate_limits.is_limited() {
1166 let was_enforced =
1167 bucket_limiter.enforce_limits(&rate_limits, &self.inner.metric_outcomes);
1168
1169 if was_enforced {
1170 self.inner
1172 .project_cache
1173 .get(scoping.project_key)
1174 .rate_limits()
1175 .merge(rate_limits);
1176 }
1177 }
1178 }
1179
1180 bucket_limiter.into_buckets()
1181 }
1182
1183 #[cfg(feature = "processing")]
1190 async fn encode_metrics_processing(
1191 &self,
1192 message: FlushBuckets,
1193 store_forwarder: &Addr<Store>,
1194 ) {
1195 use crate::constants::DEFAULT_EVENT_RETENTION;
1196 use crate::services::store::StoreMetrics;
1197
1198 for ProjectBuckets {
1199 buckets,
1200 scoping,
1201 project_info,
1202 ..
1203 } in message.buckets.into_values()
1204 {
1205 let mut buckets = self
1206 .rate_limit_buckets(scoping, &project_info, buckets)
1207 .await;
1208
1209 if buckets.is_empty() {
1210 continue;
1211 }
1212
1213 self.inner
1215 .metric_outcomes
1216 .track_accepted_outcome(scoping, &mut buckets);
1217
1218 let retention = project_info
1219 .config
1220 .event_retention
1221 .unwrap_or(DEFAULT_EVENT_RETENTION);
1222
1223 store_forwarder.send(StoreMetrics {
1226 buckets,
1227 scoping,
1228 retention,
1229 });
1230 }
1231 }
1232
1233 fn encode_metrics_envelope(&self, message: FlushBuckets) {
1243 let FlushBuckets {
1244 partition_key,
1245 buckets,
1246 } = message;
1247
1248 let batch_size = self.inner.config.metrics_max_batch_size_bytes();
1249 let upstream = self.inner.config.upstream();
1250
1251 for ProjectBuckets {
1252 buckets,
1253 scoping,
1254 project_info,
1255 ..
1256 } in buckets.values()
1257 {
1258 let dsn = PartialDsn::outbound(scoping, upstream);
1259
1260 relay_statsd::metric!(
1261 distribution(RelayDistributions::PartitionKeys) = u64::from(partition_key)
1262 );
1263
1264 let mut num_batches = 0;
1265 for batch in BucketsView::from(buckets).by_size(batch_size) {
1266 let mut envelope = Envelope::from_request(None, RequestMeta::outbound(dsn.clone()));
1267
1268 let mut item = Item::new(ItemType::MetricBuckets);
1269 item.set_source_quantities(crate::metrics::extract_quantities(batch));
1270 item.set_payload(ContentType::Json, serde_json::to_vec(&buckets).unwrap());
1271 envelope.add_item(item);
1272
1273 let mut envelope =
1274 ManagedEnvelope::new(envelope, self.inner.addrs.outcome_aggregator.clone());
1275 envelope
1276 .set_partition_key(Some(partition_key))
1277 .scope(*scoping);
1278
1279 relay_statsd::metric!(
1280 distribution(RelayDistributions::BucketsPerBatch) = batch.len() as u64
1281 );
1282
1283 self.submit_envelope_upstream(envelope, project_info.upstream.clone());
1284 num_batches += 1;
1285 }
1286
1287 relay_statsd::metric!(
1288 distribution(RelayDistributions::BatchesPerPartition) = num_batches
1289 );
1290 }
1291 }
1292
1293 fn send_global_partition(
1295 &self,
1296 upstream: Option<UpstreamDescriptor>,
1297 partition_key: u32,
1298 partition: &mut Partition<'_>,
1299 ) {
1300 if partition.is_empty() {
1301 return;
1302 }
1303
1304 let (unencoded, project_info) = partition.take();
1305 let http_encoding = self.inner.config.http_encoding();
1306 let encoded = match encode_payload(&unencoded, http_encoding) {
1307 Ok(payload) => payload,
1308 Err(error) => {
1309 let error = &error as &dyn std::error::Error;
1310 relay_log::error!(error, "failed to encode metrics payload");
1311 return;
1312 }
1313 };
1314
1315 let request = SendMetricsRequest {
1316 upstream,
1317 partition_key: partition_key.to_string(),
1318 unencoded,
1319 encoded,
1320 project_info,
1321 http_encoding,
1322 metric_outcomes: self.inner.metric_outcomes.clone(),
1323 };
1324
1325 self.inner.addrs.upstream_relay.send(SendRequest(request));
1326 }
1327
1328 fn encode_metrics_global(&self, message: FlushBuckets) {
1339 let FlushBuckets {
1340 partition_key,
1341 buckets,
1342 } = message;
1343
1344 let batch_size = self.inner.config.metrics_max_batch_size_bytes();
1345 let mut partitions = BTreeMap::new();
1346 let mut partition_splits = 0;
1347
1348 for ProjectBuckets {
1349 buckets,
1350 scoping,
1351 project_info,
1352 ..
1353 } in buckets.values()
1354 {
1355 let partition = match partitions.get_mut(&project_info.upstream) {
1356 Some(partition) => partition,
1357 None => partitions
1358 .entry(project_info.upstream.clone())
1359 .or_insert_with(|| Partition::new(batch_size)),
1360 };
1361
1362 for bucket in buckets {
1363 let mut remaining = Some(BucketView::new(bucket));
1364
1365 while let Some(bucket) = remaining.take() {
1366 if let Some(next) = partition.insert(bucket, *scoping) {
1367 self.send_global_partition(
1371 project_info.upstream.clone(),
1372 partition_key,
1373 partition,
1374 );
1375 remaining = Some(next);
1376 partition_splits += 1;
1377 }
1378 }
1379 }
1380 }
1381
1382 if partition_splits > 0 {
1383 metric!(distribution(RelayDistributions::PartitionSplits) = partition_splits);
1384 }
1385
1386 for (upstream, mut partition) in partitions {
1387 self.send_global_partition(upstream, partition_key, &mut partition);
1388 }
1389 }
1390
1391 fn encode_metrics_client_reports(&self, mut message: FlushBuckets) -> FlushBuckets {
1395 for ProjectBuckets {
1396 buckets, scoping, ..
1397 } in message.buckets.values_mut()
1398 {
1399 let client_reports = outcome::metric::extract_client_reports(buckets).collect();
1400
1401 self.handle_submit_client_reports(SubmitClientReports {
1402 client_reports,
1403 scoping: *scoping,
1404 });
1405 }
1406
1407 message
1408 }
1409
1410 async fn handle_flush_buckets(&self, mut message: FlushBuckets) {
1411 for (project_key, pb) in message.buckets.iter_mut() {
1412 let buckets = std::mem::take(&mut pb.buckets);
1413 pb.buckets =
1414 self.check_buckets(*project_key, &pb.project_info, &pb.rate_limits, buckets);
1415 }
1416
1417 #[cfg(feature = "processing")]
1418 if self.inner.config.processing_enabled()
1419 && let Some(ref store_forwarder) = self.inner.addrs.store_forwarder
1420 {
1421 return self
1422 .encode_metrics_processing(message, store_forwarder)
1423 .await;
1424 }
1425
1426 if self.inner.config.emit_outcomes() == EmitOutcomes::AsClientReports {
1429 message = self.encode_metrics_client_reports(message);
1432 }
1433
1434 if self.inner.config.http_global_metrics() {
1435 self.encode_metrics_global(message)
1436 } else {
1437 self.encode_metrics_envelope(message)
1438 }
1439 }
1440
1441 #[cfg(all(test, feature = "processing"))]
1442 fn redis_rate_limiter_enabled(&self) -> bool {
1443 self.inner.rate_limiter.is_some()
1444 }
1445
1446 async fn handle_message(self, message: EnvelopeProcessor) {
1447 let ty = message.variant();
1448 let feature_weights = self.feature_weights(&message);
1449
1450 metric!(timer(RelayTimers::ProcessMessageDuration), message = ty, {
1451 let mut cogs = self.inner.cogs.timed(ResourceId::Relay, feature_weights);
1452
1453 match message {
1454 EnvelopeProcessor::ProcessEnvelope(m) => {
1455 self.handle_process_envelope(&mut cogs, *m).await
1456 }
1457 EnvelopeProcessor::ProcessProjectMetrics(m) => {
1458 self.handle_process_metrics(&mut cogs, *m)
1459 }
1460 EnvelopeProcessor::ProcessBatchedMetrics(m) => {
1461 self.handle_process_batched_metrics(&mut cogs, *m)
1462 }
1463 EnvelopeProcessor::FlushBuckets(m) => self.handle_flush_buckets(*m).await,
1464 EnvelopeProcessor::SubmitClientReports(m) => self.handle_submit_client_reports(*m),
1465 }
1466 });
1467 }
1468
1469 fn feature_weights(&self, message: &EnvelopeProcessor) -> FeatureWeights {
1470 match message {
1471 EnvelopeProcessor::ProcessEnvelope(_) => AppFeature::Unattributed.into(),
1473 EnvelopeProcessor::ProcessProjectMetrics(_) => AppFeature::Unattributed.into(),
1474 EnvelopeProcessor::ProcessBatchedMetrics(_) => AppFeature::Unattributed.into(),
1475 EnvelopeProcessor::FlushBuckets(v) => v
1476 .buckets
1477 .values()
1478 .map(|s| {
1479 if self.inner.config.processing_enabled() {
1480 relay_metrics::cogs::ByCount(&s.buckets).into()
1483 } else {
1484 relay_metrics::cogs::BySize(&s.buckets).into()
1485 }
1486 })
1487 .fold(FeatureWeights::none(), FeatureWeights::merge),
1488 EnvelopeProcessor::SubmitClientReports(_) => AppFeature::ClientReports.into(),
1489 }
1490 }
1491}
1492
1493impl Service for EnvelopeProcessorService {
1494 type Interface = EnvelopeProcessor;
1495
1496 async fn run(self, mut rx: relay_system::Receiver<Self::Interface>) {
1497 while let Some(message) = rx.recv().await {
1498 let service = self.clone();
1499 let hub = relay_log::Hub::new_from_top(relay_log::Hub::current());
1501
1502 self.inner
1503 .pool
1504 .spawn_async(Box::pin(service.handle_message(message).bind_hub(hub)))
1505 .await;
1506 }
1507 }
1508}
1509
1510pub fn encode_payload(body: &Bytes, http_encoding: HttpEncoding) -> Result<Bytes, std::io::Error> {
1511 let envelope_body: Vec<u8> = match http_encoding {
1512 HttpEncoding::Identity => return Ok(body.clone()),
1513 HttpEncoding::Deflate => {
1514 let mut encoder = ZlibEncoder::new(Vec::new(), Compression::default());
1515 encoder.write_all(body.as_ref())?;
1516 encoder.finish()?
1517 }
1518 HttpEncoding::Gzip => {
1519 let mut encoder = GzEncoder::new(Vec::new(), Compression::default());
1520 encoder.write_all(body.as_ref())?;
1521 encoder.finish()?
1522 }
1523 HttpEncoding::Br => {
1524 let mut encoder = BrotliEncoder::new(Vec::new(), 0, 5, 22);
1526 encoder.write_all(body.as_ref())?;
1527 encoder.into_inner()
1528 }
1529 HttpEncoding::Zstd => {
1530 let mut encoder = ZstdEncoder::new(Vec::new(), 1)?;
1533 encoder.write_all(body.as_ref())?;
1534 encoder.finish()?
1535 }
1536 };
1537
1538 Ok(envelope_body.into())
1539}
1540
1541#[derive(Debug)]
1543pub struct SendEnvelope {
1544 pub upstream: Option<UpstreamDescriptor>,
1545 pub envelope: ManagedEnvelope,
1546 pub body: Bytes,
1547 pub http_encoding: HttpEncoding,
1548 pub project_cache: ProjectCacheHandle,
1549}
1550
1551impl UpstreamRequest for SendEnvelope {
1552 fn upstream(&self) -> Option<&UpstreamDescriptor> {
1553 self.upstream.as_ref()
1554 }
1555
1556 fn method(&self) -> reqwest::Method {
1557 reqwest::Method::POST
1558 }
1559
1560 fn path(&self) -> Cow<'_, str> {
1561 format!("/api/{}/envelope/", self.envelope.scoping().project_id).into()
1562 }
1563
1564 fn route(&self) -> &'static str {
1565 "envelope"
1566 }
1567
1568 fn build(&mut self, builder: &mut http::RequestBuilder) -> Result<(), http::HttpError> {
1569 let envelope_body = self.body.clone();
1570
1571 let meta = &self.envelope.meta();
1572 let shard = self.envelope.partition_key().map(|p| p.to_string());
1573 builder
1574 .content_encoding(self.http_encoding)
1575 .header_opt("Origin", meta.origin().map(|url| url.as_str()))
1576 .header_opt("User-Agent", meta.user_agent())
1577 .header("X-Sentry-Auth", meta.auth_header())
1578 .header("X-Forwarded-For", meta.forwarded_for())
1579 .header("Content-Type", envelope::CONTENT_TYPE)
1580 .header_opt("X-Sentry-Relay-Shard", shard)
1581 .body(envelope_body);
1582
1583 Ok(())
1584 }
1585
1586 fn sign(&mut self) -> Option<Sign> {
1587 Some(Sign::Optional(SignatureType::RequestSign))
1588 }
1589
1590 fn respond(
1591 self: Box<Self>,
1592 result: Result<http::Response, UpstreamRequestError>,
1593 ) -> Pin<Box<dyn Future<Output = ()> + Send + Sync>> {
1594 Box::pin(async move {
1595 let result = match result {
1596 Ok(mut response) => response.consume().await.map_err(UpstreamRequestError::Http),
1597 Err(error) => Err(error),
1598 };
1599
1600 match result {
1601 Ok(()) => self.envelope.accept(),
1602 Err(error) if error.is_received() => {
1603 let scoping = self.envelope.scoping();
1604 self.envelope.accept();
1605
1606 if let UpstreamRequestError::RateLimited(limits) = error {
1607 self.project_cache
1608 .get(scoping.project_key)
1609 .rate_limits()
1610 .merge(limits.scope(&scoping));
1611 }
1612 }
1613 Err(error) => {
1614 let mut envelope = self.envelope;
1617 envelope.reject(Outcome::Invalid(DiscardReason::Internal));
1618 relay_log::error!(
1619 error = &error as &dyn Error,
1620 tags.project_key = %envelope.scoping().project_key,
1621 "error sending envelope"
1622 );
1623 }
1624 }
1625 })
1626 }
1627}
1628
1629#[derive(Debug)]
1636struct Partition<'a> {
1637 max_size: usize,
1638 remaining: usize,
1639 views: HashMap<ProjectKey, Vec<BucketView<'a>>>,
1640 project_info: HashMap<ProjectKey, Scoping>,
1641}
1642
1643impl<'a> Partition<'a> {
1644 pub fn new(size: usize) -> Self {
1646 Self {
1647 max_size: size,
1648 remaining: size,
1649 views: HashMap::new(),
1650 project_info: HashMap::new(),
1651 }
1652 }
1653
1654 pub fn insert(&mut self, bucket: BucketView<'a>, scoping: Scoping) -> Option<BucketView<'a>> {
1665 let (current, next) = bucket.split(self.remaining, Some(self.max_size));
1666
1667 if let Some(current) = current {
1668 self.remaining = self.remaining.saturating_sub(current.estimated_size());
1669 self.views
1670 .entry(scoping.project_key)
1671 .or_default()
1672 .push(current);
1673
1674 self.project_info
1675 .entry(scoping.project_key)
1676 .or_insert(scoping);
1677 }
1678
1679 next
1680 }
1681
1682 fn is_empty(&self) -> bool {
1684 self.views.is_empty()
1685 }
1686
1687 fn take(&mut self) -> (Bytes, HashMap<ProjectKey, Scoping>) {
1691 #[derive(serde::Serialize)]
1692 struct Wrapper<'a> {
1693 buckets: &'a HashMap<ProjectKey, Vec<BucketView<'a>>>,
1694 }
1695
1696 let buckets = &self.views;
1697 let payload = serde_json::to_vec(&Wrapper { buckets }).unwrap().into();
1698
1699 let scopings = std::mem::take(&mut self.project_info);
1700
1701 self.views.clear();
1702 self.remaining = self.max_size;
1703
1704 (payload, scopings)
1705 }
1706}
1707
1708#[derive(Debug)]
1712struct SendMetricsRequest {
1713 upstream: Option<UpstreamDescriptor>,
1715 partition_key: String,
1717 unencoded: Bytes,
1719 encoded: Bytes,
1721 project_info: HashMap<ProjectKey, Scoping>,
1725 http_encoding: HttpEncoding,
1727 metric_outcomes: MetricOutcomes,
1729}
1730
1731impl SendMetricsRequest {
1732 fn create_error_outcomes(self) {
1733 #[derive(serde::Deserialize)]
1734 struct Wrapper {
1735 buckets: HashMap<ProjectKey, Vec<MinimalTrackableBucket>>,
1736 }
1737
1738 let buckets = match serde_json::from_slice(&self.unencoded) {
1739 Ok(Wrapper { buckets }) => buckets,
1740 Err(err) => {
1741 relay_log::error!(
1742 error = &err as &dyn std::error::Error,
1743 "failed to parse buckets from failed transmission"
1744 );
1745 return;
1746 }
1747 };
1748
1749 for (key, buckets) in buckets {
1750 let Some(&scoping) = self.project_info.get(&key) else {
1751 relay_log::error!("missing scoping for project key");
1752 continue;
1753 };
1754
1755 self.metric_outcomes.track(
1756 scoping,
1757 &buckets,
1758 Outcome::Invalid(DiscardReason::Internal),
1759 );
1760 }
1761 }
1762}
1763
1764impl UpstreamRequest for SendMetricsRequest {
1765 fn upstream(&self) -> Option<&UpstreamDescriptor> {
1766 self.upstream.as_ref()
1767 }
1768
1769 fn set_relay_id(&self) -> bool {
1770 true
1771 }
1772
1773 fn sign(&mut self) -> Option<Sign> {
1774 Some(Sign::Required(SignatureType::Body(self.unencoded.clone())))
1775 }
1776
1777 fn method(&self) -> reqwest::Method {
1778 reqwest::Method::POST
1779 }
1780
1781 fn path(&self) -> Cow<'_, str> {
1782 "/api/0/relays/metrics/".into()
1783 }
1784
1785 fn route(&self) -> &'static str {
1786 "global_metrics"
1787 }
1788
1789 fn build(&mut self, builder: &mut http::RequestBuilder) -> Result<(), http::HttpError> {
1790 builder
1791 .content_encoding(self.http_encoding)
1792 .header("X-Sentry-Relay-Shard", self.partition_key.as_bytes())
1793 .header(header::CONTENT_TYPE, b"application/json")
1794 .body(self.encoded.clone());
1795
1796 Ok(())
1797 }
1798
1799 fn respond(
1800 self: Box<Self>,
1801 result: Result<http::Response, UpstreamRequestError>,
1802 ) -> Pin<Box<dyn Future<Output = ()> + Send + Sync>> {
1803 Box::pin(async {
1804 match result {
1805 Ok(mut response) => {
1806 response.consume().await.ok();
1807 }
1808 Err(error) => {
1809 relay_log::error!(error = &error as &dyn Error, "Failed to send metrics batch");
1810
1811 if error.is_received() {
1814 return;
1815 }
1816
1817 self.create_error_outcomes()
1818 }
1819 }
1820 })
1821 }
1822}
1823
1824#[derive(Copy, Clone, Debug)]
1826#[cfg(feature = "processing")]
1827struct CombinedQuotas<'a> {
1828 global_quotas: &'a [Quota],
1829 project_quotas: &'a [Quota],
1830}
1831
1832#[cfg(feature = "processing")]
1833impl<'a> CombinedQuotas<'a> {
1834 pub fn new(global_config: &'a GlobalConfig, project_quotas: &'a [Quota]) -> Self {
1836 Self {
1837 global_quotas: &global_config.quotas,
1838 project_quotas,
1839 }
1840 }
1841}
1842
1843#[cfg(feature = "processing")]
1844impl<'a> IntoIterator for CombinedQuotas<'a> {
1845 type Item = &'a Quota;
1846 type IntoIter = std::iter::Chain<std::slice::Iter<'a, Quota>, std::slice::Iter<'a, Quota>>;
1847
1848 fn into_iter(self) -> Self::IntoIter {
1849 self.global_quotas.iter().chain(self.project_quotas.iter())
1850 }
1851}
1852
1853#[cfg(test)]
1854mod tests {
1855 use insta::assert_debug_snapshot;
1856 use relay_common::glob2::LazyGlob;
1857 use relay_dynamic_config::ProjectConfig;
1858 use relay_event_normalization::{
1859 NormalizationConfig, RedactionRule, TransactionNameConfig, TransactionNameRule,
1860 };
1861 use relay_event_schema::protocol::{Event, EventId, TransactionSource};
1862 use relay_pii::DataScrubbingConfig;
1863 use relay_protocol::Annotated;
1864 #[cfg(feature = "processing")]
1865 use relay_quotas::DataCategory;
1866 use similar_asserts::assert_eq;
1867
1868 use crate::testutils::{create_test_processor, create_test_processor_with_addrs};
1869
1870 #[cfg(feature = "processing")]
1871 use {
1872 relay_metrics::BucketValue,
1873 relay_quotas::{QuotaScope, ReasonCode},
1874 relay_test::mock_service,
1875 };
1876
1877 use super::*;
1878
1879 async fn process_to_single_envelope<'a>(
1880 processor: &EnvelopeProcessorService,
1881 envelope: ManagedEnvelope,
1882 ctx: processing::Context<'a>,
1883 ) -> Box<Envelope> {
1884 let mut outputs = processor.process(envelope, ctx).await;
1885 assert_eq!(outputs.len(), 1);
1886
1887 let Output { main, metrics } = outputs.pop().unwrap();
1888
1889 if let Some(metrics) = metrics {
1890 metrics.accept(drop);
1891 }
1892
1893 main.unwrap()
1894 .serialize_envelope(ctx.to_forward())
1895 .unwrap()
1896 .accept(|envelope| envelope)
1897 }
1898
1899 #[cfg(feature = "processing")]
1900 fn mock_quota(id: &str) -> Quota {
1901 Quota {
1902 id: Some(id.into()),
1903 categories: [DataCategory::MetricBucket].into(),
1904 scope: QuotaScope::Organization,
1905 scope_id: None,
1906 limit: Some(0),
1907 window: None,
1908 reason_code: None,
1909 namespace: None,
1910 }
1911 }
1912
1913 #[cfg(feature = "processing")]
1914 #[test]
1915 fn test_dynamic_quotas() {
1916 let global_config = relay_dynamic_config::GlobalConfig {
1917 quotas: vec![mock_quota("foo"), mock_quota("bar")],
1918 ..Default::default()
1919 };
1920
1921 let project_quotas = vec![mock_quota("baz"), mock_quota("qux")];
1922
1923 let dynamic_quotas = CombinedQuotas::new(&global_config, &project_quotas);
1924
1925 let quota_ids = dynamic_quotas.into_iter().filter_map(|q| q.id.as_deref());
1926 assert!(quota_ids.eq(["foo", "bar", "baz", "qux"]));
1927 }
1928
1929 #[cfg(feature = "processing")]
1932 #[tokio::test]
1933 async fn test_ratelimit_per_batch() {
1934 use relay_base_schema::organization::OrganizationId;
1935 use relay_protocol::FiniteF64;
1936
1937 let rate_limited_org = Scoping {
1938 organization_id: OrganizationId::new(1),
1939 project_id: ProjectId::new(21),
1940 project_key: ProjectKey::parse("00000000000000000000000000000000").unwrap(),
1941 key_id: Some(17),
1942 };
1943
1944 let not_rate_limited_org = Scoping {
1945 organization_id: OrganizationId::new(2),
1946 project_id: ProjectId::new(21),
1947 project_key: ProjectKey::parse("11111111111111111111111111111111").unwrap(),
1948 key_id: Some(17),
1949 };
1950
1951 let message = {
1952 let project_info = {
1953 let quota = Quota {
1954 id: Some("testing".into()),
1955 categories: [DataCategory::MetricBucket].into(),
1956 scope: relay_quotas::QuotaScope::Organization,
1957 scope_id: Some(rate_limited_org.organization_id.to_string().into()),
1958 limit: Some(0),
1959 window: None,
1960 reason_code: Some(ReasonCode::new("test")),
1961 namespace: None,
1962 };
1963
1964 let mut config = ProjectConfig::default();
1965 config.quotas.push(quota);
1966
1967 Arc::new(ProjectInfo {
1968 config,
1969 ..Default::default()
1970 })
1971 };
1972
1973 let project_metrics = |scoping| ProjectBuckets {
1974 buckets: vec![Bucket {
1975 name: "d:spans/bar".into(),
1976 value: BucketValue::Counter(FiniteF64::new(1.0).unwrap()),
1977 timestamp: UnixTimestamp::now(),
1978 tags: Default::default(),
1979 width: 10,
1980 metadata: BucketMetadata::default(),
1981 }],
1982 rate_limits: Default::default(),
1983 project_info: project_info.clone(),
1984 scoping,
1985 };
1986
1987 let buckets = hashbrown::HashMap::from([
1988 (
1989 rate_limited_org.project_key,
1990 project_metrics(rate_limited_org),
1991 ),
1992 (
1993 not_rate_limited_org.project_key,
1994 project_metrics(not_rate_limited_org),
1995 ),
1996 ]);
1997
1998 FlushBuckets {
1999 partition_key: 0,
2000 buckets,
2001 }
2002 };
2003
2004 assert_eq!(message.buckets.keys().count(), 2);
2006
2007 let config = {
2008 let config_json = serde_json::json!({
2009 "processing": {
2010 "enabled": true,
2011 "kafka_config": [],
2012 "redis": {
2013 "server": std::env::var("RELAY_REDIS_URL").unwrap_or_else(|_| "redis://127.0.0.1:6379".to_owned()),
2014 }
2015 }
2016 });
2017 Config::from_json_value(config_json).unwrap()
2018 };
2019
2020 let (store, handle) = {
2021 let f = |org_ids: &mut Vec<OrganizationId>, msg: Store| {
2022 let org_id = match msg {
2023 Store::Metrics(x) => x.scoping.organization_id,
2024 _ => panic!("received envelope when expecting only metrics"),
2025 };
2026 org_ids.push(org_id);
2027 };
2028
2029 mock_service("store_forwarder", vec![], f)
2030 };
2031
2032 let processor = create_test_processor(config).await;
2033 assert!(processor.redis_rate_limiter_enabled());
2034
2035 processor.encode_metrics_processing(message, &store).await;
2036
2037 drop(store);
2038 let orgs_not_ratelimited = handle.await.unwrap();
2039
2040 assert_eq!(
2041 orgs_not_ratelimited,
2042 vec![not_rate_limited_org.organization_id]
2043 );
2044 }
2045
2046 #[tokio::test]
2047 async fn test_browser_version_extraction_with_pii_like_data() {
2048 let processor = create_test_processor(Default::default()).await;
2049 let outcome_aggregator = Addr::dummy();
2050 let event_id = EventId::new();
2051
2052 let dsn = "https://e12d836b15bb49d7bbf99e64295d995b:@sentry.io/42"
2053 .parse()
2054 .unwrap();
2055
2056 let request_meta = RequestMeta::new(dsn);
2057 let mut envelope = Envelope::from_request(Some(event_id), request_meta);
2058
2059 envelope.add_item({
2060 let mut item = Item::new(ItemType::Event);
2061 item.set_payload(
2062 ContentType::Json,
2063 r#"
2064 {
2065 "request": {
2066 "headers": [
2067 ["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"]
2068 ]
2069 }
2070 }
2071 "#,
2072 );
2073 item
2074 });
2075
2076 let mut datascrubbing_settings = DataScrubbingConfig::default();
2077 datascrubbing_settings.scrub_data = true;
2079 datascrubbing_settings.scrub_defaults = true;
2080 datascrubbing_settings.scrub_ip_addresses = true;
2081
2082 let pii_config = serde_json::from_str(r#"{"applications": {"**": ["@ip:mask"]}}"#).unwrap();
2084
2085 let config = ProjectConfig {
2086 datascrubbing_settings,
2087 pii_config: Some(pii_config),
2088 ..Default::default()
2089 };
2090
2091 let project_info = ProjectInfo {
2092 config,
2093 ..Default::default()
2094 };
2095
2096 let envelope = ManagedEnvelope::new(envelope, outcome_aggregator);
2097
2098 let ctx = processing::Context {
2099 project_info: &project_info,
2100 ..processing::Context::for_test()
2101 };
2102
2103 let new_envelope = process_to_single_envelope(&processor, envelope, ctx).await;
2104
2105 let event_item = new_envelope.items().last().unwrap();
2106 let annotated_event: Annotated<Event> =
2107 Annotated::from_json_bytes(&event_item.payload()).unwrap();
2108 let event = annotated_event.into_value().unwrap();
2109 let headers = event
2110 .request
2111 .into_value()
2112 .unwrap()
2113 .headers
2114 .into_value()
2115 .unwrap();
2116
2117 assert_eq!(
2119 Some(
2120 "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/********* Safari/537.36"
2121 ),
2122 headers.get_header("User-Agent")
2123 );
2124 let contexts = event.contexts.into_value().unwrap();
2126 let browser = contexts.0.get("browser").unwrap();
2127 assert_eq!(
2128 r#"{"browser":"Chrome 103.0.0","name":"Chrome","version":"103.0.0","type":"browser"}"#,
2129 browser.to_json().unwrap()
2130 );
2131 }
2132
2133 #[tokio::test]
2134 #[cfg(feature = "processing")]
2135 async fn test_materialize_dsc() {
2136 use crate::services::projects::project::PublicKeyConfig;
2137
2138 let dsn = "https://e12d836b15bb49d7bbf99e64295d995b:@sentry.io/42"
2139 .parse()
2140 .unwrap();
2141 let request_meta = RequestMeta::new(dsn);
2142 let mut envelope = Envelope::from_request(None, request_meta);
2143
2144 let dsc = r#"{
2145 "trace_id": "00000000-0000-0000-0000-000000000001",
2146 "public_key": "e12d836b15bb49d7bbf99e64295d995b",
2147 "sample_rate": "0.2"
2148 }"#;
2149 envelope.set_dsc(serde_json::from_str(dsc).unwrap());
2150
2151 let mut item = Item::new(ItemType::Event);
2152 item.set_payload(ContentType::Json, r#"{}"#);
2153 envelope.add_item(item);
2154
2155 let outcome_aggregator = Addr::dummy();
2156 let managed_envelope = ManagedEnvelope::new(envelope, outcome_aggregator);
2157
2158 let mut project_info = ProjectInfo::default();
2159 project_info.public_keys.push(PublicKeyConfig {
2160 public_key: ProjectKey::parse("e12d836b15bb49d7bbf99e64295d995b").unwrap(),
2161 numeric_id: Some(1),
2162 });
2163
2164 let config = serde_json::json!({
2165 "processing": {
2166 "enabled": true,
2167 "kafka_config": [],
2168 }
2169 });
2170
2171 let processor =
2172 create_test_processor(Config::from_json_value(config.clone()).unwrap()).await;
2173 let config = Config::from_json_value(config).unwrap();
2174 let ctx = processing::Context {
2175 config: &config,
2176 project_info: &project_info,
2177 sampling_project_info: Some(&project_info),
2178 ..processing::Context::for_test()
2179 };
2180
2181 let envelope = process_to_single_envelope(&processor, managed_envelope, ctx).await;
2182 let event = envelope
2183 .get_item_by(|item| item.ty() == &ItemType::Event)
2184 .unwrap();
2185
2186 let event = Annotated::<Event>::from_json_bytes(&event.payload()).unwrap();
2187 insta::assert_debug_snapshot!(event.value().unwrap()._dsc, @r###"
2188 Object(
2189 {
2190 "environment": ~,
2191 "public_key": String(
2192 "e12d836b15bb49d7bbf99e64295d995b",
2193 ),
2194 "release": ~,
2195 "replay_id": ~,
2196 "sample_rate": String(
2197 "0.2",
2198 ),
2199 "trace_id": String(
2200 "00000000000000000000000000000001",
2201 ),
2202 "transaction": ~,
2203 },
2204 )
2205 "###);
2206 }
2207
2208 fn capture_test_event(transaction_name: &str, source: TransactionSource) -> Vec<String> {
2209 let mut event = Annotated::<Event>::from_json(
2210 r#"
2211 {
2212 "type": "transaction",
2213 "transaction": "/foo/",
2214 "timestamp": 946684810.0,
2215 "start_timestamp": 946684800.0,
2216 "contexts": {
2217 "trace": {
2218 "trace_id": "4c79f60c11214eb38604f4ae0781bfb2",
2219 "span_id": "fa90fdead5f74053",
2220 "op": "http.server",
2221 "type": "trace"
2222 }
2223 },
2224 "transaction_info": {
2225 "source": "url"
2226 }
2227 }
2228 "#,
2229 )
2230 .unwrap();
2231 let e = event.value_mut().as_mut().unwrap();
2232 e.transaction.set_value(Some(transaction_name.into()));
2233
2234 e.transaction_info
2235 .value_mut()
2236 .as_mut()
2237 .unwrap()
2238 .source
2239 .set_value(Some(source));
2240
2241 relay_statsd::with_capturing_test_client(|| {
2242 utils::log_transaction_name_metrics(&mut event, |event| {
2243 let config = NormalizationConfig {
2244 transaction_name_config: TransactionNameConfig {
2245 rules: &[TransactionNameRule {
2246 pattern: LazyGlob::new("/foo/*/**".to_owned()),
2247 expiry: DateTime::<Utc>::MAX_UTC,
2248 redaction: RedactionRule::Replace {
2249 substitution: "*".to_owned(),
2250 },
2251 }],
2252 },
2253 ..Default::default()
2254 };
2255 relay_event_normalization::normalize_event(event, &config)
2256 });
2257 })
2258 }
2259
2260 #[test]
2261 fn test_log_transaction_metrics_none() {
2262 let captures = capture_test_event("/nothing", TransactionSource::Url);
2263 insta::assert_debug_snapshot!(captures, @r###"
2264 [
2265 "event.transaction_name_changes:1|c|#source_in:url,changes:none,source_out:sanitized,is_404:false",
2266 ]
2267 "###);
2268 }
2269
2270 #[test]
2271 fn test_log_transaction_metrics_rule() {
2272 let captures = capture_test_event("/foo/john/denver", TransactionSource::Url);
2273 insta::assert_debug_snapshot!(captures, @r###"
2274 [
2275 "event.transaction_name_changes:1|c|#source_in:url,changes:rule,source_out:sanitized,is_404:false",
2276 ]
2277 "###);
2278 }
2279
2280 #[test]
2281 fn test_log_transaction_metrics_pattern() {
2282 let captures = capture_test_event("/something/12345", TransactionSource::Url);
2283 insta::assert_debug_snapshot!(captures, @r###"
2284 [
2285 "event.transaction_name_changes:1|c|#source_in:url,changes:pattern,source_out:sanitized,is_404:false",
2286 ]
2287 "###);
2288 }
2289
2290 #[test]
2291 fn test_log_transaction_metrics_both() {
2292 let captures = capture_test_event("/foo/john/12345", TransactionSource::Url);
2293 insta::assert_debug_snapshot!(captures, @r###"
2294 [
2295 "event.transaction_name_changes:1|c|#source_in:url,changes:both,source_out:sanitized,is_404:false",
2296 ]
2297 "###);
2298 }
2299
2300 #[test]
2301 fn test_log_transaction_metrics_no_match() {
2302 let captures = capture_test_event("/foo/john/12345", TransactionSource::Route);
2303 insta::assert_debug_snapshot!(captures, @r###"
2304 [
2305 "event.transaction_name_changes:1|c|#source_in:route,changes:none,source_out:route,is_404:false",
2306 ]
2307 "###);
2308 }
2309
2310 #[tokio::test]
2311 async fn test_process_metrics_bucket_metadata() {
2312 let mut token = Cogs::noop().timed(ResourceId::Relay, AppFeature::Unattributed);
2313 let project_key = ProjectKey::parse("a94ae32be2584e0bbd7a4cbb95971fee").unwrap();
2314 let received_at = Utc::now();
2315 let config = Config::default();
2316
2317 let (aggregator, mut aggregator_rx) = Addr::custom();
2318 let processor = create_test_processor_with_addrs(
2319 config,
2320 Addrs {
2321 aggregator,
2322 ..Default::default()
2323 },
2324 )
2325 .await;
2326
2327 let mut item = Item::new(ItemType::Statsd);
2328 item.set_payload(ContentType::Text, "spans/foo:3182887624:4267882815|s");
2329 for (source, expected_received_at) in [
2330 (
2331 BucketSource::External,
2332 Some(UnixTimestamp::from_datetime(received_at).unwrap()),
2333 ),
2334 (BucketSource::Internal, None),
2335 ] {
2336 let message = ProcessMetrics {
2337 data: MetricData::Raw(vec![item.clone()]),
2338 project_key,
2339 source,
2340 received_at,
2341 sent_at: Some(Utc::now()),
2342 };
2343 processor.handle_process_metrics(&mut token, message);
2344
2345 let Aggregator::MergeBuckets(merge_buckets) = aggregator_rx.recv().await.unwrap();
2346 let buckets = merge_buckets.buckets;
2347 assert_eq!(buckets.len(), 1);
2348 assert_eq!(buckets[0].metadata.received_at, expected_received_at);
2349 }
2350 }
2351
2352 #[tokio::test]
2353 async fn test_process_batched_metrics() {
2354 let mut token = Cogs::noop().timed(ResourceId::Relay, AppFeature::Unattributed);
2355 let received_at = Utc::now();
2356 let config = Config::default();
2357
2358 let (aggregator, mut aggregator_rx) = Addr::custom();
2359 let processor = create_test_processor_with_addrs(
2360 config,
2361 Addrs {
2362 aggregator,
2363 ..Default::default()
2364 },
2365 )
2366 .await;
2367
2368 let payload = r#"{
2369 "buckets": {
2370 "11111111111111111111111111111111": [
2371 {
2372 "timestamp": 1615889440,
2373 "width": 0,
2374 "name": "d:transactions/endpoint.response_time@millisecond",
2375 "type": "d",
2376 "value": [
2377 68.0
2378 ],
2379 "tags": {
2380 "route": "user_index"
2381 }
2382 }
2383 ],
2384 "22222222222222222222222222222222": [
2385 {
2386 "timestamp": 1615889440,
2387 "width": 0,
2388 "name": "d:transactions/endpoint.cache_rate@none",
2389 "type": "d",
2390 "value": [
2391 36.0
2392 ]
2393 }
2394 ]
2395 }
2396}
2397"#;
2398 let message = ProcessBatchedMetrics {
2399 payload: Bytes::from(payload),
2400 source: BucketSource::Internal,
2401 received_at,
2402 sent_at: Some(Utc::now()),
2403 };
2404 processor.handle_process_batched_metrics(&mut token, message);
2405
2406 let Aggregator::MergeBuckets(mb1) = aggregator_rx.recv().await.unwrap();
2407 let Aggregator::MergeBuckets(mb2) = aggregator_rx.recv().await.unwrap();
2408
2409 let mut messages = vec![mb1, mb2];
2410 messages.sort_by_key(|mb| mb.project_key);
2411
2412 let actual = messages
2413 .into_iter()
2414 .map(|mb| (mb.project_key, mb.buckets))
2415 .collect::<Vec<_>>();
2416
2417 assert_debug_snapshot!(actual, @r###"
2418 [
2419 (
2420 ProjectKey("11111111111111111111111111111111"),
2421 [
2422 Bucket {
2423 timestamp: UnixTimestamp(1615889440),
2424 width: 0,
2425 name: MetricName(
2426 "d:transactions/endpoint.response_time@millisecond",
2427 ),
2428 value: Distribution(
2429 [
2430 68.0,
2431 ],
2432 ),
2433 tags: {
2434 "route": "user_index",
2435 },
2436 metadata: BucketMetadata {
2437 merges: 1,
2438 received_at: None,
2439 extracted_from_indexed: false,
2440 },
2441 },
2442 ],
2443 ),
2444 (
2445 ProjectKey("22222222222222222222222222222222"),
2446 [
2447 Bucket {
2448 timestamp: UnixTimestamp(1615889440),
2449 width: 0,
2450 name: MetricName(
2451 "d:transactions/endpoint.cache_rate@none",
2452 ),
2453 value: Distribution(
2454 [
2455 36.0,
2456 ],
2457 ),
2458 tags: {},
2459 metadata: BucketMetadata {
2460 merges: 1,
2461 received_at: None,
2462 extracted_from_indexed: false,
2463 },
2464 },
2465 ],
2466 ),
2467 ]
2468 "###);
2469 }
2470}