1use std::sync::Arc;
9
10use krabka_client_core::{
11 Connection,
12 telemetry::{TelemetryConnections, client_instance_id},
13};
14use krabka_units::Time;
15
16use crate::{AdminClient, AdminError, RecoveringConnection};
17
18pub struct AdminTelemetryConnection(pub(crate) Arc<RecoveringConnection>);
22
23impl TelemetryConnections for AdminTelemetryConnection {
24 fn open_connection(&self, _preferred: Option<i32>) -> Option<(i32, Arc<Connection>)> {
25 let connection = self.0.inner.try_read().ok()?;
28 (!connection.is_closed()).then(|| (-1, Arc::clone(&connection)))
29 }
30}
31
32impl AdminClient {
33 pub async fn client_instance_id(
52 &self,
53 timeout: Time,
54 ) -> Result<Option<uuid::Uuid>, AdminError> {
55 Ok(client_instance_id(self.telemetry.as_ref(), timeout).await?)
56 }
57
58 pub async fn close(self) {
64 if let Some(telemetry) = &self.telemetry {
65 telemetry.close().await;
66 }
67 }
68}
69
70#[cfg(test)]
71mod tests {
72 use std::time::Duration;
73
74 use assert2::assert;
75 use bytes::BytesMut;
76 use krabka_client_core::{
77 MockBroker,
78 telemetry::otlp::{MetricData, MetricsData, NumberValue},
79 };
80 use krabka_protocol::{
81 Decode, Encode,
82 owned::{
83 api_versions_request,
84 api_versions_response::{ApiVersion, ApiVersionsResponse},
85 get_telemetry_subscriptions_request::{self, GetTelemetrySubscriptionsRequest},
86 get_telemetry_subscriptions_response::GetTelemetrySubscriptionsResponse,
87 push_telemetry_request::{self, PushTelemetryRequest},
88 push_telemetry_response::PushTelemetryResponse,
89 },
90 primitives::uuid::Uuid as WireUuid,
91 };
92 use tokio::{sync::mpsc, time};
93
94 use crate::AdminClientConfig;
95
96 const INSTANCE: WireUuid = WireUuid([7; 16]);
97 const CONNECTION_COUNT: &str = "org.apache.kafka.admin.client.connection.count";
98
99 #[derive(Clone, Debug, PartialEq)]
102 enum Seen {
103 Subscriptions(WireUuid),
104 Push {
105 client_instance_id: WireUuid,
106 terminating: bool,
107 metrics: Vec<(String, Option<NumberValue>)>,
108 },
109 }
110
111 fn request_body(frame: &[u8]) -> &[u8] {
113 let client_id_len = usize::try_from(i16::from_be_bytes([frame[0], frame[1]])).unwrap_or(0);
114 &frame[2 + client_id_len + 1..]
115 }
116
117 fn flexible_response(response: &impl Encode) -> Vec<u8> {
118 let mut body = BytesMut::from(&[0u8][..]);
119 response.encode(&mut body, 0).unwrap();
120 body.to_vec()
121 }
122
123 fn seen_push(request: &PushTelemetryRequest) -> Seen {
124 let data = MetricsData::decode(&request.metrics).unwrap();
125 let metrics = data
126 .resource_metrics
127 .into_iter()
128 .flat_map(|resource| resource.scope_metrics)
129 .flat_map(|scope| scope.metrics)
130 .map(|metric| {
131 let value = match metric.data {
132 Some(
133 MetricData::Gauge { data_points } | MetricData::Sum { data_points, .. },
134 ) => data_points.first().and_then(|point| point.value),
135 None => None,
136 };
137 (metric.name, value)
138 })
139 .collect();
140 Seen::Push {
141 client_instance_id: request.client_instance_id,
142 terminating: request.terminating,
143 metrics,
144 }
145 }
146
147 async fn start_broker(
151 advertise_telemetry: bool,
152 ) -> (MockBroker, mpsc::UnboundedReceiver<Seen>) {
153 let (seen_tx, seen_rx) = mpsc::unbounded_channel();
154 let broker = MockBroker::start(move |api_key, version, _corr, frame| match api_key {
155 api_versions_request::API_KEY => {
156 let mut keys = vec![(api_versions_request::API_KEY, 3)];
157 if advertise_telemetry {
158 keys.push((get_telemetry_subscriptions_request::API_KEY, 0));
159 keys.push((push_telemetry_request::API_KEY, 0));
160 }
161 let response = ApiVersionsResponse {
162 api_keys: keys
163 .into_iter()
164 .map(|(api_key, max_version)| ApiVersion {
165 api_key,
166 min_version: 0,
167 max_version,
168 ..Default::default()
169 })
170 .collect(),
171 ..Default::default()
172 };
173 let mut body = BytesMut::new();
174 response.encode(&mut body, version).unwrap();
175 Some(body.to_vec())
176 }
177 get_telemetry_subscriptions_request::API_KEY => {
178 let mut body = request_body(frame);
179 let request = GetTelemetrySubscriptionsRequest::decode(&mut body, version).unwrap();
180 let _ = seen_tx.send(Seen::Subscriptions(request.client_instance_id));
181 Some(flexible_response(&GetTelemetrySubscriptionsResponse {
182 client_instance_id: INSTANCE,
183 subscription_id: 5,
184 push_interval_ms: 200,
185 telemetry_max_bytes: 1 << 20,
186 requested_metrics: vec![CONNECTION_COUNT.to_owned()],
187 ..Default::default()
188 }))
189 }
190 push_telemetry_request::API_KEY => {
191 let mut body = request_body(frame);
192 let request = PushTelemetryRequest::decode(&mut body, version).unwrap();
193 let _ = seen_tx.send(seen_push(&request));
194 Some(flexible_response(&PushTelemetryResponse::default()))
195 }
196 _ => None,
197 })
198 .await
199 .unwrap();
200 (broker, seen_rx)
201 }
202
203 async fn next(seen: &mut mpsc::UnboundedReceiver<Seen>) -> Seen {
204 time::timeout(Duration::from_secs(10), seen.recv())
205 .await
206 .expect("a telemetry request within 10 s")
207 .expect("the broker is running")
208 }
209
210 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
216 async fn admin_client_pushes_metrics_and_reports_its_instance_id() {
217 let disabled =
218 "client-core: Telemetry is not enabled. Set config `enable.metrics.push` to \
219 `true`."
220 .to_owned();
221 let push = |terminating| Seen::Push {
222 client_instance_id: INSTANCE,
223 terminating,
224 metrics: vec![(CONNECTION_COUNT.to_owned(), Some(NumberValue::Double(1.0)))],
225 };
226 let cases = [
227 (
228 "enable.metrics.push=true",
229 true,
230 true,
231 2,
232 Ok(Some(uuid::Uuid::from_bytes(INSTANCE.0))),
233 vec![Seen::Subscriptions(WireUuid::ZERO), push(false), push(true)],
234 ),
235 (
236 "enable.metrics.push=false",
237 false,
238 true,
239 0,
240 Err(disabled),
241 vec![],
242 ),
243 ("broker without KIP-714", true, false, 0, Ok(None), vec![]),
244 ];
245 for (name, enable_metrics_push, advertise_telemetry, count, expected_id, expected) in cases
246 {
247 let (broker, mut seen) = start_broker(advertise_telemetry).await;
248 let admin = crate::AdminClient::connect_with_config(
249 &[broker.addr.to_string()],
250 AdminClientConfig {
251 enable_metrics_push,
252 ..AdminClientConfig::default()
253 },
254 )
255 .await
256 .expect("admin connects");
257 let timeout = if count == 0 {
258 krabka_units::millis(300)
259 } else {
260 krabka_units::secs(10)
261 };
262 let id = admin
263 .client_instance_id(timeout)
264 .await
265 .map_err(|error| error.to_string());
266 let mut requests = Vec::new();
267 for _ in 0..count {
268 requests.push(next(&mut seen).await);
269 }
270 time::timeout(Duration::from_secs(20), admin.close())
271 .await
272 .expect("the close ends");
273 broker.stop();
274 while let Ok(request) = seen.try_recv() {
275 requests.push(request);
276 }
277 assert!((id, requests) == (expected_id, expected), "{name}");
278 }
279 }
280}