Repository: hynek/structlog

Issue:
ConsoleRender accepts only string as input
Hello :)

First of all I would like to thank you for this very handy package. 

I have observed the following:

```python
import structlog

structlog.configure(
    processors=[
        structlog.stdlib.add_logger_name,
        structlog.stdlib.add_log_level,
        structlog.stdlib.PositionalArgumentsFormatter(),
        structlog.processors.TimeStamper(fmt="iso"),
        structlog.processors.StackInfoRenderer(),
        structlog.processors.format_exc_info,
        structlog.dev.ConsoleRenderer()
    ],
    context_class=dict,
    logger_factory=structlog.stdlib.LoggerFactory(),
    wrapper_class=structlog.stdlib.BoundLogger,
    cache_logger_on_first_use=True,
)

mylogger = structlog.get_logger('foo').new()

mylogger.info('bar')
>> 2018-11-08T10:49:46.903028Z [info  ] bar              [foo]

mylogger.info({'value': 'bar'})
>> Raises TypeError: unsupported operand type(s) for +: 'dict' and 'str'
```

After checking the code for ConsoleRender, it seems that it has been implemented to accept only str as value to log. On the other hand when using JSONRenderer, it logs without any problem a single dictionary as log message. Is this by design ? Am I missing something ?

Comments:

Patch:
diff --git a/src/structlog/dev.py b/src/structlog/dev.py
index 0160650..372f7fc 100644
--- a/src/structlog/dev.py
+++ b/src/structlog/dev.py
@@ -8,7 +8,7 @@ Helpers that make development with ``structlog`` more pleasant.
 
 from __future__ import absolute_import, division, print_function
 
-from six import StringIO
+from six import PY2, StringIO, string_types
 
 
 try:
@@ -213,7 +213,11 @@ class ConsoleRenderer(object):
                 + "] "
             )
 
+        # force event to str for compatibility with standard library
         event = event_dict.pop("event")
+        if not PY2 or not isinstance(event, string_types):
+            event = str(event)
+
         if event_dict:
             event = _pad(event, self._pad_event) + self._styles.reset + " "
         else:


Test patch:
diff --git a/tests/test_dev.py b/tests/test_dev.py
index ee048a4..11690ba 100644
--- a/tests/test_dev.py
+++ b/tests/test_dev.py
@@ -81,6 +81,27 @@ class TestConsoleRenderer(object):
 
         assert (styles.timestamp + "42" + styles.reset + " " + unpadded) == rv
 
+    def test_event_stringified(self, cr, styles, unpadded):
+        """
+        Event is cast to string.
+        """
+        not_a_string = Exception("test")
+
+        rv = cr(None, None, {"event": not_a_string})
+
+        assert unpadded == rv
+
+    @pytest.mark.skipif(not six.PY2, reason="Problem only exists on Python 2.")
+    @pytest.mark.parametrize("s", [u"\xc3\xa4".encode("utf-8"), u"ä", "ä"])
+    def test_event_py2_only_stringify_non_strings(self, cr, s, styles):
+        """
+        If event is a string type already, leave it be on Python 2. Running
+        str() on unicode strings with non-ascii characters raises an error.
+        """
+        rv = cr(None, None, {"event": s})
+
+        assert styles.bright + s + styles.reset == rv
+
     def test_level(self, cr, styles, padded):
         """
         Levels are rendered aligned, in square brackets, and color coded.