From 2f17d6a38afbb568d6c0592b84cb96393a703c0d Mon Sep 17 00:00:00 2001 From: Nina Rei Date: Tue, 29 Sep 2026 09:32:19 -0400 Subject: [PATCH 1/8] feat(compat): add Azure inventory payload core --- Cargo.lock | 9 + .../Cargo.toml | 14 ++ .../src/lib.rs | 36 +++ .../src/payload.rs | 95 ++++++++ .../src/platform/azure.rs | 206 ++++++++++++++++++ .../src/platform/mod.rs | 23 ++ 6 files changed, 383 insertions(+) create mode 100644 crates/datadog-serverless-compat-inventory/Cargo.toml create mode 100644 crates/datadog-serverless-compat-inventory/src/lib.rs create mode 100644 crates/datadog-serverless-compat-inventory/src/payload.rs create mode 100644 crates/datadog-serverless-compat-inventory/src/platform/azure.rs create mode 100644 crates/datadog-serverless-compat-inventory/src/platform/mod.rs diff --git a/Cargo.lock b/Cargo.lock index f13b00ed..7aff8d49 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -677,6 +677,15 @@ dependencies = [ "zstd", ] +[[package]] +name = "datadog-serverless-compat-inventory" +version = "0.1.0" +dependencies = [ + "libdd-common", + "libdd-trace-utils", + "serde_json", +] + [[package]] name = "datadog-serverless-logging" version = "0.1.0" diff --git a/crates/datadog-serverless-compat-inventory/Cargo.toml b/crates/datadog-serverless-compat-inventory/Cargo.toml new file mode 100644 index 00000000..17bdaedb --- /dev/null +++ b/crates/datadog-serverless-compat-inventory/Cargo.toml @@ -0,0 +1,14 @@ +# Copyright 2025-Present Datadog, Inc. https://www.datadoghq.com/ +# SPDX-License-Identifier: Apache-2.0 + +[package] +name = "datadog-serverless-compat-inventory" +version = "0.1.0" +edition.workspace = true +license.workspace = true +description = "Fleet Automation inventory reporter for the Serverless Compat mini-agent" + +[dependencies] +libdd-common = { workspace = true } +libdd-trace-utils = { workspace = true } +serde_json = { version = "1.0", default-features = false, features = ["alloc"] } diff --git a/crates/datadog-serverless-compat-inventory/src/lib.rs b/crates/datadog-serverless-compat-inventory/src/lib.rs new file mode 100644 index 00000000..17ae9c5b --- /dev/null +++ b/crates/datadog-serverless-compat-inventory/src/lib.rs @@ -0,0 +1,36 @@ +// Copyright 2023-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 + +mod payload; +mod platform; + +use libdd_trace_utils::trace_utils::EnvironmentType; + +/// A serialized inventory report and the identity used for operational logs. +pub struct InventoryReport { + pub body: Vec, + pub resource_id: String, + pub workload_type: &'static str, +} + +/// Builds an inventory report for a supported serverless environment. +/// +/// Returns `None` when the environment is unsupported or its required cloud +/// identity is unavailable. The async API leaves room for platforms such as +/// GCP that may need metadata-server lookups to complete their identity. +pub async fn build_inventory_report( + env_type: &EnvironmentType, + process_id: &str, + report_reason: &str, +) -> Result, serde_json::Error> { + let Some(platform) = platform::collect(env_type).await else { + return Ok(None); + }; + + let body = payload::build(process_id, report_reason, &platform)?; + Ok(Some(InventoryReport { + body, + resource_id: platform.resource_id, + workload_type: platform.workload_type, + })) +} diff --git a/crates/datadog-serverless-compat-inventory/src/payload.rs b/crates/datadog-serverless-compat-inventory/src/payload.rs new file mode 100644 index 00000000..328305bc --- /dev/null +++ b/crates/datadog-serverless-compat-inventory/src/payload.rs @@ -0,0 +1,95 @@ +// Copyright 2023-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 + +use crate::platform::PlatformData; +use serde_json::Value; +use std::env; +use std::time::{SystemTime, UNIX_EPOCH}; + +pub(crate) fn build( + process_id: &str, + report_reason: &str, + platform: &PlatformData, +) -> Result, serde_json::Error> { + // EPRW expects nanoseconds, matching time.Now().UnixNano(). + let timestamp = SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|duration| duration.as_nanos() as i64) + .unwrap_or(0); + + let compat_version = env::var("DD_SERVERLESS_COMPAT_VERSION") + .ok() + .filter(|value| !value.is_empty()) + .unwrap_or_else(|| env!("CARGO_PKG_VERSION").to_string()); + + let mut metadata = serde_json::json!({ + "flavor": "serverless-compat", + "workload_type": platform.workload_type, + "report_reason": report_reason, + "resource_id": platform.resource_id, + "resource_name": platform.resource_name, + "serverless_compat_version": compat_version, + }); + + for (key, value) in &platform.metadata { + metadata[key] = value.clone(); + } + + for (env_key, metadata_key) in [ + ("DD_ENV", "dd_env"), + ("DD_SERVICE", "dd_service"), + ("DD_VERSION", "dd_version"), + ("DD_SITE", "dd_site"), + ("DD_SERVERLESS_COMPAT_RUNTIME", "runtime"), + ( + "DD_SERVERLESS_COMPAT_RUNTIME_VERSION", + "serverless_compat_runtime_version", + ), + ] { + if let Ok(value) = env::var(env_key) + && !value.is_empty() + { + metadata[metadata_key] = Value::String(value); + } + } + + // Hostname is intentionally absent: setting it causes EPRW to attempt a + // host_id lookup that is invalid for serverless workloads. + serde_json::to_vec(&serde_json::json!({ + "uuid": process_id, + "timestamp": timestamp, + "agent_metadata": metadata, + })) +} + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::{Map, Value}; + + fn azure_platform() -> PlatformData { + let mut metadata = Map::new(); + metadata.insert("region".into(), Value::String("eastus".into())); + PlatformData { + workload_type: "azure_function", + resource_id: "/subscriptions/sub/resourcegroups/rg/providers/microsoft.web/sites/app" + .into(), + resource_name: "app".into(), + metadata, + } + } + + #[test] + fn builds_azure_payload_shape() { + let body = build("process-id", "startup", &azure_platform()).unwrap(); + let payload: Value = serde_json::from_slice(&body).unwrap(); + + assert_eq!(payload["uuid"], "process-id"); + assert!(payload["timestamp"].as_i64().unwrap() > 0); + assert_eq!(payload["agent_metadata"]["flavor"], "serverless-compat"); + assert_eq!(payload["agent_metadata"]["workload_type"], "azure_function"); + assert_eq!(payload["agent_metadata"]["report_reason"], "startup"); + assert_eq!(payload["agent_metadata"]["region"], "eastus"); + assert!(payload.get("hostname").is_none()); + } +} diff --git a/crates/datadog-serverless-compat-inventory/src/platform/azure.rs b/crates/datadog-serverless-compat-inventory/src/platform/azure.rs new file mode 100644 index 00000000..60458790 --- /dev/null +++ b/crates/datadog-serverless-compat-inventory/src/platform/azure.rs @@ -0,0 +1,206 @@ +// Copyright 2023-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 + +use super::PlatformData; +use libdd_common::azure_app_services::{AzureMetadata, QueryEnv, UNKNOWN_VALUE}; +use serde_json::{Map, Value}; +use std::env; + +struct ProcessEnv; + +impl QueryEnv for ProcessEnv { + fn get_var(&self, name: &str) -> Option { + env::var(name) + .ok() + .map(|value| value.trim().to_string()) + .filter(|value| !value.is_empty()) + } +} + +fn known_value(value: &str) -> Option<&str> { + (value != UNKNOWN_VALUE && !value.is_empty()).then_some(value) +} + +pub(super) fn collect() -> Option { + let azure = AzureMetadata::new_function(ProcessEnv)?; + let resource_name = known_value(azure.get_site_name())?.to_string(); + let base_resource_id = known_value(azure.get_resource_id())?; + + // Non-production deployment slots share WEBSITE_SITE_NAME with the parent + // app but have their own ARM path and therefore need a distinct inventory ID. + let slot = env::var("WEBSITE_SLOT_NAME") + .ok() + .map(|value| value.trim().to_string()) + .filter(|value| !value.is_empty() && !value.eq_ignore_ascii_case("production")); + let resource_id = match slot { + Some(slot) => format!("{base_resource_id}/slots/{}", slot.to_lowercase()), + None => base_resource_id.to_string(), + }; + + let mut metadata = Map::new(); + if let Some(region) = env::var("REGION_NAME") + .ok() + .map(|value| value.trim().to_string()) + .filter(|value| !value.is_empty()) + { + metadata.insert("region".into(), Value::String(region)); + } + + for (field, value) in [ + ("azure_subscription_id", azure.get_subscription_id()), + ("azure_resource_group", azure.get_resource_group()), + // Preserve Azure's documented values instead of inventing a second + // runtime taxonomy. + ("runtime", azure.get_runtime()), + ( + "serverless_compat_runtime_version", + azure.get_runtime_version(), + ), + ] { + if let Some(value) = known_value(value) { + metadata.insert(field.into(), Value::String(value.to_string())); + } + } + + Some(PlatformData { + workload_type: "azure_function", + resource_id, + resource_name, + metadata, + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + static ENV_LOCK: std::sync::LazyLock> = + std::sync::LazyLock::new(|| std::sync::Mutex::new(())); + + unsafe fn clear_env() { + for name in [ + "DD_AZURE_RESOURCE_GROUP", + "FUNCTIONS_EXTENSION_VERSION", + "FUNCTIONS_WORKER_RUNTIME", + "FUNCTIONS_WORKER_RUNTIME_VERSION", + "REGION_NAME", + "WEBSITE_OWNER_NAME", + "WEBSITE_RESOURCE_GROUP", + "WEBSITE_SITE_NAME", + "WEBSITE_SKU", + "WEBSITE_SLOT_NAME", + ] { + unsafe { env::remove_var(name) }; + } + } + + unsafe fn set_base_env() { + unsafe { + env::set_var("FUNCTIONS_WORKER_RUNTIME", "python"); + env::set_var("WEBSITE_OWNER_NAME", "abc123+my-rg-eastuswebspace"); + env::set_var("WEBSITE_RESOURCE_GROUP", "my-rg"); + env::set_var("WEBSITE_SITE_NAME", "my-func-app"); + } + } + + #[test] + fn builds_full_identity() { + let _lock = ENV_LOCK.lock().unwrap(); + unsafe { + clear_env(); + set_base_env(); + } + + let data = collect().expect("Azure identity should be available"); + assert_eq!(data.resource_name, "my-func-app"); + assert_eq!( + data.resource_id, + "/subscriptions/abc123/resourcegroups/my-rg/providers/microsoft.web/sites/my-func-app" + ); + + unsafe { clear_env() }; + } + + #[test] + fn uses_flex_consumption_resource_group() { + let _lock = ENV_LOCK.lock().unwrap(); + unsafe { + clear_env(); + env::set_var("FUNCTIONS_WORKER_RUNTIME", "python"); + env::set_var("WEBSITE_OWNER_NAME", "abc123+flex-host"); + env::set_var("WEBSITE_SITE_NAME", "my-flex-func"); + env::set_var("WEBSITE_SKU", "FlexConsumption"); + env::set_var("DD_AZURE_RESOURCE_GROUP", "My-Flex-RG"); + } + + let data = collect().expect("Flex identity should be available"); + assert!(data.resource_id.contains("/my-flex-rg/")); + + unsafe { clear_env() }; + } + + #[test] + fn missing_name_skips_inventory() { + let _lock = ENV_LOCK.lock().unwrap(); + unsafe { + clear_env(); + env::set_var("FUNCTIONS_WORKER_RUNTIME", "python"); + env::set_var("WEBSITE_OWNER_NAME", "abc123+my-rg-eastuswebspace"); + env::set_var("WEBSITE_RESOURCE_GROUP", "my-rg"); + } + + assert!(collect().is_none()); + unsafe { clear_env() }; + } + + #[test] + fn appends_non_production_slot() { + let _lock = ENV_LOCK.lock().unwrap(); + unsafe { + clear_env(); + set_base_env(); + env::set_var("WEBSITE_SLOT_NAME", "Staging"); + } + + let data = collect().expect("slot identity should be available"); + assert!(data.resource_id.ends_with("/slots/staging")); + + unsafe { clear_env() }; + } + + #[test] + fn omits_production_slot() { + let _lock = ENV_LOCK.lock().unwrap(); + unsafe { + clear_env(); + set_base_env(); + env::set_var("WEBSITE_SLOT_NAME", "Production"); + } + + let data = collect().expect("production identity should be available"); + assert!(!data.resource_id.contains("/slots/")); + + unsafe { clear_env() }; + } + + #[test] + fn enriches_platform_fields() { + let _lock = ENV_LOCK.lock().unwrap(); + unsafe { + clear_env(); + set_base_env(); + env::set_var("FUNCTIONS_WORKER_RUNTIME", "dotnet-isolated"); + env::set_var("FUNCTIONS_WORKER_RUNTIME_VERSION", "8.0"); + env::set_var("REGION_NAME", "East US 2"); + } + + let data = collect().expect("Azure metadata should be available"); + assert_eq!(data.metadata["runtime"], "dotnet-isolated"); + assert_eq!(data.metadata["serverless_compat_runtime_version"], "8.0"); + assert_eq!(data.metadata["region"], "East US 2"); + assert_eq!(data.metadata["azure_subscription_id"], "abc123"); + assert_eq!(data.metadata["azure_resource_group"], "my-rg"); + + unsafe { clear_env() }; + } +} diff --git a/crates/datadog-serverless-compat-inventory/src/platform/mod.rs b/crates/datadog-serverless-compat-inventory/src/platform/mod.rs new file mode 100644 index 00000000..238ac184 --- /dev/null +++ b/crates/datadog-serverless-compat-inventory/src/platform/mod.rs @@ -0,0 +1,23 @@ +// Copyright 2023-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 + +mod azure; + +use libdd_trace_utils::trace_utils::EnvironmentType; +use serde_json::{Map, Value}; + +pub(crate) struct PlatformData { + pub workload_type: &'static str, + pub resource_id: String, + pub resource_name: String, + pub metadata: Map, +} + +pub(crate) async fn collect(env_type: &EnvironmentType) -> Option { + match env_type { + EnvironmentType::AzureFunction => azure::collect(), + EnvironmentType::CloudFunction + | EnvironmentType::LambdaFunction + | EnvironmentType::AzureSpringApp => None, + } +} From e93490403e129d1b0f1ae48d2af74b1cceb4aad3 Mon Sep 17 00:00:00 2001 From: Nina Rei Date: Tue, 29 Sep 2026 10:32:58 -0400 Subject: [PATCH 2/8] fix(compat): address Azure inventory review feedback --- .../build-datadog-serverless-compat.yml | 14 ++ .github/workflows/publish.yml | 1 + .../src/lib.rs | 35 ++++ .../src/payload.rs | 101 +++++++++- .../src/platform/azure.rs | 173 ++++++------------ 5 files changed, 203 insertions(+), 121 deletions(-) diff --git a/.github/workflows/build-datadog-serverless-compat.yml b/.github/workflows/build-datadog-serverless-compat.yml index 070938fe..e7e5a772 100644 --- a/.github/workflows/build-datadog-serverless-compat.yml +++ b/.github/workflows/build-datadog-serverless-compat.yml @@ -6,6 +6,9 @@ on: runner: required: true type: string + version: + required: false + type: string jobs: setup: @@ -27,6 +30,17 @@ jobs: runs-on: ${{ inputs.runner }} steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - name: Embed Serverless Compat version + shell: bash + env: + INPUT_VERSION: ${{ inputs.version }} + run: | + if [[ "$GITHUB_REF" == refs/tags/datadog-serverless-compat/v* ]]; then + VERSION="${GITHUB_REF#refs/tags/datadog-serverless-compat/v}" + else + VERSION="$INPUT_VERSION" + fi + echo "DD_SERVERLESS_COMPAT_VERSION=$VERSION" >> "$GITHUB_ENV" - name: Install Protoc Binary shell: bash run: chmod +x ./scripts/install-protoc.sh && ./scripts/install-protoc.sh $HOME diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 73954695..cad16fcc 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -28,6 +28,7 @@ jobs: uses: ./.github/workflows/build-datadog-serverless-compat.yml with: runner: ${{ matrix.runner }} + version: ${{ inputs.version }} package: runs-on: ubuntu-24.04 diff --git a/crates/datadog-serverless-compat-inventory/src/lib.rs b/crates/datadog-serverless-compat-inventory/src/lib.rs index 17ae9c5b..5f014eff 100644 --- a/crates/datadog-serverless-compat-inventory/src/lib.rs +++ b/crates/datadog-serverless-compat-inventory/src/lib.rs @@ -4,7 +4,21 @@ mod payload; mod platform; +use libdd_common::azure_app_services::QueryEnv; use libdd_trace_utils::trace_utils::EnvironmentType; +use std::env; + +#[derive(Clone, Copy)] +struct ProcessEnv; + +impl QueryEnv for ProcessEnv { + fn get_var(&self, name: &str) -> Option { + env::var(name) + .ok() + .map(|value| value.trim().to_string()) + .filter(|value| !value.is_empty()) + } +} /// A serialized inventory report and the identity used for operational logs. pub struct InventoryReport { @@ -34,3 +48,24 @@ pub async fn build_inventory_report( workload_type: platform.workload_type, })) } + +#[cfg(test)] +mod test_env { + use libdd_common::azure_app_services::QueryEnv; + use std::collections::HashMap; + + #[derive(Clone, Default)] + pub(crate) struct FakeEnv(HashMap<&'static str, &'static str>); + + impl FakeEnv { + pub(crate) fn new(values: &[(&'static str, &'static str)]) -> Self { + Self(values.iter().copied().collect()) + } + } + + impl QueryEnv for FakeEnv { + fn get_var(&self, name: &str) -> Option { + self.0.get(name).map(|value| value.to_string()) + } + } +} diff --git a/crates/datadog-serverless-compat-inventory/src/payload.rs b/crates/datadog-serverless-compat-inventory/src/payload.rs index 328305bc..30489b20 100644 --- a/crates/datadog-serverless-compat-inventory/src/payload.rs +++ b/crates/datadog-serverless-compat-inventory/src/payload.rs @@ -1,15 +1,31 @@ // Copyright 2023-Present Datadog, Inc. https://www.datadoghq.com/ // SPDX-License-Identifier: Apache-2.0 -use crate::platform::PlatformData; +use crate::{ProcessEnv, platform::PlatformData}; +use libdd_common::azure_app_services::QueryEnv; use serde_json::Value; -use std::env; use std::time::{SystemTime, UNIX_EPOCH}; pub(crate) fn build( process_id: &str, report_reason: &str, platform: &PlatformData, +) -> Result, serde_json::Error> { + build_with_env( + process_id, + report_reason, + platform, + &ProcessEnv, + option_env!("DD_SERVERLESS_COMPAT_VERSION"), + ) +} + +fn build_with_env( + process_id: &str, + report_reason: &str, + platform: &PlatformData, + env: &impl QueryEnv, + embedded_version: Option<&str>, ) -> Result, serde_json::Error> { // EPRW expects nanoseconds, matching time.Now().UnixNano(). let timestamp = SystemTime::now() @@ -17,9 +33,14 @@ pub(crate) fn build( .map(|duration| duration.as_nanos() as i64) .unwrap_or(0); - let compat_version = env::var("DD_SERVERLESS_COMPAT_VERSION") - .ok() + let compat_version = env + .get_var("DD_SERVERLESS_COMPAT_VERSION") .filter(|value| !value.is_empty()) + .or_else(|| { + embedded_version + .filter(|value| !value.is_empty()) + .map(str::to_string) + }) .unwrap_or_else(|| env!("CARGO_PKG_VERSION").to_string()); let mut metadata = serde_json::json!({ @@ -46,9 +67,7 @@ pub(crate) fn build( "serverless_compat_runtime_version", ), ] { - if let Ok(value) = env::var(env_key) - && !value.is_empty() - { + if let Some(value) = env.get_var(env_key).filter(|value| !value.is_empty()) { metadata[metadata_key] = Value::String(value); } } @@ -65,6 +84,7 @@ pub(crate) fn build( #[cfg(test)] mod tests { use super::*; + use crate::test_env::FakeEnv; use serde_json::{Map, Value}; fn azure_platform() -> PlatformData { @@ -81,7 +101,14 @@ mod tests { #[test] fn builds_azure_payload_shape() { - let body = build("process-id", "startup", &azure_platform()).unwrap(); + let body = build_with_env( + "process-id", + "startup", + &azure_platform(), + &FakeEnv::default(), + Some("1.2.3"), + ) + .unwrap(); let payload: Value = serde_json::from_slice(&body).unwrap(); assert_eq!(payload["uuid"], "process-id"); @@ -90,6 +117,64 @@ mod tests { assert_eq!(payload["agent_metadata"]["workload_type"], "azure_function"); assert_eq!(payload["agent_metadata"]["report_reason"], "startup"); assert_eq!(payload["agent_metadata"]["region"], "eastus"); + assert_eq!( + payload["agent_metadata"]["serverless_compat_version"], + "1.2.3" + ); assert!(payload.get("hostname").is_none()); } + + #[test] + fn runtime_handoff_overrides_platform_metadata() { + let env = FakeEnv::new(&[ + ("DD_SERVERLESS_COMPAT_RUNTIME", "python-custom"), + ("DD_SERVERLESS_COMPAT_RUNTIME_VERSION", "3.13.1"), + ]); + let body = + build_with_env("pid", "startup", &azure_platform(), &env, Some("1.2.3")).unwrap(); + let payload: Value = serde_json::from_slice(&body).unwrap(); + + assert_eq!(payload["agent_metadata"]["runtime"], "python-custom"); + assert_eq!( + payload["agent_metadata"]["serverless_compat_runtime_version"], + "3.13.1" + ); + } + + #[test] + fn empty_runtime_handoff_preserves_platform_metadata() { + let mut platform = azure_platform(); + platform + .metadata + .insert("runtime".into(), Value::String("dotnet".into())); + platform.metadata.insert( + "serverless_compat_runtime_version".into(), + Value::String("8.0".into()), + ); + let env = FakeEnv::new(&[ + ("DD_SERVERLESS_COMPAT_RUNTIME", ""), + ("DD_SERVERLESS_COMPAT_RUNTIME_VERSION", ""), + ]); + let body = build_with_env("pid", "startup", &platform, &env, Some("1.2.3")).unwrap(); + let payload: Value = serde_json::from_slice(&body).unwrap(); + + assert_eq!(payload["agent_metadata"]["runtime"], "dotnet"); + assert_eq!( + payload["agent_metadata"]["serverless_compat_runtime_version"], + "8.0" + ); + } + + #[test] + fn runtime_compat_version_overrides_embedded_version() { + let env = FakeEnv::new(&[("DD_SERVERLESS_COMPAT_VERSION", "2.4.6")]); + let body = + build_with_env("pid", "startup", &azure_platform(), &env, Some("1.2.3")).unwrap(); + let payload: Value = serde_json::from_slice(&body).unwrap(); + + assert_eq!( + payload["agent_metadata"]["serverless_compat_version"], + "2.4.6" + ); + } } diff --git a/crates/datadog-serverless-compat-inventory/src/platform/azure.rs b/crates/datadog-serverless-compat-inventory/src/platform/azure.rs index 60458790..771ae349 100644 --- a/crates/datadog-serverless-compat-inventory/src/platform/azure.rs +++ b/crates/datadog-serverless-compat-inventory/src/platform/azure.rs @@ -2,35 +2,27 @@ // SPDX-License-Identifier: Apache-2.0 use super::PlatformData; +use crate::ProcessEnv; use libdd_common::azure_app_services::{AzureMetadata, QueryEnv, UNKNOWN_VALUE}; use serde_json::{Map, Value}; -use std::env; - -struct ProcessEnv; - -impl QueryEnv for ProcessEnv { - fn get_var(&self, name: &str) -> Option { - env::var(name) - .ok() - .map(|value| value.trim().to_string()) - .filter(|value| !value.is_empty()) - } -} fn known_value(value: &str) -> Option<&str> { (value != UNKNOWN_VALUE && !value.is_empty()).then_some(value) } pub(super) fn collect() -> Option { - let azure = AzureMetadata::new_function(ProcessEnv)?; + collect_from(ProcessEnv) +} + +fn collect_from(env: E) -> Option { + let azure = AzureMetadata::new_function(env.clone())?; let resource_name = known_value(azure.get_site_name())?.to_string(); let base_resource_id = known_value(azure.get_resource_id())?; // Non-production deployment slots share WEBSITE_SITE_NAME with the parent // app but have their own ARM path and therefore need a distinct inventory ID. - let slot = env::var("WEBSITE_SLOT_NAME") - .ok() - .map(|value| value.trim().to_string()) + let slot = env + .get_var("WEBSITE_SLOT_NAME") .filter(|value| !value.is_empty() && !value.eq_ignore_ascii_case("production")); let resource_id = match slot { Some(slot) => format!("{base_resource_id}/slots/{}", slot.to_lowercase()), @@ -38,11 +30,7 @@ pub(super) fn collect() -> Option { }; let mut metadata = Map::new(); - if let Some(region) = env::var("REGION_NAME") - .ok() - .map(|value| value.trim().to_string()) - .filter(|value| !value.is_empty()) - { + if let Some(region) = env.get_var("REGION_NAME") { metadata.insert("region".into(), Value::String(region)); } @@ -73,134 +61,93 @@ pub(super) fn collect() -> Option { #[cfg(test)] mod tests { use super::*; - - static ENV_LOCK: std::sync::LazyLock> = - std::sync::LazyLock::new(|| std::sync::Mutex::new(())); - - unsafe fn clear_env() { - for name in [ - "DD_AZURE_RESOURCE_GROUP", - "FUNCTIONS_EXTENSION_VERSION", - "FUNCTIONS_WORKER_RUNTIME", - "FUNCTIONS_WORKER_RUNTIME_VERSION", - "REGION_NAME", - "WEBSITE_OWNER_NAME", - "WEBSITE_RESOURCE_GROUP", - "WEBSITE_SITE_NAME", - "WEBSITE_SKU", - "WEBSITE_SLOT_NAME", - ] { - unsafe { env::remove_var(name) }; - } - } - - unsafe fn set_base_env() { - unsafe { - env::set_var("FUNCTIONS_WORKER_RUNTIME", "python"); - env::set_var("WEBSITE_OWNER_NAME", "abc123+my-rg-eastuswebspace"); - env::set_var("WEBSITE_RESOURCE_GROUP", "my-rg"); - env::set_var("WEBSITE_SITE_NAME", "my-func-app"); - } + use crate::test_env::FakeEnv; + + fn base_env() -> FakeEnv { + FakeEnv::new(&[ + ("FUNCTIONS_WORKER_RUNTIME", "python"), + ("WEBSITE_OWNER_NAME", "abc123+my-rg-eastuswebspace"), + ("WEBSITE_RESOURCE_GROUP", "my-rg"), + ("WEBSITE_SITE_NAME", "my-func-app"), + ]) } #[test] fn builds_full_identity() { - let _lock = ENV_LOCK.lock().unwrap(); - unsafe { - clear_env(); - set_base_env(); - } - - let data = collect().expect("Azure identity should be available"); + let data = collect_from(base_env()).expect("Azure identity should be available"); assert_eq!(data.resource_name, "my-func-app"); assert_eq!( data.resource_id, "/subscriptions/abc123/resourcegroups/my-rg/providers/microsoft.web/sites/my-func-app" ); - - unsafe { clear_env() }; } #[test] fn uses_flex_consumption_resource_group() { - let _lock = ENV_LOCK.lock().unwrap(); - unsafe { - clear_env(); - env::set_var("FUNCTIONS_WORKER_RUNTIME", "python"); - env::set_var("WEBSITE_OWNER_NAME", "abc123+flex-host"); - env::set_var("WEBSITE_SITE_NAME", "my-flex-func"); - env::set_var("WEBSITE_SKU", "FlexConsumption"); - env::set_var("DD_AZURE_RESOURCE_GROUP", "My-Flex-RG"); - } - - let data = collect().expect("Flex identity should be available"); + let data = collect_from(FakeEnv::new(&[ + ("FUNCTIONS_WORKER_RUNTIME", "python"), + ("WEBSITE_OWNER_NAME", "abc123+flex-host"), + ("WEBSITE_SITE_NAME", "my-flex-func"), + ("WEBSITE_SKU", "FlexConsumption"), + ("DD_AZURE_RESOURCE_GROUP", "My-Flex-RG"), + ])) + .expect("Flex identity should be available"); assert!(data.resource_id.contains("/my-flex-rg/")); - - unsafe { clear_env() }; } #[test] fn missing_name_skips_inventory() { - let _lock = ENV_LOCK.lock().unwrap(); - unsafe { - clear_env(); - env::set_var("FUNCTIONS_WORKER_RUNTIME", "python"); - env::set_var("WEBSITE_OWNER_NAME", "abc123+my-rg-eastuswebspace"); - env::set_var("WEBSITE_RESOURCE_GROUP", "my-rg"); - } - - assert!(collect().is_none()); - unsafe { clear_env() }; + assert!( + collect_from(FakeEnv::new(&[ + ("FUNCTIONS_WORKER_RUNTIME", "python"), + ("WEBSITE_OWNER_NAME", "abc123+my-rg-eastuswebspace"), + ("WEBSITE_RESOURCE_GROUP", "my-rg"), + ])) + .is_none() + ); } #[test] fn appends_non_production_slot() { - let _lock = ENV_LOCK.lock().unwrap(); - unsafe { - clear_env(); - set_base_env(); - env::set_var("WEBSITE_SLOT_NAME", "Staging"); - } - - let data = collect().expect("slot identity should be available"); + let data = collect_from(FakeEnv::new(&[ + ("FUNCTIONS_WORKER_RUNTIME", "python"), + ("WEBSITE_OWNER_NAME", "abc123+my-rg-eastuswebspace"), + ("WEBSITE_RESOURCE_GROUP", "my-rg"), + ("WEBSITE_SITE_NAME", "my-func-app"), + ("WEBSITE_SLOT_NAME", "Staging"), + ])) + .expect("slot identity should be available"); assert!(data.resource_id.ends_with("/slots/staging")); - - unsafe { clear_env() }; } #[test] fn omits_production_slot() { - let _lock = ENV_LOCK.lock().unwrap(); - unsafe { - clear_env(); - set_base_env(); - env::set_var("WEBSITE_SLOT_NAME", "Production"); - } - - let data = collect().expect("production identity should be available"); + let data = collect_from(FakeEnv::new(&[ + ("FUNCTIONS_WORKER_RUNTIME", "python"), + ("WEBSITE_OWNER_NAME", "abc123+my-rg-eastuswebspace"), + ("WEBSITE_RESOURCE_GROUP", "my-rg"), + ("WEBSITE_SITE_NAME", "my-func-app"), + ("WEBSITE_SLOT_NAME", "Production"), + ])) + .expect("production identity should be available"); assert!(!data.resource_id.contains("/slots/")); - - unsafe { clear_env() }; } #[test] fn enriches_platform_fields() { - let _lock = ENV_LOCK.lock().unwrap(); - unsafe { - clear_env(); - set_base_env(); - env::set_var("FUNCTIONS_WORKER_RUNTIME", "dotnet-isolated"); - env::set_var("FUNCTIONS_WORKER_RUNTIME_VERSION", "8.0"); - env::set_var("REGION_NAME", "East US 2"); - } - - let data = collect().expect("Azure metadata should be available"); + let data = collect_from(FakeEnv::new(&[ + ("FUNCTIONS_WORKER_RUNTIME", "dotnet-isolated"), + ("FUNCTIONS_WORKER_RUNTIME_VERSION", "8.0"), + ("REGION_NAME", "East US 2"), + ("WEBSITE_OWNER_NAME", "abc123+my-rg-eastuswebspace"), + ("WEBSITE_RESOURCE_GROUP", "my-rg"), + ("WEBSITE_SITE_NAME", "my-func-app"), + ])) + .expect("Azure metadata should be available"); assert_eq!(data.metadata["runtime"], "dotnet-isolated"); assert_eq!(data.metadata["serverless_compat_runtime_version"], "8.0"); assert_eq!(data.metadata["region"], "East US 2"); assert_eq!(data.metadata["azure_subscription_id"], "abc123"); assert_eq!(data.metadata["azure_resource_group"], "my-rg"); - - unsafe { clear_env() }; } } From 444059b77e0d762638243598d7f7b73a42f7d7cc Mon Sep 17 00:00:00 2001 From: Nina Rei Date: Tue, 29 Sep 2026 15:11:47 -0400 Subject: [PATCH 3/8] fix(compat): harden Azure inventory release metadata --- .../build-datadog-serverless-compat.yml | 10 ++++++- .../release-datadog-serverless-compat.yml | 11 ++++++-- .../src/payload.rs | 27 ++++++++++--------- .../src/platform/azure.rs | 26 +++++++++++++----- 4 files changed, 53 insertions(+), 21 deletions(-) diff --git a/.github/workflows/build-datadog-serverless-compat.yml b/.github/workflows/build-datadog-serverless-compat.yml index e7e5a772..b001addb 100644 --- a/.github/workflows/build-datadog-serverless-compat.yml +++ b/.github/workflows/build-datadog-serverless-compat.yml @@ -37,9 +37,17 @@ jobs: run: | if [[ "$GITHUB_REF" == refs/tags/datadog-serverless-compat/v* ]]; then VERSION="${GITHUB_REF#refs/tags/datadog-serverless-compat/v}" - else + elif [[ -n "$INPUT_VERSION" ]]; then VERSION="$INPUT_VERSION" + else + exit 0 fi + + if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$ ]]; then + echo "Invalid Serverless Compat version: $VERSION" + exit 1 + fi + echo "DD_SERVERLESS_COMPAT_VERSION=$VERSION" >> "$GITHUB_ENV" - name: Install Protoc Binary shell: bash diff --git a/.github/workflows/release-datadog-serverless-compat.yml b/.github/workflows/release-datadog-serverless-compat.yml index ae17f253..00fbdbe3 100644 --- a/.github/workflows/release-datadog-serverless-compat.yml +++ b/.github/workflows/release-datadog-serverless-compat.yml @@ -1,6 +1,12 @@ name: Release Datadog Serverless Compat -on: workflow_dispatch +on: + workflow_dispatch: + inputs: + version: + description: "Serverless Compat release version" + required: true + type: string permissions: {} @@ -12,7 +18,8 @@ jobs: runner: [ubuntu-24.04, windows-2022] uses: ./.github/workflows/build-datadog-serverless-compat.yml with: - runner: ${{matrix.runner}} + runner: ${{ matrix.runner }} + version: ${{ inputs.version }} release: runs-on: ubuntu-24.04 needs: build diff --git a/crates/datadog-serverless-compat-inventory/src/payload.rs b/crates/datadog-serverless-compat-inventory/src/payload.rs index 30489b20..0a75ca0e 100644 --- a/crates/datadog-serverless-compat-inventory/src/payload.rs +++ b/crates/datadog-serverless-compat-inventory/src/payload.rs @@ -38,23 +38,20 @@ fn build_with_env( .filter(|value| !value.is_empty()) .or_else(|| { embedded_version + .map(str::trim) .filter(|value| !value.is_empty()) .map(str::to_string) }) .unwrap_or_else(|| env!("CARGO_PKG_VERSION").to_string()); - let mut metadata = serde_json::json!({ - "flavor": "serverless-compat", - "workload_type": platform.workload_type, - "report_reason": report_reason, - "resource_id": platform.resource_id, - "resource_name": platform.resource_name, - "serverless_compat_version": compat_version, - }); - - for (key, value) in &platform.metadata { - metadata[key] = value.clone(); - } + // Required identity fields take precedence over platform-specific metadata. + let mut metadata = Value::Object(platform.metadata.clone()); + metadata["flavor"] = Value::String("serverless-compat".into()); + metadata["workload_type"] = Value::String(platform.workload_type.into()); + metadata["report_reason"] = Value::String(report_reason.into()); + metadata["resource_id"] = Value::String(platform.resource_id.clone()); + metadata["resource_name"] = Value::String(platform.resource_name.clone()); + metadata["serverless_compat_version"] = Value::String(compat_version); for (env_key, metadata_key) in [ ("DD_ENV", "dd_env"), @@ -90,6 +87,8 @@ mod tests { fn azure_platform() -> PlatformData { let mut metadata = Map::new(); metadata.insert("region".into(), Value::String("eastus".into())); + metadata.insert("flavor".into(), Value::String("incorrect".into())); + metadata.insert("resource_id".into(), Value::String("incorrect".into())); PlatformData { workload_type: "azure_function", resource_id: "/subscriptions/sub/resourcegroups/rg/providers/microsoft.web/sites/app" @@ -114,6 +113,10 @@ mod tests { assert_eq!(payload["uuid"], "process-id"); assert!(payload["timestamp"].as_i64().unwrap() > 0); assert_eq!(payload["agent_metadata"]["flavor"], "serverless-compat"); + assert_eq!( + payload["agent_metadata"]["resource_id"], + "/subscriptions/sub/resourcegroups/rg/providers/microsoft.web/sites/app" + ); assert_eq!(payload["agent_metadata"]["workload_type"], "azure_function"); assert_eq!(payload["agent_metadata"]["report_reason"], "startup"); assert_eq!(payload["agent_metadata"]["region"], "eastus"); diff --git a/crates/datadog-serverless-compat-inventory/src/platform/azure.rs b/crates/datadog-serverless-compat-inventory/src/platform/azure.rs index 771ae349..5f2fdb90 100644 --- a/crates/datadog-serverless-compat-inventory/src/platform/azure.rs +++ b/crates/datadog-serverless-compat-inventory/src/platform/azure.rs @@ -96,15 +96,29 @@ mod tests { } #[test] - fn missing_name_skips_inventory() { - assert!( - collect_from(FakeEnv::new(&[ + fn missing_required_identity_skips_inventory() { + let incomplete_environments = [ + FakeEnv::new(&[ ("FUNCTIONS_WORKER_RUNTIME", "python"), ("WEBSITE_OWNER_NAME", "abc123+my-rg-eastuswebspace"), ("WEBSITE_RESOURCE_GROUP", "my-rg"), - ])) - .is_none() - ); + ]), + FakeEnv::new(&[ + ("FUNCTIONS_WORKER_RUNTIME", "python"), + ("WEBSITE_RESOURCE_GROUP", "my-rg"), + ("WEBSITE_SITE_NAME", "my-func-app"), + ]), + FakeEnv::new(&[ + ("FUNCTIONS_WORKER_RUNTIME", "python"), + ("WEBSITE_OWNER_NAME", "abc123+flex-host"), + ("WEBSITE_SITE_NAME", "my-flex-func"), + ("WEBSITE_SKU", "FlexConsumption"), + ]), + ]; + + for environment in incomplete_environments { + assert!(collect_from(environment).is_none()); + } } #[test] From c6e5612b71023c2fb228ca23ffd5b3890818e385 Mon Sep 17 00:00:00 2001 From: Nina Rei Date: Mon, 5 Oct 2026 16:08:54 -0400 Subject: [PATCH 4/8] fix(compat): address remaining Azure inventory feedback --- .../build-datadog-serverless-compat.yml | 22 ------------- .github/workflows/publish.yml | 1 - .../release-datadog-serverless-compat.yml | 11 ++----- Cargo.lock | 18 +++++------ .../Cargo.toml | 6 ++-- .../src/lib.rs | 2 +- .../src/payload.rs | 28 ++++++++++++++--- .../src/platform/azure.rs | 31 +++++++++++-------- .../src/platform/mod.rs | 2 +- 9 files changed, 58 insertions(+), 63 deletions(-) diff --git a/.github/workflows/build-datadog-serverless-compat.yml b/.github/workflows/build-datadog-serverless-compat.yml index b001addb..070938fe 100644 --- a/.github/workflows/build-datadog-serverless-compat.yml +++ b/.github/workflows/build-datadog-serverless-compat.yml @@ -6,9 +6,6 @@ on: runner: required: true type: string - version: - required: false - type: string jobs: setup: @@ -30,25 +27,6 @@ jobs: runs-on: ${{ inputs.runner }} steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - name: Embed Serverless Compat version - shell: bash - env: - INPUT_VERSION: ${{ inputs.version }} - run: | - if [[ "$GITHUB_REF" == refs/tags/datadog-serverless-compat/v* ]]; then - VERSION="${GITHUB_REF#refs/tags/datadog-serverless-compat/v}" - elif [[ -n "$INPUT_VERSION" ]]; then - VERSION="$INPUT_VERSION" - else - exit 0 - fi - - if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$ ]]; then - echo "Invalid Serverless Compat version: $VERSION" - exit 1 - fi - - echo "DD_SERVERLESS_COMPAT_VERSION=$VERSION" >> "$GITHUB_ENV" - name: Install Protoc Binary shell: bash run: chmod +x ./scripts/install-protoc.sh && ./scripts/install-protoc.sh $HOME diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index cad16fcc..73954695 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -28,7 +28,6 @@ jobs: uses: ./.github/workflows/build-datadog-serverless-compat.yml with: runner: ${{ matrix.runner }} - version: ${{ inputs.version }} package: runs-on: ubuntu-24.04 diff --git a/.github/workflows/release-datadog-serverless-compat.yml b/.github/workflows/release-datadog-serverless-compat.yml index 00fbdbe3..ae17f253 100644 --- a/.github/workflows/release-datadog-serverless-compat.yml +++ b/.github/workflows/release-datadog-serverless-compat.yml @@ -1,12 +1,6 @@ name: Release Datadog Serverless Compat -on: - workflow_dispatch: - inputs: - version: - description: "Serverless Compat release version" - required: true - type: string +on: workflow_dispatch permissions: {} @@ -18,8 +12,7 @@ jobs: runner: [ubuntu-24.04, windows-2022] uses: ./.github/workflows/build-datadog-serverless-compat.yml with: - runner: ${{ matrix.runner }} - version: ${{ inputs.version }} + runner: ${{matrix.runner}} release: runs-on: ubuntu-24.04 needs: build diff --git a/Cargo.lock b/Cargo.lock index 7aff8d49..4f748dc1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -677,15 +677,6 @@ dependencies = [ "zstd", ] -[[package]] -name = "datadog-serverless-compat-inventory" -version = "0.1.0" -dependencies = [ - "libdd-common", - "libdd-trace-utils", - "serde_json", -] - [[package]] name = "datadog-serverless-logging" version = "0.1.0" @@ -1588,6 +1579,15 @@ version = "0.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8fae54786f62fb2918dcfae3d568594e50eb9b5c25bf04371af6fe7516452fb" +[[package]] +name = "inventory" +version = "0.1.0" +dependencies = [ + "libdd-common", + "libdd-trace-utils", + "serde_json", +] + [[package]] name = "ipnet" version = "2.12.0" diff --git a/crates/datadog-serverless-compat-inventory/Cargo.toml b/crates/datadog-serverless-compat-inventory/Cargo.toml index 17bdaedb..f72ce27f 100644 --- a/crates/datadog-serverless-compat-inventory/Cargo.toml +++ b/crates/datadog-serverless-compat-inventory/Cargo.toml @@ -1,12 +1,12 @@ -# Copyright 2025-Present Datadog, Inc. https://www.datadoghq.com/ +# Copyright 2026-Present Datadog, Inc. https://www.datadoghq.com/ # SPDX-License-Identifier: Apache-2.0 [package] -name = "datadog-serverless-compat-inventory" +name = "inventory" version = "0.1.0" edition.workspace = true license.workspace = true -description = "Fleet Automation inventory reporter for the Serverless Compat mini-agent" +description = "Fleet Automation inventory reporter for the Serverless Compat" [dependencies] libdd-common = { workspace = true } diff --git a/crates/datadog-serverless-compat-inventory/src/lib.rs b/crates/datadog-serverless-compat-inventory/src/lib.rs index 5f014eff..377653e3 100644 --- a/crates/datadog-serverless-compat-inventory/src/lib.rs +++ b/crates/datadog-serverless-compat-inventory/src/lib.rs @@ -1,4 +1,4 @@ -// Copyright 2023-Present Datadog, Inc. https://www.datadoghq.com/ +// Copyright 2026-Present Datadog, Inc. https://www.datadoghq.com/ // SPDX-License-Identifier: Apache-2.0 mod payload; diff --git a/crates/datadog-serverless-compat-inventory/src/payload.rs b/crates/datadog-serverless-compat-inventory/src/payload.rs index 0a75ca0e..3ce23979 100644 --- a/crates/datadog-serverless-compat-inventory/src/payload.rs +++ b/crates/datadog-serverless-compat-inventory/src/payload.rs @@ -1,4 +1,4 @@ -// Copyright 2023-Present Datadog, Inc. https://www.datadoghq.com/ +// Copyright 2026-Present Datadog, Inc. https://www.datadoghq.com/ // SPDX-License-Identifier: Apache-2.0 use crate::{ProcessEnv, platform::PlatformData}; @@ -41,8 +41,7 @@ fn build_with_env( .map(str::trim) .filter(|value| !value.is_empty()) .map(str::to_string) - }) - .unwrap_or_else(|| env!("CARGO_PKG_VERSION").to_string()); + }); // Required identity fields take precedence over platform-specific metadata. let mut metadata = Value::Object(platform.metadata.clone()); @@ -51,7 +50,9 @@ fn build_with_env( metadata["report_reason"] = Value::String(report_reason.into()); metadata["resource_id"] = Value::String(platform.resource_id.clone()); metadata["resource_name"] = Value::String(platform.resource_name.clone()); - metadata["serverless_compat_version"] = Value::String(compat_version); + if let Some(compat_version) = compat_version { + metadata["serverless_compat_version"] = Value::String(compat_version); + } for (env_key, metadata_key) in [ ("DD_ENV", "dd_env"), @@ -180,4 +181,23 @@ mod tests { "2.4.6" ); } + + #[test] + fn omits_unknown_compat_version() { + let body = build_with_env( + "pid", + "startup", + &azure_platform(), + &FakeEnv::default(), + None, + ) + .unwrap(); + let payload: Value = serde_json::from_slice(&body).unwrap(); + + assert!( + payload["agent_metadata"] + .get("serverless_compat_version") + .is_none() + ); + } } diff --git a/crates/datadog-serverless-compat-inventory/src/platform/azure.rs b/crates/datadog-serverless-compat-inventory/src/platform/azure.rs index 5f2fdb90..c5954452 100644 --- a/crates/datadog-serverless-compat-inventory/src/platform/azure.rs +++ b/crates/datadog-serverless-compat-inventory/src/platform/azure.rs @@ -1,4 +1,4 @@ -// Copyright 2023-Present Datadog, Inc. https://www.datadoghq.com/ +// Copyright 2026-Present Datadog, Inc. https://www.datadoghq.com/ // SPDX-License-Identifier: Apache-2.0 use super::PlatformData; @@ -14,32 +14,37 @@ pub(super) fn collect() -> Option { collect_from(ProcessEnv) } -fn collect_from(env: E) -> Option { - let azure = AzureMetadata::new_function(env.clone())?; - let resource_name = known_value(azure.get_site_name())?.to_string(); - let base_resource_id = known_value(azure.get_resource_id())?; - - // Non-production deployment slots share WEBSITE_SITE_NAME with the parent - // app but have their own ARM path and therefore need a distinct inventory ID. - let slot = env +fn collect_from(env: E) -> Option { + // These values are inventory-specific enrichments that are not exposed by + // the published libdd-common 5.2 API used by this repository. + let region = env + .get_var("REGION_NAME") + .map(|value| value.trim().to_string()) + .filter(|value| !value.is_empty()); + let deployment_slot = env .get_var("WEBSITE_SLOT_NAME") + .map(|value| value.trim().to_string()) .filter(|value| !value.is_empty() && !value.eq_ignore_ascii_case("production")); - let resource_id = match slot { + + let azure = AzureMetadata::new_function(env)?; + let resource_name = known_value(azure.get_site_name())?.to_string(); + let base_resource_id = known_value(azure.get_resource_id())?; + let resource_id = match deployment_slot { Some(slot) => format!("{base_resource_id}/slots/{}", slot.to_lowercase()), None => base_resource_id.to_string(), }; let mut metadata = Map::new(); - if let Some(region) = env.get_var("REGION_NAME") { + if let Some(region) = region { metadata.insert("region".into(), Value::String(region)); } for (field, value) in [ ("azure_subscription_id", azure.get_subscription_id()), ("azure_resource_group", azure.get_resource_group()), - // Preserve Azure's documented values instead of inventing a second - // runtime taxonomy. ("runtime", azure.get_runtime()), + // This established downstream field name contains the application + // runtime version (for example Python 3.13), not the Compat package. ( "serverless_compat_runtime_version", azure.get_runtime_version(), diff --git a/crates/datadog-serverless-compat-inventory/src/platform/mod.rs b/crates/datadog-serverless-compat-inventory/src/platform/mod.rs index 238ac184..f424e5c7 100644 --- a/crates/datadog-serverless-compat-inventory/src/platform/mod.rs +++ b/crates/datadog-serverless-compat-inventory/src/platform/mod.rs @@ -1,4 +1,4 @@ -// Copyright 2023-Present Datadog, Inc. https://www.datadoghq.com/ +// Copyright 2026-Present Datadog, Inc. https://www.datadoghq.com/ // SPDX-License-Identifier: Apache-2.0 mod azure; From 9c97a581abe3648a664013d2b53c3dd33ff6f749 Mon Sep 17 00:00:00 2001 From: Nina Rei Date: Tue, 6 Oct 2026 19:24:54 -0400 Subject: [PATCH 5/8] fix(compat): address Azure inventory feedback --- .../Cargo.toml | 0 .../src/lib.rs | 0 .../src/payload.rs | 79 ++++++------------- .../src/platform/azure.rs | 39 +++++++-- .../src/platform/mod.rs | 0 5 files changed, 56 insertions(+), 62 deletions(-) rename crates/{datadog-serverless-compat-inventory => inventory}/Cargo.toml (100%) rename crates/{datadog-serverless-compat-inventory => inventory}/src/lib.rs (100%) rename crates/{datadog-serverless-compat-inventory => inventory}/src/payload.rs (71%) rename crates/{datadog-serverless-compat-inventory => inventory}/src/platform/azure.rs (83%) rename crates/{datadog-serverless-compat-inventory => inventory}/src/platform/mod.rs (100%) diff --git a/crates/datadog-serverless-compat-inventory/Cargo.toml b/crates/inventory/Cargo.toml similarity index 100% rename from crates/datadog-serverless-compat-inventory/Cargo.toml rename to crates/inventory/Cargo.toml diff --git a/crates/datadog-serverless-compat-inventory/src/lib.rs b/crates/inventory/src/lib.rs similarity index 100% rename from crates/datadog-serverless-compat-inventory/src/lib.rs rename to crates/inventory/src/lib.rs diff --git a/crates/datadog-serverless-compat-inventory/src/payload.rs b/crates/inventory/src/payload.rs similarity index 71% rename from crates/datadog-serverless-compat-inventory/src/payload.rs rename to crates/inventory/src/payload.rs index 3ce23979..24775c61 100644 --- a/crates/datadog-serverless-compat-inventory/src/payload.rs +++ b/crates/inventory/src/payload.rs @@ -35,13 +35,10 @@ fn build_with_env( let compat_version = env .get_var("DD_SERVERLESS_COMPAT_VERSION") - .filter(|value| !value.is_empty()) - .or_else(|| { - embedded_version - .map(str::trim) - .filter(|value| !value.is_empty()) - .map(str::to_string) - }); + .filter(|value| !value.is_empty()); + let compat_binary_version = embedded_version + .map(str::trim) + .filter(|value| !value.is_empty()); // Required identity fields take precedence over platform-specific metadata. let mut metadata = Value::Object(platform.metadata.clone()); @@ -53,17 +50,16 @@ fn build_with_env( if let Some(compat_version) = compat_version { metadata["serverless_compat_version"] = Value::String(compat_version); } + if let Some(compat_binary_version) = compat_binary_version { + metadata["serverless_compat_binary_version"] = + Value::String(compat_binary_version.to_string()); + } for (env_key, metadata_key) in [ ("DD_ENV", "dd_env"), ("DD_SERVICE", "dd_service"), ("DD_VERSION", "dd_version"), ("DD_SITE", "dd_site"), - ("DD_SERVERLESS_COMPAT_RUNTIME", "runtime"), - ( - "DD_SERVERLESS_COMPAT_RUNTIME_VERSION", - "serverless_compat_runtime_version", - ), ] { if let Some(value) = env.get_var(env_key).filter(|value| !value.is_empty()) { metadata[metadata_key] = Value::String(value); @@ -122,55 +118,19 @@ mod tests { assert_eq!(payload["agent_metadata"]["report_reason"], "startup"); assert_eq!(payload["agent_metadata"]["region"], "eastus"); assert_eq!( - payload["agent_metadata"]["serverless_compat_version"], + payload["agent_metadata"]["serverless_compat_binary_version"], "1.2.3" ); - assert!(payload.get("hostname").is_none()); - } - - #[test] - fn runtime_handoff_overrides_platform_metadata() { - let env = FakeEnv::new(&[ - ("DD_SERVERLESS_COMPAT_RUNTIME", "python-custom"), - ("DD_SERVERLESS_COMPAT_RUNTIME_VERSION", "3.13.1"), - ]); - let body = - build_with_env("pid", "startup", &azure_platform(), &env, Some("1.2.3")).unwrap(); - let payload: Value = serde_json::from_slice(&body).unwrap(); - - assert_eq!(payload["agent_metadata"]["runtime"], "python-custom"); - assert_eq!( - payload["agent_metadata"]["serverless_compat_runtime_version"], - "3.13.1" - ); - } - - #[test] - fn empty_runtime_handoff_preserves_platform_metadata() { - let mut platform = azure_platform(); - platform - .metadata - .insert("runtime".into(), Value::String("dotnet".into())); - platform.metadata.insert( - "serverless_compat_runtime_version".into(), - Value::String("8.0".into()), - ); - let env = FakeEnv::new(&[ - ("DD_SERVERLESS_COMPAT_RUNTIME", ""), - ("DD_SERVERLESS_COMPAT_RUNTIME_VERSION", ""), - ]); - let body = build_with_env("pid", "startup", &platform, &env, Some("1.2.3")).unwrap(); - let payload: Value = serde_json::from_slice(&body).unwrap(); - - assert_eq!(payload["agent_metadata"]["runtime"], "dotnet"); - assert_eq!( - payload["agent_metadata"]["serverless_compat_runtime_version"], - "8.0" + assert!( + payload["agent_metadata"] + .get("serverless_compat_version") + .is_none() ); + assert!(payload.get("hostname").is_none()); } #[test] - fn runtime_compat_version_overrides_embedded_version() { + fn reports_runtime_and_binary_versions_separately() { let env = FakeEnv::new(&[("DD_SERVERLESS_COMPAT_VERSION", "2.4.6")]); let body = build_with_env("pid", "startup", &azure_platform(), &env, Some("1.2.3")).unwrap(); @@ -180,6 +140,10 @@ mod tests { payload["agent_metadata"]["serverless_compat_version"], "2.4.6" ); + assert_eq!( + payload["agent_metadata"]["serverless_compat_binary_version"], + "1.2.3" + ); } #[test] @@ -199,5 +163,10 @@ mod tests { .get("serverless_compat_version") .is_none() ); + assert!( + payload["agent_metadata"] + .get("serverless_compat_binary_version") + .is_none() + ); } } diff --git a/crates/datadog-serverless-compat-inventory/src/platform/azure.rs b/crates/inventory/src/platform/azure.rs similarity index 83% rename from crates/datadog-serverless-compat-inventory/src/platform/azure.rs rename to crates/inventory/src/platform/azure.rs index c5954452..bba51b23 100644 --- a/crates/datadog-serverless-compat-inventory/src/platform/azure.rs +++ b/crates/inventory/src/platform/azure.rs @@ -10,6 +10,12 @@ fn known_value(value: &str) -> Option<&str> { (value != UNKNOWN_VALUE && !value.is_empty()).then_some(value) } +fn runtime_version(value: &str) -> Option<&str> { + known_value(value) + .map(|value| value.trim_start_matches('~')) + .filter(|value| !value.is_empty()) +} + pub(super) fn collect() -> Option { collect_from(ProcessEnv) } @@ -43,17 +49,19 @@ fn collect_from(env: E) -> Option { ("azure_subscription_id", azure.get_subscription_id()), ("azure_resource_group", azure.get_resource_group()), ("runtime", azure.get_runtime()), - // This established downstream field name contains the application - // runtime version (for example Python 3.13), not the Compat package. - ( - "serverless_compat_runtime_version", - azure.get_runtime_version(), - ), ] { if let Some(value) = known_value(value) { metadata.insert(field.into(), Value::String(value.to_string())); } } + // Azure may prefix runtime versions with `~` to express a version range. + // Inventory stores the canonical version value without that marker. + if let Some(value) = runtime_version(azure.get_runtime_version()) { + metadata.insert( + "serverless_compat_runtime_version".into(), + Value::String(value.to_string()), + ); + } Some(PlatformData { workload_type: "azure_function", @@ -156,7 +164,7 @@ mod tests { fn enriches_platform_fields() { let data = collect_from(FakeEnv::new(&[ ("FUNCTIONS_WORKER_RUNTIME", "dotnet-isolated"), - ("FUNCTIONS_WORKER_RUNTIME_VERSION", "8.0"), + ("FUNCTIONS_WORKER_RUNTIME_VERSION", "~8.0"), ("REGION_NAME", "East US 2"), ("WEBSITE_OWNER_NAME", "abc123+my-rg-eastuswebspace"), ("WEBSITE_RESOURCE_GROUP", "my-rg"), @@ -169,4 +177,21 @@ mod tests { assert_eq!(data.metadata["azure_subscription_id"], "abc123"); assert_eq!(data.metadata["azure_resource_group"], "my-rg"); } + + #[test] + fn omits_unknown_runtime_version() { + let data = collect_from(FakeEnv::new(&[ + ("FUNCTIONS_WORKER_RUNTIME", "dotnet"), + ("FUNCTIONS_WORKER_RUNTIME_VERSION", "unknown"), + ("WEBSITE_OWNER_NAME", "abc123+my-rg-eastuswebspace"), + ("WEBSITE_RESOURCE_GROUP", "my-rg"), + ("WEBSITE_SITE_NAME", "my-func-app"), + ])) + .expect("Azure metadata should be available"); + assert!( + data.metadata + .get("serverless_compat_runtime_version") + .is_none() + ); + } } diff --git a/crates/datadog-serverless-compat-inventory/src/platform/mod.rs b/crates/inventory/src/platform/mod.rs similarity index 100% rename from crates/datadog-serverless-compat-inventory/src/platform/mod.rs rename to crates/inventory/src/platform/mod.rs From 6f20b7543f4bd5243cffbd54c2574c4b3d9ddefb Mon Sep 17 00:00:00 2001 From: Nina Rei Date: Wed, 7 Oct 2026 22:37:55 -0400 Subject: [PATCH 6/8] fix(compat): clarify Azure runtime inventory fields --- crates/inventory/src/payload.rs | 12 ++++++---- crates/inventory/src/platform/azure.rs | 32 +++++++++++++------------- 2 files changed, 23 insertions(+), 21 deletions(-) diff --git a/crates/inventory/src/payload.rs b/crates/inventory/src/payload.rs index 24775c61..a8e6f423 100644 --- a/crates/inventory/src/payload.rs +++ b/crates/inventory/src/payload.rs @@ -84,6 +84,7 @@ mod tests { fn azure_platform() -> PlatformData { let mut metadata = Map::new(); metadata.insert("region".into(), Value::String("eastus".into())); + metadata.insert("runtime".into(), Value::String("python3.13".into())); metadata.insert("flavor".into(), Value::String("incorrect".into())); metadata.insert("resource_id".into(), Value::String("incorrect".into())); PlatformData { @@ -130,20 +131,21 @@ mod tests { } #[test] - fn reports_runtime_and_binary_versions_separately() { - let env = FakeEnv::new(&[("DD_SERVERLESS_COMPAT_VERSION", "2.4.6")]); + fn reports_runtime_compat_and_binary_versions_separately() { + let env = FakeEnv::new(&[("DD_SERVERLESS_COMPAT_VERSION", "0.18.0")]); let body = - build_with_env("pid", "startup", &azure_platform(), &env, Some("1.2.3")).unwrap(); + build_with_env("pid", "startup", &azure_platform(), &env, Some("0.28.0")).unwrap(); let payload: Value = serde_json::from_slice(&body).unwrap(); assert_eq!( payload["agent_metadata"]["serverless_compat_version"], - "2.4.6" + "0.18.0" ); assert_eq!( payload["agent_metadata"]["serverless_compat_binary_version"], - "1.2.3" + "0.28.0" ); + assert_eq!(payload["agent_metadata"]["runtime"], "python3.13"); } #[test] diff --git a/crates/inventory/src/platform/azure.rs b/crates/inventory/src/platform/azure.rs index bba51b23..c5318bc4 100644 --- a/crates/inventory/src/platform/azure.rs +++ b/crates/inventory/src/platform/azure.rs @@ -16,6 +16,14 @@ fn runtime_version(value: &str) -> Option<&str> { .filter(|value| !value.is_empty()) } +fn runtime_with_version(runtime: &str, version: &str) -> Option { + let runtime = known_value(runtime)?; + Some(match runtime_version(version) { + Some(version) => format!("{runtime}{version}"), + None => runtime.to_string(), + }) +} + pub(super) fn collect() -> Option { collect_from(ProcessEnv) } @@ -48,19 +56,14 @@ fn collect_from(env: E) -> Option { for (field, value) in [ ("azure_subscription_id", azure.get_subscription_id()), ("azure_resource_group", azure.get_resource_group()), - ("runtime", azure.get_runtime()), + ("os", azure.get_operating_system()), ] { if let Some(value) = known_value(value) { metadata.insert(field.into(), Value::String(value.to_string())); } } - // Azure may prefix runtime versions with `~` to express a version range. - // Inventory stores the canonical version value without that marker. - if let Some(value) = runtime_version(azure.get_runtime_version()) { - metadata.insert( - "serverless_compat_runtime_version".into(), - Value::String(value.to_string()), - ); + if let Some(runtime) = runtime_with_version(azure.get_runtime(), azure.get_runtime_version()) { + metadata.insert("runtime".into(), Value::String(runtime)); } Some(PlatformData { @@ -165,21 +168,22 @@ mod tests { let data = collect_from(FakeEnv::new(&[ ("FUNCTIONS_WORKER_RUNTIME", "dotnet-isolated"), ("FUNCTIONS_WORKER_RUNTIME_VERSION", "~8.0"), + ("WEBSITE_OS", "windows"), ("REGION_NAME", "East US 2"), ("WEBSITE_OWNER_NAME", "abc123+my-rg-eastuswebspace"), ("WEBSITE_RESOURCE_GROUP", "my-rg"), ("WEBSITE_SITE_NAME", "my-func-app"), ])) .expect("Azure metadata should be available"); - assert_eq!(data.metadata["runtime"], "dotnet-isolated"); - assert_eq!(data.metadata["serverless_compat_runtime_version"], "8.0"); + assert_eq!(data.metadata["runtime"], "dotnet-isolated8.0"); + assert_eq!(data.metadata["os"], "windows"); assert_eq!(data.metadata["region"], "East US 2"); assert_eq!(data.metadata["azure_subscription_id"], "abc123"); assert_eq!(data.metadata["azure_resource_group"], "my-rg"); } #[test] - fn omits_unknown_runtime_version() { + fn uses_runtime_without_unknown_version() { let data = collect_from(FakeEnv::new(&[ ("FUNCTIONS_WORKER_RUNTIME", "dotnet"), ("FUNCTIONS_WORKER_RUNTIME_VERSION", "unknown"), @@ -188,10 +192,6 @@ mod tests { ("WEBSITE_SITE_NAME", "my-func-app"), ])) .expect("Azure metadata should be available"); - assert!( - data.metadata - .get("serverless_compat_runtime_version") - .is_none() - ); + assert_eq!(data.metadata["runtime"], "dotnet"); } } From 0f430eae91c628e22d26888c1498a5fd6bd4e89a Mon Sep 17 00:00:00 2001 From: Nina Rei Date: Wed, 14 Oct 2026 14:58:23 -0400 Subject: [PATCH 7/8] fix(inventory): clarify compat runtime package version --- crates/inventory/src/payload.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/crates/inventory/src/payload.rs b/crates/inventory/src/payload.rs index a8e6f423..b6ffc851 100644 --- a/crates/inventory/src/payload.rs +++ b/crates/inventory/src/payload.rs @@ -33,7 +33,7 @@ fn build_with_env( .map(|duration| duration.as_nanos() as i64) .unwrap_or(0); - let compat_version = env + let compat_runtime_version = env .get_var("DD_SERVERLESS_COMPAT_VERSION") .filter(|value| !value.is_empty()); let compat_binary_version = embedded_version @@ -47,8 +47,8 @@ fn build_with_env( metadata["report_reason"] = Value::String(report_reason.into()); metadata["resource_id"] = Value::String(platform.resource_id.clone()); metadata["resource_name"] = Value::String(platform.resource_name.clone()); - if let Some(compat_version) = compat_version { - metadata["serverless_compat_version"] = Value::String(compat_version); + if let Some(compat_runtime_version) = compat_runtime_version { + metadata["serverless_compat_runtime_version"] = Value::String(compat_runtime_version); } if let Some(compat_binary_version) = compat_binary_version { metadata["serverless_compat_binary_version"] = @@ -124,7 +124,7 @@ mod tests { ); assert!( payload["agent_metadata"] - .get("serverless_compat_version") + .get("serverless_compat_runtime_version") .is_none() ); assert!(payload.get("hostname").is_none()); @@ -138,7 +138,7 @@ mod tests { let payload: Value = serde_json::from_slice(&body).unwrap(); assert_eq!( - payload["agent_metadata"]["serverless_compat_version"], + payload["agent_metadata"]["serverless_compat_runtime_version"], "0.18.0" ); assert_eq!( @@ -162,7 +162,7 @@ mod tests { assert!( payload["agent_metadata"] - .get("serverless_compat_version") + .get("serverless_compat_runtime_version") .is_none() ); assert!( From 6c46ed089876b07b3f2611e47ef1ff310b00e4f0 Mon Sep 17 00:00:00 2001 From: Nina Rei Date: Fri, 16 Oct 2026 16:03:58 -0400 Subject: [PATCH 8/8] fix(inventory): distinguish embedded compat binary version --- crates/inventory/src/payload.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/inventory/src/payload.rs b/crates/inventory/src/payload.rs index b6ffc851..0ed0a20a 100644 --- a/crates/inventory/src/payload.rs +++ b/crates/inventory/src/payload.rs @@ -16,7 +16,7 @@ pub(crate) fn build( report_reason, platform, &ProcessEnv, - option_env!("DD_SERVERLESS_COMPAT_VERSION"), + option_env!("DD_SERVERLESS_COMPAT_BINARY_VERSION"), ) }