util: Advanced filtering of the logging modules

refs: #3918

Change-Id: I766deb5ccd0d7f4472679d9955c8e55bad41f375
diff --git a/src/util/logger.cpp b/src/util/logger.cpp
index 8cb7d80..64cb5a2 100644
--- a/src/util/logger.cpp
+++ b/src/util/logger.cpp
@@ -1,5 +1,5 @@
 /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
-/**
+/*
  * Copyright (c) 2013-2017 Regents of the University of California.
  *
  * This file is part of ndn-cxx library (NDN C++ library with eXperimental eXtensions).
@@ -25,7 +25,7 @@
 #include "time.hpp"
 
 #include <cinttypes>
-#include <stdio.h>
+#include <cstring>
 
 namespace ndn {
 namespace util {
@@ -78,9 +78,33 @@
   BOOST_THROW_EXCEPTION(std::invalid_argument("unrecognized log level '" + s + "'"));
 }
 
+/**
+ * \brief checks if incoming logger name meets criteria
+ * \param name name of logger
+ */
+static bool
+isValidLoggerName(const std::string& name)
+{
+  // acceptable characters for Logger name
+  const char* okChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789~#%_<>.-";
+  if (std::strspn(name.c_str(), okChars) != name.size()) {
+    return false;
+  }
+  if (name.empty() || name.front() == '.' || name.back() == '.') {
+    return false;
+  }
+  if (name.find("..") != std::string::npos) {
+    return false;
+  }
+  return true;
+}
+
 Logger::Logger(const std::string& name)
   : m_moduleName(name)
 {
+  if (!isValidLoggerName(name)) {
+    BOOST_THROW_EXCEPTION(std::invalid_argument("Logger name " + name + " is invalid"));
+  }
   this->setLevel(LogLevel::NONE);
   Logging::addLogger(*this);
 }
diff --git a/src/util/logger.hpp b/src/util/logger.hpp
index e9efba9..548390b 100644
--- a/src/util/logger.hpp
+++ b/src/util/logger.hpp
@@ -1,6 +1,6 @@
 /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
-/**
- * Copyright (c) 2013-2016 Regents of the University of California.
+/*
+ * Copyright (c) 2013-2017 Regents of the University of California.
  *
  * This file is part of ndn-cxx library (NDN C++ library with eXperimental eXtensions).
  *
@@ -93,6 +93,10 @@
 };
 
 /** \brief declare a log module
+ *
+ *  \note Logger name is restricted to alphanumeric characters and a select set of
+ *  symbols: `~`, `#`, `%`, `_`, `<`, `>`, `.`, `-`
+ *  A logger name must not start or end with `.` or contain consecutive `.`
  */
 #define NDN_LOG_INIT(name) \
   namespace { \
diff --git a/src/util/logging.cpp b/src/util/logging.cpp
index 06948c1..21a573c 100644
--- a/src/util/logging.cpp
+++ b/src/util/logging.cpp
@@ -25,6 +25,7 @@
 #include <boost/log/expressions.hpp>
 #include <boost/range/adaptor/map.hpp>
 #include <boost/range/algorithm/copy.hpp>
+#include <boost/range/iterator_range.hpp>
 
 #include <cstdlib>
 #include <iostream>
@@ -65,18 +66,14 @@
   std::lock_guard<std::mutex> lock(m_mutex);
 
   const std::string& moduleName = logger.getModuleName();
-  m_loggers.insert({moduleName, &logger});
+  m_loggers.emplace(moduleName, &logger);
 
-  auto levelIt = m_enabledLevel.find(moduleName);
-  if (levelIt == m_enabledLevel.end()) {
-    levelIt = m_enabledLevel.find("*");
-  }
-  LogLevel level = levelIt == m_enabledLevel.end() ? INITIAL_DEFAULT_LEVEL : levelIt->second;
+  LogLevel level = findLevel(moduleName);
   logger.setLevel(level);
 }
 
 std::set<std::string>
-Logging::getLoggerNamesImpl()
+Logging::getLoggerNamesImpl() const
 {
   std::lock_guard<std::mutex> lock(m_mutex);
 
@@ -85,6 +82,42 @@
   return loggerNames;
 }
 
+LogLevel
+Logging::findLevel(const std::string& moduleName) const
+{
+  std::string mn = moduleName;
+  while (!mn.empty()) {
+    auto it = m_enabledLevel.find(mn);
+    if (it != m_enabledLevel.end()) {
+      return it->second;
+    }
+    size_t pos = mn.find_last_of('.');
+    if (pos < mn.size() - 1) {
+      mn = mn.substr(0, pos + 1);
+    }
+    else if (pos == mn.size() - 1) {
+      mn.pop_back();
+      pos = mn.find_last_of('.');
+      if (pos != std::string::npos) {
+        mn = mn.substr(0, pos + 1);
+      }
+      else {
+        mn = "";
+      }
+    }
+    else {
+      mn = "";
+    }
+  }
+  auto it = m_enabledLevel.find(mn);
+  if (it != m_enabledLevel.end()) {
+    return it->second;
+  }
+  else {
+    return INITIAL_DEFAULT_LEVEL;
+  }
+}
+
 #ifdef NDN_CXX_HAVE_TESTS
 bool
 Logging::removeLogger(Logger& logger)
@@ -102,30 +135,38 @@
 #endif // NDN_CXX_HAVE_TESTS
 
 void
-Logging::setLevelImpl(const std::string& moduleName, LogLevel level)
+Logging::setLevelImpl(const std::string& prefix, LogLevel level)
 {
   std::lock_guard<std::mutex> lock(m_mutex);
 
-  if (moduleName == "*") {
-    this->setDefaultLevel(level);
-    return;
+  if (prefix.empty() || prefix.back() == '*') {
+    std::string p = prefix;
+    if (!p.empty()) {
+      p.pop_back();
+    }
+
+    for (auto i = m_enabledLevel.begin(); i != m_enabledLevel.end();) {
+      if (i->first.compare(0, p.size(), p) == 0) {
+        i = m_enabledLevel.erase(i);
+      }
+      else {
+        ++i;
+      }
+    }
+    m_enabledLevel[p] = level;
+
+    for (auto&& it : m_loggers) {
+      if (it.first.compare(0, p.size(), p) == 0) {
+        it.second->setLevel(level);
+      }
+    }
   }
-
-  m_enabledLevel[moduleName] = level;
-  auto range = m_loggers.equal_range(moduleName);
-  for (auto i = range.first; i != range.second; ++i) {
-    i->second->setLevel(level);
-  }
-}
-
-void
-Logging::setDefaultLevel(LogLevel level)
-{
-  m_enabledLevel.clear();
-  m_enabledLevel["*"] = level;
-
-  for (auto i = m_loggers.begin(); i != m_loggers.end(); ++i) {
-    i->second->setLevel(level);
+  else {
+    m_enabledLevel[prefix] = level;
+    auto range = boost::make_iterator_range(m_loggers.equal_range(prefix));
+    for (auto&& it : range) {
+      it.second->setLevel(level);
+    }
   }
 }
 
@@ -141,43 +182,16 @@
     }
 
     std::string moduleName = configModule.substr(0, ind);
-    LogLevel level = parseLogLevel(configModule.substr(ind+1));
-
+    LogLevel level = parseLogLevel(configModule.substr(ind + 1));
     this->setLevelImpl(moduleName, level);
   }
 }
 
 #ifdef NDN_CXX_HAVE_TESTS
-std::string
-Logging::getLevels() const
-{
-  std::ostringstream os;
-
-  auto defaultLevelIt = m_enabledLevel.find("*");
-  if (defaultLevelIt != m_enabledLevel.end()) {
-    os << "*=" << defaultLevelIt->second << ':';
-  }
-
-  for (auto it = m_enabledLevel.begin(); it != m_enabledLevel.end(); ++it) {
-    if (it->first == "*") {
-      continue;
-    }
-    os << it->first << '=' << it->second << ':';
-  }
-
-  std::string s = os.str();
-  if (!s.empty()) {
-    s.pop_back(); // delete last ':'
-  }
-  return s;
-}
-#endif // NDN_CXX_HAVE_TESTS
-
-#ifdef NDN_CXX_HAVE_TESTS
 void
 Logging::resetLevels()
 {
-  this->setDefaultLevel(INITIAL_DEFAULT_LEVEL);
+  this->setLevelImpl("*", INITIAL_DEFAULT_LEVEL);
   m_enabledLevel.clear();
 }
 #endif // NDN_CXX_HAVE_TESTS
@@ -193,11 +207,11 @@
 {
   std::lock_guard<std::mutex> lock(m_mutex);
 
-  m_destination = os;
+  m_destination = std::move(os);
 
   auto backend = boost::make_shared<boost::log::sinks::text_ostream_backend>();
   backend->auto_flush(true);
-  backend->add_stream(boost::shared_ptr<std::ostream>(os.get(), bind([]{})));
+  backend->add_stream(boost::shared_ptr<std::ostream>(m_destination.get(), bind([]{})));
 
   if (m_sink != nullptr) {
     boost::log::core::get()->remove_sink(m_sink);
@@ -212,10 +226,25 @@
 
 #ifdef NDN_CXX_HAVE_TESTS
 shared_ptr<std::ostream>
-Logging::getDestination()
+Logging::getDestination() const
 {
   return m_destination;
 }
+
+void
+Logging::setLevelImpl(const std::unordered_map<std::string, LogLevel>& prefixRules)
+{
+  resetLevels();
+  for (const auto& rule : prefixRules) {
+    setLevelImpl(rule.first, rule.second);
+  }
+}
+
+const std::unordered_map<std::string, LogLevel>&
+Logging::getLevels() const
+{
+  return m_enabledLevel;
+}
 #endif // NDN_CXX_HAVE_TESTS
 
 void
diff --git a/src/util/logging.hpp b/src/util/logging.hpp
index 459cf2b..d85b524 100644
--- a/src/util/logging.hpp
+++ b/src/util/logging.hpp
@@ -1,5 +1,5 @@
 /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
-/**
+/*
  * Copyright (c) 2013-2017 Regents of the University of California.
  *
  * This file is part of ndn-cxx library (NDN C++ library with eXperimental eXtensions).
@@ -58,16 +58,15 @@
   getLoggerNames();
 
   /** \brief set severity level
-   *  \param moduleName logger name, or "*" for default level
+   *  \param prefix logger prefix; this can be a specific logger name, a general prefix like ndn.a.*
+   *   to apply a setting for all modules that contain this prefix, or "*" for all modules
    *  \param level minimum severity level
    *
    *  Log messages are output only if its severity is greater than the set minimum severity level.
    *  Initial default severity level is \p LogLevel::NONE which enables FATAL only.
-   *
-   *  Changing the default level overwrites individual settings.
    */
   static void
-  setLevel(const std::string& moduleName, LogLevel level);
+  setLevel(const std::string& prefix, LogLevel level);
 
   /** \brief set severity levels with a config string
    *  \param config colon-separate key=value pairs
@@ -117,13 +116,25 @@
   addLoggerImpl(Logger& logger);
 
   std::set<std::string>
-  getLoggerNamesImpl();
+  getLoggerNamesImpl() const;
+
+  /**
+   * \brief finds the appropriate LogLevel for a logger
+   * \param moduleName name of logger
+   *
+   * This searches m_enabledLevel map to determine which LogLevel is appropriate for
+   * the incoming logger. It looks for the most specific prefix and broadens its
+   * prefix scope if a setting is not found. For example, when an incoming logger
+   * name is "ndn.a.b", it will search for "ndn.a.b" first. If this prefix is not
+   * contained in m_enabledLevel, it will search for "ndn.a.*", then "ndn.*", and
+   * finally "*". It defaults to INITIAL_DEFAULT_LEVEL if a matching prefix is not
+   * found.
+   */
+  LogLevel
+  findLevel(const std::string& moduleName) const;
 
   void
-  setLevelImpl(const std::string& moduleName, LogLevel level);
-
-  void
-  setDefaultLevel(LogLevel level);
+  setLevelImpl(const std::string& prefix, LogLevel level);
 
   void
   setLevelImpl(const std::string& config);
@@ -142,19 +153,22 @@
   bool
   removeLogger(Logger& logger);
 
-  std::string
-  getLevels() const;
-
   void
   resetLevels();
 
   shared_ptr<std::ostream>
-  getDestination();
+  getDestination() const;
+
+  void
+  setLevelImpl(const std::unordered_map<std::string, LogLevel>& prefixRules);
+
+  const std::unordered_map<std::string, LogLevel>&
+  getLevels() const;
 #endif // NDN_CXX_HAVE_TESTS
 
 private:
-  std::mutex m_mutex;
-  std::unordered_map<std::string, LogLevel> m_enabledLevel; ///< moduleName => minimum level
+  mutable std::mutex m_mutex;
+  std::unordered_map<std::string, LogLevel> m_enabledLevel; ///< module prefix => minimum level
   std::unordered_multimap<std::string, Logger*> m_loggers; ///< moduleName => logger
 
   shared_ptr<std::ostream> m_destination;
@@ -175,9 +189,9 @@
 }
 
 inline void
-Logging::setLevel(const std::string& moduleName, LogLevel level)
+Logging::setLevel(const std::string& prefix, LogLevel level)
 {
-  get().setLevelImpl(moduleName, level);
+  get().setLevelImpl(prefix, level);
 }
 
 inline void
@@ -189,7 +203,7 @@
 inline void
 Logging::setDestination(shared_ptr<std::ostream> os)
 {
-  get().setDestinationImpl(os);
+  get().setDestinationImpl(std::move(os));
 }
 
 inline void
@@ -198,7 +212,6 @@
   get().flushImpl();
 }
 
-
 } // namespace util
 } // namespace ndn
 
diff --git a/tests/unit-tests/util/log-filter-module.cpp b/tests/unit-tests/util/log-filter-module.cpp
new file mode 100644
index 0000000..4086b84
--- /dev/null
+++ b/tests/unit-tests/util/log-filter-module.cpp
@@ -0,0 +1,44 @@
+/* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
+/*
+ * Copyright (c) 2013-2017 Regents of the University of California.
+ *
+ * This file is part of ndn-cxx library (NDN C++ library with eXperimental eXtensions).
+ *
+ * ndn-cxx library is free software: you can redistribute it and/or modify it under the
+ * terms of the GNU Lesser General Public License as published by the Free Software
+ * Foundation, either version 3 of the License, or (at your option) any later version.
+ *
+ * ndn-cxx library is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+ * PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more details.
+ *
+ * You should have received copies of the GNU General Public License and GNU Lesser
+ * General Public License along with ndn-cxx, e.g., in COPYING.md file.  If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * See AUTHORS.md for complete list of ndn-cxx authors and contributors.
+ */
+
+#include "util/logger.hpp"
+
+NDN_LOG_INIT(fm.FilterModule);
+
+namespace ndn {
+namespace util {
+namespace tests {
+
+void
+logFromFilterModule()
+{
+  NDN_LOG_TRACE("traceFM");
+  NDN_LOG_DEBUG("debugFM");
+  NDN_LOG_INFO("infoFM");
+  NDN_LOG_WARN("warnFM");
+  NDN_LOG_ERROR("errorFM");
+  NDN_LOG_FATAL("fatalFM");
+}
+
+} // namespace tests
+} // namespace util
+} // namespace ndn
+
diff --git a/tests/unit-tests/util/logger.t.cpp b/tests/unit-tests/util/logger.t.cpp
new file mode 100644
index 0000000..1443178
--- /dev/null
+++ b/tests/unit-tests/util/logger.t.cpp
@@ -0,0 +1,82 @@
+/* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
+/*
+ * Copyright (c) 2013-2017 Regents of the University of California.
+ *
+ * This file is part of ndn-cxx library (NDN C++ library with eXperimental eXtensions).
+ *
+ * ndn-cxx library is free software: you can redistribute it and/or modify it under the
+ * terms of the GNU Lesser General Public License as published by the Free Software
+ * Foundation, either version 3 of the License, or (at your option) any later version.
+ *
+ * ndn-cxx library is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+ * PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more details.
+ *
+ * You should have received copies of the GNU General Public License and GNU Lesser
+ * General Public License along with ndn-cxx, e.g., in COPYING.md file.  If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * See AUTHORS.md for complete list of ndn-cxx authors and contributors.
+ */
+
+#include "util/logger.hpp"
+#include "util/logging.hpp"
+
+#include "boost-test.hpp"
+
+namespace ndn {
+namespace util {
+namespace tests {
+
+BOOST_AUTO_TEST_SUITE(Util)
+BOOST_AUTO_TEST_SUITE(TestLogger)
+
+BOOST_AUTO_TEST_CASE(LegalAlphanumeric)
+{
+  Logger logger("ndnTest123");
+  auto mNames = Logging::getLoggerNames();
+  BOOST_CHECK_EQUAL(mNames.count("ndnTest123"), 1);
+  BOOST_CHECK(logger.isLevelEnabled(LogLevel::NONE));
+  Logging::get().removeLogger(logger);
+}
+
+BOOST_AUTO_TEST_CASE(AllLegalSymbols)
+{
+  Logger logger("ndn.~#%.test_<check>1-2-3");
+  auto mNames = Logging::getLoggerNames();
+  BOOST_CHECK_EQUAL(mNames.count("ndn.~#%.test_<check>1-2-3"), 1);
+  BOOST_CHECK(logger.isLevelEnabled(LogLevel::NONE));
+  Logging::get().removeLogger(logger);
+}
+
+BOOST_AUTO_TEST_CASE(EmptyLogger)
+{
+  BOOST_CHECK_THROW(Logger logger(""), std::invalid_argument);
+}
+
+BOOST_AUTO_TEST_CASE(InvalidSymbol)
+{
+  BOOST_CHECK_THROW(Logger logger("ndn.test.*"), std::invalid_argument);
+}
+
+BOOST_AUTO_TEST_CASE(StartsWithPeriod)
+{
+  BOOST_CHECK_THROW(Logger logger(".ndn.test"), std::invalid_argument);
+}
+
+BOOST_AUTO_TEST_CASE(EndsWithPeriod)
+{
+  BOOST_CHECK_THROW(Logger logger("ndn.test."), std::invalid_argument);
+}
+
+BOOST_AUTO_TEST_CASE(ConsecutivePeriods)
+{
+  BOOST_CHECK_THROW(Logger logger("ndn..test"), std::invalid_argument);
+}
+
+BOOST_AUTO_TEST_SUITE_END() // TestLogger
+BOOST_AUTO_TEST_SUITE_END() // Util
+
+} // namespace tests
+} // namespace util
+} // namespace ndn
diff --git a/tests/unit-tests/util/logging.t.cpp b/tests/unit-tests/util/logging.t.cpp
index 3447968..cced73b 100644
--- a/tests/unit-tests/util/logging.t.cpp
+++ b/tests/unit-tests/util/logging.t.cpp
@@ -1,5 +1,5 @@
 /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
-/**
+/*
  * Copyright (c) 2013-2017 Regents of the University of California.
  *
  * This file is part of ndn-cxx library (NDN C++ library with eXperimental eXtensions).
@@ -21,10 +21,10 @@
 
 #include "util/logging.hpp"
 #include "util/logger.hpp"
+#include "../unit-test-time-fixture.hpp"
 
 #include "boost-test.hpp"
 #include <boost/test/output_test_stream.hpp>
-#include "../unit-test-time-fixture.hpp"
 
 namespace ndn {
 namespace util {
@@ -42,9 +42,11 @@
 logFromModule2();
 
 void
+logFromFilterModule();
+
+static void
 logFromNewLogger(const std::string& moduleName)
 {
-  // clang complains -Wreturn-stack-address on OSX-10.9 if Logger is allocated on stack
   auto loggerPtr = make_unique<Logger>(moduleName);
   Logger& logger = *loggerPtr;
 
@@ -67,7 +69,7 @@
 protected:
   explicit
   LoggingFixture()
-    : m_oldLevels(Logging::get().getLevels())
+    : m_oldEnabledLevel(Logging::get().getLevels())
     , m_oldDestination(Logging::get().getDestination())
   {
     this->systemClock->setNow(LOG_SYSTIME);
@@ -77,7 +79,7 @@
 
   ~LoggingFixture()
   {
-    Logging::setLevel(m_oldLevels);
+    Logging::get().setLevelImpl(m_oldEnabledLevel);
     Logging::setDestination(m_oldDestination);
   }
 
@@ -85,7 +87,7 @@
   output_test_stream os;
 
 private:
-  std::string m_oldLevels;
+  std::unordered_map<std::string, LogLevel> m_oldEnabledLevel;
   shared_ptr<std::ostream> m_oldDestination;
 };
 
@@ -291,7 +293,8 @@
 BOOST_AUTO_TEST_CASE(SetEmpty)
 {
   Logging::setLevel("");
-  BOOST_CHECK_EQUAL(Logging::get().getLevels(), "");
+  const auto& prefixMap = Logging::get().getLevels();
+  BOOST_CHECK_EQUAL(prefixMap.size(), 0);
   logFromModule1();
   logFromModule2();
 
@@ -305,7 +308,10 @@
 BOOST_AUTO_TEST_CASE(SetDefault)
 {
   Logging::setLevel("*=WARN");
-  BOOST_CHECK_EQUAL(Logging::get().getLevels(), "*=WARN");
+  const auto& prefixMap = Logging::get().getLevels();
+  // "*" is treated as "" internally
+  BOOST_CHECK_EQUAL(prefixMap.size(), 1);
+  BOOST_CHECK_EQUAL(prefixMap.at(""), LogLevel::WARN);
   logFromModule1();
   logFromModule2();
 
@@ -323,7 +329,9 @@
 BOOST_AUTO_TEST_CASE(SetModule)
 {
   Logging::setLevel("Module1=ERROR");
-  BOOST_CHECK_EQUAL(Logging::get().getLevels(), "Module1=ERROR");
+  const auto& prefixMap = Logging::get().getLevels();
+  BOOST_CHECK_EQUAL(prefixMap.size(), 1);
+  BOOST_CHECK_EQUAL(prefixMap.at("Module1"), LogLevel::ERROR);
   logFromModule1();
   logFromModule2();
 
@@ -338,7 +346,11 @@
 BOOST_AUTO_TEST_CASE(SetOverride)
 {
   Logging::setLevel("*=WARN:Module2=DEBUG");
-  BOOST_CHECK_EQUAL(Logging::get().getLevels(), "*=WARN:Module2=DEBUG");
+  const auto& prefixMap = Logging::get().getLevels();
+  BOOST_CHECK_EQUAL(prefixMap.size(), 2);
+  // "*" is treated as "" internally
+  BOOST_CHECK_EQUAL(prefixMap.at(""), LogLevel::WARN);
+  BOOST_CHECK_EQUAL(prefixMap.at("Module2"), LogLevel::DEBUG);
   logFromModule1();
   logFromModule2();
 
@@ -359,7 +371,11 @@
 {
   Logging::setLevel("*=WARN");
   Logging::setLevel("Module2=DEBUG");
-  BOOST_CHECK_EQUAL(Logging::get().getLevels(), "*=WARN:Module2=DEBUG");
+  const auto& prefixMap = Logging::get().getLevels();
+  BOOST_CHECK_EQUAL(prefixMap.size(), 2);
+  // "*" is treated as "" internally
+  BOOST_CHECK_EQUAL(prefixMap.at(""), LogLevel::WARN);
+  BOOST_CHECK_EQUAL(prefixMap.at("Module2"), LogLevel::DEBUG);
   logFromModule1();
   logFromModule2();
 
@@ -380,7 +396,10 @@
 {
   Logging::setLevel("Module2=DEBUG");
   Logging::setLevel("*=ERROR");
-  BOOST_CHECK_EQUAL(Logging::get().getLevels(), "*=ERROR");
+  const auto& prefixMap = Logging::get().getLevels();
+  BOOST_CHECK_EQUAL(prefixMap.size(), 1);
+  // "*" is treated as "" internally
+  BOOST_CHECK_EQUAL(prefixMap.at(""), LogLevel::ERROR);
   logFromModule1();
   logFromModule2();
 
@@ -399,6 +418,74 @@
   BOOST_CHECK_THROW(Logging::setLevel("Module1-MISSING-EQUAL-SIGN"), std::invalid_argument);
 }
 
+BOOST_AUTO_TEST_CASE(SetFilter)
+{
+  Logging::setLevel("*=FATAL:fm.*=DEBUG");
+  const auto& prefixMap = Logging::get().getLevels();
+  BOOST_CHECK_EQUAL(prefixMap.size(), 2);
+  // "*" is treated as "" internally
+  BOOST_CHECK_EQUAL(prefixMap.at(""), LogLevel::FATAL);
+  // "name.*" is treated as "name." internally
+  BOOST_CHECK_EQUAL(prefixMap.at("fm."), LogLevel::DEBUG);
+  logFromModule1();
+  logFromFilterModule();
+
+  Logging::flush();
+  BOOST_CHECK(os.is_equal(
+    LOG_SYSTIME_STR + " FATAL: [Module1] fatal1\n" +
+    LOG_SYSTIME_STR + " DEBUG: [fm.FilterModule] debugFM\n" +
+    LOG_SYSTIME_STR + " INFO: [fm.FilterModule] infoFM\n" +
+    LOG_SYSTIME_STR + " WARNING: [fm.FilterModule] warnFM\n" +
+    LOG_SYSTIME_STR + " ERROR: [fm.FilterModule] errorFM\n" +
+    LOG_SYSTIME_STR + " FATAL: [fm.FilterModule] fatalFM\n"
+    ));
+}
+
+BOOST_AUTO_TEST_CASE(SetOverrideFilter)
+{
+  Logging::setLevel("*=FATAL:fm.FilterModule=DEBUG");
+  Logging::setLevel("fm.*", LogLevel::INFO);
+  const auto& prefixMap = Logging::get().getLevels();
+  BOOST_CHECK_EQUAL(prefixMap.size(), 2);
+  // "*" is treated as "" internally
+  BOOST_CHECK_EQUAL(prefixMap.at(""), LogLevel::FATAL);
+  // "name.*" is treated as "name." internally
+  BOOST_CHECK_EQUAL(prefixMap.at("fm."), LogLevel::INFO);
+  logFromModule1();
+  logFromFilterModule();
+
+  Logging::flush();
+  BOOST_CHECK(os.is_equal(
+    LOG_SYSTIME_STR + " FATAL: [Module1] fatal1\n" +
+    LOG_SYSTIME_STR + " INFO: [fm.FilterModule] infoFM\n" +
+    LOG_SYSTIME_STR + " WARNING: [fm.FilterModule] warnFM\n" +
+    LOG_SYSTIME_STR + " ERROR: [fm.FilterModule] errorFM\n" +
+    LOG_SYSTIME_STR + " FATAL: [fm.FilterModule] fatalFM\n"
+    ));
+}
+
+BOOST_AUTO_TEST_CASE(FindPrefixRule)
+{
+  Logging::setLevel("*=FATAL:fm.a.*=ERROR:fm.a.b=INFO");
+  logFromNewLogger("fm.a.b");
+  logFromNewLogger("fm.a.b.c");
+  logFromNewLogger("fm.a.b.d");
+  logFromNewLogger("fm.b");
+
+  Logging::flush();
+  BOOST_CHECK(os.is_equal(
+    LOG_SYSTIME_STR + " INFO: [fm.a.b] infofm.a.b\n" +
+    LOG_SYSTIME_STR + " WARNING: [fm.a.b] warnfm.a.b\n" +
+    LOG_SYSTIME_STR + " ERROR: [fm.a.b] errorfm.a.b\n" +
+    LOG_SYSTIME_STR + " FATAL: [fm.a.b] fatalfm.a.b\n" +
+    LOG_SYSTIME_STR + " ERROR: [fm.a.b.c] errorfm.a.b.c\n" +
+    LOG_SYSTIME_STR + " FATAL: [fm.a.b.c] fatalfm.a.b.c\n" +
+    LOG_SYSTIME_STR + " ERROR: [fm.a.b.d] errorfm.a.b.d\n" +
+    LOG_SYSTIME_STR + " FATAL: [fm.a.b.d] fatalfm.a.b.d\n" +
+    LOG_SYSTIME_STR + " FATAL: [fm.b] fatalfm.b\n"
+    ));
+}
+
 BOOST_AUTO_TEST_SUITE_END() // SeverityConfig
 
 BOOST_AUTO_TEST_CASE(ChangeDestination)