Projet

Général

Profil

0003-add-integrated-log-system-14191.patch

Josué Kouka, 01 mars 2017 18:37

Télécharger (15,7 ko)

Voir les différences:

Subject: [PATCH 3/3] add integrated log system (#14191)

 passerelle/base/migrations/0003_resourcelog.py     | 31 +++++++
 passerelle/base/models.py                          | 98 ++++++++++++++++++++--
 passerelle/base/templatetags/passerelle.py         | 25 +++++-
 .../passerelle/includes/resource-logs-table.html   | 50 +++++++++++
 .../templates/passerelle/manage/service_view.html  | 11 +++
 passerelle/views.py                                |  1 +
 tests/test_generic_endpoint.py                     | 82 +++++++++++++++++-
 7 files changed, 291 insertions(+), 7 deletions(-)
 create mode 100644 passerelle/base/migrations/0003_resourcelog.py
 create mode 100644 passerelle/templates/passerelle/includes/resource-logs-table.html
passerelle/base/migrations/0003_resourcelog.py
1
# -*- coding: utf-8 -*-
2
from __future__ import unicode_literals
3

  
4
from django.db import migrations, models
5
import jsonfield.fields
6

  
7

  
8
class Migration(migrations.Migration):
9

  
10
    dependencies = [
11
        ('base', '0002_auto_20151009_0326'),
12
    ]
13

  
14
    operations = [
15
        migrations.CreateModel(
16
            name='ResourceLog',
17
            fields=[
18
                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
19
                ('timestamp', models.DateTimeField(auto_now_add=True)),
20
                ('appname', models.CharField(max_length=128, null=True, verbose_name=b'appname')),
21
                ('slug', models.CharField(max_length=128, null=True, verbose_name=b'slug')),
22
                ('loglevel', models.CharField(max_length=16, verbose_name=b'log level')),
23
                ('ipsource', models.GenericIPAddressField(null=True, verbose_name='IP Address', blank=True)),
24
                ('_message', models.TextField(max_length=2048, verbose_name=b'message')),
25
                ('extra', jsonfield.fields.JSONField(default={}, verbose_name=b'extras')),
26
            ],
27
            options={
28
                'permissions': (('view_resourcelog', 'Can view resource logs'),),
29
            },
30
        ),
31
    ]
passerelle/base/models.py
1 1
import logging
2
import base64
2 3

  
3 4
from django.conf import settings
4 5
from django.core.exceptions import ValidationError, PermissionDenied
......
6 7
from django.db import models
7 8
from django.db.models import Q
8 9
from django.utils.translation import ugettext_lazy as _
9
from django.utils.text import slugify
10 10

  
11 11
from django.contrib.contenttypes.models import ContentType
12 12
from django.contrib.contenttypes import fields
13 13

  
14 14
from model_utils.managers import InheritanceManager as ModelUtilsInheritanceManager
15 15

  
16
import jsonfield
17

  
16 18
import passerelle
17 19

  
18 20
KEYTYPE_CHOICES = (
......
97 99

  
98 100
    def __init__(self, *args, **kwargs):
99 101
        super(BaseResource, self).__init__(*args, **kwargs)
100
        self.logger = logging.getLogger('passerelle.resource.%s.%s' % (
101
            slugify(unicode(self.__class__.__name__)), self.slug)
102
        )
103
        self.logger.setLevel(getattr(logging, self.log_level))
102
        self.logger = ProxyLogger(self.log_level, self.get_connector_slug(), self.slug)
104 103

  
105 104
    def __unicode__(self):
106 105
        return self.title
......
166 165

  
167 166
    def __unicode__(self):
168 167
        return '%s (on %s <%s>) (for %s)' % (self.codename, self.resource_type, self.resource_pk, self.apiuser)
168

  
169

  
170
class ResourceLog(models.Model):
171
    timestamp = models.DateTimeField(auto_now_add=True)
172
    appname = models.CharField(max_length=128, verbose_name='appname', null=True)
173
    slug = models.CharField(max_length=128, verbose_name='slug', null=True)
174
    loglevel = models.CharField(max_length=16, verbose_name='log level')
175
    ipsource = models.GenericIPAddressField(blank=True, null=True, verbose_name=_('IP Address'))
176
    _message = models.TextField(max_length=2048, verbose_name='message')
177
    extra = jsonfield.JSONField(verbose_name='extras', default={})
178

  
179
    class Meta:
180
        permissions = (
181
            ('view_resourcelog', 'Can view resource logs'),
182
        )
183

  
184
    def __unicode__(self):
185
        return '%s %s %s' % (self.timestamp, self.loglevel, self.appname, self.slug)
186

  
187
    @property
188
    def message(self):
189
        return base64.b64decode(self._message)
190

  
191
    @message.setter
192
    def message(self, value):
193
        self._message = base64.b64encode(value)
194

  
195
    def save(self, *args, **kwargs):
196
        try:
197
            base64.b64decode(self._message)
198
        except (TypeError,):
199
            self._message = base64.encode(self._message)
200

  
201
        return super(ResourceLog, self).save(*args, **kwargs)
202

  
203

  
204
class ProxyLogger(object):
205

  
206
    def __init__(self, level, appname=None, slug=None):
207
        self.appname = appname
208
        self.slug = slug
209
        if appname:
210
            logger_name = 'passerelle.resource.%s.%s' % (self.appname, self.slug)
211
        else:
212
            logger_name = 'passerelle.resource'
213

  
214
        self._logger = logging.getLogger(logger_name)
215
        self._logger.setLevel(level)
216

  
217
    def _log(self, levelname, message, *args, **kwargs):
218
        attr = {}
219
        attr['loglevel'] = levelname
220
        attr['message'] = message
221
        attr['appname'] = self.appname
222
        attr['slug'] = self.slug
223
        attr['extra'] = kwargs.get('extra', {})
224
        request = kwargs.pop('request', None)
225

  
226
        if getattr(request, 'META', None):
227
            if 'HTTP_X_FORWARDED_FOR' in request.META:
228
                ipsource = request.META.get('HTTP_X_FORWARDED_FOR', '').split(",")[0].strip()
229
            else:
230
                ipsource = request.META.get('REMOTE_ADDR')
231
        else:
232
            ipsource = None
233
        attr['ipsource'] = ipsource
234

  
235
        if self._logger.level <= getattr(logging, levelname):
236
            ResourceLog.objects.create(**attr)
237

  
238
        getattr(self._logger, levelname.lower())(message, *args, **kwargs)
239

  
240
    def debug(self, message, *args, **kwargs):
241
        self._log('DEBUG', message, *args, **kwargs)
242

  
243
    def info(self, message, *args, **kwargs):
244
        self._log('INFO', message, *args, **kwargs)
245

  
246
    def warning(self, message, *args, **kwargs):
247
        self._log('WARNING', message, *args, **kwargs)
248

  
249
    def critical(self, message, *args, **kwargs):
250
        self._log('CRITICAL', message, *args, **kwargs)
251

  
252
    def error(self, message, *args, **kwargs):
253
        self._log('ERROR', message, *args, **kwargs)
254

  
255
    def fatal(self, message, *args, **kwargs):
256
        self._log('FATAL', message, *args, **kwargs)
passerelle/base/templatetags/passerelle.py
3 3
from django import template
4 4
from django.contrib.contenttypes.models import ContentType
5 5
from django.contrib.auth import get_permission_codename
6
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
6 7

  
7 8
from passerelle.utils import get_trusted_services
8
from ..models import AccessRight
9
from ..models import AccessRight, ResourceLog
9 10

  
10 11
register = template.Library()
11 12

  
......
22 23
    return context
23 24

  
24 25

  
26
@register.inclusion_tag('passerelle/includes/resource-logs-table.html', takes_context=True)
27
def resource_logs_table(context, resource):
28
    request = context.get('request')
29
    page = request.GET.get('page', 1)
30

  
31
    connector = resource.get_connector_slug()
32
    context['connector'] = connector
33
    context['slug'] = resource.slug
34
    qs = ResourceLog.objects.filter(appname=connector, slug=resource.slug).order_by('-timestamp')
35

  
36
    paginator = Paginator(qs, 10)
37
    try:
38
        logrecords = paginator.page(page)
39
    except PageNotAnInteger:
40
        logrecords = paginator.page(1)
41
    except (EmptyPage,):
42
        logrecords = paginator.page(paginator.num_pages)
43

  
44
    context['logrecords'] = logrecords
45
    return context
46

  
47

  
25 48
@register.filter
26 49
def can_edit(obj, user):
27 50
    return user.has_perm(get_permission_codename('change', obj._meta), obj=obj)
passerelle/templates/passerelle/includes/resource-logs-table.html
1
{% load i18n passerelle %}
2
{% load tz %}
3

  
4
{% block content %}
5
{% if logrecords %}
6
<table class="main">
7
    <thead>
8
        <th>Id</th>
9
        <th>{% trans 'Timestamp' %}</th>
10
        <th>{% trans 'Level' %}</th>
11
        <th>{% trans 'Ip Source' %}</th>
12
        <th>{% trans 'Message' %}</th>
13
    </thead>
14
    <tbody>
15
    {% for record in logrecords %}
16
    <tr>
17
        <td>{{ record.id }}</td>
18
        <td>{{ record.timestamp|localtime }}</td>
19
        <td>{{ record.loglevel }}</td>
20
        <td>{{ record.ipsource }}</td>
21
        <td>{{ record.message}}</td>
22
    </tr>
23
    {% endfor %}
24
    </tbody>
25
</table>
26

  
27
{% if logrecords.has_other_pages %}
28
<p class="paginator">
29
  {% if logrecords.has_previous %}
30
      <a href="?page={{ logrecords.previous_page_number }}#logs">&lt;&lt;</a>
31
  {% else %}
32
  <span>&lt;&lt;</span>
33
  {% endif %}
34
    &nbsp;
35
    <span class="current">
36
        {{ logrecords.number }} / {{ logrecords.paginator.num_pages }}
37
    </span>
38
     &nbsp;
39
    {% if logrecords.has_next %}
40
        <a href="?page={{ logrecords.next_page_number }}#logs">&gt;&gt;</a>
41
    {% else %}
42
        <span>&gt;&gt;</span>
43
    {% endif %}
44
        </div>
45
    {% endif %}
46

  
47
{% else %}
48
<p>{% trans 'No records found' %}</p>
49
{% endif %}
50
{% endblock %}
passerelle/templates/passerelle/manage/service_view.html
48 48
{% endif %}
49 49
</div>
50 50

  
51
{% if perms.base.view_resourcelog %}
52
<div id="logs">
53
    <h3>{% trans "Logs" %}</h3>
54
    {% block logs %}
55
        {% resource_logs_table resource=object %}
56
    {% endblock %}
57
</div>
58
{% endif %}
59

  
51 60
{% endblock %}
61

  
62

  
passerelle/views.py
246 246
        payload = request.body[:5000]
247 247
        connector.logger.debug('endpoint %s %s (%r) ' %
248 248
                               (request.method, url, payload),
249
                               request=request,
249 250
                               extra={
250 251
                                   'connector': connector_name,
251 252
                                   'connector_endpoint': endpoint_name,
tests/test_generic_endpoint.py
19 19

  
20 20
import os
21 21
import json
22
import base64
22 23

  
23 24
import mock
24 25
import pytest
25 26

  
26 27
import utils
27 28

  
29
from passerelle.base.models import ResourceLog, ProxyLogger
28 30
from passerelle.contrib.mdel.models import MDEL
31
from passerelle.contrib.arcgis.models import Arcgis
29 32

  
30 33

  
31 34
@pytest.fixture
......
33 36
    return utils.setup_access_rights(MDEL.objects.create(slug='test'))
34 37

  
35 38

  
39
@pytest.fixture
40
def arcgis(db):
41
    return utils.setup_access_rights(Arcgis.objects.create(slug='test', log_level='DEBUG'))
42

  
43

  
36 44
DEMAND_STATUS = {
37 45
    'closed': True,
38 46
    'status': 'accepted',
......
57 65

  
58 66
    records = [record for record in caplog.records() if record.name == 'passerelle.resource.mdel.test']
59 67
    for record in records:
60
        assert record.module == 'views'
61 68
        assert record.levelname == 'DEBUG'
62 69
        assert record.connector == 'mdel'
63 70
        if record.connector_endpoint_method == 'POST':
......
67 74
            assert 'endpoint GET /mdel/test/status?demand_id=1-14-ILE-LA' in record.message
68 75
            assert record.connector_endpoint == 'status'
69 76
            assert record.connector_endpoint_url == '/mdel/test/status?demand_id=1-14-ILE-LA'
77

  
78

  
79
@mock.patch('passerelle.utils.LoggedRequest.get')
80
def test_proxy_logger(mocked_get, caplog, app, arcgis):
81
    payload = file(os.path.join(os.path.dirname(__file__), 'data', 'nancy_arcgis', 'sigresponse.json')).read()
82
    mocked_get.return_value = utils.FakedResponse(content=payload, status_code=200)
83

  
84
    # simple logger
85
    logger = ProxyLogger('DEBUG')
86
    logger.debug('this is a debug test')
87
    logger.info('this is an info test')
88

  
89
    assert ResourceLog.objects.count() == 2
90
    for log in ResourceLog.objects.all():
91
        if log.loglevel == 'DEBUG':
92
            assert log.message == 'this is a debug test'
93
        else:
94
            assert log.message == 'this is an info test'
95

  
96
    resp = app.get('/arcgis/test/district', {'lon': 6.172122, 'lat': 48.673836}, status=200)
97

  
98
    logger.debug('new token: %s (timeout %ss)', 'hfgjsfg=', 45)
99

  
100
    # Resource Custom DB Logger
101
    log = ResourceLog.objects.filter(appname='arcgis', slug='test').first()
102
    assert log.appname == 'arcgis'
103
    assert log.slug == 'test'
104
    assert log.loglevel == 'DEBUG'
105
    assert log.ipsource == '127.0.0.1'
106
    assert log.extra['connector'] == 'arcgis'
107
    assert log.extra['connector_endpoint'] == 'district'
108
    assert log.extra['connector_endpoint_method'] == 'GET'
109
    assert log.extra['connector_endpoint_url'] == '/arcgis/test/district?lat=48.673836&lon=6.172122'
110

  
111
    # Resource Generic Logger
112
    for record in caplog.records():
113
        if record.name != 'passerelle.resource.arcgis.test':
114
            continue
115
        assert record.levelno == 10
116
        assert record.levelname == 'DEBUG'
117
        assert record.name == 'passerelle.resource.arcgis.test'
118
        assert record.message == u"endpoint GET /arcgis/test/district?lat=48.673836&lon=6.172122 ('') "
119

  
120
    data = resp.json['data']
121
    assert data['id'] == 4
122
    assert data['text'] == 'HAUSSONVILLE / BLANDAN / MON DESERT / SAURUPT'
123

  
124
    # when changing log level
125
    ResourceLog.objects.all().delete()
126
    arcgis.log_level = 'INFO'
127
    arcgis.save()
128
    app.get('/arcgis/test/district', {'lon': 6.172122, 'lat': 48.673836}, status=200)
129
    assert ResourceLog.objects.count() == 0
130

  
131
    arcgis.logger.info('testing info log message')
132
    assert ResourceLog.objects.count() == 1
133
    log = ResourceLog.objects.first()
134
    assert log.loglevel == 'INFO'
135
    assert log.message == 'testing info log message'
136

  
137
    arcgis.logger.warning('first warning')
138
    assert ResourceLog.objects.count() == 2
139
    assert ResourceLog.objects.last().message == 'first warning'
140
    assert ResourceLog.objects.last().loglevel == 'WARNING'
141

  
142
    # when logging a file
143
    ResourceLog.objects.all().delete()
144
    filename = os.path.join(os.path.dirname(__file__), 'data', 'iparapheur_test.pdf')
145
    message = '<file_content>(%r)</file_content>' % file(filename).read()
146
    logger.debug(message)
147
    assert ResourceLog.objects.count() == 1
148
    assert ResourceLog.objects.first().loglevel == 'DEBUG'
149
    assert ResourceLog.objects.first()._message == base64.b64encode(message)
70
-