Skip to main content

sozu_command_lib/proto/
mod.rs

1use std::collections::BTreeMap;
2
3use command::{
4    AggregatedMetrics, BackendMetrics, Bucket, FilteredHistogram, FilteredMetrics, Percentiles,
5    filtered_metrics::Inner,
6};
7use prost::UnknownEnumValue;
8
9/// Contains all types received by and sent from Sōzu
10pub mod command;
11
12/// Implementation of fmt::Display for the protobuf types, used in the CLI
13pub 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
29// Simple helper to build ResponseContent from ContentType
30impl 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
38// Simple helper to build Request from RequestType
39impl 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    /// Merge metrics that were received from several workers
691    ///
692    /// Each worker gather the same kind of metrics,
693    /// for its own proxying logic, and for the same clusters with their backends.
694    /// This means we have to reduce each metric from N instances to 1.
695    pub fn merge_metrics(&mut self) {
696        // avoid copying the worker metrics, by taking them
697        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, // the bucket less-or-equal limits are normalized: 0, 1, 3, 7, 15, ...
780                            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                // You cannot statistically merge two percentile summaries
795                // without the underlying samples. The companion
796                // `<name>_histogram` Inner::Histogram value is the source
797                // of truth for accurate aggregation and merges correctly
798                // above. We still propagate the percentile shape so legacy
799                // consumers reading it observe at least the worst-case
800                // upper bound across workers — element-wise max preserves
801                // the "is anyone slow?" intent. `samples` and `sum` add so
802                // the totals reflect cross-worker volume.
803                *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            // Inner::Time and Inner::Timeserie are never used in Sōzu
828            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        // Regression: a one-worker fleet must populate `clusters` and
1803        // `proxying` so CLI/TUI consumers reading those maps see the
1804        // worker's data. `std::mem::take(&mut self.workers)` empties the
1805        // per-worker map after relocation, which is the documented
1806        // contract when the caller asked for the merged shape.
1807        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        // Multi-worker percentile aggregation propagates the worst-case
1899        // quantile across workers and accumulates samples + sum so the
1900        // surfaced summary remains the "is anyone slow?" upper bound.
1901        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                    // note: there is no bucket for "le: 31"
1974                ],
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 }, // note: the total count of histogram b is added, even though histogram b has no bucket
1991                ],
1992            })),
1993        };
1994
1995        assert_eq!(histogram_a, merged_histogram);
1996    }
1997}