Skip to content
Merged
Show file tree
Hide file tree
Changes from 15 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
9 changes: 5 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,15 @@ jobs:
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2

# Build the packages using the different feature flags
# Build the packages
- run: cargo build
- run: cargo build --features=metrics
- run: cargo build --features=prometheus
- run: cargo build --features=custom-objective-percentile,custom-objective-latency

# Run the tests
# Run the tests with each of the different metrics libraries
- run: cargo test --features=prometheus-exporter
- run: cargo test --no-default-features --features=prometheus-exporter,metrics --tests
- run: cargo test --no-default-features --features=prometheus-exporter,prometheus --tests
- run: cargo test --no-default-features --features=prometheus-exporter,prometheus-client --tests

# Compile the examples
- run: cargo build --package example-axum
Expand Down
13 changes: 6 additions & 7 deletions autometrics/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@ readme = "README.md"
default = ["opentelemetry"]
metrics = ["dep:metrics"]
opentelemetry = ["opentelemetry_api"]
prometheus = ["const_format", "dep:prometheus", "once_cell"]
prometheus = ["dep:prometheus"]
prometheus-client = ["dep:prometheus-client"]
prometheus-exporter = [
"metrics-exporter-prometheus",
"once_cell",
"opentelemetry-prometheus",
"opentelemetry_sdk",
"prometheus"
Expand All @@ -29,7 +29,8 @@ custom-objective-latency = []

[dependencies]
autometrics-macros = { workspace = true }
spez = { version = "0.1.2" }
spez = "0.1.2"
once_cell = "1.17"

# Used for opentelemetry feature
opentelemetry_api = { version = "0.18", default-features = false, features = ["metrics"], optional = true }
Expand All @@ -39,17 +40,15 @@ metrics = { version = "0.21", default-features = false, optional = true }

# Used for prometheus-exporter feature
metrics-exporter-prometheus = { version = "0.12", default-features = false, optional = true }
once_cell = { version = "1.17", optional = true }
opentelemetry-prometheus = { version = "0.11", optional = true }
opentelemetry_sdk = { version = "0.18", default-features = false, features = ["metrics"], optional = true }
prometheus = { version = "0.13", default-features = false, optional = true }

# Used for prometheus feature
const_format = { version = "0.2", features = ["rust_1_51"], optional = true }
# Used for prometheus-client feature
prometheus-client = { version = "0.21.1", optional = true }

[dev-dependencies]
axum = { version = "0.6", features = ["tokio"] }
regex = "1.7"
http = "0.2"
tokio = { version = "1", features = ["full"] }
trybuild = "1.0"
Expand Down
10 changes: 9 additions & 1 deletion autometrics/src/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ pub const HISTOGRAM_NAME: &str = "function.calls.duration";
pub const GAUGE_NAME: &str = "function.calls.concurrent";
pub const BUILD_INFO_NAME: &str = "build_info";

// Prometheus-flavored metric names
pub const COUNTER_NAME_PROMETHEUS: &str = "function_calls_count";
pub const HISTOGRAM_NAME_PROMETHEUS: &str = "function_calls_duration";
pub const GAUGE_NAME_PROMETHEUS: &str = "function_calls_concurrent";

// Descriptions
pub const COUNTER_DESCRIPTION: &str = "Autometrics counter for tracking function calls";
pub const HISTOGRAM_DESCRIPTION: &str = "Autometrics histogram for tracking function call duration";
Expand All @@ -19,8 +24,11 @@ pub const RESULT_KEY: &'static str = "result";
pub const OK_KEY: &'static str = "ok";
pub const ERROR_KEY: &'static str = "error";
pub const OBJECTIVE_NAME: &'static str = "objective.name";
pub const OBJECTIVE_NAME_PROMETHEUS: &'static str = "objective_name";
pub const OBJECTIVE_PERCENTILE: &'static str = "objective.percentile";
pub const OBJECTIVE_LATENCY_THRESHOLD: &'static str = "objective.latency_threshold";
pub const OBJECTIVE_PERCENTILE_PROMETHEUS: &'static str = "objective_percentile";
pub const OBJECTIVE_LATENCY_THRESHOLD: &'static str = "objective.latency.threshold";
pub const OBJECTIVE_LATENCY_THRESHOLD_PROMETHEUS: &'static str = "objective_latency_threshold";
pub const VERSION_KEY: &'static str = "version";
pub const COMMIT_KEY: &'static str = "commit";
pub const BRANCH_KEY: &'static str = "branch";
146 changes: 112 additions & 34 deletions autometrics/src/labels.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
use crate::{constants::*, objectives::*};
#[cfg(feature = "prometheus-client")]
use prometheus_client::encoding::{EncodeLabelSet, EncodeLabelValue, LabelValueEncoder};
use std::ops::Deref;

pub(crate) type Label = (&'static str, &'static str);
pub type ResultAndReturnTypeLabels = (&'static str, Option<&'static str>);

/// These are the labels used for the `build_info` metric.
#[cfg_attr(
feature = "prometheus-client",
derive(EncodeLabelSet, Debug, Clone, PartialEq, Eq, Hash)
)]
pub struct BuildInfoLabels {
pub(crate) version: &'static str,
pub(crate) commit: &'static str,
pub(crate) branch: &'static str,
pub(crate) commit: &'static str,
pub(crate) version: &'static str,
}

impl BuildInfoLabels {
Expand All @@ -30,12 +36,47 @@ impl BuildInfoLabels {
}

/// These are the labels used for the `function.calls.count` metric.
#[cfg_attr(
feature = "prometheus-client",
derive(EncodeLabelSet, Debug, Clone, PartialEq, Eq, Hash)
)]
pub struct CounterLabels {
pub(crate) function: &'static str,
pub(crate) module: &'static str,
pub(crate) caller: &'static str,
pub(crate) result: Option<ResultAndReturnTypeLabels>,
pub(crate) objective: Option<(&'static str, ObjectivePercentile)>,
pub(crate) result: Option<ResultLabel>,
pub(crate) ok: Option<&'static str>,
pub(crate) error: Option<&'static str>,
pub(crate) objective_name: Option<&'static str>,
pub(crate) objective_percentile: Option<ObjectivePercentile>,
}

#[cfg_attr(
feature = "prometheus-client",
derive(Debug, Clone, PartialEq, Eq, Hash)
)]
pub(crate) enum ResultLabel {
Ok,
Error,
}

impl ResultLabel {
pub(crate) const fn as_str(&self) -> &'static str {
match self {
ResultLabel::Ok => OK_KEY,
ResultLabel::Error => ERROR_KEY,
}
}
}

#[cfg(feature = "prometheus-client")]
impl EncodeLabelValue for ResultLabel {
fn encode(&self, encoder: &mut LabelValueEncoder) -> Result<(), std::fmt::Error> {
match self {
ResultLabel::Ok => EncodeLabelValue::encode(&OK_KEY, encoder),
ResultLabel::Error => EncodeLabelValue::encode(&ERROR_KEY, encoder),
}
}
}

impl CounterLabels {
Expand All @@ -46,21 +87,33 @@ impl CounterLabels {
result: Option<ResultAndReturnTypeLabels>,
objective: Option<Objective>,
) -> Self {
let objective = if let Some(objective) = objective {
let (objective_name, objective_percentile) = if let Some(objective) = objective {
if let Some(success_rate) = objective.success_rate {
Some((objective.name, success_rate))
(Some(objective.name), Some(success_rate))
} else {
None
(None, None)
}
} else {
(None, None)
};
let (result, ok, error) = if let Some((result, return_value_type)) = result {
match result {
OK_KEY => (Some(ResultLabel::Ok), return_value_type, None),
ERROR_KEY => (Some(ResultLabel::Error), None, return_value_type),
_ => (None, None, None),
}
} else {
None
(None, None, None)
};
Self {
function,
module,
caller,
objective_name,
objective_percentile,
result,
objective,
ok,
error,
}
}

Expand All @@ -70,62 +123,86 @@ impl CounterLabels {
(MODULE_KEY, self.module),
(CALLER_KEY, self.caller),
];
if let Some((result, return_value_type)) = self.result {
labels.push((RESULT_KEY, result));
if let Some(return_value_type) = return_value_type {
labels.push((result, return_value_type));
}
if let Some(result) = &self.result {
labels.push((RESULT_KEY, result.as_str()));
}
if let Some(ok) = self.ok {
labels.push((OK_KEY, ok));
}
if let Some(error) = self.error {
labels.push((ERROR_KEY, error));
}
if let Some(objective_name) = self.objective_name {
labels.push((OBJECTIVE_NAME, objective_name));
}
if let Some((name, percentile)) = &self.objective {
labels.push((OBJECTIVE_NAME, name));
labels.push((OBJECTIVE_PERCENTILE, percentile.as_str()));
if let Some(objective_percentile) = &self.objective_percentile {
labels.push((OBJECTIVE_PERCENTILE, objective_percentile.as_str()));
}

labels
}
}

/// These are the labels used for the `function.calls.duration` metric.
#[cfg_attr(
feature = "prometheus-client",
derive(EncodeLabelSet, Debug, Clone, PartialEq, Eq, Hash)
)]
pub struct HistogramLabels {
pub function: &'static str,
pub module: &'static str,
/// The SLO name, objective percentile, and latency threshold
pub objective: Option<(&'static str, ObjectivePercentile, ObjectiveLatency)>,
pub objective_name: Option<&'static str>,
pub objective_percentile: Option<ObjectivePercentile>,
pub objective_latency_threshold: Option<ObjectiveLatency>,
}

impl HistogramLabels {
pub fn new(function: &'static str, module: &'static str, objective: Option<Objective>) -> Self {
let objective = if let Some(objective) = objective {
if let Some((latency, percentile)) = objective.latency {
Some((objective.name, percentile, latency))
let (objective_name, objective_percentile, objective_latency_threshold) =
if let Some(objective) = objective {
if let Some((latency, percentile)) = objective.latency {
(Some(objective.name), Some(percentile), Some(latency))
} else {
(None, None, None)
}
} else {
None
}
} else {
None
};
(None, None, None)
};

Self {
function,
module,
objective,
objective_name,
objective_percentile,
objective_latency_threshold,
}
}

pub fn to_vec(&self) -> Vec<Label> {
let mut labels = vec![(FUNCTION_KEY, self.function), (MODULE_KEY, self.module)];

if let Some((name, percentile, latency)) = &self.objective {
labels.push((OBJECTIVE_NAME, name));
labels.push((OBJECTIVE_PERCENTILE, percentile.as_str()));
labels.push((OBJECTIVE_LATENCY_THRESHOLD, latency.as_str()));
if let Some(objective_name) = self.objective_name {
labels.push((OBJECTIVE_NAME, objective_name));
}
if let Some(objective_percentile) = &self.objective_percentile {
labels.push((OBJECTIVE_PERCENTILE, objective_percentile.as_str()));
}
if let Some(objective_latency_threshold) = &self.objective_latency_threshold {
labels.push((
OBJECTIVE_LATENCY_THRESHOLD,
objective_latency_threshold.as_str(),
));
}

labels
}
}

/// These are the labels used for the `function.calls.concurrent` metric.
#[cfg_attr(
feature = "prometheus-client",
derive(EncodeLabelSet, Debug, Clone, PartialEq, Eq, Hash)
)]
pub struct GaugeLabels {
pub function: &'static str,
pub module: &'static str,
Expand Down Expand Up @@ -235,11 +312,12 @@ impl_trait_for_types!(GetStaticStr);
/// The macro uses the autoref specialization trick through spez to get the labels for the type in a variety of circumstances.
/// Specifically, if the value is a Result, it will add the ok or error label accordingly unless one or both of the types that
/// the Result<T, E> is generic over implements the GetLabels trait. The label allows to override the inferred label, and the
/// [`ResultLabels`](crate::result_labels) macro implements the GetLabels trait for the user using annotations.
/// [`ResultLabels`](crate::ResultLabels) macro implements the GetLabels trait for the user using annotations.
///
/// The macro is meant to be called with a reference as argument: `get_result_labels_for_value(&return_value)`
///
/// Ref: https://github.com/dtolnay/case-studies/blob/master/autoref-specialization/README.md
/// See: <https://github.com/dtolnay/case-studies/blob/master/autoref-specialization/README.md>
#[doc(hidden)]
#[macro_export]
macro_rules! get_result_labels_for_value {
($e:expr) => {{
Expand Down
14 changes: 13 additions & 1 deletion autometrics/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -186,9 +186,21 @@ pub use autometrics_macros::ResultLabels;
#[cfg(feature = "prometheus-exporter")]
pub use self::prometheus_exporter::*;

/// Functionality specific to the libraries used to collect metrics
pub mod integrations {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Still not sure I like this name, but I can't think of anything better. metrics_libraries, producers, ...

Copy link
Contributor

Choose a reason for hiding this comment

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

couldn't we just put the integrations as the top level name, getting rid of integrations? so the resolved path would end up being smth like autometrics::prometheus_client instead of autometrics::integrations::prometheus_client

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hmm that would also be an option. The main downside with that is we may want some other submodules exported from the crate root that are different, and they could get a bit lost. For example #90. We'd have 4-5 modules that are all related to the metrics libraries and 1-2 that aren't.

Copy link
Contributor

Choose a reason for hiding this comment

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

seems fine imo

#[cfg(feature = "prometheus-client")]
pub mod prometheus_client {
pub use crate::tracker::prometheus_client::PROMETHEUS_CLIENT_REGISTRY;
}
}

/// We use the histogram buckets recommended by the OpenTelemetry specification
/// https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk.md#explicit-bucket-histogram-aggregation
#[cfg(any(feature = "prometheus", feature = "prometheus-exporter"))]
#[cfg(any(
feature = "prometheus",
feature = "prometheus-client",
feature = "prometheus-exporter"
))]
pub(crate) const HISTOGRAM_BUCKETS: [f64; 14] = [
0.005, 0.01, 0.025, 0.05, 0.075, 0.1, 0.25, 0.5, 0.75, 1.0, 2.5, 5.0, 7.5, 10.0,
];
Expand Down
25 changes: 25 additions & 0 deletions autometrics/src/objectives.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@
//! }
//! ```

#[cfg(feature = "prometheus-client")]
use prometheus_client::encoding::{EncodeLabelValue, LabelValueEncoder};

/// A Service-Level Objective (SLO) for a function or group of functions.
///
/// The objective should be given a descriptive name and can represent
Expand Down Expand Up @@ -119,6 +122,10 @@ impl Objective {
}

/// The percentage of requests that must meet the given criteria (success rate or latency).
#[cfg_attr(
feature = "prometheus-client",
derive(Clone, Debug, PartialEq, Eq, Hash)
Copy link
Contributor

Choose a reason for hiding this comment

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

can also be Copy, which would make it easier to use

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hmm, true. Do you think we should derive all of those traits all of the time (not just when using prometheus-client)? I'm somewhat hesitant to do that proactively because it technically expands the surface area of the API and (maybe?) increases compile times. But maybe it's fine to just do it for this type of enum

Copy link
Contributor

Choose a reason for hiding this comment

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

saving Copy makes it way nicer to use. just be sure that u dont plan to add more in the future which may not be Copy, as removing Copy is a breaking change and requires a major version bump (unlike adding it)

)]
#[non_exhaustive]
pub enum ObjectivePercentile {
/// 90%
Expand Down Expand Up @@ -152,7 +159,18 @@ impl ObjectivePercentile {
}
}

#[cfg(feature = "prometheus-client")]
impl EncodeLabelValue for ObjectivePercentile {
fn encode(&self, encoder: &mut LabelValueEncoder) -> Result<(), std::fmt::Error> {
self.as_str().encode(encoder)
}
}

/// The latency threshold, in milliseoncds, for a given objective.
#[cfg_attr(
feature = "prometheus-client",
derive(Clone, Debug, PartialEq, Eq, Hash)
)]
#[non_exhaustive]
pub enum ObjectiveLatency {
/// 5 milliseconds
Expand Down Expand Up @@ -234,3 +252,10 @@ impl ObjectiveLatency {
}
}
}

#[cfg(feature = "prometheus-client")]
impl EncodeLabelValue for ObjectiveLatency {
fn encode(&self, encoder: &mut LabelValueEncoder) -> Result<(), std::fmt::Error> {
self.as_str().encode(encoder)
}
}
Loading