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
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/metadata/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 = "metadata"
version = "0.1.0"
edition.workspace = true
license.workspace = true
description = "Fleet Automation metadata 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/metadata/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 metadata report and the identity used for operational logs.
pub struct MetadataReport {
pub body: Vec<u8>,
pub resource_id: String,
pub workload_type: &'static str,
}

/// Builds a metadata 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_metadata_report(
env_type: &EnvironmentType,
process_id: &str,
report_reason: &str,
) -> Result<Option<MetadataReport>, 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(MetadataReport {
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/metadata/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_BINARY_VERSION"),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Embed the binary version in release builds

When artifacts are built by the repository's publish or release workflows, this compile-time variable is never defined: the current build-datadog-serverless-compat.yml accepts only runner, while publish.yml resolves the version only after downloading the binaries. Consequently option_env! is always None for official artifacts and every report omits serverless_compat_binary_version. Fresh evidence against the earlier thread's claim is that neither current workflow sets or forwards the renamed DD_SERVERLESS_COMPAT_BINARY_VERSION; pass the resolved version into the reusable build and export it for each cargo build.

AGENTS.md reference: AGENTS.md:L24-L31

Useful? React with 👍 / 👎.

)
}

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