Browse Source

Merge branch 'header_footer'

* header_footer:
  Add tests for JSONLayout completeness.
  Move LoggingTest object to own header.
  Test header and footer for PatternLayout
  Clean up the Pattern Layout code.
  Move code around and fix build errors.
  Support complete log in JsonLayout and header/footer PatternLayout.
  Remove logger_fwd.h
  Remove direct access to format.cxx
  Remove some unhelpful headers. Move logger_impl into the source-code section since it's not a public header.
  Convert to using a slightly smarter wrapper object that manages the relation between the layout manager and the appender.
Sam Jaffe 6 years ago
parent
commit
c35b98439c

+ 2 - 1
include/logger/c_logger.h

@@ -13,7 +13,7 @@
 #include <memory>
 #include <string>
 
-#include "logger_fwd.h"
+#include "level.h"
 
 #define VA_LOGN(level, size)          \
   do {                                \
@@ -34,6 +34,7 @@
   inline void lvl(std::string const & msg) { log(level::lvl, msg); }
 
 namespace logging {
+  enum class level : int;
   class logger_impl;
   
   class c_logger {

+ 17 - 8
include/logger/detail/appender.h

@@ -1,20 +1,29 @@
 #pragma once
 
-#include "logger/logger_fwd.h"
+#include <iosfwd>
 
 namespace logging {
   class layout;
+  struct logpacket;
+  class properties;
+  enum class level : int;
+  
   struct appender {
-    appender(level min = level::debug) : min_log_level(min) {}
     virtual ~appender() = default;
-    virtual void write(logpacket const & pkt) = 0;
+    virtual std::ostream & stream() = 0;
+    virtual void write(logpacket const & packet, layout & withLayout) = 0;
+    virtual bool should_log(level ll) const = 0;
     virtual void flush() = 0;
+  };
+  
+  struct simple_appender : appender {
+    simple_appender() = default;
+    simple_appender(properties const & props);
     
-    bool should_log(level ll) const {
-      return ll >= min_log_level;
-    }
+    void write(logpacket const & packet, layout & withLayout) override;
+    bool should_log(level ll) const override;
+    void flush() override;
     
-    std::shared_ptr<layout> layout;
-    level min_log_level;
+    level threshold;
   };
 }

+ 5 - 2
include/logger/detail/layout.h

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

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

@@ -1,20 +0,0 @@
-#pragma once
-
-#include "logger/logger_fwd.h"
-
-#include <memory>
-#include <vector>
-#include <utility>
-
-namespace logging {
-  class appender;
-
-  struct logger_impl {
-    bool should_log(level ll) const;
-    void write(logpacket const & pkt);
-    void flush();
-
-    std::vector<std::shared_ptr<appender>> impls;
-    level min_log_level;
-  };
-}

+ 0 - 21
include/logger/detail/to_json.h

@@ -1,21 +0,0 @@
-//
-//  to_json.h
-//  logger
-//
-//  Created by Sam Jaffe on 4/4/19.
-//
-
-#pragma once
-
-#include <string>
-
-#include <json/json.h>
-
-namespace logging { namespace detail {
-  template <typename T> std::string to_string(T const & obj);
-  
-  template <typename T>
-  Json::Value to_json(T const & obj) {
-    return to_string(obj);
-  }
-} }

+ 0 - 25
include/logger/detail/to_string.h

@@ -1,25 +0,0 @@
-//
-//  to_string.h
-//  logger
-//
-//  Created by Sam Jaffe on 4/4/19.
-//
-
-#pragma once
-
-#include <iostream>
-#include <sstream>
-
-namespace logging { namespace detail {
-  template <typename T>
-  void to_stream(T const & obj, std::ostream & os) {
-    os << obj;
-  }
-
-  template <typename T>
-  std::string to_string(T const & obj) {
-    std::stringstream ss;
-    to_stream(obj, ss);
-    return ss.str();
-  }
-} }

+ 1 - 19
include/logger/logger_fwd.h

@@ -11,25 +11,6 @@
 
 #define log_here { __FILE__, __LINE__, __FUNCTION__ }
 
-namespace logging {
-  enum class level : int;
-  struct logpacket;
-  
-  std::ostream & operator<<(std::ostream & os, level l);
-  
-  struct location_info {
-    char const * filename = "???";
-    int line = 0;
-    char const * function = "";
-  };
-  
-#if defined( _WIN32 )
-  constexpr char const * const NEWLINE_TOKEN{"\r\n"};
-#else
-  constexpr char const * const NEWLINE_TOKEN{"\n"};
-#endif
-}
-
 #define LIST_OF_LOGGING_LEVELS \
   X(trace) X(debug) X(info) X(warning) X(error) X(critical) X(fatal) X(none)
 #define X(token) token,
@@ -37,5 +18,6 @@ namespace logging {
   enum class level : int {
     LIST_OF_LOGGING_LEVELS warn = warning
   };
+  std::ostream & operator<<(std::ostream & os, level l);
 }
 #undef X

+ 9 - 6
include/logger/logger.h

@@ -23,15 +23,16 @@
 #include <memory>
 #include <string>
 
-#include "logger_fwd.h"
-#include "format.h"
+#include "message.h"
 
 #define log_message( logger, lvl, ... )   \
   logger.log(level::lvl, log_here, __VA_ARGS__)
 
 namespace logging {
+  enum class level : int;
+  struct location_info;
   class logger_impl;
-  class manager;
+  struct logpacket;
 
   class logger {
   public:
@@ -39,15 +40,17 @@ namespace logging {
 
     template <typename... Args>
     inline void log(level ll, std::string const & interp, Args && ...args) {
-      log(ll, location_info{}, interp, std::forward<Args>(args)...);
+      log(ll, message(interp, std::forward<Args>(args)...));
     }
     
     template <typename... Args>
-    inline void log(level ll, location_info info,
+    inline void log(level ll, location_info const & info,
                     std::string const & interp, Args && ...args) {
       log(ll, info, message(interp, std::forward<Args>(args)...));
     }
     
+    void log(level ll, message const&);
+
     void flush();
     
   protected:
@@ -55,7 +58,7 @@ namespace logging {
     
   private:
     friend class manager;
-    void log(level ll, location_info info, message const&);
+    void log(level ll, location_info const & info, message const&);
 
   private:
     std::string const logger_name_;

+ 14 - 2
include/logger/logpacket.h

@@ -1,10 +1,16 @@
 
 #pragma once
 
-#include "logger_fwd.h"
-#include "format.h"
+#include "level.h"
+#include "message.h"
 
 namespace logging {
+  struct location_info {
+    char const * filename = "???";
+    int line = 0;
+    char const * function = "";
+  };
+  
   struct logpacket {
     struct timeval time;
     //    int thread_id;
@@ -13,4 +19,10 @@ namespace logging {
     std::string logger;
     message message;
   };
+  
+#if defined( _WIN32 )
+  constexpr char const * const NEWLINE_TOKEN{"\r\n"};
+#else
+  constexpr char const * const NEWLINE_TOKEN{"\n"};
+#endif
 }

+ 1 - 22
include/logger/format.h

@@ -7,33 +7,12 @@
 
 #pragma once
 
-#include <functional>
-#include <sstream>
 #include <string>
 #include <vector>
 
-#include "logger_fwd.h"
 #include "wrapper_object.h"
 
-namespace logging {
-  class format {
-  public:
-    enum class token_id {
-      DATE, PRIORITY, CATEGORY, MESSAGE, STRING, NEWLINE
-    };
-    
-    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:
-    std::vector<generator> gen;
-  };
-    
-  using string_generator = std::function<std::string(logpacket const &)>;
-  string_generator get_date_formatter(std::string fmt);
-  
+namespace logging {  
   class message {
   public:
     message() = default;

+ 18 - 2
include/logger/wrapper_object.h

@@ -9,9 +9,9 @@
 
 #include <iostream>
 #include <string>
+#include <sstream>
 
-#include "detail/to_string.h"
-#include "detail/to_json.h"
+#include <json/json.h>
 
 namespace logging { namespace detail {
   class object {
@@ -33,6 +33,22 @@ namespace logging { namespace detail {
     Json::Value (*to_json_)(void*);
   };
   
+  template <typename T>
+  void to_stream(T const & obj, std::ostream & os) {
+    os << obj;
+  }
+  
+  template <typename T>
+  std::string to_string(T const & obj) {
+    std::stringstream ss;
+    to_stream(obj, ss);
+    return ss.str();
+  }
+  
+  template <typename T>
+  Json::Value to_json(T const & obj) {
+    return to_string(obj);
+  }
   
   template <typename T>
   std::string object::to_string_impl(void * ptr) {

+ 20 - 12
logger.xcodeproj/project.pbxproj

@@ -15,10 +15,9 @@
 		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 */; };
+		CD3C80C01D6A2CA300ACC795 /* pattern_layout_format.cxx in Sources */ = {isa = PBXBuildFile; fileRef = CD3C80BE1D6A2CA300ACC795 /* pattern_layout_format.cxx */; };
 		CD6F73EC225187BE0081ED74 /* logger in Headers */ = {isa = PBXBuildFile; fileRef = CD6F73EA225187A10081ED74 /* logger */; settings = {ATTRIBUTES = (Public, ); }; };
 		CD6F7406225187F40081ED74 /* liblogging.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 0ECAC4AF1BC00AC500FDAE14 /* liblogging.dylib */; };
 		CD6F740C225187FD0081ED74 /* logger_test.cxx in Sources */ = {isa = PBXBuildFile; fileRef = CD6F73FC225187E10081ED74 /* logger_test.cxx */; };
@@ -32,7 +31,8 @@
 		CD88E9572252BDFC00927F40 /* log_manager.cxx in Sources */ = {isa = PBXBuildFile; fileRef = CD88E9552252BDFC00927F40 /* log_manager.cxx */; };
 		CD88E95F2252D3EF00927F40 /* c_logger.cxx in Sources */ = {isa = PBXBuildFile; fileRef = CD88E95D2252D3EF00927F40 /* c_logger.cxx */; };
 		CD88E9632252D67A00927F40 /* common.cxx in Sources */ = {isa = PBXBuildFile; fileRef = CD88E9612252D67A00927F40 /* common.cxx */; };
-		CDA494DE2256D5F40041620C /* date_format.cxx in Sources */ = {isa = PBXBuildFile; fileRef = CDA494DD2256D5F40041620C /* date_format.cxx */; };
+		CDA494DE2256D5F40041620C /* pattern_layout_date.cxx in Sources */ = {isa = PBXBuildFile; fileRef = CDA494DD2256D5F40041620C /* pattern_layout_date.cxx */; };
+		CDC0E0512269378E001EDAB7 /* message.cxx in Sources */ = {isa = PBXBuildFile; fileRef = CDC0E0502269378E001EDAB7 /* message.cxx */; };
 		CDEA62D5225A3B0B00A6FAE0 /* json_layout.cxx in Sources */ = {isa = PBXBuildFile; fileRef = CDEA62D4225A3B0B00A6FAE0 /* json_layout.cxx */; };
 /* End PBXBuildFile section */
 
@@ -86,10 +86,9 @@
 		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>"; };
+		CD3C80BE1D6A2CA300ACC795 /* pattern_layout_format.cxx */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = pattern_layout_format.cxx; sourceTree = "<group>"; };
 		CD6F73EA225187A10081ED74 /* logger */ = {isa = PBXFileReference; lastKnownFileType = folder; name = logger; path = include/logger; sourceTree = "<group>"; };
 		CD6F73FC225187E10081ED74 /* logger_test.cxx */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = logger_test.cxx; sourceTree = "<group>"; };
 		CD6F7401225187F40081ED74 /* logger_test.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = logger_test.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -106,7 +105,12 @@
 		CD88E95D2252D3EF00927F40 /* c_logger.cxx */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = c_logger.cxx; sourceTree = "<group>"; };
 		CD88E9612252D67A00927F40 /* common.cxx */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = common.cxx; sourceTree = "<group>"; };
 		CD88E9642252D6C700927F40 /* common.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = common.h; sourceTree = "<group>"; };
-		CDA494DD2256D5F40041620C /* date_format.cxx */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = date_format.cxx; sourceTree = "<group>"; };
+		CDA494DD2256D5F40041620C /* pattern_layout_date.cxx */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = pattern_layout_date.cxx; sourceTree = "<group>"; };
+		CDC0E0472267EA30001EDAB7 /* logger_impl.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = logger_impl.h; sourceTree = "<group>"; };
+		CDC0E04C2267F8A9001EDAB7 /* pattern_layout.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = pattern_layout.h; sourceTree = "<group>"; };
+		CDC0E0502269378E001EDAB7 /* message.cxx */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = message.cxx; sourceTree = "<group>"; };
+		CDC0E06022694966001EDAB7 /* logger_test_obj.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = logger_test_obj.h; sourceTree = "<group>"; };
+		CDC0E0652269EE9E001EDAB7 /* header_test_obj.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = header_test_obj.h; sourceTree = "<group>"; };
 		CDEA62D4225A3B0B00A6FAE0 /* json_layout.cxx */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = json_layout.cxx; sourceTree = "<group>"; };
 /* End PBXFileReference section */
 
@@ -158,7 +162,10 @@
 			isa = PBXGroup;
 			children = (
 				CD1CDE862252E5B900E5B6B2 /* properties.cxx */,
+				CDA494DD2256D5F40041620C /* pattern_layout_date.cxx */,
+				CD3C80BE1D6A2CA300ACC795 /* pattern_layout_format.cxx */,
 				CD1CDEB42256C94000E5B6B2 /* pattern_layout.cxx */,
+				CDC0E04C2267F8A9001EDAB7 /* pattern_layout.h */,
 				CDEA62D4225A3B0B00A6FAE0 /* json_layout.cxx */,
 				CD1CDEB022557FB600E5B6B2 /* default_layout.cxx */,
 				CD1CDE8A2252E61800E5B6B2 /* console_appender.cxx */,
@@ -173,12 +180,12 @@
 				CD1CDE812252E54100E5B6B2 /* loggers */,
 				CD88E95D2252D3EF00927F40 /* c_logger.cxx */,
 				CD2973991D7B401F00E37217 /* logger.cxx */,
+				CDC0E0472267EA30001EDAB7 /* logger_impl.h */,
 				CD1CDEAE22556B7E00E5B6B2 /* logger_impl.cxx */,
 				CD88E9552252BDFC00927F40 /* log_manager.cxx */,
+				CDC0E0502269378E001EDAB7 /* message.cxx */,
 				CD88E9642252D6C700927F40 /* common.h */,
 				CD88E9612252D67A00927F40 /* common.cxx */,
-				CDA494DD2256D5F40041620C /* date_format.cxx */,
-				CD3C80BE1D6A2CA300ACC795 /* format.cxx */,
 			);
 			path = src;
 			sourceTree = "<group>";
@@ -187,7 +194,6 @@
 			isa = PBXGroup;
 			children = (
 				CD6F73FC225187E10081ED74 /* logger_test.cxx */,
-				CD1CDEB22256B04600E5B6B2 /* format_test.cxx */,
 				CD760CB822621776008A62DE /* pattern_layout_test.cxx */,
 				CD760CC822627202008A62DE /* json_layout_test.cxx */,
 				CD760CBE226221F6008A62DE /* console_appender_test.cxx */,
@@ -196,6 +202,8 @@
 				CD1CDE8F22542CC500E5B6B2 /* log_manager_test.cxx */,
 				CD1CDE9122543E7E00E5B6B2 /* test_properties.cxx */,
 				CD1CDE8E22540DEA00E5B6B2 /* mock_logger.h */,
+				CDC0E06022694966001EDAB7 /* logger_test_obj.h */,
+				CDC0E0652269EE9E001EDAB7 /* header_test_obj.h */,
 			);
 			path = test;
 			sourceTree = "<group>";
@@ -370,8 +378,8 @@
 				CD29739B1D7B401F00E37217 /* logger.cxx in Sources */,
 				CDEA62D5225A3B0B00A6FAE0 /* json_layout.cxx in Sources */,
 				CD88E9572252BDFC00927F40 /* log_manager.cxx in Sources */,
-				CDA494DE2256D5F40041620C /* date_format.cxx in Sources */,
-				CD3C80C01D6A2CA300ACC795 /* format.cxx in Sources */,
+				CDA494DE2256D5F40041620C /* pattern_layout_date.cxx in Sources */,
+				CD3C80C01D6A2CA300ACC795 /* pattern_layout_format.cxx in Sources */,
 				CD88E95F2252D3EF00927F40 /* c_logger.cxx in Sources */,
 				CD1CDE872252E5B900E5B6B2 /* properties.cxx in Sources */,
 				CD1CDE8B2252E61800E5B6B2 /* console_appender.cxx in Sources */,
@@ -380,6 +388,7 @@
 				CD1CDEB52256C94000E5B6B2 /* pattern_layout.cxx in Sources */,
 				CD88E9632252D67A00927F40 /* common.cxx in Sources */,
 				CD1CDEB122557FB600E5B6B2 /* default_layout.cxx in Sources */,
+				CDC0E0512269378E001EDAB7 /* message.cxx in Sources */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};
@@ -394,7 +403,6 @@
 				CD760CC1226226CC008A62DE /* file_appender_test.cxx in Sources */,
 				CD1CDE9222543E7E00E5B6B2 /* test_properties.cxx in Sources */,
 				CD760CC922627202008A62DE /* json_layout_test.cxx in Sources */,
-				CD1CDEB32256B04600E5B6B2 /* format_test.cxx in Sources */,
 				CD1CDE8D22540D9B00E5B6B2 /* c_logger_test.cxx in Sources */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;

+ 1 - 1
src/c_logger.cxx

@@ -8,7 +8,7 @@
 #include "logger/c_logger.h"
 
 #include "common.h"
-#include "logger/detail/logger_impl.h"
+#include "logger_impl.h"
 #include "logger/logpacket.h"
 
 using namespace logging;

+ 1 - 0
src/common.cxx

@@ -11,6 +11,7 @@
 #include <ctime>
 
 #include "logger/exception.h"
+#include "logger/logpacket.h"
 
 namespace logging {
   timeval now() {

+ 2 - 2
src/common.h

@@ -7,11 +7,11 @@
 
 #pragma once
 
+#include <string>
 #include <sys/time.h>
 
-#include "logger/logger_fwd.h"
-
 namespace logging {
+  enum class level : int;
   timeval now();
 
   level level_from_string(std::string const & value);

+ 0 - 64
src/date_format.cxx

@@ -1,64 +0,0 @@
-//
-//  date_format.cxx
-//  logging
-//
-//  Created by Sam Jaffe on 4/4/19.
-//
-
-#include <ctime>
-#include <functional>
-#include <string>
-
-#include "logger/exception.h"
-#include "logger/format.h"
-#include "logger/logger_fwd.h"
-#include "logger/logpacket.h"
-
-namespace logging {
-  std::string fmt_time(struct timeval round, char const * const fmt) {
-    struct tm 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;
-  }
-  
-  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/1000);
-    return fmt_time(round, buf);
-  }
-  
-  string_generator get_date_formatter(std::string fmt) {
-    if (fmt.find("%_ms") != std::string::npos) {
-      size_t pos = 0;
-      while ((pos = fmt.find("%", pos)) != std::string::npos) {
-        fmt.replace(pos, 1, "%%");
-        pos += 2;
-      }
-      fmt.replace(fmt.find("%%_ms"), 5, "%.03d");
-      return [=](logpacket const & lp) {
-        return fmt_time_with_milis(lp.time, fmt);
-      };
-    } else {
-      return [=](logpacket const & lp) {
-        return fmt_time(lp.time, fmt.c_str());
-      };
-    }
-  }
-  
-  string_generator parse_date_format_string(char const * str) {
-    char const * const end = strchr(str, '}');
-    if (end == nullptr) {
-      std::string error_msg{"expected date-format specifier to terminate"};
-      throw format_parsing_exception{error_msg};
-    }
-    return get_date_formatter(std::string(str, end));
-  }
-}

+ 0 - 171
src/format.cxx

@@ -1,171 +0,0 @@
-//
-//  format.cpp
-//  logger
-//
-//  Created by Sam Jaffe on 8/21/16.
-//
-
-#include "logger/format.h"
-
-#include <cstdint>
-#include <cstring>
-#include <ctime>
-#include <iomanip>
-#include <iostream>
-#include <map>
-#include <stdexcept>
-#include <string>
-
-#include "common.h"
-#include "logger/exception.h"
-#include "logger/logger.h"
-#include "logger/logpacket.h"
-
-namespace logging {
-  std::string fmt_time_with_milis(struct timeval, std::string const &);
-  string_generator parse_date_format_string(char const *);
-  
-#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 = "%%Y-%%m-%%d %%H:%%M:%%S,%.03d";
-    if (is_string("{ISO8601}")) {
-      predef_format = "%%Y-%%m-%%dT%%H:%%M:%%S.%.03dZ";
-    } else if (is_string("{ABSOLUTE}")) {
-      predef_format = "%%H:%%M:%%S,%.03d";
-    } else if (is_string("{DATE}")) {
-      predef_format = "%%d %%b %%Y %%H:%%M:%%S,%.03d";
-    } else if (is('{')) {
-      return parse_date_format_string(next+1);
-    }
-    return [=](logpacket const & lp ){
-      return fmt_time_with_milis(lp.time, predef_format);
-    };
-  }
-
-  string_generator string_token(std::string str) {
-    return [=](logpacket const &){ return str; };
-  }
-  
-  string_generator handle( char const * & next ) {
-    if (is('c')) {
-      return [](logpacket const & lp){ return lp.logger; };
-    } else if (is('p')) {
-      return [](logpacket const & lp){ return to_string(lp.level, true); };
-    } else if (is('m')) {
-      return [](logpacket const & lp){ return lp.message.str(); };
-    } else if (is('t')) {
-      return [](logpacket const & lp){ return "???"; };
-    } else {
-      std::string error_msg{"unknown format character: '"};
-      throw unknown_format_specifier{error_msg + *next + "'"};
-    }
-  }
-  
-  int eat(char const * & token) {
-    size_t chars = 0;
-    int rval = std::stoi(token, &chars);
-    token += chars;
-    return rval;
-  }
-  
-  std::pair<int, size_t> get_bounds(char const * & next) {
-    int min = *next == '.' ? 0 : eat(next);
-    size_t max = *next == '.' ? eat(++next) : std::string::npos;
-    return std::make_pair(min, max);
-  }
-  
-  format::generator parse_with_bounds(char const * & next) {
-    bool const is_left = *next == '-';
-    auto align = is_left ? std::left : std::right;
-    if (is_left) ++next;
-    
-    auto bounds = get_bounds(next);
-    string_generator gen = handle(next);
-    
-    return [=](logpacket const & lp, std::ostream & out) {
-      std::string str = gen(lp);
-      if (str.length() > bounds.second)
-        str.erase(str.begin()+bounds.second, str.end());
-      out << align << std::setw(bounds.first) << str;
-    };
-  }
-  
-  format::generator convert( string_generator gen ) {
-    return [=](logpacket const & lp, std::ostream & out) {
-      out << gen( lp );
-    };
-  }
-  
-  format format::parse_format_string( std::string const & fmt ) {
-    format out;
-    char const * curr = fmt.c_str();
-    char const * next = nullptr;
-    char const * const end = curr + fmt.size();
-    
-    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) {
-        out.gen.push_back(convert(string_token({curr, next - 1})));
-      }
-
-      if (is('d')) {
-        ++next;
-        out.gen.push_back(convert(date_token(next)));
-        if (is('{')) next = std::strchr(next, '}');
-      } else if (is('n')) {
-        out.gen.push_back(convert(string_token(NEWLINE_TOKEN)));
-      } else if (is('%')) {
-        out.gen.push_back(convert(string_token("%")));
-      } else if (is('.') || is('-') || isdigit( *next )) {
-        out.gen.push_back(parse_with_bounds(next));
-      } else {
-        out.gen.push_back(convert(handle(next)));
-      }
-      curr = ++next;
-    }
-    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();
-  }
-
-  void format_msg(std::ostream & os, std::string const & interp, size_t pos,
-                  std::vector<detail::object> const & objs, size_t idx) {
-    size_t next = interp.find("{}", pos);
-    os << interp.substr(pos, next);
-    if (next == std::string::npos) {
-      return; // throw?
-    } else if (!strncmp(interp.c_str() + next - 1, "{{}}", 4)) {
-      format_msg(os, interp, next+2, objs, idx);
-    } else {
-      format_msg(os << objs[idx], interp, next+2, objs, idx+1);
-    }
-  }
-  
-  std::string message::str() const {
-    std::stringstream ss;
-    format_msg(ss, format_code, 0, objects, 0);
-    return ss.str();
-  }
-  
-  Json::Value message::json() const {
-    return objects[0].json();
-  }
-}

+ 7 - 10
src/log_manager.cxx

@@ -14,20 +14,18 @@
 #include "resource_factory/prototype_factory.hpp"
 
 #include "common.h"
+#include "logger_impl.h"
 #include "logger/c_logger.h"
-#include "logger/detail/appender.h"
 #include "logger/exception.h"
 #include "logger/logger.h"
 #include "logger/properties.h"
-#include "logger_impl.h"
 
 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_logapp = std::shared_ptr<log_appender>;
 using p_logger = std::shared_ptr<logger_impl>;
 
 struct logging::manager_impl {
@@ -39,7 +37,7 @@ struct logging::manager_impl {
   void configure_loggers(properties const & props);
 
   p_logger default_logger;
-  cache<p_appender> appenders;
+  cache<p_logapp> appenders;
   cache<p_logger> loggers;
 };
 
@@ -85,8 +83,8 @@ static p_layout load_layout(std::string const & source,
 
 template <typename T>
 void inject_log_level(properties const & props, T & val) {
-  if (props.contains("level")) {
-    val.min_log_level = level_from_string(props["level"]);
+  if (props.contains("threshold")) {
+    val.min_log_level = level_from_string(props["threshold"]);
   }
 }
 
@@ -94,9 +92,8 @@ void manager_impl::configure_appenders(properties const & props) {
   // TODO: Support multiple File loggers, etc.
   for (auto & app : props["appenders"].object()) {
     auto appender = load_appender(app.first, app.second);
-    appender->layout = load_layout(app.first, app.second);
-    inject_log_level(app.second, *appender);
-    appenders[app.first] = appender;
+    auto layout = load_layout(app.first, app.second);
+    appenders[app.first].reset(new log_appender(appender, layout));
   }
 }
 

+ 7 - 2
src/logger.cxx

@@ -8,7 +8,7 @@
 #include "logger/logger.h"
 
 #include "common.h"
-#include "logger/detail/logger_impl.h"
+#include "logger_impl.h"
 #include "logger/logpacket.h"
 
 namespace logging {
@@ -20,7 +20,12 @@ namespace logging {
     if (impl_) impl_->flush();
   }
   
-  void logger::log(level ll, location_info info, message const & msg) {
+  void logger::log(level ll, message const & msg) {
+    impl_->write({ now(), ll, {}, logger_name_, msg });
+  }
+  
+  void logger::log(level ll, location_info const & info,
+                   message const & msg) {
     impl_->write({ now(), ll, info, logger_name_, msg });
   }
   

+ 45 - 0
src/logger_impl.cxx

@@ -7,12 +7,57 @@
 
 #include "logger_impl.h"
 
+#include "common.h"
 #include "logger/detail/appender.h"
 #include "logger/detail/layout.h"
 #include "logger/logpacket.h"
+#include "logger/properties.h"
 
 using namespace logging;
 
+simple_appender::simple_appender(properties const & props)
+: threshold(level_from_string(props["threshold"])) {}
+
+void simple_appender::write(logpacket const & packet, layout & withLayout) {
+  withLayout.format(stream(), packet);
+}
+
+bool simple_appender::should_log(level ll) const {
+  return ll >= threshold;
+}
+
+void simple_appender::flush() {
+  stream().flush();
+}
+
+log_appender::log_appender(p_appender append, p_layout layout)
+: appender_(append),
+  layout_(layout),
+  has_logged_(false)
+{
+  appender_->stream() << layout_->header();
+}
+
+log_appender::~log_appender() {
+  appender_->stream() << layout_->footer();
+}
+
+void log_appender::write(logpacket const & packet) {
+  if (has_logged_) {
+    appender_->stream() << layout_->separator();
+  }
+  appender_->write(packet, *layout_);
+  has_logged_ = true;
+}
+
+bool log_appender::should_log(level ll) const {
+  return appender_->should_log(ll);
+}
+
+void log_appender::flush() {
+  appender_->flush();
+}
+
 bool logger_impl::should_log(level ll) const {
   return ll >= min_log_level;
 }

+ 38 - 0
src/logger_impl.h

@@ -0,0 +1,38 @@
+#pragma once
+
+#include <memory>
+#include <vector>
+#include <utility>
+
+namespace logging {
+  class appender;
+  class layout;
+  enum class level : int;
+  struct logpacket;
+  using p_appender = std::shared_ptr<appender>;
+  using p_layout = std::shared_ptr<layout>;
+
+  class log_appender {
+  public:
+    log_appender(p_appender append, p_layout layout);
+    ~log_appender();
+    
+    void write(logpacket const & packet);
+    bool should_log(level ll) const;
+    void flush();
+    
+  private:
+    p_appender appender_;
+    p_layout layout_;
+    bool has_logged_;
+  };
+
+  struct logger_impl {
+    bool should_log(level ll) const;
+    void write(logpacket const & pkt);
+    void flush();
+
+    std::vector<std::shared_ptr<log_appender>> impls;
+    level min_log_level;
+  };
+}

+ 18 - 17
src/loggers/console_appender.cxx

@@ -17,39 +17,40 @@
 
 using namespace logging;
 
-class console_appender : public appender {
+class console_appender : public simple_appender {
 public:
   static std::shared_ptr<appender> create(properties const& props);
   
-  explicit console_appender(std::ostream& os);
+  explicit console_appender(properties const& props);
   
-  void write(logpacket const & pkt) override;
-  
-  void flush() override;
+  std::ostream & stream() override { return out_; }
 private:
   std::ostream& out_;
 };
 
+using namespace logging::property;
+properties const DEFAULT_CONSOLE_APPENDER{_obj({
+  {"target", {}},
+  {"threshold", _v("ERROR")}
+})};
+
 std::shared_ptr<appender> console_appender::create(properties const& props) {
-  const std::string target = props["target"];
+  properties const actual = DEFAULT_CONSOLE_APPENDER.mergedWith(props);
+  return std::make_shared<console_appender>(actual);
+}
+
+static std::ostream & get_ostream(std::string const & target) {
   if (target == "SYSTEM_OUT") {
-    return std::make_shared<console_appender>(std::cout);
+    return std::cout;
   } else if (target == "SYSTEM_ERR") {
-    return std::make_shared<console_appender>(std::cerr);
+    return std::cerr;
   } else {
     throw std::logic_error{target + " is not a valid console"};
   }
 }
 
-console_appender::console_appender(std::ostream& os) : out_(os) {}
-
-void console_appender::write(logpacket const & pkt) {
-  layout->format(out_, pkt);
-}
-
-void console_appender::flush() {
-  out_.flush();
-}
+console_appender::console_appender(properties const& props)
+: simple_appender(props), out_(get_ostream(props["target"])) {}
 
 namespace {
   bool _ = appenders::instance().bind("Console", console_appender::create);

+ 7 - 13
src/loggers/file_appender.cxx

@@ -17,25 +17,21 @@
 
 using namespace logging;
 
-namespace logging {
-  level level_from_string(std::string const & value);
-}
-
 struct buffer : std::filebuf {
   buffer(properties const & props);
   buffer * setbuf(char* data, std::streamsize n);
 };
 
-class file_appender : public appender {
+class file_appender : public simple_appender {
 public:
   static std::shared_ptr<appender> create(properties const & props);
   
   explicit file_appender(properties const & props);
   
-  void write(logpacket const & pkt) override;
-  void flush() override;
+  std::ostream & stream() override { return out_; }
+  void write(logpacket const & packet, layout & withLayout) override;
 private:
-  bool flush_immediately_{false};
+  bool flush_immediately_;
   buffer rdbuf_;
   std::ostream out_;
   std::unique_ptr<char[]> buffer_;
@@ -72,7 +68,7 @@ buffer * buffer::setbuf(char* data, std::streamsize n) {
 }
 
 file_appender::file_appender(properties const & props)
-: appender(level_from_string(props["threshold"])),
+: simple_appender(props),
   flush_immediately_(props["immediateFlush"]),
   rdbuf_(props), out_(&rdbuf_) {
   if (props["bufferedIO"]) {
@@ -82,13 +78,11 @@ file_appender::file_appender(properties const & props)
   }
 }
 
-void file_appender::write(logpacket const & pkt) {
-  layout->format(out_, pkt);
+void file_appender::write(logpacket const & packet, layout & withLayout) {
+  withLayout.format(out_, packet);
   if (flush_immediately_) { flush(); }
 }
 
-void file_appender::flush() { out_.flush(); }
-
 namespace {
   bool _ = appenders::instance().bind("File", file_appender::create);
 }

+ 7 - 3
src/loggers/json_layout.cxx

@@ -24,10 +24,13 @@ public:
   
   json_layout(properties const & props);
   void format(std::ostream & os, logpacket const & pkt) const override;
-  
+  std::string header() const override { return complete_ ? "[" : ""; }
+  std::string footer() const override { return complete_ ? "]" : ""; }
+  std::string separator() const override { return complete_ ? "," : ""; }
+
 private:
   Json::StreamWriterBuilder build;
-  bool eol_, log_as_json_, include_location_;
+  bool eol_, log_as_json_, include_location_, complete_;
 };
 
 using namespace logging::property;
@@ -49,7 +52,8 @@ std::shared_ptr<layout> json_layout::create(properties const & props) {
 json_layout::json_layout(properties const & props)
 : eol_(props["eventEol"]),
   log_as_json_(props["objectMessageAsJsonObject"]),
-  include_location_(props["locationInfo"])
+  include_location_(props["locationInfo"]),
+  complete_(props["complete"])
 {
   build["indentation"] = props["compact"] ? "" : "  ";
 }

+ 13 - 4
src/loggers/pattern_layout.cxx

@@ -8,9 +8,9 @@
 #include "resource_factory/prototype_factory.hpp"
 
 #include "logger/detail/layout.h"
-#include "logger/format.h"
 #include "logger/log_manager.h"
 #include "logger/properties.h"
+#include "pattern_layout.h"
 
 using namespace logging;
 
@@ -19,8 +19,12 @@ struct pattern_layout : public layout {
   
   pattern_layout(properties const & props);
   void format(std::ostream & os, logpacket const & pkt) const override;
-  
+  std::string header() const override { return header_; }
+  std::string footer() const override { return footer_; }
+
   class format formatter;
+  std::string header_;
+  std::string footer_;
 };
 
 using namespace logging::property;
@@ -40,8 +44,13 @@ std::shared_ptr<layout> pattern_layout::create(properties const & props) {
   return std::make_shared<pattern_layout>(actual);
 }
 
-pattern_layout::pattern_layout(properties const & props) :
-  formatter(format::parse_format_string(props["pattern"])) {}
+pattern_layout::pattern_layout(properties const & props)
+: formatter(format::parse_format_string(props["pattern"])),
+  header_(props["header"]),
+  footer_(props["footer"])
+{
+  
+}
 
 void pattern_layout::format(std::ostream & os, logpacket const & pkt) const {
   os << formatter.process(pkt);

+ 38 - 0
src/loggers/pattern_layout.h

@@ -0,0 +1,38 @@
+//
+//  pattern_layout.h
+//  logger
+//
+//  Created by Sam Jaffe on 4/17/19.
+//
+
+#pragma once
+
+#include <functional>
+#include <iostream>
+#include <vector>
+
+namespace logging {
+  struct logpacket;
+  class format {
+  public:
+    struct generator_t {
+      virtual ~generator_t() = default;
+      virtual std::string str(logpacket const & lp) const = 0;
+      virtual void write(logpacket const & lp, std::ostream & os) const {
+        os << str(lp);
+      }
+    };
+  public:
+    enum class token_id {
+      DATE, PRIORITY, CATEGORY, MESSAGE, STRING, NEWLINE
+    };
+    
+    using generator = std::shared_ptr<generator_t>;
+    
+    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:
+    std::vector<generator> gen;
+  };
+}

+ 91 - 0
src/loggers/pattern_layout_date.cxx

@@ -0,0 +1,91 @@
+//
+//  date_format.cxx
+//  logging
+//
+//  Created by Sam Jaffe on 4/4/19.
+//
+
+#include <ctime>
+#include <functional>
+#include <string>
+
+#include "logger/exception.h"
+#include "logger/logpacket.h"
+#include "logger/message.h"
+#include "pattern_layout.h"
+
+static std::string fmt_time(struct timeval round, char const * const fmt) {
+  struct tm 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;
+}
+
+static 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/1000);
+  return fmt_time(round, buf);
+}
+
+namespace {
+  struct time_gen : public logging::format::generator_t {
+    enum resolution { seconds, milis };
+    time_gen(std::string const & pre, resolution res)
+    : predef_format(pre), res(res) {}
+    std::string str(logging::logpacket const & lp) const override {
+      switch (res) {
+        case seconds:
+          return fmt_time(lp.time, predef_format.c_str());
+        case milis:
+          return fmt_time_with_milis(lp.time, predef_format);
+      }
+    }
+    std::string predef_format;
+    resolution res;
+  };
+}
+
+namespace logging {
+  format::generator parse_date_format_string(char const * str) {
+    char const * const end = strchr(str, '}');
+    if (end == nullptr) {
+      std::string error_msg{"expected date-format specifier to terminate"};
+      throw format_parsing_exception{error_msg};
+    }
+    std::string fmt(str, end);
+    if (fmt.find("%_ms") != std::string::npos) {
+      size_t pos = 0;
+      while ((pos = fmt.find("%", pos)) != std::string::npos) {
+        fmt.replace(pos, 1, "%%");
+        pos += 2;
+      }
+      fmt.replace(fmt.find("%%_ms"), 5, "%.03d");
+      return std::make_shared<time_gen>(fmt, time_gen::milis);
+    } else {
+      return std::make_shared<time_gen>(fmt, time_gen::seconds);
+    }
+  }
+  
+#define is( chr ) *next == chr
+#define is_string( str ) ! strncmp(next, str, strlen(str))
+  format::generator date_token(char const * next) {
+    std::string predef_format = "%%Y-%%m-%%d %%H:%%M:%%S,%.03d";
+    if (is_string("{ISO8601}")) {
+      predef_format = "%%Y-%%m-%%dT%%H:%%M:%%S.%.03dZ";
+    } else if (is_string("{ABSOLUTE}")) {
+      predef_format = "%%H:%%M:%%S,%.03d";
+    } else if (is_string("{DATE}")) {
+      predef_format = "%%d %%b %%Y %%H:%%M:%%S,%.03d";
+    } else if (is('{')) {
+      return parse_date_format_string(next+1);
+    }
+    return std::make_shared<time_gen>(predef_format, time_gen::milis);
+  }
+}

+ 171 - 0
src/loggers/pattern_layout_format.cxx

@@ -0,0 +1,171 @@
+//
+//  format.cpp
+//  logger
+//
+//  Created by Sam Jaffe on 8/21/16.
+//
+
+#include <cstdint>
+#include <cstring>
+#include <ctime>
+#include <iomanip>
+#include <iostream>
+#include <map>
+#include <stdexcept>
+#include <string>
+
+#include "common.h"
+#include "logger/exception.h"
+#include "logger/logger.h"
+#include "logger/logpacket.h"
+#include "logger/message.h"
+#include "pattern_layout.h"
+
+namespace logging { namespace {
+  struct log_handle_gen : public format::generator_t {
+    std::string str(logpacket const & lp) const override {
+      return lp.logger;
+    }
+    void write(logpacket const & lp, std::ostream & os) const override {
+      os << lp.logger;
+    }
+  };
+
+  struct level_gen : public format::generator_t {
+    std::string str(logpacket const & lp) const override {
+      return to_string(lp.level, true);
+    }
+  };
+
+  struct message_gen : public format::generator_t {
+    std::string str(logpacket const & lp) const override {
+      return lp.message.str();
+    }
+  };
+  
+  struct literal_gen : public format::generator_t {
+    literal_gen(std::string const & value) : value(value) {}
+    std::string str(logpacket const & lp) const override {
+      return value;
+    }
+    void write(logpacket const & lp, std::ostream & os) const override {
+      os << value;
+    }
+    std::string value;
+  };
+  
+  struct bounds_gen : public format::generator_t {
+    bounds_gen(format::generator impl, bool is_left, int min, size_t max)
+    : impl(impl), is_left(is_left), min(min), max(max) {}
+    std::string str(logpacket const & lp) const override {
+      return impl->str(lp);
+    }
+    void write(logpacket const & lp, std::ostream & os) const override {
+      auto align = is_left ? std::left : std::right;
+      std::string str = impl->str(lp);
+      if (str.length() > max) {
+        str.erase(str.begin()+max, str.end());
+      }
+      os << align << std::setw(min) << str;
+    }
+    format::generator impl;
+    bool is_left;
+    int min;
+    size_t max;
+  };
+} }
+
+namespace logging {
+#define is( chr ) *next == chr
+#define is_string( str ) ! strncmp(next, str, strlen(str))
+  format::generator date_token(char const * next);
+
+  format::generator handle( char const * & next ) {
+    if (is('c')) {
+      return std::make_shared<log_handle_gen>();
+    } else if (is('p')) {
+      return std::make_shared<level_gen>();
+    } else if (is('m')) {
+      return std::make_shared<message_gen>();
+    } else if (is('t')) {
+      return std::make_shared<literal_gen>("???");
+    } else {
+      std::string error_msg{"unknown format character: '"};
+      throw unknown_format_specifier{error_msg + *next + "'"};
+    }
+  }
+  
+  int eat(char const * & token) {
+    size_t chars = 0;
+    int rval = std::stoi(token, &chars);
+    token += chars;
+    return rval;
+  }
+  
+  std::pair<int, size_t> get_bounds(char const * & next) {
+    int min = *next == '.' ? 0 : eat(next);
+    size_t max = *next == '.' ? eat(++next) : std::string::npos;
+    return std::make_pair(min, max);
+  }
+  
+  format::generator parse_with_bounds(char const * & next) {
+    bool const is_left = *next == '-';
+    if (is_left) ++next;
+    
+    auto bounds = get_bounds(next);
+    
+    bounds_gen gen{handle(next), is_left, bounds.first, bounds.second};
+    return std::make_shared<bounds_gen>(gen);
+  }
+  
+  format format::parse_format_string( std::string const & fmt ) {
+    format out;
+    char const * curr = fmt.c_str();
+    char const * next = nullptr;
+    char const * const end = curr + fmt.size();
+    
+    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) {
+        std::string lit{curr, next - 1};
+        out.gen.push_back(std::make_shared<literal_gen>(lit));
+      }
+
+      if (is('d')) {
+        ++next;
+        out.gen.push_back(date_token(next));
+        if (is('{')) next = std::strchr(next, '}');
+      } else if (is('n')) {
+        out.gen.push_back(std::make_shared<literal_gen>(NEWLINE_TOKEN));
+      } else if (is('%')) {
+        out.gen.push_back(std::make_shared<literal_gen>("%"));
+      } else if (is('.') || is('-') || isdigit( *next )) {
+        out.gen.push_back(parse_with_bounds(next));
+      } else {
+        out.gen.push_back(handle(next));
+      }
+      curr = ++next;
+    }
+    if (curr < end) {
+      std::string lit{curr, end};
+      out.gen.push_back(std::make_shared<literal_gen>(lit));
+    }
+    return out;
+  }
+#undef is
+  
+  void format::process(logpacket const & pkt, std::ostream & os) const {
+    for (auto func : gen) { func->write(pkt, os); }
+  }
+  
+  std::string format::process(logpacket const & pkt) const {
+    std::stringstream ss;
+    process(pkt, ss);
+    return ss.str();
+  }
+}

+ 35 - 0
src/message.cxx

@@ -0,0 +1,35 @@
+//
+//  message.cxx
+//  logging
+//
+//  Created by Sam Jaffe on 4/18/19.
+//
+
+#include "logger/message.h"
+
+#include <sstream>
+
+namespace logging {
+  void format_msg(std::ostream & os, std::string const & interp, size_t pos,
+                  std::vector<detail::object> const & objs, size_t idx) {
+    size_t next = interp.find("{}", pos);
+    os << interp.substr(pos, next);
+    if (next == std::string::npos) {
+      return; // throw?
+    } else if (!strncmp(interp.c_str() + next - 1, "{{}}", 4)) {
+      format_msg(os, interp, next+2, objs, idx);
+    } else {
+      format_msg(os << objs[idx], interp, next+2, objs, idx+1);
+    }
+  }
+  
+  std::string message::str() const {
+    std::stringstream ss;
+    format_msg(ss, format_code, 0, objects, 0);
+    return ss.str();
+  }
+  
+  Json::Value message::json() const {
+    return objects[0].json();
+  }
+}

+ 11 - 7
test/c_logger_test.cxx

@@ -5,7 +5,7 @@
 //  Created by Sam Jaffe on 4/2/19.
 //
 
-#include "mock_logger.h"
+#include "logger_test_obj.h"
 
 #include "logger/c_logger.h"
 
@@ -31,40 +31,44 @@ TEST_F(CLoggerTest, FlushesOnFlushCall) {
 }
 
 TEST_F(CLoggerTest, LogsWithFmtCode) {
-  EXPECT_CALL(*appender, write(MessageEq("5"))).Times(1);
+  using testing::_;
+  EXPECT_CALL(*appender, write(MessageEq("5"), _)).Times(1);
   t_logger("", pimpl).errorf("%d", 5);
 }
 
 // TODO: This is wrong
 TEST_F(CLoggerTest, FmtLogHasNameInHeader) {
+  using testing::_;
   using testing::Field;
-  EXPECT_CALL(*appender, write(Field(&logpacket::logger, "TEST"))).Times(1);
+  EXPECT_CALL(*appender, write(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::Field;
   auto IsError = Field(&logpacket::level, level::error);
-  EXPECT_CALL(*appender, write(IsError)).Times(1);
+  EXPECT_CALL(*appender, write(IsError, _)).Times(1);
   t_logger("TEST", pimpl).errorf("%d", 5);
 }
 
 TEST_F(CLoggerTest, LogsRawData) {
-  EXPECT_CALL(*appender, write(MessageEq("5"))).Times(1);
+  using testing::_;
+  EXPECT_CALL(*appender, write(MessageEq("5"), _)).Times(1);
   t_logger("", pimpl).error("5");
 }
 
 TEST_F(CLoggerTest, DoesNotLogAboveLevel) {
   using testing::_;
   pimpl->min_log_level = level::fatal;
-  EXPECT_CALL(*appender, write(_)).Times(0);
+  EXPECT_CALL(*appender, write(_, _)).Times(0);
   t_logger("", pimpl).errorf("%d", 5);
 }
 
 TEST_F(CLoggerTest, DoesNotRawLogAboveLevel) {
   using testing::_;
   pimpl->min_log_level = level::fatal;
-  EXPECT_CALL(*appender, write(_)).Times(0);
+  EXPECT_CALL(*appender, write(_, _)).Times(0);
   t_logger("", pimpl).error("5");
 }

+ 5 - 4
test/console_appender_test.cxx

@@ -12,6 +12,7 @@
 
 #include "logger/log_manager.h"
 #include "logger/properties.h"
+#include "logger_impl.h"
 #include "mock_logger.h"
 
 class ConsoleAppenderTest : public testing::Test {
@@ -21,7 +22,8 @@ protected:
   
   std::string cout() const { return cout_->str(); }
   std::string cerr() const { return cerr_->str(); }
-  std::shared_ptr<logging::appender> get(logging::properties const &) const;
+  std::shared_ptr<logging::log_appender>
+  get(logging::properties const &) const;
 private:
   std::shared_ptr<StubLayout> playout;
   std::unique_ptr<scoped_buffer_capture_t> cout_, cerr_;
@@ -39,11 +41,10 @@ void ConsoleAppenderTest::TearDown() {
   playout.reset();
 }
 
-std::shared_ptr<logging::appender>
+std::shared_ptr<logging::log_appender>
 ConsoleAppenderTest::get(logging::properties const & props) const {
   auto pappender = logging::appenders::instance().get("Console", props);
-  pappender->layout = playout;
-  return pappender;
+  return std::make_shared<logging::log_appender>(pappender, playout);
 }
 
 TEST_F(ConsoleAppenderTest, ErrorOnUnknownTarget) {

+ 5 - 4
test/file_appender_test.cxx

@@ -14,6 +14,7 @@
 #include "logger/exception.h"
 #include "logger/log_manager.h"
 #include "logger/properties.h"
+#include "logger_impl.h"
 #include "mock_logger.h"
 
 class FileAppenderTest : public testing::Test {
@@ -22,7 +23,8 @@ protected:
   void TearDown() override;
   
   std::string filename() const { return data; }
-  std::shared_ptr<logging::appender> get(logging::properties const &) const;
+  std::shared_ptr<logging::log_appender>
+  get(logging::properties const &) const;
 private:
   std::shared_ptr<StubLayout> playout;
   char data[24];
@@ -45,11 +47,10 @@ void FileAppenderTest::TearDown() {
   memset(data, 0, sizeof(data));
 }
 
-std::shared_ptr<logging::appender>
+std::shared_ptr<logging::log_appender>
 FileAppenderTest::get(logging::properties const & props) const {
   auto pappender = logging::appenders::instance().get("File", props);
-  pappender->layout = playout;
-  return pappender;
+  return std::make_shared<logging::log_appender>(pappender, playout);
 }
 
 TEST_F(FileAppenderTest, ThrowsIfNoFilename) {

+ 0 - 163
test/format_test.cxx

@@ -1,163 +0,0 @@
-//
-//  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"
-#include "logger/logpacket.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
-namespace {
-  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({{}, level::error, {}, "UNIT_TEST", "HELLO"}),
-              Eq("UNIT_TEST"));
-}
-
-TEST(FormatTest, LogLevelIsPToken) {
-  using testing::Eq;
-  auto fmt = format::parse_format_string("%p");
-  EXPECT_THAT(fmt.process({{}, level::error, {}, "UNIT_TEST", "HELLO"}),
-              Eq("ERROR"));
-}
-
-TEST(FormatTest, LogMessageIsMToken) {
-  using testing::Eq;
-  auto fmt = format::parse_format_string("%m");
-  EXPECT_THAT(fmt.process({{}, level::error, {}, "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({{}, level::error, {}, "UNIT_TEST", "HELLO"}),
-              Eq("HEL"));
-}
-
-TEST(FormatTest, TokenCanBeLeftPadded) {
-  using testing::Eq;
-  auto fmt = format::parse_format_string("%6m");
-  EXPECT_THAT(fmt.process({{}, level::error, {}, "UNIT_TEST", "HELLO"}),
-              Eq(" HELLO"));
-}
-
-TEST(FormatTest, TokenCanBeRightPadded) {
-  using testing::Eq;
-  auto fmt = format::parse_format_string("%-6m");
-  EXPECT_THAT(fmt.process({{}, level::error, {}, "UNIT_TEST", "HELLO"}),
-              Eq("HELLO "));
-}
-
-TEST(FormatTest, TokenCanBeSizeBound) {
-  using testing::Eq;
-  auto fmt = format::parse_format_string("%6.8m");
-  EXPECT_THAT(fmt.process({{}, level::error, {}, "UNIT_TEST", "HELLO"}),
-              Eq(" HELLO"));
-  EXPECT_THAT(fmt.process({{}, level::error, {}, "UNIT_TEST", "HELLO FRIEND"}),
-              Eq("HELLO FR"));
-}

+ 32 - 0
test/header_test_obj.h

@@ -0,0 +1,32 @@
+//
+//  header_test_obj.h
+//  logger
+//
+//  Created by Sam Jaffe on 4/19/19.
+//
+
+#pragma once
+
+#include <gmock/gmock.h>
+
+#include "logger_impl.h"
+#include "mock_logger.h"
+
+class HeaderFooterTest : public testing::Test {
+protected:
+  void SetUp() override {
+    appender = std::make_shared<StubAppender>();
+    auto GetStub = [this](logging::properties const &) {
+      return appender;
+    };
+    abinding_ = logging::appenders::instance().bind_scoped("Stub", GetStub);
+  }
+  void TearDown() override {
+    abinding_.reset();
+    appender.reset();
+  }
+protected:
+  std::shared_ptr<StubAppender> appender;
+private:
+  logging::appenders::scoped_binding abinding_;
+};

+ 48 - 0
test/json_layout_test.cxx

@@ -188,3 +188,51 @@ TEST(JsonLayoutTest, ObjectLackingJsonDefaultsToString) {
   using testing::Eq;
   EXPECT_THAT(ss.str(), Eq(struct_output));
 }
+
+#include "header_test_obj.h"
+#include "logger/logger.h"
+
+using namespace logging;
+using namespace logging::property;
+properties const JSON_HEADER_SCHEMA{_obj({
+  {"configuration", _obj({
+    {"appenders", _obj({
+      {"Stub", _obj({
+        {"JsonLayout", _obj({
+          {"complete", _v(true)},
+          {"compact", _v(true)},
+          {"eventEol", _v(true)}
+        })}
+      })}
+    })},
+    {"loggers", _obj({
+      {"root", _obj({{"appenders", _obj({{"ref", _v("Stub")}})}})}
+    })}
+  })}
+})};
+
+using JsonLayoutHeaderTest = HeaderFooterTest;
+
+TEST_F(JsonLayoutHeaderTest, ProvidesArrayBounds) {
+  {
+    manager mgr;
+    mgr.configure(JSON_HEADER_SCHEMA);
+  }
+  using testing::Eq;
+  EXPECT_THAT(appender->sstream.str(), Eq("[]"));
+}
+
+TEST_F(JsonLayoutHeaderTest, SeparatesLogsWithComma) {
+  using testing::Eq;
+  using testing::EndsWith;
+  using testing::StartsWith;
+  manager mgr;
+  mgr.configure(JSON_HEADER_SCHEMA);
+  mgr.get().log(level::error, "HELLO");
+  // Newline is printed as a part of the message...
+  EXPECT_THAT(appender->sstream.str(), EndsWith("}\n"));
+  appender->sstream.str("");
+  mgr.get().log(level::error, "HELLO");
+  // So the dividing comma gets attached to the next log
+  EXPECT_THAT(appender->sstream.str(), StartsWith(",{"));
+}

+ 15 - 7
test/log_manager_test.cxx

@@ -27,9 +27,17 @@ private:
   layouts::scoped_binding lbinding_;
 };
 
+namespace logging {
+  level level_from_string(std::string const & value);
+}
+
 void LogManagerTest::SetUp() {
-  auto GetMock = [this](properties const &) {
-    return appender = std::make_shared<MockAppender>();
+  auto GetMock = [this](properties const & props) {
+    appender = std::make_shared<MockAppender>();
+    if (props.contains("threshold")) {
+      appender->threshold = level_from_string(props["threshold"]);
+    }
+    return appender;
   };
   abinding_ = appenders::instance().bind_scoped("Mock", GetMock);
   auto GetMockLayout = [this](properties const &) {
@@ -64,7 +72,7 @@ TEST_F(LogManagerTest, CanFetchInjectedMock) {
   mgr.configure(MIN_PROPERTY_SCHEMA);
   
   EXPECT_CALL(*appender, flush()).Times(AnyNumber());
-  EXPECT_CALL(*appender, write(MessageEq("TEST MESSAGE")));
+  EXPECT_CALL(*appender, write(MessageEq("TEST MESSAGE"), _));
 
   c_logger l = mgr.c_get();
   l.error("TEST MESSAGE");
@@ -75,7 +83,7 @@ TEST_F(LogManagerTest, MultiplexMockLogsToMultipleImpls) {
   mgr.configure(MULTIPLEX_PROPERTY_SCHEMA);
   
   EXPECT_CALL(*appender, flush()).Times(AnyNumber());
-  EXPECT_CALL(*appender, write(MessageEq("TEST MESSAGE"))).Times(2);
+  EXPECT_CALL(*appender, write(MessageEq("TEST MESSAGE"), _)).Times(2);
 
   c_logger l = mgr.c_get();
   l.error("TEST MESSAGE");
@@ -86,8 +94,8 @@ TEST_F(LogManagerTest, LevelCanBeBakedIntoAppenderProperties) {
   mgr.configure(APPENDER_LEVEL_PROPERTY_SCHEMA);
   
   EXPECT_CALL(*appender, flush()).Times(AnyNumber());
-  EXPECT_CALL(*appender, write(MessageEq("TEST MESSAGE"))).Times(1);
-  EXPECT_CALL(*appender, write(MessageEq("LOWER MESSAGE"))).Times(0);
+  EXPECT_CALL(*appender, write(MessageEq("TEST MESSAGE"), _)).Times(1);
+  EXPECT_CALL(*appender, write(MessageEq("LOWER MESSAGE"), _)).Times(0);
   
   c_logger l = mgr.c_get();
   l.warn("TEST MESSAGE");
@@ -99,7 +107,7 @@ TEST_F(LogManagerTest, LevelCanBeBakedIntoLoggerProperties) {
   mgr.configure(LOGGER_LEVEL_PROPERTY_SCHEMA);
   
   EXPECT_CALL(*appender, flush()).Times(AnyNumber());
-  EXPECT_CALL(*appender, write(_)).Times(0);
+  EXPECT_CALL(*appender, write(_, _)).Times(0);
   
   c_logger l = mgr.c_get();
   l.warn("TEST MESSAGE");

+ 4 - 4
test/logger_test.cxx

@@ -5,7 +5,7 @@
 //  Created by Sam Jaffe on 3/31/19.
 //
 
-#include "mock_logger.h"
+#include "logger_test_obj.h"
 
 #include "logger/logger.h"
 
@@ -30,19 +30,19 @@ TEST_F(LoggerTest, FlushesOnFlushCall) {
 
 TEST_F(LoggerTest, LogsWithBraceFmtCode) {
   using testing::_;
-  EXPECT_CALL(*appender, write(MessageEq("5"))).Times(1);
+  EXPECT_CALL(*appender, write(MessageEq("5"), _)).Times(1);
   t_logger("", pimpl).log(level::error, "{}", 5);
 }
 
 TEST_F(LoggerTest, DoesNotLogAboveLevel) {
   using testing::_;
   pimpl->min_log_level = level::fatal;
-  EXPECT_CALL(*appender, write(_)).Times(0);
+  EXPECT_CALL(*appender, write(_, _)).Times(0);
   t_logger("", pimpl).log(level::error, "{}", 5);
 }
 
 TEST_F(LoggerTest, LogCurlyBraceLiteralByDoubling) {
   using testing::_;
-  EXPECT_CALL(*appender, write(MessageEq("{}"))).Times(1);
+  EXPECT_CALL(*appender, write(MessageEq("{}"), _)).Times(1);
   t_logger("", pimpl).log(level::error, "{{}}", 5);
 }

+ 36 - 0
test/logger_test_obj.h

@@ -0,0 +1,36 @@
+//
+//  logger_test_obj.h
+//  logger
+//
+//  Created by Sam Jaffe on 4/18/19.
+//
+
+#pragma once
+
+#include <gmock/gmock.h>
+
+#include "logger_impl.h"
+#include "mock_logger.h"
+
+struct LoggerTest : public testing::Test {
+  void SetUp() override {
+    appender.reset(new MockAppender);
+    pimpl = std::make_shared<logging::logger_impl>();
+    auto layout = std::make_shared<StubLayout>();
+    auto log = std::make_shared<logging::log_appender>(appender, layout);
+    pimpl->impls.push_back(log);
+    
+    using testing::_;
+    using testing::AnyNumber;
+    
+    EXPECT_CALL(*appender, write(_, _)).Times(AnyNumber());
+    EXPECT_CALL(*appender, flush()).Times(AnyNumber());
+  }
+  void TearDown() override {
+    pimpl.reset();
+    appender.reset();
+  }
+  
+  std::shared_ptr<MockAppender> appender;
+  std::shared_ptr<logging::logger_impl> pimpl;
+};

+ 15 - 34
test/mock_logger.h

@@ -14,7 +14,6 @@
 
 #include "logger/detail/appender.h"
 #include "logger/detail/layout.h"
-#include "logger/detail/logger_impl.h"
 #include "logger/logpacket.h"
 
 namespace logging {
@@ -34,21 +33,24 @@ namespace logging {
   }
 }
 
-struct MockAppender : public logging::appender {
-  MockAppender() { SetLogLevel(logging::level::trace); }
-  void SetLogLevel(logging::level ll) { min_log_level = ll; }
-  MOCK_METHOD0(flush, void());
-  MOCK_METHOD1(write, void(logging::logpacket const &));
-};
-
 struct StubAppender : public logging::appender {
-  StubAppender() { SetLogLevel(logging::level::trace); }
-  void SetLogLevel(logging::level ll) { min_log_level = ll; }
+  StubAppender() : threshold(logging::level::trace) { }
+  std::ostream & stream() override { return sstream; }
   void flush() override {}
-  void write(logging::logpacket const & pkt) override {
-    layout->format(stream, pkt);
+  void write(logging::logpacket const & pkt, logging::layout & lay) override {
+    lay.format(sstream, pkt);
+  }
+  bool should_log(logging::level ll) const override {
+    return ll >= threshold;
   }
-  std::stringstream stream;
+  logging::level threshold;
+  std::stringstream sstream;
+};
+
+struct MockAppender : public StubAppender {
+  MockAppender() { }
+  MOCK_METHOD0(flush, void());
+  MOCK_METHOD2(write, void(logging::logpacket const &, logging::layout &));
 };
 
 struct MockLayout : public logging::layout {
@@ -69,25 +71,4 @@ MATCHER_P(MessageEq, value, "") {
   return arg.message.str() == value;
 }
 
-struct LoggerTest : public testing::Test {
-  void SetUp() override {
-    appender.reset(new MockAppender);
-    pimpl = std::make_shared<logging::logger_impl>();
-    pimpl->impls.push_back(appender);
-    
-    using testing::_;
-    using testing::AnyNumber;
-
-    EXPECT_CALL(*appender, write(_)).Times(AnyNumber());
-    EXPECT_CALL(*appender, flush()).Times(AnyNumber());
-  }
-  void TearDown() override {
-    pimpl.reset();
-    appender.reset();
-  }
-  
-  std::shared_ptr<MockAppender> appender;
-  std::shared_ptr<logging::logger_impl> pimpl;
-};
-
 #endif /* mock_logger_h */

+ 190 - 11
test/pattern_layout_test.cxx

@@ -10,6 +10,7 @@
 #include "resource_factory/prototype_factory.hpp"
 
 #include "logger/detail/layout.h"
+#include "logger/exception.h"
 #include "logger/log_manager.h"
 #include "logger/logpacket.h"
 #include "logger/properties.h"
@@ -19,19 +20,197 @@ namespace {
   constexpr const int NOW = 1554401840;
 }
 
-TEST(PatternLayoutTest, TestInvokesFormatter) {
+std::shared_ptr<logging::layout>
+GetPatternLayout(std::string const & fmt) {
   using namespace logging;
   using namespace logging::property;
-  properties props{_obj({
-    {"pattern", _v("%d{%I:%M:%S.%_ms} [%%] %-5.5p %.36c - %m")}
-  })};
-  auto playout = layouts::instance().get("PatternLayout", props);
-  
+  properties props{_obj({{"pattern", _v(fmt)}})};
+  return layouts::instance().get("PatternLayout", props);
+}
+
+std::string DoFormat(std::string const & fmt,
+                     logging::logpacket const & pkt) {
   std::stringstream ss;
-  playout->format(ss, {{NOW, 0}, level::warning, {}, "UnitTest",
-    "This is a test message"});
-  
+  GetPatternLayout(fmt)->format(ss, pkt);
+  return ss.str();
+}
+
+using namespace logging;
+
+logpacket getpkt(std::string const & msg) {
+  return logpacket{{}, level::error, {}, "UNIT_TEST", msg};
+};
+
+TEST(PatternLayoutTest, EmptyFormatterCanParse) {
+  EXPECT_NO_THROW(GetPatternLayout(""));
+}
+
+TEST(PatternLayoutTest, ThrowsForEndOfStringAfterPct) {
+  EXPECT_THROW(GetPatternLayout("%"),
+               logging::format_parsing_exception);
+}
+
+TEST(PatternLayoutTest, RawStringFmtReturnsSelf) {
+  using testing::Eq;
+  EXPECT_THAT(DoFormat("TEST STRING", {}), Eq("TEST STRING"));
+}
+
+TEST(PatternLayoutTest, NCharReturnsNewLine) {
+  using testing::Eq;
+  EXPECT_THAT(DoFormat("%n", {}), Eq("\n"));
+}
+
+TEST(PatternLayoutTest, DoublePctIsLiteral) {
+  using testing::Eq;
+  EXPECT_THAT(DoFormat("%%", {}), Eq("%"));
+}
+
+TEST(PatternLayoutTest, CatchesRawContentBeforeFmt) {
+  using testing::Eq;
+  EXPECT_THAT(DoFormat("TEST%%", {}), Eq("TEST%"));
+}
+
+TEST(PatternLayoutTest, CatchesRawContentAfterFmt) {
+  using testing::Eq;
+  EXPECT_THAT(DoFormat("%%TEST", {}), Eq("%TEST"));
+}
+
+TEST(PatternLayoutTest, HandlesDateFormatter) {
+  using testing::Eq;
+  EXPECT_THAT(DoFormat("%d", {{NOW,0}}),
+              Eq("2019-04-04 18:17:20,000"));
+}
+
+TEST(PatternLayoutTest, FormatsMilliseconds) {
+  using testing::Eq;
+  EXPECT_THAT(DoFormat("%d", {{NOW,123000}}),
+              Eq("2019-04-04 18:17:20,123"));
+}
+
+TEST(PatternLayoutTest, ThrowsIfCustomFmtUnterminated) {
+  using testing::Eq;
+  EXPECT_THROW(GetPatternLayout("%d{%"),
+               logging::format_parsing_exception);
+}
+
+TEST(PatternLayoutTest, SupportsCustomFormatWithBrace) {
+  using testing::Eq;
+  EXPECT_THAT(DoFormat("%d{%Y}", {{NOW,0}}), Eq("2019"));
+}
+
+TEST(PatternLayoutTest, FormatsCustomMilliseconds) {
+  using testing::Eq;
+  EXPECT_THAT(DoFormat("%d{%_ms}", {{NOW,123000}}), Eq("123"));
+}
+
+TEST(PatternLayoutTest, SupportsISO8601Format) {
+  using testing::Eq;
+  EXPECT_THAT(DoFormat("%d{ISO8601}", {{NOW,0}}),
+              Eq("2019-04-04T18:17:20.000Z"));
+}
+
+TEST(PatternLayoutTest, SupportsSingleDayFormat) {
+  using testing::Eq;
+  EXPECT_THAT(DoFormat("%d{ABSOLUTE}", {{NOW,0}}),
+              Eq("18:17:20,000"));
+}
+
+TEST(PatternLayoutTest, SupportsHumanDateFormat) {
+  using testing::Eq;
+  EXPECT_THAT(DoFormat("%d{DATE}", {{NOW,0}}),
+              Eq("04 Apr 2019 18:17:20,000"));
+}
+
+TEST(PatternLayoutTest, LoggerIdIsCToken) {
+  using testing::Eq;
+  EXPECT_THAT(DoFormat("%c", getpkt("HELLO")),
+              Eq("UNIT_TEST"));
+}
+
+TEST(PatternLayoutTest, LogLevelIsPToken) {
+  using testing::Eq;
+  EXPECT_THAT(DoFormat("%p", getpkt("HELLO")),
+              Eq("ERROR"));
+}
+
+TEST(PatternLayoutTest, LogMessageIsMToken) {
+  using testing::Eq;
+  EXPECT_THAT(DoFormat("%m", getpkt("HELLO")),
+              Eq("HELLO"));
+}
+
+TEST(PatternLayoutTest, ThrowsOnUnknownToken) {
+  using testing::Eq;
+  EXPECT_THROW(GetPatternLayout("%q"),
+               logging::unknown_format_specifier);
+}
+
+TEST(PatternLayoutTest, TokenCanBeTruncatedInFormat) {
+  using testing::Eq;
+  EXPECT_THAT(DoFormat("%.3m", getpkt("HELLO")),
+              Eq("HEL"));
+}
+
+TEST(PatternLayoutTest, TokenCanBeLeftPadded) {
+  using testing::Eq;
+  EXPECT_THAT(DoFormat("%6m", getpkt("HELLO")),
+              Eq(" HELLO"));
+}
+
+TEST(PatternLayoutTest, TokenCanBeRightPadded) {
+  using testing::Eq;
+  EXPECT_THAT(DoFormat("%-6m", getpkt("HELLO")),
+              Eq("HELLO "));
+}
+
+TEST(PatternLayoutTest, TokenCanBeSizeBound) {
+  using testing::Eq;
+  EXPECT_THAT(DoFormat("%6.8m", getpkt("HELLO")),
+              Eq(" HELLO"));
+  EXPECT_THAT(DoFormat("%6.8m", getpkt("HELLO FRIEND")),
+              Eq("HELLO FR"));
+}
+
+#include "header_test_obj.h"
+#include "logger/logger.h"
+
+using namespace logging;
+using namespace logging::property;
+properties const PATTERN_HEADER_SCHEMA{_obj({
+  {"configuration", _obj({
+    {"appenders", _obj({
+      {"Stub", _obj({
+        {"PatternLayout", _obj({
+          {"pattern", _v("%m")},
+          {"header", _v("HEADER-")},
+          {"footer", _v("-FOOTER")}
+        })}
+      })}
+    })},
+    {"loggers", _obj({
+      {"root", _obj({{"appenders", _obj({{"ref", _v("Stub")}})}})}
+    })}
+  })}
+})};
+
+using PatternLayoutHeaderTest = HeaderFooterTest;
+
+TEST_F(PatternLayoutHeaderTest, ProvidesHeader) {
+  manager mgr;
+  mgr.configure(PATTERN_HEADER_SCHEMA);
+  using testing::Eq;
+  EXPECT_THAT(appender->sstream.str(), Eq("HEADER-"));
+  mgr.get().log(level::error, "HELLO");
+  EXPECT_THAT(appender->sstream.str(), Eq("HEADER-HELLO"));
+}
+
+TEST_F(PatternLayoutHeaderTest, ProvidesFooter) {
   using testing::Eq;
-  EXPECT_THAT(ss.str(), Eq("06:17:20.000 [%] WARNI UnitTest -"
-                           " This is a test message"));
+  {
+    manager mgr;
+    mgr.configure(PATTERN_HEADER_SCHEMA);
+    appender->sstream.str("");
+    mgr.get().log(level::error, "HELLO");
+  }
+  EXPECT_THAT(appender->sstream.str(), Eq("HELLO-FOOTER"));
 }

+ 3 - 3
test/test_properties.cxx

@@ -36,7 +36,7 @@ properties const APPENDER_LEVEL_PROPERTY_SCHEMA{_obj({
   {"configuration", _obj({
     {"appenders", _obj({
       {"Mock", _obj({
-        {"level", _v("Warning")},
+        {"threshold", _v("Warning")},
         {"MockLayout", _v("")}
       })}
     })},
@@ -54,13 +54,13 @@ properties const LOGGER_LEVEL_PROPERTY_SCHEMA{_obj({
   {"configuration", _obj({
     {"appenders", _obj({
       {"Mock", _obj({
-        {"level", _v("Warning")},
+        {"threshold", _v("Warning")},
         {"MockLayout", _v("")}
       })}
     })},
     {"loggers", _obj({
       {"root", _obj({
-        {"level", _v("Error")},
+        {"threshold", _v("Error")},
         {"appenders", _obj({
           {"ref", _v("Mock")}
         })}