diff --git a/samcli/local/apigw/local_apigw_service.py b/samcli/local/apigw/local_apigw_service.py index 400c91858c..c0764d315f 100644 --- a/samcli/local/apigw/local_apigw_service.py +++ b/samcli/local/apigw/local_apigw_service.py @@ -362,11 +362,7 @@ def _request_handler(self, **kwargs): except FunctionNotFound: return ServiceErrorResponses.lambda_not_found_response() - lambda_response, lambda_logs, _ = LambdaOutputParser.get_lambda_output(stdout_stream) - - if self.stderr and lambda_logs: - # Write the logs to stderr if available. - self.stderr.write(lambda_logs) + lambda_response, _ = LambdaOutputParser.get_lambda_output(stdout_stream) try: if route.event_type == Route.HTTP and ( diff --git a/samcli/local/lambda_service/local_lambda_invoke_service.py b/samcli/local/lambda_service/local_lambda_invoke_service.py index 74b1d169c9..545ae4eec8 100644 --- a/samcli/local/lambda_service/local_lambda_invoke_service.py +++ b/samcli/local/lambda_service/local_lambda_invoke_service.py @@ -167,13 +167,7 @@ def _invoke_request_handler(self, function_name): LOG.debug("%s was not found to invoke.", function_name) return LambdaErrorResponses.resource_not_found(function_name) - lambda_response, lambda_logs, is_lambda_user_error_response = LambdaOutputParser.get_lambda_output( - stdout_stream - ) - - if self.stderr and lambda_logs: - # Write the logs to stderr if available. - self.stderr.write(lambda_logs) + lambda_response, is_lambda_user_error_response = LambdaOutputParser.get_lambda_output(stdout_stream) if is_lambda_user_error_response: return self.service_response( diff --git a/samcli/local/services/base_local_service.py b/samcli/local/services/base_local_service.py index 8b875b07b8..94d06bb673 100644 --- a/samcli/local/services/base_local_service.py +++ b/samcli/local/services/base_local_service.py @@ -1,7 +1,8 @@ """Base class for all Services that interact with Local Lambda""" - +import io import json import logging +from typing import Tuple from flask import Response @@ -81,7 +82,7 @@ def service_response(body, headers, status_code): class LambdaOutputParser: @staticmethod - def get_lambda_output(stdout_stream): + def get_lambda_output(stdout_stream: io.BytesIO) -> Tuple[str, bool]: """ This method will extract read the given stream and return the response from Lambda function separated out from any log statements it might have outputted. Logs end up in the stdout stream if the Lambda function @@ -96,31 +97,10 @@ def get_lambda_output(stdout_stream): ------- str String data containing response from Lambda function - str - String data containng logs statements, if any. bool If the response is an error/exception from the container """ - # We only want the last line of stdout, because it's possible that - # the function may have written directly to stdout using - # System.out.println or similar, before docker-lambda output the result - stdout_data = stdout_stream.getvalue().rstrip(b"\n") - - # Usually the output is just one line and contains response as JSON string, but if the Lambda function - # wrote anything directly to stdout, there will be additional lines. So just extract the last line as - # response and everything else as log output. - lambda_response = stdout_data - lambda_logs = None - - last_line_position = stdout_data.rfind(b"\n") - if last_line_position >= 0: - # So there are multiple lines. Separate them out. - # Everything but the last line are logs - lambda_logs = stdout_data[:last_line_position] - # Last line is Lambda response. Make sure to strip() so we get rid of extra whitespaces & newlines around - lambda_response = stdout_data[last_line_position:].strip() - - lambda_response = lambda_response.decode("utf-8") + lambda_response = stdout_stream.getvalue().decode("utf-8") # When the Lambda Function returns an Error/Exception, the output is added to the stdout of the container. From # our perspective, the container returned some value, which is not always true. Since the output is the only @@ -128,7 +108,7 @@ def get_lambda_output(stdout_stream): # error is_lambda_user_error_response = LambdaOutputParser.is_lambda_error_response(lambda_response) - return lambda_response, lambda_logs, is_lambda_user_error_response + return lambda_response, is_lambda_user_error_response @staticmethod def is_lambda_error_response(lambda_response): diff --git a/tests/integration/local/invoke/test_integrations_cli.py b/tests/integration/local/invoke/test_integrations_cli.py index 54849668f7..335f3e126b 100644 --- a/tests/integration/local/invoke/test_integrations_cli.py +++ b/tests/integration/local/invoke/test_integrations_cli.py @@ -261,16 +261,18 @@ def test_invoke_when_function_writes_stderr(self): "WriteToStderrFunction", template_path=self.template_path, event_path=self.event_path ) - process = Popen(command_list, stderr=PIPE) + process = Popen(command_list, stderr=PIPE, stdout=PIPE) try: - _, stderr = process.communicate(timeout=TIMEOUT) + stdout, stderr = process.communicate(timeout=TIMEOUT) except TimeoutExpired: process.kill() raise process_stderr = stderr.strip() + process_stdout = stdout.strip() self.assertIn("Docker Lambda is writing to stderr", process_stderr.decode("utf-8")) + self.assertIn("wrote to stderr", process_stdout.decode("utf-8")) @pytest.mark.flaky(reruns=3) def test_invoke_returns_expected_result_when_no_event_given(self): diff --git a/tests/unit/local/apigw/test_local_apigw_service.py b/tests/unit/local/apigw/test_local_apigw_service.py index 450d47a8e7..e759e2e9db 100644 --- a/tests/unit/local/apigw/test_local_apigw_service.py +++ b/tests/unit/local/apigw/test_local_apigw_service.py @@ -258,10 +258,9 @@ def test_request_handler_returns_process_stdout_when_making_response(self, lambd parse_output_mock.return_value = ("status_code", Headers({"headers": "headers"}), "body") self.api_service._parse_v1_payload_format_lambda_output = parse_output_mock - lambda_logs = "logs" lambda_response = "response" is_customer_error = False - lambda_output_parser_mock.get_lambda_output.return_value = lambda_response, lambda_logs, is_customer_error + lambda_output_parser_mock.get_lambda_output.return_value = lambda_response, is_customer_error service_response_mock = Mock() service_response_mock.return_value = make_response_mock self.api_service.service_response = service_response_mock @@ -273,8 +272,6 @@ def test_request_handler_returns_process_stdout_when_making_response(self, lambd # Make sure the parse method is called only on the returned response and not on the raw data from stdout parse_output_mock.assert_called_with(lambda_response, ANY, ANY, Route.API) - # Make sure the logs are written to stderr - self.stderr.write.assert_called_with(lambda_logs) @patch.object(LocalApigwService, "get_request_methods_endpoints") def test_request_handler_returns_make_response(self, request_mock): diff --git a/tests/unit/local/lambda_service/test_local_lambda_invoke_service.py b/tests/unit/local/lambda_service/test_local_lambda_invoke_service.py index b93eff0e9f..b717b74cc7 100644 --- a/tests/unit/local/lambda_service/test_local_lambda_invoke_service.py +++ b/tests/unit/local/lambda_service/test_local_lambda_invoke_service.py @@ -50,7 +50,7 @@ def test_create_service_endpoints(self, flask_mock, error_handling_mock): @patch("samcli.local.lambda_service.local_lambda_invoke_service.LocalLambdaInvokeService.service_response") @patch("samcli.local.lambda_service.local_lambda_invoke_service.LambdaOutputParser") def test_invoke_request_handler(self, lambda_output_parser_mock, service_response_mock): - lambda_output_parser_mock.get_lambda_output.return_value = "hello world", None, False + lambda_output_parser_mock.get_lambda_output.return_value = "hello world", False service_response_mock.return_value = "request response" request_mock = Mock() @@ -98,10 +98,9 @@ def test_request_handler_returns_process_stdout_when_making_response( request_mock.get_data.return_value = b"{}" local_lambda_invoke_service.request = request_mock - lambda_logs = "logs" lambda_response = "response" is_customer_error = False - lambda_output_parser_mock.get_lambda_output.return_value = lambda_response, lambda_logs, is_customer_error + lambda_output_parser_mock.get_lambda_output.return_value = lambda_response, is_customer_error service_response_mock.return_value = "request response" @@ -116,9 +115,6 @@ def test_request_handler_returns_process_stdout_when_making_response( self.assertEqual(result, "request response") lambda_output_parser_mock.get_lambda_output.assert_called_with(ANY) - # Make sure the logs are written to stderr - stderr_mock.write.assert_called_with(lambda_logs) - @patch("samcli.local.lambda_service.local_lambda_invoke_service.LambdaErrorResponses") def test_construct_error_handling(self, lambda_error_response_mock): service = LocalLambdaInvokeService(lambda_runner=Mock(), port=3000, host="localhost", stderr=Mock()) @@ -138,7 +134,7 @@ def test_construct_error_handling(self, lambda_error_response_mock): @patch("samcli.local.lambda_service.local_lambda_invoke_service.LocalLambdaInvokeService.service_response") @patch("samcli.local.lambda_service.local_lambda_invoke_service.LambdaOutputParser") def test_invoke_request_handler_with_lambda_that_errors(self, lambda_output_parser_mock, service_response_mock): - lambda_output_parser_mock.get_lambda_output.return_value = "hello world", None, True + lambda_output_parser_mock.get_lambda_output.return_value = "hello world", True service_response_mock.return_value = "request response" request_mock = Mock() request_mock.get_data.return_value = b"{}" @@ -159,7 +155,7 @@ def test_invoke_request_handler_with_lambda_that_errors(self, lambda_output_pars @patch("samcli.local.lambda_service.local_lambda_invoke_service.LocalLambdaInvokeService.service_response") @patch("samcli.local.lambda_service.local_lambda_invoke_service.LambdaOutputParser") def test_invoke_request_handler_with_no_data(self, lambda_output_parser_mock, service_response_mock): - lambda_output_parser_mock.get_lambda_output.return_value = "hello world", None, False + lambda_output_parser_mock.get_lambda_output.return_value = "hello world", False service_response_mock.return_value = "request response" request_mock = Mock() diff --git a/tests/unit/local/services/test_base_local_service.py b/tests/unit/local/services/test_base_local_service.py index f1112fc03e..fec13e25c9 100644 --- a/tests/unit/local/services/test_base_local_service.py +++ b/tests/unit/local/services/test_base_local_service.py @@ -66,30 +66,26 @@ def test_create_returns_not_implemented(self): class TestLambdaOutputParser(TestCase): @parameterized.expand( [ + param("with mixed data and json response", b'data\n{"a": "b"}', 'data\n{"a": "b"}'), + param("with response as string", b"response", "response"), + param("with json response only", b'{"a": "b"}', '{"a": "b"}'), + param("with one new line and json", b'\n{"a": "b"}', '\n{"a": "b"}'), + param("with response only as string", b"this is the response line", "this is the response line"), + param("with whitespaces", b'data\n{"a": "b"} \n\n\n', 'data\n{"a": "b"} \n\n\n'), + param("with empty data", b"", ""), + param("with just new lines", b"\n\n", "\n\n"), param( - "with both logs and response", b'this\nis\nlog\ndata\n{"a": "b"}', b"this\nis\nlog\ndata", '{"a": "b"}' - ), - param("with response as string", b"logs\nresponse", b"logs", "response"), - param("with response only", b'{"a": "b"}', None, '{"a": "b"}'), - param("with one new line and response", b'\n{"a": "b"}', b"", '{"a": "b"}'), - param("with response only as string", b"this is the response line", None, "this is the response line"), - param("with whitespaces", b'log\ndata\n{"a": "b"} \n\n\n', b"log\ndata", '{"a": "b"}'), - param("with empty data", b"", None, ""), - param("with just new lines", b"\n\n", None, ""), - param( - "with no data but with whitespaces", + "with whitespaces", b"\n \n \n", - b"\n ", - "", # Log data with whitespaces will be in the output unchanged + "\n \n \n", ), ] ) - def test_get_lambda_output_extracts_response(self, test_case_name, stdout_data, expected_logs, expected_response): + def test_get_lambda_output_extracts_response(self, test_case_name, stdout_data, expected_response): stdout = Mock() stdout.getvalue.return_value = stdout_data - response, logs, is_customer_error = LambdaOutputParser.get_lambda_output(stdout) - self.assertEqual(logs, expected_logs) + response, is_customer_error = LambdaOutputParser.get_lambda_output(stdout) self.assertEqual(response, expected_response) self.assertFalse(is_customer_error)