diff --git a/moto/logs/models.py b/moto/logs/models.py index dcc0e85e1..8425f87f2 100644 --- a/moto/logs/models.py +++ b/moto/logs/models.py @@ -485,20 +485,39 @@ class LogsBackend(BaseBackend): def describe_log_groups(self, limit, log_group_name_prefix, next_token): if log_group_name_prefix is None: log_group_name_prefix = "" - if next_token is None: - next_token = 0 groups = [ group.to_describe_dict() for name, group in self.groups.items() if name.startswith(log_group_name_prefix) ] - groups = sorted(groups, key=lambda x: x["creationTime"], reverse=True) - groups_page = groups[next_token : next_token + limit] + groups = sorted(groups, key=lambda x: x["logGroupName"]) - next_token += limit - if next_token >= len(groups): - next_token = None + index_start = 0 + if next_token: + try: + index_start = ( + next( + index + for (index, d) in enumerate(groups) + if d["logGroupName"] == next_token + ) + + 1 + ) + except StopIteration: + index_start = 0 + # AWS returns an empty list if it receives an invalid token. + groups = [] + + index_end = index_start + limit + if index_end > len(groups): + index_end = len(groups) + + groups_page = groups[index_start:index_end] + + next_token = None + if groups_page and index_end < len(groups): + next_token = groups_page[-1]["logGroupName"] return groups_page, next_token diff --git a/moto/logs/responses.py b/moto/logs/responses.py index 9e6886a42..715c4b5c1 100644 --- a/moto/logs/responses.py +++ b/moto/logs/responses.py @@ -42,7 +42,10 @@ class LogsResponse(BaseResponse): groups, next_token = self.logs_backend.describe_log_groups( limit, log_group_name_prefix, next_token ) - return json.dumps({"logGroups": groups, "nextToken": next_token}) + result = {"logGroups": groups} + if next_token: + result["nextToken"] = next_token + return json.dumps(result) def create_log_stream(self): log_group_name = self._get_param("logGroupName") diff --git a/tests/test_logs/test_logs.py b/tests/test_logs/test_logs.py index e234cc561..648d561aa 100644 --- a/tests/test_logs/test_logs.py +++ b/tests/test_logs/test_logs.py @@ -458,3 +458,39 @@ def test_describe_subscription_filters_errors(): ex.response["Error"]["Message"].should.equal( "The specified log group does not exist" ) + + +@mock_logs +def test_describe_log_groups_paging(): + client = boto3.client("logs", "us-east-1") + + group_names = [ + "/aws/lambda/lowercase-dev", + "/aws/lambda/FileMonitoring", + "/aws/events/GetMetricData", + "/aws/lambda/fileAvailable", + ] + + for name in group_names: + client.create_log_group(logGroupName=name) + + resp = client.describe_log_groups() + resp["logGroups"].should.have.length_of(4) + resp.should_not.have.key("nextToken") + + resp = client.describe_log_groups(limit=2) + resp["logGroups"].should.have.length_of(2) + resp["nextToken"].should.equal("/aws/lambda/FileMonitoring") + + resp = client.describe_log_groups(nextToken=resp["nextToken"], limit=1) + resp["logGroups"].should.have.length_of(1) + resp["nextToken"].should.equal("/aws/lambda/fileAvailable") + + resp = client.describe_log_groups(nextToken=resp["nextToken"]) + resp["logGroups"].should.have.length_of(1) + resp["logGroups"][0]["logGroupName"].should.equal("/aws/lambda/lowercase-dev") + resp.should_not.have.key("nextToken") + + resp = client.describe_log_groups(nextToken="invalid-token") + resp["logGroups"].should.have.length_of(0) + resp.should_not.have.key("nextToken")