From f1168469f30cf94904af077ad21fde25e6793ce8 Mon Sep 17 00:00:00 2001 From: Roni Riska Date: Thu, 4 Jul 2019 12:28:47 +0300 Subject: [PATCH] First release version Change-Id: I1c868f6927754884cae2d2280580b84452a5cb65 Signed-off-by: Roni Riska --- .gitignore | 23 +++++ LICENSES.txt | 34 +++++++ README.md | 104 ++++++++++++++++++++ mdclogpy/Logger.py | 149 ++++++++++++++++++++++++++++ mdclogpy/__init__.py | 85 ++++++++++++++++ mdclogpy/tst/__init__.py | 14 +++ mdclogpy/tst/mdclogtestutils.py | 31 ++++++ mdclogpy/tst/test_Logger.py | 210 ++++++++++++++++++++++++++++++++++++++++ mdclogpy/tst/test_mdclogpy.py | 119 +++++++++++++++++++++++ setup.py | 39 ++++++++ 10 files changed, 808 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSES.txt create mode 100644 README.md create mode 100644 mdclogpy/Logger.py create mode 100644 mdclogpy/__init__.py create mode 100644 mdclogpy/tst/__init__.py create mode 100644 mdclogpy/tst/mdclogtestutils.py create mode 100644 mdclogpy/tst/test_Logger.py create mode 100644 mdclogpy/tst/test_mdclogpy.py create mode 100644 setup.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..598ee7e --- /dev/null +++ b/.gitignore @@ -0,0 +1,23 @@ +# Copyright (c) 2019 AT&T Intellectual Property. +# Copyright (c) 2018-2019 Nokia. +# +# 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. + +# Compiled python modules. +*.pyc + +# Setuptools distribution folder. +/dist/ + +# Python egg metadata, regenerated from source files by setuptools. +/*.egg-info diff --git a/LICENSES.txt b/LICENSES.txt new file mode 100644 index 0000000..40d7038 --- /dev/null +++ b/LICENSES.txt @@ -0,0 +1,34 @@ +LICENSES.txt + + +Unless otherwise specified, all software contained herein is licensed +under the Apache License, Version 2.0 (the "Software License"); +you may not use this software except in compliance with the Software +License. You may obtain a copy of the Software License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the Software License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the Software License for the specific language governing permissions +and limitations under the Software License. + + + +Unless otherwise specified, all documentation contained herein is licensed +under the Creative Commons License, Attribution 4.0 Intl. (the +"Documentation License"); you may not use this documentation except in +compliance with the Documentation License. You may obtain a copy of the +Documentation License at + +https://creativecommons.org/licenses/by/4.0/ + +Unless required by applicable law or agreed to in writing, documentation +distributed under the Documentation License is distributed on an "AS IS" +BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +implied. See the Documentation License for the specific language governing +permissions and limitations under the Documentation License. + + + diff --git a/README.md b/README.md new file mode 100644 index 0000000..9b75be2 --- /dev/null +++ b/README.md @@ -0,0 +1,104 @@ +Mdclogpy +======== + +Structured logging library with Mapped Diagnostic Context + +* Outputs the log entries to standard out in structured format, json currently. +* Severity based filtering. +* Supports Mapped Diagnostic Context (MDC). + Set MDC pairs are automatically added to log entries by the library. + + +Log entry format +---------------- + +Each log entry written with mdclog_write() function contains + +* Timestamp +* Logger identity +* Log entry severity +* All existing MDC pairs +* Log message text + +Currently the library only supports JSON formatted output written to standard +out of the process. + +*Example log output* + +`{"ts": 1559285893047, "crit": "INFO", "id": "myprog", "mdc": {"second key":"other value","mykey":"keyval"}, "msg": "Hello world!"}` + +Install +------- + +Install from PyPi + +``` +python3 -m pip install mdclogpy +``` + +Install using the source + +``` +python3 setup.py install +``` + +Usage +----- + +The library can be used in two ways shown below. + +1) Use the root logger + +```python + import mdclogpy + mdclogpy.error("This is an error log") +``` + +2) Create a logger instance + +```python + from mdclogpy import Logger + my_logger = Logger() + my_logger.error("This is an error log") +``` + +A program can create several logger instances. + + +Mapped Diagnostics Context +-------------------------- + +The MDCs are logger instance specific key-value pairs, which are included to +all log entries written via the logger instance. + +By default, the library implements a root logger instance. +MDCs added to the root logger instance are added only to the log entries +written via the root logger instance. + + +License +------- + +Copyright (c) 2019 AT&T Intellectual Property. +Copyright (c) 2018-2019 Nokia. + +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. + + +Unit testing +------------ + +To run the unit tests run the following command in the package directory:: +` +python3 -m unittest discover +` diff --git a/mdclogpy/Logger.py b/mdclogpy/Logger.py new file mode 100644 index 0000000..93e4cf2 --- /dev/null +++ b/mdclogpy/Logger.py @@ -0,0 +1,149 @@ +# Copyright (c) 2019 AT&T Intellectual Property. +# Copyright (c) 2018-2019 Nokia. +# +# 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. + +"""Structured logging library with Mapped Diagnostic Context + +Outputs the log entries to standard out in structured format, json currently. +Severity based filtering. +Supports Mapped Diagnostic Context (MDC). + +Set MDC pairs are automatically added to log entries by the library. +""" +from typing import TypeVar +from enum import IntEnum +import sys +import json +import time + + +class Level(IntEnum): + """Severity levels of the log messages.""" + DEBUG = 10 + INFO = 20 + WARNING = 30 + ERROR = 40 + + +LEVEL_STRINGS = {Level.DEBUG: "DEBUG", + Level.INFO: "INFO", + Level.WARNING: "WARNING", + Level.ERROR: "ERROR"} + + +Value = TypeVar('Value', str, int) + + +class Logger(): + """Initialize the mdclogging module. + Calling of the function is optional. If not called, the process name + (sys.argv[0]) is used by default. + + Keyword arguments: + name -- name of the component. The name will appear as part of the log + entries. + """ + def __init__(self, name: str = sys.argv[0], level: Level = Level.DEBUG): + """Initialize a Logger instance. + + Keyword arguments: + name -- name of the component. The name will appear as part of the + log entries. + """ + self.procname = name + self.current_level = level + self.mdc = {} + + def _output_log(self, log: str): + """Output the log, currently to stdout.""" + print(log) + + def log(self, level: Level, message: str): + """Log a message. + + Logs the message with the given severity if it is equal or higher than + the current logging level. + + Keyword arguments: + level -- severity of the log message + message -- log message + """ + if level >= self.current_level: + log_entry = {} + log_entry["ts"] = int(round(time.time() * 1000)) + log_entry["crit"] = LEVEL_STRINGS[level] + log_entry["id"] = self.procname + log_entry["mdc"] = self.mdc + log_entry["msg"] = message + self._output_log(json.dumps(log_entry)) + + def error(self, message: str): + """Log an error message. Equals to log(ERROR, msg).""" + self.log(Level.ERROR, message) + + def warning(self, message: str): + """Log a warning message. Equals to log(WARNING, msg).""" + self.log(Level.WARNING, message) + + def info(self, message: str): + """Log an info message. Equals to log(INFO, msg).""" + self.log(Level.INFO, message) + + def debug(self, message: str): + """Log a debug message. Equals to log(DEBUG, msg).""" + self.log(Level.DEBUG, message) + + def set_level(self, level: Level): + """Set current logging level. + + Keyword arguments: + level -- logging level. Log messages with lower severity will be + filtered. + """ + if level in Level: + self.current_level = level + + def get_level(self) -> Level: + """Return the current logging level.""" + return self.current_level + + def add_mdc(self, key: str, value: Value): + """Add a logger specific MDC. + + If an MDC with the given key exists, it is replaced with the new one. + An MDC can be removed with remove_mdc() or clean_mdc(). + + Keyword arguments: + key -- MDC key + value -- MDC value + """ + self.mdc[key] = value + + def get_mdc(self, key: str) -> Value: + """Return logger's MDC value with the given key or None.""" + try: + return self.mdc[key] + except KeyError: + return None + + def remove_mdc(self, key: str): + """Remove logger's MDC with the given key.""" + try: + del self.mdc[key] + except KeyError: + pass + + def clean_mdc(self): + """Remove all MDCs of the logger instance.""" + self.mdc = {} diff --git a/mdclogpy/__init__.py b/mdclogpy/__init__.py new file mode 100644 index 0000000..4afb9a3 --- /dev/null +++ b/mdclogpy/__init__.py @@ -0,0 +1,85 @@ +# Copyright (c) 2019 AT&T Intellectual Property. +# Copyright (c) 2018-2019 Nokia. +# +# 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. + +"""Structured logging library with Mapped Diagnostic Context + +Outputs the log entries to standard out in structured format, json currently. +Severity based filtering. +Supports Mapped Diagnostic Context (MDC). + +Set MDC pairs are automatically added to log entries by the library. +""" + +from .Logger import Logger +from .Logger import Level +from .Logger import Value + + +_root_logger = Logger() + + +def log(level: Level, message: str): + """Log a message.""" + _root_logger.log(level, message) + + +def error(message: str): + """Log an error message. Equals to log(ERROR, msg).""" + _root_logger.log(Level.ERROR, message) + + +def warning(message: str): + """Log a warning message. Equals to log(WARNING, msg).""" + _root_logger.log(Level.WARNING, message) + + +def info(message: str): + """Log an info message. Equals to log(INFO, msg).""" + _root_logger.log(Level.INFO, message) + + +def debug(message: str): + """Log a debug message. Equals to log(DEBUG, msg).""" + _root_logger.log(Level.DEBUG, message) + + +def set_level(level: Level): + """Set current logging level.""" + _root_logger.set_level(level) + + +def get_level() -> Level: + """Return the current logging level.""" + return _root_logger.get_level() + + +def add_mdc(key: str, value: Value): + """Add an MDC to the root logger.""" + _root_logger.add_mdc(key, value) + + +def get_mdc(key: str) -> Value: + """Return root logger's MDC with the given key or None.""" + return _root_logger.get_mdc(key) + + +def remove_mdc(key: str): + """Remove root logger's MDC with the given key.""" + _root_logger.remove_mdc(key) + + +def clean_mdc(): + """Remove all MDCs from the root logger.""" + _root_logger.clean_mdc() diff --git a/mdclogpy/tst/__init__.py b/mdclogpy/tst/__init__.py new file mode 100644 index 0000000..f928659 --- /dev/null +++ b/mdclogpy/tst/__init__.py @@ -0,0 +1,14 @@ +# Copyright (c) 2019 AT&T Intellectual Property. +# Copyright (c) 2018-2019 Nokia. +# +# 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/mdclogpy/tst/mdclogtestutils.py b/mdclogpy/tst/mdclogtestutils.py new file mode 100644 index 0000000..4fb362c --- /dev/null +++ b/mdclogpy/tst/mdclogtestutils.py @@ -0,0 +1,31 @@ +# Copyright (c) 2019 AT&T Intellectual Property. +# Copyright (c) 2018-2019 Nokia. +# +# 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. + +"""Helper functions for mdclogpy unit tests.""" + +import json + +class TestMdcLogUtils(): + """Helper functions for unit tests.""" + + @staticmethod + def get_logs(call_args_list): + """Return the logs as a list of strings from the call_args_list.""" + return [x[0][0] for x in call_args_list] + + @staticmethod + def get_logs_as_json(call_args_list): + """Return the logs as a list of json objects from the call_args_list.""" + return list(map(json.loads, TestMdcLogUtils.get_logs(call_args_list))) diff --git a/mdclogpy/tst/test_Logger.py b/mdclogpy/tst/test_Logger.py new file mode 100644 index 0000000..d40b9ba --- /dev/null +++ b/mdclogpy/tst/test_Logger.py @@ -0,0 +1,210 @@ +# Copyright (c) 2019 AT&T Intellectual Property. +# Copyright (c) 2018-2019 Nokia. +# +# 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. + +"""Unit tests for Logger.py""" +import unittest +from unittest.mock import patch +import sys + +from mdclogpy import Logger +from mdclogpy import Level +import mdclogpy +from .mdclogtestutils import TestMdcLogUtils + + +class TestMdcLog(unittest.TestCase): + """Unit tests for mdclog.py""" + + def setUp(self): + self.logger = Logger() + + def tearDown(self): + pass + + + def test_that_get_level_returns_the_current_log_level(self): + + # default level is DEBUG + self.assertEqual(self.logger.get_level(), Level.DEBUG) + self.logger.set_level(Level.INFO) + self.assertEqual(self.logger.get_level(), Level.INFO) + self.logger.set_level(Level.WARNING) + self.assertEqual(self.logger.get_level(), Level.WARNING) + self.logger.set_level(Level.ERROR) + self.assertEqual(self.logger.get_level(), Level.ERROR) + self.logger.set_level(Level.DEBUG) + self.assertEqual(self.logger.get_level(), Level.DEBUG) + + def test_that_set_level_does_not_accept_incorrect_level(self): + + self.logger.set_level(Level.INFO) + self.logger.set_level(55) + self.assertEqual(self.logger.get_level(), Level.INFO) + + @patch('mdclogpy.Logger._output_log') + def test_that_logs_with_lower_than_current_level_(self, output_mock): + + self.logger.set_level(Level.WARNING) + self.logger.log(Level.DEBUG, "DEBUG") + self.logger.log(Level.INFO, "INFO") + self.logger.log(Level.WARNING, "WARNING") + self.logger.log(Level.ERROR, "ERROR") + + self.assertEqual(2, output_mock.call_count) + logs = TestMdcLogUtils.get_logs_as_json(output_mock.call_args_list) + self.assertEqual(logs[0]["msg"], "WARNING") + self.assertEqual(logs[1]["msg"], "ERROR") + + @patch('mdclogpy.Logger._output_log') + def test_that_logs_with_lower_than_current_level_are_not_logged(self, output_mock): + + self.logger.set_level(Level.WARNING) + self.logger.log(Level.DEBUG, "DEBUG") + self.logger.log(Level.INFO, "INFO") + self.logger.log(Level.WARNING, "WARNING") + self.logger.log(Level.ERROR, "ERROR") + + self.assertEqual(2, output_mock.call_count) + logs = TestMdcLogUtils.get_logs_as_json(output_mock.call_args_list) + self.assertEqual(logs[0]["msg"], "WARNING") + self.assertEqual(logs[1]["msg"], "ERROR") + + @patch('mdclogpy.Logger._output_log') + def test_that_log_contains_correct_criticality(self, output_mock): + + self.logger.set_level(Level.DEBUG) + + self.logger.log(Level.DEBUG, "debug test log") + self.logger.log(Level.INFO, "info test log") + self.logger.log(Level.WARNING, "warning test log") + self.logger.log(Level.ERROR, "error test log") + + self.logger.debug("another debug test log") + self.logger.info("another info test log") + self.logger.warning("another warning test log") + self.logger.error("another error test log") + + self.assertEqual(8, output_mock.call_count) + logs = TestMdcLogUtils.get_logs_as_json(output_mock.call_args_list) + self.assertEqual(logs[0]["crit"], "DEBUG") + self.assertEqual(logs[1]["crit"], "INFO") + self.assertEqual(logs[2]["crit"], "WARNING") + self.assertEqual(logs[3]["crit"], "ERROR") + self.assertEqual(logs[4]["crit"], "DEBUG") + self.assertEqual(logs[5]["crit"], "INFO") + self.assertEqual(logs[6]["crit"], "WARNING") + self.assertEqual(logs[7]["crit"], "ERROR") + + @patch('time.time') + @patch('mdclogpy.Logger._output_log') + def test_that_log_contains_correct_timestamp(self, output_mock, mock_time): + + mock_time.return_value = 1554806251.4388545 + self.logger.info("timestamp test") + + logs = TestMdcLogUtils.get_logs_as_json(output_mock.call_args_list) + self.assertEqual(logs[0]["ts"], 1554806251439) + + @patch('mdclogpy.Logger._output_log') + def test_that_log_contains_correct_message(self, output_mock): + + self.logger.info("message test") + logs = TestMdcLogUtils.get_logs_as_json(output_mock.call_args_list) + self.assertEqual(logs[0]["msg"], "message test") + + @patch('mdclogpy.Logger._output_log') + def test_that_log_message_is_escaped_to_valid_json_string(self, output_mock): + + self.logger.set_level(Level.DEBUG) + + self.logger.info('\ and "') + + logs = TestMdcLogUtils.get_logs(output_mock.call_args_list) + self.assertTrue(r'\\ and \"' in logs[0]) + logs = TestMdcLogUtils.get_logs_as_json(output_mock.call_args_list) + self.assertEqual(logs[0]["msg"], '\ and "') + + + @patch('mdclogpy.Logger._output_log') + def test_that_empty_mdc_is_logged_correctly(self, output_mock): + + self.logger.error("empty mdc test") + logs = TestMdcLogUtils.get_logs_as_json(output_mock.call_args_list) + self.assertEqual(logs[0]["mdc"], {}) + + @patch('mdclogpy.Logger._output_log') + def test_that_mdc_values_are_logged_correctly(self, output_mock): + + self.logger.add_mdc("key1", "value1") + self.logger.add_mdc("key2", "value2") + self.logger.error("mdc test") + + logs = TestMdcLogUtils.get_logs_as_json(output_mock.call_args_list) + self.assertEqual(logs[0]["mdc"]["key1"], "value1") + self.assertEqual(logs[0]["mdc"]["key2"], "value2") + + def test_that_mdc_values_can_be_added_and_removed(self): + + self.logger.add_mdc("key1", "value1") + self.logger.add_mdc("key2", "value2") + self.assertEqual(self.logger.get_mdc("key2"), "value2") + self.assertEqual(self.logger.get_mdc("key1"), "value1") + self.assertEqual(self.logger.get_mdc("non_existent"), None) + self.logger.remove_mdc("key1") + self.assertEqual(self.logger.get_mdc("key1"), None) + self.logger.remove_mdc("non_existent") + self.logger.clean_mdc() + self.assertEqual(self.logger.get_mdc("key2"), None) + + @patch('mdclogpy.Logger._output_log') + def test_multiple_logger_instances(self, output_mock): + + logger1 = Logger("logger1") + logger2 = Logger("logger2") + logger1.add_mdc("logger1_key1", "logger1_value1") + logger1.add_mdc("logger1_key2", "logger1_value2") + logger2.add_mdc("logger2_key1", "logger2_value1") + logger2.add_mdc("logger2_key2", "logger2_value2") + mdclogpy.add_mdc("key", "value") + + logger1.error("error msg") + logger2.warning("warning msg") + mdclogpy.info("info msg") + + logs = TestMdcLogUtils.get_logs_as_json(output_mock.call_args_list) + self.assertEqual(3, output_mock.call_count) + + self.assertEqual(logs[0]["id"], "logger1") + self.assertEqual(logs[0]["crit"], "ERROR") + self.assertEqual(logs[0]["msg"], "error msg") + self.assertEqual(logs[0]["mdc"]["logger1_key1"], "logger1_value1") + self.assertEqual(logs[0]["mdc"]["logger1_key2"], "logger1_value2") + self.assertEqual(len(logs[0]["mdc"]), 2) + + self.assertEqual(logs[1]["id"], "logger2") + self.assertEqual(logs[1]["crit"], "WARNING") + self.assertEqual(logs[1]["msg"], "warning msg") + self.assertEqual(logs[1]["mdc"]["logger2_key1"], "logger2_value1") + self.assertEqual(logs[1]["mdc"]["logger2_key2"], "logger2_value2") + self.assertEqual(len(logs[1]["mdc"]), 2) + + self.assertEqual(logs[2]["id"], sys.argv[0]) + self.assertEqual(logs[2]["crit"], "INFO") + self.assertEqual(logs[2]["msg"], "info msg") + self.assertEqual(logs[2]["mdc"]["key"], "value") + self.assertEqual(len(logs[2]["mdc"]), 1) + +if __name__ == '__main__': + unittest.main() diff --git a/mdclogpy/tst/test_mdclogpy.py b/mdclogpy/tst/test_mdclogpy.py new file mode 100644 index 0000000..3fb0455 --- /dev/null +++ b/mdclogpy/tst/test_mdclogpy.py @@ -0,0 +1,119 @@ +# Copyright (c) 2019 AT&T Intellectual Property. +# Copyright (c) 2018-2019 Nokia. +# +# 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. + +"""Unit tests for mdclogpy root logger""" +import unittest +from unittest.mock import patch +import sys + +import mdclogpy +from .mdclogtestutils import TestMdcLogUtils + + +class TestMdcLog(unittest.TestCase): + """Unit tests for mdclog.py""" + + def setUp(self): + self.prog_id = sys.argv[0] + + def tearDown(self): + pass + + + @patch('mdclogpy.Logger._output_log') + def test_that_root_logger_logs_the_message_using_the_proc_name(self, output_mock): + + mdclogpy.log(mdclogpy.Level.DEBUG, "This is a test log") + mdclogpy.error("This is an error log") + mdclogpy.warning("This is a warning log") + mdclogpy.info("This is an info log") + mdclogpy.debug("This is a debug log") + + logs = TestMdcLogUtils.get_logs_as_json(output_mock.call_args_list) + self.assertEqual(self.prog_id, logs[0]["id"]) + self.assertEqual(self.prog_id, logs[1]["id"]) + self.assertEqual(self.prog_id, logs[2]["id"]) + self.assertEqual(self.prog_id, logs[3]["id"]) + self.assertEqual(self.prog_id, logs[4]["id"]) + self.assertEqual("This is a test log", logs[0]["msg"]) + self.assertEqual("This is an error log", logs[1]["msg"]) + self.assertEqual("This is a warning log", logs[2]["msg"]) + self.assertEqual("This is an info log", logs[3]["msg"]) + self.assertEqual("This is a debug log", logs[4]["msg"]) + + def test_that_root_logger_get_level_returns_the_current_log_level(self): + + # default level is DEBUG + self.assertEqual(mdclogpy.get_level(), mdclogpy.Level.DEBUG) + mdclogpy.set_level(mdclogpy.Level.INFO) + self.assertEqual(mdclogpy.get_level(), mdclogpy.Level.INFO) + mdclogpy.set_level(mdclogpy.Level.WARNING) + self.assertEqual(mdclogpy.get_level(), mdclogpy.Level.WARNING) + mdclogpy.set_level(mdclogpy.Level.ERROR) + self.assertEqual(mdclogpy.get_level(), mdclogpy.Level.ERROR) + mdclogpy.set_level(mdclogpy.Level.DEBUG) + self.assertEqual(mdclogpy.get_level(), mdclogpy.Level.DEBUG) + + @patch('mdclogpy.Logger._output_log') + def test_that_root_logger_logs_with_correct_criticality(self, output_mock): + + mdclogpy.set_level(mdclogpy.Level.DEBUG) + + mdclogpy.log(mdclogpy.Level.DEBUG, "debug test log") + mdclogpy.log(mdclogpy.Level.INFO, "info test log") + mdclogpy.log(mdclogpy.Level.WARNING, "warning test log") + mdclogpy.log(mdclogpy.Level.ERROR, "error test log") + + mdclogpy.debug("another debug test log") + mdclogpy.info("another info test log") + mdclogpy.warning("another warning test log") + mdclogpy.error("another error test log") + + logs = TestMdcLogUtils.get_logs_as_json(output_mock.call_args_list) + self.assertEqual(8, output_mock.call_count) + self.assertEqual(logs[0]["crit"], "DEBUG") + self.assertEqual(logs[1]["crit"], "INFO") + self.assertEqual(logs[2]["crit"], "WARNING") + self.assertEqual(logs[3]["crit"], "ERROR") + self.assertEqual(logs[4]["crit"], "DEBUG") + self.assertEqual(logs[5]["crit"], "INFO") + self.assertEqual(logs[6]["crit"], "WARNING") + self.assertEqual(logs[7]["crit"], "ERROR") + + @patch('mdclogpy.Logger._output_log') + def test_that_root_logger_logs_mdc_values_correctly(self, output_mock): + + mdclogpy.add_mdc("key1", "value1") + mdclogpy.add_mdc("key2", "value2") + mdclogpy.error("mdc test") + + logs = TestMdcLogUtils.get_logs_as_json(output_mock.call_args_list) + self.assertEqual(logs[0]["mdc"]["key1"], "value1") + self.assertEqual(logs[0]["mdc"]["key2"], "value2") + + @patch('mdclogpy.Logger._output_log') + def test_that_non_printable_characters_are_logged_correctly(self, output_mock): + + mdclogpy.set_level(mdclogpy.Level.DEBUG) + mdclogpy.info("line feed\ntest") + mdclogpy.info("tab\ttest") + mdclogpy.info("carriage return\rtest") + logs = TestMdcLogUtils.get_logs_as_json(output_mock.call_args_list) + self.assertEqual(logs[0]["msg"], "line feed\ntest") + self.assertEqual(logs[1]["msg"], "tab\ttest") + self.assertEqual(logs[2]["msg"], "carriage return\rtest") + +if __name__ == '__main__': + unittest.main() diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..1c4d762 --- /dev/null +++ b/setup.py @@ -0,0 +1,39 @@ +# Copyright (c) 2019 AT&T Intellectual Property. +# Copyright (c) 2018-2019 Nokia. +# +# 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. + +"""Setup file for mdclogpy library.""" + +from setuptools import setup + +def readme(): + with open('README.md') as f: + return f.read() + +setup(name='mdclogpy', + version='1.0', + description='Structured logging library with Mapped Diagnostic Context', + long_description=readme(), + long_description_content_type="text/markdown", + classifiers=[ + 'Development Status :: 5 - Production/Stable', + 'License :: OSI Approved :: Apache Software License', + 'Programming Language :: Python :: 3 :: Only', + 'Topic :: Software Development :: Libraries :: Python Modules', + ], + url='https://gerrit.o-ran-sc.org/r/admin/repos/com/pylog', + author_email='kturunen@nokia.com', + license='Apache Software License', + packages=['mdclogpy'], + zip_safe=False) \ No newline at end of file -- 2.16.6