pw_{log, log_rpc}: Separate Filter Service

- Separate the log and log filter services to make the first not rely on
the second.
- Separated unit tests for both services with minor fixes to existing
unit tests.
- Updated documentation.

Fixes: 570
Change-Id: I08945088f17e9c93623fe44819fa39be5af7a262
Requires: pigweed-internal:18860
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/72502
Reviewed-by: Keir Mierle <keir@google.com>
Pigweed-Auto-Submit: Carlos Chinchilla <cachinchilla@google.com>
Commit-Queue: Auto-Submit <auto-submit@pigweed.google.com.iam.gserviceaccount.com>
diff --git a/pw_log/log.proto b/pw_log/log.proto
index 30e312d..56cfbea 100644
--- a/pw_log/log.proto
+++ b/pw_log/log.proto
@@ -160,14 +160,17 @@
   // optional bytes data = ?;
 }
 
-message LogRequest {
-  // This will include fields for configuring log filtering.
-}
+message LogRequest {}
 
 message LogEntries {
   repeated LogEntry entries = 1;
 }
 
+// RPC service for accessing logs.
+service Logs {
+  rpc Listen(LogRequest) returns (stream LogEntries);
+}
+
 message FilterRule {
   // Log level values match pw_log/levels.h. Enum names avoid collissions with
   // possible macros.
@@ -220,9 +223,8 @@
   repeated bytes filter_id = 1 [(tokenizer.format) = TOKENIZATION_OPTIONAL];
 }
 
-// RPC service for accessing logs.
-service Logs {
-  rpc Listen(LogRequest) returns (stream LogEntries);
+// RPC service for retrieving and modifying log filters.
+service Filters {
   rpc SetFilter(SetFilterRequest) returns (pw.protobuf.Empty);
   rpc GetFilter(GetFilterRequest) returns (Filter);
   rpc ListFilterIds(FilterIdListRequest) returns (FilterIdListResponse);
diff --git a/pw_log_rpc/BUILD.bazel b/pw_log_rpc/BUILD.bazel
index a293c2c..3db6e85 100644
--- a/pw_log_rpc/BUILD.bazel
+++ b/pw_log_rpc/BUILD.bazel
@@ -36,6 +36,19 @@
         "//pw_log",
         "//pw_log:log_pwpb",
         "//pw_log:protos.raw_rpc",
+    ],
+)
+
+pw_cc_library(
+    name = "log_filter_service",
+    srcs = ["log_filter_service.cc"],
+    hdrs = ["public/pw_log_rpc/log_filter_service.h"],
+    includes = ["public"],
+    deps = [
+        ":log_filter",
+        "//pw_log",
+        "//pw_log:log_pwpb",
+        "//pw_log:protos.raw_rpc",
         "//pw_protobuf",
         "//pw_protobuf:bytes_utils",
     ],
@@ -121,6 +134,21 @@
 )
 
 pw_cc_test(
+    name = "log_filter_service_test",
+    srcs = ["log_filter_service_test.cc"],
+    deps = [
+        ":log_filter",
+        ":log_filter_service",
+        "//pw_log:log_pwpb",
+        "//pw_protobuf",
+        "//pw_protobuf:bytes_utils",
+        "//pw_result",
+        "//pw_rpc/raw:test_method_context",
+        "//pw_unit_test",
+    ],
+)
+
+pw_cc_test(
     name = "log_filter_test",
     srcs = ["log_filter_test.cc"],
     deps = [
diff --git a/pw_log_rpc/BUILD.gn b/pw_log_rpc/BUILD.gn
index a30d4df..7582ddc 100644
--- a/pw_log_rpc/BUILD.gn
+++ b/pw_log_rpc/BUILD.gn
@@ -46,11 +46,23 @@
     ":log_config",
     "$dir_pw_log",
     "$dir_pw_log:protos.pwpb",
+  ]
+  public_deps = [
+    ":rpc_log_drain",
+    "$dir_pw_log:protos.raw_rpc",
+  ]
+}
+
+pw_source_set("log_filter_service") {
+  public_configs = [ ":public_include_path" ]
+  public = [ "public/pw_log_rpc/log_filter_service.h" ]
+  sources = [ "log_filter_service.cc" ]
+  deps = [
+    "$dir_pw_log:protos.pwpb",
     "$dir_pw_protobuf",
   ]
   public_deps = [
     ":log_filter",
-    ":rpc_log_drain",
     "$dir_pw_log:protos.raw_rpc",
     "$dir_pw_protobuf:bytes_utils",
   ]
@@ -133,6 +145,20 @@
   ]
 }
 
+pw_test("log_filter_service_test") {
+  sources = [ "log_filter_service_test.cc" ]
+  deps = [
+    ":log_filter",
+    ":log_filter_service",
+    "$dir_pw_log:protos.pwpb",
+    "$dir_pw_protobuf",
+    "$dir_pw_protobuf:bytes_utils",
+    "$dir_pw_result",
+    "$dir_pw_rpc/raw:test_method_context",
+    "$dir_pw_status",
+  ]
+}
+
 pw_test("log_filter_test") {
   sources = [ "log_filter_test.cc" ]
   deps = [
@@ -162,6 +188,7 @@
 pw_test_group("tests") {
   tests = [
     ":log_filter_test",
+    ":log_filter_service_test",
     ":log_service_test",
     ":rpc_log_drain_test",
   ]
diff --git a/pw_log_rpc/docs.rst b/pw_log_rpc/docs.rst
index d0dfd91..951b89b 100644
--- a/pw_log_rpc/docs.rst
+++ b/pw_log_rpc/docs.rst
@@ -9,6 +9,10 @@
 .. warning::
   This module is under construction and might change in the future.
 
+-----------
+RPC Logging
+-----------
+
 How to Use
 ==========
 1. Set up RPC
@@ -144,6 +148,35 @@
 Calling ``OpenUnrequestedLogStream()`` is a convenient way to set up a log
 stream that is started without the need to receive an RCP request for logs.
 
+-------------
+Log Filtering
+-------------
+A ``Filter`` anywhere in the path of a ``LogEntry`` proto, for example, in the
+``PW_LOG*`` macro implementation, or in an ``RpcLogDrain`` if using RPC logging.
+The log filtering service provides read and modify access to the ``Filter``\s
+registered in the ``FilterMap``.
+
+How to Use
+==========
+1. Set up RPC
+-------------
+Set up RPC for your target device. See :ref:`module-pw_rpc` for details.
+
+2. Create ``Filter``\s
+----------------------
+Provide each ``Filter`` with its own container for the ``FilterRules`` as big as
+the number of rules desired. These rules can be pre-poluated.
+
+3. Create a ``FilterMap`` and ``FilterService``
+-----------------------------------------------
+Set up the ``FilterMap`` with the filters than can be modified with the
+``FilterService``. Register the service with the RPC server.
+
+4. Use RPCs to retrieve and modify filter rules
+-----------------------------------------------
+
+Components Overview
+===================
 Filter::Rule
 ------------
 Contains a set of values that are compared against a log when set. All
@@ -163,24 +196,25 @@
 
 Filter
 ------
-``Filter`` encapsulates a collection of zero or more ``Filter::Rule``\s and has
+Encapsulates a collection of zero or more ``Filter::Rule``\s and has
 an ID used to modify or retrieve its contents.
 
 FilterMap
 ---------
 Provides a convenient way to retrieve register filters by ID.
 
-Logging example
-===============
+----------------------------
+Logging with filters example
+----------------------------
 The following code shows a sample setup to defer the log handling to the
 ``RpcLogDrainThread`` to avoid having the log streaming block at the log
 callsite.
 
 main.cc
--------
+=======
 .. code-block:: cpp
 
-  #include "foo/foo_log.h"
+  #include "foo/log.h"
   #include "pw_log/log.h"
   #include "pw_thread/detached_thread.h"
   #include "pw_thread_stl/options.h"
@@ -188,7 +222,8 @@
   namespace {
 
   void RegisterServices() {
-    pw::rpc::system_server::Server().RegisterService(foo_log::log_service);
+    pw::rpc::system_server::Server().RegisterService(foo::log::log_service);
+    pw::rpc::system_server::Server().RegisterService(foo::log::filter_service);
   }
   }  // namespace
 
@@ -196,25 +231,28 @@
     PW_LOG_INFO("Deferred logging over RPC example");
     pw::rpc::system_server::Init();
     RegisterServices();
-    pw::thread::DetachedThread(pw::thread::stl::Options(), foo_log::log_thread);
+    pw::thread::DetachedThread(pw::thread::stl::Options(), foo::log::log_thread);
     pw::rpc::system_server::Start();
     return 0;
   }
 
-foo_log.cc
-----------
+foo/log.cc
+==========
 Example of a log backend implementation, where logs enter the ``MultiSink`` and
-log drains are set up.
+log drains and filters are set up.
 
 .. code-block:: cpp
 
-  #include "foo/foo_log.h"
+  #include "foo/log.h"
 
   #include <array>
   #include <cstdint>
 
   #include "pw_chrono/system_clock.h"
   #include "pw_log/proto_utils.h"
+  #include "pw_log_rpc/log_filter.h"
+  #include "pw_log_rpc/log_filter_map.h"
+  #include "pw_log_rpc/log_filter_service.h"
   #include "pw_log_rpc/log_service.h"
   #include "pw_log_rpc/rpc_log_drain.h"
   #include "pw_log_rpc/rpc_log_drain_map.h"
@@ -225,7 +263,7 @@
   #include "pw_sync/mutex.h"
   #include "pw_tokenizer/tokenize_to_global_handler_with_payload.h"
 
-  namespace foo_log {
+  namespace foo::log {
   namespace {
   constexpr size_t kLogBufferSize = 5000;
   // Tokenized logs are typically 12-24 bytes.
@@ -259,6 +297,22 @@
   std::array<std::byte, kMaxLogEntrySize> log_encode_buffer
       PW_GUARDED_BY(log_encode_lock);
 
+  std::array<Filter::Rule, 2> logs_to_host_filter_rules;
+  std::array<Filter::Rule, 2> logs_to_server_filter_rules{{
+      {
+          .action = Filter::Rule::Action::kKeep,
+          .level_greater_than_or_equal = pw::log::FilterRule::Level::INFO_LEVEL,
+      },
+      {
+          .action = Filter::Rule::Action::kDrop,
+      },
+  }};
+  std::array<Filter, 2> filters{
+      Filter(std::as_bytes(std::span("HOST", 4)), logs_to_host_filter_rules),
+      Filter(std::as_bytes(std::span("WEB", 3)), logs_to_server_filter_rules),
+  };
+  pw::log_rpc::FilterMap filter_map(filters);
+
   extern "C" void pw_tokenizer_HandleEncodedMessageWithPayload(
       pw_tokenizer_Payload metadata, const uint8_t message[], size_t size_bytes) {
     int64_t timestamp =
@@ -279,15 +333,16 @@
   pw::log_rpc::RpcLogDrainMap drain_map(drains);
   pw::log_rpc::RpcLogDrainThread log_thread(GetMultiSink(), drain_map);
   pw::log_rpc::LogService log_service(drain_map);
+  pw::log_rpc::FilterService filter_service(filter_map);
 
   pw::multisink::MultiSink& GetMultiSink() {
     static pw::multisink::MultiSink multisink(multisink_buffer);
     return multisink;
   }
-  }  // namespace foo_log
+  }  // namespace foo::log
 
 Logging in other source files
 -----------------------------
 To defer logging, other source files must simply include ``pw_log/log.h`` and
 use the :ref:`module-pw_log` APIs, as long as the source set that includes
-``foo_log.cc`` is setup as the log backend.
+``foo/log.cc`` is setup as the log backend.
diff --git a/pw_log_rpc/log_filter_service.cc b/pw_log_rpc/log_filter_service.cc
new file mode 100644
index 0000000..b56f396
--- /dev/null
+++ b/pw_log_rpc/log_filter_service.cc
@@ -0,0 +1,86 @@
+// Copyright 2020 The Pigweed Authors
+//
+// 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
+//
+//     https://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.
+#include "pw_log_rpc/log_filter_service.h"
+
+#include "pw_log/log.h"
+#include "pw_log/proto/log.pwpb.h"
+#include "pw_log_rpc/log_filter.h"
+#include "pw_protobuf/decoder.h"
+
+namespace pw::log_rpc {
+StatusWithSize FilterService::SetFilter(ConstByteSpan request, ByteSpan) {
+  protobuf::Decoder decoder(request);
+  PW_TRY_WITH_SIZE(decoder.Next());
+  if (static_cast<log::SetFilterRequest::Fields>(decoder.FieldNumber()) !=
+      log::SetFilterRequest::Fields::FILTER_ID) {
+    return StatusWithSize::InvalidArgument();
+  }
+  ConstByteSpan filter_id;
+  PW_TRY_WITH_SIZE(decoder.ReadBytes(&filter_id));
+  Result<Filter*> filter = filter_map_.GetFilterFromId(filter_id);
+  if (!filter.ok()) {
+    return StatusWithSize::NotFound();
+  }
+
+  PW_TRY_WITH_SIZE(decoder.Next());
+  ConstByteSpan filter_buffer;
+  if (static_cast<log::SetFilterRequest::Fields>(decoder.FieldNumber()) !=
+      log::SetFilterRequest::Fields::FILTER) {
+    return StatusWithSize::InvalidArgument();
+  }
+  PW_TRY_WITH_SIZE(decoder.ReadBytes(&filter_buffer));
+  PW_TRY_WITH_SIZE(filter.value()->UpdateRulesFromProto(filter_buffer));
+  return StatusWithSize();
+}
+
+StatusWithSize FilterService::GetFilter(ConstByteSpan request,
+                                        ByteSpan response) {
+  protobuf::Decoder decoder(request);
+  PW_TRY_WITH_SIZE(decoder.Next());
+  if (static_cast<log::GetFilterRequest::Fields>(decoder.FieldNumber()) !=
+      log::GetFilterRequest::Fields::FILTER_ID) {
+    return StatusWithSize::InvalidArgument();
+  }
+  ConstByteSpan filter_id;
+  PW_TRY_WITH_SIZE(decoder.ReadBytes(&filter_id));
+  Result<Filter*> filter = filter_map_.GetFilterFromId(filter_id);
+  if (!filter.ok()) {
+    return StatusWithSize::NotFound();
+  }
+
+  log::Filter::MemoryEncoder encoder(response);
+  for (auto& rule : (*filter)->rules()) {
+    log::FilterRule::StreamEncoder rule_encoder = encoder.GetRuleEncoder();
+    rule_encoder.WriteLevelGreaterThanOrEqual(rule.level_greater_than_or_equal)
+        .IgnoreError();
+    rule_encoder.WriteModuleEquals(rule.module_equals).IgnoreError();
+    rule_encoder.WriteAnyFlagsSet(rule.any_flags_set).IgnoreError();
+    rule_encoder.WriteAction(static_cast<log::FilterRule::Action>(rule.action))
+        .IgnoreError();
+    PW_TRY_WITH_SIZE(rule_encoder.status());
+  }
+  PW_TRY_WITH_SIZE(encoder.status());
+
+  return StatusWithSize(encoder.size());
+}
+
+StatusWithSize FilterService::ListFilterIds(ConstByteSpan, ByteSpan response) {
+  log::FilterIdListResponse::MemoryEncoder encoder(response);
+  for (auto& filter : filter_map_.filters()) {
+    PW_TRY_WITH_SIZE(encoder.WriteFilterId(filter.id()));
+  }
+  return StatusWithSize(encoder.size());
+}
+
+}  // namespace pw::log_rpc
diff --git a/pw_log_rpc/log_filter_service_test.cc b/pw_log_rpc/log_filter_service_test.cc
new file mode 100644
index 0000000..232e18e
--- /dev/null
+++ b/pw_log_rpc/log_filter_service_test.cc
@@ -0,0 +1,378 @@
+// Copyright 2021 The Pigweed Authors
+//
+// 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
+//
+//     https://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.
+
+#include "pw_log_rpc/log_filter_service.h"
+
+#include <array>
+#include <cstdint>
+#include <limits>
+
+#include "gtest/gtest.h"
+#include "pw_bytes/endian.h"
+#include "pw_log/proto/log.pwpb.h"
+#include "pw_log_rpc/log_filter.h"
+#include "pw_log_rpc/log_filter_map.h"
+#include "pw_protobuf/bytes_utils.h"
+#include "pw_protobuf/decoder.h"
+#include "pw_result/result.h"
+#include "pw_rpc/channel.h"
+#include "pw_rpc/raw/test_method_context.h"
+
+namespace pw::log_rpc {
+namespace {
+
+class FilterServiceTest : public ::testing::Test {
+ public:
+  FilterServiceTest() : filter_map_(filters_) {}
+
+ protected:
+  FilterMap filter_map_;
+  static constexpr size_t kMaxFilterRules = 3;
+  std::array<Filter::Rule, kMaxFilterRules> rules1_;
+  std::array<Filter::Rule, kMaxFilterRules> rules2_;
+  std::array<Filter::Rule, kMaxFilterRules> rules3_;
+  static constexpr std::array<std::byte, cfg::kMaxFilterIdBytes> filter_id1_{
+      std::byte(65), std::byte(66), std::byte(67), std::byte(0)};
+  static constexpr std::array<std::byte, cfg::kMaxFilterIdBytes> filter_id2_{
+      std::byte(68), std::byte(69), std::byte(70), std::byte(0)};
+  static constexpr std::array<std::byte, cfg::kMaxFilterIdBytes> filter_id3_{
+      std::byte(71), std::byte(72), std::byte(73), std::byte(0)};
+  static constexpr size_t kMaxFilters = 3;
+  std::array<Filter, kMaxFilters> filters_ = {
+      Filter(filter_id1_, rules1_),
+      Filter(filter_id2_, rules2_),
+      Filter(filter_id3_, rules3_),
+  };
+};
+
+TEST_F(FilterServiceTest, GetFilterIds) {
+  PW_RAW_TEST_METHOD_CONTEXT(FilterService, ListFilterIds, 1, 128)
+  context(filter_map_);
+  context.call({});
+  ASSERT_TRUE(context.done());
+  ASSERT_EQ(context.responses().size(), 1u);
+  protobuf::Decoder decoder(context.responses()[0]);
+
+  for (const auto& filter : filter_map_.filters()) {
+    ASSERT_EQ(decoder.Next(), OkStatus());
+    ASSERT_EQ(decoder.FieldNumber(), 1u);  // filter_id
+    ConstByteSpan filter_id;
+    ASSERT_EQ(decoder.ReadBytes(&filter_id), OkStatus());
+    ASSERT_EQ(filter_id.size(), filter.id().size());
+    EXPECT_EQ(
+        std::memcmp(filter_id.data(), filter.id().data(), filter_id.size()), 0);
+  }
+  EXPECT_FALSE(decoder.Next().ok());
+
+  // No IDs reported when the filter map is empty.
+  FilterMap empty_filter_map({});
+  PW_RAW_TEST_METHOD_CONTEXT(FilterService, ListFilterIds, 1, 128)
+  no_filter_context(empty_filter_map);
+  no_filter_context.call({});
+  ASSERT_TRUE(no_filter_context.done());
+  ASSERT_EQ(no_filter_context.responses().size(), 1u);
+  protobuf::Decoder no_filter_decoder(no_filter_context.responses()[0]);
+  uint32_t filter_count = 0;
+  while (no_filter_decoder.Next().ok()) {
+    EXPECT_EQ(no_filter_decoder.FieldNumber(), 1u);  // filter_id
+    ++filter_count;
+  }
+  EXPECT_EQ(filter_count, 0u);
+}
+
+Status EncodeFilterRule(const Filter::Rule& rule,
+                        log::FilterRule::StreamEncoder& encoder) {
+  PW_TRY(
+      encoder.WriteLevelGreaterThanOrEqual(rule.level_greater_than_or_equal));
+  PW_TRY(encoder.WriteModuleEquals(rule.module_equals));
+  PW_TRY(encoder.WriteAnyFlagsSet(rule.any_flags_set));
+  return encoder.WriteAction(static_cast<log::FilterRule::Action>(rule.action));
+}
+
+Status EncodeFilter(const Filter& filter, log::Filter::StreamEncoder& encoder) {
+  for (auto& rule : filter.rules()) {
+    log::FilterRule::StreamEncoder rule_encoder = encoder.GetRuleEncoder();
+    PW_TRY(EncodeFilterRule(rule, rule_encoder));
+  }
+  return OkStatus();
+}
+
+Result<ConstByteSpan> EncodeFilterRequest(const Filter& filter,
+                                          ByteSpan buffer) {
+  stream::MemoryWriter writer(buffer);
+  std::byte encode_buffer[256];
+  protobuf::StreamEncoder encoder(writer, encode_buffer);
+  PW_TRY(encoder.WriteBytes(
+      static_cast<uint32_t>(log::SetFilterRequest::Fields::FILTER_ID),
+      filter.id()));
+  {
+    log::Filter::StreamEncoder filter_encoder = encoder.GetNestedEncoder(
+        static_cast<uint32_t>(log::SetFilterRequest::Fields::FILTER));
+    PW_TRY(EncodeFilter(filter, filter_encoder));
+  }  // Let the StreamEncoder destructor finalize the data.
+  return ConstByteSpan(writer.data(), writer.bytes_written());
+}
+
+void VerifyRule(const Filter::Rule& rule, const Filter::Rule& expected_rule) {
+  EXPECT_EQ(rule.level_greater_than_or_equal,
+            expected_rule.level_greater_than_or_equal);
+  EXPECT_EQ(rule.module_equals, expected_rule.module_equals);
+  EXPECT_EQ(rule.any_flags_set, expected_rule.any_flags_set);
+  EXPECT_EQ(rule.action, expected_rule.action);
+}
+
+TEST_F(FilterServiceTest, SetFilterRules) {
+  const std::array<Filter::Rule, 4> new_rules{{
+      {
+          .action = Filter::Rule::Action::kKeep,
+          .level_greater_than_or_equal = log::FilterRule::Level::DEBUG_LEVEL,
+          .any_flags_set = 0x0f,
+          .module_equals{std::byte(123)},
+      },
+      {
+          .action = Filter::Rule::Action::kInactive,
+          .level_greater_than_or_equal = log::FilterRule::Level::ANY_LEVEL,
+          .any_flags_set = 0xef,
+          .module_equals{},
+      },
+      {
+          .action = Filter::Rule::Action::kKeep,
+          .level_greater_than_or_equal = log::FilterRule::Level::INFO_LEVEL,
+          .any_flags_set = 0x1234,
+          .module_equals{std::byte(99)},
+      },
+      {
+          .action = Filter::Rule::Action::kDrop,
+          .level_greater_than_or_equal = log::FilterRule::Level::ANY_LEVEL,
+          .any_flags_set = 0,
+          .module_equals{std::byte(4)},
+      },
+  }};
+  const Filter new_filter(filters_[0].id(),
+                          const_cast<std::array<Filter::Rule, 4>&>(new_rules));
+
+  std::byte request_buffer[512];
+  const auto request = EncodeFilterRequest(new_filter, request_buffer);
+  ASSERT_EQ(request.status(), OkStatus());
+
+  PW_RAW_TEST_METHOD_CONTEXT(FilterService, SetFilter, 1, 128)
+  context(filter_map_);
+  context.call(request.value());
+
+  size_t i = 0;
+  for (const auto& rule : filters_[0].rules()) {
+    VerifyRule(rule, new_rules[i++]);
+  }
+}
+
+TEST_F(FilterServiceTest, SetFilterRulesWhenUsedByDrain) {
+  const std::array<Filter::Rule, 4> new_filter_rules{{
+      {
+          .action = Filter::Rule::Action::kKeep,
+          .level_greater_than_or_equal = log::FilterRule::Level::CRITICAL_LEVEL,
+          .any_flags_set = 0xfd,
+          .module_equals{std::byte(543)},
+      },
+      {
+          .action = Filter::Rule::Action::kInactive,
+          .level_greater_than_or_equal = log::FilterRule::Level::ANY_LEVEL,
+          .any_flags_set = 0xca,
+          .module_equals{},
+      },
+      {
+          .action = Filter::Rule::Action::kKeep,
+          .level_greater_than_or_equal = log::FilterRule::Level::INFO_LEVEL,
+          .any_flags_set = 0xabcd,
+          .module_equals{std::byte(9000)},
+      },
+      {
+          .action = Filter::Rule::Action::kDrop,
+          .level_greater_than_or_equal = log::FilterRule::Level::ANY_LEVEL,
+          .any_flags_set = 0,
+          .module_equals{std::byte(123)},
+      },
+  }};
+  Filter& filter = filters_[0];
+  const Filter new_filter(
+      filter.id(), const_cast<std::array<Filter::Rule, 4>&>(new_filter_rules));
+
+  std::byte request_buffer[256];
+  const auto request = EncodeFilterRequest(new_filter, request_buffer);
+  ASSERT_EQ(request.status(), OkStatus());
+
+  PW_RAW_TEST_METHOD_CONTEXT(FilterService, SetFilter, 1, 128)
+  context(filter_map_);
+  context.call(request.value());
+
+  size_t i = 0;
+  for (const auto& rule : filter.rules()) {
+    VerifyRule(rule, new_filter_rules[i++]);
+  }
+
+  // An empty request should not modify the filter.
+  PW_RAW_TEST_METHOD_CONTEXT(FilterService, SetFilter, 1, 128)
+  context_no_filter(filter_map_);
+  context_no_filter.call({});
+  i = 0;
+  for (const auto& rule : filter.rules()) {
+    VerifyRule(rule, new_filter_rules[i++]);
+  }
+
+  // A new request for logs with a new filter updates filter.
+  const std::array<Filter::Rule, 4> second_filter_rules{{
+      {
+          .action = Filter::Rule::Action::kKeep,
+          .level_greater_than_or_equal = log::FilterRule::Level::DEBUG_LEVEL,
+          .any_flags_set = 0xab,
+          .module_equals{},
+      },
+      {
+          .action = Filter::Rule::Action::kDrop,
+          .level_greater_than_or_equal = log::FilterRule::Level::ANY_LEVEL,
+          .any_flags_set = 0x11,
+          .module_equals{std::byte(34)},
+      },
+      {
+          .action = Filter::Rule::Action::kKeep,
+          .level_greater_than_or_equal = log::FilterRule::Level::ANY_LEVEL,
+          .any_flags_set = 0xef,
+          .module_equals{std::byte(23)},
+      },
+      {
+          .action = Filter::Rule::Action::kDrop,
+          .level_greater_than_or_equal = log::FilterRule::Level::ANY_LEVEL,
+          .any_flags_set = 0x0f,
+          .module_equals{},
+      },
+  }};
+  const Filter second_filter(
+      filter.id(),
+      const_cast<std::array<Filter::Rule, 4>&>(second_filter_rules));
+
+  std::memset(request_buffer, 0, sizeof(request_buffer));
+  const auto second_filter_request =
+      EncodeFilterRequest(second_filter, request_buffer);
+  ASSERT_EQ(second_filter_request.status(), OkStatus());
+  PW_RAW_TEST_METHOD_CONTEXT(FilterService, SetFilter, 1, 128)
+  context_new_filter(filter_map_);
+  context_new_filter.call(second_filter_request.value());
+
+  i = 0;
+  for (const auto& rule : filter.rules()) {
+    VerifyRule(rule, second_filter_rules[i++]);
+  }
+}
+
+void VerifyFilterRule(protobuf::Decoder& decoder,
+                      const Filter::Rule& expected_rule) {
+  ASSERT_TRUE(decoder.Next().ok());
+  ASSERT_EQ(decoder.FieldNumber(), 1u);  // level_greater_than_or_equal
+  log::FilterRule::Level level_greater_than_or_equal;
+  ASSERT_EQ(decoder.ReadUint32(
+                reinterpret_cast<uint32_t*>(&level_greater_than_or_equal)),
+            OkStatus());
+  EXPECT_EQ(level_greater_than_or_equal,
+            expected_rule.level_greater_than_or_equal);
+
+  ASSERT_TRUE(decoder.Next().ok());
+  ASSERT_EQ(decoder.FieldNumber(), 2u);  // module_equals
+  ConstByteSpan module_equals;
+  ASSERT_EQ(decoder.ReadBytes(&module_equals), OkStatus());
+  ASSERT_EQ(module_equals.size(), expected_rule.module_equals.size());
+  EXPECT_EQ(std::memcmp(module_equals.data(),
+                        expected_rule.module_equals.data(),
+                        module_equals.size()),
+            0);
+
+  ASSERT_TRUE(decoder.Next().ok());
+  ASSERT_EQ(decoder.FieldNumber(), 3u);  // any_flags_set
+  uint32_t any_flags_set;
+  ASSERT_EQ(decoder.ReadUint32(&any_flags_set), OkStatus());
+  EXPECT_EQ(any_flags_set, expected_rule.any_flags_set);
+
+  ASSERT_TRUE(decoder.Next().ok());
+  ASSERT_EQ(decoder.FieldNumber(), 4u);  // action
+  Filter::Rule::Action action;
+  ASSERT_EQ(decoder.ReadUint32(reinterpret_cast<uint32_t*>(&action)),
+            OkStatus());
+  EXPECT_EQ(action, expected_rule.action);
+}
+
+void VerifyFilterRules(protobuf::Decoder& decoder,
+                       std::span<const Filter::Rule> expected_rules) {
+  size_t rules_found = 0;
+  while (decoder.Next().ok()) {
+    ConstByteSpan rule;
+    EXPECT_TRUE(decoder.ReadBytes(&rule).ok());
+    protobuf::Decoder rule_decoder(rule);
+    if (rules_found >= expected_rules.size()) {
+      break;
+    }
+    VerifyFilterRule(rule_decoder, expected_rules[rules_found]);
+    ++rules_found;
+  }
+  EXPECT_EQ(rules_found, expected_rules.size());
+}
+
+TEST_F(FilterServiceTest, GetFilterRules) {
+  PW_RAW_TEST_METHOD_CONTEXT(FilterService, GetFilter, 1, 128)
+  context(filter_map_);
+
+  std::byte request_buffer[64];
+  log::GetFilterRequest::MemoryEncoder encoder(request_buffer);
+  encoder.WriteFilterId(filter_id1_);
+  const auto request = ConstByteSpan(encoder);
+  context.call(request);
+  ASSERT_TRUE(context.done());
+  ASSERT_EQ(context.responses().size(), 1u);
+
+  // Verify against empty rules.
+  protobuf::Decoder decoder(context.responses()[0]);
+  VerifyFilterRules(decoder, rules1_);
+
+  // Partially populate rules.
+  rules1_[0].action = Filter::Rule::Action::kKeep;
+  rules1_[0].level_greater_than_or_equal = log::FilterRule::Level::DEBUG_LEVEL;
+  rules1_[0].any_flags_set = 0xab;
+  const std::array<std::byte, 2> module1{std::byte(123), std::byte(0xab)};
+  rules1_[0].module_equals.assign(module1.begin(), module1.end());
+  rules1_[1].action = Filter::Rule::Action::kDrop;
+  rules1_[1].level_greater_than_or_equal = log::FilterRule::Level::ERROR_LEVEL;
+  rules1_[1].any_flags_set = 0;
+
+  PW_RAW_TEST_METHOD_CONTEXT(FilterService, GetFilter, 1, 128)
+  context2(filter_map_);
+  context2.call(request);
+  ASSERT_EQ(context2.responses().size(), 1u);
+  protobuf::Decoder decoder2(context2.responses()[0]);
+  VerifyFilterRules(decoder2, rules1_);
+
+  // Modify the rest of the filter rules.
+  rules1_[2].action = Filter::Rule::Action::kKeep;
+  rules1_[2].level_greater_than_or_equal = log::FilterRule::Level::FATAL_LEVEL;
+  rules1_[2].any_flags_set = 0xcd;
+  const std::array<std::byte, 2> module2{std::byte(1), std::byte(2)};
+  rules1_[2].module_equals.assign(module2.begin(), module2.end());
+  rules1_[3].action = Filter::Rule::Action::kInactive;
+
+  PW_RAW_TEST_METHOD_CONTEXT(FilterService, GetFilter, 1, 128)
+  context3(filter_map_);
+  context3.call(request);
+  ASSERT_EQ(context3.responses().size(), 1u);
+  protobuf::Decoder decoder3(context3.responses()[0]);
+  VerifyFilterRules(decoder3, rules1_);
+}
+
+}  // namespace
+}  // namespace pw::log_rpc
diff --git a/pw_log_rpc/log_service.cc b/pw_log_rpc/log_service.cc
index 731b7ce..c700af4 100644
--- a/pw_log_rpc/log_service.cc
+++ b/pw_log_rpc/log_service.cc
@@ -20,8 +20,6 @@
 
 #include "pw_log/log.h"
 #include "pw_log/proto/log.pwpb.h"
-#include "pw_log_rpc/log_filter.h"
-#include "pw_protobuf/decoder.h"
 
 namespace pw::log_rpc {
 
@@ -38,77 +36,4 @@
   }
 }
 
-StatusWithSize LogService::SetFilter(ConstByteSpan request, ByteSpan) {
-  if (filters_ == nullptr) {
-    return StatusWithSize::NotFound();
-  }
-
-  protobuf::Decoder decoder(request);
-  PW_TRY_WITH_SIZE(decoder.Next());
-  if (static_cast<log::SetFilterRequest::Fields>(decoder.FieldNumber()) !=
-      log::SetFilterRequest::Fields::FILTER_ID) {
-    return StatusWithSize::InvalidArgument();
-  }
-  ConstByteSpan filter_id;
-  PW_TRY_WITH_SIZE(decoder.ReadBytes(&filter_id));
-  Result<Filter*> filter = filters_->GetFilterFromId(filter_id);
-  if (!filter.ok()) {
-    return StatusWithSize::NotFound();
-  }
-
-  PW_TRY_WITH_SIZE(decoder.Next());
-  ConstByteSpan filter_buffer;
-  if (static_cast<log::SetFilterRequest::Fields>(decoder.FieldNumber()) !=
-      log::SetFilterRequest::Fields::FILTER) {
-    return StatusWithSize::InvalidArgument();
-  }
-  PW_TRY_WITH_SIZE(decoder.ReadBytes(&filter_buffer));
-  PW_TRY_WITH_SIZE(filter.value()->UpdateRulesFromProto(filter_buffer));
-  return StatusWithSize();
-}
-
-StatusWithSize LogService::GetFilter(ConstByteSpan request, ByteSpan response) {
-  if (filters_ == nullptr) {
-    return StatusWithSize::NotFound();
-  }
-  protobuf::Decoder decoder(request);
-  PW_TRY_WITH_SIZE(decoder.Next());
-  if (static_cast<log::GetFilterRequest::Fields>(decoder.FieldNumber()) !=
-      log::GetFilterRequest::Fields::FILTER_ID) {
-    return StatusWithSize::InvalidArgument();
-  }
-  ConstByteSpan filter_id;
-  PW_TRY_WITH_SIZE(decoder.ReadBytes(&filter_id));
-  Result<Filter*> filter = filters_->GetFilterFromId(filter_id);
-  if (!filter.ok()) {
-    return StatusWithSize::NotFound();
-  }
-
-  log::Filter::MemoryEncoder encoder(response);
-  for (auto& rule : (*filter)->rules()) {
-    log::FilterRule::StreamEncoder rule_encoder = encoder.GetRuleEncoder();
-    rule_encoder.WriteLevelGreaterThanOrEqual(rule.level_greater_than_or_equal)
-        .IgnoreError();
-    rule_encoder.WriteModuleEquals(rule.module_equals).IgnoreError();
-    rule_encoder.WriteAnyFlagsSet(rule.any_flags_set).IgnoreError();
-    rule_encoder.WriteAction(static_cast<log::FilterRule::Action>(rule.action))
-        .IgnoreError();
-    PW_TRY_WITH_SIZE(rule_encoder.status());
-  }
-  PW_TRY_WITH_SIZE(encoder.status());
-
-  return StatusWithSize(encoder.size());
-}
-
-StatusWithSize LogService::ListFilterIds(ConstByteSpan, ByteSpan response) {
-  if (filters_ == nullptr) {
-    return StatusWithSize::NotFound();
-  }
-  log::FilterIdListResponse::MemoryEncoder encoder(response);
-  for (auto& filter : filters_->filters()) {
-    PW_TRY_WITH_SIZE(encoder.WriteFilterId(filter.id()));
-  }
-  return StatusWithSize(encoder.size());
-}
-
 }  // namespace pw::log_rpc
diff --git a/pw_log_rpc/log_service_test.cc b/pw_log_rpc/log_service_test.cc
index 6ebd177..0f79f12 100644
--- a/pw_log_rpc/log_service_test.cc
+++ b/pw_log_rpc/log_service_test.cc
@@ -70,10 +70,7 @@
 // add to the multisink, and which drain to use.
 class LogServiceTest : public ::testing::Test {
  public:
-  LogServiceTest()
-      : multisink_(multisink_buffer_),
-        drain_map_(drains_),
-        filter_map_(filters_) {
+  LogServiceTest() : multisink_(multisink_buffer_), drain_map_(drains_) {
     for (auto& drain : drain_map_.drains()) {
       multisink_.AttachDrain(drain);
     }
@@ -106,8 +103,7 @@
   multisink::MultiSink multisink_;
   RpcLogDrainMap drain_map_;
   std::array<std::byte, kMaxLogEntrySize> entry_encode_buffer_;
-  FilterMap filter_map_;
-  static constexpr size_t kMaxFilterRules = 4;
+  static constexpr size_t kMaxFilterRules = 3;
   std::array<Filter::Rule, kMaxFilterRules> rules1_;
   std::array<Filter::Rule, kMaxFilterRules> rules2_;
   std::array<Filter::Rule, kMaxFilterRules> rules3_;
@@ -253,7 +249,7 @@
   // Create context directed to drain with ID 1.
   RpcLogDrain& active_drain = drains_[0];
   const uint32_t drain_channel_id = active_drain.channel_id();
-  LOG_SERVICE_METHOD_CONTEXT context(drain_map_, &filter_map_);
+  LOG_SERVICE_METHOD_CONTEXT context(drain_map_);
   context.set_channel_id(drain_channel_id);
 
   // Call RPC, which sets the drain's writer.
@@ -269,7 +265,7 @@
 
   // Calling an ongoing log stream must not change the active drain's
   // writer, and the second writer must not get any responses.
-  LOG_SERVICE_METHOD_CONTEXT second_call_context(drain_map_, &filter_map_);
+  LOG_SERVICE_METHOD_CONTEXT second_call_context(drain_map_);
   second_call_context.set_channel_id(drain_channel_id);
   second_call_context.call(rpc_request_buffer);
   EXPECT_EQ(active_drain.Flush(), OkStatus());
@@ -278,7 +274,7 @@
 
   // Setting a new writer on a closed stream is allowed.
   ASSERT_EQ(active_drain.Close(), OkStatus());
-  LOG_SERVICE_METHOD_CONTEXT third_call_context(drain_map_, &filter_map_);
+  LOG_SERVICE_METHOD_CONTEXT third_call_context(drain_map_);
   third_call_context.set_channel_id(drain_channel_id);
   third_call_context.call(rpc_request_buffer);
   EXPECT_EQ(active_drain.Flush(), OkStatus());
@@ -290,7 +286,7 @@
 TEST_F(LogServiceTest, StartAndEndStream) {
   RpcLogDrain& active_drain = drains_[2];
   const uint32_t drain_channel_id = active_drain.channel_id();
-  LOG_SERVICE_METHOD_CONTEXT context(drain_map_, &filter_map_);
+  LOG_SERVICE_METHOD_CONTEXT context(drain_map_);
   context.set_channel_id(drain_channel_id);
 
   // Add log entries.
@@ -328,7 +324,7 @@
 TEST_F(LogServiceTest, HandleDropped) {
   RpcLogDrain& active_drain = drains_[0];
   const uint32_t drain_channel_id = active_drain.channel_id();
-  LOG_SERVICE_METHOD_CONTEXT context(drain_map_, &filter_map_);
+  LOG_SERVICE_METHOD_CONTEXT context(drain_map_);
   context.set_channel_id(drain_channel_id);
 
   // Add log entries.
@@ -366,7 +362,7 @@
 }
 
 TEST_F(LogServiceTest, HandleSmallBuffer) {
-  LOG_SERVICE_METHOD_CONTEXT context(drain_map_, &filter_map_);
+  LOG_SERVICE_METHOD_CONTEXT context(drain_map_);
   context.set_channel_id(kSmallBufferDrainId);
   auto small_buffer_drain =
       drain_map_.GetDrainFromChannelId(kSmallBufferDrainId);
@@ -400,7 +396,7 @@
 TEST_F(LogServiceTest, FlushDrainWithoutMultisink) {
   auto& detached_drain = drains_[0];
   multisink_.DetachDrain(detached_drain);
-  LOG_SERVICE_METHOD_CONTEXT context(drain_map_, &filter_map_);
+  LOG_SERVICE_METHOD_CONTEXT context(drain_map_);
   context.set_channel_id(detached_drain.channel_id());
 
   // Add log entries.
@@ -445,7 +441,7 @@
   // Start log stream.
   RpcLogDrain& active_drain = drains_[0];
   const uint32_t drain_channel_id = active_drain.channel_id();
-  LOG_SERVICE_METHOD_CONTEXT context(drain_map_, &filter_map_);
+  LOG_SERVICE_METHOD_CONTEXT context(drain_map_);
   context.set_channel_id(drain_channel_id);
   context.call(rpc_request_buffer);
   ASSERT_EQ(active_drain.Flush(), OkStatus());
@@ -467,7 +463,7 @@
   auto drain = drain_map_.GetDrainFromChannelId(drain_channel_id);
   ASSERT_TRUE(drain.ok());
 
-  LogService log_service(drain_map_, &filter_map_);
+  LogService log_service(drain_map_);
   const size_t output_buffer_size = 128;
   const size_t max_packets = 10;
   rpc::RawFakeChannelOutput<10, output_buffer_size, 512> output;
@@ -555,7 +551,7 @@
   auto drain = drain_map_.GetDrainFromChannelId(drain_channel_id);
   ASSERT_TRUE(drain.ok());
 
-  LogService log_service(drain_map_, &filter_map_);
+  LogService log_service(drain_map_);
   const size_t output_buffer_size = 128;
   const size_t max_packets = 20;
   rpc::RawFakeChannelOutput<max_packets, output_buffer_size, 512> output;
@@ -629,229 +625,6 @@
   EXPECT_TRUE(output.done());
 }
 
-TEST_F(LogServiceTest, GetFilterIds) {
-  PW_RAW_TEST_METHOD_CONTEXT(LogService, ListFilterIds, 1, 128)
-  context(drain_map_, &filter_map_);
-  context.call({});
-  ASSERT_TRUE(context.done());
-  ASSERT_EQ(context.responses().size(), 1u);
-  protobuf::Decoder decoder(context.responses()[0]);
-
-  for (const auto& filter : filter_map_.filters()) {
-    ASSERT_EQ(decoder.Next(), OkStatus());
-    ASSERT_EQ(decoder.FieldNumber(), 1u);  // filter_id
-    ConstByteSpan filter_id;
-    ASSERT_EQ(decoder.ReadBytes(&filter_id), OkStatus());
-    ASSERT_EQ(filter_id.size(), filter.id().size());
-    EXPECT_EQ(
-        std::memcmp(filter_id.data(), filter.id().data(), filter_id.size()), 0);
-  }
-  EXPECT_FALSE(decoder.Next().ok());
-
-  // No IDs reported when none registered in the filter map.
-  PW_RAW_TEST_METHOD_CONTEXT(LogService, ListFilterIds, 1, 128)
-  no_filter_context(drain_map_, nullptr);
-  no_filter_context.call({});
-  ASSERT_TRUE(no_filter_context.done());
-  ASSERT_EQ(no_filter_context.responses().size(), 1u);
-  protobuf::Decoder no_filter_decoder(no_filter_context.responses()[0]);
-  uint32_t filter_count = 0;
-  while (no_filter_decoder.Next().ok()) {
-    EXPECT_EQ(no_filter_decoder.FieldNumber(), 1u);  // filter_id
-    ++filter_count;
-  }
-  EXPECT_EQ(filter_count, 0u);
-}
-
-Status EncodeFilterRule(const Filter::Rule& rule,
-                        log::FilterRule::StreamEncoder& encoder) {
-  PW_TRY(
-      encoder.WriteLevelGreaterThanOrEqual(rule.level_greater_than_or_equal));
-  PW_TRY(encoder.WriteModuleEquals(rule.module_equals));
-  PW_TRY(encoder.WriteAnyFlagsSet(rule.any_flags_set));
-  return encoder.WriteAction(static_cast<log::FilterRule::Action>(rule.action));
-}
-
-Status EncodeFilter(const Filter& filter, log::Filter::StreamEncoder& encoder) {
-  for (auto& rule : filter.rules()) {
-    log::FilterRule::StreamEncoder rule_encoder = encoder.GetRuleEncoder();
-    PW_TRY(EncodeFilterRule(rule, rule_encoder));
-  }
-  return OkStatus();
-}
-
-Result<ConstByteSpan> EncodeFilterRequest(const Filter& filter,
-                                          ByteSpan buffer) {
-  stream::MemoryWriter writer(buffer);
-  std::byte encode_buffer[256];
-  protobuf::StreamEncoder encoder(writer, encode_buffer);
-  PW_TRY(encoder.WriteBytes(
-      static_cast<uint32_t>(log::SetFilterRequest::Fields::FILTER_ID),
-      filter.id()));
-  {
-    log::Filter::StreamEncoder filter_encoder = encoder.GetNestedEncoder(
-        static_cast<uint32_t>(log::SetFilterRequest::Fields::FILTER));
-    PW_TRY(EncodeFilter(filter, filter_encoder));
-  }  // Let the StreamEncoder destructor finalize the data.
-  return ConstByteSpan(writer.data(), writer.bytes_written());
-}
-
-void VerifyRule(const Filter::Rule& rule, const Filter::Rule& expected_rule) {
-  EXPECT_EQ(rule.level_greater_than_or_equal,
-            expected_rule.level_greater_than_or_equal);
-  EXPECT_EQ(rule.module_equals, expected_rule.module_equals);
-  EXPECT_EQ(rule.any_flags_set, expected_rule.any_flags_set);
-  EXPECT_EQ(rule.action, expected_rule.action);
-}
-
-TEST_F(LogServiceTest, SetFilterRules) {
-  const std::array<Filter::Rule, 4> new_rules{{
-      {
-          .action = Filter::Rule::Action::kKeep,
-          .level_greater_than_or_equal = log::FilterRule::Level::DEBUG_LEVEL,
-          .any_flags_set = 0x0f,
-          .module_equals{std::byte(123)},
-      },
-      {
-          .action = Filter::Rule::Action::kInactive,
-          .level_greater_than_or_equal = log::FilterRule::Level::ANY_LEVEL,
-          .any_flags_set = 0xef,
-          .module_equals{},
-      },
-      {
-          .action = Filter::Rule::Action::kKeep,
-          .level_greater_than_or_equal = log::FilterRule::Level::INFO_LEVEL,
-          .any_flags_set = 0x1234,
-          .module_equals{std::byte(99)},
-      },
-      {
-          .action = Filter::Rule::Action::kDrop,
-          .level_greater_than_or_equal = log::FilterRule::Level::ANY_LEVEL,
-          .any_flags_set = 0,
-          .module_equals{std::byte(4)},
-      },
-  }};
-  const Filter new_filter(filters_[0].id(),
-                          const_cast<std::array<Filter::Rule, 4>&>(new_rules));
-
-  std::byte request_buffer[512];
-  const auto request = EncodeFilterRequest(new_filter, request_buffer);
-  ASSERT_EQ(request.status(), OkStatus());
-
-  PW_RAW_TEST_METHOD_CONTEXT(LogService, SetFilter, 1, 128)
-  context(drain_map_, &filter_map_);
-  context.call(request.value());
-
-  size_t i = 0;
-  for (const auto& rule : filters_[0].rules()) {
-    VerifyRule(rule, new_rules[i++]);
-  }
-}
-
-TEST_F(LogServiceTest, SetFilterRulesWhenUsedByDrain) {
-  const std::array<Filter::Rule, 4> new_filter_rules{{
-      {
-          .action = Filter::Rule::Action::kKeep,
-          .level_greater_than_or_equal = log::FilterRule::Level::CRITICAL_LEVEL,
-          .any_flags_set = 0xfd,
-          .module_equals{std::byte(543)},
-      },
-      {
-          .action = Filter::Rule::Action::kInactive,
-          .level_greater_than_or_equal = log::FilterRule::Level::ANY_LEVEL,
-          .any_flags_set = 0xca,
-          .module_equals{},
-      },
-      {
-          .action = Filter::Rule::Action::kKeep,
-          .level_greater_than_or_equal = log::FilterRule::Level::INFO_LEVEL,
-          .any_flags_set = 0xabcd,
-          .module_equals{std::byte(9000)},
-      },
-      {
-          .action = Filter::Rule::Action::kDrop,
-          .level_greater_than_or_equal = log::FilterRule::Level::ANY_LEVEL,
-          .any_flags_set = 0,
-          .module_equals{std::byte(123)},
-      },
-  }};
-  Filter& filter = filters_[0];
-  const Filter new_filter(
-      filter.id(), const_cast<std::array<Filter::Rule, 4>&>(new_filter_rules));
-
-  // Add callback to drain.
-  RpcLogDrain& drain = drains_[0];
-
-  std::byte request_buffer[256];
-  const auto request = EncodeFilterRequest(new_filter, request_buffer);
-  ASSERT_EQ(request.status(), OkStatus());
-
-  PW_RAW_TEST_METHOD_CONTEXT(LogService, SetFilter, 1, 128)
-  context(drain_map_, &filter_map_);
-  context.set_channel_id(drain.channel_id());
-  context.call(request.value());
-
-  size_t i = 0;
-  for (const auto& rule : filter.rules()) {
-    VerifyRule(rule, new_filter_rules[i++]);
-  }
-
-  // A request for logs without a filter should not modify the filter.
-  PW_RAW_TEST_METHOD_CONTEXT(LogService, SetFilter, 1, 128)
-  context_no_filter(drain_map_, &filter_map_);
-  context_no_filter.set_channel_id(drain.channel_id());
-  context_no_filter.call({});
-  i = 0;
-  for (const auto& rule : filter.rules()) {
-    VerifyRule(rule, new_filter_rules[i++]);
-  }
-
-  // A new request for logs with a new filter updates filter.
-  const std::array<Filter::Rule, 4> second_filter_rules{{
-      {
-          .action = Filter::Rule::Action::kKeep,
-          .level_greater_than_or_equal = log::FilterRule::Level::DEBUG_LEVEL,
-          .any_flags_set = 0xab,
-          .module_equals{},
-      },
-      {
-          .action = Filter::Rule::Action::kDrop,
-          .level_greater_than_or_equal = log::FilterRule::Level::ANY_LEVEL,
-          .any_flags_set = 0x11,
-          .module_equals{std::byte(34)},
-      },
-      {
-          .action = Filter::Rule::Action::kKeep,
-          .level_greater_than_or_equal = log::FilterRule::Level::ANY_LEVEL,
-          .any_flags_set = 0xef,
-          .module_equals{std::byte(23)},
-      },
-      {
-          .action = Filter::Rule::Action::kDrop,
-          .level_greater_than_or_equal = log::FilterRule::Level::ANY_LEVEL,
-          .any_flags_set = 0x0f,
-          .module_equals{},
-      },
-  }};
-  const Filter second_filter(
-      filter.id(),
-      const_cast<std::array<Filter::Rule, 4>&>(second_filter_rules));
-
-  std::memset(request_buffer, 0, sizeof(request_buffer));
-  const auto second_filter_request =
-      EncodeFilterRequest(second_filter, request_buffer);
-  ASSERT_EQ(second_filter_request.status(), OkStatus());
-  PW_RAW_TEST_METHOD_CONTEXT(LogService, SetFilter, 1, 128)
-  context_new_filter(drain_map_, &filter_map_);
-  context_new_filter.set_channel_id(drain.channel_id());
-  context_new_filter.call(second_filter_request.value());
-
-  i = 0;
-  for (const auto& rule : filter.rules()) {
-    VerifyRule(rule, second_filter_rules[i++]);
-  }
-}
-
 TEST_F(LogServiceTest, FilterLogs) {
   // Add a variety of logs.
   const uint32_t module = 0xcafe;
@@ -879,7 +652,7 @@
       AddLogEntry(kMessage, different_module_metadata, kSampleTimestamp).ok());
 
   // Add messages to the stack in the reverse order they are sent.
-  Vector<TestLogEntry, 6> message_stack;
+  Vector<TestLogEntry, 3> message_stack;
   message_stack.push_back(
       {.metadata = error_metadata,
        .timestamp = kSampleTimestamp,
@@ -893,41 +666,29 @@
        .timestamp = kSampleTimestamp,
        .tokenized_data = std::as_bytes(std::span(std::string_view(kMessage)))});
 
-  // Create request with filter.
+  // Set up filter rules for drain at drains_[1].
+  RpcLogDrain& drain = drains_[1];
+  for (auto& rule : rules2_) {
+    rule = {};
+  }
   const auto module_little_endian =
       bytes::CopyInOrder<uint32_t>(std::endian::little, module);
-  const std::array<Filter::Rule, 2> rules{{
-      {.action = Filter::Rule::Action::kKeep,
-       .level_greater_than_or_equal = log::FilterRule::Level::INFO_LEVEL,
-       .any_flags_set = flags,
-       .module_equals{module_little_endian.begin(),
-                      module_little_endian.end()}},
-      {
-          .action = Filter::Rule::Action::kDrop,
-          .level_greater_than_or_equal = log::FilterRule::Level::ANY_LEVEL,
-          .any_flags_set = 0,
-          .module_equals{},
-      },
-  }};
-
-  RpcLogDrain& drain = drains_[1];
-  Filter& filter = filters_[1];
-  const Filter new_filter(filter.id(),
-                          const_cast<std::array<Filter::Rule, 2>&>(rules));
-
-  // Set filter.
-  std::byte request_buffer[256];
-  const auto request = EncodeFilterRequest(new_filter, request_buffer);
-  ASSERT_EQ(request.status(), OkStatus());
-  PW_RAW_TEST_METHOD_CONTEXT(LogService, SetFilter, 1, 128)
-  set_filter_context(drain_map_, &filter_map_);
-  set_filter_context.set_channel_id(drain.channel_id());
-  set_filter_context.call(request.value());
+  rules2_[0] = {
+      .action = Filter::Rule::Action::kKeep,
+      .level_greater_than_or_equal = log::FilterRule::Level::INFO_LEVEL,
+      .any_flags_set = flags,
+      .module_equals{module_little_endian.begin(), module_little_endian.end()}};
+  rules2_[1] = {
+      .action = Filter::Rule::Action::kDrop,
+      .level_greater_than_or_equal = log::FilterRule::Level::ANY_LEVEL,
+      .any_flags_set = 0,
+      .module_equals{},
+  };
 
   // Request logs.
-  LOG_SERVICE_METHOD_CONTEXT context(drain_map_, &filter_map_);
+  LOG_SERVICE_METHOD_CONTEXT context(drain_map_);
   context.set_channel_id(drain.channel_id());
-  context.call(request.value());
+  context.call({});
   ASSERT_EQ(drain.Flush(), OkStatus());
 
   size_t entries_found = 0;
@@ -962,105 +723,5 @@
   EXPECT_EQ(drain.value()->Flush(), OkStatus());
 }
 
-void VerifyFilterRule(protobuf::Decoder& decoder,
-                      const Filter::Rule& expected_rule) {
-  ASSERT_TRUE(decoder.Next().ok());
-  ASSERT_EQ(decoder.FieldNumber(), 1u);  // level_greater_than_or_equal
-  log::FilterRule::Level level_greater_than_or_equal;
-  ASSERT_EQ(decoder.ReadUint32(
-                reinterpret_cast<uint32_t*>(&level_greater_than_or_equal)),
-            OkStatus());
-  EXPECT_EQ(level_greater_than_or_equal,
-            expected_rule.level_greater_than_or_equal);
-
-  ASSERT_TRUE(decoder.Next().ok());
-  ASSERT_EQ(decoder.FieldNumber(), 2u);  // module_equals
-  ConstByteSpan module_equals;
-  ASSERT_EQ(decoder.ReadBytes(&module_equals), OkStatus());
-  ASSERT_EQ(module_equals.size(), expected_rule.module_equals.size());
-  EXPECT_EQ(std::memcmp(module_equals.data(),
-                        expected_rule.module_equals.data(),
-                        module_equals.size()),
-            0);
-
-  ASSERT_TRUE(decoder.Next().ok());
-  ASSERT_EQ(decoder.FieldNumber(), 3u);  // any_flags_set
-  uint32_t any_flags_set;
-  ASSERT_EQ(decoder.ReadUint32(&any_flags_set), OkStatus());
-  EXPECT_EQ(any_flags_set, expected_rule.any_flags_set);
-
-  ASSERT_TRUE(decoder.Next().ok());
-  ASSERT_EQ(decoder.FieldNumber(), 4u);  // action
-  Filter::Rule::Action action;
-  ASSERT_EQ(decoder.ReadUint32(reinterpret_cast<uint32_t*>(&action)),
-            OkStatus());
-  EXPECT_EQ(action, expected_rule.action);
-}
-
-void VerifyFilterRules(protobuf::Decoder& decoder,
-                       std::span<const Filter::Rule> expected_rules) {
-  size_t rules_found = 0;
-  while (decoder.Next().ok()) {
-    ConstByteSpan rule;
-    EXPECT_TRUE(decoder.ReadBytes(&rule).ok());
-    protobuf::Decoder rule_decoder(rule);
-    if (rules_found >= expected_rules.size()) {
-      break;
-    }
-    VerifyFilterRule(rule_decoder, expected_rules[rules_found]);
-    ++rules_found;
-  }
-  EXPECT_EQ(rules_found, expected_rules.size());
-}
-
-TEST_F(LogServiceTest, GetFilterRules) {
-  PW_RAW_TEST_METHOD_CONTEXT(LogService, GetFilter, 1, 128)
-  context(drain_map_, &filter_map_);
-
-  std::byte request_buffer[64];
-  log::GetFilterRequest::MemoryEncoder encoder(request_buffer);
-  encoder.WriteFilterId(filter_id1_);
-  const auto request = ConstByteSpan(encoder);
-  context.call(request);
-  ASSERT_TRUE(context.done());
-  ASSERT_EQ(context.responses().size(), 1u);
-
-  // Verify against empty rules.
-  protobuf::Decoder decoder(context.responses()[0]);
-  VerifyFilterRules(decoder, rules1_);
-
-  // Partially populate rules.
-  rules1_[0].action = Filter::Rule::Action::kKeep;
-  rules1_[0].level_greater_than_or_equal = log::FilterRule::Level::DEBUG_LEVEL;
-  rules1_[0].any_flags_set = 0xab;
-  const std::array<std::byte, 2> module1{std::byte(123), std::byte(0xab)};
-  rules1_[0].module_equals.assign(module1.begin(), module1.end());
-  rules1_[1].action = Filter::Rule::Action::kDrop;
-  rules1_[1].level_greater_than_or_equal = log::FilterRule::Level::ERROR_LEVEL;
-  rules1_[1].any_flags_set = 0;
-
-  PW_RAW_TEST_METHOD_CONTEXT(LogService, GetFilter, 1, 128)
-  context2(drain_map_, &filter_map_);
-  context2.call(request);
-  ASSERT_EQ(context2.responses().size(), 1u);
-  protobuf::Decoder decoder2(context2.responses()[0]);
-  VerifyFilterRules(decoder2, rules1_);
-
-  // Modify the rest of the filter rules.
-  rules1_[2].action = Filter::Rule::Action::kKeep;
-  rules1_[2].level_greater_than_or_equal = log::FilterRule::Level::FATAL_LEVEL;
-  rules1_[2].any_flags_set = 0xcd;
-  const std::array<std::byte, 2> module2{std::byte(1), std::byte(2)};
-  rules1_[2].module_equals.assign(module2.begin(), module2.end());
-  rules1_[3].action = Filter::Rule::Action::kInactive;
-
-  PW_RAW_TEST_METHOD_CONTEXT(LogService, GetFilter, 1, 128)
-  context3(drain_map_, &filter_map_);
-  context3.call(request);
-  ASSERT_EQ(context3.responses().size(), 1u);
-  protobuf::Decoder decoder3(context3.responses()[0]);
-  VerifyFilterRules(decoder3, rules1_);
-}
-
 }  // namespace
 }  // namespace pw::log_rpc
diff --git a/pw_log_rpc/public/pw_log_rpc/log_filter_service.h b/pw_log_rpc/public/pw_log_rpc/log_filter_service.h
new file mode 100644
index 0000000..88b4fe2
--- /dev/null
+++ b/pw_log_rpc/public/pw_log_rpc/log_filter_service.h
@@ -0,0 +1,43 @@
+// Copyright 2021 The Pigweed Authors
+//
+// 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
+//
+//     https://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.
+
+#pragma once
+
+#include "pw_log/proto/log.raw_rpc.pb.h"
+#include "pw_log_rpc/log_filter_map.h"
+#include "pw_status/status_with_size.h"
+
+namespace pw::log_rpc {
+
+// Provides a way to retrieve and modify log filters.
+class FilterService final
+    : public log::pw_rpc::raw::Filters::Service<FilterService> {
+ public:
+  FilterService(FilterMap& filter_map) : filter_map_(filter_map) {}
+
+  //  Modifies a log filter and its rules. The filter must be registered in the
+  //  provided filter map.
+  StatusWithSize SetFilter(ConstByteSpan request, ByteSpan);
+
+  // Retrieves a log filter and its rules. The filter must be registered in the
+  // provided filter map.
+  StatusWithSize GetFilter(ConstByteSpan request, ByteSpan response);
+
+  StatusWithSize ListFilterIds(ConstByteSpan, ByteSpan response);
+
+ private:
+  FilterMap& filter_map_;
+};
+
+}  // namespace pw::log_rpc
diff --git a/pw_log_rpc/public/pw_log_rpc/log_service.h b/pw_log_rpc/public/pw_log_rpc/log_service.h
index 9e0d828c..d4e15dd 100644
--- a/pw_log_rpc/public/pw_log_rpc/log_service.h
+++ b/pw_log_rpc/public/pw_log_rpc/log_service.h
@@ -15,7 +15,6 @@
 #pragma once
 
 #include "pw_log/proto/log.raw_rpc.pb.h"
-#include "pw_log_rpc/log_filter_map.h"
 #include "pw_log_rpc/rpc_log_drain_map.h"
 #include "pw_status/status.h"
 
@@ -26,8 +25,7 @@
 // and delegated outside the service.
 class LogService final : public log::pw_rpc::raw::Logs::Service<LogService> {
  public:
-  LogService(RpcLogDrainMap& drains, FilterMap* filters = nullptr)
-      : drains_(drains), filters_(filters) {}
+  LogService(RpcLogDrainMap& drains) : drains_(drains) {}
 
   // Starts listening to logs on the given RPC channel and writer. The call is
   // ignored if the channel was not pre-registered in the drain map. If there is
@@ -36,20 +34,8 @@
   // stream using the previous writer continues.
   void Listen(ConstByteSpan, rpc::RawServerWriter& writer);
 
-  // TODO(pwbug/570): make log filter be its own service.
-  //  Modifies a log filter and its rules. The filter must be registered in the
-  //  provided filter map.
-  StatusWithSize SetFilter(ConstByteSpan request, ByteSpan);
-
-  // Retrieves a log filter and its rules. The filter must be registered in the
-  // provided filter map.
-  StatusWithSize GetFilter(ConstByteSpan request, ByteSpan response);
-
-  StatusWithSize ListFilterIds(ConstByteSpan, ByteSpan response);
-
  private:
   RpcLogDrainMap& drains_;
-  FilterMap* filters_;
 };
 
 }  // namespace pw::log_rpc
diff --git a/pw_log_rpc/rpc_log_drain_test.cc b/pw_log_rpc/rpc_log_drain_test.cc
index 3652950..7f9ad5f 100644
--- a/pw_log_rpc/rpc_log_drain_test.cc
+++ b/pw_log_rpc/rpc_log_drain_test.cc
@@ -106,7 +106,7 @@
                   nullptr),
   };
   RpcLogDrainMap drain_map(drains);
-  LogService log_service(drain_map, nullptr);
+  LogService log_service(drain_map);
 
   rpc::RawFakeChannelOutput<3, 128> output;
   rpc::Channel channel(rpc::Channel::Create<drain_id>(&output));
@@ -145,7 +145,7 @@
                   nullptr),
   };
   RpcLogDrainMap drain_map(drains);
-  LogService log_service(drain_map, nullptr);
+  LogService log_service(drain_map);
 
   rpc::RawFakeChannelOutput<1, 128> output;
   rpc::Channel channel(rpc::Channel::Create<drain_id>(&output));