Skip to content

Commit 4484dd1

Browse files
authored
services/logs: Add ListLogGroups operation (#13337)
1 parent a2d83bf commit 4484dd1

File tree

4 files changed

+79
-23
lines changed

4 files changed

+79
-23
lines changed

localstack-core/localstack/services/logs/provider.py

Lines changed: 36 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,13 @@
2222
InputLogEvents,
2323
InvalidParameterException,
2424
KmsKeyId,
25+
ListLogGroupsRequest,
26+
ListLogGroupsResponse,
2527
ListTagsForResourceResponse,
2628
ListTagsLogGroupResponse,
2729
LogGroupClass,
2830
LogGroupName,
31+
LogGroupSummary,
2932
LogsApi,
3033
LogStreamName,
3134
PutLogEventsResponse,
@@ -43,7 +46,7 @@
4346
from localstack.utils.aws import arns
4447
from localstack.utils.aws.client_types import ServicePrincipal
4548
from localstack.utils.bootstrap import is_api_enabled
46-
from localstack.utils.common import is_number
49+
from localstack.utils.numbers import is_number
4750
from localstack.utils.patch import patch
4851

4952
LOG = logging.getLogger(__name__)
@@ -60,8 +63,8 @@ def put_log_events(
6063
log_group_name: LogGroupName,
6164
log_stream_name: LogStreamName,
6265
log_events: InputLogEvents,
63-
sequence_token: SequenceToken = None,
64-
entity: Entity = None,
66+
sequence_token: SequenceToken | None = None,
67+
entity: Entity | None = None,
6568
**kwargs,
6669
) -> PutLogEventsResponse:
6770
logs_backend = get_moto_logs_backend(context.account_id, context.region)
@@ -97,33 +100,32 @@ def describe_log_groups(
97100
) -> DescribeLogGroupsResponse:
98101
region_backend = get_moto_logs_backend(context.account_id, context.region)
99102

100-
prefix: str = request.get("logGroupNamePrefix", "")
101-
pattern: str = request.get("logGroupNamePattern", "")
103+
prefix: str | None = request.get("logGroupNamePrefix", "")
104+
pattern: str | None = request.get("logGroupNamePattern", "")
102105

103106
if pattern and prefix:
104107
raise InvalidParameterException(
105108
"LogGroup name prefix and LogGroup name pattern are mutually exclusive parameters."
106109
)
107110

108-
copy_groups = copy.deepcopy(dict(region_backend.groups))
111+
moto_groups = copy.deepcopy(dict(region_backend.groups)).values()
109112

110113
groups = [
111-
group.to_describe_dict()
112-
for name, group in copy_groups.items()
114+
{"logGroupClass": LogGroupClass.STANDARD} | group.to_describe_dict()
115+
for group in sorted(moto_groups, key=lambda g: g.name)
113116
if not (prefix or pattern)
114-
or (prefix and name.startswith(prefix))
115-
or (pattern and pattern in name)
117+
or (prefix and group.name.startswith(prefix))
118+
or (pattern and pattern in group.name)
116119
]
117120

118-
groups = sorted(groups, key=lambda x: x["logGroupName"])
119121
return DescribeLogGroupsResponse(logGroups=groups)
120122

121123
@handler("DescribeLogStreams", expand=False)
122124
def describe_log_streams(
123125
self, context: RequestContext, request: DescribeLogStreamsRequest
124126
) -> DescribeLogStreamsResponse:
125-
log_group_name: str = request.get("logGroupName")
126-
log_group_identifier: str = request.get("logGroupIdentifier")
127+
log_group_name: str | None = request.get("logGroupName")
128+
log_group_identifier: str | None = request.get("logGroupIdentifier")
127129

128130
if log_group_identifier and log_group_name:
129131
raise CommonServiceException(
@@ -138,13 +140,29 @@ def describe_log_streams(
138140

139141
return moto.call_moto_with_request(context, request_copy)
140142

143+
@handler("ListLogGroups", expand=False)
144+
def list_log_groups(
145+
self, context: RequestContext, request: ListLogGroupsRequest
146+
) -> ListLogGroupsResponse:
147+
pattern: str | None = request.get("logGroupNamePattern")
148+
region_backend: LogsBackend = get_moto_logs_backend(context.account_id, context.region)
149+
moto_groups = copy.deepcopy(region_backend.groups).values()
150+
groups = [
151+
LogGroupSummary(
152+
logGroupName=group.name, logGroupArn=group.arn, logGroupClass=LogGroupClass.STANDARD
153+
)
154+
for group in sorted(moto_groups, key=lambda g: g.name)
155+
if not pattern or pattern in group.name
156+
]
157+
return ListLogGroupsResponse(logGroups=groups)
158+
141159
def create_log_group(
142160
self,
143161
context: RequestContext,
144162
log_group_name: LogGroupName,
145-
kms_key_id: KmsKeyId = None,
146-
tags: Tags = None,
147-
log_group_class: LogGroupClass = None,
163+
kms_key_id: KmsKeyId | None = None,
164+
tags: Tags | None = None,
165+
log_group_class: LogGroupClass | None = None,
148166
**kwargs,
149167
) -> None:
150168
call_moto(context)
@@ -442,10 +460,9 @@ def moto_to_describe_dict(target, self):
442460
# reported race condition in https://github.com/localstack/localstack/issues/8011
443461
# making copy of "streams" dict here to avoid issues while summing up storedBytes
444462
copy_streams = copy.deepcopy(self.streams)
445-
# parity tests shows that the arn ends with ":*"
446-
arn = self.arn if self.arn.endswith(":*") else f"{self.arn}:*"
447463
log_group = {
448-
"arn": arn,
464+
"arn": f"{self.arn}:*",
465+
"logGroupArn": self.arn,
449466
"creationTime": self.creation_time,
450467
"logGroupName": self.name,
451468
"metricFilterCount": 0,

tests/aws/services/logs/test_logs.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,9 +166,9 @@ def test_list_tags_log_group(self, snapshot, aws_client):
166166
# TODO 'describe-log-groups' returns different attributes on AWS when using
167167
# 'logGroupNamePattern' compared to 'logGroupNamePrefix' (for the same log group)
168168
# seems like a weird issue on AWS side, we just exclude the paths here for this particular call
169-
"$..describe-log-groups-pattern.logGroups..metricFilterCount",
170169
"$..describe-log-groups-pattern.logGroups..storedBytes",
171170
"$..describe-log-groups-pattern.nextToken",
171+
"$..list-log-groups-pattern-match.nextToken",
172172
]
173173
)
174174
@markers.aws.validated
@@ -204,6 +204,13 @@ def test_create_and_delete_log_stream(self, logs_log_group, aws_client, region_n
204204
)
205205
snapshot.match("error-describe-logs-group", ctx.value.response)
206206

207+
response = aws_client.logs.list_log_groups(logGroupNamePattern="no-such-group")
208+
snapshot.match("list-log-groups-pattern-no-match", response)
209+
response = aws_client.logs.list_log_groups(
210+
logGroupNamePattern=logs_log_group.split("-")[-1]
211+
)
212+
snapshot.match("list-log-groups-pattern-match", response)
213+
207214
aws_client.logs.create_log_stream(logGroupName=logs_log_group, logStreamName=test_name)
208215
log_streams_between = aws_client.logs.describe_log_streams(logGroupName=logs_log_group).get(
209216
"logStreams", []

tests/aws/services/logs/test_logs.snapshot.json

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -140,13 +140,15 @@
140140
}
141141
},
142142
"tests/aws/services/logs/test_logs.py::TestCloudWatchLogs::test_create_and_delete_log_stream": {
143-
"recorded-date": "06-04-2023, 11:42:42",
143+
"recorded-date": "05-11-2025, 17:25:32",
144144
"recorded-content": {
145145
"describe-log-groups-prefix": {
146146
"logGroups": [
147147
{
148148
"arn": "arn:<partition>:logs:<region>:111111111111:log-group:<log-group-name:1>:*",
149149
"creationTime": "timestamp",
150+
"logGroupArn": "arn:<partition>:logs:<region>:111111111111:log-group:<log-group-name:1>",
151+
"logGroupClass": "STANDARD",
150152
"logGroupName": "<log-group-name:1>",
151153
"metricFilterCount": 0,
152154
"storedBytes": 0
@@ -162,7 +164,10 @@
162164
{
163165
"arn": "arn:<partition>:logs:<region>:111111111111:log-group:<log-group-name:1>:*",
164166
"creationTime": "timestamp",
165-
"logGroupName": "<log-group-name:1>"
167+
"logGroupArn": "arn:<partition>:logs:<region>:111111111111:log-group:<log-group-name:1>",
168+
"logGroupClass": "STANDARD",
169+
"logGroupName": "<log-group-name:1>",
170+
"metricFilterCount": 0
166171
}
167172
],
168173
"nextToken": "<next_token>",
@@ -181,6 +186,27 @@
181186
"HTTPStatusCode": 400
182187
}
183188
},
189+
"list-log-groups-pattern-no-match": {
190+
"logGroups": [],
191+
"ResponseMetadata": {
192+
"HTTPHeaders": {},
193+
"HTTPStatusCode": 200
194+
}
195+
},
196+
"list-log-groups-pattern-match": {
197+
"logGroups": [
198+
{
199+
"logGroupArn": "arn:<partition>:logs:<region>:111111111111:log-group:<log-group-name:1>",
200+
"logGroupClass": "STANDARD",
201+
"logGroupName": "<log-group-name:1>"
202+
}
203+
],
204+
"nextToken": "<next_token>",
205+
"ResponseMetadata": {
206+
"HTTPHeaders": {},
207+
"HTTPStatusCode": 200
208+
}
209+
},
184210
"logs_log_group": [
185211
{
186212
"logStreamName": "<log-stream-name:1>",

tests/aws/services/logs/test_logs.validation.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,13 @@
33
"last_validated_date": "2024-05-24T13:57:11+00:00"
44
},
55
"tests/aws/services/logs/test_logs.py::TestCloudWatchLogs::test_create_and_delete_log_stream": {
6-
"last_validated_date": "2023-04-06T09:42:42+00:00"
6+
"last_validated_date": "2025-11-05T17:25:41+00:00",
7+
"durations_in_seconds": {
8+
"setup": 1.91,
9+
"call": 3.46,
10+
"teardown": 0.16,
11+
"total": 5.53
12+
}
713
},
814
"tests/aws/services/logs/test_logs.py::TestCloudWatchLogs::test_filter_log_events_response_header": {
915
"last_validated_date": "2024-05-24T13:58:30+00:00"

0 commit comments

Comments
 (0)