From 61fa7fff0dd42eaa7ad8c9489175cbf477527683 Mon Sep 17 00:00:00 2001 From: Chet Nichols III Date: Tue, 14 Jul 2026 23:09:43 -0700 Subject: [PATCH] feat(nvue-client): measure the switch-config REST calls The nvue REST client -- the switch-configuration path -- logged and measured nothing, so a failed NVUE request was invisible. Every public method funnels through one `execute`, so metering that once covers the client. - `execute` takes an `operation` name and wraps its request in `red::instrumented("nvue", operation, ...)`, recording `carbide_external_call_duration_milliseconds{backend = "nvue", operation, outcome}` and a WARN on failure. A non-success HTTP status counts as an error, not only a transport failure. - Each public method passes its own operation (`get_applied_config`, `apply_config_revision`, the DELETE and PATCH halves of a config replace, and so on); the composite helpers inherit the measurement from the primitives they call. It reuses the outbound-RED family, so there are no new catalogue rows. This supports https://github.com/NVIDIA/infra-controller/issues/3173 Signed-off-by: Chet Nichols III --- Cargo.lock | 1 + crates/nvue-client/Cargo.toml | 1 + crates/nvue-client/src/client.rs | 50 ++++++++++++++++++-------------- 3 files changed, 31 insertions(+), 21 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f61320fa9c..e950bfdcdf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7738,6 +7738,7 @@ dependencies = [ name = "nvue-client" version = "0.0.0" dependencies = [ + "carbide-instrument", "glob", "reqwest 0.13.4", "serde", diff --git a/crates/nvue-client/Cargo.toml b/crates/nvue-client/Cargo.toml index 8bf907a52e..e472074741 100644 --- a/crates/nvue-client/Cargo.toml +++ b/crates/nvue-client/Cargo.toml @@ -28,6 +28,7 @@ name = "nvue_client" [features] [dependencies] +carbide-instrument = { path = "../instrument" } reqwest = { features = ["rustls", "json"], workspace = true } serde = { features = ["derive"], workspace = true } serde_json = { workspace = true } diff --git a/crates/nvue-client/src/client.rs b/crates/nvue-client/src/client.rs index b67bc27a65..069c323717 100644 --- a/crates/nvue-client/src/client.rs +++ b/crates/nvue-client/src/client.rs @@ -18,6 +18,7 @@ use std::collections::{BTreeMap, HashMap}; use std::path::PathBuf; +use carbide_instrument::red; use reqwest::header::{ACCEPT, HeaderMap, HeaderValue}; use reqwest::{Client, ClientBuilder, Method, Response, Url}; pub use serde_json::Value as JsonValue; @@ -75,31 +76,38 @@ impl NvueClient { Ok(builder) } - async fn execute(&self, request: reqwest::Request) -> Result { + async fn execute( + &self, + operation: &'static str, + request: reqwest::Request, + ) -> Result { let method = request.method().clone(); let url = request.url().clone(); let body = request .body() .and_then(|b| b.as_bytes()) .map(|b| String::from_utf8_lossy(b).into_owned()); - self.client - .execute(request) - .await - .and_then(|response| response.error_for_status()) - .map_err(|source| { - NvueClientError::RequestFailed(Box::new(RequestFailed { - method, - url, - body, - source, - })) - }) + red::instrumented("nvue", operation, async move { + self.client + .execute(request) + .await + .and_then(|response| response.error_for_status()) + }) + .await + .map_err(|source| { + NvueClientError::RequestFailed(Box::new(RequestFailed { + method, + url, + body, + source, + })) + }) } pub async fn get_api(&self) -> Result { const PATH: &str = "/nvue_v1/system/api?rev=applied"; let request = self.request(Method::GET, PATH)?.build()?; - self.execute(request).await + self.execute("get_api", request).await } /// Return the config that is tagged as "applied" (in other words, the one @@ -107,7 +115,7 @@ impl NvueClient { pub async fn get_applied_config(&self) -> Result { const PATH: &str = "/nvue_v1/?rev=applied&filled=false"; let request = self.request(Method::GET, PATH)?.build()?; - let response = self.execute(request).await?; + let response = self.execute("get_applied_config", request).await?; let nvue_config = response.json().await?; Ok(nvue_config) } @@ -116,7 +124,7 @@ impl NvueClient { pub async fn create_config_revision(&self) -> Result { const PATH: &str = "/nvue_v1/revision"; let request = self.request(Method::POST, PATH)?.build()?; - let response = self.execute(request).await?; + let response = self.execute("create_config_revision", request).await?; let revision: NvueRevision = response.json().await?; let revision_id = revision .get_revision_id() @@ -138,12 +146,12 @@ impl NvueClient { let empty_config: HashMap = HashMap::new(); let builder = builder.json(&empty_config); let request = builder.build()?; - let _response = self.execute(request).await?; + let _response = self.execute("replace_config.delete", request).await?; let builder = self.request(Method::PATCH, &revision_path)?; let builder = builder.json(&config); let request = builder.build()?; - let _response = self.execute(request).await?; + let _response = self.execute("replace_config.patch", request).await?; Ok(()) } @@ -153,7 +161,7 @@ impl NvueClient { let body = NvueApplyData::force_apply(); let builder = builder.json(&body); let request = builder.build()?; - let _response = self.execute(request).await?; + let _response = self.execute("apply_config_revision", request).await?; // FIXME: we should poll on the revision path until it reaches an // "applied" state @@ -178,7 +186,7 @@ impl NvueClient { let path = "/nvue_v1/system"; let builder = self.request(Method::GET, path)?; let request = builder.build()?; - let response = self.execute(request).await?; + let response = self.execute("system_info", request).await?; let resonse_body = response.json().await?; Ok(resonse_body) } @@ -216,7 +224,7 @@ impl NvueClient { let path = format!("/nvue_v1/bridge/domain/{bridge_domain}/mac-table"); let builder = self.request(Method::GET, &path)?; let request = builder.build()?; - let response = self.execute(request).await?; + let response = self.execute("bridge_mac_table", request).await?; let resonse_body: BTreeMap = response.json().await?; let response = resonse_body.into_values().collect(); Ok(response)