Projet

Général

Profil

0001-logging-add-requests-and-responses-max-size-to-conne.patch

Nicolas Roche, 02 octobre 2019 17:18

Télécharger (10,9 ko)

Voir les différences:

Subject: [PATCH] logging: add requests and responses max size to connector log
 parameters (#36596)

 .../migrations/0016_auto_20191002_1443.py     | 25 +++++++++++++++++++
 passerelle/base/models.py                     | 10 ++++++++
 passerelle/base/views.py                      |  6 ++++-
 passerelle/settings.py                        |  2 +-
 passerelle/utils/__init__.py                  | 12 +++++++--
 passerelle/utils/jsonresponse.py              |  6 ++++-
 passerelle/views.py                           |  2 +-
 tests/settings.py                             |  2 ++
 tests/test_api_access.py                      |  9 +++++--
 tests/test_requests.py                        |  9 ++++---
 10 files changed, 72 insertions(+), 11 deletions(-)
 create mode 100644 passerelle/base/migrations/0016_auto_20191002_1443.py
passerelle/base/migrations/0016_auto_20191002_1443.py
1
# -*- coding: utf-8 -*-
2
# Generated by Django 1.11.18 on 2019-10-02 12:43
3
from __future__ import unicode_literals
4

  
5
from django.db import migrations, models
6

  
7

  
8
class Migration(migrations.Migration):
9

  
10
    dependencies = [
11
        ('base', '0015_auto_20190921_0347'),
12
    ]
13

  
14
    operations = [
15
        migrations.AddField(
16
            model_name='loggingparameters',
17
            name='requests_max_size',
18
            field=models.PositiveIntegerField(default=5000, help_text='Maximum HTTP request size to log', verbose_name='Requests maximum size'),
19
        ),
20
        migrations.AddField(
21
            model_name='loggingparameters',
22
            name='responses_max_size',
23
            field=models.PositiveIntegerField(default=4096, help_text='Maximum HTTP reponse size to log', verbose_name='Responses maximum size'),
24
        ),
25
    ]
passerelle/base/models.py
592 592
        help_text=_('One address per line (empty for site administrators)'),
593 593
        blank=True
594 594
    )
595
    requests_max_size = models.PositiveIntegerField(
596
        verbose_name=_('Requests maximum size'),
597
        help_text=_('Maximum HTTP request size to log'),
598
        default=settings.LOGGED_REQUESTS_MAX_SIZE
599
    )
600
    responses_max_size = models.PositiveIntegerField(
601
        verbose_name=_('Responses maximum size'),
602
        help_text=_('Maximum HTTP reponse size to log'),
603
        default=settings.LOGGED_RESPONSES_MAX_SIZE
604
    )
595 605

  
596 606
    class Meta:
597 607
        unique_together = (('resource_type', 'resource_pk'))
passerelle/base/views.py
136 136
    def get_form_class(self):
137 137
        form_class = model_forms.modelform_factory(
138 138
            LoggingParameters,
139
            fields=['log_level', 'trace_emails'])
139
            fields=['log_level', 'trace_emails', 'requests_max_size', 'responses_max_size'])
140 140
        form_class.base_fields['trace_emails'].widget.attrs['rows'] = '3'
141 141
        return form_class
142 142

  
......
147 147
        parameters = self.get_resource().logging_parameters
148 148
        d['log_level'] = parameters.log_level
149 149
        d['trace_emails'] = parameters.trace_emails
150
        d['requests_max_size'] = parameters.requests_max_size
151
        d['responses_max_size'] = parameters.responses_max_size
150 152
        return d
151 153

  
152 154
    def get_resource(self):
......
160 162
        parameters = self.get_resource().logging_parameters
161 163
        parameters.log_level = form.cleaned_data['log_level']
162 164
        parameters.trace_emails = form.cleaned_data['trace_emails']
165
        parameters.requests_max_size = form.cleaned_data['requests_max_size']
166
        parameters.responses_max_size = form.cleaned_data['responses_max_size']
163 167
        parameters.save()
164 168
        return super(LoggingParametersUpdateView, self).form_valid(form)
165 169

  
passerelle/settings.py
212 212
LOGGED_RESPONSES_MAX_SIZE = 4096
213 213

  
214 214
# Max size of the request to log
215
LOGGED_REQUEST_MAX_SIZE = 5000
215
LOGGED_REQUESTS_MAX_SIZE = 5000
216 216

  
217 217
# Number of days to keep logs
218 218
LOG_RETENTION_DAYS = 7
passerelle/utils/__init__.py
158 158
    if logger.level == 10:  # DEBUG
159 159
        extra['request_headers'] = dict(request.headers.items())
160 160
        if request.body:
161
            extra['request_payload'] = repr(request.body[:settings.LOGGED_REQUEST_MAX_SIZE])
161
            if hasattr(logger, 'connector'):
162
                max_size = logger.connector.logging_parameters.requests_max_size
163
            else:
164
                max_size = settings.LOGGED_REQUESTS_MAX_SIZE
165
            extra['request_payload'] = repr(request.body[:max_size])
162 166
    if response is not None:
163 167
        message = message + ' (=> %s)' % response.status_code
164 168
        extra['response_status'] = response.status_code
......
166 170
            extra['response_headers'] = dict(response.headers.items())
167 171
            # log body only if content type is allowed
168 172
            if content_type_match(response.headers.get('Content-Type')):
169
                content = response.content[:settings.LOGGED_RESPONSES_MAX_SIZE]
173
                if hasattr(logger, 'connector'):
174
                    max_size = logger.connector.logging_parameters.responses_max_size
175
                else:
176
                    max_size = settings.LOGGED_RESPONSES_MAX_SIZE
177
                content = response.content[:max_size]
170 178
                extra['response_content'] = repr(content)
171 179
        if response.status_code // 100 == 3:
172 180
            log_function = logger.warning
passerelle/utils/jsonresponse.py
132 132
        except Exception as e:
133 133
            extras = {'method': req.method, 'exception': exception_to_text(e), 'request': req}
134 134
            if req.method == 'POST':
135
                extras.update({'body': repr(req.body[:settings.LOGGED_REQUEST_MAX_SIZE])})
135
                if hasattr(logger, 'connector'):
136
                    max_size = logger.connector.logging_parameters.requests_max_size
137
                else:
138
                    max_size = settings.LOGGED_REQUESTS_MAX_SIZE
139
                extras.update({'body': repr(req.body[:max_size])})
136 140
            if (not isinstance(e, (Http404, PermissionDenied, ObjectDoesNotExist, RequestException))
137 141
                    and getattr(e, 'log_error', True)):
138 142
                logger.exception("Error occurred while processing request", extra=extras)
passerelle/views.py
394 394
        connector_name, endpoint_name = kwargs['connector'], kwargs['endpoint']
395 395
        connector = self.get_object()
396 396
        url = request.get_full_path()
397
        payload = request.body[:settings.LOGGED_REQUEST_MAX_SIZE]
397
        payload = request.body[:connector.logging_parameters.requests_max_size]
398 398
        try:
399 399
            payload.decode('utf-8')
400 400
        except UnicodeDecodeError:
tests/settings.py
67 67
        },
68 68
    }
69 69
}
70

  
71
LOGGED_REQUESTS_MAX_SIZE = 4999
tests/test_api_access.py
184 184
    assert resp.json['err'] == 1
185 185
    assert resp.json['err_desc'] == 'Payload error: missing "message" in JSON payload'
186 186

  
187
def test_logged_request_max_size(app, oxyd, settings):
187
def test_logged_requests_max_size(app, oxyd, settings):
188
    assert oxyd.logging_parameters.requests_max_size == 4999
188 189
    endpoint_url = reverse('generic-endpoint',
189 190
            kwargs={'connector': 'oxyd', 'slug': oxyd.slug, 'endpoint': 'send'})
190 191
    api = ApiUser.objects.create(username='public',
......
202 203

  
203 204
    # for empty payload the connector returns an APIError with
204 205
    assert 'Error occurred' in ResourceLog.objects.all()[1].message
206
    assert ResourceLog.objects.all()[1].extra['body'] == '\'{"foo": "bar"}\''
205 207

  
206 208
    # troncate logs
207
    settings.LOGGED_REQUEST_MAX_SIZE = 6
209
    parameters = oxyd.logging_parameters
210
    parameters.requests_max_size = 6
211
    parameters.save()
208 212
    resp = app.post_json(endpoint_url, params={'foo': 'bar'})
209 213
    assert ResourceLog.objects.all()[2].message[-8:] == '"foo"\') '
214
    assert ResourceLog.objects.all()[3].extra['body'] == '\'{"foo"\''
tests/test_requests.py
105 105
            assert not hasattr(record, 'response_content')
106 106
            assert not hasattr(record, 'response_headers')
107 107

  
108
def test_log_error_request_max_size(caplog, log_level, settings):
108
def test_log_error_http_max_sizes(caplog, log_level, settings):
109 109
    url = 'https://httperror.org/plop'
110 110

  
111 111
    logger = logging.getLogger('requests')
112 112
    logger.setLevel(log_level)
113 113

  
114
    settings.LOGGED_REQUEST_MAX_SIZE = 8
114
    assert settings.LOGGED_REQUESTS_MAX_SIZE == 4999
115
    assert settings.LOGGED_RESPONSES_MAX_SIZE == 4096
116
    settings.LOGGED_REQUESTS_MAX_SIZE = 8
117
    settings.LOGGED_RESPONSES_MAX_SIZE = 7
115 118
    with HTTMock(http400_mock):
116 119
        requests = Request(logger=logger)
117 120
        response = requests.post(url, json={'name':'josh'})
118 121

  
119
    print logger.level
120 122
    if logger.level == 10:  # DEBUG
121 123
        records = [record for record in caplog.records if record.name == 'requests']
122 124
        assert records[0].request_payload == '\'{"name":\''
125
        assert records[0].response_content == '\'{"foo":\''
123 126

  
124 127

  
125 128
@pytest.fixture(params=['xml', 'whatever', 'jpeg', 'pdf'])
126
-