Skip to content
Draft
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
10 changes: 10 additions & 0 deletions Cargo.lock

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

11 changes: 11 additions & 0 deletions crates/datadog-metrics-collector/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
[package]
name = "datadog-metrics-collector"
version = "0.1.0"
edition.workspace = true
license.workspace = true
description = "Collector to read, compute, and submit enhanced metrics in Serverless environments"

[dependencies]
dogstatsd = { path = "../dogstatsd", default-features = true }
tracing = { version = "0.1", default-features = false }
libdd-common = { git = "https://github.com/DataDog/libdatadog", rev = "8c88979985154d6d97c0fc2ca9039682981eacad", default-features = false }
100 changes: 100 additions & 0 deletions crates/datadog-metrics-collector/src/instance.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
// Copyright 2023-Present Datadog, Inc. https://www.datadoghq.com/
// SPDX-License-Identifier: Apache-2.0

//! Instance identity metric collector for Azure Functions.
//!
//! Submits `azure.functions.enhanced.instance` with value 1.0 on each
//! collection tick, tagged with the instance identifier.

use dogstatsd::aggregator::AggregatorHandle;
use dogstatsd::metric::{Metric, MetricValue, SortedTags};
use std::env;
use tracing::{error, warn};

const INSTANCE_METRIC: &str = "azure.functions.enhanced.instance";

/// Resolves the instance ID from explicit values (used by tests).
fn resolve_instance_id_from(
website_instance_id: Option<&str>,
website_pod_name: Option<&str>,
container_name: Option<&str>,
) -> Option<String> {
website_instance_id
.or(website_pod_name)
.or(container_name)
.map(String::from)
Comment on lines +16 to +25
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

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

resolve_instance_id_from treats empty strings as valid instance IDs. If (for example) WEBSITE_INSTANCE_ID is set but empty, it will block fallbacks and produce an instance: tag with an empty value. Consider filtering out empty/whitespace-only strings before applying the fallback chain.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

These env vars should either be injected by Azure with a value or not set at all

}

/// Resolves the instance ID from environment variables.
///
/// Checks in order:
/// 1. `WEBSITE_INSTANCE_ID` (Elastic Premium / Premium plans)
/// 2. `WEBSITE_POD_NAME` (Flex Consumption / Consumption plans)
/// 3. `CONTAINER_NAME` (Flex Consumption / Consumption plans)
fn resolve_instance_id() -> Option<String> {
resolve_instance_id_from(
env::var("WEBSITE_INSTANCE_ID").ok().as_deref(),
env::var("WEBSITE_POD_NAME").ok().as_deref(),
env::var("CONTAINER_NAME").ok().as_deref(),
)
}

pub struct InstanceMetricsCollector {
aggregator: AggregatorHandle,
tags: Option<SortedTags>,
}

impl InstanceMetricsCollector {
/// Creates a new collector, returning `None` if no instance ID is found.
pub fn new(aggregator: AggregatorHandle, tags: Option<SortedTags>) -> Option<Self> {
let instance_id = resolve_instance_id();
let Some(instance_id) = instance_id else {
warn!("No instance ID found, instance metric will not be submitted");
return None;
};

// Precompute tags: enhanced metrics tags + instance tag
let instance_tag = format!("instance:{}", instance_id);
let tags = match tags {
Some(mut existing) => {
if let Ok(id_tag) = SortedTags::parse(&instance_tag) {
existing.extend(&id_tag);
}
Some(existing)
}
None => SortedTags::parse(&instance_tag).ok(),
};

Some(Self { aggregator, tags })
}

pub fn collect_and_submit(&self) {
let metric = Metric::new(
INSTANCE_METRIC.into(),
MetricValue::gauge(1.0),
self.tags.clone(),
None,
);

if let Err(e) = self.aggregator.insert_batch(vec![metric]) {
error!("Failed to insert instance metric: {}", e);
}
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_resolve_instance_id_falls_back_to_pod_name() {
let id = resolve_instance_id_from(None, Some("pod-xyz"), Some("container-123"));
assert_eq!(id, Some("pod-xyz".to_string()));
}

#[test]
fn test_resolve_instance_id_falls_back_to_container_name() {
let id = resolve_instance_id_from(None, None, Some("container-123"));
assert_eq!(id, Some("container-123".to_string()));
}
Comment on lines +89 to +99
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

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

The instance ID resolution tests only cover the fallback cases. To fully lock in the intended precedence, add a test asserting WEBSITE_INSTANCE_ID wins over WEBSITE_POD_NAME/CONTAINER_NAME, and (optionally) a test for the all-None case returning None.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

If any of these env vars exist, they should give the correct instance value, so I think this test case is unnecessary. The all-None case also isn't relevant since these are env vars injected by Azure. End to end tests to ensure these instance-identifying environment variables don't change would be more helpful

}
11 changes: 11 additions & 0 deletions crates/datadog-metrics-collector/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// Copyright 2023-Present Datadog, Inc. https://www.datadoghq.com/
// SPDX-License-Identifier: Apache-2.0

#![cfg_attr(not(test), deny(clippy::panic))]
#![cfg_attr(not(test), deny(clippy::unwrap_used))]
#![cfg_attr(not(test), deny(clippy::expect_used))]
#![cfg_attr(not(test), deny(clippy::todo))]
#![cfg_attr(not(test), deny(clippy::unimplemented))]

pub mod instance;
pub mod tags;
55 changes: 55 additions & 0 deletions crates/datadog-metrics-collector/src/tags.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// Copyright 2023-Present Datadog, Inc. https://www.datadoghq.com/
// SPDX-License-Identifier: Apache-2.0

//! Shared tag builder for enhanced metrics.
//!
//! Tags are attached to all enhanced metrics submitted by the metrics collector.

use dogstatsd::metric::SortedTags;
use libdd_common::azure_app_services;
use std::env;

/// Builds the common tags for all enhanced metrics.
///
/// Sources:
/// - Azure metadata (resource_group, subscription_id, name) from libdd_common
/// - Environment variables (region, plan_tier, service, env, version, serverless_compat_version)
///
/// The DogStatsD origin tag (e.g. `origin:azurefunction`) is added by the metrics aggregator,
/// not here.
pub fn build_enhanced_metrics_tags() -> Option<SortedTags> {
let mut tag_parts = Vec::new();

if let Some(aas_metadata) = &*azure_app_services::AAS_METADATA_FUNCTION {
let aas_tags = [
("resource_group", aas_metadata.get_resource_group()),
("subscription_id", aas_metadata.get_subscription_id()),
("name", aas_metadata.get_site_name()),
];
for (name, value) in aas_tags {
if value != "unknown" {
tag_parts.push(format!("{}:{}", name, value));
}
}
}

for (tag_name, env_var) in [
("region", "REGION_NAME"),
("plan_tier", "WEBSITE_SKU"),
("service", "DD_SERVICE"),
("env", "DD_ENV"),
("version", "DD_VERSION"),
("serverless_compat_version", "DD_SERVERLESS_COMPAT_VERSION"),
] {
if let Ok(val) = env::var(env_var)
&& !val.is_empty()
{
tag_parts.push(format!("{}:{}", tag_name, val));
}
Comment on lines +36 to +48
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

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

build_enhanced_metrics_tags concatenates raw environment variable values into a comma-separated tag string and then parses it. If any value contains a comma, it will be split into multiple tags (producing incorrect tags or parse failures). Consider sanitizing/escaping tag values (or dropping values containing reserved delimiters like ,/|) before building the tag list.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

}

if tag_parts.is_empty() {
return None;
}
SortedTags::parse(&tag_parts.join(",")).ok()
}
1 change: 1 addition & 0 deletions crates/datadog-serverless-compat/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ windows-pipes = ["datadog-trace-agent/windows-pipes", "dogstatsd/windows-pipes"]

[dependencies]
datadog-logs-agent = { path = "../datadog-logs-agent" }
datadog-metrics-collector = { path = "../datadog-metrics-collector" }
datadog-trace-agent = { path = "../datadog-trace-agent" }
libdd-trace-utils = { git = "https://github.com/DataDog/libdatadog", rev = "8c88979985154d6d97c0fc2ca9039682981eacad" }
datadog-fips = { path = "../datadog-fips", default-features = false }
Expand Down
Loading
Loading