util: support declaring loggers as class members

Change-Id: If6373023d27cf655c1a7f29009edaee0ae21d5b7
Refs: #4552
diff --git a/src/util/logger.cpp b/src/util/logger.cpp
index d582228..92a1039 100644
--- a/src/util/logger.cpp
+++ b/src/util/logger.cpp
@@ -111,8 +111,10 @@
   Logging::addLogger(*this);
 }
 
+namespace detail {
+
 std::ostream&
-operator<<(std::ostream& os, const LoggerTimestamp&)
+operator<<(std::ostream& os, LoggerTimestamp)
 {
   using namespace ndn::time;
 
@@ -135,5 +137,6 @@
   return os << buffer;
 }
 
+} // namespace detail
 } // namespace util
 } // namespace ndn
diff --git a/src/util/logger.hpp b/src/util/logger.hpp
index 548390b..62443a8 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-2017 Regents of the University of California.
+ * Copyright (c) 2013-2018 Regents of the University of California.
  *
  * This file is part of ndn-cxx library (NDN C++ library with eXperimental eXtensions).
  *
@@ -35,7 +35,7 @@
 namespace ndn {
 namespace util {
 
-/** \brief indicates the severity level of a log message
+/** \brief Indicates the severity level of a log message.
  */
 enum class LogLevel {
   FATAL   = -1,   ///< fatal (will be logged unconditionally)
@@ -48,20 +48,20 @@
   ALL     = 255   ///< all messages
 };
 
-/** \brief output LogLevel as string
+/** \brief Output LogLevel as string.
  *  \throw std::invalid_argument unknown \p level
  */
 std::ostream&
 operator<<(std::ostream& os, LogLevel level);
 
-/** \brief parse LogLevel from string
+/** \brief Parse LogLevel from string.
  *  \throw std::invalid_argument unknown level name
  */
 LogLevel
 parseLogLevel(const std::string& s);
 
-/** \brief represents a logger in logging facility
- *  \note User should declare a new logger with \p NDN_LOG_INIT macro.
+/** \brief Represents a log module in the logging facility.
+ *  \note New loggers should be defined using #NDN_LOG_INIT or #NDN_LOG_MEMBER_INIT.
  */
 class Logger : public boost::log::sources::logger_mt
 {
@@ -92,23 +92,9 @@
   std::atomic<LogLevel> m_currentLevel;
 };
 
-/** \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 { \
-    inline ::ndn::util::Logger& getNdnCxxLogger() \
-    { \
-      static ::ndn::util::Logger logger(BOOST_STRINGIZE(name)); \
-      return logger; \
-    } \
-  } \
-  struct ndn_cxx__allow_trailing_semicolon
+namespace detail {
 
-/** \brief a tag that writes a timestamp upon stream output
+/** \brief A tag type used to output a timestamp to a stream.
  *  \code
  *  std::clog << LoggerTimestamp();
  *  \endcode
@@ -117,57 +103,151 @@
 {
 };
 
-/** \brief write a timestamp to \p os
+/** \brief Write a timestamp to \p os.
  *  \note This function is thread-safe.
  */
 std::ostream&
-operator<<(std::ostream& os, const LoggerTimestamp&);
+operator<<(std::ostream& os, LoggerTimestamp);
 
-#if (BOOST_VERSION >= 105900) && (BOOST_VERSION < 106000)
+/** \cond */
+template<class T>
+struct ExtractArgument;
+
+template<class T, class U>
+struct ExtractArgument<T(U)>
+{
+  using type = U;
+};
+
+template<class T>
+using ArgumentType = typename ExtractArgument<T>::type;
+/** \endcond */
+
+} // namespace detail
+
+/** \cond */
+#define NDN_LOG_INIT_FUNCTION_BODY(name) \
+  { \
+    static ::ndn::util::Logger logger(BOOST_STRINGIZE(name)); \
+    return logger; \
+  }
+/** \endcond */
+
+/** \brief Define a non-member log module.
+ *
+ *  This macro can be used in global scope to define a log module for an entire translation
+ *  unit, or in namespace scope to define a log module for the enclosing namespace.
+ *  Use #NDN_LOG_MEMBER_INIT to define a log module as a class or struct member.
+ *
+ *  \warning Do not use this macro in header files unless you know what you're doing,
+ *           as it can easily trigger ODR violations if used incorrectly.
+ *
+ *  \param name the logger name
+ *  \note The logger name is restricted to alphanumeric characters and a select set of
+ *        symbols: `~`, `#`, `%`, `_`, `<`, `>`, `.`, `-`. It must not start or end with
+ *        a dot (`.`), or contain multiple consecutive dots.
+ */
+#define NDN_LOG_INIT(name) \
+  namespace { \
+    ::ndn::util::Logger& ndn_cxx_getLogger() \
+    NDN_LOG_INIT_FUNCTION_BODY(name) \
+  } \
+  struct ndn_cxx_allow_trailing_semicolon
+
+/** \brief Define a member log module.
+ *
+ *  This macro should only be used to define a log module as a class or struct member.
+ *  Use #NDN_LOG_INIT to define a non-member log module.
+ *
+ *  \param name the logger name
+ *  \note The logger name is restricted to alphanumeric characters and a select set of
+ *        symbols: `~`, `#`, `%`, `_`, `<`, `>`, `.`, `-`. It must not start or end with
+ *        a dot (`.`), or contain multiple consecutive dots.
+ */
+#define NDN_LOG_MEMBER_INIT(name) \
+  private: \
+  static ::ndn::util::Logger& ndn_cxx_getLogger() \
+  NDN_LOG_INIT_FUNCTION_BODY(name) \
+  struct ndn_cxx_allow_trailing_semicolon
+
+/** \brief Forward-declare a member log module, without fully defining it.
+ *
+ *  This macro can be used to declare a log module as a member of a class template.
+ *  Use this macro in conjunction with #NDN_LOG_MEMBER_DECL_SPECIALIZED and
+ *  #NDN_LOG_MEMBER_INIT_SPECIALIZED to provide different loggers for different
+ *  template specializations.
+ */
+#define NDN_LOG_MEMBER_DECL() \
+  private: \
+  static ::ndn::util::Logger& ndn_cxx_getLogger()
+
+/** \brief Declare an explicit specialization of a member log module of a class template.
+ *
+ *  \param cls fully specialized class name; wrap in parentheses if it contains commas
+ */
+#define NDN_LOG_MEMBER_DECL_SPECIALIZED(cls) \
+  template<> ::ndn::util::Logger& detail::ArgumentType<void(cls)>::ndn_cxx_getLogger()
+
+/** \brief Define an explicit specialization of a member log module of a class template.
+ *
+ *  \param cls fully specialized class name; wrap in parentheses if it contains commas
+ *  \param name the logger name
+ *  \note The logger name is restricted to alphanumeric characters and a select set of
+ *        symbols: `~`, `#`, `%`, `_`, `<`, `>`, `.`, `-`. It must not start or end with
+ *        a dot (`.`), or contain multiple consecutive dots.
+ */
+#define NDN_LOG_MEMBER_INIT_SPECIALIZED(cls, name) \
+  template<> inline ::ndn::util::Logger& detail::ArgumentType<void(cls)>::ndn_cxx_getLogger() \
+  NDN_LOG_INIT_FUNCTION_BODY(name) \
+  struct ndn_cxx_allow_trailing_semicolon
+
+/** \cond */
+#if BOOST_VERSION == 105900
 // workaround Boost bug 11549
 #define NDN_BOOST_LOG(x) BOOST_LOG(x) << ""
 #else
 #define NDN_BOOST_LOG(x) BOOST_LOG(x)
 #endif
 
-#define NDN_LOG(lvl, lvlstr, expression) \
+#define NDN_LOG_INTERNAL(lvl, lvlstr, expression) \
   do { \
-    if (getNdnCxxLogger().isLevelEnabled(::ndn::util::LogLevel::lvl)) { \
-      NDN_BOOST_LOG(getNdnCxxLogger()) << ::ndn::util::LoggerTimestamp{} \
-        << " " BOOST_STRINGIZE(lvlstr) ": [" << getNdnCxxLogger().getModuleName() << "] " \
+    if (ndn_cxx_getLogger().isLevelEnabled(::ndn::util::LogLevel::lvl)) { \
+      NDN_BOOST_LOG(ndn_cxx_getLogger()) << ::ndn::util::detail::LoggerTimestamp{} \
+        << " " BOOST_STRINGIZE(lvlstr) ": [" << ndn_cxx_getLogger().getModuleName() << "] " \
         << expression; \
     } \
   } while (false)
+/** \endcond */
 
-/** \brief log at TRACE level
- *  \pre A log module must be declared in the same translation unit.
+/** \brief Log at TRACE level.
+ *  \pre A log module must be declared in the same translation unit, class, struct, or namespace.
  */
-#define NDN_LOG_TRACE(expression) NDN_LOG(TRACE, TRACE, expression)
+#define NDN_LOG_TRACE(expression) NDN_LOG_INTERNAL(TRACE, TRACE, expression)
 
-/** \brief log at DEBUG level
- *  \pre A log module must be declared in the same translation unit.
+/** \brief Log at DEBUG level.
+ *  \pre A log module must be declared in the same translation unit, class, struct, or namespace.
  */
-#define NDN_LOG_DEBUG(expression) NDN_LOG(DEBUG, DEBUG, expression)
+#define NDN_LOG_DEBUG(expression) NDN_LOG_INTERNAL(DEBUG, DEBUG, expression)
 
-/** \brief log at INFO level
- *  \pre A log module must be declared in the same translation unit.
+/** \brief Log at INFO level.
+ *  \pre A log module must be declared in the same translation unit, class, struct, or namespace.
  */
-#define NDN_LOG_INFO(expression) NDN_LOG(INFO, INFO, expression)
+#define NDN_LOG_INFO(expression) NDN_LOG_INTERNAL(INFO, INFO, expression)
 
-/** \brief log at WARN level
- *  \pre A log module must be declared in the same translation unit.
+/** \brief Log at WARN level.
+ *  \pre A log module must be declared in the same translation unit, class, struct, or namespace.
  */
-#define NDN_LOG_WARN(expression) NDN_LOG(WARN, WARNING, expression)
+#define NDN_LOG_WARN(expression) NDN_LOG_INTERNAL(WARN, WARNING, expression)
 
-/** \brief log at ERROR level
- *  \pre A log module must be declared in the same translation unit.
+/** \brief Log at ERROR level.
+ *  \pre A log module must be declared in the same translation unit, class, struct, or namespace.
  */
-#define NDN_LOG_ERROR(expression) NDN_LOG(ERROR, ERROR, expression)
+#define NDN_LOG_ERROR(expression) NDN_LOG_INTERNAL(ERROR, ERROR, expression)
 
-/** \brief log at FATAL level
- *  \pre A log module must be declared in the same translation unit.
+/** \brief Log at FATAL level.
+ *  \pre A log module must be declared in the same translation unit, class, struct, or namespace.
  */
-#define NDN_LOG_FATAL(expression) NDN_LOG(FATAL, FATAL, expression)
+#define NDN_LOG_FATAL(expression) NDN_LOG_INTERNAL(FATAL, FATAL, expression)
 
 } // namespace util
 } // namespace ndn
diff --git a/tests/unit-tests/util/logging.t.cpp b/tests/unit-tests/util/logging.t.cpp
index 693f844..172d58c 100644
--- a/tests/unit-tests/util/logging.t.cpp
+++ b/tests/unit-tests/util/logging.t.cpp
@@ -21,8 +21,8 @@
 
 #include "util/logging.hpp"
 #include "util/logger.hpp"
-#include "../unit-test-time-fixture.hpp"
 
+#include "../unit-test-time-fixture.hpp"
 #include "boost-test.hpp"
 
 namespace ndn {
@@ -31,7 +31,6 @@
 
 NDN_LOG_INIT(ndn.util.tests.Logging);
 
-using namespace ndn::tests;
 using boost::test_tools::output_test_stream;
 
 void
@@ -46,10 +45,9 @@
 static void
 logFromNewLogger(const std::string& moduleName)
 {
-  auto loggerPtr = make_unique<Logger>(moduleName);
-  Logger& logger = *loggerPtr;
+  Logger logger(moduleName);
+  auto ndn_cxx_getLogger = [&logger] () -> Logger& { return logger; };
 
-  auto getNdnCxxLogger = [&logger] () -> Logger& { return logger; };
   NDN_LOG_TRACE("trace" << moduleName);
   NDN_LOG_DEBUG("debug" << moduleName);
   NDN_LOG_INFO("info" << moduleName);
@@ -60,13 +58,82 @@
   BOOST_CHECK(Logging::get().removeLogger(logger));
 }
 
-const time::system_clock::Duration LOG_SYSTIME = time::microseconds(1468108800311239LL);
-const std::string LOG_SYSTIME_STR = "1468108800.311239";
+namespace ns1 {
 
-class LoggingFixture : public UnitTestTimeFixture
+NDN_LOG_INIT(ndn.util.tests.ns1);
+
+static void
+logFromNamespace1()
+{
+  NDN_LOG_INFO("hello world from ns1");
+}
+
+} // namespace ns1
+
+namespace ns2 {
+
+NDN_LOG_INIT(ndn.util.tests.ns2);
+
+static void
+logFromNamespace2()
+{
+  NDN_LOG_INFO("hi there from ns2");
+}
+
+} // namespace ns2
+
+class ClassWithLogger
+{
+public:
+  void
+  logFromConstMemberFunction() const
+  {
+    NDN_LOG_INFO("const member function");
+  }
+
+  static void
+  logFromStaticMemberFunction()
+  {
+    NDN_LOG_INFO("static member function");
+  }
+
+private:
+  NDN_LOG_MEMBER_INIT(ndn.util.tests.ClassWithLogger);
+};
+
+template<class T, class U>
+class ClassTemplateWithLogger
+{
+public:
+  void
+  logFromMemberFunction()
+  {
+    NDN_LOG_INFO("class template non-static member function");
+  }
+
+  static void
+  logFromStaticMemberFunction()
+  {
+    NDN_LOG_INFO("class template static member function");
+  }
+
+private:
+  NDN_LOG_MEMBER_DECL();
+};
+
+// Technically this declaration is not necessary in this case,
+// but we want to test that the macro expands to well-formed code
+NDN_LOG_MEMBER_DECL_SPECIALIZED((ClassTemplateWithLogger<int, double>));
+
+NDN_LOG_MEMBER_INIT_SPECIALIZED((ClassTemplateWithLogger<int, double>), ndn.util.tests.Specialized1);
+NDN_LOG_MEMBER_INIT_SPECIALIZED((ClassTemplateWithLogger<int, std::string>), ndn.util.tests.Specialized2);
+
+const time::microseconds LOG_SYSTIME(1468108800311239LL);
+const std::string LOG_SYSTIME_STR("1468108800.311239");
+
+class LoggingFixture : public ndn::tests::UnitTestTimeFixture
 {
 protected:
-  explicit
   LoggingFixture()
     : m_oldEnabledLevel(Logging::get().getLevels())
     , m_oldDestination(Logging::get().getDestination())
@@ -96,8 +163,8 @@
 BOOST_AUTO_TEST_CASE(GetLoggerNames)
 {
   NDN_LOG_TRACE("GetLoggerNames"); // to avoid unused function warning
-  std::set<std::string> names = Logging::getLoggerNames();
-  BOOST_CHECK_GT(names.size(), 0);
+  auto names = Logging::getLoggerNames();
+  BOOST_CHECK(!names.empty());
   BOOST_CHECK_EQUAL(names.count("ndn.util.tests.Logging"), 1);
 }
 
@@ -202,6 +269,53 @@
 
 BOOST_AUTO_TEST_SUITE_END() // Severity
 
+BOOST_AUTO_TEST_CASE(NamespaceLogger)
+{
+  Logging::setLevel("ndn.util.tests.ns1", LogLevel::INFO);
+  Logging::setLevel("ndn.util.tests.ns2", LogLevel::DEBUG);
+
+  const auto& levels = Logging::get().getLevels();
+  BOOST_CHECK_EQUAL(levels.size(), 2);
+  BOOST_CHECK_EQUAL(levels.at("ndn.util.tests.ns1"), LogLevel::INFO);
+  BOOST_CHECK_EQUAL(levels.at("ndn.util.tests.ns2"), LogLevel::DEBUG);
+
+  ns1::logFromNamespace1();
+  ns2::logFromNamespace2();
+
+  Logging::flush();
+  BOOST_CHECK(os.is_equal(
+    LOG_SYSTIME_STR + " INFO: [ndn.util.tests.ns1] hello world from ns1\n" +
+    LOG_SYSTIME_STR + " INFO: [ndn.util.tests.ns2] hi there from ns2\n"
+    ));
+}
+
+BOOST_AUTO_TEST_CASE(MemberLogger)
+{
+  Logging::setLevel("ndn.util.tests.ClassWithLogger", LogLevel::INFO);
+  Logging::setLevel("ndn.util.tests.Specialized1", LogLevel::INFO);
+  // ndn.util.tests.Specialized2 is not enabled
+
+  ClassWithLogger::logFromStaticMemberFunction();
+  ClassWithLogger{}.logFromConstMemberFunction();
+
+  Logging::flush();
+  BOOST_CHECK(os.is_equal(
+    LOG_SYSTIME_STR + " INFO: [ndn.util.tests.ClassWithLogger] static member function\n" +
+    LOG_SYSTIME_STR + " INFO: [ndn.util.tests.ClassWithLogger] const member function\n"
+    ));
+
+  ClassTemplateWithLogger<int, double>::logFromStaticMemberFunction();
+  ClassTemplateWithLogger<int, double>{}.logFromMemberFunction();
+  ClassTemplateWithLogger<int, std::string>::logFromStaticMemberFunction();
+  ClassTemplateWithLogger<int, std::string>{}.logFromMemberFunction();
+
+  Logging::flush();
+  BOOST_CHECK(os.is_equal(
+    LOG_SYSTIME_STR + " INFO: [ndn.util.tests.Specialized1] class template static member function\n" +
+    LOG_SYSTIME_STR + " INFO: [ndn.util.tests.Specialized1] class template non-static member function\n"
+    ));
+}
+
 BOOST_AUTO_TEST_CASE(SameNameLoggers)
 {
   Logging::setLevel("Module1", LogLevel::WARN);
@@ -221,7 +335,7 @@
 
 BOOST_AUTO_TEST_CASE(LateRegistration)
 {
-  BOOST_CHECK_NO_THROW(Logging::setLevel("Module3", LogLevel::DEBUG));
+  Logging::setLevel("Module3", LogLevel::DEBUG);
   logFromNewLogger("Module3");
 
   Logging::flush();