diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8dd94fbd..e06060e0 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 --workspace --no-deps + env: + RUSTDOCFLAGS: "--deny warnings" + reproducible-build: runs-on: ubuntu-22.04 steps: diff --git a/Cargo.lock b/Cargo.lock index a282eab4..251bf8a2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -308,7 +308,10 @@ name = "canhttp" version = "0.1.0" dependencies = [ "ic-cdk", + "maplit", + "pin-project", "thiserror 2.0.11", + "tokio", "tower", ] @@ -1453,9 +1456,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" @@ -1874,6 +1877,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" @@ -2963,9 +2986,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", @@ -2981,9 +3004,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 e3187a7f..d6fd1ea5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -70,13 +70,16 @@ 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" proptest = "1.6.0" 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 ec2ab67a..7ae2c005 100644 --- a/canhttp/Cargo.toml +++ b/canhttp/Cargo.toml @@ -11,5 +11,10 @@ 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 +thiserror = { workspace = true } + +[dev-dependencies] +maplit = {workspace = true} +tokio = {workspace = true, features = ["full"]} \ No newline at end of file diff --git a/canhttp/src/lib.rs b/canhttp/src/lib.rs index 7fae0e83..ff34fdd8 100644 --- a/canhttp/src/lib.rs +++ b/canhttp/src/lib.rs @@ -12,3 +12,4 @@ pub use cycles::{ mod client; mod cycles; +pub mod observability; diff --git a/canhttp/src/observability/mod.rs b/canhttp/src/observability/mod.rs new file mode 100644 index 00000000..1ee19261 --- /dev/null +++ b/canhttp/src/observability/mod.rs @@ -0,0 +1,389 @@ +//! Middleware that adds high level observability (e.g., logging, metrics) to a [`Service`]. +//! +//! # 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. 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. 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. +//! +//! # 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?; +//! +//! 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?; +//! +//! 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(()) +//! # } +//! ``` +//! +//! [`Service`]: tower::Service +//! [`tower_http`]: https://crates.io/crates/tower-http + +use pin_project::pin_project; +use std::future::Future; +use std::pin::Pin; +use std::task::{Context, Poll}; +use tower::{Layer, Service}; + +/// [`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, + on_error: OnError, +} + +impl ObservabilityLayer<(), (), ()> { + /// Creates a new [`ObservabilityLayer`] that does nothing. + pub fn new() -> Self { + Self { + on_request: (), + on_response: (), + on_error: (), + } + } +} + +impl Default for ObservabilityLayer<(), (), ()> { + fn default() -> Self { + Self::new() + } +} + +impl ObservabilityLayer { + /// Customize what to do when a request is received. + /// + /// `NewOnRequest` is expected to implement [`RequestObserver`]. + 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, + } + } + + /// Customize what to do when a response has been produced. + /// + /// `NewOnResponse` is expected to implement [`ResponseObserver`]. + 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, + } + } + + /// Customize what to do when an error has been produced. + /// + /// `NewOnError` is expected to implement [`ResponseObserver`]. + 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(), + } + } +} + +/// 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, + 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(), + } + } +} + +/// 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 { + /// Observe the response (typically an instance of [`std::result::Result`]) and the request data produced by a [`RequestObserver`]. + fn observe_response(&self, request_data: RequestData, value: &Response); +} + +impl ResponseObserver for () { + fn observe_response(&self, _request_data: RequestData, _value: &Response) { + //NOP + } +} + +impl ResponseObserver for F +where + F: Fn(RequestData, &Response), +{ + fn observe_response(&self, request_data: RequestData, value: &Response) { + self(request_data, value); + } +} + +/// Response future for [`Observability`]. +#[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_response(request_data, response); + } + Err(error) => { + this.on_error.observe_response(request_data, error); + } + } + } + Poll::Pending => {} + } + result_fut + } +} 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, diff --git a/src/http.rs b/src/http.rs index e0940046..fe07e551 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,17 @@ use crate::{ types::{MetricRpcHost, MetricRpcMethod, ResolvedRpcService}, util::canonicalize_json, }; -use canhttp::CyclesAccountingError; +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, TransformContext, }; use num_traits::ToPrimitive; -use tower::Service; +use tower::{BoxError, Service, ServiceBuilder}; pub fn json_rpc_request_arg( service: ResolvedRpcService, @@ -65,7 +69,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 +79,140 @@ 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::() { + 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; 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();