volga 0.8.9

Easy & Fast Web Framework for Rust
Documentation
//! Tools for tracing, logging and observability

use hyper::header::HeaderName;

use crate::App;

const DEFAULT_SPAN_HEADER_NAME: &str = "request-id";

/// Represents a tracing configuration
#[derive(Debug, Clone)]
#[cfg_attr(feature = "config", derive(serde::Deserialize))]
#[cfg_attr(feature = "config", serde(default))]
pub struct TracingConfig {
    /// Specifies whether to include a span id HTTP header
    ///
    /// Default: `false`
    pub(super) include_header: bool,

    /// Specifies a span id HTTP header name
    ///
    /// Default: `request-id`
    #[cfg_attr(feature = "config", serde(deserialize_with = "deser_header_name"))]
    pub(super) span_header_name: HeaderName,
}

impl Default for TracingConfig {
    #[inline]
    fn default() -> Self {
        Self {
            include_header: false,
            span_header_name: HeaderName::from_static(DEFAULT_SPAN_HEADER_NAME),
        }
    }
}

#[cfg(feature = "config")]
fn deser_header_name<'de, D: serde::Deserializer<'de>>(d: D) -> Result<HeaderName, D::Error> {
    use serde::Deserialize;
    let s = String::deserialize(d)?;
    HeaderName::try_from(s.as_str()).map_err(serde::de::Error::custom)
}

impl TracingConfig {
    /// Creates a default tracing configuration
    ///
    /// Defaults:
    /// - include_header: `false`
    /// - span_header_name: `request-id`
    pub fn new() -> Self {
        Self::default()
    }

    /// Configures tracing to include the span id as an HTTP header
    ///
    /// Default: `false`
    pub fn with_header(mut self) -> Self {
        self.include_header = true;
        self
    }

    /// Configures tracing to use a specific HTTP header name if the `include_header` is set to `true`
    ///
    /// Default: `request-id`
    ///
    /// # Panics
    ///
    /// Panics if `name` is not a valid HTTP header name.
    pub fn with_header_name(mut self, name: impl AsRef<str>) -> Self {
        self.span_header_name =
            HeaderName::try_from(name.as_ref()).expect("invalid HTTP header name");
        self
    }
}

impl App {
    /// Configures web server with the default Tracing configurations
    ///
    /// Defaults:
    /// - include_header: `false`
    /// - span_header_name: `request-id`
    pub fn with_default_tracing(mut self) -> Self {
        self.tracing_config = Some(TracingConfig::default());
        self
    }

    /// Configures web server with specific Tracing configurations.
    ///
    /// Defaults:
    /// - include_header: `false`
    /// - span_header_name: `request-id`
    ///
    /// # Example
    /// ```no_run
    /// use volga::App;
    ///
    /// let app = App::new()
    ///     .with_tracing(|config| config.with_header());
    /// ```
    ///
    /// If tracing was already preconfigured, it does not overwrite it
    /// ```no_run
    /// use volga::App;
    /// use volga::tracing::TracingConfig;
    ///
    /// let app = App::new()
    ///     .set_tracing(TracingConfig::new().with_header()) // sets include_header to true
    ///     .with_tracing(|config| config
    ///         .with_header_name("x-span-id"));               // sets a specific header name, include_header remains true
    /// ```
    pub fn with_tracing<T>(mut self, config: T) -> Self
    where
        T: FnOnce(TracingConfig) -> TracingConfig,
    {
        self.tracing_config = Some(config(self.tracing_config.unwrap_or_default()));
        self
    }

    /// Configures web server with specific Tracing configurations
    ///
    /// Defaults:
    /// - include_header: `false`
    /// - span_header_name: `request-id`
    pub fn set_tracing(mut self, config: TracingConfig) -> Self {
        self.tracing_config = Some(config);
        self
    }
}

#[cfg(test)]
mod tests {
    use hyper::header::HeaderName;

    use super::{DEFAULT_SPAN_HEADER_NAME, TracingConfig};
    use crate::App;

    #[test]
    fn it_creates_new() {
        let tracing_config = TracingConfig::new();

        assert!(!tracing_config.include_header);
        assert_eq!(tracing_config.span_header_name, DEFAULT_SPAN_HEADER_NAME);
    }

    #[test]
    fn it_creates_default() {
        let tracing_config = TracingConfig::default();

        assert!(!tracing_config.include_header);
        assert_eq!(tracing_config.span_header_name, DEFAULT_SPAN_HEADER_NAME);
    }

    #[test]
    fn it_creates_with_include_header() {
        let tracing_config = TracingConfig::new().with_header();

        assert!(tracing_config.include_header);
        assert_eq!(tracing_config.span_header_name, DEFAULT_SPAN_HEADER_NAME);
    }

    #[test]
    fn it_creates_with_header_name() {
        let tracing_config = TracingConfig::new()
            .with_header()
            .with_header_name("correlation-id");

        assert!(tracing_config.include_header);
        assert_eq!(tracing_config.span_header_name, "correlation-id");
    }

    #[test]
    fn it_creates_app_with_default_tracing() {
        let app = App::new().with_default_tracing();
        let tracing_config = app.tracing_config.unwrap();

        assert!(!tracing_config.include_header);
        assert_eq!(
            tracing_config.span_header_name,
            HeaderName::from_static(DEFAULT_SPAN_HEADER_NAME)
        )
    }

    #[test]
    fn it_creates_app_with_span_header() {
        let app = App::new().set_tracing(TracingConfig::new().with_header());

        let tracing_config = app.tracing_config.unwrap();

        assert!(tracing_config.include_header);
        assert_eq!(
            tracing_config.span_header_name,
            HeaderName::from_static(DEFAULT_SPAN_HEADER_NAME)
        )
    }

    #[test]
    fn it_creates_app_with_span_header_name() {
        let app = App::new()
            .with_tracing(|tracing| tracing.with_header().with_header_name("correlation-id"));

        let tracing_config = app.tracing_config.unwrap();

        assert!(tracing_config.include_header);
        assert_eq!(tracing_config.span_header_name, "correlation-id")
    }
}