[VOL-2311]Logging - Implement dynamic log levels in OpenONU Adapter

Change-Id: Ib2dc238d704349f62a000e2bdb41ae5e70c46dc6
diff --git a/VERSION b/VERSION
index a724a9c..197c4d5 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-2.4.0-dev
+2.4.0
diff --git a/pyvoltha/adapters/common/kvstore/twisted_etcd_store.py b/pyvoltha/adapters/common/kvstore/twisted_etcd_store.py
index 7003a74..c31dcf5 100644
--- a/pyvoltha/adapters/common/kvstore/twisted_etcd_store.py
+++ b/pyvoltha/adapters/common/kvstore/twisted_etcd_store.py
@@ -17,7 +17,6 @@
 
 import etcd3
 
-
 class TwistedEtcdStore(object):
 
     def __init__(self, host, port, path_prefix):
@@ -59,6 +58,19 @@
         deferred.addErrback(failure)
         return deferred
 
+    def watch(self, key, callback):
+
+        def success(results):
+            return results
+
+        def failure(exception):
+            raise exception
+
+        deferred = threads.deferToThread(self._etcd.add_watch_callback, self.make_path(key), callback)
+        deferred.addCallback(success)
+        deferred.addErrback(failure)
+        return deferred
+
     def delete(self, key):
 
         def success(results):
diff --git a/pyvoltha/adapters/log_controller.py b/pyvoltha/adapters/log_controller.py
new file mode 100644
index 0000000..0a8df36
--- /dev/null
+++ b/pyvoltha/adapters/log_controller.py
@@ -0,0 +1,144 @@
+#
+# Copyright 2020 the original author or authors.
+#
+# 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.
+#
+import os
+import structlog
+from pyvoltha.adapters.common.kvstore.twisted_etcd_store import TwistedEtcdStore
+from pyvoltha.common.structlog_setup import setup_logging, update_logging, string_to_int
+from twisted.internet.defer import inlineCallbacks, returnValue
+
+
+COMPONENT_NAME = os.environ.get("COMPONENT_NAME")
+GLOBAL_CONFIG_ROOT_NODE = "global"
+DEFAULT_KV_STORE_CONFIG_PATH = "config"
+KV_STORE_DATA_PATH_PREFIX = "service/voltha"
+KV_STORE_PATH_SEPARATOR = "/"
+CONFIG_TYPE = "loglevel"
+DEFAULT_PACKAGE_NAME = "default"
+GLOBAL_DEFAULT_LOGLEVEL = "WARN"
+
+class LogController():
+    instance_id = None
+    active_log_level = None
+
+
+    def __init__(self, etcd_host, etcd_port):
+        self.log = structlog.get_logger()
+        self.etcd_host = etcd_host
+        self.etcd_port = etcd_port
+        self.etcd_client = TwistedEtcdStore(self.etcd_host, self.etcd_port, KV_STORE_DATA_PATH_PREFIX)
+
+    def make_config_path(self, key):
+        return (DEFAULT_KV_STORE_CONFIG_PATH + KV_STORE_PATH_SEPARATOR + key + KV_STORE_PATH_SEPARATOR + CONFIG_TYPE + KV_STORE_PATH_SEPARATOR + DEFAULT_PACKAGE_NAME)
+
+
+    @inlineCallbacks
+    def get_global_loglevel(self):
+
+        global_default_loglevel = ""
+
+        try:
+            level = yield self.etcd_client.get(self.global_config_path)
+            if level is not None:
+                level_int = string_to_int(str(level, 'utf-8'))
+
+                if level_int == 0:
+                    self.log.warn("Unsupported loglevel at global config path", level)
+                else:
+                    global_default_loglevel = level
+                    self.log.debug("Retrieved global default loglevel", global_default_loglevel)
+
+        except KeyError:
+            self.log.warn("Failed to retrive default global loglevel")
+
+        returnValue(global_default_loglevel)
+
+
+    @inlineCallbacks
+    def get_component_loglevel(self, global_default_loglevel):
+
+        component_default_loglevel = global_default_loglevel
+
+        try:
+            level = yield self.etcd_client.get(self.component_config_path)
+            if level is not None:
+                level_int = string_to_int(str(level, 'utf-8'))
+
+                if level_int == 0:
+                    self.log.warn("Unsupported loglevel at component config path", level)
+
+                else:
+                    component_default_loglevel = level
+                    self.log.debug("Retrieved component default loglevel", component_default_loglevel)
+
+        except KeyError:
+            self.log.warn("Failed to retrive default component loglevel")
+
+        if component_default_loglevel == "":
+            component_default_loglevel = GLOBAL_DEFAULT_LOGLEVEL.encode('utf-8')
+
+        returnValue(component_default_loglevel)
+
+
+    @inlineCallbacks
+    def start_watch_log_config_change(self, instance_id, initial_default_loglevel):
+
+        self.log.debug("Start watching for log config change")
+        LogController.instance_id = instance_id
+
+        if COMPONENT_NAME == None:
+            raise Exception("Unable to retrive pod component name from runtime env")
+
+        self.global_config_path = self.make_config_path(GLOBAL_CONFIG_ROOT_NODE)
+        self.component_config_path = self.make_config_path(COMPONENT_NAME)
+
+        self.set_default_loglevel(self.global_config_path, self.component_config_path, initial_default_loglevel.upper())
+        self.process_log_config_change()
+
+        yield self.etcd_client.watch(self.global_config_path, self.watch_callback)
+        yield self.etcd_client.watch(self.component_config_path, self.watch_callback)
+
+
+    def watch_callback(self, event):
+        self.process_log_config_change()
+
+
+    @inlineCallbacks
+    def process_log_config_change(self):
+        self.log.debug("Processing log config change")
+
+        global_default_level = yield self.get_global_loglevel()
+        level = yield self.get_component_loglevel(global_default_level)
+
+        level_int = string_to_int(str(level, 'utf-8'))
+
+        current_log_level = level_int
+        if LogController.active_log_level != current_log_level:
+            LogController.active_log_level = current_log_level
+            self.log.debug("Applying updated loglevel")
+            update_logging(LogController.instance_id, None, verbosity_adjust=level_int)
+
+        else:
+            self.log.debug("Loglevel not updated")
+
+
+    @inlineCallbacks
+    def set_default_loglevel(self, global_config_path, component_config_path, initial_default_loglevel):
+
+        if (yield self.etcd_client.get(global_config_path)) == None:
+            yield self.etcd_client.set(global_config_path, GLOBAL_DEFAULT_LOGLEVEL)
+
+        if (yield self.etcd_client.get(component_config_path)) == None:
+            yield self.etcd_client.set(component_config_path, initial_default_loglevel)
diff --git a/pyvoltha/common/structlog_setup.py b/pyvoltha/common/structlog_setup.py
index f0f9c87..f0f9ef3 100644
--- a/pyvoltha/common/structlog_setup.py
+++ b/pyvoltha/common/structlog_setup.py
@@ -80,7 +80,7 @@
 
     # Configure standard logging
     logging.config.dictConfig(log_config)
-    logging.root.level += 10 * verbosity_adjust
+    logging.root.level = verbosity_adjust
 
     processors = [
         add_exc_info_flag_for_exception,
@@ -100,7 +100,17 @@
     return log
 
 
-def update_logging(instance_id, vcore_id):
+def string_to_int(loglevel):
+    l = loglevel.upper()
+    if l == "DEBUG": return 10
+    elif l == "INFO": return 20
+    elif l == "WARN": return 30
+    elif l == "ERROR": return 40
+    elif l == "FATAL": return 50
+    else: return 0
+
+
+def update_logging(instance_id, vcore_id, verbosity_adjust=0):
     """
     Add the vcore id to the structured logger
     :param vcore_id:  The assigned vcore id
@@ -119,6 +129,8 @@
         event_dict['vcore_id'] = vcore_id
         return event_dict
 
+    logging.root.level = verbosity_adjust
+
     processors = [
         add_exc_info_flag_for_exception,
         structlog.processors.StackInfoRenderer(),
diff --git a/requirements.txt b/requirements.txt
index 016156c..259f140 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -5,7 +5,7 @@
 bitstring==3.1.5
 confluent-kafka==0.11.5
 docker-py==1.10.6
-etcd3==0.7.0
+etcd3==0.11.1
 future==0.18.2
 jsonpatch==1.16
 netaddr==0.7.19