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 |
'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 |
- |