Skip to content
Merged
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
10 changes: 10 additions & 0 deletions docs/reference/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -876,3 +876,13 @@ RUST_LOG=debug quickwit run
# run with log level info, except for indexing related logs
RUST_LOG=info,quickwit_indexing=debug quickwit run
```

### QW_METRICS_LABELS

Attach the same labels to every Quickwit metric. Specify labels as a comma-separated list of `name=value` pairs. Label names and values must not be empty.

*Example*

`QW_METRICS_LABELS="environment=production,region=us-east-1" quickwit run`

The environment variable is read once during telemetry initialization. Set it before starting Quickwit; changing it while Quickwit is running has no effect. If any entry does not use the `name=value` format, telemetry initialization fails.
1 change: 1 addition & 0 deletions quickwit/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions quickwit/quickwit-metrics/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ description = "Type-safe, zero-allocation metric declarations built on the metri
categories = ["development-tools::profiling"]

[dependencies]
anyhow = { workspace = true }
metrics = { workspace = true }
metrics-util = { workspace = true }
inventory = { workspace = true }
Expand Down
24 changes: 22 additions & 2 deletions quickwit/quickwit-metrics/src/inner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,19 @@ use std::hash::{Hash, Hasher};

#[doc(hidden)]
pub use const_format::concatcp as __concatcp;
use metrics::Label;
use rustc_hash::FxHasher;

// ─── Helper functions ───
use crate::LABELS_ENV_VAR;

/// Returns the labels from the environment variable.
#[doc(hidden)]
pub fn __labels_env_var() -> &'static [Label] {
match LABELS_ENV_VAR.get() {
Some(labels) => labels.as_ref(),
None => &[],
}
}

/// Counts the number of token-tree arguments at compile time.
#[doc(hidden)]
Expand Down Expand Up @@ -94,7 +104,17 @@ macro_rules! __key_info_metadata {
static LABELS: [$crate::__metrics::Label; $crate::__count!($($label)*)] = [
$($crate::__metrics::Label::from_static_parts($label, $value)),*
];
static KEY: $crate::__metrics::Key = $crate::__metrics::Key::from_static_parts(KEY_NAME, &LABELS);
static KEY: std::sync::LazyLock<$crate::__metrics::Key> = std::sync::LazyLock::new(|| {
let labels_env_var = $crate::__labels_env_var();
if labels_env_var.is_empty() {
$crate::__metrics::Key::from_static_parts(KEY_NAME, &LABELS)
} else {
let mut labels = Vec::with_capacity(LABELS.len() + labels_env_var.len());
labels.extend(LABELS.iter().cloned());
labels.extend(labels_env_var.iter().cloned());
Comment thread
Mallets marked this conversation as resolved.
$crate::__metrics::Key::from_parts(KEY_NAME, labels)
}
});
};
}

Expand Down
131 changes: 130 additions & 1 deletion quickwit/quickwit-metrics/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,10 @@
pub const SYSTEM: &str = "quickwit";
pub const SEPARATOR: &str = "_";

/// The name of the environment variable that contains the injected labels for all the metrics.
pub const QW_METRICS_LABELS_ENV_VAR: &str = "QW_METRICS_LABELS";
pub(crate) static LABELS_ENV_VAR: OnceLock<Box<[Label]>> = OnceLock::new();

// ─── Metric modules ───
mod counter;
mod gauge;
Expand All @@ -276,6 +280,8 @@ mod histogram;
mod inner;
mod labels;

use std::sync::OnceLock;

// ─── Internal helpers (re-exported for macro expansion) ───
//
// These re-exports exist so that downstream crates only need
Expand All @@ -288,7 +294,7 @@ pub use gauge::__gauge_get_or_register;
#[doc(hidden)]
pub use histogram::__histogram_get_or_register;
#[doc(hidden)]
pub use inner::{__concatcp, __key_hash, __sep};
pub use inner::{__concatcp, __key_hash, __labels_env_var, __sep};

// Re-exports of `metrics` and `inventory` used inside macro expansions.
#[doc(hidden)]
Expand All @@ -305,6 +311,7 @@ pub use counter::{Counter, LazyCounter};
pub use gauge::{Gauge, GaugeGuard, LazyGauge};
pub use histogram::{Histogram, HistogramConfig, HistogramTimer, LazyHistogram};
pub use labels::{LabelNames, Labels};
use metrics::Label;
// ─── metrics-rs re-exports ───
pub use metrics::{CounterFn, GaugeFn, HistogramFn};
pub use metrics_util::MetricKind;
Expand Down Expand Up @@ -382,3 +389,125 @@ pub fn histogram_buckets() -> impl Iterator<Item = (&'static str, Vec<f64>)> {
(c.info.key_name, buckets)
})
}

/// Initializes the global metrics labels from the environment variable.
pub fn init_metrics_labels_env_var() -> anyhow::Result<()> {
// If the labels environment variable is already initialized, return early.
if LABELS_ENV_VAR.get().is_some() {
return Ok(());
}

// quickwit-common defines common helpers for getting environment variables.
// However, we need to use the `std::env::var` function directly here because
// quickwit-common depends on quickwit-metrics and it would cause a circular dependency.
let labels = match std::env::var(QW_METRICS_LABELS_ENV_VAR) {
Ok(labels) => labels,
Err(std::env::VarError::NotPresent) => String::new(),
Err(std::env::VarError::NotUnicode(_)) => {
anyhow::bail!("{QW_METRICS_LABELS_ENV_VAR} must contain valid Unicode")
}
};
let parsed_labels = parse_metrics_labels(&labels)?;
LABELS_ENV_VAR.get_or_init(|| parsed_labels.into_boxed_slice());

Ok(())
}

// The format of the environment variable is:
// QW_METRICS_LABELS="environment=test,region=us-east-1,foo=bar"
fn parse_metrics_labels(labels: &str) -> anyhow::Result<Vec<Label>> {
let mut parsed_labels: Vec<Label> = Vec::new();
if labels.trim().is_empty() {
return Ok(parsed_labels);
}

const LABELS_SEPARATOR: char = ',';
const KEY_VALUE_SEPARATOR: char = '=';

for label in labels.split(LABELS_SEPARATOR) {
let (name, value) = label.split_once(KEY_VALUE_SEPARATOR).ok_or_else(|| {
anyhow::anyhow!(
"{} contains invalid label format: {}",
QW_METRICS_LABELS_ENV_VAR,
label
)
})?;
let name = name.trim();
if name.is_empty() {
anyhow::bail!(
"{} contains an empty label name: {}",
QW_METRICS_LABELS_ENV_VAR,
label
);
}
let value = value.trim();
if value.is_empty() {
anyhow::bail!(
"{} contains an empty label value: {}",
QW_METRICS_LABELS_ENV_VAR,
label
);
}
let label = Label::new(name.to_string(), value.to_string());
parsed_labels.push(label);
Comment thread
Mallets marked this conversation as resolved.
}

Ok(parsed_labels)
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_parse_metrics_labels() {
let labels = parse_metrics_labels("environment=production, region=us-east-1 ")
.expect("labels should be valid");

assert_eq!(labels.len(), 2);
assert_eq!(labels[0].key(), "environment");
assert_eq!(labels[0].value(), "production");
assert_eq!(labels[1].key(), "region");
assert_eq!(labels[1].value(), "us-east-1");
}

#[test]
fn test_parse_metrics_labels_accepts_empty_input() {
let labels = parse_metrics_labels(" ").expect("empty input should be valid");

assert!(labels.is_empty());
}

#[test]
fn test_parse_metrics_labels_rejects_missing_separator() {
let error = parse_metrics_labels("environment=production,region")
.expect_err("label without a separator should be rejected");

assert_eq!(
error.to_string(),
"QW_METRICS_LABELS contains invalid label format: region"
);
}

#[test]
fn test_parse_metrics_labels_rejects_empty_name() {
let error =
parse_metrics_labels(" =production").expect_err("empty label name should be rejected");

assert_eq!(
error.to_string(),
"QW_METRICS_LABELS contains an empty label name: =production"
);
}

#[test]
fn test_parse_metrics_labels_rejects_empty_value() {
let error = parse_metrics_labels("environment= ")
.expect_err("empty label value should be rejected");

assert_eq!(
error.to_string(),
"QW_METRICS_LABELS contains an empty label value: environment= "
);
}
}
2 changes: 2 additions & 0 deletions quickwit/quickwit-telemetry-exporters/src/metrics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ pub(crate) fn init_metrics_provider(
service_version: &str,
otlp_config: &OtlpExporterConfig,
) -> anyhow::Result<Option<SdkMeterProvider>> {
quickwit_metrics::init_metrics_labels_env_var()?;

let prometheus_recorder = crate::prometheus::metrics::build_recorder()?;

let (recorder, meter_provider) = if otlp_config.is_enabled() {
Expand Down
Loading