Repository: StackStorm/st2

Issue:
Incorrect casing for log level config causes errors
Support any case?

Example (EL6),
in `/etc/st2/logging.actionrunner.conf`
if you have
```
[logger_root]
level=debug
```
instead of 
```
[logger_root]
level=DEBUG
```

..this happens
```
$ sudo /opt/stackstorm/st2/bin/python /opt/stackstorm/st2/bin/st2actionrunner --config-file /etc/st2/st2.conf
2017-06-23 11:26:37,074 DEBUG [-] Using config files: /etc/st2/st2.conf
2017-06-23 11:26:37,075 DEBUG [-] Using logging config: /etc/st2/logging.actionrunner.conf
ERROR: Traceback (most recent call last):
  File "/opt/stackstorm/st2/lib/python2.7/site-packages/st2common/log.py", line 186, in setup
    disable_existing_loggers=disable_existing_loggers)
  File "/usr/share/python/st2python/lib/python2.7/logging/config.py", line 85, in fileConfig
    handlers = _install_handlers(cp, formatters)
  File "/usr/share/python/st2python/lib/python2.7/logging/config.py", line 166, in _install_handlers
    h.setLevel(logging._levelNames[level])
KeyError: 'debug'
2017-06-23 11:26:37,077 ERROR [-] (PID=1189) Worker quit due to exception.
Traceback (most recent call last):
  File "/opt/stackstorm/st2/lib/python2.7/site-packages/st2actions/cmd/actionrunner.py", line 81, in main
    _setup()
  File "/opt/stackstorm/st2/lib/python2.7/site-packages/st2actions/cmd/actionrunner.py", line 36, in _setup
    register_signal_handlers=True)
  File "/opt/stackstorm/st2/lib/python2.7/site-packages/st2common/service_setup.py", line 92, in setup
    excludes=cfg.CONF.log.excludes)
  File "/opt/stackstorm/st2/lib/python2.7/site-packages/st2common/log.py", line 196, in setup
    raise Exception(six.text_type(exc))
Exception: 'debug'
```


Discussion:

Resolving patch:
diff --git a/st2common/st2common/log.py b/st2common/st2common/log.py
--- a/st2common/st2common/log.py
+++ b/st2common/st2common/log.py
@@ -189,8 +189,16 @@ def setup(config_file, redirect_stderr=True, excludes=None, disable_existing_log
         if redirect_stderr:
             _redirect_stderr()
     except Exception as exc:
+        exc_cls = type(exc)
+        tb_msg = traceback.format_exc()
+
+        msg = str(exc)
+        msg += '\n\n' + tb_msg
+
         # revert stderr redirection since there is no logger in place.
         sys.stderr = sys.__stderr__
+
         # No logger yet therefore write to stderr
-        sys.stderr.write('ERROR: %s' % traceback.format_exc())
-        raise Exception(six.text_type(exc))
+        sys.stderr.write('ERROR: %s' % (msg))
+
+        raise exc_cls(six.text_type(msg))
diff --git a/st2common/st2common/service_setup.py b/st2common/st2common/service_setup.py
--- a/st2common/st2common/service_setup.py
+++ b/st2common/st2common/service_setup.py
@@ -20,6 +20,7 @@
 from __future__ import absolute_import
 
 import os
+import traceback
 
 from oslo_config import cfg
 
@@ -88,8 +89,18 @@ def setup(service, config, setup_db=True, register_mq_exchanges=True,
     logging_config_path = os.path.abspath(logging_config_path)
 
     LOG.debug('Using logging config: %s', logging_config_path)
-    logging.setup(logging_config_path, redirect_stderr=cfg.CONF.log.redirect_stderr,
-                  excludes=cfg.CONF.log.excludes)
+
+    try:
+        logging.setup(logging_config_path, redirect_stderr=cfg.CONF.log.redirect_stderr,
+                      excludes=cfg.CONF.log.excludes)
+    except KeyError as e:
+        tb_msg = traceback.format_exc()
+        if 'log.setLevel' in tb_msg:
+            msg = 'Invalid log level selected. Log level names need to be all uppercase.'
+            msg += '\n\n' + getattr(e, 'message', str(e))
+            raise KeyError(msg)
+        else:
+            raise e
 
     if cfg.CONF.debug or cfg.CONF.system.debug:
         enable_debugging()


Test patch:
diff --git a/st2common/tests/unit/test_service_setup.py b/st2common/tests/unit/test_service_setup.py
new file mode 100644
--- /dev/null
+++ b/st2common/tests/unit/test_service_setup.py
@@ -0,0 +1,89 @@
+# Licensed to the StackStorm, Inc ('StackStorm') under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You 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 tempfile
+
+from st2common import service_setup
+
+from st2tests.base import CleanFilesTestCase
+from st2tests import config
+
+__all__ = [
+    'ServiceSetupTestCase'
+]
+
+MOCK_LOGGING_CONFIG_INVALID_LOG_LEVEL = """
+[loggers]
+keys=root
+
+[handlers]
+keys=consoleHandler
+
+[formatters]
+keys=simpleConsoleFormatter
+
+[logger_root]
+level=invalid_log_level
+handlers=consoleHandler
+
+[handler_consoleHandler]
+class=StreamHandler
+level=DEBUG
+formatter=simpleConsoleFormatter
+args=(sys.stdout,)
+
+[formatter_simpleConsoleFormatter]
+class=st2common.logging.formatters.ConsoleLogFormatter
+format=%(asctime)s %(levelname)s [-] %(message)s
+datefmt=
+""".strip()
+
+
+class ServiceSetupTestCase(CleanFilesTestCase):
+    def test_no_logging_config_found(self):
+        def mock_get_logging_config_path():
+            return ''
+
+        config.get_logging_config_path = mock_get_logging_config_path
+
+        expected_msg = "No section: .*"
+        self.assertRaisesRegexp(Exception, expected_msg,
+                                service_setup.setup, service='api',
+                                config=config,
+                                setup_db=False, register_mq_exchanges=False,
+                                register_signal_handlers=False,
+                                register_internal_trigger_types=False,
+                                run_migrations=False)
+
+    def test_invalid_log_level_friendly_error_message(self):
+        _, mock_logging_config_path = tempfile.mkstemp()
+        self.to_delete_files.append(mock_logging_config_path)
+
+        with open(mock_logging_config_path, 'w') as fp:
+            fp.write(MOCK_LOGGING_CONFIG_INVALID_LOG_LEVEL)
+
+        def mock_get_logging_config_path():
+            return mock_logging_config_path
+
+        config.get_logging_config_path = mock_get_logging_config_path
+
+        expected_msg = 'Invalid log level selected. Log level names need to be all uppercase'
+        self.assertRaisesRegexp(KeyError, expected_msg,
+                                service_setup.setup, service='api',
+                                config=config,
+                                setup_db=False, register_mq_exchanges=False,
+                                register_signal_handlers=False,
+                                register_internal_trigger_types=False,
+                                run_migrations=False)