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 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 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(()), 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(()), 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 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 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 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 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 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 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 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 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 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 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 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
852fn 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
1059fn 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
1069fn 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#[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
1315fn 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}