relay_kafka/producer/
utils.rs1use std::error::Error;
2
3use rdkafka::message::{Header, OwnedHeaders, ToBytes};
4use rdkafka::producer::{DeliveryResult, ProducerContext};
5use rdkafka::{ClientContext, Message};
6use relay_statsd::metric;
7use sentry_arroyo::backends::kafka::producer::ProducerContext as ArroyoProducerContext;
8
9use crate::statsd::KafkaCounters;
10
11pub struct KafkaHeaders(Option<OwnedHeaders>);
16
17impl KafkaHeaders {
18 pub fn new() -> Self {
19 Self(None)
20 }
21
22 pub fn insert<V>(&mut self, header: Header<'_, &V>)
23 where
24 V: ToBytes + ?Sized,
25 {
26 self.extend(Some(header));
27 }
28
29 pub fn into_inner(self) -> Option<OwnedHeaders> {
30 self.0
31 }
32}
33
34impl<'a, 'b, V> Extend<Header<'a, &'b V>> for KafkaHeaders
35where
36 V: ToBytes + ?Sized,
37{
38 fn extend<T: IntoIterator<Item = Header<'a, &'b V>>>(&mut self, iter: T) {
39 let mut iter = iter.into_iter();
40
41 let Some(first) = iter.next() else {
43 return;
44 };
45
46 let mut headers = self.0.take().unwrap_or_else(|| {
47 let size = iter.size_hint().0 + 2;
54 OwnedHeaders::new_with_capacity(size)
55 });
56 headers = headers.insert(first);
57 for remaining in iter {
58 headers = headers.insert(remaining);
59 }
60
61 self.0 = Some(headers);
62 }
63}
64
65impl<'a, 'b, V> FromIterator<Header<'a, &'b V>> for KafkaHeaders
66where
67 V: ToBytes + ?Sized,
68{
69 fn from_iter<I: IntoIterator<Item = Header<'a, &'b V>>>(iter: I) -> Self {
70 let mut c = Self::new();
71 c.extend(iter);
72 c
73 }
74}
75
76#[derive(Clone)]
81pub struct Context {
82 producer_name: String,
84 arroyo_statistics: ArroyoProducerContext,
86}
87
88impl Context {
89 pub fn new(producer_name: String) -> Self {
90 Self {
91 arroyo_statistics: ArroyoProducerContext::new(producer_name.clone()),
92 producer_name,
93 }
94 }
95
96 pub fn producer_name(&self) -> &str {
97 &self.producer_name
98 }
99}
100
101impl ClientContext for Context {
102 fn stats(&self, statistics: rdkafka::Statistics) {
106 self.arroyo_statistics.stats(statistics);
107 }
108}
109
110impl ProducerContext for Context {
111 type DeliveryOpaque = ();
112
113 fn delivery(&self, result: &DeliveryResult, _delivery_opaque: Self::DeliveryOpaque) {
116 match result {
120 Ok(message) => {
121 metric!(
122 counter(KafkaCounters::ProduceStatusSuccess) += 1,
123 topic = message.topic(),
124 producer_name = self.producer_name.as_str(),
125 );
126 metric!(
127 counter(KafkaCounters::ProcessingMessageProduced) += 1,
128 topic = message.topic(),
129 producer_name = self.producer_name.as_str(),
130 );
131 }
132 Err((error, message)) => {
133 relay_log::error!(
134 error = error as &dyn Error,
135 payload_len = message.payload_len(),
136 tags.topic = message.topic(),
137 "failed to produce message to Kafka (delivery callback)",
138 );
139
140 metric!(
141 counter(KafkaCounters::ProduceStatusError) += 1,
142 topic = message.topic(),
143 producer_name = self.producer_name.as_str(),
144 );
145 }
146 }
147 }
148}
149
150#[cfg(test)]
151mod tests {
152 use rdkafka::ClientContext;
153 use rdkafka::statistics::{Broker, Statistics};
154 use relay_statsd::with_capturing_test_client;
155
156 use super::Context;
157
158 #[test]
159 fn test_statistics_backend() {
160 let context = Context::new("test-producer".to_owned());
161 let statistics = Statistics {
162 msg_cnt: 42,
163 brokers: [(
164 "broker-id".to_owned(),
165 Broker {
166 name: "broker-name".to_owned(),
167 state: "UP".to_owned(),
168 outbuf_cnt: 7,
169 ..Default::default()
170 },
171 )]
172 .into(),
173 ..Default::default()
174 };
175
176 let metrics = with_capturing_test_client(|| context.stats(statistics));
177 let prefix = "arroyo.producer.librdkafka.";
178 let broker_metric = "broker_outbuf_requests";
179
180 assert!(metrics.contains(&format!(
181 "{prefix}message_count:42|g|#producer_name:test-producer"
182 )));
183 assert!(metrics.contains(&format!("{prefix}{broker_metric}:7|g|#")));
184 assert!(metrics.iter().all(|metric| metric.starts_with(prefix)));
185 }
186}