Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
8 changes: 5 additions & 3 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,12 @@ jobs:
run: |
sudo apt-get update
sudo apt-get install -y protobuf-compiler
- name: Set up Rust Nightly
uses: actions-rust-lang/setup-rust-toolchain@46268bd060767258de96ed93c1251119784f2ab6 # v1.16
# `make generate-api-docs` installs and pins the nightly toolchain it needs (see
# RUST_NIGHTLY_VERSION in the Makefile); this step just provides rustup/cargo.
- name: Set up Rust
uses: actions-rust-lang/setup-rust-toolchain@166cdcfd11aee3cb47222f9ddb555ce30ddb9659 # v1.17
with:
toolchain: nightly
toolchain: stable
cache: false
rustflags: ""
- name: Generate API documentation
Expand Down
4 changes: 2 additions & 2 deletions Cargo.lock

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

28 changes: 19 additions & 9 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@ MACOS_TEST_AGENT_INSTALL_DIR ?= /tmp/saluki-dda/datadog-agent
export GO_BUILD_IMAGE ?= golang:1.23-bullseye
export GO_APP_IMAGE ?= ubuntu:24.04

# Pinned nightly toolchain shared by the Miri tests and API-doc generation, both of which rely on
# nightly-only features. Keeping it in one variable ensures the two stay in lockstep; bump here to
# move both at once.
export RUST_NIGHTLY_VERSION ?= nightly-2026-07-05

# Tool configuration.
export AUTOINSTALL ?= true
export CARGO_BIN_DIR := $(shell echo "${HOME}/.cargo/bin")
Expand Down Expand Up @@ -486,10 +491,10 @@ check-all: ## Check everything
check-all: check-fmt check-clippy check-docs check-deny check-licenses check-unused-deps generate-api-docs check-features

.PHONY: generate-api-docs
generate-api-docs: check-rust-build-tools
generate-api-docs: check-rust-build-tools ensure-rust-nightly
generate-api-docs: ## Check that API documentation builds without errors
@echo "[*] Checking API documentation build..."
@RUSTDOCFLAGS="--enable-index-page -Zunstable-options" cargo +nightly doc --no-deps -Zrustdoc-map --lib
@RUSTDOCFLAGS="--enable-index-page -Zunstable-options" cargo +$(RUST_NIGHTLY_VERSION) doc --no-deps -Zrustdoc-map --lib

.PHONY: check-clippy
check-clippy: check-rust-build-tools
Expand Down Expand Up @@ -567,7 +572,7 @@ test-docs: ## Runs all doctests
test-miri: check-rust-build-tools ensure-rust-miri
test-miri: ## Runs all Miri-specific unit tests
@echo "[*] Running Miri-specific unit tests..."
cargo +nightly-2025-06-16 miri test -p stringtheory
cargo +$(RUST_NIGHTLY_VERSION) miri test -p stringtheory

.PHONY: test-loom
test-loom: check-rust-build-tools
Expand Down Expand Up @@ -725,15 +730,20 @@ provision-macos-test-env: ## Installs the pinned Datadog Agent ($(MACOS_TEST_AGE
.PHONY: test-integration-macos-ci
test-integration-macos-ci: build-panoramic build-adp-host provision-macos-test-env test-integration-macos-run ## CI entry point: builds binaries, ensures Agent + cert are provisioned, then runs the `mac`-runtime integration tests

.PHONY: ensure-rust-miri
ensure-rust-miri:
.PHONY: ensure-rust-nightly
ensure-rust-nightly:
ifeq ($(shell command -v rustup >/dev/null || echo not-found), not-found)
$(error "Rustup must be present to install nightly toolchain/Miri component: https://www.rust-lang.org/tools/install")
$(error "Rustup must be present to install the nightly toolchain: https://www.rust-lang.org/tools/install")
endif
@echo "[*] Installing/updating nightly Rust (2025-06-16) and Miri component..."
@rustup toolchain install nightly-2025-06-16 --component miri
@echo "[*] Installing/updating nightly Rust ($(RUST_NIGHTLY_VERSION))..."
@rustup toolchain install $(RUST_NIGHTLY_VERSION) --profile minimal

.PHONY: ensure-rust-miri
ensure-rust-miri: ensure-rust-nightly
@echo "[*] Installing/updating Miri component..."
@rustup component add miri --toolchain $(RUST_NIGHTLY_VERSION)
@echo "[*] Ensuring Miri is setup..."
@cargo +nightly-2025-06-16 miri setup
@cargo +$(RUST_NIGHTLY_VERSION) miri setup

##@ Antithesis

Expand Down
82 changes: 81 additions & 1 deletion lib/saluki-components/src/common/datadog/endpoints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,31 @@ static DD_URL_REGEX: LazyLock<Regex> =

pub const DEFAULT_SITE: &str = "datadoghq.com";

/// The primary endpoint URL that is constructed when both `site` and `dd_url` are at their defaults.
///
/// The Core Agent sends `dd_url` at this value even when the operator only configured `site`.
/// A `dd_url` equal to this constant carries no override intent and must not shadow `site`.
const DEFAULT_PRIMARY_ENDPOINT: &str = "https://app.datadoghq.com";

fn default_site() -> String {
DEFAULT_SITE.to_owned()
}

/// Deserializes an optional `dd_url`, treating the schema-default URL as absent.
///
/// The Core Agent always sends `dd_url` at its schema default (`https://app.datadoghq.com`) even
/// when the operator only configured `site`. Filtering here, at deserialization, ensures that a
/// value equal to the default is treated as `None`, allowing `site` to determine the endpoint. This
/// only affects the serde path; programmatic callers such as `set_dd_url` bypass serde and are
/// unaffected.
fn deserialize_dd_url<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
where
D: serde::Deserializer<'de>,
{
let val = Option::<String>::deserialize(deserializer)?;
Ok(val.filter(|url| url.as_str() != DEFAULT_PRIMARY_ENDPOINT))
}

/// Error type for invalid endpoints.
#[derive(Debug, Snafu)]
#[snafu(context(suffix(false)))]
Expand Down Expand Up @@ -147,7 +168,7 @@ pub struct EndpointConfiguration {
/// which are both useful when proxying traffic to an intermediate destination before forwarding to Datadog.
///
/// Defaults to unset.
#[serde(default, alias = "url")]
#[serde(default, alias = "url", deserialize_with = "deserialize_dd_url")]
Comment thread
jszwedko marked this conversation as resolved.
dd_url: Option<String>,

/// Enables sending data to multiple endpoints and/or with multiple API keys via dual shipping.
Expand Down Expand Up @@ -846,4 +867,63 @@ mod tests {
.expect("error calculating override API endpoint");
assert_eq!(expected_endpoint, resolved.endpoint().to_string());
}

#[test]
fn deserialize_dd_url_filters_default_value() {
// The default dd_url (what the Agent sends when operator only configured site) should
// deserialize to None so that site takes precedence.
let config_str = r#"{"api_key": "test-key", "dd_url": "https://app.datadoghq.com"}"#;
let config: EndpointConfiguration = serde_json::from_str(config_str).expect("deserialization should succeed");
assert_eq!(None, config.dd_url);
}

#[test]
fn deserialize_dd_url_preserves_explicit_override() {
// A dd_url that differs from the default should deserialize as-is.
let config_str = r#"{"api_key": "test-key", "dd_url": "https://proxy.internal.example.com:3128"}"#;
let config: EndpointConfiguration = serde_json::from_str(config_str).expect("deserialization should succeed");
assert_eq!(
Some("https://proxy.internal.example.com:3128".to_string()),
config.dd_url
);
}

#[test]
fn set_dd_url_is_not_filtered() {
// Programmatic calls to set_dd_url bypass serde and are never filtered, even if set to the default value.
// This is important for MRF and other override paths that may explicitly set the default URL.
let config_str = r#"{"api_key": "test-key", "site": "datadoghq.eu"}"#;
let mut config: EndpointConfiguration =
serde_json::from_str(config_str).expect("deserialization should succeed");
config.set_dd_url("https://app.datadoghq.com".to_string());
assert_eq!(Some("https://app.datadoghq.com".to_string()), config.dd_url);
}

#[test]
fn site_takes_precedence_when_dd_url_is_default() {
// When dd_url is at its default (sent by Agent with source="default"), site should determine the endpoint.
// This is tested through deserialization so the default-filtering occurs.
let config_str = r#"{"api_key": "test-key", "site": "datadoghq.eu", "dd_url": "https://app.datadoghq.com"}"#;
let config: EndpointConfiguration = serde_json::from_str(config_str).expect("deserialization should succeed");

let resolved = config
.build_primary_endpoint(None)
.expect("error building primary endpoint");
// The site path applies a version prefix, so assert the host resolves to the eu site rather than the
// default US1 intake.
let host = resolved.endpoint().host_str().unwrap();
assert!(host.ends_with("datadoghq.eu"), "expected eu site, got {host}");
}

#[test]
fn explicit_dd_url_overrides_site() {
// A dd_url that diverges from the default should take precedence over site.
let config_str = r#"{"api_key": "test-key", "site": "datadoghq.eu", "dd_url": "https://dogpound.io/"}"#;
let config: EndpointConfiguration = serde_json::from_str(config_str).expect("deserialization should succeed");

let resolved = config
.build_primary_endpoint(None)
.expect("error building primary endpoint");
assert_eq!("dogpound.io", resolved.endpoint().host_str().unwrap());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ where
// decrease the error count.
let _ = self
.error_count
.fetch_update(AcqRel, Relaxed, |count| Some(count.saturating_sub(factor)));
.try_update(AcqRel, Relaxed, |count| Some(count.saturating_sub(factor)));
}
None => {
debug!("Resetting error count to zero after successful response.");
Expand Down
4 changes: 2 additions & 2 deletions lib/saluki-metrics/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ impl GaugeStorage {
impl GaugeFn for GaugeStorage {
fn increment(&self, value: f64) {
self.current
.fetch_update(SeqCst, SeqCst, |v| {
.try_update(SeqCst, SeqCst, |v| {
let new = f64::from_bits(v) + value;
Some(new.to_bits())
})
Expand All @@ -54,7 +54,7 @@ impl GaugeFn for GaugeStorage {

fn decrement(&self, value: f64) {
self.current
.fetch_update(SeqCst, SeqCst, |v| {
.try_update(SeqCst, SeqCst, |v| {
let new = f64::from_bits(v) - value;
Some(new.to_bits())
})
Expand Down
Loading