Skip to content

Commit 00e90ad

Browse files
darklight3itDavide Melfi
andauthored
feat(lambda-runtime): log non-2xx Lambda Runtime API responses with status and body (#1109)
* feat: ⚡ Improved Logging when Lambda Runtime Api fails, this will be useful for the customer for timeouts --------- Co-authored-by: Davide Melfi <dmelfi@amazon.com>
1 parent 30a0963 commit 00e90ad

File tree

5 files changed

+243
-8
lines changed

5 files changed

+243
-8
lines changed

lambda-runtime/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ pin-project-lite = { workspace = true }
7878
tracing-appender = "0.2"
7979
tracing-capture = "0.1.0"
8080
tracing-subscriber = { version = "0.3", features = ["registry"] }
81+
tracing-test = "0.2"
8182

8283
[package.metadata.docs.rs]
8384
all-features = true

lambda-runtime/src/layers/api_client.rs

Lines changed: 222 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,10 +85,230 @@ where
8585
.boxed();
8686
self.set(RuntimeApiClientFuture::Second(next_fut));
8787
}
88-
Err(err) => break Err(err),
88+
Err(err) => {
89+
log_or_print!(
90+
tracing: tracing::error!(error = ?err, "failed to build Lambda Runtime API request"),
91+
fallback: eprintln!("failed to build Lambda Runtime API request: {err:?}")
92+
);
93+
break Err(err);
94+
}
95+
},
96+
RuntimeApiClientFutureProj::Second(fut) => match ready!(fut.poll(cx)) {
97+
Ok(resp) if !resp.status().is_success() => {
98+
let status = resp.status();
99+
100+
// TODO
101+
// we should consume the response body of the call in order to give a more specific message.
102+
// https://github.com/aws/aws-lambda-rust-runtime/issues/1110
103+
104+
log_or_print!(
105+
tracing: tracing::error!(status = %status, "Lambda Runtime API returned non-200 response"),
106+
fallback: eprintln!("Lambda Runtime API returned non-200 response: status={status}")
107+
);
108+
109+
// Adding more information on top of 410 Gone, to make it more clear since we cannot access the body of the message
110+
if status == 410 {
111+
log_or_print!(
112+
tracing: tracing::error!("Lambda function timeout!"),
113+
fallback: eprintln!("Lambda function timeout!")
114+
);
115+
}
116+
117+
// Return Ok to maintain existing contract - runtime continues despite API errors
118+
break Ok(());
119+
}
120+
Ok(_) => break Ok(()),
121+
Err(err) => {
122+
log_or_print!(
123+
tracing: tracing::error!(error = ?err, "Lambda Runtime API request failed"),
124+
fallback: eprintln!("Lambda Runtime API request failed: {err:?}")
125+
);
126+
break Err(err);
127+
}
89128
},
90-
RuntimeApiClientFutureProj::Second(fut) => break ready!(fut.poll(cx)).map(|_| ()),
91129
}
92130
})
93131
}
94132
}
133+
134+
#[cfg(test)]
135+
mod tests {
136+
use super::*;
137+
use http::StatusCode;
138+
use http_body_util::Full;
139+
use hyper::body::Bytes;
140+
use lambda_runtime_api_client::body::Body;
141+
use std::convert::Infallible;
142+
use tokio::net::TcpListener;
143+
use tracing_test::traced_test;
144+
145+
async fn start_mock_server(status: StatusCode) -> String {
146+
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
147+
let addr = listener.local_addr().unwrap();
148+
let url = format!("http://{}", addr);
149+
150+
tokio::spawn(async move {
151+
let (stream, _) = listener.accept().await.unwrap();
152+
let io = hyper_util::rt::TokioIo::new(stream);
153+
154+
let service = hyper::service::service_fn(move |_req| async move {
155+
Ok::<_, Infallible>(
156+
http::Response::builder()
157+
.status(status)
158+
.body(Full::new(Bytes::from("test response")))
159+
.unwrap(),
160+
)
161+
});
162+
163+
let _ = hyper_util::server::conn::auto::Builder::new(hyper_util::rt::TokioExecutor::new())
164+
.serve_connection(io, service)
165+
.await;
166+
});
167+
168+
// Give the server a moment to start
169+
tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
170+
url
171+
}
172+
173+
#[tokio::test]
174+
#[traced_test]
175+
async fn test_successful_response() {
176+
let url = start_mock_server(StatusCode::OK).await;
177+
let client = Arc::new(
178+
lambda_runtime_api_client::Client::builder()
179+
.with_endpoint(url.parse().unwrap())
180+
.build()
181+
.unwrap(),
182+
);
183+
184+
let request_fut =
185+
async { Ok::<_, BoxError>(http::Request::builder().uri("/test").body(Body::empty()).unwrap()) };
186+
187+
let future = RuntimeApiClientFuture::First(request_fut, client);
188+
let result = future.await;
189+
190+
assert!(result.is_ok());
191+
// No error logs should be present
192+
assert!(!logs_contain("Lambda Runtime API returned non-200 response"));
193+
}
194+
195+
#[tokio::test]
196+
#[traced_test]
197+
async fn test_410_timeout_error() {
198+
let url = start_mock_server(StatusCode::GONE).await;
199+
let client = Arc::new(
200+
lambda_runtime_api_client::Client::builder()
201+
.with_endpoint(url.parse().unwrap())
202+
.build()
203+
.unwrap(),
204+
);
205+
206+
let request_fut =
207+
async { Ok::<_, BoxError>(http::Request::builder().uri("/test").body(Body::empty()).unwrap()) };
208+
209+
let future = RuntimeApiClientFuture::First(request_fut, client);
210+
let result = future.await;
211+
212+
// Returns Ok to maintain contract, but logs the error
213+
assert!(result.is_ok());
214+
215+
// Verify the error was logged
216+
assert!(logs_contain("Lambda Runtime API returned non-200 response"));
217+
assert!(logs_contain("Lambda function timeout!"));
218+
}
219+
220+
#[tokio::test]
221+
#[traced_test]
222+
async fn test_500_error() {
223+
let url = start_mock_server(StatusCode::INTERNAL_SERVER_ERROR).await;
224+
let client = Arc::new(
225+
lambda_runtime_api_client::Client::builder()
226+
.with_endpoint(url.parse().unwrap())
227+
.build()
228+
.unwrap(),
229+
);
230+
231+
let request_fut =
232+
async { Ok::<_, BoxError>(http::Request::builder().uri("/test").body(Body::empty()).unwrap()) };
233+
234+
let future = RuntimeApiClientFuture::First(request_fut, client);
235+
let result = future.await;
236+
237+
// Returns Ok to maintain contract, but logs the error
238+
assert!(result.is_ok());
239+
240+
// Verify the error was logged with status code
241+
assert!(logs_contain("Lambda Runtime API returned non-200 response"));
242+
}
243+
244+
#[tokio::test]
245+
#[traced_test]
246+
async fn test_404_error() {
247+
let url = start_mock_server(StatusCode::NOT_FOUND).await;
248+
let client = Arc::new(
249+
lambda_runtime_api_client::Client::builder()
250+
.with_endpoint(url.parse().unwrap())
251+
.build()
252+
.unwrap(),
253+
);
254+
255+
let request_fut =
256+
async { Ok::<_, BoxError>(http::Request::builder().uri("/test").body(Body::empty()).unwrap()) };
257+
258+
let future = RuntimeApiClientFuture::First(request_fut, client);
259+
let result = future.await;
260+
261+
// Returns Ok to maintain contract, but logs the error
262+
assert!(result.is_ok());
263+
264+
// Verify the error was logged
265+
assert!(logs_contain("Lambda Runtime API returned non-200 response"));
266+
}
267+
268+
#[tokio::test]
269+
#[traced_test]
270+
async fn test_request_build_error() {
271+
let client = Arc::new(
272+
lambda_runtime_api_client::Client::builder()
273+
.with_endpoint("http://localhost:9001".parse().unwrap())
274+
.build()
275+
.unwrap(),
276+
);
277+
278+
let request_fut = async { Err::<http::Request<Body>, BoxError>("Request build error".into()) };
279+
280+
let future = RuntimeApiClientFuture::First(request_fut, client);
281+
let result = future.await;
282+
283+
assert!(result.is_err());
284+
let err = result.unwrap_err();
285+
assert!(err.to_string().contains("Request build error"));
286+
287+
// Verify the error was logged
288+
assert!(logs_contain("failed to build Lambda Runtime API request"));
289+
}
290+
291+
#[tokio::test]
292+
#[traced_test]
293+
async fn test_network_error() {
294+
// Use an invalid endpoint that will fail to connect
295+
let client = Arc::new(
296+
lambda_runtime_api_client::Client::builder()
297+
.with_endpoint("http://127.0.0.1:1".parse().unwrap()) // Port 1 should be unreachable
298+
.build()
299+
.unwrap(),
300+
);
301+
302+
let request_fut =
303+
async { Ok::<_, BoxError>(http::Request::builder().uri("/test").body(Body::empty()).unwrap()) };
304+
305+
let future = RuntimeApiClientFuture::First(request_fut, client);
306+
let result = future.await;
307+
308+
// Network errors should propagate as Err
309+
assert!(result.is_err());
310+
311+
// Verify the error was logged
312+
assert!(logs_contain("Lambda Runtime API request failed"));
313+
}
314+
}

lambda-runtime/src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ use tokio_stream::Stream;
1919
use tower::util::ServiceFn;
2020
pub use tower::{self, service_fn, Service};
2121

22+
#[macro_use]
23+
mod macros;
24+
2225
/// Diagnostic utilities to convert Rust types into Lambda Error types.
2326
pub mod diagnostic;
2427
pub use diagnostic::Diagnostic;

lambda-runtime/src/macros.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// Logs using tracing `error!` if a dispatcher is set, otherwise falls back to `eprintln!`.
2+
macro_rules! log_or_print {
3+
(tracing: $tracing_expr:expr, fallback: $fallback_expr:expr) => {
4+
if tracing::dispatcher::has_been_set() {
5+
$tracing_expr;
6+
} else {
7+
$fallback_expr;
8+
}
9+
};
10+
}

lambda-runtime/src/runtime.rs

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -363,13 +363,12 @@ where
363363
/// unset.
364364
pub async fn run(self) -> Result<(), BoxError> {
365365
if let Some(raw) = concurrency_env_value() {
366-
if tracing::dispatcher::has_been_set() {
367-
tracing::warn!(
366+
log_or_print!(
367+
tracing: tracing::warn!(
368368
"AWS_LAMBDA_MAX_CONCURRENCY is set to '{raw}', but the concurrency-tokio feature is not enabled; running sequentially",
369-
);
370-
} else {
371-
eprintln!("AWS_LAMBDA_MAX_CONCURRENCY is set to '{raw}', but the concurrency-tokio feature is not enabled; running sequentially");
372-
}
369+
),
370+
fallback: eprintln!("AWS_LAMBDA_MAX_CONCURRENCY is set to '{raw}', but the concurrency-tokio feature is not enabled; running sequentially")
371+
);
373372
}
374373
let incoming = incoming(&self.client);
375374
Self::run_with_incoming(self.service, self.config, incoming).await
@@ -938,6 +937,8 @@ mod endpoint_tests {
938937

939938
#[tokio::test]
940939
#[cfg(feature = "concurrency-tokio")]
940+
#[traced_test]
941+
#[cfg(feature = "tokio-concurrent-runtime")]
941942
async fn test_concurrent_structured_logging_isolation() -> Result<(), Error> {
942943
use std::collections::HashSet;
943944
use tracing::info;

0 commit comments

Comments
 (0)