diff --git a/src/cloudformation_cli_python_lib/hook.py b/src/cloudformation_cli_python_lib/hook.py index f92f445..5504d6a 100644 --- a/src/cloudformation_cli_python_lib/hook.py +++ b/src/cloudformation_cli_python_lib/hook.py @@ -65,13 +65,17 @@ def wrapper(self: Any, event: MutableMapping[str, Any], context: Any) -> Any: class Hook: def __init__( - self, type_name: str, type_configuration_model_cls: Type[BaseModel] + self, + type_name: str, + type_configuration_model_cls: Type[BaseModel], + log_format: Optional[logging.Formatter] = None, ) -> None: self.type_name = type_name self._type_configuration_model_cls: Type[ BaseModel ] = type_configuration_model_cls self._handlers: MutableMapping[HookInvocationPoint, HandlerSignature] = {} + self.log_format = log_format def handler( self, invocation_point: HookInvocationPoint @@ -247,7 +251,7 @@ def print_or_log(message: str) -> None: metrics = MetricsPublisherProxy() if event.requestData.providerLogGroupName and provider_sess: - HookProviderLogHandler.setup(event, provider_sess) + HookProviderLogHandler.setup(event, provider_sess, self.log_format) logs_setup = True metrics.add_hook_metrics_publisher( provider_sess, event.hookTypeName, event.awsAccountId diff --git a/src/cloudformation_cli_python_lib/log_delivery.py b/src/cloudformation_cli_python_lib/log_delivery.py index d620899..3a38a0c 100644 --- a/src/cloudformation_cli_python_lib/log_delivery.py +++ b/src/cloudformation_cli_python_lib/log_delivery.py @@ -35,7 +35,10 @@ def _get_existing_logger(cls) -> Optional["ProviderLogHandler"]: @classmethod def setup( - cls, request: HandlerRequest, provider_sess: Optional[SessionProxy] + cls, + request: HandlerRequest, + provider_sess: Optional[SessionProxy], + log_format: Optional[logging.Formatter] = None, ) -> None: log_group = request.requestData.providerLogGroupName if request.stackId and request.requestData.logicalResourceId: @@ -50,11 +53,16 @@ def setup( # we just refresh the client with new creds log_handler.client = provider_sess.client("logs") return + # filter provider messages from platform provider = request.resourceType.replace("::", "_").lower() log_handler = cls( group=log_group, stream=stream_name, session=provider_sess ) + + if log_format: + log_handler.setFormatter(log_format) + # add log handler to root, so that provider gets plugin logs too logging.getLogger().addHandler(log_handler) logging.getLogger().handlers[0].addFilter(ProviderFilter(provider)) @@ -114,7 +122,10 @@ def _get_existing_logger(cls) -> Optional["HookProviderLogHandler"]: @classmethod def setup( # type: ignore - cls, request: HookInvocationRequest, provider_sess: Optional[SessionProxy] + cls, + request: HookInvocationRequest, + provider_sess: Optional[SessionProxy], + log_format: Optional[logging.Formatter] = None, ) -> None: log_group = request.requestData.providerLogGroupName if request.stackId and request.requestData.targetLogicalId: @@ -129,11 +140,16 @@ def setup( # type: ignore # we just refresh the client with new creds log_handler.client = provider_sess.client("logs") return + # filter provider messages from platform provider = request.hookTypeName.replace("::", "_").lower() logging.getLogger().handlers[0].addFilter(ProviderFilter(provider)) log_handler = cls( group=log_group, stream=stream_name, session=provider_sess ) + + if log_format: + log_handler.setFormatter(log_format) + # add log handler to root, so that provider gets plugin logs too logging.getLogger().addHandler(log_handler) diff --git a/src/cloudformation_cli_python_lib/resource.py b/src/cloudformation_cli_python_lib/resource.py index ace3116..c4d8cd1 100644 --- a/src/cloudformation_cli_python_lib/resource.py +++ b/src/cloudformation_cli_python_lib/resource.py @@ -61,6 +61,7 @@ def __init__( type_name: str, resouce_model_cls: Type[BaseModel], type_configuration_model_cls: Optional[Type[BaseModel]] = None, + log_format: Optional[logging.Formatter] = None, ) -> None: self.type_name = type_name self._model_cls: Type[BaseModel] = resouce_model_cls @@ -68,6 +69,7 @@ def __init__( Type[BaseModel] ] = type_configuration_model_cls self._handlers: MutableMapping[Action, HandlerSignature] = {} + self.log_format = log_format def handler(self, action: Action) -> Callable[[HandlerSignature], HandlerSignature]: def _add_handler(f: HandlerSignature) -> HandlerSignature: @@ -200,7 +202,7 @@ def print_or_log(message: str) -> None: metrics = MetricsPublisherProxy() if event.requestData.providerLogGroupName and provider_sess: - ProviderLogHandler.setup(event, provider_sess) + ProviderLogHandler.setup(event, provider_sess, self.log_format) logs_setup = True metrics.add_metrics_publisher(provider_sess, event.resourceType) diff --git a/src/setup.py b/src/setup.py index 1a98285..0efe6aa 100644 --- a/src/setup.py +++ b/src/setup.py @@ -3,7 +3,7 @@ setup( name="cloudformation-cli-python-lib", - version="2.1.12", + version="2.1.13a1", description=__doc__, author="Amazon Web Services", author_email="aws-cloudformation-developers@amazon.com", diff --git a/tests/lib/log_delivery_test.py b/tests/lib/log_delivery_test.py index 8cfeaff..002437f 100644 --- a/tests/lib/log_delivery_test.py +++ b/tests/lib/log_delivery_test.py @@ -90,6 +90,15 @@ def setup_patches(mock_logger): ) +@pytest.fixture +def mock_handler_set_formatter(): + patch__set_handler_formatter = patch.object(ProviderLogHandler, "setFormatter") + patch__set_hook_handler_formatter = patch.object( + HookProviderLogHandler, "setFormatter" + ) + return patch__set_handler_formatter, patch__set_hook_handler_formatter + + @pytest.fixture def mock_provider_handler(): plh = ProviderLogHandler( @@ -197,6 +206,21 @@ def test_setup_existing_logger(setup_patches, mock_session): mock_log.return_value.addHandler.assert_not_called() +def test_setup_with_formatter(setup_patches, mock_session, mock_handler_set_formatter): + payload, _hook_payload, p_logger, p__get_logger, _p__get_hook_logger = setup_patches + ( + p__set_handler_formatter, + _p__set_hook_handler_formatter, + ) = mock_handler_set_formatter + formatter = logging.Formatter() + with p_logger as mock_log, p__get_logger as mock_get, p__set_handler_formatter as mock_set_formatter: # pylint: disable=C0301 # noqa: B950 + mock_get.return_value = None + ProviderLogHandler.setup(payload, mock_session, formatter) + mock_session.client.assert_called_once_with("logs") + mock_log.return_value.addHandler.assert_called_once() + mock_set_formatter.assert_called_once_with(formatter) + + def test_setup_without_log_group_should_not_set_up(mock_logger, mock_session): patch_logger = patch( "cloudformation_cli_python_lib.log_delivery.logging.getLogger", @@ -387,6 +411,23 @@ def test_setup_existing_hook_logger(setup_patches, mock_session): mock_log.return_value.addHandler.assert_not_called() +def test_setup_with_hook_formatter( + setup_patches, mock_session, mock_handler_set_formatter +): + _payload, hook_payload, p_logger, p__get_logger, _p__get_hook_logger = setup_patches + ( + _p__set_handler_formatter, + p__set_hook_handler_formatter, + ) = mock_handler_set_formatter + formatter = logging.Formatter() + with p_logger as mock_log, p__get_logger as mock_get, p__set_hook_handler_formatter as mock_set_formatter: # pylint: disable=C0301 # noqa: B950 + mock_get.return_value = None + HookProviderLogHandler.setup(hook_payload, mock_session, formatter) + mock_session.client.assert_called_once_with("logs") + mock_log.return_value.addHandler.assert_called_once() + mock_set_formatter.assert_called_once_with(formatter) + + def test_setup_without_hook_log_group_should_not_set_up(mock_logger, mock_session): patch_logger = patch( "cloudformation_cli_python_lib.log_delivery.logging.getLogger",