From 2ff0586adb32ede7b918f5939ee4ed46f594c8e5 Mon Sep 17 00:00:00 2001 From: gregorydemay Date: Fri, 14 Feb 2025 16:22:15 +0100 Subject: [PATCH 01/17] test: added test for metric and request --- tests/tests.rs | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/tests/tests.rs b/tests/tests.rs index 47b0ddd8..49357ab4 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -1515,6 +1515,39 @@ fn candid_rpc_should_return_inconsistent_results_with_consensus_error() { ); } +#[test] +fn should_have_metrics_for_generic_request() { + use evm_rpc::types::MetricRpcMethod; + + let setup = EvmRpcSetup::new().mock_api_keys(); + let response = setup + .request( + RpcService::Custom(RpcApi { + url: MOCK_REQUEST_URL.to_string(), + headers: None, + }), + MOCK_REQUEST_PAYLOAD, + MOCK_REQUEST_RESPONSE_BYTES, + ) + .mock_http(MockOutcallBuilder::new(200, MOCK_REQUEST_RESPONSE)) + .wait(); + assert_eq!(response, Ok(MOCK_REQUEST_RESPONSE.to_string())); + + let rpc_method = || MetricRpcMethod("request".to_string()); + assert_eq!( + setup.get_metrics(), + Metrics { + requests: hashmap! { + (rpc_method(), CLOUDFLARE_HOSTNAME.into()) => 1, + }, + responses: hashmap! { + (rpc_method(), CLOUDFLARE_HOSTNAME.into(), 200.into()) => 1, + }, + ..Default::default() + } + ); +} + #[test] fn candid_rpc_should_return_inconsistent_results_with_unexpected_http_status() { let setup = EvmRpcSetup::new().mock_api_keys(); From a4a991e2bcd5d0f97e9dc644b0c4e0295c326284 Mon Sep 17 00:00:00 2001 From: gregorydemay Date: Fri, 14 Feb 2025 16:25:21 +0100 Subject: [PATCH 02/17] feat: observability layer --- Cargo.lock | 21 ++++ Cargo.toml | 1 + canhttp/Cargo.toml | 1 + canhttp/src/lib.rs | 1 + canhttp/src/observability/mod.rs | 208 +++++++++++++++++++++++++++++++ 5 files changed, 232 insertions(+) create mode 100644 canhttp/src/observability/mod.rs diff --git a/Cargo.lock b/Cargo.lock index a282eab4..c8b2b14c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -308,6 +308,7 @@ name = "canhttp" version = "0.1.0" dependencies = [ "ic-cdk", + "pin-project", "thiserror 2.0.11", "tower", ] @@ -1874,6 +1875,26 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" +[[package]] +name = "pin-project" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfe2e71e1471fe07709406bf725f710b02927c9c54b2b5b2ec0e8087d97c327d" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6e859e6e5bd50440ab63c47e3ebabc90f26251f7c73c3d3e837b74a1cc3fa67" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + [[package]] name = "pin-project-lite" version = "0.2.14" diff --git a/Cargo.toml b/Cargo.toml index e3187a7f..abcd98be 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -72,6 +72,7 @@ ic-metrics-encoder = "1.1" ic-stable-structures = "0.6.7" minicbor = { version = "0.25.1", features = ["alloc", "derive"] } num-bigint = "0.4.6" +pin-project = "1.1.9" proptest = "1.6.0" serde = "1.0" serde_json = "1.0" diff --git a/canhttp/Cargo.toml b/canhttp/Cargo.toml index ec2ab67a..df0d05a1 100644 --- a/canhttp/Cargo.toml +++ b/canhttp/Cargo.toml @@ -11,5 +11,6 @@ documentation = "https://docs.rs/canhttp" [dependencies] ic-cdk = { workspace = true } +pin-project = {workspace = true} tower = { workspace = true, features = ["filter", "retry"] } thiserror = { workspace = true } \ No newline at end of file diff --git a/canhttp/src/lib.rs b/canhttp/src/lib.rs index 7fae0e83..3c87683d 100644 --- a/canhttp/src/lib.rs +++ b/canhttp/src/lib.rs @@ -12,3 +12,4 @@ pub use cycles::{ mod client; mod cycles; +mod observability; diff --git a/canhttp/src/observability/mod.rs b/canhttp/src/observability/mod.rs new file mode 100644 index 00000000..4645c4a1 --- /dev/null +++ b/canhttp/src/observability/mod.rs @@ -0,0 +1,208 @@ +use pin_project::pin_project; +use std::future::Future; +use std::pin::Pin; +use std::task::{Context, Poll}; +use tower::{Layer, Service}; + +/// TODO +pub struct ObservabilityLayer { + on_request: OnRequest, + on_response: OnResponse, + on_error: OnError, +} + +impl ObservabilityLayer<(), (), ()> { + /// TODO + pub fn new() -> Self { + Self { + on_request: (), + on_response: (), + on_error: (), + } + } +} + +impl Default for ObservabilityLayer<(), (), ()> { + fn default() -> Self { + Self::new() + } +} + +impl ObservabilityLayer { + /// TODO + pub fn on_request( + self, + new_on_request: NewOnRequest, + ) -> ObservabilityLayer { + ObservabilityLayer { + on_request: new_on_request, + on_response: self.on_response, + on_error: self.on_error, + } + } + + /// TODO + pub fn on_response( + self, + new_on_response: NewOnResponse, + ) -> ObservabilityLayer { + ObservabilityLayer { + on_request: self.on_request, + on_response: new_on_response, + on_error: self.on_error, + } + } + + /// TODO + pub fn on_error( + self, + new_on_error: NewOnError, + ) -> ObservabilityLayer { + ObservabilityLayer { + on_request: self.on_request, + on_response: self.on_response, + on_error: new_on_error, + } + } +} + +impl Layer +for ObservabilityLayer +where + OnRequest: Clone, + OnResponse: Clone, + OnError: Clone, +{ + type Service = Observability; + + fn layer(&self, inner: S) -> Self::Service { + Self::Service { + inner, + on_request: self.on_request.clone(), + on_response: self.on_response.clone(), + on_error: self.on_error.clone(), + } + } +} + +/// TODO +pub struct Observability { + inner: S, + on_request: OnRequest, + on_response: OnResponse, + on_error: OnError, +} + +impl Service +for Observability +where + S: Service, + OnRequest: RequestObserver, + OnResponse: ResponseObserver + Clone, + OnError: ResponseObserver + Clone, +{ + type Response = S::Response; + type Error = S::Error; + type Future = ResponseFuture; + + fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { + self.inner.poll_ready(cx) + } + + fn call(&mut self, req: Request) -> Self::Future { + let req_data = self.on_request.observe_request(&req); + ResponseFuture { + response_future: self.inner.call(req), + request_data: Some(req_data), + on_response: self.on_response.clone(), + on_error: self.on_error.clone(), + } + } +} + +///TODO +pub trait ResponseObserver { + ///TODO + fn observe(&self, request_data: RequestData, value: &Result); +} + +impl ResponseObserver for () { + fn observe(&self, _request_data: ReqData, _value: &Result) { + //NOP + } +} + +impl ResponseObserver for F +where + F: Fn(ReqData, &T), +{ + fn observe(&self, request_data: ReqData, value: &T) { + self(request_data, value); + } +} + +#[pin_project] +pub struct ResponseFuture { + #[pin] + response_future: F, + request_data: Option, + on_response: OnResponse, + on_error: OnError, +} + +impl Future +for ResponseFuture +where + F: Future>, + OnResponse: ResponseObserver, + OnError: ResponseObserver, +{ + type Output = Result; + + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + let this = self.project(); + let result_fut = this.response_future.poll(cx); + match &result_fut { + Poll::Ready(result) => { + let request_data = this.request_data.take().unwrap(); + match result { + Ok(response) => { + this.on_response.observe(request_data, response); + } + Err(error) => { + this.on_error.observe(request_data, error); + } + } + } + Poll::Pending => {} + } + result_fut + } +} + +/// TODO +pub trait RequestObserver { + /// TODO + type ObservableRequestData; + /// TODO + fn observe_request(&self, request: &Request) -> Self::ObservableRequestData; +} + +impl RequestObserver for () { + type ObservableRequestData = (); + + fn observe_request(&self, _request: &Request) -> Self::ObservableRequestData { + //NOP + } +} + +impl RequestObserver for F +where + F: Fn(&Request) -> RequestData, +{ + type ObservableRequestData = RequestData; + + fn observe_request(&self, request: &Request) -> Self::ObservableRequestData { + self(request) + } +} \ No newline at end of file From e0cd3053a90fd322f8c6aec79c9d937a85eda6b2 Mon Sep 17 00:00:00 2001 From: gregorydemay Date: Fri, 14 Feb 2025 17:01:35 +0100 Subject: [PATCH 03/17] refactor: move http_client to http.rs --- canhttp/src/lib.rs | 1 + canhttp/src/observability/mod.rs | 8 +- src/http.rs | 167 +++++++++++++++++++++++++------ src/main.rs | 4 +- src/memory.rs | 58 ----------- 5 files changed, 142 insertions(+), 96 deletions(-) diff --git a/canhttp/src/lib.rs b/canhttp/src/lib.rs index 3c87683d..043db8ee 100644 --- a/canhttp/src/lib.rs +++ b/canhttp/src/lib.rs @@ -9,6 +9,7 @@ pub use client::{Client, IcError}; pub use cycles::{ CyclesAccounting, CyclesAccountingError, CyclesChargingPolicy, CyclesCostEstimator, }; +pub use observability::ObservabilityLayer; mod client; mod cycles; diff --git a/canhttp/src/observability/mod.rs b/canhttp/src/observability/mod.rs index 4645c4a1..aacc0a75 100644 --- a/canhttp/src/observability/mod.rs +++ b/canhttp/src/observability/mod.rs @@ -67,7 +67,7 @@ impl ObservabilityLayer Layer -for ObservabilityLayer + for ObservabilityLayer where OnRequest: Clone, OnResponse: Clone, @@ -94,7 +94,7 @@ pub struct Observability { } impl Service -for Observability + for Observability where S: Service, OnRequest: RequestObserver, @@ -151,7 +151,7 @@ pub struct ResponseFuture { } impl Future -for ResponseFuture + for ResponseFuture where F: Future>, OnResponse: ResponseObserver, @@ -205,4 +205,4 @@ where fn observe_request(&self, request: &Request) -> Self::ObservableRequestData { self(request) } -} \ No newline at end of file +} diff --git a/src/http.rs b/src/http.rs index e0940046..0551c997 100644 --- a/src/http.rs +++ b/src/http.rs @@ -1,4 +1,5 @@ -use crate::memory::http_client; +use crate::constants::COLLATERAL_CYCLES_PER_NODE; +use crate::memory::{get_num_subnet_nodes, is_demo_active}; use crate::{ add_metric_entry, constants::{CONTENT_TYPE_HEADER_LOWERCASE, CONTENT_TYPE_VALUE}, @@ -6,14 +7,14 @@ use crate::{ types::{MetricRpcHost, MetricRpcMethod, ResolvedRpcService}, util::canonicalize_json, }; -use canhttp::CyclesAccountingError; +use canhttp::{CyclesAccounting, CyclesAccountingError, CyclesChargingPolicy, ObservabilityLayer}; use evm_rpc_types::{HttpOutcallError, ProviderError, RpcError, RpcResult, ValidationError}; use ic_cdk::api::management_canister::http_request::{ CanisterHttpRequestArgument, HttpHeader, HttpMethod, HttpResponse, TransformArgs, TransformContext, }; use num_traits::ToPrimitive; -use tower::Service; +use tower::{BoxError, Service, ServiceBuilder}; pub fn json_rpc_request_arg( service: ResolvedRpcService, @@ -65,7 +66,7 @@ pub async fn http_request( return Err(ValidationError::Custom(format!("Error parsing URL: {}", url)).into()) } }; - let host = match parsed_url.host_str() { + let _host = match parsed_url.host_str() { Some(host) => host, None => { return Err(ValidationError::Custom(format!( @@ -75,39 +76,141 @@ pub async fn http_request( .into()) } }; + http_client(rpc_method).call(request).await +} - let rpc_host = MetricRpcHost(host.to_string()); - add_metric_entry!(requests, (rpc_method.clone(), rpc_host.clone()), 1); - match http_client().call(request).await { - Ok(response) => { - let status: u32 = response.status.0.clone().try_into().unwrap_or(0); - add_metric_entry!(responses, (rpc_method, rpc_host, status.into()), 1); - Ok(response) - } - Err(e) => { - if let Some(charging_error) = e.downcast_ref::() { - return match charging_error { - CyclesAccountingError::InsufficientCyclesError { expected, received } => { - Err(ProviderError::TooFewCycles { - expected: *expected, - received: *received, - } - .into()) +pub fn http_client( + rpc_method: MetricRpcMethod, +) -> impl Service { + ServiceBuilder::new() + .layer( + ObservabilityLayer::new() + .on_request(move |req: &CanisterHttpRequestArgument| { + let req_data = MetricData::new(rpc_method.clone(), req); + add_metric_entry!( + requests, + (req_data.method.clone(), req_data.host.clone()), + 1 + ); + req_data + }) + .on_response(|req_data: MetricData, response: &HttpResponse| { + let status: u32 = response.status.0.clone().try_into().unwrap_or(0); + add_metric_entry!( + responses, + (req_data.method, req_data.host, status.into()), + 1 + ); + }) + .on_error(|req_data: MetricData, error: &RpcError| { + if let RpcError::HttpOutcallError(HttpOutcallError::IcError { + code, + message: _, + }) = error + { + add_metric_entry!( + err_http_outcall, + (req_data.method, req_data.host, *code), + 1 + ); } - }; - } - if let Some(canhttp::IcError { code, message }) = e.downcast_ref::() { - add_metric_entry!(err_http_outcall, (rpc_method, rpc_host, *code), 1); - return Err(HttpOutcallError::IcError { - code: *code, - message: message.clone(), + }), + ) + .map_err(map_error) + .filter(CyclesAccounting::new( + get_num_subnet_nodes(), + ChargingPolicyWithCollateral::default(), + )) + .service(canhttp::Client) +} + +struct MetricData { + method: MetricRpcMethod, + host: MetricRpcHost, +} + +impl MetricData { + pub fn new(method: MetricRpcMethod, request: &CanisterHttpRequestArgument) -> Self { + Self { + method, + host: MetricRpcHost( + url::Url::parse(&request.url) + .unwrap() + .host_str() + .unwrap() + .to_string(), + ), + } + } +} + +fn map_error(e: BoxError) -> RpcError { + if let Some(charging_error) = e.downcast_ref::() { + return match charging_error { + CyclesAccountingError::InsufficientCyclesError { expected, received } => { + ProviderError::TooFewCycles { + expected: *expected, + received: *received, } - .into()); + .into() } - Err(RpcError::ProviderError(ProviderError::InvalidRpcConfig( - format!("Unknown error: {}", e), - ))) + }; + } + if let Some(canhttp::IcError { code, message }) = e.downcast_ref::() { + // add_metric_entry!(err_http_outcall, (rpc_method, rpc_host, *code), 1); + return HttpOutcallError::IcError { + code: *code, + message: message.clone(), + } + .into(); + } + RpcError::ProviderError(ProviderError::InvalidRpcConfig(format!( + "Unknown error: {}", + e + ))) +} + +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct ChargingPolicyWithCollateral { + charge_user: bool, + collateral_cycles: u128, +} + +impl ChargingPolicyWithCollateral { + pub fn new( + num_nodes_in_subnet: u32, + charge_user: bool, + collateral_cycles_per_node: u128, + ) -> Self { + let collateral_cycles = + collateral_cycles_per_node.saturating_mul(num_nodes_in_subnet as u128); + Self { + charge_user, + collateral_cycles, + } + } +} + +impl Default for ChargingPolicyWithCollateral { + fn default() -> Self { + Self::new( + get_num_subnet_nodes(), + !is_demo_active(), + COLLATERAL_CYCLES_PER_NODE, + ) + } +} + +impl CyclesChargingPolicy for ChargingPolicyWithCollateral { + fn cycles_to_charge( + &self, + _request: &CanisterHttpRequestArgument, + attached_cycles: u128, + ) -> u128 { + if self.charge_user { + return attached_cycles.saturating_add(self.collateral_cycles); } + 0 } } diff --git a/src/main.rs b/src/main.rs index f73b1d01..7bee07ac 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,12 +1,12 @@ use candid::candid_method; use canhttp::{CyclesChargingPolicy, CyclesCostEstimator}; use evm_rpc::candid_rpc::CandidRpcClient; -use evm_rpc::http::get_http_response_body; +use evm_rpc::http::{get_http_response_body, ChargingPolicyWithCollateral}; use evm_rpc::logs::INFO; use evm_rpc::memory::{ get_num_subnet_nodes, insert_api_key, is_api_key_principal, is_demo_active, remove_api_key, set_api_key_principals, set_demo_active, set_log_filter, set_num_subnet_nodes, - set_override_provider, ChargingPolicyWithCollateral, + set_override_provider, }; use evm_rpc::metrics::encode_metrics; use evm_rpc::providers::{find_provider, resolve_rpc_service, PROVIDERS, SERVICE_PROVIDER_MAP}; diff --git a/src/memory.rs b/src/memory.rs index f82525ab..0add6978 100644 --- a/src/memory.rs +++ b/src/memory.rs @@ -1,8 +1,5 @@ -use crate::constants::COLLATERAL_CYCLES_PER_NODE; use crate::types::{ApiKey, LogFilter, Metrics, OverrideProvider, ProviderId}; use candid::Principal; -use canhttp::{CyclesAccounting, CyclesChargingPolicy}; -use ic_cdk::api::management_canister::http_request::{CanisterHttpRequestArgument, HttpResponse}; use ic_stable_structures::memory_manager::VirtualMemory; use ic_stable_structures::{ memory_manager::{MemoryId, MemoryManager}, @@ -10,7 +7,6 @@ use ic_stable_structures::{ }; use ic_stable_structures::{Cell, StableBTreeMap}; use std::cell::RefCell; -use tower::{BoxError, Service, ServiceBuilder}; const IS_DEMO_ACTIVE_MEMORY_ID: MemoryId = MemoryId::new(4); const API_KEY_MAP_MEMORY_ID: MemoryId = MemoryId::new(5); @@ -129,60 +125,6 @@ pub fn set_num_subnet_nodes(nodes: u32) { }); } -pub fn http_client( -) -> impl Service { - ServiceBuilder::new() - .filter(CyclesAccounting::new( - get_num_subnet_nodes(), - ChargingPolicyWithCollateral::default(), - )) - .service(canhttp::Client) -} - -#[derive(Debug, Clone, Eq, PartialEq)] -pub struct ChargingPolicyWithCollateral { - charge_user: bool, - collateral_cycles: u128, -} - -impl ChargingPolicyWithCollateral { - pub fn new( - num_nodes_in_subnet: u32, - charge_user: bool, - collateral_cycles_per_node: u128, - ) -> Self { - let collateral_cycles = - collateral_cycles_per_node.saturating_mul(num_nodes_in_subnet as u128); - Self { - charge_user, - collateral_cycles, - } - } -} - -impl Default for ChargingPolicyWithCollateral { - fn default() -> Self { - Self::new( - get_num_subnet_nodes(), - !is_demo_active(), - COLLATERAL_CYCLES_PER_NODE, - ) - } -} - -impl CyclesChargingPolicy for ChargingPolicyWithCollateral { - fn cycles_to_charge( - &self, - _request: &CanisterHttpRequestArgument, - attached_cycles: u128, - ) -> u128 { - if self.charge_user { - return attached_cycles.saturating_add(self.collateral_cycles); - } - 0 - } -} - #[cfg(test)] mod test { use candid::Principal; From f0f3910a08e94f8f88498ecfdd0237a263cf7cac Mon Sep 17 00:00:00 2001 From: gregorydemay Date: Fri, 14 Feb 2025 17:32:59 +0100 Subject: [PATCH 04/17] docs: added docs --- canhttp/src/observability/mod.rs | 103 ++++++++++++++++++++----------- 1 file changed, 68 insertions(+), 35 deletions(-) diff --git a/canhttp/src/observability/mod.rs b/canhttp/src/observability/mod.rs index aacc0a75..edb45f3c 100644 --- a/canhttp/src/observability/mod.rs +++ b/canhttp/src/observability/mod.rs @@ -1,10 +1,29 @@ +//! Middleware that adds high level observability (e.g., logging, metrics) to a [`Service`]. +//! +//! # Comparison with [`tower_http::trace`]. +//! This middleware is strongly inspired by the functionality offered by [`tower_http::trace`]. +//! The reason for not using this middleware directly is it cannot be used inside a canister: +//! 1. The [`tower_http::Trace`] service measures the latency of a call by calling +//! [`Instant::now`](https://github.com/tower-rs/tower-http/blob/469bdac3193ed22da9ea524a454d8cda93ffa0d5/tower-http/src/trace/service.rs#L302), +//! which will fail when run from a canister. +//! 2. The [`tower_http::Trace`] can deal with streaming responses, which is unnecessary for HTTPs outcalls, +//! since the response is available to a canister at once. This flexibility brings some complexity +//! (body can only be fetched asynchronously, end of stream errors, etc.) which is not useful in a canister environment. +//! +//! [`Service`]: tower::Service + use pin_project::pin_project; use std::future::Future; use std::pin::Pin; use std::task::{Context, Poll}; use tower::{Layer, Service}; -/// TODO +/// [`Layer`] that adds high level observability to a [`Service`]. +/// +/// See the [module docs](crate::observability) for more details. +/// +/// [`Layer`]: tower::Layer +/// [`Service`]: tower::Service pub struct ObservabilityLayer { on_request: OnRequest, on_response: OnResponse, @@ -12,7 +31,7 @@ pub struct ObservabilityLayer { } impl ObservabilityLayer<(), (), ()> { - /// TODO + /// Creates a new [`ObservabilityLayer`] that does nothing. pub fn new() -> Self { Self { on_request: (), @@ -29,7 +48,9 @@ impl Default for ObservabilityLayer<(), (), ()> { } impl ObservabilityLayer { - /// TODO + /// Customize what to do when a request is received. + /// + /// `NewOnRequest` is expected to implement [`RequestObserver`]. pub fn on_request( self, new_on_request: NewOnRequest, @@ -41,7 +62,9 @@ impl ObservabilityLayer( self, new_on_response: NewOnResponse, @@ -53,7 +76,9 @@ impl ObservabilityLayer( self, new_on_error: NewOnError, @@ -85,7 +110,11 @@ where } } -/// TODO +/// Middleware that adds high level observability to a [`Service`]. +/// +/// See the [module docs](crate::observability) for an example. +/// +/// [`Service`]: tower::Service pub struct Observability { inner: S, on_request: OnRequest, @@ -120,9 +149,39 @@ where } } -///TODO +/// Trait used to tell [`Observability`] what to do when a request is received. +pub trait RequestObserver { + /// Type of data that can be observed from the request (e.g., URL, host, etc.) + /// when the response will be processed. + type ObservableRequestData; + + /// Observe the given request and produce observable data based on the request. + /// This observable data will be passed on to the response observer. + fn observe_request(&self, request: &Request) -> Self::ObservableRequestData; +} + +impl RequestObserver for () { + type ObservableRequestData = (); + + fn observe_request(&self, _request: &Request) -> Self::ObservableRequestData { + //NOP + } +} + +impl RequestObserver for F +where + F: Fn(&Request) -> RequestData, +{ + type ObservableRequestData = RequestData; + + fn observe_request(&self, request: &Request) -> Self::ObservableRequestData { + self(request) + } +} + +/// Trait used to tell [`Observability`] what to do when a response is received. pub trait ResponseObserver { - ///TODO + /// Observe the response (typically an instance of [`std::Result`] and the request data produced by a [`RequestObserver`]. fn observe(&self, request_data: RequestData, value: &Result); } @@ -141,6 +200,7 @@ where } } +/// Response future for [`Observability`]. #[pin_project] pub struct ResponseFuture { #[pin] @@ -179,30 +239,3 @@ where result_fut } } - -/// TODO -pub trait RequestObserver { - /// TODO - type ObservableRequestData; - /// TODO - fn observe_request(&self, request: &Request) -> Self::ObservableRequestData; -} - -impl RequestObserver for () { - type ObservableRequestData = (); - - fn observe_request(&self, _request: &Request) -> Self::ObservableRequestData { - //NOP - } -} - -impl RequestObserver for F -where - F: Fn(&Request) -> RequestData, -{ - type ObservableRequestData = RequestData; - - fn observe_request(&self, request: &Request) -> Self::ObservableRequestData { - self(request) - } -} From f848b3090db6f1f8ad61806e2600ced14449e190 Mon Sep 17 00:00:00 2001 From: gregorydemay <112856886+gregorydemay@users.noreply.github.com> Date: Mon, 24 Feb 2025 10:36:29 +0100 Subject: [PATCH 05/17] Expose service type and trait Co-authored-by: Louis Pahlavi --- canhttp/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/canhttp/src/lib.rs b/canhttp/src/lib.rs index 043db8ee..9a14e7ef 100644 --- a/canhttp/src/lib.rs +++ b/canhttp/src/lib.rs @@ -9,7 +9,7 @@ pub use client::{Client, IcError}; pub use cycles::{ CyclesAccounting, CyclesAccountingError, CyclesChargingPolicy, CyclesCostEstimator, }; -pub use observability::ObservabilityLayer; +pub use observability::{Observability, ObservabilityLayer, RequestObserver, ResponseObserver}; mod client; mod cycles; From 662c741934abb32645a75932c6e31be6663b3e14 Mon Sep 17 00:00:00 2001 From: gregorydemay Date: Mon, 24 Feb 2025 10:51:08 +0100 Subject: [PATCH 06/17] ci: added cargo doc job --- .github/workflows/ci.yml | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8dd94fbd..b7421a3a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,6 +9,10 @@ on: - v* paths-ignore: - "README.md" +env: + CARGO_TERM_COLOR: always + RUSTFLAGS: "-Dwarnings" + jobs: cargo-clippy: runs-on: ubuntu-latest @@ -28,6 +32,22 @@ jobs: cargo clippy -- -D clippy::all -D warnings -A clippy::manual_range_contains cargo clippy --tests --benches -- -D clippy::all -D warnings -A clippy::manual_range_contains + cargo-doc: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@master + + - uses: Swatinem/rust-cache@v2 + + - run: rustup component add clippy + + - name: Cargo doc + run: | + cargo doc + env: + RUSTDOCFLAGS: "--deny warnings" + reproducible-build: runs-on: ubuntu-22.04 steps: From e7f8bff2a3d13889d4eadfe560561e001ad39696 Mon Sep 17 00:00:00 2001 From: gregorydemay Date: Mon, 24 Feb 2025 11:03:16 +0100 Subject: [PATCH 07/17] refactor: make observability module public --- canhttp/src/lib.rs | 3 +-- src/http.rs | 5 ++++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/canhttp/src/lib.rs b/canhttp/src/lib.rs index 9a14e7ef..ff34fdd8 100644 --- a/canhttp/src/lib.rs +++ b/canhttp/src/lib.rs @@ -9,8 +9,7 @@ pub use client::{Client, IcError}; pub use cycles::{ CyclesAccounting, CyclesAccountingError, CyclesChargingPolicy, CyclesCostEstimator, }; -pub use observability::{Observability, ObservabilityLayer, RequestObserver, ResponseObserver}; mod client; mod cycles; -mod observability; +pub mod observability; diff --git a/src/http.rs b/src/http.rs index 0551c997..e776655e 100644 --- a/src/http.rs +++ b/src/http.rs @@ -7,7 +7,10 @@ use crate::{ types::{MetricRpcHost, MetricRpcMethod, ResolvedRpcService}, util::canonicalize_json, }; -use canhttp::{CyclesAccounting, CyclesAccountingError, CyclesChargingPolicy, ObservabilityLayer}; +use canhttp::{ + observability::ObservabilityLayer, CyclesAccounting, CyclesAccountingError, + CyclesChargingPolicy, +}; use evm_rpc_types::{HttpOutcallError, ProviderError, RpcError, RpcResult, ValidationError}; use ic_cdk::api::management_canister::http_request::{ CanisterHttpRequestArgument, HttpHeader, HttpMethod, HttpResponse, TransformArgs, From 50d4afd9a1a4125320b2aa2c2085e54096dafabe Mon Sep 17 00:00:00 2001 From: gregorydemay Date: Mon, 24 Feb 2025 11:11:55 +0100 Subject: [PATCH 08/17] doc: fix links in observability module --- canhttp/src/observability/mod.rs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/canhttp/src/observability/mod.rs b/canhttp/src/observability/mod.rs index edb45f3c..daa12a4b 100644 --- a/canhttp/src/observability/mod.rs +++ b/canhttp/src/observability/mod.rs @@ -1,16 +1,17 @@ //! Middleware that adds high level observability (e.g., logging, metrics) to a [`Service`]. //! -//! # Comparison with [`tower_http::trace`]. -//! This middleware is strongly inspired by the functionality offered by [`tower_http::trace`]. +//! # Comparison with the `Trace` service of the [`tower_http`] crate. +//! This middleware is strongly inspired by the functionality offered by `Trace`. //! The reason for not using this middleware directly is it cannot be used inside a canister: -//! 1. The [`tower_http::Trace`] service measures the latency of a call by calling +//! 1. It measures the latency of a call by calling //! [`Instant::now`](https://github.com/tower-rs/tower-http/blob/469bdac3193ed22da9ea524a454d8cda93ffa0d5/tower-http/src/trace/service.rs#L302), //! which will fail when run from a canister. -//! 2. The [`tower_http::Trace`] can deal with streaming responses, which is unnecessary for HTTPs outcalls, +//! 2. It can deal with streaming responses, which is unnecessary for HTTPs outcalls, //! since the response is available to a canister at once. This flexibility brings some complexity //! (body can only be fetched asynchronously, end of stream errors, etc.) which is not useful in a canister environment. //! //! [`Service`]: tower::Service +//! [`tower_http`]: https://crates.io/crates/tower-http use pin_project::pin_project; use std::future::Future; @@ -181,7 +182,7 @@ where /// Trait used to tell [`Observability`] what to do when a response is received. pub trait ResponseObserver { - /// Observe the response (typically an instance of [`std::Result`] and the request data produced by a [`RequestObserver`]. + /// Observe the response (typically an instance of [`std::result::Result`] and the request data produced by a [`RequestObserver`]. fn observe(&self, request_data: RequestData, value: &Result); } From e624cd8aeb343e7e24fbb80c1e7705706545cb88 Mon Sep 17 00:00:00 2001 From: gregorydemay Date: Mon, 24 Feb 2025 11:13:01 +0100 Subject: [PATCH 09/17] doc: fix links --- evm_rpc_types/src/request/mod.rs | 2 +- evm_rpc_types/src/response/mod.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/evm_rpc_types/src/request/mod.rs b/evm_rpc_types/src/request/mod.rs index a9abf057..f654a0f0 100644 --- a/evm_rpc_types/src/request/mod.rs +++ b/evm_rpc_types/src/request/mod.rs @@ -62,7 +62,7 @@ pub struct GetTransactionCountArgs { pub struct CallArgs { pub transaction: TransactionRequest, /// Integer block number, or "latest" for the last mined block or "pending", "earliest" for not yet mined transactions. - /// Default to "latest" if unspecified, see https://github.com/ethereum/execution-apis/issues/461. + /// Default to "latest" if unspecified, see . pub block: Option, } diff --git a/evm_rpc_types/src/response/mod.rs b/evm_rpc_types/src/response/mod.rs index f44cb9d4..a9fb2991 100644 --- a/evm_rpc_types/src/response/mod.rs +++ b/evm_rpc_types/src/response/mod.rs @@ -193,7 +193,7 @@ pub struct Block { /// Total difficulty is the sum of all difficulty values up to and including this block. /// /// Note: this field was removed from the official JSON-RPC specification in - /// https://github.com/ethereum/execution-apis/pull/570 and may no longer be served by providers. + /// and may no longer be served by providers. #[serde(rename = "totalDifficulty")] pub total_difficulty: Option, From 50c73ad2443b36a4d5ee4ee8be50407fa78dee04 Mon Sep 17 00:00:00 2001 From: gregorydemay Date: Mon, 24 Feb 2025 13:21:59 +0100 Subject: [PATCH 10/17] doc: only build necessary docs --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b7421a3a..e06060e0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -44,7 +44,7 @@ jobs: - name: Cargo doc run: | - cargo doc + cargo doc --workspace --no-deps env: RUSTDOCFLAGS: "--deny warnings" From dded7758dcd4465d45f4b94b2516eddb5b5adf6e Mon Sep 17 00:00:00 2001 From: gregorydemay Date: Mon, 24 Feb 2025 15:07:23 +0100 Subject: [PATCH 11/17] added example --- Cargo.lock | 13 +++---- Cargo.toml | 1 + canhttp/Cargo.toml | 5 ++- canhttp/src/observability/mod.rs | 60 ++++++++++++++++++++++++++++++++ 4 files changed, 72 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c8b2b14c..9f7aec5f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -310,6 +310,7 @@ dependencies = [ "ic-cdk", "pin-project", "thiserror 2.0.11", + "tokio", "tower", ] @@ -1454,9 +1455,9 @@ checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67" [[package]] name = "libc" -version = "0.2.158" +version = "0.2.170" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439" +checksum = "875b3680cb2f8f71bdcf9a30f38d48282f5d3c95cbf9b3fa57269bb5d5c06828" [[package]] name = "libredox" @@ -2984,9 +2985,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.42.0" +version = "1.43.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cec9b21b0450273377fc97bd4c33a8acffc8c996c987a7c5b319a0083707551" +checksum = "3d61fa4ffa3de412bfea335c6ecff681de2b609ba3c77ef3e00e521813a9ed9e" dependencies = [ "backtrace", "bytes", @@ -3002,9 +3003,9 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index abcd98be..8b3888d0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -78,6 +78,7 @@ serde = "1.0" serde_json = "1.0" serde_bytes = "0.11.15" strum = { version = "0.26", features = ["derive"] } +tokio = "1.43.0" tower = "0.5.2" thiserror = "2.0.11" url = "2.5" diff --git a/canhttp/Cargo.toml b/canhttp/Cargo.toml index df0d05a1..fbcddf0d 100644 --- a/canhttp/Cargo.toml +++ b/canhttp/Cargo.toml @@ -13,4 +13,7 @@ documentation = "https://docs.rs/canhttp" ic-cdk = { workspace = true } pin-project = {workspace = true} tower = { workspace = true, features = ["filter", "retry"] } -thiserror = { workspace = true } \ No newline at end of file +thiserror = { workspace = true } + +[dev-dependencies] +tokio = {workspace = true, features = ["full"]} \ No newline at end of file diff --git a/canhttp/src/observability/mod.rs b/canhttp/src/observability/mod.rs index daa12a4b..63e52139 100644 --- a/canhttp/src/observability/mod.rs +++ b/canhttp/src/observability/mod.rs @@ -10,6 +10,66 @@ //! since the response is available to a canister at once. This flexibility brings some complexity //! (body can only be fetched asynchronously, end of stream errors, etc.) which is not useful in a canister environment. //! +//! # Examples +//! +//! To add a basic observability layer, for example tracking the number of request and responses/errors inside a canister: +//! +//! ```rust +//! use canhttp::{IcError, observability::ObservabilityLayer}; +//! use ic_cdk::api::management_canister::http_request::{CanisterHttpRequestArgument as IcHttpRequest, HttpResponse as IcHttpResponse}; +//! use tower::{Service, ServiceBuilder, ServiceExt}; +//! use std::cell::RefCell; +//! +//! async fn handle(request: IcHttpRequest) -> Result { +//! Ok(IcHttpResponse::default()) +//! } +//! +//! #[derive(Clone, Debug, Default, PartialEq, Eq)] +//! pub struct Metrics { +//! pub num_requests: u64, +//! pub num_responses: u64, +//! pub num_errors: u64 +//! } +//! +//! thread_local! { +//! static METRICS: RefCell = RefCell::new(Metrics::default()) +//! } +//! +//! # #[tokio::main] +//! # async fn main() -> Result<(), Box> { +//! +//! let mut service = ServiceBuilder::new() +//! .layer(ObservabilityLayer::new() +//! .on_request(|req: &IcHttpRequest| { +//! METRICS.with_borrow_mut(|m| m.num_requests += 1); +//! }) +//! .on_response(|req_data: (), response: &IcHttpResponse| { +//! METRICS.with_borrow_mut(|m| m.num_responses += 1); +//! }) +//! .on_error(|req_data: (), response: &IcError| { +//! METRICS.with_borrow_mut(|m| m.num_errors += 1); +//! }) +//! ) +//! .service_fn(handle); +//! +//! let request = IcHttpRequest::default(); +//! +//! let response = service +//! .ready() +//! .await? +//! .call(request) +//! .await?; +//! +//! METRICS.with(|m| { +//! let m = m.borrow(); +//! assert_eq!(m.num_requests, 1); +//! assert_eq!(m.num_responses, 1); +//! assert_eq!(m.num_errors, 0); +//! }); +//! # Ok(()) +//! # } +//! ``` +//! //! [`Service`]: tower::Service //! [`tower_http`]: https://crates.io/crates/tower-http From fb73afb2a6cb5235524f0308f63145cb50b81dc7 Mon Sep 17 00:00:00 2001 From: gregorydemay Date: Mon, 24 Feb 2025 15:09:44 +0100 Subject: [PATCH 12/17] fix unmatched bracket. --- canhttp/src/observability/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/canhttp/src/observability/mod.rs b/canhttp/src/observability/mod.rs index 63e52139..bb041073 100644 --- a/canhttp/src/observability/mod.rs +++ b/canhttp/src/observability/mod.rs @@ -242,7 +242,7 @@ where /// Trait used to tell [`Observability`] what to do when a response is received. pub trait ResponseObserver { - /// Observe the response (typically an instance of [`std::result::Result`] and the request data produced by a [`RequestObserver`]. + /// Observe the response (typically an instance of [`std::result::Result`]) and the request data produced by a [`RequestObserver`]. fn observe(&self, request_data: RequestData, value: &Result); } From 9e416076d271bbc6065df3768401d3c604dbcb31 Mon Sep 17 00:00:00 2001 From: gregorydemay Date: Mon, 24 Feb 2025 15:15:11 +0100 Subject: [PATCH 13/17] rename generic parameter for ResponseObserver --- canhttp/src/observability/mod.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/canhttp/src/observability/mod.rs b/canhttp/src/observability/mod.rs index bb041073..9a88e65f 100644 --- a/canhttp/src/observability/mod.rs +++ b/canhttp/src/observability/mod.rs @@ -11,7 +11,7 @@ //! (body can only be fetched asynchronously, end of stream errors, etc.) which is not useful in a canister environment. //! //! # Examples -//! +//! //! To add a basic observability layer, for example tracking the number of request and responses/errors inside a canister: //! //! ```rust @@ -241,22 +241,22 @@ where } /// Trait used to tell [`Observability`] what to do when a response is received. -pub trait ResponseObserver { +pub trait ResponseObserver { /// Observe the response (typically an instance of [`std::result::Result`]) and the request data produced by a [`RequestObserver`]. - fn observe(&self, request_data: RequestData, value: &Result); + fn observe(&self, request_data: RequestData, value: &Response); } -impl ResponseObserver for () { - fn observe(&self, _request_data: ReqData, _value: &Result) { +impl ResponseObserver for () { + fn observe(&self, _request_data: RequestData, _value: &Response) { //NOP } } -impl ResponseObserver for F +impl ResponseObserver for F where - F: Fn(ReqData, &T), + F: Fn(RequestData, &Response), { - fn observe(&self, request_data: ReqData, value: &T) { + fn observe(&self, request_data: RequestData, value: &Response) { self(request_data, value); } } From f202e2c08d174230eb8aab530e158fad6aa9489b Mon Sep 17 00:00:00 2001 From: gregorydemay Date: Mon, 24 Feb 2025 15:24:49 +0100 Subject: [PATCH 14/17] rename trait method --- canhttp/src/observability/mod.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/canhttp/src/observability/mod.rs b/canhttp/src/observability/mod.rs index 9a88e65f..c3e6bd8f 100644 --- a/canhttp/src/observability/mod.rs +++ b/canhttp/src/observability/mod.rs @@ -243,11 +243,11 @@ where /// Trait used to tell [`Observability`] what to do when a response is received. pub trait ResponseObserver { /// Observe the response (typically an instance of [`std::result::Result`]) and the request data produced by a [`RequestObserver`]. - fn observe(&self, request_data: RequestData, value: &Response); + fn observe_response(&self, request_data: RequestData, value: &Response); } impl ResponseObserver for () { - fn observe(&self, _request_data: RequestData, _value: &Response) { + fn observe_response(&self, _request_data: RequestData, _value: &Response) { //NOP } } @@ -256,7 +256,7 @@ impl ResponseObserver for F where F: Fn(RequestData, &Response), { - fn observe(&self, request_data: RequestData, value: &Response) { + fn observe_response(&self, request_data: RequestData, value: &Response) { self(request_data, value); } } @@ -288,10 +288,10 @@ where let request_data = this.request_data.take().unwrap(); match result { Ok(response) => { - this.on_response.observe(request_data, response); + this.on_response.observe_response(request_data, response); } Err(error) => { - this.on_error.observe(request_data, error); + this.on_error.observe_response(request_data, error); } } } From 860e009a2128988e305c4b368bed991dc7642fee Mon Sep 17 00:00:00 2001 From: gregorydemay Date: Mon, 24 Feb 2025 15:26:08 +0100 Subject: [PATCH 15/17] remove commented line --- src/http.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/http.rs b/src/http.rs index e776655e..fe07e551 100644 --- a/src/http.rs +++ b/src/http.rs @@ -160,7 +160,6 @@ fn map_error(e: BoxError) -> RpcError { }; } if let Some(canhttp::IcError { code, message }) = e.downcast_ref::() { - // add_metric_entry!(err_http_outcall, (rpc_method, rpc_host, *code), 1); return HttpOutcallError::IcError { code: *code, message: message.clone(), From ce5313c7718370304e1817162f939c41ad5ca29b Mon Sep 17 00:00:00 2001 From: gregorydemay Date: Tue, 25 Feb 2025 09:01:22 +0100 Subject: [PATCH 16/17] added example with request data --- Cargo.lock | 1 + Cargo.toml | 1 + canhttp/Cargo.toml | 1 + canhttp/src/observability/mod.rs | 99 ++++++++++++++++++++++++++++++-- 4 files changed, 96 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9f7aec5f..251bf8a2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -308,6 +308,7 @@ name = "canhttp" version = "0.1.0" dependencies = [ "ic-cdk", + "maplit", "pin-project", "thiserror 2.0.11", "tokio", diff --git a/Cargo.toml b/Cargo.toml index 8b3888d0..d6fd1ea5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -70,6 +70,7 @@ ic-cdk-macros = "0.17.1" ic-certified-map = "0.4" ic-metrics-encoder = "1.1" ic-stable-structures = "0.6.7" +maplit = "1.0.2" minicbor = { version = "0.25.1", features = ["alloc", "derive"] } num-bigint = "0.4.6" pin-project = "1.1.9" diff --git a/canhttp/Cargo.toml b/canhttp/Cargo.toml index fbcddf0d..7ae2c005 100644 --- a/canhttp/Cargo.toml +++ b/canhttp/Cargo.toml @@ -16,4 +16,5 @@ tower = { workspace = true, features = ["filter", "retry"] } thiserror = { workspace = true } [dev-dependencies] +maplit = {workspace = true} tokio = {workspace = true, features = ["full"]} \ No newline at end of file diff --git a/canhttp/src/observability/mod.rs b/canhttp/src/observability/mod.rs index c3e6bd8f..6887e1cc 100644 --- a/canhttp/src/observability/mod.rs +++ b/canhttp/src/observability/mod.rs @@ -59,13 +59,100 @@ //! .await? //! .call(request) //! .await?; +//! +//! let metrics = METRICS.with_borrow(|m| m.clone()); +//! assert_eq!( +//! metrics, +//! Metrics { +//! num_requests: 1, +//! num_responses: 1, +//! num_errors: 0 +//! } +//! ); +//! # Ok(()) +//! # } +//! ``` +//! +//! The previous example can be refined by extracting request data (such as the request URL) to observe the responses/errors: +//! ```rust +//! use canhttp::{IcError, observability::ObservabilityLayer}; +//! use ic_cdk::api::management_canister::http_request::{CanisterHttpRequestArgument as IcHttpRequest, HttpResponse as IcHttpResponse}; +//! use maplit::btreemap; +//! use tower::{Service, ServiceBuilder, ServiceExt}; +//! use std::cell::RefCell; +//! use std::collections::BTreeMap; +//! +//! async fn handle(request: IcHttpRequest) -> Result { +//! Ok(IcHttpResponse::default()) +//! } +//! +//! pub type Url = String; +//! +//! #[derive(Clone, Debug, Default, PartialEq, Eq)] +//! pub struct Metrics { +//! pub num_requests: BTreeMap, +//! pub num_responses: BTreeMap, +//! pub num_errors: BTreeMap +//! } +//! +//! thread_local! { +//! static METRICS: RefCell = RefCell::new(Metrics::default()) +//! } +//! +//! # #[tokio::main] +//! # async fn main() -> Result<(), Box> { +//! +//! let mut service = ServiceBuilder::new() +//! .layer( +//! ObservabilityLayer::new() +//! .on_request(|req: &IcHttpRequest| { +//! METRICS.with_borrow_mut(|m| { +//! m.num_requests +//! .entry(req.url.clone()) +//! .and_modify(|c| *c += 1) +//! .or_insert(1); +//! }); +//! req.url.clone() //First parameter in on_response/on_error +//! }) +//! .on_response(|req_data: Url, response: &IcHttpResponse| { +//! METRICS.with_borrow_mut(|m| { +//! m.num_responses +//! .entry(req_data) +//! .and_modify(|c| *c += 1) +//! .or_insert(1); +//! }); +//! }) +//! .on_error(|req_data: Url, response: &IcError| { +//! METRICS.with_borrow_mut(|m| { +//! m.num_errors +//! .entry(req_data) +//! .and_modify(|c| *c += 1) +//! .or_insert(1); +//! }); +//! }), +//! ) +//! .service_fn(handle); +//! +//! let request = IcHttpRequest { +//! url: "https://internetcomputer.org/".to_string(), +//! ..Default::default() +//! }; +//! +//! let response = service +//! .ready() +//! .await? +//! .call(request) +//! .await?; //! -//! METRICS.with(|m| { -//! let m = m.borrow(); -//! assert_eq!(m.num_requests, 1); -//! assert_eq!(m.num_responses, 1); -//! assert_eq!(m.num_errors, 0); -//! }); +//! let metrics = METRICS.with_borrow(|m| m.clone()); +//! assert_eq!( +//! metrics, +//! Metrics { +//! num_requests: btreemap! {"https://internetcomputer.org/".to_string() => 1}, +//! num_responses: btreemap! {"https://internetcomputer.org/".to_string() => 1}, +//! num_errors: btreemap! {} +//! } +//! ); //! # Ok(()) //! # } //! ``` From 9bac90017306a33bda12c6416232c7e37b42d292 Mon Sep 17 00:00:00 2001 From: gregorydemay Date: Tue, 25 Feb 2025 09:09:58 +0100 Subject: [PATCH 17/17] remove whitespace --- canhttp/src/observability/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/canhttp/src/observability/mod.rs b/canhttp/src/observability/mod.rs index 6887e1cc..1ee19261 100644 --- a/canhttp/src/observability/mod.rs +++ b/canhttp/src/observability/mod.rs @@ -59,7 +59,7 @@ //! .await? //! .call(request) //! .await?; -//! +//! //! let metrics = METRICS.with_borrow(|m| m.clone()); //! assert_eq!( //! metrics,