[MRG+1] Retry stats
diff --git a/scrapy/downloadermiddlewares/retry.py b/scrapy/downloadermiddlewares/retry.py
index c9c512be8a6..549d74f46f6 100644
--- a/scrapy/downloadermiddlewares/retry.py
+++ b/scrapy/downloadermiddlewares/retry.py
@@ -22,6 +22,7 @@
 from scrapy.exceptions import NotConfigured
 from scrapy.utils.response import response_status_message
 from scrapy.core.downloader.handlers.http11 import TunnelError
+from scrapy.utils.python import global_object_name
 
 logger = logging.getLogger(__name__)
 
@@ -62,6 +63,7 @@ def process_exception(self, request, exception, spider):
     def _retry(self, request, reason, spider):
         retries = request.meta.get('retry_times', 0) + 1
 
+        stats = spider.crawler.stats
         if retries <= self.max_retry_times:
             logger.debug("Retrying %(request)s (failed %(retries)d times): %(reason)s",
                          {'request': request, 'retries': retries, 'reason': reason},
@@ -70,8 +72,15 @@ def _retry(self, request, reason, spider):
             retryreq.meta['retry_times'] = retries
             retryreq.dont_filter = True
             retryreq.priority = request.priority + self.priority_adjust
+
+            if isinstance(reason, Exception):
+                reason = global_object_name(reason.__class__)
+
+            stats.inc_value('retry/count')
+            stats.inc_value('retry/reason_count/%s' % reason)
             return retryreq
         else:
+            stats.inc_value('retry/max_reached')
             logger.debug("Gave up retrying %(request)s (failed %(retries)d times): %(reason)s",
                          {'request': request, 'retries': retries, 'reason': reason},
                          extra={'spider': spider})
diff --git a/scrapy/downloadermiddlewares/stats.py b/scrapy/downloadermiddlewares/stats.py
index 9c0ad90a597..ef0aafce0fe 100644
--- a/scrapy/downloadermiddlewares/stats.py
+++ b/scrapy/downloadermiddlewares/stats.py
@@ -1,6 +1,8 @@
 from scrapy.exceptions import NotConfigured
 from scrapy.utils.request import request_httprepr
 from scrapy.utils.response import response_httprepr
+from scrapy.utils.python import global_object_name
+
 
 class DownloaderStats(object):
 
@@ -27,6 +29,6 @@ def process_response(self, request, response, spider):
         return response
 
     def process_exception(self, request, exception, spider):
-        ex_class = "%s.%s" % (exception.__class__.__module__, exception.__class__.__name__)
+        ex_class = global_object_name(exception.__class__)
         self.stats.inc_value('downloader/exception_count', spider=spider)
         self.stats.inc_value('downloader/exception_type_count/%s' % ex_class, spider=spider)
diff --git a/scrapy/utils/misc.py b/scrapy/utils/misc.py
index 30c9e5058bf..35f8550072f 100644
--- a/scrapy/utils/misc.py
+++ b/scrapy/utils/misc.py
@@ -113,7 +113,7 @@ def md5sum(file):
         m.update(d)
     return m.hexdigest()
 
+
 def rel_has_nofollow(rel):
     """Return True if link rel attribute has nofollow type"""
     return True if rel is not None and 'nofollow' in rel.split() else False
-    
diff --git a/scrapy/utils/python.py b/scrapy/utils/python.py
index 42fbbda7fb1..4c500abf4cc 100644
--- a/scrapy/utils/python.py
+++ b/scrapy/utils/python.py
@@ -344,3 +344,14 @@ def without_none_values(iterable):
         return {k: v for k, v in six.iteritems(iterable) if v is not None}
     except AttributeError:
         return type(iterable)((v for v in iterable if v is not None))
+
+
+def global_object_name(obj):
+    """
+    Return full name of a global object.
+
+    >>> from scrapy import Request
+    >>> global_object_name(Request)
+    'scrapy.http.request.Request'
+    """
+    return "%s.%s" % (obj.__module__, obj.__name__)
diff --git a/scrapy/utils/response.py b/scrapy/utils/response.py
index deb5741be1a..bf276b5caa9 100644
--- a/scrapy/utils/response.py
+++ b/scrapy/utils/response.py
@@ -43,7 +43,8 @@ def get_meta_refresh(response):
 def response_status_message(status):
     """Return status code plus status text descriptive message
     """
-    return '%s %s' % (status, to_native_str(http.RESPONSES.get(int(status), "Unknown Status")))
+    message = http.RESPONSES.get(int(status), "Unknown Status")
+    return '%s %s' % (status, to_native_str(message))
 
 
 def response_httprepr(response):
diff --git a/tests/test_downloadermiddleware_retry.py b/tests/test_downloadermiddleware_retry.py
index e129b71f8bf..b833cb4488b 100644
--- a/tests/test_downloadermiddleware_retry.py
+++ b/tests/test_downloadermiddleware_retry.py
@@ -13,9 +13,9 @@
 
 class RetryTest(unittest.TestCase):
     def setUp(self):
-        crawler = get_crawler(Spider)
-        self.spider = crawler._create_spider('foo')
-        self.mw = RetryMiddleware.from_crawler(crawler)
+        self.crawler = get_crawler(Spider)
+        self.spider = self.crawler._create_spider('foo')
+        self.mw = RetryMiddleware.from_crawler(self.crawler)
         self.mw.max_retry_times = 2
 
     def test_priority_adjust(self):
@@ -70,6 +70,10 @@ def test_503(self):
         # discard it
         assert self.mw.process_response(req, rsp, self.spider) is rsp
 
+        assert self.crawler.stats.get_value('retry/max_reached') == 1
+        assert self.crawler.stats.get_value('retry/reason_count/503 Service Unavailable') == 2
+        assert self.crawler.stats.get_value('retry/count') == 2
+
     def test_twistederrors(self):
         exceptions = [defer.TimeoutError, TCPTimedOutError, TimeoutError,
                 DNSLookupError, ConnectionRefusedError, ConnectionDone,
@@ -79,6 +83,11 @@ def test_twistederrors(self):
             req = Request('http://www.scrapytest.org/%s' % exc.__name__)
             self._test_retry_exception(req, exc('foo'))
 
+        stats = self.crawler.stats
+        assert stats.get_value('retry/max_reached') == len(exceptions)
+        assert stats.get_value('retry/count') == len(exceptions) * 2
+        assert stats.get_value('retry/reason_count/twisted.internet.defer.TimeoutError') == 2
+
     def _test_retry_exception(self, req, exception):
         # first retry
         req = self.mw.process_exception(req, exception, self.spider)
diff --git a/tests/test_proxy_connect.py b/tests/test_proxy_connect.py
index 0f06fd53dea..6213a51e866 100644
--- a/tests/test_proxy_connect.py
+++ b/tests/test_proxy_connect.py
@@ -101,7 +101,9 @@ def test_https_noconnect_auth_error(self):
         self._assert_got_response_code(407, l)
 
     def _assert_got_response_code(self, code, log):
+        print(log)
         self.assertEqual(str(log).count('Crawled (%d)' % code), 1)
 
     def _assert_got_tunnel_error(self, log):
-        self.assertEqual(str(log).count('TunnelError'), 1)
+        print(log)
+        self.assertIn('TunnelError', str(log))

What do you think about providing retry counts in scrapy stats?

~~This PR is not backwards compatible for anyone who used `RetryMiddleware.__init__` directly.~~