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
158 changes: 158 additions & 0 deletions quickwit/quickwit-common/src/env.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
// Copyright 2021-Present Datadog, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

use std::fmt::Debug;
use std::str::FromStr;

use tracing::{error, info};

fn get_from_env_opt_aux<T: Debug>(
key: &str,
parse_fn: impl FnOnce(&str) -> Option<T>,
sensitive: bool,
) -> Option<T> {
let value_str = std::env::var(key).ok()?;
let Some(value) = parse_fn(&value_str) else {
error!(value=%value_str, "failed to parse environment variable `{key}` value");
return None;
};
if sensitive {
info!("using environment variable `{key}` value");
} else {
info!(value=?value, "using environment variable `{key}` value");
}
Some(value)
}

pub fn get_from_env<T: FromStr + Debug>(key: &str, default_value: T, sensitive: bool) -> T {
if let Some(value) = get_from_env_opt(key, sensitive) {
value
} else {
info!(default_value=?default_value, "using environment variable `{key}` default value");
default_value
}
}

pub fn get_from_env_opt<T: FromStr + Debug>(key: &str, sensitive: bool) -> Option<T> {
get_from_env_opt_aux(key, |val_str| val_str.parse().ok(), sensitive)
}

pub fn get_bool_from_env_opt(key: &str) -> Option<bool> {
get_from_env_opt_aux(key, parse_bool_lenient, false)
}

pub fn get_bool_from_env(key: &str, default_value: bool) -> bool {
if let Some(flag_value) = get_bool_from_env_opt(key) {
flag_value
} else {
info!(default_value=%default_value, "using environment variable `{key}` default value");
default_value
}
}

pub fn parse_bool_lenient(bool_str: &str) -> Option<bool> {
let trimmed_bool_str = bool_str.trim();

for truthy_value in ["true", "yes", "1"] {
if trimmed_bool_str.eq_ignore_ascii_case(truthy_value) {
return Some(true);
}
}
for falsy_value in ["false", "no", "0"] {
if trimmed_bool_str.eq_ignore_ascii_case(falsy_value) {
return Some(false);
}
}
None
}

/// Reads and parses an environment variable exactly once, caching the result for the lifetime of
/// the process via a per-call-site [`std::sync::LazyLock`].
///
/// Prefer this over [`get_from_env`](crate::get_from_env) on paths that may run repeatedly (e.g.
/// constructors that are re-invoked): it avoids re-reading and, more importantly, re-logging the
/// same variable on every call.
///
/// The type is required because the backing `static` needs a concrete type. The key and default
/// must be `const` expressions or literals — a `static` initializer cannot capture locals.
///
/// ```no_run
/// # use quickwit_common::get_from_env_cached;
/// let max_concurrency: usize = get_from_env_cached!(usize, "QW_S3_MAX_CONCURRENCY", 10_000, false);
/// ```
#[macro_export]
macro_rules! get_from_env_cached {
($ty:ty, $key:expr, $default:expr, $sensitive:expr $(,)?) => {{
static CACHED: ::std::sync::LazyLock<$ty> =
::std::sync::LazyLock::new(|| $crate::get_from_env::<$ty>($key, $default, $sensitive));
// `LazyLock<T>` derefs to `T`; clone so callers receive an owned value, matching
// `get_from_env`'s return type (a no-op copy for the common `Copy` cases).
#[allow(clippy::clone_on_copy)]
let value = (*CACHED).clone();
value
}};
}

/// Boolean counterpart of [`get_from_env_cached`], using the same lenient parsing as
/// [`get_bool_from_env`](crate::get_bool_from_env). See that macro for the caching semantics and
/// constraints.
///
/// ```no_run
/// # use quickwit_common::get_bool_from_env_cached;
/// let cors_debug: bool = get_bool_from_env_cached!("QW_ENABLE_CORS_DEBUG", false);
/// ```
#[macro_export]
macro_rules! get_bool_from_env_cached {
($key:expr, $default:expr $(,)?) => {{
static CACHED: ::std::sync::LazyLock<bool> =
::std::sync::LazyLock::new(|| $crate::get_bool_from_env($key, $default));
*CACHED
}};
}

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

#[test]
fn test_get_from_env() {
// SAFETY: this test may not be entirely sound if not run with nextest or --test-threads=1
// as this is only a test, and it would be extremely inconvenient to run it in a different
// way, we are keeping it that way

const TEST_KEY: &str = "TEST_KEY";
assert_eq!(get_from_env(TEST_KEY, 10, false), 10);
unsafe { std::env::set_var(TEST_KEY, "15") };
assert_eq!(get_from_env(TEST_KEY, 10, false), 15);
unsafe { std::env::set_var(TEST_KEY, "1invalidnumber") };
assert_eq!(get_from_env(TEST_KEY, 10, false), 10);
}

#[test]
fn test_parse_bool_lenient() {
assert_eq!(parse_bool_lenient("true"), Some(true));
assert_eq!(parse_bool_lenient("TRUE"), Some(true));
assert_eq!(parse_bool_lenient("True"), Some(true));
assert_eq!(parse_bool_lenient("yes"), Some(true));
assert_eq!(parse_bool_lenient(" 1"), Some(true));

assert_eq!(parse_bool_lenient("false"), Some(false));
assert_eq!(parse_bool_lenient("FALSE"), Some(false));
assert_eq!(parse_bool_lenient("False"), Some(false));
assert_eq!(parse_bool_lenient("no"), Some(false));
assert_eq!(parse_bool_lenient("0 "), Some(false));

assert_eq!(parse_bool_lenient("foo"), None);
}
}
102 changes: 6 additions & 96 deletions quickwit/quickwit-common/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
#![deny(clippy::disallowed_methods)]

mod coolid;
mod env;

#[cfg(feature = "jemalloc-profiled")]
pub(crate) mod alloc_tracker;
Expand Down Expand Up @@ -51,15 +52,16 @@ pub mod type_map;
pub mod uri;

mod metrics_specific;
pub use env::{
get_bool_from_env, get_bool_from_env_opt, get_from_env, get_from_env_opt, parse_bool_lenient,
};
pub use metrics_specific::*;

mod socket_addr_legacy_hash;

use std::env;
use std::fmt::{Debug, Display};
use std::fmt::Display;
use std::future::Future;
use std::ops::{Range, RangeInclusive};
use std::str::FromStr;

pub use coolid::new_coolid;
pub use cpus::num_cpus;
Expand All @@ -68,7 +70,6 @@ pub use path_hasher::PathHasher;
pub use progress::{Progress, ProtectedZoneGuard};
pub use socket_addr_legacy_hash::SocketAddrLegacyHash;
pub use stream_utils::{BoxStream, ServiceStream};
use tracing::{error, info};

/// Returns true at compile time. This function is mostly used with serde to initialize boolean
/// fields to true.
Expand Down Expand Up @@ -112,50 +113,6 @@ pub fn split_file(split_id: impl Display) -> String {
format!("{split_id}.split")
}

fn get_from_env_opt_aux<T: Debug>(
key: &str,
parse_fn: impl FnOnce(&str) -> Option<T>,
sensitive: bool,
) -> Option<T> {
let value_str = std::env::var(key).ok()?;
let Some(value) = parse_fn(&value_str) else {
error!(value=%value_str, "failed to parse environment variable `{key}` value");
return None;
};
if sensitive {
info!("using environment variable `{key}` value");
} else {
info!(value=?value, "using environment variable `{key}` value");
}
Some(value)
}

pub fn get_from_env<T: FromStr + Debug>(key: &str, default_value: T, sensitive: bool) -> T {
if let Some(value) = get_from_env_opt(key, sensitive) {
value
} else {
info!(default_value=?default_value, "using environment variable `{key}` default value");
default_value
}
}

pub fn get_from_env_opt<T: FromStr + Debug>(key: &str, sensitive: bool) -> Option<T> {
get_from_env_opt_aux(key, |val_str| val_str.parse().ok(), sensitive)
}

pub fn get_bool_from_env_opt(key: &str) -> Option<bool> {
get_from_env_opt_aux(key, parse_bool_lenient, false)
}

pub fn get_bool_from_env(key: &str, default_value: bool) -> bool {
if let Some(flag_value) = get_bool_from_env_opt(key) {
flag_value
} else {
info!(default_value=%default_value, "using environment variable `{key}` default value");
default_value
}
}

pub fn truncate_str(text: &str, max_len: usize) -> &str {
if max_len > text.len() {
return text;
Expand Down Expand Up @@ -201,7 +158,7 @@ pub fn is_false(value: &bool) -> bool {
}

pub fn no_color() -> bool {
matches!(env::var("NO_COLOR"), Ok(value) if !value.is_empty())
matches!(std::env::var("NO_COLOR"), Ok(value) if !value.is_empty())
}

#[macro_export]
Expand Down Expand Up @@ -335,42 +292,12 @@ where
.unwrap()
}

pub fn parse_bool_lenient(bool_str: &str) -> Option<bool> {
let trimmed_bool_str = bool_str.trim();

for truthy_value in ["true", "yes", "1"] {
if trimmed_bool_str.eq_ignore_ascii_case(truthy_value) {
return Some(true);
}
}
for falsy_value in ["false", "no", "0"] {
if trimmed_bool_str.eq_ignore_ascii_case(falsy_value) {
return Some(false);
}
}
None
}

#[cfg(test)]
mod tests {
use std::io::ErrorKind;

use super::*;

#[test]
fn test_get_from_env() {
// SAFETY: this test may not be entirely sound if not run with nextest or --test-threads=1
// as this is only a test, and it would be extremely inconvenient to run it in a different
// way, we are keeping it that way

const TEST_KEY: &str = "TEST_KEY";
assert_eq!(super::get_from_env(TEST_KEY, 10, false), 10);
unsafe { std::env::set_var(TEST_KEY, "15") };
assert_eq!(super::get_from_env(TEST_KEY, 10, false), 15);
unsafe { std::env::set_var(TEST_KEY, "1invalidnumber") };
assert_eq!(super::get_from_env(TEST_KEY, 10, false), 10);
}

#[test]
fn test_truncate_str() {
assert_eq!(truncate_str("", 0), "");
Expand Down Expand Up @@ -429,21 +356,4 @@ mod tests {
assert_eq!(div_ceil_u32(1, 3), 1);
assert_eq!(div_ceil_u32(0, 3), 0);
}

#[test]
fn test_parse_bool_lenient() {
assert_eq!(parse_bool_lenient("true"), Some(true));
assert_eq!(parse_bool_lenient("TRUE"), Some(true));
assert_eq!(parse_bool_lenient("True"), Some(true));
assert_eq!(parse_bool_lenient("yes"), Some(true));
assert_eq!(parse_bool_lenient(" 1"), Some(true));

assert_eq!(parse_bool_lenient("false"), Some(false));
assert_eq!(parse_bool_lenient("FALSE"), Some(false));
assert_eq!(parse_bool_lenient("False"), Some(false));
assert_eq!(parse_bool_lenient("no"), Some(false));
assert_eq!(parse_bool_lenient("0 "), Some(false));

assert_eq!(parse_bool_lenient("foo"), None);
}
}
8 changes: 3 additions & 5 deletions quickwit/quickwit-common/src/metrics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,14 @@
// See the License for the specific language governing permissions and
// limitations under the License.

use std::sync::LazyLock;

pub use prometheus::{exponential_buckets, linear_buckets};
use quickwit_metrics::{Gauge, LazyCounter, LazyGauge, gauge, lazy_counter, lazy_gauge};

pub fn index_label(index_id: &str) -> &str {
static PER_INDEX_METRICS_ENABLED: LazyLock<bool> =
LazyLock::new(|| !crate::get_bool_from_env("QW_DISABLE_PER_INDEX_METRICS", false));
let per_index_metrics_enabled =
!crate::get_bool_from_env_cached!("QW_DISABLE_PER_INDEX_METRICS", false);

if *PER_INDEX_METRICS_ENABLED {
if per_index_metrics_enabled {
index_id
} else {
"__any__"
Expand Down
8 changes: 5 additions & 3 deletions quickwit/quickwit-common/src/rate_limited_tracing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ use coarsetime::{Duration, Instant};
pub enum ShouldLog {
/// Emit the log normally, within the rate limit.
Yes,
/// Emit the log; `N` similar messages were suppressed since the last emission.
/// Emit the log, annotated with a `num_suppressed = N` field recording how many similar
/// messages were suppressed since the last emission of this call site.
YesAfterSuppression(u32),
/// Suppressed — do not emit.
No,
Expand Down Expand Up @@ -165,8 +166,9 @@ macro_rules! rate_limited_tracing {
::tracing::$log_fn!($($args)*);
}
$crate::rate_limited_tracing::ShouldLog::YesAfterSuppression(skipped) => {
::tracing::$log_fn!("suppressed {skipped} similar log messages in the last minute");
::tracing::$log_fn!($($args)*);
// Attach the count of messages suppressed since this call site last emitted as a
// field on the emitted line, rather than as a separate preceding log line.
::tracing::$log_fn!(num_suppressed = skipped, $($args)*);
}
}
}};
Expand Down
9 changes: 2 additions & 7 deletions quickwit/quickwit-config/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ use std::sync::LazyLock;

use anyhow::{Context, bail, ensure};
use json_comments::StripComments;
use quickwit_common::get_bool_from_env;
use quickwit_common::net::is_valid_hostname;
use quickwit_common::uri::Uri;
use quickwit_proto::types::NodeIdRef;
Expand Down Expand Up @@ -89,16 +88,12 @@ pub use crate::storage_config::{

/// Returns true if the ingest API v2 is enabled.
pub fn enable_ingest_v2() -> bool {
static ENABLE_INGEST_V2: LazyLock<bool> =
LazyLock::new(|| get_bool_from_env("QW_ENABLE_INGEST_V2", true));
*ENABLE_INGEST_V2
quickwit_common::get_bool_from_env_cached!("QW_ENABLE_INGEST_V2", true)
}

/// Returns true if the ingest API v1 is disabled.
pub fn disable_ingest_v1() -> bool {
static DISABLE_INGEST_V1: LazyLock<bool> =
LazyLock::new(|| get_bool_from_env("QW_DISABLE_INGEST_V1", false));
*DISABLE_INGEST_V1
quickwit_common::get_bool_from_env_cached!("QW_DISABLE_INGEST_V1", false)
}

#[derive(utoipa::OpenApi)]
Expand Down
Loading
Loading