Skip to main content

sozu_command_lib/proto/
display.rs

1use std::{
2    collections::{BTreeMap, HashMap, HashSet},
3    fmt::{self, Display, Formatter},
4    net::SocketAddr,
5};
6
7use prettytable::{Row, Table, cell, row};
8use time::format_description;
9use x509_parser::time::ASN1Time;
10
11use super::command::FilteredHistogram;
12use crate::{
13    AsString,
14    proto::{
15        DisplayError,
16        command::{
17            AggregatedMetrics, AvailableMetrics, CertificateAndKey, CertificateSummary,
18            CertificatesWithFingerprints, ClusterMetrics, CustomHttpAnswers, Event, EventKind,
19            FilteredMetrics, HealthChecksList, HttpEndpoint, HttpListenerConfig,
20            HttpsListenerConfig, ListOfCertificatesByAddress, ListedFrontends, ListenersList,
21            MetricDetailStatus, ProtobufEndpoint, QueryCertificatesFilters, RequestCounts,
22            Response, ResponseContent, ResponseStatus, RunState, SocketAddress, TlsVersion,
23            WorkerInfos, WorkerMetrics, WorkerResponses, filtered_metrics, protobuf_endpoint,
24            request::RequestType, response_content::ContentType,
25        },
26    },
27};
28
29impl Display for CertificateAndKey {
30    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
31        let versions = self.versions.iter().fold(String::new(), |acc, tls_v| {
32            acc + " "
33                + match TlsVersion::try_from(*tls_v) {
34                    Ok(v) => v.as_str_name(),
35                    Err(_) => "",
36                }
37        });
38        write!(
39            f,
40            "\tcertificate: {}\n\tcertificate_chain: {:?}\n\tkey: {}\n\tTLS versions: {}\n\tnames: {:?}",
41            self.certificate,
42            self.certificate_chain,
43            self.key,
44            versions,
45            concatenate_vector(&self.names)
46        )
47    }
48}
49
50impl Display for CertificateSummary {
51    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
52        write!(f, "{}:\t{}", self.fingerprint, self.domain)
53    }
54}
55
56impl Display for QueryCertificatesFilters {
57    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
58        if let Some(d) = self.domain.clone() {
59            write!(f, "domain:{d}")
60        } else if let Some(fp) = self.fingerprint.clone() {
61            write!(f, "fingerprint:{fp}")
62        } else {
63            write!(f, "all certificates")
64        }
65    }
66}
67
68pub fn concatenate_vector(vec: &[String]) -> String {
69    vec.join(", ")
70}
71
72pub fn format_request_type(request_type: &RequestType) -> &str {
73    match request_type {
74        RequestType::SaveState(_) => "SaveState",
75        RequestType::LoadState(_) => "LoadState",
76        RequestType::CountRequests(_) => "CountRequests",
77        RequestType::ListWorkers(_) => "ListWorkers",
78        RequestType::ListFrontends(_) => "ListFrontends",
79        RequestType::ListListeners(_) => "ListListeners",
80        RequestType::LaunchWorker(_) => "LaunchWorker",
81        RequestType::UpgradeMain(_) => "UpgradeMain",
82        RequestType::UpgradeWorker(_) => "UpgradeWorker",
83        RequestType::SubscribeEvents(_) => "SubscribeEvents",
84        RequestType::ReloadConfiguration(_) => "ReloadConfiguration",
85        RequestType::Status(_) => "Status",
86        RequestType::AddCluster(_) => "AddCluster",
87        RequestType::RemoveCluster(_) => "RemoveCluster",
88        RequestType::AddHttpFrontend(_) => "AddHttpFrontend",
89        RequestType::RemoveHttpFrontend(_) => "RemoveHttpFrontend",
90        RequestType::AddHttpsFrontend(_) => "AddHttpsFrontend",
91        RequestType::RemoveHttpsFrontend(_) => "RemoveHttpsFrontend",
92        RequestType::AddCertificate(_) => "AddCertificate",
93        RequestType::ReplaceCertificate(_) => "ReplaceCertificate",
94        RequestType::RemoveCertificate(_) => "RemoveCertificate",
95        RequestType::AddTcpFrontend(_) => "AddTcpFrontend",
96        RequestType::RemoveTcpFrontend(_) => "RemoveTcpFrontend",
97        RequestType::AddBackend(_) => "AddBackend",
98        RequestType::RemoveBackend(_) => "RemoveBackend",
99        RequestType::AddHttpListener(_) => "AddHttpListener",
100        RequestType::AddHttpsListener(_) => "AddHttpsListener",
101        RequestType::AddTcpListener(_) => "AddTcpListener",
102        RequestType::AddUdpListener(_) => "AddUdpListener",
103        RequestType::UpdateUdpListener(_) => "UpdateUdpListener",
104        RequestType::AddUdpFrontend(_) => "AddUdpFrontend",
105        RequestType::RemoveUdpFrontend(_) => "RemoveUdpFrontend",
106        RequestType::RemoveListener(_) => "RemoveListener",
107        RequestType::ActivateListener(_) => "ActivateListener",
108        RequestType::DeactivateListener(_) => "DeactivateListener",
109        RequestType::QueryClusterById(_) => "QueryClusterById",
110        RequestType::QueryClustersByDomain(_) => "QueryClustersByDomain",
111        RequestType::QueryClustersHashes(_) => "QueryClustersHashes",
112        RequestType::QueryMetrics(_) => "QueryMetrics",
113        RequestType::SoftStop(_) => "SoftStop",
114        RequestType::HardStop(_) => "HardStop",
115        RequestType::ConfigureMetrics(_) => "ConfigureMetrics",
116        RequestType::Logging(_) => "Logging",
117        RequestType::ReturnListenSockets(_) => "ReturnListenSockets",
118        RequestType::QueryCertificatesFromTheState(_) => "QueryCertificatesFromTheState",
119        RequestType::QueryCertificatesFromWorkers(_) => "QueryCertificatesFromWorkers",
120        RequestType::UpdateHttpListener(_) => "UpdateHttpListener",
121        RequestType::UpdateHttpsListener(_) => "UpdateHttpsListener",
122        RequestType::UpdateTcpListener(_) => "UpdateTcpListener",
123        RequestType::SetMaxConnectionsPerIp(_) => "SetMaxConnectionsPerIp",
124        RequestType::QueryMaxConnectionsPerIp(_) => "QueryMaxConnectionsPerIp",
125        RequestType::SetHealthCheck(_) => "SetHealthCheck",
126        RequestType::RemoveHealthCheck(_) => "RemoveHealthCheck",
127        RequestType::QueryHealthChecks(_) => "QueryHealthChecks",
128        RequestType::SetMetricDetail(_) => "SetMetricDetail",
129    }
130}
131
132pub fn print_json_response<T: ::serde::Serialize>(input: &T) -> Result<(), DisplayError> {
133    let pretty_json = serde_json::to_string_pretty(&input).map_err(DisplayError::Json)?;
134    println!("{pretty_json}");
135    Ok(())
136}
137
138impl Response {
139    pub fn display(&self, json: bool) -> Result<(), DisplayError> {
140        match self.status() {
141            ResponseStatus::Ok => {
142                // avoid displaying anything else than JSON
143                if !json {
144                    println!("Success: {}", self.message)
145                }
146            }
147            ResponseStatus::Failure => println!("Failure: {}", self.message),
148            ResponseStatus::Processing => {
149                return Err(DisplayError::WrongResponseType(
150                    "ResponseStatus::Processing".to_string(),
151                ));
152            }
153        }
154
155        match &self.content {
156            Some(content) => content.display(json),
157            None => {
158                if json {
159                    println!("{{}}");
160                } else {
161                    println!("No content");
162                }
163                Ok(())
164            }
165        }
166    }
167}
168
169impl ResponseContent {
170    fn display(&self, json: bool) -> Result<(), DisplayError> {
171        let content_type = match &self.content_type {
172            Some(content_type) => content_type,
173            None => {
174                println!("No content");
175                return Ok(());
176            }
177        };
178
179        if json {
180            return print_json_response(&content_type);
181        }
182
183        match content_type {
184            ContentType::Workers(worker_infos) => print_status(worker_infos),
185            ContentType::Metrics(aggr_metrics) => print_metrics(aggr_metrics),
186            ContentType::FrontendList(frontends) => print_frontends(frontends),
187            ContentType::ListenersList(listeners) => print_listeners(listeners),
188            ContentType::WorkerMetrics(worker_metrics) => print_worker_metrics(worker_metrics),
189            ContentType::AvailableMetrics(list) => print_available_metrics(list),
190            ContentType::RequestCounts(request_counts) => print_request_counts(request_counts),
191            ContentType::CertificatesWithFingerprints(certs) => {
192                print_certificates_with_validity(certs)
193            }
194            ContentType::WorkerResponses(worker_responses) => {
195                // exception when displaying clusters
196                if worker_responses.contain_cluster_infos() {
197                    print_cluster_infos(worker_responses)
198                } else if worker_responses.contain_cluster_hashes() {
199                    print_cluster_hashes(worker_responses)
200                } else {
201                    print_responses_by_worker(worker_responses, json)
202                }
203            }
204            ContentType::Clusters(_) | ContentType::ClusterHashes(_) => Ok(()), // not displayed directly, see print_cluster_responses
205            ContentType::CertificatesByAddress(certs) => print_certificates_by_address(certs),
206            ContentType::MetricDetailStatus(status) => print_metric_detail_status(status),
207            ContentType::MaxConnectionsPerIpLimit(limit_info) => {
208                if limit_info.limit == 0 {
209                    println!("Max connections per (cluster, source-IP): unlimited (0)");
210                } else {
211                    println!(
212                        "Max connections per (cluster, source-IP): {}",
213                        limit_info.limit
214                    );
215                }
216                Ok(())
217            }
218            ContentType::HealthChecksList(list) => print_health_checks(list),
219            ContentType::Event(_event) => Ok(()), // not event displayed yet!
220            // Per-worker SetMetricDetail status payload. The aggregated
221            // MetricDetailStatus is what operators read at the
222            // `sozu` CLI surface; the per-worker variant flows
223            // master-side only (collected by SetMetricDetailTask) and
224            // is never printed directly. Silent OK keeps the match
225            // exhaustive without surfacing internal IPC payloads on
226            // the operator's terminal.
227            ContentType::WorkerMetricDetailStatus(_) => Ok(()),
228        }
229    }
230}
231
232impl WorkerResponses {
233    fn contain_cluster_infos(&self) -> bool {
234        for (_worker_id, response) in self.map.iter() {
235            if let Some(content_type) = &response.content_type {
236                if matches!(content_type, ContentType::Clusters(_)) {
237                    return true;
238                }
239            }
240        }
241        false
242    }
243
244    fn contain_cluster_hashes(&self) -> bool {
245        for (_worker_id, response) in self.map.iter() {
246            if let Some(content_type) = &response.content_type {
247                if matches!(content_type, ContentType::ClusterHashes(_)) {
248                    return true;
249                }
250            }
251        }
252        false
253    }
254}
255
256pub fn print_status(worker_infos: &WorkerInfos) -> Result<(), DisplayError> {
257    let mut table = Table::new();
258    table.set_format(*prettytable::format::consts::FORMAT_BOX_CHARS);
259    table.add_row(row!["worker id", "pid", "run state"]);
260
261    let mut sorted_infos = worker_infos.vec.clone();
262    sorted_infos.sort_by_key(|worker| worker.id);
263
264    for worker_info in &sorted_infos {
265        let row = row!(
266            worker_info.id,
267            worker_info.pid,
268            RunState::try_from(worker_info.run_state)
269                .map_err(DisplayError::DecodeError)?
270                .as_str_name()
271        );
272        table.add_row(row);
273    }
274
275    table.printstd();
276    Ok(())
277}
278
279pub fn print_metrics(aggregated_metrics: &AggregatedMetrics) -> Result<(), DisplayError> {
280    // main process metrics
281    println!("\nMAIN PROCESS\n============");
282    print_proxy_metrics(&aggregated_metrics.main);
283
284    if !aggregated_metrics.proxying.is_empty() {
285        println!("\nPROXYING\n============");
286        print_proxy_metrics(&aggregated_metrics.proxying);
287    }
288
289    // workers
290    for (worker_id, worker) in aggregated_metrics.workers.iter() {
291        if !worker.clusters.is_empty() && !worker.proxy.is_empty() {
292            println!("\nWorker {worker_id}\n=========");
293            print_worker_metrics(worker)?;
294        }
295    }
296
297    // clusters
298    if !aggregated_metrics.clusters.is_empty() {
299        println!("\nClusters\n=======");
300        print_cluster_metrics(&aggregated_metrics.clusters);
301    }
302
303    Ok(())
304}
305
306fn print_proxy_metrics(proxy_metrics: &BTreeMap<String, FilteredMetrics>) {
307    let filtered = filter_metrics(proxy_metrics);
308    print_gauges_and_counts(&filtered);
309    print_percentiles(&filtered);
310    print_histograms(&filtered);
311}
312
313fn print_worker_metrics(worker_metrics: &WorkerMetrics) -> Result<(), DisplayError> {
314    print_proxy_metrics(&worker_metrics.proxy);
315    print_cluster_metrics(&worker_metrics.clusters);
316
317    Ok(())
318}
319
320fn print_cluster_metrics(cluster_metrics: &BTreeMap<String, ClusterMetrics>) {
321    for (cluster_id, cluster_metrics_data) in cluster_metrics.iter() {
322        println!("\nCluster {cluster_id}\n--------");
323
324        let filtered = filter_metrics(&cluster_metrics_data.cluster);
325        print_gauges_and_counts(&filtered);
326        print_percentiles(&filtered);
327        print_histograms(&filtered);
328
329        // backend_id -> (metric_name -> value )
330        let mut backend_metric_acc: HashMap<String, BTreeMap<String, FilteredMetrics>> =
331            HashMap::new();
332
333        for backend in &cluster_metrics_data.backends {
334            for (metric_name, value) in &backend.metrics {
335                backend_metric_acc
336                    .entry(backend.backend_id.clone())
337                    .and_modify(|map| {
338                        map.insert(metric_name.clone(), value.clone());
339                    })
340                    .or_insert(BTreeMap::from([(metric_name.clone(), value.clone())]));
341            }
342        }
343
344        for (backend_id, metrics) in backend_metric_acc {
345            println!("\n{cluster_id}/{backend_id}\n--------");
346            let filtered = filter_metrics(&metrics);
347            print_gauges_and_counts(&filtered);
348            print_percentiles(&filtered);
349            print_histograms(&filtered);
350        }
351    }
352}
353
354fn filter_metrics(
355    metrics: &BTreeMap<String, FilteredMetrics>,
356) -> BTreeMap<String, FilteredMetrics> {
357    let mut filtered_metrics = BTreeMap::new();
358
359    for (metric_key, filtered_value) in metrics.iter() {
360        filtered_metrics.insert(
361            metric_key.replace('\t', ".").to_string(),
362            filtered_value.clone(),
363        );
364    }
365    filtered_metrics
366}
367
368fn print_gauges_and_counts(filtered_metrics: &BTreeMap<String, FilteredMetrics>) {
369    let mut titles: Vec<String> = filtered_metrics
370        .iter()
371        .filter_map(|(title, filtered_data)| match filtered_data.inner {
372            Some(filtered_metrics::Inner::Count(_)) | Some(filtered_metrics::Inner::Gauge(_)) => {
373                Some(title.to_owned())
374            }
375            _ => None,
376        })
377        .collect();
378
379    // sort the titles so they always appear in the same order
380    titles.sort();
381
382    if titles.is_empty() {
383        return;
384    }
385
386    let mut table = Table::new();
387    table.set_format(*prettytable::format::consts::FORMAT_BOX_CHARS);
388
389    table.set_titles(Row::new(vec![cell!(""), cell!("gauge"), cell!("count")]));
390
391    for title in titles {
392        let mut row = vec![cell!(title)];
393        match filtered_metrics.get(&title) {
394            Some(filtered_metrics) => match filtered_metrics.inner {
395                Some(filtered_metrics::Inner::Count(c)) => {
396                    row.push(cell!(""));
397                    row.push(cell!(c))
398                }
399                Some(filtered_metrics::Inner::Gauge(c)) => {
400                    row.push(cell!(c));
401                    row.push(cell!(""))
402                }
403                _ => {}
404            },
405            _ => row.push(cell!("")),
406        }
407        table.add_row(Row::new(row));
408    }
409
410    table.printstd();
411}
412
413fn print_percentiles(filtered_metrics: &BTreeMap<String, FilteredMetrics>) {
414    let mut percentile_titles: Vec<String> = filtered_metrics
415        .iter()
416        .filter_map(|(title, filtered_data)| match filtered_data.inner.clone() {
417            Some(filtered_metrics::Inner::Percentiles(_)) => Some(title.to_owned()),
418            _ => None,
419        })
420        .collect();
421
422    // sort the metrics so they always appear in the same order
423    percentile_titles.sort();
424
425    if percentile_titles.is_empty() {
426        return;
427    }
428
429    let mut percentile_table = Table::new();
430    percentile_table.set_format(*prettytable::format::consts::FORMAT_BOX_CHARS);
431
432    percentile_table.set_titles(Row::new(vec![
433        cell!("Percentiles"),
434        cell!("samples"),
435        cell!("p50"),
436        cell!("p90"),
437        cell!("p99"),
438        cell!("p99.9"),
439        cell!("p99.99"),
440        cell!("p99.999"),
441        cell!("p100"),
442    ]));
443
444    for title in percentile_titles {
445        if let Some(FilteredMetrics {
446            inner: Some(filtered_metrics::Inner::Percentiles(percentiles)),
447        }) = filtered_metrics.get(&title)
448        {
449            percentile_table.add_row(Row::new(vec![
450                cell!(title),
451                cell!(percentiles.samples),
452                cell!(percentiles.p_50),
453                cell!(percentiles.p_90),
454                cell!(percentiles.p_99),
455                cell!(percentiles.p_99_9),
456                cell!(percentiles.p_99_99),
457                cell!(percentiles.p_99_999),
458                cell!(percentiles.p_100),
459            ]));
460        } else {
461            println!("Something went VERY wrong here");
462        }
463    }
464
465    percentile_table.printstd();
466}
467
468fn print_histograms(filtered_metrics: &BTreeMap<String, FilteredMetrics>) {
469    let histograms: BTreeMap<String, FilteredHistogram> = filtered_metrics
470        .iter()
471        .filter_map(|(name, metric)| match metric.inner.clone() {
472            Some(filtered_metrics::Inner::Histogram(hist)) => Some((name.to_owned(), hist)),
473            _ => None,
474        })
475        .collect();
476
477    let mut histogram_titles: Vec<String> = histograms.keys().map(ToOwned::to_owned).collect();
478    histogram_titles.sort();
479    if histogram_titles.is_empty() {
480        return;
481    }
482
483    let mut histogram_table = Table::new();
484    histogram_table.set_format(*prettytable::format::consts::FORMAT_BOX_CHARS);
485
486    let mut first_row = Row::new(vec![cell!("Histograms (ms)"), cell!("sum"), cell!("count")]);
487
488    let biggest_hist_length = histograms
489        .values()
490        .map(|hist| hist.buckets.len())
491        .max()
492        .unwrap_or(0);
493
494    // 0, 1, 3, 7... in the upper row
495    for exponent in 0..biggest_hist_length {
496        first_row.add_cell(cell!(format!("{}", (1 << exponent) - 1)));
497    }
498    histogram_table.set_titles(first_row);
499
500    for title in histogram_titles {
501        if let Some(hist) = histograms.get(&title) {
502            let trimmed_name = title.strip_suffix("_histogram").unwrap_or_default();
503            let mut row = Row::new(vec![
504                cell!(trimmed_name),
505                cell!(hist.sum),
506                cell!(hist.count),
507            ]);
508            // display the count by bucket, not the incremented count
509            let mut last_bucket_count = 0;
510            for bucket in &hist.buckets {
511                row.add_cell(cell!(bucket.count - last_bucket_count));
512                last_bucket_count = bucket.count;
513            }
514            histogram_table.add_row(row);
515        }
516    }
517
518    histogram_table.printstd();
519}
520
521fn print_available_metrics(available_metrics: &AvailableMetrics) -> Result<(), DisplayError> {
522    println!("Available metrics on the proxy level:");
523    for metric_name in &available_metrics.proxy_metrics {
524        println!("\t{metric_name}");
525    }
526    println!("Available metrics on the cluster level:");
527    for metric_name in &available_metrics.cluster_metrics {
528        println!("\t{metric_name}");
529    }
530    Ok(())
531}
532
533fn print_frontends(frontends: &ListedFrontends) -> Result<(), DisplayError> {
534    trace!(" We received this frontends to display {:#?}", frontends);
535    // HTTP frontends
536    if !frontends.http_frontends.is_empty() {
537        let mut table = Table::new();
538        table.set_format(*prettytable::format::consts::FORMAT_BOX_CHARS);
539        table.add_row(row!["HTTP frontends "]);
540        table.add_row(row![
541            "cluster_id",
542            "address",
543            "hostname",
544            "path",
545            "method",
546            "position",
547            "tags"
548        ]);
549        for http_frontend in frontends.http_frontends.iter() {
550            table.add_row(row!(
551                http_frontend
552                    .cluster_id
553                    .clone()
554                    .unwrap_or("Deny".to_owned()),
555                http_frontend.address.to_string(),
556                http_frontend.hostname.to_string(),
557                format!("{:?}", http_frontend.path),
558                format!("{:?}", http_frontend.method),
559                format!("{:?}", http_frontend.position),
560                format_tags_to_string(&http_frontend.tags)
561            ));
562        }
563        table.printstd();
564    }
565
566    // HTTPS frontends
567    if !frontends.https_frontends.is_empty() {
568        let mut table = Table::new();
569        table.set_format(*prettytable::format::consts::FORMAT_BOX_CHARS);
570        table.add_row(row!["HTTPS frontends"]);
571        table.add_row(row![
572            "cluster_id",
573            "address",
574            "hostname",
575            "path",
576            "method",
577            "position",
578            "tags"
579        ]);
580        for https_frontend in frontends.https_frontends.iter() {
581            table.add_row(row!(
582                https_frontend
583                    .cluster_id
584                    .clone()
585                    .unwrap_or("Deny".to_owned()),
586                https_frontend.address.to_string(),
587                https_frontend.hostname.to_string(),
588                format!("{:?}", https_frontend.path),
589                format!("{:?}", https_frontend.method),
590                format!("{:?}", https_frontend.position),
591                format_tags_to_string(&https_frontend.tags)
592            ));
593        }
594        table.printstd();
595    }
596
597    // TCP frontends
598    if !frontends.tcp_frontends.is_empty() {
599        let mut table = Table::new();
600        table.set_format(*prettytable::format::consts::FORMAT_BOX_CHARS);
601        table.add_row(row!["TCP frontends  "]);
602        table.add_row(row!["Cluster ID", "address", "tags"]);
603        for tcp_frontend in frontends.tcp_frontends.iter() {
604            table.add_row(row!(
605                tcp_frontend.cluster_id,
606                tcp_frontend.address,
607                format_tags_to_string(&tcp_frontend.tags)
608            ));
609        }
610        table.printstd();
611    }
612
613    // UDP frontends
614    if !frontends.udp_frontends.is_empty() {
615        let mut table = Table::new();
616        table.set_format(*prettytable::format::consts::FORMAT_BOX_CHARS);
617        table.add_row(row!["UDP frontends  "]);
618        table.add_row(row!["Cluster ID", "address", "tags"]);
619        for udp_frontend in frontends.udp_frontends.iter() {
620            table.add_row(row!(
621                udp_frontend.cluster_id,
622                udp_frontend.address,
623                format_tags_to_string(&udp_frontend.tags)
624            ));
625        }
626        table.printstd();
627    }
628    Ok(())
629}
630
631pub fn print_listeners(listeners_list: &ListenersList) -> Result<(), DisplayError> {
632    println!("\nHTTP LISTENERS\n================");
633
634    for (_, http_listener) in listeners_list.http_listeners.iter() {
635        println!("{http_listener}");
636    }
637
638    println!("\nHTTPS LISTENERS\n================");
639
640    for (_, https_listener) in listeners_list.https_listeners.iter() {
641        println!("{https_listener}");
642    }
643
644    println!("\nTCP LISTENERS\n================");
645
646    if !listeners_list.tcp_listeners.is_empty() {
647        let mut table = Table::new();
648        table.set_format(*prettytable::format::consts::FORMAT_BOX_CHARS);
649        table.add_row(row!["TCP frontends"]);
650        table.add_row(row![
651            "socket address",
652            "public address",
653            "expect proxy",
654            "front timeout",
655            "back timeout",
656            "connect timeout",
657            "activated"
658        ]);
659        for (_, tcp_listener) in listeners_list.tcp_listeners.iter() {
660            table.add_row(row![
661                format!("{:?}", tcp_listener.address),
662                format!("{:?}", tcp_listener.public_address),
663                tcp_listener.expect_proxy,
664                tcp_listener.front_timeout,
665                tcp_listener.back_timeout,
666                tcp_listener.connect_timeout,
667                tcp_listener.active,
668            ]);
669        }
670        table.printstd();
671    }
672
673    println!("\nUDP LISTENERS\n================");
674
675    if !listeners_list.udp_listeners.is_empty() {
676        let mut table = Table::new();
677        table.set_format(*prettytable::format::consts::FORMAT_BOX_CHARS);
678        table.add_row(row!["UDP listeners"]);
679        // UDP has no expect_proxy / connect_timeout (no connect handshake);
680        // it carries datagram-specific knobs instead.
681        table.add_row(row![
682            "socket address",
683            "public address",
684            "front timeout",
685            "back timeout",
686            "max rx datagram size",
687            "max flows",
688            "activated"
689        ]);
690        for (_, udp_listener) in listeners_list.udp_listeners.iter() {
691            table.add_row(row![
692                format!("{:?}", udp_listener.address),
693                format!("{:?}", udp_listener.public_address),
694                udp_listener.front_timeout,
695                udp_listener.back_timeout,
696                udp_listener.max_rx_datagram_size,
697                udp_listener.max_flows,
698                udp_listener.active,
699            ]);
700        }
701        table.printstd();
702    }
703    Ok(())
704}
705
706fn print_cluster_infos(worker_responses: &WorkerResponses) -> Result<(), DisplayError> {
707    let mut cluster_table = create_cluster_table(
708        vec!["id", "sticky_session", "https_redirect"],
709        &worker_responses.map,
710    );
711
712    let mut frontend_table =
713        create_cluster_table(vec!["id", "hostname", "path"], &worker_responses.map);
714
715    let mut https_frontend_table =
716        create_cluster_table(vec!["id", "hostname", "path"], &worker_responses.map);
717
718    let mut tcp_frontend_table = create_cluster_table(vec!["id", "address"], &worker_responses.map);
719
720    let mut udp_frontend_table = create_cluster_table(vec!["id", "address"], &worker_responses.map);
721
722    let mut backend_table = create_cluster_table(
723        vec!["backend id", "IP address", "Backup"],
724        &worker_responses.map,
725    );
726
727    let worker_ids: HashSet<&String> = worker_responses.map.keys().collect();
728
729    let mut cluster_infos = BTreeMap::new();
730    let mut http_frontends = BTreeMap::new();
731    let mut https_frontends = BTreeMap::new();
732    let mut tcp_frontends = BTreeMap::new();
733    let mut udp_frontends = BTreeMap::new();
734    let mut backends = BTreeMap::new();
735
736    for (worker_id, response_content) in worker_responses.map.iter() {
737        if let Some(ContentType::Clusters(clusters)) = &response_content.content_type {
738            for cluster in clusters.vec.iter() {
739                if cluster.configuration.is_some() {
740                    let entry = cluster_infos.entry(cluster).or_insert(Vec::new());
741                    entry.push(worker_id.to_owned());
742                }
743
744                for frontend in cluster.http_frontends.iter() {
745                    let entry = http_frontends.entry(frontend).or_insert(Vec::new());
746                    entry.push(worker_id.to_owned());
747                }
748
749                for frontend in cluster.https_frontends.iter() {
750                    let entry = https_frontends.entry(frontend).or_insert(Vec::new());
751                    entry.push(worker_id.to_owned());
752                }
753
754                for frontend in cluster.tcp_frontends.iter() {
755                    let entry = tcp_frontends.entry(frontend).or_insert(Vec::new());
756                    entry.push(worker_id.to_owned());
757                }
758
759                for frontend in cluster.udp_frontends.iter() {
760                    let entry = udp_frontends.entry(frontend).or_insert(Vec::new());
761                    entry.push(worker_id.to_owned());
762                }
763
764                for backend in cluster.backends.iter() {
765                    let entry = backends.entry(backend).or_insert(Vec::new());
766                    entry.push(worker_id.to_owned());
767                }
768            }
769        }
770    }
771
772    if cluster_infos.is_empty() {
773        println!("no cluster found");
774        return Ok(());
775    }
776
777    println!("Cluster level configuration:\n");
778
779    for (cluster_info, workers_the_cluster_is_present_on) in cluster_infos.iter() {
780        let mut row = Vec::new();
781        row.push(cell!(
782            cluster_info
783                .configuration
784                .as_ref()
785                .map(|conf| conf.cluster_id.to_owned())
786                .unwrap_or_else(|| String::from("None"))
787        ));
788        row.push(cell!(
789            cluster_info
790                .configuration
791                .as_ref()
792                .map(|conf| conf.sticky_session)
793                .unwrap_or_else(|| false)
794        ));
795        row.push(cell!(
796            cluster_info
797                .configuration
798                .as_ref()
799                .map(|conf| conf.https_redirect)
800                .unwrap_or_else(|| false)
801        ));
802
803        for worker in workers_the_cluster_is_present_on {
804            if worker_ids.contains(worker) {
805                row.push(cell!("X"));
806            } else {
807                row.push(cell!(""));
808            }
809        }
810
811        cluster_table.add_row(Row::new(row));
812    }
813
814    cluster_table.printstd();
815
816    println!("\nHTTP frontends configuration for:\n");
817
818    for (key, values) in http_frontends.iter() {
819        let mut row = Vec::new();
820        match &key.cluster_id {
821            Some(cluster_id) => row.push(cell!(cluster_id)),
822            None => row.push(cell!("-")),
823        }
824        row.push(cell!(key.hostname));
825        row.push(cell!(key.path));
826
827        for val in values.iter() {
828            if worker_ids.contains(val) {
829                row.push(cell!("X"));
830            } else {
831                row.push(cell!(""));
832            }
833        }
834
835        frontend_table.add_row(Row::new(row));
836    }
837
838    frontend_table.printstd();
839
840    println!("\nHTTPS frontends configuration for:\n");
841
842    for (key, values) in https_frontends.iter() {
843        let mut row = Vec::new();
844        match &key.cluster_id {
845            Some(cluster_id) => row.push(cell!(cluster_id)),
846            None => row.push(cell!("-")),
847        }
848        row.push(cell!(key.hostname));
849        row.push(cell!(key.path));
850
851        for val in values.iter() {
852            if worker_ids.contains(val) {
853                row.push(cell!("X"));
854            } else {
855                row.push(cell!(""));
856            }
857        }
858
859        https_frontend_table.add_row(Row::new(row));
860    }
861
862    https_frontend_table.printstd();
863
864    println!("\nTCP frontends configuration:\n");
865
866    for (key, values) in tcp_frontends.iter() {
867        let mut row = vec![cell!(key.cluster_id), cell!(format!("{}", key.address))];
868
869        for val in values.iter() {
870            if worker_ids.contains(val) {
871                row.push(cell!(String::from("X")));
872            } else {
873                row.push(cell!(String::from("")));
874            }
875        }
876
877        tcp_frontend_table.add_row(Row::new(row));
878    }
879
880    tcp_frontend_table.printstd();
881
882    println!("\nUDP frontends configuration:\n");
883
884    for (key, values) in udp_frontends.iter() {
885        let mut row = vec![cell!(key.cluster_id), cell!(format!("{}", key.address))];
886
887        for val in values.iter() {
888            if worker_ids.contains(val) {
889                row.push(cell!(String::from("X")));
890            } else {
891                row.push(cell!(String::from("")));
892            }
893        }
894
895        udp_frontend_table.add_row(Row::new(row));
896    }
897
898    udp_frontend_table.printstd();
899
900    println!("\nbackends configuration:\n");
901
902    for (key, values) in backends.iter() {
903        let mut row = vec![
904            cell!(key.backend_id),
905            cell!(format!("{}", key.address)),
906            cell!(
907                key.backup
908                    .map(|b| if b { "X" } else { "" })
909                    .unwrap_or_else(|| "")
910            ),
911        ];
912
913        for val in values {
914            if worker_ids.contains(&val) {
915                row.push(cell!("X"));
916            } else {
917                row.push(cell!(""));
918            }
919        }
920
921        backend_table.add_row(Row::new(row));
922    }
923
924    backend_table.printstd();
925
926    Ok(())
927}
928
929/// display all clusters in a simplified table showing their hashes
930fn print_cluster_hashes(worker_responses: &WorkerResponses) -> Result<(), DisplayError> {
931    let mut clusters_table = Table::new();
932    clusters_table.set_format(*prettytable::format::consts::FORMAT_BOX_CHARS);
933    let mut header = vec![cell!("cluster id")];
934    for worker_id in worker_responses.map.keys() {
935        header.push(cell!(format!("worker {}", worker_id)));
936    }
937    header.push(cell!("desynchronized"));
938    clusters_table.add_row(Row::new(header));
939
940    let mut cluster_hashes = HashMap::new();
941
942    for response_content in worker_responses.map.values() {
943        if let Some(ContentType::ClusterHashes(hashes)) = &response_content.content_type {
944            for (cluster_id, hash) in hashes.map.iter() {
945                cluster_hashes
946                    .entry(cluster_id)
947                    .or_insert(Vec::new())
948                    .push(hash);
949            }
950        }
951    }
952
953    for (cluster_id, hashes) in cluster_hashes.iter() {
954        let mut row = vec![cell!(cluster_id)];
955        for val in hashes.iter() {
956            row.push(cell!(format!("{val}")));
957        }
958
959        let hs: HashSet<&u64> = hashes.iter().cloned().collect();
960        if hs.len() > 1 {
961            row.push(cell!("X"));
962        } else {
963            row.push(cell!(""));
964        }
965
966        clusters_table.add_row(Row::new(row));
967    }
968
969    clusters_table.printstd();
970    Ok(())
971}
972
973fn print_responses_by_worker(
974    worker_responses: &WorkerResponses,
975    json: bool,
976) -> Result<(), DisplayError> {
977    for (worker_id, content) in worker_responses.map.iter() {
978        println!("Worker {worker_id}");
979        content.display(json)?;
980    }
981
982    Ok(())
983}
984
985pub fn print_certificates_with_validity(
986    certs: &CertificatesWithFingerprints,
987) -> Result<(), DisplayError> {
988    if certs.certs.is_empty() {
989        println!("No certificates match your request.");
990        return Ok(());
991    }
992
993    let mut table = Table::new();
994    table.set_format(*prettytable::format::consts::FORMAT_CLEAN);
995    table.add_row(row![
996        "fingeprint",
997        "valid not before",
998        "valide not after",
999        "domain names",
1000    ]);
1001
1002    for (fingerprint, cert) in &certs.certs {
1003        let (_unparsed, pem_certificate) =
1004            x509_parser::pem::parse_x509_pem(cert.certificate.as_bytes())
1005                .expect("Could not parse pem certificate");
1006
1007        let x509_certificate = pem_certificate
1008            .parse_x509()
1009            .expect("Could not parse x509 certificate");
1010
1011        let validity = x509_certificate.validity();
1012
1013        table.add_row(row!(
1014            fingerprint,
1015            format_datetime(validity.not_before)?,
1016            format_datetime(validity.not_after)?,
1017            concatenate_vector(&cert.names),
1018        ));
1019    }
1020    table.printstd();
1021
1022    Ok(())
1023}
1024
1025fn print_certificates_by_address(list: &ListOfCertificatesByAddress) -> Result<(), DisplayError> {
1026    for certs in list.certificates.iter() {
1027        println!("\t{}:", certs.address);
1028
1029        for summary in certs.certificate_summaries.iter() {
1030            println!("\t\t{summary}");
1031        }
1032    }
1033    Ok(())
1034}
1035
1036fn print_metric_detail_status(status: &MetricDetailStatus) -> Result<(), DisplayError> {
1037    let mut table = Table::new();
1038    table.set_format(*prettytable::format::consts::FORMAT_BOX_CHARS);
1039    table.add_row(row![
1040        "scope",
1041        "configured",
1042        "previous effective",
1043        "effective",
1044        "active leases"
1045    ]);
1046    table.add_row(row!(
1047        "main",
1048        status.configured().as_str_name(),
1049        status.previous_effective().as_str_name(),
1050        status.effective().as_str_name(),
1051        "—",
1052    ));
1053    for (worker_id, w) in &status.workers {
1054        table.add_row(row!(
1055            format!("worker:{worker_id}"),
1056            w.configured().as_str_name(),
1057            w.previous_effective().as_str_name(),
1058            w.effective().as_str_name(),
1059            w.active_lease_count,
1060        ));
1061    }
1062    table.printstd();
1063    Ok(())
1064}
1065
1066fn print_request_counts(request_counts: &RequestCounts) -> Result<(), DisplayError> {
1067    let mut table = Table::new();
1068    table.set_format(*prettytable::format::consts::FORMAT_BOX_CHARS);
1069    table.add_row(row!["request type", "count"]);
1070
1071    for (request_type, count) in &request_counts.map {
1072        table.add_row(row!(request_type, count));
1073    }
1074    table.printstd();
1075    Ok(())
1076}
1077
1078fn print_health_checks(list: &HealthChecksList) -> Result<(), DisplayError> {
1079    if list.map.is_empty() {
1080        println!("No health checks configured.");
1081        return Ok(());
1082    }
1083
1084    let mut table = Table::new();
1085    table.set_format(*prettytable::format::consts::FORMAT_BOX_CHARS);
1086    table.add_row(row![
1087        "cluster",
1088        "uri",
1089        "interval",
1090        "timeout",
1091        "healthy threshold",
1092        "unhealthy threshold",
1093        "expected status"
1094    ]);
1095
1096    let mut entries: Vec<_> = list.map.iter().collect();
1097    entries.sort_by_key(|(id, _)| id.as_str());
1098
1099    for (cluster_id, config) in entries {
1100        let expected = if config.expected_status == 0 {
1101            "any 2xx".to_owned()
1102        } else {
1103            config.expected_status.to_string()
1104        };
1105
1106        table.add_row(row![
1107            cluster_id,
1108            config.uri,
1109            format!("{}s", config.interval),
1110            format!("{}s", config.timeout),
1111            config.healthy_threshold,
1112            config.unhealthy_threshold,
1113            expected
1114        ]);
1115    }
1116    table.printstd();
1117    Ok(())
1118}
1119
1120fn format_tags_to_string(tags: &BTreeMap<String, String>) -> String {
1121    tags.iter()
1122        .map(|(k, v)| format!("{k}={v}"))
1123        .collect::<Vec<_>>()
1124        .join(", ")
1125}
1126
1127fn list_string_vec(vec: &[String]) -> String {
1128    let mut output = String::new();
1129    for item in vec.iter() {
1130        output.push_str(item);
1131        output.push('\n');
1132    }
1133    output
1134}
1135
1136// ISO 8601
1137fn format_datetime(asn1_time: ASN1Time) -> Result<String, DisplayError> {
1138    let datetime = asn1_time.to_datetime();
1139
1140    let formatted = datetime
1141        .format(&format_description::well_known::Iso8601::DEFAULT)
1142        .map_err(|_| DisplayError::DateTime)?;
1143    Ok(formatted)
1144}
1145
1146/// Creates an empty table of the form
1147/// ```text
1148/// ┌────────────┬─────────────┬───────────┬────────┐
1149/// │            │ header      │ header    │ header │
1150/// ├────────────┼─────────────┼───────────┼────────┤
1151/// │ cluster_id │             │           │        │
1152/// ├────────────┼─────────────┼───────────┼────────┤
1153/// │ cluster_id │             │           │        │
1154/// ├────────────┼─────────────┼───────────┼────────┤
1155/// │ cluster_id │             │           │        │
1156/// └────────────┴─────────────┴───────────┴────────┘
1157/// ```
1158fn create_cluster_table(headers: Vec<&str>, data: &BTreeMap<String, ResponseContent>) -> Table {
1159    let mut table = Table::new();
1160    table.set_format(*prettytable::format::consts::FORMAT_BOX_CHARS);
1161    let mut row_header: Vec<_> = headers.iter().map(|h| cell!(h)).collect();
1162    for ref key in data.keys() {
1163        row_header.push(cell!(&key));
1164    }
1165    table.add_row(Row::new(row_header));
1166    table
1167}
1168
1169impl Display for SocketAddress {
1170    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
1171        write!(f, "{}", SocketAddr::from(*self))
1172    }
1173}
1174
1175impl Display for ProtobufEndpoint {
1176    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
1177        match &self.inner {
1178            Some(protobuf_endpoint::Inner::Http(HttpEndpoint {
1179                method,
1180                authority,
1181                path,
1182                status,
1183                ..
1184            })) => write!(
1185                f,
1186                "{} {} {} -> {}",
1187                authority.as_string_or("-"),
1188                method.as_string_or("-"),
1189                path.as_string_or("-"),
1190                status.as_string_or("-"),
1191            ),
1192            Some(protobuf_endpoint::Inner::Tcp(_)) => {
1193                write!(f, "-")
1194            }
1195            None => Ok(()),
1196        }
1197    }
1198}
1199
1200impl Display for HttpListenerConfig {
1201    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
1202        let mut table = Table::new();
1203        table.set_format(*prettytable::format::consts::FORMAT_BOX_CHARS);
1204        table.add_row(row!["socket address", format!("{:?}", self.address)]);
1205        table.add_row(row!["public address", format!("{:?}", self.public_address),]);
1206        for http_answer_row in CustomHttpAnswers::to_rows(&self.http_answers) {
1207            table.add_row(http_answer_row);
1208        }
1209        table.add_row(row!["expect proxy", self.expect_proxy]);
1210        table.add_row(row!["sticky name", self.sticky_name]);
1211        table.add_row(row!["front timeout", self.front_timeout]);
1212        table.add_row(row!["back timeout", self.back_timeout]);
1213        table.add_row(row!["connect timeout", self.connect_timeout]);
1214        table.add_row(row!["request timeout", self.request_timeout]);
1215        table.add_row(row!["activated", self.active]);
1216        add_h2_flood_rows(
1217            &mut table,
1218            &self.h2_max_rst_stream_per_window,
1219            &self.h2_max_ping_per_window,
1220            &self.h2_max_settings_per_window,
1221            &self.h2_max_empty_data_per_window,
1222            &self.h2_max_window_update_stream0_per_window,
1223            &self.h2_max_continuation_frames,
1224            &self.h2_max_glitch_count,
1225            &self.h2_max_rst_stream_lifetime,
1226            &self.h2_max_rst_stream_abusive_lifetime,
1227            &self.h2_max_rst_stream_emitted_lifetime,
1228            &self.h2_max_header_list_size,
1229            &self.h2_max_header_table_size,
1230            &self.h2_max_header_fields,
1231        );
1232        add_h2_connection_rows(
1233            &mut table,
1234            &self.h2_initial_connection_window,
1235            &self.h2_max_concurrent_streams,
1236            &self.h2_stream_shrink_ratio,
1237        );
1238        if let Some(v) = &self.h2_stream_idle_timeout_seconds {
1239            table.add_row(row!["h2 stream idle timeout (seconds)", v]);
1240        }
1241        if let Some(v) = &self.h2_graceful_shutdown_deadline_seconds {
1242            table.add_row(row!["h2 graceful shutdown deadline (seconds)", v]);
1243        }
1244        if let Some(v) = &self.sozu_id_header {
1245            table.add_row(row!["Sozu-Id correlation header name", v]);
1246        }
1247        if let Some(v) = self.elide_x_real_ip {
1248            table.add_row(row!["elide X-Real-IP (anti-spoof)", v]);
1249        }
1250        if let Some(v) = self.send_x_real_ip {
1251            table.add_row(row!["send X-Real-IP (peer IP)", v]);
1252        }
1253        write!(f, "{table}")
1254    }
1255}
1256
1257impl Display for HttpsListenerConfig {
1258    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
1259        let mut table = Table::new();
1260        table.set_format(*prettytable::format::consts::FORMAT_BOX_CHARS);
1261        let mut tls_versions = String::new();
1262        for tls_version in self.versions.iter() {
1263            tls_versions.push_str(&format!("{tls_version:?}\n"));
1264        }
1265
1266        table.add_row(row!["socket address", format!("{:?}", self.address)]);
1267        table.add_row(row!["public address", format!("{:?}", self.public_address)]);
1268        for http_answer_row in CustomHttpAnswers::to_rows(&self.http_answers) {
1269            table.add_row(http_answer_row);
1270        }
1271        table.add_row(row!["versions", tls_versions]);
1272        table.add_row(row!["cipher list", list_string_vec(&self.cipher_list),]);
1273        table.add_row(row!["cipher suites", list_string_vec(&self.cipher_suites),]);
1274        table.add_row(row![
1275            "signature algorithms",
1276            list_string_vec(&self.signature_algorithms),
1277        ]);
1278        table.add_row(row!["groups list", list_string_vec(&self.groups_list),]);
1279        table.add_row(row![
1280            "alpn protocols",
1281            list_string_vec(&self.alpn_protocols),
1282        ]);
1283        table.add_row(row!["key", format!("{:?}", self.key),]);
1284        table.add_row(row!["expect proxy", self.expect_proxy]);
1285        table.add_row(row!["sticky name", self.sticky_name]);
1286        table.add_row(row!["front timeout", self.front_timeout]);
1287        table.add_row(row!["back timeout", self.back_timeout]);
1288        table.add_row(row!["connect timeout", self.connect_timeout]);
1289        table.add_row(row!["request timeout", self.request_timeout]);
1290        table.add_row(row!["activated", self.active]);
1291        add_h2_flood_rows(
1292            &mut table,
1293            &self.h2_max_rst_stream_per_window,
1294            &self.h2_max_ping_per_window,
1295            &self.h2_max_settings_per_window,
1296            &self.h2_max_empty_data_per_window,
1297            &self.h2_max_window_update_stream0_per_window,
1298            &self.h2_max_continuation_frames,
1299            &self.h2_max_glitch_count,
1300            &self.h2_max_rst_stream_lifetime,
1301            &self.h2_max_rst_stream_abusive_lifetime,
1302            &self.h2_max_rst_stream_emitted_lifetime,
1303            &self.h2_max_header_list_size,
1304            &self.h2_max_header_table_size,
1305            &self.h2_max_header_fields,
1306        );
1307        add_h2_connection_rows(
1308            &mut table,
1309            &self.h2_initial_connection_window,
1310            &self.h2_max_concurrent_streams,
1311            &self.h2_stream_shrink_ratio,
1312        );
1313        if let Some(v) = self.strict_sni_binding {
1314            table.add_row(row!["strict sni binding", v]);
1315        }
1316        if let Some(v) = self.disable_http11 {
1317            table.add_row(row!["disable http/1.1", v]);
1318        }
1319        if let Some(v) = &self.h2_stream_idle_timeout_seconds {
1320            table.add_row(row!["h2 stream idle timeout (seconds)", v]);
1321        }
1322        if let Some(v) = &self.h2_graceful_shutdown_deadline_seconds {
1323            table.add_row(row!["h2 graceful shutdown deadline (seconds)", v]);
1324        }
1325        if let Some(v) = &self.sozu_id_header {
1326            table.add_row(row!["Sozu-Id correlation header name", v]);
1327        }
1328        if let Some(v) = self.elide_x_real_ip {
1329            table.add_row(row!["elide X-Real-IP (anti-spoof)", v]);
1330        }
1331        if let Some(v) = self.send_x_real_ip {
1332            table.add_row(row!["send X-Real-IP (peer IP)", v]);
1333        }
1334        write!(f, "{table}")
1335    }
1336}
1337
1338/// Add H2 flood detection threshold rows to a display table.
1339/// Only shows rows for values that have been explicitly configured.
1340#[allow(clippy::too_many_arguments)]
1341fn add_h2_flood_rows(
1342    table: &mut Table,
1343    max_rst_stream: &Option<u32>,
1344    max_ping: &Option<u32>,
1345    max_settings: &Option<u32>,
1346    max_empty_data: &Option<u32>,
1347    max_window_update_stream0: &Option<u32>,
1348    max_continuation: &Option<u32>,
1349    max_glitch: &Option<u32>,
1350    max_rst_stream_lifetime: &Option<u64>,
1351    max_rst_stream_abusive_lifetime: &Option<u64>,
1352    max_rst_stream_emitted_lifetime: &Option<u64>,
1353    max_header_list_size: &Option<u32>,
1354    max_header_table_size: &Option<u32>,
1355    max_header_fields: &Option<u32>,
1356) {
1357    if let Some(v) = max_rst_stream {
1358        table.add_row(row!["h2 max rst_stream/window", v]);
1359    }
1360    if let Some(v) = max_ping {
1361        table.add_row(row!["h2 max ping/window", v]);
1362    }
1363    if let Some(v) = max_settings {
1364        table.add_row(row!["h2 max settings/window", v]);
1365    }
1366    if let Some(v) = max_empty_data {
1367        table.add_row(row!["h2 max empty_data/window", v]);
1368    }
1369    if let Some(v) = max_window_update_stream0 {
1370        table.add_row(row!["h2 max window_update stream0/window", v]);
1371    }
1372    if let Some(v) = max_continuation {
1373        table.add_row(row!["h2 max continuation frames", v]);
1374    }
1375    if let Some(v) = max_glitch {
1376        table.add_row(row!["h2 max glitch count", v]);
1377    }
1378    if let Some(v) = max_rst_stream_lifetime {
1379        table.add_row(row!["h2 max rst_stream lifetime", v]);
1380    }
1381    if let Some(v) = max_rst_stream_abusive_lifetime {
1382        table.add_row(row!["h2 max rst_stream abusive lifetime", v]);
1383    }
1384    if let Some(v) = max_rst_stream_emitted_lifetime {
1385        table.add_row(row!["h2 max rst_stream emitted lifetime", v]);
1386    }
1387    if let Some(v) = max_header_list_size {
1388        table.add_row(row!["h2 max header list size", v]);
1389    }
1390    if let Some(v) = max_header_table_size {
1391        table.add_row(row!["h2 max header table size", v]);
1392    }
1393    if let Some(v) = max_header_fields {
1394        table.add_row(row!["h2 max header fields", v]);
1395    }
1396}
1397
1398/// Add H2 connection tuning rows to a display table.
1399/// Only shows rows for values that have been explicitly configured.
1400fn add_h2_connection_rows(
1401    table: &mut Table,
1402    window: &Option<u32>,
1403    max_streams: &Option<u32>,
1404    shrink_ratio: &Option<u32>,
1405) {
1406    if let Some(v) = window {
1407        table.add_row(row!["h2 initial connection window", v]);
1408    }
1409    if let Some(v) = max_streams {
1410        table.add_row(row!["h2 max concurrent streams", v]);
1411    }
1412    if let Some(v) = shrink_ratio {
1413        table.add_row(row!["h2 stream shrink ratio", v]);
1414    }
1415}
1416
1417impl CustomHttpAnswers {
1418    fn to_rows(option: &Option<Self>) -> Vec<Row> {
1419        let mut rows = Vec::new();
1420        if let Some(answers) = option {
1421            if let Some(a) = &answers.answer_301 {
1422                rows.push(row!("301", a));
1423            }
1424            if let Some(a) = &answers.answer_400 {
1425                rows.push(row!("400", a));
1426            }
1427            if let Some(a) = &answers.answer_404 {
1428                rows.push(row!("404", a));
1429            }
1430            if let Some(a) = &answers.answer_408 {
1431                rows.push(row!("408", a));
1432            }
1433            if let Some(a) = &answers.answer_413 {
1434                rows.push(row!("413", a));
1435            }
1436            if let Some(a) = &answers.answer_421 {
1437                rows.push(row!("421", a));
1438            }
1439            if let Some(a) = &answers.answer_502 {
1440                rows.push(row!("502", a));
1441            }
1442            if let Some(a) = &answers.answer_503 {
1443                rows.push(row!("503", a));
1444            }
1445            if let Some(a) = &answers.answer_504 {
1446                rows.push(row!("504", a));
1447            }
1448            if let Some(a) = &answers.answer_507 {
1449                rows.push(row!("507", a));
1450            }
1451        }
1452        rows
1453    }
1454}
1455
1456impl Display for Event {
1457    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
1458        let kind = match self.kind() {
1459            EventKind::BackendDown => "backend down",
1460            EventKind::BackendUp => "backend up",
1461            EventKind::NoAvailableBackends => "no available backends",
1462            EventKind::RemovedBackendHasNoConnections => "removed backend has no connections",
1463            EventKind::ClusterAdded => "cluster added",
1464            EventKind::ClusterRemoved => "cluster removed",
1465            EventKind::FrontendAdded => "frontend added",
1466            EventKind::FrontendRemoved => "frontend removed",
1467            EventKind::CertificateAdded => "certificate added",
1468            EventKind::CertificateRemoved => "certificate removed",
1469            EventKind::CertificateReplaced => "certificate replaced",
1470            EventKind::ListenerActivated => "listener activated",
1471            EventKind::ListenerDeactivated => "listener deactivated",
1472            EventKind::ConfigurationReloaded => "configuration reloaded",
1473            EventKind::WorkerKilled => "worker killed",
1474            EventKind::WorkerRelaunched => "worker relaunched",
1475            EventKind::LoggingLevelChanged => "logging level changed",
1476            EventKind::MetricsConfigured => "metrics configured",
1477            EventKind::ListenerUpdated => "listener updated",
1478            EventKind::StateLoaded => "state loaded",
1479            EventKind::StateSaved => "state saved",
1480            EventKind::ListenerAdded => "listener added",
1481            EventKind::ListenerRemoved => "listener removed",
1482            EventKind::SozuStopRequested => "stop requested",
1483            EventKind::MainUpgraded => "main upgraded",
1484            EventKind::WorkerUpgraded => "worker upgraded",
1485            EventKind::EventsSubscribed => "events subscribed",
1486            EventKind::HealthCheckHealthy => "health check: backend healthy",
1487            EventKind::HealthCheckUnhealthy => "health check: backend unhealthy",
1488            EventKind::ClusterRecovered => "cluster recovered",
1489            EventKind::MetricDetailChanged => "metric detail changed",
1490        };
1491        let address = match &self.address {
1492            Some(a) => a.to_string(),
1493            None => String::new(),
1494        };
1495        write!(
1496            f,
1497            "{}, backend={}, cluster={}, address={}",
1498            kind,
1499            self.backend_id(),
1500            self.cluster_id(),
1501            address,
1502        )
1503    }
1504}