Skip to content
This repository was archived by the owner on Mar 23, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 36 additions & 19 deletions localstack-core/localstack/services/logs/provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,13 @@
InputLogEvents,
InvalidParameterException,
KmsKeyId,
ListLogGroupsRequest,
ListLogGroupsResponse,
ListTagsForResourceResponse,
ListTagsLogGroupResponse,
LogGroupClass,
LogGroupName,
LogGroupSummary,
LogsApi,
LogStreamName,
PutLogEventsResponse,
Expand All @@ -43,7 +46,7 @@
from localstack.utils.aws import arns
from localstack.utils.aws.client_types import ServicePrincipal
from localstack.utils.bootstrap import is_api_enabled
from localstack.utils.common import is_number
from localstack.utils.numbers import is_number
from localstack.utils.patch import patch

LOG = logging.getLogger(__name__)
Expand All @@ -60,8 +63,8 @@ def put_log_events(
log_group_name: LogGroupName,
log_stream_name: LogStreamName,
log_events: InputLogEvents,
sequence_token: SequenceToken = None,
entity: Entity = None,
sequence_token: SequenceToken | None = None,
entity: Entity | None = None,
**kwargs,
) -> PutLogEventsResponse:
logs_backend = get_moto_logs_backend(context.account_id, context.region)
Expand Down Expand Up @@ -97,33 +100,32 @@ def describe_log_groups(
) -> DescribeLogGroupsResponse:
region_backend = get_moto_logs_backend(context.account_id, context.region)

prefix: str = request.get("logGroupNamePrefix", "")
pattern: str = request.get("logGroupNamePattern", "")
prefix: str | None = request.get("logGroupNamePrefix", "")
pattern: str | None = request.get("logGroupNamePattern", "")

if pattern and prefix:
raise InvalidParameterException(
"LogGroup name prefix and LogGroup name pattern are mutually exclusive parameters."
)

copy_groups = copy.deepcopy(dict(region_backend.groups))
moto_groups = copy.deepcopy(dict(region_backend.groups)).values()

groups = [
group.to_describe_dict()
for name, group in copy_groups.items()
{"logGroupClass": LogGroupClass.STANDARD} | group.to_describe_dict()
for group in sorted(moto_groups, key=lambda g: g.name)
if not (prefix or pattern)
or (prefix and name.startswith(prefix))
or (pattern and pattern in name)
or (prefix and group.name.startswith(prefix))
or (pattern and pattern in group.name)
]

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

@handler("DescribeLogStreams", expand=False)
def describe_log_streams(
self, context: RequestContext, request: DescribeLogStreamsRequest
) -> DescribeLogStreamsResponse:
log_group_name: str = request.get("logGroupName")
log_group_identifier: str = request.get("logGroupIdentifier")
log_group_name: str | None = request.get("logGroupName")
log_group_identifier: str | None = request.get("logGroupIdentifier")

if log_group_identifier and log_group_name:
raise CommonServiceException(
Expand All @@ -138,13 +140,29 @@ def describe_log_streams(

return moto.call_moto_with_request(context, request_copy)

@handler("ListLogGroups", expand=False)
def list_log_groups(
self, context: RequestContext, request: ListLogGroupsRequest
) -> ListLogGroupsResponse:
pattern: str | None = request.get("logGroupNamePattern")
region_backend: LogsBackend = get_moto_logs_backend(context.account_id, context.region)
moto_groups = copy.deepcopy(region_backend.groups).values()
groups = [
LogGroupSummary(
logGroupName=group.name, logGroupArn=group.arn, logGroupClass=LogGroupClass.STANDARD
)
for group in sorted(moto_groups, key=lambda g: g.name)
if not pattern or pattern in group.name
]
return ListLogGroupsResponse(logGroups=groups)

def create_log_group(
self,
context: RequestContext,
log_group_name: LogGroupName,
kms_key_id: KmsKeyId = None,
tags: Tags = None,
log_group_class: LogGroupClass = None,
kms_key_id: KmsKeyId | None = None,
tags: Tags | None = None,
log_group_class: LogGroupClass | None = None,
**kwargs,
) -> None:
call_moto(context)
Expand Down Expand Up @@ -442,10 +460,9 @@ def moto_to_describe_dict(target, self):
# reported race condition in https://github.com/localstack/localstack/issues/8011
# making copy of "streams" dict here to avoid issues while summing up storedBytes
copy_streams = copy.deepcopy(self.streams)
# parity tests shows that the arn ends with ":*"
arn = self.arn if self.arn.endswith(":*") else f"{self.arn}:*"
log_group = {
"arn": arn,
"arn": f"{self.arn}:*",
"logGroupArn": self.arn,
"creationTime": self.creation_time,
"logGroupName": self.name,
"metricFilterCount": 0,
Expand Down
9 changes: 8 additions & 1 deletion tests/aws/services/logs/test_logs.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,9 +166,9 @@ def test_list_tags_log_group(self, snapshot, aws_client):
# TODO 'describe-log-groups' returns different attributes on AWS when using
# 'logGroupNamePattern' compared to 'logGroupNamePrefix' (for the same log group)
# seems like a weird issue on AWS side, we just exclude the paths here for this particular call
"$..describe-log-groups-pattern.logGroups..metricFilterCount",
"$..describe-log-groups-pattern.logGroups..storedBytes",
"$..describe-log-groups-pattern.nextToken",
"$..list-log-groups-pattern-match.nextToken",
]
)
@markers.aws.validated
Expand Down Expand Up @@ -204,6 +204,13 @@ def test_create_and_delete_log_stream(self, logs_log_group, aws_client, region_n
)
snapshot.match("error-describe-logs-group", ctx.value.response)

response = aws_client.logs.list_log_groups(logGroupNamePattern="no-such-group")
snapshot.match("list-log-groups-pattern-no-match", response)
response = aws_client.logs.list_log_groups(
logGroupNamePattern=logs_log_group.split("-")[-1]
)
snapshot.match("list-log-groups-pattern-match", response)

aws_client.logs.create_log_stream(logGroupName=logs_log_group, logStreamName=test_name)
log_streams_between = aws_client.logs.describe_log_streams(logGroupName=logs_log_group).get(
"logStreams", []
Expand Down
30 changes: 28 additions & 2 deletions tests/aws/services/logs/test_logs.snapshot.json
Original file line number Diff line number Diff line change
Expand Up @@ -140,13 +140,15 @@
}
},
"tests/aws/services/logs/test_logs.py::TestCloudWatchLogs::test_create_and_delete_log_stream": {
"recorded-date": "06-04-2023, 11:42:42",
"recorded-date": "05-11-2025, 17:25:32",
"recorded-content": {
"describe-log-groups-prefix": {
"logGroups": [
{
"arn": "arn:<partition>:logs:<region>:111111111111:log-group:<log-group-name:1>:*",
"creationTime": "timestamp",
"logGroupArn": "arn:<partition>:logs:<region>:111111111111:log-group:<log-group-name:1>",
"logGroupClass": "STANDARD",
"logGroupName": "<log-group-name:1>",
"metricFilterCount": 0,
"storedBytes": 0
Expand All @@ -162,7 +164,10 @@
{
"arn": "arn:<partition>:logs:<region>:111111111111:log-group:<log-group-name:1>:*",
"creationTime": "timestamp",
"logGroupName": "<log-group-name:1>"
"logGroupArn": "arn:<partition>:logs:<region>:111111111111:log-group:<log-group-name:1>",
"logGroupClass": "STANDARD",
"logGroupName": "<log-group-name:1>",
"metricFilterCount": 0
}
],
"nextToken": "<next_token>",
Expand All @@ -181,6 +186,27 @@
"HTTPStatusCode": 400
}
},
"list-log-groups-pattern-no-match": {
"logGroups": [],
"ResponseMetadata": {
"HTTPHeaders": {},
"HTTPStatusCode": 200
}
},
"list-log-groups-pattern-match": {
"logGroups": [
{
"logGroupArn": "arn:<partition>:logs:<region>:111111111111:log-group:<log-group-name:1>",
"logGroupClass": "STANDARD",
"logGroupName": "<log-group-name:1>"
}
],
"nextToken": "<next_token>",
"ResponseMetadata": {
"HTTPHeaders": {},
"HTTPStatusCode": 200
}
},
"logs_log_group": [
{
"logStreamName": "<log-stream-name:1>",
Expand Down
8 changes: 7 additions & 1 deletion tests/aws/services/logs/test_logs.validation.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,13 @@
"last_validated_date": "2024-05-24T13:57:11+00:00"
},
"tests/aws/services/logs/test_logs.py::TestCloudWatchLogs::test_create_and_delete_log_stream": {
"last_validated_date": "2023-04-06T09:42:42+00:00"
"last_validated_date": "2025-11-05T17:25:41+00:00",
"durations_in_seconds": {
"setup": 1.91,
"call": 3.46,
"teardown": 0.16,
"total": 5.53
}
},
"tests/aws/services/logs/test_logs.py::TestCloudWatchLogs::test_filter_log_events_response_header": {
"last_validated_date": "2024-05-24T13:58:30+00:00"
Expand Down
Loading