Skip to main content

relay_server/services/
store.rs

1//! This module contains the service that forwards events and attachments to the Sentry store.
2//! The service uses Kafka topics to forward data to Sentry
3
4use std::borrow::Cow;
5use std::collections::BTreeMap;
6use std::error::Error;
7use std::pin::Pin;
8use std::sync::Arc;
9use std::task;
10
11use bytes::Bytes;
12use chrono::{DateTime, SecondsFormat, Utc};
13use prost::Message as _;
14use relay_base_schema::events::EventType;
15use sentry_protos::snuba::v1::{TraceItem, TraceItemType};
16use serde::Serialize;
17use uuid::Uuid;
18
19use relay_base_schema::data_category::DataCategory;
20use relay_base_schema::organization::OrganizationId;
21use relay_base_schema::project::ProjectId;
22use relay_common::time::UnixTimestamp;
23use relay_config::Config;
24use relay_event_schema::protocol::{Event, EventId, SpanV2, datetime_to_timestamp};
25use relay_kafka::{ClientError, KafkaClient, KafkaTopic, Message, SerializationOutput};
26use relay_metrics::{
27    Bucket, BucketView, BucketViewValue, BucketsView, ByNamespace, GaugeValue, MetricName,
28    MetricNamespace, SetView,
29};
30use relay_protocol::{Annotated, FiniteF64, SerializableAnnotated};
31use relay_quotas::Scoping;
32use relay_statsd::metric;
33use relay_system::{FromMessage, Interface, NoResponse, Service};
34use relay_threading::AsyncPool;
35
36use crate::envelope::{AttachmentPlaceholder, AttachmentType, ContentType, Item};
37use crate::managed::{Counted, Managed, OutcomeError, Quantities, Rejected};
38use crate::metrics::{ArrayEncoding, BucketEncoder, MetricOutcomes};
39
40use crate::service::ServiceError;
41use crate::services::global_config::GlobalConfigHandle;
42use crate::services::objectstore::ObjectstoreKey;
43use crate::services::outcome::{self, DiscardReason, Outcome, OutcomeId};
44use crate::services::upload::{Final, SignedLocation};
45use crate::statsd::{RelayCounters, RelayGauges, RelayTimers};
46use crate::utils;
47
48mod sessions;
49
50/// Fallback name used for attachment items without a `filename` header.
51const UNNAMED_ATTACHMENT: &str = "Unnamed Attachment";
52
53#[derive(Debug, thiserror::Error)]
54pub enum StoreError {
55    #[error("failed to send the message to kafka: {0}")]
56    SendFailed(#[from] ClientError),
57    #[error("failed to encode data: {0}")]
58    EncodingFailed(std::io::Error),
59    #[error("failed to serialize data: {0}")]
60    Serialize(#[from] serde_json::Error),
61    #[error("failed to store event because event id was missing")]
62    NoEventId,
63    #[error("invalid attachment reference")]
64    InvalidAttachmentRef,
65}
66
67impl OutcomeError for StoreError {
68    type Error = Self;
69
70    fn consume(self) -> (Option<Outcome>, Self::Error) {
71        let outcome = match self {
72            StoreError::SendFailed(_)
73            | StoreError::EncodingFailed(_)
74            | StoreError::Serialize(_)
75            | StoreError::NoEventId => Some(Outcome::Invalid(DiscardReason::Internal)),
76            StoreError::InvalidAttachmentRef => {
77                Some(Outcome::Invalid(DiscardReason::InvalidAttachmentRef))
78            }
79        };
80        (outcome, self)
81    }
82}
83
84struct Producer {
85    client: KafkaClient,
86}
87
88impl Producer {
89    pub fn create(config: &Config) -> anyhow::Result<Self> {
90        let mut client_builder = KafkaClient::builder();
91
92        for topic in KafkaTopic::iter() {
93            let kafka_configs = config.kafka_configs(*topic)?;
94            client_builder = client_builder
95                .add_kafka_topic_config(*topic, &kafka_configs, config.kafka_validate_topics())
96                .map_err(|e| ServiceError::Kafka(e.to_string()))?;
97        }
98
99        Ok(Self {
100            client: client_builder.build(),
101        })
102    }
103}
104
105/// Publishes an [`Event`] and its attachments to Kafka.
106#[derive(Debug)]
107pub struct StoreEvent {
108    /// The data category the [`Self::event`] is counted in.
109    pub event_category: DataCategory,
110    /// The event to be stored.
111    pub event: Annotated<Event>,
112    /// A list of attachments associated with the event.
113    pub attachments: Vec<Item>,
114    /// A list of user reports associated with the event.
115    pub user_reports: Vec<Item>,
116    /// Event retention in days.
117    pub retention_days: u16,
118}
119
120impl Counted for StoreEvent {
121    fn quantities(&self) -> Quantities {
122        let mut quantities = smallvec::smallvec![(self.event_category, 1)];
123        quantities.extend(self.attachments.quantities());
124        quantities.extend(self.user_reports.quantities());
125        quantities
126    }
127}
128
129/// Publishes a list of [`Bucket`]s to the Sentry core application through Kafka topics.
130#[derive(Clone, Debug)]
131pub struct StoreMetrics {
132    pub buckets: Vec<Bucket>,
133    pub scoping: Scoping,
134    pub retention: u16,
135}
136
137/// Publishes a log item to the Sentry core application through Kafka.
138#[derive(Debug)]
139pub struct StoreTraceItem {
140    /// The final trace item which will be produced to Kafka.
141    pub trace_item: TraceItem,
142}
143
144impl Counted for StoreTraceItem {
145    fn quantities(&self) -> Quantities {
146        self.trace_item.quantities()
147    }
148}
149
150/// Publishes a span item to the Sentry core application through Kafka.
151#[derive(Debug)]
152pub struct StoreSpanV2 {
153    /// Routing key to assign a Kafka partition.
154    pub routing_key: Option<Uuid>,
155    /// Default retention of the span.
156    pub retention_days: u16,
157    /// Downsampled retention of the span.
158    pub downsampled_retention_days: u16,
159    /// Optional event id, if the span was extracted from a transaction.
160    pub event_id: Option<EventId>,
161    /// The final Sentry compatible span item.
162    pub item: SpanV2,
163    // Whether to run issue detection on the transaction or on the segment span.
164    // (only true for segment spans created from a transaction).
165    pub performance_issues_spans: bool,
166}
167
168impl Counted for StoreSpanV2 {
169    fn quantities(&self) -> Quantities {
170        smallvec::smallvec![(DataCategory::SpanIndexed, 1)]
171    }
172}
173
174/// Publishes a singular profile chunk to Kafka.
175#[derive(Debug)]
176pub struct StoreProfileChunk {
177    /// Default retention of the span.
178    pub retention_days: u16,
179    /// The serialized profile chunk payload.
180    pub payload: Bytes,
181    /// Additional attachments associated with this profile chunk.
182    pub attachments: Vec<ProfileAttachment>,
183    /// Outcome quantities associated with this profile.
184    ///
185    /// Quantities are different for backend and ui profile chunks.
186    pub quantities: Quantities,
187}
188
189/// Optional raw binary blob associated with [`profile chunk`](StoreProfileChunk).
190#[derive(Debug)]
191pub struct ProfileAttachment {
192    /// Name of the attachment,
193    pub name: String,
194    /// Content type of the attachment.
195    pub content_type: ContentType,
196    /// Objectstore id of the attachment.
197    ///
198    /// Using this id the attachment can be retrieved again.
199    pub stored_id: ObjectstoreKey,
200}
201
202impl Counted for StoreProfileChunk {
203    fn quantities(&self) -> Quantities {
204        self.quantities.clone()
205    }
206}
207
208/// A replay to be stored to Kafka.
209#[derive(Debug)]
210pub struct StoreReplay {
211    /// The event ID.
212    pub event_id: EventId,
213    /// Number of days to retain.
214    pub retention_days: u16,
215    /// The recording payload (rrweb data).
216    pub recording: Bytes,
217    /// Optional replay event payload (JSON).
218    pub event: Option<Bytes>,
219    /// Optional replay video.
220    pub video: Option<Bytes>,
221    /// Outcome quantities associated with this replay.
222    ///
223    /// Quantities are different for web and native replays.
224    pub quantities: Quantities,
225}
226
227impl Counted for StoreReplay {
228    fn quantities(&self) -> Quantities {
229        self.quantities.clone()
230    }
231}
232
233/// An attachment to be stored to Kafka.
234#[derive(Debug)]
235pub struct StoreAttachment {
236    /// The event ID.
237    pub event_id: EventId,
238    /// That attachment item.
239    pub attachment: Item,
240    /// Outcome quantities associated with this attachment.
241    pub quantities: Quantities,
242    /// Data retention in days for this attachment.
243    pub retention: u16,
244}
245
246impl Counted for StoreAttachment {
247    fn quantities(&self) -> Quantities {
248        self.quantities.clone()
249    }
250}
251
252/// A user report to be stored to Kafka.
253#[derive(Debug)]
254pub struct StoreUserReport {
255    /// The event ID.
256    pub event_id: EventId,
257    /// The user report.
258    pub report: Item,
259}
260
261impl Counted for StoreUserReport {
262    fn quantities(&self) -> Quantities {
263        smallvec::smallvec![(DataCategory::UserReportV2, 1)]
264    }
265}
266
267/// A profile to be stored to Kafka.
268#[derive(Debug)]
269pub struct StoreProfile {
270    /// Number of days to retain.
271    pub retention_days: u16,
272    /// The profile.
273    pub profile: Item,
274    /// Outcome quantities associated with this profile.
275    pub quantities: Quantities,
276}
277
278impl Counted for StoreProfile {
279    fn quantities(&self) -> Quantities {
280        self.quantities.clone()
281    }
282}
283
284/// A monitor check-in to be stored to Kafka.
285#[derive(Debug)]
286pub struct StoreCheckIn {
287    /// The serialized check-in.
288    pub check_in: Item,
289    /// The SDK client which produced the check-in.
290    pub sdk: Option<String>,
291    /// Check-in retention in days.
292    pub retention_days: u16,
293}
294
295impl Counted for StoreCheckIn {
296    fn quantities(&self) -> Quantities {
297        self.check_in.quantities()
298    }
299}
300
301/// The asynchronous thread pool used for scheduling storing tasks in the envelope store.
302pub type StoreServicePool = AsyncPool<StoreTask>;
303
304/// Service interface for storing items in Kafka.
305#[derive(Debug)]
306pub enum Store {
307    /// An [`Event`] and its associated items.
308    Event(Managed<Box<StoreEvent>>),
309    /// Aggregated generic metrics.
310    Metrics(StoreMetrics),
311    /// A singular [`TraceItem`].
312    TraceItem(Managed<StoreTraceItem>),
313    /// A singular Span.
314    Span(Managed<Box<StoreSpanV2>>),
315    /// A singular profile chunk.
316    ProfileChunk(Managed<StoreProfileChunk>),
317    /// A singular replay.
318    Replay(Managed<StoreReplay>),
319    /// A singular attachment.
320    Attachment(Managed<StoreAttachment>),
321    /// A singular user report.
322    UserReport(Managed<StoreUserReport>),
323    /// A single profile.
324    Profile(Managed<StoreProfile>),
325    /// A singular monitor check-in.
326    CheckIn(Managed<StoreCheckIn>),
327}
328
329impl Store {
330    /// Returns the name of the message variant.
331    fn variant(&self) -> &'static str {
332        match self {
333            Store::Event(_) => "event",
334            Store::Metrics(_) => "metrics",
335            Store::TraceItem(_) => "trace_item",
336            Store::Span(_) => "span",
337            Store::ProfileChunk(_) => "profile_chunk",
338            Store::Replay(_) => "replay",
339            Store::Attachment(_) => "attachment",
340            Store::UserReport(_) => "user_report",
341            Store::Profile(_) => "profile",
342            Store::CheckIn(_) => "check_in",
343        }
344    }
345}
346
347impl Interface for Store {}
348
349impl FromMessage<Managed<Box<StoreEvent>>> for Store {
350    type Response = NoResponse;
351
352    fn from_message(message: Managed<Box<StoreEvent>>, _: ()) -> Self {
353        Self::Event(message)
354    }
355}
356
357impl FromMessage<StoreMetrics> for Store {
358    type Response = NoResponse;
359
360    fn from_message(message: StoreMetrics, _: ()) -> Self {
361        Self::Metrics(message)
362    }
363}
364
365impl FromMessage<Managed<StoreTraceItem>> for Store {
366    type Response = NoResponse;
367
368    fn from_message(message: Managed<StoreTraceItem>, _: ()) -> Self {
369        Self::TraceItem(message)
370    }
371}
372
373impl FromMessage<Managed<Box<StoreSpanV2>>> for Store {
374    type Response = NoResponse;
375
376    fn from_message(message: Managed<Box<StoreSpanV2>>, _: ()) -> Self {
377        Self::Span(message)
378    }
379}
380
381impl FromMessage<Managed<StoreProfileChunk>> for Store {
382    type Response = NoResponse;
383
384    fn from_message(message: Managed<StoreProfileChunk>, _: ()) -> Self {
385        Self::ProfileChunk(message)
386    }
387}
388
389impl FromMessage<Managed<StoreReplay>> for Store {
390    type Response = NoResponse;
391
392    fn from_message(message: Managed<StoreReplay>, _: ()) -> Self {
393        Self::Replay(message)
394    }
395}
396
397impl FromMessage<Managed<StoreAttachment>> for Store {
398    type Response = NoResponse;
399
400    fn from_message(message: Managed<StoreAttachment>, _: ()) -> Self {
401        Self::Attachment(message)
402    }
403}
404
405impl FromMessage<Managed<StoreUserReport>> for Store {
406    type Response = NoResponse;
407
408    fn from_message(message: Managed<StoreUserReport>, _: ()) -> Self {
409        Self::UserReport(message)
410    }
411}
412
413impl FromMessage<Managed<StoreProfile>> for Store {
414    type Response = NoResponse;
415
416    fn from_message(message: Managed<StoreProfile>, _: ()) -> Self {
417        Self::Profile(message)
418    }
419}
420
421impl FromMessage<Managed<StoreCheckIn>> for Store {
422    type Response = NoResponse;
423
424    fn from_message(message: Managed<StoreCheckIn>, _: ()) -> Self {
425        Self::CheckIn(message)
426    }
427}
428
429/// Service implementing the [`Store`] interface.
430pub struct StoreService {
431    pool: StoreServicePool,
432    config: Arc<Config>,
433    global_config: GlobalConfigHandle,
434    metric_outcomes: MetricOutcomes,
435    producer: Producer,
436}
437
438impl StoreService {
439    pub fn create(
440        pool: StoreServicePool,
441        config: Arc<Config>,
442        global_config: GlobalConfigHandle,
443        metric_outcomes: MetricOutcomes,
444    ) -> anyhow::Result<Self> {
445        let producer = Producer::create(&config)?;
446        Ok(Self {
447            pool,
448            config,
449            global_config,
450            metric_outcomes,
451            producer,
452        })
453    }
454
455    fn handle_message(&self, message: Store) {
456        let ty = message.variant();
457        relay_statsd::metric!(timer(RelayTimers::StoreServiceDuration), message = ty, {
458            let result = match message {
459                Store::Event(message) => self.handle_store_event(message),
460                Store::Metrics(message) => {
461                    self.handle_store_metrics(message);
462                    Ok(())
463                }
464                Store::TraceItem(message) => self.handle_store_trace_item(message),
465                Store::Span(message) => self.handle_store_span(message),
466                Store::ProfileChunk(message) => self.handle_store_profile_chunk(message),
467                Store::Replay(message) => self.handle_store_replay(message),
468                Store::Attachment(message) => self.handle_store_attachment(message),
469                Store::UserReport(message) => self.handle_user_report(message),
470                Store::Profile(message) => self.handle_profile(message),
471                Store::CheckIn(message) => self.handle_check_in(message),
472            };
473            if let Err(error) = result {
474                relay_log::error!(
475                    error = &error as &dyn Error,
476                    tags.message = ty,
477                    "failed to store message"
478                );
479            }
480        })
481    }
482
483    fn handle_store_event(
484        &self,
485        message: Managed<Box<StoreEvent>>,
486    ) -> Result<(), Rejected<StoreError>> {
487        let received_at = message.received_at();
488        let scoping = message.scoping();
489        let remote_addr = message.remote_addr().map(|ip| ip.to_string());
490
491        message.try_accept(|m| self.do_store_event(*m, scoping, received_at, remote_addr))
492    }
493
494    fn do_store_event(
495        &self,
496        store: StoreEvent,
497        scoping: Scoping,
498        received_at: DateTime<Utc>,
499        remote_addr: Option<String>,
500    ) -> Result<(), StoreError> {
501        let event_id = store.event.value().and_then(|e| e.id.value()).copied();
502        let event_id = event_id.ok_or(StoreError::NoEventId)?;
503
504        let event_type = store.event.value().and_then(|e| e.ty.value());
505        let send_individual_attachments = matches!(
506            event_type,
507            Some(&EventType::Transaction) | Some(&EventType::UserReportV2)
508        );
509
510        let mut attachments = Vec::new();
511        for attachment in store.attachments {
512            // Note: Technically when an error occurs we may have already produced some items to Kafka,
513            // but since we don't keep track of that properly and just error out here, outcomes will
514            // also report items which were already produced to Kafka as dropped.
515            //
516            // We specifically accept this here, since this is an extreme edge case with minimal practical
517            // consequences.
518            if let Some(attachment) = self.produce_attachment(
519                event_id,
520                scoping.project_id,
521                scoping.organization_id,
522                &attachment,
523                send_individual_attachments,
524                store.retention_days,
525            )? {
526                attachments.push(attachment);
527            }
528        }
529
530        for user_report in &store.user_reports {
531            self.produce_user_report(
532                event_id,
533                scoping.project_id,
534                scoping.organization_id,
535                received_at,
536                user_report,
537            )?;
538        }
539
540        let event_topic = if event_type == Some(&EventType::Transaction) {
541            KafkaTopic::Transactions
542        } else if event_type == Some(&EventType::UserReportV2) {
543            KafkaTopic::Feedback
544        } else if !attachments.is_empty() || !store.user_reports.is_empty() {
545            KafkaTopic::Attachments
546        } else {
547            KafkaTopic::Events
548        };
549
550        let payload = store.event.to_json()?.into_bytes().into();
551        self.produce(
552            event_topic,
553            KafkaMessage::Event(EventKafkaMessage {
554                payload,
555                start_time: safe_timestamp(received_at),
556                event_id,
557                project_id: scoping.project_id,
558                org_id: scoping.organization_id,
559                remote_addr,
560                attachments,
561            }),
562        )
563    }
564
565    fn handle_store_metrics(&self, message: StoreMetrics) {
566        let StoreMetrics {
567            buckets,
568            scoping,
569            retention,
570        } = message;
571
572        let batch_size = self.config.metrics_max_batch_size_bytes();
573        let mut error = None;
574
575        let global_config = self.global_config.current().unwrap_or_default();
576        let mut encoder = BucketEncoder::new(&global_config);
577
578        let emit_sessions_to_eap = utils::is_rolled_out(
579            scoping.organization_id.value(),
580            global_config.options.sessions_eap_rollout_rate,
581        )
582        .is_keep();
583
584        let now = UnixTimestamp::now();
585        let mut delay_stats = ByNamespace::<(u64, u64, u64)>::default();
586
587        for mut bucket in buckets {
588            let namespace = encoder.prepare(&mut bucket);
589
590            if let Some(received_at) = bucket.metadata.received_at {
591                let delay = now.as_secs().saturating_sub(received_at.as_secs());
592                let (total, count, max) = delay_stats.get_mut(namespace);
593                *total += delay;
594                *count += 1;
595                *max = (*max).max(delay);
596            }
597
598            // Create a local bucket view to avoid splitting buckets unnecessarily. Since we produce
599            // each bucket separately, we only need to split buckets that exceed the size, but not
600            // batches.
601            for view in BucketsView::new(std::slice::from_ref(&bucket))
602                .by_size(batch_size)
603                .flatten()
604            {
605                let message =
606                    self.create_metric_message(&scoping, &mut encoder, namespace, &view, retention);
607
608                let result =
609                    message.and_then(|message| self.send_metric_message(namespace, message));
610
611                let outcome = match result {
612                    Ok(()) => Outcome::Accepted,
613                    Err(e) => {
614                        error.get_or_insert(e);
615                        Outcome::Invalid(DiscardReason::Internal)
616                    }
617                };
618
619                self.metric_outcomes.track(scoping, &[view], outcome);
620            }
621
622            if emit_sessions_to_eap
623                && let Some(trace_item) = sessions::to_trace_item(scoping, bucket, retention)
624            {
625                let message = KafkaMessage::for_item(scoping, trace_item);
626                let res = self.produce(KafkaTopic::Items, message);
627                if let Err(error) = res {
628                    relay_log::error!(
629                        error = &error as &dyn std::error::Error,
630                        "failed to produce session metrics to EAP"
631                    )
632                }
633            }
634        }
635
636        if let Some(error) = error {
637            relay_log::error!(
638                error = &error as &dyn std::error::Error,
639                "failed to produce metric buckets: {error}"
640            );
641        }
642
643        for (namespace, (total, count, max)) in delay_stats {
644            if count == 0 {
645                continue;
646            }
647            metric!(
648                counter(RelayCounters::MetricDelaySum) += total,
649                namespace = namespace.as_str()
650            );
651            metric!(
652                counter(RelayCounters::MetricDelayCount) += count,
653                namespace = namespace.as_str()
654            );
655            metric!(
656                gauge(RelayGauges::MetricDelayMax) = max,
657                namespace = namespace.as_str()
658            );
659        }
660    }
661
662    fn handle_store_trace_item(
663        &self,
664        message: Managed<StoreTraceItem>,
665    ) -> Result<(), Rejected<StoreError>> {
666        let scoping = message.scoping();
667
668        message.try_accept(|item| {
669            let message = KafkaMessage::for_item(scoping, item.trace_item);
670            self.produce(KafkaTopic::Items, message)
671        })?;
672
673        Ok(())
674    }
675
676    fn handle_store_span(
677        &self,
678        message: Managed<Box<StoreSpanV2>>,
679    ) -> Result<(), Rejected<StoreError>> {
680        let scoping = message.scoping();
681        let received_at = message.received_at();
682
683        let meta = SpanMeta {
684            organization_id: scoping.organization_id,
685            project_id: scoping.project_id,
686            key_id: scoping.key_id,
687            event_id: message.event_id,
688            retention_days: message.retention_days,
689            downsampled_retention_days: message.downsampled_retention_days,
690            received: datetime_to_timestamp(received_at),
691            performance_issues_spans: message.performance_issues_spans,
692        };
693
694        message.try_accept(|span| {
695            let item = Annotated::new(span.item);
696            let message = KafkaMessage::SpanV2 {
697                routing_key: span.routing_key,
698                headers: BTreeMap::from([(
699                    "project_id".to_owned(),
700                    scoping.project_id.to_string(),
701                )]),
702                message: SpanKafkaMessage {
703                    meta,
704                    span: SerializableAnnotated(&item),
705                },
706                org_id: scoping.organization_id,
707            };
708
709            self.produce(KafkaTopic::Spans, message)
710        })?;
711
712        relay_statsd::metric!(
713            counter(RelayCounters::SpanV2Produced) += 1,
714            via = "processing"
715        );
716
717        Ok(())
718    }
719
720    fn handle_store_profile_chunk(
721        &self,
722        message: Managed<StoreProfileChunk>,
723    ) -> Result<(), Rejected<StoreError>> {
724        let scoping = message.scoping();
725        let received_at = message.received_at();
726
727        message.try_accept(|message| {
728            let message = ProfileChunkKafkaMessage {
729                organization_id: scoping.organization_id,
730                project_id: scoping.project_id,
731                received: safe_timestamp(received_at),
732                retention_days: message.retention_days,
733                headers: BTreeMap::from([(
734                    "project_id".to_owned(),
735                    scoping.project_id.to_string(),
736                )]),
737                payload: message.payload,
738                attachments: message
739                    .attachments
740                    .into_iter()
741                    .map(|attachment| ProfileChunkKafkaAttachment {
742                        name: attachment.name,
743                        content_type: attachment.content_type.as_str(),
744                        stored_id: attachment.stored_id.into_inner(),
745                    })
746                    .collect(),
747            };
748
749            self.produce(KafkaTopic::Profiles, KafkaMessage::ProfileChunk(message))
750        })
751    }
752
753    fn handle_store_replay(
754        &self,
755        message: Managed<StoreReplay>,
756    ) -> Result<(), Rejected<StoreError>> {
757        let scoping = message.scoping();
758        let received_at = message.received_at();
759
760        message.try_accept(|replay| {
761            let kafka_msg =
762                KafkaMessage::ReplayRecordingNotChunked(ReplayRecordingNotChunkedKafkaMessage {
763                    replay_id: replay.event_id,
764                    key_id: scoping.key_id,
765                    org_id: scoping.organization_id,
766                    project_id: scoping.project_id,
767                    received: safe_timestamp(received_at),
768                    retention_days: replay.retention_days,
769                    payload: &replay.recording,
770                    replay_event: replay.event.as_deref(),
771                    replay_video: replay.video.as_deref(),
772                    // Hardcoded to `true` to indicate to the consumer that it should always publish the
773                    // replay_event as relay no longer does it.
774                    relay_snuba_publish_disabled: true,
775                });
776            self.produce(KafkaTopic::ReplayRecordings, kafka_msg)
777        })
778    }
779
780    fn handle_store_attachment(
781        &self,
782        message: Managed<StoreAttachment>,
783    ) -> Result<(), Rejected<StoreError>> {
784        let scoping = message.scoping();
785        message.try_accept(|attachment| {
786            let result = self.produce_attachment(
787                attachment.event_id,
788                scoping.project_id,
789                scoping.organization_id,
790                &attachment.attachment,
791                // Hardcoded to `true` since standalone attachments are 'individual attachments'.
792                true,
793                attachment.retention,
794            );
795            // Since we are sending an 'individual attachment' this function should never return a
796            // `ChunkedAttachment`.
797            debug_assert!(!matches!(result, Ok(Some(_))));
798            result.map(|_| ())
799        })
800    }
801
802    fn handle_user_report(
803        &self,
804        message: Managed<StoreUserReport>,
805    ) -> Result<(), Rejected<StoreError>> {
806        let scoping = message.scoping();
807        let received_at = message.received_at();
808
809        message.try_accept(|report| {
810            let kafka_msg = KafkaMessage::UserReport(UserReportKafkaMessage {
811                project_id: scoping.project_id,
812                event_id: report.event_id,
813                start_time: safe_timestamp(received_at),
814                payload: report.report.payload(),
815                org_id: scoping.organization_id,
816            });
817            self.produce(KafkaTopic::Attachments, kafka_msg)
818        })
819    }
820
821    fn handle_profile(&self, message: Managed<StoreProfile>) -> Result<(), Rejected<StoreError>> {
822        let scoping = message.scoping();
823        let received_at = message.received_at();
824
825        message.try_accept(|profile| {
826            self.produce_profile(
827                scoping.organization_id,
828                scoping.project_id,
829                scoping.key_id,
830                received_at,
831                profile.retention_days,
832                &profile.profile,
833            )
834        })
835    }
836
837    fn handle_check_in(&self, message: Managed<StoreCheckIn>) -> Result<(), Rejected<StoreError>> {
838        let scoping = message.scoping();
839        let received_at = message.received_at();
840
841        message.try_accept(|check_in| {
842            let message = KafkaMessage::CheckIn(CheckInKafkaMessage {
843                message_type: CheckInMessageType::CheckIn,
844                project_id: scoping.project_id,
845                org_id: scoping.organization_id,
846                retention_days: check_in.retention_days,
847                start_time: safe_timestamp(received_at),
848                sdk: check_in.sdk,
849                payload: check_in.check_in.payload(),
850                routing_key_hint: check_in.check_in.routing_hint(),
851            });
852
853            self.produce(KafkaTopic::Monitors, message)
854        })
855    }
856
857    fn create_metric_message<'a>(
858        &self,
859        scoping: &Scoping,
860        encoder: &'a mut BucketEncoder,
861        namespace: MetricNamespace,
862        view: &BucketView<'a>,
863        retention_days: u16,
864    ) -> Result<MetricKafkaMessage<'a>, StoreError> {
865        let value = match view.value() {
866            BucketViewValue::Counter(c) => MetricValue::Counter(c),
867            BucketViewValue::Distribution(data) => MetricValue::Distribution(
868                encoder
869                    .encode_distribution(namespace, data)
870                    .map_err(StoreError::EncodingFailed)?,
871            ),
872            BucketViewValue::Set(data) => MetricValue::Set(
873                encoder
874                    .encode_set(namespace, data)
875                    .map_err(StoreError::EncodingFailed)?,
876            ),
877            BucketViewValue::Gauge(g) => MetricValue::Gauge(g),
878        };
879
880        Ok(MetricKafkaMessage {
881            org_id: scoping.organization_id,
882            project_id: scoping.project_id,
883            key_id: scoping.key_id,
884            name: view.name(),
885            value,
886            timestamp: view.timestamp(),
887            tags: view.tags(),
888            retention_days,
889            received_at: view.metadata().received_at,
890        })
891    }
892
893    fn produce(
894        &self,
895        topic: KafkaTopic,
896        // Takes message by value to ensure it is not being produced twice.
897        message: KafkaMessage,
898    ) -> Result<(), StoreError> {
899        relay_log::trace!(
900            "Sending kafka message of type {} to {topic:?}",
901            message.variant()
902        );
903
904        let topic_name = self.producer.client.send_message(topic, &message)?;
905
906        match &message {
907            KafkaMessage::Metric {
908                message: metric, ..
909            } => {
910                metric!(
911                    counter(RelayCounters::ProcessingMessageProduced) += 1,
912                    event_type = message.variant(),
913                    topic = topic_name,
914                    metric_type = metric.value.variant(),
915                    metric_encoding = metric.value.encoding().unwrap_or(""),
916                );
917            }
918            KafkaMessage::ReplayRecordingNotChunked(replay) => {
919                let has_video = replay.replay_video.is_some();
920
921                metric!(
922                    counter(RelayCounters::ProcessingMessageProduced) += 1,
923                    event_type = message.variant(),
924                    topic = topic_name,
925                    has_video = bool_to_str(has_video),
926                );
927            }
928            message => {
929                metric!(
930                    counter(RelayCounters::ProcessingMessageProduced) += 1,
931                    event_type = message.variant(),
932                    topic = topic_name,
933                );
934            }
935        }
936
937        Ok(())
938    }
939
940    fn chunked_attachment_from_placeholder(
941        &self,
942        item: &Item,
943        retention_days: u16,
944    ) -> Result<ChunkedAttachment, StoreError> {
945        debug_assert!(
946            item.stored_key().is_none(),
947            "AttachmentRef should not have been uploaded to objectstore"
948        );
949
950        let payload = item.payload();
951        let placeholder: AttachmentPlaceholder<'_> =
952            serde_json::from_slice(&payload).map_err(|_| StoreError::InvalidAttachmentRef)?;
953        let location = SignedLocation::<Final>::try_from_str(placeholder.location)
954            .ok_or(StoreError::InvalidAttachmentRef)?
955            .verify(Utc::now(), &self.config)
956            .map_err(|_| StoreError::InvalidAttachmentRef)?;
957
958        let store_key = location.key;
959
960        Ok(ChunkedAttachment {
961            id: Uuid::new_v4().to_string(),
962            name: item.filename().unwrap_or(UNNAMED_ATTACHMENT).to_owned(),
963            rate_limited: item.rate_limited(),
964            content_type: placeholder.content_type,
965            attachment_type: item.attachment_type().unwrap_or_default(),
966            size: item.attachment_body_size(),
967            retention_days,
968            payload: AttachmentPayload::Stored(store_key),
969        })
970    }
971
972    fn chunked_attachment_from_attachment(
973        &self,
974        event_id: EventId,
975        project_id: ProjectId,
976        org_id: OrganizationId,
977        item: &Item,
978        send_individual_attachments: bool,
979        retention_days: u16,
980    ) -> Result<ChunkedAttachment, StoreError> {
981        let id = Uuid::new_v4().to_string();
982
983        let payload = item.payload();
984        let size = item.len();
985        let max_chunk_size = self.config.attachment_chunk_size();
986
987        let payload = if size == 0 {
988            AttachmentPayload::Chunked(0)
989        } else if let Some(stored_key) = item.stored_key() {
990            AttachmentPayload::Stored(stored_key.into())
991        } else if send_individual_attachments && size < max_chunk_size {
992            // When sending individual attachments, and we have a single chunk, we want to send the
993            // `data` inline in the `attachment` message.
994            // This avoids a needless roundtrip through the attachments cache on the Sentry side.
995            AttachmentPayload::Inline(payload)
996        } else {
997            let mut chunk_index = 0;
998            let mut offset = 0;
999            // This skips chunks for empty attachments. The consumer does not require chunks for
1000            // empty attachments. `chunks` will be `0` in this case.
1001            while offset < size {
1002                let chunk_size = std::cmp::min(max_chunk_size, size - offset);
1003                let chunk_message = AttachmentChunkKafkaMessage {
1004                    payload: payload.slice(offset..offset + chunk_size),
1005                    event_id,
1006                    project_id,
1007                    id: id.clone(),
1008                    chunk_index,
1009                    org_id,
1010                };
1011
1012                self.produce(
1013                    KafkaTopic::Attachments,
1014                    KafkaMessage::AttachmentChunk(chunk_message),
1015                )?;
1016                offset += chunk_size;
1017                chunk_index += 1;
1018            }
1019
1020            // The chunk_index is incremented after every loop iteration. After we exit the loop, it
1021            // is one larger than the last chunk, so it is equal to the number of chunks.
1022            AttachmentPayload::Chunked(chunk_index)
1023        };
1024
1025        Ok(ChunkedAttachment {
1026            id,
1027            name: match item.filename() {
1028                Some(name) => name.to_owned(),
1029                None => UNNAMED_ATTACHMENT.to_owned(),
1030            },
1031            rate_limited: item.rate_limited(),
1032            content_type: item.raw_content_type().map(|s| s.to_ascii_lowercase()),
1033            attachment_type: item.attachment_type().unwrap_or_default(),
1034            size,
1035            retention_days,
1036            payload,
1037        })
1038    }
1039
1040    /// Produces Kafka messages for the content and metadata of an attachment item.
1041    ///
1042    /// The `send_individual_attachments` controls whether the metadata of an attachment
1043    /// is produced directly as an individual `attachment` message, or returned from this function
1044    /// to be later sent as part of an `event` message.
1045    ///
1046    /// Attachment contents are chunked and sent as multiple `attachment_chunk` messages,
1047    /// unless the `send_individual_attachments` flag is set, and the content is small enough
1048    /// to fit inside a message.
1049    /// In that case, no `attachment_chunk` is produced, but the content is sent as part
1050    /// of the `attachment` message instead.
1051    fn produce_attachment(
1052        &self,
1053        event_id: EventId,
1054        project_id: ProjectId,
1055        org_id: OrganizationId,
1056        item: &Item,
1057        send_individual_attachments: bool,
1058        retention_days: u16,
1059    ) -> Result<Option<ChunkedAttachment>, StoreError> {
1060        let attachment = if item.is_attachment_ref() {
1061            self.chunked_attachment_from_placeholder(item, retention_days)
1062        } else {
1063            self.chunked_attachment_from_attachment(
1064                event_id,
1065                project_id,
1066                org_id,
1067                item,
1068                send_individual_attachments,
1069                retention_days,
1070            )
1071        }?;
1072
1073        if send_individual_attachments {
1074            let message = KafkaMessage::Attachment(AttachmentKafkaMessage {
1075                event_id,
1076                project_id,
1077                attachment,
1078                org_id,
1079            });
1080            self.produce(KafkaTopic::Attachments, message)?;
1081            Ok(None)
1082        } else {
1083            Ok(Some(attachment))
1084        }
1085    }
1086
1087    fn produce_user_report(
1088        &self,
1089        event_id: EventId,
1090        project_id: ProjectId,
1091        org_id: OrganizationId,
1092        received_at: DateTime<Utc>,
1093        item: &Item,
1094    ) -> Result<(), StoreError> {
1095        let message = KafkaMessage::UserReport(UserReportKafkaMessage {
1096            project_id,
1097            event_id,
1098            start_time: safe_timestamp(received_at),
1099            payload: item.payload(),
1100            org_id,
1101        });
1102
1103        self.produce(KafkaTopic::Attachments, message)
1104    }
1105
1106    fn send_metric_message(
1107        &self,
1108        namespace: MetricNamespace,
1109        message: MetricKafkaMessage,
1110    ) -> Result<(), StoreError> {
1111        let topic = match namespace {
1112            MetricNamespace::Sessions => KafkaTopic::MetricsSessions,
1113            MetricNamespace::Outcomes => {
1114                return self.send_metric_based_outcome(message);
1115            }
1116            MetricNamespace::Unsupported => {
1117                relay_log::error!(
1118                    metric_message.name = message.name.as_ref(),
1119                    "store service dropping unknown metric usecase"
1120                );
1121                return Ok(());
1122            }
1123            _ => KafkaTopic::MetricsGeneric,
1124        };
1125
1126        let headers = BTreeMap::from([("namespace".to_owned(), namespace.to_string())]);
1127        self.produce(topic, KafkaMessage::Metric { headers, message })?;
1128        Ok(())
1129    }
1130
1131    fn send_metric_based_outcome(&self, message: MetricKafkaMessage) -> Result<(), StoreError> {
1132        let Some(outcome) = outcome::metric::to_outcome_id(message.name) else {
1133            relay_log::error!(
1134                mri = message.name.as_ref(),
1135                "invalid outcome metric, cannot infer outcome id from metric name"
1136            );
1137            return Ok(());
1138        };
1139        let quantity = match message.value {
1140            MetricValue::Counter(c) => c.to_f64() as _,
1141            v => {
1142                relay_log::error!(
1143                    mri = message.name.as_ref(),
1144                    "invalid outcome metric, expected a counter got '{}'",
1145                    v.variant()
1146                );
1147                return Ok(());
1148            }
1149        };
1150
1151        let outcome = OutcomeMessage {
1152            timestamp: message
1153                .timestamp
1154                .as_datetime()
1155                .unwrap_or_else(Utc::now)
1156                .to_rfc3339_opts(SecondsFormat::Micros, true),
1157            org_id: Some(message.org_id).filter(|id| id.value() != 0),
1158            project_id: message.project_id,
1159            key_id: message.key_id,
1160            outcome,
1161            reason: message.tags.get("reason").map(|s| s.as_str()),
1162            event_id: message.tags.get("event_id").map(|s| s.as_str()),
1163            remote_addr: message.tags.get("remote_addr").map(|s| s.as_str()),
1164            source: message.tags.get("source").map(|s| s.as_str()),
1165            category: message.tags.get("category").and_then(|s| s.parse().ok()),
1166            quantity: Some(quantity),
1167        };
1168
1169        let topic = match outcome.outcome.is_billing() {
1170            true => KafkaTopic::OutcomesBilling,
1171            false => KafkaTopic::Outcomes,
1172        };
1173
1174        self.produce(topic, KafkaMessage::Outcome(outcome))
1175    }
1176
1177    fn produce_profile(
1178        &self,
1179        organization_id: OrganizationId,
1180        project_id: ProjectId,
1181        key_id: Option<u64>,
1182        received_at: DateTime<Utc>,
1183        retention_days: u16,
1184        item: &Item,
1185    ) -> Result<(), StoreError> {
1186        let message = ProfileKafkaMessage {
1187            organization_id,
1188            project_id,
1189            key_id,
1190            received: safe_timestamp(received_at),
1191            retention_days,
1192            headers: BTreeMap::from([
1193                (
1194                    "sampled".to_owned(),
1195                    if item.sampled() { "true" } else { "false" }.to_owned(),
1196                ),
1197                ("project_id".to_owned(), project_id.to_string()),
1198            ]),
1199            payload: item.payload(),
1200        };
1201        self.produce(KafkaTopic::Profiles, KafkaMessage::Profile(message))?;
1202        Ok(())
1203    }
1204}
1205
1206impl Service for StoreService {
1207    type Interface = Store;
1208
1209    async fn run(self, mut rx: relay_system::Receiver<Self::Interface>) {
1210        let this = Arc::new(self);
1211
1212        relay_log::info!("store forwarder started");
1213
1214        while let Some(message) = rx.recv().await {
1215            let task = StoreTask {
1216                service: Arc::clone(&this),
1217                message: Some(message),
1218            };
1219            this.pool.spawn_async(task).await;
1220        }
1221
1222        relay_log::info!("store forwarder stopped");
1223    }
1224}
1225
1226/// Task executed by [`StoreService`].
1227pub struct StoreTask {
1228    service: Arc<StoreService>,
1229    message: Option<Store>,
1230}
1231
1232impl Future for StoreTask {
1233    type Output = ();
1234
1235    fn poll(mut self: Pin<&mut Self>, _: &mut task::Context<'_>) -> task::Poll<Self::Output> {
1236        let message = self
1237            .message
1238            .take()
1239            .expect("StoreTask polled after completion");
1240        let () = relay_log::with_scope(|_| {}, || self.service.handle_message(message));
1241        task::Poll::Ready(())
1242    }
1243}
1244
1245/// This signifies how the attachment payload is being transfered.
1246#[derive(Debug, Serialize)]
1247enum AttachmentPayload {
1248    /// The payload has been split into multiple chunks.
1249    ///
1250    /// The individual chunks are being sent as separate [`AttachmentChunkKafkaMessage`] messages.
1251    /// If the payload `size == 0`, the number of chunks will also be `0`.
1252    #[serde(rename = "chunks")]
1253    Chunked(usize),
1254
1255    /// The payload is inlined here directly, and thus into the [`ChunkedAttachment`].
1256    #[serde(rename = "data")]
1257    Inline(Bytes),
1258
1259    /// The attachment has already been stored into the objectstore, with the given Id.
1260    #[serde(rename = "stored_id")]
1261    Stored(String),
1262}
1263
1264/// Common attributes for both standalone attachments and processing-relevant attachments.
1265#[derive(Debug, Serialize)]
1266struct ChunkedAttachment {
1267    /// The attachment ID within the event.
1268    ///
1269    /// The triple `(project_id, event_id, id)` identifies an attachment uniquely.
1270    id: String,
1271
1272    /// File name of the attachment file.
1273    name: String,
1274
1275    /// Whether this attachment was rate limited and should be removed after processing.
1276    ///
1277    /// By default, rate limited attachments are immediately removed from Envelopes. For processing,
1278    /// native crash reports still need to be retained. These attachments are marked with the
1279    /// `rate_limited` header, which signals to the processing pipeline that the attachment should
1280    /// not be persisted after processing.
1281    rate_limited: bool,
1282
1283    /// Content type of the attachment payload.
1284    #[serde(skip_serializing_if = "Option::is_none")]
1285    content_type: Option<String>,
1286
1287    /// The Sentry-internal attachment type used in the processing pipeline.
1288    #[serde(serialize_with = "serialize_attachment_type")]
1289    attachment_type: AttachmentType,
1290
1291    /// The size of the attachment in bytes.
1292    size: usize,
1293
1294    /// The retention in days for this attachment.
1295    retention_days: u16,
1296
1297    /// The attachment payload, chunked, inlined, or already stored.
1298    #[serde(flatten)]
1299    payload: AttachmentPayload,
1300}
1301
1302/// A hack to make rmp-serde behave more like serde-json when serializing enums.
1303///
1304/// Cannot serialize bytes.
1305///
1306/// See <https://github.com/3Hren/msgpack-rust/pull/214>
1307fn serialize_attachment_type<S, T>(t: &T, serializer: S) -> Result<S::Ok, S::Error>
1308where
1309    S: serde::Serializer,
1310    T: serde::Serialize,
1311{
1312    serde_json::to_value(t)
1313        .map_err(|e| serde::ser::Error::custom(e.to_string()))?
1314        .serialize(serializer)
1315}
1316
1317/// Container payload for event messages.
1318#[derive(Debug, Serialize)]
1319struct EventKafkaMessage {
1320    /// Raw event payload.
1321    payload: Bytes,
1322    /// Time at which the event was received by Relay.
1323    start_time: u64,
1324    /// The event id.
1325    event_id: EventId,
1326    /// The project id for the current event.
1327    project_id: ProjectId,
1328    /// The client ip address.
1329    remote_addr: Option<String>,
1330    /// Attachments that are potentially relevant for processing.
1331    attachments: Vec<ChunkedAttachment>,
1332
1333    /// Used for [`KafkaMessage::key`]
1334    #[serde(skip)]
1335    org_id: OrganizationId,
1336}
1337
1338/// Container payload for chunks of attachments.
1339#[derive(Debug, Serialize)]
1340struct AttachmentChunkKafkaMessage {
1341    /// Chunk payload of the attachment.
1342    payload: Bytes,
1343    /// The event id.
1344    event_id: EventId,
1345    /// The project id for the current event.
1346    project_id: ProjectId,
1347    /// The attachment ID within the event.
1348    ///
1349    /// The triple `(project_id, event_id, id)` identifies an attachment uniquely.
1350    id: String,
1351    /// Sequence number of chunk. Starts at 0 and ends at `AttachmentKafkaMessage.num_chunks - 1`.
1352    chunk_index: usize,
1353
1354    /// Used for [`KafkaMessage::key`]
1355    #[serde(skip)]
1356    org_id: OrganizationId,
1357}
1358
1359/// A "standalone" attachment.
1360///
1361/// Still belongs to an event but can be sent independently (like UserReport) and is not
1362/// considered in processing.
1363#[derive(Debug, Serialize)]
1364struct AttachmentKafkaMessage {
1365    /// The event id.
1366    event_id: EventId,
1367    /// The project id for the current event.
1368    project_id: ProjectId,
1369    /// The attachment.
1370    attachment: ChunkedAttachment,
1371
1372    /// Used for [`KafkaMessage::key`]
1373    #[serde(skip)]
1374    org_id: OrganizationId,
1375}
1376
1377#[derive(Debug, Serialize)]
1378struct ReplayRecordingNotChunkedKafkaMessage<'a> {
1379    replay_id: EventId,
1380    key_id: Option<u64>,
1381    org_id: OrganizationId,
1382    project_id: ProjectId,
1383    received: u64,
1384    retention_days: u16,
1385    #[serde(with = "serde_bytes")]
1386    payload: &'a [u8],
1387    #[serde(with = "serde_bytes")]
1388    replay_event: Option<&'a [u8]>,
1389    #[serde(with = "serde_bytes")]
1390    replay_video: Option<&'a [u8]>,
1391    relay_snuba_publish_disabled: bool,
1392}
1393
1394/// User report for an event wrapped up in a message ready for consumption in Kafka.
1395///
1396/// Is always independent of an event and can be sent as part of any envelope.
1397#[derive(Debug, Serialize)]
1398struct UserReportKafkaMessage {
1399    /// The project id for the current event.
1400    project_id: ProjectId,
1401    start_time: u64,
1402    payload: Bytes,
1403
1404    /// Used for [`KafkaMessage::key`]
1405    #[serde(skip)]
1406    event_id: EventId,
1407    /// Used for [`KafkaMessage::key`]
1408    #[serde(skip)]
1409    org_id: OrganizationId,
1410}
1411
1412#[derive(Clone, Debug, Serialize)]
1413struct MetricKafkaMessage<'a> {
1414    org_id: OrganizationId,
1415    project_id: ProjectId,
1416    #[serde(skip)]
1417    key_id: Option<u64>,
1418    name: &'a MetricName,
1419    #[serde(flatten)]
1420    value: MetricValue<'a>,
1421    timestamp: UnixTimestamp,
1422    tags: &'a BTreeMap<String, String>,
1423    retention_days: u16,
1424    #[serde(skip_serializing_if = "Option::is_none")]
1425    received_at: Option<UnixTimestamp>,
1426}
1427
1428#[derive(Clone, Debug, Serialize)]
1429#[serde(tag = "type", content = "value")]
1430enum MetricValue<'a> {
1431    #[serde(rename = "c")]
1432    Counter(FiniteF64),
1433    #[serde(rename = "d")]
1434    Distribution(ArrayEncoding<'a, &'a [FiniteF64]>),
1435    #[serde(rename = "s")]
1436    Set(ArrayEncoding<'a, SetView<'a>>),
1437    #[serde(rename = "g")]
1438    Gauge(GaugeValue),
1439}
1440
1441impl MetricValue<'_> {
1442    fn variant(&self) -> &'static str {
1443        match self {
1444            Self::Counter(_) => "counter",
1445            Self::Distribution(_) => "distribution",
1446            Self::Set(_) => "set",
1447            Self::Gauge(_) => "gauge",
1448        }
1449    }
1450
1451    fn encoding(&self) -> Option<&'static str> {
1452        match self {
1453            Self::Distribution(ae) => Some(ae.name()),
1454            Self::Set(ae) => Some(ae.name()),
1455            _ => None,
1456        }
1457    }
1458}
1459
1460/// Raw representation of an outcome for Kafka.
1461#[derive(Debug, Serialize, Clone)]
1462pub struct OutcomeMessage<'a> {
1463    /// The timespan of the event outcome.
1464    timestamp: String,
1465    /// Organization id.
1466    #[serde(skip_serializing_if = "Option::is_none")]
1467    org_id: Option<OrganizationId>,
1468    /// Project id.
1469    project_id: ProjectId,
1470    /// The DSN project key id.
1471    #[serde(skip_serializing_if = "Option::is_none")]
1472    key_id: Option<u64>,
1473    /// The outcome.
1474    outcome: OutcomeId,
1475    /// Reason for the outcome.
1476    #[serde(skip_serializing_if = "Option::is_none")]
1477    reason: Option<&'a str>,
1478    /// The event id.
1479    #[serde(skip_serializing_if = "Option::is_none")]
1480    event_id: Option<&'a str>,
1481    /// The client ip address.
1482    #[serde(skip_serializing_if = "Option::is_none")]
1483    remote_addr: Option<&'a str>,
1484    /// The source of the outcome (which Relay sent it)
1485    #[serde(skip_serializing_if = "Option::is_none")]
1486    source: Option<&'a str>,
1487    /// The event's data category.
1488    #[serde(skip_serializing_if = "Option::is_none")]
1489    category: Option<u8>,
1490    /// The number of events or total attachment size in bytes.
1491    #[serde(skip_serializing_if = "Option::is_none")]
1492    quantity: Option<u64>,
1493}
1494
1495#[derive(Clone, Debug, Serialize)]
1496struct ProfileKafkaMessage {
1497    organization_id: OrganizationId,
1498    project_id: ProjectId,
1499    key_id: Option<u64>,
1500    received: u64,
1501    retention_days: u16,
1502    #[serde(skip)]
1503    headers: BTreeMap<String, String>,
1504    payload: Bytes,
1505}
1506
1507/// Used to discriminate cron monitor ingestion messages.
1508///
1509/// There are two types of messages that end up in the ingest-monitors kafka topic, "check_in" (the
1510/// ones produced here in relay) and "clock_pulse" messages, which are produced externally and are
1511/// intended to ensure the clock continues to run even when ingestion volume drops.
1512#[allow(dead_code)]
1513#[derive(Debug, Serialize)]
1514#[serde(rename_all = "snake_case")]
1515enum CheckInMessageType {
1516    ClockPulse,
1517    CheckIn,
1518}
1519
1520#[derive(Debug, Serialize)]
1521struct CheckInKafkaMessage {
1522    /// Used by the consumer to discrinminate the message.
1523    message_type: CheckInMessageType,
1524    /// Raw event payload.
1525    payload: Bytes,
1526    /// Time at which the event was received by Relay.
1527    start_time: u64,
1528    /// The SDK client which produced the event.
1529    sdk: Option<String>,
1530    /// The project id for the current event.
1531    project_id: ProjectId,
1532    /// Number of days to retain.
1533    retention_days: u16,
1534
1535    /// Used for [`KafkaMessage::key`]
1536    #[serde(skip)]
1537    routing_key_hint: Option<Uuid>,
1538    /// Used for [`KafkaMessage::key`]
1539    #[serde(skip)]
1540    org_id: OrganizationId,
1541}
1542
1543#[derive(Debug, Serialize)]
1544struct SpanKafkaMessage<'a> {
1545    #[serde(flatten)]
1546    meta: SpanMeta,
1547    #[serde(flatten)]
1548    span: SerializableAnnotated<'a, SpanV2>,
1549}
1550
1551#[derive(Debug, Serialize)]
1552struct SpanMeta {
1553    organization_id: OrganizationId,
1554    project_id: ProjectId,
1555    // Required for the buffer to emit outcomes scoped to the DSN.
1556    #[serde(skip_serializing_if = "Option::is_none")]
1557    key_id: Option<u64>,
1558    #[serde(skip_serializing_if = "Option::is_none")]
1559    event_id: Option<EventId>,
1560    /// Time at which the event was received by Relay. Not to be confused with `start_timestamp_ms`.
1561    received: f64,
1562    /// Number of days until these data should be deleted.
1563    retention_days: u16,
1564    /// Number of days until the downsampled version of this data should be deleted.
1565    downsampled_retention_days: u16,
1566    /// Whether the segment span should be used for issue detection instead of the transaction.
1567    #[serde(rename = "_performance_issues_spans", skip_serializing_if = "is_false")]
1568    performance_issues_spans: bool,
1569}
1570
1571fn is_false(val: &bool) -> bool {
1572    !val
1573}
1574
1575#[derive(Clone, Debug, Serialize)]
1576struct ProfileChunkKafkaMessage {
1577    organization_id: OrganizationId,
1578    project_id: ProjectId,
1579    received: u64,
1580    retention_days: u16,
1581    #[serde(skip)]
1582    headers: BTreeMap<String, String>,
1583    payload: Bytes,
1584    #[serde(skip_serializing_if = "Vec::is_empty")]
1585    attachments: Vec<ProfileChunkKafkaAttachment>,
1586}
1587
1588#[derive(Clone, Debug, Serialize)]
1589struct ProfileChunkKafkaAttachment {
1590    name: String,
1591    content_type: &'static str,
1592    stored_id: String,
1593}
1594
1595/// An enum over all possible ingest messages.
1596#[derive(Debug, Serialize)]
1597#[serde(tag = "type", rename_all = "snake_case")]
1598#[allow(clippy::large_enum_variant)]
1599enum KafkaMessage<'a> {
1600    Event(EventKafkaMessage),
1601    UserReport(UserReportKafkaMessage),
1602    Metric {
1603        #[serde(skip)]
1604        headers: BTreeMap<String, String>,
1605        #[serde(flatten)]
1606        message: MetricKafkaMessage<'a>,
1607    },
1608    CheckIn(CheckInKafkaMessage),
1609    Item {
1610        #[serde(skip)]
1611        headers: BTreeMap<String, String>,
1612        #[serde(skip)]
1613        item_type: TraceItemType,
1614        #[serde(skip)]
1615        message: TraceItem,
1616    },
1617    SpanV2 {
1618        #[serde(skip)]
1619        routing_key: Option<Uuid>,
1620        #[serde(skip)]
1621        headers: BTreeMap<String, String>,
1622        #[serde(flatten)]
1623        message: SpanKafkaMessage<'a>,
1624
1625        /// Used for [`KafkaMessage::key`]
1626        #[serde(skip)]
1627        org_id: OrganizationId,
1628    },
1629
1630    Attachment(AttachmentKafkaMessage),
1631    AttachmentChunk(AttachmentChunkKafkaMessage),
1632
1633    Profile(ProfileKafkaMessage),
1634    ProfileChunk(ProfileChunkKafkaMessage),
1635
1636    ReplayRecordingNotChunked(ReplayRecordingNotChunkedKafkaMessage<'a>),
1637
1638    Outcome(OutcomeMessage<'a>),
1639}
1640
1641impl KafkaMessage<'_> {
1642    /// Creates a [`KafkaMessage`] for a [`TraceItem`].
1643    fn for_item(scoping: Scoping, item: TraceItem) -> KafkaMessage<'static> {
1644        let item_type = item.item_type();
1645        KafkaMessage::Item {
1646            headers: BTreeMap::from([
1647                ("project_id".to_owned(), scoping.project_id.to_string()),
1648                ("item_type".to_owned(), (item_type as i32).to_string()),
1649            ]),
1650            message: item,
1651            item_type,
1652        }
1653    }
1654}
1655
1656impl Message for KafkaMessage<'_> {
1657    fn variant(&self) -> &'static str {
1658        match self {
1659            KafkaMessage::Event(_) => "event",
1660            KafkaMessage::UserReport(_) => "user_report",
1661            KafkaMessage::Metric { message, .. } => match message.name.namespace() {
1662                MetricNamespace::Sessions => "metric_sessions",
1663                MetricNamespace::Spans => "metric_spans",
1664                MetricNamespace::Transactions => "metric_transactions",
1665                MetricNamespace::Outcomes => "metric_outcomes",
1666                MetricNamespace::Unsupported => "metric_unsupported",
1667            },
1668            KafkaMessage::CheckIn(_) => "check_in",
1669            KafkaMessage::SpanV2 { .. } => "span",
1670            KafkaMessage::Item { item_type, .. } => item_type.as_str_name(),
1671
1672            KafkaMessage::Attachment(_) => "attachment",
1673            KafkaMessage::AttachmentChunk(_) => "attachment_chunk",
1674
1675            KafkaMessage::Profile(_) => "profile",
1676            KafkaMessage::ProfileChunk(_) => "profile_chunk",
1677
1678            KafkaMessage::ReplayRecordingNotChunked(_) => "replay_recording_not_chunked",
1679
1680            KafkaMessage::Outcome(_) => "outcome",
1681        }
1682    }
1683
1684    /// Returns the partitioning key for this Kafka message determining.
1685    fn key(&self) -> Option<relay_kafka::Key> {
1686        match self {
1687            Self::Event(message) => Some((message.event_id.0, message.org_id)),
1688            Self::UserReport(message) => Some((message.event_id.0, message.org_id)),
1689            Self::SpanV2 {
1690                routing_key,
1691                org_id,
1692                ..
1693            } => routing_key.map(|r| (r, *org_id)),
1694
1695            // Monitor check-ins use the hinted UUID passed through from the Envelope.
1696            //
1697            // XXX(epurkhiser): In the future it would be better if all KafkaMessage's would
1698            // recieve the routing_key_hint form their envelopes.
1699            Self::CheckIn(message) => message.routing_key_hint.map(|r| (r, message.org_id)),
1700
1701            Self::Attachment(message) => Some((message.event_id.0, message.org_id)),
1702            Self::AttachmentChunk(message) => Some((message.event_id.0, message.org_id)),
1703
1704            // Random partitioning
1705            Self::Metric { .. }
1706            | Self::Item { .. }
1707            | Self::Profile(_)
1708            | Self::ProfileChunk(_)
1709            | Self::ReplayRecordingNotChunked(_)
1710            | Self::Outcome(_) => None,
1711        }
1712        .filter(|(uuid, _)| !uuid.is_nil())
1713        .map(|(uuid, org_id)| {
1714            // mix key with org id for better paritioning in case of incidents where messages
1715            // across orgs get the same id
1716            let mut res = uuid.into_bytes();
1717            for (i, &b) in org_id.value().to_be_bytes().iter().enumerate() {
1718                res[i] ^= b;
1719            }
1720            u128::from_be_bytes(res)
1721        })
1722    }
1723
1724    fn headers(&self) -> Option<&BTreeMap<String, String>> {
1725        match &self {
1726            KafkaMessage::Metric { headers, .. }
1727            | KafkaMessage::SpanV2 { headers, .. }
1728            | KafkaMessage::Item { headers, .. }
1729            | KafkaMessage::Profile(ProfileKafkaMessage { headers, .. })
1730            | KafkaMessage::ProfileChunk(ProfileChunkKafkaMessage { headers, .. }) => Some(headers),
1731
1732            KafkaMessage::Event(_)
1733            | KafkaMessage::UserReport(_)
1734            | KafkaMessage::CheckIn(_)
1735            | KafkaMessage::Attachment(_)
1736            | KafkaMessage::AttachmentChunk(_)
1737            | KafkaMessage::ReplayRecordingNotChunked(_)
1738            | KafkaMessage::Outcome(_) => None,
1739        }
1740    }
1741
1742    fn serialize(&self) -> Result<SerializationOutput<'_>, ClientError> {
1743        match self {
1744            KafkaMessage::Metric { message, .. } => serialize_as_json(message),
1745            KafkaMessage::SpanV2 { message, .. } => serialize_as_json(message),
1746            KafkaMessage::Item { message, .. } => {
1747                let mut payload = Vec::new();
1748                match message.encode(&mut payload) {
1749                    Ok(_) => Ok(SerializationOutput::Protobuf(Cow::Owned(payload))),
1750                    Err(_) => Err(ClientError::ProtobufEncodingFailed),
1751                }
1752            }
1753            KafkaMessage::Outcome(outcome) => serialize_as_json(outcome),
1754            KafkaMessage::Event(_)
1755            | KafkaMessage::UserReport(_)
1756            | KafkaMessage::CheckIn(_)
1757            | KafkaMessage::Attachment(_)
1758            | KafkaMessage::AttachmentChunk(_)
1759            | KafkaMessage::Profile(_)
1760            | KafkaMessage::ProfileChunk(_)
1761            | KafkaMessage::ReplayRecordingNotChunked(_) => match rmp_serde::to_vec_named(&self) {
1762                Ok(x) => Ok(SerializationOutput::MsgPack(Cow::Owned(x))),
1763                Err(err) => Err(ClientError::InvalidMsgPack(err)),
1764            },
1765        }
1766    }
1767}
1768
1769fn serialize_as_json<T: serde::Serialize>(
1770    value: &T,
1771) -> Result<SerializationOutput<'_>, ClientError> {
1772    match serde_json::to_vec(value) {
1773        Ok(vec) => Ok(SerializationOutput::Json(Cow::Owned(vec))),
1774        Err(err) => Err(ClientError::InvalidJson(err)),
1775    }
1776}
1777
1778fn bool_to_str(value: bool) -> &'static str {
1779    if value { "true" } else { "false" }
1780}
1781
1782/// Returns a safe timestamp for Kafka.
1783///
1784/// Kafka expects timestamps to be in UTC and in seconds since epoch.
1785fn safe_timestamp(timestamp: DateTime<Utc>) -> u64 {
1786    let ts = timestamp.timestamp();
1787    if ts >= 0 {
1788        return ts as u64;
1789    }
1790
1791    // We assume this call can't return < 0.
1792    Utc::now().timestamp() as u64
1793}