Projet

Général

Profil

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

Josué Kouka, 18 janvier 2017 16:24

Télécharger (19,3 ko)

Voir les différences:

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

 passerelle/base/migrations/0003_resourcelog.py     | 32 ++++++++
 passerelle/base/models.py                          | 92 ++++++++++++++++++++--
 passerelle/base/urls.py                            |  3 +
 passerelle/settings.py                             |  4 +-
 .../templates/passerelle/manage/log_detail.html    | 39 +++++++++
 .../templates/passerelle/manage/log_view.html      | 58 ++++++++++++++
 .../templates/passerelle/manage/service_view.html  |  3 +
 passerelle/urls.py                                 | 12 ++-
 passerelle/views.py                                | 26 +++++-
 tests/test_generic_endpoint.py                     | 75 +++++++++++++++++-
 10 files changed, 333 insertions(+), 11 deletions(-)
 create mode 100644 passerelle/base/migrations/0003_resourcelog.py
 create mode 100644 passerelle/templates/passerelle/manage/log_detail.html
 create mode 100644 passerelle/templates/passerelle/manage/log_view.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
                ('connector', models.CharField(max_length=128, null=True, verbose_name=b'connector')),
21
                ('appname', models.CharField(max_length=128, null=True, verbose_name=b'appname')),
22
                ('slug', models.CharField(max_length=128, null=True, verbose_name=b'slug')),
23
                ('loglevel', models.CharField(max_length=16, verbose_name=b'log level')),
24
                ('ipsource', models.GenericIPAddressField(null=True, verbose_name='IP Address', blank=True)),
25
                ('message', models.TextField(max_length=2048, verbose_name=b'message')),
26
                ('extra', jsonfield.fields.JSONField(default={}, verbose_name=b'extras')),
27
            ],
28
            options={
29
                'permissions': (('view_resourcelog', 'Can view resource logs'),),
30
            },
31
        ),
32
    ]
passerelle/base/models.py
6 6
from django.db import models
7 7
from django.db.models import Q
8 8
from django.utils.translation import ugettext_lazy as _
9
from django.utils.text import slugify
10 9

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

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

  
15
import jsonfield
16

  
16 17
import passerelle
17 18

  
18 19
KEYTYPE_CHOICES = (
......
97 98

  
98 99
    def __init__(self, *args, **kwargs):
99 100
        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))
101
        self.logger = ProxyLogger(self.log_level, self.get_connector_slug(), self.slug)
104 102

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

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

  
168

  
169
class ResourceLog(models.Model):
170
    timestamp = models.DateTimeField(auto_now_add=True)
171
    connector = models.CharField(max_length=128, verbose_name='connector', null=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.connector)
186

  
187

  
188
class ProxyLogger(object):
189

  
190
    def __init__(self, level, appname=None, slug=None):
191
        self.appname = appname
192
        self.slug = slug
193
        self.levelname = level
194
        if appname:
195
            logger_name = 'passerelle.resource.%s.%s' % (self.appname, self.slug)
196
        else:
197
            logger_name = 'passerelle.resource'
198

  
199
        self._logger = logging.getLogger(logger_name)
200
        self._logger.setLevel(level)
201

  
202
    def _log(self, levelname, message, *args, **kwargs):
203
        attr = {}
204
        attr['loglevel'] = levelname
205
        attr['message'] = message
206
        attr['appname'] = self.appname
207
        attr['slug'] = self.slug
208
        attr['extra'] = kwargs.get('extra', {})
209
        request = kwargs.pop('request', None)
210

  
211
        if getattr(request, 'META', None):
212
            if 'HTTP_X_FORWARDED_FOR' in request.META:
213
                ipsource = request.META.get('HTTP_X_FORWARDED_FOR', '').split(",")[0].strip()
214
            else:
215
                ipsource = request.META.get('REMOTE_ADDR')
216
        else:
217
            ipsource = None
218

  
219
        if self.appname and self.slug:
220
            connector = '%s-%s' % (self.appname, self.slug)
221
        else:
222
            connector = None
223

  
224
        attr['ipsource'] = ipsource
225
        attr['connector'] = connector
226

  
227
        # Resource Custom DB Loggger
228
        if self._logger.level <= getattr(logging, levelname):
229
            ResourceLog.objects.create(**attr)
230

  
231
        # Default Resource Logger
232
        getattr(self._logger, levelname.lower())(message, *args, **kwargs)
233

  
234
    def debug(self, message, *args, **kwargs):
235
        self._log('DEBUG', message, *args, **kwargs)
236

  
237
    def info(self, message, *args, **kwargs):
238
        self._log('INFO', message, *args, **kwargs)
239

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

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

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

  
249
    def fatal(self, message, *args, **kwargs):
250
        self._log('FATAL', message, *args, **kwargs)
passerelle/base/urls.py
14 14
    url(r'^accessright/add/(?P<resource_type>[\w,-]+)/(?P<resource_pk>[\w,-]+)/(?P<codename>[\w,-]+)/',
15 15
        AccessRightCreateView.as_view(), name='access-right-add')
16 16
)
17

  
18
# logs_urlpatterns = patterns('',
19
#     url(r'^$', ResourceLogListView.as_view(), name='resourcelog-list'),)
passerelle/settings.py
173 173
    'handlers': {
174 174
        'console': {
175 175
            'level': 'DEBUG',
176
            'class': 'logging.StreamHandler',
177
            },
176
            'class': 'logging.StreamHandler'
177
        },
178 178
    },
179 179
    'loggers': {
180 180
        'django.request': {
passerelle/templates/passerelle/manage/log_detail.html
1
{% extends "passerelle/manage.html" %}
2
{% load i18n passerelle %}
3
{% load tz %}
4

  
5
{% block content %}
6
<table class="main">
7
    <tbody>
8
        <tr>
9
            <td>{% trans 'Timestamp' %}</td>
10
            <td>{{object.timestamp}}</td>
11
        </tr>
12
        <tr>
13
            <td>{% trans 'Level' %}</td>
14
            <td>{{object.loglevel|upper}}</td>
15
        </tr>
16
        <tr>
17
            <td>{% trans 'Ip Source' %}</td>
18
            <td>{{object.ipsource}}</td>
19
        </tr>
20
        <tr>
21
            <td>{% trans 'Message' %}</td>
22
            <td>{{object.message}}</td>
23
        </tr>
24
        <tr>
25
            <td>{% trans 'Extra' %}</td>
26
            <td>
27
                <table class="main">
28
                    {% for key, value in object.extra.items%}
29
                    <tr>
30
                        <td>{{key}}</td>
31
                        <td>{{value}}</td>
32
                    </tr>
33
                    {% endfor %}
34
                </table>
35
            </td>
36
        </tr>
37
    </tbody>
38
</table>
39
{% endblock %}
passerelle/templates/passerelle/manage/log_view.html
1
{% extends "passerelle/manage.html" %}
2
{% load i18n passerelle %}
3
{% load tz %}
4

  
5
{% block appbar %}
6
    <h2>{% trans 'Log Records'%}</h2>
7
{% endblock %}
8

  
9
{% block content %}
10
{% if object_list %}
11
<table class="main">
12
    <thead>
13
        <th>{% trans 'Id' %}</th>
14
        <th>{% trans 'Timestamp' %}</th>
15
        <th>{% trans 'Level' %}</th>
16
        <th>{% trans 'Ip Source' %}</th>
17
        <th>{% trans 'Message preview' %}</th>
18
    </thead>
19
    <tbody>
20
    {% for record in object_list %}
21
    <tr>
22
        <td>
23
            {% url 'connector-log-detail' connector slug record.id as record_detail %}
24
            <a href="{{record_detail}}">{{record.id}}</a>
25
        </td>
26
        <td>{{ record.timestamp|localtime }}</td>
27
        <td>{{ record.loglevel }}</td>
28
        <td>{{ record.ipsource }}</td>
29
        <td>{{ record.message|truncatechars:64 }}</td>
30
    </tr>
31
    {% endfor %}
32
    </tbody>
33
</table>
34

  
35
{% if is_paginated %}
36
<p class="paginator">
37
  {% if page_obj.has_previous %}
38
      <a href="?page={{ page_obj.previous_page_number }}">&lt;&lt;</a>
39
  {% else %}
40
  <span>&lt;&lt;</span>
41
  {% endif %}
42
    &nbsp;
43
    <span class="current">
44
        {{ page_obj.number }} / {{ page_obj.paginator.num_pages }}
45
    </span>
46
     &nbsp;
47
    {% if page_obj.has_next %}
48
        <a href="?page={{ page_obj.next_page_number }}">&gt;&gt;</a>
49
    {% else %}
50
        <span>&gt;&gt;</span>
51
    {% endif %}
52
        </div>
53
    {% endif %}
54

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

  
51
<div id="logs">
52
</div>
53

  
51 54
{% endblock %}
passerelle/urls.py
9 9
from .views import (HomePageView, ManageView, ManageAddView,
10 10
        GenericCreateConnectorView, GenericDeleteConnectorView,
11 11
        GenericEditConnectorView, GenericEndpointView, GenericConnectorView,
12
        LEGACY_APPS_PATTERNS, LegacyPageView, login, logout)
12
        LEGACY_APPS_PATTERNS, LegacyPageView, login, logout,
13
        GenericConnectorLogDetailView, GenericConnectorLogView)
13 14
from .urls_utils import decorated_includes, required, app_enabled, manager_required
14 15
from .base.urls import access_urlpatterns
15 16
from .plugins import register_apps_urls
......
17 18
import choosit.urls
18 19
import pastell.urls
19 20

  
21

  
20 22
admin.autodiscover()
21 23

  
22 24
urlpatterns = patterns('',
......
84 86
                GenericEditConnectorView.as_view(), name='edit-connector'),
85 87
        )))))
86 88

  
89
# Intagrated logs
90
urlpatterns += patterns('',
91
    url(r'^(?P<connector>[\w,-]+)/(?P<slug>[\w,-]+)/_logs/$',
92
        GenericConnectorLogView.as_view(), name='connector-log-list'),
93
    url(r'^(?P<connector>[\w,-]+)/(?P<slug>[\w,-]+)/_logs/(?P<pk>[\w,-]+)$',
94
        GenericConnectorLogDetailView.as_view(), name='connector-log-detail'),
95
)
96

  
87 97
urlpatterns += patterns('',
88 98
    url(r'^(?P<connector>[\w,-]+)/(?P<slug>[\w,-]+)/$',
89 99
        GenericConnectorView.as_view(), name='view-connector'),
passerelle/views.py
8 8
from django.http import HttpResponseBadRequest, HttpResponseRedirect, Http404
9 9
from django.views.decorators.csrf import csrf_exempt
10 10
from django.views.generic import (RedirectView, View, TemplateView, CreateView,
11
        DeleteView, UpdateView, DetailView)
11
        DeleteView, UpdateView, DetailView, ListView)
12 12
from django.views.generic.detail import SingleObjectMixin
13 13
from django.conf import settings
14 14
from django.db import models
......
22 22
else:
23 23
    get_idps = lambda: []
24 24

  
25
from passerelle.base.models import BaseResource
25
from passerelle.base.models import BaseResource, ResourceLog
26 26

  
27 27
from .utils import to_json, response_for_json, is_authorized
28 28
from .forms import GenericConnectorForm
......
228 228
        payload = request.body[:5000]
229 229
        connector.logger.debug('endpoint %s %s (%r) ' %
230 230
                               (request.method, url, payload),
231
                               request=request,
231 232
                               extra={
232 233
                                   'connector': connector_name,
233 234
                                   'connector_endpoint': endpoint_name,
......
257 258
        return self.get(request, *args, **kwargs)
258 259

  
259 260

  
261
class GenericConnectorLogView(ListView):
262
    template_name = 'passerelle/manage/log_view.html'
263
    model = ResourceLog
264
    paginate_by = 10
265

  
266
    def get_context_data(self, **kwargs):
267
        context = super(GenericConnectorLogView, self).get_context_data(**kwargs)
268
        context.update(self.kwargs)
269
        return context
270

  
271
    def get_queryset(self, **kwargs):
272
        connector = self.kwargs.get('connector')
273
        slug = self.kwargs.get('slug')
274
        return ResourceLog.objects.filter(appname=connector, slug=slug).order_by('-timestamp')
275

  
276

  
277
class GenericConnectorLogDetailView(DetailView):
278
    template_name = 'passerelle/manage/log_detail.html'
279
    model = ResourceLog
280

  
281

  
260 282
# legacy
261 283
LEGACY_APPS_PATTERNS = {
262 284
    'datasources': {'url': 'data', 'name': 'Data Sources'},
tests/test_generic_endpoint.py
25 25

  
26 26
import utils
27 27

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

  
30 32

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

  
35 37

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

  
42

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

  
58 65
    records = [record for record in caplog.records() if record.name == 'passerelle.resource.mdel.test']
59 66
    for record in records:
60
        assert record.module == 'views'
67
        # 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(connector='arcgis-test')[0]
102
    assert log.connector == 'arcgis-test'
103
    assert log.appname == 'arcgis'
104
    assert log.slug == 'test'
105
    assert log.loglevel == 'DEBUG'
106
    assert log.ipsource == '127.0.0.1'
107
    assert log.extra['connector'] == 'arcgis'
108
    assert log.extra['connector_endpoint'] == 'district'
109
    assert log.extra['connector_endpoint_method'] == 'GET'
110
    assert log.extra['connector_endpoint_url'] == '/arcgis/test/district?lat=48.673836&lon=6.172122'
111

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

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

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

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

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