=== modified file '.gitignore'
--- a/.gitignore	2019-03-20 20:18:48 +0000
+++ b/.gitignore	2020-03-28 10:45:00 +0000
@@ -55,3 +55,5 @@
 
 # Files created by releasenotes build
 releasenotes/build
+RELEASENOTES.rst
+releasenotes/notes/reno.cache

=== modified file '.zuul.yaml'
--- a/.zuul.yaml	2019-09-04 09:46:58 +0000
+++ b/.zuul.yaml	2020-03-28 10:45:00 +0000
@@ -41,7 +41,6 @@
       - ^doc/.*$
       - ^releasenotes/.*$
 
-
 - project:
     check:
       jobs:
@@ -52,11 +51,9 @@
         - oslo.log-jsonformatter
     templates:
       - check-requirements
-      - lib-forward-testing
       - lib-forward-testing-python3
       - openstack-lower-constraints-jobs
-      - openstack-python-jobs
-      - openstack-python3-train-jobs
+      - openstack-python3-ussuri-jobs
       - periodic-stable-jobs
       - publish-openstack-docs-pti
       - release-notes-jobs-python3

=== modified file 'debian/changelog'
--- a/debian/changelog	2019-10-20 22:20:53 +0000
+++ b/debian/changelog	2020-03-28 10:45:00 +0000
@@ -1,3 +1,9 @@
+python-oslo.log (4.1.0-1) UNRELEASED; urgency=medium
+
+  * New upstream release.
+
+ -- Debian Janitor <janitor@jelmer.uk>  Sat, 28 Mar 2020 10:44:44 +0000
+
 python-oslo.log (3.44.1-2) unstable; urgency=medium
 
   [ Ondřej Nový ]

=== modified file 'debian/patches/skip-py3.7-test.patch'
--- a/debian/patches/skip-py3.7-test.patch	2019-03-20 20:36:46 +0000
+++ b/debian/patches/skip-py3.7-test.patch	2020-03-28 10:45:33 +0000
@@ -2,7 +2,7 @@
 ===================================================================
 --- python-oslo.log.orig/oslo_log/tests/unit/test_log.py
 +++ python-oslo.log/oslo_log/tests/unit/test_log.py
-@@ -1630,6 +1630,7 @@ keys=
+@@ -1626,6 +1626,7 @@ keys=
          root.error("boo")
          self.assertEqual("", stream.getvalue())
  

=== modified file 'doc/requirements.txt'
--- a/doc/requirements.txt	2019-09-04 09:46:58 +0000
+++ b/doc/requirements.txt	2020-03-28 10:45:00 +0000
@@ -2,8 +2,7 @@
 # of appearance. Changing the order has an impact on the overall integration
 # process, which may cause wedges in the gate later.
 
-sphinx!=1.6.6,!=1.6.7,>=1.6.2,<2.0.0;python_version=='2.7' # BSD
-sphinx!=1.6.6,!=1.6.7,!=2.1.0,>=1.6.2;python_version>='3.4' # BSD
+sphinx>=1.8.0,!=2.1.0 # BSD
 openstackdocstheme>=1.20.0 # Apache-2.0
 reno>=2.5.0 # Apache-2.0
 

=== modified file 'lower-constraints.txt'
--- a/lower-constraints.txt	2019-09-04 09:46:58 +0000
+++ b/lower-constraints.txt	2020-03-28 10:45:00 +0000
@@ -13,7 +13,7 @@
 flake8==2.5.5
 gitdb2==2.0.3
 GitPython==2.1.8
-hacking==0.12.0
+hacking==2.0.0
 idna==2.6
 imagesize==1.0.0
 iso8601==0.1.12
@@ -57,7 +57,7 @@
 six==1.11.0
 smmap2==2.0.3
 snowballstemmer==1.2.1
-Sphinx==1.6.5
+Sphinx==1.8.0
 sphinxcontrib-websupport==1.0.1
 stestr==2.0.0
 stevedore==1.28.0
@@ -65,6 +65,5 @@
 testrepository==0.0.20
 testtools==2.3.0
 traceback2==1.4.0
-unittest2==1.1.0
 urllib3==1.22
 wrapt==1.10.11

=== modified file 'oslo_log/formatters.py'
--- a/oslo_log/formatters.py	2019-09-04 09:46:58 +0000
+++ b/oslo_log/formatters.py	2020-03-28 10:45:00 +0000
@@ -34,15 +34,6 @@
     from functools import reduce
 
 
-try:
-    # Test if to_primitive() has the fallback parameter added
-    # in oslo.serialization 2.21.1
-    jsonutils.to_primitive(1, fallback=repr)
-    _HAVE_JSONUTILS_FALLBACK = True
-except TypeError:
-    _HAVE_JSONUTILS_FALLBACK = False
-
-
 def _dictify_context(context):
     if getattr(context, 'get_logging_values', None):
         return context.get_logging_values()
@@ -184,6 +175,16 @@
 _MSG_KEY_REGEX = re.compile(r'(%+)\((\w+)\)')
 
 
+def _json_dumps_with_fallback(obj):
+    # Bug #1593641: If an object cannot be serialized to JSON, convert
+    # it using repr() to prevent serialization errors. Using repr() is
+    # not ideal, but serialization errors are unexpected on logs,
+    # especially when the code using logs is not aware that the
+    # JSONFormatter will be used.
+    convert = functools.partial(jsonutils.to_primitive, fallback=repr)
+    return jsonutils.dumps(obj, default=convert)
+
+
 class JSONFormatter(logging.Formatter):
     def __init__(self, fmt=None, datefmt=None, style='%'):
         # NOTE(sfinucan) we ignore the fmt and style arguments, but they're
@@ -271,16 +272,7 @@
         if record.exc_info:
             message['traceback'] = self.formatException(record.exc_info)
 
-        if _HAVE_JSONUTILS_FALLBACK:
-            # Bug #1593641: If an object cannot be serialized to JSON, convert
-            # it using repr() to prevent serialization errors. Using repr() is
-            # not ideal, but serialization errors are unexpected on logs,
-            # especially when the code using logs is not aware that the
-            # JSONFormatter will be used.
-            convert = functools.partial(jsonutils.to_primitive, fallback=repr)
-            return jsonutils.dumps(message, default=convert)
-        else:
-            return jsonutils.dumps(message)
+        return _json_dumps_with_fallback(message)
 
 
 class FluentFormatter(logging.Formatter):
@@ -352,6 +344,12 @@
         else:
             message['context'] = {}
         extra.pop('context', None)
+        # NOTE(vdrok): try to dump complex objects
+        primitive_types = six.string_types + six.integer_types + (
+            bool, type(None), float, list, dict)
+        for key, value in extra.items():
+            if not isinstance(value, primitive_types):
+                extra[key] = _json_dumps_with_fallback(value)
         message['extra'] = extra
 
         if record.exc_info:

=== modified file 'oslo_log/log.py'
--- a/oslo_log/log.py	2019-09-04 09:46:58 +0000
+++ b/oslo_log/log.py	2020-03-28 10:45:00 +0000
@@ -455,6 +455,7 @@
                                   conf.rate_limit_interval,
                                   conf.rate_limit_except)
 
+
 _loggers = {}
 
 

=== modified file 'oslo_log/rate_limit.py'
--- a/oslo_log/rate_limit.py	2016-09-19 13:09:43 +0000
+++ b/oslo_log/rate_limit.py	2020-03-28 10:45:00 +0000
@@ -132,6 +132,7 @@
     for logger in _iter_loggers():
         logger.addFilter(log_filter)
 
+
 install_filter.log_filter = None
 install_filter.logger_class = None
 

=== modified file 'oslo_log/tests/unit/test_log.py'
--- a/oslo_log/tests/unit/test_log.py	2019-03-20 20:18:48 +0000
+++ b/oslo_log/tests/unit/test_log.py	2020-03-28 10:45:00 +0000
@@ -396,9 +396,9 @@
         log.setup(self.CONF, 'testing')
 
     def test_emit(self):
-        l = log.getLogger('nova-test.foo')
+        logger = log.getLogger('nova-test.foo')
         local_context = _fake_new_context()
-        l.info("Foo", context=local_context)
+        logger.info("Foo", context=local_context)
         self.assertEqual(
             mock.call(mock.ANY, CODE_FILE=mock.ANY, CODE_FUNC='test_emit',
                       CODE_LINE=mock.ANY, LOGGER_LEVEL='INFO',
@@ -421,12 +421,12 @@
             self.assertIsInstance(arg, six.string_types + (six.binary_type,))
 
     def test_emit_exception(self):
-        l = log.getLogger('nova-exception.foo')
+        logger = log.getLogger('nova-exception.foo')
         local_context = _fake_new_context()
         try:
             raise Exception("Some exception")
         except Exception:
-            l.exception("Foo", context=local_context)
+            logger.exception("Foo", context=local_context)
         self.assertEqual(
             mock.call(mock.ANY, CODE_FILE=mock.ANY,
                       CODE_FUNC='test_emit_exception',
@@ -485,12 +485,12 @@
         self.assertEqual(log.TRACE, self.log_trace.logger.getEffectiveLevel())
 
     def test_child_log_has_level_of_parent_flag(self):
-        l = log.getLogger('nova-test.foo')
-        self.assertEqual(logging.INFO, l.logger.getEffectiveLevel())
+        logger = log.getLogger('nova-test.foo')
+        self.assertEqual(logging.INFO, logger.logger.getEffectiveLevel())
 
     def test_child_log_has_level_of_parent_flag_for_trace(self):
-        l = log.getLogger('nova-trace.foo')
-        self.assertEqual(log.TRACE, l.logger.getEffectiveLevel())
+        logger = log.getLogger('nova-trace.foo')
+        self.assertEqual(log.TRACE, logger.logger.getEffectiveLevel())
 
     def test_get_loggers(self):
         log._loggers['sentinel_log'] = mock.sentinel.sentinel_log
@@ -661,10 +661,6 @@
                          data['error_summary'])
 
     def test_fallback(self):
-        if not formatters._HAVE_JSONUTILS_FALLBACK:
-            self.skipTest("need the fallback parameter of "
-                          "jsonutils.to_primitive() added in "
-                          "oslo_serialization 2.21.1")
 
         class MyObject(object):
             def __str__(self):
@@ -1944,14 +1940,14 @@
 
     def test_oslo_dot(self):
         logger_name = 'oslo.subname'
-        l = log.getLogger(logger_name)
-        self.assertEqual(logger_name, l.logger.name)
+        logger = log.getLogger(logger_name)
+        self.assertEqual(logger_name, logger.logger.name)
 
     def test_oslo_underscore(self):
         logger_name = 'oslo_subname'
         expected = logger_name.replace('_', '.')
-        l = log.getLogger(logger_name)
-        self.assertEqual(expected, l.logger.name)
+        logger = log.getLogger(logger_name)
+        self.assertEqual(expected, logger.logger.name)
 
 
 class IsDebugEnabledTestCase(test_base.BaseTestCase):

=== modified file 'oslo_log/tests/unit/test_versionutils.py'
--- a/oslo_log/tests/unit/test_versionutils.py	2017-08-04 19:28:29 +0000
+++ b/oslo_log/tests/unit/test_versionutils.py	2020-03-28 10:45:00 +0000
@@ -13,11 +13,12 @@
 #    License for the specific language governing permissions and limitations
 #    under the License.
 
+import unittest
+
 import mock
 from oslotest import base as test_base
 import six
 from testtools import matchers
-import unittest2
 
 from oslo_log import versionutils
 
@@ -247,7 +248,7 @@
                                as_of='Juno',
                                remove_in='Kilo')
 
-    @unittest2.skipIf(
+    @unittest.skipIf(
         six.PY3,
         'Deprecated exception detection does not work for Python 3')
     @mock.patch('oslo_log.versionutils.report_deprecated_feature')

=== modified file 'oslo_log/versionutils.py'
--- a/oslo_log/versionutils.py	2019-09-04 09:46:58 +0000
+++ b/oslo_log/versionutils.py	2020-03-28 10:45:00 +0000
@@ -73,6 +73,9 @@
     'R': 'Rocky',
     'S': 'Stein',
     'T': 'Train',
+    'U': 'Ussuri',
+    'V': 'Victoria',
+    'W': 'Wallaby',
 }
 
 
@@ -150,6 +153,7 @@
     ROCKY = 'R'
     STEIN = 'S'
     TRAIN = 'T'
+    USSURI = 'U'
 
     def __init__(self, as_of, in_favor_of=None, remove_in=2, what=None):
         """Initialize decorator

=== modified file 'playbooks/legacy/oslo.log-src-grenade-devstack/run.yaml'
--- a/playbooks/legacy/oslo.log-src-grenade-devstack/run.yaml	2019-09-04 09:46:58 +0000
+++ b/playbooks/legacy/oslo.log-src-grenade-devstack/run.yaml	2020-03-28 10:45:00 +0000
@@ -31,6 +31,7 @@
           export PYTHONUNBUFFERED=true
           export DEVSTACK_GATE_TEMPEST=1
           export DEVSTACK_GATE_GRENADE=pullup
+          export DEVSTACK_GATE_USE_PYTHON3=True
           export BRANCH_OVERRIDE=default
           if [ "$BRANCH_OVERRIDE" != "default" ] ; then
               export OVERRIDE_ZUUL_BRANCH=$BRANCH_OVERRIDE

=== added file 'releasenotes/notes/drop-python27-support-0fe4909a5468feb3.yaml'
--- a/releasenotes/notes/drop-python27-support-0fe4909a5468feb3.yaml	1970-01-01 00:00:00 +0000
+++ b/releasenotes/notes/drop-python27-support-0fe4909a5468feb3.yaml	2020-01-30 15:15:33 +0000
@@ -0,0 +1,5 @@
+---
+upgrade:
+  - |
+    Python 2.7 is no longer supported. The minimum supported version of Python
+    is now Python 3.6.

=== modified file 'releasenotes/source/index.rst'
--- a/releasenotes/source/index.rst	2019-09-04 09:46:58 +0000
+++ b/releasenotes/source/index.rst	2020-03-28 10:45:00 +0000
@@ -6,6 +6,7 @@
     :maxdepth: 1
 
     unreleased
+    train
     stein
     rocky
     queens

=== added file 'releasenotes/source/train.rst'
--- a/releasenotes/source/train.rst	1970-01-01 00:00:00 +0000
+++ b/releasenotes/source/train.rst	2019-09-20 16:33:49 +0000
@@ -0,0 +1,6 @@
+==========================
+Train Series Release Notes
+==========================
+
+.. release-notes::
+   :branch: stable/train

=== modified file 'setup.cfg'
--- a/setup.cfg	2019-09-04 09:46:58 +0000
+++ b/setup.cfg	2020-03-28 10:45:00 +0000
@@ -6,6 +6,7 @@
 author = OpenStack
 author-email = openstack-discuss@lists.openstack.org
 home-page = https://docs.openstack.org/oslo.log/latest
+python-requires = >=3.6
 classifier =
     Environment :: OpenStack
     Intended Audience :: Information Technology
@@ -13,11 +14,11 @@
     License :: OSI Approved :: Apache Software License
     Operating System :: POSIX :: Linux
     Programming Language :: Python
-    Programming Language :: Python :: 2
-    Programming Language :: Python :: 2.7
     Programming Language :: Python :: 3
     Programming Language :: Python :: 3.6
     Programming Language :: Python :: 3.7
+    Programming Language :: Python :: 3 :: Only
+    Programming Language :: Python :: Implementation :: CPython
 
 [files]
 packages =
@@ -35,9 +36,6 @@
 console_scripts =
     convert-json = oslo_log.cmds.convert_json:main
 
-[upload_sphinx]
-upload-dir = doc/build/html
-
 [compile_catalog]
 directory = oslo_log/locale
 domain = oslo_log
@@ -51,6 +49,3 @@
 keywords = _ gettext ngettext l_ lazy_gettext
 mapping_file = babel.cfg
 output_file = oslo_log/locale/oslo_log.pot
-
-[bdist_wheel]
-universal = 1

=== modified file 'setup.py'
--- a/setup.py	2017-08-04 19:28:29 +0000
+++ b/setup.py	2020-03-28 10:45:00 +0000
@@ -13,7 +13,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# THIS FILE IS MANAGED BY THE GLOBAL REQUIREMENTS REPO - DO NOT EDIT
 import setuptools
 
 # In python < 2.7.4, a lazy loading of package `pbr` will break

=== modified file 'test-requirements.txt'
--- a/test-requirements.txt	2019-09-04 09:46:58 +0000
+++ b/test-requirements.txt	2020-03-28 10:45:00 +0000
@@ -2,7 +2,7 @@
 # of appearance. Changing the order has an impact on the overall integration
 # process, which may cause wedges in the gate later.
 
-hacking!=0.13.0,<0.14,>=0.12.0 # Apache-2.0
+hacking>=2.0.0,<2.1.0 # Apache-2.0
 
 stestr>=2.0.0 # Apache-2.0
 testtools>=2.3.0 # MIT

=== modified file 'tox.ini'
--- a/tox.ini	2019-09-04 09:46:58 +0000
+++ b/tox.ini	2020-03-28 10:45:00 +0000
@@ -1,8 +1,10 @@
 [tox]
-minversion = 2.0
-envlist = py27,py36,py37,pep8
+minversion = 3.1
+envlist = py36,py37,pep8
+ignore_basepython_conflict = true
 
 [testenv]
+basepython = python3
 whitelist_externals =
   find
 deps =
@@ -14,32 +16,33 @@
   stestr slowest
 
 [testenv:pep8]
-basepython = python3
 commands =
   flake8
   # Run security linter
   bandit -r oslo_log -x tests -n5
 
 [testenv:venv]
-basepython = python3
 commands = {posargs}
 
 [testenv:docs]
-basepython = python3
+whitelist_externals = rm
 deps =
   -c{env:UPPER_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master}
   -r{toxinidir}/doc/requirements.txt
-commands = sphinx-build -a -E -W -b html doc/source doc/build/html
+commands =
+  rm -fr doc/build
+  sphinx-build -a -E -W --keep-going -b html doc/source doc/build/html
 
 [testenv:releasenotes]
-basepython = python3
+whitelist_externals = rm
 deps =
   -c{env:UPPER_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master}
   -r{toxinidir}/doc/requirements.txt
-commands = sphinx-build -a -E -W -b html releasenotes/source releasenotes/build/html
+commands =
+  rm -rf releasenotes/build
+  sphinx-build -a -E -W --keep-going -b html releasenotes/source releasenotes/build/html
 
 [testenv:cover]
-basepython = python3
 commands =
   coverage erase
   {[testenv]commands}
@@ -49,11 +52,9 @@
   coverage report --show-missing
 
 [testenv:bandit]
-basepython = python3
 commands = bandit -r oslo_log -x tests -n5
 
 [testenv:lower-constraints]
-basepython = python3
 deps =
   -c{toxinidir}/lower-constraints.txt
   -r{toxinidir}/test-requirements.txt
@@ -61,8 +62,9 @@
 
 [flake8]
 # E123, E125 skipped as they are invalid PEP-8.
+# W503, W504 skipped: https://www.python.org/dev/peps/pep-0008/#should-a-line-break-before-or-after-a-binary-operator
 show-source = True
-ignore = E123,E125,H405
+ignore = E123,E125,H405,W503,W504
 exclude=.venv,.git,.tox,dist,doc,*lib/python*,*egg,build,__init__.py
 
 [hacking]