Skip to content

Commit d60d0a4

Browse files
authored
fix(libdd-traceutils): Update cloud environment detection logic for Serverless [SVLS-8799] (#1857)
# What does this PR do? - Updates cloud environment detection logic to be more specific and aligned with the Serverless Compatibility Layers ([example](https://github.com/DataDog/datadog-serverless-compat-js/blob/7e68fce247490f721995c26def258b539a6d788c/src/index.ts#L18-L45)) by requiring multiple cloud-specific environment variables to identify each platform - Adds structured logging: debug when one environment is detected, error when zero or multiple are detected, and error when a platform is detected but its name variable is missing - Return None when multiple conflicting environments are detected instead of silently returning the first match - Adds unit tests covering all detection paths, missing name vars, single-var-only cases, multiple environments, and no environment Platform | Before | After -- | -- | -- AWS Lambda | AWS_LAMBDA_FUNCTION_NAME | AWS_LAMBDA_INITIALIZATION_TYPE (detect) + AWS_LAMBDA_FUNCTION_NAME (name) Azure Functions | WEBSITE_SITE_NAME | FUNCTIONS_EXTENSION_VERSION + FUNCTIONS_WORKER_RUNTIME (detect) + WEBSITE_SITE_NAME (name) GCP Cloud Functions Gen 1 | FUNCTION_NAME | FUNCTION_NAME + GCP_PROJECT GCP Cloud Run Functions Gen 2 | K_SERVICE | K_SERVICE + FUNCTION_TARGET Azure Spring Apps | ASCSVCRT_SPRING__APPLICATION__NAME | unchanged # Motivation A customer instrumenting a Java Azure Function had set `FUNCTION_NAME` as an environment variable. This led the current libdatadog logic to return `EnvironmentType::CloudFunction`, which led to [this](https://github.com/DataDog/serverless-components/blob/96ab942a20979aeb8640214f027cb94e5da7b6fe/crates/datadog-trace-agent/src/env_verifier.rs#L96) being called in serverless-components which returned an error after running a Google-specific metadata check. As a result, we want the environment detection logic to check multiple cloud-specific env vars at once. # Additional Notes Once this PR is merged, we plan to make a new PR in serverless-components to update the libdatadog commit hash. # How to test the change? Unit tests: Run `cargo test -p libdd-trace-utils config_utils` 1. Use git log to find this PR's most recent commit hash 2. Clone [serverless-components](https://github.com/DataDog/serverless-components/tree/main) and update the commit hash in [datadog-trace-agent/Cargo.toml](https://github.com/DataDog/serverless-components/blob/main/crates/datadog-trace-agent/Cargo.toml) everywhere that libdatadog is used 3. Follow the instructions in the [Serverless Compatibility Layer docs](https://datadoghq.atlassian.net/wiki/spaces/SLS/pages/2977497119/Serverless+Compatibility+Layer) to deploy sample apps 4. Turn `DD_LOG_LEVEL=DEBUG` in GCP/Azure functions to make sure the correct environment is detected 5. Set `FUNCTION_NAME` as an env var in Azure Functions to make sure it isn't identified as a Google Cloud Function 6. To test with Google Cloud Gen 1, deploy a gen1 app and use the .whl from `serverless-compat-self-monitoring/azure_functions/code/compat/python/datadog_serverless_compat-0.0.0-py3-none-any.whl` and drop it in the root of the app. Replace `datadog-serverless-compat` in `requirements.txt` to this file name. Deployed a Java Azure Function without this change with `FUNCTION_NAME=myfunction` and got the same error: <img width="591" height="224" alt="image" src="https://github.com/user-attachments/assets/69a7db4a-7fc3-4532-94c8-f6d8d8a853f4" /> Redeployed the Azure Function with this change. The error disappeared and we're getting traces: <img width="687" height="270" alt="image" src="https://github.com/user-attachments/assets/5e087889-3345-4fb4-a514-124194965613" /> Also deployed a Python Google Cloud Function Gen 1 with this change. Interestingly, it's getting detected as Gen 2. <img width="706" height="279" alt="image" src="https://github.com/user-attachments/assets/c0db4123-683e-4234-bda7-5a8efbc36685" /> I printed out the env vars and `FUNCTION_TARGET` and `K_SERVICE` are being set, but not `FUNCTION_NAME` and `GCP_PROJECT`: ``` DEFAULT 2026-04-09T21:52:43.186346Z [resource.labels.functionName: gen1-func-envvars] [labels.executionId: j3djq5xx9qqk] FUNCTION_NAME=NOT SET DEFAULT 2026-04-09T21:52:43.186349Z [resource.labels.functionName: gen1-func-envvars] [labels.executionId: j3djq5xx9qqk] GCP_PROJECT=NOT SET DEFAULT 2026-04-09T21:52:43.186393Z [resource.labels.functionName: gen1-func-envvars] [labels.executionId: j3djq5xx9qqk] FUNCTION_TARGET=main DEFAULT 2026-04-09T21:52:43.186399Z [resource.labels.functionName: gen1-func-envvars] [labels.executionId: j3djq5xx9qqk] K_SERVICE=gen1-func-envvars ``` Serverless compatibility layers that don't check `FUNCTION_TARGET` or `K_SERVICE` should be updated (e.g. [serverless-compat-java](https://github.com/DataDog/datadog-serverless-compat-java/blob/a1bceb25ce55cd811938e89d750b4f10319c4336/src/main/java/com/datadog/ServerlessCompatAgent.java#L64)) to have the same logic as other runtimes ([serverless-compat-py](https://github.com/DataDog/datadog-serverless-compat-py/blob/71b0cb39ceff4bb6fc5ed822c1545ecdd2fd47fa/datadog_serverless_compat/main.py#L34)) To update: - serverless-compat-java - serverless-compat-dotnet Co-authored-by: kathie.huang <kathie.huang@datadoghq.com>
1 parent 3c34f14 commit d60d0a4

1 file changed

Lines changed: 244 additions & 14 deletions

File tree

libdd-trace-utils/src/config_utils.rs

Lines changed: 244 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,33 +3,71 @@
33

44
use crate::trace_utils;
55
use std::env;
6+
use tracing::{debug, error};
67

78
pub const PROD_INTAKE_SUBDOMAIN: &str = "trace.agent";
89

910
const TRACE_INTAKE_ROUTE: &str = "/api/v0.2/traces";
1011
const TRACE_STATS_INTAKE_ROUTE: &str = "/api/v0.2/stats";
1112

1213
pub fn read_cloud_env() -> Option<(String, trace_utils::EnvironmentType)> {
13-
if let Ok(res) = env::var("AWS_LAMBDA_FUNCTION_NAME") {
14-
return Some((res, trace_utils::EnvironmentType::LambdaFunction));
14+
let mut detected: Vec<(String, trace_utils::EnvironmentType)> = Vec::new();
15+
16+
if env::var("AWS_LAMBDA_INITIALIZATION_TYPE").is_ok() {
17+
match env::var("AWS_LAMBDA_FUNCTION_NAME") {
18+
Ok(name) => detected.push((name, trace_utils::EnvironmentType::LambdaFunction)),
19+
Err(_) => {
20+
error!("AWS Lambda environment detected but AWS_LAMBDA_FUNCTION_NAME is not set");
21+
}
22+
}
1523
}
16-
if let Ok(res) = env::var("K_SERVICE") {
17-
// Set by Google Cloud Functions for newer runtimes
18-
return Some((res, trace_utils::EnvironmentType::CloudFunction));
24+
25+
if env::var("FUNCTIONS_EXTENSION_VERSION").is_ok()
26+
&& env::var("FUNCTIONS_WORKER_RUNTIME").is_ok()
27+
{
28+
match env::var("WEBSITE_SITE_NAME") {
29+
Ok(name) => detected.push((name, trace_utils::EnvironmentType::AzureFunction)),
30+
Err(_) => {
31+
error!("Azure Functions environment detected but WEBSITE_SITE_NAME is not set");
32+
}
33+
}
1934
}
20-
if let Ok(res) = env::var("FUNCTION_NAME") {
35+
36+
if let (Ok(name), Ok(_)) = (env::var("K_SERVICE"), env::var("FUNCTION_TARGET")) {
37+
// Set by Google Cloud Functions for newer runtimes
38+
detected.push((name, trace_utils::EnvironmentType::CloudFunction));
39+
} else if let (Ok(name), Ok(_)) = (env::var("FUNCTION_NAME"), env::var("GCP_PROJECT")) {
2140
// Set by Google Cloud Functions for older runtimes
22-
return Some((res, trace_utils::EnvironmentType::CloudFunction));
41+
detected.push((name, trace_utils::EnvironmentType::CloudFunction));
2342
}
24-
if let Ok(res) = env::var("WEBSITE_SITE_NAME") {
25-
// Set by Azure Functions
26-
return Some((res, trace_utils::EnvironmentType::AzureFunction));
27-
}
28-
if let Ok(res) = env::var("ASCSVCRT_SPRING__APPLICATION__NAME") {
43+
44+
if let Ok(name) = env::var("ASCSVCRT_SPRING__APPLICATION__NAME") {
2945
// Set by Azure Spring Apps
30-
return Some((res, trace_utils::EnvironmentType::AzureSpringApp));
46+
detected.push((name, trace_utils::EnvironmentType::AzureSpringApp));
47+
}
48+
49+
match detected.len() {
50+
0 => {
51+
error!("No cloud environment detected");
52+
None
53+
}
54+
1 => {
55+
let (ref name, ref env_type) = detected[0];
56+
debug!("Cloud environment detected: {env_type:?} ({name})");
57+
detected.into_iter().next()
58+
}
59+
_ => {
60+
let env_names: Vec<String> = detected
61+
.iter()
62+
.map(|(name, env_type)| format!("{env_type:?}({name})"))
63+
.collect();
64+
error!(
65+
"Multiple cloud environments detected: {}",
66+
env_names.join(", ")
67+
);
68+
None
69+
}
3170
}
32-
None
3371
}
3472

3573
pub fn trace_intake_url(site: &str) -> String {
@@ -51,3 +89,195 @@ pub fn trace_stats_url_prefixed(endpoint_prefix: &str) -> String {
5189
fn construct_trace_intake_url(prefix: &str, route: &str) -> String {
5290
format!("https://{PROD_INTAKE_SUBDOMAIN}.{prefix}{route}")
5391
}
92+
93+
#[cfg(test)]
94+
mod tests {
95+
use super::*;
96+
use std::sync::Mutex;
97+
98+
// Mutex to ensure environment variable tests run sequentially
99+
static ENV_TEST_LOCK: Mutex<()> = Mutex::new(());
100+
101+
fn clear_all_env_vars() {
102+
unsafe {
103+
env::remove_var("AWS_LAMBDA_INITIALIZATION_TYPE");
104+
env::remove_var("AWS_LAMBDA_FUNCTION_NAME");
105+
env::remove_var("FUNCTIONS_EXTENSION_VERSION");
106+
env::remove_var("FUNCTIONS_WORKER_RUNTIME");
107+
env::remove_var("WEBSITE_SITE_NAME");
108+
env::remove_var("FUNCTION_NAME");
109+
env::remove_var("GCP_PROJECT");
110+
env::remove_var("K_SERVICE");
111+
env::remove_var("FUNCTION_TARGET");
112+
env::remove_var("ASCSVCRT_SPRING__APPLICATION__NAME");
113+
}
114+
}
115+
116+
#[test]
117+
fn test_aws_lambda_detected() {
118+
let _lock = ENV_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
119+
clear_all_env_vars();
120+
unsafe {
121+
env::set_var("AWS_LAMBDA_INITIALIZATION_TYPE", "on-demand");
122+
env::set_var("AWS_LAMBDA_FUNCTION_NAME", "my-function");
123+
}
124+
let result = read_cloud_env();
125+
assert_eq!(
126+
result,
127+
Some((
128+
"my-function".to_string(),
129+
trace_utils::EnvironmentType::LambdaFunction
130+
))
131+
);
132+
}
133+
134+
#[test]
135+
fn test_aws_lambda_missing_function_name() {
136+
let _lock = ENV_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
137+
clear_all_env_vars();
138+
unsafe { env::set_var("AWS_LAMBDA_INITIALIZATION_TYPE", "on-demand") };
139+
let result = read_cloud_env();
140+
assert_eq!(result, None);
141+
}
142+
143+
#[test]
144+
fn test_aws_lambda_not_detected_without_init_type() {
145+
let _lock = ENV_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
146+
clear_all_env_vars();
147+
unsafe { env::set_var("AWS_LAMBDA_FUNCTION_NAME", "my-function") };
148+
let result = read_cloud_env();
149+
assert_eq!(result, None);
150+
}
151+
152+
#[test]
153+
fn test_azure_function_detected() {
154+
let _lock = ENV_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
155+
clear_all_env_vars();
156+
unsafe {
157+
env::set_var("FUNCTIONS_EXTENSION_VERSION", "~4");
158+
env::set_var("FUNCTIONS_WORKER_RUNTIME", "java");
159+
env::set_var("WEBSITE_SITE_NAME", "my-azure-app");
160+
}
161+
let result = read_cloud_env();
162+
assert_eq!(
163+
result,
164+
Some((
165+
"my-azure-app".to_string(),
166+
trace_utils::EnvironmentType::AzureFunction
167+
))
168+
);
169+
}
170+
171+
#[test]
172+
fn test_azure_function_missing_site_name() {
173+
let _lock = ENV_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
174+
clear_all_env_vars();
175+
unsafe {
176+
env::set_var("FUNCTIONS_EXTENSION_VERSION", "~4");
177+
env::set_var("FUNCTIONS_WORKER_RUNTIME", "java");
178+
}
179+
let result = read_cloud_env();
180+
assert_eq!(result, None);
181+
}
182+
183+
#[test]
184+
fn test_azure_function_not_detected_with_only_one_var() {
185+
let _lock = ENV_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
186+
clear_all_env_vars();
187+
unsafe { env::set_var("FUNCTIONS_EXTENSION_VERSION", "~4") };
188+
let result = read_cloud_env();
189+
assert_eq!(result, None);
190+
}
191+
192+
#[test]
193+
fn test_gcp_1st_gen_detected() {
194+
let _lock = ENV_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
195+
clear_all_env_vars();
196+
unsafe {
197+
env::set_var("FUNCTION_NAME", "my-gcp-function");
198+
env::set_var("GCP_PROJECT", "my-project");
199+
}
200+
let result = read_cloud_env();
201+
assert_eq!(
202+
result,
203+
Some((
204+
"my-gcp-function".to_string(),
205+
trace_utils::EnvironmentType::CloudFunction
206+
))
207+
);
208+
}
209+
210+
#[test]
211+
fn test_gcp_1st_gen_not_detected_without_gcp_project() {
212+
let _lock = ENV_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
213+
clear_all_env_vars();
214+
unsafe { env::set_var("FUNCTION_NAME", "my-gcp-function") };
215+
let result = read_cloud_env();
216+
assert_eq!(result, None);
217+
}
218+
219+
#[test]
220+
fn test_gcp_2nd_gen_detected() {
221+
let _lock = ENV_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
222+
clear_all_env_vars();
223+
unsafe {
224+
env::set_var("K_SERVICE", "my-cloud-run-fn");
225+
env::set_var("FUNCTION_TARGET", "myHandler");
226+
}
227+
let result = read_cloud_env();
228+
assert_eq!(
229+
result,
230+
Some((
231+
"my-cloud-run-fn".to_string(),
232+
trace_utils::EnvironmentType::CloudFunction
233+
))
234+
);
235+
}
236+
237+
#[test]
238+
fn test_gcp_2nd_gen_not_detected_without_function_target() {
239+
let _lock = ENV_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
240+
clear_all_env_vars();
241+
unsafe { env::set_var("K_SERVICE", "my-cloud-run-fn") };
242+
let result = read_cloud_env();
243+
assert_eq!(result, None);
244+
}
245+
246+
#[test]
247+
fn test_azure_spring_app_detected() {
248+
let _lock = ENV_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
249+
clear_all_env_vars();
250+
unsafe { env::set_var("ASCSVCRT_SPRING__APPLICATION__NAME", "my-spring-app") };
251+
let result = read_cloud_env();
252+
assert_eq!(
253+
result,
254+
Some((
255+
"my-spring-app".to_string(),
256+
trace_utils::EnvironmentType::AzureSpringApp
257+
))
258+
);
259+
}
260+
261+
#[test]
262+
fn test_no_environment_detected() {
263+
let _lock = ENV_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
264+
clear_all_env_vars();
265+
let result = read_cloud_env();
266+
assert_eq!(result, None);
267+
}
268+
269+
#[test]
270+
fn test_multiple_environments_returns_none() {
271+
let _lock = ENV_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
272+
clear_all_env_vars();
273+
unsafe {
274+
env::set_var("AWS_LAMBDA_INITIALIZATION_TYPE", "on-demand");
275+
env::set_var("AWS_LAMBDA_FUNCTION_NAME", "my-lambda");
276+
env::set_var("FUNCTIONS_EXTENSION_VERSION", "~4");
277+
env::set_var("FUNCTIONS_WORKER_RUNTIME", "java");
278+
env::set_var("WEBSITE_SITE_NAME", "my-azure-app");
279+
}
280+
let result = read_cloud_env();
281+
assert_eq!(result, None);
282+
}
283+
}

0 commit comments

Comments
 (0)