From 6ce33c3c4adf3fb9610ef4bf49bd2ec1ac4b9201 Mon Sep 17 00:00:00 2001 From: Georgi Vachkov Date: Mon, 18 Jun 2018 13:35:37 +0300 Subject: [PATCH 01/35] Enable per thread correlation ID in job logging --- README.rst | 9 +++----- sap/cf_logging/job_logging/context.py | 4 +++- tests/test_job_logging.py | 30 +++++++++++++++++++++++++++ 3 files changed, 36 insertions(+), 7 deletions(-) diff --git a/README.rst b/README.rst index 3c86551..84721cf 100644 --- a/README.rst +++ b/README.rst @@ -159,7 +159,8 @@ Setting and getting correlation ID """""""""""""""""""""""""""""""""" When using cf_logging in a web application you don't need to set the correlation ID, because the logging library will fetch it from the HTTP headers and set it. -For non web applications you could set the correlation ID manually, so that the log entries can be filtered later on based on the correlation_id. +For non web applications you could set the correlation ID manually, so that the log entries can be filtered later on based on the ``correlation_id`` log property. +In this case the correlation ID is kept in a thread local variable and each thread should set its own correlation ID. Setting and getting the correlation_id can be done via: @@ -168,11 +169,7 @@ Setting and getting the correlation_id can be done via: cf_logging.FRAMEWORK.context.get_correlation_id() cf_logging.FRAMEWORK.context.set_correlation_id(value) -Whenever a correlation id is set after initializing cf_logging without a specific framework (ex: cf_logging.init()) - this same correlation ID would be used for each log record. - -If you need a *thread safe* correlation ID, you can reuse the ``cf_logging.job_loffing.framework.JobFramework`` and provide your own context implementation that is *thread safe*. - -If you need to get the correlation_id in a web application, take into account the framework you are using. +If you need to get the correlation ID in a web application, take into account the framework you are using. In async frameworks like Sanic and Falcon the context is stored into the request object and you need to provide the request to the call: .. code:: python diff --git a/sap/cf_logging/job_logging/context.py b/sap/cf_logging/job_logging/context.py index 4c0e7cd..0ba5036 100644 --- a/sap/cf_logging/job_logging/context.py +++ b/sap/cf_logging/job_logging/context.py @@ -1,11 +1,13 @@ """ Job logging context - used by the logging package to keep log data """ +import threading from sap.cf_logging.core.context import Context -class JobContext(Context): +class JobContext(Context, threading.local): """ Stores logging context in dict """ def __init__(self): + super(JobContext, self).__init__() self._mem_store = {} def set(self, key, value, request): diff --git a/tests/test_job_logging.py b/tests/test_job_logging.py index 971e96a..3290d82 100644 --- a/tests/test_job_logging.py +++ b/tests/test_job_logging.py @@ -1,5 +1,8 @@ """ Module to test the cf_logging library """ +import uuid import logging +import time +import threading from json import JSONDecoder import pytest from json_validator.validator import JsonValidator @@ -42,3 +45,30 @@ def test_set_correlation_id(): assert error == {} assert log_json['correlation_id'] == correlation_id assert cf_logging.FRAMEWORK.context.get_correlation_id() == correlation_id + +def test_thread_safety(): + """ test context keeps separate correlation ID per thread """ + class _SampleThread(threading.Thread): + def __init__(self): + super(_SampleThread, self).__init__() + self.correlation_id = str(uuid.uuid1()) + self.read_correlation_id = '' + + def run(self): + cf_logging.FRAMEWORK.context.set_correlation_id(self.correlation_id) + time.sleep(0.1) + self.read_correlation_id = cf_logging.FRAMEWORK.context.get_correlation_id() + + cf_logging.init(level=logging.DEBUG) + + thread_one = _SampleThread() + thread_two = _SampleThread() + + thread_one.start() + thread_two.start() + + thread_one.join() + thread_two.join() + + assert thread_one.correlation_id == thread_one.read_correlation_id + assert thread_two.correlation_id == thread_two.read_correlation_id From adee737e78e0ab1cff4fa1aeb13f4d8a970b630c Mon Sep 17 00:00:00 2001 From: Georgi Vachkov Date: Mon, 18 Jun 2018 16:26:26 +0300 Subject: [PATCH 02/35] Version 3.3.1 (#27) --- CHANGELOG.md | 5 +++++ sap/cf_logging/__init__.py | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ddb8d2..88670de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,11 @@ This project adheres to [Semantic Versioning](http://semver.org/). The format is based on [Keep a Changelog](http://keepachangelog.com/). +## 3.3.1 - 2018-06-18 + +### Fixed + - Correlation ID should be thread safe + ## 3.3.0 - 2018-06-07 ### Added diff --git a/sap/cf_logging/__init__.py b/sap/cf_logging/__init__.py index b7917dc..be194c1 100644 --- a/sap/cf_logging/__init__.py +++ b/sap/cf_logging/__init__.py @@ -10,7 +10,7 @@ from sap.cf_logging.record.request_log_record import RequestWebRecord from sap.cf_logging.record.simple_log_record import SimpleLogRecord -__version__ = '3.3.0' +__version__ = '3.3.1' _SETUP_DONE = False FRAMEWORK = None From f99127d88444b86888e35714cbac92460334e8d9 Mon Sep 17 00:00:00 2001 From: georgi-bozhinov <40486425+georgi-bozhinov@users.noreply.github.com> Date: Tue, 3 Jul 2018 10:26:26 +0300 Subject: [PATCH 03/35] 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 --- README.rst | 7 ++- sap/cf_logging/core/constants.py | 2 + sap/cf_logging/falcon_logging/__init__.py | 6 +-- .../formatters/stacktrace_formatter.py | 51 +++++++++++++++++++ sap/cf_logging/record/simple_log_record.py | 8 +++ tests/log_schemas.py | 2 +- tests/test_falcon_logging.py | 18 ++++--- tests/test_flask_logging.py | 7 ++- tests/test_job_logging.py | 17 +++++++ tests/test_sanic_logging.py | 11 +++- .../formatters/test_stacktrace_formatter.py | 24 +++++++++ 11 files changed, 135 insertions(+), 18 deletions(-) create mode 100644 sap/cf_logging/formatters/stacktrace_formatter.py create mode 100644 tests/unit/formatters/test_stacktrace_formatter.py diff --git a/README.rst b/README.rst index 84721cf..7a3f5e6 100644 --- a/README.rst +++ b/README.rst @@ -121,12 +121,13 @@ Falcon import falcon from sap.cf_logging import falcon_logging + from sap.cf_logging.core.constants import REQUEST_KEY class Resource: def on_get(self, req, resp): - # Use the log() method of the req object to log additional messages - req.log('Resource requested') + extra = {REQUEST_KEY: req} + logging.getLogger('my.logger').log('Resource requested', extra=extra) resp.media = {'name': 'Cloud Foundry'} @@ -136,8 +137,6 @@ Falcon app.add_route('/resource', Resource()) falcon_logging.init(app) -**Note**: Use the ``log`` method of ``req`` since it will include the ``correlation_id`` from the ``req`` object in the logs. - General ^^^^^^^ diff --git a/sap/cf_logging/core/constants.py b/sap/cf_logging/core/constants.py index 98a2dea..2005287 100644 --- a/sap/cf_logging/core/constants.py +++ b/sap/cf_logging/core/constants.py @@ -5,3 +5,5 @@ LOG_SENSITIVE_CONNECTION_DATA = 'LOG_SENSITIVE_CONNECTION_DATA' LOG_REMOTE_USER = 'LOG_REMOTE_USER' LOG_REFERER = 'LOG_REFERER' + +STACKTRACE_MAX_SIZE = 55 * 1024 diff --git a/sap/cf_logging/falcon_logging/__init__.py b/sap/cf_logging/falcon_logging/__init__.py index 8c94a6b..51259a3 100644 --- a/sap/cf_logging/falcon_logging/__init__.py +++ b/sap/cf_logging/falcon_logging/__init__.py @@ -15,13 +15,13 @@ FALCON_FRAMEWORK_NAME = 'falcon.framework' -class LoggingMiddleware: +class LoggingMiddleware(object): """ Falcon logging middleware """ def __init__(self, logger_name='cf.falcon.logger'): self._logger_name = logger_name - def process_request(self, request, response): # pylint: disable=unused-argument + def process_request(self, request, response): # pylint: disable=unused-argument,no-self-use """Process the request before routing it. :param request: - Falcon Request object @@ -31,8 +31,6 @@ def process_request(self, request, response): # pylint: disable=unused-argument cid = framework.request_reader.get_correlation_id(request) framework.context.set_correlation_id(cid, request) framework.context.set('request_started_at', datetime.utcnow(), request) - request.log = lambda msg, lvl=logging.INFO, extra={}: logging.getLogger( - self._logger_name).log(lvl, msg, extra=extra.update({REQUEST_KEY: request}) or extra) def process_response(self, request, response, resource, req_succeeded): # pylint: disable=unused-argument """Post-processing of the response (after routing). diff --git a/sap/cf_logging/formatters/stacktrace_formatter.py b/sap/cf_logging/formatters/stacktrace_formatter.py new file mode 100644 index 0000000..3b908f2 --- /dev/null +++ b/sap/cf_logging/formatters/stacktrace_formatter.py @@ -0,0 +1,51 @@ +""" +Module for formatting utilities for stacktrace +generated by user logging.exception call +""" +import re + +from sap.cf_logging.core import constants + + +def format_stacktrace(stacktrace): + """ + Removes newline and tab characters + Truncates stacktrace to maximum size + + :param stacktrace: string representation of a stacktrace + """ + if not isinstance(stacktrace, str): + return '' + + stacktrace = re.sub('\n|\t', ' ', stacktrace) + + if len(stacktrace) <= constants.STACKTRACE_MAX_SIZE: + return stacktrace + + stacktrace_beginning = _stacktrace_beginning( + stacktrace, constants.STACKTRACE_MAX_SIZE // 3 + ) + + stacktrace_end = _stacktrace_end( + stacktrace, (constants.STACKTRACE_MAX_SIZE // 3) * 2 + ) + + new_stacktrace = "-------- STACK TRACE TRUNCATED --------" + stacktrace_beginning +\ + "-------- OMITTED --------" + stacktrace_end + + return new_stacktrace + +def _stacktrace_beginning(stacktrace, size): + """ Gets the first `size` bytes of the stacktrace """ + if len(stacktrace) <= size: + return stacktrace + + return stacktrace[:size] + +def _stacktrace_end(stacktrace, size): + """ Gets the last `size` bytes of the stacktrace """ + stacktrace_length = len(stacktrace) + if stacktrace_length <= size: + return stacktrace + + return stacktrace[:-(stacktrace_length-size)] diff --git a/sap/cf_logging/record/simple_log_record.py b/sap/cf_logging/record/simple_log_record.py index 256f12d..47cc7d3 100644 --- a/sap/cf_logging/record/simple_log_record.py +++ b/sap/cf_logging/record/simple_log_record.py @@ -1,11 +1,15 @@ """ Module SimpleLogRecord """ import logging +import traceback + from datetime import datetime from sap.cf_logging import defaults from sap.cf_logging.core.constants import REQUEST_KEY, RESPONSE_KEY from sap.cf_logging.record import application_info from sap.cf_logging.record import util +from sap.cf_logging.formatters.stacktrace_formatter import format_stacktrace + _SKIP_ATTRIBUTES = ["type", "written_at", "written_ts", "correlation_id", "remote_user", "referer", "x_forwarded_for", "protocol", "method", "remote_ip", "request_size_b", "remote_host", "remote_port", "request_received_at", "direction", @@ -63,5 +67,9 @@ def format(self): 'msg': self.getMessage(), }) + if self.levelno == logging.ERROR: + stacktrace = ''.join(traceback.format_exception(*self.exc_info)) + record['stacktrace'] = format_stacktrace(stacktrace) + record.update(self.extra) return record diff --git a/tests/log_schemas.py b/tests/log_schemas.py index 6b215f7..c4e7641 100644 --- a/tests/log_schemas.py +++ b/tests/log_schemas.py @@ -26,7 +26,7 @@ 'written_at': u.iso_datetime(), 'written_ts': u.pos_num(), 'msg': u.string(u.WORD), - 'component_type': u.string(r'^application$') + 'component_type': u.string(r'^application$'), }) WEB_LOG_SCHEMA = u.extend(CF_ATTRIBUTES_SCHEMA, { diff --git a/tests/test_falcon_logging.py b/tests/test_falcon_logging.py index ec48427..4e442c7 100644 --- a/tests/test_falcon_logging.py +++ b/tests/test_falcon_logging.py @@ -6,11 +6,17 @@ from falcon_auth import FalconAuthMiddleware, BasicAuthBackend from sap import cf_logging from sap.cf_logging import falcon_logging +from sap.cf_logging.core.constants import REQUEST_KEY from tests.log_schemas import WEB_LOG_SCHEMA, JOB_LOG_SCHEMA from tests.common_test_params import ( v_str, auth_basic, get_web_record_header_fixtures ) -from tests.util import check_log_record, config_root_logger, enable_sensitive_fields_logging +from tests.util import ( + check_log_record, + config_root_logger, + enable_sensitive_fields_logging, + config_logger +) # pylint: disable=protected-access, missing-docstring,too-few-public-methods @@ -101,16 +107,16 @@ def _set_up_falcon_logging(app, *args): falcon_logging.init(app, logging.DEBUG, *args) -class UserResourceRoute: - +class UserResourceRoute(object): def __init__(self, extra, expected): self.extra = extra self.expected = expected + self.logger, self.stream = config_logger('user.logging') def on_get(self, req, resp): - _, stream = config_root_logger('user.logging') - req.log('in route headers', extra=self.extra) - assert check_log_record(stream, JOB_LOG_SCHEMA, self.expected) == {} + self.extra.update({REQUEST_KEY: req}) + self.logger.log(logging.INFO, 'in route headers', extra=self.extra) + assert check_log_record(self.stream, JOB_LOG_SCHEMA, self.expected) == {} resp.set_header('Content-Type', 'text/plain') resp.status = falcon.HTTP_200 diff --git a/tests/test_flask_logging.py b/tests/test_flask_logging.py index dd1b4a2..db90440 100644 --- a/tests/test_flask_logging.py +++ b/tests/test_flask_logging.py @@ -7,7 +7,11 @@ from sap.cf_logging import flask_logging from tests.log_schemas import WEB_LOG_SCHEMA, JOB_LOG_SCHEMA from tests.common_test_params import v_str, v_num, auth_basic, get_web_record_header_fixtures -from tests.util import check_log_record, config_root_logger, enable_sensitive_fields_logging +from tests.util import ( + check_log_record, + enable_sensitive_fields_logging, + config_root_logger, +) # pylint: disable=protected-access @@ -26,6 +30,7 @@ def test_flask_requires_valid_app(): @pytest.fixture(autouse=True) def before_each(): + # pylint: disable=duplicate-code """ enable all fields to be logged """ enable_sensitive_fields_logging() yield diff --git a/tests/test_job_logging.py b/tests/test_job_logging.py index 3290d82..cf982d2 100644 --- a/tests/test_job_logging.py +++ b/tests/test_job_logging.py @@ -46,6 +46,23 @@ def test_set_correlation_id(): assert log_json['correlation_id'] == correlation_id assert cf_logging.FRAMEWORK.context.get_correlation_id() == correlation_id + +def test_exception_stacktrace(): + """ Test exception stacktrace is logged """ + cf_logging.init(level=logging.DEBUG) + logger, stream = config_root_logger('cli.test') + + try: + return 1 / 0 + except ZeroDivisionError: + logger.exception('zero division error') + log_json = JSONDecoder().decode(stream.getvalue()) + _, error = JsonValidator(JOB_LOG_SCHEMA).validate(log_json) + + assert error == {} + assert 'ZeroDivisionError' in str(log_json['stacktrace']) + + def test_thread_safety(): """ test context keeps separate correlation ID per thread """ class _SampleThread(threading.Thread): diff --git a/tests/test_sanic_logging.py b/tests/test_sanic_logging.py index df338b8..c28b584 100644 --- a/tests/test_sanic_logging.py +++ b/tests/test_sanic_logging.py @@ -9,7 +9,11 @@ from tests.log_schemas import WEB_LOG_SCHEMA, JOB_LOG_SCHEMA from tests.common_test_params import v_str, v_num, get_web_record_header_fixtures from tests.schema_util import extend -from tests.util import check_log_record, config_logger, enable_sensitive_fields_logging +from tests.util import ( + check_log_record, + config_logger, + enable_sensitive_fields_logging, +) # pylint: disable=protected-access @@ -22,12 +26,14 @@ def test_sanic_requires_valid_app(): FIXTURE = get_web_record_header_fixtures() FIXTURE.append(({'no-content-length': '1'}, {'response_size_b': v_num(-1)})) + @pytest.fixture(autouse=True) def before_each(): """ enable all fields to be logged """ enable_sensitive_fields_logging() yield + @pytest.mark.parametrize("headers, expected", FIXTURE) def test_sanic_request_log(headers, expected): """ Test that the JSON logs contain the expected properties based on the @@ -45,7 +51,8 @@ async def _headers_route(request): _, stream = config_logger('cf.sanic.logger') client = app.test_client - _check_expected_response(client.get('/test/path', headers=headers)[1], 200, 'ok') + _check_expected_response(client.get( + '/test/path', headers=headers)[1], 200, 'ok') assert check_log_record(stream, WEB_LOG_SCHEMA, expected) == {} diff --git a/tests/unit/formatters/test_stacktrace_formatter.py b/tests/unit/formatters/test_stacktrace_formatter.py new file mode 100644 index 0000000..ffc4efd --- /dev/null +++ b/tests/unit/formatters/test_stacktrace_formatter.py @@ -0,0 +1,24 @@ +""" Module testing the functionality of the StacktraceFormatter class """ +from sap.cf_logging.formatters.stacktrace_formatter import format_stacktrace + + +STACKTRACE = ''.join(['Traceback (most recent call last):\n', + 'File "nonexistent_file.py", line 100, in nonexistent_function\n', + 'raise ValueError("Oh no")\n', + 'ValueError: Oh no']) + + +def test_stacktrace_not_truncated(): + """ Test that stacktrace is not truncated when smaller than the stacktrace maximum size """ + formatted = format_stacktrace(STACKTRACE) + assert "TRUNCATED" not in formatted + assert "OMITTED" not in formatted + + +def test_stacktrace_truncated(monkeypatch): + """ Test that stacktrace is truncated when bigger than the stacktrace maximum size """ + monkeypatch.setattr('sap.cf_logging.core.constants.STACKTRACE_MAX_SIZE', 120) + + formatted = format_stacktrace(STACKTRACE) + assert "TRUNCATED" in formatted + assert "OMITTED" in formatted From 4358c3e0b56e99e12f1cc02d2fee00bf75c63645 Mon Sep 17 00:00:00 2001 From: Georgi Vachkov Date: Tue, 3 Jul 2018 17:33:51 +0300 Subject: [PATCH 04/35] Update pytest version (#29) --- test-requirements.txt | 2 +- tests/test_falcon_logging.py | 3 +-- tests/test_flask_logging.py | 6 +++--- tests/test_job_logging.py | 8 ++++---- tests/util.py | 8 -------- 5 files changed, 9 insertions(+), 18 deletions(-) diff --git a/test-requirements.txt b/test-requirements.txt index 188d8f5..4a0be2c 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -5,7 +5,7 @@ Flask sanic; python_version >= '3.5' aiohttp; python_version >= '3.5' sonic182-json-validator==0.0.12 -pytest==3.2.2 +pytest==3.6.2 pytest-cov==2.5.1 pytest-mock==1.6.3 pylint diff --git a/tests/test_falcon_logging.py b/tests/test_falcon_logging.py index 4e442c7..9e50e06 100644 --- a/tests/test_falcon_logging.py +++ b/tests/test_falcon_logging.py @@ -13,7 +13,6 @@ ) from tests.util import ( check_log_record, - config_root_logger, enable_sensitive_fields_logging, config_logger ) @@ -79,7 +78,7 @@ def test_falcon_request_logs_user(user): def _check_falcon_request_log(app, headers, expected): - _, stream = config_root_logger('cf.falcon.logger') + _, stream = config_logger('cf.falcon.logger') client = testing.TestClient(app) _check_expected_response( diff --git a/tests/test_flask_logging.py b/tests/test_flask_logging.py index db90440..ac60953 100644 --- a/tests/test_flask_logging.py +++ b/tests/test_flask_logging.py @@ -10,7 +10,7 @@ from tests.util import ( check_log_record, enable_sensitive_fields_logging, - config_root_logger, + config_logger, ) @@ -47,7 +47,7 @@ def _root(): return Response('ok', mimetype='text/plain') _set_up_flask_logging(app) - _, stream = config_root_logger('cf.flask.logger') + _, stream = config_logger('cf.flask.logger') client = app.test_client() _check_expected_response(client.get('/test/path', headers=headers)) @@ -77,7 +77,7 @@ def _user_logging(headers, extra, expected): @app.route('/test/user/logging') def _logging_correlation_id_route(): - logger, stream = config_root_logger('user.logging') + logger, stream = config_logger('user.logging') logger.info('in route headers', extra=extra) assert check_log_record(stream, JOB_LOG_SCHEMA, expected) == {} return Response('ok') diff --git a/tests/test_job_logging.py b/tests/test_job_logging.py index cf982d2..2e48415 100644 --- a/tests/test_job_logging.py +++ b/tests/test_job_logging.py @@ -8,7 +8,7 @@ from json_validator.validator import JsonValidator from sap import cf_logging from tests.log_schemas import JOB_LOG_SCHEMA -from tests.util import config_root_logger +from tests.util import config_logger # pylint: disable=protected-access @@ -22,7 +22,7 @@ def before_each(): def test_log_in_expected_format(): """ Test the cf_logger as a standalone """ cf_logging.init(level=logging.DEBUG) - logger, stream = config_root_logger('cli.test') + logger, stream = config_logger('cli.test') logger.info('hi') log_json = JSONDecoder().decode(stream.getvalue()) _, error = JsonValidator(JOB_LOG_SCHEMA).validate(log_json) @@ -36,7 +36,7 @@ def test_set_correlation_id(): cf_logging.init(level=logging.DEBUG) cf_logging.FRAMEWORK.context.set_correlation_id(correlation_id) - logger, stream = config_root_logger('cli.test') + logger, stream = config_logger('cli.test') logger.info('hi') log_json = JSONDecoder().decode(stream.getvalue()) @@ -50,7 +50,7 @@ def test_set_correlation_id(): def test_exception_stacktrace(): """ Test exception stacktrace is logged """ cf_logging.init(level=logging.DEBUG) - logger, stream = config_root_logger('cli.test') + logger, stream = config_logger('cli.test') try: return 1 / 0 diff --git a/tests/util.py b/tests/util.py index 4afa5dc..04c7086 100644 --- a/tests/util.py +++ b/tests/util.py @@ -25,14 +25,6 @@ def check_log_record(stream, schema, expected): return error -def config_root_logger(logger_name): - """ Function to configure a JSONLogger and print the output into a stream""" - stream = io.StringIO() - logging.getLogger().handlers[0].stream = stream - logger = logging.getLogger(logger_name) - return logger, stream - - def config_logger(logger_name): """ Function to configure a JSONLogger and print the output into a stream""" stream = io.StringIO() From 8df3a2296ead63e87f3639bcfa00b4e1c5a95a93 Mon Sep 17 00:00:00 2001 From: Georgi Vachkov Date: Wed, 4 Jul 2018 10:57:51 +0300 Subject: [PATCH 05/35] Version 4.0.0 --- CHANGELOG.md | 8 ++++++++ sap/cf_logging/__init__.py | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 88670de..8ff4b6f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,14 @@ This project adheres to [Semantic Versioning](http://semver.org/). The format is based on [Keep a Changelog](http://keepachangelog.com/). +## 4.0.0 - 2018-07-04 + +### Added + - Log exception stacktrace + +### Changed + - Incompatible change: removed `log` function from request in falcon support + ## 3.3.1 - 2018-06-18 ### Fixed diff --git a/sap/cf_logging/__init__.py b/sap/cf_logging/__init__.py index be194c1..ef14dad 100644 --- a/sap/cf_logging/__init__.py +++ b/sap/cf_logging/__init__.py @@ -10,7 +10,7 @@ from sap.cf_logging.record.request_log_record import RequestWebRecord from sap.cf_logging.record.simple_log_record import SimpleLogRecord -__version__ = '3.3.1' +__version__ = '4.0.0' _SETUP_DONE = False FRAMEWORK = None From 3834f108cad8797728ffc58146d5ba812cdfdb76 Mon Sep 17 00:00:00 2001 From: Georgi Vachkov Date: Tue, 10 Jul 2018 09:46:12 +0300 Subject: [PATCH 06/35] Fix log error throws an exception (#31) --- sap/cf_logging/record/simple_log_record.py | 2 +- tests/schema_util.py | 2 +- tests/test_job_logging.py | 11 +++++++++-- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/sap/cf_logging/record/simple_log_record.py b/sap/cf_logging/record/simple_log_record.py index 47cc7d3..9c030b0 100644 --- a/sap/cf_logging/record/simple_log_record.py +++ b/sap/cf_logging/record/simple_log_record.py @@ -67,7 +67,7 @@ def format(self): 'msg': self.getMessage(), }) - if self.levelno == logging.ERROR: + if self.levelno == logging.ERROR and self.exc_info: stacktrace = ''.join(traceback.format_exception(*self.exc_info)) record['stacktrace'] = format_stacktrace(stacktrace) diff --git a/tests/schema_util.py b/tests/schema_util.py index a355d0e..98e230b 100644 --- a/tests/schema_util.py +++ b/tests/schema_util.py @@ -6,7 +6,7 @@ IP = r'[[0-9]+|.?]+\d$' HOST_NAME = r'[[0-9]+|.?]+\d$' -LEVEL = ['CRITICAL', 'ERROR', 'WARNING', 'INFO', 'NOTSET'] +LEVEL = ['CRITICAL', 'ERROR', 'WARNING', 'INFO', 'DEBUG', 'NOTSET'] def num(val=None): diff --git a/tests/test_job_logging.py b/tests/test_job_logging.py index 2e48415..29c9706 100644 --- a/tests/test_job_logging.py +++ b/tests/test_job_logging.py @@ -19,11 +19,18 @@ def before_each(): cf_logging._SETUP_DONE = False -def test_log_in_expected_format(): +@pytest.mark.parametrize('log_callback', [ + lambda logger, msg: logger.debug('message: %s', msg), + lambda logger, msg: logger.info('message: %s', msg), + lambda logger, msg: logger.warning('message: %s', msg), + lambda logger, msg: logger.error('message: %s', msg), + lambda logger, msg: logger.critical('message: %s', msg) +]) +def test_log_in_expected_format(log_callback): """ Test the cf_logger as a standalone """ cf_logging.init(level=logging.DEBUG) logger, stream = config_logger('cli.test') - logger.info('hi') + log_callback(logger, 'hi') log_json = JSONDecoder().decode(stream.getvalue()) _, error = JsonValidator(JOB_LOG_SCHEMA).validate(log_json) From 32199a958ba61247a271368f64aeb592a063d390 Mon Sep 17 00:00:00 2001 From: Georgi Vachkov Date: Tue, 10 Jul 2018 09:50:59 +0300 Subject: [PATCH 07/35] Version 4.0.1 --- CHANGELOG.md | 6 ++++++ sap/cf_logging/__init__.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ff4b6f..63f06bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ This project adheres to [Semantic Versioning](http://semver.org/). The format is based on [Keep a Changelog](http://keepachangelog.com/). +## 4.0.1 - 2018-07-10 + +### Fixed + + - Log error throws an exception + ## 4.0.0 - 2018-07-04 ### Added diff --git a/sap/cf_logging/__init__.py b/sap/cf_logging/__init__.py index ef14dad..ac556c1 100644 --- a/sap/cf_logging/__init__.py +++ b/sap/cf_logging/__init__.py @@ -10,7 +10,7 @@ from sap.cf_logging.record.request_log_record import RequestWebRecord from sap.cf_logging.record.simple_log_record import SimpleLogRecord -__version__ = '4.0.0' +__version__ = '4.0.1' _SETUP_DONE = False FRAMEWORK = None From e4d413c313cf9da8379fcddae94b68fefc31ad1b Mon Sep 17 00:00:00 2001 From: georgi-bozhinov <40486425+georgi-bozhinov@users.noreply.github.com> Date: Fri, 27 Jul 2018 10:22:13 +0300 Subject: [PATCH 08/35] Feature django logging (#30) * Setup django app config for testing purposes * Add django context, req reader and resp reader * Add django logging support * Export logging middleware to be added in django app for logging capabilities * Write tests for logging in sample django app * Fix pylint issues * Rename django test app to be more descriptive * Add README notes for django support * Fix some errors in docstrings * Apply pull request comment changes * Make pylint version fixed * Remove readme note about django versions --- README.rst | 68 +++++++++++++++ conftest.py | 1 + sap/cf_logging/django_logging/__init__.py | 64 ++++++++++++++ sap/cf_logging/django_logging/context.py | 24 ++++++ .../django_logging/request_reader.py | 34 ++++++++ .../django_logging/response_reader.py | 15 ++++ test-requirements.txt | 3 +- tests/common_test_params.py | 2 +- tests/django_logging/__init__.py | 0 tests/django_logging/test_app/__init__.py | 0 tests/django_logging/test_app/settings.py | 17 ++++ .../test_app/test_django_logging.py | 85 +++++++++++++++++++ tests/django_logging/test_app/urls.py | 9 ++ tests/django_logging/test_app/views.py | 34 ++++++++ tests/test_falcon_logging.py | 6 +- 15 files changed, 357 insertions(+), 5 deletions(-) create mode 100644 sap/cf_logging/django_logging/__init__.py create mode 100644 sap/cf_logging/django_logging/context.py create mode 100644 sap/cf_logging/django_logging/request_reader.py create mode 100644 sap/cf_logging/django_logging/response_reader.py create mode 100644 tests/django_logging/__init__.py create mode 100644 tests/django_logging/test_app/__init__.py create mode 100644 tests/django_logging/test_app/settings.py create mode 100644 tests/django_logging/test_app/test_django_logging.py create mode 100644 tests/django_logging/test_app/urls.py create mode 100644 tests/django_logging/test_app/views.py diff --git a/README.rst b/README.rst index 7a3f5e6..17472d9 100644 --- a/README.rst +++ b/README.rst @@ -23,6 +23,7 @@ Features * `Flask 0.1x `__ * `Sanic 0.5.x `__ * `Falcon `__ + * `Django `__ * Extensible to support others 6. Includes CF-specific information (space id, app id, etc.) to logs. @@ -137,6 +138,73 @@ Falcon app.add_route('/resource', Resource()) falcon_logging.init(app) +Django +^^^^^^ + +.. code:: bash + + django-admin startproject example + +.. code:: python + + # example/settings.py + + MIDDLEWARES = [ + # ..., + 'sap.cf_logging.django_logging.LoggingMiddleware' + ] + + # example/wsgi.py + + # ... + from sap.cf_logging import django_logging + + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "sap_logtesting.settings") + django_logging.init() + + # ... + +Create a new app + +.. code:: bash + + python manage.py startapp example_app + +.. code:: python + + # example_app/views.py + + import logging + + from django.http import HTTPResponse + from sap.cf_logging.core.constants import REQUEST_KEY + + def index(request): + extra = {REQUEST_KEY: request} + logger = logging.getLogger('my.logger') + logger.info("Resource requested", extra=extra) + return HttpResponse("ok") + + # example_app/urls.py + + from django.conf.urls import url + + from . import views + + urlpatterns = [ + url('^$', views.index) + ] + + # example/urls.py + + from django.contrib import admin + from django.conf.urls import url, include + + urlpatterns = [ + url('admin/', admin.site.urls), + url('example/', include('example_app.urls')) + ] + General ^^^^^^^ diff --git a/conftest.py b/conftest.py index 2912813..3f00811 100644 --- a/conftest.py +++ b/conftest.py @@ -1,4 +1,5 @@ import sys + if sys.version_info < (3, 5): collect_ignore = ['tests/test_sanic_logging.py'] diff --git a/sap/cf_logging/django_logging/__init__.py b/sap/cf_logging/django_logging/__init__.py new file mode 100644 index 0000000..b8cb95d --- /dev/null +++ b/sap/cf_logging/django_logging/__init__.py @@ -0,0 +1,64 @@ +""" Logging support for Django based applications """ +import logging +from datetime import datetime + +from sap import cf_logging +from sap.cf_logging import defaults +from sap.cf_logging.core.constants import REQUEST_KEY, RESPONSE_KEY +from sap.cf_logging.core.framework import Framework +from sap.cf_logging.django_logging.context import DjangoContext +from sap.cf_logging.django_logging.request_reader import DjangoRequestReader +from sap.cf_logging.django_logging.response_reader import DjangoResponseReader + +DJANGO_FRAMEWORK_NAME = 'django.framework' + + +class LoggingMiddleware(object): + """ Django logging middleware """ + + def __init__(self, get_response, logger_name='cf.django.logger'): + self._logger_name = logger_name + self._get_response = get_response + + def __call__(self, request): + self.process_request(request) + response = self._get_response(request) + + response = self.process_response(request, response) + return response + + def process_request(self, request):# pylint: disable=no-self-use + """ + Process the request before routing it. + + :param request: - Django Request object + """ + framework = cf_logging.FRAMEWORK + cid = framework.request_reader.get_correlation_id(request) + framework.context.set_correlation_id(cid, request) + framework.context.set('request_started_at', datetime.utcnow(), request) + + def process_response(self, request, response): + """ + Post-processing of the response (after routing). + + :param request: - Django Request object + :param request: - Django Response object + """ + cf_logging.FRAMEWORK.context.set( + 'response_sent_at', datetime.utcnow(), request) + extra = {REQUEST_KEY: request, RESPONSE_KEY: response} + logging.getLogger(self._logger_name).info('', extra=extra) + return response + + +def init(level=defaults.DEFAULT_LOGGING_LEVEL): + """ + Initializes logging in JSON format. + + :param level: - valid log level from standard logging package (optional) + """ + framework = Framework(DJANGO_FRAMEWORK_NAME, DjangoContext(), + DjangoRequestReader(), DjangoResponseReader()) + + cf_logging.init(framework, level) diff --git a/sap/cf_logging/django_logging/context.py b/sap/cf_logging/django_logging/context.py new file mode 100644 index 0000000..ea1acea --- /dev/null +++ b/sap/cf_logging/django_logging/context.py @@ -0,0 +1,24 @@ +""" +Django logging context - used by the logging package to keep +request specific data, needed for logging purposes. +For example correlation_id needs to be stored during request processing, +so all log entries contain it. +""" + +from sap.cf_logging.core.context import Context + + +def _init_context(request): + if not hasattr(request, 'context'): + request.context = {} + +class DjangoContext(Context): + """ Stores logging context in Django's request objecct """ + + def set(self, key, value, request): + _init_context(request) + request.context[key] = value + + def get(self, key, request): + _init_context(request) + return request.context.get(key) if request else None diff --git a/sap/cf_logging/django_logging/request_reader.py b/sap/cf_logging/django_logging/request_reader.py new file mode 100644 index 0000000..e912974 --- /dev/null +++ b/sap/cf_logging/django_logging/request_reader.py @@ -0,0 +1,34 @@ +""" Django request reader """ + +from sap.cf_logging import defaults +from sap.cf_logging.core.request_reader import RequestReader + + +class DjangoRequestReader(RequestReader): + """ Read log related properties out of Django request """ + + def get_remote_user(self, request): + return request.META.get('REMOTE_USER') or defaults.UNKNOWN + + def get_protocol(self, request): + return request.scheme + + def get_content_length(self, request): + return request.META.get('CONTENT_LENGTH') or defaults.UNKNOWN + + def get_remote_ip(self, request): + return request.META.get('REMOTE_ADDR') + + def get_remote_port(self, request): + return request.META.get('SERVER_PORT') or defaults.UNKNOWN + + def get_http_header(self, request, header_name, default=None): + if request is None: + return default + + if header_name in request.META: + return request.META.get(header_name) + if header_name.upper() in request.META: + return request.META.get(header_name.upper()) + + return default diff --git a/sap/cf_logging/django_logging/response_reader.py b/sap/cf_logging/django_logging/response_reader.py new file mode 100644 index 0000000..7338f93 --- /dev/null +++ b/sap/cf_logging/django_logging/response_reader.py @@ -0,0 +1,15 @@ +""" Django response reader """ +from sap.cf_logging.core.response_reader import ResponseReader + + +class DjangoResponseReader(ResponseReader): + """ Read log related properties out of Django response """ + + def get_status_code(self, response): + return response.status_code + + def get_response_size(self, response): + return len(response.content) + + def get_content_type(self, response): + return response.get('Content-Type') diff --git a/test-requirements.txt b/test-requirements.txt index 4a0be2c..4de55c8 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -8,5 +8,6 @@ sonic182-json-validator==0.0.12 pytest==3.6.2 pytest-cov==2.5.1 pytest-mock==1.6.3 -pylint +pylint==1.9.2 tox +django diff --git a/tests/common_test_params.py b/tests/common_test_params.py index 78debfc..e971738 100644 --- a/tests/common_test_params.py +++ b/tests/common_test_params.py @@ -22,7 +22,7 @@ def get_web_record_header_fixtures(): 'response_content_type': v_str('text/plain'), 'request': v_str('/test/path')}) ] - for header in ['x-Correlation-ID', 'X-CorrelationID', 'X-Request-ID', 'X-Vcap-Request-Id']: + for header in ['X-Correlation-ID', 'X-CorrelationID', 'X-Request-ID', 'X-Vcap-Request-Id']: test_cases.append(({header: '298ebf9d-be1d-11e7-88ff-2c44fd152864'}, {'correlation_id': v_str('298ebf9d-be1d-11e7-88ff-2c44fd152864')})) diff --git a/tests/django_logging/__init__.py b/tests/django_logging/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/django_logging/test_app/__init__.py b/tests/django_logging/test_app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/django_logging/test_app/settings.py b/tests/django_logging/test_app/settings.py new file mode 100644 index 0000000..11df7b2 --- /dev/null +++ b/tests/django_logging/test_app/settings.py @@ -0,0 +1,17 @@ +""" Example django test app settings """ +SECRET_KEY = 'fake-key' +INSTALLED_APPS = [ + "tests", +] + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3' + } +} + +ROOT_URLCONF = 'tests.django_logging.test_app.urls' + +MIDDLEWARE = [ + 'sap.cf_logging.django_logging.LoggingMiddleware', +] diff --git a/tests/django_logging/test_app/test_django_logging.py b/tests/django_logging/test_app/test_django_logging.py new file mode 100644 index 0000000..4a74bde --- /dev/null +++ b/tests/django_logging/test_app/test_django_logging.py @@ -0,0 +1,85 @@ +""" Module that tests the integration of cf_logging with Django """ +import sys +import os +import pytest + +from django.test import Client +from django.conf.urls import url +from django.conf import settings + +from sap import cf_logging +from sap.cf_logging import django_logging +from tests.log_schemas import WEB_LOG_SCHEMA +from tests.common_test_params import ( + v_str, get_web_record_header_fixtures +) +from tests.util import ( + check_log_record, + enable_sensitive_fields_logging, + config_logger +) + +from tests.django_logging.test_app.views import UserLoggingView + + +os.environ['DJANGO_SETTINGS_MODULE'] = 'tests.django_logging.test_app.settings' + + +@pytest.fixture(autouse=True) +def before_each(): + """ enable all fields to be logged """ + enable_sensitive_fields_logging() + yield + + +FIXTURE = get_web_record_header_fixtures() + +@pytest.mark.parametrize('headers, expected', FIXTURE) +def test_django_request_log(headers, expected): + """ That the expected records are logged by the logging library """ + _set_up_django_logging() + _check_django_request_log(headers, expected) + + +def test_web_log(): + """ That the custom properties are logged """ + _user_logging({}, {'myproperty': 'myval'}, {'myproperty': v_str('myval')}) + + +def test_correlation_id(): + """ Test the correlation id is logged when coming from the headers """ + _user_logging( + {'X-CorrelationID': '298ebf9d-be1d-11e7-88ff-2c44fd152860'}, + {}, + {'correlation_id': v_str('298ebf9d-be1d-11e7-88ff-2c44fd152860')} + ) + + +def _check_django_request_log(headers, expected): + _, stream = config_logger('cf.django.logger') + + client = Client() + _check_expected_response(client.get('/test/path', **headers), body='Hello test!') + assert check_log_record(stream, WEB_LOG_SCHEMA, expected) == {} + + +# Helper functions +def _set_up_django_logging(): + cf_logging._SETUP_DONE = False # pylint: disable=protected-access + django_logging.init() + + +def _check_expected_response(response, status_code=200, body='ok'): + assert response.status_code == status_code + if body is not None: + assert response.content.decode() == body + +def _user_logging(headers, extra, expected): + sys.modules[settings.ROOT_URLCONF].urlpatterns.append( + url('^test/user/logging$', UserLoggingView.as_view(), + {'extra': extra, 'expected': expected})) + + + _set_up_django_logging() + client = Client() + _check_expected_response(client.get('/test/user/logging', **headers)) diff --git a/tests/django_logging/test_app/urls.py b/tests/django_logging/test_app/urls.py new file mode 100644 index 0000000..f4c2337 --- /dev/null +++ b/tests/django_logging/test_app/urls.py @@ -0,0 +1,9 @@ +""" Urls for example django test app """ +from django.conf.urls import url + +from tests.django_logging.test_app.views import IndexView + +# pylint: disable=invalid-name +urlpatterns = [ + url("^test/path$", IndexView.as_view(), name='log-index') +] diff --git a/tests/django_logging/test_app/views.py b/tests/django_logging/test_app/views.py new file mode 100644 index 0000000..d204795 --- /dev/null +++ b/tests/django_logging/test_app/views.py @@ -0,0 +1,34 @@ +""" Views for example django test app """ +import logging + +from django.http import HttpResponse +from django.views import generic + +from sap.cf_logging.core.constants import REQUEST_KEY +from tests.util import config_logger, check_log_record +from tests.log_schemas import JOB_LOG_SCHEMA + +# pylint: disable=unused-argument + +class IndexView(generic.View): + """ View that is hit on the index route """ + def get(self, request): # pylint: disable=no-self-use + """ Return a basic http response """ + return HttpResponse("Hello test!", content_type='text/plain') + + +class UserLoggingView(generic.View): + """ View that logs custom user information """ + def __init__(self, *args, **kwargs): + self.logger, self.stream = config_logger('user.logging') + super(UserLoggingView, self).__init__(*args, **kwargs) + + def get(self, request, *args, **kwargs): + """ Log a custom user message with the logger """ + expected = kwargs.get('expected') or {} + extra = kwargs.get('extra') or {} + extra.update({REQUEST_KEY: request}) + + self.logger.log(logging.INFO, 'in route headers', extra=extra) + assert check_log_record(self.stream, JOB_LOG_SCHEMA, expected) == {} + return HttpResponse("ok", content_type='text/plain') diff --git a/tests/test_falcon_logging.py b/tests/test_falcon_logging.py index 9e50e06..53d56d3 100644 --- a/tests/test_falcon_logging.py +++ b/tests/test_falcon_logging.py @@ -13,8 +13,8 @@ ) from tests.util import ( check_log_record, - enable_sensitive_fields_logging, - config_logger + config_logger, + enable_sensitive_fields_logging ) @@ -94,7 +94,7 @@ def test_web_log(): def test_correlation_id(): """ Test the correlation id is logged when coming from the headers """ _user_logging( - {'X-CorrelationID': '298ebf9d-be1d-11e7-88ff-2c44fd152860'}, + {'X-Correlation-ID': '298ebf9d-be1d-11e7-88ff-2c44fd152860'}, {}, {'correlation_id': v_str('298ebf9d-be1d-11e7-88ff-2c44fd152860')} ) From 96ccedc59cc2f4b8928d99ae8582d467c7e04f72 Mon Sep 17 00:00:00 2001 From: georgi-bozhinov <40486425+georgi-bozhinov@users.noreply.github.com> Date: Wed, 12 Sep 2018 14:51:45 +0300 Subject: [PATCH 09/35] Fix/django attribute error (#35) * Check if request is None when creating django context * Add test for missing request to django logging --- sap/cf_logging/django_logging/context.py | 4 ++++ .../test_app/test_django_logging.py | 18 +++++++++++++++--- tests/django_logging/test_app/views.py | 5 ++++- 3 files changed, 23 insertions(+), 4 deletions(-) diff --git a/sap/cf_logging/django_logging/context.py b/sap/cf_logging/django_logging/context.py index ea1acea..c35cc9b 100644 --- a/sap/cf_logging/django_logging/context.py +++ b/sap/cf_logging/django_logging/context.py @@ -16,9 +16,13 @@ class DjangoContext(Context): """ Stores logging context in Django's request objecct """ def set(self, key, value, request): + if request is None: + return _init_context(request) request.context[key] = value def get(self, key, request): + if request is None: + return None _init_context(request) return request.context.get(key) if request else None diff --git a/tests/django_logging/test_app/test_django_logging.py b/tests/django_logging/test_app/test_django_logging.py index 4a74bde..a60cb8d 100644 --- a/tests/django_logging/test_app/test_django_logging.py +++ b/tests/django_logging/test_app/test_django_logging.py @@ -51,7 +51,18 @@ def test_correlation_id(): _user_logging( {'X-CorrelationID': '298ebf9d-be1d-11e7-88ff-2c44fd152860'}, {}, - {'correlation_id': v_str('298ebf9d-be1d-11e7-88ff-2c44fd152860')} + {'correlation_id': v_str('298ebf9d-be1d-11e7-88ff-2c44fd152860')}, + True + ) + + +def test_missing_request(): + """ That the correlation id is missing when the request is missing """ + _user_logging( + {'X-CorrelationID': '298ebf9d-be1d-11e7-88ff-2c44fd152860'}, + {}, + {'correlation_id': v_str('-')}, + False ) @@ -74,9 +85,10 @@ def _check_expected_response(response, status_code=200, body='ok'): if body is not None: assert response.content.decode() == body -def _user_logging(headers, extra, expected): + +def _user_logging(headers, extra, expected, provide_request=False): sys.modules[settings.ROOT_URLCONF].urlpatterns.append( - url('^test/user/logging$', UserLoggingView.as_view(), + url('^test/user/logging$', UserLoggingView.as_view(provide_request=provide_request), {'extra': extra, 'expected': expected})) diff --git a/tests/django_logging/test_app/views.py b/tests/django_logging/test_app/views.py index d204795..a01b26c 100644 --- a/tests/django_logging/test_app/views.py +++ b/tests/django_logging/test_app/views.py @@ -19,6 +19,8 @@ def get(self, request): # pylint: disable=no-self-use class UserLoggingView(generic.View): """ View that logs custom user information """ + provide_request = False + def __init__(self, *args, **kwargs): self.logger, self.stream = config_logger('user.logging') super(UserLoggingView, self).__init__(*args, **kwargs) @@ -27,7 +29,8 @@ def get(self, request, *args, **kwargs): """ Log a custom user message with the logger """ expected = kwargs.get('expected') or {} extra = kwargs.get('extra') or {} - extra.update({REQUEST_KEY: request}) + if self.provide_request: + extra.update({REQUEST_KEY: request}) self.logger.log(logging.INFO, 'in route headers', extra=extra) assert check_log_record(self.stream, JOB_LOG_SCHEMA, expected) == {} From 5081f17d1a723311d3f7ee2fc68a8e51c3d7fe9f Mon Sep 17 00:00:00 2001 From: Georgi Vachkov Date: Thu, 13 Sep 2018 16:09:10 +0300 Subject: [PATCH 10/35] Version 4.1.0 (#36) --- CHANGELOG.md | 7 +++++++ sap/cf_logging/__init__.py | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 63f06bb..02401de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,13 @@ This project adheres to [Semantic Versioning](http://semver.org/). The format is based on [Keep a Changelog](http://keepachangelog.com/). +## 4.1.0 - 2018-09-13 + +### Added + + - Django support + + ## 4.0.1 - 2018-07-10 ### Fixed diff --git a/sap/cf_logging/__init__.py b/sap/cf_logging/__init__.py index ac556c1..8a92667 100644 --- a/sap/cf_logging/__init__.py +++ b/sap/cf_logging/__init__.py @@ -10,7 +10,7 @@ from sap.cf_logging.record.request_log_record import RequestWebRecord from sap.cf_logging.record.simple_log_record import SimpleLogRecord -__version__ = '4.0.1' +__version__ = '4.1.0' _SETUP_DONE = False FRAMEWORK = None From 17c7cd677a7b38aba7cddd2096cb16085e79dae0 Mon Sep 17 00:00:00 2001 From: Georgi Vachkov Date: Fri, 19 Apr 2019 18:31:22 +0300 Subject: [PATCH 11/35] Fix logging not usable outside request (#37) --- sap/cf_logging/falcon_logging/context.py | 3 ++- sap/cf_logging/flask_logging/context.py | 5 +++-- test-requirements.txt | 2 +- tests/test_falcon_logging.py | 10 ++++++++++ tests/test_flask_logging.py | 9 +++++++++ 5 files changed, 25 insertions(+), 4 deletions(-) diff --git a/sap/cf_logging/falcon_logging/context.py b/sap/cf_logging/falcon_logging/context.py index 046ee4e..317385b 100644 --- a/sap/cf_logging/falcon_logging/context.py +++ b/sap/cf_logging/falcon_logging/context.py @@ -11,7 +11,8 @@ class FalconContext(Context): """ Stores logging context in Falcon's request object""" def set(self, key, value, request): - request.context[key] = value + if request: + request.context[key] = value def get(self, key, request): return request.context.get(key) if request else None diff --git a/sap/cf_logging/flask_logging/context.py b/sap/cf_logging/flask_logging/context.py index 0e3dd71..cd74ff9 100644 --- a/sap/cf_logging/flask_logging/context.py +++ b/sap/cf_logging/flask_logging/context.py @@ -11,7 +11,8 @@ class FlaskContext(Context): """ Stores logging context in Flask's request scope """ def set(self, key, value, request): - setattr(g, key, value) + if g: + setattr(g, key, value) def get(self, key, request): - return getattr(g, key, None) + return getattr(g, key, None) if g else None diff --git a/test-requirements.txt b/test-requirements.txt index 4de55c8..bdfbb1e 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -10,4 +10,4 @@ pytest-cov==2.5.1 pytest-mock==1.6.3 pylint==1.9.2 tox -django +django < 2.2 diff --git a/tests/test_falcon_logging.py b/tests/test_falcon_logging.py index 53d56d3..8c252b0 100644 --- a/tests/test_falcon_logging.py +++ b/tests/test_falcon_logging.py @@ -90,6 +90,16 @@ def test_web_log(): """ That the custom properties are logged """ _user_logging({}, {'myprop': 'myval'}, {'myprop': v_str('myval')}) +def test_logging_without_request(): + """ Test logger is usable in non request context """ + app = falcon.API() + _set_up_falcon_logging(app) + cf_logging.FRAMEWORK.context.set_correlation_id('value') + + logger, stream = config_logger('main.logger') + logger.info('works') + assert check_log_record(stream, JOB_LOG_SCHEMA, {'msg': v_str('works')}) == {} + def test_correlation_id(): """ Test the correlation id is logged when coming from the headers """ diff --git a/tests/test_flask_logging.py b/tests/test_flask_logging.py index ac60953..49a9754 100644 --- a/tests/test_flask_logging.py +++ b/tests/test_flask_logging.py @@ -66,6 +66,15 @@ def test_correlation_id(): {'correlation_id': v_str('298ebf9d-be1d-11e7-88ff-2c44fd152860')}) +def test_logging_without_request(): + """ Test logger is usable in non request context """ + app = Flask(__name__) + _set_up_flask_logging(app) + logger, stream = config_logger('main.logger') + logger.info('works') + assert check_log_record(stream, JOB_LOG_SCHEMA, {'msg': v_str('works')}) == {} + + # Helper functions def _set_up_flask_logging(app, level=logging.DEBUG): cf_logging._SETUP_DONE = False From bc430b749ba6ea016eb61de2ee380f5e39ac8a16 Mon Sep 17 00:00:00 2001 From: Georgi Vachkov Date: Sat, 20 Apr 2019 10:19:21 +0300 Subject: [PATCH 12/35] Version 4.1.1 (#38) --- CHANGELOG.md | 7 +++++++ sap/cf_logging/__init__.py | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 02401de..802fcc9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,13 @@ This project adheres to [Semantic Versioning](http://semver.org/). The format is based on [Keep a Changelog](http://keepachangelog.com/). +## 4.1.1 - 2019-04-19 + +### Fixed + + - Fix logging not usable outside request + + ## 4.1.0 - 2018-09-13 ### Added diff --git a/sap/cf_logging/__init__.py b/sap/cf_logging/__init__.py index 8a92667..4063372 100644 --- a/sap/cf_logging/__init__.py +++ b/sap/cf_logging/__init__.py @@ -10,7 +10,7 @@ from sap.cf_logging.record.request_log_record import RequestWebRecord from sap.cf_logging.record.simple_log_record import SimpleLogRecord -__version__ = '4.1.0' +__version__ = '4.1.1' _SETUP_DONE = False FRAMEWORK = None From ad47a0791458ad1c306e47beb85055c4383fdc8e Mon Sep 17 00:00:00 2001 From: Alexander Penev Date: Tue, 26 May 2020 16:50:40 +0300 Subject: [PATCH 13/35] Update README.rst --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 17472d9..d885806 100644 --- a/README.rst +++ b/README.rst @@ -176,7 +176,7 @@ Create a new app import logging - from django.http import HTTPResponse + from django.http import HttpResponse from sap.cf_logging.core.constants import REQUEST_KEY def index(request): From 7d6f109c3d357370314941ea47ae8c626e750090 Mon Sep 17 00:00:00 2001 From: Alexander Penev Date: Thu, 11 Jun 2020 09:51:37 +0300 Subject: [PATCH 14/35] Update Django (#40) * Update Django * Fix tests --- test-requirements.txt | 6 ++++-- tox.ini | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/test-requirements.txt b/test-requirements.txt index bdfbb1e..0875dc2 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -2,7 +2,7 @@ asyncio falcon falcon-auth Flask -sanic; python_version >= '3.5' +sanic==19.12.2; python_version >= '3.5' aiohttp; python_version >= '3.5' sonic182-json-validator==0.0.12 pytest==3.6.2 @@ -10,4 +10,6 @@ pytest-cov==2.5.1 pytest-mock==1.6.3 pylint==1.9.2 tox -django < 2.2 +django==1.11; python_version == '2.7' +django==2.2.13; python_version >= '3.5' +attrs < 19.2 diff --git a/tox.ini b/tox.ini index 41591b2..ea16ac0 100644 --- a/tox.ini +++ b/tox.ini @@ -14,4 +14,4 @@ commands = py.test --cov=sap tests {posargs} basepython=python3.6 commands= pylint sap - pylint tests + pylint --extension-pkg-whitelist=falcon tests From 3b8e61130de6dde42f31421c1d7a35965fbf336e Mon Sep 17 00:00:00 2001 From: Alexander Penev Date: Tue, 2 Feb 2021 13:18:26 +0200 Subject: [PATCH 15/35] Fix context store for Sanic (#43) * Fix context for Sanic * Optimize getattr call --- sap/cf_logging/core/context.py | 2 +- sap/cf_logging/core/framework.py | 2 +- sap/cf_logging/core/request_reader.py | 2 +- sap/cf_logging/core/response_reader.py | 2 +- sap/cf_logging/django_logging/__init__.py | 2 +- sap/cf_logging/falcon_logging/__init__.py | 2 +- sap/cf_logging/record/application_info.py | 2 +- sap/cf_logging/sanic_logging/context.py | 17 ++++------------- test-requirements.txt | 13 ++++++------- tests/test_falcon_logging.py | 4 ++-- tests/unit/formatters/test_json_formatter.py | 2 +- 11 files changed, 20 insertions(+), 30 deletions(-) diff --git a/sap/cf_logging/core/context.py b/sap/cf_logging/core/context.py index a07f484..5c1add9 100644 --- a/sap/cf_logging/core/context.py +++ b/sap/cf_logging/core/context.py @@ -2,7 +2,7 @@ _CORRELATION_ID_KEY = 'correlation_id' -class Context(object): +class Context(object): # pylint: disable=useless-object-inheritance """ Class for getting and setting context variables """ def set(self, key, value, request): diff --git a/sap/cf_logging/core/framework.py b/sap/cf_logging/core/framework.py index 17a890c..e084591 100644 --- a/sap/cf_logging/core/framework.py +++ b/sap/cf_logging/core/framework.py @@ -13,7 +13,7 @@ def _check_instance(obj, clazz): raise TypeError('Provided object is not valid {}'.format(clazz.__name__)) -class Framework(object): +class Framework(object): # pylint: disable=useless-object-inheritance """ Framework class holds Context, RequestReader, ResponseReader """ def __init__(self, name, context, request_reader, response_reader): diff --git a/sap/cf_logging/core/request_reader.py b/sap/cf_logging/core/request_reader.py index 80459b3..83012f1 100644 --- a/sap/cf_logging/core/request_reader.py +++ b/sap/cf_logging/core/request_reader.py @@ -5,7 +5,7 @@ 'X-CorrelationID', 'X-Request-ID', 'X-Vcap-Request-Id'] -class RequestReader(object): +class RequestReader(object): # pylint: disable=useless-object-inheritance """ Helper class for extracting logging-relevant information from HTTP request object """ diff --git a/sap/cf_logging/core/response_reader.py b/sap/cf_logging/core/response_reader.py index 8d5a1f1..55c8530 100644 --- a/sap/cf_logging/core/response_reader.py +++ b/sap/cf_logging/core/response_reader.py @@ -1,7 +1,7 @@ """ Module for the ResponseReader class """ -class ResponseReader(object): +class ResponseReader(object): # pylint: disable=useless-object-inheritance """ Helper class for extracting logging-relevant information from HTTP response object """ diff --git a/sap/cf_logging/django_logging/__init__.py b/sap/cf_logging/django_logging/__init__.py index b8cb95d..96f370d 100644 --- a/sap/cf_logging/django_logging/__init__.py +++ b/sap/cf_logging/django_logging/__init__.py @@ -13,7 +13,7 @@ DJANGO_FRAMEWORK_NAME = 'django.framework' -class LoggingMiddleware(object): +class LoggingMiddleware(object): # pylint: disable=useless-object-inheritance """ Django logging middleware """ def __init__(self, get_response, logger_name='cf.django.logger'): diff --git a/sap/cf_logging/falcon_logging/__init__.py b/sap/cf_logging/falcon_logging/__init__.py index 51259a3..ac56889 100644 --- a/sap/cf_logging/falcon_logging/__init__.py +++ b/sap/cf_logging/falcon_logging/__init__.py @@ -15,7 +15,7 @@ FALCON_FRAMEWORK_NAME = 'falcon.framework' -class LoggingMiddleware(object): +class LoggingMiddleware(object): # pylint: disable=useless-object-inheritance """ Falcon logging middleware """ def __init__(self, logger_name='cf.falcon.logger'): diff --git a/sap/cf_logging/record/application_info.py b/sap/cf_logging/record/application_info.py index 8ea112f..f866f0a 100644 --- a/sap/cf_logging/record/application_info.py +++ b/sap/cf_logging/record/application_info.py @@ -8,7 +8,7 @@ LAYER = 'python' COMPONENT_ID = util.get_vcap_param('application_id', defaults.UNKNOWN) COMPONENT_NAME = util.get_vcap_param('name', defaults.UNKNOWN) -COMPONENT_INSTANCE = os.getenv('CF_INSTANCE_INDEX', 0) +COMPONENT_INSTANCE = int(os.getenv('CF_INSTANCE_INDEX', "0")) SPACE_ID = util.get_vcap_param('space_id', defaults.UNKNOWN) SPACE_NAME = util.get_vcap_param('space_name', defaults.UNKNOWN) CONTAINER_ID = os.getenv('CF_INSTANCE_IP', defaults.UNKNOWN) diff --git a/sap/cf_logging/sanic_logging/context.py b/sap/cf_logging/sanic_logging/context.py index 22f381e..8776ad6 100644 --- a/sap/cf_logging/sanic_logging/context.py +++ b/sap/cf_logging/sanic_logging/context.py @@ -7,24 +7,15 @@ CONTEXT_NAME = 'cf_logger_context' -def _init_context(request): - if CONTEXT_NAME not in request: - request[CONTEXT_NAME] = {} - - class SanicContext(Context): """ Stores logging context in Sanic's request object """ def set(self, key, value, request): - if request is None: - return - _init_context(request) - request[CONTEXT_NAME][key] = value + if request is not None: + setattr(request.ctx, key, value) def get(self, key, request): if request is None: return None - _init_context(request) - if key in request[CONTEXT_NAME]: - return request[CONTEXT_NAME][key] - return None + + return getattr(request.ctx, key, None) diff --git a/test-requirements.txt b/test-requirements.txt index 0875dc2..67ff32f 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -2,14 +2,13 @@ asyncio falcon falcon-auth Flask -sanic==19.12.2; python_version >= '3.5' +sanic; python_version >= '3.5' aiohttp; python_version >= '3.5' -sonic182-json-validator==0.0.12 -pytest==3.6.2 -pytest-cov==2.5.1 -pytest-mock==1.6.3 -pylint==1.9.2 +sonic182-json-validator +pytest +pytest-cov +pytest-mock +pylint tox django==1.11; python_version == '2.7' django==2.2.13; python_version >= '3.5' -attrs < 19.2 diff --git a/tests/test_falcon_logging.py b/tests/test_falcon_logging.py index 8c252b0..8517c0d 100644 --- a/tests/test_falcon_logging.py +++ b/tests/test_falcon_logging.py @@ -52,7 +52,7 @@ def test_falcon_request_log(headers, expected): _check_falcon_request_log(app, headers, expected) -class User(object): +class User(object): # pylint: disable=useless-object-inheritance def __init__(self, key, name): self.key = key self.name = name @@ -116,7 +116,7 @@ def _set_up_falcon_logging(app, *args): falcon_logging.init(app, logging.DEBUG, *args) -class UserResourceRoute(object): +class UserResourceRoute(object): # pylint: disable=useless-object-inheritance def __init__(self, extra, expected): self.extra = extra self.expected = expected diff --git a/tests/unit/formatters/test_json_formatter.py b/tests/unit/formatters/test_json_formatter.py index c63e233..28bf819 100644 --- a/tests/unit/formatters/test_json_formatter.py +++ b/tests/unit/formatters/test_json_formatter.py @@ -18,7 +18,7 @@ def test_unknown_records_format(): def test_non_json_serializable(): """ test json formatter handles non JSON serializable object """ - class _MyClass(object): # pylint: disable=too-few-public-methods + class _MyClass(object): # pylint: disable=too-few-public-methods,useless-object-inheritance pass extra = {'cls': _MyClass()} From 05273a4f8dc193e8177b85b3179d95c4c1fbbf01 Mon Sep 17 00:00:00 2001 From: Alexander Penev Date: Thu, 11 Feb 2021 12:04:48 +0200 Subject: [PATCH 16/35] Add support for custom fields (#46) --- README.rst | 33 +++++++++++++++++++ sap/cf_logging/__init__.py | 4 +-- sap/cf_logging/core/framework.py | 9 ++++- sap/cf_logging/django_logging/__init__.py | 5 +-- sap/cf_logging/falcon_logging/__init__.py | 5 +-- sap/cf_logging/flask_logging/__init__.py | 9 ++--- sap/cf_logging/job_logging/framework.py | 5 +-- sap/cf_logging/record/simple_log_record.py | 21 +++++++++++- sap/cf_logging/sanic_logging/__init__.py | 5 +-- test-requirements.txt | 3 +- .../test_app/test_django_logging.py | 6 +++- tests/log_schemas.py | 16 +++++++++ tests/test_falcon_logging.py | 7 +++- tests/test_flask_logging.py | 11 +++++-- tests/test_job_logging.py | 4 +++ tests/test_sanic_logging.py | 8 ++++- tests/unit/test_init.py | 5 ++- tox.ini | 1 + 18 files changed, 134 insertions(+), 23 deletions(-) diff --git a/README.rst b/README.rst index d885806..ea3946f 100644 --- a/README.rst +++ b/README.rst @@ -222,6 +222,39 @@ General be left untouched. When using Flask and Sanic with the logging library before and after request middleware is attached, and it will capture response times for each request. + +Custom Fields +""""""""""""" + +To use custom fields. Pass a dictionary property custom_fields to the initialize method: + +.. code:: python + + import logging + from sap import cf_logging + cf_logging.init(custom_fields={"foo": "default", "bar": None}) + +Here we mark the two fields: foo and bar as custom_fields. Logging with: + +.. code:: python + + logging.getLogger('my.logger').debug('Hi') + +The property foo will be output as a custom field with a value "default". The property bar will not be logged, as it does not have a value. + +To log bar, provide a value when logging: + +.. code:: python + + logging.getLogger('my.logger').debug('Hi', extra={"bar": "new_value"}) + +It is also possible to log foo with a different value: + +.. code:: python + + logging.getLogger('my.logger').debug('Hi', extra={"foo": "hello"}) + + Setting and getting correlation ID """""""""""""""""""""""""""""""""" diff --git a/sap/cf_logging/__init__.py b/sap/cf_logging/__init__.py index 4063372..98eb086 100644 --- a/sap/cf_logging/__init__.py +++ b/sap/cf_logging/__init__.py @@ -35,7 +35,7 @@ def makeRecord(self, name, level, fn, lno, msg, msgargs, exc_info, return cls(extra, FRAMEWORK, name, level, fn, lno, msg, msgargs, exc_info, func, *args, **kwargs) -def init(cfl_framework=None, level=defaults.DEFAULT_LOGGING_LEVEL): +def init(cfl_framework=None, level=defaults.DEFAULT_LOGGING_LEVEL, custom_fields=None): """ Initialize function. It sets up the logging library to output JSON formatted messages. @@ -50,7 +50,7 @@ def init(cfl_framework=None, level=defaults.DEFAULT_LOGGING_LEVEL): raise TypeError('expecting framework of type {}'.format(Framework.__name__)) _SETUP_DONE = True - FRAMEWORK = cfl_framework or JobFramework() + FRAMEWORK = cfl_framework or JobFramework(custom_fields=custom_fields) logging.setLoggerClass(CfLogger) diff --git a/sap/cf_logging/core/framework.py b/sap/cf_logging/core/framework.py index e084591..eca7618 100644 --- a/sap/cf_logging/core/framework.py +++ b/sap/cf_logging/core/framework.py @@ -16,7 +16,8 @@ def _check_instance(obj, clazz): class Framework(object): # pylint: disable=useless-object-inheritance """ Framework class holds Context, RequestReader, ResponseReader """ - def __init__(self, name, context, request_reader, response_reader): + # pylint: disable=too-many-arguments + def __init__(self, name, context, request_reader, response_reader, custom_fields=None): if not name or not isinstance(name, STR_CLASS): raise TypeError('Provided name is not valid string') _check_instance(context, Context) @@ -26,6 +27,12 @@ def __init__(self, name, context, request_reader, response_reader): self._context = context self._request_reader = request_reader self._response_reader = response_reader + self._custom_fields = custom_fields or {} + + @property + def custom_fields(self): + """ Get the custom fields """ + return self._custom_fields @property def context(self): diff --git a/sap/cf_logging/django_logging/__init__.py b/sap/cf_logging/django_logging/__init__.py index 96f370d..a171abd 100644 --- a/sap/cf_logging/django_logging/__init__.py +++ b/sap/cf_logging/django_logging/__init__.py @@ -52,13 +52,14 @@ def process_response(self, request, response): return response -def init(level=defaults.DEFAULT_LOGGING_LEVEL): +def init(level=defaults.DEFAULT_LOGGING_LEVEL, custom_fields=None): """ Initializes logging in JSON format. :param level: - valid log level from standard logging package (optional) """ framework = Framework(DJANGO_FRAMEWORK_NAME, DjangoContext(), - DjangoRequestReader(), DjangoResponseReader()) + DjangoRequestReader(), DjangoResponseReader(), + custom_fields=custom_fields) cf_logging.init(framework, level) diff --git a/sap/cf_logging/falcon_logging/__init__.py b/sap/cf_logging/falcon_logging/__init__.py index ac56889..988f84c 100644 --- a/sap/cf_logging/falcon_logging/__init__.py +++ b/sap/cf_logging/falcon_logging/__init__.py @@ -47,7 +47,7 @@ def process_response(self, request, response, resource, req_succeeded): # pylint logging.getLogger(self._logger_name).info('', extra=extra) -def init(app, level=defaults.DEFAULT_LOGGING_LEVEL, username_key='username'): +def init(app, level=defaults.DEFAULT_LOGGING_LEVEL, username_key='username', custom_fields=None): """ Initializes logging in JSON format. :param app: - Falcon application object @@ -60,5 +60,6 @@ def init(app, level=defaults.DEFAULT_LOGGING_LEVEL, username_key='username'): raise TypeError('application should be instance of Falcon API') framework = Framework(FALCON_FRAMEWORK_NAME, FalconContext(), - FalconRequestReader(username_key), FalconResponseReader()) + FalconRequestReader(username_key), FalconResponseReader(), + custom_fields=custom_fields) cf_logging.init(framework, level) diff --git a/sap/cf_logging/flask_logging/__init__.py b/sap/cf_logging/flask_logging/__init__.py index 5a09db5..384e5ca 100644 --- a/sap/cf_logging/flask_logging/__init__.py +++ b/sap/cf_logging/flask_logging/__init__.py @@ -47,7 +47,7 @@ def _wrapper(response): return _wrapper -def init(app, level=defaults.DEFAULT_LOGGING_LEVEL): +def init(app, level=defaults.DEFAULT_LOGGING_LEVEL, custom_fields=None): """ Initializes logging in JSON format. Adds before and after request handlers to `app` object to enable request info log. @@ -57,7 +57,7 @@ def init(app, level=defaults.DEFAULT_LOGGING_LEVEL): if not isinstance(app, flask.Flask): raise TypeError('application should be instance of Flask') - _init_framework(level) + _init_framework(level, custom_fields=custom_fields) @app.before_request @before_request @@ -70,9 +70,10 @@ def _app_after_request(response): return response -def _init_framework(level): +def _init_framework(level, custom_fields): logging.getLogger('werkzeug').disabled = True framework = Framework(FLASK_FRAMEWORK_NAME, - FlaskContext(), FlaskRequestReader(), FlaskResponseReader()) + FlaskContext(), FlaskRequestReader(), FlaskResponseReader(), + custom_fields=custom_fields) cf_logging.init(framework, level) diff --git a/sap/cf_logging/job_logging/framework.py b/sap/cf_logging/job_logging/framework.py index 61f7f8b..3533e61 100644 --- a/sap/cf_logging/job_logging/framework.py +++ b/sap/cf_logging/job_logging/framework.py @@ -11,10 +11,11 @@ class JobFramework(Framework): """ Simple framework using default request and response readers. Uses JobContext to keeping properties in memory """ - def __init__(self, context=None): + def __init__(self, context=None, custom_fields=None): super(JobFramework, self).__init__( JOB_FRAMEWORK_NAME, context or JobContext(), RequestReader(), - ResponseReader() + ResponseReader(), + custom_fields=custom_fields ) diff --git a/sap/cf_logging/record/simple_log_record.py b/sap/cf_logging/record/simple_log_record.py index 9c030b0..12d2fcb 100644 --- a/sap/cf_logging/record/simple_log_record.py +++ b/sap/cf_logging/record/simple_log_record.py @@ -33,8 +33,17 @@ def __init__(self, extra, framework, *args, **kwargs): self.correlation_id = framework.context.get_correlation_id(request) or defaults.UNKNOWN + self.custom_fields = {} + for key, value in framework.custom_fields.items(): + if extra and key in extra: + if extra[key] is not None: + self.custom_fields[key] = extra[key] + elif value is not None: + self.custom_fields[key] = value + self.extra = dict((key, value) for key, value in extra.items() - if key not in _SKIP_ATTRIBUTES) if extra else {} + if key not in _SKIP_ATTRIBUTES and + key not in framework.custom_fields.keys()) if extra else {} for key, value in self.extra.items(): setattr(self, key, value) @@ -72,4 +81,14 @@ def format(self): record['stacktrace'] = format_stacktrace(stacktrace) record.update(self.extra) + if len(self.custom_fields) > 0: + record.update(self._format_custom_fields()) return record + + def _format_custom_fields(self): + res = {"#cf": {"string": []}} + for i, (key, value) in enumerate(self.custom_fields.items()): + res['#cf']['string'].append( + {"k": str(key), "v": str(value), "i": i} + ) + return res diff --git a/sap/cf_logging/sanic_logging/__init__.py b/sap/cf_logging/sanic_logging/__init__.py index 87fe562..ec1be21 100644 --- a/sap/cf_logging/sanic_logging/__init__.py +++ b/sap/cf_logging/sanic_logging/__init__.py @@ -48,7 +48,7 @@ def _wrapper(request, response): return _wrapper -def init(app, level=defaults.DEFAULT_LOGGING_LEVEL, custom_framework=None): +def init(app, level=defaults.DEFAULT_LOGGING_LEVEL, custom_framework=None, custom_fields=None): """ Initializes logging in JSON format. Adds before and after request handlers to the `app` object to enable request info log. @@ -65,7 +65,8 @@ def init(app, level=defaults.DEFAULT_LOGGING_LEVEL, custom_framework=None): SANIC_FRAMEWORK_NAME, SanicContext(), SanicRequestReader(), - SanicResponseReader() + SanicResponseReader(), + custom_fields=custom_fields ) cf_logging.init(framework, level) diff --git a/test-requirements.txt b/test-requirements.txt index 67ff32f..235a480 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -8,7 +8,8 @@ sonic182-json-validator pytest pytest-cov pytest-mock -pylint +pylint==1.9.5; python_version == '2.7' +pylint==2.5.3; python_version >= '3.5' tox django==1.11; python_version == '2.7' django==2.2.13; python_version >= '3.5' diff --git a/tests/django_logging/test_app/test_django_logging.py b/tests/django_logging/test_app/test_django_logging.py index a60cb8d..6d4c681 100644 --- a/tests/django_logging/test_app/test_django_logging.py +++ b/tests/django_logging/test_app/test_django_logging.py @@ -65,6 +65,10 @@ def test_missing_request(): False ) +def test_custom_fields_set(): + """ Test custom fields are set up """ + _set_up_django_logging() + assert 'cf1' in cf_logging.FRAMEWORK.custom_fields.keys() def _check_django_request_log(headers, expected): _, stream = config_logger('cf.django.logger') @@ -77,7 +81,7 @@ def _check_django_request_log(headers, expected): # Helper functions def _set_up_django_logging(): cf_logging._SETUP_DONE = False # pylint: disable=protected-access - django_logging.init() + django_logging.init(custom_fields={'cf1': None}) def _check_expected_response(response, status_code=200, body='ok'): diff --git a/tests/log_schemas.py b/tests/log_schemas.py index c4e7641..086ec6f 100644 --- a/tests/log_schemas.py +++ b/tests/log_schemas.py @@ -29,6 +29,22 @@ 'component_type': u.string(r'^application$'), }) +CUST_FIELD_SCHEMA = u.extend(JOB_LOG_SCHEMA, { + '#cf': { + 'type': dict, + 'properties': { + 'string': {'type' : list, 'items': { + 'type': dict, + 'properties': { + 'v': u.string(u.WORD), + 'k': u.string(u.WORD), + 'i': u.pos_num() + } + }} + } + } +}) + WEB_LOG_SCHEMA = u.extend(CF_ATTRIBUTES_SCHEMA, { 'type': u.string('^request$'), 'written_at': u.iso_datetime(), diff --git a/tests/test_falcon_logging.py b/tests/test_falcon_logging.py index 8517c0d..229bb69 100644 --- a/tests/test_falcon_logging.py +++ b/tests/test_falcon_logging.py @@ -109,11 +109,16 @@ def test_correlation_id(): {'correlation_id': v_str('298ebf9d-be1d-11e7-88ff-2c44fd152860')} ) +def test_custom_fields_set(): + """ Test custom fields are set up """ + app = falcon.API() + _set_up_falcon_logging(app) + assert 'cf1' in cf_logging.FRAMEWORK.custom_fields.keys() # Helper functions def _set_up_falcon_logging(app, *args): cf_logging._SETUP_DONE = False - falcon_logging.init(app, logging.DEBUG, *args) + falcon_logging.init(app, logging.DEBUG, *args, custom_fields={'cf1': None}) class UserResourceRoute(object): # pylint: disable=useless-object-inheritance diff --git a/tests/test_flask_logging.py b/tests/test_flask_logging.py index 49a9754..65a4c72 100644 --- a/tests/test_flask_logging.py +++ b/tests/test_flask_logging.py @@ -5,7 +5,7 @@ from flask import Response from sap import cf_logging from sap.cf_logging import flask_logging -from tests.log_schemas import WEB_LOG_SCHEMA, JOB_LOG_SCHEMA +from tests.log_schemas import WEB_LOG_SCHEMA, JOB_LOG_SCHEMA, CUST_FIELD_SCHEMA from tests.common_test_params import v_str, v_num, auth_basic, get_web_record_header_fixtures from tests.util import ( check_log_record, @@ -74,11 +74,18 @@ def test_logging_without_request(): logger.info('works') assert check_log_record(stream, JOB_LOG_SCHEMA, {'msg': v_str('works')}) == {} +def test_custom_field_loggin(): + """ Test custom fields are generated """ + app = Flask(__name__) + _set_up_flask_logging(app) + logger, stream = config_logger('main.logger') + logger.info('works', extra={'cf1': 'yes'}) + assert check_log_record(stream, CUST_FIELD_SCHEMA, {}) == {} # Helper functions def _set_up_flask_logging(app, level=logging.DEBUG): cf_logging._SETUP_DONE = False - flask_logging.init(app, level) + flask_logging.init(app, level, custom_fields={'cf1': None, 'cf2': None}) def _user_logging(headers, extra, expected): diff --git a/tests/test_job_logging.py b/tests/test_job_logging.py index 29c9706..7cb2aaa 100644 --- a/tests/test_job_logging.py +++ b/tests/test_job_logging.py @@ -69,6 +69,10 @@ def test_exception_stacktrace(): assert error == {} assert 'ZeroDivisionError' in str(log_json['stacktrace']) +def test_custom_fields_set(): + """ Test custom fields are set up """ + cf_logging.init(level=logging.DEBUG, custom_fields={'cf1': None}) + assert 'cf1' in cf_logging.FRAMEWORK.custom_fields.keys() def test_thread_safety(): """ test context keeps separate correlation ID per thread """ diff --git a/tests/test_sanic_logging.py b/tests/test_sanic_logging.py index c28b584..56993ac 100644 --- a/tests/test_sanic_logging.py +++ b/tests/test_sanic_logging.py @@ -77,11 +77,17 @@ def test_logs_correlation_id(): '298ebf9d-be1d-11e7-88ff-2c44fd152860')}, True) +def test_custom_fields_set(): + """ Test custom fields are set up """ + app = sanic.Sanic('test cf_logging') + _set_up_sanic_logging(app) + assert 'cf1' in cf_logging.FRAMEWORK.custom_fields.keys() + # Helper functions def _set_up_sanic_logging(app, level=logging.DEBUG): cf_logging._SETUP_DONE = False - sanic_logging.init(app, level) + sanic_logging.init(app, level, custom_fields={'cf1': None}) def _user_logging(headers, extra, expected, provide_request=False): diff --git a/tests/unit/test_init.py b/tests/unit/test_init.py index db49d5a..aeaeaec 100644 --- a/tests/unit/test_init.py +++ b/tests/unit/test_init.py @@ -32,8 +32,11 @@ def _make_record(extra): return cf_logger.makeRecord('', '', '', '', '', '', '', '', extra=extra) -def test_init_cf_logger_simple_log(): +def test_init_cf_logger_simple_log(mocker): """ tests CfLogger creates SimpleLogRecord if extra is incomplete """ + framework = mocker.Mock(Framework) + mocker.patch.object(framework, 'custom_fields', return_value=None) + cf_logging.init(framework) assert isinstance(_make_record(extra={}), SimpleLogRecord) assert isinstance(_make_record(extra={REQUEST_KEY: {}}), SimpleLogRecord) assert isinstance(_make_record(extra={RESPONSE_KEY: {}}), SimpleLogRecord) diff --git a/tox.ini b/tox.ini index ea16ac0..b768aa5 100644 --- a/tox.ini +++ b/tox.ini @@ -8,6 +8,7 @@ deps = -rtest-requirements.txt commands = py.test {posargs} [testenv:py36] +setenv = SANIC_REGISTER = False commands = py.test --cov=sap tests {posargs} [testenv:lint] From 3971fba71785f42a33ef93161b6238919b7e6e1c Mon Sep 17 00:00:00 2001 From: Alexander Penev Date: Fri, 12 Feb 2021 10:52:14 +0200 Subject: [PATCH 17/35] Update python version in tox (#48) * Update python version in tox * Fix falcon-auth dep for py27 --- .travis.yml | 6 +++--- setup.py | 1 + test-requirements.txt | 3 ++- tox.ini | 6 +++--- 4 files changed, 9 insertions(+), 7 deletions(-) diff --git a/.travis.yml b/.travis.yml index 49b5c7a..d047e91 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,9 +5,9 @@ matrix: include: - python: 2.7 env: TOXENV=py27 - - python: 3.6 - env: TOXENV=py36 - - python: 3.6 + - python: 3.8 + env: TOXENV=py38 + - python: 3.8 env: TOXENV=lint install: - pip install tox diff --git a/setup.py b/setup.py index 2d86cd2..9a04ca7 100644 --- a/setup.py +++ b/setup.py @@ -37,6 +37,7 @@ 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.8', 'Topic :: Software Development :: Libraries :: Python Modules', 'Topic :: Software Development', 'Topic :: System :: Logging', diff --git a/test-requirements.txt b/test-requirements.txt index 235a480..f43f5fd 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,6 +1,7 @@ asyncio falcon -falcon-auth +falcon-auth==1.0.3; python_version == '2.7' +falcon-auth; python_version >= '3.5' Flask sanic; python_version >= '3.5' aiohttp; python_version >= '3.5' diff --git a/tox.ini b/tox.ini index b768aa5..7dad122 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py27,py36,lint +envlist = py27,py38,lint [testenv] deps = -rtest-requirements.txt @@ -7,12 +7,12 @@ deps = -rtest-requirements.txt [testenv:py27] commands = py.test {posargs} -[testenv:py36] +[testenv:py38] setenv = SANIC_REGISTER = False commands = py.test --cov=sap tests {posargs} [testenv:lint] -basepython=python3.6 +basepython=python3.8 commands= pylint sap pylint --extension-pkg-whitelist=falcon tests From 6fcea39f91281e6137145453b34efea21ac01382 Mon Sep 17 00:00:00 2001 From: Georgi Vachkov Date: Tue, 23 Feb 2021 11:55:11 +0200 Subject: [PATCH 18/35] Version 4.2.1 (#49) --- CHANGELOG.md | 11 +++++++++++ sap/cf_logging/__init__.py | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 802fcc9..d797299 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,17 @@ This project adheres to [Semantic Versioning](http://semver.org/). The format is based on [Keep a Changelog](http://keepachangelog.com/). +## 4.1.2 - 2021-02-23 + +### Added + + - Add support for custom fields + +### Fixed + + - Fix context store for Sanic + + ## 4.1.1 - 2019-04-19 ### Fixed diff --git a/sap/cf_logging/__init__.py b/sap/cf_logging/__init__.py index 98eb086..2f15077 100644 --- a/sap/cf_logging/__init__.py +++ b/sap/cf_logging/__init__.py @@ -10,7 +10,7 @@ from sap.cf_logging.record.request_log_record import RequestWebRecord from sap.cf_logging.record.simple_log_record import SimpleLogRecord -__version__ = '4.1.1' +__version__ = '4.2.1' _SETUP_DONE = False FRAMEWORK = None From b25943a3a57f75e942f643769abef8f22158ffe4 Mon Sep 17 00:00:00 2001 From: Alexander Penev Date: Wed, 17 Mar 2021 11:36:10 +0200 Subject: [PATCH 19/35] Fix stacktrace format (#50) --- sap/cf_logging/formatters/stacktrace_formatter.py | 8 ++++---- sap/cf_logging/record/simple_log_record.py | 3 +-- tests/log_schemas.py | 1 - tests/unit/formatters/test_stacktrace_formatter.py | 2 +- 4 files changed, 6 insertions(+), 8 deletions(-) diff --git a/sap/cf_logging/formatters/stacktrace_formatter.py b/sap/cf_logging/formatters/stacktrace_formatter.py index 3b908f2..44543d5 100644 --- a/sap/cf_logging/formatters/stacktrace_formatter.py +++ b/sap/cf_logging/formatters/stacktrace_formatter.py @@ -9,7 +9,7 @@ def format_stacktrace(stacktrace): """ - Removes newline and tab characters + Removes tab characters Truncates stacktrace to maximum size :param stacktrace: string representation of a stacktrace @@ -17,7 +17,7 @@ def format_stacktrace(stacktrace): if not isinstance(stacktrace, str): return '' - stacktrace = re.sub('\n|\t', ' ', stacktrace) + stacktrace = re.sub('\t', ' ', stacktrace) if len(stacktrace) <= constants.STACKTRACE_MAX_SIZE: return stacktrace @@ -30,8 +30,8 @@ def format_stacktrace(stacktrace): stacktrace, (constants.STACKTRACE_MAX_SIZE // 3) * 2 ) - new_stacktrace = "-------- STACK TRACE TRUNCATED --------" + stacktrace_beginning +\ - "-------- OMITTED --------" + stacktrace_end + new_stacktrace = "-------- STACK TRACE TRUNCATED --------\n" + stacktrace_beginning +\ + "-------- OMITTED --------\n" + stacktrace_end return new_stacktrace diff --git a/sap/cf_logging/record/simple_log_record.py b/sap/cf_logging/record/simple_log_record.py index 12d2fcb..ade6146 100644 --- a/sap/cf_logging/record/simple_log_record.py +++ b/sap/cf_logging/record/simple_log_record.py @@ -72,13 +72,12 @@ def format(self): 'logger': self.name, 'thread': self.threadName, 'level': self.levelname, - 'line_no': self.lineno, 'msg': self.getMessage(), }) if self.levelno == logging.ERROR and self.exc_info: stacktrace = ''.join(traceback.format_exception(*self.exc_info)) - record['stacktrace'] = format_stacktrace(stacktrace) + record['stacktrace'] = format_stacktrace(stacktrace).split('\n') record.update(self.extra) if len(self.custom_fields) > 0: diff --git a/tests/log_schemas.py b/tests/log_schemas.py index 086ec6f..39cbafc 100644 --- a/tests/log_schemas.py +++ b/tests/log_schemas.py @@ -22,7 +22,6 @@ 'logger': u.string(u.WORD), 'thread': u.string(u.WORD), 'level': u.enum(u.LEVEL), - 'line_no': u.pos_num(), 'written_at': u.iso_datetime(), 'written_ts': u.pos_num(), 'msg': u.string(u.WORD), diff --git a/tests/unit/formatters/test_stacktrace_formatter.py b/tests/unit/formatters/test_stacktrace_formatter.py index ffc4efd..086461a 100644 --- a/tests/unit/formatters/test_stacktrace_formatter.py +++ b/tests/unit/formatters/test_stacktrace_formatter.py @@ -19,6 +19,6 @@ def test_stacktrace_truncated(monkeypatch): """ Test that stacktrace is truncated when bigger than the stacktrace maximum size """ monkeypatch.setattr('sap.cf_logging.core.constants.STACKTRACE_MAX_SIZE', 120) - formatted = format_stacktrace(STACKTRACE) + formatted = ''.join(format_stacktrace(STACKTRACE)) assert "TRUNCATED" in formatted assert "OMITTED" in formatted From f7ac5e4dd8fdecf73162ec1963328d67f9c56b8c Mon Sep 17 00:00:00 2001 From: Alexander Penev Date: Fri, 19 Mar 2021 12:09:16 +0200 Subject: [PATCH 20/35] Update django==1.11.29 for py2.7 (#52) --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index f43f5fd..45c4ca1 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -12,5 +12,5 @@ pytest-mock pylint==1.9.5; python_version == '2.7' pylint==2.5.3; python_version >= '3.5' tox -django==1.11; python_version == '2.7' +django==1.11.29; python_version == '2.7' django==2.2.13; python_version >= '3.5' From 583640dae7ff0d8c9935083ea5a26546032f4408 Mon Sep 17 00:00:00 2001 From: Alexander Penev Date: Thu, 1 Apr 2021 13:56:57 +0300 Subject: [PATCH 21/35] Fix sanic tests (#54) --- test-requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/test-requirements.txt b/test-requirements.txt index 45c4ca1..f06157b 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -4,6 +4,7 @@ falcon-auth==1.0.3; python_version == '2.7' falcon-auth; python_version >= '3.5' Flask sanic; python_version >= '3.5' +sanic-testing==0.3.0; python_version >= '3.5' aiohttp; python_version >= '3.5' sonic182-json-validator pytest From 315bbe31ea9f7755006e62d43ee7100a0c23289e Mon Sep 17 00:00:00 2001 From: Alexander Penev Date: Thu, 1 Apr 2021 14:37:32 +0300 Subject: [PATCH 22/35] Version 4.2.2 (#53) --- CHANGELOG.md | 9 ++++++++- sap/cf_logging/__init__.py | 2 +- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d797299..1bbaaa9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,14 @@ This project adheres to [Semantic Versioning](http://semver.org/). The format is based on [Keep a Changelog](http://keepachangelog.com/). -## 4.1.2 - 2021-02-23 +## 4.2.2 - 2021-04-01 + +### Fixed + + - Fix stacktrace format + + +## 4.2.1 - 2021-02-23 ### Added diff --git a/sap/cf_logging/__init__.py b/sap/cf_logging/__init__.py index 2f15077..22dd41a 100644 --- a/sap/cf_logging/__init__.py +++ b/sap/cf_logging/__init__.py @@ -10,7 +10,7 @@ from sap.cf_logging.record.request_log_record import RequestWebRecord from sap.cf_logging.record.simple_log_record import SimpleLogRecord -__version__ = '4.2.1' +__version__ = '4.2.2' _SETUP_DONE = False FRAMEWORK = None From 86b7e8ef35b935401bdec7da275fe4e09302988c Mon Sep 17 00:00:00 2001 From: Sebastian Wolf Date: Fri, 1 Oct 2021 13:36:14 +0200 Subject: [PATCH 23/35] chore: Integrate REUSE tool (#61) * chore: Integrate REUSE tool * Change upstream contact after handover * chore: remove upstream contact --- .reuse/dep5 | 28 ++++++++++++++++ LICENSES/Apache-2.0.txt | 73 +++++++++++++++++++++++++++++++++++++++++ README.rst | 9 +++-- 3 files changed, 105 insertions(+), 5 deletions(-) create mode 100755 .reuse/dep5 create mode 100644 LICENSES/Apache-2.0.txt diff --git a/.reuse/dep5 b/.reuse/dep5 new file mode 100755 index 0000000..8b2f31e --- /dev/null +++ b/.reuse/dep5 @@ -0,0 +1,28 @@ +Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ +Upstream-Name: cf-python-logging-support +Source: https://github.com/SAP/cf-python-logging-support +Disclaimer: The code in this project may include calls to APIs (“API Calls”) of + SAP or third-party products or services developed outside of this project + (“External Products”). + “APIs” means application programming interfaces, as well as their respective + specifications and implementing code that allows software to communicate with + other software. + API Calls to External Products are not licensed under the open source license + that governs this project. The use of such API Calls and related External + Products are subject to applicable additional agreements with the relevant + provider of the External Products. In no event shall the open source license + that governs this project grant any rights in or to any External Products,or + alter, expand or supersede any terms of the applicable additional agreements. + If you have a valid license agreement with SAP for the use of a particular SAP + External Product, then you may make use of any API Calls included in this + project’s code for that SAP External Product, subject to the terms of such + license agreement. If you do not have a valid license agreement for the use of + a particular SAP External Product, then you may only make use of any API Calls + in this project for that SAP External Product for your internal, non-productive + and non-commercial test and evaluation of such API Calls. Nothing herein grants + you any rights to use or access any SAP External Product, or provide any third + parties the right to use of access any SAP External Product, through API Calls. + +Files: * +Copyright: 2017-2021 SAP SE or an SAP affiliate company and cf-python-logging-support contributors +License: Apache-2.0 \ No newline at end of file diff --git a/LICENSES/Apache-2.0.txt b/LICENSES/Apache-2.0.txt new file mode 100644 index 0000000..137069b --- /dev/null +++ b/LICENSES/Apache-2.0.txt @@ -0,0 +1,73 @@ +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: + + (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. + + You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + +To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/README.rst b/README.rst index ea3946f..c95b330 100644 --- a/README.rst +++ b/README.rst @@ -2,6 +2,9 @@ Python logging library to emit JSON logs in a SAP CloudFoundry environment. =========================================================================== +.. image:: https://api.reuse.software/badge/github.com/SAP/cf-python-logging-support + :target: https://api.reuse.software/info/github.com/SAP/cf-python-logging-support + This is a collection of support libraries for Python applications running on Cloud Foundry that serve two main purposes: provide (a) means to emit structured application log messages and (b) instrument web applications of your application stack to collect request metrics. @@ -340,9 +343,5 @@ See `CHANGELOG file `__. - - - +Copyright (c) 2017-2021 SAP SE or an SAP affiliate company and cf-python-logging-support contributors. Please see our `LICENSE file `__ for copyright and license information. Detailed information including third-party components and their licensing/copyright information is available `via the REUSE tool `__. From bc5213bdc4dfc2fc7fd3b3a92a2b39f059981ab0 Mon Sep 17 00:00:00 2001 From: Michael Haas Date: Wed, 23 Mar 2022 15:41:43 +0100 Subject: [PATCH 24/35] Fix: stacktrace not shown in kibana (#63) * Fix: stacktrace not shown in kibana When logging exceptions with logging.exception(), the cf-python-logging library will emit the stacktrace as a separate field in the JSON. This field is now being filtered by the BTP and no longer visible in Kibana. This commit adds the stacktrace to the main 'msg' field and thus makes it possible again to debug exceptions in our Python applications. * fix version Co-authored-by: Kiril Maslenkov --- CHANGELOG.md | 6 +++++ sap/cf_logging/__init__.py | 2 +- sap/cf_logging/record/simple_log_record.py | 6 ++++- tests/unit/formatters/test_json_formatter.py | 23 ++++++++++++++++++++ 4 files changed, 35 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1bbaaa9..8ce2317 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ This project adheres to [Semantic Versioning](http://semver.org/). The format is based on [Keep a Changelog](http://keepachangelog.com/). +## 4.2.3 - 2022-03-23 + +### Fixed + + - Fix stacktrace not showing in kibana + ## 4.2.2 - 2021-04-01 ### Fixed diff --git a/sap/cf_logging/__init__.py b/sap/cf_logging/__init__.py index 22dd41a..a9f07a0 100644 --- a/sap/cf_logging/__init__.py +++ b/sap/cf_logging/__init__.py @@ -10,7 +10,7 @@ from sap.cf_logging.record.request_log_record import RequestWebRecord from sap.cf_logging.record.simple_log_record import SimpleLogRecord -__version__ = '4.2.2' +__version__ = '4.2.3' _SETUP_DONE = False FRAMEWORK = None diff --git a/sap/cf_logging/record/simple_log_record.py b/sap/cf_logging/record/simple_log_record.py index ade6146..59174fe 100644 --- a/sap/cf_logging/record/simple_log_record.py +++ b/sap/cf_logging/record/simple_log_record.py @@ -77,7 +77,11 @@ def format(self): if self.levelno == logging.ERROR and self.exc_info: stacktrace = ''.join(traceback.format_exception(*self.exc_info)) - record['stacktrace'] = format_stacktrace(stacktrace).split('\n') + stacktrace = format_stacktrace(stacktrace) + record['stacktrace'] = stacktrace.split('\n') + record['msg'] += "\n" + record['msg'] += stacktrace + record.update(self.extra) if len(self.custom_fields) > 0: diff --git a/tests/unit/formatters/test_json_formatter.py b/tests/unit/formatters/test_json_formatter.py index 28bf819..3144c99 100644 --- a/tests/unit/formatters/test_json_formatter.py +++ b/tests/unit/formatters/test_json_formatter.py @@ -27,3 +27,26 @@ class _MyClass(object): # pylint: disable=too-few-public-methods,useless-object- record_object = json.loads(FORMATTER.format(log_record)) assert record_object.get('cls') is not None assert 'MyClass' in record_object.get('cls') + +def test_stacktrace_is_added_to_msg_field(): + """ + Tests that JSONFormatter adds stracktrace to msg field. The stacktrace field + is no longer rendered in Kibana, see https://github.com/SAP/cf-python-logging-support/issues/45 + for related report. + """ + # Generate exception for the test + try: + raise ValueError("Dummy Exception") + except ValueError as e: + exc_info = (type(e), e, e.__traceback__) + + framework = JobFramework() + extra = {} + + log_record = SimpleLogRecord(extra, framework, 'name', logging.ERROR, FILE, LINE, 'Error found!', [], exc_info) + record_object = json.loads(FORMATTER.format(log_record)) + assert "Dummy Exception" in "".join(record_object["stacktrace"]) + expected_msg = "Error found!" + expected_msg += "\n" + expected_msg += "\n".join(record_object["stacktrace"]) + assert record_object["msg"] == expected_msg From c105ec5eba47ff2be39f9ffeddc1255838c17142 Mon Sep 17 00:00:00 2001 From: kmaslenkovsap <91725578+kmaslenkovsap@users.noreply.github.com> Date: Wed, 11 May 2022 15:01:52 +0300 Subject: [PATCH 25/35] update django version 2.2.28 (#66) --- CHANGELOG.md | 6 ++++++ sap/cf_logging/__init__.py | 2 +- test-requirements.txt | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ce2317..b658b6c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ This project adheres to [Semantic Versioning](http://semver.org/). The format is based on [Keep a Changelog](http://keepachangelog.com/). +## 4.2.4 - 2022-04-26 + +### Update + + - Update django version => 2.2.28 + ## 4.2.3 - 2022-03-23 ### Fixed diff --git a/sap/cf_logging/__init__.py b/sap/cf_logging/__init__.py index a9f07a0..8610d08 100644 --- a/sap/cf_logging/__init__.py +++ b/sap/cf_logging/__init__.py @@ -10,7 +10,7 @@ from sap.cf_logging.record.request_log_record import RequestWebRecord from sap.cf_logging.record.simple_log_record import SimpleLogRecord -__version__ = '4.2.3' +__version__ = '4.2.4' _SETUP_DONE = False FRAMEWORK = None diff --git a/test-requirements.txt b/test-requirements.txt index f06157b..e6cf178 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -14,4 +14,4 @@ pylint==1.9.5; python_version == '2.7' pylint==2.5.3; python_version >= '3.5' tox django==1.11.29; python_version == '2.7' -django==2.2.13; python_version >= '3.5' +django==2.2.28; python_version >= '3.5' From b7007c74e74c9370976184aa0b4d736cb3beb973 Mon Sep 17 00:00:00 2001 From: Michael Haas Date: Tue, 1 Aug 2023 13:10:23 +0200 Subject: [PATCH 26/35] fix: include stacktrace for non-error logs (#69) The Python logging framework allows users to pass an exc_info kwarg to the logging methods, in which case the stacktrace is included even if loggers other than Logger.exception() is used. --- CHANGELOG.md | 6 ++++++ sap/cf_logging/__init__.py | 2 +- sap/cf_logging/record/simple_log_record.py | 2 +- setup.py | 3 +++ tests/test_job_logging.py | 19 +++++++++++++++++++ 5 files changed, 30 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b658b6c..ac7b605 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ This project adheres to [Semantic Versioning](http://semver.org/). The format is based on [Keep a Changelog](http://keepachangelog.com/). +## 4.2.5 - 2023-07-28 + +### Fixed + +- Include stacktrace also for non-error log level if exc_info is present + ## 4.2.4 - 2022-04-26 ### Update diff --git a/sap/cf_logging/__init__.py b/sap/cf_logging/__init__.py index 8610d08..1d4fdef 100644 --- a/sap/cf_logging/__init__.py +++ b/sap/cf_logging/__init__.py @@ -10,7 +10,7 @@ from sap.cf_logging.record.request_log_record import RequestWebRecord from sap.cf_logging.record.simple_log_record import SimpleLogRecord -__version__ = '4.2.4' +__version__ = '4.2.5' _SETUP_DONE = False FRAMEWORK = None diff --git a/sap/cf_logging/record/simple_log_record.py b/sap/cf_logging/record/simple_log_record.py index 59174fe..87accb4 100644 --- a/sap/cf_logging/record/simple_log_record.py +++ b/sap/cf_logging/record/simple_log_record.py @@ -75,7 +75,7 @@ def format(self): 'msg': self.getMessage(), }) - if self.levelno == logging.ERROR and self.exc_info: + if self.exc_info: stacktrace = ''.join(traceback.format_exception(*self.exc_info)) stacktrace = format_stacktrace(stacktrace) record['stacktrace'] = stacktrace.split('\n') diff --git a/setup.py b/setup.py index 9a04ca7..48e8966 100644 --- a/setup.py +++ b/setup.py @@ -38,6 +38,9 @@ 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', + 'Programming Language :: Python :: 3.10', + 'Programming Language :: Python :: 3.11', 'Topic :: Software Development :: Libraries :: Python Modules', 'Topic :: Software Development', 'Topic :: System :: Logging', diff --git a/tests/test_job_logging.py b/tests/test_job_logging.py index 7cb2aaa..6e22fc4 100644 --- a/tests/test_job_logging.py +++ b/tests/test_job_logging.py @@ -68,6 +68,25 @@ def test_exception_stacktrace(): assert error == {} assert 'ZeroDivisionError' in str(log_json['stacktrace']) + assert 'ZeroDivisionError' in log_json["msg"] + + +def test_exception_stacktrace_info_level(): + """ Test exception stacktrace is logged """ + cf_logging.init(level=logging.DEBUG) + logger, stream = config_logger('cli.test') + + try: + return 1 / 0 + except ZeroDivisionError as exc: + logger.info('zero division error', exc_info=exc) + log_json = JSONDecoder().decode(stream.getvalue()) + _, error = JsonValidator(JOB_LOG_SCHEMA).validate(log_json) + + assert error == {} + assert 'ZeroDivisionError' in str(log_json['stacktrace']) + assert 'ZeroDivisionError' in log_json["msg"] + def test_custom_fields_set(): """ Test custom fields are set up """ From 0eb770052ffcefb769c16de9816c19f8a48fa912 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 7 Feb 2024 17:56:49 +0000 Subject: [PATCH 27/35] Bump django from 1.11.29 to 3.2.24 Bumps [django](https://github.com/django/django) from 1.11.29 to 3.2.24. - [Commits](https://github.com/django/django/compare/1.11.29...3.2.24) --- updated-dependencies: - dependency-name: django dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index e6cf178..8a5f424 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -14,4 +14,4 @@ pylint==1.9.5; python_version == '2.7' pylint==2.5.3; python_version >= '3.5' tox django==1.11.29; python_version == '2.7' -django==2.2.28; python_version >= '3.5' +django==3.2.24; python_version >= '3.5' From 7b70bae912941eac781b931418d49fd24124dce3 Mon Sep 17 00:00:00 2001 From: tnikolova82 <97507942+tnikolova82@users.noreply.github.com> Date: Mon, 26 Feb 2024 14:57:57 +0200 Subject: [PATCH 28/35] Update CHANGELOG.md --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ac7b605..9d4756a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ This project adheres to [Semantic Versioning](http://semver.org/). The format is based on [Keep a Changelog](http://keepachangelog.com/). +## 4.2.6 - 2024-02-26 + +### Update + + - Update django version => 3.2.24 + ## 4.2.5 - 2023-07-28 ### Fixed From 768868acd004a29c259662c62867cbcddde116a4 Mon Sep 17 00:00:00 2001 From: i343759 Date: Mon, 26 Feb 2024 15:12:02 +0200 Subject: [PATCH 29/35] intit.py update version 4.2.6 --- sap/cf_logging/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sap/cf_logging/__init__.py b/sap/cf_logging/__init__.py index 1d4fdef..6b5db7f 100644 --- a/sap/cf_logging/__init__.py +++ b/sap/cf_logging/__init__.py @@ -10,7 +10,7 @@ from sap.cf_logging.record.request_log_record import RequestWebRecord from sap.cf_logging.record.simple_log_record import SimpleLogRecord -__version__ = '4.2.5' +__version__ = '4.2.6' _SETUP_DONE = False FRAMEWORK = None From b787c6a6b496c207c1094bdcde8c14bd35ce4bba Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 18 Mar 2024 21:50:59 +0000 Subject: [PATCH 30/35] Bump django from 1.11.29 to 3.2.25 Bumps [django](https://github.com/django/django) from 1.11.29 to 3.2.25. - [Commits](https://github.com/django/django/compare/1.11.29...3.2.25) --- updated-dependencies: - dependency-name: django dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index 8a5f424..8758159 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -14,4 +14,4 @@ pylint==1.9.5; python_version == '2.7' pylint==2.5.3; python_version >= '3.5' tox django==1.11.29; python_version == '2.7' -django==3.2.24; python_version >= '3.5' +django==3.2.25; python_version >= '3.5' From 5a84f713f3e5185a46f96c7c6f700779e61aec69 Mon Sep 17 00:00:00 2001 From: i041084 Date: Wed, 26 Jun 2024 15:23:06 +0300 Subject: [PATCH 31/35] remove stacktrace from message --- sap/cf_logging/record/simple_log_record.py | 2 +- tests/test_job_logging.py | 4 ++-- tests/unit/formatters/test_json_formatter.py | 1 - 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/sap/cf_logging/record/simple_log_record.py b/sap/cf_logging/record/simple_log_record.py index 87accb4..5054894 100644 --- a/sap/cf_logging/record/simple_log_record.py +++ b/sap/cf_logging/record/simple_log_record.py @@ -80,7 +80,7 @@ def format(self): stacktrace = format_stacktrace(stacktrace) record['stacktrace'] = stacktrace.split('\n') record['msg'] += "\n" - record['msg'] += stacktrace + # record['msg'] += stacktrace record.update(self.extra) diff --git a/tests/test_job_logging.py b/tests/test_job_logging.py index 6e22fc4..c3abc79 100644 --- a/tests/test_job_logging.py +++ b/tests/test_job_logging.py @@ -68,7 +68,7 @@ def test_exception_stacktrace(): assert error == {} assert 'ZeroDivisionError' in str(log_json['stacktrace']) - assert 'ZeroDivisionError' in log_json["msg"] + assert log_json["msg"] == 'zero division error\n' def test_exception_stacktrace_info_level(): @@ -85,7 +85,7 @@ def test_exception_stacktrace_info_level(): assert error == {} assert 'ZeroDivisionError' in str(log_json['stacktrace']) - assert 'ZeroDivisionError' in log_json["msg"] + assert log_json["msg"] == 'zero division error\n' def test_custom_fields_set(): diff --git a/tests/unit/formatters/test_json_formatter.py b/tests/unit/formatters/test_json_formatter.py index 3144c99..251de4f 100644 --- a/tests/unit/formatters/test_json_formatter.py +++ b/tests/unit/formatters/test_json_formatter.py @@ -48,5 +48,4 @@ def test_stacktrace_is_added_to_msg_field(): assert "Dummy Exception" in "".join(record_object["stacktrace"]) expected_msg = "Error found!" expected_msg += "\n" - expected_msg += "\n".join(record_object["stacktrace"]) assert record_object["msg"] == expected_msg From 535af4d18115726bd5bb5ca02d66db69c895b614 Mon Sep 17 00:00:00 2001 From: i041084 Date: Wed, 26 Jun 2024 15:44:10 +0300 Subject: [PATCH 32/35] clean --- sap/cf_logging/record/simple_log_record.py | 1 - 1 file changed, 1 deletion(-) diff --git a/sap/cf_logging/record/simple_log_record.py b/sap/cf_logging/record/simple_log_record.py index 5054894..674207a 100644 --- a/sap/cf_logging/record/simple_log_record.py +++ b/sap/cf_logging/record/simple_log_record.py @@ -80,7 +80,6 @@ def format(self): stacktrace = format_stacktrace(stacktrace) record['stacktrace'] = stacktrace.split('\n') record['msg'] += "\n" - # record['msg'] += stacktrace record.update(self.extra) From ee97b6a019c027cab98f09d6c7247b1a4d3229a8 Mon Sep 17 00:00:00 2001 From: Tsvetelina Marinova Date: Thu, 27 Jun 2024 16:30:59 +0300 Subject: [PATCH 33/35] version update --- sap/cf_logging/__init__.py | 2 +- sap/cf_logging/record/simple_log_record.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/sap/cf_logging/__init__.py b/sap/cf_logging/__init__.py index 6b5db7f..2a45bc9 100644 --- a/sap/cf_logging/__init__.py +++ b/sap/cf_logging/__init__.py @@ -10,7 +10,7 @@ from sap.cf_logging.record.request_log_record import RequestWebRecord from sap.cf_logging.record.simple_log_record import SimpleLogRecord -__version__ = '4.2.6' +__version__ = '4.2.7' _SETUP_DONE = False FRAMEWORK = None diff --git a/sap/cf_logging/record/simple_log_record.py b/sap/cf_logging/record/simple_log_record.py index 674207a..af8f151 100644 --- a/sap/cf_logging/record/simple_log_record.py +++ b/sap/cf_logging/record/simple_log_record.py @@ -79,7 +79,6 @@ def format(self): stacktrace = ''.join(traceback.format_exception(*self.exc_info)) stacktrace = format_stacktrace(stacktrace) record['stacktrace'] = stacktrace.split('\n') - record['msg'] += "\n" record.update(self.extra) From 226c79bdf415f8e0650766fa3b8f55ed74e9bdcc Mon Sep 17 00:00:00 2001 From: i041084 Date: Thu, 27 Jun 2024 16:41:28 +0300 Subject: [PATCH 34/35] adapt tests and changelog --- CHANGELOG.md | 6 ++++++ tests/test_job_logging.py | 4 ++-- tests/unit/formatters/test_json_formatter.py | 1 - 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d4756a..a6c6f76 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ This project adheres to [Semantic Versioning](http://semver.org/). The format is based on [Keep a Changelog](http://keepachangelog.com/). +## 4.2.7 - 2024-06-27 + +### Fixed + + - Remove stacktrace from the message element of the log + ## 4.2.6 - 2024-02-26 ### Update diff --git a/tests/test_job_logging.py b/tests/test_job_logging.py index c3abc79..b7e1aec 100644 --- a/tests/test_job_logging.py +++ b/tests/test_job_logging.py @@ -68,7 +68,7 @@ def test_exception_stacktrace(): assert error == {} assert 'ZeroDivisionError' in str(log_json['stacktrace']) - assert log_json["msg"] == 'zero division error\n' + assert log_json["msg"] == 'zero division error' def test_exception_stacktrace_info_level(): @@ -85,7 +85,7 @@ def test_exception_stacktrace_info_level(): assert error == {} assert 'ZeroDivisionError' in str(log_json['stacktrace']) - assert log_json["msg"] == 'zero division error\n' + assert log_json["msg"] == 'zero division error' def test_custom_fields_set(): diff --git a/tests/unit/formatters/test_json_formatter.py b/tests/unit/formatters/test_json_formatter.py index 251de4f..56b3b8d 100644 --- a/tests/unit/formatters/test_json_formatter.py +++ b/tests/unit/formatters/test_json_formatter.py @@ -47,5 +47,4 @@ def test_stacktrace_is_added_to_msg_field(): record_object = json.loads(FORMATTER.format(log_record)) assert "Dummy Exception" in "".join(record_object["stacktrace"]) expected_msg = "Error found!" - expected_msg += "\n" assert record_object["msg"] == expected_msg From 06c6febbe6585d830963953b97b75d91650d6b93 Mon Sep 17 00:00:00 2001 From: Tsvetelina Marinova Date: Fri, 28 Jun 2024 10:38:58 +0300 Subject: [PATCH 35/35] version 4.2.7 (#78) Co-authored-by: Tsvetelina Marinova --- CHANGELOG.md | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a6c6f76..7a6dfaf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ This project adheres to [Semantic Versioning](http://semver.org/). The format is based on [Keep a Changelog](http://keepachangelog.com/). + ## 4.2.7 - 2024-06-27 ### Fixed @@ -41,7 +42,6 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/). - Fix stacktrace format - ## 4.2.1 - 2021-02-23 ### Added @@ -52,21 +52,18 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/). - Fix context store for Sanic - ## 4.1.1 - 2019-04-19 ### Fixed - Fix logging not usable outside request - ## 4.1.0 - 2018-09-13 ### Added - Django support - ## 4.0.1 - 2018-07-10 ### Fixed