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