From 9e0567dcb24fa3e743d6c9e9e9089f0009573bbd Mon Sep 17 00:00:00 2001
From: Tom Prince <tom.prince@private.storage>
Date: Tue, 9 Nov 2021 15:55:34 -0700
Subject: [PATCH] Add an decorator that logs attr fields of exceptions to
 eliot.

---
 setup.cfg                               |  2 +-
 src/_zkapauthorizer/eliot.py            | 13 ++++++-
 src/_zkapauthorizer/tests/test_eliot.py | 48 +++++++++++++++++++++++++
 3 files changed, 61 insertions(+), 2 deletions(-)
 create mode 100644 src/_zkapauthorizer/tests/test_eliot.py

diff --git a/setup.cfg b/setup.cfg
index 931ec68..15f3b2a 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -32,7 +32,7 @@ packages =
     twisted.plugins
 
 install_requires =
-    attrs
+    attrs >= 21.3.0
     zope.interface
     eliot >= 1.11,<2
     aniso8601
diff --git a/src/_zkapauthorizer/eliot.py b/src/_zkapauthorizer/eliot.py
index e9f4d49..65a6f3e 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 attrs
+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 attrs exception class with eliot.
+
+    The fields of the exception will be included when the exception is logged.
+    """
+    register_exception_extractor(cls, attrs.asdict)
+    return cls
diff --git a/src/_zkapauthorizer/tests/test_eliot.py b/src/_zkapauthorizer/tests/test_eliot.py
new file mode 100644
index 0000000..84b9351
--- /dev/null
+++ b/src/_zkapauthorizer/tests/test_eliot.py
@@ -0,0 +1,48 @@
+# 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, capture_logging
+from testtools import TestCase
+
+from ..eliot import register_attr_exception
+
+
+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"}
+        )
-- 
GitLab