Repository: tintoy/seqlog (language: python)

Issue:
Updated ToxEnv to py310 (python 3.10)
running tox as `tox -e py310` worked locally for me. The last two builds for this branch failed, so hopefully this fixes it.


Discussion / hints:

Fix patch:
diff --git a/.flake8 b/.flake8
index 6ff2de0..b6947a8 100644
--- a/.flake8
+++ b/.flake8
@@ -1,4 +1,3 @@
 [flake8]
-ignore = E501
-per-file-ignores =
-    */__init__.py: F401
+ignore = E501, F401
+
diff --git a/.travis.yml b/.travis.yml
index 5c17fba..3fd113e 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -11,7 +11,7 @@ script: tox -e ${TOXENV}
 
 deploy:
   on:
-    condition: $TOXENV == py36
+    condition: $TOXENV == py310
     repo: tintoy/seqlog
     tags: true
 
diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst
index c544b13..a51119b 100644
--- a/CONTRIBUTING.rst
+++ b/CONTRIBUTING.rst
@@ -101,7 +101,7 @@ Before you submit a pull request, check that it meets these guidelines:
 2. If the pull request adds functionality, the docs should be updated. Put
    your new functionality into a function with a docstring, and add the
    feature to the list in README.rst.
-3. The pull request should work for Python 2.6, 2.7, 3.3, 3.4 and 3.5, and for PyPy. Check
+3. The pull request should work for Python 3.6 and 3.8, and for PyPy. Check
    https://travis-ci.org/tintoy/seqlog/pull_requests
    and make sure that the tests pass for all supported Python versions.
 
@@ -110,5 +110,5 @@ Tips
 
 To run a subset of tests::
 
-$ py.test tests.test_seqlog
+$ py.test tests/test_consumer.py
 
diff --git a/HISTORY.rst b/HISTORY.rst
index 3921e6a..a0bfaed 100755
--- a/HISTORY.rst
+++ b/HISTORY.rst
@@ -1,127 +1,132 @@
-=======
-History
-=======
-
-0.3.21 (TBA)
-------------
-
-* added checking if None was passed as element of a message
-* fixed #13
-
-0.3.20 (2020-10-15)
--------------------
-
-* Added a callable to be called each time log submission fails
-
-0.3.19 (2020-10-15)
--------------------
-
-* Fixed logging messages containing braces
-
-0.3.18 (2020-09-13)
--------------------
-
-* Add an option to specify a callable in global properties
-
-0.3.17 (2020-02-14)
--------------------
-
-* Improve handling of log entries with log-record arguments of type "bytes" (tintoy/seqlog#25).
-
-0.3.16 (2019-11-02)
--------------------
-
-* Pass exception details when posting events to Seq (tintoy/seqlog#22).
-
-0.3.15 (2018-11-20)
--------------------
-
-* Fix deprecated use of `yaml.load` (tintoy/seqlog#20).
-
-0.3.13 (2018-11-20)
--------------------
-
-* Explicitly set ``Content-Type`` header to ``application/json`` when posting events to Seq (tintoy/seqlog#17).
-
-0.3.12 (2018-11-19)
--------------------
-
-* If logging fails to submit an event to Seq then log the response body, if available (tintoy/seqlog#17).
-
-0.3.11 (2018-09-22)
--------------------
-
-* Support custom ``JSONEncoder`` implementations (tintoy/seqlog#7 and tintoy/seqlog#13).
-
-0.3.10 (2018-08-11)
--------------------
-
-* Fix incorrect behaviour when configuring logging from a file (tintoy/seqlog#10).  
-  **Breaking change**: Configuring logging from file or dict will now by default override the default logger class to be ``StructuredLogger`` (this can be reverted to previous behaviour by passing ``use_structured_logger=False``).
-
-0.3.9 (2018-01-09)
-------------------
-
-* Add PyYAML as a dependency (tintoy/seqlog#6).
-
-0.3.8 (2018-01-05)
-------------------
-
-* Improve documentation for logging configuration from file (#3)
-
-0.3.7 (2018-01-05)
-------------------
-
-* Implement and document logging configuration from file (#3)
-
-0.3.4 (2017-11-27)
-------------------
-
-* Fix sample code (#2).
-
-0.3.3 (2016-11-18)
-------------------
-
-* Use streaming mode when posting to Seq (#1)
-
-0.3.2 (2016-11-18)
-------------------
-
-* Updated release notes
-
-0.3.1 (2016-11-18)
-------------------
-
-* Further work relating to intermittent "RuntimeError: The content for this response was already consumed" when publishing log entries (#1)
-
-0.3.0 (2016-11-16)
-------------------
-
-* Fix for intermittent "RuntimeError: The content for this response was already consumed" when publishing log entries (#1)
-
-0.2.0 (2016-07-09)
-------------------
-
-* Support for configuring additional log handlers when calling log_to_seq.
-* Support for global log properties (statically-configured properties that are added to all outgoing log entries).
-
-0.0.1 (2016-07-07)
-------------------
-
-* First release on PyPI.
-
-0.0.7 (2016-07-09)
-------------------
-
-* ``log_to_seq`` now returns the SeqLogHandler to enable forced flushing of log records to Seq.
-* Change ``auto_flush_timeout`` to a ``float`` representing seconds (instead of milliseconds).
-* Update ``testharness.py`` to actually log to Seq.
-  You can override the server URL and API key using the ``SEQ_SERVER_URL`` and ``SEQ_API_KEY`` environment variables.
-* Update usage information in documentation.
-* Python 3 only for now (sorry, but logging in Python 2 doesn't have all the required extensibility points). If the need to support Python 2 becomes great enough then I'll try to find a way.
-
-0.1.0 (2016-07-09)
-------------------
-
-* Proper versioning starts today :)
-
+=======
+History
+=======
+
+0.3.22 (TBA)
+------------
+
+* Add support for handling extra log properties via the `extra` argument (tintoy/seqlog#41).
+
+0.3.21 (TBA)
+------------
+
+* added checking if None was passed as element of a message
+* fixed #13
+
+0.3.20 (2020-10-15)
+-------------------
+
+* Added a callable to be called each time log submission fails
+
+0.3.19 (2020-10-15)
+-------------------
+
+* Fixed logging messages containing braces
+
+0.3.18 (2020-09-13)
+-------------------
+
+* Add an option to specify a callable in global properties
+
+0.3.17 (2020-02-14)
+-------------------
+
+* Improve handling of log entries with log-record arguments of type "bytes" (tintoy/seqlog#25).
+
+0.3.16 (2019-11-02)
+-------------------
+
+* Pass exception details when posting events to Seq (tintoy/seqlog#22).
+
+0.3.15 (2018-11-20)
+-------------------
+
+* Fix deprecated use of `yaml.load` (tintoy/seqlog#20).
+
+0.3.13 (2018-11-20)
+-------------------
+
+* Explicitly set ``Content-Type`` header to ``application/json`` when posting events to Seq (tintoy/seqlog#17).
+
+0.3.12 (2018-11-19)
+-------------------
+
+* If logging fails to submit an event to Seq then log the response body, if available (tintoy/seqlog#17).
+
+0.3.11 (2018-09-22)
+-------------------
+
+* Support custom ``JSONEncoder`` implementations (tintoy/seqlog#7 and tintoy/seqlog#13).
+
+0.3.10 (2018-08-11)
+-------------------
+
+* Fix incorrect behaviour when configuring logging from a file (tintoy/seqlog#10).  
+  **Breaking change**: Configuring logging from file or dict will now by default override the default logger class to be ``StructuredLogger`` (this can be reverted to previous behaviour by passing ``use_structured_logger=False``).
+
+0.3.9 (2018-01-09)
+------------------
+
+* Add PyYAML as a dependency (tintoy/seqlog#6).
+
+0.3.8 (2018-01-05)
+------------------
+
+* Improve documentation for logging configuration from file (#3)
+
+0.3.7 (2018-01-05)
+------------------
+
+* Implement and document logging configuration from file (#3)
+
+0.3.4 (2017-11-27)
+------------------
+
+* Fix sample code (#2).
+
+0.3.3 (2016-11-18)
+------------------
+
+* Use streaming mode when posting to Seq (#1)
+
+0.3.2 (2016-11-18)
+------------------
+
+* Updated release notes
+
+0.3.1 (2016-11-18)
+------------------
+
+* Further work relating to intermittent "RuntimeError: The content for this response was already consumed" when publishing log entries (#1)
+
+0.3.0 (2016-11-16)
+------------------
+
+* Fix for intermittent "RuntimeError: The content for this response was already consumed" when publishing log entries (#1)
+
+0.2.0 (2016-07-09)
+------------------
+
+* Support for configuring additional log handlers when calling log_to_seq.
+* Support for global log properties (statically-configured properties that are added to all outgoing log entries).
+
+0.0.1 (2016-07-07)
+------------------
+
+* First release on PyPI.
+
+0.0.7 (2016-07-09)
+------------------
+
+* ``log_to_seq`` now returns the SeqLogHandler to enable forced flushing of log records to Seq.
+* Change ``auto_flush_timeout`` to a ``float`` representing seconds (instead of milliseconds).
+* Update ``testharness.py`` to actually log to Seq.
+  You can override the server URL and API key using the ``SEQ_SERVER_URL`` and ``SEQ_API_KEY`` environment variables.
+* Update usage information in documentation.
+* Python 3 only for now (sorry, but logging in Python 2 doesn't have all the required extensibility points). If the need to support Python 2 becomes great enough then I'll try to find a way.
+
+0.1.0 (2016-07-09)
+------------------
+
+* Proper versioning starts today :)
+
diff --git a/docs/usage.rst b/docs/usage.rst
index 8e8f97a..4b24dac 100644
--- a/docs/usage.rst
+++ b/docs/usage.rst
@@ -1,212 +1,213 @@
-=====
-Usage
-=====
-
-Configure logging programmatically
-----------------------------------
-
-.. code-block:: python
-
-   import seqlog
-
-   seqlog.log_to_seq(
-      server_url="http://my-seq-server:5341/",
-      api_key="My API Key",
-      level=logging.INFO,
-      batch_size=10,
-      auto_flush_timeout=10,  # seconds
-      override_root_logger=True,
-      json_encoder_class=json.encoder.JSONEncoder  # Optional; only specify this if you want to use a custom JSON encoder
-   )
-
-For the best experience, use ``{x}``-style named format arguments (passing those format arguments as keyword arguments to the log functions ``info``, ``warning``, ``error``, ``critical``, etc).
-Using unnamed "holes" (i.e. ``{}``) is not currently supported.
-
-For example:
-
-.. code-block:: python
-
-   logging.info("Hello, {name}!", name="World")
-
-If you specify ordinal arguments, the log message is interpreted as a "%s"-style format string.
-The ordinal format arguments are stored in the log entry properties using the 0-based ordinal index as the property name.
-
-.. code-block:: python
-
-   logging.info("Hello, %s!", "World")
-
-Note that mixing named and ordinal arguments is not currently supported.
-
-Configure logging from a file
------------------------------
-
-Seqlog can also use a YAML-format file to describe the desired logging configuration. This file has the schema specified in Python's `logging.config <https://docs.python.org/3/library/logging.config.html#logging-config-dictschema>`_ module.
-
-First, create your configuration file (e.g. ``/foo/bar/my_config.yml``):
-
-.. code-block:: yaml
-
-    # This is the Python logging schema version (currently, only the value 1 is supported here).
-    version: 1
-
-    # Configure logging from scratch.
-    disable_existing_loggers: True
-
-    # Configure the root logger to use Seq
-    root:
-      level: INFO
-      handlers:
-      - seq
-      - console
-
-    # You can also configure non-root loggers.
-    loggers:
-      another_logger:
-          propagate: False
-          level: INFO
-          handlers:
-          - seq
-          - console
-
-    handlers:
-    # Log to STDOUT
-      console:
-        class: seqlog.structured_logging.ConsoleStructuredLogHandler
-        formatter: seq
-
-    # Log to Seq
-      seq:
-        class: seqlog.structured_logging.SeqLogHandler
-        formatter: seq
-
-        # Seq-specific settings (add any others you need, they're just kwargs for SeqLogHandler's constructor).
-        server_url: 'http://localhost:5341'
-        api_key: 'your_api_key_if_you_have_one'
-
-        # Use a custom JSON encoder, if you need to.
-        json_encoder_class: json.encoder.JSONEncoder
-
-    formatters:
-      seq:
-        style: '{'
-
-Then, call ``seqlog.configure_from_file()``:
-
-.. code-block:: python
-
-    seqlog.configure_from_file('/foo/bar/my_config.yml')
-
-    # Use the root logger.
-    root_logger = logging.getLogger()
-    root_logger.info('This is the root logger.')
-
-    # Use another logger
-    another_logger = logging.getLogger('another_logger')
-    another_logger.info('This is another logger.')
-
-Configuring logging from a dictionary
--------------------------------------
-
-Seqlog can also use a dictionary to describe the desired logging configuration.
-This dictionary has the schema specified in Python's `logging.config <https://docs.python.org/3/library/logging.config.html#logging-config-dictschema>`_ module.
-
-.. code-block:: python
-
-    config = {
-      # configuration goes here
-    }
-
-    seqlog.configure_from_dict(config)
-
-    # Use the root logger.
-    root_logger = logging.getLogger()
-    root_logger.info('This is the root logger.')
-
-    # Use another logger
-    another_logger = logging.getLogger('another_logger')
-    another_logger.info('This is another logger.')
-
-Batching and auto-flush
------------------------
-
-By default SeqLog will wait until it has a batch of 10 messages before sending them to Seq.
-You can control the batch size by passing a value for ``batch_size``.
-
-If you also want it to publish the current batch of events when not enough of them have arrived within a certain period, you can pass ``auto_flush_timeout`` (a ``float`` representing the number of seconds before an incomplete batch is published).
-
-Overriding the root logger
---------------------------
-
-By default, SeqLog does not modify the root logger (and so calls to ``logging.info()`` and friends do not support named format arguments).
-To also override the root logger, pass ``True`` for ``override_root_logger``.
-
-Additional LogHandlers
-----------------------
-
-By default, ``log_to_seq`` only configures a single SeqLogHandler.
-
-To configure additional LogHandlers, pass them via ``additional_handlers``.
-
-Global log properties
----------------------
-
-SeqLog can also add static properties to each log entry that is sent to Seq.
-By default, the following properties are added:
-
-* ``MachineName`` The local machine's fully-qualified host name.
-* ``ProcessId`` The current process Id.
-
-To configure global log properties, call ``set_global_log_properties``, passing the properties as keyword arguments:
-
-.. code-block:: python
-
-    import seqlog
-
-    seqlog.set_global_log_properties(
-        GlobalProperty1="foo",
-        GlobalProperty2="bar"
-        GlobalProperty3=26
-    )
-
-Note that you can also clear the global log properties (so no properties are added) by calling ``clear_global_log_properties``, and reset the global log properties to their defaults by calling ``reset_global_log_properties``.
-
-Note that is you specify a callable as part of global log properties, it will be called
-with no arguments right before logging:
-
-.. code-block:: python
-
-    import seqlog
-
-    def get_trace_id():
-        if tracer.active_span is not None:
-            return hex(tracer.active_span.context.trace_id)
-        else:
-            return None
-
-    seqlog.set_global_log_properties(
-        trace_id=get_trace_id,
-    )
-
-If the callable returns None, it won't be added.
-
-Callback on log submission failure
-----------------------------------
-
-If you wish to set a callable to be invoked each time log submission fails, 
-use the following function:
-
-.. code-block:: python
-
-    from seqlog import set_callback_on_failure
-    
-    def handle_a_failure(e):    # type: (requests.RequestException) -> None
-        print('Failure occurred during log submission: %s' % (e, ))
-        
-   set_callback_on_failure(handle_a_failure)
-
-The callable that you provide will accept a single positional argument, 
-which is the requests exception instance that was the reason for the fail.
-
-.. note:: This callable will be called only for I/O errors, errors stemming
-          from seqlog not being able to convert your records into JSON won't
-          show up here!
+=====
+Usage
+=====
+
+Configure logging programmatically
+----------------------------------
+
+.. code-block:: python
+
+   import seqlog
+
+   seqlog.log_to_seq(
+      server_url="http://my-seq-server:5341/",
+      api_key="My API Key",
+      level=logging.INFO,
+      batch_size=10,
+      auto_flush_timeout=10,  # seconds
+      override_root_logger=True,
+      json_encoder_class=json.encoder.JSONEncoder,  # Optional; only specify this if you want to use a custom JSON encoder
+      support_extra_properties=True # Optional; only specify this if you want to pass additional log record properties via the "extra" argument.
+   )
+
+For the best experience, use ``{x}``-style named format arguments (passing those format arguments as keyword arguments to the log functions ``info``, ``warning``, ``error``, ``critical``, etc).
+Using unnamed "holes" (i.e. ``{}``) is not currently supported.
+
+For example:
+
+.. code-block:: python
+
+   logging.info("Hello, {name}!", name="World")
+
+If you specify ordinal arguments, the log message is interpreted as a "%s"-style format string.
+The ordinal format arguments are stored in the log entry properties using the 0-based ordinal index as the property name.
+
+.. code-block:: python
+
+   logging.info("Hello, %s!", "World")
+
+Note that mixing named and ordinal arguments is not currently supported.
+
+Configure logging from a file
+-----------------------------
+
+Seqlog can also use a YAML-format file to describe the desired logging configuration. This file has the schema specified in Python's `logging.config <https://docs.python.org/3/library/logging.config.html#logging-config-dictschema>`_ module.
+
+First, create your configuration file (e.g. ``/foo/bar/my_config.yml``):
+
+.. code-block:: yaml
+
+    # This is the Python logging schema version (currently, only the value 1 is supported here).
+    version: 1
+
+    # Configure logging from scratch.
+    disable_existing_loggers: True
+
+    # Configure the root logger to use Seq
+    root:
+      level: INFO
+      handlers:
+      - seq
+      - console
+
+    # You can also configure non-root loggers.
+    loggers:
+      another_logger:
+          propagate: False
+          level: INFO
+          handlers:
+          - seq
+          - console
+
+    handlers:
+    # Log to STDOUT
+      console:
+        class: seqlog.structured_logging.ConsoleStructuredLogHandler
+        formatter: seq
+
+    # Log to Seq
+      seq:
+        class: seqlog.structured_logging.SeqLogHandler
+        formatter: seq
+
+        # Seq-specific settings (add any others you need, they're just kwargs for SeqLogHandler's constructor).
+        server_url: 'http://localhost:5341'
+        api_key: 'your_api_key_if_you_have_one'
+
+        # Use a custom JSON encoder, if you need to.
+        json_encoder_class: json.encoder.JSONEncoder
+
+    formatters:
+      seq:
+        style: '{'
+
+Then, call ``seqlog.configure_from_file()``:
+
+.. code-block:: python
+
+    seqlog.configure_from_file('/foo/bar/my_config.yml')
+
+    # Use the root logger.
+    root_logger = logging.getLogger()
+    root_logger.info('This is the root logger.')
+
+    # Use another logger
+    another_logger = logging.getLogger('another_logger')
+    another_logger.info('This is another logger.')
+
+Configuring logging from a dictionary
+-------------------------------------
+
+Seqlog can also use a dictionary to describe the desired logging configuration.
+This dictionary has the schema specified in Python's `logging.config <https://docs.python.org/3/library/logging.config.html#logging-config-dictschema>`_ module.
+
+.. code-block:: python
+
+    config = {
+      # configuration goes here
+    }
+
+    seqlog.configure_from_dict(config)
+
+    # Use the root logger.
+    root_logger = logging.getLogger()
+    root_logger.info('This is the root logger.')
+
+    # Use another logger
+    another_logger = logging.getLogger('another_logger')
+    another_logger.info('This is another logger.')
+
+Batching and auto-flush
+-----------------------
+
+By default SeqLog will wait until it has a batch of 10 messages before sending them to Seq.
+You can control the batch size by passing a value for ``batch_size``.
+
+If you also want it to publish the current batch of events when not enough of them have arrived within a certain period, you can pass ``auto_flush_timeout`` (a ``float`` representing the number of seconds before an incomplete batch is published).
+
+Overriding the root logger
+--------------------------
+
+By default, SeqLog does not modify the root logger (and so calls to ``logging.info()`` and friends do not support named format arguments).
+To also override the root logger, pass ``True`` for ``override_root_logger``.
+
+Additional LogHandlers
+----------------------
+
+By default, ``log_to_seq`` only configures a single SeqLogHandler.
+
+To configure additional LogHandlers, pass them via ``additional_handlers``.
+
+Global log properties
+---------------------
+
+SeqLog can also add static properties to each log entry that is sent to Seq.
+By default, the following properties are added:
+
+* ``MachineName`` The local machine's fully-qualified host name.
+* ``ProcessId`` The current process Id.
+
+To configure global log properties, call ``set_global_log_properties``, passing the properties as keyword arguments:
+
+.. code-block:: python
+
+    import seqlog
+
+    seqlog.set_global_log_properties(
+        GlobalProperty1="foo",
+        GlobalProperty2="bar"
+        GlobalProperty3=26
+    )
+
+Note that you can also clear the global log properties (so no properties are added) by calling ``clear_global_log_properties``, and reset the global log properties to their defaults by calling ``reset_global_log_properties``.
+
+Note that is you specify a callable as part of global log properties, it will be called
+with no arguments right before logging:
+
+.. code-block:: python
+
+    import seqlog
+
+    def get_trace_id():
+        if tracer.active_span is not None:
+            return hex(tracer.active_span.context.trace_id)
+        else:
+            return None
+
+    seqlog.set_global_log_properties(
+        trace_id=get_trace_id,
+    )
+
+If the callable returns None, it won't be added.
+
+Callback on log submission failure
+----------------------------------
+
+If you wish to set a callable to be invoked each time log submission fails, 
+use the following function:
+
+.. code-block:: python
+
+    from seqlog import set_callback_on_failure
+    
+    def handle_a_failure(e):    # type: (requests.RequestException) -> None
+        print('Failure occurred during log submission: %s' % (e, ))
+        
+   set_callback_on_failure(handle_a_failure)
+
+The callable that you provide will accept a single positional argument, 
+which is the requests exception instance that was the reason for the fail.
+
+.. note:: This callable will be called only for I/O errors, errors stemming
+          from seqlog not being able to convert your records into JSON won't
+          show up here!
diff --git a/meta.yaml b/meta.yaml
index ffe267b..6e6d7c3 100644
--- a/meta.yaml
+++ b/meta.yaml
@@ -1,20 +1,20 @@
-package:
-  name: seqlog
-  version: 0.3.20
-
-requirements:
-  host:
-    - python
-    - setuptools
-
-  run:
-    - python
-
-source:
-  git_url: http://github.com/tintoy/seqlog.git
-
-about:
-  home: http://github.com/tintoy/seqlog
-  license: MIT
-  license_file: LICENSE
-
+package:
+  name: seqlog
+  version: 0.3.22
+
+requirements:
+  host:
+    - python
+    - setuptools
+
+  run:
+    - python
+
+source:
+  git_url: http://github.com/tintoy/seqlog.git
+
+about:
+  home: http://github.com/tintoy/seqlog
+  license: MIT
+  license_file: LICENSE
+
diff --git a/requirements_dev.txt b/requirements_dev.txt
index 2efc24b..3725df3 100644
--- a/requirements_dev.txt
+++ b/requirements_dev.txt
@@ -1,12 +1,12 @@
-pip>=8.1.2
-bumpversion>=0.5.3
-wheel>=0.29.0
-watchdog>=0.8.3
-flake8>=2.6.0
-tox>=2.3.1
-coverage>=4.1
-Sphinx>=1.4.4
-cryptography==1.6
-PyYAML>=3.11
-pytest>=2.9.2
-httmock>=1.2.5
+pip>=8.1.2
+bumpversion>=0.5.3
+wheel>=0.29.0
+watchdog>=0.8.3
+flake8>=2.6.0
+tox>=2.3.1
+coverage>=4.1
+Sphinx>=1.4.4
+cryptography==3.4.8
+PyYAML>=3.11
+pytest>=2.9.2
+httmock>=1.2.5
diff --git a/seqlog/__init__.py b/seqlog/__init__.py
index 7c612a4..6c5d8cc 100644
--- a/seqlog/__init__.py
+++ b/seqlog/__init__.py
@@ -1,181 +1,223 @@
-# -*- coding: utf-8 -*-
-
-import logging
-import logging.config
-import yaml
-
-from seqlog.structured_logging import StructuredLogger, StructuredRootLogger
-from seqlog.structured_logging import SeqLogHandler, ConsoleStructuredLogHandler
-from seqlog.structured_logging import get_global_log_properties as _get_global_log_properties
-from seqlog.structured_logging import set_global_log_properties as _set_global_log_properties
-from seqlog.structured_logging import clear_global_log_properties as _clear_global_log_properties
-from seqlog.structured_logging import reset_global_log_properties as _reset_global_log_properties
-from seqlog.structured_logging import set_callback_on_failure
-
-__author__ = 'Adam Friedman'
-__email__ = 'tintoy@tintoy.io'
-__version__ = '0.3.20'
-
-
-def configure_from_file(file_name, override_root_logger=True, use_structured_logger=True):
-    """
-    Configure Seq logging using YAML-format configuration file.
-
-    Uses `logging.config.dictConfig()`.
-
-    :param file_name: The name of the configuration file to use.
-    :type file_name: str
-    :param override_root_logger: Override the root logger to use a Seq-specific implementation? (default: True)
-    :param use_structured_logger: Configure the default logger class to be StructuredLogger, which support named format arguments? (default: True)
-    :type override_root_logger: bool
-    """
-
-    with open(file_name) as config_file:
-        config = yaml.load(config_file, Loader=yaml.SafeLoader)
-
-    configure_from_dict(config, override_root_logger)
-
-
-def configure_from_dict(config, override_root_logger=True, use_structured_logger=True):
-    """
-    Configure Seq logging using a dictionary.
-
-    Uses `logging.config.dictConfig()`.
-
-    :param config: A dict containing the configuration.
-    :type config: dict
-    :param override_root_logger: Override the root logger to use a Seq-specific implementation? (default: True)
-    :param use_structured_logger: Configure the default logger class to be StructuredLogger, which support named format arguments? (default: True)
-    :type override_root_logger: bool
-    """
-
-    if override_root_logger:
-        _override_root_logger()
-
-    # Must use StructuredLogger to support named format argments.
-    if use_structured_logger:
-        logging.setLoggerClass(StructuredLogger)
-
-    logging.config.dictConfig(config)
-
-
-def log_to_seq(server_url, api_key=None, level=logging.WARNING,
-               batch_size=10, auto_flush_timeout=None,
-               additional_handlers=None, override_root_logger=False,
-               json_encoder_class=None,
-               **kwargs):
-    """
-    Configure the logging system to send log entries to Seq.
-
-    Note that the root logger will not log to Seq by default.
-
-    :param server_url: The Seq server URL.
-    :param api_key: The Seq API key (optional).
-    :param level: The minimum level at which to log.
-    :param batch_size: The number of log entries to collect before publishing to Seq.
-    :param auto_flush_timeout: If specified, the time (in seconds) before the current batch is automatically flushed.
-    :param additional_handlers: Additional `LogHandler`s (if any).
-    :param override_root_logger: Override the root logger, too?
-                                 Note - this might cause problems if third-party components try to be clever
-                                 when using the logging.XXX functions.
-    :json_encoder_class: The custom JSONEncoder class (if any) to use. It not specified, the default JSONEncoder will be used.
-    :return: The `SeqLogHandler` that sends events to Seq. Can be used to forcibly flush records to Seq.
-    :rtype: SeqLogHandler
-    """
-
-    logging.setLoggerClass(StructuredLogger)
-
-    if override_root_logger:
-        _override_root_logger()
-
-    log_handlers = [
-        SeqLogHandler(server_url, api_key, batch_size, auto_flush_timeout, json_encoder_class)
-    ]
-
-    if additional_handlers:
-        for additional_handler in additional_handlers:
-            log_handlers.append(additional_handler)
-
-    logging.basicConfig(
-        style='{',
-        handlers=log_handlers,
-        level=level,
-        **kwargs
-    )
-
-    return log_handlers[0]
-
-
-def log_to_console(level=logging.WARNING, override_root_logger=False, **kwargs):
-    """
-    Configure the logging system to send log entries to the console.
-
-    Note that the root logger will not log to Seq by default.
-
-    :param level: The minimum level at which to log.
-    :param override_root_logger: Override the root logger, too?
-                                 Note - this might cause problems if third-party components try to be clever
-                                 when using the logging.XXX functions.
-    """
-
-    logging.setLoggerClass(StructuredLogger)
-
-    if override_root_logger:
-        _override_root_logger()
-
-    logging.basicConfig(
-        style='{',
-        handlers=[
-            ConsoleStructuredLogHandler()
-        ],
-        level=level,
-        **kwargs
-    )
-
-
-def get_global_log_properties():
-    """
-    Get the properties to be added to all structured log entries.
-
-    :return: A copy of the global log properties.
-    :rtype: dict
-    """
-
-    return _get_global_log_properties()
-
-
-def set_global_log_properties(**properties):
-    """
-    Configure the properties to be added to all structured log entries.
-
-    :param properties: Keyword arguments representing the properties.
-    :type properties: dict
-    """
-
-    _set_global_log_properties(**properties)
-
-
-def reset_global_log_properties():
-    """
-    Initialize global log properties to their default values.
-    """
-
-    _reset_global_log_properties()
-
-
-def clear_global_log_properties():
-    """
-    Remove all global properties.
-    """
-
-    _clear_global_log_properties()
-
-
-def _override_root_logger():
-    """
-    Override the root logger with a `StructuredRootLogger`.
-    """
-
-    logging.root = StructuredRootLogger(logging.WARNING)
-    logging.Logger.root = logging.root
-    logging.Logger.manager = logging.Manager(logging.Logger.root)
+# -*- coding: utf-8 -*-
+
+import logging
+import logging.config
+import typing
+import yaml
+
+from seqlog.feature_flags import FeatureFlag, configure_feature
+from seqlog.structured_logging import StructuredLogger, StructuredRootLogger
+from seqlog.structured_logging import SeqLogHandler, ConsoleStructuredLogHandler
+from seqlog.structured_logging import get_global_log_properties as _get_global_log_properties
+from seqlog.structured_logging import set_global_log_properties as _set_global_log_properties
+from seqlog.structured_logging import clear_global_log_properties as _clear_global_log_properties
+from seqlog.structured_logging import reset_global_log_properties as _reset_global_log_properties
+from seqlog.structured_logging import set_callback_on_failure as _set_callback_on_failure
+
+__author__ = 'Adam Friedman'
+__email__ = 'tintoy@tintoy.io'
+__version__ = '0.3.23'
+
+
+def configure_from_file(file_name, override_root_logger=True, support_extra_properties=False, support_stack_info=False):
+    """
+    Configure Seq logging using YAML-format configuration file.
+
+    Uses `logging.config.dictConfig()`.
+
+    :param file_name: The name of the configuration file to use.
+    :type file_name: str
+    :param override_root_logger: Override the root logger to use a Seq-specific implementation? (default: True)
+    :type override_root_logger: bool
+    :param support_extra_properties: Support passing of additional properties to log via the `extra` argument?
+    :type support_extra_properties: bool
+    :param support_stack_info: Support attaching of stack-trace information (if available) to log records?
+    :type support_stack_info: bool
+    """
+
+    configure_feature(FeatureFlag.EXTRA_PROPERTIES, support_extra_properties)
+    configure_feature(FeatureFlag.STACK_INFO, support_stack_info)
+
+    with open(file_name) as config_file:
+        config = yaml.load(config_file, Loader=yaml.SafeLoader)
+
+    configure_from_dict(config, override_root_logger)
+
+
+def configure_from_dict(config, override_root_logger=True, use_structured_logger=True, support_extra_properties=False, support_stack_info=False):
+    """
+    Configure Seq logging using a dictionary.
+
+    Uses `logging.config.dictConfig()`.
+
+    :param config: A dict containing the configuration.
+    :type config: dict
+    :param override_root_logger: Override the root logger to use a Seq-specific implementation? (default: True)
+    :type override_root_logger: bool
+    :param use_structured_logger: Configure the default logger class to be StructuredLogger, which support named format arguments? (default: True)
+    :type use_structured_logger: bool
+    :param support_extra_properties: Support passing of additional properties to log via the `extra` argument?
+    :type support_extra_properties: bool
+    :param support_stack_info: Support attaching of stack-trace information (if available) to log records?
+    :type support_stack_info: bool
+    """
+
+    configure_feature(FeatureFlag.EXTRA_PROPERTIES, support_extra_properties)
+    configure_feature(FeatureFlag.STACK_INFO, support_stack_info)
+
+    if override_root_logger:
+        _override_root_logger()
+
+    # Must use StructuredLogger to support named format argments.
+    if use_structured_logger:
+        logging.setLoggerClass(StructuredLogger)
+
+    logging.config.dictConfig(config)
+
+
+def log_to_seq(server_url, api_key=None, level=logging.WARNING,
+               batch_size=10, auto_flush_timeout=None,
+               additional_handlers=None, override_root_logger=False,
+               json_encoder_class=None, support_extra_properties=False,
+               support_stack_info=False,
+               **kwargs):
+    """
+    Configure the logging system to send log entries to Seq.
+
+    Note that the root logger will not log to Seq by default.
+
+    :param server_url: The Seq server URL.
+    :param api_key: The Seq API key (optional).
+    :param level: The minimum level at which to log.
+    :param batch_size: The number of log entries to collect before publishing to Seq.
+    :param auto_flush_timeout: If specified, the time (in seconds) before the current batch is automatically flushed.
+    :param additional_handlers: Additional `LogHandler`s (if any).
+    :param override_root_logger: Override the root logger, too?
+                                 Note - this might cause problems if third-party components try to be clever
+                                 when using the logging.XXX functions.
+    :param json_encoder_class: The custom JSONEncoder class (if any) to use. It not specified, the default JSONEncoder will be used.
+    :param support_extra_properties: Support passing of additional properties to log via the `extra` argument?
+    :type support_extra_properties: bool
+    :param support_stack_info: Support attaching of stack-trace information (if available) to log records?
+    :type support_stack_info: bool
+    :return: The `SeqLogHandler` that sends events to Seq. Can be used to forcibly flush records to Seq.
+    :rtype: SeqLogHandler
+    """
+
+    configure_feature(FeatureFlag.EXTRA_PROPERTIES, support_extra_properties)
+    configure_feature(FeatureFlag.STACK_INFO, support_stack_info)
+
+    logging.setLoggerClass(StructuredLogger)
+
+    if override_root_logger:
+        _override_root_logger()
+
+    log_handlers = [
+        SeqLogHandler(server_url, api_key, batch_size, auto_flush_timeout, json_encoder_class)
+    ]
+
+    if additional_handlers:
+        for additional_handler in additional_handlers:
+            log_handlers.append(additional_handler)
+
+    logging.basicConfig(
+        style='{',
+        handlers=log_handlers,
+        level=level,
+        **kwargs
+    )
+
+    return log_handlers[0]
+
+
+def log_to_console(level=logging.WARNING, override_root_logger=False, support_extra_properties=False, support_stack_info=False, **kwargs):
+    """
+    Configure the logging system to send log entries to the console.
+
+    Note that the root logger will not log to Seq by default.
+
+    :param level: The minimum level at which to log.
+    :param override_root_logger: Override the root logger, too?
+                                 Note - this might cause problems if third-party components try to be clever
+                                 when using the logging.XXX functions.
+    :param support_extra_properties: Support passing of additional properties to log via the `extra` argument?
+    :type support_extra_properties: bool
+    :param support_stack_info: Support attaching of stack-trace information (if available) to log records?
+    :type support_stack_info: bool
+    """
+
+    configure_feature(FeatureFlag.EXTRA_PROPERTIES, support_extra_properties)
+    configure_feature(FeatureFlag.STACK_INFO, support_stack_info)
+
+    logging.setLoggerClass(StructuredLogger)
+
+    if override_root_logger:
+        _override_root_logger()
+
+    logging.basicConfig(
+        style='{',
+        handlers=[
+            ConsoleStructuredLogHandler()
+        ],
+        level=level,
+        **kwargs
+    )
+
+
+def set_callback_on_failure(callback):  # type: (typing.Callable[[Exception], None]) -> None
+    """
+    Configure a callback to be invoked each time logging fails.
+
+    :param callback: A callable that takes an Exception (representing the logging failure) as its only argument.
+    :type callback: callable
+    """
+
+    _set_callback_on_failure(callback)
+
+
+def get_global_log_properties():
+    """
+    Get the properties to be added to all structured log entries.
+
+    :return: A copy of the global log properties.
+    :rtype: dict
+    """
+
+    return _get_global_log_properties()
+
+
+def set_global_log_properties(**properties):
+    """
+    Configure the properties to be added to all structured log entries.
+
+    :param properties: Keyword arguments representing the properties.
+    :type properties: dict
+    """
+
+    _set_global_log_properties(**properties)
+
+
+def reset_global_log_properties():
+    """
+    Initialize global log properties to their default values.
+    """
+
+    _reset_global_log_properties()
+
+
+def clear_global_log_properties():
+    """
+    Remove all global properties.
+    """
+
+    _clear_global_log_properties()
+
+
+def _override_root_logger():
+    """
+    Override the root logger with a `StructuredRootLogger`.
+    """
+
+    logging.root = StructuredRootLogger(logging.WARNING)
+    logging.Logger.root = logging.root
+    logging.Logger.manager = logging.Manager(logging.Logger.root)
diff --git a/seqlog/feature_flags.py b/seqlog/feature_flags.py
new file mode 100644
index 0000000..fa0d4cd
--- /dev/null
+++ b/seqlog/feature_flags.py
@@ -0,0 +1,79 @@
+# -*- coding: utf-8 -*-
+
+from enum import Enum
+
+
+class FeatureFlag(Enum):
+    """
+    Well-known feature flags.
+    """
+
+    def __new__(cls, value, doc=None):
+        self = object.__new__(cls)  # calling super().__new__(value) here would fail
+        self._value_ = value
+
+        if doc is not None:
+            self.__doc__ = doc
+
+        return self
+
+    EXTRA_PROPERTIES = 1,
+    "Support passing of additional properties to log via the `extra` argument?"
+
+    STACK_INFO = 2,
+    "Support attaching of stack-trace information (if available) to log records?"
+
+
+_features = {
+    FeatureFlag.EXTRA_PROPERTIES: False,
+    FeatureFlag.STACK_INFO: False,
+}
+"Configured feature flags"
+
+
+def is_feature_enabled(feature: FeatureFlag):
+    """
+    Is a feature enabled?
+
+    :param feature: A `FeatureFlag` value representing the feature.
+    :type feature: FeatureFlag
+    :return: `True`, if the feature is enabled; otherwise, `False`.
+    :rtype: bool
+    """
+
+    return _features.get(feature, False)
+
+
+def enable_feature(feature: FeatureFlag):
+    """
+    Enable a feature.
+
+    :param feature: A `FeatureFlag` value representing the feature to enable.
+    :type feature: FeatureFlag
+    """
+
+    configure_feature(feature, True)
+
+
+def disable_feature(feature: FeatureFlag):
+    """
+    Disable a feature.
+
+    :param feature: A `FeatureFlag` value representing the feature to disable.
+    :type feature: FeatureFlag
+    """
+
+    configure_feature(feature, False)
+
+
+def configure_feature(feature: FeatureFlag, enable: bool):
+    """
+    Enable or disable a feature.
+
+    :param feature: A `FeatureFlag` value representing the feature to configure.
+    :type feature: FeatureFlag
+    :param enable: `True`, to enable the feature; `False` to disable it.
+    :type enable: bool
+    """
+
+    _features[feature] = enable
diff --git a/seqlog/structured_logging.py b/seqlog/structured_logging.py
index edb7895..a6102c9 100644
--- a/seqlog/structured_logging.py
+++ b/seqlog/structured_logging.py
@@ -1,597 +1,614 @@
-# -*- coding: utf-8 -*-
-
-import base64
-import copy
-import json
-import importlib
-import inspect
-import typing as tp
-import logging
-import os
-import socket
-import sys
-import warnings
-from datetime import datetime
-from dateutil.tz import tzlocal
-from queue import Queue
-import requests
-
-from .consumer import QueueConsumer
-
-# Well-known keyword arguments used by the logging system.
-_well_known_logger_kwargs = {"extra", "exc_info", "func", "sinfo"}
-
-# Default global log properties.
-_default_global_log_props = {
-    "MachineName": socket.gethostname(),
-    "ProcessId": os.getpid()
-}
-
-# Global properties attached to all log entries.
-_global_log_props = _default_global_log_props
-# Whether the _global_log_props DOES NOT contain any callables
-_global_log_props_is_raw_dict = True
-_callback_on_failure = None     # type: tp.Callable[[Exception], None]
-
-
-def get_global_log_properties(logger_name=None):
-    """
-    Get the properties to be added to all structured log entries.
-
-    :param logger_name: An optional logger name to be added to the log entry.
-    :type logger_name: str
-    :return: A copy of the global log properties.
-    :rtype: dict
-    """
-    if _global_log_props_is_raw_dict:
-        global_log_properties = copy.copy(_global_log_props)
-    else:
-        global_log_properties = {}
-        for k, v in _global_log_props.items():
-            if callable(v):
-                v = v()
-                if v is None:
-                    continue
-            global_log_properties[k] = v
-
-    if logger_name:
-        global_log_properties["LoggerName"] = logger_name
-
-    return global_log_properties
-
-
-def set_global_log_properties(**properties):
-    """
-    Configure the properties to be added to all structured log entries.
-
-    :param properties: Keyword arguments representing the properties.
-    :type properties: dict
-    """
-
-    global _global_log_props, _global_log_props_is_raw_dict
-    _global_log_props_is_raw_dict = not any(callable(v) for v in properties.values())
-    _global_log_props = copy.copy(properties)
-
-
-def reset_global_log_properties():
-    """
-    Initialize global log properties to their default values.
-    """
-
-    global _global_log_props, _global_log_props_is_raw_dict
-    _global_log_props_is_raw_dict = True
-    _global_log_props = _default_global_log_props
-
-
-def clear_global_log_properties():
-    """
-    Remove all global properties.
-    """
-
-    global _global_log_props, _global_log_props_is_raw_dict
-    _global_log_props_is_raw_dict = True
-    _global_log_props = {}
-
-
-def set_callback_on_failure(callback):  # type: (tp.Callable[[Exception], None]) -> None
-    """
-    Configure a callback to be invoked each time logging fails.
-
-    :param callback: A callable that takes an Exception (representing the logging failure) as its only argument.
-    :type callback: callable
-    """
-
-    global _callback_on_failure
-    assert callable(callback), 'Given callback is not callable'
-    _callback_on_failure = callback
-
-
-class StructuredLogRecord(logging.LogRecord):
-    """
-    An extended LogRecord that with custom properties to be logged to Seq.
-    """
-
-    def __init__(self, name, level, pathname, lineno, msg, args,
-                 exc_info, func=None, sinfo=None, log_props=None, **kwargs):
-
-        """
-        Create a new StructuredLogRecord.
-        :param name: The name of the logger that produced the log record.
-        :param level: The logging level (severity) associated with the logging record.
-        :param pathname: The name of the file (if known) where the log entry was created.
-        :param lineno: The line number (if known) in the file where the log entry was created.
-        :param msg: The log message (or message template).
-        :param args: Ordinal message format arguments (if any).
-        :param exc_info: Exception information to be included in the log entry.
-        :param func: The function (if known) where the log entry was created.
-        :param sinfo: Stack trace information (if known) for the log entry.
-        :param log_props: Named message format arguments (if any).
-        :param kwargs: Keyword (named) message format arguments.
-        """
-
-        super().__init__(name, level, pathname, lineno, msg, args, exc_info, func, sinfo, **kwargs)
-
-        self.log_props = log_props or {}
-
-        if self.thread and "ThreadId" not in self.log_props:
-            self.log_props["ThreadId"] = self.thread
-
-        if self.threadName and "ThreadName" not in self.log_props:
-            self.log_props["ThreadName"] = self.threadName
-
-    def getMessage(self):
-        """
-        Get a formatted message representing the log record (with arguments replaced by values as appropriate).
-        :return: The formatted message.
-        """
-        if self.msg is None:
-            warnings.warn('You just passed a None as a message content!', UserWarning)
-            self.msg = ''
-
-        if self.args:
-            return self.msg % self.args
-        elif self.log_props:
-            try:
-                return self.msg.format(**self.log_props)
-            except (KeyError, IndexError):
-                # IndexError because sometimes the wrong log messages go like {existing_prop[0]}
-                return self.msg   # handle the situation where we have braces in the logging value
-        else:
-            return self.msg
-
-
-class StructuredLogger(logging.Logger):
-    """
-    Custom (dummy) logger that understands named log arguments.
-    """
-
-    def __init__(self, name, level=logging.NOTSET):
-        """
-        Create a new StructuredLogger
-        :param name: The logger name.
-        :param level: The logger minimum level (severity).
-        """
-
-        super().__init__(name, level)
-
-    def _log(self, level, msg, args, exc_info=None, extra=None, stack_info=False, **kwargs):
-        """
-        Called by public logger methods to generate a log entry.
-        :param level: The level (severity) for the log entry.
-        :param msg: The log message or message template.
-        :param args: Ordinal arguments for the message format template.
-        :param exc_info: Exception information to be included in the log entry.
-        :param extra: Extra information to be included in the log entry.
-        :param stack_info: Include stack-trace information in the log entry?
-        :param kwargs: Keyword arguments (if any) passed to the public logger method that called _log.
-        """
-
-        # Slightly hacky:
-        #
-        # We take keyword arguments provided to public logger methods (except
-        # well-known ones used by the logging system itself) and move them
-        # into the `extra` argument as a sub-dictionary.
-
-        # Start off with a copy of the global log properties.
-        log_props = get_global_log_properties(self.name)
-
-        # Add supplied keyword arguments.
-        for prop in kwargs.keys():
-            if prop in _well_known_logger_kwargs:
-                continue
-
-            log_props[prop] = kwargs[prop]
-
-        extra = extra or {}
-        extra['log_props'] = log_props
-
-        super()._log(level, msg, args, exc_info, extra, stack_info)
-
-    def makeRecord(self, name, level, fn, lno, msg, args, exc_info, func=None, extra=None, sinfo=None):
-        """
-        Create a LogRecord.
-
-        :param name: The name of the logger that produced the log record.
-        :param level: The logging level (severity) associated with the logging record.
-        :param fn: The name of the file (if known) where the log entry was created.
-        :param lno: The line number (if known) in the file where the log entry was created.
-        :param msg: The log message (or message template).
-        :param args: Ordinal message format arguments (if any).
-        :param exc_info: Exception information to be included in the log entry.
-        :param func: The function (if known) where the log entry was created.
-        :param extra: Extra information (if any) to add to the log record.
-        :param sinfo: Stack trace information (if known) for the log entry.
-        """
-
-        # Do we have named format arguments?
-        if extra and 'log_props' in extra:
-            return StructuredLogRecord(name, level, fn, lno, msg, args, exc_info, func, sinfo, extra['log_props'])
-
-        return super().makeRecord(name, level, fn, lno, msg, args, exc_info, func, extra, sinfo)
-
-
-class StructuredRootLogger(logging.RootLogger):
-    """
-    Custom root logger that understands named log arguments.
-    """
-
-    def __init__(self, level=logging.NOTSET):
-        """
-        Create a `StructuredRootLogger`.
-        """
-
-        super().__init__(level)
-
-    def _log(self, level, msg, args, exc_info=None, extra=None, stack_info=False, **kwargs):
-        """
-        Called by public logger methods to generate a log entry.
-
-        :param level: The level (severity) for the log entry.
-        :param msg: The log message or message template.
-        :param args: Ordinal arguments for the message format template.
-        :param exc_info: Exception information to be included in the log entry.
-        :param extra: Extra information to be included in the log entry.
-        :param stack_info: Include stack-trace information in the log entry?
-        :param kwargs: Keyword arguments (if any) passed to the public logger method that called _log.
-        """
-
-        # Slightly hacky:
-        #
-        # We take keyword arguments provided to public logger methods (except
-        # well-known ones used by the logging system itself) and move them
-        # into the `extra` argument as a sub-dictionary.
-        log_props = get_global_log_properties(self.name)
-        for prop in kwargs.keys():
-            if prop in _well_known_logger_kwargs:
-                continue
-
-            log_props[prop] = kwargs[prop]
-
-        extra = extra or {}
-        extra['log_props'] = log_props
-
-        super()._log(level, msg, args, exc_info, extra, stack_info)
-
-    def makeRecord(self, name, level, fn, lno, msg, args, exc_info, func=None, extra=None, sinfo=None):
-        """
-        Create a `LogRecord`.
-
-        :param name: The name of the logger that produced the log record.
-        :param level: The logging level (severity) associated with the logging record.
-        :param fn: The name of the file (if known) where the log entry was created.
-        :param lno: The line number (if known) in the file where the log entry was created.
-        :param msg: The log message (or message template).
-        :param args: Ordinal message format arguments (if any).
-        :param exc_info: Exception information to be included in the log entry.
-        :param func: The function (if known) where the log entry was created.
-        :param extra: Extra information (if any) to add to the log record.
-        :param sinfo: Stack trace information (if known) for the log entry.
-        """
-
-        # Do we have named format arguments?
-        if extra and 'log_props' in extra:
-            return StructuredLogRecord(name, level, fn, lno, msg, args, exc_info, func, sinfo, extra['log_props'])
-
-        return super().makeRecord(name, level, fn, lno, msg, args, exc_info, func, extra, sinfo)
-
-
-class ConsoleStructuredLogHandler(logging.Handler):
-    def __init__(self):
-        super().__init__()
-
-    def emit(self, record):
-        msg = self.format(record)
-
-        print(msg)
-        if hasattr(record, 'kwargs'):
-            print("\tLog entry properties: {}".format(repr(record.kwargs)))
-
-
-def best_effort_json_encode(arg):
-    try:
-        return json.dumps(arg)
-    except TypeError:
-        try:
-            return str(arg)
-        except TypeError:
-            try:
-                return repr(arg)
-            except TypeError:
-                return '<type %s>' % (type(arg), )
-    except ReferenceError:
-        return '<gone weak reference>'
-
-
-class SeqLogHandler(logging.Handler):
-    """
-    Log handler that posts to Seq.
-    """
-
-    def __init__(self, server_url, api_key=None, batch_size=10, auto_flush_timeout=None, json_encoder_class=None):
-        """
-        Create a new `SeqLogHandler`.
-
-        :param server_url: The Seq server URL.
-        :param api_key: The Seq API key (if any).
-        :param batch_size: The number of messages to batch up before posting to Seq.
-        :param auto_flush_timeout: If specified, the time (in seconds) before
-                                   the current batch is automatically flushed.
-        :param json_encoder_class: The custom JSON encoder class (or fully-qualified class name), if any, to use.
-        """
-
-        super().__init__()
-
-        self.server_url = server_url
-        if not self.server_url.endswith("/"):
-            self.server_url += "/"
-        self.server_url += "api/events/raw"
-
-        self.session = requests.Session()
-        self.session.headers["Content-Type"] = "application/json"
-
-        if api_key:
-            self.session.headers["X-Seq-ApiKey"] = api_key
-
-        json_encoder_class = json_encoder_class or json.encoder.JSONEncoder
-        self.json_encoder_class = _ensure_class(json_encoder_class, compatible_class=json.encoder.JSONEncoder)
-
-        self.log_queue = Queue()
-        self.consumer = QueueConsumer(
-            name="SeqLogHandler",
-            queue=self.log_queue,
-            callback=self.publish_log_batch,
-            batch_size=batch_size,
-            auto_flush_timeout=auto_flush_timeout
-        )
-        self.consumer.start()
-
-    def flush(self):
-        try:
-            self.consumer.flush()
-        finally:
-            super().flush()
-
-    def emit(self, record):
-        """
-        Emit a log record.
-
-        :param record: The LogRecord.
-        """
-
-        self.log_queue.put(record, block=False)
-
-    def close(self):
-        """
-        Close the log handler.
-        """
-
-        try:
-            if self.consumer.is_running:
-                self.consumer.stop()
-
-            # TODO: Implement QueueConsumer.join() so we can wait
-            # for processing to complete before closing the HTTP session
-
-            # self.consumer.join()
-
-            self.session.close()
-        finally:
-            super().close()
-
-    def publish_log_batch(self, batch):     # type: (tp.Iterable[StructuredLogRecord]) -> None
-        """
-        Publish a batch of log records.
-
-        :param batch: A list representing the batch.
-        """
-
-        if len(batch) == 0:
-            return
-
-        processed_records = []
-        for record in batch:
-            resp = self._build_event_data(record)
-            try:
-                resp = json.dumps(resp, cls=self.json_encoder_class)
-            except TypeError:
-                # cannot serialize to JSON
-                # report an serialization error and continue serializing what you can
-                self.handleError(record)
-                continue
-            processed_records.append(resp)
-
-        request_body_json = '{"Events": [%s]}' % (','.join(processed_records), )
-
-        self.acquire()
-        response = None
-        try:
-            response = self.session.post(
-                self.server_url,
-                data=request_body_json,
-                stream=True  # prevent '362'
-            )
-            response.raise_for_status()
-        except requests.RequestException as requestFailed:
-            # Only notify for the first record in the batch, or we'll be generating too much noise.
-            self.handleError(batch[0])
-
-            if _callback_on_failure is not None:
-                _callback_on_failure(requestFailed)
-
-            # Attempt to log error response
-            if not requestFailed.response:
-                sys.stderr.write('Response from Seq was unavailable.\n')
-            elif not requestFailed.response.text:
-                sys.stderr.write('Response body from Seq was empty.\n')
-            else:
-                sys.stderr.write('Response body from Seq:\n{0}\n'.format(
-                    requestFailed.response.text
-                ))
-        finally:
-            self.release()
-
-    def _build_event_data(self, record):
-        """
-        Build an event data dictionary from the specified log record for submission to Seq.
-
-        :param record: The LogRecord.
-        :type record: StructuredLogRecord
-        :return: A dictionary containing event data representing the log record.
-        :rtype: dict
-        """
-
-        if record.args:
-            # Standard (unnamed) format arguments (use 0-base index as property name).
-            log_props_shim = get_global_log_properties(record.name)
-
-            for (arg_index, arg) in enumerate(record.args or []):
-                # bytes is not serialisable to JSON; encode appropriately.
-                arg = _encode_bytes_if_required(arg)
-                arg = best_effort_json_encode(arg)
-                log_props_shim[str(arg_index)] = arg
-
-            event_data = {
-                "Timestamp": _get_local_timestamp(record),
-                "Level": logging.getLevelName(record.levelno),
-                "MessageTemplate": record.getMessage(),
-                "Properties": log_props_shim
-            }
-        elif isinstance(record, StructuredLogRecord):
-            # Named format arguments (and, therefore, log event properties).
-
-            if record.log_props:
-                for log_prop_name in record.log_props.keys():
-                    # bytes is not serialisable to JSON; encode appropriately.
-                    log_prop = record.log_props[log_prop_name]
-                    arg = _encode_bytes_if_required(log_prop)
-                    arg = best_effort_json_encode(arg)
-                    record.log_props[log_prop_name] = arg
-
-            event_data = {
-                "Timestamp": _get_local_timestamp(record),
-                "Level": logging.getLevelName(record.levelno),
-                "MessageTemplate": record.msg,
-                "Properties": record.log_props
-            }
-        else:
-            # No format arguments; interpret message as-is.
-            event_data = {
-                "Timestamp": _get_local_timestamp(record),
-                "Level": logging.getLevelName(record.levelno),
-                "MessageTemplate": record.getMessage(),
-                "Properties": get_global_log_properties()
-            }
-
-        if record.exc_text:
-            # Rendered exception has already been cached
-            event_data["Exception"] = record.exc_text
-        elif isinstance(record.exc_info, tuple):
-            # Exception info is present
-            event_data["Exception"] = record.exc_text = self.formatter.formatException(record.exc_info)
-        elif record.exc_info:
-            # Exception info needs to be captured
-            exc_info = sys.exc_info()
-            if exc_info and exc_info[0] is not None:
-                event_data["Exception"] = record.exc_text = self.formatter.formatException(record.exc_info)
-
-        return event_data
-
-
-def _get_local_timestamp(record):
-    """
-    Get the record's UTC timestamp as an ISO-formatted date / time string.
-
-    :param record: The LogRecord.
-    :type record: StructuredLogRecord
-    :return: The ISO-formatted date / time string.
-    :rtype: str
-    """
-
-    timestamp = datetime.fromtimestamp(
-        timestamp=record.created,
-        tz=tzlocal()
-    )
-
-    return timestamp.isoformat(sep=' ')
-
-
-def _ensure_class(class_or_class_name, compatible_class=None):
-    """
-    Ensure that the supplied value is either a class or a fully-qualified class name.
-
-    :param name: A class or a fully-qualified class name.
-    :param expected_base_class: If specified then the class must be, or (eventually) derive from, this class.
-    :return: The class represented by class_or_class_name.
-    """
-
-    target_class = class_or_class_name
-    if isinstance(class_or_class_name, str):
-        name_parts = class_or_class_name.split('.')
-        module_name = '.'.join(
-            name_parts[:-1]
-        )
-        target_class_name = name_parts[-1]
-
-        # Raises ModuleNotFoundError if we can't resolve part of the module path
-        target_module = importlib.import_module(module_name)
-
-        target_class = getattr(target_module, target_class_name, None)
-        if not target_class:
-            raise ImportError("Class not found: '{}'.".format(class_or_class_name))
-
-    if not inspect.isclass(target_class):
-        raise TypeError(
-            "'{}' is not a class.".format(class_or_class_name)
-        )
-
-    if compatible_class and not issubclass(target_class, compatible_class):
-        raise ValueError(
-            "Class '{}' does not derive from '{}.{}'.".format(
-                class_or_class_name,
-                compatible_class.__module__,
-                compatible_class.__name__
-            )
-        )
-
-    return target_class
-
-
-def _encode_bytes_if_required(data):
-    """
-    If the specified data is represented as bytes, convert it to a UTF8 string (using Base64 encoding if the bytes do not represent valid UTF8).
-
-    This is needed because json.dumps cannot serialise bytes.
-
-    :param data: The data to encode.
-    :return: If the data is represented as bytes, an equivalent UTF8 or Base64-encoded string; otherwise, the original data.
-    """
-
-    if (not isinstance(data, bytes)):
-        return data
-
-    try:
-        data = data.decode('utf8')
-    except UnicodeDecodeError:
-        data = base64.encodebytes(data).decode('ascii')
-
-    return data
+# -*- coding: utf-8 -*-
+
+import base64
+import copy
+import json
+import importlib
+import inspect
+import logging
+import os
+import socket
+import sys
+import typing as tp
+import warnings
+from datetime import datetime
+from dateutil.tz import tzlocal
+from queue import Queue
+import requests
+
+from seqlog.consumer import QueueConsumer
+from seqlog.feature_flags import FeatureFlag, is_feature_enabled
+
+# Well-known keyword arguments used by the logging system.
+_well_known_logger_kwargs = {"extra", "exc_info", "func", "sinfo"}
+
+# Default global log properties.
+_default_global_log_props = {
+    "MachineName": socket.gethostname(),
+    "ProcessId": os.getpid()
+}
+
+# Global properties attached to all log entries.
+_global_log_props = _default_global_log_props
+# Whether the _global_log_props DOES NOT contain any callables
+_global_log_props_is_raw_dict = True
+_callback_on_failure = None     # type: tp.Callable[[Exception], None]
+
+
+def get_global_log_properties(logger_name=None):
+    """
+    Get the properties to be added to all structured log entries.
+
+    :param logger_name: An optional logger name to be added to the log entry.
+    :type logger_name: str
+    :return: A copy of the global log properties.
+    :rtype: dict
+    """
+    if _global_log_props_is_raw_dict:
+        global_log_properties = copy.copy(_global_log_props)
+    else:
+        global_log_properties = {}
+        for k, v in _global_log_props.items():
+            if callable(v):
+                v = v()
+                if v is None:
+                    continue
+            global_log_properties[k] = v
+
+    if logger_name:
+        global_log_properties["LoggerName"] = logger_name
+
+    return global_log_properties
+
+
+def set_global_log_properties(**properties):
+    """
+    Configure the properties to be added to all structured log entries.
+
+    :param properties: Keyword arguments representing the properties.
+    :type properties: dict
+    """
+
+    global _global_log_props, _global_log_props_is_raw_dict
+    _global_log_props_is_raw_dict = not any(callable(v) for v in properties.values())
+    _global_log_props = copy.copy(properties)
+
+
+def reset_global_log_properties():
+    """
+    Initialize global log properties to their default values.
+    """
+
+    global _global_log_props, _global_log_props_is_raw_dict
+    _global_log_props_is_raw_dict = True
+    _global_log_props = _default_global_log_props
+
+
+def clear_global_log_properties():
+    """
+    Remove all global properties.
+    """
+
+    global _global_log_props, _global_log_props_is_raw_dict
+    _global_log_props_is_raw_dict = True
+    _global_log_props = {}
+
+
+def set_callback_on_failure(callback):  # type: (tp.Callable[[Exception], None]) -> None
+    """
+    Configure a callback to be invoked each time logging fails.
+
+    :param callback: A callable that takes an Exception (representing the logging failure) as its only argument.
+    :type callback: callable
+    """
+
+    global _callback_on_failure
+    assert callable(callback), 'Given callback is not callable'
+    _callback_on_failure = callback
+
+
+class StructuredLogRecord(logging.LogRecord):
+    """
+    An extended LogRecord that with custom properties to be logged to Seq.
+    """
+
+    def __init__(self, name, level, pathname, lineno, msg, args,
+                 exc_info, func=None, sinfo=None, log_props=None, **kwargs):
+
+        """
+        Create a new StructuredLogRecord.
+        :param name: The name of the logger that produced the log record.
+        :param level: The logging level (severity) associated with the logging record.
+        :param pathname: The name of the file (if known) where the log entry was created.
+        :param lineno: The line number (if known) in the file where the log entry was created.
+        :param msg: The log message (or message template).
+        :param args: Ordinal message format arguments (if any).
+        :param exc_info: Exception information to be included in the log entry.
+        :param func: The function (if known) where the log entry was created.
+        :param sinfo: Stack trace information (if known) for the log entry.
+        :param log_props: Named message format arguments (if any).
+        :param kwargs: Keyword (named) message format arguments.
+        """
+
+        super().__init__(name, level, pathname, lineno, msg, args, exc_info, func, sinfo, **kwargs)
+
+        self.log_props = log_props or {}
+
+        if self.thread and "ThreadId" not in self.log_props:
+            self.log_props["ThreadId"] = self.thread
+
+        if self.threadName and "ThreadName" not in self.log_props:
+            self.log_props["ThreadName"] = self.threadName
+
+    def getMessage(self):
+        """
+        Get a formatted message representing the log record (with arguments replaced by values as appropriate).
+        :return: The formatted message.
+        """
+        if self.msg is None:
+            warnings.warn('You just passed a None as a message content!', UserWarning)
+            self.msg = ''
+
+        if self.args:
+            return self.msg % self.args
+        elif self.log_props:
+            try:
+                return self.msg.format(**self.log_props)
+            except (KeyError, IndexError):
+                # IndexError because sometimes the wrong log messages go like {existing_prop[0]}
+                return self.msg   # handle the situation where we have braces in the logging value
+        else:
+            return self.msg
+
+
+class StructuredLogger(logging.Logger):
+    """
+    Custom (dummy) logger that understands named log arguments.
+    """
+
+    def __init__(self, name, level=logging.NOTSET):
+        """
+        Create a new StructuredLogger
+        :param name: The logger name.
+        :param level: The logger minimum level (severity).
+        """
+
+        super().__init__(name, level)
+
+        self._support_extra_properties = is_feature_enabled(FeatureFlag.EXTRA_PROPERTIES)
+
+    def _log(self, level, msg, args, exc_info=None, extra=None, stack_info=False, **kwargs):
+        """
+        Called by public logger methods to generate a log entry.
+        :param level: The level (severity) for the log entry.
+        :param msg: The log message or message template.
+        :param args: Ordinal arguments for the message format template.
+        :param exc_info: Exception information to be included in the log entry.
+        :param extra: Extra information to be included in the log entry.
+        :param stack_info: Include stack-trace information in the log entry?
+        :param kwargs: Keyword arguments (if any) passed to the public logger method that called _log.
+        """
+
+        # Slightly hacky:
+        #
+        # We take keyword arguments provided to public logger methods (except
+        # well-known ones used by the logging system itself) and move them
+        # into the `extra` argument as a sub-dictionary.
+
+        # Start off with a copy of the global log properties.
+        log_props = get_global_log_properties(self.name)
+
+        # Add supplied keyword arguments.
+        for prop in kwargs.keys():
+            if prop in _well_known_logger_kwargs:
+                continue
+
+            log_props[prop] = kwargs[prop]
+
+        if extra and self._support_extra_properties:
+            for extra_prop in extra.keys():
+                log_props['Extra_' + extra_prop] = extra[extra_prop]
+
+        extra = extra or {}
+        extra['log_props'] = log_props
+
+        super()._log(level, msg, args, exc_info, extra, stack_info)
+
+    def makeRecord(self, name, level, fn, lno, msg, args, exc_info, func=None, extra=None, sinfo=None):
+        """
+        Create a LogRecord.
+
+        :param name: The name of the logger that produced the log record.
+        :param level: The logging level (severity) associated with the logging record.
+        :param fn: The name of the file (if known) where the log entry was created.
+        :param lno: The line number (if known) in the file where the log entry was created.
+        :param msg: The log message (or message template).
+        :param args: Ordinal message format arguments (if any).
+        :param exc_info: Exception information to be included in the log entry.
+        :param func: The function (if known) where the log entry was created.
+        :param extra: Extra information (if any) to add to the log record.
+        :param sinfo: Stack trace information (if known) for the log entry.
+        """
+
+        # Do we have named format arguments?
+        if extra and 'log_props' in extra:
+            record = StructuredLogRecord(name, level, fn, lno, msg, args, exc_info, func, sinfo, extra['log_props'])
+        else:
+            record = super().makeRecord(name, level, fn, lno, msg, args, exc_info, func, extra, sinfo)
+
+        if sinfo and not record.exc_text:
+            setattr(record, 'exc_text', sinfo)
+
+        return record
+
+
+class StructuredRootLogger(logging.RootLogger):
+    """
+    Custom root logger that understands named log arguments.
+    """
+
+    def __init__(self, level=logging.NOTSET):
+        """
+        Create a `StructuredRootLogger`.
+        """
+
+        super().__init__(level)
+
+    def _log(self, level, msg, args, exc_info=None, extra=None, stack_info=False, **kwargs):
+        """
+        Called by public logger methods to generate a log entry.
+
+        :param level: The level (severity) for the log entry.
+        :param msg: The log message or message template.
+        :param args: Ordinal arguments for the message format template.
+        :param exc_info: Exception information to be included in the log entry.
+        :param extra: Extra information to be included in the log entry.
+        :param stack_info: Include stack-trace information in the log entry?
+        :param kwargs: Keyword arguments (if any) passed to the public logger method that called _log.
+        """
+
+        # Slightly hacky:
+        #
+        # We take keyword arguments provided to public logger methods (except
+        # well-known ones used by the logging system itself) and move them
+        # into the `extra` argument as a sub-dictionary.
+        log_props = get_global_log_properties(self.name)
+        for prop in kwargs.keys():
+            if prop in _well_known_logger_kwargs:
+                continue
+
+            log_props[prop] = kwargs[prop]
+
+        extra = extra or {}
+        extra['log_props'] = log_props
+
+        super()._log(level, msg, args, exc_info, extra, stack_info)
+
+    def makeRecord(self, name, level, fn, lno, msg, args, exc_info, func=None, extra=None, sinfo=None):
+        """
+        Create a `LogRecord`.
+
+        :param name: The name of the logger that produced the log record.
+        :param level: The logging level (severity) associated with the logging record.
+        :param fn: The name of the file (if known) where the log entry was created.
+        :param lno: The line number (if known) in the file where the log entry was created.
+        :param msg: The log message (or message template).
+        :param args: Ordinal message format arguments (if any).
+        :param exc_info: Exception information to be included in the log entry.
+        :param func: The function (if known) where the log entry was created.
+        :param extra: Extra information (if any) to add to the log record.
+        :param sinfo: Stack trace information (if known) for the log entry.
+        """
+
+        # Do we have named format arguments?
+        if extra and 'log_props' in extra:
+            return StructuredLogRecord(name, level, fn, lno, msg, args, exc_info, func, sinfo, extra['log_props'])
+
+        return super().makeRecord(name, level, fn, lno, msg, args, exc_info, func, extra, sinfo)
+
+
+class ConsoleStructuredLogHandler(logging.Handler):
+    def __init__(self):
+        super().__init__()
+
+    def emit(self, record):
+        msg = self.format(record)
+
+        print(msg)
+        if hasattr(record, 'kwargs'):
+            print("\tLog entry properties: {}".format(repr(record.kwargs)))
+
+
+def best_effort_json_encode(arg):
+    try:
+        return json.dumps(arg)
+    except TypeError:
+        try:
+            return str(arg)
+        except TypeError:
+            try:
+                return repr(arg)
+            except TypeError:
+                return '<type %s>' % (type(arg), )
+    except ReferenceError:
+        return '<gone weak reference>'
+
+
+class SeqLogHandler(logging.Handler):
+    """
+    Log handler that posts to Seq.
+    """
+
+    def __init__(self, server_url, api_key=None, batch_size=10, auto_flush_timeout=None, json_encoder_class=None):
+        """
+        Create a new `SeqLogHandler`.
+
+        :param server_url: The Seq server URL.
+        :param api_key: The Seq API key (if any).
+        :param batch_size: The number of messages to batch up before posting to Seq.
+        :param auto_flush_timeout: If specified, the time (in seconds) before
+                                   the current batch is automatically flushed.
+        :param json_encoder_class: The custom JSON encoder class (or fully-qualified class name), if any, to use.
+        """
+
+        super().__init__()
+
+        self._support_stack_info = is_feature_enabled(FeatureFlag.STACK_INFO)
+
+        self.server_url = server_url
+        if not self.server_url.endswith("/"):
+            self.server_url += "/"
+        self.server_url += "api/events/raw"
+
+        self.session = requests.Session()
+        self.session.headers["Content-Type"] = "application/json"
+
+        if api_key:
+            self.session.headers["X-Seq-ApiKey"] = api_key
+
+        json_encoder_class = json_encoder_class or json.encoder.JSONEncoder
+        self.json_encoder_class = _ensure_class(json_encoder_class, compatible_class=json.encoder.JSONEncoder)
+
+        self.log_queue = Queue()
+        self.consumer = QueueConsumer(
+            name="SeqLogHandler",
+            queue=self.log_queue,
+            callback=self.publish_log_batch,
+            batch_size=batch_size,
+            auto_flush_timeout=auto_flush_timeout
+        )
+        self.consumer.start()
+
+    def flush(self):
+        try:
+            self.consumer.flush()
+        finally:
+            super().flush()
+
+    def emit(self, record):
+        """
+        Emit a log record.
+
+        :param record: The LogRecord.
+        """
+
+        self.log_queue.put(record, block=False)
+
+    def close(self):
+        """
+        Close the log handler.
+        """
+
+        try:
+            if self.consumer.is_running:
+                self.consumer.stop()
+
+            # TODO: Implement QueueConsumer.join() so we can wait
+            # for processing to complete before closing the HTTP session
+
+            # self.consumer.join()
+
+            self.session.close()
+        finally:
+            super().close()
+
+    def publish_log_batch(self, batch):     # type: (tp.Iterable[StructuredLogRecord]) -> None
+        """
+        Publish a batch of log records.
+
+        :param batch: A list representing the batch.
+        """
+
+        if len(batch) == 0:
+            return
+
+        processed_records = []
+        for record in batch:
+            resp = self._build_event_data(record)
+            try:
+                resp = json.dumps(resp, cls=self.json_encoder_class)
+            except TypeError:
+                # cannot serialize to JSON
+                # report an serialization error and continue serializing what you can
+                self.handleError(record)
+                continue
+            processed_records.append(resp)
+
+        request_body_json = '{"Events": [%s]}' % (','.join(processed_records), )
+
+        self.acquire()
+        response = None
+        try:
+            response = self.session.post(
+                self.server_url,
+                data=request_body_json,
+                stream=True  # prevent '362'
+            )
+            response.raise_for_status()
+        except requests.RequestException as requestFailed:
+            # Only notify for the first record in the batch, or we'll be generating too much noise.
+            self.handleError(batch[0])
+
+            if _callback_on_failure is not None:
+                _callback_on_failure(requestFailed)
+
+            # Attempt to log error response
+            if not requestFailed.response:
+                sys.stderr.write('Response from Seq was unavailable.\n')
+            elif not requestFailed.response.text:
+                sys.stderr.write('Response body from Seq was empty.\n')
+            else:
+                sys.stderr.write('Response body from Seq:\n{0}\n'.format(
+                    requestFailed.response.text
+                ))
+        finally:
+            self.release()
+
+    def _build_event_data(self, record):
+        """
+        Build an event data dictionary from the specified log record for submission to Seq.
+
+        :param record: The LogRecord.
+        :type record: StructuredLogRecord
+        :return: A dictionary containing event data representing the log record.
+        :rtype: dict
+        """
+
+        if record.args:
+            # Standard (unnamed) format arguments (use 0-base index as property name).
+            log_props_shim = get_global_log_properties(record.name)
+
+            for (arg_index, arg) in enumerate(record.args or []):
+                # bytes is not serialisable to JSON; encode appropriately.
+                arg = _encode_bytes_if_required(arg)
+                arg = best_effort_json_encode(arg)
+                log_props_shim[str(arg_index)] = arg
+
+            event_data = {
+                "Timestamp": _get_local_timestamp(record),
+                "Level": logging.getLevelName(record.levelno),
+                "MessageTemplate": record.getMessage(),
+                "Properties": log_props_shim
+            }
+        elif isinstance(record, StructuredLogRecord):
+            # Named format arguments (and, therefore, log event properties).
+
+            if record.log_props:
+                for log_prop_name in record.log_props.keys():
+                    # bytes is not serialisable to JSON; encode appropriately.
+                    log_prop = record.log_props[log_prop_name]
+                    arg = _encode_bytes_if_required(log_prop)
+                    arg = best_effort_json_encode(arg)
+                    record.log_props[log_prop_name] = arg
+
+            event_data = {
+                "Timestamp": _get_local_timestamp(record),
+                "Level": logging.getLevelName(record.levelno),
+                "MessageTemplate": record.msg,
+                "Properties": record.log_props
+            }
+        else:
+            # No format arguments; interpret message as-is.
+            event_data = {
+                "Timestamp": _get_local_timestamp(record),
+                "Level": logging.getLevelName(record.levelno),
+                "MessageTemplate": record.getMessage(),
+                "Properties": get_global_log_properties()
+            }
+
+        if record.exc_text:
+            # Rendered exception has already been cached
+            event_data["Exception"] = record.exc_text
+        elif self._support_stack_info and record.stack_info and not record.exc_info:
+            # Feature flag is set: fall back to stack_info (sinfo) if exc_info is not present
+            event_data["Exception"] = record.stack_info
+        elif isinstance(record.exc_info, tuple):
+            # Exception info is present
+            event_data["Exception"] = record.exc_text = self.formatter.formatException(record.exc_info)
+        elif record.exc_info:
+            # Exception info needs to be captured
+            exc_info = sys.exc_info()
+            if exc_info and exc_info[0] is not None:
+                event_data["Exception"] = record.exc_text = self.formatter.formatException(record.exc_info)
+
+        return event_data
+
+
+def _get_local_timestamp(record):
+    """
+    Get the record's UTC timestamp as an ISO-formatted date / time string.
+
+    :param record: The LogRecord.
+    :type record: StructuredLogRecord
+    :return: The ISO-formatted date / time string.
+    :rtype: str
+    """
+
+    timestamp = datetime.fromtimestamp(
+        timestamp=record.created,
+        tz=tzlocal()
+    )
+
+    return timestamp.isoformat(sep=' ')
+
+
+def _ensure_class(class_or_class_name, compatible_class=None):
+    """
+    Ensure that the supplied value is either a class or a fully-qualified class name.
+
+    :param name: A class or a fully-qualified class name.
+    :param expected_base_class: If specified then the class must be, or (eventually) derive from, this class.
+    :return: The class represented by class_or_class_name.
+    """
+
+    target_class = class_or_class_name
+    if isinstance(class_or_class_name, str):
+        name_parts = class_or_class_name.split('.')
+        module_name = '.'.join(
+            name_parts[:-1]
+        )
+        target_class_name = name_parts[-1]
+
+        # Raises ModuleNotFoundError if we can't resolve part of the module path
+        target_module = importlib.import_module(module_name)
+
+        target_class = getattr(target_module, target_class_name, None)
+        if not target_class:
+            raise ImportError("Class not found: '{}'.".format(class_or_class_name))
+
+    if not inspect.isclass(target_class):
+        raise TypeError(
+            "'{}' is not a class.".format(class_or_class_name)
+        )
+
+    if compatible_class and not issubclass(target_class, compatible_class):
+        raise ValueError(
+            "Class '{}' does not derive from '{}.{}'.".format(
+                class_or_class_name,
+                compatible_class.__module__,
+                compatible_class.__name__
+            )
+        )
+
+    return target_class
+
+
+def _encode_bytes_if_required(data):
+    """
+    If the specified data is represented as bytes, convert it to a UTF8 string (using Base64 encoding if the bytes do not represent valid UTF8).
+
+    This is needed because json.dumps cannot serialise bytes.
+
+    :param data: The data to encode.
+    :return: If the data is represented as bytes, an equivalent UTF8 or Base64-encoded string; otherwise, the original data.
+    """
+
+    if (not isinstance(data, bytes)):
+        return data
+
+    try:
+        data = data.decode('utf8')
+    except UnicodeDecodeError:
+        data = base64.encodebytes(data).decode('ascii')
+
+    return data
diff --git a/setup.py b/setup.py
index 5216565..cf89a6b 100644
--- a/setup.py
+++ b/setup.py
@@ -1,62 +1,61 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-from setuptools import setup
-
-with open('README.rst') as readme_file:
-    readme = readme_file.read()
-
-with open('HISTORY.rst') as history_file:
-    history = history_file.read()
-
-requirements = [
-    'python_dateutil>=2.5.3',
-    'requests>=2.10.0',
-    'PyYAML>=3.11',
-]
-
-test_requirements = [
-    'pip>=8.1.2',
-    'bumpversion>=0.5.3',
-    'wheel>=0.29.0',
-    'watchdog>=0.8.3',
-    'flake8>=2.6.0',
-    'tox>=2.3.1',
-    'coverage>=4.1',
-    'Sphinx>=1.4.4',
-    'cryptography>=1.4',
-    'PyYAML>=3.11',
-    'pytest>=2.9.2',
-    'httmock>=1.2.5'
-]
-
-setup(
-    name='seqlog',
-    version='0.3.21a1',
-    description="SeqLog enables logging from Python to Seq.",
-    long_description=readme + '\n\n' + history,
-    author="Adam Friedman",
-    author_email='tintoy@tintoy.io',
-    url='https://github.com/tintoy/seqlog',
-    packages=[
-        'seqlog',
-    ],
-    package_dir={'seqlog':
-                 'seqlog'},
-    include_package_data=True,
-    install_requires=requirements,
-    license="MIT license",
-    zip_safe=False,
-    keywords='seqlog',
-    classifiers=[
-        'Development Status :: 4 - Beta',
-        'Intended Audience :: Developers',
-        'License :: OSI Approved :: MIT License',
-        'Natural Language :: English',
-        'Programming Language :: Python :: 3.3',
-        'Programming Language :: Python :: 3.4',
-        'Programming Language :: Python :: 3.5',
-    ],
-    test_suite='tests',
-    tests_require=test_requirements
-)
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+from setuptools import setup
+
+with open('README.rst') as readme_file:
+    readme = readme_file.read()
+
+with open('HISTORY.rst') as history_file:
+    history = history_file.read()
+
+requirements = [
+    'python_dateutil>=2.5.3',
+    'requests>=2.10.0',
+    'PyYAML>=3.11',
+]
+
+test_requirements = [
+    'pip>=8.1.2',
+    'bumpversion>=0.5.3',
+    'wheel>=0.29.0',
+    'watchdog>=0.8.3',
+    'flake8>=2.6.0',
+    'tox>=2.3.1',
+    'coverage>=4.1',
+    'Sphinx>=1.4.4',
+    'cryptography==3.4.8',
+    'PyYAML>=3.11',
+    'pytest>=2.9.2',
+    'httmock>=1.2.5'
+]
+
+setup(
+    name='seqlog',
+    version='0.3.23',
+    description="SeqLog enables logging from Python to Seq.",
+    long_description=readme + '\n\n' + history,
+    author="Adam Friedman",
+    author_email='tintoy@tintoy.io',
+    url='https://github.com/tintoy/seqlog',
+    packages=[
+        'seqlog',
+    ],
+    package_dir={'seqlog':
+                 'seqlog'},
+    include_package_data=True,
+    install_requires=requirements,
+    license="MIT license",
+    zip_safe=False,
+    keywords='seqlog',
+    classifiers=[
+        'Development Status :: 4 - Beta',
+        'Intended Audience :: Developers',
+        'License :: OSI Approved :: MIT License',
+        'Natural Language :: English',
+        'Programming Language :: Python :: 3.6',
+        'Programming Language :: Python :: 3.8',
+    ],
+    test_suite='tests',
+    tests_require=test_requirements
+)
diff --git a/tox.ini b/tox.ini
index ac617ea..e9e2649 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,5 +1,5 @@
 [tox]
-envlist = py33, py34, py35, py36, flake8
+envlist = py36, py38, flake8
 
 [testenv:flake8]
 basepython=python


Test patch:
diff --git a/tests/test_consumer.py b/tests/test_consumer.py
index 4983ba0..832472c 100644
--- a/tests/test_consumer.py
+++ b/tests/test_consumer.py
@@ -1,106 +1,107 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-test_seqlog
-----------------------------------
-
-Tests for `seqlog.consumer.QueueConsumer` module.
-"""
-import logging
-from queue import Queue
-from threading import Event
-from time import sleep
-
-import seqlog
-
-from seqlog.structured_logging import StructuredLogRecord
-
-from seqlog import SeqLogHandler
-from seqlog.consumer import QueueConsumer
-
-
-class TestLogRecordConsumer(object):
-
-    def test_callable_failures(self):
-        lh = SeqLogHandler('localhost')
-        le = StructuredLogRecord('test', logging.INFO, '/dev/null', 1, 'Hello world!', (), None)
-        callable_called = False
-
-        def handle_failure(e):
-            nonlocal callable_called
-            callable_called = True
-
-        seqlog.set_callback_on_failure(handle_failure)
-
-        lh.publish_log_batch([le])
-        assert callable_called
-    #
-    # Without flush timeout
-    #
-    def test_batchsize_2_pre_fill(self):
-        record_queue = Queue()
-        record_queue.put("Item1")
-        record_queue.put("Item2")
-
-        batch_received = Event()
-
-        def handler(record_batch):
-            assert len(record_batch) == 2, \
-                "Incorrect batch size (expected 2, but found {}.".format(len(record_batch))
-
-            batch_received.set()
-
-        consumer = QueueConsumer("Test Consumer", record_queue, handler, batch_size=2)
-        consumer.start()
-
-        batch_received.wait(timeout=2000)
-
-        consumer.stop()
-
-    def test_batchsize_2_post_fill(self):
-        record_queue = Queue()
-
-        batch_received = Event()
-
-        def handler(record_batch):
-            assert len(record_batch) == 2, \
-                "Incorrect batch size (expected 2, but found {}.".format(len(record_batch))
-
-            batch_received.set()
-
-        consumer = QueueConsumer("Test Consumer", record_queue, handler, batch_size=2)
-        consumer.start()
-
-        record_queue.put("Item1")
-        record_queue.put("Item2")
-
-        batch_received.wait(timeout=2000)
-
-        consumer.stop()
-
-    #
-    # With flush timeout
-    #
-    def test_batchsize_3_post_fill_flush_timeout(self):
-        record_queue = Queue()
-
-        batch_received = Event()
-
-        def handler(record_batch):
-            assert len(record_batch) == 2, \
-                "Incorrect batch size (expected 2, but found {}.".format(len(record_batch))
-
-            batch_received.set()
-
-        consumer = QueueConsumer("Test Consumer", record_queue, handler, batch_size=3, auto_flush_timeout=0.2)
-        consumer.start()
-
-        record_queue.put("Item1")
-        record_queue.put("Item2")
-        sleep(300 / 1000)
-        record_queue.put("Item3")
-
-        batch_received.wait(timeout=2000)
-
-        consumer.stop()
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+"""
+test_seqlog
+----------------------------------
+
+Tests for `seqlog.consumer.QueueConsumer` module.
+"""
+import logging
+from queue import Queue
+from threading import Event
+from time import sleep
+
+import seqlog
+
+from seqlog.structured_logging import StructuredLogRecord
+
+from seqlog import SeqLogHandler
+from seqlog.consumer import QueueConsumer
+
+
+class TestLogRecordConsumer(object):
+
+    def test_callable_failures(self):
+        lh = SeqLogHandler('localhost')
+        le = StructuredLogRecord('test', logging.INFO, '/dev/null', 1, 'Hello world!', (), None)
+        callable_called = False
+
+        def handle_failure(e):
+            nonlocal callable_called
+            callable_called = True
+
+        seqlog.set_callback_on_failure(handle_failure)
+
+        lh.publish_log_batch([le])
+        assert callable_called
+
+    #
+    # Without flush timeout
+    #
+    def test_batchsize_2_pre_fill(self):
+        record_queue = Queue()
+        record_queue.put("Item1")
+        record_queue.put("Item2")
+
+        batch_received = Event()
+
+        def handler(record_batch):
+            assert len(record_batch) == 2, \
+                "Incorrect batch size (expected 2, but found {}.".format(len(record_batch))
+
+            batch_received.set()
+
+        consumer = QueueConsumer("Test Consumer", record_queue, handler, batch_size=2)
+        consumer.start()
+
+        batch_received.wait(timeout=2000)
+
+        consumer.stop()
+
+    def test_batchsize_2_post_fill(self):
+        record_queue = Queue()
+
+        batch_received = Event()
+
+        def handler(record_batch):
+            assert len(record_batch) == 2, \
+                "Incorrect batch size (expected 2, but found {}.".format(len(record_batch))
+
+            batch_received.set()
+
+        consumer = QueueConsumer("Test Consumer", record_queue, handler, batch_size=2)
+        consumer.start()
+
+        record_queue.put("Item1")
+        record_queue.put("Item2")
+
+        batch_received.wait(timeout=2000)
+
+        consumer.stop()
+
+    #
+    # With flush timeout
+    #
+    def test_batchsize_3_post_fill_flush_timeout(self):
+        record_queue = Queue()
+
+        batch_received = Event()
+
+        def handler(record_batch):
+            assert len(record_batch) == 2, \
+                "Incorrect batch size (expected 2, but found {}.".format(len(record_batch))
+
+            batch_received.set()
+
+        consumer = QueueConsumer("Test Consumer", record_queue, handler, batch_size=3, auto_flush_timeout=0.2)
+        consumer.start()
+
+        record_queue.put("Item1")
+        record_queue.put("Item2")
+        sleep(300 / 1000)
+        record_queue.put("Item3")
+
+        batch_received.wait(timeout=2000)
+
+        consumer.stop()
diff --git a/tests/test_structured_log_record.py b/tests/test_structured_log_record.py
index 5f4c127..80ddbba 100644
--- a/tests/test_structured_log_record.py
+++ b/tests/test_structured_log_record.py
@@ -10,12 +10,11 @@
 
 import logging
 
-
 from seqlog.structured_logging import StructuredLogRecord
 import tests.assertions as expect
 
 # Turn off thread-related logging, since it would interfere with these tests.
-logging.threading = False
+logging.logThreads = False
 
 
 class TestStructuredLogRecord(object):