Skip to content
Open
9 changes: 9 additions & 0 deletions Cargo.lock

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

14 changes: 14 additions & 0 deletions crates/inventory/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Copyright 2026-Present Datadog, Inc. https://www.datadoghq.com/
# SPDX-License-Identifier: Apache-2.0

[package]
name = "inventory"
version = "0.1.0"
edition.workspace = true
license.workspace = true
description = "Fleet Automation inventory reporter for the Serverless Compat"

[dependencies]
libdd-common = { workspace = true }
libdd-trace-utils = { workspace = true }
serde_json = { version = "1.0", default-features = false, features = ["alloc"] }
71 changes: 71 additions & 0 deletions crates/inventory/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// Copyright 2026-Present Datadog, Inc. https://www.datadoghq.com/
// SPDX-License-Identifier: Apache-2.0

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<String> {
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 {
pub body: Vec<u8>,
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<Option<InventoryReport>, 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,
}))
}

#[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<String> {
self.0.get(name).map(|value| value.to_string())
}
}
}
174 changes: 174 additions & 0 deletions crates/inventory/src/payload.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
// Copyright 2026-Present Datadog, Inc. https://www.datadoghq.com/
// SPDX-License-Identifier: Apache-2.0

use crate::{ProcessEnv, platform::PlatformData};
use libdd_common::azure_app_services::QueryEnv;
use serde_json::Value;
use std::time::{SystemTime, UNIX_EPOCH};

pub(crate) fn build(
process_id: &str,
report_reason: &str,
platform: &PlatformData,
) -> Result<Vec<u8>, serde_json::Error> {
build_with_env(
process_id,
report_reason,
platform,
&ProcessEnv,
option_env!("DD_SERVERLESS_COMPAT_VERSION"),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

See #161 (comment)

I suggest renaming this to DD_SERVERLESS_COMPAT_BINARY_VERSION to avoid confusion with the environment variable that's used to store the .NET, Java, Node.js, Python, Go packages.

)
}

fn build_with_env(
process_id: &str,
report_reason: &str,
platform: &PlatformData,
env: &impl QueryEnv,
embedded_version: Option<&str>,
) -> Result<Vec<u8>, 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_runtime_version = env
.get_var("DD_SERVERLESS_COMPAT_VERSION")
.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());
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());
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"] =
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"),
] {
if let Some(value) = env.get_var(env_key).filter(|value| !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 crate::test_env::FakeEnv;
use serde_json::{Map, Value};

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 {
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_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");
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");
assert_eq!(
payload["agent_metadata"]["serverless_compat_binary_version"],
"1.2.3"
);
assert!(
payload["agent_metadata"]
.get("serverless_compat_runtime_version")
.is_none()
);
assert!(payload.get("hostname").is_none());
}

#[test]
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("0.28.0")).unwrap();
let payload: Value = serde_json::from_slice(&body).unwrap();

assert_eq!(
payload["agent_metadata"]["serverless_compat_runtime_version"],
"0.18.0"
);
assert_eq!(
payload["agent_metadata"]["serverless_compat_binary_version"],
"0.28.0"
);
assert_eq!(payload["agent_metadata"]["runtime"], "python3.13");
}

#[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_runtime_version")
.is_none()
);
assert!(
payload["agent_metadata"]
.get("serverless_compat_binary_version")
.is_none()
);
}
}
Loading
Loading