[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