--- /dev/null
+# 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
--- /dev/null
+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.
+
+
+
--- /dev/null
+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
+`
--- /dev/null
+# 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 = {}
--- /dev/null
+# 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()
--- /dev/null
+# 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.
--- /dev/null
+# 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)))
--- /dev/null
+# 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()
--- /dev/null
+# 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()
--- /dev/null
+# 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