diff --git a/.travis.yml b/.travis.yml
index 8813f7b..04f68af 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -15,6 +15,7 @@ python:
   - pypy3
 
 install:
+  - pip install pip setuptools --upgrade
   - pip install -e .\[dev\]
 
 script:
diff --git a/tinymr/_mrtools.py b/tinymr/_mrtools.py
index 1a200db..ce3f9ef 100644
--- a/tinymr/_mrtools.py
+++ b/tinymr/_mrtools.py
@@ -82,7 +82,8 @@ def sort_partitioned_values(kv_stream):
     return ((k, sorter(v, key=lambda x: x[0])) for k, v in kv_stream)
 
 
-class ReduceJob(namedtuple('ReduceJob', ['reducer', 'sort', 'jobs', 'chunksize'])):
+class ReduceJobConf(
+        namedtuple('ReduceJob', ['reducer', 'sort', 'jobs', 'chunksize'])):
 
     """
     Describes a reduce job.  Makes keeping track of multiple reducers easier.
diff --git a/tinymr/base.py b/tinymr/base.py
index eba8905..c29bbb6 100644
--- a/tinymr/base.py
+++ b/tinymr/base.py
@@ -3,7 +3,9 @@ Base classes.  Subclass away!
 """
 
 
+import inspect
 from itertools import chain
+import logging
 
 import six
 
@@ -201,7 +203,8 @@ class BaseMapReduce(object):
         Sort the output from each `reducer()` before executing the next or
         before being passed to `output()`.
 
-        Define one property per reducer, so `reducer2()` would be `sort_reduce2`.
+        Define one property per reducer, so `reducer2()` would be
+        `sort_reduce2`.
 
         Returns
         -------
@@ -210,6 +213,16 @@ class BaseMapReduce(object):
 
         return self.sort
 
+    @property
+    def logger(self):
+
+        """
+        Each MapReduce task gets its own logger with a name like
+        `tinymr-ClassName`.
+        """
+
+        return logging.getLogger('tinymr-{}'.format(self.__class__.__name__))
+
     def close(self):
 
         """
@@ -318,18 +331,43 @@ class BaseMapReduce(object):
         return ((key, tuple(values)) for key, values in pairs)
 
     @property
-    def _reduce_jobs(self):
-
-        reducers = tools.sorter(filter(
-            lambda x: not x.startswith('_') and 'reducer' in x,
-            dir(self)))
-
-        for r in reducers:
-            yield _mrtools.ReduceJob(
-                reducer=getattr(self, r),
-                sort=getattr(self, 'sort_{}'.format(r.replace('reducer', 'reduce'))),
-                jobs=getattr(self, '{}_jobs'.format(r.replace('reducer', 'reduce'))),
-                chunksize=getattr(self, '{}_jobs'.format(r.replace('reducer', 'reduce'))))
+    def _reduce_job_confs(self):
+
+        """
+        The user can define multiple reduce operations, each with their own
+        independent job configuration, to be executed in a specified order.
+        This method produces one `_mrtools.ReduceJobConf()` per reduce
+        operation in execution order.
+
+        Returns
+        -------
+        tuple
+        """
+
+        # We encourage user's to add their own properties and methods, so
+        # we want to be really confident that we are _only_ grabbing the
+        # reducer methods, otherwise difficult to debug failures might pop up.
+        # Can't assume the reducers were defined in order.
+        reducers = {}
+        for method in (m for m in dir(self) if m != '_reduce_job_confs'):
+
+            if method.startswith('reducer') and \
+                    inspect.ismethod(getattr(self, method)):
+
+                str_idx = method.lstrip('reducer') or '-1'
+
+                sort_method = 'sort_reduce{}'.format(str_idx)
+                jobs_method = 'reduce{}_jobs'.format(str_idx)
+                chunksize_method = 'reduce{}_chunksize'.format(str_idx)
+
+                reducers[int(str_idx)] = _mrtools.ReduceJobConf(
+                    reducer=getattr(self, method),
+                    sort=getattr(self, sort_method, self.sort_reduce),
+                    jobs=getattr(self, jobs_method, self.reduce_jobs),
+                    chunksize=getattr(
+                        self, chunksize_method, self.reduce_chunksize))
+
+        return [reducers.pop(i) for i in sorted(reducers.keys())]
 
     def _map_combine_partition(self, stream):
 
diff --git a/tinymr/memory.py b/tinymr/memory.py
index f48b4a9..97bf1d6 100644
--- a/tinymr/memory.py
+++ b/tinymr/memory.py
@@ -12,38 +12,54 @@ from tinymr import tools
 from tinymr.tools import runner
 
 
-logger = logging.getLogger('tinymr')
-logger.setLevel(logging.DEBUG)
-
-
 class MemMapReduce(base.BaseMapReduce):
 
-    def __call__(self, stream):
+    def __call__(self, stream, log_level=logging.NOTSET):
+
+        original_log_level = self.logger.level
+        self.logger.setLevel(log_level)
 
         sliced = tools.slicer(stream, self.map_chunksize)
 
         # Map, partition, combine, partition
+        self.logger.info(
+            "Running map, combine, and partition phase with %s jobs, chunksize "
+            "%s, sort_map=%s, and sort_combine=%s",
+            self.map_jobs, self.map_chunksize, self.sort_map, self.sort_combine)
+
         with runner(self._map_combine_partition, sliced, self.map_jobs) as mcp:
             partitioned = tools.merge_partitions(*mcp, sort=self.sort_combine)
 
+        self.logger.info("Finished map with %s keys", len(partitioned))
+        self.logger.info("Initializing reduce phase")
         self.init_reduce()
 
         # Run all partition jobs
         reducer_input = partitioned
-        for rj in self._reduce_jobs:
+        for rj in self._reduce_job_confs:
 
+            self.logger.info("Running reduce job %s", rj)
+
+            # Pin the reduce job so we can treat it like a lambda
             func = functools.partial(
                 self._reduce_partition, reducer=rj.reducer, sort=rj.sort)
 
             reducer_input = _mrtools.strip_sort_key(reducer_input)
             sliced = tools.slicer(reducer_input, rj.chunksize)
-
             with runner(func, sliced, rj.jobs) as reduced:
                 partitioned = tools.merge_partitions(*reduced, sort=rj.sort)
 
+            self.logger.info(
+                "Finished reduce job %s with %s keys", rj, len(partitioned))
+
         partitioned = _mrtools.strip_sort_key(partitioned)
 
+        self.logger.info("Sorting output data by key")
         if self.sort_output:
             partitioned = self._output_sorter(partitioned)
 
-        return self.output(partitioned)
+        try:
+            self.logger.info("Producing output dataset")
+            return self.output(partitioned)
+        finally:
+            self.logger.setLevel(original_log_level)

ReduceJob().chunksize is populated with reduce_jobs
diff --git a/tests/test_base.py b/tests/test_base.py
index 7468888..47309ac 100644
--- a/tests/test_base.py
+++ b/tests/test_base.py
@@ -5,6 +5,7 @@ Unittests for tinymr.base
 
 import pytest
 
+from tinymr import _mrtools
 from tinymr import base
 from tinymr import errors
 
@@ -36,6 +37,48 @@ def test_default_methods():
     assert list(mr.output(expected)) == expected
 
 
+def test_reduce_job_confs():
+    # Make sure attributes are coming from the correct location
+    class MR(base.BaseMapReduce):
+
+        jobs = 4
+        reduce2_chunksize = 10
+        reduce10_jobs = 2
+        sort = False
+        sort_reduce2 = True
+
+        # Define out of order to test sorting
+        def reducer10(self, key, values):
+            pass
+
+        def reducer(self, key, values):
+            pass
+
+        def reducer2(self, key, values):
+            pass
+
+    mr = MR()
+
+    rj = _mrtools.ReduceJobConf(
+        reducer=mr.reducer,
+        sort=False,
+        jobs=4,
+        chunksize=1)
+    rj2 = _mrtools.ReduceJobConf(
+        reducer=mr.reducer2,
+        sort=True,
+        jobs=4,
+        chunksize=10)
+    rj10 = _mrtools.ReduceJobConf(
+        reducer=mr.reducer10,
+        sort=False,
+        jobs=2,
+        chunksize=1)
+
+    assert mr._reduce_job_confs == [rj, rj2, rj10]
+
+
+
 # def test_context_manager():
 #
 #     class MapReduce(base.BaseMapReduce):

pip install -e .[dev]
parse_log_pytest
pytest
pytest
3.5
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
attrs @ file:///opt/conda/conda-bld/attrs_1642510447205/work
certifi==2021.5.30
charset-normalizer==2.0.12
coverage==6.2
coveralls==3.3.1
docopt==0.6.2
idna==3.10
importlib-metadata @ file:///tmp/build/80754af9/importlib-metadata_1631916693255/work
iniconfig @ file:///home/linux1/recipes/ci/iniconfig_1610983019677/work
more-itertools @ file:///tmp/build/80754af9/more-itertools_1637733554872/work
packaging @ file:///tmp/build/80754af9/packaging_1637314298585/work
pluggy @ file:///tmp/build/80754af9/pluggy_1615976315926/work
py @ file:///opt/conda/conda-bld/py_1644396412707/work
pyparsing @ file:///tmp/build/80754af9/pyparsing_1635766073266/work
pytest==6.2.4
pytest-cov==4.0.0
requests==2.27.1
six==1.17.0
-e git+https://github.com/geowurster/tinymr.git@5dbf46845a8caba995916f76d1e681860f1e198f#egg=tinymr
toml @ file:///tmp/build/80754af9/toml_1616166611790/work
tomli==1.2.3
typing_extensions @ file:///opt/conda/conda-bld/typing_extensions_1647553014482/work
urllib3==1.26.20
zipp @ file:///tmp/build/80754af9/zipp_1633618647012/work

name: tinymr
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - attrs=21.4.0=pyhd3eb1b0_0
  - ca-certificates=2025.2.25=h06a4308_0
  - certifi=2021.5.30=py36h06a4308_0
  - importlib-metadata=4.8.1=py36h06a4308_0
  - importlib_metadata=4.8.1=hd3eb1b0_0
  - iniconfig=1.1.1=pyhd3eb1b0_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.3=he6710b0_2
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - more-itertools=8.12.0=pyhd3eb1b0_0
  - ncurses=6.4=h6a678d5_0
  - openssl=1.1.1w=h7f8727e_0
  - packaging=21.3=pyhd3eb1b0_0
  - pip=21.2.2=py36h06a4308_0
  - pluggy=0.13.1=py36h06a4308_0
  - py=1.11.0=pyhd3eb1b0_0
  - pyparsing=3.0.4=pyhd3eb1b0_0
  - pytest=6.2.4=py36h06a4308_2
  - python=3.6.13=h12debd9_1
  - readline=8.2=h5eee18b_0
  - setuptools=58.0.4=py36h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - toml=0.10.2=pyhd3eb1b0_0
  - typing_extensions=4.1.1=pyh06a4308_0
  - wheel=0.37.1=pyhd3eb1b0_0
  - xz=5.6.4=h5eee18b_1
  - zipp=3.6.0=pyhd3eb1b0_0
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - charset-normalizer==2.0.12
      - coverage==6.2
      - coveralls==3.3.1
      - docopt==0.6.2
      - idna==3.10
      - pytest-cov==4.0.0
      - requests==2.27.1
      - six==1.17.0
      - tomli==1.2.3
      - urllib3==1.26.20
prefix: /opt/conda/envs/tinymr

tests/test_base.py::test_reduce_job_confs
tests/test_base.py::test_not_implemented_methods tests/test_base.py::test_default_settings tests/test_base.py::test_default_methods
tinymr/_mrtools.py .travis.yml tinymr/base.py tinymr/memory.py
tinymr/_mrtools.py .travis.yml tinymr/base.py tinymr/memory.py