diff --git a/design/src/rfcs/rfc0024_request_id.md b/design/src/rfcs/rfc0024_request_id.md index aa25a41f0c..a10d5e92e7 100644 --- a/design/src/rfcs/rfc0024_request_id.md +++ b/design/src/rfcs/rfc0024_request_id.md @@ -1,7 +1,7 @@ RFC: RequestID in business logic handlers ============= -> Status: RFC +> Status: Implemented > > Applies to: server @@ -85,7 +85,7 @@ where } fn call(&mut self, mut req: http::Request) -> Self::Future { - request.extensions_mut().insert(ServerRequestId::new()); + req.extensions_mut().insert(ServerRequestId::new()); self.inner.call(req) } } @@ -131,7 +131,12 @@ Although the generated ID is opaque, this will give guarantees to customers as t Changes checklist ----------------- -- [ ] Implement `ServerRequestId`: a `new()` function that generates a UUID, with `Display`, `Debug` and `ToStr` implementations +- [x] Implement `ServerRequestId`: a `new()` function that generates a UUID, with `Display`, `Debug` and `ToStr` implementations - [ ] Implement `ClientRequestId`: `new()` that wraps a string (the header value) and the header in which the value could be found, with `Display`, `Debug` and `ToStr` implementations - [x] Implement `FromParts` for `Extension` -- [x] Implement `FromParts` for `Extension` +- [ ] Implement `FromParts` for `Extension` + +Changes since the RFC has been approved +--------------------------------------- + +This RFC has been changed to only implement `ServerRequestId`. diff --git a/rust-runtime/aws-smithy-http-server/Cargo.toml b/rust-runtime/aws-smithy-http-server/Cargo.toml index 4497eb8806..b58e75bd89 100644 --- a/rust-runtime/aws-smithy-http-server/Cargo.toml +++ b/rust-runtime/aws-smithy-http-server/Cargo.toml @@ -15,6 +15,7 @@ publish = true [features] aws-lambda = ["dep:lambda_http"] unredacted-logging = [] +request-id = ["dep:uuid"] [dependencies] aws-smithy-http = { path = "../aws-smithy-http", features = ["rt-tokio"] } @@ -40,6 +41,7 @@ tracing = "0.1.35" tokio = { version = "1.8.4", features = ["full"] } tower = { version = "0.4.11", features = ["util", "make"], default-features = false } tower-http = { version = "0.3", features = ["add-extension", "map-response-body"] } +uuid = { version = "1", features = ["v4", "fast-rng"], optional = true } [dev-dependencies] pretty_assertions = "1" diff --git a/rust-runtime/aws-smithy-http-server/examples/pokemon-service/Cargo.toml b/rust-runtime/aws-smithy-http-server/examples/pokemon-service/Cargo.toml index 06738fcc54..da4cb284c0 100644 --- a/rust-runtime/aws-smithy-http-server/examples/pokemon-service/Cargo.toml +++ b/rust-runtime/aws-smithy-http-server/examples/pokemon-service/Cargo.toml @@ -44,7 +44,7 @@ futures-util = "0.3" lambda_http = "0.7.1" # Local paths -aws-smithy-http-server = { path = "../../", features = ["aws-lambda"] } +aws-smithy-http-server = { path = "../../", features = ["aws-lambda", "request-id"] } pokemon-service-server-sdk = { path = "../pokemon-service-server-sdk/" } [dev-dependencies] diff --git a/rust-runtime/aws-smithy-http-server/examples/pokemon-service/src/bin/pokemon-service-connect-info.rs b/rust-runtime/aws-smithy-http-server/examples/pokemon-service/src/bin/pokemon-service-connect-info.rs index c366f747db..b6e47b4274 100644 --- a/rust-runtime/aws-smithy-http-server/examples/pokemon-service/src/bin/pokemon-service-connect-info.rs +++ b/rust-runtime/aws-smithy-http-server/examples/pokemon-service/src/bin/pokemon-service-connect-info.rs @@ -5,15 +5,16 @@ use std::net::{IpAddr, SocketAddr}; -use aws_smithy_http_server::request::connect_info::ConnectInfo; -use clap::Parser; -use pokemon_service::{ - capture_pokemon, check_health, do_nothing, get_pokemon_species, get_server_statistics, setup_tracing, +use aws_smithy_http_server::{ + request::connect_info::ConnectInfo, request::request_id::ServerRequestId, + request::request_id::ServerRequestIdProviderLayer, }; +use clap::Parser; +use pokemon_service::{capture_pokemon, check_health, get_pokemon_species, get_server_statistics, setup_tracing}; use pokemon_service_server_sdk::{ error::{GetStorageError, NotAuthorized}, - input::GetStorageInput, - output::GetStorageOutput, + input::{DoNothingInput, GetStorageInput}, + output::{DoNothingOutput, GetStorageOutput}, PokemonService, }; @@ -55,6 +56,14 @@ pub async fn get_storage_with_local_approved( Err(GetStorageError::NotAuthorized(NotAuthorized {})) } +pub async fn do_nothing_but_log_request_ids( + _input: DoNothingInput, + server_request_id: ServerRequestId, +) -> DoNothingOutput { + tracing::debug!("This request has this server ID: {}", server_request_id); + DoNothingOutput {} +} + #[tokio::main] async fn main() { let args = Args::parse(); @@ -64,11 +73,13 @@ async fn main() { .get_storage(get_storage_with_local_approved) .get_server_statistics(get_server_statistics) .capture_pokemon(capture_pokemon) - .do_nothing(do_nothing) + .do_nothing(do_nothing_but_log_request_ids) .check_health(check_health) .build() .expect("failed to build an instance of PokemonService"); + let app = app.layer(&ServerRequestIdProviderLayer::new()); + // Start the [`hyper::Server`]. let bind: SocketAddr = format!("{}:{}", args.address, args.port) .parse() diff --git a/rust-runtime/aws-smithy-http-server/src/request/mod.rs b/rust-runtime/aws-smithy-http-server/src/request/mod.rs index 2fa8bb4185..1b08c63289 100644 --- a/rust-runtime/aws-smithy-http-server/src/request/mod.rs +++ b/rust-runtime/aws-smithy-http-server/src/request/mod.rs @@ -67,6 +67,9 @@ pub mod extension; #[cfg(feature = "aws-lambda")] #[cfg_attr(docsrs, doc(cfg(feature = "aws-lambda")))] pub mod lambda; +#[cfg(feature = "request-id")] +#[cfg_attr(docsrs, doc(cfg(feature = "request-id")))] +pub mod request_id; fn internal_server_error() -> http::Response { let mut response = http::Response::new(empty()); diff --git a/rust-runtime/aws-smithy-http-server/src/request/request_id.rs b/rust-runtime/aws-smithy-http-server/src/request/request_id.rs new file mode 100644 index 0000000000..7894d9806e --- /dev/null +++ b/rust-runtime/aws-smithy-http-server/src/request/request_id.rs @@ -0,0 +1,152 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +//! # Request IDs +//! +//! `aws-smithy-http-server` provides the [`ServerRequestId`]. +//! +//! ## `ServerRequestId` +//! +//! A [`ServerRequestId`] is an opaque random identifier generated by the server every time it receives a request. +//! It uniquely identifies the request within that service instance. It can be used to collate all logs, events and +//! data related to a single operation. +//! +//! The [`ServerRequestId`] can be returned to the caller, who can in turn share the [`ServerRequestId`] to help the service owner in troubleshooting issues related to their usage of the service. +//! +//! The [`ServerRequestId`] is not meant to be propagated to downstream dependencies of the service. You should rely on a distributed tracing implementation for correlation purposes (e.g. OpenTelemetry). +//! +//! ## Examples +//! +//! Your handler can now optionally take as input a [`ServerRequestId`]. +//! +//! ```rust,ignore +//! pub async fn handler( +//! _input: Input, +//! server_request_id: ServerRequestId, +//! ) -> Output { +//! /* Use server_request_id */ +//! todo!() +//! } +//! +//! let app = Service::builder_without_plugins() +//! .operation(handler) +//! .build().unwrap(); +//! +//! let app = app.layer(&ServerRequestIdProviderLayer::new()); /* Generate a server request ID */ +//! +//! let bind: std::net::SocketAddr = format!("{}:{}", args.address, args.port) +//! .parse() +//! .expect("unable to parse the server bind address and port"); +//! let server = hyper::Server::bind(&bind).serve(app.into_make_service()); +//! ``` + +use std::{ + fmt::Display, + task::{Context, Poll}, +}; + +use http::request::Parts; +use thiserror::Error; +use tower::{Layer, Service}; +use uuid::Uuid; + +use crate::{body::BoxBody, response::IntoResponse}; + +use super::{internal_server_error, FromParts}; + +/// Opaque type for Server Request IDs. +/// +/// If it is missing, the request will be rejected with a `500 Internal Server Error` response. +#[derive(Clone, Debug)] +pub struct ServerRequestId { + id: Uuid, +} + +/// The server request ID has not been added to the [`Request`](http::Request) or has been previously removed. +#[non_exhaustive] +#[derive(Debug, Error)] +#[error("the `ServerRequestId` is not present in the `http::Request`")] +pub struct MissingServerRequestId; + +impl ServerRequestId { + pub fn new() -> Self { + Self { id: Uuid::new_v4() } + } +} + +impl Display for ServerRequestId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.id.fmt(f) + } +} + +impl

FromParts

for ServerRequestId { + type Rejection = MissingServerRequestId; + + fn from_parts(parts: &mut Parts) -> Result { + parts.extensions.remove().ok_or(MissingServerRequestId) + } +} + +impl Default for ServerRequestId { + fn default() -> Self { + Self::new() + } +} + +#[derive(Clone)] +pub struct ServerRequestIdProvider { + inner: S, +} + +/// A layer that provides services with a unique request ID instance +#[derive(Debug)] +#[non_exhaustive] +pub struct ServerRequestIdProviderLayer; + +impl ServerRequestIdProviderLayer { + /// Generate a new unique request ID + pub fn new() -> Self { + Self {} + } +} + +impl Default for ServerRequestIdProviderLayer { + fn default() -> Self { + Self::new() + } +} + +impl Layer for ServerRequestIdProviderLayer { + type Service = ServerRequestIdProvider; + + fn layer(&self, inner: S) -> Self::Service { + ServerRequestIdProvider { inner } + } +} + +impl Service> for ServerRequestIdProvider +where + S: Service>, +{ + type Response = S::Response; + type Error = S::Error; + type Future = S::Future; + + fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { + self.inner.poll_ready(cx) + } + + fn call(&mut self, mut req: http::Request) -> Self::Future { + req.extensions_mut().insert(ServerRequestId::new()); + self.inner.call(req) + } +} + +impl IntoResponse for MissingServerRequestId { + fn into_response(self) -> http::Response { + internal_server_error() + } +}