Projet

Général

Profil

0001-debian-add-debug-log-in-var-log-app-debug-29149.patch

Benjamin Dauvergne, 22 août 2020 23:55

Télécharger (13,8 ko)

Voir les différences:

Subject: [PATCH 1/5] debian: add debug log in /var/log/<app>/debug (#29149)

It's configured through the DEBUG_LOG settings:
* DEBUG_LOG = False, no debug log
* DEBUG_LOG = True, all debug log go to /var/log/<app>/debug
* DEBUG_LOG = app1,app2, only debug log of logger app1 and app2
  go to /var/log/<app>/debug

/var/log/<app>/debug is emptied everyday at midnight.
 debian/debian_config_common.py                |  33 ++++-
 hobo/logger.py                                | 115 +++++++++++++++++-
 tests_authentic/settings.py                   |   2 +
 tests_multitenant/test_logger.py              | 107 ++++++++++++++++
 .../test_request_context_filter.py            |  34 +++++-
 tests_passerelle/settings.py                  |   1 +
 6 files changed, 284 insertions(+), 8 deletions(-)
 create mode 100644 tests_multitenant/test_logger.py
debian/debian_config_common.py
46 46
# mode for newly updated files
47 47
FILE_UPLOAD_PERMISSIONS = 0o644
48 48

  
49
DEBUG_LOG_PATH = '/var/log/%s/debug' % PROJECT_NAME
50
DEBUG_LOG_FORMAT = (
51
    '%(asctime)s \x1f%(tenant)s \x1f%(ip)s \x1f%(user)r \x1f%(request_id)s \x1f'
52
    '%(levelname)s \x1f%(name)s \x1f%(message)s'
53
)
54

  
49 55
LOGGING = {
50 56
    'version': 1,
51 57
    'disable_existing_loggers': True,
......
62 68
        'force_debug': {
63 69
            '()': 'hobo.logger.ForceDebugFilter',
64 70
        },
71
        'debug_log': {
72
            '()': 'hobo.logger.DebugLogFilter',
73
        },
65 74
    },
66 75
    'formatters': {
67 76
        'syslog': {
68 77
            'format': '%(application)s %(levelname)s %(tenant)s %(ip)s %(user)s %(request_id)s'
69 78
                      ' %(message)s',
70 79
        },
80
        'debug': {
81
            'format': DEBUG_LOG_FORMAT,
82
        },
71 83
        'syslog_no_filter': {
72 84
            'format': '%(levelname)s %(message)s',
73 85
        },
74 86
    },
75 87
    'handlers': {
76 88
        'syslog': {
77
            'level': 'DEBUG',
89
            'level': 'INFO',
78 90
            'address': '/dev/log',
79 91
            'class': 'logging.handlers.SysLogHandler',
80 92
            'formatter': 'syslog',
81
            'filters': ['request_context'],
82 93
        },
83 94
        'syslog_no_filter': {
84
            'level': 'DEBUG',
95
            'level': 'INFO',
85 96
            'address': '/dev/log',
86 97
            'class': 'logging.handlers.SysLogHandler',
87 98
            'formatter': 'syslog_no_filter',
......
94 105
        'null': {
95 106
            'class': 'logging.NullHandler',
96 107
        },
108
        'debug': {
109
            'level': 'DEBUG',
110
            'class': 'hobo.logger.TimedRotatingFileHandler',
111
            'formatter': 'debug',
112
            'filename': DEBUG_LOG_PATH,
113
            'when': 'midnight',
114
            'backupCount': 1,
115
            'interval': 1,
116
            'filters': ['debug_log'],
117
        }
97 118
    },
98 119
    'loggers': {
99 120
        'django.db': {
......
157 178
            'propagate': False,
158 179
        },
159 180
        '': {
160
            'level': hobo.logger.SettingsLogLevel(
161
                default_log_level='INFO'),
162
            'handlers': ['syslog', 'mail_admins'],
181
            'level': 'DEBUG',
182
            'filters': ['request_context'],
183
            'handlers': ['syslog', 'mail_admins', 'debug'],
163 184
        },
164 185
    },
165 186
}
hobo/logger.py
1 1
# hobo - portal to configure and deploy applications
2
# Copyright (C) 2015-2019  Entr'ouvert
2
# Copyright (C) 2015-2020  Entr'ouvert
3 3
#
4 4
# This program is free software: you can redistribute it and/or modify it
5 5
# under the terms of the GNU Affero General Public License as published
......
14 14
# You should have received a copy of the GNU Affero General Public License
15 15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16 16

  
17
import ast
18
import datetime
17 19
import logging
20
import logging.handlers
21
import os
22
import pytz
23
import time
18 24

  
19 25
from django.conf import settings
20 26
from django.db import connection
......
126 132
        record.levelno = logging.DEBUG
127 133
        record.levelname = 'DEBUG'
128 134
        return super(ForceDebugFilter, self).filter(record)
135

  
136

  
137
class LogRecord(logging.LogRecord):
138
    '''Subclass LogRecord to make multiline log parseable'''
139
    def getMessage(self):
140
        return super(LogRecord, self).getMessage().replace('\n', '\n ')
141

  
142

  
143
class TimedRotatingFileHandler(logging.handlers.TimedRotatingFileHandler):
144
    def format(self, record):
145
        old_class = record.__class__
146
        record.__class__ = LogRecord
147
        try:
148
            return super(TimedRotatingFileHandler, self).format(record)
149
        finally:
150
            record.__class__ = old_class
151

  
152

  
153
class DebugLogFilter(object):
154
    '''Filter debug log records based on the DEBUG_LOG setting'''
155

  
156
    def filter(self, record):
157
        debug_log = getattr(settings, 'DEBUG_LOG', False)
158

  
159
        # add space after newlines to reconstruct message when reading
160
        # debug.log
161
        if not debug_log:
162
            return False
163

  
164
        # change class to add space after newlines in message
165
        record.__class__ = LogRecord
166
        if debug_log is True:
167
            return True
168
        elif hasattr(debug_log, 'encode'):
169
            # debug_log is a string
170
            domains = [domain.strip() for domain in debug_log.split(',')]
171
            return any(record.name == domain or (record.name.startswith(domain) and record.name[len(domain)] == '.')
172
                       for domain in domains)
173
        else:
174
            return bool(debug_log)
175

  
176

  
177
class DebugLog(object):
178
    def __init__(self, path):
179
        self.path = path
180

  
181
    def _pre_lines(self, cursor=0):
182
        if not os.path.exists(self.path):
183
            return
184
        with open(self.path, 'rb') as fd:
185
            accum = ''
186

  
187
            try:
188
                fd.seek(cursor)
189
            except Exception:
190
                return
191

  
192
            for line in fd:
193
                size = len(line)
194
                cursor += size
195
                line = line.decode('utf-8')
196

  
197
                if not accum:
198
                    accum = line
199
                elif not line.startswith(' '):
200
                    yield cursor - size, accum
201
                    accum = line
202
                else:
203
                    accum += line[1:]
204
            if accum:
205
                yield cursor, accum
206

  
207
    keys = ['tenant', 'ip', 'user', 'request_id', 'level', 'logger']
208

  
209
    def _parse(self, cursor=0):
210
        for cursor, line in self._pre_lines(cursor=cursor):
211
            if line.endswith('\n'):
212
                line = line[:-1]
213
            parts = line.split(' \x1f', settings.DEBUG_LOG_FORMAT.count(' \x1f'))
214
            try:
215
                timestamp = datetime.datetime.strptime(parts[0], '%Y-%m-%d %H:%M:%S,%f')
216
                timestamp = pytz.timezone(time.tzname[0]).localize(timestamp)
217
            except ValueError:
218
                continue
219
            message = parts[-1]
220
            d = {key: value for key, value in zip(self.keys, parts[1:-1])}
221
            if 'user' in d:
222
                try:
223
                    d['user'] = ast.literal_eval(d['user'])
224
                except SyntaxError:
225
                    pass
226
            d.update({
227
                'cursor': cursor,
228
                'timestamp': timestamp,
229
                'message': message,
230
            })
231
            yield d
232

  
233
    @classmethod
234
    def lines(cls, cursor=0):
235
        debug_log_path = getattr(settings, 'DEBUG_LOG_PATH', None)
236
        if not debug_log_path:
237
            return
238
        if not os.path.exists(debug_log_path):
239
            return
240
        for record in cls(debug_log_path)._parse(cursor=cursor):
241
            yield record
tests_authentic/settings.py
46 46
CSRF_COOKIE_SECURE = False
47 47

  
48 48
LANGUAGE_CODE = 'en'
49

  
50
LOGGING['handlers']['debug']['filename'] = 'debug.log'
tests_multitenant/test_logger.py
1
# hobo - portal to configure and deploy applications
2
# Copyright (C) 2019  Entr'ouvert
3
#
4
# This program is free software: you can redistribute it and/or modify it
5
# under the terms of the GNU Affero General Public License as published
6
# by the Free Software Foundation, either version 3 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU Affero General Public License for more details.
13
#
14
# You should have received a copy of the GNU Affero General Public License
15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16

  
17
import datetime
18
import logging
19
import logging.config
20
import time
21
import pytz
22

  
23
import pytest
24

  
25
from tenant_schemas.utils import tenant_context
26

  
27
from hobo.logger import DebugLog
28

  
29

  
30
@pytest.fixture
31
def debug_log(settings, tmpdir):
32
    debug_log_path = str(tmpdir / 'debug.log')
33
    settings.DEBUG_LOG_PATH = debug_log_path
34

  
35
    LOGGING = {
36
        'version': 1,
37
        'disable_existing_loggers': True,
38
        'filters': {
39
            'debug_log': {
40
                '()': 'hobo.logger.DebugLogFilter',
41
            },
42
            'request_context': {
43
                '()': 'hobo.logger.RequestContextFilter',
44
            },
45
        },
46
        'formatters': {
47
            'debug': {
48
                'format': settings.DEBUG_LOG_FORMAT,
49
            },
50
        },
51
        'handlers': {
52
            'debug': {
53
                'level': 'DEBUG',
54
                'class': 'hobo.logger.TimedRotatingFileHandler',
55
                'formatter': 'debug',
56
                'filename': debug_log_path,
57
                'when': 'midnight',
58
                'backupCount': 1,
59
                'interval': 1,
60
                'filters': ['request_context', 'debug_log'],
61
            }
62
        },
63
        'loggers': {
64
            'multitenant': {
65
                'level': 'DEBUG',
66
                'handlers': ['debug'],
67
            },
68
        },
69
    }
70
    logging.config.dictConfig(LOGGING)
71

  
72
    logger = logging.getLogger('multitenant')
73
    yield logger
74
    logger.removeHandler(logger.handlers[0])
75

  
76

  
77
def test_debug_log(tenants, settings, app, rf, debug_log, freezer):
78
    freezer.move_to('2020-4-20')
79
    request = rf.get('/path/')
80
    debug_log.info('test %s is ok', 1, extra={'request': request, 'tenant': 'yes'})
81
    lines = list(DebugLog.lines())
82
    assert len(lines) == 0
83

  
84
    settings.DEBUG_LOG = True
85
    with tenant_context(tenants[0]):
86
        debug_log.info('log %s is \nok', 2, extra={'request': request, 'tenant': 'tenant1', 'user': 'jean darmette'})
87
        debug_log.debug('log %s is \nok', 3, extra={'request': request})
88
    lines = list(DebugLog.lines())
89
    assert len(lines) == 2
90
    request_id = hex(id(request))[2:].upper()
91
    assert lines[0] == {
92
        'cursor': 111,
93
        'ip': '127.0.0.1',
94
        'request_id': 'r:' + request_id,
95
        'message': 'log 2 is \nok',
96
        'level': 'INFO',
97
        'tenant': 'tenant1.example.net',
98
        'timestamp': pytz.timezone(time.tzname[0]).localize(
99
            datetime.datetime(2020, 4, 20, 2, 0)),
100
        'user': "-",
101
        'logger': 'multitenant',
102
    }
103

  
104
    # check that seeking by cursor gives the same lines
105
    lines2 = list(DebugLog.lines(cursor=lines[0]['cursor']))
106
    assert len(lines2) == 1
107
    assert lines[1] == lines2[0]
tests_multitenant/test_request_context_filter.py
3 3

  
4 4
import logging
5 5

  
6
from hobo.logger import RequestContextFilter
6
from hobo.logger import RequestContextFilter, DebugLogFilter
7 7

  
8 8
from tenant_schemas.utils import tenant_context
9 9

  
......
105 105
        assert kwargs['USER_DISPLAY_NAME'] == 'John Doe'
106 106
        assert kwargs['USER_UUID'] == 'ab' * 16
107 107
        assert kwargs['APPLICATION'] == 'fake-agent'
108

  
109

  
110
def test_debug_log_filter(caplog, settings):
111
    # default caplog log level is INFO
112
    caplog.set_level(logging.DEBUG)
113

  
114
    root_logger = logging.getLogger()
115
    caplog.handler.addFilter(DebugLogFilter())
116

  
117
    root_logger.debug('l1')
118
    assert 'l1' not in caplog.text
119

  
120
    settings.DEBUG_LOG = True
121
    root_logger.debug('l2')
122
    assert 'l2' in caplog.text
123

  
124
    settings.DEBUG_LOG = False
125
    root_logger.debug('l3')
126
    assert 'l3' not in caplog.text
127

  
128
    settings.DEBUG_LOG = 'app1,app2'
129
    root_logger.debug('l4')
130
    assert 'l4' not in caplog.text
131

  
132
    logging.getLogger('app3').debug('l5')
133
    assert 'l5' not in caplog.text
134

  
135
    logging.getLogger('app1').debug('l6')
136
    assert 'l6' in caplog.text
137

  
138
    logging.getLogger('app2').debug('l7')
139
    assert 'l7' in caplog.text
tests_passerelle/settings.py
31 31
        'handlers': ['mail_admins'],
32 32
        'propagate': True,
33 33
}
34
LOGGING['handlers']['debug']['filename'] = 'debug.log'
34
-