1use std::collections::BTreeMap;
2
3use command::{
4 AggregatedMetrics, BackendMetrics, Bucket, FilteredHistogram, FilteredMetrics, Percentiles,
5 filtered_metrics::Inner,
6};
7use prost::UnknownEnumValue;
8
9pub mod command;
11
12pub mod display;
14
15#[derive(thiserror::Error, Debug)]
16pub enum DisplayError {
17 #[error("Could not display content")]
18 DisplayContent(String),
19 #[error("Error while parsing response to JSON")]
20 Json(serde_json::Error),
21 #[error("got the wrong response content type: {0}")]
22 WrongResponseType(String),
23 #[error("Could not format the datetime to ISO 8601")]
24 DateTime,
25 #[error("unrecognized protobuf variant: {0}")]
26 DecodeError(UnknownEnumValue),
27}
28
29impl From<command::response_content::ContentType> for command::ResponseContent {
31 fn from(value: command::response_content::ContentType) -> Self {
32 Self {
33 content_type: Some(value),
34 }
35 }
36}
37
38impl From<command::request::RequestType> for command::Request {
40 fn from(value: command::request::RequestType) -> Self {
41 Self {
42 request_type: Some(value),
43 }
44 }
45}
46
47impl std::fmt::Debug for command::Request {
48 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
49 let mut request = f.debug_struct("Request");
50 match self.request_type.as_ref() {
51 Some(request_type) => request.field("request_type", request_type),
52 None => request.field("request_type", &"Unallowed"),
53 };
54 request.finish()
55 }
56}
57
58impl std::fmt::Debug for command::request::RequestType {
59 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
60 use command::request::RequestType;
61
62 match self {
63 RequestType::AddCluster(value) => f.debug_tuple("AddCluster").field(value).finish(),
64 RequestType::AddHttpFrontend(value) => {
65 f.debug_tuple("AddHttpFrontend").field(value).finish()
66 }
67 RequestType::RemoveHttpFrontend(value) => {
68 f.debug_tuple("RemoveHttpFrontend").field(value).finish()
69 }
70 RequestType::AddHttpsFrontend(value) => {
71 f.debug_tuple("AddHttpsFrontend").field(value).finish()
72 }
73 RequestType::RemoveHttpsFrontend(value) => {
74 f.debug_tuple("RemoveHttpsFrontend").field(value).finish()
75 }
76 RequestType::AddCertificate(value) => {
77 f.debug_tuple("AddCertificate").field(value).finish()
78 }
79 RequestType::ReplaceCertificate(value) => {
80 f.debug_tuple("ReplaceCertificate").field(value).finish()
81 }
82 RequestType::RemoveCertificate(value) => {
83 f.debug_tuple("RemoveCertificate").field(value).finish()
84 }
85 RequestType::AddTcpFrontend(value) => {
86 f.debug_tuple("AddTcpFrontend").field(value).finish()
87 }
88 RequestType::RemoveTcpFrontend(value) => {
89 f.debug_tuple("RemoveTcpFrontend").field(value).finish()
90 }
91 RequestType::AddHttpListener(value) => {
92 f.debug_tuple("AddHttpListener").field(value).finish()
93 }
94 RequestType::AddHttpsListener(value) => {
95 f.debug_tuple("AddHttpsListener").field(value).finish()
96 }
97 RequestType::UpdateHttpListener(value) => {
98 f.debug_tuple("UpdateHttpListener").field(value).finish()
99 }
100 RequestType::UpdateHttpsListener(value) => {
101 f.debug_tuple("UpdateHttpsListener").field(value).finish()
102 }
103 RequestType::QueryCertificatesFromTheState(value) => f
104 .debug_tuple("QueryCertificatesFromTheState")
105 .field(value)
106 .finish(),
107 RequestType::QueryCertificatesFromWorkers(value) => f
108 .debug_tuple("QueryCertificatesFromWorkers")
109 .field(value)
110 .finish(),
111 other => f.write_str(display::format_request_type(other)),
112 }
113 }
114}
115
116impl std::fmt::Debug for command::CertificateAndKey {
117 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
118 let certificate_chain_len = self
119 .certificate_chain
120 .iter()
121 .map(String::len)
122 .fold(0usize, usize::saturating_add);
123
124 f.debug_struct("CertificateAndKey")
125 .field("certificate", &"[redacted]")
126 .field("certificate_len", &self.certificate.len())
127 .field("certificate_chain", &"[redacted]")
128 .field("certificate_chain_count", &self.certificate_chain.len())
129 .field("certificate_chain_len", &certificate_chain_len)
130 .field("key", &"[redacted]")
131 .field("key_len", &self.key.len())
132 .field("versions_count", &self.versions.len())
133 .field("names_count", &self.names.len())
134 .field("names_len", &total_string_len(&self.names))
135 .finish()
136 }
137}
138
139impl std::fmt::Debug for command::RemoveCertificate {
140 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
141 f.debug_struct("RemoveCertificate")
142 .field("address", &self.address)
143 .field("fingerprint_len", &self.fingerprint.len())
144 .finish_non_exhaustive()
145 }
146}
147
148impl std::fmt::Debug for command::ReplaceCertificate {
149 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
150 f.debug_struct("ReplaceCertificate")
151 .field("address", &self.address)
152 .field("new_certificate", &self.new_certificate)
153 .field("fingerprint_len", &self.old_fingerprint.len())
154 .field("new_expired_at", &self.new_expired_at)
155 .finish_non_exhaustive()
156 }
157}
158
159impl std::fmt::Debug for command::Cluster {
160 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
161 let (answers_count, answers_key_len, answers_value_len) =
162 summarize_string_map(&self.answers);
163
164 f.debug_struct("Cluster")
165 .field("cluster_id_len", &self.cluster_id.len())
166 .field("sticky_session", &self.sticky_session)
167 .field("https_redirect", &self.https_redirect)
168 .field("proxy_protocol", &self.proxy_protocol)
169 .field("load_balancing", &self.load_balancing)
170 .field("answer_503_len", &self.answer_503.as_ref().map(String::len))
171 .field("load_metric", &self.load_metric)
172 .field("http2", &self.http2)
173 .field("answers_count", &answers_count)
174 .field("answers_key_len", &answers_key_len)
175 .field("answers_value_len", &answers_value_len)
176 .field("https_redirect_port", &self.https_redirect_port)
177 .field("authorized_hashes_count", &self.authorized_hashes.len())
178 .field(
179 "authorized_hashes_len",
180 &total_string_len(&self.authorized_hashes),
181 )
182 .field(
183 "www_authenticate_len",
184 &self.www_authenticate.as_ref().map(String::len),
185 )
186 .field("max_connections_per_ip", &self.max_connections_per_ip)
187 .field("retry_after", &self.retry_after)
188 .field("health_check_present", &self.health_check.is_some())
189 .field("udp_present", &self.udp.is_some())
190 .finish_non_exhaustive()
191 }
192}
193
194impl std::fmt::Debug for command::Header {
195 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
196 f.debug_struct("Header")
197 .field("position", &self.position)
198 .field("key_len", &self.key.len())
199 .field("val_len", &self.val.len())
200 .finish_non_exhaustive()
201 }
202}
203
204impl std::fmt::Debug for command::RequestHttpFrontend {
205 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
206 let tags_len = self.tags.iter().fold(0usize, |total, (key, value)| {
207 total.saturating_add(key.len()).saturating_add(value.len())
208 });
209 let headers_len = self.headers.iter().fold(0usize, |total, header| {
210 total
211 .saturating_add(header.key.len())
212 .saturating_add(header.val.len())
213 });
214
215 f.debug_struct("RequestHttpFrontend")
216 .field("cluster_id_len", &self.cluster_id.as_ref().map(String::len))
217 .field("address", &self.address)
218 .field("hostname_len", &self.hostname.len())
219 .field("path_kind", &self.path.kind)
220 .field("path_len", &self.path.value.len())
221 .field("method_len", &self.method.as_ref().map(String::len))
222 .field("position", &self.position)
223 .field("tags_count", &self.tags.len())
224 .field("tags_len", &tags_len)
225 .field("redirect", &self.redirect)
226 .field("required_auth", &self.required_auth)
227 .field("redirect_scheme", &self.redirect_scheme)
228 .field(
229 "redirect_template_len",
230 &self.redirect_template.as_ref().map(String::len),
231 )
232 .field(
233 "rewrite_host_len",
234 &self.rewrite_host.as_ref().map(String::len),
235 )
236 .field(
237 "rewrite_path_len",
238 &self.rewrite_path.as_ref().map(String::len),
239 )
240 .field("rewrite_port", &self.rewrite_port)
241 .field("headers_count", &self.headers.len())
242 .field("headers_len", &headers_len)
243 .field("hsts", &self.hsts)
244 .finish_non_exhaustive()
245 }
246}
247
248impl std::fmt::Debug for command::RequestTcpFrontend {
249 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
250 let (tags_count, tags_key_len, tags_value_len) = summarize_string_map(&self.tags);
251
252 f.debug_struct("RequestTcpFrontend")
253 .field("cluster_id_len", &self.cluster_id.len())
254 .field("address", &self.address)
255 .field("tags_count", &tags_count)
256 .field("tags_key_len", &tags_key_len)
257 .field("tags_value_len", &tags_value_len)
258 .field("sni_len", &self.sni.as_ref().map(String::len))
259 .field("alpn_count", &self.alpn.len())
260 .field("alpn_len", &total_string_len(&self.alpn))
261 .finish_non_exhaustive()
262 }
263}
264
265fn total_string_len<'a>(values: impl IntoIterator<Item = &'a String>) -> usize {
266 values
267 .into_iter()
268 .map(String::len)
269 .fold(0usize, usize::saturating_add)
270}
271
272fn summarize_http_answers(
273 answers: Option<&command::CustomHttpAnswers>,
274) -> (Option<&'static str>, Option<usize>, Option<usize>) {
275 let Some(answers) = answers else {
276 return (None, None, None);
277 };
278 let values = [
279 answers.answer_301.as_ref(),
280 answers.answer_400.as_ref(),
281 answers.answer_401.as_ref(),
282 answers.answer_404.as_ref(),
283 answers.answer_408.as_ref(),
284 answers.answer_413.as_ref(),
285 answers.answer_421.as_ref(),
286 answers.answer_429.as_ref(),
287 answers.answer_502.as_ref(),
288 answers.answer_503.as_ref(),
289 answers.answer_504.as_ref(),
290 answers.answer_507.as_ref(),
291 ];
292
293 (
294 Some("[redacted]"),
295 Some(values.iter().flatten().count()),
296 Some(total_string_len(values.iter().flatten().copied())),
297 )
298}
299
300fn summarize_string_map(
301 values: &std::collections::BTreeMap<String, String>,
302) -> (usize, usize, usize) {
303 (
304 values.len(),
305 total_string_len(values.keys()),
306 total_string_len(values.values()),
307 )
308}
309
310impl std::fmt::Debug for command::HttpListenerConfig {
311 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
312 let (http_answers, http_answers_count, http_answers_len) =
313 summarize_http_answers(self.http_answers.as_ref());
314 let (answers_count, answers_key_len, answers_value_len) =
315 summarize_string_map(&self.answers);
316
317 f.debug_struct("HttpListenerConfig")
318 .field("address", &self.address)
319 .field("public_address", &self.public_address)
320 .field("expect_proxy", &self.expect_proxy)
321 .field("sticky_name_len", &self.sticky_name.len())
322 .field("front_timeout", &self.front_timeout)
323 .field("back_timeout", &self.back_timeout)
324 .field("connect_timeout", &self.connect_timeout)
325 .field("request_timeout", &self.request_timeout)
326 .field("active", &self.active)
327 .field("http_answers", &http_answers)
328 .field("http_answers_count", &http_answers_count)
329 .field("http_answers_len", &http_answers_len)
330 .field(
331 "sozu_id_header_len",
332 &self.sozu_id_header.as_ref().map(String::len),
333 )
334 .field("answers_count", &answers_count)
335 .field("answers_key_len", &answers_key_len)
336 .field("answers_value_len", &answers_value_len)
337 .field("elide_x_real_ip", &self.elide_x_real_ip)
338 .field("send_x_real_ip", &self.send_x_real_ip)
339 .finish_non_exhaustive()
340 }
341}
342
343impl std::fmt::Debug for command::UpdateHttpListenerConfig {
344 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
345 let (http_answers, http_answers_count, http_answers_len) =
346 summarize_http_answers(self.http_answers.as_ref());
347 let (answers_count, answers_key_len, answers_value_len) =
348 summarize_string_map(&self.answers);
349
350 f.debug_struct("UpdateHttpListenerConfig")
351 .field("address", &self.address)
352 .field("public_address", &self.public_address)
353 .field(
354 "sticky_name_len",
355 &self.sticky_name.as_ref().map(String::len),
356 )
357 .field("http_answers", &http_answers)
358 .field("http_answers_count", &http_answers_count)
359 .field("http_answers_len", &http_answers_len)
360 .field(
361 "sozu_id_header_len",
362 &self.sozu_id_header.as_ref().map(String::len),
363 )
364 .field("answers_count", &answers_count)
365 .field("answers_key_len", &answers_key_len)
366 .field("answers_value_len", &answers_value_len)
367 .finish_non_exhaustive()
368 }
369}
370
371impl std::fmt::Debug for command::UpdateHttpsListenerConfig {
372 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
373 let (http_answers, http_answers_count, http_answers_len) =
374 summarize_http_answers(self.http_answers.as_ref());
375 let alpn_protocols_count = self
376 .alpn_protocols
377 .as_ref()
378 .map(|protocols| protocols.values.len());
379 let alpn_protocols_len = self
380 .alpn_protocols
381 .as_ref()
382 .map(|protocols| total_string_len(&protocols.values));
383 let (answers_count, answers_key_len, answers_value_len) =
384 summarize_string_map(&self.answers);
385
386 f.debug_struct("UpdateHttpsListenerConfig")
387 .field("address", &self.address)
388 .field("public_address", &self.public_address)
389 .field(
390 "sticky_name_len",
391 &self.sticky_name.as_ref().map(String::len),
392 )
393 .field("http_answers", &http_answers)
394 .field("http_answers_count", &http_answers_count)
395 .field("http_answers_len", &http_answers_len)
396 .field("alpn_protocols_count", &alpn_protocols_count)
397 .field("alpn_protocols_len", &alpn_protocols_len)
398 .field(
399 "sozu_id_header_len",
400 &self.sozu_id_header.as_ref().map(String::len),
401 )
402 .field("answers_count", &answers_count)
403 .field("answers_key_len", &answers_key_len)
404 .field("answers_value_len", &answers_value_len)
405 .finish_non_exhaustive()
406 }
407}
408
409impl std::fmt::Debug for command::HttpsListenerConfig {
410 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
411 let certificate = self.certificate.as_ref().map(|_| "[redacted]");
412 let certificate_len = self.certificate.as_ref().map(String::len);
413 let certificate_chain_len = total_string_len(&self.certificate_chain);
414 let key = self.key.as_ref().map(|_| "[redacted]");
415 let key_len = self.key.as_ref().map(String::len);
416 let (http_answers, http_answers_count, http_answers_len) =
417 summarize_http_answers(self.http_answers.as_ref());
418 let sozu_id_header_len = self.sozu_id_header.as_ref().map(String::len);
419 let (answers_count, answers_key_len, answers_value_len) =
420 summarize_string_map(&self.answers);
421
422 f.debug_struct("HttpsListenerConfig")
423 .field("address", &self.address)
424 .field("public_address", &self.public_address)
425 .field("expect_proxy", &self.expect_proxy)
426 .field("sticky_name_len", &self.sticky_name.len())
427 .field("front_timeout", &self.front_timeout)
428 .field("back_timeout", &self.back_timeout)
429 .field("connect_timeout", &self.connect_timeout)
430 .field("request_timeout", &self.request_timeout)
431 .field("active", &self.active)
432 .field("versions_count", &self.versions.len())
433 .field("cipher_list_count", &self.cipher_list.len())
434 .field("cipher_list_len", &total_string_len(&self.cipher_list))
435 .field("cipher_suites_count", &self.cipher_suites.len())
436 .field("cipher_suites_len", &total_string_len(&self.cipher_suites))
437 .field(
438 "signature_algorithms_count",
439 &self.signature_algorithms.len(),
440 )
441 .field(
442 "signature_algorithms_len",
443 &total_string_len(&self.signature_algorithms),
444 )
445 .field("groups_list_count", &self.groups_list.len())
446 .field("groups_list_len", &total_string_len(&self.groups_list))
447 .field("certificate", &certificate)
448 .field("certificate_len", &certificate_len)
449 .field("certificate_chain", &"[redacted]")
450 .field("certificate_chain_count", &self.certificate_chain.len())
451 .field("certificate_chain_len", &certificate_chain_len)
452 .field("key", &key)
453 .field("key_len", &key_len)
454 .field("send_tls13_tickets", &self.send_tls13_tickets)
455 .field("http_answers", &http_answers)
456 .field("http_answers_count", &http_answers_count)
457 .field("http_answers_len", &http_answers_len)
458 .field("alpn_protocols_count", &self.alpn_protocols.len())
459 .field(
460 "alpn_protocols_len",
461 &total_string_len(&self.alpn_protocols),
462 )
463 .field(
464 "h2_max_rst_stream_per_window",
465 &self.h2_max_rst_stream_per_window,
466 )
467 .field("h2_max_ping_per_window", &self.h2_max_ping_per_window)
468 .field(
469 "h2_max_settings_per_window",
470 &self.h2_max_settings_per_window,
471 )
472 .field(
473 "h2_max_empty_data_per_window",
474 &self.h2_max_empty_data_per_window,
475 )
476 .field(
477 "h2_max_continuation_frames",
478 &self.h2_max_continuation_frames,
479 )
480 .field("h2_max_glitch_count", &self.h2_max_glitch_count)
481 .field(
482 "h2_initial_connection_window",
483 &self.h2_initial_connection_window,
484 )
485 .field("h2_max_concurrent_streams", &self.h2_max_concurrent_streams)
486 .field("h2_stream_shrink_ratio", &self.h2_stream_shrink_ratio)
487 .field(
488 "h2_max_rst_stream_lifetime",
489 &self.h2_max_rst_stream_lifetime,
490 )
491 .field(
492 "h2_max_rst_stream_abusive_lifetime",
493 &self.h2_max_rst_stream_abusive_lifetime,
494 )
495 .field(
496 "h2_max_rst_stream_emitted_lifetime",
497 &self.h2_max_rst_stream_emitted_lifetime,
498 )
499 .field("h2_max_header_list_size", &self.h2_max_header_list_size)
500 .field("strict_sni_binding", &self.strict_sni_binding)
501 .field("disable_http11", &self.disable_http11)
502 .field(
503 "h2_stream_idle_timeout_seconds",
504 &self.h2_stream_idle_timeout_seconds,
505 )
506 .field("h2_max_header_table_size", &self.h2_max_header_table_size)
507 .field(
508 "h2_graceful_shutdown_deadline_seconds",
509 &self.h2_graceful_shutdown_deadline_seconds,
510 )
511 .field(
512 "h2_max_window_update_stream0_per_window",
513 &self.h2_max_window_update_stream0_per_window,
514 )
515 .field("sozu_id_header_len", &sozu_id_header_len)
516 .field("answers_count", &answers_count)
517 .field("answers_key_len", &answers_key_len)
518 .field("answers_value_len", &answers_value_len)
519 .field("elide_x_real_ip", &self.elide_x_real_ip)
520 .field("send_x_real_ip", &self.send_x_real_ip)
521 .field("hsts", &self.hsts)
522 .field("h2_max_header_fields", &self.h2_max_header_fields)
523 .finish()
524 }
525}
526
527impl std::fmt::Debug for command::QueryCertificatesFilters {
528 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
529 f.debug_struct("QueryCertificatesFilters")
530 .field("domain_len", &self.domain.as_ref().map(String::len))
531 .field(
532 "fingerprint_len",
533 &self.fingerprint.as_ref().map(String::len),
534 )
535 .finish()
536 }
537}
538
539impl std::fmt::Debug for command::CertificateSummary {
540 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
541 f.debug_struct("CertificateSummary")
542 .field("domain_len", &self.domain.len())
543 .field("fingerprint_len", &self.fingerprint.len())
544 .finish()
545 }
546}
547
548impl std::fmt::Debug for command::CertificatesByAddress {
549 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
550 let domain_len = self
551 .certificate_summaries
552 .iter()
553 .map(|summary| summary.domain.len())
554 .fold(0usize, usize::saturating_add);
555 let fingerprint_len = self
556 .certificate_summaries
557 .iter()
558 .map(|summary| summary.fingerprint.len())
559 .fold(0usize, usize::saturating_add);
560
561 f.debug_struct("CertificatesByAddress")
562 .field("address", &self.address)
563 .field("certificates_count", &self.certificate_summaries.len())
564 .field("domain_len", &domain_len)
565 .field("fingerprint_len", &fingerprint_len)
566 .finish()
567 }
568}
569
570impl std::fmt::Debug for command::ListOfCertificatesByAddress {
571 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
572 let certificates_count = self
573 .certificates
574 .iter()
575 .map(|entry| entry.certificate_summaries.len())
576 .fold(0usize, usize::saturating_add);
577 let domain_len = self
578 .certificates
579 .iter()
580 .flat_map(|entry| &entry.certificate_summaries)
581 .map(|summary| summary.domain.len())
582 .fold(0usize, usize::saturating_add);
583 let fingerprint_len = self
584 .certificates
585 .iter()
586 .flat_map(|entry| &entry.certificate_summaries)
587 .map(|summary| summary.fingerprint.len())
588 .fold(0usize, usize::saturating_add);
589
590 f.debug_struct("ListOfCertificatesByAddress")
591 .field("listeners_count", &self.certificates.len())
592 .field("certificates_count", &certificates_count)
593 .field("domain_len", &domain_len)
594 .field("fingerprint_len", &fingerprint_len)
595 .finish()
596 }
597}
598
599impl std::fmt::Debug for command::CertificatesWithFingerprints {
600 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
601 let fingerprint_len = total_string_len(self.certs.keys());
602 let names_count = self
603 .certs
604 .values()
605 .map(|certificate| certificate.names.len())
606 .fold(0usize, usize::saturating_add);
607 let names_len = self
608 .certs
609 .values()
610 .flat_map(|certificate| &certificate.names)
611 .map(String::len)
612 .fold(0usize, usize::saturating_add);
613
614 f.debug_struct("CertificatesWithFingerprints")
615 .field("certificates_count", &self.certs.len())
616 .field("fingerprint_len", &fingerprint_len)
617 .field("names_count", &names_count)
618 .field("names_len", &names_len)
619 .finish()
620 }
621}
622
623fn response_content_kind(content: Option<&command::ResponseContent>) -> Option<&'static str> {
624 use command::response_content::ContentType;
625
626 content
627 .and_then(|response| response.content_type.as_ref())
628 .map(|content_type| match content_type {
629 ContentType::Workers(_) => "workers",
630 ContentType::Metrics(_) => "metrics",
631 ContentType::WorkerResponses(_) => "worker_responses",
632 ContentType::Event(_) => "event",
633 ContentType::FrontendList(_) => "frontend_list",
634 ContentType::ListenersList(_) => "listeners_list",
635 ContentType::WorkerMetrics(_) => "worker_metrics",
636 ContentType::AvailableMetrics(_) => "available_metrics",
637 ContentType::Clusters(_) => "clusters",
638 ContentType::ClusterHashes(_) => "cluster_hashes",
639 ContentType::CertificatesByAddress(_) => "certificates_by_address",
640 ContentType::CertificatesWithFingerprints(_) => "certificates_with_fingerprints",
641 ContentType::RequestCounts(_) => "request_counts",
642 ContentType::MaxConnectionsPerIpLimit(_) => "max_connections_per_ip_limit",
643 ContentType::HealthChecksList(_) => "health_checks_list",
644 ContentType::MetricDetailStatus(_) => "metric_detail_status",
645 ContentType::WorkerMetricDetailStatus(_) => "worker_metric_detail_status",
646 })
647}
648
649impl std::fmt::Debug for command::ResponseContent {
650 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
651 f.debug_struct("ResponseContent")
652 .field("content_type", &response_content_kind(Some(self)))
653 .finish_non_exhaustive()
654 }
655}
656
657impl std::fmt::Debug for command::WorkerResponses {
658 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
659 f.debug_struct("WorkerResponses")
660 .field("workers_count", &self.map.len())
661 .field("worker_ids_len", &total_string_len(self.map.keys()))
662 .finish_non_exhaustive()
663 }
664}
665
666impl std::fmt::Debug for command::WorkerRequest {
667 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
668 f.debug_struct("WorkerRequest")
669 .field("id_len", &self.id.len())
670 .field("content", &self.content)
671 .finish()
672 }
673}
674
675impl std::fmt::Debug for command::WorkerResponse {
676 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
677 f.debug_struct("WorkerResponse")
678 .field("id_len", &self.id.len())
679 .field("status", &self.status)
680 .field("message_len", &self.message.len())
681 .field(
682 "content_type",
683 &response_content_kind(self.content.as_ref()),
684 )
685 .finish()
686 }
687}
688
689impl AggregatedMetrics {
690 pub fn merge_metrics(&mut self) {
696 let workers = std::mem::take(&mut self.workers);
698
699 for (_worker_id, worker) in workers {
700 for (metric_name, new_value) in worker.proxy {
701 if new_value.is_mergeable() {
702 self.proxying
703 .entry(metric_name)
704 .and_modify(|old_value| old_value.merge(&new_value))
705 .or_insert(new_value);
706 }
707 }
708
709 for (cluster_id, mut cluster_metrics) in worker.clusters {
710 for (metric_name, new_value) in cluster_metrics.cluster {
711 if new_value.is_mergeable() {
712 let cluster = self.clusters.entry(cluster_id.to_owned()).or_default();
713
714 cluster
715 .cluster
716 .entry(metric_name)
717 .and_modify(|old_value| old_value.merge(&new_value))
718 .or_insert(new_value);
719 }
720 }
721
722 for backend in cluster_metrics.backends.drain(..) {
723 for (metric_name, new_value) in backend.metrics {
724 if new_value.is_mergeable() {
725 let cluster = self.clusters.entry(cluster_id.to_owned()).or_default();
726
727 let found_backend = cluster
728 .backends
729 .iter_mut()
730 .find(|present| present.backend_id == backend.backend_id);
731
732 if let Some(existing_backend) = found_backend {
733 let _ = existing_backend
734 .metrics
735 .entry(metric_name)
736 .and_modify(|old_value| old_value.merge(&new_value))
737 .or_insert(new_value);
738 } else {
739 cluster.backends.push(BackendMetrics {
740 backend_id: backend.backend_id.clone(),
741 metrics: BTreeMap::from([(metric_name, new_value)]),
742 });
743 };
744 }
745 }
746 }
747 }
748 }
749 }
750}
751
752impl FilteredMetrics {
753 pub fn merge(&mut self, right: &Self) {
754 match (&self.inner, &right.inner) {
755 (Some(Inner::Gauge(a)), Some(Inner::Gauge(b))) => {
756 *self = Self {
757 inner: Some(Inner::Gauge(a + b)),
758 };
759 }
760 (Some(Inner::Count(a)), Some(Inner::Count(b))) => {
761 *self = Self {
762 inner: Some(Inner::Count(a + b)),
763 };
764 }
765 (Some(Inner::Histogram(a)), Some(Inner::Histogram(b))) => {
766 let longest_len = a.buckets.len().max(b.buckets.len());
767
768 let mut a_count = 0;
769 let mut b_count = 0;
770 let buckets = (0..longest_len)
771 .map(|i| {
772 if let Some(a_bucket) = a.buckets.get(i) {
773 a_count = a_bucket.count;
774 }
775 if let Some(b_bucket) = b.buckets.get(i) {
776 b_count = b_bucket.count;
777 }
778 Bucket {
779 le: (1 << i) - 1, count: a_count + b_count,
781 }
782 })
783 .collect();
784
785 *self = Self {
786 inner: Some(Inner::Histogram(FilteredHistogram {
787 count: a.count + b.count,
788 sum: a.sum + b.sum,
789 buckets,
790 })),
791 };
792 }
793 (Some(Inner::Percentiles(a)), Some(Inner::Percentiles(b))) => {
794 *self = Self {
804 inner: Some(Inner::Percentiles(Percentiles {
805 samples: a.samples + b.samples,
806 p_50: a.p_50.max(b.p_50),
807 p_90: a.p_90.max(b.p_90),
808 p_99: a.p_99.max(b.p_99),
809 p_99_9: a.p_99_9.max(b.p_99_9),
810 p_99_99: a.p_99_99.max(b.p_99_99),
811 p_99_999: a.p_99_999.max(b.p_99_999),
812 p_100: a.p_100.max(b.p_100),
813 sum: a.sum + b.sum,
814 })),
815 };
816 }
817 _ => {}
818 }
819 }
820
821 fn is_mergeable(&self) -> bool {
822 match &self.inner {
823 Some(Inner::Gauge(_))
824 | Some(Inner::Count(_))
825 | Some(Inner::Histogram(_))
826 | Some(Inner::Percentiles(_)) => true,
827 Some(Inner::Time(_)) | Some(Inner::TimeSerie(_)) | None => false,
829 }
830 }
831}
832
833#[cfg(test)]
834mod tests {
835 use std::{
836 collections::{BTreeMap, HashMap},
837 net::SocketAddr,
838 };
839
840 use crate::{certificate::Fingerprint, state::ConfigState};
841
842 use super::AggregatedMetrics;
843 use super::command::{
844 AddCertificate, AlpnProtocols, Bucket, CertificateAndKey, CertificateSummary,
845 CertificatesByAddress, CertificatesWithFingerprints, Cluster, ClusterMetrics,
846 CustomHttpAnswers, FilteredHistogram, FilteredMetrics, Header, HeaderPosition,
847 HttpListenerConfig, HttpsListenerConfig, ListOfCertificatesByAddress, PathRule,
848 PathRuleKind, Percentiles, QueryCertificatesFilters, RedirectPolicy, RedirectScheme,
849 RemoveCertificate, ReplaceCertificate, Request, RequestHttpFrontend, RequestTcpFrontend,
850 ResponseContent, ResponseStatus, RulePosition, TlsVersion, UpdateHttpListenerConfig,
851 UpdateHttpsListenerConfig, WorkerMetrics, WorkerRequest, WorkerResponse, WorkerResponses,
852 filtered_metrics::Inner, request::RequestType, response_content::ContentType,
853 };
854
855 #[test]
856 fn request_debug_redacts_string_variant_payloads_directly_and_when_nested() {
857 const PATH_SECRET: &str = "REQUEST_SAVE_STATE_PATH_SECRET_SENTINEL";
858
859 let request_type = RequestType::SaveState(format!("{PATH_SECRET}{}", "x".repeat(4096)));
860 let direct_output = format!("{request_type:?}");
861 let request = Request::from(request_type);
862 let worker_request = WorkerRequest {
863 id: "safe-request-id".to_owned(),
864 content: request.clone(),
865 };
866 let outputs = [
867 direct_output,
868 format!("{request:?}"),
869 format!("{worker_request:?}"),
870 ];
871
872 for output in outputs {
873 assert!(
874 !output.contains(PATH_SECRET),
875 "Request Debug leaked its string variant payload: {output}"
876 );
877 assert!(
878 output.contains("SaveState"),
879 "Request Debug omitted the bounded request kind: {output}"
880 );
881 assert!(
882 output.len() <= 256,
883 "Request Debug output is not bounded: {} bytes",
884 output.len()
885 );
886 }
887 }
888
889 #[test]
890 fn listener_and_patch_debug_redacts_user_controlled_material() {
891 const HTTP_STICKY_SECRET: &str = "HTTP_LISTENER_STICKY_SECRET_SENTINEL";
892 const HTTP_LEGACY_ANSWER_SECRET: &str = "HTTP_LISTENER_LEGACY_ANSWER_SECRET_SENTINEL";
893 const HTTP_HEADER_SECRET: &str = "HTTP_LISTENER_HEADER_SECRET_SENTINEL";
894 const HTTP_ANSWER_KEY_SECRET: &str = "HTTP_LISTENER_ANSWER_KEY_SECRET_SENTINEL";
895 const HTTP_ANSWER_BODY_SECRET: &str = "HTTP_LISTENER_ANSWER_BODY_SECRET_SENTINEL";
896 const HTTP_PATCH_STICKY_SECRET: &str = "HTTP_PATCH_STICKY_SECRET_SENTINEL";
897 const HTTP_PATCH_LEGACY_ANSWER_SECRET: &str = "HTTP_PATCH_LEGACY_ANSWER_SECRET_SENTINEL";
898 const HTTP_PATCH_HEADER_SECRET: &str = "HTTP_PATCH_HEADER_SECRET_SENTINEL";
899 const HTTP_PATCH_ANSWER_KEY_SECRET: &str = "HTTP_PATCH_ANSWER_KEY_SECRET_SENTINEL";
900 const HTTP_PATCH_ANSWER_BODY_SECRET: &str = "HTTP_PATCH_ANSWER_BODY_SECRET_SENTINEL";
901 const HTTPS_PATCH_STICKY_SECRET: &str = "HTTPS_PATCH_STICKY_SECRET_SENTINEL";
902 const HTTPS_PATCH_LEGACY_ANSWER_SECRET: &str = "HTTPS_PATCH_LEGACY_ANSWER_SECRET_SENTINEL";
903 const HTTPS_PATCH_ALPN_SECRET: &str = "HTTPS_PATCH_ALPN_SECRET_SENTINEL";
904 const HTTPS_PATCH_HEADER_SECRET: &str = "HTTPS_PATCH_HEADER_SECRET_SENTINEL";
905 const HTTPS_PATCH_ANSWER_KEY_SECRET: &str = "HTTPS_PATCH_ANSWER_KEY_SECRET_SENTINEL";
906 const HTTPS_PATCH_ANSWER_BODY_SECRET: &str = "HTTPS_PATCH_ANSWER_BODY_SECRET_SENTINEL";
907
908 let long_value = |marker: &str| format!("{marker}{}", "x".repeat(4096));
909 let http_address: SocketAddr = "127.0.0.1:8080"
910 .parse()
911 .expect("test HTTP listener address must parse");
912 let http_listener = HttpListenerConfig {
913 address: http_address.into(),
914 sticky_name: long_value(HTTP_STICKY_SECRET),
915 http_answers: Some(CustomHttpAnswers {
916 answer_503: Some(long_value(HTTP_LEGACY_ANSWER_SECRET)),
917 ..Default::default()
918 }),
919 sozu_id_header: Some(long_value(HTTP_HEADER_SECRET)),
920 answers: BTreeMap::from([(
921 long_value(HTTP_ANSWER_KEY_SECRET),
922 long_value(HTTP_ANSWER_BODY_SECRET),
923 )]),
924 ..Default::default()
925 };
926 let http_request = Request::from(RequestType::AddHttpListener(http_listener.clone()));
927 let http_worker_request = WorkerRequest {
928 id: "safe-add-http-listener-id".to_owned(),
929 content: http_request.clone(),
930 };
931
932 let http_patch = UpdateHttpListenerConfig {
933 address: http_address.into(),
934 sticky_name: Some(long_value(HTTP_PATCH_STICKY_SECRET)),
935 http_answers: Some(CustomHttpAnswers {
936 answer_503: Some(long_value(HTTP_PATCH_LEGACY_ANSWER_SECRET)),
937 ..Default::default()
938 }),
939 sozu_id_header: Some(long_value(HTTP_PATCH_HEADER_SECRET)),
940 answers: BTreeMap::from([(
941 long_value(HTTP_PATCH_ANSWER_KEY_SECRET),
942 long_value(HTTP_PATCH_ANSWER_BODY_SECRET),
943 )]),
944 ..Default::default()
945 };
946 let http_patch_request = Request::from(RequestType::UpdateHttpListener(http_patch.clone()));
947 let http_patch_worker_request = WorkerRequest {
948 id: "safe-update-http-listener-id".to_owned(),
949 content: http_patch_request.clone(),
950 };
951
952 let https_address: SocketAddr = "127.0.0.1:8443"
953 .parse()
954 .expect("test HTTPS listener address must parse");
955 let https_patch = UpdateHttpsListenerConfig {
956 address: https_address.into(),
957 sticky_name: Some(long_value(HTTPS_PATCH_STICKY_SECRET)),
958 http_answers: Some(CustomHttpAnswers {
959 answer_503: Some(long_value(HTTPS_PATCH_LEGACY_ANSWER_SECRET)),
960 ..Default::default()
961 }),
962 alpn_protocols: Some(AlpnProtocols {
963 values: vec![long_value(HTTPS_PATCH_ALPN_SECRET)],
964 }),
965 sozu_id_header: Some(long_value(HTTPS_PATCH_HEADER_SECRET)),
966 answers: BTreeMap::from([(
967 long_value(HTTPS_PATCH_ANSWER_KEY_SECRET),
968 long_value(HTTPS_PATCH_ANSWER_BODY_SECRET),
969 )]),
970 ..Default::default()
971 };
972 let https_patch_request =
973 Request::from(RequestType::UpdateHttpsListener(https_patch.clone()));
974 let https_patch_worker_request = WorkerRequest {
975 id: "safe-update-https-listener-id".to_owned(),
976 content: https_patch_request.clone(),
977 };
978
979 let debug_outputs = [
980 ("HttpListenerConfig", format!("{http_listener:?}")),
981 ("AddHttpListener Request", format!("{http_request:?}")),
982 (
983 "AddHttpListener WorkerRequest",
984 format!("{http_worker_request:?}"),
985 ),
986 ("UpdateHttpListenerConfig", format!("{http_patch:?}")),
987 (
988 "UpdateHttpListener Request",
989 format!("{http_patch_request:?}"),
990 ),
991 (
992 "UpdateHttpListener WorkerRequest",
993 format!("{http_patch_worker_request:?}"),
994 ),
995 ("UpdateHttpsListenerConfig", format!("{https_patch:?}")),
996 (
997 "UpdateHttpsListener Request",
998 format!("{https_patch_request:?}"),
999 ),
1000 (
1001 "UpdateHttpsListener WorkerRequest",
1002 format!("{https_patch_worker_request:?}"),
1003 ),
1004 ];
1005 let secrets = [
1006 HTTP_STICKY_SECRET,
1007 HTTP_LEGACY_ANSWER_SECRET,
1008 HTTP_HEADER_SECRET,
1009 HTTP_ANSWER_KEY_SECRET,
1010 HTTP_ANSWER_BODY_SECRET,
1011 HTTP_PATCH_STICKY_SECRET,
1012 HTTP_PATCH_LEGACY_ANSWER_SECRET,
1013 HTTP_PATCH_HEADER_SECRET,
1014 HTTP_PATCH_ANSWER_KEY_SECRET,
1015 HTTP_PATCH_ANSWER_BODY_SECRET,
1016 HTTPS_PATCH_STICKY_SECRET,
1017 HTTPS_PATCH_LEGACY_ANSWER_SECRET,
1018 HTTPS_PATCH_ALPN_SECRET,
1019 HTTPS_PATCH_HEADER_SECRET,
1020 HTTPS_PATCH_ANSWER_KEY_SECRET,
1021 HTTPS_PATCH_ANSWER_BODY_SECRET,
1022 ];
1023 let mut leaks = Vec::new();
1024 for (label, output) in &debug_outputs {
1025 for secret in secrets {
1026 if output.contains(secret) {
1027 leaks.push(format!("{label}:{secret}"));
1028 }
1029 }
1030 }
1031 assert!(
1032 leaks.is_empty(),
1033 "listener Debug leaked secret markers through {}",
1034 leaks.join(", ")
1035 );
1036
1037 let expected_metadata = [
1038 (0, "sticky_name_len: 4132"),
1039 (0, "http_answers_count: Some(1)"),
1040 (0, "answers_count: 1"),
1041 (3, "sticky_name_len: Some(4129)"),
1042 (3, "http_answers_count: Some(1)"),
1043 (3, "answers_count: 1"),
1044 (6, "sticky_name_len: Some(4130)"),
1045 (6, "http_answers_count: Some(1)"),
1046 (6, "alpn_protocols_count: Some(1)"),
1047 (6, "answers_count: 1"),
1048 ];
1049 for (output_index, safe_metadata) in expected_metadata {
1050 assert!(
1051 debug_outputs[output_index].1.contains(safe_metadata),
1052 "{} Debug omitted safe metadata {safe_metadata}: {}",
1053 debug_outputs[output_index].0,
1054 debug_outputs[output_index].1,
1055 );
1056 }
1057 for (label, output) in debug_outputs {
1058 assert!(
1059 output.len() <= 4096,
1060 "{label} Debug output is not bounded: {} bytes",
1061 output.len()
1062 );
1063 }
1064 }
1065
1066 #[test]
1067 fn https_listener_debug_redacts_user_controlled_material() {
1068 const STICKY_NAME_SECRET: &str = "HTTPS_LISTENER_STICKY_NAME_SECRET_SENTINEL";
1069 const CIPHER_LIST_SECRET: &str = "HTTPS_LISTENER_CIPHER_LIST_SECRET_SENTINEL";
1070 const CIPHER_SUITES_SECRET: &str = "HTTPS_LISTENER_CIPHER_SUITES_SECRET_SENTINEL";
1071 const SIGNATURE_ALGORITHMS_SECRET: &str =
1072 "HTTPS_LISTENER_SIGNATURE_ALGORITHMS_SECRET_SENTINEL";
1073 const GROUPS_LIST_SECRET: &str = "HTTPS_LISTENER_GROUPS_LIST_SECRET_SENTINEL";
1074 const CERTIFICATE_SECRET: &str = "HTTPS_LISTENER_CERTIFICATE_SECRET_SENTINEL";
1075 const CHAIN_SECRET: &str = "HTTPS_LISTENER_CHAIN_SECRET_SENTINEL";
1076 const KEY_SECRET: &str = "HTTPS_LISTENER_KEY_SECRET_SENTINEL";
1077 const HTTP_ANSWER_SECRET: &str = "HTTPS_LISTENER_HTTP_ANSWER_SECRET_SENTINEL";
1078 const ALPN_SECRET: &str = "HTTPS_LISTENER_ALPN_SECRET_SENTINEL";
1079 const SOZU_ID_HEADER_SECRET: &str = "HTTPS_LISTENER_SOZU_ID_HEADER_SECRET_SENTINEL";
1080 const ANSWER_KEY_SECRET: &str = "HTTPS_LISTENER_ANSWER_KEY_SECRET_SENTINEL";
1081 const ANSWER_VALUE_SECRET: &str = "HTTPS_LISTENER_ANSWER_VALUE_SECRET_SENTINEL";
1082
1083 let long_value = |marker: &str| format!("{marker}{}", "x".repeat(4096));
1084 let address: SocketAddr = "127.0.0.1:8443"
1085 .parse()
1086 .expect("test HTTPS listener address must parse");
1087 let public_address: SocketAddr = "203.0.113.1:443"
1088 .parse()
1089 .expect("test public HTTPS listener address must parse");
1090 let listener = HttpsListenerConfig {
1091 address: address.into(),
1092 public_address: Some(public_address.into()),
1093 sticky_name: long_value(STICKY_NAME_SECRET),
1094 versions: vec![TlsVersion::TlsV13 as i32],
1095 cipher_list: vec![long_value(CIPHER_LIST_SECRET)],
1096 cipher_suites: vec![long_value(CIPHER_SUITES_SECRET)],
1097 signature_algorithms: vec![long_value(SIGNATURE_ALGORITHMS_SECRET)],
1098 groups_list: vec![long_value(GROUPS_LIST_SECRET)],
1099 certificate: Some(long_value(CERTIFICATE_SECRET)),
1100 certificate_chain: vec![long_value(CHAIN_SECRET)],
1101 key: Some(long_value(KEY_SECRET)),
1102 http_answers: Some(CustomHttpAnswers {
1103 answer_404: Some(long_value(HTTP_ANSWER_SECRET)),
1104 ..Default::default()
1105 }),
1106 alpn_protocols: vec![long_value(ALPN_SECRET)],
1107 sozu_id_header: Some(long_value(SOZU_ID_HEADER_SECRET)),
1108 answers: BTreeMap::from([(
1109 long_value(ANSWER_KEY_SECRET),
1110 long_value(ANSWER_VALUE_SECRET),
1111 )]),
1112 ..Default::default()
1113 };
1114 let add_https_listener = RequestType::AddHttpsListener(listener.clone());
1115 let request = Request::from(add_https_listener.clone());
1116 let worker_request = WorkerRequest {
1117 id: "safe-add-https-listener-id".to_owned(),
1118 content: request.clone(),
1119 };
1120 let state = ConfigState {
1121 https_listeners: BTreeMap::from([(address, listener.clone())]),
1122 ..Default::default()
1123 };
1124 let debug_outputs = [
1125 ("HttpsListenerConfig", format!("{listener:?}")),
1126 (
1127 "RequestType::AddHttpsListener",
1128 format!("{add_https_listener:?}"),
1129 ),
1130 ("Request", format!("{request:?}")),
1131 ("WorkerRequest", format!("{worker_request:?}")),
1132 ("ConfigState", format!("{state:?}")),
1133 ];
1134 let secrets = [
1135 STICKY_NAME_SECRET,
1136 CIPHER_LIST_SECRET,
1137 CIPHER_SUITES_SECRET,
1138 SIGNATURE_ALGORITHMS_SECRET,
1139 GROUPS_LIST_SECRET,
1140 CERTIFICATE_SECRET,
1141 CHAIN_SECRET,
1142 KEY_SECRET,
1143 HTTP_ANSWER_SECRET,
1144 ALPN_SECRET,
1145 SOZU_ID_HEADER_SECRET,
1146 ANSWER_KEY_SECRET,
1147 ANSWER_VALUE_SECRET,
1148 ];
1149 let mut leaks = Vec::new();
1150 for (label, output) in &debug_outputs {
1151 for secret in secrets {
1152 if output.contains(secret) {
1153 leaks.push(format!("{label}:{secret}"));
1154 }
1155 }
1156 }
1157 assert!(
1158 leaks.is_empty(),
1159 "HTTPS listener Debug leaked secret markers through {}",
1160 leaks.join(", ")
1161 );
1162
1163 let direct_output = &debug_outputs[0].1;
1164 let expected_metadata = [
1165 format!("sticky_name_len: {}", long_value(STICKY_NAME_SECRET).len()),
1166 "versions_count: 1".to_owned(),
1167 "cipher_list_count: 1".to_owned(),
1168 format!("cipher_list_len: {}", long_value(CIPHER_LIST_SECRET).len()),
1169 "cipher_suites_count: 1".to_owned(),
1170 "signature_algorithms_count: 1".to_owned(),
1171 "groups_list_count: 1".to_owned(),
1172 "certificate: Some(\"[redacted]\")".to_owned(),
1173 format!(
1174 "certificate_len: Some({})",
1175 long_value(CERTIFICATE_SECRET).len()
1176 ),
1177 "certificate_chain: \"[redacted]\"".to_owned(),
1178 "certificate_chain_count: 1".to_owned(),
1179 "key: Some(\"[redacted]\")".to_owned(),
1180 "http_answers_count: Some(1)".to_owned(),
1181 "alpn_protocols_count: 1".to_owned(),
1182 format!(
1183 "sozu_id_header_len: Some({})",
1184 long_value(SOZU_ID_HEADER_SECRET).len()
1185 ),
1186 "answers_count: 1".to_owned(),
1187 ];
1188 for safe_metadata in expected_metadata {
1189 assert!(
1190 direct_output.contains(&safe_metadata),
1191 "HttpsListenerConfig Debug omitted safe metadata {safe_metadata}: {direct_output}"
1192 );
1193 }
1194 for (label, output) in debug_outputs {
1195 assert!(
1196 output.len() <= 4096,
1197 "{label} Debug output is not bounded: {} bytes",
1198 output.len()
1199 );
1200 }
1201 }
1202
1203 #[test]
1204 fn header_debug_redacts_key_and_value() {
1205 const KEY_SECRET: &str = "HEADER_KEY_SECRET_SENTINEL";
1206 const VALUE_SECRET: &str = "HEADER_VALUE_SECRET_SENTINEL";
1207
1208 let long_value = |marker: &str| format!("{marker}{}", "x".repeat(4096));
1209 let header = Header {
1210 position: HeaderPosition::Unspecified as i32,
1211 key: long_value(KEY_SECRET),
1212 val: long_value(VALUE_SECRET),
1213 };
1214 let output = format!("{header:?}");
1215
1216 for secret in [KEY_SECRET, VALUE_SECRET] {
1217 assert!(
1218 !output.contains(secret),
1219 "Header Debug leaked user-controlled marker {secret}"
1220 );
1221 }
1222 for metadata in [
1223 "position: 0".to_owned(),
1224 format!("key_len: {}", long_value(KEY_SECRET).len()),
1225 format!("val_len: {}", long_value(VALUE_SECRET).len()),
1226 ] {
1227 assert!(
1228 output.contains(&metadata),
1229 "Header Debug omitted safe metadata {metadata}: {output}"
1230 );
1231 }
1232 assert!(
1233 output.len() <= 256,
1234 "Header Debug output is not bounded: {} bytes",
1235 output.len()
1236 );
1237 }
1238
1239 #[test]
1240 fn http_frontend_request_debug_redacts_user_controlled_material() {
1241 const CLUSTER_SECRET: &str = "HTTP_FRONTEND_CLUSTER_SECRET_SENTINEL";
1242 const HOSTNAME_SECRET: &str = "HTTP_FRONTEND_HOSTNAME_SECRET_SENTINEL";
1243 const PATH_SECRET: &str = "HTTP_FRONTEND_PATH_SECRET_SENTINEL";
1244 const METHOD_SECRET: &str = "HTTP_FRONTEND_METHOD_SECRET_SENTINEL";
1245 const TAG_KEY_SECRET: &str = "HTTP_FRONTEND_TAG_KEY_SECRET_SENTINEL";
1246 const TAG_VALUE_SECRET: &str = "HTTP_FRONTEND_TAG_VALUE_SECRET_SENTINEL";
1247 const REDIRECT_SECRET: &str = "HTTP_FRONTEND_REDIRECT_SECRET_SENTINEL";
1248 const REWRITE_HOST_SECRET: &str = "HTTP_FRONTEND_REWRITE_HOST_SECRET_SENTINEL";
1249 const REWRITE_PATH_SECRET: &str = "HTTP_FRONTEND_REWRITE_PATH_SECRET_SENTINEL";
1250 const HEADER_KEY_SECRET: &str = "HTTP_FRONTEND_HEADER_KEY_SECRET_SENTINEL";
1251 const HEADER_VALUE_SECRET: &str = "HTTP_FRONTEND_HEADER_VALUE_SECRET_SENTINEL";
1252
1253 let long_value = |marker: &str| format!("{marker}{}", "x".repeat(4096));
1254 let frontend = RequestHttpFrontend {
1255 cluster_id: Some(long_value(CLUSTER_SECRET)),
1256 address: Default::default(),
1257 hostname: long_value(HOSTNAME_SECRET),
1258 path: PathRule {
1259 value: long_value(PATH_SECRET),
1260 kind: PathRuleKind::Prefix as i32,
1261 },
1262 method: Some(long_value(METHOD_SECRET)),
1263 position: RulePosition::Tree as i32,
1264 tags: BTreeMap::from([(long_value(TAG_KEY_SECRET), long_value(TAG_VALUE_SECRET))]),
1265 redirect: Some(RedirectPolicy::Forward as i32),
1266 required_auth: Some(true),
1267 redirect_scheme: Some(RedirectScheme::UseSame as i32),
1268 redirect_template: Some(long_value(REDIRECT_SECRET)),
1269 rewrite_host: Some(long_value(REWRITE_HOST_SECRET)),
1270 rewrite_path: Some(long_value(REWRITE_PATH_SECRET)),
1271 rewrite_port: Some(8443),
1272 headers: vec![Header {
1273 position: HeaderPosition::Request as i32,
1274 key: long_value(HEADER_KEY_SECRET),
1275 val: long_value(HEADER_VALUE_SECRET),
1276 }],
1277 hsts: None,
1278 };
1279 let request_type = RequestType::AddHttpsFrontend(frontend.clone());
1280 let request = Request::from(request_type.clone());
1281 let worker_request = WorkerRequest {
1282 id: "safe-http-frontend-request-id".to_owned(),
1283 content: request.clone(),
1284 };
1285 let debug_outputs = [
1286 ("RequestHttpFrontend", format!("{frontend:?}")),
1287 ("RequestType", format!("{request_type:?}")),
1288 ("Request", format!("{request:?}")),
1289 ("WorkerRequest", format!("{worker_request:?}")),
1290 ];
1291 let secrets = [
1292 CLUSTER_SECRET,
1293 HOSTNAME_SECRET,
1294 PATH_SECRET,
1295 METHOD_SECRET,
1296 TAG_KEY_SECRET,
1297 TAG_VALUE_SECRET,
1298 REDIRECT_SECRET,
1299 REWRITE_HOST_SECRET,
1300 REWRITE_PATH_SECRET,
1301 HEADER_KEY_SECRET,
1302 HEADER_VALUE_SECRET,
1303 ];
1304
1305 for (label, output) in &debug_outputs {
1306 for secret in secrets {
1307 assert!(
1308 !output.contains(secret),
1309 "{label} Debug leaked user-controlled marker {secret}: {output}"
1310 );
1311 }
1312 assert!(
1313 output.len() <= 2048,
1314 "{label} Debug output is not bounded: {} bytes",
1315 output.len()
1316 );
1317 }
1318
1319 let direct_output = &debug_outputs[0].1;
1320 let expected_metadata = [
1321 format!("cluster_id_len: Some({})", long_value(CLUSTER_SECRET).len()),
1322 format!("hostname_len: {}", long_value(HOSTNAME_SECRET).len()),
1323 format!("path_len: {}", long_value(PATH_SECRET).len()),
1324 format!("method_len: Some({})", long_value(METHOD_SECRET).len()),
1325 "tags_count: 1".to_owned(),
1326 "headers_count: 1".to_owned(),
1327 ];
1328 for safe_metadata in expected_metadata {
1329 assert!(
1330 direct_output.contains(&safe_metadata),
1331 "RequestHttpFrontend Debug omitted safe metadata {safe_metadata}: {direct_output}"
1332 );
1333 }
1334 }
1335
1336 #[test]
1337 fn certificate_query_request_debug_redacts_filters_across_command_wrappers() {
1338 const DOMAIN_SECRET: &str = "QUERY_REQUEST_DOMAIN_SECRET_SENTINEL";
1339 const FINGERPRINT_SECRET: &str = "QUERY_REQUEST_FINGERPRINT_SECRET_SENTINEL";
1340 const REQUEST_ID_SECRET: &str = "QUERY_REQUEST_ID_SECRET_SENTINEL";
1341
1342 let long_value = |marker: &str| format!("{marker}{}", "x".repeat(4096));
1343 let filters = QueryCertificatesFilters {
1344 domain: Some(long_value(DOMAIN_SECRET)),
1345 fingerprint: Some(long_value(FINGERPRINT_SECRET)),
1346 };
1347 let request_type = RequestType::QueryCertificatesFromWorkers(filters.clone());
1348 let request = Request::from(request_type.clone());
1349 let worker_request = WorkerRequest {
1350 id: long_value(REQUEST_ID_SECRET),
1351 content: request.clone(),
1352 };
1353 let outputs = [
1354 ("QueryCertificatesFilters", format!("{filters:?}")),
1355 ("RequestType", format!("{request_type:?}")),
1356 ("Request", format!("{request:?}")),
1357 ("WorkerRequest", format!("{worker_request:?}")),
1358 ];
1359
1360 for (label, output) in &outputs {
1361 for secret in [DOMAIN_SECRET, FINGERPRINT_SECRET, REQUEST_ID_SECRET] {
1362 assert!(
1363 !output.contains(secret),
1364 "{label} Debug leaked certificate-query marker {secret}: {output}"
1365 );
1366 }
1367 assert!(
1368 output.len() <= 1024,
1369 "{label} Debug output is not bounded: {} bytes",
1370 output.len()
1371 );
1372 }
1373
1374 let filter_output = &outputs[0].1;
1375 for metadata in [
1376 format!("domain_len: Some({})", long_value(DOMAIN_SECRET).len()),
1377 format!(
1378 "fingerprint_len: Some({})",
1379 long_value(FINGERPRINT_SECRET).len()
1380 ),
1381 ] {
1382 assert!(
1383 filter_output.contains(&metadata),
1384 "QueryCertificatesFilters Debug omitted bounded metadata {metadata}: {filter_output}"
1385 );
1386 }
1387 assert!(
1388 outputs[3]
1389 .1
1390 .contains(&format!("id_len: {}", long_value(REQUEST_ID_SECRET).len())),
1391 "WorkerRequest Debug omitted the bounded request-id length: {}",
1392 outputs[3].1
1393 );
1394 }
1395
1396 #[test]
1397 fn certificate_query_response_debug_redacts_results_across_worker_wrappers() {
1398 const DOMAIN_SECRET: &str = "QUERY_RESPONSE_DOMAIN_SECRET_SENTINEL";
1399 const FINGERPRINT_SECRET: &str = "QUERY_RESPONSE_FINGERPRINT_SECRET_SENTINEL";
1400 const MAP_KEY_SECRET: &str = "QUERY_RESPONSE_MAP_KEY_SECRET_SENTINEL";
1401 const CERTIFICATE_SECRET: &str = "QUERY_RESPONSE_CERTIFICATE_SECRET_SENTINEL";
1402 const KEY_SECRET: &str = "QUERY_RESPONSE_KEY_SECRET_SENTINEL";
1403 const NAME_SECRET: &str = "QUERY_RESPONSE_NAME_SECRET_SENTINEL";
1404 const WORKER_ID_SECRET: &str = "QUERY_RESPONSE_WORKER_ID_SECRET_SENTINEL";
1405 const RESPONSE_ID_SECRET: &str = "QUERY_RESPONSE_ID_SECRET_SENTINEL";
1406 const MESSAGE_SECRET: &str = "QUERY_RESPONSE_MESSAGE_SECRET_SENTINEL";
1407
1408 let long_value = |marker: &str| format!("{marker}{}", "x".repeat(4096));
1409 let summary = CertificateSummary {
1410 domain: long_value(DOMAIN_SECRET),
1411 fingerprint: long_value(FINGERPRINT_SECRET),
1412 };
1413 let by_address = CertificatesByAddress {
1414 address: Default::default(),
1415 certificate_summaries: vec![summary.clone()],
1416 };
1417 let list = ListOfCertificatesByAddress {
1418 certificates: vec![by_address.clone()],
1419 };
1420 let worker_content =
1421 ResponseContent::from(ContentType::CertificatesByAddress(list.clone()));
1422 let worker_response = WorkerResponse {
1423 id: long_value(RESPONSE_ID_SECRET),
1424 status: ResponseStatus::Ok as i32,
1425 message: long_value(MESSAGE_SECRET),
1426 content: Some(worker_content.clone()),
1427 };
1428 let worker_responses = WorkerResponses {
1429 map: BTreeMap::from([(long_value(WORKER_ID_SECRET), worker_content.clone())]),
1430 };
1431 let gathered_content =
1432 ResponseContent::from(ContentType::WorkerResponses(worker_responses.clone()));
1433 let main_certificates = CertificatesWithFingerprints {
1434 certs: BTreeMap::from([(
1435 long_value(MAP_KEY_SECRET),
1436 CertificateAndKey {
1437 certificate: long_value(CERTIFICATE_SECRET),
1438 certificate_chain: Vec::new(),
1439 key: long_value(KEY_SECRET),
1440 versions: Vec::new(),
1441 names: vec![long_value(NAME_SECRET)],
1442 },
1443 )]),
1444 };
1445 let main_content = ResponseContent::from(ContentType::CertificatesWithFingerprints(
1446 main_certificates.clone(),
1447 ));
1448 let outputs = [
1449 ("CertificateSummary", format!("{summary:?}")),
1450 ("CertificatesByAddress", format!("{by_address:?}")),
1451 ("ListOfCertificatesByAddress", format!("{list:?}")),
1452 ("worker ResponseContent", format!("{worker_content:?}")),
1453 ("WorkerResponse", format!("{worker_response:?}")),
1454 ("WorkerResponses", format!("{worker_responses:?}")),
1455 ("gathered ResponseContent", format!("{gathered_content:?}")),
1456 (
1457 "CertificatesWithFingerprints",
1458 format!("{main_certificates:?}"),
1459 ),
1460 ("main ResponseContent", format!("{main_content:?}")),
1461 ];
1462 let secrets = [
1463 DOMAIN_SECRET,
1464 FINGERPRINT_SECRET,
1465 MAP_KEY_SECRET,
1466 CERTIFICATE_SECRET,
1467 KEY_SECRET,
1468 NAME_SECRET,
1469 WORKER_ID_SECRET,
1470 RESPONSE_ID_SECRET,
1471 MESSAGE_SECRET,
1472 ];
1473
1474 for (label, output) in &outputs {
1475 for secret in secrets {
1476 assert!(
1477 !output.contains(secret),
1478 "{label} Debug leaked certificate-query marker {secret}: {output}"
1479 );
1480 }
1481 assert!(
1482 output.len() <= 1024,
1483 "{label} Debug output is not bounded: {} bytes",
1484 output.len()
1485 );
1486 }
1487
1488 for metadata in ["listeners_count: 1", "certificates_count: 1"] {
1489 assert!(
1490 outputs[2].1.contains(metadata),
1491 "ListOfCertificatesByAddress Debug omitted bounded metadata {metadata}: {}",
1492 outputs[2].1
1493 );
1494 }
1495 for metadata in ["id_len:", "message_len:", "content_type:"] {
1496 assert!(
1497 outputs[4].1.contains(metadata),
1498 "WorkerResponse Debug omitted bounded metadata {metadata}: {}",
1499 outputs[4].1
1500 );
1501 }
1502 }
1503
1504 #[test]
1505 fn certificate_debug_redacts_pem_material() {
1506 const CERTIFICATE_SECRET: &str = "CERTIFICATE_SECRET_SENTINEL";
1507 const CHAIN_SECRET: &str = "CHAIN_SECRET_SENTINEL";
1508 const KEY_SECRET: &str = "KEY_SECRET_SENTINEL";
1509 const NAME_SECRET: &str = "CERTIFICATE_NAME_SECRET_SENTINEL";
1510
1511 let certificate = CertificateAndKey {
1512 certificate: CERTIFICATE_SECRET.to_owned(),
1513 certificate_chain: vec![CHAIN_SECRET.to_owned()],
1514 key: KEY_SECRET.to_owned(),
1515 versions: vec![i32::MIN; 1_024],
1516 names: vec![format!("{NAME_SECRET}{}", "x".repeat(4096))],
1517 };
1518 let add_certificate = AddCertificate {
1519 address: Default::default(),
1520 certificate: certificate.clone(),
1521 expired_at: None,
1522 };
1523 let replace_certificate = ReplaceCertificate {
1524 address: Default::default(),
1525 new_certificate: certificate.clone(),
1526 old_fingerprint: "safe-old-fingerprint".to_owned(),
1527 new_expired_at: None,
1528 };
1529 let add_request = Request::from(RequestType::AddCertificate(add_certificate.clone()));
1530 let replace_request =
1531 Request::from(RequestType::ReplaceCertificate(replace_certificate.clone()));
1532 let add_worker_request = WorkerRequest {
1533 id: "safe-add-request-id".to_owned(),
1534 content: add_request.clone(),
1535 };
1536 let replace_worker_request = WorkerRequest {
1537 id: "safe-replace-request-id".to_owned(),
1538 content: replace_request.clone(),
1539 };
1540 let state_address: SocketAddr = "127.0.0.1:443"
1541 .parse()
1542 .expect("test certificate address must parse");
1543 let state = ConfigState {
1544 certificates: HashMap::from([(
1545 state_address,
1546 HashMap::from([(Fingerprint(vec![0; 32]), certificate.clone())]),
1547 )]),
1548 ..Default::default()
1549 };
1550
1551 let debug_outputs = [
1552 format!("{certificate:?}"),
1553 format!("{add_certificate:?}"),
1554 format!("{replace_certificate:?}"),
1555 format!("{add_request:?}"),
1556 format!("{replace_request:?}"),
1557 format!("{add_worker_request:?}"),
1558 format!("{replace_worker_request:?}"),
1559 ];
1560 let state_debug = format!("{state:?}");
1561
1562 for output in debug_outputs {
1563 for secret in [CERTIFICATE_SECRET, CHAIN_SECRET, KEY_SECRET, NAME_SECRET] {
1564 assert!(
1565 !output.contains(secret),
1566 "debug output leaked secret marker {secret}: {output}"
1567 );
1568 }
1569 assert!(
1570 output.contains("[redacted]"),
1571 "debug output must make redaction explicit: {output}"
1572 );
1573 assert!(
1574 output.contains("names_count: 1") && output.contains("names_len: 4128"),
1575 "debug output must retain bounded certificate-name metadata: {output}"
1576 );
1577 assert!(
1578 output.contains("versions_count: 1024"),
1579 "debug output must retain bounded TLS-version metadata: {output}"
1580 );
1581 assert!(
1582 output.len() <= 4096,
1583 "certificate Debug output is not bounded: {} bytes",
1584 output.len()
1585 );
1586 }
1587
1588 for secret in [CERTIFICATE_SECRET, CHAIN_SECRET, KEY_SECRET, NAME_SECRET] {
1589 assert!(
1590 !state_debug.contains(secret),
1591 "ConfigState Debug leaked secret marker {secret}: {state_debug}"
1592 );
1593 }
1594 assert!(
1595 state_debug.contains("certificate_addresses_count: 1")
1596 && state_debug.contains("certificates_count: 1"),
1597 "ConfigState Debug must retain count-only certificate metadata: {state_debug}"
1598 );
1599 assert!(
1600 state_debug.len() <= 4096,
1601 "ConfigState Debug output is not bounded: {} bytes",
1602 state_debug.len()
1603 );
1604 }
1605
1606 #[test]
1607 fn certificate_fingerprint_control_debug_is_redacted_across_wrappers() {
1608 const FINGERPRINT_SECRET: &str = "CERTIFICATE_CONTROL_FINGERPRINT_SECRET_SENTINEL";
1609
1610 let fingerprint = format!("{FINGERPRINT_SECRET}{}", "f".repeat(4096));
1611 let remove = RemoveCertificate {
1612 address: Default::default(),
1613 fingerprint: fingerprint.clone(),
1614 };
1615 let replace = ReplaceCertificate {
1616 address: Default::default(),
1617 new_certificate: CertificateAndKey::default(),
1618 old_fingerprint: fingerprint.clone(),
1619 new_expired_at: None,
1620 };
1621 let remove_request = Request::from(RequestType::RemoveCertificate(remove.clone()));
1622 let replace_request = Request::from(RequestType::ReplaceCertificate(replace.clone()));
1623 let outputs = [
1624 ("RemoveCertificate", format!("{remove:?}")),
1625 ("ReplaceCertificate", format!("{replace:?}")),
1626 ("remove Request", format!("{remove_request:?}")),
1627 ("replace Request", format!("{replace_request:?}")),
1628 (
1629 "remove WorkerRequest",
1630 format!(
1631 "{:?}",
1632 WorkerRequest {
1633 id: "remove-certificate-test".to_owned(),
1634 content: remove_request,
1635 }
1636 ),
1637 ),
1638 (
1639 "replace WorkerRequest",
1640 format!(
1641 "{:?}",
1642 WorkerRequest {
1643 id: "replace-certificate-test".to_owned(),
1644 content: replace_request,
1645 }
1646 ),
1647 ),
1648 ];
1649
1650 for (label, output) in outputs {
1651 assert!(
1652 !output.contains(FINGERPRINT_SECRET),
1653 "{label} Debug leaked the certificate fingerprint: {output}"
1654 );
1655 assert!(
1656 output.contains(&format!("fingerprint_len: {}", fingerprint.len())),
1657 "{label} Debug omitted bounded fingerprint metadata: {output}"
1658 );
1659 assert!(
1660 output.len() <= 1024,
1661 "{label} Debug output is not bounded: {} bytes",
1662 output.len()
1663 );
1664 }
1665 }
1666
1667 #[test]
1668 fn cluster_debug_redacts_answers_and_authorization_data_across_wrappers() {
1669 const CLUSTER_SECRET: &str = "CLUSTER_ID_SECRET_SENTINEL";
1670 const LEGACY_ANSWER_SECRET: &str = "CLUSTER_LEGACY_ANSWER_SECRET_SENTINEL";
1671 const ANSWER_KEY_SECRET: &str = "CLUSTER_ANSWER_KEY_SECRET_SENTINEL";
1672 const ANSWER_BODY_SECRET: &str = "CLUSTER_ANSWER_BODY_SECRET_SENTINEL";
1673 const AUTH_HASH_SECRET: &str = "CLUSTER_AUTH_HASH_SECRET_SENTINEL";
1674 const REALM_SECRET: &str = "CLUSTER_AUTH_REALM_SECRET_SENTINEL";
1675
1676 let long_value = |marker: &str| format!("{marker}{}", "x".repeat(4096));
1677 let cluster = Cluster {
1678 cluster_id: long_value(CLUSTER_SECRET),
1679 answer_503: Some(long_value(LEGACY_ANSWER_SECRET)),
1680 answers: BTreeMap::from([(
1681 long_value(ANSWER_KEY_SECRET),
1682 long_value(ANSWER_BODY_SECRET),
1683 )]),
1684 authorized_hashes: vec![long_value(AUTH_HASH_SECRET)],
1685 www_authenticate: Some(long_value(REALM_SECRET)),
1686 ..Default::default()
1687 };
1688 let request = Request::from(RequestType::AddCluster(cluster.clone()));
1689 let outputs = [
1690 ("Cluster", format!("{cluster:?}")),
1691 ("Request", format!("{request:?}")),
1692 (
1693 "WorkerRequest",
1694 format!(
1695 "{:?}",
1696 WorkerRequest {
1697 id: "cluster-redaction-test".to_owned(),
1698 content: request,
1699 }
1700 ),
1701 ),
1702 ];
1703
1704 for (label, output) in outputs {
1705 for secret in [
1706 CLUSTER_SECRET,
1707 LEGACY_ANSWER_SECRET,
1708 ANSWER_KEY_SECRET,
1709 ANSWER_BODY_SECRET,
1710 AUTH_HASH_SECRET,
1711 REALM_SECRET,
1712 ] {
1713 assert!(
1714 !output.contains(secret),
1715 "{label} Debug leaked cluster marker {secret}: {output}"
1716 );
1717 }
1718 for metadata in [
1719 "cluster_id_len:",
1720 "answers_count: 1",
1721 "authorized_hashes_count: 1",
1722 ] {
1723 assert!(
1724 output.contains(metadata),
1725 "{label} Debug omitted bounded cluster metadata {metadata}: {output}"
1726 );
1727 }
1728 assert!(
1729 output.len() <= 2048,
1730 "{label} Debug output is not bounded: {} bytes",
1731 output.len()
1732 );
1733 }
1734 }
1735
1736 #[test]
1737 fn tcp_frontend_debug_redacts_stored_sni_routing_data_across_wrappers() {
1738 const CLUSTER_SECRET: &str = "TCP_FRONTEND_CLUSTER_SECRET_SENTINEL";
1739 const TAG_KEY_SECRET: &str = "TCP_FRONTEND_TAG_KEY_SECRET_SENTINEL";
1740 const TAG_VALUE_SECRET: &str = "TCP_FRONTEND_TAG_VALUE_SECRET_SENTINEL";
1741 const SNI_SECRET: &str = "TCP_FRONTEND_SNI_SECRET_SENTINEL";
1742 const ALPN_SECRET: &str = "TCP_FRONTEND_ALPN_SECRET_SENTINEL";
1743
1744 let long_value = |marker: &str| format!("{marker}{}", "x".repeat(4096));
1745 let frontend = RequestTcpFrontend {
1746 cluster_id: long_value(CLUSTER_SECRET),
1747 address: Default::default(),
1748 tags: BTreeMap::from([(long_value(TAG_KEY_SECRET), long_value(TAG_VALUE_SECRET))]),
1749 sni: Some(long_value(SNI_SECRET)),
1750 alpn: vec![long_value(ALPN_SECRET)],
1751 };
1752 let request = Request::from(RequestType::AddTcpFrontend(frontend.clone()));
1753 let outputs = [
1754 ("RequestTcpFrontend", format!("{frontend:?}")),
1755 ("Request", format!("{request:?}")),
1756 (
1757 "WorkerRequest",
1758 format!(
1759 "{:?}",
1760 WorkerRequest {
1761 id: "tcp-frontend-redaction-test".to_owned(),
1762 content: request,
1763 }
1764 ),
1765 ),
1766 ];
1767
1768 for (label, output) in outputs {
1769 for secret in [
1770 CLUSTER_SECRET,
1771 TAG_KEY_SECRET,
1772 TAG_VALUE_SECRET,
1773 SNI_SECRET,
1774 ALPN_SECRET,
1775 ] {
1776 assert!(
1777 !output.contains(secret),
1778 "{label} Debug leaked TCP frontend marker {secret}: {output}"
1779 );
1780 }
1781 for metadata in [
1782 "cluster_id_len:",
1783 "tags_count: 1",
1784 "sni_len: Some(",
1785 "alpn_count: 1",
1786 ] {
1787 assert!(
1788 output.contains(metadata),
1789 "{label} Debug omitted bounded TCP frontend metadata {metadata}: {output}"
1790 );
1791 }
1792 assert!(
1793 output.len() <= 1024,
1794 "{label} Debug output is not bounded: {} bytes",
1795 output.len()
1796 );
1797 }
1798 }
1799
1800 #[test]
1801 fn merge_relocates_single_worker_to_top_level() {
1802 let mut worker = WorkerMetrics {
1808 proxy: BTreeMap::new(),
1809 clusters: BTreeMap::new(),
1810 };
1811 worker.proxy.insert(
1812 "requests".to_owned(),
1813 FilteredMetrics {
1814 inner: Some(Inner::Count(42)),
1815 },
1816 );
1817 let mut cluster = ClusterMetrics {
1818 cluster: BTreeMap::new(),
1819 backends: Vec::new(),
1820 };
1821 cluster.cluster.insert(
1822 "requests".to_owned(),
1823 FilteredMetrics {
1824 inner: Some(Inner::Count(7)),
1825 },
1826 );
1827 worker.clusters.insert("cluster-a".to_owned(), cluster);
1828
1829 let mut agg = AggregatedMetrics {
1830 main: BTreeMap::new(),
1831 workers: BTreeMap::from([("0".to_owned(), worker)]),
1832 clusters: BTreeMap::new(),
1833 proxying: BTreeMap::new(),
1834 };
1835
1836 agg.merge_metrics();
1837
1838 assert!(
1839 agg.workers.is_empty(),
1840 "merge takes ownership of the per-worker map"
1841 );
1842 assert_eq!(
1843 agg.proxying.get("requests"),
1844 Some(&FilteredMetrics {
1845 inner: Some(Inner::Count(42)),
1846 }),
1847 "single worker's proxy counter must surface in proxying"
1848 );
1849 let cluster_a = agg
1850 .clusters
1851 .get("cluster-a")
1852 .expect("cluster row must surface in top-level clusters");
1853 assert_eq!(
1854 cluster_a.cluster.get("requests"),
1855 Some(&FilteredMetrics {
1856 inner: Some(Inner::Count(7)),
1857 })
1858 );
1859 }
1860
1861 #[test]
1862 fn merge_counts_and_gauges() {
1863 let mut gauge_a = FilteredMetrics {
1864 inner: Some(Inner::Gauge(4)),
1865 };
1866 let gauge_b = FilteredMetrics {
1867 inner: Some(Inner::Gauge(4)),
1868 };
1869
1870 gauge_a.merge(&gauge_b);
1871
1872 assert_eq!(
1873 gauge_a,
1874 FilteredMetrics {
1875 inner: Some(Inner::Gauge(8)),
1876 }
1877 );
1878
1879 let mut count_a = FilteredMetrics {
1880 inner: Some(Inner::Count(3)),
1881 };
1882 let count_b = FilteredMetrics {
1883 inner: Some(Inner::Count(3)),
1884 };
1885
1886 count_a.merge(&count_b);
1887
1888 assert_eq!(
1889 count_a,
1890 FilteredMetrics {
1891 inner: Some(Inner::Count(6)),
1892 }
1893 );
1894 }
1895
1896 #[test]
1897 fn merge_percentiles_takes_max_per_quantile() {
1898 let mut left = FilteredMetrics {
1902 inner: Some(Inner::Percentiles(Percentiles {
1903 samples: 100,
1904 p_50: 5,
1905 p_90: 20,
1906 p_99: 100,
1907 p_99_9: 200,
1908 p_99_99: 250,
1909 p_99_999: 300,
1910 p_100: 400,
1911 sum: 12_000,
1912 })),
1913 };
1914 let right = FilteredMetrics {
1915 inner: Some(Inner::Percentiles(Percentiles {
1916 samples: 50,
1917 p_50: 7,
1918 p_90: 15,
1919 p_99: 80,
1920 p_99_9: 240,
1921 p_99_99: 245,
1922 p_99_999: 290,
1923 p_100: 380,
1924 sum: 6_000,
1925 })),
1926 };
1927 left.merge(&right);
1928 assert_eq!(
1929 left,
1930 FilteredMetrics {
1931 inner: Some(Inner::Percentiles(Percentiles {
1932 samples: 150,
1933 p_50: 7,
1934 p_90: 20,
1935 p_99: 100,
1936 p_99_9: 240,
1937 p_99_99: 250,
1938 p_99_999: 300,
1939 p_100: 400,
1940 sum: 18_000,
1941 })),
1942 }
1943 );
1944 }
1945
1946 #[test]
1947 fn merge_histograms() {
1948 let mut histogram_a = FilteredMetrics {
1949 inner: Some(Inner::Histogram(FilteredHistogram {
1950 sum: 95,
1951 count: 30,
1952 buckets: vec![
1953 Bucket { le: 0, count: 1 },
1954 Bucket { le: 1, count: 2 },
1955 Bucket { le: 3, count: 10 },
1956 Bucket { le: 7, count: 25 },
1957 Bucket { le: 15, count: 27 },
1958 Bucket { le: 31, count: 30 },
1959 ],
1960 })),
1961 };
1962
1963 let histogram_b = FilteredMetrics {
1964 inner: Some(Inner::Histogram(FilteredHistogram {
1965 sum: 82,
1966 count: 40,
1967 buckets: vec![
1968 Bucket { le: 0, count: 0 },
1969 Bucket { le: 1, count: 0 },
1970 Bucket { le: 3, count: 12 },
1971 Bucket { le: 7, count: 30 },
1972 Bucket { le: 15, count: 40 },
1973 ],
1975 })),
1976 };
1977
1978 histogram_a.merge(&histogram_b);
1979
1980 let merged_histogram = FilteredMetrics {
1981 inner: Some(Inner::Histogram(FilteredHistogram {
1982 sum: 177,
1983 count: 70,
1984 buckets: vec![
1985 Bucket { le: 0, count: 1 },
1986 Bucket { le: 1, count: 2 },
1987 Bucket { le: 3, count: 22 },
1988 Bucket { le: 7, count: 55 },
1989 Bucket { le: 15, count: 67 },
1990 Bucket { le: 31, count: 70 }, ],
1992 })),
1993 };
1994
1995 assert_eq!(histogram_a, merged_histogram);
1996 }
1997}