First release version 65/465/2
authorRoni Riska <roni.riska@nokia.com>
Thu, 4 Jul 2019 09:28:47 +0000 (12:28 +0300)
committerRoni Riska <roni.riska@nokia.com>
Thu, 4 Jul 2019 12:03:49 +0000 (15:03 +0300)
Change-Id: I1c868f6927754884cae2d2280580b84452a5cb65
Signed-off-by: Roni Riska <roni.riska@nokia.com>
.gitignore [new file with mode: 0644]
LICENSES.txt [new file with mode: 0644]
README.md [new file with mode: 0644]
mdclogpy/Logger.py [new file with mode: 0644]
mdclogpy/__init__.py [new file with mode: 0644]
mdclogpy/tst/__init__.py [new file with mode: 0644]
mdclogpy/tst/mdclogtestutils.py [new file with mode: 0644]
mdclogpy/tst/test_Logger.py [new file with mode: 0644]
mdclogpy/tst/test_mdclogpy.py [new file with mode: 0644]
setup.py [new file with mode: 0644]

diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..598ee7e
--- /dev/null
@@ -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 (file)
index 0000000..40d7038
--- /dev/null
@@ -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 (file)
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 (file)
index 0000000..93e4cf2
--- /dev/null
@@ -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 (file)
index 0000000..4afb9a3
--- /dev/null
@@ -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 (file)
index 0000000..f928659
--- /dev/null
@@ -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 (file)
index 0000000..4fb362c
--- /dev/null
@@ -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 (file)
index 0000000..d40b9ba
--- /dev/null
@@ -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 (file)
index 0000000..3fb0455
--- /dev/null
@@ -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 (file)
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