diff --git a/indra/llcommon/lleventfilter.cpp b/indra/llcommon/lleventfilter.cpp
index 9fb18dc67dd3fa9067a1b427cf08bf3ec8d1cc97..06b3cb769e2ab6a8bd6778cc9e67348cb13b4c06 100644
--- a/indra/llcommon/lleventfilter.cpp
+++ b/indra/llcommon/lleventfilter.cpp
@@ -37,6 +37,7 @@
 // other Linden headers
 #include "llerror.h"                // LL_ERRS
 #include "llsdutil.h"               // llsd_matches()
+#include "stringize.h"
 
 /*****************************************************************************
 *   LLEventFilter
@@ -409,3 +410,61 @@ void LLEventBatchThrottle::setSize(std::size_t size)
         flush();
     }
 }
+
+/*****************************************************************************
+*   LLEventLogProxy
+*****************************************************************************/
+LLEventLogProxy::LLEventLogProxy(LLEventPump& source, const std::string& name, bool tweak):
+    // note: we are NOT using the constructor that implicitly connects!
+    LLEventFilter(name, tweak),
+    // instead we simply capture a reference to the subject LLEventPump
+    mPump(source)
+{
+}
+
+bool LLEventLogProxy::post(const LLSD& event) /* override */
+{
+    auto counter = mCounter++;
+    auto eventplus = event;
+    if (eventplus.type() == LLSD::TypeMap)
+    {
+        eventplus["_cnt"] = counter;
+    }
+    std::string hdr{STRINGIZE(getName() << ": post " << counter)};
+    LL_INFOS("LogProxy") << hdr << ": " << event << LL_ENDL;
+    bool result = mPump.post(eventplus);
+    LL_INFOS("LogProxy") << hdr << " => " << result << LL_ENDL;
+    return result;
+}
+
+LLBoundListener LLEventLogProxy::listen_impl(const std::string& name,
+                                             const LLEventListener& target,
+                                             const NameList& after,
+                                             const NameList& before)
+{
+    LL_DEBUGS("LogProxy") << "LLEventLogProxy('" << getName() << "').listen('"
+                          << name << "')" << LL_ENDL;
+    return mPump.listen(name,
+                        [this, name, target](const LLSD& event)->bool
+                        { return listener(name, target, event); },
+                        after,
+                        before);
+}
+
+bool LLEventLogProxy::listener(const std::string& name,
+                               const LLEventListener& target,
+                               const LLSD& event) const
+{
+    auto eventminus = event;
+    std::string counter{"**"};
+    if (eventminus.has("_cnt"))
+    {
+        counter = stringize(eventminus["_cnt"].asInteger());
+        eventminus.erase("_cnt");
+    }
+    std::string hdr{STRINGIZE(getName() << " to " << name << " " << counter)};
+    LL_INFOS("LogProxy") << hdr << ": " << eventminus << LL_ENDL;
+    bool result = target(eventminus);
+    LL_INFOS("LogProxy") << hdr << " => " << result << LL_ENDL;
+    return result;
+}
diff --git a/indra/llcommon/lleventfilter.h b/indra/llcommon/lleventfilter.h
index 8e7c07558159fcba0a1a468c42d62bcf071b2fad..79319353a7355af85efa832ba041b99d62b7ff29 100644
--- a/indra/llcommon/lleventfilter.h
+++ b/indra/llcommon/lleventfilter.h
@@ -427,4 +427,99 @@ class LLStoreListener: public LLEventFilter
     const bool mConsume;
 };
 
+/*****************************************************************************
+*   LLEventLogProxy
+*****************************************************************************/
+/**
+ * LLEventLogProxy is a little different than the other LLEventFilter
+ * subclasses declared in this header file, in that it completely wraps the
+ * passed LLEventPump (both input and output) instead of simply processing its
+ * output. Of course, if someone directly posts to the wrapped LLEventPump by
+ * looking up its string name in LLEventPumps, LLEventLogProxy can't intercept
+ * that post() call. But as long as consuming code is willing to access the
+ * LLEventLogProxy instance instead of the wrapped LLEventPump, all event data
+ * both post()ed and received is logged.
+ *
+ * The proxy role means that LLEventLogProxy intercepts more of LLEventPump's
+ * API than a typical LLEventFilter subclass.
+ */
+class LLEventLogProxy: public LLEventFilter
+{
+    typedef LLEventFilter super;
+public:
+    /**
+     * Construct LLEventLogProxy, wrapping the specified LLEventPump.
+     * Unlike a typical LLEventFilter subclass, the name parameter is @emph
+     * not optional because typically you want LLEventLogProxy to completely
+     * replace the wrapped LLEventPump. So you give the subject LLEventPump
+     * some other name and give the LLEventLogProxy the name that would have
+     * been used for the subject LLEventPump.
+     */
+    LLEventLogProxy(LLEventPump& source, const std::string& name, bool tweak=false);
+
+    /// register a new listener
+    LLBoundListener listen_impl(const std::string& name, const LLEventListener& target,
+                                const NameList& after, const NameList& before);
+
+    /// Post an event to all listeners
+    virtual bool post(const LLSD& event) /* override */;
+
+private:
+    /// This method intercepts each call to any target listener. We pass it
+    /// the listener name and the caller's intended target listener plus the
+    /// posted LLSD event.
+    bool listener(const std::string& name,
+                  const LLEventListener& target,
+                  const LLSD& event) const;
+
+    LLEventPump& mPump;
+    LLSD::Integer mCounter{0};
+};
+
+/**
+ * LLEventPumpHolder<T> is a helper for LLEventLogProxyFor<T>. It simply
+ * stores an instance of T, presumably a subclass of LLEventPump. We derive
+ * LLEventLogProxyFor<T> from LLEventPumpHolder<T>, ensuring that
+ * LLEventPumpHolder's contained mWrappedPump is fully constructed before
+ * passing it to LLEventLogProxyFor's LLEventLogProxy base class constructor.
+ * But since LLEventPumpHolder<T> presents none of the LLEventPump API,
+ * LLEventLogProxyFor<T> inherits its methods unambiguously from
+ * LLEventLogProxy.
+ */
+template <class T>
+class LLEventPumpHolder
+{
+protected:
+    LLEventPumpHolder(const std::string& name, bool tweak=false):
+        mWrappedPump(name, tweak)
+    {}
+    T mWrappedPump;
+};
+
+/**
+ * LLEventLogProxyFor<T> is a wrapper around any of the LLEventPump subclasses.
+ * Instantiating an LLEventLogProxy<T> instantiates an internal T. Otherwise
+ * it behaves like LLEventLogProxy.
+ */
+template <class T>
+class LLEventLogProxyFor: private LLEventPumpHolder<T>, public LLEventLogProxy
+{
+    // We derive privately from LLEventPumpHolder because it's an
+    // implementation detail of LLEventLogProxyFor. The only reason it's a
+    // base class at all is to guarantee that it's constructed first so we can
+    // pass it to our LLEventLogProxy base class constructor.
+    typedef LLEventPumpHolder<T> holder;
+    typedef LLEventLogProxy super;
+
+public:
+    LLEventLogProxyFor(const std::string& name, bool tweak=false):
+        // our wrapped LLEventPump subclass instance gets a name suffix
+        // because that's not the LLEventPump we want consumers to obtain when
+        // they ask LLEventPumps for this name
+        holder(name + "-", tweak),
+        // it's our LLEventLogProxy that gets the passed name
+        super(holder::mWrappedPump, name, tweak)
+    {}
+};
+
 #endif /* ! defined(LL_LLEVENTFILTER_H) */
diff --git a/indra/llcommon/tests/lleventcoro_test.cpp b/indra/llcommon/tests/lleventcoro_test.cpp
index 4e774b27d9f793183d3cf26c41af1b56307481bc..c13920eefd51d7db1fbf0bb9f2e0d3558112c1c1 100644
--- a/indra/llcommon/tests/lleventcoro_test.cpp
+++ b/indra/llcommon/tests/lleventcoro_test.cpp
@@ -37,12 +37,14 @@
 
 #include <iostream>
 #include <string>
+#include <typeinfo>
 
 #include "../test/lltut.h"
 #include "llsd.h"
 #include "llsdutil.h"
 #include "llevents.h"
 #include "llcoros.h"
+#include "lleventfilter.h"
 #include "lleventcoro.h"
 #include "../test/debug.h"
 #include "../test/sync.h"
@@ -255,4 +257,80 @@ namespace tut
         LLCoros::instance().launch("test<5>", [this](){ coroPumpPost(); });
         ensure_equals(result.asInteger(), 18);
     }
+
+    template <class PUMP>
+    void test()
+    {
+        PUMP pump(typeid(PUMP).name());
+        bool running{false};
+        LLSD data{LLSD::emptyArray()};
+        // start things off by posting once before even starting the listener
+        // coro
+        LL_DEBUGS() << "test() posting first" << LL_ENDL;
+        LLSD first{LLSDMap("desc", "first")("value", 0)};
+        bool consumed = pump.post(first);
+        ensure("should not have consumed first", ! consumed);
+        // now launch the coro
+        LL_DEBUGS() << "test() launching listener coro" << LL_ENDL;
+        running = true;
+        LLCoros::instance().launch(
+            "listener",
+            [&pump, &running, &data](){
+                // important for this test that we consume posted values
+                LLCoros::instance().set_consuming(true);
+                // should immediately retrieve 'first' without waiting
+                LL_DEBUGS() << "listener coro waiting for first" << LL_ENDL;
+                data.append(llcoro::suspendUntilEventOnWithTimeout(pump, 0.1, LLSD()));
+                // Don't use ensure() from within the coro -- ensure() failure
+                // throws tut::fail, which won't propagate out to the main
+                // test driver, which will result in an odd failure.
+                // Wait for 'second' because it's not already pending.
+                LL_DEBUGS() << "listener coro waiting for second" << LL_ENDL;
+                data.append(llcoro::suspendUntilEventOnWithTimeout(pump, 0.1, LLSD()));
+                // and wait for 'third', which should involve no further waiting
+                LL_DEBUGS() << "listener coro waiting for third" << LL_ENDL;
+                data.append(llcoro::suspendUntilEventOnWithTimeout(pump, 0.1, LLSD()));
+                LL_DEBUGS() << "listener coro done" << LL_ENDL;
+                running = false;
+            });
+        // back from coro at the point where it's waiting for 'second'
+        LL_DEBUGS() << "test() posting second" << LL_ENDL;
+        LLSD second{llsd::map("desc", "second", "value", 1)};
+        consumed = pump.post(second);
+        ensure("should have consumed second", consumed);
+        // This is a key point: even though we've post()ed the value for which
+        // the coroutine is waiting, it's actually still suspended until we
+        // pause for some other reason. The coroutine will only pick up one
+        // value at a time from our 'pump'. It's important to exercise the
+        // case when we post() two values before it picks up either.
+        LL_DEBUGS() << "test() posting third" << LL_ENDL;
+        LLSD third{llsd::map("desc", "third", "value", 2)};
+        consumed = pump.post(third);
+        ensure("should NOT yet have consumed third", ! consumed);
+        // now just wait for coro to finish -- which it eventually will, given
+        // that all its suspend calls have short timeouts.
+        while (running)
+        {
+            LL_DEBUGS() << "test() waiting for coro done" << LL_ENDL;
+            llcoro::suspendUntilTimeout(0.1);
+        }
+        // okay, verify expected results
+        ensure_equals("should have received three values", data,
+                      llsd::array(first, second, third));
+        LL_DEBUGS() << "test() done" << LL_ENDL;
+    }
+
+    template<> template<>
+    void object::test<6>()
+    {
+        set_test_name("LLEventMailDrop");
+        tut::test<LLEventMailDrop>();
+    }
+
+    template<> template<>
+    void object::test<7>()
+    {
+        set_test_name("LLEventLogProxyFor<LLEventMailDrop>");
+        tut::test< LLEventLogProxyFor<LLEventMailDrop> >();
+    }
 }
diff --git a/indra/llcommon/tests/lleventfilter_test.cpp b/indra/llcommon/tests/lleventfilter_test.cpp
index 1875013794fe1abd6c5703d6b322341211856c65..fa2cb03e958073c745480d61391a58da35c78fb5 100644
--- a/indra/llcommon/tests/lleventfilter_test.cpp
+++ b/indra/llcommon/tests/lleventfilter_test.cpp
@@ -36,9 +36,12 @@
 // other Linden headers
 #include "../test/lltut.h"
 #include "stringize.h"
+#include "llsdutil.h"
 #include "listener.h"
 #include "tests/wrapllerrs.h"
 
+#include <typeinfo>
+
 /*****************************************************************************
 *   Test classes
 *****************************************************************************/
@@ -401,6 +404,78 @@ namespace tut
         throttle.post(";17");
         ensure_equals("17", cat.result, "136;12;17"); // "17" delivered
     }
+
+    template<class PUMP>
+    void test()
+    {
+        PUMP pump(typeid(PUMP).name());
+        LLSD data{LLSD::emptyArray()};
+        bool consumed{true};
+        // listener that appends to 'data'
+        // but that also returns the current value of 'consumed'
+        // Instantiate this separately because we're going to listen()
+        // multiple times with the same lambda: LLEventMailDrop only replays
+        // queued events on a new listen() call.
+        auto lambda =
+            [&data, &consumed](const LLSD& event)->bool
+            {
+                data.append(event);
+                return consumed;
+            };
+        {
+            LLTempBoundListener conn = pump.listen("lambda", lambda);
+            pump.post("first");
+        }
+        // first post() should certainly be received by listener
+        ensure_equals("first", data, llsd::array("first"));
+        // the question is, since consumed was true, did it queue the value?
+        data = LLSD::emptyArray();
+        {
+            // if it queued the value, it would be delivered on subsequent
+            // listen() call
+            LLTempBoundListener conn = pump.listen("lambda", lambda);
+        }
+        ensure_equals("empty1", data, LLSD::emptyArray());
+        data = LLSD::emptyArray();
+        // now let's NOT consume the posted data
+        consumed = false;
+        {
+            LLTempBoundListener conn = pump.listen("lambda", lambda);
+            pump.post("second");
+            pump.post("third");
+        }
+        // the two events still arrive
+        ensure_equals("second,third1", data, llsd::array("second", "third"));
+        data = LLSD::emptyArray();
+        {
+            // when we reconnect, these should be delivered again
+            // but this time they should be consumed
+            consumed = true;
+            LLTempBoundListener conn = pump.listen("lambda", lambda);
+        }
+        // unconsumed events were delivered again
+        ensure_equals("second,third2", data, llsd::array("second", "third"));
+        data = LLSD::emptyArray();
+        {
+            // when we reconnect this time, no more unconsumed events
+            LLTempBoundListener conn = pump.listen("lambda", lambda);
+        }
+        ensure_equals("empty2", data, LLSD::emptyArray());
+    }
+
+    template<> template<>
+    void filter_object::test<6>()
+    {
+        set_test_name("LLEventMailDrop");
+        tut::test<LLEventMailDrop>();
+    }
+
+    template<> template<>
+    void filter_object::test<7>()
+    {
+        set_test_name("LLEventLogProxyFor<LLEventMailDrop>");
+        tut::test< LLEventLogProxyFor<LLEventMailDrop> >();
+    }
 } // namespace tut
 
 /*****************************************************************************