Repository: scrapy/scrapy

Issue:
Logformatter for errors
## Summary

Allow users to specify the logging behaviour in case of errors in the item pipeline

## Motivation

Looking at https://github.com/scrapy/scrapy/blob/3adf09bc4f1468f54a703481d6d7ebfe25b6f232/scrapy/core/scraper.py, it is possible to override the logging behaviour for the `DropItem` exception, but there is no equivalent
`self.logformatter.error`.

In our case, sometimes the item can be very large, in which case it being printed completely to the log is problematic -- specifically, on ScrapyingHub the log entry is refused and the exception is not visible.

## Describe alternatives you've considered
Modify that file adding a generic error logformatter function.



Discussion:

Resolving patch:
diff --git a/scrapy/core/scraper.py b/scrapy/core/scraper.py
--- a/scrapy/core/scraper.py
+++ b/scrapy/core/scraper.py
@@ -231,9 +231,9 @@ def _itemproc_finished(self, output, item, response, spider):
                     signal=signals.item_dropped, item=item, response=response,
                     spider=spider, exception=output.value)
             else:
-                logger.error('Error processing %(item)s', {'item': item},
-                             exc_info=failure_to_exc_info(output),
-                             extra={'spider': spider})
+                logkws = self.logformatter.error(item, ex, response, spider)
+                logger.log(*logformatter_adapter(logkws), extra={'spider': spider},
+                           exc_info=failure_to_exc_info(output))
                 return self.signals.send_catch_log_deferred(
                     signal=signals.item_error, item=item, response=response,
                     spider=spider, failure=output)
diff --git a/scrapy/logformatter.py b/scrapy/logformatter.py
--- a/scrapy/logformatter.py
+++ b/scrapy/logformatter.py
@@ -8,6 +8,7 @@
 SCRAPEDMSG = u"Scraped from %(src)s" + os.linesep + "%(item)s"
 DROPPEDMSG = u"Dropped: %(exception)s" + os.linesep + "%(item)s"
 CRAWLEDMSG = u"Crawled (%(status)s) %(request)s%(request_flags)s (referer: %(referer)s)%(response_flags)s"
+ERRORMSG = u"'Error processing %(item)s'"
 
 
 class LogFormatter(object):
@@ -92,6 +93,16 @@ def dropped(self, item, exception, response, spider):
             }
         }
 
+    def error(self, item, exception, response, spider):
+        """Logs a message when an item causes an error while it is passing through the item pipeline."""
+        return {
+            'level': logging.ERROR,
+            'msg': ERRORMSG,
+            'args': {
+                'item': item,
+            }
+        }
+
     @classmethod
     def from_crawler(cls, crawler):
         return cls()


Test patch:
diff --git a/tests/test_logformatter.py b/tests/test_logformatter.py
--- a/tests/test_logformatter.py
+++ b/tests/test_logformatter.py
@@ -23,13 +23,13 @@ def __str__(self):
         return "name: %s" % self['name']
 
 
-class LoggingContribTest(unittest.TestCase):
+class LogFormatterTestCase(unittest.TestCase):
 
     def setUp(self):
         self.formatter = LogFormatter()
         self.spider = Spider('default')
 
-    def test_crawled(self):
+    def test_crawled_with_referer(self):
         req = Request("http://www.example.com")
         res = Response("http://www.example.com")
         logkws = self.formatter.crawled(req, res, self.spider)
@@ -37,6 +37,7 @@ def test_crawled(self):
         self.assertEqual(logline,
             "Crawled (200) <GET http://www.example.com> (referer: None)")
 
+    def test_crawled_without_referer(self):
         req = Request("http://www.example.com", headers={'referer': 'http://example.com'})
         res = Response("http://www.example.com", flags=['cached'])
         logkws = self.formatter.crawled(req, res, self.spider)
@@ -61,6 +62,16 @@ def test_dropped(self):
         lines = logline.splitlines()
         assert all(isinstance(x, six.text_type) for x in lines)
         self.assertEqual(lines, [u"Dropped: \u2018", '{}'])
+    
+    def test_error(self):
+        # In practice, the complete traceback is shown by passing the
+        # 'exc_info' argument to the logging function
+        item = {'key': 'value'}
+        exception = Exception()
+        response = Response("http://www.example.com")
+        logkws = self.formatter.error(item, exception, response, self.spider)
+        logline = logkws['msg'] % logkws['args']
+        self.assertEqual(logline, u"'Error processing {'key': 'value'}'")
 
     def test_scraped(self):
         item = CustomItem()
@@ -75,26 +86,46 @@ def test_scraped(self):
 
 class LogFormatterSubclass(LogFormatter):
     def crawled(self, request, response, spider):
-        kwargs = super(LogFormatterSubclass, self).crawled(
-        request, response, spider)
+        kwargs = super(LogFormatterSubclass, self).crawled(request, response, spider)
         CRAWLEDMSG = (
-            u"Crawled (%(status)s) %(request)s (referer: "
-            u"%(referer)s)%(flags)s"
+            u"Crawled (%(status)s) %(request)s (referer: %(referer)s) %(flags)s"
         )
+        log_args = kwargs['args']
+        log_args['flags'] = str(request.flags)
         return {
             'level': kwargs['level'],
             'msg': CRAWLEDMSG,
-            'args': kwargs['args']
+            'args': log_args,
         }
 
 
-class LogformatterSubclassTest(LoggingContribTest):
+class LogformatterSubclassTest(LogFormatterTestCase):
     def setUp(self):
         self.formatter = LogFormatterSubclass()
         self.spider = Spider('default')
 
+    def test_crawled_with_referer(self):
+        req = Request("http://www.example.com")
+        res = Response("http://www.example.com")
+        logkws = self.formatter.crawled(req, res, self.spider)
+        logline = logkws['msg'] % logkws['args']
+        self.assertEqual(logline,
+            "Crawled (200) <GET http://www.example.com> (referer: None) []")
+
+    def test_crawled_without_referer(self):
+        req = Request("http://www.example.com", headers={'referer': 'http://example.com'}, flags=['cached'])
+        res = Response("http://www.example.com")
+        logkws = self.formatter.crawled(req, res, self.spider)
+        logline = logkws['msg'] % logkws['args']
+        self.assertEqual(logline,
+            "Crawled (200) <GET http://www.example.com> (referer: http://example.com) ['cached']")
+
     def test_flags_in_request(self):
-        pass
+        req = Request("http://www.example.com", flags=['test','flag'])
+        res = Response("http://www.example.com")
+        logkws = self.formatter.crawled(req, res, self.spider)
+        logline = logkws['msg'] % logkws['args']
+        self.assertEqual(logline, "Crawled (200) <GET http://www.example.com> (referer: None) ['test', 'flag']")
 
 
 class SkipMessagesLogFormatter(LogFormatter):