1use 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
50const 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#[derive(Debug)]
107pub struct StoreEvent {
108 pub event_category: DataCategory,
110 pub event: Annotated<Event>,
112 pub attachments: Vec<Item>,
114 pub user_reports: Vec<Item>,
116 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#[derive(Clone, Debug)]
131pub struct StoreMetrics {
132 pub buckets: Vec<Bucket>,
133 pub scoping: Scoping,
134 pub retention: u16,
135}
136
137#[derive(Debug)]
139pub struct StoreTraceItem {
140 pub trace_item: TraceItem,
142}
143
144impl Counted for StoreTraceItem {
145 fn quantities(&self) -> Quantities {
146 self.trace_item.quantities()
147 }
148}
149
150#[derive(Debug)]
152pub struct StoreSpanV2 {
153 pub routing_key: Option<Uuid>,
155 pub retention_days: u16,
157 pub downsampled_retention_days: u16,
159 pub event_id: Option<EventId>,
161 pub item: SpanV2,
163 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#[derive(Debug)]
176pub struct StoreProfileChunk {
177 pub retention_days: u16,
179 pub payload: Bytes,
181 pub attachments: Vec<ProfileAttachment>,
183 pub quantities: Quantities,
187}
188
189#[derive(Debug)]
191pub struct ProfileAttachment {
192 pub name: String,
194 pub content_type: ContentType,
196 pub stored_id: ObjectstoreKey,
200}
201
202impl Counted for StoreProfileChunk {
203 fn quantities(&self) -> Quantities {
204 self.quantities.clone()
205 }
206}
207
208#[derive(Debug)]
210pub struct StoreReplay {
211 pub event_id: EventId,
213 pub retention_days: u16,
215 pub recording: Bytes,
217 pub event: Option<Bytes>,
219 pub video: Option<Bytes>,
221 pub quantities: Quantities,
225}
226
227impl Counted for StoreReplay {
228 fn quantities(&self) -> Quantities {
229 self.quantities.clone()
230 }
231}
232
233#[derive(Debug)]
235pub struct StoreAttachment {
236 pub event_id: EventId,
238 pub attachment: Item,
240 pub quantities: Quantities,
242 pub retention: u16,
244}
245
246impl Counted for StoreAttachment {
247 fn quantities(&self) -> Quantities {
248 self.quantities.clone()
249 }
250}
251
252#[derive(Debug)]
254pub struct StoreUserReport {
255 pub event_id: EventId,
257 pub report: Item,
259}
260
261impl Counted for StoreUserReport {
262 fn quantities(&self) -> Quantities {
263 smallvec::smallvec![(DataCategory::UserReportV2, 1)]
264 }
265}
266
267#[derive(Debug)]
269pub struct StoreProfile {
270 pub retention_days: u16,
272 pub profile: Item,
274 pub quantities: Quantities,
276}
277
278impl Counted for StoreProfile {
279 fn quantities(&self) -> Quantities {
280 self.quantities.clone()
281 }
282}
283
284#[derive(Debug)]
286pub struct StoreCheckIn {
287 pub check_in: Item,
289 pub sdk: Option<String>,
291 pub retention_days: u16,
293}
294
295impl Counted for StoreCheckIn {
296 fn quantities(&self) -> Quantities {
297 self.check_in.quantities()
298 }
299}
300
301pub type StoreServicePool = AsyncPool<StoreTask>;
303
304#[derive(Debug)]
306pub enum Store {
307 Event(Managed<Box<StoreEvent>>),
309 Metrics(StoreMetrics),
311 TraceItem(Managed<StoreTraceItem>),
313 Span(Managed<Box<StoreSpanV2>>),
315 ProfileChunk(Managed<StoreProfileChunk>),
317 Replay(Managed<StoreReplay>),
319 Attachment(Managed<StoreAttachment>),
321 UserReport(Managed<StoreUserReport>),
323 Profile(Managed<StoreProfile>),
325 CheckIn(Managed<StoreCheckIn>),
327}
328
329impl Store {
330 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
429pub 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 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 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 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 true,
793 attachment.retention,
794 );
795 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 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 AttachmentPayload::Inline(payload)
996 } else {
997 let mut chunk_index = 0;
998 let mut offset = 0;
999 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 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 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
1226pub 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#[derive(Debug, Serialize)]
1247enum AttachmentPayload {
1248 #[serde(rename = "chunks")]
1253 Chunked(usize),
1254
1255 #[serde(rename = "data")]
1257 Inline(Bytes),
1258
1259 #[serde(rename = "stored_id")]
1261 Stored(String),
1262}
1263
1264#[derive(Debug, Serialize)]
1266struct ChunkedAttachment {
1267 id: String,
1271
1272 name: String,
1274
1275 rate_limited: bool,
1282
1283 #[serde(skip_serializing_if = "Option::is_none")]
1285 content_type: Option<String>,
1286
1287 #[serde(serialize_with = "serialize_attachment_type")]
1289 attachment_type: AttachmentType,
1290
1291 size: usize,
1293
1294 retention_days: u16,
1296
1297 #[serde(flatten)]
1299 payload: AttachmentPayload,
1300}
1301
1302fn 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#[derive(Debug, Serialize)]
1319struct EventKafkaMessage {
1320 payload: Bytes,
1322 start_time: u64,
1324 event_id: EventId,
1326 project_id: ProjectId,
1328 remote_addr: Option<String>,
1330 attachments: Vec<ChunkedAttachment>,
1332
1333 #[serde(skip)]
1335 org_id: OrganizationId,
1336}
1337
1338#[derive(Debug, Serialize)]
1340struct AttachmentChunkKafkaMessage {
1341 payload: Bytes,
1343 event_id: EventId,
1345 project_id: ProjectId,
1347 id: String,
1351 chunk_index: usize,
1353
1354 #[serde(skip)]
1356 org_id: OrganizationId,
1357}
1358
1359#[derive(Debug, Serialize)]
1364struct AttachmentKafkaMessage {
1365 event_id: EventId,
1367 project_id: ProjectId,
1369 attachment: ChunkedAttachment,
1371
1372 #[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#[derive(Debug, Serialize)]
1398struct UserReportKafkaMessage {
1399 project_id: ProjectId,
1401 start_time: u64,
1402 payload: Bytes,
1403
1404 #[serde(skip)]
1406 event_id: EventId,
1407 #[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#[derive(Debug, Serialize, Clone)]
1462pub struct OutcomeMessage<'a> {
1463 timestamp: String,
1465 #[serde(skip_serializing_if = "Option::is_none")]
1467 org_id: Option<OrganizationId>,
1468 project_id: ProjectId,
1470 #[serde(skip_serializing_if = "Option::is_none")]
1472 key_id: Option<u64>,
1473 outcome: OutcomeId,
1475 #[serde(skip_serializing_if = "Option::is_none")]
1477 reason: Option<&'a str>,
1478 #[serde(skip_serializing_if = "Option::is_none")]
1480 event_id: Option<&'a str>,
1481 #[serde(skip_serializing_if = "Option::is_none")]
1483 remote_addr: Option<&'a str>,
1484 #[serde(skip_serializing_if = "Option::is_none")]
1486 source: Option<&'a str>,
1487 #[serde(skip_serializing_if = "Option::is_none")]
1489 category: Option<u8>,
1490 #[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#[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 message_type: CheckInMessageType,
1524 payload: Bytes,
1526 start_time: u64,
1528 sdk: Option<String>,
1530 project_id: ProjectId,
1532 retention_days: u16,
1534
1535 #[serde(skip)]
1537 routing_key_hint: Option<Uuid>,
1538 #[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 #[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 received: f64,
1562 retention_days: u16,
1564 downsampled_retention_days: u16,
1566 #[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#[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 #[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 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 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 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 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 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
1782fn safe_timestamp(timestamp: DateTime<Utc>) -> u64 {
1786 let ts = timestamp.timestamp();
1787 if ts >= 0 {
1788 return ts as u64;
1789 }
1790
1791 Utc::now().timestamp() as u64
1793}