1mod metrics;
27pub mod otlp;
28mod sender;
29
30use std::{sync::Arc, time::Duration};
31
32use krabka_protocol::ProtocolRequest;
33use krabka_units::{Time, convert::TimeExt as _};
34use tokio::{
35 sync::watch,
36 time::{self, Instant, sleep},
37};
38use tokio_util::sync::CancellationToken;
39
40pub(crate) use self::metrics::ConnectionMetrics;
41pub use self::metrics::{
42 ClientMetrics, Counter, Gauge, MetricKey, NetworkMetrics, TELEMETRY_METRIC_DOMAIN,
43};
44use self::{
45 metrics::{Collector, unix_nanos_now},
46 otlp::{KeyValue, Resource},
47 sender::{Failure, TelemetryRequest, TelemetrySender, TelemetryState},
48};
49use crate::{backoff, connection::Connection, error::ClientError};
50
51#[derive(Clone, Copy, Debug, PartialEq, Eq)]
53pub enum TelemetryClientType {
54 Producer,
55 Consumer,
56 Admin,
57}
58
59impl TelemetryClientType {
60 #[must_use]
63 pub const fn metric_group(self) -> &'static str {
64 match self {
65 Self::Producer => "producer-metrics",
66 Self::Consumer => "consumer-metrics",
67 Self::Admin => "admin-client-metrics",
68 }
69 }
70}
71
72#[derive(Clone, Debug, PartialEq, Eq)]
75pub struct ClientTelemetryConfig {
76 pub client_type: TelemetryClientType,
77 pub resource_attributes: Vec<(String, String)>,
79}
80
81impl ClientTelemetryConfig {
82 #[must_use]
84 pub fn producer(transactional_id: Option<&str>) -> Self {
85 Self::with_attributes(
86 TelemetryClientType::Producer,
87 [("transactional_id", transactional_id)],
88 )
89 }
90
91 #[must_use]
94 pub fn consumer(
95 group_id: Option<&str>,
96 group_instance_id: Option<&str>,
97 client_rack: Option<&str>,
98 ) -> Self {
99 Self::with_attributes(
100 TelemetryClientType::Consumer,
101 [
102 ("group_id", group_id),
103 ("group_instance_id", group_instance_id),
104 ("client_rack", client_rack),
105 ],
106 )
107 }
108
109 #[must_use]
111 pub fn admin() -> Self {
112 Self::with_attributes(TelemetryClientType::Admin, [])
113 }
114
115 fn with_attributes<'a>(
116 client_type: TelemetryClientType,
117 attributes: impl IntoIterator<Item = (&'a str, Option<&'a str>)>,
118 ) -> Self {
119 Self {
120 client_type,
121 resource_attributes: attributes
122 .into_iter()
123 .filter_map(|(key, value)| {
124 value
125 .filter(|value| !value.is_empty())
126 .map(|value| (key.to_owned(), value.to_owned()))
127 })
128 .collect(),
129 }
130 }
131
132 fn resource(&self) -> Resource {
133 Resource {
134 attributes: self
135 .resource_attributes
136 .iter()
137 .map(|(key, value)| KeyValue::new(key, value))
138 .collect(),
139 }
140 }
141}
142
143pub trait TelemetryConnections: Send + Sync + 'static {
145 fn open_connection(&self, preferred: Option<i32>) -> Option<(i32, Arc<Connection>)>;
149}
150
151#[derive(Clone, Copy, Debug, PartialEq, Eq)]
153pub struct TelemetryTiming {
154 pub request_timeout: Duration,
157 pub reconnect_backoff: Duration,
159}
160
161#[derive(Debug)]
164pub struct ClientTelemetry {
165 close: CancellationToken,
166 done: CancellationToken,
167 instance_id: Arc<watch::Sender<Option<uuid::Uuid>>>,
170}
171
172impl ClientTelemetry {
173 #[must_use]
179 pub fn start(
180 connections: impl TelemetryConnections,
181 config: &ClientTelemetryConfig,
182 metrics: ClientMetrics,
183 timing: TelemetryTiming,
184 ) -> Self {
185 let close = CancellationToken::new();
186 let done = CancellationToken::new();
187 let instance_id = Arc::new(watch::Sender::new(None));
188 let sender = TelemetrySender::new(Collector::new(metrics, config.resource()));
189 tokio::spawn(run(
190 connections,
191 sender,
192 timing,
193 Arc::clone(&instance_id),
194 (close.clone(), done.clone()),
195 ));
196 Self {
197 close,
198 done,
199 instance_id,
200 }
201 }
202
203 pub async fn client_instance_id(&self, timeout: Duration) -> Option<uuid::Uuid> {
208 let mut loaded = self.instance_id.subscribe();
209 time::timeout(timeout, loaded.wait_for(Option::is_some))
210 .await
211 .ok()
212 .and_then(Result::ok)
213 .and_then(|id| *id)
214 }
215
216 pub async fn close(&self) {
221 self.close.cancel();
222 self.done.cancelled().await;
223 }
224}
225
226impl Drop for ClientTelemetry {
227 fn drop(&mut self) {
228 self.close.cancel();
229 }
230}
231
232pub async fn client_instance_id(
245 telemetry: Option<&ClientTelemetry>,
246 timeout: Time,
247) -> Result<Option<uuid::Uuid>, ClientError> {
248 if !timeout.secs_f64().is_finite() || timeout < Time::ZERO {
249 return Err(ClientError::InvalidArgument(
250 "The timeout cannot be negative.".to_owned(),
251 ));
252 }
253 let telemetry = telemetry.ok_or(ClientError::TelemetryDisabled)?;
254 Ok(telemetry.client_instance_id(timeout.to_std()).await)
255}
256
257async fn run(
260 connections: impl TelemetryConnections,
261 mut sender: TelemetrySender,
262 timing: TelemetryTiming,
263 instance_id: Arc<watch::Sender<Option<uuid::Uuid>>>,
264 (close, done): (CancellationToken, CancellationToken),
265) {
266 let mut closing: Option<Instant> = None;
267 let mut sticky: Option<i32> = None;
268 loop {
269 if closing.is_none() && close.is_cancelled() {
270 closing = Some(Instant::now() + timing.request_timeout);
271 if !sender.initiate_close() {
272 break;
273 }
274 }
275 let now = Instant::now();
276 if closing.is_some_and(|deadline| now >= deadline) {
277 break;
278 }
279 match sender.time_to_next_update(now, timing.request_timeout) {
280 Some(wait) if wait.is_zero() => {}
281 _ if closing.is_some() => break,
284 Some(wait) => {
285 tokio::select! {
286 () = sleep(wait) => {}
287 () = close.cancelled() => {}
288 }
289 continue;
290 }
291 None => {
292 close.cancelled().await;
293 continue;
294 }
295 }
296 let open = connections.open_connection(sticky);
297 let Some((broker_id, connection)) = open else {
298 sticky = None;
299 if closing.is_some() {
300 break;
301 }
302 tokio::select! {
303 () = sleep(timing.reconnect_backoff) => {}
304 () = close.cancelled() => {}
305 }
306 continue;
307 };
308 sticky = Some(broker_id);
309 let Some(request) = sender.create_request(unix_nanos_now()) else {
310 continue;
311 };
312 let failed = match request {
313 TelemetryRequest::Subscriptions(request) => match send(&connection, request).await {
314 Ok(response) => {
315 sender.handle_subscriptions_response(
316 &response,
317 Instant::now(),
318 backoff::random_unit(),
319 );
320 let loaded = sender
322 .client_instance_id()
323 .map(|id| uuid::Uuid::from_bytes(id.0));
324 instance_id.send_if_modified(|current| {
325 let changed = loaded.is_some() && *current != loaded;
326 if changed {
327 *current = loaded;
328 }
329 changed
330 });
331 None
332 }
333 Err(failure) => Some(failure),
334 },
335 TelemetryRequest::Push(request) => match send(&connection, request).await {
336 Ok(response) => {
337 sender.handle_push_response(&response, Instant::now());
338 None
339 }
340 Err(failure) => Some(failure),
341 },
342 };
343 if let Some(failure) = failed {
344 sticky = None;
346 sender.handle_failed_request(failure, Instant::now());
347 }
348 if sender.state() == TelemetryState::TerminatingPushInProgress {
349 break;
350 }
351 }
352 sender.close();
353 done.cancel();
354}
355
356async fn send<R: ProtocolRequest>(
359 connection: &Connection,
360 request: R,
361) -> Result<R::Response, Failure> {
362 if connection.advertised_api_range(R::API_KEY).is_none() {
363 tracing::debug!(
364 api_key = R::API_KEY,
365 "broker does not support client telemetry"
366 );
367 return Err(Failure::Fatal);
368 }
369 connection.send(request).await.map_err(|error| {
370 tracing::debug!(%error, api_key = R::API_KEY, "telemetry request failed");
371 failure(&error)
372 })
373}
374
375fn failure(error: &ClientError) -> Failure {
377 match error {
378 ClientError::Disconnected
379 | ClientError::Timeout(_)
380 | ClientError::Io(_)
381 | ClientError::Connect { .. }
382 | ClientError::Tls { .. }
383 | ClientError::Sasl { .. } => Failure::Retriable,
384 _ => Failure::Fatal,
385 }
386}
387
388#[cfg(test)]
389mod tests;