diff --git a/Cargo.toml b/Cargo.toml index 27d779b..36f6fc8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,7 +5,7 @@ description = "Simple, modern, ergonomic JSON-RPC 2.0 router built with tower an keywords = ["json-rpc", "jsonrpc", "json"] categories = ["web-programming::http-server", "web-programming::websocket"] -version = "0.3.4" +version = "0.4.0" edition = "2021" rust-version = "1.81" authors = ["init4", "James Prestwich"] @@ -15,6 +15,7 @@ repository = "https://github.com/init4tech/ajj" [dependencies] bytes = "1.9.0" +opentelemetry = "0.31.0" pin-project = "1.1.8" serde = { version = "1.0.217", features = ["derive"] } serde_json = { version = "1.0.135", features = ["raw_value"] } @@ -23,10 +24,12 @@ tokio = { version = "1.43.0", features = ["sync", "rt", "macros"] } tokio-util = { version = "0.7.13", features = ["io", "rt"] } tower = { version = "0.5.2", features = ["util"] } tracing = "0.1.41" +tracing-opentelemetry = "0.32.0" # axum axum = { version = "0.8.1", optional = true } mime = { version = "0.3.17", optional = true } +opentelemetry-http = { version = "0.31.0", optional = true } # pubsub tokio-stream = { version = "0.1.17", optional = true } @@ -37,6 +40,7 @@ interprocess = { version = "2.2.2", features = ["async", "tokio"], optional = tr # ws tokio-tungstenite = { version = "0.26.1", features = ["rustls-tls-webpki-roots"], optional = true } futures-util = { version = "0.3.31", optional = true } +metrics = "0.24.2" [dev-dependencies] ajj = { path = "./", features = ["axum", "ws", "ipc"] } @@ -51,7 +55,7 @@ eyre = "0.6.12" [features] default = ["axum", "ws", "ipc"] -axum = ["dep:axum", "dep:mime"] +axum = ["dep:axum", "dep:mime", "dep:opentelemetry-http"] pubsub = ["dep:tokio-stream", "axum?/ws"] ipc = ["pubsub", "dep:interprocess"] ws = ["pubsub", "dep:tokio-tungstenite", "dep:futures-util"] diff --git a/README.md b/README.md index dfaa536..22ae42e 100644 --- a/README.md +++ b/README.md @@ -71,6 +71,23 @@ implementations. See the [crate documentation on docs.rs] for more detailed examples. +## Specification Complinace + +`ajj` aims to be fully compliant with the [JSON-RPC 2.0] specification. If any +issues are found, please [open an issue]! + +`ajj` produces [`tracing`] spans and events that meet the [OpenTelemetry +semantic conventions] for JSON-RPC servers with the following exceptions: + +- The `server.address` attribute is NOT set, as the server address is not always + known to the ajj system. +- `rpc.message` events are included in AJJ system spans for the batch request, + which technically does not comply with semantic conventions. The semantic + conventions do not specify how to handle batch requests, and assume that each + message corresponds to a separate request. In AJJ, batch requests are a single + message, and result in a single `rpc.message` event at receipt and at + response. + ## Note on code provenance Some code in this project has been reproduced or adapted from other projects. @@ -94,3 +111,6 @@ reproduced from the following projects, and we are grateful for their work: [`interprocess::local_socket::ListenerOptions`]: https://docs.rs/interprocess/latest/interprocess/local_socket/struct.ListenerOptions.html [std::net::SocketAddr]: https://doc.rust-lang.org/std/net/enum.SocketAddr.html [alloy]: https://docs.rs/alloy/latest/alloy/ +[open an issue]: https://github.com/init4tech/ajj/issues/new +[OpenTelemetry semantic conventions]: https://opentelemetry.io/docs/specs/semconv/rpc/json-rpc/ +[`tracing`]: https://docs.rs/tracing/latest/tracing/ diff --git a/src/axum.rs b/src/axum.rs index 7bb337e..3e4e74c 100644 --- a/src/axum.rs +++ b/src/axum.rs @@ -1,6 +1,6 @@ use crate::{ types::{InboundData, Response}, - HandlerCtx, TaskSet, + HandlerCtx, TaskSet, TracingInfo, }; use axum::{ extract::FromRequest, @@ -8,8 +8,13 @@ use axum::{ response::IntoResponse, }; use bytes::Bytes; -use std::{future::Future, pin::Pin}; +use std::{ + future::Future, + pin::Pin, + sync::{atomic::AtomicU32, Arc}, +}; use tokio::runtime::Handle; +use tracing::{Instrument, Span}; /// A wrapper around an [`Router`] that implements the /// [`axum::handler::Handler`] trait. This struct is an implementation detail @@ -21,7 +26,13 @@ use tokio::runtime::Handle; #[derive(Debug, Clone)] pub(crate) struct IntoAxum { pub(crate) router: crate::Router, + pub(crate) task_set: TaskSet, + + /// Counter for OTEL messages received. + pub(crate) rx_msg_id: Arc, + /// Counter for OTEL messages sent. + pub(crate) tx_msg_id: Arc, } impl From> for IntoAxum { @@ -29,6 +40,8 @@ impl From> for IntoAxum { Self { router, task_set: Default::default(), + rx_msg_id: Arc::new(AtomicU32::new(1)), + tx_msg_id: Arc::new(AtomicU32::new(1)), } } } @@ -39,12 +52,26 @@ impl IntoAxum { Self { router, task_set: handle.into(), + rx_msg_id: Arc::new(AtomicU32::new(1)), + tx_msg_id: Arc::new(AtomicU32::new(1)), } } +} - /// Get a new context, built from the task set. - fn ctx(&self) -> HandlerCtx { - self.task_set.clone().into() +impl IntoAxum +where + S: Clone + Send + Sync + 'static, +{ + fn ctx(&self, req: &axum::extract::Request) -> HandlerCtx { + let parent_context = opentelemetry::global::get_text_map_propagator(|propagator| { + propagator.extract(&opentelemetry_http::HeaderExtractor(req.headers())) + }); + + HandlerCtx::new( + None, + self.task_set.clone(), + TracingInfo::new_with_context(self.router.service_name(), parent_context), + ) } } @@ -56,25 +83,50 @@ where fn call(self, req: axum::extract::Request, state: S) -> Self::Future { Box::pin(async move { + let ctx = self.ctx(&req); + ctx.init_request_span(&self.router, Some(&Span::current())); + let Ok(bytes) = Bytes::from_request(req, &state).await else { + crate::metrics::record_parse_error(self.router.service_name()); return Box::::from(Response::parse_error()).into_response(); }; - // If the inbound data is not currently parsable, we - // send an empty one it to the router, as the router enforces - // the specification. - let req = InboundData::try_from(bytes).unwrap_or_default(); + // https://github.com/open-telemetry/semantic-conventions/blob/main/docs/rpc/rpc-spans.md#message-event + let req = ctx.span().in_scope(|| { + message_event!( + @received, + counter: &self.rx_msg_id, + bytes: bytes.len(), + ); + + // If the inbound data is not currently parsable, we + // send an empty one it to the router, as the router enforces + // the specification. + InboundData::try_from(bytes).unwrap_or_default() + }); + let span = ctx.span().clone(); if let Some(response) = self .router - .call_batch_with_state(self.ctx(), req, state) + .call_batch_with_state(ctx, req, state) + .instrument(span.clone()) .await { let headers = [( header::CONTENT_TYPE, HeaderValue::from_static(mime::APPLICATION_JSON.as_ref()), )]; + let body = Box::::from(response); + + span.in_scope(|| { + message_event!( + @sent, + counter: &self.tx_msg_id, + bytes: body.len(), + ); + }); + (headers, body).into_response() } else { ().into_response() diff --git a/src/lib.rs b/src/lib.rs index a5da1e6..37c297e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -24,7 +24,7 @@ //! }) //! // Routes get a ctx, which can be used to send notifications. //! .route("notify", |ctx: HandlerCtx| async move { -//! if ctx.notifications().is_none() { +//! if !ctx.notifications_enabled() { //! // This error will appear in the ResponsePayload's `data` field. //! return Err("notifications are disabled"); //! } @@ -159,6 +159,8 @@ mod axum; mod error; pub use error::RegistrationError; +pub(crate) mod metrics; + mod primitives; pub use primitives::{BorrowedRpcObject, MethodId, RpcBorrow, RpcObject, RpcRecv, RpcSend}; @@ -171,6 +173,7 @@ pub use pubsub::ReadJsonStream; mod routes; pub use routes::{ BatchFuture, Handler, HandlerArgs, HandlerCtx, NotifyError, Params, RouteFuture, State, + TracingInfo, }; pub(crate) use routes::{BoxedIntoRoute, ErasedIntoRoute, Method, Route}; @@ -206,7 +209,8 @@ pub(crate) mod test_utils { mod test { use crate::{ - router::RouterInner, routes::HandlerArgs, test_utils::assert_rv_eq, ResponsePayload, + router::RouterInner, routes::HandlerArgs, test_utils::assert_rv_eq, HandlerCtx, + ResponsePayload, }; use bytes::Bytes; use serde_json::value::RawValue; @@ -231,10 +235,7 @@ mod test { let res = router .call_with_state( - HandlerArgs { - ctx: Default::default(), - req: req.try_into().unwrap(), - }, + HandlerArgs::new(HandlerCtx::mock(), req.try_into().unwrap()), (), ) .await @@ -250,10 +251,7 @@ mod test { let res2 = router .call_with_state( - HandlerArgs { - ctx: Default::default(), - req: req2.try_into().unwrap(), - }, + HandlerArgs::new(HandlerCtx::mock(), req2.try_into().unwrap()), (), ) .await diff --git a/src/macros.rs b/src/macros.rs index 105eaa4..89529d3 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -54,6 +54,201 @@ macro_rules! unwrap_infallible { }; } +/// Log a message event to the current span. +/// +/// See +macro_rules! message_event { + ($type:literal, counter: $counter:expr, bytes: $bytes:expr,) => {{ + ::tracing::info!( + "rpc.message.id" = $counter.fetch_add(1, ::std::sync::atomic::Ordering::Relaxed), + "rpc.message.type" = $type, + "rpc.message.uncompressed_size" = $bytes, + "rpc.message" + ); + }}; + + (@received, counter: $counter:expr, bytes: $bytes:expr, ) => { + message_event!("RECEIVED", counter: $counter, bytes: $bytes,); + }; + + (@sent, counter: $counter:expr, bytes: $bytes:expr, ) => { + message_event!("SENT", counter: $counter, bytes: $bytes,); + }; +} + +/// Implement a `Handler` call, with metrics recording and response building. +macro_rules! impl_handler_call { + (@metrics, $success:expr, $id:expr, $service:expr, $method:expr) => {{ + // Record the metrics. + $crate::metrics::record_execution($success, $service, $method); + $crate::metrics::record_output($id.is_some(), $service, $method); + }}; + + (@record_span, $span:expr, $payload:expr) => { + if let Some(e) = $payload.as_error() { + use tracing_opentelemetry::OpenTelemetrySpanExt; + $span.record("rpc.jsonrpc.error_code", e.code); + $span.record("rpc.jsonrpc.error_message", e.message.as_ref()); + $span.set_status(::opentelemetry::trace::Status::Error { + description: e.message.clone(), + }); + } + }; + + // Hit the metrics and return the payload if any. + (@finish $span:expr, $id:expr, $service:expr, $method:expr, $payload:expr) => {{ + impl_handler_call!(@metrics, $payload.is_success(), $id, $service, $method); + impl_handler_call!(@record_span, $span, $payload); + return Response::build_response($id.as_deref(), $payload); + }}; + + (@unpack_params $span:expr, $id:expr, $service:expr, $method:expr, $req:expr) => {{ + let Ok(params) = $req.deser_params() else { + impl_handler_call!(@finish $span, $id, $service, $method, &ResponsePayload::<(), ()>::invalid_params()); + }; + drop($req); // no longer needed + params + }}; + + (@unpack_struct_params $span:expr, $id:expr, $service:expr, $method:expr, $req:expr) => {{ + let Ok(params) = $req.deser_params() else { + impl_handler_call!(@finish $span, $id, $service, $method, &ResponsePayload::<(), ()>::invalid_params()); + }; + drop($req); // no longer needed + params + }}; + + (@unpack $args:expr) => {{ + let id = $args.id_owned(); + let (ctx, req) = $args.into_parts(); + let inst = ctx.span().clone(); + let span = ctx.span().clone(); + let method = req.method().to_string(); + let service = ctx.service_name(); + + (id, ctx, inst, span, method, service, req) + }}; + + // NO ARGS + ($args:expr, $this:ident()) => {{ + let (id, ctx, inst, span, method, service, req) = impl_handler_call!(@unpack $args); + drop(ctx); // no longer needed + drop(req); // no longer needed + + Box::pin( + async move { + let payload: $crate::ResponsePayload<_, _> = $this().await.into(); + impl_handler_call!(@finish span, id, service, &method, &payload); + } + .instrument(inst), + ) + }}; + + // CTX only + ($args:expr, $this:ident(ctx)) => {{ + let (id, ctx, inst, span, method, service, req) = impl_handler_call!(@unpack $args); + drop(req); // no longer needed + + Box::pin( + async move { + let payload: $crate::ResponsePayload<_, _> = $this(ctx).await.into(); + impl_handler_call!(@finish span, id, service, &method, &payload); + } + .instrument(inst), + ) + }}; + + + // PARAMS only + ($args:expr, $this:ident(params: $params_ty:ty)) => {{ + let (id, ctx, inst, span, method, service, req) = impl_handler_call!(@unpack $args); + drop(ctx); // no longer needed + + Box::pin( + async move { + let params: $params_ty = impl_handler_call!(@unpack_params span, id, service, &method, req); + let payload: $crate::ResponsePayload<_, _> = $this(params.into()).await.into(); + impl_handler_call!(@finish span, id, service, &method, &payload); + } + .instrument(inst), + ) + }}; + + + // STATE only + ($args:expr, $this:ident($state:expr)) => {{ + let (id, ctx, inst, span, method, service, req) = impl_handler_call!(@unpack $args); + drop(ctx); // no longer needed + drop(req); // no longer needed + + Box::pin( + async move { + let payload: $crate::ResponsePayload<_, _> = $this($state).await.into(); + impl_handler_call!(@finish span, id, service, &method, &payload); + } + .instrument(inst), + ) + }}; + + + // CTX and PARAMS + ($args:expr, $this:ident(ctx, params: $params_ty:ty)) => {{ + let (id, ctx, inst, span, method, service, req) = impl_handler_call!(@unpack $args); + + Box::pin( + async move { + let params: $params_ty = impl_handler_call!(@unpack_params span, id, service, &method, req); + let payload: $crate::ResponsePayload<_, _> = $this(ctx, params.into()).await.into(); + impl_handler_call!(@finish span, id, service, &method, &payload); + } + .instrument(inst), + ) + }}; + + // CTX and STATE + ($args:expr, $this:ident(ctx, $state:expr)) => {{ + let (id, ctx, inst, span, method, service, req) = impl_handler_call!(@unpack $args); + drop(req); // no longer needed + + Box::pin( + async move { + let payload: $crate::ResponsePayload<_, _> = $this(ctx, $state).await.into(); + impl_handler_call!(@finish span, id, service, &method, &payload); + } + .instrument(inst), + ) + }}; + + // PARAMS and STATE + ($args:expr, $this:ident(params: $params_ty:ty, $state:expr)) => {{ + let (id, ctx, inst, span, method, service, req) = impl_handler_call!(@unpack $args); + drop(ctx); // no longer needed + + Box::pin( + async move { + let params: $params_ty = impl_handler_call!(@unpack_params span, id, service, &method, req); + let payload: $crate::ResponsePayload<_, _> = $this(params.into(), $state).await.into(); + impl_handler_call!(@finish span, id, service, &method, &payload); + } + .instrument(inst), + ) + }}; + + // CTX and PARAMS and STATE + ($args:expr, $this:ident(ctx, params: $params_ty:ty, $state:expr)) => {{ + let (id, ctx, inst, span, method, service, req) = impl_handler_call!(@unpack $args); + + Box::pin( + async move { + let params: $params_ty = impl_handler_call!(@unpack_params span, id, service, &method, req); + let payload: $crate::ResponsePayload<_, _> = $this(ctx, params.into(), $state).await.into(); + impl_handler_call!(@finish span, id, service, &method, &payload); + } + .instrument(inst), + ) + }}; +} + // Some code is this file is reproduced under the terms of the MIT license. It // originates from the `axum` crate. The original source code can be found at // the following URL, and the original license is included below. diff --git a/src/metrics.rs b/src/metrics.rs new file mode 100644 index 0000000..1530066 --- /dev/null +++ b/src/metrics.rs @@ -0,0 +1,246 @@ +use metrics::{counter, gauge, Counter, Gauge}; +use std::sync::LazyLock; + +/// Metric name for counting router calls. +pub(crate) const ROUTER_CALLS: &str = "ajj.router.calls"; +pub(crate) const ROUTER_CALLS_HELP: &str = + "Number of calls to ajj router methods. Not all requests will result in a response."; + +/// Metric name for counting router error execution. +pub(crate) const ROUTER_ERRORS: &str = "ajj.router.errors"; +pub(crate) const ROUTER_ERRORS_HELP: &str = + "Number of errored executions by ajj router methods. This does NOT imply a response was sent."; + +// Metric name for counting router successful executions. +pub(crate) const ROUTER_SUCCESSES: &str = "ajj.router.successes"; +pub(crate) const ROUTER_SUCCESSES_HELP: &str = + "Number of successful executions by ajj router methods. This does NOT imply a response was sent."; + +/// Metric name for counting router responses. +pub(crate) const ROUTER_RESPONSES: &str = "ajj.router.responses"; +pub(crate) const ROUTER_RESPONSES_HELP: &str = + "Number of responses sent by ajj router methods. Not all requests will result in a response."; + +// Metric name for counting omitted notification responses. +pub(crate) const ROUTER_NOTIFICATION_RESPONSE_OMITTED: &str = + "ajj.router.notification_response_omitted"; +pub(crate) const ROUTER_NOTIFICATION_RESPONSE_OMITTED_HELP: &str = + "Number of times ajj router methods omitted a response to a notification"; + +// Metric for counting parse errors. +pub(crate) const ROUTER_PARSE_ERRORS: &str = "ajj.router.parse_errors"; +pub(crate) const ROUTER_PARSE_ERRORS_HELP: &str = + "Number of parse errors encountered by ajj router methods. This implies no response was sent."; + +/// Metric for counting method not found errors. +pub(crate) const ROUTER_METHOD_NOT_FOUND: &str = "ajj.router.method_not_found"; +pub(crate) const ROUTER_METHOD_NOT_FOUND_HELP: &str = + "Number of times ajj router methods encountered a method not found error. This implies a response was sent."; + +/// Metric for tracking active calls. +pub(crate) const ACTIVE_CALLS: &str = "ajj.router.active_calls"; +pub(crate) const ACTIVE_CALLS_HELP: &str = "Number of active calls being processed"; + +/// Metric for tracking completed calls. +pub(crate) const COMPLETED_CALLS: &str = "ajj.router.completed_calls"; +pub(crate) const COMPLETED_CALLS_HELP: &str = "Number of completed calls handled"; + +static DESCRIBE: LazyLock<()> = LazyLock::new(|| { + metrics::describe_counter!(ROUTER_CALLS, metrics::Unit::Count, ROUTER_CALLS_HELP); + metrics::describe_counter!(ROUTER_ERRORS, metrics::Unit::Count, ROUTER_ERRORS_HELP); + metrics::describe_counter!( + ROUTER_SUCCESSES, + metrics::Unit::Count, + ROUTER_SUCCESSES_HELP + ); + metrics::describe_counter!( + ROUTER_RESPONSES, + metrics::Unit::Count, + ROUTER_RESPONSES_HELP + ); + metrics::describe_counter!( + ROUTER_NOTIFICATION_RESPONSE_OMITTED, + metrics::Unit::Count, + ROUTER_NOTIFICATION_RESPONSE_OMITTED_HELP + ); + metrics::describe_counter!( + ROUTER_PARSE_ERRORS, + metrics::Unit::Count, + ROUTER_PARSE_ERRORS_HELP + ); + metrics::describe_counter!( + ROUTER_METHOD_NOT_FOUND, + metrics::Unit::Count, + ROUTER_METHOD_NOT_FOUND_HELP + ); + metrics::describe_gauge!(ACTIVE_CALLS, metrics::Unit::Count, ACTIVE_CALLS_HELP); + metrics::describe_counter!(COMPLETED_CALLS, metrics::Unit::Count, COMPLETED_CALLS_HELP); +}); + +/// Get or register a counter for calls to a specific service and method. +fn calls(service_name: &'static str, method: &str) -> Counter { + let _ = &DESCRIBE; + counter!( + ROUTER_CALLS, + "service" => service_name.to_string(), + "method" => method.to_string() + ) +} + +/// Record a call to a specific service and method. +pub(crate) fn record_call(service_name: &'static str, method: &str) { + let counter = calls(service_name, method); + counter.increment(1); + increment_active_calls(service_name, method); +} + +/// Get or register a counter for errors from a specific service and method. +fn errors(service_name: &'static str, method: &str) -> Counter { + let _ = &DESCRIBE; + counter!( + ROUTER_ERRORS, + "service" => service_name.to_string(), + "method" => method.to_string() + ) +} + +/// Record an error from a specific service and method. +fn record_execution_error(service_name: &'static str, method: &str) { + let counter = errors(service_name, method); + counter.increment(1); +} + +/// Get or register a counter for successes from a specific service and method. +fn successes(service_name: &'static str, method: &str) -> Counter { + let _ = &DESCRIBE; + counter!( + ROUTER_SUCCESSES, + "service" => service_name.to_string(), + "method" => method.to_string() + ) +} + +/// Record a success from a specific service and method. +fn record_execution_success(service_name: &'static str, method: &str) { + let counter = successes(service_name, method); + counter.increment(1); +} + +/// Record a response from a specific service and method, incrementing either +/// the success or error counter. +pub(crate) fn record_execution(success: bool, service_name: &'static str, method: &str) { + if success { + record_execution_success(service_name, method); + } else { + record_execution_error(service_name, method); + } +} + +/// Get or register a counter for responses from a specific service and method. +fn responses(service_name: &'static str, method: &str) -> Counter { + let _ = &DESCRIBE; + counter!( + ROUTER_RESPONSES, + "service" => service_name.to_string(), + "method" => method.to_string() + ) +} + +/// Record a response from a specific service and method. +fn record_response(service_name: &'static str, method: &str) { + let counter = responses(service_name, method); + counter.increment(1); +} + +/// Get or register a counter for omitted notification responses from a specific service and method. +fn response_omitted(service_name: &'static str, method: &str) -> Counter { + let _ = &DESCRIBE; + counter!( + ROUTER_NOTIFICATION_RESPONSE_OMITTED, + "service" => service_name.to_string(), + "method" => method.to_string() + ) +} + +/// Record an omitted notification response from a specific service and method. +fn record_response_omitted(service_name: &'static str, method: &str) { + let counter = response_omitted(service_name, method); + counter.increment(1); +} + +/// Record either a response sent or an omitted notification response. +pub(crate) fn record_output(response_sent: bool, service_name: &'static str, method: &str) { + if response_sent { + record_response(service_name, method); + } else { + record_response_omitted(service_name, method); + } + record_completed_call(service_name, method); + decrement_active_calls(service_name, method); +} + +/// Get or register a counter for parse errors. +fn parse_errors(service_name: &'static str) -> Counter { + let _ = &DESCRIBE; + counter!(ROUTER_PARSE_ERRORS, "service" => service_name.to_string()) +} + +/// Record a parse error. +pub(crate) fn record_parse_error(service_name: &'static str) { + let counter = parse_errors(service_name); + counter.increment(1); +} + +/// Get or register a counter for method not found errors. +fn method_not_found_errors(service_name: &'static str, method: &str) -> Counter { + let _ = &DESCRIBE; + counter!(ROUTER_METHOD_NOT_FOUND, "service" => service_name.to_string(), "method" => method.to_string()) +} + +/// Record a method not found error. +pub(crate) fn record_method_not_found( + response_sent: bool, + service_name: &'static str, + method: &str, +) { + let counter = method_not_found_errors(service_name, method); + counter.increment(1); + record_output(response_sent, service_name, method); +} + +/// Get or register a gauge for active calls to a specific service. +fn active_calls(service_name: &'static str, method: &str) -> Gauge { + let _ = &DESCRIBE; + gauge!(ACTIVE_CALLS, "service" => service_name.to_string(), "method" => method.to_string()) +} + +/// Increment the active calls gauge for a specific service. +fn increment_active_calls(service_name: &'static str, method: &str) { + let _ = &DESCRIBE; + let gauge = active_calls(service_name, method); + gauge.increment(1); +} + +/// Decrement the active calls gauge for a specific service. +fn decrement_active_calls(service_name: &'static str, method: &str) { + let _ = &DESCRIBE; + let gauge = active_calls(service_name, method); + gauge.decrement(1); +} + +/// Get or register a counter for completed calls to a specific service. +fn completed_calls(service_name: &'static str, method: &str) -> Counter { + let _ = &DESCRIBE; + counter!( + COMPLETED_CALLS, + "service" => service_name.to_string(), + "method" => method.to_string() + ) +} + +/// Record a completed call to a specific service and method. +fn record_completed_call(service_name: &'static str, method: &str) { + let _ = &DESCRIBE; + let counter = completed_calls(service_name, method); + counter.increment(1); +} diff --git a/src/pubsub/axum.rs b/src/pubsub/axum.rs index 338b099..ad305d1 100644 --- a/src/pubsub/axum.rs +++ b/src/pubsub/axum.rs @@ -126,6 +126,8 @@ impl AxumWsCfg { next_id: arc.next_id.clone(), router: arc.router.clone(), notification_buffer_per_task: arc.notification_buffer_per_task, + tx_msg_id: arc.tx_msg_id.clone(), + rx_msg_id: arc.rx_msg_id.clone(), }, } } diff --git a/src/pubsub/mod.rs b/src/pubsub/mod.rs index 64dfb79..51aa896 100644 --- a/src/pubsub/mod.rs +++ b/src/pubsub/mod.rs @@ -95,6 +95,7 @@ mod ipc; pub use ipc::ReadJsonStream; mod shared; +pub(crate) use shared::WriteItem; pub use shared::{ConnectionId, DEFAULT_NOTIFICATION_BUFFER_PER_CLIENT}; mod shutdown; diff --git a/src/pubsub/shared.rs b/src/pubsub/shared.rs index 74ed4b3..26c3900 100644 --- a/src/pubsub/shared.rs +++ b/src/pubsub/shared.rs @@ -1,15 +1,18 @@ use crate::{ pubsub::{In, JsonSink, Listener, Out}, types::InboundData, - HandlerCtx, TaskSet, + HandlerCtx, TaskSet, TracingInfo, }; use core::fmt; use serde_json::value::RawValue; -use std::sync::{atomic::AtomicU64, Arc}; +use std::sync::{ + atomic::{AtomicU32, AtomicU64, Ordering}, + Arc, +}; use tokio::{pin, runtime::Handle, select, sync::mpsc, task::JoinHandle}; use tokio_stream::StreamExt; use tokio_util::sync::WaitForCancellationFutureOwned; -use tracing::{debug, debug_span, error, trace, Instrument}; +use tracing::{debug, error, trace, Instrument}; /// Default notification buffer size per task. pub const DEFAULT_NOTIFICATION_BUFFER_PER_CLIENT: usize = 16; @@ -67,6 +70,10 @@ pub(crate) struct ConnectionManager { pub(crate) router: crate::Router<()>, pub(crate) notification_buffer_per_task: usize, + + // OTEL message counters + pub(crate) tx_msg_id: Arc, + pub(crate) rx_msg_id: Arc, } impl ConnectionManager { @@ -77,6 +84,8 @@ impl ConnectionManager { next_id: AtomicU64::new(0).into(), router, notification_buffer_per_task: DEFAULT_NOTIFICATION_BUFFER_PER_CLIENT, + tx_msg_id: Arc::new(AtomicU32::new(1)), + rx_msg_id: Arc::new(AtomicU32::new(1)), } } @@ -105,8 +114,7 @@ impl ConnectionManager { /// Increment the connection ID counter and return an unused ID. fn next_id(&self) -> ConnectionId { - self.next_id - .fetch_add(1, std::sync::atomic::Ordering::Relaxed) + self.next_id.fetch_add(1, Ordering::Relaxed) } /// Get a clone of the router. @@ -131,13 +139,15 @@ impl ConnectionManager { write_task: tx, requests, tasks: tasks.clone(), + rx_msg_id: self.rx_msg_id.clone(), }; let wt = WriteTask { tasks, conn_id, - json: rx, + items: rx, connection, + tx_msg_id: self.tx_msg_id.clone(), }; (rt, wt) @@ -168,11 +178,14 @@ struct RouteTask { /// Connection ID for the connection serviced by this task. pub(crate) conn_id: ConnectionId, /// Sender to the write task. - pub(crate) write_task: mpsc::Sender>, + pub(crate) write_task: mpsc::Sender, /// Stream of requests. pub(crate) requests: In, /// The task set for this connection pub(crate) tasks: TaskSet, + + /// Counter for OTEL messages received. + pub(crate) rx_msg_id: Arc, } impl fmt::Debug for RouteTask { @@ -199,6 +212,7 @@ where mut requests, write_task, tasks, + rx_msg_id, .. } = self; @@ -224,6 +238,8 @@ where break; }; + let item_bytes = item.len(); + // If the inbound data is not currently parsable, we // send an empty one it to the router, as the router // enforces the specification. @@ -234,15 +250,32 @@ where // if the client stops accepting responses, we do not keep // handling inbound requests. let Ok(permit) = write_task.clone().reserve_owned().await else { - tracing::error!("write task dropped while waiting for permit"); + error!("write task dropped while waiting for permit"); break; }; + // This span is populated with as much detail as + // possible, and then given to the Handler ctx. It + // will be populated with request-specific details + // (e.g. method) during ctx instantiation. + let tracing = TracingInfo::new(router.service_name()); + let ctx = HandlerCtx::new( Some(write_task.clone()), children.clone(), + tracing, ); + ctx.init_request_span(&router, None); + + let span = ctx.span().clone(); + span.in_scope(|| { + message_event!( + @received, + counter: &rx_msg_id, + bytes: item_bytes, + ); + }); // Run the future in a new task. let fut = router.handle_request_batch(ctx, reqs); @@ -252,9 +285,9 @@ where // Send the response to the write task. // we don't care if the receiver has gone away, // as the task is done regardless. - if let Some(rv) = fut.await { + if let Some(json) = fut.await { let _ = permit.send( - rv + WriteItem { span, json } ); } } @@ -275,6 +308,13 @@ where } } +/// An item to be written to an outbound JSON pubsub stream. +#[derive(Debug, Clone)] +pub(crate) struct WriteItem { + pub(crate) span: tracing::Span, + pub(crate) json: Box, +} + /// The Write Task is responsible for writing JSON to the outbound connection. struct WriteTask { /// Task set @@ -287,10 +327,13 @@ struct WriteTask { /// /// Dropping this channel will cause the associated [`RouteTask`] to /// shutdown. - pub(crate) json: mpsc::Receiver>, + pub(crate) items: mpsc::Receiver, /// Outbound connections. pub(crate) connection: Out, + + /// Counter for OTEL messages sent. + pub(crate) tx_msg_id: Arc, } impl WriteTask { @@ -305,8 +348,9 @@ impl WriteTask { pub(crate) async fn task_future(self) { let WriteTask { tasks, - mut json, + mut items, mut connection, + tx_msg_id, .. } = self; @@ -318,12 +362,19 @@ impl WriteTask { debug!("Shutdown signal received"); break; } - json = json.recv() => { - let Some(json) = json else { + item = items.recv() => { + let Some(WriteItem { span, json }) = item else { tracing::error!("Json stream has closed"); break; }; - let span = debug_span!("WriteTask", conn_id = self.conn_id); + span.in_scope(|| { + message_event!( + @sent, + counter: &tx_msg_id, + bytes: json.get().len(), + ); + }); + if let Err(err) = connection.send_json(json).instrument(span).await { debug!(%err, conn_id = self.conn_id, "Failed to send json"); break; diff --git a/src/router.rs b/src/router.rs index a589896..c53b97f 100644 --- a/src/router.rs +++ b/src/router.rs @@ -95,6 +95,35 @@ where } } + /// Create a new, empty router with the specified OpenTelemetry service + /// name. + pub fn new_named(service_name: &'static str) -> Self { + Self { + inner: Arc::new(RouterInner { + service_name: Some(service_name), + ..RouterInner::new() + }), + } + } + + /// Get the OpenTelemetry service name for this router. + pub fn service_name(&self) -> &'static str { + self.inner.service_name() + } + + /// Set the OpenTelemetry service name for this router, overriding any + /// existing name. + /// + /// ## Note + /// + /// Routers wrap an `Arc`. If multiple references to the router exist, + /// this method will clone the inner state before setting the name. + pub fn set_name(self, service_name: &'static str) -> Self { + tap_inner!(self, mut this => { + this.service_name = Some(service_name); + }) + } + /// If this router is the only reference to its inner state, return the /// inner state. Otherwise, clone the inner state and return the clone. fn into_inner(self) -> RouterInner { @@ -104,6 +133,7 @@ where routes: arc.routes.clone(), last_id: arc.last_id, fallback: arc.fallback.clone(), + service_name: arc.service_name, name_to_id: arc.name_to_id.clone(), id_to_name: arc.id_to_name.clone(), }, @@ -301,7 +331,7 @@ where pub fn call_with_state(&self, args: HandlerArgs, state: S) -> RouteFuture { let id = args.req().id_owned(); let method = args.req().method(); - let span = debug_span!(parent: None, "Router::call_with_state", %method, ?id); + let span = debug_span!(parent: args.span(), "Router::call_with_state", %method, ?id); self.inner.call_with_state(args, state).with_span(span) } @@ -313,23 +343,29 @@ where inbound: InboundData, state: S, ) -> BatchFuture { - let mut fut = BatchFuture::new_with_capacity(inbound.single(), inbound.len()); + let mut fut = + BatchFuture::new_with_capacity(inbound.single(), self.service_name(), inbound.len()); // According to spec, non-parsable requests should still receive a // response. - let span = debug_span!(parent: None, "BatchFuture::poll", reqs = inbound.len(), futs = tracing::field::Empty); + let batch_span = debug_span!(parent: ctx.span(), "BatchFuture::poll", reqs = inbound.len(), futs = tracing::field::Empty); - for (batch_idx, req) in inbound.iter().enumerate() { + for req in inbound.iter() { let req = req.map(|req| { - let span = debug_span!(parent: &span, "RouteFuture::poll", batch_idx, method = req.method(), id = ?req.id()); - let args = HandlerArgs::new(ctx.clone(), req); + // This resets the `TracingInfo`, which means each + // ctx has a separate span with similar metadata. + let ctx = ctx.child_ctx(self, Some(&batch_span)); + let request_span = ctx.span().clone(); + + // Several span fields are populated in `HandlerArgs::new`. + let args = HandlerArgs::new(ctx, req); self.inner .call_with_state(args, state.clone()) - .with_span(span) + .with_span(request_span) }); fut.push_parse_result(req); } - span.record("futs", fut.len()); - fut.with_span(span) + batch_span.record("futs", fut.len()); + fut.with_span(batch_span) } /// Nest this router into a new Axum router, with the specified path and the currently-running @@ -473,6 +509,10 @@ pub(crate) struct RouterInner { /// The handler to call when no method is found. fallback: Method, + /// An optional service name for OpenTelemetry tracing. This is not + /// set by default. + service_name: Option<&'static str>, + // next 2 fields are used for reverse lookup of method names /// A map from method names to their IDs. name_to_id: BTreeMap, MethodId>, @@ -502,6 +542,8 @@ impl RouterInner { fallback: Method::Ready(Route::default_fallback()), + service_name: None, + name_to_id: BTreeMap::new(), id_to_name: BTreeMap::new(), } @@ -523,11 +565,17 @@ impl RouterInner { .collect(), fallback: self.fallback.with_state(state), last_id: self.last_id, + service_name: self.service_name, name_to_id: self.name_to_id, id_to_name: self.id_to_name, } } + /// Get the OpenTelemetry service name for this router. + fn service_name(&self) -> &'static str { + self.service_name.unwrap_or("ajj") + } + /// Get the next available ID. fn get_id(&mut self) -> MethodId { self.last_id += 1; @@ -612,6 +660,9 @@ impl RouterInner { #[track_caller] pub(crate) fn call_with_state(&self, args: HandlerArgs, state: S) -> RouteFuture { let method = args.req().method(); + + crate::metrics::record_call(self.service_name(), method); + self.method_by_name(method) .unwrap_or(&self.fallback) .call_with_state(args, state) diff --git a/src/routes/ctx.rs b/src/routes/ctx.rs index 3a1553a..0ff15a7 100644 --- a/src/routes/ctx.rs +++ b/src/routes/ctx.rs @@ -1,9 +1,15 @@ -use crate::{types::Request, RpcSend, TaskSet}; +use crate::{pubsub::WriteItem, types::Request, Router, RpcSend, TaskSet}; +use ::tracing::info_span; +use opentelemetry::trace::TraceContextExt; use serde_json::value::RawValue; -use std::future::Future; -use tokio::{runtime::Handle, sync::mpsc, task::JoinHandle}; +use std::{future::Future, sync::OnceLock}; +use tokio::{ + sync::mpsc::{self, error::SendError}, + task::JoinHandle, +}; use tokio_util::sync::WaitForCancellationFutureOwned; -use tracing::error; +use tracing::{enabled, error, Level}; +use tracing_opentelemetry::OpenTelemetrySpanExt; /// Errors that can occur when sending notifications. #[derive(thiserror::Error, Debug)] @@ -13,7 +19,142 @@ pub enum NotifyError { Serde(#[from] serde_json::Error), /// The notification channel was closed. #[error("notification channel closed")] - Send(#[from] mpsc::error::SendError>), + Send(#[from] SendError>), +} + +impl From> for NotifyError { + fn from(value: SendError) -> Self { + SendError(value.0.json).into() + } +} + +/// Tracing information for OpenTelemetry. This struct is used to store +/// information about the current request that can be used for tracing. +#[derive(Debug, Clone)] +#[non_exhaustive] +pub struct TracingInfo { + /// The OpenTelemetry service name. + pub service: &'static str, + + /// The open telemetry Context, + pub context: Option, + + /// The tracing span for this request. + span: OnceLock, +} + +impl TracingInfo { + /// Create a new tracing info with the given service name and no context. + #[allow(dead_code)] // used in some features + pub const fn new(service: &'static str) -> Self { + Self { + service, + context: None, + span: OnceLock::new(), + } + } + + /// Create a new tracing info with the given service name and context. + pub const fn new_with_context( + service: &'static str, + context: opentelemetry::context::Context, + ) -> Self { + Self { + service, + context: Some(context), + span: OnceLock::new(), + } + } + + fn make_span( + &self, + router: &Router, + with_notifications: bool, + parent: Option<&tracing::Span>, + ) -> tracing::Span + where + S: Clone + Send + Sync + 'static, + { + let span = info_span!( + parent: parent.and_then(|p| p.id()), + "AjjRequest", + "otel.kind" = "server", + "rpc.system" = "jsonrpc", + "rpc.jsonrpc.version" = "2.0", + "rpc.service" = router.service_name(), + notifications_enabled = with_notifications, + "trace_id" = ::tracing::field::Empty, + "otel.name" = ::tracing::field::Empty, + "otel.status_code" = ::tracing::field::Empty, + "rpc.jsonrpc.request_id" = ::tracing::field::Empty, + "rpc.jsonrpc.error_code" = ::tracing::field::Empty, + "rpc.jsonrpc.error_message" = ::tracing::field::Empty, + "rpc.method" = ::tracing::field::Empty, + params = ::tracing::field::Empty, + ); + if let Some(context) = &self.context { + let _ = span.set_parent(context.clone()); + + span.record( + "trace_id", + context.span().span_context().trace_id().to_string(), + ); + } + span + } + + /// Create a request span for a handler invocation. + fn init_request_span( + &self, + router: &Router, + with_notifications: bool, + parent: Option<&tracing::Span>, + ) -> &tracing::Span + where + S: Clone + Send + Sync + 'static, + { + // This span is populated with as much detail as possible, and then + // given to the Request. It will be populated with request-specific + // details (e.g. method) during request setup. + self.span + .get_or_init(|| self.make_span(router, with_notifications, parent)) + } + + /// Create a child tracing info for a new handler context. + pub fn child( + &self, + router: &Router, + with_notifications: bool, + parent: Option<&tracing::Span>, + ) -> Self { + let span = self.make_span(router, with_notifications, parent); + Self { + service: self.service, + context: self.context.clone(), + span: OnceLock::from(span), + } + } + + /// Get a reference to the tracing span for this request. + /// + /// ## Panics + /// + /// Panics if the span has not been initialized via + /// [`Self::init_request_span`]. + #[track_caller] + fn request_span(&self) -> &tracing::Span { + self.span.get().expect("span not initialized") + } + + /// Create a mock tracing info for testing. + #[cfg(test)] + pub fn mock() -> Self { + Self { + service: "test", + context: None, + span: OnceLock::from(info_span!("")), + } + } } /// A context for handler requests that allow the handler to send notifications @@ -25,52 +166,81 @@ pub enum NotifyError { /// - Sending notifications to pubsub clients via [`HandlerCtx::notify`]. /// Notifcations SHOULD be valid JSON-RPC objects, but this is /// not enforced by the type system. -#[derive(Debug, Clone, Default)] +#[derive(Debug, Clone)] pub struct HandlerCtx { - pub(crate) notifications: Option>>, + pub(crate) notifications: Option>, /// A task set on which to spawn tasks. This is used to coordinate pub(crate) tasks: TaskSet, + + /// Tracing information for OpenTelemetry. + pub(crate) tracing: TracingInfo, } -impl From for HandlerCtx { - fn from(tasks: TaskSet) -> Self { +impl HandlerCtx { + /// Create a new handler context. + pub(crate) const fn new( + notifications: Option>, + tasks: TaskSet, + tracing: TracingInfo, + ) -> Self { Self { - notifications: None, + notifications, tasks, + tracing, } } -} -impl From for HandlerCtx { - fn from(handle: Handle) -> Self { + /// Create a mock handler context for testing. + #[cfg(test)] + pub fn mock() -> Self { Self { notifications: None, - tasks: handle.into(), + tasks: TaskSet::default(), + tracing: TracingInfo::mock(), } } -} -impl HandlerCtx { - /// Create a new handler context. - #[allow(dead_code)] // used in pubsub and axum features - pub(crate) const fn new( - notifications: Option>>, - tasks: TaskSet, + /// Create a child handler context for a new handler invocation. + /// + /// This is used when handling batch requests, to give each handler + /// its own context. + pub fn child_ctx( + &self, + router: &Router, + parent: Option<&tracing::Span>, ) -> Self { Self { - notifications, - tasks, + notifications: self.notifications.clone(), + tasks: self.tasks.clone(), + tracing: self + .tracing + .child(router, self.notifications_enabled(), parent), } } - /// Get a reference to the notification sender. This is used to - /// send notifications over pubsub transports. - pub const fn notifications(&self) -> Option<&mpsc::Sender>> { - self.notifications.as_ref() + /// Get a reference to the tracing information for this handler context. + pub const fn tracing_info(&self) -> &TracingInfo { + &self.tracing } - /// Check if notiifcations can be sent to the client. This will be false + /// Get the OpenTelemetry service name for this handler context. + pub const fn service_name(&self) -> &'static str { + self.tracing.service + } + + /// Get a reference to the tracing span for this handler context. + #[track_caller] + pub fn span(&self) -> &tracing::Span { + self.tracing.request_span() + } + + /// Set the tracing information for this handler context. + pub fn set_tracing_info(&mut self, tracing: TracingInfo) { + self.tracing = tracing; + } + + /// Check if notifications can be sent to the client. This will be false /// when either the transport does not support notifications, or the /// notification channel has been closed (due the the client going away). pub fn notifications_enabled(&self) -> bool { @@ -80,11 +250,29 @@ impl HandlerCtx { .unwrap_or_default() } + /// Create a request span for a handler invocation. + pub fn init_request_span( + &self, + router: &Router, + parent: Option<&tracing::Span>, + ) -> &tracing::Span + where + S: Clone + Send + Sync + 'static, + { + self.tracing_info() + .init_request_span(router, self.notifications_enabled(), parent) + } + /// Notify a client of an event. pub async fn notify(&self, t: &T) -> Result<(), NotifyError> { if let Some(notifications) = self.notifications.as_ref() { let rv = serde_json::value::to_raw_value(t)?; - notifications.send(rv).await?; + notifications + .send(WriteItem { + span: self.span().clone(), + json: rv, + }) + .await?; } Ok(()) @@ -211,15 +399,43 @@ impl HandlerCtx { #[derive(Debug, Clone)] pub struct HandlerArgs { /// The handler context. - pub(crate) ctx: HandlerCtx, + ctx: HandlerCtx, /// The JSON-RPC request. - pub(crate) req: Request, + req: Request, + + /// prevent instantation outside of this module + _seal: (), } impl HandlerArgs { /// Create new handler arguments. - pub const fn new(ctx: HandlerCtx, req: Request) -> Self { - Self { ctx, req } + /// + /// ## Panics + /// + /// If the ctx tracing span has not been initialized via + /// [`HandlerCtx::init_request_span`]. + #[track_caller] + pub fn new(ctx: HandlerCtx, req: Request) -> Self { + let this = Self { + ctx, + req, + _seal: (), + }; + + let span = this.span(); + span.record("otel.name", this.otel_span_name()); + span.record("rpc.method", this.req.method()); + span.record("rpc.jsonrpc.request_id", this.req.id()); + if enabled!(Level::TRACE) { + span.record("params", this.req.params()); + } + + this + } + + /// Decompose the handler arguments into its parts. + pub fn into_parts(self) -> (HandlerCtx, Request) { + (self.ctx, self.req) } /// Get a reference to the handler context. @@ -227,8 +443,39 @@ impl HandlerArgs { &self.ctx } + /// Get a reference to the tracing span for this handler invocation. + /// + /// ## Panics + /// + /// If the span has not been initialized via + /// [`HandlerCtx::init_request_span`]. + #[track_caller] + pub fn span(&self) -> &tracing::Span { + self.ctx.span() + } + /// Get a reference to the JSON-RPC request. pub const fn req(&self) -> &Request { &self.req } + + /// Get the ID of the JSON-RPC request, if any. + pub fn id_owned(&self) -> Option> { + self.req.id_owned() + } + + /// Get the method of the JSON-RPC request. + pub fn method(&self) -> &str { + self.req.method() + } + + /// Get the OpenTelemetry span name for this handler invocation. + pub fn otel_span_name(&self) -> String { + format!("{}/{}", self.ctx.service_name(), self.req.method()) + } + + /// Get the service name for this handler invocation. + pub const fn service_name(&self) -> &'static str { + self.ctx.service_name() + } } diff --git a/src/routes/future.rs b/src/routes/future.rs index 35b9846..71a97ba 100644 --- a/src/routes/future.rs +++ b/src/routes/future.rs @@ -119,6 +119,9 @@ pub struct BatchFuture { /// Whether the batch was a single request. single: bool, + /// The service name, for tracing and metrics. + service_name: &'static str, + /// The span (if any). span: Option, } @@ -134,11 +137,16 @@ impl fmt::Debug for BatchFuture { impl BatchFuture { /// Create a new batch future with a capacity. - pub(crate) fn new_with_capacity(single: bool, capacity: usize) -> Self { + pub(crate) fn new_with_capacity( + single: bool, + service_name: &'static str, + capacity: usize, + ) -> Self { Self { futs: BatchFutureInner::Prepping(Vec::with_capacity(capacity)), resps: Vec::with_capacity(capacity), single, + service_name, span: None, } } @@ -166,6 +174,7 @@ impl BatchFuture { /// Push a parse error into the batch. pub(crate) fn push_parse_error(&mut self) { + crate::metrics::record_parse_error(self.service_name); self.push_resp(Response::parse_error()); } @@ -196,6 +205,7 @@ impl std::future::Future for BatchFuture { if matches!(self.futs, BatchFutureInner::Prepping(_)) { // SPEC: empty arrays are invalid if self.futs.is_empty() && self.resps.is_empty() { + crate::metrics::record_parse_error(self.service_name); return Poll::Ready(Some(Response::parse_error())); } self.futs.run(); @@ -227,7 +237,11 @@ impl std::future::Future for BatchFuture { // SPEC: single requests return a single response // Batch requests return an array of responses let resp = if *this.single { - this.resps.pop().unwrap_or_else(Response::parse_error) + this.resps.pop().unwrap_or_else(|| { + // this should never happen, but just in case... + crate::metrics::record_parse_error(this.service_name); + Response::parse_error() + }) } else { // otherwise, we have an array of responses serde_json::value::to_raw_value(&this.resps) diff --git a/src/routes/handler.rs b/src/routes/handler.rs index dda25da..51bbd13 100644 --- a/src/routes/handler.rs +++ b/src/routes/handler.rs @@ -3,16 +3,7 @@ use crate::{ }; use serde_json::value::RawValue; use std::{convert::Infallible, future::Future, marker::PhantomData, pin::Pin, task}; -use tracing::{debug_span, enabled, trace, Instrument, Level}; - -macro_rules! convert_result { - ($res:expr) => {{ - match $res { - Ok(val) => ResponsePayload::Success(val), - Err(err) => ResponsePayload::internal_error_with_obj(err), - } - }}; -} +use tracing::{trace, Instrument}; /// Hint type for differentiating certain handler impls. See the [`Handler`] /// trait "Handler argument type inference" section for more information. @@ -26,6 +17,12 @@ pub struct State(pub S); #[repr(transparent)] pub struct Params(pub T); +impl From for Params { + fn from(value: T) -> Self { + Self(value) + } +} + /// Marker type used for differentiating certain handler impls. #[allow(missing_debug_implementations, unreachable_pub)] pub struct PhantomState(PhantomData); @@ -227,7 +224,7 @@ pub struct PhantomParams(PhantomData); /// let cant_infer_err = || async { Ok(2) }; /// /// // cannot infer type of the type parameter `ErrData` declared on the enum `ResponsePayload` -/// let cant_infer_failure = || async { ResponsePayload::Success(3) }; +/// let cant_infer_failure = || async { ResponsePayload(Ok(3)) }; /// /// // cannot infer type of the type parameter `ErrData` declared on the enum `ResponsePayload` /// let cant_infer_success = || async { ResponsePayload::internal_error_with_obj(4) }; @@ -245,7 +242,7 @@ pub struct PhantomParams(PhantomData); /// let handler_b = || async { Err::<(), _>(2) }; /// /// // specify the ErrData on your Success -/// let handler_c = || async { ResponsePayload::Success::<_, ()>(3) }; +/// let handler_c = || async { ResponsePayload::<_, ()>(Ok(3)) }; /// /// // specify the Payload on your Failure /// let handler_d = || async { ResponsePayload::<(), _>::internal_error_with_obj(4) }; @@ -412,16 +409,9 @@ where fn call(&mut self, args: HandlerArgs) -> Self::Future { let this = self.clone(); Box::pin(async move { - let notifications_enabled = args.ctx.notifications_enabled(); - - let span = debug_span!( - "HandlerService::call", - notifications_enabled, - params = tracing::field::Empty - ); - if enabled!(Level::TRACE) { - span.record("params", args.req.params()); - } + // This span captures standard OpenTelemetry attributes for + // JSON-RPC according to OTEL semantic conventions. + let span = args.span().clone(); Ok(this .handler @@ -450,6 +440,7 @@ pub struct OutputResponsePayload { _sealed: (), } +// Takes nothing, returns ResponsePayload impl Handler<(OutputResponsePayload,), S> for F where F: FnOnce() -> Fut + Clone + Send + Sync + 'static, @@ -460,16 +451,11 @@ where type Future = Pin>> + Send>>; fn call_with_state(self, args: HandlerArgs, _state: S) -> Self::Future { - let id = args.req.id_owned(); - - Box::pin(async move { - let payload = self().await; - - Response::maybe(id.as_deref(), &payload) - }) + impl_handler_call!(args, self()) } } +// Takes Ctx, returns ResponsePayload impl Handler<(OutputResponsePayload, HandlerCtx), S> for F where F: FnOnce(HandlerCtx) -> Fut + Clone + Send + Sync + 'static, @@ -480,16 +466,11 @@ where type Future = Pin>> + Send>>; fn call_with_state(self, args: HandlerArgs, _state: S) -> Self::Future { - let id = args.req.id_owned(); - let ctx = args.ctx; - - Box::pin(async move { - let payload = self(ctx).await; - Response::maybe(id.as_deref(), &payload) - }) + impl_handler_call!(args, self(ctx)) } } +// Takes Params, returns ResponsePayload impl Handler<(OutputResponsePayload, PhantomParams), S> for F where @@ -502,19 +483,11 @@ where type Future = Pin>> + Send>>; fn call_with_state(self, args: HandlerArgs, _state: S) -> Self::Future { - let HandlerArgs { req, .. } = args; - Box::pin(async move { - let id = req.id_owned(); - let Ok(params) = req.deser_params() else { - return Response::maybe_invalid_params(id.as_deref()); - }; - - let payload = self(params).await; - Response::maybe(id.as_deref(), &payload) - }) + impl_handler_call!(args, self(params: Input)) } } +// Takes Params, returns ResponsePayload impl Handler<(OutputResponsePayload, Params), S> for F where F: FnOnce(Params) -> Fut + Clone + Send + Sync + 'static, @@ -526,19 +499,11 @@ where type Future = Pin>> + Send>>; fn call_with_state(self, args: HandlerArgs, _state: S) -> Self::Future { - let HandlerArgs { req, .. } = args; - Box::pin(async move { - let id = req.id_owned(); - let Ok(params) = req.deser_params() else { - return Response::maybe_invalid_params(id.as_deref()); - }; - - let payload = self(Params(params)).await; - Response::maybe(id.as_deref(), &payload) - }) + impl_handler_call!(args, self(params: Input)) } } +// Takes State, returns ResponsePayload impl Handler<(OutputResponsePayload, PhantomState), S> for F where F: FnOnce(S) -> Fut + Clone + Send + Sync + 'static, @@ -550,14 +515,11 @@ where type Future = Pin>> + Send>>; fn call_with_state(self, args: HandlerArgs, state: S) -> Self::Future { - let id = args.req.id_owned(); - Box::pin(async move { - let payload = self(state).await; - Response::maybe(id.as_deref(), &payload) - }) + impl_handler_call!(args, self(state)) } } +// Takes State, returns ResponsePayload impl Handler<(OutputResponsePayload, State), S> for F where F: FnOnce(State) -> Fut + Clone + Send + Sync + 'static, @@ -569,14 +531,11 @@ where type Future = Pin>> + Send>>; fn call_with_state(self, args: HandlerArgs, state: S) -> Self::Future { - let id = args.req.id_owned(); - Box::pin(async move { - let payload = self(State(state)).await; - Response::maybe(id.as_deref(), &payload) - }) + impl_handler_call!(args, self(State(state))) } } +// Takes Ctx, Params, returns ResponsePayload impl Handler<(OutputResponsePayload, HandlerCtx, PhantomParams), S> for F where @@ -589,22 +548,11 @@ where type Future = Pin>> + Send>>; fn call_with_state(self, args: HandlerArgs, _state: S) -> Self::Future { - Box::pin(async move { - let HandlerArgs { ctx, req } = args; - - let id = req.id_owned(); - let Ok(params) = req.deser_params() else { - return Response::maybe_invalid_params(id.as_deref()); - }; - - drop(req); // deallocate explicitly. No funny business. - - let payload = self(ctx, params).await; - Response::maybe(id.as_deref(), &payload) - }) + impl_handler_call!(args, self(ctx, params: Input)) } } +// Takes Ctx, Params, returns ResponsePayload impl Handler<(OutputResponsePayload, HandlerCtx, Params), S> for F where @@ -617,22 +565,11 @@ where type Future = Pin>> + Send>>; fn call_with_state(self, args: HandlerArgs, _state: S) -> Self::Future { - Box::pin(async move { - let HandlerArgs { ctx, req } = args; - - let id = req.id_owned(); - let Ok(params) = req.deser_params() else { - return Response::maybe_invalid_params(id.as_deref()); - }; - - drop(req); // deallocate explicitly. No funny business. - - let payload = self(ctx, Params(params)).await; - Response::maybe(id.as_deref(), &payload) - }) + impl_handler_call!(args, self(ctx, params: Input)) } } +// Takes Params, State, returns ResponsePayload impl Handler<(OutputResponsePayload, Input, S), S> for F where F: FnOnce(Input, S) -> Fut + Clone + Send + Sync + 'static, @@ -645,22 +582,11 @@ where type Future = Pin>> + Send>>; fn call_with_state(self, args: HandlerArgs, state: S) -> Self::Future { - Box::pin(async move { - let HandlerArgs { req, .. } = args; - - let id = req.id_owned(); - let Ok(params) = req.deser_params() else { - return Response::maybe_invalid_params(id.as_deref()); - }; - drop(req); // deallocate explicitly. No funny business. - - let payload = self(params, state).await; - - Response::maybe(id.as_deref(), &payload) - }) + impl_handler_call!(args, self(params: Input, state)) } } +// Takes Ctx, State, returns ResponsePayload impl Handler<(OutputResponsePayload, HandlerCtx, PhantomState), S> for F where @@ -673,20 +599,11 @@ where type Future = Pin>> + Send>>; fn call_with_state(self, args: HandlerArgs, state: S) -> Self::Future { - Box::pin(async move { - let HandlerArgs { ctx, req } = args; - - let id = req.id_owned(); - - drop(req); // deallocate explicitly. No funny business. - - let payload = self(ctx, state).await; - - Response::maybe(id.as_deref(), &payload) - }) + impl_handler_call!(args, self(ctx, state)) } } +// Takes Ctx, State, returns ResponsePayload impl Handler<(OutputResponsePayload, HandlerCtx, State), S> for F where F: FnOnce(HandlerCtx, State) -> Fut + Clone + Send + Sync + 'static, @@ -698,20 +615,11 @@ where type Future = Pin>> + Send>>; fn call_with_state(self, args: HandlerArgs, state: S) -> Self::Future { - Box::pin(async move { - let HandlerArgs { ctx, req } = args; - - let id = req.id_owned(); - - drop(req); // deallocate explicitly. No funny business. - - let payload = self(ctx, State(state)).await; - - Response::maybe(id.as_deref(), &payload) - }) + impl_handler_call!(args, self(ctx, State(state))) } } +// Takes Ctx, Params, State, returns ResponsePayload impl Handler<(OutputResponsePayload, HandlerCtx, Input, S), S> for F where @@ -725,22 +633,11 @@ where type Future = Pin>> + Send>>; fn call_with_state(self, args: HandlerArgs, state: S) -> Self::Future { - Box::pin(async move { - let HandlerArgs { ctx, req } = args; - - let id = req.id_owned(); - let Ok(params) = req.deser_params() else { - return Response::maybe_invalid_params(id.as_deref()); - }; - - drop(req); // deallocate explicitly. No funny business. - - let payload = self(ctx, params, state).await; - Response::maybe(id.as_deref(), &payload) - }) + impl_handler_call!(args, self(ctx, params: Input, state)) } } +// Takes nothing, returns Result impl Handler<(OutputResult,), S> for F where F: FnOnce() -> Fut + Clone + Send + Sync + 'static, @@ -751,12 +648,7 @@ where type Future = Pin>> + Send>>; fn call_with_state(self, args: HandlerArgs, _state: S) -> Self::Future { - let id = args.req.id_owned(); - drop(args); - Box::pin(async move { - let payload = self().await; - Response::maybe(id.as_deref(), &convert_result!(payload)) - }) + impl_handler_call!(args, self()) } } @@ -770,16 +662,7 @@ where type Future = Pin>> + Send>>; fn call_with_state(self, args: HandlerArgs, _state: S) -> Self::Future { - let HandlerArgs { ctx, req } = args; - - let id = req.id_owned(); - - drop(req); - - Box::pin(async move { - let payload = convert_result!(self(ctx).await); - Response::maybe(id.as_deref(), &payload) - }) + impl_handler_call!(args, self(ctx)) } } @@ -794,20 +677,7 @@ where type Future = Pin>> + Send>>; fn call_with_state(self, args: HandlerArgs, _state: S) -> Self::Future { - Box::pin(async move { - let HandlerArgs { req, .. } = args; - - let id = req.id_owned(); - let Ok(params) = req.deser_params() else { - return Response::maybe_invalid_params(id.as_deref()); - }; - - drop(req); // deallocate explicitly. No funny business. - - let payload = convert_result!(self(params).await); - - Response::maybe(id.as_deref(), &payload) - }) + impl_handler_call!(args, self(params: Input)) } } @@ -822,20 +692,7 @@ where type Future = Pin>> + Send>>; fn call_with_state(self, args: HandlerArgs, _state: S) -> Self::Future { - Box::pin(async move { - let HandlerArgs { req, .. } = args; - - let id = req.id_owned(); - let Ok(params) = req.deser_params() else { - return Response::maybe_invalid_params(id.as_deref()); - }; - - drop(req); // deallocate explicitly. No funny business. - - let payload = convert_result!(self(Params(params)).await); - - Response::maybe(id.as_deref(), &payload) - }) + impl_handler_call!(args, self(params: Input)) } } @@ -850,11 +707,7 @@ where type Future = Pin>> + Send>>; fn call_with_state(self, args: HandlerArgs, state: S) -> Self::Future { - let id = args.req.id_owned(); - Box::pin(async move { - let payload = convert_result!(self(state).await); - Response::maybe(id.as_deref(), &payload) - }) + impl_handler_call!(args, self(state)) } } @@ -869,11 +722,7 @@ where type Future = Pin>> + Send>>; fn call_with_state(self, args: HandlerArgs, state: S) -> Self::Future { - let id = args.req.id_owned(); - Box::pin(async move { - let payload = convert_result!(self(State(state)).await); - Response::maybe(id.as_deref(), &payload) - }) + impl_handler_call!(args, self(State(state))) } } @@ -889,20 +738,7 @@ where type Future = Pin>> + Send>>; fn call_with_state(self, args: HandlerArgs, _state: S) -> Self::Future { - Box::pin(async move { - let HandlerArgs { ctx, req } = args; - - let id = req.id_owned(); - let Ok(params) = req.deser_params() else { - return Response::maybe_invalid_params(id.as_deref()); - }; - - drop(req); // deallocate explicitly. No funny business. - - let payload = convert_result!(self(ctx, params).await); - - Response::maybe(id.as_deref(), &payload) - }) + impl_handler_call!(args, self(ctx, params: Input)) } } @@ -917,20 +753,7 @@ where type Future = Pin>> + Send>>; fn call_with_state(self, args: HandlerArgs, _state: S) -> Self::Future { - Box::pin(async move { - let HandlerArgs { ctx, req } = args; - - let id = req.id_owned(); - let Ok(params) = req.deser_params() else { - return Response::maybe_invalid_params(id.as_deref()); - }; - - drop(req); // deallocate explicitly. No funny business. - - let payload = convert_result!(self(ctx, Params(params)).await); - - Response::maybe(id.as_deref(), &payload) - }) + impl_handler_call!(args, self(ctx, params: Input)) } } @@ -946,20 +769,7 @@ where type Future = Pin>> + Send>>; fn call_with_state(self, args: HandlerArgs, state: S) -> Self::Future { - Box::pin(async move { - let HandlerArgs { req, .. } = args; - - let id = req.id_owned(); - let Ok(params) = req.deser_params() else { - return Response::maybe_invalid_params(id.as_deref()); - }; - - drop(req); // deallocate explicitly. No funny business. - - let payload = convert_result!(self(params, state).await); - - Response::maybe(id.as_deref(), &payload) - }) + impl_handler_call!(args, self(params: Input, state)) } } @@ -974,17 +784,7 @@ where type Future = Pin>> + Send>>; fn call_with_state(self, args: HandlerArgs, state: S) -> Self::Future { - let HandlerArgs { ctx, req } = args; - - let id = req.id_owned(); - - drop(req); - - Box::pin(async move { - let payload = convert_result!(self(ctx, state).await); - - Response::maybe(id.as_deref(), &payload) - }) + impl_handler_call!(args, self(ctx, state)) } } @@ -999,17 +799,7 @@ where type Future = Pin>> + Send>>; fn call_with_state(self, args: HandlerArgs, state: S) -> Self::Future { - let HandlerArgs { ctx, req } = args; - - let id = req.id_owned(); - - drop(req); - - Box::pin(async move { - let payload = convert_result!(self(ctx, State(state)).await); - - Response::maybe(id.as_deref(), &payload) - }) + impl_handler_call!(args, self(ctx, State(state))) } } @@ -1025,20 +815,7 @@ where type Future = Pin>> + Send>>; fn call_with_state(self, args: HandlerArgs, state: S) -> Self::Future { - Box::pin(async move { - let HandlerArgs { ctx, req } = args; - - let id = req.id_owned(); - let Ok(params) = req.deser_params() else { - return Response::maybe_invalid_params(id.as_deref()); - }; - - drop(req); // deallocate explicitly. No funny business. - - let payload = convert_result!(self(ctx, params, state).await); - - Response::maybe(id.as_deref(), &payload) - }) + impl_handler_call!(args, self(ctx, params: Input, state)) } } @@ -1051,7 +828,7 @@ mod test { struct NewType; async fn resp_ok() -> ResponsePayload<(), ()> { - ResponsePayload::Success(()) + ResponsePayload(Ok(())) } async fn ok() -> Result<(), ()> { diff --git a/src/routes/mod.rs b/src/routes/mod.rs index ecafb38..9493716 100644 --- a/src/routes/mod.rs +++ b/src/routes/mod.rs @@ -1,5 +1,5 @@ mod ctx; -pub use ctx::{HandlerArgs, HandlerCtx, NotifyError}; +pub use ctx::{HandlerArgs, HandlerCtx, NotifyError, TracingInfo}; mod erased; pub(crate) use erased::{BoxedIntoRoute, ErasedIntoRoute, MakeErasedHandler}; @@ -14,6 +14,7 @@ pub use handler::{Handler, Params, State}; mod method; pub(crate) use method::Method; +use crate::types::Response; use serde_json::value::RawValue; use std::{ convert::Infallible, @@ -22,8 +23,6 @@ use std::{ use tower::{util::BoxCloneSyncService, Service, ServiceExt}; use tracing::{debug_span, enabled, Level}; -use crate::types::Response; - /// A JSON-RPC handler for a specific method. /// /// A route is a [`BoxCloneSyncService`] that takes JSON parameters and may @@ -52,10 +51,13 @@ impl Route { /// Create a default fallback route that returns a method not found error. pub(crate) fn default_fallback() -> Self { Self::new(tower::service_fn(|args: HandlerArgs| async { - let HandlerArgs { req, .. } = args; - let id = req.id_owned(); - drop(req); - + let id = args.id_owned(); + crate::metrics::record_method_not_found( + id.is_some(), + args.service_name(), + args.method(), + ); + drop(args); // no longer needed Ok(Response::maybe_method_not_found(id.as_deref())) })) } @@ -102,7 +104,7 @@ impl Service for Route { params = tracing::field::Empty, ); if enabled!(Level::TRACE) { - span.record("params", args.req.params()); + span.record("params", args.req().params()); } self.oneshot_inner(args) } diff --git a/src/types/batch.rs b/src/types/batch.rs index 9ee745e..cdbabc1 100644 --- a/src/types/batch.rs +++ b/src/types/batch.rs @@ -3,7 +3,7 @@ use bytes::Bytes; use serde::Deserialize; use serde_json::value::RawValue; use std::ops::Range; -use tracing::{debug, enabled, instrument, Level}; +use tracing::{debug, enabled, instrument, span::Span, Level}; /// UTF-8, partially deserialized JSON-RPC request batch. #[derive(Default)] @@ -56,8 +56,12 @@ impl TryFrom for InboundData { #[instrument(level = "debug", skip(bytes), fields(buf_len = bytes.len(), bytes = tracing::field::Empty))] fn try_from(bytes: Bytes) -> Result { if enabled!(Level::TRACE) { - tracing::span::Span::current().record("bytes", format!("0x{:x}", bytes)); + Span::current().record("bytes", format!("0x{:x}", bytes)); } + + // This event exists only so that people who use default console + // logging setups still see the span details. Without this event, the + // span would not show up in logs. debug!("Parsing inbound data"); // We set up the deserializer to read from the byte buffer. diff --git a/src/types/resp/mod.rs b/src/types/resp/mod.rs new file mode 100644 index 0000000..b20095b --- /dev/null +++ b/src/types/resp/mod.rs @@ -0,0 +1,5 @@ +mod payload; +pub use payload::{ErrorPayload, ResponsePayload}; + +mod ser; +pub(crate) use ser::Response; diff --git a/src/types/resp.rs b/src/types/resp/payload.rs similarity index 56% rename from src/types/resp.rs rename to src/types/resp/payload.rs index 3b2fe4e..8b19c52 100644 --- a/src/types/resp.rs +++ b/src/types/resp/payload.rs @@ -1,158 +1,60 @@ use crate::RpcSend; -use serde::{ser::SerializeMap, Serialize, Serializer}; +use serde::Serialize; use serde_json::value::{to_raw_value, RawValue}; use std::borrow::Cow; use std::fmt; const INTERNAL_ERROR: Cow<'_, str> = Cow::Borrowed("Internal error"); -/// Response struct. -#[derive(Debug, Clone)] -pub(crate) struct Response<'a, 'b, T, E> { - pub(crate) id: &'b RawValue, - pub(crate) payload: &'a ResponsePayload, -} - -impl Response<'_, '_, (), ()> { - /// Parse error response, used when the request is not valid JSON. - #[allow(dead_code)] // used in features - pub(crate) fn parse_error() -> Box { - Response::<(), ()> { - id: RawValue::NULL, - payload: &ResponsePayload::parse_error(), - } - .to_json() - } - - /// Invalid params response, used when the params field does not - /// deserialize into the expected type. - pub(crate) fn invalid_params(id: &RawValue) -> Box { - Response::<(), ()> { - id, - payload: &ResponsePayload::invalid_params(), - } - .to_json() - } - - /// Invalid params response, used when the params field does not - /// deserialize into the expected type. This function exists to simplify - /// notification responses, which should be omitted. - pub(crate) fn maybe_invalid_params(id: Option<&RawValue>) -> Option> { - id.map(Self::invalid_params) - } - - /// Method not found response, used in default fallback handler. - pub(crate) fn method_not_found(id: &RawValue) -> Box { - Response::<(), ()> { - id, - payload: &ResponsePayload::method_not_found(), - } - .to_json() - } - - /// Method not found response, used in default fallback handler. This - /// function exists to simplify notification responses, which should be - /// omitted. - pub(crate) fn maybe_method_not_found(id: Option<&RawValue>) -> Option> { - id.map(Self::method_not_found) - } - - /// Response failed to serialize - pub(crate) fn serialization_failure(id: &RawValue) -> Box { - RawValue::from_string(format!( - r#"{{"jsonrpc":"2.0","id":{},"error":{{"code":-32700,"message":"response serialization error"}}}}"#, - id.get() - )) - .expect("valid json") - } -} - -impl<'a, 'b, T, E> Response<'a, 'b, T, E> -where - T: Serialize, - E: Serialize, -{ - pub(crate) fn maybe( - id: Option<&'b RawValue>, - payload: &'a ResponsePayload, - ) -> Option> { - id.map(|id| Self { id, payload }.to_json()) - } - - pub(crate) fn to_json(&self) -> Box { - serde_json::value::to_raw_value(self).unwrap_or_else(|err| { - tracing::debug!(%err, id = ?self.id, "failed to serialize response"); - Response::serialization_failure(self.id) - }) - } -} +/// A JSON-RPC 2.0 response payload. +/// +/// This is a thin wrapper around a [`Result`] type containing either +/// the successful payload or an error payload. +#[derive(Clone, Debug, PartialEq, Eq)] +#[repr(transparent)] +pub struct ResponsePayload(pub Result>); -impl Serialize for Response<'_, '_, T, E> +impl From> for ResponsePayload where - T: Serialize, - E: Serialize, + E: RpcSend, { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - let mut map = serializer.serialize_map(Some(3))?; - map.serialize_entry("jsonrpc", "2.0")?; - map.serialize_entry("id", &self.id)?; - match &self.payload { - ResponsePayload::Success(result) => { - map.serialize_entry("result", result)?; - } - ResponsePayload::Failure(error) => { - map.serialize_entry("error", error)?; - } + fn from(res: Result) -> Self { + match res { + Ok(payload) => Self(Ok(payload)), + Err(err) => Self(Err(ErrorPayload::internal_error_with_obj(err))), } - map.end() } } -/// A JSON-RPC 2.0 response payload. -/// -/// This enum covers both the success and error cases of a JSON-RPC 2.0 -/// response. It is used to represent the `result` and `error` fields of a -/// response object. -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum ResponsePayload { - /// A successful response payload. - Success(Payload), - /// An error response payload. - Failure(ErrorPayload), -} - impl ResponsePayload { /// Create a new error payload for a parse error. pub const fn parse_error() -> Self { - Self::Failure(ErrorPayload::parse_error()) + Self(Err(ErrorPayload::parse_error())) } /// Create a new error payload for an invalid request. pub const fn invalid_request() -> Self { - Self::Failure(ErrorPayload::invalid_request()) + Self(Err(ErrorPayload::invalid_request())) } /// Create a new error payload for a method not found error. pub const fn method_not_found() -> Self { - Self::Failure(ErrorPayload::method_not_found()) + Self(Err(ErrorPayload::method_not_found())) } /// Create a new error payload for an invalid params error. pub const fn invalid_params() -> Self { - Self::Failure(ErrorPayload::invalid_params()) + Self(Err(ErrorPayload::invalid_params())) } /// Create a new error payload for an internal error. pub const fn internal_error() -> Self { - Self::Failure(ErrorPayload::internal_error()) + Self(Err(ErrorPayload::internal_error())) } /// Create a new error payload for an internal error with a custom message. pub const fn internal_error_message(message: Cow<'static, str>) -> Self { - Self::Failure(ErrorPayload::internal_error_message(message)) + Self(Err(ErrorPayload::internal_error_message(message))) } /// Create a new error payload for an internal error with a custom message @@ -161,7 +63,7 @@ impl ResponsePayload { where ErrData: RpcSend, { - Self::Failure(ErrorPayload::internal_error_with_obj(data)) + Self(Err(ErrorPayload::internal_error_with_obj(data))) } /// Create a new error payload for an internal error with a custom message @@ -173,15 +75,15 @@ impl ResponsePayload { where ErrData: RpcSend, { - Self::Failure(ErrorPayload::internal_error_with_message_and_obj( + Self(Err(ErrorPayload::internal_error_with_message_and_obj( message, data, - )) + ))) } /// Fallible conversion to the successful payload. pub const fn as_success(&self) -> Option<&Payload> { match self { - Self::Success(payload) => Some(payload), + Self(Ok(payload)) => Some(payload), _ => None, } } @@ -189,19 +91,19 @@ impl ResponsePayload { /// Fallible conversion to the error object. pub const fn as_error(&self) -> Option<&ErrorPayload> { match self { - Self::Failure(payload) => Some(payload), + Self(Err(payload)) => Some(payload), _ => None, } } /// Returns `true` if the response payload is a success. pub const fn is_success(&self) -> bool { - matches!(self, Self::Success(_)) + matches!(self, Self(Ok(_))) } /// Returns `true` if the response payload is an error. pub const fn is_error(&self) -> bool { - matches!(self, Self::Failure(_)) + matches!(self, Self(Err(_))) } /// Convert a result into a response payload, by converting the error into @@ -211,8 +113,8 @@ impl ResponsePayload { T: Into>, { match res { - Ok(payload) => Self::Success(payload), - Err(err) => Self::Failure(ErrorPayload::internal_error_message(err.into())), + Ok(payload) => Self(Ok(payload)), + Err(err) => Self(Err(ErrorPayload::internal_error_message(err.into()))), } } } @@ -403,51 +305,3 @@ impl fmt::Display for ErrorPayload { ) } } - -#[cfg(test)] -mod test { - use super::*; - use crate::test_utils::assert_rv_eq; - - #[test] - fn ser_failure() { - let id = RawValue::from_string("1".to_string()).unwrap(); - let res = Response::<(), ()>::serialization_failure(&id); - assert_rv_eq( - &res, - r#"{"jsonrpc":"2.0","id":1,"error":{"code":-32700,"message":"response serialization error"}}"#, - ); - } -} - -// Some code is this file is reproduced under the terms of the MIT license. It -// originates from the `alloy` crate. The original source code can be found at -// the following URL, and the original license is included below. -// -// https://github.com/alloy-rs/alloy -// -// The MIT License (MIT) -// -// Permission is hereby granted, free of charge, to any -// person obtaining a copy of this software and associated -// documentation files (the "Software"), to deal in the -// Software without restriction, including without -// limitation the rights to use, copy, modify, merge, -// publish, distribute, sublicense, and/or sell copies of -// the Software, and to permit persons to whom the Software -// is furnished to do so, subject to the following -// conditions: -// -// The above copyright notice and this permission notice -// shall be included in all copies or substantial portions -// of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF -// ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED -// TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A -// PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT -// SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -// CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR -// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -// DEALINGS IN THE SOFTWARE. diff --git a/src/types/resp/ser.rs b/src/types/resp/ser.rs new file mode 100644 index 0000000..dda923d --- /dev/null +++ b/src/types/resp/ser.rs @@ -0,0 +1,138 @@ +use crate::ResponsePayload; +use serde::{ser::SerializeMap, Serialize, Serializer}; +use serde_json::value::RawValue; + +/// Response struct. +#[derive(Debug, Clone)] +pub(crate) struct Response<'a, 'b, T, E> { + pub(crate) id: &'b RawValue, + pub(crate) payload: &'a ResponsePayload, +} + +impl Response<'_, '_, (), ()> { + /// Parse error response, used when the request is not valid JSON. + pub(crate) fn parse_error() -> Box { + Response::<(), ()> { + id: RawValue::NULL, + payload: &ResponsePayload::parse_error(), + } + .to_json() + } + + /// Method not found response, used in default fallback handler. + pub(crate) fn method_not_found(id: &RawValue) -> Box { + Response::<(), ()> { + id, + payload: &ResponsePayload::method_not_found(), + } + .to_json() + } + + /// Method not found response, used in default fallback handler. This + /// function exists to simplify notification responses, which should be + /// omitted. + pub(crate) fn maybe_method_not_found(id: Option<&RawValue>) -> Option> { + id.map(Self::method_not_found) + } + + /// Response failed to serialize + pub(crate) fn serialization_failure(id: &RawValue) -> Box { + RawValue::from_string(format!( + r#"{{"jsonrpc":"2.0","id":{},"error":{{"code":-32700,"message":"response serialization error"}}}}"#, + id.get() + )) + .expect("valid json") + } +} + +impl<'a, 'b, T, E> Response<'a, 'b, T, E> +where + T: Serialize, + E: Serialize, +{ + pub(crate) fn build_response( + id: Option<&'b RawValue>, + payload: &'a ResponsePayload, + ) -> Option> { + id.map(move |id| Self { id, payload }.to_json()) + } + + pub(crate) fn to_json(&self) -> Box { + serde_json::value::to_raw_value(self).unwrap_or_else(|err| { + tracing::debug!(%err, id = ?self.id, "failed to serialize response"); + Response::serialization_failure(self.id) + }) + } +} + +impl Serialize for Response<'_, '_, T, E> +where + T: Serialize, + E: Serialize, +{ + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let mut map = serializer.serialize_map(Some(3))?; + map.serialize_entry("jsonrpc", "2.0")?; + map.serialize_entry("id", &self.id)?; + match &self.payload { + ResponsePayload(Ok(result)) => { + map.serialize_entry("result", result)?; + } + ResponsePayload(Err(error)) => { + map.serialize_entry("error", error)?; + } + } + map.end() + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::test_utils::assert_rv_eq; + + #[test] + fn ser_failure() { + let id = RawValue::from_string("1".to_string()).unwrap(); + let res = Response::<(), ()>::serialization_failure(&id); + assert_rv_eq( + &res, + r#"{"jsonrpc":"2.0","id":1,"error":{"code":-32700,"message":"response serialization error"}}"#, + ); + } +} + +// Some code is this file is reproduced under the terms of the MIT license. It +// originates from the `alloy` crate. The original source code can be found at +// the following URL, and the original license is included below. +// +// https://github.com/alloy-rs/alloy +// +// The MIT License (MIT) +// +// Permission is hereby granted, free of charge, to any +// person obtaining a copy of this software and associated +// documentation files (the "Software"), to deal in the +// Software without restriction, including without +// limitation the rights to use, copy, modify, merge, +// publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software +// is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice +// shall be included in all copies or substantial portions +// of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +// ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +// TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +// PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +// SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +// CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. diff --git a/tests/common/mod.rs b/tests/common/mod.rs index b42212a..edad5f4 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -104,13 +104,17 @@ async fn test_ping(client: &mut T) { /// basic tests of the test router pub async fn basic_tests(client: &mut T) { - test_ping(client).await; + timeout(Duration::from_secs(10), async move { + test_ping(client).await; - test_double(client).await; + test_double(client).await; - test_call_me_later(client).await; + test_call_me_later(client).await; - test_notification(client).await; + test_notification(client).await; + }) + .await + .unwrap(); } /// Test when a full batch request fails to parse @@ -205,11 +209,15 @@ async fn batch_contains_only_notifications(client: &mut T) { /// Test of batch requests pub async fn batch_tests(client: &mut T) { - batch_parse_fail(client).await; + timeout(Duration::from_secs(10), async move { + batch_parse_fail(client).await; - batch_single_req_parse_fail(client).await; + batch_single_req_parse_fail(client).await; - batch_contains_notification(client).await; + batch_contains_notification(client).await; - batch_contains_only_notifications(client).await; + batch_contains_only_notifications(client).await; + }) + .await + .unwrap(); }