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
30 changes: 29 additions & 1 deletion Cargo.lock

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

2 changes: 2 additions & 0 deletions LICENSE-3rdparty.csv
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ headers,https://github.com/hyperium/headers,MIT,Sean McArthur <sean@seanmonstar.
headers-core,https://github.com/hyperium/headers,MIT,Sean McArthur <sean@seanmonstar.com>
heck,https://github.com/withoutboats/heck,MIT OR Apache-2.0,The heck Authors
heck,https://github.com/withoutboats/heck,MIT OR Apache-2.0,Without Boats <woboats@gmail.com>
hermit-abi,https://github.com/hermit-os/hermit-rs,MIT OR Apache-2.0,Stefan Lankes
hex,https://github.com/KokaKiwi/rust-hex,MIT OR Apache-2.0,KokaKiwi <kokakiwi@kokakiwi.net>
home,https://github.com/rust-lang/cargo,MIT OR Apache-2.0,Brian Anderson <andersrb@gmail.com>
http,https://github.com/hyperium/http,MIT OR Apache-2.0,"Alex Crichton <alex@alexcrichton.com>, Carl Lerche <me@carllerche.com>, Sean McArthur <sean@seanmonstar.com>"
Expand Down Expand Up @@ -119,6 +120,7 @@ multimap,https://github.com/havarnov/multimap,MIT OR Apache-2.0,Håvar Nøvik <h
nix,https://github.com/nix-rust/nix,MIT,The nix-rust Project Developers
nu-ansi-term,https://github.com/nushell/nu-ansi-term,MIT,"ogham@bsago.me, Ryan Scheel (Havvy) <ryan.havvy@gmail.com>, Josh Triplett <josh@joshtriplett.org>, The Nushell Project Developers"
num-traits,https://github.com/rust-num/num-traits,MIT OR Apache-2.0,The Rust Project Developers
num_cpus,https://github.com/seanmonstar/num_cpus,MIT OR Apache-2.0,Sean McArthur <sean@seanmonstar.com>
once_cell,https://github.com/matklad/once_cell,MIT OR Apache-2.0,Aleksey Kladov <aleksey.kladov@gmail.com>
openssl-probe,https://github.com/rustls/openssl-probe,MIT OR Apache-2.0,Alex Crichton <alex@alexcrichton.com>
ordered-float,https://github.com/reem/rust-ordered-float,MIT,"Jonathan Reem <jonathan.reem@gmail.com>, Matt Brubeck <mbrubeck@limpet.net>"
Expand Down
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 }
num_cpus = "1.16"
tracing = { version = "0.1", default-features = false }
143 changes: 143 additions & 0 deletions crates/datadog-metrics-collector/src/cpu.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
// Copyright 2023-Present Datadog, Inc. https://www.datadoghq.com/
// SPDX-License-Identifier: Apache-2.0

//! CPU metrics collector for Azure Functions
//!
//! This module provides OS-agnostic CPU metrics collection, computing CPU usage
//! adnd limit and submitting them as distribution metrics to Datadog.
//!
//! All CPU metrics are reported in nanocores (1 core = 1,000,000,000 nanocores).

use dogstatsd::aggregator_service::AggregatorHandle;
use dogstatsd::metric::{Metric, MetricValue, SortedTags};
use tracing::{debug, error};

const CPU_USAGE_METRIC: &str = "azure.functions.enhanced.test.cpu.usage";
const CPU_USAGE_PRECISE_METRIC: &str = "azure.functions.enhanced.test.cpu.usage.precise";
const CPU_LIMIT_METRIC: &str = "azure.functions.enhanced.test.cpu.limit";

/// Computed CPU total and limit metrics
pub struct CpuStats {
pub total: f64, // Cumulative CPU usage in nanoseconds
pub limit: Option<f64>, // CPU limit in nanocores
pub defaulted_limit: bool, // Whether CPU limit was defaulted to host CPU count
}

pub trait CpuStatsReader {
fn read(&self) -> Option<CpuStats>;
}

pub struct CpuMetricsCollector {
reader: Box<dyn CpuStatsReader>,
aggregator: AggregatorHandle,
tags: Option<SortedTags>,
collection_interval_secs: u64,
last_usage_ns: f64,
last_collection_time: std::time::Instant,
}

impl CpuMetricsCollector {
/// Creates a new CpuMetricsCollector
///
/// # Arguments
///
/// * `aggregator` - The aggregator handle to submit metrics to
/// * `tags` - Optional tags to attach to all metrics
/// * `collection_interval_secs` - The interval in seconds to collect the metrics
pub fn new(
aggregator: AggregatorHandle,
tags: Option<SortedTags>,
collection_interval_secs: u64,
) -> Self {
#[cfg(target_os = "windows")]
let reader: Box<dyn CpuStatsReader> = Box::new(crate::windows::WindowsCpuStatsReader);
#[cfg(not(target_os = "windows"))]
let reader: Box<dyn CpuStatsReader> = Box::new(crate::linux::LinuxCpuStatsReader);
Self {
reader,
aggregator,
tags,
collection_interval_secs,
last_usage_ns: -1.0,
last_collection_time: std::time::Instant::now(),
}
}

pub fn collect_and_submit(&mut self) {
if let Some(cpu_stats) = self.reader.read() {
// Submit metrics
debug!("Collected cpu stats!");
let current_usage_ns = cpu_stats.total;
debug!("CPU usage: {}", cpu_stats.total);
let now_instant = std::time::Instant::now();

// Skip first collection
if self.last_usage_ns == -1.0 {
debug!("First CPU collection, skipping rate computation");
self.last_usage_ns = current_usage_ns;
self.last_collection_time = now_instant;
return;
}

let delta_ns = current_usage_ns - self.last_usage_ns;
self.last_usage_ns = current_usage_ns;
let elapsed_secs = self.last_collection_time.elapsed().as_secs_f64();
debug!("Elapsed time: {} seconds", elapsed_secs);
self.last_collection_time = now_instant;

// Divide nanoseconds delta by collection interval to get usage rate in nanocores
let usage_rate_nc = delta_ns / self.collection_interval_secs as f64;
debug!("Usage rate: {} nanocores/s", usage_rate_nc);
let precise_usage_rate_nc = delta_ns / elapsed_secs;
debug!("Precise usage rate: {} nanocores/s", precise_usage_rate_nc);

let now = std::time::UNIX_EPOCH
.elapsed()
.map(|d| d.as_secs())
.unwrap_or(0)
.try_into()
.unwrap_or(0);

let precise_metric = Metric::new(
CPU_USAGE_PRECISE_METRIC.into(),
MetricValue::distribution(precise_usage_rate_nc),
self.tags.clone(),
Some(now),
);

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

let usage_metric = Metric::new(
CPU_USAGE_METRIC.into(),
MetricValue::distribution(usage_rate_nc),
self.tags.clone(),
Some(now),
);

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

if let Some(limit) = cpu_stats.limit {
debug!("CPU limit: {}", limit);
if cpu_stats.defaulted_limit {
debug!("CPU limit defaulted to host CPU count");
}
let limit_metric = Metric::new(
CPU_LIMIT_METRIC.into(),
MetricValue::distribution(limit),
self.tags.clone(),
Some(now),
);
if let Err(e) = self.aggregator.insert_batch(vec![limit_metric]) {
error!("Failed to insert CPU limit metric: {}", e);
}
}
debug!("Submitting CPU metrics!");
} else {
debug!("Skipping CPU metrics collection - could not find data to generate CPU usage and limit enhanced metrics");
}
}
}
14 changes: 14 additions & 0 deletions crates/datadog-metrics-collector/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// 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 cpu;
#[cfg(not(target_os = "windows"))]
pub mod linux;
#[cfg(target_os = "windows")]
pub mod windows;
Loading
Loading