From 0f08e35c101b5790b5fe0a13c64e0eff1d8f7436 Mon Sep 17 00:00:00 2001 From: Bert Blommers Date: Sat, 28 Dec 2024 12:55:16 -0100 Subject: [PATCH] Logs: describe_log_groups() now returns the logStreamArn-property --- moto/logs/models.py | 36 +- moto/logs/responses.py | 30 +- tests/test_logs/test_export_tasks.py | 12 +- tests/test_logs/test_logs.py | 419 ++++-------------- tests/test_logs/test_logs_cloudformation.py | 37 +- ...lter.py => test_logs_filter_log_events.py} | 0 tests/test_logs/test_logs_metric_filters.py | 337 ++++++++++++++ tests/test_logs/test_logs_tags.py | 2 +- .../test_resourcegroupstagging_logs.py | 4 +- 9 files changed, 509 insertions(+), 368 deletions(-) rename tests/test_logs/{test_logs_filter.py => test_logs_filter_log_events.py} (100%) create mode 100644 tests/test_logs/test_logs_metric_filters.py diff --git a/moto/logs/models.py b/moto/logs/models.py index 3d78a740dcb9..646cb10838b2 100644 --- a/moto/logs/models.py +++ b/moto/logs/models.py @@ -1,3 +1,4 @@ +import re from datetime import datetime, timedelta from gzip import compress as gzip_compress from typing import Any, Dict, Iterable, List, Optional, Tuple @@ -598,7 +599,8 @@ def filter_log_events( def to_describe_dict(self) -> Dict[str, Any]: log_group = { - "arn": self.arn, + "arn": f"{self.arn}:*", + "logGroupArn": self.arn, "creationTime": self.creation_time, "logGroupName": self.name, "metricFilterCount": 0, @@ -898,12 +900,12 @@ def describe_log_streams( descending: bool, limit: int, log_group_name: str, + log_group_id: str, log_stream_name_prefix: str, next_token: Optional[str], order_by: str, ) -> Tuple[List[Dict[str, Any]], Optional[str]]: - if log_group_name not in self.groups: - raise ResourceNotFoundException() + log_group = self._find_log_group(log_group_id, log_group_name) if limit > 50: raise InvalidParameterException( constraint="Member must have value less than or equal to 50", @@ -920,7 +922,6 @@ def describe_log_streams( raise InvalidParameterException( msg="Cannot order by LastEventTime with a logStreamNamePrefix." ) - log_group = self.groups[log_group_name] return log_group.describe_log_streams( descending=descending, limit=limit, @@ -968,6 +969,7 @@ def put_log_events( def get_log_events( self, log_group_name: str, + log_group_id: str, log_stream_name: str, start_time: str, end_time: str, @@ -975,15 +977,15 @@ def get_log_events( next_token: Optional[str], start_from_head: str, ) -> Tuple[List[Dict[str, Any]], Optional[str], Optional[str]]: - if log_group_name not in self.groups: - raise ResourceNotFoundException() + log_group = self._find_log_group( + log_group_id=log_group_id, log_group_name=log_group_name + ) if limit and limit > 10000: raise InvalidParameterException( constraint="Member must have value less than or equal to 10000", parameter="limit", value=limit, ) - log_group = self.groups[log_group_name] return log_group.get_log_events( log_stream_name, start_time, end_time, limit, next_token, start_from_head ) @@ -1327,5 +1329,25 @@ def tag_resource(self, arn: str, tags: Dict[str, str]) -> None: def untag_resource(self, arn: str, tag_keys: List[str]) -> None: self.tagger.untag_resource_using_names(arn, tag_keys) + def _find_log_group(self, log_group_id: str, log_group_name: str) -> LogGroup: + log_group: Optional[LogGroup] = None + if log_group_name: + log_group = self.groups.get(log_group_name) + elif log_group_id: + if not re.fullmatch(r"[\w#+=/:,.@-]*", log_group_id): + raise InvalidParameterException( + msg=f"1 validation error detected: Value '{log_group_id}' at 'logGroupIdentifier' failed to satisfy constraint: Member must satisfy regular expression pattern: [\\w#+=/:,.@-]*" + ) + for log_group in self.groups.values(): + if log_group.arn == log_group_id: + log_group = log_group + else: + raise InvalidParameterException( + "Should provider either name or id, but not both" + ) + if not log_group: + raise ResourceNotFoundException() + return log_group + logs_backends = BackendDict(LogsBackend, "logs") diff --git a/moto/logs/responses.py b/moto/logs/responses.py index da45f04724e7..44e7ffb0670e 100644 --- a/moto/logs/responses.py +++ b/moto/logs/responses.py @@ -236,6 +236,7 @@ def delete_log_stream(self) -> str: def describe_log_streams(self) -> str: log_group_name = self._get_param("logGroupName") + log_group_id = self._get_param("logGroupIdentifier") log_stream_name_prefix = self._get_param("logStreamNamePrefix", "") descending = self._get_param("descending", False) limit = self._get_param("limit", 50) @@ -243,12 +244,13 @@ def describe_log_streams(self) -> str: order_by = self._get_param("orderBy", "LogStreamName") streams, next_token = self.logs_backend.describe_log_streams( - descending, - limit, - log_group_name, - log_stream_name_prefix, - next_token, - order_by, + descending=descending, + limit=limit, + log_group_name=log_group_name, + log_group_id=log_group_id, + log_stream_name_prefix=log_stream_name_prefix, + next_token=next_token, + order_by=order_by, ) return json.dumps({"logStreams": streams, "nextToken": next_token}) @@ -272,6 +274,7 @@ def put_log_events(self) -> str: def get_log_events(self) -> str: log_group_name = self._get_param("logGroupName") + log_group_id = self._get_param("logGroupIdentifier") log_stream_name = self._get_param("logStreamName") start_time = self._get_param("startTime") end_time = self._get_param("endTime") @@ -284,13 +287,14 @@ def get_log_events(self) -> str: next_backward_token, next_forward_token, ) = self.logs_backend.get_log_events( - log_group_name, - log_stream_name, - start_time, - end_time, - limit, - next_token, - start_from_head, + log_group_name=log_group_name, + log_group_id=log_group_id, + log_stream_name=log_stream_name, + start_time=start_time, + end_time=end_time, + limit=limit, + next_token=next_token, + start_from_head=start_from_head, ) return json.dumps( { diff --git a/tests/test_logs/test_export_tasks.py b/tests/test_logs/test_export_tasks.py index 2ca54efca856..1b86da62c2d2 100644 --- a/tests/test_logs/test_export_tasks.py +++ b/tests/test_logs/test_export_tasks.py @@ -1,6 +1,5 @@ import copy import json -import os from datetime import datetime, timedelta from uuid import UUID, uuid4 @@ -8,14 +7,9 @@ import pytest from botocore.exceptions import ClientError -from moto import mock_aws, settings +from moto import mock_aws from moto.core.utils import unix_time_millis - -TEST_REGION = "us-east-1" if settings.TEST_SERVER_MODE else "us-west-2" -allow_aws_request = ( - os.environ.get("MOTO_TEST_ALLOW_AWS_REQUEST", "false").lower() == "true" -) - +from tests import allow_aws_request S3_POLICY = { "Version": "2012-10-17", @@ -49,7 +43,7 @@ @pytest.fixture() def logs(): - if allow_aws_request: + if allow_aws_request(): yield boto3.client("logs", region_name="us-east-1") else: with mock_aws(): diff --git a/tests/test_logs/test_logs.py b/tests/test_logs/test_logs.py index be8ebdb2904e..cec52981c563 100644 --- a/tests/test_logs/test_logs.py +++ b/tests/test_logs/test_logs.py @@ -1,5 +1,6 @@ import json from datetime import timedelta +from uuid import uuid4 import boto3 import pytest @@ -9,6 +10,7 @@ from moto import mock_aws, settings from moto.core.utils import unix_time_millis, utcnow from moto.logs.models import MAX_RESOURCE_POLICIES_PER_REGION +from tests import allow_aws_request, aws_verified TEST_REGION = "us-east-1" if settings.TEST_SERVER_MODE else "us-west-2" @@ -47,258 +49,21 @@ ) -@mock_aws -def test_describe_metric_filters_happy_prefix(): - conn = boto3.client("logs", "us-west-2") - - response1 = put_metric_filter(conn, count=1) - assert response1["ResponseMetadata"]["HTTPStatusCode"] == 200 - response2 = put_metric_filter(conn, count=2) - assert response2["ResponseMetadata"]["HTTPStatusCode"] == 200 - - response = conn.describe_metric_filters(filterNamePrefix="filter") - - assert len(response["metricFilters"]) == 2 - assert response["metricFilters"][0]["filterName"] == "filterName1" - assert response["metricFilters"][1]["filterName"] == "filterName2" - - -@mock_aws -def test_describe_metric_filters_happy_log_group_name(): - conn = boto3.client("logs", "us-west-2") - - response1 = put_metric_filter(conn, count=1) - assert response1["ResponseMetadata"]["HTTPStatusCode"] == 200 - response2 = put_metric_filter(conn, count=2) - assert response2["ResponseMetadata"]["HTTPStatusCode"] == 200 - - response = conn.describe_metric_filters(logGroupName="logGroupName2") - - assert len(response["metricFilters"]) == 1 - assert response["metricFilters"][0]["logGroupName"] == "logGroupName2" - - -@mock_aws -def test_describe_metric_filters_happy_metric_name(): - conn = boto3.client("logs", "us-west-2") - - response1 = put_metric_filter(conn, count=1) - assert response1["ResponseMetadata"]["HTTPStatusCode"] == 200 - response2 = put_metric_filter(conn, count=2) - assert response2["ResponseMetadata"]["HTTPStatusCode"] == 200 - - response = conn.describe_metric_filters( - metricName="metricName1", metricNamespace="metricNamespace1" - ) - - assert len(response["metricFilters"]) == 1 - metrics = response["metricFilters"][0]["metricTransformations"] - assert metrics[0]["metricName"] == "metricName1" - assert metrics[0]["metricNamespace"] == "metricNamespace1" +@pytest.fixture(name="log_group_name") +def create_log_group(): + if allow_aws_request(): + yield from _create_log_group() + else: + with mock_aws(): + yield from _create_log_group() -@mock_aws -def test_put_metric_filters_validation(): - conn = boto3.client("logs", "us-west-2") - - invalid_filter_name = "X" * 513 - invalid_filter_pattern = "X" * 1025 - invalid_metric_transformations = [ - { - "defaultValue": 1, - "metricName": "metricName", - "metricNamespace": "metricNamespace", - "metricValue": "metricValue", - }, - { - "defaultValue": 1, - "metricName": "metricName", - "metricNamespace": "metricNamespace", - "metricValue": "metricValue", - }, - ] - - test_cases = [ - build_put_case(name="Invalid filter name", filter_name=invalid_filter_name), - build_put_case( - name="Invalid filter pattern", filter_pattern=invalid_filter_pattern - ), - build_put_case( - name="Invalid filter metric transformations", - metric_transformations=invalid_metric_transformations, - ), - ] - - for test_case in test_cases: - with pytest.raises(ClientError) as exc: - conn.put_metric_filter(**test_case["input"]) - response = exc.value.response - assert response["ResponseMetadata"]["HTTPStatusCode"] == 400 - assert response["Error"]["Code"] == "InvalidParameterException" - - -@mock_aws -def test_describe_metric_filters_validation(): - conn = boto3.client("logs", "us-west-2") - - length_over_512 = "X" * 513 - length_over_255 = "X" * 256 - - test_cases = [ - build_describe_case( - name="Invalid filter name prefix", filter_name_prefix=length_over_512 - ), - build_describe_case( - name="Invalid log group name", log_group_name=length_over_512 - ), - build_describe_case(name="Invalid metric name", metric_name=length_over_255), - build_describe_case( - name="Invalid metric namespace", metric_namespace=length_over_255 - ), - ] - - for test_case in test_cases: - with pytest.raises(ClientError) as exc: - conn.describe_metric_filters(**test_case["input"]) - response = exc.value.response - assert response["ResponseMetadata"]["HTTPStatusCode"] == 400 - assert response["Error"]["Code"] == "InvalidParameterException" - - -@mock_aws -def test_describe_metric_filters_multiple_happy(): - conn = boto3.client("logs", "us-west-2") - - response = put_metric_filter(conn, 1) - assert response["ResponseMetadata"]["HTTPStatusCode"] == 200 - - response = put_metric_filter(conn, 2) - assert response["ResponseMetadata"]["HTTPStatusCode"] == 200 - response = conn.describe_metric_filters( - filterNamePrefix="filter", logGroupName="logGroupName1" - ) - assert response["metricFilters"][0]["filterName"] == "filterName1" - - response = conn.describe_metric_filters(filterNamePrefix="filter") - assert response["metricFilters"][0]["filterName"] == "filterName1" - - response = conn.describe_metric_filters(logGroupName="logGroupName1") - assert response["metricFilters"][0]["filterName"] == "filterName1" - - response = conn.describe_metric_filters( - metricName="metricName1", metricNamespace="metricNamespace1" - ) - assert response["metricFilters"][0]["filterName"] == "filterName1" - - -@mock_aws -def test_put_and_describe_metric_filter_with_non_alphanumerics_in_namespace(): - """ - Should allow namespaces as described here: - https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/cloudwatch_concepts.html#Namespace - """ - conn = boto3.client("logs", "us-west-2") - namespace = "A.B-c_d/1#2:metricNamespace" - response = conn.put_metric_filter( - filterName="filterName", - filterPattern="filterPattern", - logGroupName="logGroupName", - metricTransformations=[ - { - "metricName": "metricName", - "metricNamespace": namespace, - "metricValue": "metricValue", - }, - ], - ) - assert response["ResponseMetadata"]["HTTPStatusCode"] == 200 - - response = conn.describe_metric_filters( - metricName="metricName", metricNamespace=namespace - ) - assert response["metricFilters"][0]["filterName"] == "filterName" - - -@mock_aws -def test_delete_metric_filter(): - client = boto3.client("logs", "us-west-2") - - lg_name = "/hello-world/my-cool-endpoint" - client.create_log_group(logGroupName=lg_name) - client.put_metric_filter( - logGroupName=lg_name, - filterName="my-cool-filter", - filterPattern="{ $.val = * }", - metricTransformations=[ - { - "metricName": "my-metric", - "metricNamespace": "my-namespace", - "metricValue": "$.value", - } - ], - ) - - response = client.delete_metric_filter( - filterName="filterName", logGroupName=lg_name - ) - assert response["ResponseMetadata"]["HTTPStatusCode"] == 200 - - response = client.describe_metric_filters( - filterNamePrefix="filter", logGroupName="logGroupName2" - ) - assert response["metricFilters"] == [] - - -@mock_aws -@pytest.mark.parametrize( - "filter_name, failing_constraint", - [ - ( - "X" * 513, - "Minimum length of 1. Maximum length of 512.", - ), # filterName too long - ("x:x", "Must match pattern"), # invalid filterName pattern - ], -) -def test_delete_metric_filter_invalid_filter_name(filter_name, failing_constraint): - conn = boto3.client("logs", "us-west-2") - with pytest.raises(ClientError) as exc: - conn.delete_metric_filter(filterName=filter_name, logGroupName="valid") - response = exc.value.response - assert response["ResponseMetadata"]["HTTPStatusCode"] == 400 - assert response["Error"]["Code"] == "InvalidParameterException" - assert ( - f"Value '{filter_name}' at 'filterName' failed to satisfy constraint" - in response["Error"]["Message"] - ) - assert failing_constraint in response["Error"]["Message"] - - -@mock_aws -@pytest.mark.parametrize( - "log_group_name, failing_constraint", - [ - ( - "X" * 513, - "Minimum length of 1. Maximum length of 512.", - ), # logGroupName too long - ("x!x", "Must match pattern"), # invalid logGroupName pattern - ], -) -def test_delete_metric_filter_invalid_log_group_name( - log_group_name, failing_constraint -): - conn = boto3.client("logs", "us-west-2") - with pytest.raises(ClientError) as exc: - conn.delete_metric_filter(filterName="valid", logGroupName=log_group_name) - response = exc.value.response - assert response["ResponseMetadata"]["HTTPStatusCode"] == 400 - assert response["Error"]["Code"] == "InvalidParameterException" - assert ( - f"Value '{log_group_name}' at 'logGroupName' failed to satisfy constraint" - in response["Error"]["Message"] - ) - assert failing_constraint in response["Error"]["Message"] +def _create_log_group(): + conn = boto3.client("logs", TEST_REGION) + log_group_name = f"test_log_group_{str(uuid4())[0:6]}" + conn.create_log_group(logGroupName=log_group_name) + yield log_group_name + conn.delete_log_group(logGroupName=log_group_name) @mock_aws @@ -377,84 +142,6 @@ def test_destinations(): assert response["ResponseMetadata"]["HTTPStatusCode"] == 200 -def put_metric_filter(conn, count=1): - count = str(count) - return conn.put_metric_filter( - filterName="filterName" + count, - filterPattern="filterPattern" + count, - logGroupName="logGroupName" + count, - metricTransformations=[ - { - "defaultValue": int(count), - "metricName": "metricName" + count, - "metricNamespace": "metricNamespace" + count, - "metricValue": "metricValue" + count, - }, - ], - ) - - -def build_put_case( - name, - filter_name="filterName", - filter_pattern="filterPattern", - log_group_name="logGroupName", - metric_transformations=None, -): - return { - "name": name, - "input": build_put_input( - filter_name, filter_pattern, log_group_name, metric_transformations - ), - } - - -def build_put_input( - filter_name, filter_pattern, log_group_name, metric_transformations -): - if metric_transformations is None: - metric_transformations = [ - { - "defaultValue": 1, - "metricName": "metricName", - "metricNamespace": "metricNamespace", - "metricValue": "metricValue", - }, - ] - return { - "filterName": filter_name, - "filterPattern": filter_pattern, - "logGroupName": log_group_name, - "metricTransformations": metric_transformations, - } - - -def build_describe_input( - filter_name_prefix, log_group_name, metric_name, metric_namespace -): - return { - "filterNamePrefix": filter_name_prefix, - "logGroupName": log_group_name, - "metricName": metric_name, - "metricNamespace": metric_namespace, - } - - -def build_describe_case( - name, - filter_name_prefix="filterNamePrefix", - log_group_name="logGroupName", - metric_name="metricName", - metric_namespace="metricNamespace", -): - return { - "name": name, - "input": build_describe_input( - filter_name_prefix, log_group_name, metric_name, metric_namespace - ), - } - - @mock_aws @pytest.mark.parametrize( "kms_key_id", @@ -885,6 +572,82 @@ def test_get_log_events(): ) +@pytest.mark.aws_verified +@aws_verified +def test_arn_formats_log_group_and_stream(account_id, log_group_name): + client = boto3.client("logs", TEST_REGION) + + # Verify that we return all LogGroup ARN's + group = client.describe_log_groups(logGroupNamePrefix=log_group_name)["logGroups"][ + 0 + ] + assert group["logGroupName"] == log_group_name + assert ( + group["arn"] + == f"arn:aws:logs:{TEST_REGION}:{account_id}:log-group:{log_group_name}:*" + ) + assert ( + group["logGroupArn"] + == f"arn:aws:logs:{TEST_REGION}:{account_id}:log-group:{log_group_name}" + ) + + client.create_log_stream(logGroupName=log_group_name, logStreamName="stream") + + # Verify that LogStreams have the correct ARN + stream = client.describe_log_streams(logGroupName=log_group_name)["logStreams"][0] + assert stream["logStreamName"] == "stream" + assert ( + stream["arn"] + == f"arn:aws:logs:{TEST_REGION}:{account_id}:log-group:{log_group_name}:log-stream:stream" + ) + + # Verify that LogStreams can be found using the logStreamIdentifier + stream = client.describe_log_streams(logGroupIdentifier=group["logGroupArn"])[ + "logStreams" + ][0] + assert stream["logStreamName"] == "stream" + + # We can't use the ARN, as that throws a ValidationError + with pytest.raises(ClientError) as exc: + client.describe_log_streams(logGroupIdentifier=group["arn"]) + err = exc.value.response["Error"] + assert err["Code"] == "InvalidParameterException" + assert ( + err["Message"] + == f"1 validation error detected: Value '{group['arn']}' at 'logGroupIdentifier' failed to satisfy constraint: Member must satisfy regular expression pattern: [\\w#+=/:,.@-]*" + ) + + +@pytest.mark.aws_verified +@aws_verified +def test_get_log_events_using_arn(account_id, log_group_name): + client = boto3.client("logs", TEST_REGION) + + group = client.describe_log_groups(logGroupNamePrefix=log_group_name)["logGroups"][ + 0 + ] + + client.create_log_stream(logGroupName=log_group_name, logStreamName="stream") + + # Verify we can call this method with all variantions + # Note that we don't verify whether it returns anything - we just want to ensure that the parameters are valid + client.get_log_events(logGroupName=log_group_name, logStreamName="stream") + client.get_log_events(logGroupIdentifier=log_group_name, logStreamName="stream") + client.get_log_events( + logGroupIdentifier=group["logGroupArn"], logStreamName="stream" + ) + + # We can't use the ARN, as that throws a ValidationError + with pytest.raises(ClientError) as exc: + client.get_log_events(logGroupIdentifier=group["arn"], logStreamName="stream") + err = exc.value.response["Error"] + assert err["Code"] == "InvalidParameterException" + assert ( + err["Message"] + == f"1 validation error detected: Value '{group['arn']}' at 'logGroupIdentifier' failed to satisfy constraint: Member must satisfy regular expression pattern: [\\w#+=/:,.@-]*" + ) + + @mock_aws def test_get_log_events_with_start_from_head(): client = boto3.client("logs", TEST_REGION) diff --git a/tests/test_logs/test_logs_cloudformation.py b/tests/test_logs/test_logs_cloudformation.py index ab39d682eeed..632c5e791dfb 100644 --- a/tests/test_logs/test_logs_cloudformation.py +++ b/tests/test_logs/test_logs_cloudformation.py @@ -1,33 +1,52 @@ import json +from uuid import uuid4 import boto3 +import pytest -from moto import mock_aws +from tests import aws_verified -@mock_aws +@aws_verified +@pytest.mark.aws_verified def test_tagging(): logs_client = boto3.client("logs", region_name="us-east-1") cf_client = boto3.client("cloudformation", region_name="us-east-1") + log_group_name = f"/moto/test/{str(uuid4())}" + template = { "AWSTemplateFormatVersion": "2010-09-09", "Resources": { "testGroup": { "Type": "AWS::Logs::LogGroup", - "Properties": {"Tags": [{"Key": "foo", "Value": "bar"}]}, + "Properties": { + "LogGroupName": log_group_name, + "Tags": [{"Key": "foo", "Value": "bar"}], + }, } }, } template_json = json.dumps(template) + stack_name = f"moto-test-{str(uuid4())[0:6]}" cf_client.create_stack( - StackName="test_stack", + StackName=stack_name, TemplateBody=template_json, ) + waiter = cf_client.get_waiter("stack_create_complete") + waiter.wait(StackName=stack_name) + + group = logs_client.describe_log_groups(logGroupNamePrefix=log_group_name)[ + "logGroups" + ][0] - arn = logs_client.describe_log_groups()["logGroups"][0]["arn"] - tags = logs_client.list_tags_for_resource(resourceArn=arn)["tags"] - assert tags == {"foo": "bar"} + tags = logs_client.list_tags_for_resource(resourceArn=group["logGroupArn"])["tags"] + assert tags["foo"] == "bar" - cf_client.delete_stack(StackName="test_stack") - assert logs_client.describe_log_groups()["logGroups"] == [] + cf_client.delete_stack(StackName=stack_name) + waiter = cf_client.get_waiter("stack_delete_complete") + waiter.wait(StackName=stack_name) + assert ( + logs_client.describe_log_groups(logGroupNamePrefix=log_group_name)["logGroups"] + == [] + ) diff --git a/tests/test_logs/test_logs_filter.py b/tests/test_logs/test_logs_filter_log_events.py similarity index 100% rename from tests/test_logs/test_logs_filter.py rename to tests/test_logs/test_logs_filter_log_events.py diff --git a/tests/test_logs/test_logs_metric_filters.py b/tests/test_logs/test_logs_metric_filters.py new file mode 100644 index 000000000000..68e889c92e28 --- /dev/null +++ b/tests/test_logs/test_logs_metric_filters.py @@ -0,0 +1,337 @@ +import boto3 +import pytest +from botocore.exceptions import ClientError + +from moto import mock_aws + + +@mock_aws +def test_describe_metric_filters_happy_prefix(): + conn = boto3.client("logs", "us-west-2") + + response1 = put_metric_filter(conn, count=1) + assert response1["ResponseMetadata"]["HTTPStatusCode"] == 200 + response2 = put_metric_filter(conn, count=2) + assert response2["ResponseMetadata"]["HTTPStatusCode"] == 200 + + response = conn.describe_metric_filters(filterNamePrefix="filter") + + assert len(response["metricFilters"]) == 2 + assert response["metricFilters"][0]["filterName"] == "filterName1" + assert response["metricFilters"][1]["filterName"] == "filterName2" + + +@mock_aws +def test_describe_metric_filters_happy_log_group_name(): + conn = boto3.client("logs", "us-west-2") + + response1 = put_metric_filter(conn, count=1) + assert response1["ResponseMetadata"]["HTTPStatusCode"] == 200 + response2 = put_metric_filter(conn, count=2) + assert response2["ResponseMetadata"]["HTTPStatusCode"] == 200 + + response = conn.describe_metric_filters(logGroupName="logGroupName2") + + assert len(response["metricFilters"]) == 1 + assert response["metricFilters"][0]["logGroupName"] == "logGroupName2" + + +@mock_aws +def test_describe_metric_filters_happy_metric_name(): + conn = boto3.client("logs", "us-west-2") + + response1 = put_metric_filter(conn, count=1) + assert response1["ResponseMetadata"]["HTTPStatusCode"] == 200 + response2 = put_metric_filter(conn, count=2) + assert response2["ResponseMetadata"]["HTTPStatusCode"] == 200 + + response = conn.describe_metric_filters( + metricName="metricName1", metricNamespace="metricNamespace1" + ) + + assert len(response["metricFilters"]) == 1 + metrics = response["metricFilters"][0]["metricTransformations"] + assert metrics[0]["metricName"] == "metricName1" + assert metrics[0]["metricNamespace"] == "metricNamespace1" + + +@mock_aws +def test_put_metric_filters_validation(): + conn = boto3.client("logs", "us-west-2") + + invalid_filter_name = "X" * 513 + invalid_filter_pattern = "X" * 1025 + invalid_metric_transformations = [ + { + "defaultValue": 1, + "metricName": "metricName", + "metricNamespace": "metricNamespace", + "metricValue": "metricValue", + }, + { + "defaultValue": 1, + "metricName": "metricName", + "metricNamespace": "metricNamespace", + "metricValue": "metricValue", + }, + ] + + test_cases = [ + build_put_case(name="Invalid filter name", filter_name=invalid_filter_name), + build_put_case( + name="Invalid filter pattern", filter_pattern=invalid_filter_pattern + ), + build_put_case( + name="Invalid filter metric transformations", + metric_transformations=invalid_metric_transformations, + ), + ] + + for test_case in test_cases: + with pytest.raises(ClientError) as exc: + conn.put_metric_filter(**test_case["input"]) + response = exc.value.response + assert response["ResponseMetadata"]["HTTPStatusCode"] == 400 + assert response["Error"]["Code"] == "InvalidParameterException" + + +@mock_aws +def test_describe_metric_filters_validation(): + conn = boto3.client("logs", "us-west-2") + + length_over_512 = "X" * 513 + length_over_255 = "X" * 256 + + test_cases = [ + build_describe_case( + name="Invalid filter name prefix", filter_name_prefix=length_over_512 + ), + build_describe_case( + name="Invalid log group name", log_group_name=length_over_512 + ), + build_describe_case(name="Invalid metric name", metric_name=length_over_255), + build_describe_case( + name="Invalid metric namespace", metric_namespace=length_over_255 + ), + ] + + for test_case in test_cases: + with pytest.raises(ClientError) as exc: + conn.describe_metric_filters(**test_case["input"]) + response = exc.value.response + assert response["ResponseMetadata"]["HTTPStatusCode"] == 400 + assert response["Error"]["Code"] == "InvalidParameterException" + + +@mock_aws +def test_describe_metric_filters_multiple_happy(): + conn = boto3.client("logs", "us-west-2") + + response = put_metric_filter(conn, 1) + assert response["ResponseMetadata"]["HTTPStatusCode"] == 200 + + response = put_metric_filter(conn, 2) + assert response["ResponseMetadata"]["HTTPStatusCode"] == 200 + response = conn.describe_metric_filters( + filterNamePrefix="filter", logGroupName="logGroupName1" + ) + assert response["metricFilters"][0]["filterName"] == "filterName1" + + response = conn.describe_metric_filters(filterNamePrefix="filter") + assert response["metricFilters"][0]["filterName"] == "filterName1" + + response = conn.describe_metric_filters(logGroupName="logGroupName1") + assert response["metricFilters"][0]["filterName"] == "filterName1" + + response = conn.describe_metric_filters( + metricName="metricName1", metricNamespace="metricNamespace1" + ) + assert response["metricFilters"][0]["filterName"] == "filterName1" + + +@mock_aws +def test_put_and_describe_metric_filter_with_non_alphanumerics_in_namespace(): + """ + Should allow namespaces as described here: + https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/cloudwatch_concepts.html#Namespace + """ + conn = boto3.client("logs", "us-west-2") + namespace = "A.B-c_d/1#2:metricNamespace" + response = conn.put_metric_filter( + filterName="filterName", + filterPattern="filterPattern", + logGroupName="logGroupName", + metricTransformations=[ + { + "metricName": "metricName", + "metricNamespace": namespace, + "metricValue": "metricValue", + }, + ], + ) + assert response["ResponseMetadata"]["HTTPStatusCode"] == 200 + + response = conn.describe_metric_filters( + metricName="metricName", metricNamespace=namespace + ) + assert response["metricFilters"][0]["filterName"] == "filterName" + + +@mock_aws +def test_delete_metric_filter(): + client = boto3.client("logs", "us-west-2") + + lg_name = "/hello-world/my-cool-endpoint" + client.create_log_group(logGroupName=lg_name) + client.put_metric_filter( + logGroupName=lg_name, + filterName="my-cool-filter", + filterPattern="{ $.val = * }", + metricTransformations=[ + { + "metricName": "my-metric", + "metricNamespace": "my-namespace", + "metricValue": "$.value", + } + ], + ) + + response = client.delete_metric_filter( + filterName="filterName", logGroupName=lg_name + ) + assert response["ResponseMetadata"]["HTTPStatusCode"] == 200 + + response = client.describe_metric_filters( + filterNamePrefix="filter", logGroupName="logGroupName2" + ) + assert response["metricFilters"] == [] + + +@mock_aws +@pytest.mark.parametrize( + "filter_name, failing_constraint", + [ + ( + "X" * 513, + "Minimum length of 1. Maximum length of 512.", + ), # filterName too long + ("x:x", "Must match pattern"), # invalid filterName pattern + ], +) +def test_delete_metric_filter_invalid_filter_name(filter_name, failing_constraint): + conn = boto3.client("logs", "us-west-2") + with pytest.raises(ClientError) as exc: + conn.delete_metric_filter(filterName=filter_name, logGroupName="valid") + response = exc.value.response + assert response["ResponseMetadata"]["HTTPStatusCode"] == 400 + assert response["Error"]["Code"] == "InvalidParameterException" + assert ( + f"Value '{filter_name}' at 'filterName' failed to satisfy constraint" + in response["Error"]["Message"] + ) + assert failing_constraint in response["Error"]["Message"] + + +@mock_aws +@pytest.mark.parametrize( + "log_group_name, failing_constraint", + [ + ( + "X" * 513, + "Minimum length of 1. Maximum length of 512.", + ), # logGroupName too long + ("x!x", "Must match pattern"), # invalid logGroupName pattern + ], +) +def test_delete_metric_filter_invalid_log_group_name( + log_group_name, failing_constraint +): + conn = boto3.client("logs", "us-west-2") + with pytest.raises(ClientError) as exc: + conn.delete_metric_filter(filterName="valid", logGroupName=log_group_name) + response = exc.value.response + assert response["ResponseMetadata"]["HTTPStatusCode"] == 400 + assert response["Error"]["Code"] == "InvalidParameterException" + assert ( + f"Value '{log_group_name}' at 'logGroupName' failed to satisfy constraint" + in response["Error"]["Message"] + ) + assert failing_constraint in response["Error"]["Message"] + + +def put_metric_filter(conn, count=1): + count = str(count) + return conn.put_metric_filter( + filterName="filterName" + count, + filterPattern="filterPattern" + count, + logGroupName="logGroupName" + count, + metricTransformations=[ + { + "defaultValue": int(count), + "metricName": "metricName" + count, + "metricNamespace": "metricNamespace" + count, + "metricValue": "metricValue" + count, + }, + ], + ) + + +def build_put_case( + name, + filter_name="filterName", + filter_pattern="filterPattern", + log_group_name="logGroupName", + metric_transformations=None, +): + return { + "name": name, + "input": build_put_input( + filter_name, filter_pattern, log_group_name, metric_transformations + ), + } + + +def build_put_input( + filter_name, filter_pattern, log_group_name, metric_transformations +): + if metric_transformations is None: + metric_transformations = [ + { + "defaultValue": 1, + "metricName": "metricName", + "metricNamespace": "metricNamespace", + "metricValue": "metricValue", + }, + ] + return { + "filterName": filter_name, + "filterPattern": filter_pattern, + "logGroupName": log_group_name, + "metricTransformations": metric_transformations, + } + + +def build_describe_input( + filter_name_prefix, log_group_name, metric_name, metric_namespace +): + return { + "filterNamePrefix": filter_name_prefix, + "logGroupName": log_group_name, + "metricName": metric_name, + "metricNamespace": metric_namespace, + } + + +def build_describe_case( + name, + filter_name_prefix="filterNamePrefix", + log_group_name="logGroupName", + metric_name="metricName", + metric_namespace="metricNamespace", +): + return { + "name": name, + "input": build_describe_input( + filter_name_prefix, log_group_name, metric_name, metric_namespace + ), + } diff --git a/tests/test_logs/test_logs_tags.py b/tests/test_logs/test_logs_tags.py index c3988e7eda81..3640420e97be 100644 --- a/tests/test_logs/test_logs_tags.py +++ b/tests/test_logs/test_logs_tags.py @@ -26,7 +26,7 @@ def test_log_groups_tags(): log_group_name = "test" logs.create_log_group(logGroupName=log_group_name, tags={"key1": "val1"}) - arn = logs.describe_log_groups()["logGroups"][0]["arn"] + arn = logs.describe_log_groups()["logGroups"][0]["logGroupArn"] _verify_tag_operations(arn, logs) diff --git a/tests/test_resourcegroupstaggingapi/test_resourcegroupstagging_logs.py b/tests/test_resourcegroupstaggingapi/test_resourcegroupstagging_logs.py index 869161ad1a10..ed313dda8831 100644 --- a/tests/test_resourcegroupstaggingapi/test_resourcegroupstagging_logs.py +++ b/tests/test_resourcegroupstaggingapi/test_resourcegroupstagging_logs.py @@ -14,7 +14,9 @@ def setUp(self) -> None: self.resources_untagged = [] for i in range(3): self.logs.create_log_group(logGroupName=f"test{i}", tags={"key1": "val1"}) - self.arns = [lg["arn"] for lg in self.logs.describe_log_groups()["logGroups"]] + self.arns = [ + lg["logGroupArn"] for lg in self.logs.describe_log_groups()["logGroups"] + ] def test_get_resources_logs(self): resp = self.rtapi.get_resources(ResourceTypeFilters=["logs"])