Repository: DataDog/dd-agent

Issue:
MySQL checker throws "'NoneType' object is not iterable" iterable without tags
If a host that the MySQL checker is running on has no tags configured, the checker defaults to a `tags` value of `None` here:

https://github.com/DataDog/dd-agent/blob/master/checks.d/mysql.py#L317

Eventually, when `tags` is used in a context that requires an iterable value here:

https://github.com/DataDog/dd-agent/blob/master/checks.d/mysql.py#L598

a `'NoneType' object is not iterable` exception is thrown.



Discussion:
Hey @berendo 

Thanks for reporting this issue. I managed to reproduce, we will work on a fix and ship it with the next bugfix release which should go out shortly.

Sorry for the trouble.

Until we push the fix, a workaround is to add `tags: []` to the failing instance in `mysql.yaml` to avoid the fallback to `None` that makes the check fail.


Resolving patch:
diff --git a/checks.d/mysql.py b/checks.d/mysql.py
--- a/checks.d/mysql.py
+++ b/checks.d/mysql.py
@@ -250,7 +250,7 @@
 }
 
 PERFORMANCE_VARS = {
-    'query_run_time_avg' : ('mysql.performance.query_run_time.avg', GAUGE),
+    'query_run_time_avg': ('mysql.performance.query_run_time.avg', GAUGE),
     'perf_digest_95th_percentile_avg_us': ('mysql.performance.digest_95th_percentile.avg_us', GAUGE),
 }
 
@@ -268,6 +268,7 @@
     'Qcache_instant_utilization': ('mysql.performance.qcache.utilization.instant', GAUGE),
 }
 
+
 class MySql(AgentCheck):
     SERVICE_CHECK_NAME = 'mysql.can_connect'
     SLAVE_SERVICE_CHECK_NAME = 'mysql.replication.slave_running'
@@ -314,7 +315,7 @@ def _get_config(self, instance):
         self.defaults_file = instance.get('defaults_file', '')
         user = instance.get('user', '')
         password = instance.get('pass', '')
-        tags = instance.get('tags', None)
+        tags = instance.get('tags', [])
         options = instance.get('options', {})
         queries = instance.get('queries', [])
         ssl = instance.get('ssl', {})
@@ -480,7 +481,6 @@ def _collect_metrics(self, host, db, tags, options, queries):
         except TypeError as e:
             self.log.error("Not all Key metrics are available, unable to compute: {0}".format(e))
 
-
         metrics.update(VARIABLES_VARS)
         metrics.update(INNODB_VARS)
         metrics.update(BINLOG_VARS)
@@ -567,7 +567,7 @@ def _collect_metrics(self, host, db, tags, options, queries):
             if k not in results:
                 metrics.pop(k, None)
 
-        #add duped metrics - reporting some as both rate and gauge
+        # add duped metrics - reporting some as both rate and gauge
         dupes = [('Table_locks_waited', 'Table_locks_waited_rate'),
                  ('Table_locks_immediate', 'Table_locks_immediate_rate')]
         for src, dst in dupes:
@@ -707,7 +707,7 @@ def _collect_dict(self, metric_type, field_metric_map, query, db, tags):
                                     "Received value is None for index %d" % col_idx)
                         except ValueError:
                             self.log.exception("Cannot find %s in the columns %s"
-                                            % (field, cursor.description))
+                                               % (field, cursor.description))
         except Exception:
             self.warning("Error while running %s\n%s" %
                          (query, traceback.format_exc()))
@@ -731,7 +731,6 @@ def _collect_system_metrics(self, host, db, tags):
                     ucpu = proc.cpu_times()[0]
                     scpu = proc.cpu_times()[1]
 
-
                 if ucpu and scpu:
                     self.rate("mysql.performance.user_time", ucpu, tags=tags)
                     # should really be system_time
@@ -970,7 +969,7 @@ def _get_stats_from_innodb_status(self, db):
                     results['Innodb_pending_normal_aio_reads'] = (long(row[4]) + long(row[5]) +
                                                                   long(row[6]) + long(row[7]))
                     results['Innodb_pending_normal_aio_writes'] = (long(row[11]) + long(row[12]) +
-                                                                  long(row[13]) + long(row[14]))
+                                                                   long(row[13]) + long(row[14]))
                 elif len(row) == 18:
                     results['Innodb_pending_normal_aio_reads'] = long(row[4])
                     results['Innodb_pending_normal_aio_writes'] = long(row[12])


Test patch:
diff --git a/tests/checks/integration/test_mysql.py b/tests/checks/integration/test_mysql.py
--- a/tests/checks/integration/test_mysql.py
+++ b/tests/checks/integration/test_mysql.py
@@ -14,7 +14,13 @@ class TestMySql(AgentCheckTest):
     METRIC_TAGS = ['tag1', 'tag2']
     SC_TAGS = ['server:localhost', 'port:unix_socket']
 
-    MYSQL_CONFIG = [{
+    MYSQL_MINIMAL_CONFIG = [{
+        'server': 'localhost',
+        'user': 'dog',
+        'pass': 'dog'
+    }]
+
+    MYSQL_COMPLEX_CONFIG = [{
         'server': 'localhost',
         'user': 'dog',
         'pass': 'dog',
@@ -307,8 +313,23 @@ def _test_optional_metrics(self, optional_metrics, at_least):
 
         self.assertTrue(after - before > at_least)
 
-    def test_check(self):
-        config = {'instances': self.MYSQL_CONFIG}
+    def test_minimal_config(self):
+        config = {'instances': self.MYSQL_MINIMAL_CONFIG}
+        self.run_check_twice(config)
+
+        # Test service check
+        self.assertServiceCheck('mysql.can_connect', status=AgentCheck.OK,
+                                tags=self.SC_TAGS, count=1)
+
+        # Test metrics
+        testable_metrics = (self.STATUS_VARS + self.VARIABLES_VARS + self.INNODB_VARS
+                            + self.BINLOG_VARS + self.SYSTEM_METRICS + self.SYNTHETIC_VARS)
+
+        for mname in testable_metrics:
+            self.assertMetric(mname, count=1)
+
+    def test_complex_config(self):
+        config = {'instances': self.MYSQL_COMPLEX_CONFIG}
         self.run_check_twice(config)
 
         # Test service check
@@ -323,7 +344,7 @@ def test_check(self):
         ver = tuple(ver)
 
         testable_metrics = (self.STATUS_VARS + self.VARIABLES_VARS + self.INNODB_VARS
-                      + self.BINLOG_VARS + self.SYSTEM_METRICS + self.SCHEMA_VARS + self.SYNTHETIC_VARS)
+                            + self.BINLOG_VARS + self.SYSTEM_METRICS + self.SCHEMA_VARS + self.SYNTHETIC_VARS)
 
         if ver >= (5, 6, 0):
             testable_metrics.extend(self.PERFORMANCE_VARS)
@@ -348,7 +369,6 @@ def test_check(self):
             else:
                 self.assertMetric(mname, tags=self.METRIC_TAGS, count=1)
 
-
         # Assert service metadata
         self.assertServiceMetadata(['version'], count=1)