Projet

Général

Profil

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

Benjamin Dauvergne, 21 avril 2020 00:27

Télécharger (13,3 ko)

Voir les différences:

Subject: [PATCH 1/2] 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                |  31 ++++-
 hobo/logger.py                                | 107 +++++++++++++++++-
 tests_authentic/settings.py                   |   2 +
 tests_multitenant/test_logger.py              | 100 ++++++++++++++++
 .../test_request_context_filter.py            |  35 +++++-
 tests_passerelle/settings.py                  |   1 +
 6 files changed, 269 insertions(+), 7 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 93
            'filters': ['request_context'],
82 94
        },
83 95
        'syslog_no_filter': {
84
            'level': 'DEBUG',
96
            'level': 'INFO',
85 97
            'address': '/dev/log',
86 98
            'class': 'logging.handlers.SysLogHandler',
87 99
            'formatter': 'syslog_no_filter',
......
91 103
            'class': 'hobo.multitenant.log.AdminEmailHandler',
92 104
            'include_html': True,
93 105
        },
106
        'debug': {
107
            'level': 'DEBUG',
108
            'class': 'hobo.logger.TimedRotatingFileHandler',
109
            'formatter': 'debug',
110
            'filename': DEBUG_LOG_PATH,
111
            'when': 'midnight',
112
            'backupCount': 1,
113
            'interval': 1,
114
            'filters': ['request_context', 'debug_log'],
115
        }
94 116
    },
95 117
    'loggers': {
96 118
        'django.db': {
......
146 168
            'propagate': False,
147 169
        },
148 170
        '': {
149
            'level': hobo.logger.SettingsLogLevel(
150
                default_log_level='INFO'),
151
            'handlers': ['syslog', 'mail_admins'],
171
            'level': 'DEBUG',
172
            'handlers': ['syslog', 'mail_admins', 'debug'],
152 173
        },
153 174
    },
154 175
}
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
18 22

  
19 23
from django.conf import settings
20 24
from django.db import connection
......
126 130
        record.levelno = logging.DEBUG
127 131
        record.levelname = 'DEBUG'
128 132
        return super(ForceDebugFilter, self).filter(record)
133

  
134

  
135
class LogRecord(logging.LogRecord):
136
    def getMessage(self):
137
        return super(LogRecord, self).getMessage().replace('\n', '\n ')
138

  
139

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

  
149

  
150
class DebugLogFilter(object):
151
    '''Filter debug log records based on the DEBUG_LOG setting'''
152

  
153
    def filter(self, record):
154
        debug_log = getattr(settings, 'DEBUG_LOG', False)
155

  
156
        # add space after newlines to reconstruct message when reading
157
        # debug.log
158
        if not debug_log:
159
            return False
160

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

  
173

  
174
class DebugLog(object):
175
    def __init__(self, path):
176
        self.path = path
177

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

  
184
            fd.seek(cursor)
185
            for line in fd:
186
                size = len(line)
187
                cursor += size
188
                line = line.decode('utf-8')
189

  
190
                if not accum:
191
                    accum = line
192
                elif not line.startswith(' '):
193
                    yield cursor - size, accum
194
                    accum = line
195
                else:
196
                    accum += line[1:]
197
            if accum:
198
                yield cursor, accum
199

  
200
    keys = ['tenant', 'ip', 'user', 'level', 'request_id', 'name']
201

  
202
    def _parse(self, cursor=0):
203
        for cursor, line in self._pre_lines(cursor=cursor):
204
            if line.endswith('\n'):
205
                line = line[:-1]
206
            parts = line.split(' \x1f', settings.DEBUG_LOG_FORMAT.count(' \x1f'))
207
            try:
208
                timestamp = datetime.datetime.strptime(parts[0], '%Y-%m-%d %H:%M:%S,%f')
209
            except ValueError:
210
                continue
211
            message = parts[-1]
212
            d = {key: value for key, value in zip(self.keys, parts[1:-1])}
213
            if 'user' in d:
214
                try:
215
                    d['user'] = ast.literal_eval(d['user'])
216
                except SyntaxError:
217
                    pass
218
            d.update({
219
                'cursor': cursor,
220
                'timestamp': timestamp,
221
                'message': message,
222
            })
223
            yield d
224

  
225
    @classmethod
226
    def lines(cls, cursor=0):
227
        debug_log_path = getattr(settings, 'DEBUG_LOG_PATH', None)
228
        if not debug_log_path:
229
            return None
230
        if not os.path.exists(debug_log_path):
231
            return None
232
        for record in cls(debug_log_path)._parse(cursor=cursor):
233
            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

  
21
import pytest
22

  
23
from hobo.logger import DebugLog
24

  
25

  
26
@pytest.fixture
27
def debug_log(settings, tmpdir):
28
    debug_log_path = str(tmpdir / 'debug.log')
29
    settings.DEBUG_LOG_PATH = debug_log_path
30

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

  
68
    logger = logging.getLogger('multitenant')
69
    yield logger
70
    logger.removeHandler(logger.handlers[0])
71

  
72

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

  
80
    settings.DEBUG_LOG = True
81
    debug_log.info('log %s is \nok', 2, extra={'request': request, 'tenant': 'tenant1', 'user': 'jean darmette'})
82
    debug_log.debug('log %s is \nok', 3, extra={'request': request})
83
    lines = list(DebugLog.lines())
84
    assert len(lines) == 2
85
    assert lines[0] == {
86
        'cursor': 93,
87
        'ip': '127.0.0.1',
88
        'level': 'r:' + hex(id(request))[2:].upper(),
89
        'message': 'log 2 is \nok',
90
        'request_id': 'INFO',
91
        'tenant': '-',
92
        'timestamp': datetime.datetime(2020, 4, 20, 2, 0),
93
        'user': "-",
94
        'name': 'multitenant',
95
    }
96

  
97
    # check that seeking by cursor gives the same lines
98
    lines2 = list(DebugLog.lines(cursor=lines[0]['cursor']))
99
    assert len(lines2) == 1
100
    assert lines[1] == lines2[0]
tests_multitenant/test_request_context_filter.py
2 2

  
3 3
import logging
4 4

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

  
7 7
from tenant_schemas.utils import tenant_context
8 8

  
......
101 101
        assert kwargs['USER_DISPLAY_NAME'] == 'John Doe'
102 102
        assert kwargs['USER_UUID'] == 'ab' * 16
103 103
        assert kwargs['APPLICATION'] == 'fake-agent'
104

  
105

  
106
def test_debug_log_filter(caplog, settings):
107
    # default caplog log level is INFO
108
    caplog.set_level(logging.DEBUG)
109

  
110
    root_logger = logging.getLogger()
111
    assert len(root_logger.handlers) == 1
112
    root_logger.handlers[0].addFilter(DebugLogFilter())
113

  
114
    root_logger.debug('l1')
115
    assert 'l1' not in caplog.text
116

  
117
    settings.DEBUG_LOG = True
118
    root_logger.debug('l2')
119
    assert 'l2' in caplog.text
120

  
121
    settings.DEBUG_LOG = False
122
    root_logger.debug('l3')
123
    assert 'l3' not in caplog.text
124

  
125
    settings.DEBUG_LOG = 'app1,app2'
126
    root_logger.debug('l4')
127
    assert 'l4' not in caplog.text
128

  
129
    logging.getLogger('app3').debug('l5')
130
    assert 'l5' not in caplog.text
131

  
132
    logging.getLogger('app1').debug('l6')
133
    assert 'l6' in caplog.text
134

  
135
    logging.getLogger('app2').debug('l7')
136
    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
-