Repository: RedHatInsights/insights-core

Issue:
exceptions logged at warn level when calling get_canonical_facts
When calling [get_canonical_facts](https://github.com/RedHatInsights/insights-core/blob/master/insights/util/canonical_facts.py#L74), some exceptions which usually get logged at debug level are getting logged at warn level from the [main component evaluation loop](https://github.com/RedHatInsights/insights-core/blob/master/insights/core/dr.py#L964). This is because some datasource failures are delayed until the first time they're accessed, which could be in the context of another component invocation (parser, etc.)

Here's an example of what's happening when the client attempts to collect canonical facts on platforms that aren't registered:

```
Traceback (most recent call last):
 File "/var/lib/insights/newest.egg/insights/core/dr.py", line 952, in run
 result = DELEGATES[component].process(broker)
 File "/var/lib/insights/newest.egg/insights/core/dr.py", line 673, in process
 return self.invoke(broker)
 File "/var/lib/insights/newest.egg/insights/core/plugins.py", line 90, in invoke
 return self.component(dep_value)
 File "/var/lib/insights/newest.egg/insights/core/__init__.py", line 82, in __init__
 self._handle_content(context)
 File "/var/lib/insights/newest.egg/insights/core/__init__.py", line 85, in _handle_content
 self.parse_content(context.content)
 File "/var/lib/insights/newest.egg/insights/core/spec_factory.py", line 110, in content
 self._content = self.load()
 File "/var/lib/insights/newest.egg/insights/core/spec_factory.py", line 331, in load
 timeout=self.timeout, env=self.create_env())
 File "/var/lib/insights/newest.egg/insights/core/context.py", line 142, in shell_out
 raw = self.check_output(cmd, timeout=timeout, keep_rc=keep_rc, env=env)
 File "/var/lib/insights/newest.egg/insights/core/context.py", line 137, in check_output
 keep_rc=keep_rc, env=env)
 File "/var/lib/insights/newest.egg/insights/util/subproc.py", line 208, in call
 res = p(keep_rc=keep_rc)
 File "/var/lib/insights/newest.egg/insights/util/subproc.py", line 121, in __call__
 raise CalledProcessError(rc, self.cmds[0], output)
CalledProcessError: (1, ['timeout', '-s', '9', '30', '/usr/bin/subscription-manager', 'identity'], "This system is not yet registered. Try 'subscription-manager register --help' for more information.\n")
```
It's just a warning from the logger, but it's an eyesore and not relevant for that particular collection.


Discussion:

Resolving patch:
diff --git a/insights/core/plugins.py b/insights/core/plugins.py
--- a/insights/core/plugins.py
+++ b/insights/core/plugins.py
@@ -43,10 +43,39 @@ class ContentException(dr.SkipComponent):
     pass
 
 
-component = dr.ComponentType
+class PluginType(dr.ComponentType):
+    """
+    PluginType is the base class of plugin types like datasource, rule, etc.
+    It provides a default invoke method that catches exceptions we don't
+    want bubbling to the top of the evaluation loop. These exceptions are
+    commonly raised by datasource components but could be in the context of any
+    component since most datasource runtime errors are lazy.
+
+    It's possible for a datasource to "succeed" and return an object but for an
+    exception to be raised when the parser tries to access the content of that
+    object. For example, when a command datasource is evaluated, it only checks
+    that the command exists and is executable. Invocation of the command itself
+    is delayed until the parser asks for its value. This helps with performance
+    and memory consumption.
+    """
+    def invoke(self, broker):
+        try:
+            return super(PluginType, self).invoke(broker)
+        except ContentException as ce:
+            log.debug(ce)
+            broker.add_exception(self.component, ce, traceback.format_exc())
+            raise dr.SkipComponent()
+        except CalledProcessError as cpe:
+            log.debug(cpe)
+            broker.add_exception(self.component, cpe, traceback.format_exc())
+            raise dr.SkipComponent()
 
 
-class datasource(dr.ComponentType):
+class component(PluginType):
+    pass
+
+
+class datasource(PluginType):
     """
     Decorates a component that one or more :class:`insights.core.Parser`
     subclasses will consume.
@@ -68,7 +97,7 @@ def invoke(self, broker):
             raise dr.SkipComponent()
 
 
-class parser(dr.ComponentType):
+class parser(PluginType):
     """
     Decorates a component responsible for parsing the output of a
     :class:`datasource`. ``@parser`` should accept only one argument, which is
@@ -87,7 +116,16 @@ def __init__(self, dep, group=dr.GROUPS.single):
     def invoke(self, broker):
         dep_value = broker[self.requires[0]]
         if not isinstance(dep_value, list):
-            return self.component(dep_value)
+            try:
+                return self.component(dep_value)
+            except ContentException as ce:
+                log.debug(ce)
+                broker.add_exception(self.component, ce, traceback.format_exc())
+                raise dr.SkipComponent()
+            except CalledProcessError as cpe:
+                log.debug(cpe)
+                broker.add_exception(self.component, cpe, traceback.format_exc())
+                raise dr.SkipComponent()
 
         results = []
         for d in dep_value:
@@ -97,6 +135,12 @@ def invoke(self, broker):
                     results.append(r)
             except dr.SkipComponent:
                 pass
+            except ContentException as ce:
+                log.debug(ce)
+                broker.add_exception(self.component, ce, traceback.format_exc())
+            except CalledProcessError as cpe:
+                log.debug(cpe)
+                broker.add_exception(self.component, cpe, traceback.format_exc())
             except Exception as ex:
                 tb = traceback.format_exc()
                 log.warn(tb)
@@ -120,7 +164,7 @@ class metadata(parser):
     requires = ["metadata.json"]
 
 
-class combiner(dr.ComponentType):
+class combiner(PluginType):
     """
     A decorator for a component that composes or "combines" other components.
 
@@ -131,12 +175,12 @@ class combiner(dr.ComponentType):
     pass
 
 
-class remoteresource(dr.ComponentType):
+class remoteresource(PluginType):
     """ ComponentType for a component for remote web resources. """
     pass
 
 
-class rule(dr.ComponentType):
+class rule(PluginType):
     """
     Decorator for components that encapsulate some logic that depends on the
     data model of a system. Rules can depend on :class:`datasource` instances,
@@ -233,7 +277,7 @@ def process(self, broker):
         return r
 
 
-class condition(dr.ComponentType):
+class condition(PluginType):
     """
     ComponentType used to encapsulate boolean logic you'd like to have analyzed
     by a rule analysis system. Conditions should return truthy values. ``None``
@@ -243,7 +287,7 @@ class condition(dr.ComponentType):
     pass
 
 
-class incident(dr.ComponentType):
+class incident(PluginType):
     """
     ComponentType for a component used by rules that allows automated
     statistical analysis.
@@ -251,7 +295,7 @@ class incident(dr.ComponentType):
     pass
 
 
-class fact(dr.ComponentType):
+class fact(PluginType):
     """
     ComponentType for a component that surfaces a dictionary or list of
     dictionaries that will be used later by cluster rules. The data from a fact


Test patch: