diff --git a/Cargo.toml b/Cargo.toml index 150cd06..eb0b812 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "metrics_server" -version = "0.10.1" +version = "0.11.0" authors = ["Dan Bond "] edition = "2021" rust-version = "1.58" diff --git a/README.md b/README.md index 145dad9..2ddf8d5 100644 --- a/README.md +++ b/README.md @@ -16,13 +16,13 @@ This crate provides a thread safe, minimalstic HTTP/S server used to buffer metr Include the lib in your `Cargo.toml` dependencies: ```toml [dependencies] -metrics_server = "0.10" +metrics_server = "0.11" ``` To enable TLS support, pass the optional feature flag: ```toml [dependencies] -metrics_server = { version = "0.10", features = ["tls"] } +metrics_server = { version = "0.11", features = ["tls"] } ``` ### HTTP @@ -32,7 +32,7 @@ use metrics_server::MetricsServer; // Create a new HTTP server and start listening for requests in the background. let server = MetricsServer::http("localhost:8001"); -// Publish you application metrics. +// Publish your application metrics. let bytes = server.update(Vec::from([1, 2, 3, 4])); assert_eq!(4, bytes); @@ -52,7 +52,23 @@ let key = include_bytes!("/path/to/key.pem").to_vec(); // Create a new HTTPS server and start listening for requests in the background. let server = MetricsServer::https("localhost:8443", cert, key); -// Publish you application metrics. +// Publish your application metrics. +let bytes = server.update(Vec::from([1, 2, 3, 4])); +assert_eq!(4, bytes); + +// Stop the server. +server.stop().unwrap(); +``` + +### Serve a custom URL +```rust +use metrics_server::MetricsServer; + +// Create a new server and specify the URL path to serve. +let mut server = MetricsServer::new("localhost:8001", None, None); +server.serve_url("/path/to/metrics"); + +// Publish your application metrics. let bytes = server.update(Vec::from([1, 2, 3, 4])); assert_eq!(4, bytes); diff --git a/src/lib.rs b/src/lib.rs index 87c8ac6..42d16e2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -48,4 +48,4 @@ mod error; mod server; pub use error::ServerError; -pub use server::MetricsServer; +pub use server::{MetricsServer, DEFAULT_METRICS_PATH}; diff --git a/src/server.rs b/src/server.rs index 4069b69..f5ad70c 100644 --- a/src/server.rs +++ b/src/server.rs @@ -8,6 +8,9 @@ use tiny_http::{Method, Response, Server}; use crate::error::ServerError; +/// The default metrics URL path of the server. +pub const DEFAULT_METRICS_PATH: &str = "/metrics"; + /// A thread-safe datastore for serving metrics via a HTTP/S server. pub struct MetricsServer { shared: Arc, @@ -71,7 +74,9 @@ impl MetricsServer { where A: ToSocketAddrs, { - MetricsServer::new(addr, None, None).unwrap().serve() + let mut server = MetricsServer::new(addr, None, None).unwrap(); + server.serve(); + server } /// Shortcut for creating an empty `MetricsServer` and starting a HTTPS server on a new thread at the given address. @@ -88,9 +93,9 @@ impl MetricsServer { where A: ToSocketAddrs, { - MetricsServer::new(addr, Some(certificate), Some(private_key)) - .unwrap() - .serve() + let mut server = MetricsServer::new(addr, Some(certificate), Some(private_key)).unwrap(); + server.serve(); + server } /// Safely updates the data in a `MetricsServer` and returns the number of bytes written. @@ -102,17 +107,29 @@ impl MetricsServer { buf.as_slice().len() } - /// Start serving requests on the underlying server. + /// Start serving requests to the /metrics URL path on the underlying server. + /// + /// The server will only respond synchronously as it blocks until receiving new requests. + /// Suqsequent calls to this method will return a no-op and not affect the underlying server. + pub fn serve(&mut self) { + self.serve_url(DEFAULT_METRICS_PATH.to_string()) + } + + /// Start serving requests to a specific URL path on the underlying server. /// /// The server will only respond synchronously as it blocks until receiving new requests. - pub fn serve(mut self) -> Self { + /// Suqsequent calls to this method will return a no-op and not affect the underlying server. + pub fn serve_url(&mut self, mut url: String) { // Check if we already have a thread running. if let Some(thread) = &self.thread { if !thread.is_finished() { - return self; + return; } } + // Ensure URL is valid. + url = parse_url(url); + // Invoking clone on Arc produces a new Arc instance, which points to the // same allocation on the heap as the source Arc, while increasing a reference count. let s = Arc::clone(&self.shared); @@ -135,7 +152,7 @@ impl MetricsServer { ); // Only serve the /metrics path. - if req.url().to_lowercase() != "/metrics" { + if req.url().to_lowercase() != url { let res = Response::empty(404); if let Err(e) = req.respond(res) { error!("metrics_server error: {}", e); @@ -161,8 +178,6 @@ impl MetricsServer { } } })); - - self } /// Stop serving requests and free thread resources. @@ -187,3 +202,31 @@ impl MetricsServer { } } } + +/// Naive URL parse that simply removes whitespace and prepends a "/" if not already present. +fn parse_url(mut url: String) -> String { + url.retain(|c| !c.is_whitespace()); + + if !url.starts_with('/') { + url = format!("/{}", url); + } + + url.to_lowercase() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_url() { + let expected = DEFAULT_METRICS_PATH.to_string(); + + // No slash prefix. + assert_eq!(parse_url("metrics".to_string()), expected); + // Leading/trailing whitespace. + assert_eq!(parse_url(" metrics ".to_string()), expected); + // Uppercase. + assert_eq!(parse_url("METRICS".to_string()), expected); + } +} diff --git a/tests/server.rs b/tests/server.rs index 96e2b47..ea82483 100644 --- a/tests/server.rs +++ b/tests/server.rs @@ -46,13 +46,12 @@ fn test_new_server_invalid_private_key() { #[test] fn test_new_server_already_running() { - let server = MetricsServer::new("localhost:8002", None, None) - .unwrap() - .serve(); + let mut server = MetricsServer::new("localhost:8002", None, None).unwrap(); // Attempt to start an already running server should be ok // as we will return the pre-existing thread. - let server = server.serve(); + server.serve(); + server.serve(); // Stop the server. server.stop().unwrap(); @@ -71,13 +70,14 @@ fn test_new_https_server() { #[test] #[should_panic] fn test_http_server_invalid_address() { - _ = MetricsServer::http("invalid:99999999"); + let _ = MetricsServer::http("invalid:99999999"); } #[test] fn test_http_server_serve() { - let server = MetricsServer::http("localhost:8001"); - // + let mut server = MetricsServer::new("localhost:8001", None, None).unwrap(); + server.serve(); + // Assert calls to non /metrics endpoint returns 404. let res = reqwest::blocking::get("http://localhost:8001/invalid").unwrap(); assert_eq!(404, res.status()); @@ -116,6 +116,23 @@ fn test_http_server_serve() { server.stop().unwrap(); } +#[test] +fn test_http_server_serve_url() { + let mut server = MetricsServer::new("localhost:8004", None, None).unwrap(); + server.serve_url("/test".to_string()); + + // Assert calls to non /metrics endpoint returns 404. + let res = reqwest::blocking::get("http://localhost:8004/metrics").unwrap(); + assert_eq!(404, res.status()); + + // Assert calls to /test returns 200. + let res = reqwest::blocking::get("http://localhost:8004/test").unwrap(); + assert_eq!(200, res.status()); + + // Stop the server. + server.stop().unwrap(); +} + #[test] #[should_panic] #[cfg(feature = "tls")]