import json
import time
from random import randint
from flask import Flask, jsonify, request, make_response
from localstack.services import generic_proxy
from localstack.utils.aws import aws_stack
from localstack.constants import TEST_AWS_ACCOUNT_ID
from localstack.utils.common import to_str
from localstack.utils.analytics import event_publisher

APP_NAME = 'es_api'
API_PREFIX = '/2015-01-01'

ES_DOMAINS = {}

app = Flask(APP_NAME)


def error_response(error_type, code=400, message='Unknown error.'):
    if not message:
        if error_type == 'ResourceNotFoundException':
            message = 'Resource not found.'
        elif error_type == 'ResourceAlreadyExistsException':
            message = 'Resource already exists.'
    response = make_response(jsonify({'error': message}))
    response.headers['x-amzn-errortype'] = error_type
    return response, code


def get_domain_config_status():
    return {
        'CreationDate': '%.2f' % time.time(),
        'PendingDeletion': False,
        'State': 'Active',
        'UpdateDate': '%.2f' % time.time(),
        'UpdateVersion': randint(1, 100)
    }


def get_domain_config(domain_name):
    config_status = get_domain_config_status()
    return {
        'DomainConfig': {
            'AccessPolicies': {
                'Options': '{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"AWS":"arn:aws:iam::%s:root"},"Action":"es:*","Resource":"arn:aws:es:%s:%s:domain/%s/*"}]}' % (TEST_AWS_ACCOUNT_ID, aws_stack.get_region(), TEST_AWS_ACCOUNT_ID, domain_name),  # noqa: E501
                'Status': config_status
            },
            'AdvancedOptions': {
                'Options': {
                    'indices.fielddata.cache.size': '',
                    'rest.action.multi.allow_explicit_index': 'true'
                },
                'Status': config_status
            },
            'EBSOptions': {
                'Options': {
                    'EBSEnabled': True,
                    'EncryptionEnabled': False,
                    'Iops': 0,
                    'VolumeSize': 10,
                    'VolumeType': 'gp2'
                },
                'Status': config_status
            },
            'ElasticsearchClusterConfig': {
                'Options': {
                    'DedicatedMasterCount': 1,
                    'DedicatedMasterEnabled': True,
                    'DedicatedMasterType': 'm3.medium.elasticsearch',
                    'InstanceCount': 1,
                    'InstanceType': 'm3.medium.elasticsearch',
                    'ZoneAwarenessEnabled': False
                },
                'Status': config_status
            },
            'ElasticsearchVersion': {
                'Options': '5.3',
                'Status': config_status
            },
            'EncryptionAtRestOptions': {
                'Options': {
                    'Enabled': False,
                    'KmsKeyId': ''
                },
                'Status': config_status
            },
            'LogPublishingOptions': {
                'Options': {
                    'INDEX_SLOW_LOGS': {
                        'CloudWatchLogsLogGroupArn': 'arn:aws:logs:%s:%s:log-group:sample-domain' % (aws_stack.get_region(), TEST_AWS_ACCOUNT_ID),  # noqa: E501
                        'Enabled': False
                    },
                    'SEARCH_SLOW_LOGS': {
                        'CloudWatchLogsLogGroupArn': 'arn:aws:logs:%s:%s:log-group:sample-domain' % (aws_stack.get_region(), TEST_AWS_ACCOUNT_ID),  # noqa: E501
                        'Enabled': False,
                    }
                },
                'Status': config_status
            },
            'SnapshotOptions': {
                'Options': {
                    'AutomatedSnapshotStartHour': randint(0, 23)
                },
                'Status': config_status
            },
            'VPCOptions': {
                'Options': {
                    'AvailabilityZones': [
                        'us-east-1b'
                    ],
                    'SecurityGroupIds': [
                        'sg-12345678'
                    ],
                    'SubnetIds': [
                        'subnet-12345678'
                    ],
                    'VPCId': 'vpc-12345678'
                },
                'Status': config_status
            }
        }
    }


def get_domain_status(domain_name, deleted=False):
    return {
        'DomainStatus': {
            'ARN': 'arn:aws:es:%s:%s:domain/%s' % (aws_stack.get_region(), TEST_AWS_ACCOUNT_ID, domain_name),
            'Created': True,
            'Deleted': deleted,
            'DomainId': '%s/%s' % (TEST_AWS_ACCOUNT_ID, domain_name),
            'DomainName': domain_name,
            'ElasticsearchClusterConfig': {
                'DedicatedMasterCount': 1,
                'DedicatedMasterEnabled': True,
                'DedicatedMasterType': 'm3.medium.elasticsearch',
                'InstanceCount': 1,
                'InstanceType': 'm3.medium.elasticsearch',
                'ZoneAwarenessEnabled': False
            },
            'ElasticsearchVersion': '6.7',
            'Endpoint': aws_stack.get_elasticsearch_endpoint(domain_name),
            'Processing': False,
            'EBSOptions': {
                'EBSEnabled': True,
                'VolumeType': 'gp2',
                'VolumeSize': 10,
                'Iops': 0
            },
        }
    }


@app.route('%s/domain' % API_PREFIX, methods=['GET'])
def list_domain_names():
    result = {
        'DomainNames': [{'DomainName': name} for name in ES_DOMAINS.keys()]
    }
    return jsonify(result)


@app.route('%s/es/domain' % API_PREFIX, methods=['POST'])
def create_domain():
    data = json.loads(to_str(request.data))
    domain_name = data['DomainName']
    if domain_name in ES_DOMAINS:
        return error_response(error_type='ResourceAlreadyExistsException')
    ES_DOMAINS[domain_name] = data
    result = get_domain_status(domain_name)
    # record event
    event_publisher.fire_event(event_publisher.EVENT_ES_CREATE_DOMAIN,
        payload={'n': event_publisher.get_hash(domain_name)})
    return jsonify(result)


@app.route('%s/es/domain/<domain_name>' % API_PREFIX, methods=['GET'])
def describe_domain(domain_name):
    if domain_name not in ES_DOMAINS:
        return error_response(error_type='ResourceNotFoundException')
    result = get_domain_status(domain_name)
    return jsonify(result)


@app.route('%s/es/domain/<domain_name>/config' % API_PREFIX, methods=['GET', 'POST'])
def domain_config(domain_name):
    config = get_domain_config(domain_name)
    return jsonify(config)


@app.route('%s/es/domain/<domain_name>' % API_PREFIX, methods=['DELETE'])
def delete_domain(domain_name):
    if domain_name not in ES_DOMAINS:
        return error_response(error_type='ResourceNotFoundException')
    result = get_domain_status(domain_name, deleted=True)
    ES_DOMAINS.pop(domain_name)
    # record event
    event_publisher.fire_event(event_publisher.EVENT_ES_DELETE_DOMAIN,
        payload={'n': event_publisher.get_hash(domain_name)})
    return jsonify(result)


@app.route('%s/tags' % API_PREFIX, methods=['GET', 'POST'])
def add_list_tags():
    if request.method == 'GET' and request.args.get('arn'):
        response = {
            'TagList': [
                {
                    'Key': 'Example1',
                    'Value': 'Value'
                },
                {
                    'Key': 'Example2',
                    'Value': 'Value'
                }
            ]
        }
        return jsonify(response)

    return jsonify({})


def serve(port, quiet=True):
    generic_proxy.serve_flask_app(app=app, port=port, quiet=quiet)

localstack/services/es/es_api.py
import json
import time
from random import randint
from flask import Flask, jsonify, request, make_response
from localstack.services import generic_proxy
from localstack.utils.aws import aws_stack
from localstack.constants import TEST_AWS_ACCOUNT_ID
from localstack.utils.common import to_str
from localstack.utils.analytics import event_publisher

APP_NAME = 'es_api'
API_PREFIX = '/2015-01-01'

ES_DOMAINS = {}

app = Flask(APP_NAME)
app.url_map.strict_slashes = False


def error_response(error_type, code=400, message='Unknown error.'):
    if not message:
        if error_type == 'ResourceNotFoundException':
            message = 'Resource not found.'
        elif error_type == 'ResourceAlreadyExistsException':
            message = 'Resource already exists.'
    response = make_response(jsonify({'error': message}))
    response.headers['x-amzn-errortype'] = error_type
    return response, code


def get_domain_config_status():
    return {
        'CreationDate': '%.2f' % time.time(),
        'PendingDeletion': False,
        'State': 'Active',
        'UpdateDate': '%.2f' % time.time(),
        'UpdateVersion': randint(1, 100)
    }


def get_domain_config(domain_name):
    config_status = get_domain_config_status()
    return {
        'DomainConfig': {
            'AccessPolicies': {
                'Options': '{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"AWS":"arn:aws:iam::%s:root"},"Action":"es:*","Resource":"arn:aws:es:%s:%s:domain/%s/*"}]}' % (TEST_AWS_ACCOUNT_ID, aws_stack.get_region(), TEST_AWS_ACCOUNT_ID, domain_name),  # noqa: E501
                'Status': config_status
            },
            'AdvancedOptions': {
                'Options': {
                    'indices.fielddata.cache.size': '',
                    'rest.action.multi.allow_explicit_index': 'true'
                },
                'Status': config_status
            },
            'EBSOptions': {
                'Options': {
                    'EBSEnabled': True,
                    'EncryptionEnabled': False,
                    'Iops': 0,
                    'VolumeSize': 10,
                    'VolumeType': 'gp2'
                },
                'Status': config_status
            },
            'ElasticsearchClusterConfig': {
                'Options': {
                    'DedicatedMasterCount': 1,
                    'DedicatedMasterEnabled': True,
                    'DedicatedMasterType': 'm3.medium.elasticsearch',
                    'InstanceCount': 1,
                    'InstanceType': 'm3.medium.elasticsearch',
                    'ZoneAwarenessEnabled': False
                },
                'Status': config_status
            },
            'ElasticsearchVersion': {
                'Options': '5.3',
                'Status': config_status
            },
            'EncryptionAtRestOptions': {
                'Options': {
                    'Enabled': False,
                    'KmsKeyId': ''
                },
                'Status': config_status
            },
            'LogPublishingOptions': {
                'Options': {
                    'INDEX_SLOW_LOGS': {
                        'CloudWatchLogsLogGroupArn': 'arn:aws:logs:%s:%s:log-group:sample-domain' % (aws_stack.get_region(), TEST_AWS_ACCOUNT_ID),  # noqa: E501
                        'Enabled': False
                    },
                    'SEARCH_SLOW_LOGS': {
                        'CloudWatchLogsLogGroupArn': 'arn:aws:logs:%s:%s:log-group:sample-domain' % (aws_stack.get_region(), TEST_AWS_ACCOUNT_ID),  # noqa: E501
                        'Enabled': False,
                    }
                },
                'Status': config_status
            },
            'SnapshotOptions': {
                'Options': {
                    'AutomatedSnapshotStartHour': randint(0, 23)
                },
                'Status': config_status
            },
            'VPCOptions': {
                'Options': {
                    'AvailabilityZones': [
                        'us-east-1b'
                    ],
                    'SecurityGroupIds': [
                        'sg-12345678'
                    ],
                    'SubnetIds': [
                        'subnet-12345678'
                    ],
                    'VPCId': 'vpc-12345678'
                },
                'Status': config_status
            }
        }
    }


def get_domain_status(domain_name, deleted=False):
    return {
        'DomainStatus': {
            'ARN': 'arn:aws:es:%s:%s:domain/%s' % (aws_stack.get_region(), TEST_AWS_ACCOUNT_ID, domain_name),
            'Created': True,
            'Deleted': deleted,
            'DomainId': '%s/%s' % (TEST_AWS_ACCOUNT_ID, domain_name),
            'DomainName': domain_name,
            'ElasticsearchClusterConfig': {
                'DedicatedMasterCount': 1,
                'DedicatedMasterEnabled': True,
                'DedicatedMasterType': 'm3.medium.elasticsearch',
                'InstanceCount': 1,
                'InstanceType': 'm3.medium.elasticsearch',
                'ZoneAwarenessEnabled': False
            },
            'ElasticsearchVersion': '6.7',
            'Endpoint': aws_stack.get_elasticsearch_endpoint(domain_name),
            'Processing': False,
            'EBSOptions': {
                'EBSEnabled': True,
                'VolumeType': 'gp2',
                'VolumeSize': 10,
                'Iops': 0
            },
        }
    }


@app.route('%s/domain' % API_PREFIX, methods=['GET'])
def list_domain_names():
    result = {
        'DomainNames': [{'DomainName': name} for name in ES_DOMAINS.keys()]
    }
    return jsonify(result)


@app.route('%s/es/domain' % API_PREFIX, methods=['POST'])
def create_domain():
    data = json.loads(to_str(request.data))
    domain_name = data['DomainName']
    if domain_name in ES_DOMAINS:
        return error_response(error_type='ResourceAlreadyExistsException')
    ES_DOMAINS[domain_name] = data
    result = get_domain_status(domain_name)
    # record event
    event_publisher.fire_event(event_publisher.EVENT_ES_CREATE_DOMAIN,
        payload={'n': event_publisher.get_hash(domain_name)})
    return jsonify(result)


@app.route('%s/es/domain/<domain_name>' % API_PREFIX, methods=['GET'])
def describe_domain(domain_name):
    if domain_name not in ES_DOMAINS:
        return error_response(error_type='ResourceNotFoundException')
    result = get_domain_status(domain_name)
    return jsonify(result)


@app.route('%s/es/domain/<domain_name>/config' % API_PREFIX, methods=['GET', 'POST'])
def domain_config(domain_name):
    config = get_domain_config(domain_name)
    return jsonify(config)


@app.route('%s/es/domain/<domain_name>' % API_PREFIX, methods=['DELETE'])
def delete_domain(domain_name):
    if domain_name not in ES_DOMAINS:
        return error_response(error_type='ResourceNotFoundException')
    result = get_domain_status(domain_name, deleted=True)
    ES_DOMAINS.pop(domain_name)
    # record event
    event_publisher.fire_event(event_publisher.EVENT_ES_DELETE_DOMAIN,
        payload={'n': event_publisher.get_hash(domain_name)})
    return jsonify(result)


@app.route('%s/tags' % API_PREFIX, methods=['GET', 'POST'])
def add_list_tags():
    if request.method == 'GET' and request.args.get('arn'):
        response = {
            'TagList': [
                {
                    'Key': 'Example1',
                    'Value': 'Value'
                },
                {
                    'Key': 'Example2',
                    'Value': 'Value'
                }
            ]
        }
        return jsonify(response)

    return jsonify({})


def serve(port, quiet=True):
    generic_proxy.serve_flask_app(app=app, port=port, quiet=quiet)

localstack/services/es/es_api.py
diff --git a/Makefile b/Makefile
index 30b5c73351da5..263e9237bc65d 100644
--- a/Makefile
+++ b/Makefile
@@ -97,7 +97,7 @@ test:              ## Run automated tests
 		($(VENV_RUN); DEBUG=$(DEBUG) PYTHONPATH=`pwd` nosetests --with-timer --with-coverage --logging-level=WARNING --nocapture --no-skip --exe --cover-erase --cover-tests --cover-inclusive --cover-package=localstack --with-xunit --exclude='$(VENV_DIR).*' --ignore-files='lambda_python3.py' $(TEST_PATH))
 
 test-java:         ## Run tests for Java/JUnit compatibility
-	cd localstack/ext/java; USE_SSL=1 SERVICES=serverless,kinesis,sns,sqs mvn $(MVN_ARGS) -q test
+	cd localstack/ext/java; USE_SSL=1 SERVICES=serverless,kinesis,sns,sqs,cloudwatch mvn $(MVN_ARGS) -q test
 
 prepare-java-tests-if-changed:
 	@(! (git log -n 1 --no-merges --raw | grep localstack/ext/java/)) || (\
diff --git a/localstack/ext/java/src/main/java/cloud/localstack/Localstack.java b/localstack/ext/java/src/main/java/cloud/localstack/Localstack.java
index 234529ec20b2e..6a88a02532bac 100644
--- a/localstack/ext/java/src/main/java/cloud/localstack/Localstack.java
+++ b/localstack/ext/java/src/main/java/cloud/localstack/Localstack.java
@@ -160,6 +160,10 @@ public String getEndpointRedshift() {
         return endpointForService(ServiceName.REDSHIFT);
     }
 
+    public String getEndpointCloudWatch() {
+        return endpointForService(ServiceName.CLOUDWATCH);
+    }
+
     public String getEndpointSES() {
         return endpointForService(ServiceName.SES);
     }
@@ -172,10 +176,6 @@ public String getEndpointCloudFormation() {
         return endpointForService(ServiceName.CLOUDFORMATION);
     }
 
-    public String getEndpointCloudWatch() {
-        return endpointForService(ServiceName.CLOUDWATCH);
-    }
-
     public String getEndpointSSM() {
         return endpointForService(ServiceName.SSM);
     }
diff --git a/localstack/ext/java/src/main/java/cloud/localstack/TestUtils.java b/localstack/ext/java/src/main/java/cloud/localstack/TestUtils.java
index 429fa848a8694..d822e2fc23a0e 100644
--- a/localstack/ext/java/src/main/java/cloud/localstack/TestUtils.java
+++ b/localstack/ext/java/src/main/java/cloud/localstack/TestUtils.java
@@ -6,6 +6,8 @@
 import com.amazonaws.auth.BasicAWSCredentials;
 import com.amazonaws.client.builder.AwsClientBuilder;
 import com.amazonaws.client.builder.ExecutorFactory;
+import com.amazonaws.services.cloudwatch.AmazonCloudWatch;
+import com.amazonaws.services.cloudwatch.AmazonCloudWatchClientBuilder;
 import com.amazonaws.services.dynamodbv2.AmazonDynamoDB;
 import com.amazonaws.services.dynamodbv2.AmazonDynamoDBClientBuilder;
 import com.amazonaws.services.dynamodbv2.AmazonDynamoDBStreams;
@@ -162,6 +164,12 @@ public static AmazonKinesisAsync getClientKinesisAsync(final ExecutorFactory exe
                 withCredentials(getCredentialsProvider()).build();
     }
 
+    public static AmazonCloudWatch getClientCloudWatch() {
+        return AmazonCloudWatchClientBuilder.standard().
+                withEndpointConfiguration(getEndpointConfigurationCloudWatch()).
+                withCredentials(getCredentialsProvider()).build();
+    }
+
     protected static AwsClientBuilder.EndpointConfiguration getEndpointConfigurationLambda() {
         return getEndpointConfiguration(Localstack.INSTANCE.getEndpointLambda());
     }
@@ -190,6 +198,10 @@ protected static AwsClientBuilder.EndpointConfiguration getEndpointConfiguration
         return getEndpointConfiguration(Localstack.INSTANCE.getEndpointSNS());
     }
 
+    protected static AwsClientBuilder.EndpointConfiguration getEndpointConfigurationCloudWatch() {
+        return getEndpointConfiguration(Localstack.INSTANCE.getEndpointCloudWatch());
+    }
+
     protected static AwsClientBuilder.EndpointConfiguration getEndpointConfigurationSecretsManager() {
         return getEndpointConfiguration(Localstack.INSTANCE.getEndpointSecretsmanager());
     }
diff --git a/localstack/ext/java/src/test/java/cloud/localstack/docker/BasicDockerFunctionalityTest.java b/localstack/ext/java/src/test/java/cloud/localstack/docker/BasicDockerFunctionalityTest.java
index 221ecb4aa1d71..9d32d809427ca 100644
--- a/localstack/ext/java/src/test/java/cloud/localstack/docker/BasicDockerFunctionalityTest.java
+++ b/localstack/ext/java/src/test/java/cloud/localstack/docker/BasicDockerFunctionalityTest.java
@@ -7,6 +7,8 @@
 import com.amazon.sqs.javamessaging.SQSConnection;
 import com.amazon.sqs.javamessaging.SQSConnectionFactory;
 import com.amazonaws.auth.AWSStaticCredentialsProvider;
+import com.amazonaws.services.cloudwatch.*;
+import com.amazonaws.services.cloudwatch.model.*;
 import com.amazonaws.services.dynamodbv2.AmazonDynamoDB;
 import com.amazonaws.services.dynamodbv2.model.AttributeDefinition;
 import com.amazonaws.services.dynamodbv2.model.CreateTableRequest;
@@ -33,6 +35,7 @@
 import org.assertj.core.api.Assertions;
 import org.junit.jupiter.api.extension.ExtendWith;
 import org.junit.runner.RunWith;
+import org.junit.Assert;
 
 import javax.jms.MessageConsumer;
 import javax.jms.MessageProducer;
@@ -171,6 +174,30 @@ public void testSQS() throws Exception {
         Assertions.assertThat(received.getText()).isEqualTo("Hello World!");
     }
 
+    @org.junit.Test
+    @org.junit.jupiter.api.Test
+    public void testCloudWatch() throws Exception {
+        AmazonCloudWatch client = TestUtils.getClientCloudWatch();
+        Dimension dimension = new Dimension()
+            .withName("UNIQUE_PAGES")
+            .withValue("URLS");
+        MetricDatum datum = new MetricDatum()
+            .withMetricName("PAGES_VISITED")
+            .withUnit(StandardUnit.None)
+            .withDimensions(dimension);
+        PutMetricDataRequest request = new PutMetricDataRequest()
+            .withNamespace("SITE/TRAFFIC")
+            .withMetricData(datum);
+        // assert no error gets thrown for null values
+        datum.setValue(null);
+        PutMetricDataResult response = client.putMetricData(request);
+        Assert.assertNotNull(response);
+        // assert success for double values
+        datum.setValue(123.4);
+        response = client.putMetricData(request);
+        Assert.assertNotNull(response);
+    }
+
     private SQSConnection createSQSConnection() throws Exception {
         SQSConnectionFactory connectionFactory = SQSConnectionFactory.builder().withEndpoint(
                 Localstack.INSTANCE.getEndpointSQS()).withAWSCredentialsProvider(
diff --git a/localstack/services/es/es_api.py b/localstack/services/es/es_api.py
index f56fed16d6bd9..2dd192508dc5d 100644
--- a/localstack/services/es/es_api.py
+++ b/localstack/services/es/es_api.py
@@ -14,6 +14,7 @@
 ES_DOMAINS = {}
 
 app = Flask(APP_NAME)
+app.url_map.strict_slashes = False
 
 
 def error_response(error_type, code=400, message='Unknown error.'):
diff --git a/tests/integration/test_cloudwatch.py b/tests/integration/test_cloudwatch.py
new file mode 100644
index 0000000000000..b81591e4f3b99
--- /dev/null
+++ b/tests/integration/test_cloudwatch.py
@@ -0,0 +1,35 @@
+import unittest
+from localstack.utils.aws import aws_stack
+
+
+class CloudWatchTest(unittest.TestCase):
+
+    def test_put_metric_data(self):
+        client = aws_stack.connect_to_service('cloudwatch')
+
+        data = [
+            {
+                'MetricName': 'm1',
+                'Dimensions': [{
+                    'Name': 'foo',
+                    'Value': 'bar'
+                }],
+                'Value': 123.45,
+                'StatisticValues': {
+                    'SampleCount': 123.0,
+                    'Sum': 123.0,
+                    'Minimum': 123.0,
+                    'Maximum': 123.0
+                },
+                'Values': [
+                    123.0,
+                ],
+                'Counts': [
+                    123.0,
+                ],
+                'Unit': 'Seconds',
+                'StorageResolution': 123
+            },
+        ]
+        response = client.put_metric_data(Namespace='string', MetricData=data)
+        self.assertEquals(response['ResponseMetadata']['HTTPStatusCode'], 200)
diff --git a/tests/integration/test_logs.py b/tests/integration/test_logs.py
new file mode 100644
index 0000000000000..9b04fef8ff788
--- /dev/null
+++ b/tests/integration/test_logs.py
@@ -0,0 +1,27 @@
+# -*- coding: utf-8 -*-
+
+import unittest
+from localstack.utils.aws import aws_stack
+from localstack.utils.common import short_uid
+
+
+class CloudWatchLogsTest(unittest.TestCase):
+
+    def test_put_events_multibyte_msg(self):
+        client = aws_stack.connect_to_service('logs')
+
+        group = 'g-%s' % short_uid()
+        stream = 's-%s' % short_uid()
+        response = client.create_log_group(logGroupName=group)
+        self.assertEquals(response['ResponseMetadata']['HTTPStatusCode'], 200)
+        response = client.create_log_stream(logGroupName=group, logStreamName=stream)
+        self.assertEquals(response['ResponseMetadata']['HTTPStatusCode'], 200)
+
+        # send message with non-ASCII (multi-byte) chars
+        body_msg = '🙀 - 参よ'
+        events = [{
+            'timestamp': 1234567,
+            'message': body_msg
+        }]
+        response = client.put_log_events(logGroupName=group, logStreamName=stream, logEvents=events)
+        self.assertEquals(response['ResponseMetadata']['HTTPStatusCode'], 200)

Elasticsearch domain managed by Terraform cannot be updated; request for /tags/? returns 404
<!-- Love localstack? Please consider supporting our collective:
👉  https://opencollective.com/localstack/donate -->

We use Terraform to create and update resources in Localstack, which has worked for services like S3 and Dynamo so far.

We hit an issue with Elasticsearch domains, where the domain is created successfully but Terraform fails to apply in subsequent runs, when it makes a request to:

```
logs: ---[ REQUEST POST-SIGN ]-----------------------------
logs: GET /2015-01-01/tags/?arn=arn%3Aaws%3Aes%3Aus-east-1%3A000000000000%3Adomain%2Fepdam-local-amd HTTP/1.1
logs: Host: localhost:4578
logs: User-Agent: aws-sdk-go/1.14.31 (go1.9.2; darwin; amd64) APN/1.0 HashiCorp/1.0 Terraform/0.11.8-dev
logs: Authorization: AWS4-HMAC-SHA256 Credential=mock_access_key/20190221/us-west-2/es/aws4_request, SignedHeaders=host;x-amz-date, Signature=26f42429e2af2240466635ab9202c8888617afe9be7b8ef91a8831d6b4160bd1
logs: X-Amz-Date: 20190221T191447Z
logs: Accept-Encoding: gzip
```

and the response is:

```
logs: ---[ RESPONSE ]--------------------------------------
logs: HTTP/1.0 404 NOT FOUND
logs: Connection: close
logs: Content-Length: 233
logs: Access-Control-Allow-Origin: *
logs: Content-Type: text/html
logs: Date: Thu, 21 Feb 2019 19:14:47 GMT
logs: Server: Werkzeug/0.14.1 Python/2.7.15
```

While a request to `localhost:4578/2015-01-01/tags/?arn=...` gets 404, a request to `localhost:4578/2015-01-01/tags?arn=...`, (without the `/` before the query params), is successful.

The reason we are reporting this against Localstack and not [terraform](https://github.com/hashicorp/terraform) or [terraform-provider-aws](https://github.com/terraform-providers/terraform-provider-aws) is that the AWS REST API apparently supports requests with slashes before query parameters, or else Terraform could not be used to manage Elasticsearch domains in AWS.
