1use std::borrow::Cow;
4use std::collections::{BTreeMap, HashMap};
5use std::fmt;
6use std::sync::Arc;
7use std::sync::atomic::{AtomicU32, Ordering};
8use std::time::{Duration, Instant};
9
10use rdkafka::message::Header;
11use rdkafka::producer::BaseRecord;
12use relay_statsd::metric;
13use sentry_arroyo::backends::ProducerError;
14use sentry_arroyo::backends::kafka::config::KafkaConfig;
15use sentry_arroyo::backends::kafka::producer::KafkaProducer as ArroyoKafkaProducer;
16use sentry_arroyo::types::Topic;
17use thiserror::Error;
18
19use crate::KafkaTopicConfig;
20use crate::config::{KafkaParams, KafkaTopic};
21use crate::debounced::Debounced;
22use crate::limits::KafkaRateLimits;
23use crate::producer::utils::KafkaHeaders;
24use crate::statsd::{KafkaCounters, KafkaDistributions, KafkaGauges};
25
26mod utils;
27use utils::Context;
28
29#[cfg(debug_assertions)]
30mod schemas;
31
32const REPORT_FREQUENCY_SECS: u64 = 1;
33const KAFKA_FETCH_METADATA_TIMEOUT: Duration = Duration::from_secs(30);
34
35pub type Key = u128;
37
38#[derive(Error, Debug)]
40pub enum ClientError {
41 #[error("failed to send kafka message")]
43 SendFailed(#[source] ProducerError),
44
45 #[error("failed to find producer for the requested kafka topic")]
47 InvalidTopicName,
48
49 #[error("failed to create kafka producer: invalid kafka config: {0}")]
51 InvalidConfig(#[source] rdkafka::error::KafkaError),
52
53 #[error("failed to serialize kafka message")]
55 InvalidMsgPack(#[source] rmp_serde::encode::Error),
56
57 #[error("failed to serialize json message")]
59 InvalidJson(#[from] serde_json::Error),
60
61 #[cfg(debug_assertions)]
63 #[error("schema validation failed")]
64 SchemaValidationFailed(#[source] schemas::SchemaError),
65
66 #[error("no kafka configuration for topic")]
68 MissingTopic,
69
70 #[error("failed to validate the topic with name {0}: {1}")]
72 TopicError(String, #[source] rdkafka::error::KafkaError),
73
74 #[error("failed to encode protobuf because the buffer is too small")]
77 ProtobufEncodingFailed,
78}
79
80pub trait Message {
82 fn key(&self) -> Option<Key>;
84
85 fn variant(&self) -> &'static str;
87
88 fn headers(&self) -> Option<&BTreeMap<String, String>>;
90
91 fn serialize(&self) -> Result<SerializationOutput<'_>, ClientError>;
97}
98
99#[derive(Debug, Clone)]
101pub enum SerializationOutput<'a> {
102 Json(Cow<'a, [u8]>),
104
105 MsgPack(Cow<'a, [u8]>),
107
108 Protobuf(Cow<'a, [u8]>),
110}
111
112impl SerializationOutput<'_> {
113 pub fn as_bytes(&self) -> &[u8] {
115 match self {
116 SerializationOutput::Json(cow) => cow,
117 SerializationOutput::MsgPack(cow) => cow,
118 SerializationOutput::Protobuf(cow) => cow,
119 }
120 }
121}
122
123struct TopicProducers {
124 producers: Vec<TopicProducer>,
126}
127
128impl TopicProducers {
129 fn new() -> Self {
130 Self {
131 producers: Vec::new(),
132 }
133 }
134
135 fn select(&self, key: u128) -> Option<&TopicProducer> {
136 debug_assert!(!self.producers.is_empty());
137
138 if self.producers.is_empty() {
139 return None;
140 } else if self.producers.len() == 1 {
141 return self.producers.first();
142 }
143
144 let select = (key % self.producers.len() as u128) as usize;
145 self.producers.get(select)
146 }
147
148 fn validate_topic(&self) -> Result<(), ClientError> {
150 for tp in &self.producers {
151 tp.producer
152 .validate_topic(Topic::new(&tp.topic_name), KAFKA_FETCH_METADATA_TIMEOUT)
153 .map_err(|error| ClientError::TopicError(tp.topic_name.clone(), error))?;
154 }
155
156 Ok(())
157 }
158}
159
160struct TopicProducer {
161 pub topic_name: String,
162 pub producer: Arc<ArroyoKafkaProducer<Context>>,
163 pub rate_limiter: Option<KafkaRateLimits>,
164}
165
166struct Producer {
168 topic_producers: TopicProducers,
170 metrics: Debounced,
172 next_key: AtomicU32,
173}
174
175impl Producer {
176 fn new(topic_producers: TopicProducers) -> Self {
177 Self {
178 topic_producers,
179 metrics: Debounced::new(REPORT_FREQUENCY_SECS),
180 next_key: AtomicU32::new(0),
181 }
182 }
183}
184
185impl Producer {
186 fn send(
188 &self,
189 key: Option<Key>,
190 headers: Option<&BTreeMap<String, String>>,
191 variant: &str,
192 payload: &[u8],
193 ) -> Result<&str, ClientError> {
194 let now = Instant::now();
195
196 let mut key = key.unwrap_or_else(|| self.next_key());
199
200 let Some(TopicProducer {
201 topic_name,
202 producer,
203 rate_limiter,
204 }) = self.topic_producers.select(key)
205 else {
206 return Err(ClientError::MissingTopic);
207 };
208
209 relay_log::configure_scope(|s| s.set_tag("topic", topic_name));
210
211 let producer_name = producer.context().producer_name();
212
213 metric!(
214 distribution(KafkaDistributions::KafkaMessageSize) = payload.len() as u64,
215 variant = variant,
216 topic = topic_name,
217 producer_name = producer_name
218 );
219
220 let mut headers = headers
221 .unwrap_or(&BTreeMap::new())
222 .iter()
223 .map(|(key, value)| Header {
224 key,
225 value: Some(value),
226 })
227 .collect::<KafkaHeaders>();
228
229 if let Some(limiter) = &rate_limiter {
233 let is_limited = limiter.try_increment(now, key, 1) < 1;
234
235 if is_limited {
236 metric!(
237 counter(KafkaCounters::ProducerPartitionKeyRateLimit) += 1,
238 variant = variant,
239 topic = topic_name,
240 producer_name = producer_name
241 );
242
243 headers.insert(Header {
244 key: "sentry-reshuffled",
245 value: Some("1"),
246 });
247
248 key = self.next_key();
250 }
251 }
252
253 let key = u128::to_be_bytes(key);
254 let mut record = BaseRecord::to(topic_name).payload(payload).key(&key);
255 if let Some(headers) = headers.into_inner() {
256 record = record.headers(headers);
257 }
258
259 self.metrics.debounce(now, || {
260 metric!(
261 gauge(KafkaGauges::InFlightCount) = producer.in_flight_count() as u64,
262 variant = variant,
263 topic = topic_name,
264 producer_name = producer_name
265 );
266 });
267
268 producer
269 .produce_record(record)
270 .map_err(ClientError::SendFailed)
271 .inspect_err(|_| {
272 metric!(
273 counter(KafkaCounters::ProducerEnqueueError) += 1,
274 variant = variant,
275 topic = topic_name,
276 producer_name = producer_name
277 );
278 })?;
279
280 Ok(topic_name)
281 }
282
283 fn next_key(&self) -> u128 {
292 self.next_key.fetch_add(1, Ordering::Relaxed) as u128
295 }
296}
297
298impl fmt::Debug for Producer {
299 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
300 let topic_names: Vec<&String> = self
301 .topic_producers
302 .producers
303 .iter()
304 .map(|tp| &tp.topic_name)
305 .collect();
306 f.debug_struct("Producer")
307 .field("topic_names", &topic_names)
308 .field("producers", &"<KafkaProducers>")
309 .finish_non_exhaustive()
310 }
311}
312
313#[derive(Debug)]
315pub struct KafkaClient {
316 producers: HashMap<KafkaTopic, Producer>,
317 #[cfg(debug_assertions)]
318 schema_validator: schemas::Validator,
319}
320
321impl KafkaClient {
322 pub fn builder() -> KafkaClientBuilder {
324 KafkaClientBuilder::new()
325 }
326
327 pub fn send_message(
331 &self,
332 topic: KafkaTopic,
333 message: &impl Message,
334 ) -> Result<&str, ClientError> {
335 let serialized = message.serialize()?;
336
337 #[cfg(debug_assertions)]
338 if let SerializationOutput::Json(ref bytes) = serialized {
339 self.schema_validator
340 .validate_message_schema(topic, bytes)
341 .map_err(ClientError::SchemaValidationFailed)?;
342 }
343 self.send(
344 topic,
345 message.key(),
346 message.headers(),
347 message.variant(),
348 serialized.as_bytes(),
349 )
350 }
351
352 fn send(
356 &self,
357 topic: KafkaTopic,
358 key: Option<Key>,
359 headers: Option<&BTreeMap<String, String>>,
360 variant: &str,
361 payload: &[u8],
362 ) -> Result<&str, ClientError> {
363 let producer = self
364 .producers
365 .get(&topic)
366 .ok_or_else(|| ClientError::InvalidTopicName)?;
367
368 producer.send(key, headers, variant, payload)
369 }
370}
371
372#[derive(Default)]
374pub struct KafkaClientBuilder {
375 reused_producers: BTreeMap<Option<String>, Arc<ArroyoKafkaProducer<Context>>>,
376 producers: HashMap<KafkaTopic, Producer>,
377}
378
379impl KafkaClientBuilder {
380 pub fn new() -> Self {
382 Self::default()
383 }
384
385 pub fn add_kafka_topic_config(
392 mut self,
393 topic: KafkaTopic,
394 topic_config: &KafkaTopicConfig<'_>,
395 validate_topic: bool,
396 ) -> Result<Self, ClientError> {
397 let mut topic_producers = TopicProducers::new();
398
399 for params in topic_config.topics() {
403 let KafkaParams {
404 topic_name,
405 config_name,
406 params: config_params,
407 key_rate_limit,
408 } = params;
409
410 let rate_limiter = key_rate_limit.map(|limit| {
411 KafkaRateLimits::new(
412 limit.limit_per_window,
413 Duration::from_secs(limit.window_secs),
414 )
415 });
416
417 let config_name = config_name.map(str::to_owned);
418
419 let kafka_producer = if let Some(producer) = self.reused_producers.get(&config_name) {
421 Arc::clone(producer)
422 } else {
423 let producer_name = config_params
425 .iter()
426 .find(|p| p.name == "client.id")
427 .map(|p| p.value.clone())
428 .or_else(|| config_name.clone())
429 .unwrap_or_else(|| "unknown".to_owned());
430
431 let context = Context::new(producer_name);
432 let params = config_params
433 .iter()
434 .map(|param| (param.name.clone(), param.value.clone()))
435 .collect();
436 let config = KafkaConfig::new_config(Vec::new(), Some(params));
437 let producer = ArroyoKafkaProducer::new_with_context(config, context)
438 .map_err(ClientError::InvalidConfig)?;
439 let producer = Arc::new(producer);
440
441 self.reused_producers
442 .insert(config_name, Arc::clone(&producer));
443
444 producer
445 };
446
447 topic_producers.producers.push(TopicProducer {
448 topic_name: topic_name.clone(),
449 producer: kafka_producer,
450 rate_limiter,
451 });
452 }
453
454 let producer = Producer::new(topic_producers);
455 if validate_topic {
456 producer.topic_producers.validate_topic()?;
457 }
458 self.producers.insert(topic, producer);
459
460 Ok(self)
461 }
462
463 pub fn build(self) -> KafkaClient {
465 KafkaClient {
466 producers: self.producers,
467 #[cfg(debug_assertions)]
468 schema_validator: schemas::Validator::default(),
469 }
470 }
471}
472
473impl fmt::Debug for KafkaClientBuilder {
474 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
475 f.debug_struct("KafkaClientBuilder")
476 .field("reused_producers", &"<CachedProducers>")
477 .field("producers", &self.producers)
478 .finish()
479 }
480}