Skip to content
Open
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
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())
}
}
}
172 changes: 172 additions & 0 deletions crates/inventory/src/payload.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
// 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"),
)
}

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_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_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"),
] {
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("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_version")
.is_none()
);
assert!(payload.get("hostname").is_none());
}

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

assert_eq!(
payload["agent_metadata"]["serverless_compat_version"],
"2.4.6"
);
assert_eq!(
payload["agent_metadata"]["serverless_compat_binary_version"],
"1.2.3"
);
}

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