Skip to content

Commit f99127d

Browse files
georgi-bozhinovgvachkov
authored andcommitted
Feature log exception stacktrace (#28)
* Add logging for stacktrace of logger exceptions * Remove log and exception methods from falcon request * Adapt README to falcon logging changes * Apply PR comment changes
1 parent adee737 commit f99127d

File tree

11 files changed

+135
-18
lines changed

11 files changed

+135
-18
lines changed

README.rst

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -121,12 +121,13 @@ Falcon
121121
122122
import falcon
123123
from sap.cf_logging import falcon_logging
124+
from sap.cf_logging.core.constants import REQUEST_KEY
124125
125126
126127
class Resource:
127128
def on_get(self, req, resp):
128-
# Use the log() method of the req object to log additional messages
129-
req.log('Resource requested')
129+
extra = {REQUEST_KEY: req}
130+
logging.getLogger('my.logger').log('Resource requested', extra=extra)
130131
resp.media = {'name': 'Cloud Foundry'}
131132
132133
@@ -136,8 +137,6 @@ Falcon
136137
app.add_route('/resource', Resource())
137138
falcon_logging.init(app)
138139
139-
**Note**: Use the ``log`` method of ``req`` since it will include the ``correlation_id`` from the ``req`` object in the logs.
140-
141140
General
142141
^^^^^^^
143142

sap/cf_logging/core/constants.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,5 @@
55
LOG_SENSITIVE_CONNECTION_DATA = 'LOG_SENSITIVE_CONNECTION_DATA'
66
LOG_REMOTE_USER = 'LOG_REMOTE_USER'
77
LOG_REFERER = 'LOG_REFERER'
8+
9+
STACKTRACE_MAX_SIZE = 55 * 1024

sap/cf_logging/falcon_logging/__init__.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,13 @@
1515
FALCON_FRAMEWORK_NAME = 'falcon.framework'
1616

1717

18-
class LoggingMiddleware:
18+
class LoggingMiddleware(object):
1919
""" Falcon logging middleware """
2020

2121
def __init__(self, logger_name='cf.falcon.logger'):
2222
self._logger_name = logger_name
2323

24-
def process_request(self, request, response): # pylint: disable=unused-argument
24+
def process_request(self, request, response): # pylint: disable=unused-argument,no-self-use
2525
"""Process the request before routing it.
2626
2727
:param request: - Falcon Request object
@@ -31,8 +31,6 @@ def process_request(self, request, response): # pylint: disable=unused-argument
3131
cid = framework.request_reader.get_correlation_id(request)
3232
framework.context.set_correlation_id(cid, request)
3333
framework.context.set('request_started_at', datetime.utcnow(), request)
34-
request.log = lambda msg, lvl=logging.INFO, extra={}: logging.getLogger(
35-
self._logger_name).log(lvl, msg, extra=extra.update({REQUEST_KEY: request}) or extra)
3634

3735
def process_response(self, request, response, resource, req_succeeded): # pylint: disable=unused-argument
3836
"""Post-processing of the response (after routing).
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
"""
2+
Module for formatting utilities for stacktrace
3+
generated by user logging.exception call
4+
"""
5+
import re
6+
7+
from sap.cf_logging.core import constants
8+
9+
10+
def format_stacktrace(stacktrace):
11+
"""
12+
Removes newline and tab characters
13+
Truncates stacktrace to maximum size
14+
15+
:param stacktrace: string representation of a stacktrace
16+
"""
17+
if not isinstance(stacktrace, str):
18+
return ''
19+
20+
stacktrace = re.sub('\n|\t', ' ', stacktrace)
21+
22+
if len(stacktrace) <= constants.STACKTRACE_MAX_SIZE:
23+
return stacktrace
24+
25+
stacktrace_beginning = _stacktrace_beginning(
26+
stacktrace, constants.STACKTRACE_MAX_SIZE // 3
27+
)
28+
29+
stacktrace_end = _stacktrace_end(
30+
stacktrace, (constants.STACKTRACE_MAX_SIZE // 3) * 2
31+
)
32+
33+
new_stacktrace = "-------- STACK TRACE TRUNCATED --------" + stacktrace_beginning +\
34+
"-------- OMITTED --------" + stacktrace_end
35+
36+
return new_stacktrace
37+
38+
def _stacktrace_beginning(stacktrace, size):
39+
""" Gets the first `size` bytes of the stacktrace """
40+
if len(stacktrace) <= size:
41+
return stacktrace
42+
43+
return stacktrace[:size]
44+
45+
def _stacktrace_end(stacktrace, size):
46+
""" Gets the last `size` bytes of the stacktrace """
47+
stacktrace_length = len(stacktrace)
48+
if stacktrace_length <= size:
49+
return stacktrace
50+
51+
return stacktrace[:-(stacktrace_length-size)]

sap/cf_logging/record/simple_log_record.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
""" Module SimpleLogRecord """
22
import logging
3+
import traceback
4+
35
from datetime import datetime
46
from sap.cf_logging import defaults
57
from sap.cf_logging.core.constants import REQUEST_KEY, RESPONSE_KEY
68
from sap.cf_logging.record import application_info
79
from sap.cf_logging.record import util
810

11+
from sap.cf_logging.formatters.stacktrace_formatter import format_stacktrace
12+
913
_SKIP_ATTRIBUTES = ["type", "written_at", "written_ts", "correlation_id", "remote_user", "referer",
1014
"x_forwarded_for", "protocol", "method", "remote_ip", "request_size_b",
1115
"remote_host", "remote_port", "request_received_at", "direction",
@@ -63,5 +67,9 @@ def format(self):
6367
'msg': self.getMessage(),
6468
})
6569

70+
if self.levelno == logging.ERROR:
71+
stacktrace = ''.join(traceback.format_exception(*self.exc_info))
72+
record['stacktrace'] = format_stacktrace(stacktrace)
73+
6674
record.update(self.extra)
6775
return record

tests/log_schemas.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
'written_at': u.iso_datetime(),
2727
'written_ts': u.pos_num(),
2828
'msg': u.string(u.WORD),
29-
'component_type': u.string(r'^application$')
29+
'component_type': u.string(r'^application$'),
3030
})
3131

3232
WEB_LOG_SCHEMA = u.extend(CF_ATTRIBUTES_SCHEMA, {

tests/test_falcon_logging.py

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,17 @@
66
from falcon_auth import FalconAuthMiddleware, BasicAuthBackend
77
from sap import cf_logging
88
from sap.cf_logging import falcon_logging
9+
from sap.cf_logging.core.constants import REQUEST_KEY
910
from tests.log_schemas import WEB_LOG_SCHEMA, JOB_LOG_SCHEMA
1011
from tests.common_test_params import (
1112
v_str, auth_basic, get_web_record_header_fixtures
1213
)
13-
from tests.util import check_log_record, config_root_logger, enable_sensitive_fields_logging
14+
from tests.util import (
15+
check_log_record,
16+
config_root_logger,
17+
enable_sensitive_fields_logging,
18+
config_logger
19+
)
1420

1521

1622
# pylint: disable=protected-access, missing-docstring,too-few-public-methods
@@ -101,16 +107,16 @@ def _set_up_falcon_logging(app, *args):
101107
falcon_logging.init(app, logging.DEBUG, *args)
102108

103109

104-
class UserResourceRoute:
105-
110+
class UserResourceRoute(object):
106111
def __init__(self, extra, expected):
107112
self.extra = extra
108113
self.expected = expected
114+
self.logger, self.stream = config_logger('user.logging')
109115

110116
def on_get(self, req, resp):
111-
_, stream = config_root_logger('user.logging')
112-
req.log('in route headers', extra=self.extra)
113-
assert check_log_record(stream, JOB_LOG_SCHEMA, self.expected) == {}
117+
self.extra.update({REQUEST_KEY: req})
118+
self.logger.log(logging.INFO, 'in route headers', extra=self.extra)
119+
assert check_log_record(self.stream, JOB_LOG_SCHEMA, self.expected) == {}
114120

115121
resp.set_header('Content-Type', 'text/plain')
116122
resp.status = falcon.HTTP_200

tests/test_flask_logging.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,11 @@
77
from sap.cf_logging import flask_logging
88
from tests.log_schemas import WEB_LOG_SCHEMA, JOB_LOG_SCHEMA
99
from tests.common_test_params import v_str, v_num, auth_basic, get_web_record_header_fixtures
10-
from tests.util import check_log_record, config_root_logger, enable_sensitive_fields_logging
10+
from tests.util import (
11+
check_log_record,
12+
enable_sensitive_fields_logging,
13+
config_root_logger,
14+
)
1115

1216

1317
# pylint: disable=protected-access
@@ -26,6 +30,7 @@ def test_flask_requires_valid_app():
2630

2731
@pytest.fixture(autouse=True)
2832
def before_each():
33+
# pylint: disable=duplicate-code
2934
""" enable all fields to be logged """
3035
enable_sensitive_fields_logging()
3136
yield

tests/test_job_logging.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,23 @@ def test_set_correlation_id():
4646
assert log_json['correlation_id'] == correlation_id
4747
assert cf_logging.FRAMEWORK.context.get_correlation_id() == correlation_id
4848

49+
50+
def test_exception_stacktrace():
51+
""" Test exception stacktrace is logged """
52+
cf_logging.init(level=logging.DEBUG)
53+
logger, stream = config_root_logger('cli.test')
54+
55+
try:
56+
return 1 / 0
57+
except ZeroDivisionError:
58+
logger.exception('zero division error')
59+
log_json = JSONDecoder().decode(stream.getvalue())
60+
_, error = JsonValidator(JOB_LOG_SCHEMA).validate(log_json)
61+
62+
assert error == {}
63+
assert 'ZeroDivisionError' in str(log_json['stacktrace'])
64+
65+
4966
def test_thread_safety():
5067
""" test context keeps separate correlation ID per thread """
5168
class _SampleThread(threading.Thread):

tests/test_sanic_logging.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,11 @@
99
from tests.log_schemas import WEB_LOG_SCHEMA, JOB_LOG_SCHEMA
1010
from tests.common_test_params import v_str, v_num, get_web_record_header_fixtures
1111
from tests.schema_util import extend
12-
from tests.util import check_log_record, config_logger, enable_sensitive_fields_logging
12+
from tests.util import (
13+
check_log_record,
14+
config_logger,
15+
enable_sensitive_fields_logging,
16+
)
1317

1418

1519
# pylint: disable=protected-access
@@ -22,12 +26,14 @@ def test_sanic_requires_valid_app():
2226
FIXTURE = get_web_record_header_fixtures()
2327
FIXTURE.append(({'no-content-length': '1'}, {'response_size_b': v_num(-1)}))
2428

29+
2530
@pytest.fixture(autouse=True)
2631
def before_each():
2732
""" enable all fields to be logged """
2833
enable_sensitive_fields_logging()
2934
yield
3035

36+
3137
@pytest.mark.parametrize("headers, expected", FIXTURE)
3238
def test_sanic_request_log(headers, expected):
3339
""" Test that the JSON logs contain the expected properties based on the
@@ -45,7 +51,8 @@ async def _headers_route(request):
4551
_, stream = config_logger('cf.sanic.logger')
4652

4753
client = app.test_client
48-
_check_expected_response(client.get('/test/path', headers=headers)[1], 200, 'ok')
54+
_check_expected_response(client.get(
55+
'/test/path', headers=headers)[1], 200, 'ok')
4956
assert check_log_record(stream, WEB_LOG_SCHEMA, expected) == {}
5057

5158

0 commit comments

Comments
 (0)