Repository: localstack/localstack

Issue:
bug: EventBridge Scheduled Rules generate an empty event
### Is there an existing issue for this?

- [X] I have searched the existing issues

### Current Behavior

When a scheduled rules runs, an empty event is generated and sent to targets.

### Expected Behavior

An event like this sample should be sent.
```json
{
  "version": "0",
  "id": "89d1a02d-5ec7-412e-82f5-13505f849b41",
  "detail-type": "Scheduled Event",
  "source": "aws.events",
  "account": "123456789012",
  "time": "2016-12-30T18:44:49Z",
  "region": "us-east-1",
  "resources": ["arn:aws:events:us-east-1:123456789012:rule/SampleRule"],
  "detail": {}
}
```

### How are you starting LocalStack?

With a docker-compose file

### Steps To Reproduce

#### How are you starting localstack (e.g., `bin/localstack` command, arguments, or `docker-compose.yml`)

    docker-compose up

#### Client commands (e.g., AWS SDK code snippet, or sequence of "awslocal" commands)

    awslocal events put-rule --name scheduled_rule --schedule-expression "rate(1 minutes)"
    awslocal events put-targets --rule scheduled_rule --targets "Id"="1","Arn"="arn:aws:logs:us-east-1:000000000000:log-group:/aws/events/echo"
    awslocal--endpoint-url=http://localhost:4566 logs tail "/aws/events/echo"

### Environment

```markdown
- OS: macOS Monterey
- LocalStack: 0.13.3
```


### Anything else?
I think [this](https://github.com/localstack/localstack/blob/1567f25a8f672f4dec23cb7cb1aadb5a744366f8/localstack/services/events/events_listener.py#L92) means an non-empty event is sent only when an input is `Constant (JSON text)`.
![image](https://user-images.githubusercontent.com/4127030/151643813-a4d1e113-dba3-41ad-8400-da57e4f674a1.png)

bug: EventBridge Scheduled Rules generate an empty event
### Is there an existing issue for this?

- [X] I have searched the existing issues

### Current Behavior

When a scheduled rules runs, an empty event is generated and sent to targets.

### Expected Behavior

An event like this sample should be sent.
```json
{
  "version": "0",
  "id": "89d1a02d-5ec7-412e-82f5-13505f849b41",
  "detail-type": "Scheduled Event",
  "source": "aws.events",
  "account": "123456789012",
  "time": "2016-12-30T18:44:49Z",
  "region": "us-east-1",
  "resources": ["arn:aws:events:us-east-1:123456789012:rule/SampleRule"],
  "detail": {}
}
```

### How are you starting LocalStack?

With a docker-compose file

### Steps To Reproduce

#### How are you starting localstack (e.g., `bin/localstack` command, arguments, or `docker-compose.yml`)

    docker-compose up

#### Client commands (e.g., AWS SDK code snippet, or sequence of "awslocal" commands)

    awslocal events put-rule --name scheduled_rule --schedule-expression "rate(1 minutes)"
    awslocal events put-targets --rule scheduled_rule --targets "Id"="1","Arn"="arn:aws:logs:us-east-1:000000000000:log-group:/aws/events/echo"
    awslocal--endpoint-url=http://localhost:4566 logs tail "/aws/events/echo"

### Environment

```markdown
- OS: macOS Monterey
- LocalStack: 0.13.3
```


### Anything else?
I think [this](https://github.com/localstack/localstack/blob/1567f25a8f672f4dec23cb7cb1aadb5a744366f8/localstack/services/events/events_listener.py#L92) means an non-empty event is sent only when an input is `Constant (JSON text)`.
![image](https://user-images.githubusercontent.com/4127030/151643813-a4d1e113-dba3-41ad-8400-da57e4f674a1.png)



Discussion:
I second this one. 

Docker container version:
```
root@6ce6e86c0347:~# /opt/code/localstack/bin/localstack --version
0.14.1
```

Commands and outputs:
```
$ awslocal logs create-log-group --log-group-name "/aws/events/echo"
$ awslocal events put-rule --name scheduled_rule --schedule-expression "rate(1 minutes)"
$ awslocal events put-targets --rule scheduled_rule --targets "Id"="1","Arn"="arn:aws:logs:us-east-1:000000000000:log-group:/aws/events/echo"
$ awslocal logs describe-log-streams --log-group-name /aws/events/echo
{
    "logStreams": [
        {
            "logStreamName": "31ccea0e-9861-4926-bdbd-8f2d34623406",
            "creationTime": 1647201075202,
            "firstEventTimestamp": 1647201075205,
            "lastEventTimestamp": 1647201075205,
            "lastIngestionTime": 1647201075211,
            "uploadSequenceToken": "1",
            "arn": "arn:aws:logs:us-east-1:000000000000:log-group:/aws/events/echo:log-stream:31ccea0e-9861-4926-bdbd-8f2d34623406",
            "storedBytes": 2
        }
    ]
}
$ awslocal logs get-log-events --log-group-name /aws/events/echo --log-stream-name 31ccea0e-9861-4926-bdbd-8f2d34623406
{
    "events": [
        {
            "timestamp": 1647201075205,
            "message": "{}",
            "ingestionTime": 1647201075211
        }
    ],
    "nextForwardToken": "f/00000000000000000000000000000000000000000000000000000000",
    "nextBackwardToken": "b/00000000000000000000000000000000000000000000000000000000"
}
```

Hi @ph-l!
I think this issue should have been fixed a while ago with https://github.com/localstack/localstack/pull/6124. /cc @lukqw @dominikschubert 
Could you please verify if this issue is still persistent in the latest version of LocalStack?
Thanks!

Hi @alexrashed,

I don't think it's fixed yet.
```
$ docker-compose up
[+] Running 1/0
 ✔ Container localstack_main  Created                                                                                                                                                                                                     0.0s 
Attaching to localstack_main
localstack_main  | 
localstack_main  | LocalStack version: 2.2.1.dev
localstack_main  | 
localstack_main  | LocalStack Docker container id: 0989e4091f48
localstack_main  | LocalStack build date: 2023-09-13
localstack_main  | LocalStack build git hash: fa10d1d7
```

```
$ awslocal logs create-log-group --log-group-name "/aws/events/echo"
$ awslocal events put-rule --name scheduled_rule --schedule-expression "rate(1 minutes)"
{
    "RuleArn": "arn:aws:events:us-east-1:000000000000:rule/scheduled_rule"
}
$ awslocal events put-targets --rule scheduled_rule --targets "Id"="1","Arn"="arn:aws:logs:us-east-1:000000000000:log-group:/aws/events/echo"
{
    "FailedEntryCount": 0,
    "FailedEntries": []
}
$ awslocal logs describe-log-streams --log-group-name /aws/events/echo
{
    "logStreams": [
        {
            "logStreamName": "232363e9-b1cc-48df-91c0-7a5c4dd69f22",
            "creationTime": 1694655112549,
            "firstEventTimestamp": 1694655112551,
            "lastEventTimestamp": 1694655112551,
            "lastIngestionTime": 1694655112555,
            "uploadSequenceToken": "1",
            "arn": "arn:aws:logs:us-east-1:000000000000:log-group:/aws/events/echo:log-stream:232363e9-b1cc-48df-91c0-7a5c4dd69f22",
            "storedBytes": 2
        }
    ]
}
$ awslocal logs get-log-events --log-group-name /aws/events/echo --log-stream-name
{
    "events": [
        {
            "timestamp": 1694655112551,
            "message": "{}",
            "ingestionTime": 1694655112555
        }
    ],
    "nextForwardToken": "f/00000000000000000000000000000000000000000000000000000000",
    "nextBackwardToken": "b/00000000000000000000000000000000000000000000000000000000"
}
```
I can confirm this issue is still present in 2.3, there's even a TODO in the code for scheduled events: https://github.com/localstack/localstack/blob/e7cf994ab6edf29d585d13699e6306722d03145a/localstack/services/events/provider.py#L112

i'm currently writing some tests for this and will add a resolution as soon as those are in place.
I second this one. 

Docker container version:
```
root@6ce6e86c0347:~# /opt/code/localstack/bin/localstack --version
0.14.1
```

Commands and outputs:
```
$ awslocal logs create-log-group --log-group-name "/aws/events/echo"
$ awslocal events put-rule --name scheduled_rule --schedule-expression "rate(1 minutes)"
$ awslocal events put-targets --rule scheduled_rule --targets "Id"="1","Arn"="arn:aws:logs:us-east-1:000000000000:log-group:/aws/events/echo"
$ awslocal logs describe-log-streams --log-group-name /aws/events/echo
{
    "logStreams": [
        {
            "logStreamName": "31ccea0e-9861-4926-bdbd-8f2d34623406",
            "creationTime": 1647201075202,
            "firstEventTimestamp": 1647201075205,
            "lastEventTimestamp": 1647201075205,
            "lastIngestionTime": 1647201075211,
            "uploadSequenceToken": "1",
            "arn": "arn:aws:logs:us-east-1:000000000000:log-group:/aws/events/echo:log-stream:31ccea0e-9861-4926-bdbd-8f2d34623406",
            "storedBytes": 2
        }
    ]
}
$ awslocal logs get-log-events --log-group-name /aws/events/echo --log-stream-name 31ccea0e-9861-4926-bdbd-8f2d34623406
{
    "events": [
        {
            "timestamp": 1647201075205,
            "message": "{}",
            "ingestionTime": 1647201075211
        }
    ],
    "nextForwardToken": "f/00000000000000000000000000000000000000000000000000000000",
    "nextBackwardToken": "b/00000000000000000000000000000000000000000000000000000000"
}
```

Hi @ph-l!
I think this issue should have been fixed a while ago with https://github.com/localstack/localstack/pull/6124. /cc @lukqw @dominikschubert 
Could you please verify if this issue is still persistent in the latest version of LocalStack?
Thanks!

Hi @alexrashed,

I don't think it's fixed yet.
```
$ docker-compose up
[+] Running 1/0
 ✔ Container localstack_main  Created                                                                                                                                                                                                     0.0s 
Attaching to localstack_main
localstack_main  | 
localstack_main  | LocalStack version: 2.2.1.dev
localstack_main  | 
localstack_main  | LocalStack Docker container id: 0989e4091f48
localstack_main  | LocalStack build date: 2023-09-13
localstack_main  | LocalStack build git hash: fa10d1d7
```

```
$ awslocal logs create-log-group --log-group-name "/aws/events/echo"
$ awslocal events put-rule --name scheduled_rule --schedule-expression "rate(1 minutes)"
{
    "RuleArn": "arn:aws:events:us-east-1:000000000000:rule/scheduled_rule"
}
$ awslocal events put-targets --rule scheduled_rule --targets "Id"="1","Arn"="arn:aws:logs:us-east-1:000000000000:log-group:/aws/events/echo"
{
    "FailedEntryCount": 0,
    "FailedEntries": []
}
$ awslocal logs describe-log-streams --log-group-name /aws/events/echo
{
    "logStreams": [
        {
            "logStreamName": "232363e9-b1cc-48df-91c0-7a5c4dd69f22",
            "creationTime": 1694655112549,
            "firstEventTimestamp": 1694655112551,
            "lastEventTimestamp": 1694655112551,
            "lastIngestionTime": 1694655112555,
            "uploadSequenceToken": "1",
            "arn": "arn:aws:logs:us-east-1:000000000000:log-group:/aws/events/echo:log-stream:232363e9-b1cc-48df-91c0-7a5c4dd69f22",
            "storedBytes": 2
        }
    ]
}
$ awslocal logs get-log-events --log-group-name /aws/events/echo --log-stream-name
{
    "events": [
        {
            "timestamp": 1694655112551,
            "message": "{}",
            "ingestionTime": 1694655112555
        }
    ],
    "nextForwardToken": "f/00000000000000000000000000000000000000000000000000000000",
    "nextBackwardToken": "b/00000000000000000000000000000000000000000000000000000000"
}
```
I can confirm this issue is still present in 2.3, there's even a TODO in the code for scheduled events: https://github.com/localstack/localstack/blob/e7cf994ab6edf29d585d13699e6306722d03145a/localstack/services/events/provider.py#L112

i'm currently writing some tests for this and will add a resolution as soon as those are in place.

Resolving patch:
diff --git a/localstack/services/events/provider.py b/localstack/services/events/provider.py
--- a/localstack/services/events/provider.py
+++ b/localstack/services/events/provider.py
@@ -9,6 +9,8 @@
 
 from moto.events import events_backends
 from moto.events.responses import EventsHandler as MotoEventsHandler
+from werkzeug import Request
+from werkzeug.exceptions import NotFound
 
 from localstack import config
 from localstack.aws.api import RequestContext
@@ -36,17 +38,20 @@
     TestEventPatternResponse,
 )
 from localstack.constants import APPLICATION_AMZ_JSON_1_1
+from localstack.http import route
+from localstack.services.edge import ROUTER
 from localstack.services.events.models import EventsStore, events_stores
 from localstack.services.events.scheduler import JobScheduler
 from localstack.services.moto import call_moto
 from localstack.services.plugins import ServiceLifecycleHook
-from localstack.utils.aws.arns import event_bus_arn
+from localstack.utils.aws.arns import event_bus_arn, parse_arn
 from localstack.utils.aws.client_types import ServicePrincipal
 from localstack.utils.aws.message_forwarding import send_event_to_target
 from localstack.utils.collections import pick_attributes
 from localstack.utils.common import TMP_FILES, mkdir, save_file, truncate
 from localstack.utils.json import extract_jsonpath
 from localstack.utils.strings import long_uid, short_uid
+from localstack.utils.time import TIMESTAMP_FORMAT_TZ, timestamp
 
 LOG = logging.getLogger(__name__)
 
@@ -68,12 +73,34 @@ class EventsProvider(EventsApi, ServiceLifecycleHook):
     def __init__(self):
         apply_patches()
 
+    def on_after_init(self):
+        ROUTER.add(self.trigger_scheduled_rule)
+
     def on_before_start(self):
         JobScheduler.start()
 
     def on_before_stop(self):
         JobScheduler.shutdown()
 
+    @route("/_aws/events/rules/<path:rule_arn>/trigger")
+    def trigger_scheduled_rule(self, request: Request, rule_arn: str):
+        """Developer endpoint to trigger a scheduled rule."""
+        arn_data = parse_arn(rule_arn)
+        account_id = arn_data["account"]
+        region = arn_data["region"]
+        rule_name = arn_data["resource"].split("/", maxsplit=1)[-1]
+
+        job_id = events_stores[account_id][region].rule_scheduled_jobs.get(rule_name)
+        if not job_id:
+            raise NotFound()
+        job = JobScheduler().instance().get_job(job_id)
+        if not job:
+            raise NotFound()
+
+        # TODO: once job scheduler is refactored, we can update the deadline of the task instead of running
+        #  it here
+        job.run()
+
     @staticmethod
     def get_store(context: RequestContext) -> EventsStore:
         return events_stores[context.account_id][context.region]
@@ -100,7 +127,9 @@ def get_scheduled_rule_func(
         event_bus_name_or_arn: Optional[EventBusNameOrArn] = None,
     ):
         def func(*args, **kwargs):
-            moto_backend = events_backends[store._account_id][store._region_name]
+            account_id = store._account_id
+            region = store._region_name
+            moto_backend = events_backends[account_id][region]
             event_bus_name = get_event_bus_name(event_bus_name_or_arn)
             event_bus = moto_backend.event_buses[event_bus_name]
             rule = event_bus.rules.get(rule_name)
@@ -113,11 +142,33 @@ def func(*args, **kwargs):
                     len(rule.targets),
                     rule_name,
                 )
+
+            default_event = {
+                "version": "0",
+                "id": long_uid(),
+                "detail-type": "Scheduled Event",
+                "source": "aws.events",
+                "account": account_id,
+                "time": timestamp(format=TIMESTAMP_FORMAT_TZ),
+                "region": region,
+                "resources": [rule.arn],
+                "detail": {},
+            }
+
             for target in rule.targets:
                 arn = target.get("Arn")
-                # TODO generate event matching aws in case no Input has been specified
-                event_str = target.get("Input") or "{}"
-                event = json.loads(event_str)
+
+                if input_ := target.get("Input"):
+                    event = json.loads(input_)
+                else:
+                    event = default_event
+                    if target.get("InputPath"):
+                        event = filter_event_with_target_input_path(target, event)
+                    if target.get("InputTransformer"):
+                        LOG.warning(
+                            "InputTransformer is currently not supported for scheduled rules"
+                        )
+
                 attr = pick_attributes(target, ["$.SqsParameters", "$.KinesisParameters"])
 
                 try:
diff --git a/localstack/services/events/scheduler.py b/localstack/services/events/scheduler.py
--- a/localstack/services/events/scheduler.py
+++ b/localstack/services/events/scheduler.py
@@ -47,6 +47,11 @@ def add_job(self, job_func, schedule, enabled=True):
         self.jobs.append(job)
         return job.job_id
 
+    def get_job(self, job_id) -> Job | None:
+        for job in self.jobs:
+            if job.job_id == job_id:
+                return job
+
     def disable_job(self, job_id):
         for job in self.jobs:
             if job.job_id == job_id:


Test patch:
diff --git a/localstack/testing/aws/eventbus_utils.py b/localstack/testing/aws/eventbus_utils.py
new file mode 100644
--- /dev/null
+++ b/localstack/testing/aws/eventbus_utils.py
@@ -0,0 +1,54 @@
+import json
+
+import requests
+
+from localstack.config import get_edge_url
+from localstack.testing.aws.util import is_aws_cloud
+from localstack.utils.aws.client_types import TypedServiceClientFactory
+
+
+def trigger_scheduled_rule(rule_arn: str):
+    """
+    Call the internal /_aws/events/rules/<rule_arn>/trigger endpoint to expire the deadline of a rule and
+    trigger it ASAP.
+
+    :param rule_arn: the rule to run
+    :raises ValueError: if the response return a >=400 code
+    """
+    if is_aws_cloud():
+        return
+
+    url = get_edge_url() + f"/_aws/events/rules/{rule_arn}/trigger"
+    response = requests.get(url)
+    if not response.ok:
+        raise ValueError(
+            f"Error triggering rule {rule_arn}: {response.status_code},{response.text}"
+        )
+
+
+def allow_event_rule_to_sqs_queue(
+    aws_client: TypedServiceClientFactory,
+    sqs_queue_url: str,
+    sqs_queue_arn: str,
+    event_rule_arn: str,
+):
+    """Creates an SQS Queue Policy that allows te given eventbus rule to write tho the given sqs queue."""
+    return aws_client.sqs.set_queue_attributes(
+        QueueUrl=sqs_queue_url,
+        Attributes={
+            "Policy": json.dumps(
+                {
+                    "Statement": [
+                        {
+                            "Sid": "AllowEventsToQueue",
+                            "Effect": "Allow",
+                            "Principal": {"Service": "events.amazonaws.com"},
+                            "Action": "sqs:SendMessage",
+                            "Resource": sqs_queue_arn,
+                            "Condition": {"ArnEquals": {"aws:SourceArn": event_rule_arn}},
+                        }
+                    ]
+                }
+            )
+        },
+    )
diff --git a/tests/aws/services/events/conftest.py b/tests/aws/services/events/conftest.py
new file mode 100644
--- /dev/null
+++ b/tests/aws/services/events/conftest.py
@@ -0,0 +1,116 @@
+import json
+
+import pytest
+
+from localstack.utils.functions import call_safe
+from localstack.utils.strings import short_uid
+
+
+@pytest.fixture
+def events_allow_event_rule_to_sqs_queue(aws_client):
+    def _allow_event_rule(sqs_queue_url, sqs_queue_arn, event_rule_arn) -> None:
+        # allow event rule to write to sqs queue
+        aws_client.sqs.set_queue_attributes(
+            QueueUrl=sqs_queue_url,
+            Attributes={
+                "Policy": json.dumps(
+                    {
+                        "Statement": [
+                            {
+                                "Sid": "AllowEventsToQueue",
+                                "Effect": "Allow",
+                                "Principal": {"Service": "events.amazonaws.com"},
+                                "Action": "sqs:SendMessage",
+                                "Resource": sqs_queue_arn,
+                                "Condition": {"ArnEquals": {"aws:SourceArn": event_rule_arn}},
+                            }
+                        ]
+                    }
+                )
+            },
+        )
+
+    return _allow_event_rule
+
+
+@pytest.fixture
+def events_put_rule(aws_client):
+    rules = []
+
+    def _factory(**kwargs):
+        if "Name" not in kwargs:
+            kwargs["Name"] = f"rule-{short_uid()}"
+
+        resp = aws_client.events.put_rule(**kwargs)
+        rules.append((kwargs["Name"], kwargs.get("EventBusName", "default")))
+        return resp
+
+    yield _factory
+
+    for rule, event_bus_name in rules:
+        targets_response = aws_client.events.list_targets_by_rule(
+            Rule=rule, EventBusName=event_bus_name
+        )
+        if targets := targets_response["Targets"]:
+            targets_ids = [target["Id"] for target in targets]
+            aws_client.events.remove_targets(
+                Rule=rule, EventBusName=event_bus_name, Ids=targets_ids
+            )
+        aws_client.events.delete_rule(Name=rule, EventBusName=event_bus_name)
+
+
+@pytest.fixture
+def events_create_event_bus(aws_client):
+    event_buses = []
+
+    def _factory(**kwargs):
+        if "Name" not in kwargs:
+            kwargs["Name"] = f"event-bus-{short_uid()}"
+        resp = aws_client.events.create_event_bus(**kwargs)
+        event_buses.append(kwargs["Name"])
+        return resp
+
+    yield _factory
+
+    for event_bus in event_buses:
+        aws_client.events.delete_event_bus(Name=event_bus)
+
+
+@pytest.fixture
+def clean_up(aws_client):
+    def _clean_up(
+        bus_name=None,
+        rule_name=None,
+        target_ids=None,
+        queue_url=None,
+        log_group_name=None,
+    ):
+        events_client = aws_client.events
+        kwargs = {"EventBusName": bus_name} if bus_name else {}
+        if target_ids:
+            target_ids = target_ids if isinstance(target_ids, list) else [target_ids]
+            call_safe(
+                events_client.remove_targets,
+                kwargs=dict(Rule=rule_name, Ids=target_ids, Force=True, **kwargs),
+            )
+        if rule_name:
+            call_safe(events_client.delete_rule, kwargs=dict(Name=rule_name, Force=True, **kwargs))
+        if bus_name:
+            call_safe(events_client.delete_event_bus, kwargs=dict(Name=bus_name))
+        if queue_url:
+            sqs_client = aws_client.sqs
+            call_safe(sqs_client.delete_queue, kwargs=dict(QueueUrl=queue_url))
+        if log_group_name:
+            logs_client = aws_client.logs
+
+            def _delete_log_group():
+                log_streams = logs_client.describe_log_streams(logGroupName=log_group_name)
+                for log_stream in log_streams["logStreams"]:
+                    logs_client.delete_log_stream(
+                        logGroupName=log_group_name, logStreamName=log_stream["logStreamName"]
+                    )
+                logs_client.delete_log_group(logGroupName=log_group_name)
+
+            call_safe(_delete_log_group)
+
+    yield _clean_up
diff --git a/tests/aws/services/events/scheduled_rules/__init__.py b/tests/aws/services/events/scheduled_rules/__init__.py
new file mode 100644
diff --git a/tests/aws/services/events/scheduled_rules/test_events_scheduled_rules_logs.py b/tests/aws/services/events/scheduled_rules/test_events_scheduled_rules_logs.py
new file mode 100644
--- /dev/null
+++ b/tests/aws/services/events/scheduled_rules/test_events_scheduled_rules_logs.py
@@ -0,0 +1,137 @@
+import json
+import logging
+
+import pytest
+
+from localstack.testing.aws.eventbus_utils import trigger_scheduled_rule
+from localstack.testing.pytest import markers
+from localstack.testing.snapshots.transformer_utility import TransformerUtility
+from localstack.utils.strings import short_uid
+from localstack.utils.sync import retry
+
+LOG = logging.getLogger(__name__)
+
+
+@pytest.fixture
+def logs_log_group(aws_client):
+    name = f"test-log-group-{short_uid()}"
+    aws_client.logs.create_log_group(logGroupName=name)
+    yield name
+    aws_client.logs.delete_log_group(logGroupName=name)
+
+
+@pytest.fixture
+def add_logs_resource_policy_for_rule(aws_client):
+    policies = []
+
+    def _provide_access(rule_arn: str, log_group_arn: str):
+        policy_name = f"test-policy-{short_uid()}"
+
+        policy = aws_client.logs.put_resource_policy(
+            policyName=policy_name,
+            policyDocument=json.dumps(
+                {
+                    "Version": "2012-10-17",
+                    "Statement": [
+                        {
+                            "Sid": "AllowPutEvents",
+                            "Effect": "Allow",
+                            "Principal": {"Service": "events.amazonaws.com"},
+                            "Action": ["logs:PutLogEvents", "logs:CreateLogStream"],
+                            "Resource": log_group_arn,
+                        },
+                    ],
+                }
+            ),
+        )
+
+        policies.append(policy_name)
+
+        return policy
+
+    yield _provide_access
+
+    for policy_name in policies:
+        aws_client.logs.delete_resource_policy(policyName=policy_name)
+
+
+@markers.aws.validated
+@markers.snapshot.skip_snapshot_verify(
+    paths=[
+        # tokens and IDs cannot be properly transformed
+        "$..eventId",
+        "$..uploadSequenceToken",
+        # FIXME: storedBytes should be implemented
+        "$..storedBytes",
+    ]
+)
+def test_scheduled_rule_logs(
+    logs_log_group,
+    events_put_rule,
+    add_logs_resource_policy_for_rule,
+    aws_client,
+    snapshot,
+):
+    schedule_expression = "rate(1 minute)"
+    rule_name = f"rule-{short_uid()}"
+    snapshot.add_transformers_list(
+        [
+            snapshot.transform.regex(rule_name, "<rule-name>"),
+            snapshot.transform.regex(logs_log_group, "<log-group-name>"),
+        ]
+    )
+    snapshot.add_transformer(TransformerUtility.logs_api())
+
+    response = aws_client.logs.describe_log_groups(logGroupNamePrefix=logs_log_group)
+    log_group_arn = response["logGroups"][0]["arn"]
+
+    rule_arn = events_put_rule(Name=rule_name, ScheduleExpression=schedule_expression)["RuleArn"]
+    add_logs_resource_policy_for_rule(rule_arn, log_group_arn)
+
+    # TODO: add target to test InputTransformer
+    aws_client.events.put_targets(
+        Rule=rule_name,
+        Targets=[
+            {"Id": "1", "Arn": log_group_arn},
+            {"Id": "2", "Arn": log_group_arn},
+        ],
+    )
+
+    trigger_scheduled_rule(rule_arn)
+
+    # wait for log stream to be created
+    def _get_log_stream():
+        result = (
+            aws_client.logs.get_paginator("describe_log_streams")
+            .paginate(logGroupName=logs_log_group)
+            .build_full_result()
+        )
+        assert len(result["logStreams"]) >= 2
+        return result["logStreams"]
+
+    log_streams = retry(_get_log_stream, 60)
+    log_streams.sort(key=lambda stream: stream["creationTime"])
+    snapshot.match("log-streams", log_streams)
+
+    # collect events from log streams in group
+    def _get_events():
+        _events = []
+
+        _response = (
+            aws_client.logs.get_paginator("filter_log_events")
+            .paginate(logGroupName=logs_log_group)
+            .build_full_result()
+        )
+        _events.extend(_response["events"])
+
+        if len(_events) < 2:
+            raise AssertionError(
+                f"Expected at least two events in log group streams, was {_events}"
+            )
+        return _events
+
+    events = retry(_get_events, retries=5)
+
+    events.sort(key=lambda event: event["timestamp"])
+
+    snapshot.match("log-events", events)
diff --git a/tests/aws/services/events/scheduled_rules/test_events_scheduled_rules_logs.snapshot.json b/tests/aws/services/events/scheduled_rules/test_events_scheduled_rules_logs.snapshot.json
new file mode 100644
--- /dev/null
+++ b/tests/aws/services/events/scheduled_rules/test_events_scheduled_rules_logs.snapshot.json
@@ -0,0 +1,69 @@
+{
+  "tests/aws/services/events/scheduled_rules/test_events_scheduled_rules_logs.py::test_scheduled_rule_logs": {
+    "recorded-date": "05-10-2023, 19:09:18",
+    "recorded-content": {
+      "log-streams": [
+        {
+          "logStreamName": "<log-stream-name:1>",
+          "creationTime": "timestamp",
+          "firstEventTimestamp": "timestamp",
+          "lastEventTimestamp": "timestamp",
+          "lastIngestionTime": "<time>",
+          "uploadSequenceToken": "49039859562777973601797067008229305122883632487254944921",
+          "arn": "arn:aws:logs:<region>:111111111111:log-group:<log-group-name>:log-stream:<log-stream-name:1>",
+          "storedBytes": 0
+        },
+        {
+          "logStreamName": "<log-stream-name:2>",
+          "creationTime": "timestamp",
+          "firstEventTimestamp": "timestamp",
+          "lastEventTimestamp": "timestamp",
+          "lastIngestionTime": "<time>",
+          "uploadSequenceToken": "49039859562777973593821699033519811020151723440824085720",
+          "arn": "arn:aws:logs:<region>:111111111111:log-group:<log-group-name>:log-stream:<log-stream-name:2>",
+          "storedBytes": 0
+        }
+      ],
+      "log-events": [
+        {
+          "logStreamName": "<log-stream-name:1>",
+          "timestamp": "timestamp",
+          "message": {
+            "version": "0",
+            "id": "<uuid:1>",
+            "detail-type": "Scheduled Event",
+            "source": "aws.events",
+            "account": "111111111111",
+            "time": "date",
+            "region": "<region>",
+            "resources": [
+              "arn:aws:events:<region>:111111111111:rule/<rule-name>"
+            ],
+            "detail": {}
+          },
+          "ingestionTime": "timestamp",
+          "eventId": "37833787759872217972232273854937524678578329357763018752"
+        },
+        {
+          "logStreamName": "<log-stream-name:2>",
+          "timestamp": "timestamp",
+          "message": {
+            "version": "0",
+            "id": "<uuid:1>",
+            "detail-type": "Scheduled Event",
+            "source": "aws.events",
+            "account": "111111111111",
+            "time": "date",
+            "region": "<region>",
+            "resources": [
+              "arn:aws:events:<region>:111111111111:rule/<rule-name>"
+            ],
+            "detail": {}
+          },
+          "ingestionTime": "timestamp",
+          "eventId": "37833787759872217972232273854930096880382512496684171264"
+        }
+      ]
+    }
+  }
+}
diff --git a/tests/aws/services/events/scheduled_rules/test_events_scheduled_rules_sqs.py b/tests/aws/services/events/scheduled_rules/test_events_scheduled_rules_sqs.py
new file mode 100644
--- /dev/null
+++ b/tests/aws/services/events/scheduled_rules/test_events_scheduled_rules_sqs.py
@@ -0,0 +1,65 @@
+import json
+import logging
+
+from localstack.testing.aws.eventbus_utils import (
+    allow_event_rule_to_sqs_queue,
+    trigger_scheduled_rule,
+)
+from localstack.testing.pytest import markers
+from localstack.testing.snapshots.transformer_utility import TransformerUtility
+from localstack.utils.strings import short_uid
+from localstack.utils.sync import retry
+
+LOG = logging.getLogger(__name__)
+
+
+@markers.aws.validated
+def test_scheduled_rule_sqs(
+    sqs_create_queue,
+    events_put_rule,
+    aws_client,
+    snapshot,
+):
+    schedule_expression = "rate(1 minute)"
+    rule_name = f"rule-{short_uid()}"
+
+    snapshot.add_transformer(TransformerUtility.sqs_api())
+    # the generated message has a date, so the MD5 will be different every time
+    snapshot.add_transformer(snapshot.transform.key_value("MD5OfBody"))
+    snapshot.add_transformer(snapshot.transform.regex(rule_name, "<rule-name>"))
+
+    queue_url = sqs_create_queue()
+    queue_arn = aws_client.sqs.get_queue_attributes(
+        QueueUrl=queue_url, AttributeNames=["QueueArn"]
+    )["Attributes"]["QueueArn"]
+
+    rule_arn = events_put_rule(Name=rule_name, ScheduleExpression=schedule_expression)["RuleArn"]
+
+    allow_event_rule_to_sqs_queue(aws_client, queue_url, queue_arn, rule_arn)
+    aws_client.events.put_targets(
+        Rule=rule_name,
+        Targets=[
+            {"Id": "1", "Arn": queue_arn},
+            {"Id": "2", "Arn": queue_arn, "Input": json.dumps({"custom-value": "somecustominput"})},
+        ],
+    )
+
+    messages = []
+
+    trigger_scheduled_rule(rule_arn)
+
+    def _collect_sqs_messages():
+        _response = aws_client.sqs.receive_message(
+            QueueUrl=queue_url, WaitTimeSeconds=20, MaxNumberOfMessages=10
+        )
+        messages.extend(_response.get("Messages", []))
+
+        if len(messages) < 2:
+            raise AssertionError(f"Expected at least 2 messages in {messages}")
+
+    retry(_collect_sqs_messages, retries=6, sleep=0.1)
+
+    # hacky sorting of messages
+    messages.sort(key=lambda m: 1 if "custom-value" in m["Body"] else 0)
+
+    snapshot.match("sqs-messages", messages)
diff --git a/tests/aws/services/events/scheduled_rules/test_events_scheduled_rules_sqs.snapshot.json b/tests/aws/services/events/scheduled_rules/test_events_scheduled_rules_sqs.snapshot.json
new file mode 100644
--- /dev/null
+++ b/tests/aws/services/events/scheduled_rules/test_events_scheduled_rules_sqs.snapshot.json
@@ -0,0 +1,35 @@
+{
+  "tests/aws/services/events/scheduled_rules/test_events_scheduled_rules_sqs.py::test_scheduled_rule_sqs": {
+    "recorded-date": "05-10-2023, 19:18:50",
+    "recorded-content": {
+      "sqs-messages": [
+        {
+          "MessageId": "<uuid:1>",
+          "ReceiptHandle": "<receipt-handle:1>",
+          "MD5OfBody": "<m-d5-of-body:1>",
+          "Body": {
+            "version": "0",
+            "id": "<uuid:2>",
+            "detail-type": "Scheduled Event",
+            "source": "aws.events",
+            "account": "111111111111",
+            "time": "date",
+            "region": "<region>",
+            "resources": [
+              "arn:aws:events:<region>:111111111111:rule/<rule-name>"
+            ],
+            "detail": {}
+          }
+        },
+        {
+          "MessageId": "<uuid:3>",
+          "ReceiptHandle": "<receipt-handle:2>",
+          "MD5OfBody": "<m-d5-of-body:2>",
+          "Body": {
+            "custom-value": "somecustominput"
+          }
+        }
+      ]
+    }
+  }
+}
diff --git a/tests/aws/services/events/test_events.py b/tests/aws/services/events/test_events.py
--- a/tests/aws/services/events/test_events.py
+++ b/tests/aws/services/events/test_events.py
@@ -15,6 +15,7 @@
 from localstack.aws.api.lambda_ import Runtime
 from localstack.constants import TEST_AWS_ACCOUNT_ID, TEST_AWS_REGION_NAME
 from localstack.services.events.provider import _get_events_tmp_dir
+from localstack.testing.aws.eventbus_utils import allow_event_rule_to_sqs_queue
 from localstack.testing.aws.util import is_aws_cloud
 from localstack.testing.pytest import markers
 from localstack.utils.aws import arns, resources
@@ -72,76 +73,6 @@
 }
 
 
-@pytest.fixture
-def events_allow_event_rule_to_sqs_queue(aws_client):
-    def _allow_event_rule(sqs_queue_url, sqs_queue_arn, event_rule_arn) -> None:
-        # allow event rule to write to sqs queue
-        aws_client.sqs.set_queue_attributes(
-            QueueUrl=sqs_queue_url,
-            Attributes={
-                "Policy": json.dumps(
-                    {
-                        "Statement": [
-                            {
-                                "Sid": "AllowEventsToQueue",
-                                "Effect": "Allow",
-                                "Principal": {"Service": "events.amazonaws.com"},
-                                "Action": "sqs:SendMessage",
-                                "Resource": sqs_queue_arn,
-                                "Condition": {"ArnEquals": {"aws:SourceArn": event_rule_arn}},
-                            }
-                        ]
-                    }
-                )
-            },
-        )
-
-    return _allow_event_rule
-
-
-@pytest.fixture
-def events_put_rule(aws_client):
-    rules = []
-
-    def _factory(**kwargs):
-        if "Name" not in kwargs:
-            kwargs["Name"] = f"rule-{short_uid()}"
-
-        resp = aws_client.events.put_rule(**kwargs)
-        rules.append((kwargs["Name"], kwargs.get("EventBusName", "default")))
-        return resp
-
-    yield _factory
-
-    for rule, event_bus_name in rules:
-        targets_response = aws_client.events.list_targets_by_rule(
-            Rule=rule, EventBusName=event_bus_name
-        )
-        if targets := targets_response["Targets"]:
-            targets_ids = [target["Id"] for target in targets]
-            aws_client.events.remove_targets(
-                Rule=rule, EventBusName=event_bus_name, Ids=targets_ids
-            )
-        aws_client.events.delete_rule(Name=rule, EventBusName=event_bus_name)
-
-
-@pytest.fixture
-def events_create_event_bus(aws_client):
-    event_buses = []
-
-    def _factory(**kwargs):
-        if "Name" not in kwargs:
-            kwargs["Name"] = f"event-bus-{short_uid()}"
-        resp = aws_client.events.create_event_bus(**kwargs)
-        event_buses.append(kwargs["Name"])
-        return resp
-
-    yield _factory
-
-    for event_bus in event_buses:
-        aws_client.events.delete_event_bus(Name=event_bus)
-
-
 class TestEvents:
     def assert_valid_event(self, event):
         expected_fields = (
@@ -374,7 +305,7 @@ def test_put_events_with_target_sqs_event_detail_match(
         )
 
     @pytest.fixture
-    def put_events_with_filter_to_sqs(self, aws_client, clean_up):
+    def put_events_with_filter_to_sqs(self, aws_client, sqs_get_queue_arn, clean_up):
         def _put_events_with_filter_to_sqs(
             pattern: Dict,
             entries_asserts: List[Tuple[List[Dict], bool]],
@@ -387,7 +318,7 @@ def _put_events_with_filter_to_sqs(
 
             sqs_client = aws_client.sqs
             queue_url = sqs_client.create_queue(QueueName=queue_name)["QueueUrl"]
-            queue_arn = self._get_queue_arn(queue_url, sqs_client)
+            queue_arn = sqs_get_queue_arn(queue_url)
             policy = {
                 "Version": "2012-10-17",
                 "Id": f"sqs-eventbridge-{short_uid()}",
@@ -540,7 +471,9 @@ def get_message(queue_url):
 
     @markers.aws.unknown
     @pytest.mark.parametrize("strategy", ["domain", "path"])
-    def test_put_events_into_event_bus(self, monkeypatch, aws_client, clean_up, strategy):
+    def test_put_events_into_event_bus(
+        self, monkeypatch, sqs_get_queue_arn, aws_client, clean_up, strategy
+    ):
         monkeypatch.setattr(config, "SQS_ENDPOINT_STRATEGY", strategy)
 
         queue_name = "queue-{}".format(short_uid())
@@ -550,7 +483,7 @@ def test_put_events_into_event_bus(self, monkeypatch, aws_client, clean_up, stra
         bus_name_2 = "bus2-{}".format(short_uid())
 
         queue_url = aws_client.sqs.create_queue(QueueName=queue_name)["QueueUrl"]
-        queue_arn = self._get_queue_arn(queue_url, aws_client.sqs)
+        queue_arn = sqs_get_queue_arn(queue_url)
 
         aws_client.events.create_event_bus(Name=bus_name_1)
         resp = aws_client.events.create_event_bus(Name=bus_name_2)
@@ -1558,7 +1491,6 @@ def test_put_events_to_default_eventbus_for_custom_eventbus(
         sqs_get_queue_arn,
         create_role,
         create_policy,
-        events_allow_event_rule_to_sqs_queue,
         s3_bucket,
         snapshot,
         aws_client,
@@ -1647,8 +1579,11 @@ def test_put_events_to_default_eventbus_for_custom_eventbus(
         rule_on_custom_bus_arn = rule_on_custom_bus["RuleArn"]
         snapshot.match("create-rule-2", rule_on_custom_bus)
 
-        events_allow_event_rule_to_sqs_queue(
-            sqs_queue_url=queue_url, sqs_queue_arn=queue_arn, event_rule_arn=rule_on_custom_bus_arn
+        allow_event_rule_to_sqs_queue(
+            aws_client=aws_client,
+            sqs_queue_url=queue_url,
+            sqs_queue_arn=queue_arn,
+            event_rule_arn=rule_on_custom_bus_arn,
         )
 
         resp = aws_client.events.put_targets(
@@ -1681,51 +1616,13 @@ def get_message():
 
         self.assert_valid_event(received_event)
 
-    def _get_queue_arn(self, queue_url, sqs_client):
-        queue_attrs = sqs_client.get_queue_attributes(
-            QueueUrl=queue_url, AttributeNames=["QueueArn"]
-        )
-        return queue_attrs["Attributes"]["QueueArn"]
-
-    @pytest.fixture
-    def clean_up(self, aws_client):
-        def _clean_up(
-            bus_name=None,
-            rule_name=None,
-            target_ids=None,
-            queue_url=None,
-            log_group_name=None,
-        ):
-            events_client = aws_client.events
-            kwargs = {"EventBusName": bus_name} if bus_name else {}
-            if target_ids:
-                target_ids = target_ids if isinstance(target_ids, list) else [target_ids]
-                events_client.remove_targets(Rule=rule_name, Ids=target_ids, Force=True, **kwargs)
-            if rule_name:
-                events_client.delete_rule(Name=rule_name, Force=True, **kwargs)
-            if bus_name:
-                events_client.delete_event_bus(Name=bus_name)
-            if queue_url:
-                sqs_client = aws_client.sqs
-                sqs_client.delete_queue(QueueUrl=queue_url)
-            if log_group_name:
-                logs_client = aws_client.logs
-                log_streams = logs_client.describe_log_streams(logGroupName=log_group_name)
-                for log_stream in log_streams["logStreams"]:
-                    logs_client.delete_log_stream(
-                        logGroupName=log_group_name, logStreamName=log_stream["logStreamName"]
-                    )
-                logs_client.delete_log_group(logGroupName=log_group_name)
-
-        yield _clean_up
-
     @markers.aws.validated
     def test_put_target_id_validation(
-        self, sqs_create_queue, events_put_rule, snapshot, aws_client
+        self, sqs_create_queue, sqs_get_queue_arn, events_put_rule, snapshot, aws_client
     ):
         rule_name = f"rule-{short_uid()}"
         queue_url = sqs_create_queue()
-        queue_arn = self._get_queue_arn(queue_url, aws_client.sqs)
+        queue_arn = sqs_get_queue_arn(queue_url)
 
         events_put_rule(
             Name=rule_name, EventPattern=json.dumps(TEST_EVENT_PATTERN), State="ENABLED"
@@ -1822,7 +1719,6 @@ def test_put_events_nonexistent_event_bus(
         sqs_create_queue,
         sqs_get_queue_arn,
         events_put_rule,
-        events_allow_event_rule_to_sqs_queue,
         snapshot,
     ):
         default_bus_rule_name = f"rule-{short_uid()}"
@@ -1848,7 +1744,8 @@ def test_put_events_nonexistent_event_bus(
             State="ENABLED",
         )
 
-        events_allow_event_rule_to_sqs_queue(
+        allow_event_rule_to_sqs_queue(
+            aws_client=aws_client,
             event_rule_arn=rule_on_default_bus["RuleArn"],
             sqs_queue_arn=queue_arn,
             sqs_queue_url=queue_url,
@@ -1946,7 +1843,6 @@ def test_put_events_time(
         sqs_create_queue,
         sqs_get_queue_arn,
         events_put_rule,
-        events_allow_event_rule_to_sqs_queue,
         snapshot,
     ):
         default_bus_rule_name = f"rule-{short_uid()}"
@@ -1968,7 +1864,8 @@ def test_put_events_time(
             State="ENABLED",
         )
 
-        events_allow_event_rule_to_sqs_queue(
+        allow_event_rule_to_sqs_queue(
+            aws_client=aws_client,
             event_rule_arn=rule_on_default_bus["RuleArn"],
             sqs_queue_arn=queue_arn,
             sqs_queue_url=queue_url,