Skip to content

Commit

Permalink
Add serve_url method (#6)
Browse files Browse the repository at this point in the history
* serve_url

Signed-off-by: Dan Bond <[email protected]>

* add DEFAULT_METRICS_PATH

Signed-off-by: Dan Bond <[email protected]>

* add tests

Signed-off-by: Dan Bond <[email protected]>

* add docs

Signed-off-by: Dan Bond <[email protected]>

* add parse_url tests

Signed-off-by: Dan Bond <[email protected]>

* update docs

Signed-off-by: Dan Bond <[email protected]>

Signed-off-by: Dan Bond <[email protected]>
  • Loading branch information
loshz authored Jan 3, 2023
1 parent e204f98 commit da87648
Show file tree
Hide file tree
Showing 5 changed files with 99 additions and 23 deletions.
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "metrics_server"
version = "0.10.1"
version = "0.11.0"
authors = ["Dan Bond <[email protected]>"]
edition = "2021"
rust-version = "1.58"
Expand Down
24 changes: 20 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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);

Expand All @@ -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);

Expand Down
2 changes: 1 addition & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,4 +48,4 @@ mod error;
mod server;

pub use error::ServerError;
pub use server::MetricsServer;
pub use server::{MetricsServer, DEFAULT_METRICS_PATH};
63 changes: 53 additions & 10 deletions src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<MetricsServerShared>,
Expand Down Expand Up @@ -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.
Expand All @@ -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.
Expand All @@ -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);
Expand All @@ -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);
Expand All @@ -161,8 +178,6 @@ impl MetricsServer {
}
}
}));

self
}

/// Stop serving requests and free thread resources.
Expand All @@ -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);
}
}
31 changes: 24 additions & 7 deletions tests/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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());
Expand Down Expand Up @@ -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")]
Expand Down

0 comments on commit da87648

Please sign in to comment.