0001-debian-add-debug-log-in-var-log-app-debug-29149.patch
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 |
- |