zebrad/components/tracing/
endpoint.rs

1//! An HTTP endpoint for dynamically setting tracing filters.
2
3#![allow(non_local_definitions)]
4
5use std::net::SocketAddr;
6
7use abscissa_core::{Component, FrameworkError};
8
9use crate::config::ZebradConfig;
10
11#[cfg(feature = "filter-reload")]
12use hyper::{
13    body::{Body, Incoming},
14    Method, Request, Response, StatusCode,
15};
16#[cfg(feature = "filter-reload")]
17use hyper_util::{
18    rt::{TokioExecutor, TokioIo},
19    server::conn::auto::Builder,
20};
21
22#[cfg(feature = "filter-reload")]
23use crate::{components::tokio::TokioComponent, prelude::*};
24
25/// Abscissa component which runs a tracing filter endpoint.
26#[derive(Debug, Component)]
27#[cfg_attr(
28    feature = "filter-reload",
29    component(inject = "init_tokio(zebrad::components::tokio::TokioComponent)")
30)]
31pub struct TracingEndpoint {
32    #[allow(dead_code)]
33    addr: Option<SocketAddr>,
34}
35
36#[cfg(feature = "filter-reload")]
37async fn read_filter(req: Request<impl Body>) -> Result<String, String> {
38    use http_body_util::BodyExt;
39
40    std::str::from_utf8(
41        req.into_body()
42            .collect()
43            .await
44            .map_err(|_| "Error reading body".to_owned())?
45            .to_bytes()
46            .as_ref(),
47    )
48    .map(|s| s.to_owned())
49    .map_err(|_| "Filter must be UTF-8".to_owned())
50}
51
52impl TracingEndpoint {
53    /// Create the component.
54    pub fn new(config: &ZebradConfig) -> Result<Self, FrameworkError> {
55        if config.tracing.endpoint_addr.is_some() && !cfg!(feature = "filter-reload") {
56            warn!(addr = ?config.tracing.endpoint_addr,
57                  "unable to activate configured tracing filter endpoint: \
58                   enable the 'filter-reload' feature when compiling zebrad",
59            );
60        }
61
62        Ok(Self {
63            addr: config.tracing.endpoint_addr,
64        })
65    }
66
67    #[cfg(feature = "filter-reload")]
68    #[allow(clippy::unwrap_in_result)]
69    pub fn init_tokio(&mut self, tokio_component: &TokioComponent) -> Result<(), FrameworkError> {
70        let addr = if let Some(addr) = self.addr {
71            addr
72        } else {
73            return Ok(());
74        };
75
76        info!("Trying to open tracing endpoint at {}...", addr);
77
78        let svc = hyper::service::service_fn(|req: Request<Incoming>| async move {
79            request_handler(req).await
80        });
81
82        tokio_component
83            .rt
84            .as_ref()
85            .expect("runtime should not be taken")
86            .spawn(async move {
87                let listener = match tokio::net::TcpListener::bind(addr).await {
88                    Ok(listener) => listener,
89                    Err(err) => {
90                        panic!(
91                            "Opening tracing endpoint listener {addr:?} failed: {err:?}. \
92                            Hint: Check if another zebrad or zcashd process is running. \
93                            Try changing the tracing endpoint_addr in the Zebra config.",
94                            addr = addr,
95                            err = err,
96                        );
97                    }
98                };
99                info!(
100                    "Opened tracing endpoint at {}",
101                    listener
102                        .local_addr()
103                        .expect("Local address must be available as the bind was successful")
104                );
105
106                while let Ok((stream, _)) = listener.accept().await {
107                    let io = TokioIo::new(stream);
108                    tokio::spawn(async move {
109                        if let Err(err) = Builder::new(TokioExecutor::new())
110                            .serve_connection(io, svc)
111                            .await
112                        {
113                            error!(
114                                "Serve connection in {addr:?} failed: {err:?}.",
115                                addr = addr,
116                                err = err
117                            );
118                        }
119                    });
120                }
121            });
122
123        Ok(())
124    }
125}
126
127#[cfg(feature = "filter-reload")]
128#[instrument]
129async fn request_handler(req: Request<Incoming>) -> Result<Response<String>, hyper::Error> {
130    use super::Tracing;
131
132    let rsp = match (req.method(), req.uri().path()) {
133        (&Method::GET, "/") => Response::new(
134            r#"
135This HTTP endpoint allows dynamic control of the filter applied to
136tracing events.
137
138To get the current filter, GET /filter:
139
140    curl -X GET localhost:3000/filter
141
142To set the filter, POST the new filter string to /filter:
143
144    curl -X POST localhost:3000/filter -d "zebrad=trace"
145"#
146            .to_string(),
147        ),
148        (&Method::GET, "/filter") => Response::builder()
149            .status(StatusCode::OK)
150            .body(
151                APPLICATION
152                    .state()
153                    .components()
154                    .get_downcast_ref::<Tracing>()
155                    .expect("Tracing component should be available")
156                    .filter(),
157            )
158            .expect("response with known status code cannot fail"),
159        (&Method::POST, "/filter") => match read_filter(req).await {
160            Ok(filter) => {
161                APPLICATION
162                    .state()
163                    .components()
164                    .get_downcast_ref::<Tracing>()
165                    .expect("Tracing component should be available")
166                    .reload_filter(filter);
167
168                Response::new("".to_string())
169            }
170            Err(e) => Response::builder()
171                .status(StatusCode::BAD_REQUEST)
172                .body(e)
173                .expect("response with known status code cannot fail"),
174        },
175        _ => Response::builder()
176            .status(StatusCode::NOT_FOUND)
177            .body("".to_string())
178            .expect("response with known status cannot fail"),
179    };
180    Ok(rsp)
181}