Repository: localstack/localstack

Issue:
Incorrect response content-type header from cloudwatch logs
When using the .NET AWSSDK connected to localstack and querying cloudwatch logs e.g. 
 var logClient = new AmazonCloudWatchLogsClient(new AmazonCloudWatchLogsConfig()
            {
                ServiceURL = "http://localhost:4586",
                UseHttp = true,
                AuthenticationRegion = "eu-central-1",
            });
            var logGroupName = @"/aws/lambda/f1";
            var events = logClient.FilterLogEventsAsync(new FilterLogEventsRequest()
            {
                LogGroupName = logGroupName,
            }).GetAwaiter().GetResult();

The response is returned from the server but cannot be parsed by the SDK client code because the response's content-type header is text/html when it should be application/x-amz-json-1.1.  This confirmed using Fiddler traces comparing the response from localstack to the response from AWS in the cloud. Can this be fixed easily?


Discussion:

Resolving patch:
diff --git a/localstack/services/logs/logs_listener.py b/localstack/services/logs/logs_listener.py
--- a/localstack/services/logs/logs_listener.py
+++ b/localstack/services/logs/logs_listener.py
@@ -1,11 +1,11 @@
 import re
 from requests.models import Request
 from localstack.utils.common import to_str
+from localstack.constants import APPLICATION_AMZ_JSON_1_1
 from localstack.services.generic_proxy import ProxyListener
 
 
 class ProxyListenerCloudWatchLogs(ProxyListener):
-
     def forward_request(self, method, path, data, headers):
         if method == 'POST' and path == '/':
             if 'nextToken' in to_str(data or ''):
@@ -16,17 +16,22 @@ def forward_request(self, method, path, data, headers):
         return True
 
     def return_response(self, method, path, data, headers, response):
+        # Fix Incorrect response content-type header from cloudwatch logs #1343
+        response.headers['content-type'] = APPLICATION_AMZ_JSON_1_1
+
         if 'nextToken' in to_str(response.content or ''):
             self._fix_next_token_response(response)
             response.headers['content-length'] = str(len(response._content))
 
-    def _fix_next_token_request(self, data):
+    @staticmethod
+    def _fix_next_token_request(data):
         # Fix for https://github.com/localstack/localstack/issues/1527
         pattern = r'"nextToken":\s*"([0-9]+)"'
         replacement = r'"nextToken": \1'
         return re.sub(pattern, replacement, to_str(data))
 
-    def _fix_next_token_response(self, response):
+    @staticmethod
+    def _fix_next_token_response(response):
         # Fix for https://github.com/localstack/localstack/issues/1527
         pattern = r'"nextToken":\s*([0-9]+)'
         replacement = r'"nextToken": "\1"'


Test patch:
diff --git a/tests/integration/test_logs.py b/tests/integration/test_logs.py
--- a/tests/integration/test_logs.py
+++ b/tests/integration/test_logs.py
@@ -1,30 +1,64 @@
 # -*- coding: utf-8 -*-
 
 import unittest
+
+from localstack.constants import APPLICATION_AMZ_JSON_1_1
 from localstack.utils.aws import aws_stack
 from localstack.utils.common import short_uid
 
 
 class CloudWatchLogsTest(unittest.TestCase):
+    def setUp(self):
+        self.logs_client = aws_stack.connect_to_service('logs')
 
-    def test_put_events_multibyte_msg(self):
-        client = aws_stack.connect_to_service('logs')
-
+    def test_put_events_multi_bytes_msg(self):
         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)
+
+        response = self.logs_client.create_log_group(logGroupName=group)
+        self.assertEqual(response['ResponseMetadata']['HTTPStatusCode'], 200)
+
+        response = self.logs_client.create_log_stream(logGroupName=group, logStreamName=stream)
+        self.assertEqual(response['ResponseMetadata']['HTTPStatusCode'], 200)
 
         # send message with non-ASCII (multi-byte) chars
         body_msg = '🙀 - 参よ - 日本語'
         events = [{
-            'timestamp': 1234567,
+            'timestamp': 1546300800,
             'message': body_msg
         }]
-        response = client.put_log_events(logGroupName=group, logStreamName=stream, logEvents=events)
-        self.assertEquals(response['ResponseMetadata']['HTTPStatusCode'], 200)
+        response = self.logs_client.put_log_events(logGroupName=group, logStreamName=stream, logEvents=events)
+        self.assertEqual(response['ResponseMetadata']['HTTPStatusCode'], 200)
+
+        events = self.logs_client.get_log_events(logGroupName=group, logStreamName=stream)['events']
+        self.assertEqual(events[0]['message'], body_msg)
+
+        # clean up
+        self.logs_client.delete_log_group(
+            logGroupName=group
+        )
+
+    def test_filter_log_events_response_header(self):
+        group = 'lg-%s' % short_uid()
+        stream = 'ls-%s' % short_uid()
+
+        self.logs_client.create_log_group(logGroupName=group)
+        self.logs_client.create_log_stream(logGroupName=group, logStreamName=stream)
+
+        events = [
+            {'timestamp': 1585902800, 'message': 'log message 1'},
+            {'timestamp': 1585902961, 'message': 'log message 2'}
+        ]
+        self.logs_client.put_log_events(logGroupName=group, logStreamName=stream, logEvents=events)
+
+        rs = self.logs_client.filter_log_events(
+            logGroupName=group
+        )
+
+        self.assertEqual(rs['ResponseMetadata']['HTTPStatusCode'], 200)
+        self.assertEqual(rs['ResponseMetadata']['HTTPHeaders']['content-type'], APPLICATION_AMZ_JSON_1_1)
 
-        events = client.get_log_events(logGroupName=group, logStreamName=stream)['events']
-        self.assertEquals(events[0]['message'], body_msg)
+        # clean up
+        self.logs_client.delete_log_group(
+            logGroupName=group
+        )