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                        );
95                    }
96                };
97                info!(
98                    "Opened tracing endpoint at {}",
99                    listener
100                        .local_addr()
101                        .expect("Local address must be available as the bind was successful")
102                );
103
104                while let Ok((stream, _)) = listener.accept().await {
105                    let io = TokioIo::new(stream);
106                    tokio::spawn(async move {
107                        if let Err(err) = Builder::new(TokioExecutor::new())
108                            .serve_connection(io, svc)
109                            .await
110                        {
111                            error!(
112                                "Serve connection in {addr:?} failed: {err:?}.",
113                                addr = addr,
114                                err = err
115                            );
116                        }
117                    });
118                }
119            });
120
121        Ok(())
122    }
123}
124
125#[cfg(feature = "filter-reload")]
126#[instrument]
127async fn request_handler(req: Request<Incoming>) -> Result<Response<String>, hyper::Error> {
128    use super::Tracing;
129
130    let rsp = match (req.method(), req.uri().path()) {
131        (&Method::GET, "/") => Response::new(
132            r#"
133This HTTP endpoint allows dynamic control of the filter applied to
134tracing events.
135
136To get the current filter, GET /filter:
137
138    curl -X GET localhost:3000/filter
139
140To set the filter, POST the new filter string to /filter:
141
142    curl -X POST localhost:3000/filter -d "zebrad=trace"
143"#
144            .to_string(),
145        ),
146        (&Method::GET, "/filter") => Response::builder()
147            .status(StatusCode::OK)
148            .body(
149                APPLICATION
150                    .state()
151                    .components()
152                    .get_downcast_ref::<Tracing>()
153                    .expect("Tracing component should be available")
154                    .filter(),
155            )
156            .expect("response with known status code cannot fail"),
157        (&Method::POST, "/filter") => match read_filter(req).await {
158            Ok(filter) => {
159                APPLICATION
160                    .state()
161                    .components()
162                    .get_downcast_ref::<Tracing>()
163                    .expect("Tracing component should be available")
164                    .reload_filter(filter);
165
166                Response::new("".to_string())
167            }
168            Err(e) => Response::builder()
169                .status(StatusCode::BAD_REQUEST)
170                .body(e)
171                .expect("response with known status code cannot fail"),
172        },
173        _ => Response::builder()
174            .status(StatusCode::NOT_FOUND)
175            .body("".to_string())
176            .expect("response with known status cannot fail"),
177    };
178    Ok(rsp)
179}