Skip to content

Commit b553d1e

Browse files
committed
feat: ⚡ Improved Logging when Lambda Runtime Api fails, this will be useful for the customer for timeouts
1 parent c565705 commit b553d1e

5 files changed

Lines changed: 237 additions & 8 deletions

File tree

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

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: 5 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

0 commit comments

Comments
 (0)