diff --git a/src/_zkapauthorizer/eliot.py b/src/_zkapauthorizer/eliot.py
index e9f4d49304300ee1be5f169f5e53ba97623b996e..26f766f552eaf5124b493f998961d96e099bc58f 100644
--- a/src/_zkapauthorizer/eliot.py
+++ b/src/_zkapauthorizer/eliot.py
@@ -16,7 +16,8 @@
 Eliot field, message, and action definitions for ZKAPAuthorizer.
 """
 
-from eliot import ActionType, Field, MessageType
+import attr
+from eliot import ActionType, Field, MessageType, register_exception_extractor
 
 PRIVACYPASS_MESSAGE = Field(
     "message",
@@ -102,3 +103,13 @@ MUTABLE_PASSES_REQUIRED = MessageType(
     [CURRENT_SIZES, TW_VECTORS_SUMMARY, NEW_SIZES, NEW_PASSES],
     "Some number of passes has been computed as the cost of updating a mutable.",
 )
+
+
+def register_attr_exception(cls):
+    """
+    Decorator that registers the decorated attr exception class with eliot.
+
+    The fields of the exception will be included when the exception is logged.
+    """
+    register_exception_extractor(cls, attr.asdict)
+    return cls
diff --git a/src/_zkapauthorizer/tests/test_eliot.py b/src/_zkapauthorizer/tests/test_eliot.py
new file mode 100644
index 0000000000000000000000000000000000000000..f191b040af251e2165051009ac806f9dc783fccb
--- /dev/null
+++ b/src/_zkapauthorizer/tests/test_eliot.py
@@ -0,0 +1,49 @@
+# Copyright PrivateStorage.io, LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""
+Tests for eliot helpers.
+"""
+from __future__ import absolute_import, division, print_function, unicode_literals
+
+import attr
+from eliot import start_action
+from eliot.testing import assertHasAction
+from testtools import TestCase
+
+from ..eliot import register_attr_exception
+from .eliot import capture_logging
+
+
+class RegisterExceptionTests(TestCase):
+    """
+    Tests for :py:`register_attr_exception`.
+    """
+
+    @capture_logging(None)
+    def test_register(self, logger):
+        @register_attr_exception
+        @attr.s(auto_exc=True)
+        class E(Exception):
+            field = attr.ib()
+
+        try:
+            with start_action(action_type="test:action"):
+                raise E(field="value")
+        except E:
+            pass
+
+        assertHasAction(
+            self, logger, "test:action", False, endFields={"field": "value"}
+        )