Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,14 @@ exclude = [".gitignore", ".travis.yml"]
[badges]
travis-ci = { repository = "nlopes/actix-web-prom", branch = "master" }

[features]
default = []
regex-replace = ["regex"]

[dependencies]
actix-service = "0.4"
actix-web = "1.0.0"
futures = "0.1"
prometheus = "0.7"
regex = { version = "1.3.1", optional = true }
log = "0.4.8"
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There should be more logging in this crate IMO, added the log crate to enable logging.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed.

96 changes: 81 additions & 15 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,9 @@ use futures::future::{ok, FutureResult};
use futures::{Async, Future, Poll};
use prometheus::{opts, Encoder, HistogramVec, IntCounterVec, Registry, TextEncoder};

#[cfg(feature = "regex-replace")]
use regex::Regex;

#[derive(Clone)]
#[must_use = "must be set up as middleware for actix-web"]
/// By default two metrics are tracked (this assumes the namespace `actix_web_prom`):
Expand All @@ -221,11 +224,15 @@ pub struct PrometheusMetrics {
pub registry: Registry,
pub(crate) namespace: String,
pub(crate) endpoint: Option<String>,

#[cfg(feature = "regex-replace")]
pub(crate) replacements: Vec<(Regex, String)>,
}

impl PrometheusMetrics {
/// Create a new PrometheusMetrics. You set the namespace and the metrics endpoint
/// through here.
#[cfg(not(feature = "regex-replace"))]
pub fn new(namespace: &str, endpoint: Option<&str>) -> Self {
let registry = Registry::new();

Expand All @@ -234,20 +241,65 @@ impl PrometheusMetrics {
}

/// Create a new PrometheusMetrics.
/// Throws error if "<`namespace`>_http_requests_total" already registered
/// Returns an error if "<`namespace`>_http_requests_total" already registered.
/// Request paths are adjusted by executing the supplied replacements before storing the metrics.
#[cfg(feature = "regex-replace")]
pub fn new_with_registry(
registry: Registry,
namespace: &str,
endpoint: Option<&str>,
replacements: Vec<(Regex, String)>,
) -> Result<Self, Box<dyn std::error::Error>> {
let (http_requests_total, http_requests_duration_seconds) =
Self::register_metrics(&registry, namespace)?;
Ok(PrometheusMetrics {
http_requests_total,
http_requests_duration_seconds,
registry,
namespace: namespace.to_string(),
endpoint: if let Some(url) = endpoint {
Some(url.to_string())
} else {
None
},
replacements,
})
}

/// Create a new PrometheusMetrics.
/// Returns an error if "<`namespace`>_http_requests_total" already registered
#[cfg(not(feature = "regex-replace"))]
pub fn new_with_registry(
registry: Registry,
namespace: &str,
endpoint: Option<&str>,
) -> Result<Self, Box<dyn std::error::Error>> {
let (http_requests_total, http_requests_duration_seconds) =
Self::register_metrics(&registry, namespace)?;
Ok(PrometheusMetrics {
http_requests_total,
http_requests_duration_seconds,
registry,
namespace: namespace.to_string(),
endpoint: if let Some(url) = endpoint {
Some(url.to_string())
} else {
None
},
})
}

fn register_metrics(
registry: &Registry,
namespace: &str,
) -> prometheus::Result<(IntCounterVec, HistogramVec)> {
let http_requests_total_opts =
opts!("http_requests_total", "Total number of HTTP requests").namespace(namespace);
let http_requests_total =
IntCounterVec::new(http_requests_total_opts, &["endpoint", "method", "status"])
.unwrap();
registry
.register(Box::new(http_requests_total.clone()))
.unwrap();

registry.register(Box::new(http_requests_total.clone()))?;

let http_requests_duration_seconds_opts = opts!(
"http_requests_duration_seconds",
Expand All @@ -259,19 +311,10 @@ impl PrometheusMetrics {
&["endpoint", "method", "status"],
)
.unwrap();

registry.register(Box::new(http_requests_duration_seconds.clone()))?;

Ok(PrometheusMetrics {
http_requests_total,
http_requests_duration_seconds,
registry,
namespace: namespace.to_string(),
endpoint: if let Some(url) = endpoint {
Some(url.to_string())
} else {
None
},
})
Ok((http_requests_total, http_requests_duration_seconds))
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally a cfg attribute on the parameter would make duplicating this method unnecessary, but that would require rust nightly, since attributes on parameters are not stabilized yet (proposed to be stabilized soon though!)

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe it's now in 1.39 so this can be changed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It requires that we bump the minimum rust version to 1.39, if that's okay we can do that.

fn metrics(&self) -> String {
Expand All @@ -290,7 +333,29 @@ impl PrometheusMetrics {
}
}

#[cfg(feature = "regex-replace")]
fn update_metrics(&self, path: &str, method: &Method, status: StatusCode, clock: SystemTime) {
log::trace!("About to replacing path capture groups: {}", path);
let mut path = String::from(path);
for (exp, repl) in self.replacements.clone() {
path = String::from(exp.replace_all(&path, &repl as &str))
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not really happy with this as it is, because each time we're updating metrics, we copy all replacements, the path once, and then the path again for each replacement. I couldn't find a way to do this with less copies though that also makes the borrow checker happy.

log::trace!("Replaced path capture groups: {}", path);
self.update_metrics_inner(&path, method, status, clock)
}

#[cfg(not(feature = "regex-replace"))]
fn update_metrics(&self, path: &str, method: &Method, status: StatusCode, clock: SystemTime) {
self.update_metrics_inner(path, method, status, clock)
}

fn update_metrics_inner(
&self,
path: &str,
method: &Method,
status: StatusCode,
clock: SystemTime,
) {
let method = method.to_string();
let status = status.as_u16().to_string();

Expand All @@ -304,6 +369,7 @@ impl PrometheusMetrics {
self.http_requests_total
.with_label_values(&[&path, &method, &status])
.inc();
log::trace!("updated metrics");
}
}

Expand Down