Parcourir la source

Merge branch 'layout'

* layout:
  Add pattern layout
  Finish coverage of format.cxx. Fix bugs: - Only erasing one character in truncate. - Not loading generator for other handlers.
  Test other predefined formats. Fix length of format specifiers...
  Add support for localtime
  Fix suffix of milliseconds formatter
  Add more custom date formatter tests. - Failing: FormatsCustomMilliseconds
  Fixing various bugs in unit tests: - Treated microseconds as milliseconds - Add a default date format - Fix ISO8601 format code - Fix starting '{' in custom date format code - Fix closing '}' in date formatting - Fix %% parsing for following tokens.
  Add some more new date formatting tests. - Failures: SupportsCustomFormatWithBrace, SupportsISO8601Format
  Add tests for default date format. - Failing: FormatsMicroseconds
  Adding tests to prove out prefix, suffix, and %-literal. - Failing: CatchesRawContentAfterFmt
  Start making tests for format.cxx
  Fix a few issues
  Break down functions
  Cleanup
  Merge all the layout things together
  Move impl.h files to subdirectory
  Fix tests
  Drop level+message log writer. Drop header from c_logger.
Sam Jaffe il y a 6 ans
Parent
commit
e291421f42

+ 0 - 1
include/logger/c_logger.h

@@ -81,7 +81,6 @@ namespace logging {
   private:
     void vlognf(log_level, size_t, char const*, va_list);
     void log(log_level, std::string const&);
-    std::string get_header(log_level level);
     
   private:
     log_level min_level_;

+ 5 - 6
include/logger/logger_impl.h

@@ -1,19 +1,18 @@
 #pragma once
 
-#include "logger_fwd.h"
+#include "logger/logger_fwd.h"
 
 namespace logging {
-  class logger_impl {
+  class appender {
   public:
-    virtual ~logger_impl() = default;
-    virtual void write(logpacket const & pkt) = 0;
-    virtual void write(log_level level, std::string const& msg) = 0;
+    virtual ~appender() = default;
+    virtual void write(std::string const & msg) = 0;
     virtual void flush() = 0;
     
     bool should_log(log_level ll) const {
       return ll >= min_log_level;
     }
-
+    
   protected:
     log_level min_log_level;
   };

+ 12 - 0
include/logger/detail/layout.h

@@ -0,0 +1,12 @@
+#pragma once
+
+#include "logger/logger_fwd.h"
+
+#include <string>
+
+namespace logging {
+  struct layout {
+    virtual ~layout() = default;
+    virtual std::string format(logpacket const & pkt) const = 0;
+  };
+}

+ 24 - 0
include/logger/detail/logger_impl.h

@@ -0,0 +1,24 @@
+#pragma once
+
+#include "logger/logger_fwd.h"
+
+#include <memory>
+#include <vector>
+#include <utility>
+
+namespace logging {
+  class appender;
+  class layout;
+
+  struct logger_impl {
+    using p_appender = std::shared_ptr<appender>;
+    using p_layout = std::shared_ptr<layout>;
+
+    bool should_log(log_level ll) const;
+    void write(logpacket const & pkt);
+    void flush();
+
+    std::vector<std::pair<p_appender, p_layout>> impls_;
+    log_level min_log_level_;
+  };
+}

+ 3 - 1
include/logger/format.h

@@ -23,8 +23,10 @@ namespace logging {
     
     using generator = std::function<void(logpacket const &, std::ostream &)>;
     
+    static format parse_format_string(std::string const &);
+    void process(logpacket const & pkt, std::ostream & os) const;
+    std::string process(logpacket const & pkt) const;
   private:
-    static format parse_format_string(std::string const &);    
     std::vector<generator> gen;
   };
   

+ 6 - 3
include/logger/log_manager.h

@@ -15,9 +15,10 @@ namespace objects { namespace prototype {
 } }
 
 namespace logging {
+  class appender;
   class c_logger;
+  class layout;
   class logger;
-  class logger_impl;
   class properties;
   
   class manager {
@@ -38,6 +39,8 @@ namespace logging {
     std::unique_ptr<struct manager_impl> pimpl_;
   };
   
-  using impl_factory = objects::prototype::factory<
-      std::shared_ptr<logger_impl>, properties const &>;
+  using appenders = objects::prototype::factory<
+      std::shared_ptr<appender>, properties const &>;
+  using layouts = objects::prototype::factory<
+      std::shared_ptr<layout>, properties const &>;
 }

+ 1 - 1
include/logger/logger_fwd.h

@@ -32,7 +32,7 @@ namespace logging {
     //    int thread_id;
     log_level level;
     location_info info;
-    const char* logger;
+    std::string logger;
     std::string message;
   };
 }

+ 24 - 8
logger.xcodeproj/project.pbxproj

@@ -8,11 +8,15 @@
 
 /* Begin PBXBuildFile section */
 		CD1CDE872252E5B900E5B6B2 /* properties.cxx in Sources */ = {isa = PBXBuildFile; fileRef = CD1CDE862252E5B900E5B6B2 /* properties.cxx */; };
-		CD1CDE892252E60900E5B6B2 /* file_logger.cxx in Sources */ = {isa = PBXBuildFile; fileRef = CD1CDE882252E60900E5B6B2 /* file_logger.cxx */; };
-		CD1CDE8B2252E61800E5B6B2 /* console_logger.cxx in Sources */ = {isa = PBXBuildFile; fileRef = CD1CDE8A2252E61800E5B6B2 /* console_logger.cxx */; };
+		CD1CDE892252E60900E5B6B2 /* file_appender.cxx in Sources */ = {isa = PBXBuildFile; fileRef = CD1CDE882252E60900E5B6B2 /* file_appender.cxx */; };
+		CD1CDE8B2252E61800E5B6B2 /* console_appender.cxx in Sources */ = {isa = PBXBuildFile; fileRef = CD1CDE8A2252E61800E5B6B2 /* console_appender.cxx */; };
 		CD1CDE8D22540D9B00E5B6B2 /* c_logger_test.cxx in Sources */ = {isa = PBXBuildFile; fileRef = CD1CDE8C22540D9B00E5B6B2 /* c_logger_test.cxx */; };
 		CD1CDE9022542CC500E5B6B2 /* log_manager_test.cxx in Sources */ = {isa = PBXBuildFile; fileRef = CD1CDE8F22542CC500E5B6B2 /* log_manager_test.cxx */; };
 		CD1CDE9222543E7E00E5B6B2 /* test_properties.cxx in Sources */ = {isa = PBXBuildFile; fileRef = CD1CDE9122543E7E00E5B6B2 /* test_properties.cxx */; };
+		CD1CDEAF22556B7E00E5B6B2 /* logger_impl.cxx in Sources */ = {isa = PBXBuildFile; fileRef = CD1CDEAE22556B7E00E5B6B2 /* logger_impl.cxx */; };
+		CD1CDEB122557FB600E5B6B2 /* default_layout.cxx in Sources */ = {isa = PBXBuildFile; fileRef = CD1CDEB022557FB600E5B6B2 /* default_layout.cxx */; };
+		CD1CDEB32256B04600E5B6B2 /* format_test.cxx in Sources */ = {isa = PBXBuildFile; fileRef = CD1CDEB22256B04600E5B6B2 /* format_test.cxx */; };
+		CD1CDEB52256C94000E5B6B2 /* pattern_layout.cxx in Sources */ = {isa = PBXBuildFile; fileRef = CD1CDEB42256C94000E5B6B2 /* pattern_layout.cxx */; };
 		CD29739B1D7B401F00E37217 /* logger.cxx in Sources */ = {isa = PBXBuildFile; fileRef = CD2973991D7B401F00E37217 /* logger.cxx */; };
 		CD3C80C01D6A2CA300ACC795 /* format.cxx in Sources */ = {isa = PBXBuildFile; fileRef = CD3C80BE1D6A2CA300ACC795 /* format.cxx */; };
 		CD6F73EC225187BE0081ED74 /* logger in Headers */ = {isa = PBXBuildFile; fileRef = CD6F73EA225187A10081ED74 /* logger */; settings = {ATTRIBUTES = (Public, ); }; };
@@ -66,12 +70,16 @@
 		0EB833481BBF45E600DDC844 /* Makefile */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.make; path = Makefile; sourceTree = "<group>"; };
 		0ECAC4AF1BC00AC500FDAE14 /* liblogging.dylib */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.dylib"; includeInIndex = 0; path = liblogging.dylib; sourceTree = BUILT_PRODUCTS_DIR; };
 		CD1CDE862252E5B900E5B6B2 /* properties.cxx */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = properties.cxx; sourceTree = "<group>"; };
-		CD1CDE882252E60900E5B6B2 /* file_logger.cxx */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = file_logger.cxx; sourceTree = "<group>"; };
-		CD1CDE8A2252E61800E5B6B2 /* console_logger.cxx */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = console_logger.cxx; sourceTree = "<group>"; };
+		CD1CDE882252E60900E5B6B2 /* file_appender.cxx */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = file_appender.cxx; sourceTree = "<group>"; };
+		CD1CDE8A2252E61800E5B6B2 /* console_appender.cxx */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = console_appender.cxx; sourceTree = "<group>"; };
 		CD1CDE8C22540D9B00E5B6B2 /* c_logger_test.cxx */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = c_logger_test.cxx; sourceTree = "<group>"; };
 		CD1CDE8E22540DEA00E5B6B2 /* mock_logger.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = mock_logger.h; sourceTree = "<group>"; };
 		CD1CDE8F22542CC500E5B6B2 /* log_manager_test.cxx */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = log_manager_test.cxx; sourceTree = "<group>"; };
 		CD1CDE9122543E7E00E5B6B2 /* test_properties.cxx */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = test_properties.cxx; sourceTree = "<group>"; };
+		CD1CDEAE22556B7E00E5B6B2 /* logger_impl.cxx */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = logger_impl.cxx; sourceTree = "<group>"; };
+		CD1CDEB022557FB600E5B6B2 /* default_layout.cxx */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = default_layout.cxx; sourceTree = "<group>"; };
+		CD1CDEB22256B04600E5B6B2 /* format_test.cxx */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = format_test.cxx; sourceTree = "<group>"; };
+		CD1CDEB42256C94000E5B6B2 /* pattern_layout.cxx */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = pattern_layout.cxx; sourceTree = "<group>"; };
 		CD2973991D7B401F00E37217 /* logger.cxx */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = logger.cxx; sourceTree = "<group>"; };
 		CD3C80BE1D6A2CA300ACC795 /* format.cxx */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = format.cxx; sourceTree = "<group>"; };
 		CD6F73EA225187A10081ED74 /* logger */ = {isa = PBXFileReference; lastKnownFileType = folder; name = logger; path = include/logger; sourceTree = "<group>"; };
@@ -133,8 +141,10 @@
 			isa = PBXGroup;
 			children = (
 				CD1CDE862252E5B900E5B6B2 /* properties.cxx */,
-				CD1CDE882252E60900E5B6B2 /* file_logger.cxx */,
-				CD1CDE8A2252E61800E5B6B2 /* console_logger.cxx */,
+				CD1CDE882252E60900E5B6B2 /* file_appender.cxx */,
+				CD1CDEB42256C94000E5B6B2 /* pattern_layout.cxx */,
+				CD1CDEB022557FB600E5B6B2 /* default_layout.cxx */,
+				CD1CDE8A2252E61800E5B6B2 /* console_appender.cxx */,
 			);
 			path = loggers;
 			sourceTree = "<group>";
@@ -144,6 +154,7 @@
 			children = (
 				CD1CDE812252E54100E5B6B2 /* loggers */,
 				CD2973991D7B401F00E37217 /* logger.cxx */,
+				CD1CDEAE22556B7E00E5B6B2 /* logger_impl.cxx */,
 				CD88E9642252D6C700927F40 /* common.h */,
 				CD88E9612252D67A00927F40 /* common.cxx */,
 				CD88E95D2252D3EF00927F40 /* c_logger.cxx */,
@@ -157,6 +168,7 @@
 			isa = PBXGroup;
 			children = (
 				CD6F73FC225187E10081ED74 /* logger_test.cxx */,
+				CD1CDEB22256B04600E5B6B2 /* format_test.cxx */,
 				CD1CDE8C22540D9B00E5B6B2 /* c_logger_test.cxx */,
 				CD1CDE8F22542CC500E5B6B2 /* log_manager_test.cxx */,
 				CD1CDE9122543E7E00E5B6B2 /* test_properties.cxx */,
@@ -336,9 +348,12 @@
 				CD3C80C01D6A2CA300ACC795 /* format.cxx in Sources */,
 				CD88E95F2252D3EF00927F40 /* c_logger.cxx in Sources */,
 				CD1CDE872252E5B900E5B6B2 /* properties.cxx in Sources */,
-				CD1CDE8B2252E61800E5B6B2 /* console_logger.cxx in Sources */,
-				CD1CDE892252E60900E5B6B2 /* file_logger.cxx in Sources */,
+				CD1CDE8B2252E61800E5B6B2 /* console_appender.cxx in Sources */,
+				CD1CDEAF22556B7E00E5B6B2 /* logger_impl.cxx in Sources */,
+				CD1CDE892252E60900E5B6B2 /* file_appender.cxx in Sources */,
+				CD1CDEB52256C94000E5B6B2 /* pattern_layout.cxx in Sources */,
 				CD88E9632252D67A00927F40 /* common.cxx in Sources */,
+				CD1CDEB122557FB600E5B6B2 /* default_layout.cxx in Sources */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};
@@ -349,6 +364,7 @@
 				CD1CDE9022542CC500E5B6B2 /* log_manager_test.cxx in Sources */,
 				CD6F740C225187FD0081ED74 /* logger_test.cxx in Sources */,
 				CD1CDE9222543E7E00E5B6B2 /* test_properties.cxx in Sources */,
+				CD1CDEB32256B04600E5B6B2 /* format_test.cxx in Sources */,
 				CD1CDE8D22540D9B00E5B6B2 /* c_logger_test.cxx in Sources */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;

+ 1 - 1
logger.xcodeproj/xcuserdata/samjaffe.xcuserdatad/xcschemes/xcschememanagement.plist

@@ -12,7 +12,7 @@
 		<key>logging.xcscheme</key>
 		<dict>
 			<key>isShown</key>
-			<false/>
+			<true/>
 			<key>orderHint</key>
 			<integer>3</integer>
 		</dict>

+ 6 - 13
src/c_logger.cxx

@@ -8,11 +8,12 @@
 #include "logger/c_logger.h"
 
 #include "common.h"
-#include "logger/logger_impl.h"
+#include "logger/detail/logger_impl.h"
 
 using namespace logging;
 
-c_logger::c_logger(std::string const & name, std::shared_ptr<logger_impl> impl)
+c_logger::c_logger(std::string const & name,
+                   std::shared_ptr<logger_impl> impl)
 : min_level_(LDEBUG), logger_name_(name), impl_(impl)
 {
 }
@@ -21,28 +22,20 @@ c_logger::~c_logger() {
   if (impl_) impl_->flush();
 }
 
-std::string c_logger::get_header(log_level level) {
-  std::string ts = timestamp();
-  char data[64];
-  snprintf(data, sizeof(data), "[%-5s] %s %s - ",
-           level_header(level), logger_name_.c_str(), ts.c_str());
-  return data;
-}
-
 void c_logger::vlognf(log_level level, size_t num_bytes, char const* fmt,
-                    va_list args) {
+                      va_list args) {
   if (level < min_level_ || !impl_->should_log(level)) return;
   const std::unique_ptr<char[]> data(new char[num_bytes]);
   int n = vsnprintf(data.get(), num_bytes, fmt, args);
   if (n >= 0) {
     data[num_bytes-1] = '\0';
-    log(level, get_header(level) + data.get() + "\n");
+    impl_->write({ now(), level, {}, logger_name_, data.get() });
   }
 }
 
 void c_logger::log(log_level level, std::string const& msg) {
   if (level < min_level_ || !impl_->should_log(level)) return;
-  impl_->write(level, msg);
+  impl_->write({ now(), level, {}, logger_name_, msg });
 }
 
 void c_logger::flush() {

+ 39 - 26
src/format.cxx

@@ -30,7 +30,12 @@ namespace logging {
   namespace {
     std::string fmt_time(struct timeval round, char const * const fmt) {
       struct tm time;
-      gmtime_r(&round.tv_sec, &time);
+      // Supports localtime when requested, but you can't really test that
+      if (strstr(fmt, "%z") || strstr(fmt, "%Z")) {
+        localtime_r(&round.tv_sec, &time);
+      } else {
+        gmtime_r(&round.tv_sec, &time);
+      }
       char buf[64] = {'\0'};
       std::strftime(buf, sizeof(buf), fmt, &time);
       return buf;
@@ -43,7 +48,7 @@ namespace logging {
     std::string fmt_time_with_milis(struct timeval round,
                                     std::string const & fmt) {
       char buf[64] = {'\0'};
-      snprintf(buf, sizeof(buf), fmt.c_str(), round.tv_usec);
+      snprintf(buf, sizeof(buf), fmt.c_str(), round.tv_usec/1000);
       return fmt_time(round, buf);
     }
   }
@@ -64,7 +69,7 @@ namespace logging {
         fmtstring.replace(pos, 1, "%%");
         pos += 2;
       }
-      fmtstring.replace(fmtstring.find("%%_ms"), 4, "%.03d");
+      fmtstring.replace(fmtstring.find("%%_ms"), 5, "%.03d");
       return [=](logpacket const & lp) {
         return fmt_time_with_milis(lp.time, fmtstring);
       };
@@ -78,15 +83,15 @@ namespace logging {
 #define is( chr ) *next == chr
 #define is_string( str ) ! strncmp(next, str, strlen(str))
   string_generator date_token(char const * next) {
-    std::string predef_format;
+    std::string predef_format = "%%Y-%%m-%%d %%H:%%M:%%S,%.03d";
     if ( is_string("{ISO8601}")) {
-      predef_format = "%%Y-%%m-%%d %%H:%%M:%%S,%.03d";
+      predef_format = "%%Y-%%m-%%dT%%H:%%M:%%S.%.03dZ";
     } else if (is_string("{ABSOLUTE}")) {
-      predef_format = "%%H:%%M:%%S,%.04d";
+      predef_format = "%%H:%%M:%%S,%.03d";
     } else if (is_string("{DATE}")) {
-      predef_format = "%%d %%b %%Y %%H:%%M:%%S,%.04d";
+      predef_format = "%%d %%b %%Y %%H:%%M:%%S,%.03d";
     } else if (is('{')) {
-      return parse_date_format_string(next);
+      return parse_date_format_string(next+1);
     }
     return [=](logpacket const & lp ){
       return fmt_time_with_milis(lp.time, predef_format);
@@ -139,10 +144,10 @@ namespace logging {
     gen = handle( next );
     
     return [=](logpacket const & lp, std::ostream & out) {
-      std::string str = gen( lp );
-      if ( str.length() > max )
-        str.erase(str.begin() + max);
-      out << align << std::setw( min ) << str;
+      std::string str = gen(lp);
+      if (str.length() > max)
+        str.erase(str.begin()+max, str.end());
+      out << align << std::setw(min) << str;
     };
   }
   
@@ -158,40 +163,48 @@ namespace logging {
     char const * next = nullptr;
     char const * const end = curr + fmt.size();
     
-    while ( ( next = std::strchr( curr, '%' ) + 1 )
-           != nullptr ) {
-      if ( end == next ) {
+    while ((next = std::strchr(curr, '%')) != nullptr) {
+      ++next;
+      if (end == next) {
         std::string error_msg{"expected format specifier, got end of string"};
         throw format_parsing_exception{error_msg}; // TODO
       }
       
-      if ( curr < next -  1 ) {
+      if (curr < next-1) {
         out.gen.push_back(convert(string_token({curr, next - 1})));
       }
 
-      if ( is('d') ) {
+      if (is('d')) {
         ++next;
-        if ( is('{') ) curr = std::strchr(next, '}');
         out.gen.push_back(convert(date_token(next)));
-      } else if ( is('n') ) {
+        if (is('{')) next = std::strchr(next, '}');
+      } else if (is('n')) {
         out.gen.push_back(convert(string_token(NEWLINE)));
-      } else if ( is('%') ) {
-        ++next;
+      } else if (is('%')) {
         out.gen.push_back(convert(string_token("%")));
-      } else if ( is('.') || is('-') ||
-                 isdigit( *next ) ) {
-        parse_with_bounds( next );
+      } else if (is('.') || is('-') || isdigit( *next )) {
+        out.gen.push_back(parse_with_bounds(next));
       } else {
-        handle( next );
+        out.gen.push_back(convert(handle(next)));
       }
       curr = ++next;
     }
-    if ( curr < end ) {
+    if (curr < end) {
       out.gen.push_back(convert(string_token({curr, end})));
     }
     return out;
   }
 #undef is
+  
+  void format::process(logpacket const & pkt, std::ostream & os) const {
+    for (auto func : gen) { func(pkt, os); }
+  }
+  
+  std::string format::process(logpacket const & pkt) const {
+    std::stringstream ss;
+    process(pkt, ss);
+    return ss.str();
+  }
 }
 
 namespace logging {

+ 70 - 53
src/log_manager.cxx

@@ -16,21 +16,82 @@
 #include "logger/c_logger.h"
 #include "logger/exception.h"
 #include "logger/logger.h"
-#include "logger/logger_impl.h"
 #include "logger/properties.h"
+#include "logger_impl.h"
 
-INSTANTIATE_PROTOTYPE_FACTORY_2(logging::impl_factory);
+INSTANTIATE_PROTOTYPE_FACTORY_2(logging::appenders);
+INSTANTIATE_PROTOTYPE_FACTORY_2(logging::layouts);
 
 using namespace logging;
 
+using p_appender = std::shared_ptr<appender>;
+using p_layout = std::shared_ptr<layout>;
+using p_logger = std::shared_ptr<logger_impl>;
+
 struct logging::manager_impl {
-  std::shared_ptr<logger_impl> get_logger(properties const & props);
-  
-  std::shared_ptr<logger_impl> default_logger;
-  std::unordered_map<std::string, std::shared_ptr<logger_impl>> appenders;
-  std::unordered_map<std::string, std::shared_ptr<logger_impl>> loggers;
+  p_logger get_logger(properties const & props);
+  void configure_appenders(properties const & props);
+  void configure_loggers(properties const & props);
+
+  p_logger default_logger;
+  std::unordered_map<std::string, std::pair<p_appender, p_layout>> appenders;
+  std::unordered_map<std::string, p_logger> loggers;
 };
 
+std::shared_ptr<logger_impl>
+manager_impl::get_logger(properties const & props) {
+  auto out = std::make_shared<logger_impl>();
+  if (props.type == properties::OBJECT) {
+    // TODO include settings on the parent
+    out->impls_.push_back(appenders.at(props["ref"]));
+  } else if (props.type == properties::ARRAY) {
+    for (auto & part : props.array) {
+      out->impls_.push_back(appenders.at(part["ref"]));
+    }
+  } else {
+    throw invalid_property_type{"appenders cannot be a STRING"};
+  }
+  return out;
+}
+
+static p_appender load_appender(std::string const & name,
+                                properties const & props) {
+  return appenders::instance().get(name, props);
+}
+
+static bool is_layout(std::string const & str) {
+  return str.find("Layout") != std::string::npos;
+}
+
+static p_layout load_layout(std::string const & source,
+                            properties const & props) {
+  for (auto & layout : props.obj) {
+    if (is_layout(layout.first)) {
+      return layouts::instance().get(layout.first, layout.second);
+    }
+  }
+  return layouts::instance().get("default", {});
+}
+
+void manager_impl::configure_appenders(properties const & props) {
+  // TODO: Load logger_impl here
+  // TODO: Support multiple File loggers, etc.
+  expects(props["appenders"].type == properties::OBJECT);
+  for (auto & app : props["appenders"].obj) {
+    auto pair = std::make_pair(load_appender(app.first, app.second),
+                               load_layout(app.first, app.second));
+    appenders.emplace(app.first, pair);
+  }
+}
+
+void manager_impl::configure_loggers(properties const & props) {
+  expects(props["loggers"].type == properties::OBJECT);
+  for (auto & log : props["loggers"].obj) {
+    loggers.emplace(log.first, get_logger(log.second["appenders"]));
+  }
+  default_logger = get_logger(props["loggers"]["root"]["appenders"]);
+}
+
 manager::manager() : pimpl_(new manager_impl) {}
 
 manager::~manager() {}
@@ -51,51 +112,7 @@ c_logger manager::c_get(std::string const & name) {
   return c_logger(name, pimpl_->loggers.at(name));
 }
 
-struct compound_logger : public logger_impl {
-  void write(logpacket const & pkt) override {
-    for (auto & log : values) { log->write(pkt); }
-  }
-  void write(log_level l, std::string const & msg) override {
-    for (auto & log : values) { log->write(l, msg); }
-  }
-  void flush() override {
-    for (auto & log : values) { log->flush(); }
-  }
-  std::vector<std::shared_ptr<logger_impl>> values;
-};
-
-std::shared_ptr<logger_impl>
-manager_impl::get_logger(properties const & props) {
-  if (props.type == properties::OBJECT) {
-    // TODO include settings on the parent
-    return appenders.at(props["ref"].value);
-  } else if (props.type == properties::ARRAY) {
-    auto out = std::make_shared<compound_logger>();
-    for (auto & part : props.array) {
-      out->values.push_back(get_logger(part));
-    }
-    return out;
-  } else {
-    throw invalid_property_type{"appenders cannot be a STRING"};
-  }
-}
-
-
 void manager::configure(properties const & props) {
-  auto & factory = impl_factory::instance();
-  // TODO: Load logger_impl here
-  auto & impls = props["configuration"]["appenders"];
-  // TODO: Support multiple File loggers, etc.
-  expects(impls.type == properties::OBJECT);
-  for (auto & app : impls.obj) {
-    pimpl_->appenders.emplace(app.first, factory.get(app.first, app.second));
-  }
-
-  auto & configs = props["configuration"]["loggers"];
-  expects(configs.type == properties::OBJECT);
-  for (auto & log : configs.obj) {
-    auto logimpl = pimpl_->get_logger(log.second["appenders"]);
-    pimpl_->loggers.emplace(log.first, logimpl);
-  }
-  pimpl_->default_logger = pimpl_->get_logger(configs["root"]["appenders"]);
+  pimpl_->configure_appenders(props["configuration"]);
+  pimpl_->configure_loggers(props["configuration"]);
 }

+ 2 - 3
src/logger.cxx

@@ -16,8 +16,7 @@
 #include <stdexcept>
 
 #include "common.h"
-#include "logger/logger_impl.h"
-//#include "properties.hpp"
+#include "logger/detail/logger_impl.h"
 
 namespace logging {
   logger::logger(std::string const & name, std::shared_ptr<logger_impl> impl)
@@ -31,7 +30,7 @@ namespace logging {
   
   void logger::log(log_level ll, location_info info,
                    std::string const & msg) {
-    impl_->write({ now(), ll, info, logger_name_.c_str(), msg });
+    impl_->write({ now(), ll, info, logger_name_, msg });
   }
   
   bool logger::should_log(log_level ll) const {

+ 31 - 0
src/logger_impl.cxx

@@ -0,0 +1,31 @@
+//
+//  logger_impl.cxx
+//  logging
+//
+//  Created by Sam Jaffe on 4/3/19.
+//
+
+#include "logger_impl.h"
+
+#include "logger/detail/appender.h"
+#include "logger/detail/layout.h"
+
+using namespace logging;
+
+bool logger_impl::should_log(log_level ll) const {
+  return ll >= min_log_level_;
+}
+
+void logger_impl::write(logpacket const & pkt) {
+  for (auto & pair : impls_) {
+    if (pair.first->should_log(pkt.level)) {
+      pair.first->write(pair.second->format(pkt));
+    }
+  }
+}
+
+void logger_impl::flush() {
+  for (auto & pair : impls_) {
+    pair.first->flush();
+  }
+}

+ 12 - 17
src/loggers/console_logger.cxx

@@ -10,8 +10,8 @@
 #include "../../../../paradigm/declarative/expect/include/expect/expect.hpp"
 #include "../../../../types/resource_factory/include/resource_factory/prototype_factory.hpp"
 
+#include "logger/detail/appender.h"
 #include "logger/log_manager.h"
-#include "logger/logger_impl.h"
 #include "logger/properties.h"
 
 using namespace logging;
@@ -25,45 +25,40 @@ static std::string validate_or_throw(logging::properties const& props) {
   return it->second.value;
 }
 
-class console_logger : public logger_impl {
+class console_appender : public appender {
 public:
-  static std::shared_ptr<logger_impl> create(properties const& props);
+  static std::shared_ptr<appender> create(properties const& props);
   
-  explicit console_logger(std::ostream& os);
+  explicit console_appender(std::ostream& os);
   
-  void write(logpacket const & pkt) override;
-  void write(log_level level, std::string const& msg) override;
+  void write(std::string const & msg) override;
   
   void flush() override;
 private:
   std::ostream& out_;
 };
 
-std::shared_ptr<logger_impl> console_logger::create(properties const& props) {
+std::shared_ptr<appender> console_appender::create(properties const& props) {
   const std::string target = validate_or_throw(props);
   if (target == "SYSTEM_OUT") {
-    return std::make_shared<console_logger>(std::cout);
+    return std::make_shared<console_appender>(std::cout);
   } else if (target == "SYSTEM_ERR") {
-    return std::make_shared<console_logger>(std::cerr);
+    return std::make_shared<console_appender>(std::cerr);
   } else {
     throw std::logic_error{target + " is not a valid console"};
   }
 }
 
-console_logger::console_logger(std::ostream& os) : out_(os) {}
+console_appender::console_appender(std::ostream& os) : out_(os) {}
 
-void console_logger::write(logpacket const & pkt) {
-  out_ << pkt.message;
-}
-
-void console_logger::write(log_level level, std::string const& msg) {
+void console_appender::write(std::string const & msg) {
   out_ << msg;
 }
 
-void console_logger::flush() {
+void console_appender::flush() {
   out_.flush();
 }
 
 namespace {
-  bool _ = impl_factory::instance().bind("Console", console_logger::create);
+  bool _ = appenders::instance().bind("Console", console_appender::create);
 }

+ 30 - 0
src/loggers/default_layout.cxx

@@ -0,0 +1,30 @@
+//
+//  default_layout.cxx
+//  logging
+//
+//  Created by Sam Jaffe on 4/3/19.
+//
+
+#include "../../../../types/resource_factory/include/resource_factory/prototype_factory.hpp"
+
+#include "logger/detail/layout.h"
+#include "logger/log_manager.h"
+
+using namespace logging;
+
+struct default_layout : public layout {
+  static std::shared_ptr<layout> create(properties const &);
+  std::string format(logpacket const & pkt) const override;
+};
+
+std::shared_ptr<layout> default_layout::create(properties const &) {
+  return std::make_shared<default_layout>();
+}
+
+std::string default_layout::format(logpacket const & pkt) const {
+  return pkt.message;
+}
+
+namespace {
+  bool _ = layouts::instance().bind("default", default_layout::create);
+}

+ 43 - 0
src/loggers/file_appender.cxx

@@ -0,0 +1,43 @@
+//
+//  file_logger.cxx
+//  logging
+//
+//  Created by Sam Jaffe on 4/1/19.
+//
+
+#include <fstream>
+
+#include "../../../../paradigm/declarative/expect/include/expect/expect.hpp"
+#include "../../../../types/resource_factory/include/resource_factory/prototype_factory.hpp"
+
+#include "logger/detail/appender.h"
+#include "logger/log_manager.h"
+#include "logger/properties.h"
+
+using namespace logging;
+
+class file_appender : public appender {
+public:
+  static std::shared_ptr<appender> create(properties const&);
+  
+  explicit file_appender(const std::string& fname);
+  
+  void flush() override;
+private:
+  std::ofstream out_;
+};
+
+std::shared_ptr<appender> file_appender::create(properties const&) {
+  return std::shared_ptr<appender>(); // TODO
+}
+
+file_appender::file_appender(const std::string& fname)
+: out_(fname) {}
+
+void file_appender::flush() {
+  out_.flush();
+}
+
+namespace {
+  bool _ = appenders::instance().bind("File", file_appender::create);
+}

+ 0 - 48
src/loggers/file_logger.cxx

@@ -1,48 +0,0 @@
-//
-//  file_logger.cxx
-//  logging
-//
-//  Created by Sam Jaffe on 4/1/19.
-//
-
-#include <fstream>
-
-#include "../../../../paradigm/declarative/expect/include/expect/expect.hpp"
-#include "../../../../types/resource_factory/include/resource_factory/prototype_factory.hpp"
-
-#include "logger/log_manager.h"
-#include "logger/logger_impl.h"
-#include "logger/properties.h"
-
-using namespace logging;
-
-class file_logger : public logger_impl {
-public:
-  static std::shared_ptr<logger_impl> create(properties const&);
-  
-  explicit file_logger(const std::string& fname);
-  
-  void write(log_level level, std::string const& msg) override;
-  void flush() override;
-private:
-  std::ofstream out_;
-};
-
-std::shared_ptr<logger_impl> file_logger::create(properties const&) {
-  return std::shared_ptr<logger_impl>(); // TODO
-}
-
-file_logger::file_logger(const std::string& fname)
-: out_(fname) {}
-
-void file_logger::write(log_level level, std::string const& msg) {
-  out_ << msg;
-}
-
-void file_logger::flush() {
-  out_.flush();
-}
-
-namespace {
-  bool _ = impl_factory::instance().bind("File", file_logger::create);
-}

+ 39 - 0
src/loggers/pattern_layout.cxx

@@ -0,0 +1,39 @@
+//
+//  pattern_layout.cxx
+//  logging
+//
+//  Created by Sam Jaffe on 4/4/19.
+//
+
+#include "../../../../types/resource_factory/include/resource_factory/prototype_factory.hpp"
+
+#include "logger/detail/layout.h"
+#include "logger/format.h"
+#include "logger/log_manager.h"
+#include "logger/properties.h"
+
+using namespace logging;
+
+struct pattern_layout : public layout {
+  static std::shared_ptr<layout> create(properties const &);
+  
+  pattern_layout(std::string const & fmt);
+  std::string format(logpacket const & pkt) const override;
+  
+  class format formatter;
+};
+
+std::shared_ptr<layout> pattern_layout::create(properties const & props) {
+  return std::make_shared<pattern_layout>(props["pattern"]);
+}
+
+pattern_layout::pattern_layout(std::string const & fmt) :
+  formatter(format::parse_format_string(fmt)) {}
+
+std::string pattern_layout::format(logpacket const & pkt) const {
+  return formatter.process(pkt);
+}
+
+namespace {
+  bool _ = layouts::instance().bind("PatternLayout", pattern_layout::create);
+}

+ 14 - 14
test/c_logger_test.cxx

@@ -21,56 +21,56 @@ struct t_logger : public c_logger {
 using CLoggerTest = LoggerTest;
 
 TEST_F(CLoggerTest, FlushesOnClose) {
-  EXPECT_CALL(*pimpl, flush()).Times(1);
+  EXPECT_CALL(*appender, flush()).Times(1);
   t_logger("", pimpl);
 }
 
 TEST_F(CLoggerTest, FlushesOnFlushCall) {
-  EXPECT_CALL(*pimpl, flush()).Times(2);
+  EXPECT_CALL(*appender, flush()).Times(2);
   t_logger("", pimpl).flush();
 }
 
 TEST_F(CLoggerTest, LogsWithFmtCode) {
   using testing::_;
-  using testing::HasSubstr;
+  using testing::Field;
   // TODO: Eq
-  EXPECT_CALL(*pimpl, write(_, HasSubstr("5"))).Times(1);
+  EXPECT_CALL(*layout, format(Field(&logpacket::message, "5"))).Times(1);
   t_logger("", pimpl).errorf("%d", 5);
 }
 
 // TODO: This is wrong
 TEST_F(CLoggerTest, FmtLogHasNameInHeader) {
   using testing::_;
-  using testing::HasSubstr;
-  EXPECT_CALL(*pimpl, write(_, HasSubstr("TEST"))).Times(1);
+  using testing::Field;
+  EXPECT_CALL(*layout, format(Field(&logpacket::logger, "TEST"))).Times(1);
   t_logger("TEST", pimpl).errorf("%d", 5);
 }
 
 // TODO: This is wrong
 TEST_F(CLoggerTest, FmtLogHasLevelInHeader) {
   using testing::_;
-  using testing::HasSubstr;
-  EXPECT_CALL(*pimpl, write(_, HasSubstr("[ERROR]"))).Times(1);
+  using testing::Field;
+  EXPECT_CALL(*layout, format(Field(&logpacket::level, LERROR))).Times(1);
   t_logger("TEST", pimpl).errorf("%d", 5);
 }
 
 TEST_F(CLoggerTest, LogsRawData) {
   using testing::_;
-  using testing::HasSubstr;
-  EXPECT_CALL(*pimpl, write(_, "5")).Times(1);
+  using testing::Field;
+  EXPECT_CALL(*layout, format(Field(&logpacket::message, "5"))).Times(1);
   t_logger("", pimpl).error("5");
 }
 
 TEST_F(CLoggerTest, DoesNotLogAboveLevel) {
   using testing::_;
-  pimpl->SetLogLevel(LFATAL);
-  EXPECT_CALL(*pimpl, write(_)).Times(0);
+  pimpl->min_log_level_ = LFATAL;
+  EXPECT_CALL(*appender, write(_)).Times(0);
   t_logger("", pimpl).errorf("%d", 5);
 }
 
 TEST_F(CLoggerTest, DoesNotRawLogAboveLevel) {
   using testing::_;
-  pimpl->SetLogLevel(LFATAL);
-  EXPECT_CALL(*pimpl, write(_)).Times(0);
+  pimpl->min_log_level_ = LFATAL;
+  EXPECT_CALL(*appender, write(_)).Times(0);
   t_logger("", pimpl).error("5");
 }

+ 160 - 0
test/format_test.cxx

@@ -0,0 +1,160 @@
+//
+//  format_test.cxx
+//  logger_test
+//
+//  Created by Sam Jaffe on 4/4/19.
+//
+
+#include <gmock/gmock.h>
+
+#include "logger/exception.h"
+#include "logger/format.h"
+
+using namespace logging;
+
+TEST(FormatTest, EmptyFormatterCanParse) {
+  EXPECT_NO_THROW(format::parse_format_string(""));
+}
+
+TEST(FormatTest, ThrowsForEndOfStringAfterPct) {
+  EXPECT_THROW(format::parse_format_string("%"),
+               format_parsing_exception);
+}
+
+TEST(FormatTest, RawStringFmtReturnsSelf) {
+  using testing::Eq;
+  auto fmt = format::parse_format_string("TEST STRING");
+  EXPECT_THAT(fmt.process({}), Eq("TEST STRING"));
+}
+
+TEST(FormatTest, NCharReturnsNewLine) {
+  using testing::Eq;
+  auto fmt = format::parse_format_string("%n");
+  EXPECT_THAT(fmt.process({}), Eq("\n"));
+}
+
+TEST(FormatTest, DoublePctIsLiteral) {
+  using testing::Eq;
+  auto fmt = format::parse_format_string("%%");
+  EXPECT_THAT(fmt.process({}), Eq("%"));
+}
+
+TEST(FormatTest, CatchesRawContentBeforeFmt) {
+  using testing::Eq;
+  auto fmt = format::parse_format_string("TEST%%");
+  EXPECT_THAT(fmt.process({}), Eq("TEST%"));
+}
+
+TEST(FormatTest, CatchesRawContentAfterFmt) {
+  using testing::Eq;
+  auto fmt = format::parse_format_string("%%TEST");
+  EXPECT_THAT(fmt.process({}), Eq("%TEST"));
+}
+
+// Thursday, April 4, 2019 6:17:20 PM GMT
+constexpr const int NOW = 1554401840;
+
+TEST(FormatTest, HandlesDateFormatter) {
+  using testing::Eq;
+  auto fmt = format::parse_format_string("%d");
+  EXPECT_THAT(fmt.process({{NOW,0}}), Eq("2019-04-04 18:17:20,000"));
+}
+
+TEST(FormatTest, FormatsMilliseconds) {
+  using testing::Eq;
+  auto fmt = format::parse_format_string("%d");
+  EXPECT_THAT(fmt.process({{NOW,123000}}), Eq("2019-04-04 18:17:20,123"));
+}
+
+TEST(FormatTest, ThrowsIfCustomFmtUnterminated) {
+  using testing::Eq;
+  EXPECT_THROW(format::parse_format_string("%d{%"),
+               format_parsing_exception);
+}
+
+TEST(FormatTest, SupportsCustomFormatWithBrace) {
+  using testing::Eq;
+  auto fmt = format::parse_format_string("%d{%Y}");
+  EXPECT_THAT(fmt.process({{NOW,0}}), Eq("2019"));
+}
+
+TEST(FormatTest, FormatsCustomMilliseconds) {
+  using testing::Eq;
+  auto fmt = format::parse_format_string("%d{%_ms}");
+  EXPECT_THAT(fmt.process({{NOW,123000}}), Eq("123"));
+}
+
+TEST(FormatTest, SupportsISO8601Format) {
+  using testing::Eq;
+  auto fmt = format::parse_format_string("%d{ISO8601}");
+  EXPECT_THAT(fmt.process({{NOW,0}}), Eq("2019-04-04T18:17:20.000Z"));
+}
+
+TEST(FormatTest, SupportsSingleDayFormat) {
+  using testing::Eq;
+  auto fmt = format::parse_format_string("%d{ABSOLUTE}");
+  EXPECT_THAT(fmt.process({{NOW,0}}), Eq("18:17:20,000"));
+}
+
+TEST(FormatTest, SupportsHumanDateFormat) {
+  using testing::Eq;
+  auto fmt = format::parse_format_string("%d{DATE}");
+  EXPECT_THAT(fmt.process({{NOW,0}}), Eq("04 Apr 2019 18:17:20,000"));
+}
+
+TEST(FormatTest, LoggerIdIsCToken) {
+  using testing::Eq;
+  auto fmt = format::parse_format_string("%c");
+  EXPECT_THAT(fmt.process({{}, LERROR, {}, "UNIT_TEST", "HELLO"}),
+              Eq("UNIT_TEST"));
+}
+
+TEST(FormatTest, LogLevelIsPToken) {
+  using testing::Eq;
+  auto fmt = format::parse_format_string("%p");
+  EXPECT_THAT(fmt.process({{}, LERROR, {}, "UNIT_TEST", "HELLO"}),
+              Eq("ERROR"));
+}
+
+TEST(FormatTest, LogMessageIsMToken) {
+  using testing::Eq;
+  auto fmt = format::parse_format_string("%m");
+  EXPECT_THAT(fmt.process({{}, LERROR, {}, "UNIT_TEST", "HELLO"}),
+              Eq("HELLO"));
+}
+
+TEST(FormatTest, ThrowsOnUnknownToken) {
+  using testing::Eq;
+  EXPECT_THROW(format::parse_format_string("%q"),
+               unknown_format_specifier);
+}
+
+TEST(FormatTest, TokenCanBeTruncatedInFormat) {
+  using testing::Eq;
+  auto fmt = format::parse_format_string("%.3m");
+  EXPECT_THAT(fmt.process({{}, LERROR, {}, "UNIT_TEST", "HELLO"}),
+              Eq("HEL"));
+}
+
+TEST(FormatTest, TokenCanBeLeftPadded) {
+  using testing::Eq;
+  auto fmt = format::parse_format_string("%6m");
+  EXPECT_THAT(fmt.process({{}, LERROR, {}, "UNIT_TEST", "HELLO"}),
+              Eq(" HELLO"));
+}
+
+TEST(FormatTest, TokenCanBeRightPadded) {
+  using testing::Eq;
+  auto fmt = format::parse_format_string("%-6m");
+  EXPECT_THAT(fmt.process({{}, LERROR, {}, "UNIT_TEST", "HELLO"}),
+              Eq("HELLO "));
+}
+
+TEST(FormatTest, TokenCanBeSizeBound) {
+  using testing::Eq;
+  auto fmt = format::parse_format_string("%6.8m");
+  EXPECT_THAT(fmt.process({{}, LERROR, {}, "UNIT_TEST", "HELLO"}),
+              Eq(" HELLO"));
+  EXPECT_THAT(fmt.process({{}, LERROR, {}, "UNIT_TEST", "HELLO FRIEND"}),
+              Eq("HELLO FR"));
+}

+ 22 - 12
test/log_manager_test.cxx

@@ -9,7 +9,6 @@
 
 #include "logger/c_logger.h"
 #include "logger/log_manager.h"
-#include "logger/logger_impl.h"
 #include "logger/properties.h"
 
 #include "mock_logger.h"
@@ -21,20 +20,27 @@ public:
   void SetUp() override;
   void TearDown() override;
 protected:
-  std::shared_ptr<MockLoggerImpl> logger;
+  std::shared_ptr<MockAppender> appender;
+  std::shared_ptr<MockLayout> layout;
 private:
-  impl_factory::scoped_binding binding_;
+  appenders::scoped_binding abinding_;
+  layouts::scoped_binding lbinding_;
 };
 
 void LogManagerTest::SetUp() {
   auto GetMock = [this](properties const &) {
-    return logger = std::make_shared<MockLoggerImpl>();
+    return appender = std::make_shared<MockAppender>();
   };
-  binding_ = impl_factory::instance().bind_scoped("Mock", GetMock);
+  abinding_ = appenders::instance().bind_scoped("Mock", GetMock);
+  auto GetMockLayout = [this](properties const &) {
+    return layout = std::make_shared<MockLayout>();
+  };
+  lbinding_ = layouts::instance().bind_scoped("MockLayout", GetMockLayout);
 }
 
 void LogManagerTest::TearDown() {
-  binding_.reset();
+  abinding_.reset();
+  lbinding_.reset();
 }
 
 extern properties const MIN_PROPERTY_SCHEMA;
@@ -45,7 +51,7 @@ TEST_F(LogManagerTest, CanInjectMock) {
 
   using ::testing::NotNull;
 
-  EXPECT_THAT(logger, ::testing::NotNull());
+  EXPECT_THAT(appender, ::testing::NotNull());
 }
 
 TEST_F(LogManagerTest, CanFetchInjectedMock) {
@@ -54,8 +60,10 @@ TEST_F(LogManagerTest, CanFetchInjectedMock) {
   
   using ::testing::_;
   using ::testing::AnyNumber;
-  EXPECT_CALL(*logger, flush()).Times(AnyNumber());
-  EXPECT_CALL(*logger, write(_, "TEST MESSAGE"));
+  using ::testing::Field;
+  EXPECT_CALL(*appender, flush()).Times(AnyNumber());
+  EXPECT_CALL(*appender, write("TEST MESSAGE"));
+  EXPECT_CALL(*layout, format(_)).WillRepeatedly(ReturnMessage());
 
   c_logger l = mgr.c_get();
   l.error("TEST MESSAGE");
@@ -67,9 +75,11 @@ TEST_F(LogManagerTest, MultiplexMockLogsToMultipleImpls) {
   
   using ::testing::_;
   using ::testing::AnyNumber;
-  EXPECT_CALL(*logger, flush()).Times(AnyNumber());
-  EXPECT_CALL(*logger, write(_, "TEST MESSAGE")).Times(2);
-  
+  using ::testing::Field;
+  EXPECT_CALL(*appender, flush()).Times(AnyNumber());
+  EXPECT_CALL(*appender, write("TEST MESSAGE")).Times(2);
+  EXPECT_CALL(*layout, format(_)).WillRepeatedly(ReturnMessage());
+
   c_logger l = mgr.c_get();
   l.error("TEST MESSAGE");
 }

+ 6 - 6
test/logger_test.cxx

@@ -19,30 +19,30 @@ struct t_logger : public logger {
 }
 
 TEST_F(LoggerTest, FlushesOnClose) {
-  EXPECT_CALL(*pimpl, flush()).Times(1);
+  EXPECT_CALL(*appender, flush()).Times(1);
   t_logger("", pimpl);
 }
 
 TEST_F(LoggerTest, FlushesOnFlushCall) {
-  EXPECT_CALL(*pimpl, flush()).Times(2);
+  EXPECT_CALL(*appender, flush()).Times(2);
   t_logger("", pimpl).flush();
 }
 
 TEST_F(LoggerTest, LogsWithBraceFmtCode) {
   using testing::Field;
-  EXPECT_CALL(*pimpl, write(Field(&logpacket::message, "5"))).Times(1);
+  EXPECT_CALL(*layout, format(Field(&logpacket::message, "5"))).Times(1);
   t_logger("", pimpl).log(LERROR, "{}", 5);
 }
 
 TEST_F(LoggerTest, DoesNotLogAboveLevel) {
   using testing::_;
-  pimpl->SetLogLevel(LFATAL);
-  EXPECT_CALL(*pimpl, write(_)).Times(0);
+  pimpl->min_log_level_ = LFATAL;
+  EXPECT_CALL(*appender, write(_)).Times(0);
   t_logger("", pimpl).log(LERROR, "{}", 5);
 }
 
 TEST_F(LoggerTest, LogCurlyBraceLiteralByDoubling) {
   using testing::Field;
-  EXPECT_CALL(*pimpl, write(Field(&logpacket::message, "{}"))).Times(1);
+  EXPECT_CALL(*layout, format(Field(&logpacket::message, "{}"))).Times(1);
   t_logger("", pimpl).log(LERROR, "{{}}", 5);
 }

+ 29 - 8
test/mock_logger.h

@@ -10,7 +10,9 @@
 
 #include <gmock/gmock.h>
 
-#include "logger/logger_impl.h"
+#include "logger/detail/appender.h"
+#include "logger/detail/layout.h"
+#include "logger/detail/logger_impl.h"
 
 namespace logging {
   inline void PrintTo(location_info const & info, std::ostream * os) {
@@ -29,25 +31,44 @@ namespace logging {
   }
 }
 
-struct MockLoggerImpl : public logging::logger_impl {
-  MockLoggerImpl() { SetLogLevel(logging::LTRACE); }
+struct MockAppender : public logging::appender {
+  MockAppender() { SetLogLevel(logging::LTRACE); }
   void SetLogLevel(logging::log_level ll) { min_log_level = ll; }
   MOCK_METHOD0(flush, void());
-  MOCK_METHOD1(write, void(logging::logpacket const &));
-  MOCK_METHOD2(write, void(logging::log_level, std::string const &));
+  MOCK_METHOD1(write, void(std::string const &));
 };
 
+struct MockLayout : public logging::layout {
+  MOCK_CONST_METHOD1(format, std::string(logging::logpacket const &));
+};
+
+ACTION(ReturnMessage) {
+  return arg0.message;
+}
+
 struct LoggerTest : public testing::Test {
   void SetUp() override {
-    pimpl.reset(new MockLoggerImpl);
+    appender.reset(new MockAppender);
+    layout.reset(new MockLayout);
+    pimpl = std::make_shared<logging::logger_impl>();
+    pimpl->impls_.push_back({appender, layout});
+    
+    using testing::_;
     using testing::AnyNumber;
-    EXPECT_CALL(*pimpl, flush()).Times(AnyNumber());
+
+    EXPECT_CALL(*appender, write(_)).Times(AnyNumber());
+    EXPECT_CALL(*appender, flush()).Times(AnyNumber());
+    ON_CALL(*layout, format(_)).WillByDefault(ReturnMessage());
   }
   void TearDown() override {
     pimpl.reset();
+    layout.reset();
+    appender.reset();
   }
   
-  std::shared_ptr<MockLoggerImpl> pimpl;
+  std::shared_ptr<MockAppender> appender;
+  std::shared_ptr<MockLayout> layout;
+  std::shared_ptr<logging::logger_impl> pimpl;
 };
 
 #endif /* mock_logger_h */

+ 6 - 2
test/test_properties.cxx

@@ -27,7 +27,9 @@ extern properties const MULTIPLEX_PROPERTY_SCHEMA;
 properties const MIN_PROPERTY_SCHEMA{_property({
   {"configuration", _property({
     {"appenders", _property({
-      {"Mock", _property({})}
+      {"Mock", _property({
+        {"MockLayout", _value("")}
+      })}
     })},
     {"loggers", _property({
       {"root", _property({
@@ -42,7 +44,9 @@ properties const MIN_PROPERTY_SCHEMA{_property({
 properties const MULTIPLEX_PROPERTY_SCHEMA{_property({
   {"configuration", _property({
     {"appenders", _property({
-      {"Mock", _property({})}
+      {"Mock", _property({
+        {"MockLayout", _value("")}
+      })}
     })},
     {"loggers", _property({
       {"root", _property({