Skip to content

Commit ad3872a

Browse files
committed
Upload Datadog average CPU usage metric in citool
1 parent 80bcdb5 commit ad3872a

File tree

6 files changed

+111
-18
lines changed

6 files changed

+111
-18
lines changed

.github/workflows/ci.yml

+2-6
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ jobs:
183183
run: src/ci/scripts/dump-environment.sh
184184

185185
# Pre-build citool before the following step uninstalls rustup
186-
# Build is into the build directory, to avoid modifying sources
186+
# Build it into the build directory, to avoid modifying sources
187187
- name: build citool
188188
run: |
189189
cd src/ci/citool
@@ -238,13 +238,9 @@ jobs:
238238
- name: upload job metrics to DataDog
239239
if: needs.calculate_matrix.outputs.run_type != 'pr'
240240
env:
241-
DATADOG_SITE: datadoghq.com
242241
DATADOG_API_KEY: ${{ secrets.DATADOG_API_KEY }}
243242
DD_GITHUB_JOB_NAME: ${{ matrix.full_name }}
244-
run: |
245-
cd src/ci
246-
npm ci
247-
python3 scripts/upload-build-metrics.py ../../build/cpu-usage.csv
243+
run: ./build/citool/debug/citool upload-build-metrics build/cpu-usage.csv
248244

249245
# This job isused to tell bors the final status of the build, as there is no practical way to detect
250246
# when a workflow is successful listening to webhooks only in our current bors implementation (homu).

src/ci/citool/Cargo.toml

+2
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,11 @@ edition = "2021"
66
[dependencies]
77
anyhow = "1"
88
clap = { version = "4.5", features = ["derive"] }
9+
csv = "1"
910
serde = { version = "1", features = ["derive"] }
1011
serde_yaml = "0.9"
1112
serde_json = "1"
13+
ureq = { version = "3", features = ["json"] }
1214

1315
build_helper = { path = "../../build_helper" }
1416

src/ci/citool/src/cpu_usage.rs

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
use std::path::Path;
2+
3+
/// Loads CPU usage records from a CSV generated by the `src/ci/scripts/collect-cpu-stats.sh`
4+
/// script.
5+
pub fn load_cpu_usage(path: &Path) -> anyhow::Result<Vec<f64>> {
6+
let reader = csv::ReaderBuilder::new().flexible(true).from_path(path)?;
7+
8+
let mut entries = vec![];
9+
for row in reader.into_records() {
10+
let row = row?;
11+
let cols = row.into_iter().collect::<Vec<&str>>();
12+
13+
// The log might contain incomplete rows or some Python exception
14+
if cols.len() == 2 {
15+
if let Ok(idle) = cols[1].parse::<f64>() {
16+
entries.push(100.0 - idle);
17+
}
18+
}
19+
}
20+
21+
Ok(entries)
22+
}

src/ci/citool/src/datadog.rs

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
use anyhow::Context;
2+
3+
use crate::utils::load_env_var;
4+
5+
/// Uploads a custom CI pipeline metric to Datadog.
6+
/// Expects to be executed from within the context of a GitHub Actions job.
7+
pub fn upload_datadog_metric(name: &str, value: f64) -> anyhow::Result<()> {
8+
let datadog_api_key = load_env_var("DATADOG_API_KEY")?;
9+
let github_server_url = load_env_var("GITHUB_SERVER_URL")?;
10+
let github_repository = load_env_var("GITHUB_REPOSITORY")?;
11+
let github_run_id = load_env_var("GITHUB_RUN_ID")?;
12+
let github_run_attempt = load_env_var("GITHUB_RUN_ATTEMPT")?;
13+
let github_job = load_env_var("GITHUB_JOB")?;
14+
let dd_github_job_name = load_env_var("DD_GITHUB_JOB_NAME")?;
15+
16+
// This API endpoint is not documented in Datadog's API reference currently.
17+
// It was reverse-engineered from the `datadog-ci measure` npm command.
18+
ureq::post("https://api.datadoghq.com/api/v2/ci/pipeline/metrics")
19+
.header("DD-API-KEY", datadog_api_key)
20+
.send_json(serde_json::json!({
21+
"data": {
22+
"attributes": {
23+
"ci_env": {
24+
"GITHUB_SERVER_URL": github_server_url,
25+
"GITHUB_REPOSITORY": github_repository,
26+
"GITHUB_RUN_ID": github_run_id,
27+
"GITHUB_RUN_ATTEMPT": github_run_attempt,
28+
"GITHUB_JOB": github_job,
29+
"DD_GITHUB_JOB_NAME": dd_github_job_name
30+
},
31+
// Job level
32+
"ci_level": 1,
33+
"metrics": {
34+
name: value
35+
},
36+
"provider": "github"
37+
},
38+
"type": "ci_custom_metric"
39+
}
40+
}))
41+
.context("cannot send metric to DataDog")?;
42+
Ok(())
43+
}

src/ci/citool/src/main.rs

+30-12
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1+
mod cpu_usage;
2+
mod datadog;
13
mod metrics;
4+
mod utils;
25

36
use std::collections::BTreeMap;
47
use std::path::{Path, PathBuf};
@@ -8,6 +11,10 @@ use anyhow::Context;
811
use clap::Parser;
912
use serde_yaml::Value;
1013

14+
use crate::cpu_usage::load_cpu_usage;
15+
use crate::datadog::upload_datadog_metric;
16+
use crate::utils::load_env_var;
17+
1118
use crate::metrics::postprocess_metrics;
1219

1320
const CI_DIRECTORY: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/..");
@@ -75,7 +82,7 @@ impl JobDatabase {
7582
}
7683

7784
fn load_job_db(path: &Path) -> anyhow::Result<JobDatabase> {
78-
let db = read_to_string(path)?;
85+
let db = utils::read_to_string(path)?;
7986
let mut db: Value = serde_yaml::from_str(&db)?;
8087

8188
// We need to expand merge keys (<<), because serde_yaml can't deal with them
@@ -148,10 +155,6 @@ impl GitHubContext {
148155
}
149156
}
150157

151-
fn load_env_var(name: &str) -> anyhow::Result<String> {
152-
std::env::var(name).with_context(|| format!("Cannot find variable {name}"))
153-
}
154-
155158
fn load_github_ctx() -> anyhow::Result<GitHubContext> {
156159
let event_name = load_env_var("GITHUB_EVENT_NAME")?;
157160
let commit_message =
@@ -325,6 +328,18 @@ fn run_workflow_locally(db: JobDatabase, job_type: JobType, name: String) -> any
325328
if !result.success() { Err(anyhow::anyhow!("Job failed")) } else { Ok(()) }
326329
}
327330

331+
fn upload_ci_metrics(cpu_usage_csv: &Path) -> anyhow::Result<()> {
332+
let usage = load_cpu_usage(cpu_usage_csv).context("Cannot load CPU usage from input CSV")?;
333+
eprintln!("CPU usage\n{usage:?}");
334+
335+
let avg = if !usage.is_empty() { usage.iter().sum::<f64>() / usage.len() as f64 } else { 0.0 };
336+
eprintln!("CPU usage average: {avg}");
337+
338+
upload_datadog_metric("avg-cpu-usage", avg).context("Cannot upload Datadog metric")?;
339+
340+
Ok(())
341+
}
342+
328343
#[derive(clap::Parser)]
329344
enum Args {
330345
/// Calculate a list of jobs that should be executed on CI.
@@ -350,6 +365,11 @@ enum Args {
350365
/// Usually, this will be GITHUB_STEP_SUMMARY on CI.
351366
summary_path: PathBuf,
352367
},
368+
/// Upload CI metrics to Datadog.
369+
UploadBuildMetrics {
370+
/// Path to a CSV containing the CI job CPU usage.
371+
cpu_usage_csv: PathBuf,
372+
},
353373
}
354374

355375
#[derive(clap::ValueEnum, Clone)]
@@ -370,7 +390,7 @@ fn main() -> anyhow::Result<()> {
370390
let jobs_path = jobs_file.as_deref().unwrap_or(default_jobs_file);
371391
let gh_ctx = load_github_ctx()
372392
.context("Cannot load environment variables from GitHub Actions")?;
373-
let channel = read_to_string(Path::new(CI_DIRECTORY).join("channel"))
393+
let channel = utils::read_to_string(Path::new(CI_DIRECTORY).join("channel"))
374394
.context("Cannot read channel file")?
375395
.trim()
376396
.to_string();
@@ -379,7 +399,10 @@ fn main() -> anyhow::Result<()> {
379399
.context("Failed to calculate job matrix")?;
380400
}
381401
Args::RunJobLocally { job_type, name } => {
382-
run_workflow_locally(load_db(default_jobs_file)?, job_type, name)?
402+
run_workflow_locally(load_db(default_jobs_file)?, job_type, name)?;
403+
}
404+
Args::UploadBuildMetrics { cpu_usage_csv } => {
405+
upload_ci_metrics(&cpu_usage_csv)?;
383406
}
384407
Args::PostprocessMetrics { metrics_path, summary_path } => {
385408
postprocess_metrics(&metrics_path, &summary_path)?;
@@ -388,8 +411,3 @@ fn main() -> anyhow::Result<()> {
388411

389412
Ok(())
390413
}
391-
392-
fn read_to_string<P: AsRef<Path>>(path: P) -> anyhow::Result<String> {
393-
let error = format!("Cannot read file {:?}", path.as_ref());
394-
std::fs::read_to_string(path).context(error)
395-
}

src/ci/citool/src/utils.rs

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
use std::path::Path;
2+
3+
use anyhow::Context;
4+
5+
pub fn load_env_var(name: &str) -> anyhow::Result<String> {
6+
std::env::var(name).with_context(|| format!("Cannot find environment variable `{name}`"))
7+
}
8+
9+
pub fn read_to_string<P: AsRef<Path>>(path: P) -> anyhow::Result<String> {
10+
let error = format!("Cannot read file {:?}", path.as_ref());
11+
std::fs::read_to_string(path).context(error)
12+
}

0 commit comments

Comments
 (0)