diff --git a/samcli/commands/init/command.py b/samcli/commands/init/command.py index 0d26b79a72..6f60ca1d86 100644 --- a/samcli/commands/init/command.py +++ b/samcli/commands/init/command.py @@ -226,6 +226,11 @@ def wrapped(*args, **kwargs): default=None, help="Enable CloudWatch Application Insights monitoring for application.", ) +@click.option( + "--structured-logging/--no-structured-logging", + default=None, + help="Enable Structured Logging for application.", +) @common_options @save_params_option @non_interactive_validation @@ -249,6 +254,7 @@ def cli( extra_context, tracing, application_insights, + structured_logging, save_params, config_file, config_env, @@ -273,6 +279,7 @@ def cli( extra_context, tracing, application_insights, + structured_logging, ) # pragma: no cover @@ -294,6 +301,7 @@ def do_cli( extra_context, tracing, application_insights, + structured_logging, ): """ Implementation of the ``cli`` method @@ -346,6 +354,7 @@ def do_cli( extra_context, tracing, application_insights, + structured_logging, ) else: if not (pt_explicit or runtime or dependency_manager or base_image or architecture): @@ -366,6 +375,7 @@ def do_cli( no_input, tracing, application_insights, + structured_logging, ) diff --git a/samcli/commands/init/core/options.py b/samcli/commands/init/core/options.py index fc5c3ac4ac..586c919ba5 100644 --- a/samcli/commands/init/core/options.py +++ b/samcli/commands/init/core/options.py @@ -25,10 +25,7 @@ CONFIGURATION_OPTION_NAMES: List[str] = ["config_env", "config_file"] + SAVE_PARAMS_OPTIONS -ADDITIONAL_OPTIONS: List[str] = [ - "tracing", - "application_insights", -] +ADDITIONAL_OPTIONS: List[str] = ["tracing", "application_insights", "structured_logging"] ALL_OPTIONS: List[str] = ( APPLICATION_OPTIONS + NON_INTERACTIVE_OPTIONS + CONFIGURATION_OPTION_NAMES + ADDITIONAL_OPTIONS + ALL_COMMON_OPTIONS diff --git a/samcli/commands/init/init_generator.py b/samcli/commands/init/init_generator.py index 8927e3fd05..454405615b 100644 --- a/samcli/commands/init/init_generator.py +++ b/samcli/commands/init/init_generator.py @@ -18,6 +18,7 @@ def do_generate( extra_context, tracing, application_insights, + structured_logging, ): try: generate_project( @@ -31,6 +32,7 @@ def do_generate( extra_context, tracing, application_insights, + structured_logging, ) except InitErrorException as e: raise UserException(str(e), wrapped_from=e.__class__.__name__) from e diff --git a/samcli/commands/init/interactive_init_flow.py b/samcli/commands/init/interactive_init_flow.py index 004243724c..8f724dbed1 100644 --- a/samcli/commands/init/interactive_init_flow.py +++ b/samcli/commands/init/interactive_init_flow.py @@ -53,6 +53,7 @@ def do_interactive( no_input, tracing, application_insights, + structured_logging, ): """ Implementation of the ``cli`` method when --interactive is provided. @@ -80,6 +81,7 @@ def do_interactive( location_opt_choice, tracing, application_insights, + structured_logging, ) @@ -98,6 +100,7 @@ def generate_application( location_opt_choice, tracing, application_insights, + structured_logging, ): # pylint: disable=too-many-arguments """ The method holds the decision logic for generating an application @@ -132,6 +135,8 @@ def generate_application( boolen value to determine if X-Ray tracing show be activated or not application_insights : bool boolean value to determine if AppInsights monitoring should be enabled or not + structured_logging: bool + boolean value to determine if Json structured logging should be enabled or not """ if location_opt_choice == "1": _generate_from_use_case( @@ -147,6 +152,7 @@ def generate_application( architecture, tracing, application_insights, + structured_logging, ) else: @@ -160,12 +166,22 @@ def generate_application( no_input, tracing, application_insights, + structured_logging, ) # pylint: disable=too-many-statements def _generate_from_location( - location, package_type, runtime, dependency_manager, output_dir, name, no_input, tracing, application_insights + location, + package_type, + runtime, + dependency_manager, + output_dir, + name, + no_input, + tracing, + application_insights, + structured_logging, ): location = click.prompt("\nTemplate location (git, mercurial, http(s), zip, path)", type=str) summary_msg = """ @@ -189,6 +205,7 @@ def _generate_from_location( None, tracing, application_insights, + structured_logging, ) @@ -206,6 +223,7 @@ def _generate_from_use_case( architecture: Optional[str], tracing: Optional[bool], application_insights: Optional[bool], + structured_logging: Optional[bool], ) -> None: templates = InitTemplates() runtime_or_base_image = runtime if runtime else base_image @@ -236,6 +254,9 @@ def _generate_from_use_case( if application_insights is None: application_insights = prompt_user_to_enable_application_insights() + if structured_logging is None: + structured_logging = prompt_user_to_enable_structured_logging() + app_template = template_chosen["appTemplate"] base_image = ( LAMBDA_IMAGES_RUNTIMES_MAP.get(str(runtime)) if not base_image and package_type == IMAGE else base_image @@ -292,6 +313,7 @@ def _generate_from_use_case( extra_context, tracing, application_insights, + structured_logging, ) # executing event_bridge logic if call is for Schema dynamic template if is_dynamic_schemas_template: @@ -426,6 +448,23 @@ def prompt_user_to_enable_application_insights(): return False +def prompt_user_to_enable_structured_logging(): + """ + Prompt user to choose if structured loggingConfig should activated + for their functions in the SAM template and vice versa + """ + if click.confirm("\nWould you like to set Structured Logging in JSON format on your Lambda functions? "): + doc_link = ( + "https://docs.aws.amazon.com/lambda/latest/dg/" + "monitoring-cloudwatchlogs.html#monitoring-cloudwatchlogs-pricing" + ) + click.echo( + f"Structured Logging in JSON format might incur an additional cost. View {doc_link} for more details" + ) + return True + return False + + def _get_choice_from_options(chosen, options, question, msg): if chosen: return chosen diff --git a/samcli/commands/local/lib/local_lambda.py b/samcli/commands/local/lib/local_lambda.py index d0d1446b5c..14f7578187 100644 --- a/samcli/commands/local/lib/local_lambda.py +++ b/samcli/commands/local/lib/local_lambda.py @@ -296,6 +296,7 @@ def _make_env_vars(self, function: Function) -> EnvironmentVariables: function.memory, function.timeout, function.handler, + function.logging_config, variables=variables, shell_env_values=shell_env, override_values=overrides, diff --git a/samcli/lib/init/__init__.py b/samcli/lib/init/__init__.py index a4eac534f5..e0b8ff0e5b 100644 --- a/samcli/lib/init/__init__.py +++ b/samcli/lib/init/__init__.py @@ -15,6 +15,7 @@ from samcli.lib.init.template_modifiers.application_insights_template_modifier import ( ApplicationInsightsTemplateModifier, ) +from samcli.lib.init.template_modifiers.structured_logging_template_modifier import StructuredLoggingTemplateModifier from samcli.lib.init.template_modifiers.xray_tracing_template_modifier import XRayTracingTemplateModifier from samcli.lib.telemetry.event import EventName, EventTracker, UsedFeature from samcli.lib.utils import osutils @@ -38,6 +39,7 @@ def generate_project( extra_context=None, tracing=False, application_insights=False, + structured_logging=False, ): """Generates project using cookiecutter and options given @@ -70,6 +72,8 @@ def generate_project( Enable or disable X-Ray Tracing application_insights: Optional[str] Enable or disable AppInsights Monitoring + structured_logging: Optional[bool] + boolean value to determine if Json structured logging should be enabled or not Raises ------ @@ -132,6 +136,8 @@ def generate_project( _enable_application_insights(application_insights, output_dir, name) + _enable_structured_logging(structured_logging, output_dir, name) + _create_default_samconfig(package_type, output_dir, name) @@ -150,6 +156,13 @@ def _enable_application_insights(application_insights: bool, output_dir: str, na EventTracker.track_event(EventName.USED_FEATURE.value, UsedFeature.INIT_WITH_APPLICATION_INSIGHTS.value) +def _enable_structured_logging(structured_logging, output_dir, name): + if structured_logging: + template_file_path = f"{output_dir}/{name}/template.yaml" + template_modifier = StructuredLoggingTemplateModifier(template_file_path) + template_modifier.modify_template() + + def _create_default_samconfig(package_type: str, output_dir: str, name: str) -> None: """ Init post-processing function to create a default samconfig.toml diff --git a/samcli/lib/init/template_modifiers/structured_logging_template_modifier.py b/samcli/lib/init/template_modifiers/structured_logging_template_modifier.py new file mode 100644 index 0000000000..798ce9cd18 --- /dev/null +++ b/samcli/lib/init/template_modifiers/structured_logging_template_modifier.py @@ -0,0 +1,98 @@ +""" +Class used to parse and update template when structured logging is enabled +""" +import logging +from typing import Any + +from ruamel.yaml import YAML +from ruamel.yaml.comments import CommentedMap +from ruamel.yaml.representer import RoundTripRepresenter + +from samcli.lib.init.template_modifiers.cli_template_modifier import TemplateModifier + +LOG = logging.getLogger(__name__) + + +class StructuredLoggingTemplateModifier(TemplateModifier): + GLOBALS = "Globals" + RESOURCE = "Resources" + FUNCTION = "Function" + LOGGING_CONFIG = "LoggingConfig" + JSON_LOGFORMAT = {"LogFormat": "JSON"} + DOC_LINK = ( + "https://docs.aws.amazon.com/serverless-application-model/latest/" + "developerguide/sam-resource-function.html#sam-function-loggingconfig" + ) + + COMMENT = ( + "More info about Globals: " + "https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst\n" + ) + + MESSAGE = ( + "You can add LoggingConfig parameters such as the Logformat, " + "Log Group, and SystemLogLevel or ApplicationLogLevel. " + f"Learn more here {DOC_LINK}.\n" + ) + + # set ignore aliases to true. This configuration avoids usage yaml aliases which is not parsed by CloudFormation. + class NonAliasingRTRepresenter(RoundTripRepresenter): + def ignore_aliases(self, data): + return True + + def __init__(self, location): + self.yaml = YAML() + self.yaml.Representer = StructuredLoggingTemplateModifier.NonAliasingRTRepresenter + super().__init__(location) + + def _get_template(self) -> Any: + with open(self.template_location) as file: + return self.yaml.load(file) + + def _update_template_fields(self): + """ + Add new field to SAM template + """ + if self.template.get(self.GLOBALS): + template_globals = self.template.get(self.GLOBALS) + + function_globals = template_globals.get(self.FUNCTION, {}) + if not function_globals: + template_globals[self.FUNCTION] = {} + template_globals[self.FUNCTION][self.LOGGING_CONFIG] = CommentedMap(self.JSON_LOGFORMAT) + template_globals[self.FUNCTION].yaml_set_comment_before_after_key( + self.LOGGING_CONFIG, before=self.MESSAGE, indent=4 + ) + + else: + self._add_logging_config_with_globals() + + def _add_logging_config_with_globals(self): + """Adds Globals and LoggingConfig fields""" + global_section = { + self.FUNCTION: {self.LOGGING_CONFIG: self.JSON_LOGFORMAT}, + } + + self.template = CommentedMap(self.template) + self.template[self.GLOBALS] = CommentedMap(global_section) + self.template[self.GLOBALS].yaml_set_comment_before_after_key(self.LOGGING_CONFIG, before=self.MESSAGE) + self.template.yaml_set_comment_before_after_key(self.GLOBALS, before=self.COMMENT) + + def _print_sanity_check_error(self): + message = ( + "Warning: Unable to add LoggingConfig to the project. " + "To learn more about LoggingConfig visit {self.DOC_LINK}" + ) + LOG.warning(message) + + def _write(self, template: list): + """ + write generated template into SAM template + + Parameters + ---------- + template : list + array with updated template data + """ + with open(self.template_location, "w") as file: + self.yaml.dump(self.template, file) diff --git a/samcli/lib/observability/cw_logs/cw_log_group_provider.py b/samcli/lib/observability/cw_logs/cw_log_group_provider.py index faee3d8297..0ff83925e5 100644 --- a/samcli/lib/observability/cw_logs/cw_log_group_provider.py +++ b/samcli/lib/observability/cw_logs/cw_log_group_provider.py @@ -24,7 +24,7 @@ class LogGroupProvider: def for_resource(boto_client_provider: BotoProviderType, resource_type: str, name: str) -> Optional[str]: log_group = None if resource_type == AWS_LAMBDA_FUNCTION: - log_group = LogGroupProvider.for_lambda_function(name) + log_group = LogGroupProvider.for_lambda_function(boto_client_provider, name) elif resource_type == AWS_APIGATEWAY_RESTAPI: log_group = LogGroupProvider.for_apigw_rest_api(name) elif resource_type == AWS_APIGATEWAY_V2_API: @@ -35,12 +35,14 @@ def for_resource(boto_client_provider: BotoProviderType, resource_type: str, nam return log_group @staticmethod - def for_lambda_function(function_name: str) -> str: + def for_lambda_function(boto_client_provider: BotoProviderType, function_name: str) -> str: """ Returns the CloudWatch Log Group Name created by default for the AWS Lambda function with given name Parameters ---------- + boto_client_provider: BotoProviderType + Boto client provider which contains region and other configurations function_name : str Name of the Lambda function @@ -49,7 +51,16 @@ def for_lambda_function(function_name: str) -> str: str Default Log Group name used by this function """ - return "/aws/lambda/{}".format(function_name) + log_group_name = "" + try: + function_configuration = boto_client_provider("lambda").get_function_configuration(function_name) + logging_config = function_configuration.get("LoggingConfig") + if logging_config: + log_group_name = logging_config.get("LogGroup") + except Exception as ex: + LOG.debug("Could not retrive function configuration for function (%s): (%s)", function_name, str(ex)) + + return log_group_name if bool(log_group_name) else f"/aws/lambda/{function_name}" @staticmethod def for_apigw_rest_api(rest_api_id: str, stage: str = "Prod") -> str: diff --git a/samcli/lib/providers/provider.py b/samcli/lib/providers/provider.py index 58e2febdb7..71c593a249 100644 --- a/samcli/lib/providers/provider.py +++ b/samcli/lib/providers/provider.py @@ -111,6 +111,8 @@ class Function(NamedTuple): stack_path: str = "" # Configuration for runtime management. Includes the fields `UpdateRuntimeOn` and `RuntimeVersionArn` (optional). runtime_management_config: Optional[Dict] = None + # LoggingConfig for Advanced logging + logging_config: Optional[Dict] = None @property def full_path(self) -> str: diff --git a/samcli/lib/providers/sam_function_provider.py b/samcli/lib/providers/sam_function_provider.py index b7adfa597a..fde30ba502 100644 --- a/samcli/lib/providers/sam_function_provider.py +++ b/samcli/lib/providers/sam_function_provider.py @@ -473,6 +473,7 @@ def _build_function_configuration( function_url_config=resource_properties.get("FunctionUrlConfig"), runtime_management_config=resource_properties.get("RuntimeManagementConfig"), function_build_info=function_build_info, + logging_config=resource_properties.get("LoggingConfig"), ) @staticmethod diff --git a/samcli/local/lambdafn/env_vars.py b/samcli/local/lambdafn/env_vars.py index 4aefb76bef..ebf52ce5be 100644 --- a/samcli/local/lambdafn/env_vars.py +++ b/samcli/local/lambdafn/env_vars.py @@ -44,6 +44,7 @@ def __init__( function_memory=None, function_timeout=None, function_handler=None, + function_logging_config=None, variables=None, shell_env_values=None, override_values=None, @@ -58,6 +59,7 @@ def __init__( :param integer function_memory: Memory size of the function in megabytes :param integer function_timeout: Function's timeout in seconds :param string function_handler: Handler of the function + :param string function_logging_config: Logging Config for the function :param dict variables: Optional. Dict whose key is the environment variable names and value is the default values for the variable. :param dict shell_env_values: Optional. Dict containing values for the variables grabbed from the shell's @@ -79,6 +81,7 @@ def __init__( self.shell_env_values = shell_env_values or {} self.override_values = override_values or {} self.aws_creds = aws_creds or {} + self.logging_config = function_logging_config or {} def resolve(self): """ @@ -179,6 +182,17 @@ def _get_aws_variables(self): if self.aws_creds.get("sessiontoken"): result["AWS_SESSION_TOKEN"] = self.aws_creds.get("sessiontoken") + # Add the ApplicationLogLevel as a env variable and also update the function's LogGroup name + log_group = self.logging_config.get("LogGroup") + if log_group: + result["AWS_LAMBDA_LOG_GROUP_NAME"] = log_group + + log_format = self.logging_config.get("LogFormat") + if log_format: + result["AWS_LAMBDA_LOG_FORMAT"] = log_format + if log_format == "JSON": + result["AWS_LAMBDA_LOG_LEVEL"] = self.logging_config.get("ApplicationLogLevel", "INFO") + return result def _stringify_value(self, value): diff --git a/schema/samcli.json b/schema/samcli.json index 609bb38407..69420d9d40 100644 --- a/schema/samcli.json +++ b/schema/samcli.json @@ -23,7 +23,7 @@ "properties": { "parameters": { "title": "Parameters for the init command", - "description": "Available parameters for the init command:\n* no_interactive:\nDisable interactive prompting for init parameters. (fail if any required values are missing)\n* architecture:\nArchitectures for Lambda functions.\n\nArchitectures: ['arm64', 'x86_64']\n* location:\nTemplate location (git, mercurial, http(s), zip, path).\n* runtime:\nLambda runtime for application.\n\nRuntimes: dotnet6, go1.x, java17, java11, java8.al2, java8, nodejs18.x, nodejs16.x, nodejs14.x, nodejs12.x, provided, provided.al2, provided.al2023, python3.9, python3.8, python3.7, python3.11, python3.10, ruby3.2, ruby2.7\n* package_type:\nLambda deployment package type.\n\nPackage Types: Zip, Image\n* base_image:\nLambda base image for deploying IMAGE based package type.\n\nBase images: amazon/dotnet6-base, amazon/go-provided.al2-base, amazon/go-provided.al2023-base, amazon/go1.x-base, amazon/java11-base, amazon/java17-base, amazon/java8-base, amazon/java8.al2-base, amazon/nodejs12.x-base, amazon/nodejs14.x-base, amazon/nodejs16.x-base, amazon/nodejs18.x-base, amazon/python3.10-base, amazon/python3.11-base, amazon/python3.7-base, amazon/python3.8-base, amazon/python3.9-base, amazon/ruby2.7-base, amazon/ruby3.2-base\n* dependency_manager:\nDependency manager for Lambda runtime.\n\nDependency managers: bundler, cli-package, gradle, maven, mod, npm, pip\n* output_dir:\nDirectory to initialize AWS SAM application.\n* name:\nName of AWS SAM Application.\n* app_template:\nIdentifier of the managed application template to be used. Alternatively, run '$sam init' without options for an interactive workflow.\n* no_input:\nDisable Cookiecutter prompting and accept default values defined in the cookiecutter config.\n* extra_context:\nOverride custom parameters in the template's cookiecutter.json configuration e.g. {\"customParam1\": \"customValue1\", \"customParam2\":\"customValue2\"}\n* tracing:\nEnable AWS X-Ray tracing for application.\n* application_insights:\nEnable CloudWatch Application Insights monitoring for application.\n* beta_features:\nEnable/Disable beta features.\n* debug:\nTurn on debug logging to print debug message generated by AWS SAM CLI and display timestamps.\n* save_params:\nSave the parameters provided via the command line to the configuration file.", + "description": "Available parameters for the init command:\n* no_interactive:\nDisable interactive prompting for init parameters. (fail if any required values are missing)\n* architecture:\nArchitectures for Lambda functions.\n\nArchitectures: ['arm64', 'x86_64']\n* location:\nTemplate location (git, mercurial, http(s), zip, path).\n* runtime:\nLambda runtime for application.\n\nRuntimes: dotnet6, go1.x, java17, java11, java8.al2, java8, nodejs18.x, nodejs16.x, nodejs14.x, nodejs12.x, provided, provided.al2, python3.9, python3.8, python3.7, python3.11, python3.10, ruby3.2, ruby2.7\n* package_type:\nLambda deployment package type.\n\nPackage Types: Zip, Image\n* base_image:\nLambda base image for deploying IMAGE based package type.\n\nBase images: amazon/dotnet6-base, amazon/go-provided.al2-base, amazon/go1.x-base, amazon/java11-base, amazon/java17-base, amazon/java8-base, amazon/java8.al2-base, amazon/nodejs12.x-base, amazon/nodejs14.x-base, amazon/nodejs16.x-base, amazon/nodejs18.x-base, amazon/python3.10-base, amazon/python3.11-base, amazon/python3.7-base, amazon/python3.8-base, amazon/python3.9-base, amazon/ruby2.7-base, amazon/ruby3.2-base\n* dependency_manager:\nDependency manager for Lambda runtime.\n\nDependency managers: bundler, cli-package, gradle, maven, mod, npm, pip\n* output_dir:\nDirectory to initialize AWS SAM application.\n* name:\nName of AWS SAM Application.\n* app_template:\nIdentifier of the managed application template to be used. Alternatively, run '$sam init' without options for an interactive workflow.\n* no_input:\nDisable Cookiecutter prompting and accept default values defined in the cookiecutter config.\n* extra_context:\nOverride custom parameters in the template's cookiecutter.json configuration e.g. {\"customParam1\": \"customValue1\", \"customParam2\":\"customValue2\"}\n* tracing:\nEnable AWS X-Ray tracing for application.\n* application_insights:\nEnable CloudWatch Application Insights monitoring for application.\n* structured_logging:\nEnable Structured Logging for application.\n* beta_features:\nEnable/Disable beta features.\n* debug:\nTurn on debug logging to print debug message generated by AWS SAM CLI and display timestamps.\n* save_params:\nSave the parameters provided via the command line to the configuration file.", "type": "object", "properties": { "no_interactive": { @@ -159,6 +159,11 @@ "type": "boolean", "description": "Enable CloudWatch Application Insights monitoring for application." }, + "structured_logging": { + "title": "structured_logging", + "type": "boolean", + "description": "Enable Structured Logging for application." + }, "beta_features": { "title": "beta_features", "type": "boolean", diff --git a/tests/functional/commands/init/test_interactive_init_flow.py b/tests/functional/commands/init/test_interactive_init_flow.py index 2d5226ba0d..da05965fea 100644 --- a/tests/functional/commands/init/test_interactive_init_flow.py +++ b/tests/functional/commands/init/test_interactive_init_flow.py @@ -60,6 +60,7 @@ def test_unknown_runtime(self, git_repo_mock, requests_mock): no_input=False, tracing=False, application_insights=False, + structured_logging=False, ) output_files = list(self.output_dir.rglob("*")) self.assertEqual(len(output_files), 9) diff --git a/tests/integration/init/test_init_command.py b/tests/integration/init/test_init_command.py index 0e6a40737f..d59519e448 100644 --- a/tests/integration/init/test_init_command.py +++ b/tests/integration/init/test_init_command.py @@ -476,6 +476,69 @@ def test_init_command_passes_with_disabled_application_insights(self): self.assertTrue(Path(temp, "sam-app").is_dir()) self._assert_template_with_cfn_lint(Path(temp, "sam-app")) + def test_init_command_passes_with_structured_logging(self): + with tempfile.TemporaryDirectory() as temp: + process = Popen( + [ + get_sam_command(), + "init", + "--runtime", + "nodejs14.x", + "--dependency-manager", + "npm", + "--app-template", + "hello-world", + "--name", + "sam-app", + "--no-interactive", + "-o", + temp, + "--tracing", + "--structured-logging", + ] + ) + try: + process.communicate(timeout=TIMEOUT) + except TimeoutExpired: + process.kill() + raise + + self.assertEqual(process.returncode, 0) + self.assertTrue(Path(temp, "sam-app").is_dir()) + # TODO: ungate once cfn-lint support `Structured Logging` + # self._assert_template_with_cfn_lint(Path(temp, "sam-app")) + + def test_init_command_passes_with_no_structured_logging(self): + with tempfile.TemporaryDirectory() as temp: + process = Popen( + [ + get_sam_command(), + "init", + "--runtime", + "nodejs14.x", + "--dependency-manager", + "npm", + "--app-template", + "hello-world", + "--name", + "sam-app", + "--no-interactive", + "-o", + temp, + "--no-tracing", + "--no-structured-logging", + ] + ) + try: + process.communicate(timeout=TIMEOUT) + except TimeoutExpired: + process.kill() + raise + + self.assertEqual(process.returncode, 0) + self.assertTrue(Path(temp, "sam-app").is_dir()) + self._assert_template_with_cfn_lint(Path(temp, "sam-app")) + def _assert_template_with_cfn_lint(self, cwd): """Assert if the generated project passes cfn-lint""" cmd_list = [ @@ -843,6 +906,7 @@ def test_interactive_init(self): # 1: Hello World Example # N: Would you like to enable X-Ray tracing on the function(s) in your application? [y/N] # Y: Would you like to enable monitoring using Cloudwatch Application Insights? [y/N] + # Y: Would you like to set Structured Logging in JSON format on your Lambda functions? [y/N] user_input = """ 1 1 @@ -852,6 +916,7 @@ def test_interactive_init(self): 1 N Y +Y sam-interactive-init-app """ with tempfile.TemporaryDirectory() as temp: @@ -872,6 +937,7 @@ def test_interactive_init_default_runtime(self): Y N N +N sam-interactive-init-app-default-runtime """ with tempfile.TemporaryDirectory() as temp: diff --git a/tests/unit/commands/init/test_cli.py b/tests/unit/commands/init/test_cli.py index 776bce490b..d6518927ed 100644 --- a/tests/unit/commands/init/test_cli.py +++ b/tests/unit/commands/init/test_cli.py @@ -118,6 +118,7 @@ def test_init_cli(self, generate_project_patch, git_repo_clone_mock): extra_context=None, tracing=False, application_insights=False, + structured_logging=True, ) # THEN we should receive no errors @@ -134,6 +135,7 @@ def test_init_cli(self, generate_project_patch, git_repo_clone_mock): self.extra_context_as_json, False, False, + True, ) @patch("samcli.lib.utils.git_repo.GitRepo.clone") @@ -158,6 +160,7 @@ def test_init_cli_node(self, generate_project_patch, git_repo_clone_mock): extra_context=None, tracing=False, application_insights=False, + structured_logging=False, ) # THEN we should receive no errors @@ -173,6 +176,7 @@ def test_init_cli_node(self, generate_project_patch, git_repo_clone_mock): {"runtime": "nodejs20.x", "project_name": "testing project", "architectures": {"value": ["x86_64"]}}, False, False, + False, ) @patch("samcli.lib.utils.git_repo.GitRepo.clone") @@ -197,6 +201,7 @@ def test_init_image_cli(self, generate_project_patch, git_repo_clone_mock): extra_context=None, tracing=False, application_insights=False, + structured_logging=False, ) # THEN we should receive no errors @@ -212,6 +217,7 @@ def test_init_image_cli(self, generate_project_patch, git_repo_clone_mock): {"runtime": "nodejs12.x", "project_name": "testing project", "architectures": {"value": [ARM64]}}, False, False, + False, ) @patch("samcli.lib.utils.git_repo.GitRepo.clone") @@ -236,6 +242,7 @@ def test_init_cli_with_tracing(self, generate_project_patch, git_repo_clone_mock extra_context=None, tracing=True, application_insights=False, + structured_logging=False, ) # THEN we should receive no errors @@ -252,6 +259,7 @@ def test_init_cli_with_tracing(self, generate_project_patch, git_repo_clone_mock self.extra_context_as_json, True, False, + False, ) @patch("samcli.lib.utils.git_repo.GitRepo.clone") @@ -276,6 +284,7 @@ def test_init_cli_with_application_insights(self, generate_project_patch, git_re extra_context=None, tracing=False, application_insights=True, + structured_logging=False, ) # THEN we should receive no errors @@ -292,6 +301,7 @@ def test_init_cli_with_application_insights(self, generate_project_patch, git_re self.extra_context_as_json, False, True, + False, ) @patch("samcli.lib.utils.git_repo.GitRepo.clone") @@ -316,6 +326,7 @@ def test_init_image_java_cli(self, generate_project_patch, git_repo_clone_mock): extra_context=None, tracing=False, application_insights=False, + structured_logging=True, ) # THEN we should receive no errors @@ -331,6 +342,7 @@ def test_init_image_java_cli(self, generate_project_patch, git_repo_clone_mock): {"runtime": "java11", "project_name": "testing project", "architectures": {"value": [X86_64]}}, False, False, + True, ) @patch("samcli.lib.utils.git_repo.GitRepo.clone") @@ -355,6 +367,7 @@ def test_init_fails_invalid_template(self, git_repo_clone_mock): extra_context=None, tracing=False, application_insights=False, + structured_logging=False, ) @patch("samcli.lib.utils.git_repo.GitRepo.clone") @@ -379,6 +392,7 @@ def test_init_fails_invalid_dep_mgr(self, git_repo_clone_mock): extra_context=None, tracing=False, application_insights=False, + structured_logging=False, ) @patch("samcli.lib.utils.git_repo.GitRepo.clone") @@ -409,6 +423,7 @@ def test_init_cli_generate_project_fails(self, generate_project_patch, git_repo_ extra_context=None, tracing=False, application_insights=False, + structured_logging=False, ) generate_project_patch.assert_called_with( @@ -420,6 +435,7 @@ def test_init_cli_generate_project_fails(self, generate_project_patch, git_repo_ self.no_input, False, False, + False, ) @patch("samcli.lib.utils.git_repo.GitRepo.clone") @@ -450,6 +466,7 @@ def test_init_cli_generate_project_image_fails(self, generate_project_patch, git extra_context=None, tracing=False, application_insights=False, + structured_logging=False, ) generate_project_patch.assert_called_with( @@ -461,6 +478,7 @@ def test_init_cli_generate_project_image_fails(self, generate_project_patch, git self.no_input, False, False, + False, ) @patch("samcli.lib.utils.git_repo.GitRepo.clone") @@ -485,6 +503,7 @@ def test_init_cli_with_extra_context_parameter_not_passed(self, generate_project extra_context=None, tracing=False, application_insights=False, + structured_logging=False, ) # THEN we should receive no errors @@ -500,6 +519,7 @@ def test_init_cli_with_extra_context_parameter_not_passed(self, generate_project self.extra_context_as_json, False, False, + False, ) @patch("samcli.lib.utils.git_repo.GitRepo.clone") @@ -524,6 +544,7 @@ def test_init_cli_with_extra_context_parameter_passed(self, generate_project_pat extra_context='{"schema_name":"events", "schema_type":"aws"}', tracing=False, application_insights=False, + structured_logging=False, ) # THEN we should receive no errors and right extra_context should be passed @@ -544,6 +565,7 @@ def test_init_cli_with_extra_context_parameter_passed(self, generate_project_pat }, False, False, + False, ) @patch("samcli.lib.utils.git_repo.GitRepo.clone") @@ -570,6 +592,7 @@ def test_init_cli_with_extra_context_not_overriding_default_parameter( extra_context='{"project_name": "my_project", "runtime": "java8", "schema_name":"events", "schema_type": "aws"}', tracing=False, application_insights=False, + structured_logging=False, ) # THEN extra_context should have not overridden default_parameters(name, runtime) @@ -590,6 +613,7 @@ def test_init_cli_with_extra_context_not_overriding_default_parameter( }, False, False, + False, ) @patch("samcli.lib.utils.git_repo.GitRepo.clone") @@ -614,6 +638,7 @@ def test_init_cli_with_extra_context_input_as_wrong_json_raises_exception(self, extra_context='{"project_name", "my_project", "runtime": "java8", "schema_name":"events", "schema_type": "aws"}', tracing=False, application_insights=False, + structured_logging=False, ) @patch("samcli.commands.init.init_generator.generate_project") @@ -637,6 +662,7 @@ def test_init_cli_must_set_default_context_when_location_is_provided(self, gener extra_context='{"schema_name":"events", "schema_type": "aws"}', tracing=False, application_insights=False, + structured_logging=False, ) # THEN should set default parameter(name, runtime) as extra_context @@ -657,6 +683,7 @@ def test_init_cli_must_set_default_context_when_location_is_provided(self, gener }, False, False, + False, ) @patch("samcli.commands.init.init_generator.generate_project") @@ -680,6 +707,7 @@ def test_init_cli_must_only_set_passed_project_name_when_location_is_provided(se extra_context='{"schema_name":"events", "schema_type": "aws"}', tracing=False, application_insights=False, + structured_logging=True, ) # THEN extra_context should be without runtime @@ -699,6 +727,7 @@ def test_init_cli_must_only_set_passed_project_name_when_location_is_provided(se }, False, False, + True, ) @patch("samcli.commands.init.init_generator.generate_project") @@ -722,6 +751,7 @@ def test_init_cli_must_only_set_passed_runtime_when_location_is_provided(self, g extra_context='{"schema_name":"events", "schema_type": "aws"}', tracing=False, application_insights=False, + structured_logging=True, ) # THEN extra_context should be without name @@ -741,6 +771,7 @@ def test_init_cli_must_only_set_passed_runtime_when_location_is_provided(self, g }, False, False, + True, ) @patch("samcli.lib.utils.git_repo.GitRepo.clone") @@ -766,6 +797,7 @@ def test_init_cli_with_extra_context_parameter_passed_as_escaped(self, generate_ extra_context='{\"schema_name\":\"events\", \"schema_type\":\"aws\"}', tracing=False, application_insights= False, + structured_logging=False # fmt: on ) @@ -787,6 +819,7 @@ def test_init_cli_with_extra_context_parameter_passed_as_escaped(self, generate_ }, False, False, + False, ) @patch.object(InitTemplates, "__init__", MockInitTemplates.__init__) @@ -900,6 +933,7 @@ def test_init_cli_int_with_event_bridge_app_template( 2 N N +N test-project Y 1 @@ -930,6 +964,7 @@ def test_init_cli_int_with_event_bridge_app_template( }, False, False, + False, ) get_schemas_client_mock.assert_called_once_with(None, "ap-northeast-1") do_extract_and_merge_schemas_code_mock.do_extract_and_merge_schemas_code_mock( @@ -984,6 +1019,7 @@ def test_init_cli_int_with_image_app_template( 1 N N +y test-project """ runner = CliRunner() @@ -1000,6 +1036,7 @@ def test_init_cli_int_with_image_app_template( {"project_name": "test-project", "runtime": "java8", "architectures": {"value": [X86_64]}}, False, False, + True, ) @patch.object(InitTemplates, "__init__", MockInitTemplates.__init__) @@ -1116,6 +1153,7 @@ def test_init_cli_int_with_event_bridge_app_template_and_aws_configuration( 2 N N +N test-project N 1 @@ -1148,6 +1186,7 @@ def test_init_cli_int_with_event_bridge_app_template_and_aws_configuration( }, False, False, + False, ) get_schemas_client_mock.assert_called_once_with("default", "us-east-1") do_extract_and_merge_schemas_code_mock.do_extract_and_merge_schemas_code("result.zip", ".", "test-project", ANY) @@ -1245,6 +1284,7 @@ def test_init_cli_int_with_event_bridge_app_template_and_aws_configuration_with_ 1 N N +N test-project N 1 @@ -1372,6 +1412,7 @@ def test_init_cli_int_with_download_manager_raises_exception( 2 N N +N test-project Y 1 @@ -1402,6 +1443,7 @@ def test_init_cli_int_with_download_manager_raises_exception( }, False, False, + False, ) get_schemas_client_mock.assert_called_once_with(None, "ap-northeast-1") do_extract_and_merge_schemas_code_mock.do_extract_and_merge_schemas_code_mock( @@ -1516,6 +1558,7 @@ def test_init_cli_int_with_schemas_details_raises_exception( 1 N N +N test-project Y 1 @@ -1551,6 +1594,7 @@ def test_init_passes_dynamic_event_bridge_template(self, generate_project_patch, architecture=ARM64, tracing=False, application_insights=False, + structured_logging=False, ) self.extra_context_as_json["architectures"] = {"value": [ARM64]} @@ -1566,6 +1610,7 @@ def test_init_passes_dynamic_event_bridge_template(self, generate_project_patch, self.extra_context_as_json, False, False, + False, ) @patch("samcli.lib.utils.git_repo.GitRepo.clone") @@ -1597,6 +1642,7 @@ def test_init_cli_int_from_location(self, generate_project_patch, git_repo_clone None, None, None, + None, ) @patch("samcli.commands.init.init_templates.InitTemplates._get_manifest") @@ -1624,6 +1670,7 @@ def test_init_cli_no_package_type(self, generate_project_patch, git_repo_clone_m "amazon/python3.8-base", "--dependency-manager", "pip", + "--no-structured-logging", ] runner = CliRunner() result = runner.invoke(init_cmd, args=args, input=user_input) @@ -1631,7 +1678,7 @@ def test_init_cli_no_package_type(self, generate_project_patch, git_repo_clone_m # THEN we should receive no errors self.assertFalse(result.exception) generate_project_patch.assert_called_once_with( - ANY, IMAGE, "python3.8", "pip", ".", "untitled6", True, ANY, False, False + ANY, IMAGE, "python3.8", "pip", ".", "untitled6", True, ANY, False, False, False ) @patch.object(InitTemplates, "__init__", MockInitTemplates.__init__) @@ -1674,6 +1721,7 @@ def test_init_cli_image_pool_with_base_image_having_multiple_managed_template_bu extra_context=self.extra_context, tracing=False, application_insights=False, + structured_logging=False, ) @patch.object(InitTemplates, "__init__", MockInitTemplates.__init__) @@ -1716,6 +1764,7 @@ def test_init_cli_image_pool_with_base_image_having_multiple_managed_template_an extra_context=self.extra_context, tracing=False, application_insights=False, + structured_logging=False, ) @patch.object(InitTemplates, "__init__", MockInitTemplates.__init__) @@ -1759,6 +1808,7 @@ def test_init_cli_image_pool_with_base_image_having_multiple_managed_template_wi extra_context=None, tracing=False, application_insights=False, + structured_logging=False, ) generate_project_patch.assert_called_once_with( ANY, # location @@ -1771,6 +1821,7 @@ def test_init_cli_image_pool_with_base_image_having_multiple_managed_template_wi ANY, False, False, + False, ) @patch.object(InitTemplates, "__init__", MockInitTemplates.__init__) @@ -1807,6 +1858,7 @@ def test_init_cli_image_pool_with_base_image_having_one_managed_template_does_no tracing=False, application_insights=False, architecture=None, + structured_logging=False, ) generate_project_patch.assert_called_once_with( ANY, # location @@ -1819,6 +1871,7 @@ def test_init_cli_image_pool_with_base_image_having_one_managed_template_does_no ANY, False, False, + False, ) @patch.object(InitTemplates, "__init__", MockInitTemplates.__init__) @@ -1855,6 +1908,7 @@ def test_init_cli_image_pool_with_base_image_having_one_managed_template_with_pr tracing=False, application_insights=False, architecture=None, + structured_logging=False, ) generate_project_patch.assert_called_once_with( ANY, # location @@ -1867,6 +1921,7 @@ def test_init_cli_image_pool_with_base_image_having_one_managed_template_with_pr ANY, False, False, + False, ) @patch.object(InitTemplates, "__init__", MockInitTemplates.__init__) @@ -1904,6 +1959,7 @@ def test_init_cli_image_pool_with_base_image_having_one_managed_template_with_pr tracing=False, application_insights=False, architecture=None, + structured_logging=False, ) @patch("samcli.lib.utils.git_repo.GitRepo.clone") @@ -1933,7 +1989,7 @@ def test_init_cli_must_pass_with_architecture_and_base_image(self, generate_proj # THEN we should receive no errors self.assertFalse(result.exception) generate_project_patch.assert_called_once_with( - ANY, IMAGE, "java11", "gradle", ".", "untitled6", True, ANY, None, None + ANY, IMAGE, "java11", "gradle", ".", "untitled6", True, ANY, None, None, None ) PackageType.explicit = ( False # Other tests fail after we pass --packge-type in this test, so let's reset this variable @@ -2008,6 +2064,7 @@ def test_init_cli_generate_default_hello_world_app( y N N +N test-project """ @@ -2025,6 +2082,7 @@ def test_init_cli_generate_default_hello_world_app( {"project_name": "test-project", "runtime": "python3.9", "architectures": {"value": ["x86_64"]}}, False, False, + False, ) @patch("samcli.commands.init.init_templates.InitTemplates.get_preprocessed_manifest") @@ -2097,6 +2155,7 @@ def test_init_cli_must_not_generate_default_hello_world_app( 1 N N +N test-project """ @@ -2114,6 +2173,7 @@ def test_init_cli_must_not_generate_default_hello_world_app( {"project_name": "test-project", "runtime": "java11", "architectures": {"value": ["x86_64"]}}, False, False, + False, ) def test_must_return_runtime_from_base_image_name(self): @@ -2278,6 +2338,7 @@ def test_init_fails_unsupported_dep_mgr_for_runtime(self, git_repo_clone_mock, c tracing=False, application_insights=False, architecture=X86_64, + structured_logging=False, ) expected_error_message = ( "Lambda Runtime java8 and dependency manager pip does not have an available initialization template." @@ -2298,14 +2359,7 @@ def test_init_cli_with_mismatch_dep_runtime(self, git_repo_clone_mock, _get_mani n """ - args = [ - "--name", - "untitled6", - "--runtime", - "go1.x", - "--dependency-manager", - "pip", - ] + args = ["--name", "untitled6", "--runtime", "go1.x", "--dependency-manager", "pip", "--no-structured-logging"] runner = CliRunner() result = runner.invoke(init_cmd, args=args, input=user_input) @@ -2379,6 +2433,7 @@ def test_init_cli_int_with_multiple_app_templates( 1 N N +N test-project """ runner = CliRunner() @@ -2395,6 +2450,7 @@ def test_init_cli_int_with_multiple_app_templates( {"project_name": "test-project", "runtime": "java11", "architectures": {"value": ["x86_64"]}}, False, False, + False, ) @patch("samcli.commands.init.init_templates.LOG") @@ -2463,6 +2519,7 @@ def test_init_cli_int_must_raise_for_unsupported_runtime( 1 N N +N test-project """ runner = CliRunner() @@ -2518,6 +2575,7 @@ def test_init_cli_int_must_raise_for_unsupported_dependency( 1 N N +N test-project """ runner = CliRunner() @@ -2591,6 +2649,7 @@ def test_init_cli_generate_hello_world_app_without_default_prompt( 1 N N +N test-project """ @@ -2608,6 +2667,7 @@ def test_init_cli_generate_hello_world_app_without_default_prompt( {"project_name": "test-project", "runtime": "java11", "architectures": {"value": ["x86_64"]}}, False, False, + False, ) @patch.object(InitTemplates, "__init__", MockInitTemplates.__init__) @@ -2682,6 +2742,7 @@ def test_init_cli_generate_app_template_provide_via_options( 1 N N +N test-project """ @@ -2699,6 +2760,7 @@ def test_init_cli_generate_app_template_provide_via_options( {"project_name": "test-project", "runtime": "java11", "architectures": {"value": ["x86_64"]}}, False, False, + False, ) def does_template_meet_filter_criteria(self): @@ -2765,6 +2827,7 @@ def test_init_cli_generate_app_template_from_local_cli_templates( 2 N N +N test-project """ @@ -2782,6 +2845,7 @@ def test_init_cli_generate_app_template_from_local_cli_templates( {"project_name": "test-project", "runtime": "java11", "architectures": {"value": ["x86_64"]}}, False, False, + False, ) @patch("samcli.local.common.runtime_template.INIT_RUNTIMES") @@ -2854,6 +2918,7 @@ def test_init_cli_generate_app_template_with_custom_runtime( 2 N N +N test-project """ @@ -2871,6 +2936,7 @@ def test_init_cli_generate_app_template_with_custom_runtime( {"project_name": "test-project", "runtime": "provided.al2", "architectures": {"value": ["x86_64"]}}, False, False, + False, ) @patch("samcli.commands.init.init_templates.InitTemplates._get_manifest") @@ -2931,6 +2997,7 @@ def test_init_cli_generate_app_template_with_custom_runtime_using_options( 1 N N +N test-project """ args = [ @@ -2952,6 +3019,7 @@ def test_init_cli_generate_app_template_with_custom_runtime_using_options( {"project_name": "test-project", "runtime": "provided.al2", "architectures": {"value": ["x86_64"]}}, False, False, + False, ) @patch("samcli.commands.init.init_templates.InitTemplates.get_preprocessed_manifest") @@ -3020,6 +3088,7 @@ def test_init_cli_generate_app_template_provide_via_tracing_options( N 1 N +N test-project """ @@ -3037,6 +3106,7 @@ def test_init_cli_generate_app_template_provide_via_tracing_options( {"project_name": "test-project", "runtime": "java11", "architectures": {"value": ["x86_64"]}}, True, False, + False, ) @patch("samcli.commands.init.init_templates.InitTemplates.get_preprocessed_manifest") @@ -3105,6 +3175,7 @@ def test_init_cli_generate_app_template_provide_via_application_insights_options N 1 N +N test-project """ @@ -3122,4 +3193,5 @@ def test_init_cli_generate_app_template_provide_via_application_insights_options {"project_name": "test-project", "runtime": "java11", "architectures": {"value": ["x86_64"]}}, False, True, + False, ) diff --git a/tests/unit/commands/local/lib/test_local_lambda.py b/tests/unit/commands/local/lib/test_local_lambda.py index dc5f4384bd..384771f753 100644 --- a/tests/unit/commands/local/lib/test_local_lambda.py +++ b/tests/unit/commands/local/lib/test_local_lambda.py @@ -253,6 +253,7 @@ def test_must_work_with_override_values( function_url_config=None, runtime_management_config=None, function_build_info=FunctionBuildInfo.BuildableZip, + logging_config={"LogFormat": "JSON"}, ) self.local_lambda.env_vars_values = env_vars_values @@ -264,6 +265,7 @@ def test_must_work_with_override_values( function.memory, function.timeout, function.handler, + function.logging_config, variables={"var1": "value1"}, shell_env_values=os_environ, override_values=expected_override_value, @@ -362,6 +364,7 @@ def test_must_work_with_invalid_environment_variable(self, environment_variable, function.memory, function.timeout, function.handler, + None, variables=None, shell_env_values=os_environ, override_values={}, diff --git a/tests/unit/commands/samconfig/test_samconfig.py b/tests/unit/commands/samconfig/test_samconfig.py index b2e0822c78..d0c373d18f 100644 --- a/tests/unit/commands/samconfig/test_samconfig.py +++ b/tests/unit/commands/samconfig/test_samconfig.py @@ -81,6 +81,7 @@ def test_init(self, do_cli_mock): '{"key": "value", "key2": "value2"}', None, ANY, + None, ) @patch("samcli.commands.validate.validate.do_cli") diff --git a/tests/unit/lib/observability/cw_logs/test_cw_log_group_provider.py b/tests/unit/lib/observability/cw_logs/test_cw_log_group_provider.py index 19c4746f37..ac7cd0843c 100644 --- a/tests/unit/lib/observability/cw_logs/test_cw_log_group_provider.py +++ b/tests/unit/lib/observability/cw_logs/test_cw_log_group_provider.py @@ -7,9 +7,32 @@ class TestLogGroupProvider_for_lambda_function(TestCase): - def test_must_return_log_group_name(self): + def test_must_return_default_log_group_name(self): expected = "/aws/lambda/my_function_name" - result = LogGroupProvider.for_lambda_function("my_function_name") + given_client_provider = Mock() + given_client_provider("Lambda").get_function_configuration.return_value = {} + + result = LogGroupProvider.for_lambda_function(given_client_provider, "my_function_name") + + self.assertEqual(expected, result) + + def test_must_return_custom_log_group_name(self): + expected = "my_log_group" + given_client_provider = Mock() + given_client_provider("Lambda").get_function_configuration.return_value = { + "LoggingConfig": {"LogGroup": "my_log_group"} + } + + result = LogGroupProvider.for_lambda_function(given_client_provider, "my_function_name") + + self.assertEqual(expected, result) + + def test_must_return_default_log_group_name_with_exception_raised(self): + expected = "/aws/lambda/my_function_name" + given_client_provider = Mock() + given_client_provider("Lambda").get_function_configuration.side_effect = Exception() + + result = LogGroupProvider.for_lambda_function(given_client_provider, "my_function_name") self.assertEqual(expected, result) diff --git a/tests/unit/local/lambdafn/test_env_vars.py b/tests/unit/local/lambdafn/test_env_vars.py index c0babe5fe5..f831bfe61a 100644 --- a/tests/unit/local/lambdafn/test_env_vars.py +++ b/tests/unit/local/lambdafn/test_env_vars.py @@ -13,15 +13,18 @@ def test_must_initialize_with_empty_values(self): memory = 123 timeout = 10 handler = "handler" + logging_config = {"logFormat": "JSON"} environ = EnvironmentVariables() environ.memory = memory environ.timeout = timeout environ.handler = handler + environ.logging_config = logging_config self.assertEqual(environ.memory, memory) self.assertEqual(environ.timeout, timeout) self.assertEqual(environ.handler, handler) + self.assertEqual(environ.logging_config, logging_config) def test_must_initialize_values_with_required_values(self): memory = 123 @@ -332,6 +335,101 @@ def test_must_work_with_partial_aws_creds(self): environ = EnvironmentVariables(self.name, self.memory, self.timeout, self.handler, aws_creds=creds) self.assertEqual(expected, environ._get_aws_variables()) + def test_must_work_with_text_logformat(self): + expected = { + "AWS_SAM_LOCAL": "true", + "AWS_LAMBDA_FUNCTION_MEMORY_SIZE": "1024", + "AWS_LAMBDA_FUNCTION_TIMEOUT": "123", + "AWS_LAMBDA_FUNCTION_HANDLER": "handler", + "AWS_LAMBDA_FUNCTION_NAME": self.name, + "AWS_LAMBDA_FUNCTION_VERSION": "$LATEST", + "AWS_LAMBDA_LOG_GROUP_NAME": f"aws/lambda/{self.name}", + "AWS_LAMBDA_LOG_STREAM_NAME": "$LATEST", + "AWS_ACCOUNT_ID": "123456789012", + # Default values assigned to these variables + "AWS_REGION": "us-east-1", + "AWS_DEFAULT_REGION": "us-east-1", + "AWS_ACCESS_KEY_ID": "defaultkey", + "AWS_SECRET_ACCESS_KEY": "defaultsecret", + "AWS_LAMBDA_LOG_FORMAT": "Text", + } + + logging_config = {"LogFormat": "Text"} + environ = EnvironmentVariables(self.name, self.memory, self.timeout, self.handler, logging_config) + self.assertEqual(expected, environ._get_aws_variables()) + + def test_must_work_with_default_json_logging_config(self): + expected = { + "AWS_SAM_LOCAL": "true", + "AWS_LAMBDA_FUNCTION_MEMORY_SIZE": "1024", + "AWS_LAMBDA_FUNCTION_TIMEOUT": "123", + "AWS_LAMBDA_FUNCTION_HANDLER": "handler", + "AWS_LAMBDA_FUNCTION_NAME": self.name, + "AWS_LAMBDA_FUNCTION_VERSION": "$LATEST", + "AWS_LAMBDA_LOG_GROUP_NAME": f"aws/lambda/{self.name}", + "AWS_LAMBDA_LOG_STREAM_NAME": "$LATEST", + "AWS_ACCOUNT_ID": "123456789012", + # Default values assigned to these variables + "AWS_REGION": "us-east-1", + "AWS_DEFAULT_REGION": "us-east-1", + "AWS_ACCESS_KEY_ID": "defaultkey", + "AWS_SECRET_ACCESS_KEY": "defaultsecret", + "AWS_LAMBDA_LOG_LEVEL": "INFO", + "AWS_LAMBDA_LOG_FORMAT": "JSON", + } + + logging_config = {"LogFormat": "JSON"} + environ = EnvironmentVariables(self.name, self.memory, self.timeout, self.handler, logging_config) + self.assertEqual(expected, environ._get_aws_variables()) + + def test_must_work_with_set_application_log_level(self): + expected = { + "AWS_SAM_LOCAL": "true", + "AWS_LAMBDA_FUNCTION_MEMORY_SIZE": "1024", + "AWS_LAMBDA_FUNCTION_TIMEOUT": "123", + "AWS_LAMBDA_FUNCTION_HANDLER": "handler", + "AWS_LAMBDA_FUNCTION_NAME": self.name, + "AWS_LAMBDA_FUNCTION_VERSION": "$LATEST", + "AWS_LAMBDA_LOG_GROUP_NAME": f"aws/lambda/{self.name}", + "AWS_LAMBDA_LOG_STREAM_NAME": "$LATEST", + "AWS_ACCOUNT_ID": "123456789012", + # Default values assigned to these variables + "AWS_REGION": "us-east-1", + "AWS_DEFAULT_REGION": "us-east-1", + "AWS_ACCESS_KEY_ID": "defaultkey", + "AWS_SECRET_ACCESS_KEY": "defaultsecret", + "AWS_LAMBDA_LOG_LEVEL": "TRACE", + "AWS_LAMBDA_LOG_FORMAT": "JSON", + } + + logging_config = {"LogFormat": "JSON", "ApplicationLogLevel": "TRACE"} + environ = EnvironmentVariables(self.name, self.memory, self.timeout, self.handler, logging_config) + self.assertEqual(expected, environ._get_aws_variables()) + + def test_must_work_with_custom_log_group_name(self): + expected = { + "AWS_SAM_LOCAL": "true", + "AWS_LAMBDA_FUNCTION_MEMORY_SIZE": "1024", + "AWS_LAMBDA_FUNCTION_TIMEOUT": "123", + "AWS_LAMBDA_FUNCTION_HANDLER": "handler", + "AWS_LAMBDA_FUNCTION_NAME": self.name, + "AWS_LAMBDA_FUNCTION_VERSION": "$LATEST", + "AWS_LAMBDA_LOG_GROUP_NAME": "myCustomLogGroup", + "AWS_LAMBDA_LOG_STREAM_NAME": "$LATEST", + "AWS_ACCOUNT_ID": "123456789012", + # Default values assigned to these variables + "AWS_REGION": "us-east-1", + "AWS_DEFAULT_REGION": "us-east-1", + "AWS_ACCESS_KEY_ID": "defaultkey", + "AWS_SECRET_ACCESS_KEY": "defaultsecret", + "AWS_LAMBDA_LOG_LEVEL": "TRACE", + "AWS_LAMBDA_LOG_FORMAT": "JSON", + } + + logging_config = {"LogFormat": "JSON", "ApplicationLogLevel": "TRACE", "LogGroup": "myCustomLogGroup"} + environ = EnvironmentVariables(self.name, self.memory, self.timeout, self.handler, logging_config) + self.assertEqual(expected, environ._get_aws_variables()) + class TestEnvironmentVariables_stringify_value(TestCase): def setUp(self):