Serenity API




What Is This Library All About?

Serenity is a C++20 logging framework library that takes inspiration from spdlog for its user customizable aspect in logging and fmtlib for its highly flexible and efficient formatting of arguments. Serenity includes a native formatter in the form of ArgFormatter, which is a fully compliant but limited scope, drop-in replacement for the standard's <format> and Victor's fmtlib that is used for its relatively faster formatting times at the cost of some robustness that the other two provide. As a framework library, the backend formatter can be swapped between any three of these without any issues.

What sets Serenity apart is that this library offers very fast single threaded log calls alongside Unicode support via UTF-Utils, which allows the input paths for files and any string-type argument provided to a log call to be automatically decoded and encoded so that you don't have to worry so much about the headache of text encodings and can just log what you need to log. What this also means is that, by default, any derived extension to log targets that you may create will inherently achieve this as well. The internal log calls also use this functionality, so writing logs to char-type based containers from your derived class just got a whole lot easier on top of having stress-free log calls.


Serenity Requires The Following:

  • C++ 20 due to the below features and libraries used
    • Lambda closure type
    • Lambda parameter pack expansions
    • Aggregate initializations
    • Heavy use of constraints and concepts
    • Heavy usage of std::remove_cvref_t<>
    • Light usage of abbreviated function templates
    • <concepts> header
    • <format> header if the build option "USE_STDFMT" is used
    • <source_location> header
    • std::jthread
    • std::stop_token
    • std::stop_source
  • A formatting backend:
    • ArgFormatter is the default backend and uses the build option "USE_NATIVEFMT"
    • fmtlib which uses the build option "USE_FMTLIB" and defaults to version 9.1.0
    • <format> which uses the build option "USE_STDFMT"
  • Utf-Utils - this project is already included and built alongside Serenity by default.

Quick Start

To start using any of the logging targets, simply `#include` any of the targets found under `serenity/Targets`.
The available targets for out-of-the-box usage are:

  • FileTarget
  • RotatingTarget
  • ConsoleTarget

For the FileTarget:

There are three total constructors for `FileTarget`:


 1.) explicit FileTarget(std::string_view fileName, bool replaceIfExists = false);
 2.) explicit FileTarget(std::string_view name, std::string_view filePath, bool replaceIfExists = false);
 3.) explicit FileTarget(std::string_view name, std::string_view formatPattern, std::string_view filePath, bool replaceIfExists = false);
                    
  • The third constructor offers the most control, allowing you to:
    • Set the Logger's name that it will be identified by
    • Set the format pattern that will be used to format the log message itself (the user flags for this can be found HERE-[PLACE_HOLDER])
    • Set the file path for the log file (if an absolute path isn't provided, then the path created will be relative to the running process's directory + file path)
    • As well as set whether or not the file should be truncated on open if the file already exists on disk.

  • The second constructor differs from the third only by the format pattern, otherwise this is more or less the same as the third constructor. The second constructor allows you to:
    • Set the Logger's name that it will be identified by
    • Set the file path for the log file (if an absolute path isn't provided, then the path created will be relative to the running process's directory + file path)
    • As well as set whether or not the file should be truncated on open if the file already exists on disk.
    • For the format pattern, it will use the default pattern of:
      "[Short_Level] DDD ddmmmyy HH:MM:SS [Logger_Name]: Log_Message + Platform_Line_Ending."


  • For the first constructor:
    • The name will use the default name of "File_Logger"
    • For the format pattern, it will use the default pattern of:
      " [Short_Level] DDD ddmmmyy HH:MM:SS [Logger_Name]: Log_Message + Platform_Line_Ending. "

    • You have the same control over the file name as you do with the other two constructors however, this is only the file name, not the file path. The file path itself will be the running process's location + "Logs" + the file name you passed in
    • You also have control over whether or not the file should be truncated on open if the file already exists on disk.

For the RotatingTarget:

There are three total constructors for `RotatingTarget`:


 1.) explicit RotatingTarget();
 2.) explicit RotatingTarget(std::string_view name, std::string_view filePath, bool replaceIfExists = false);
 3.) explicit RotatingTarget(std::string_view name, std::string_view formatPattern, std::string_view filePath, bool replaceIfExists = false);
                    
  • Like with the File Target, the Rotating Target's third constructor offers the most control, allowing you to:
    • Set the Logger's name that it will be identified by
    • Set the format pattern that will be used to format the log message itself (the user flags for this can be found HERE-[PLACE_HOLDER])
    • Set the file path for the log file (if an absolute path isn't provided, then the path created will be relative to the running process's directory + file path)
    • And finally, set whether or not to truncate the file on open if the file already exists on disk.

  • The second constructor differs from the third only by the format pattern, otherwise this is more or less identical to the third constructor.The second constructor allows you to:
    • Set the Logger's name that it will be identified by
    • Set the file path for the log file (if an absolute path isn't provided, then the path created will be relative to the running process's directory + file path)
    • And finally, set whether or not to truncate the file on open if the file already exists on disk.
    • For the format pattern, it will use the default pattern of:
      " [Short_Level] DDD ddmmmyy HH:MM:SS [Logger_Name]: Log_Message + Platform_Line_Ending. "


  • For the first constructor, everything will be set to its default values.
    • This means that the file path and file name will be the running process's directory + "Logs/RotatingLog.txt"
    • The file will truncate on open by default
    • The logger's name will be "Rotating_Logger" by default
    • For the format pattern, it will use the default pattern of:
      " [Short_Level] DDD ddmmmyy HH:MM:SS [Logger_Name]: Log_Message + Platform_Line_Ending. "



For the ColorConsoleTarget:

There are three total constructors for `ConsoleTarget`:


 1.) ConsoleTarget();
 2.) explicit ConsoleTarget(std::string_view name );
 3.) explicit ConsoleTarget(std::string_view name, std::string_view msgPattern );
                    
  • For all three constructors, the default console interface set is `stdout`
    • this can be changed via the "SetConsoleInterface(console_interface interface);" function.
  • The Message colors for all three constructors are also default set
    • These colors can be changed by calling the "SetMsgColor(LoggerLevel lvl, std::string_view color);" function
    • Where the color is an ansi color code.

  • Note: There is a header, `Color/Color.h`, that is included that can be used to simplify and aid this process as well. The color codes are broken up into the appropriate namespaces to make things clear on the intent.
The default colors that are set are:
Level Color
Trace Bright White
Info Bright Green
Debug Bright Cyan
Warning Bright Yellow
Error Basic Red
Fatal Bright Yellow On Red

As An Example Of Changing Colors Using The Helper Header `Color.h`:

      #include <serenity/Color/Color.h>

      namespace color = serenity::se_colors::bright_colors;
      namespace target = serenity::targets;
      // Create the console target with default format pattern and user-defined name
      target::ConsoleTarget example("Example_Logger");
      // Set the message level to any ansi-supported code (using helper header here)
      example.SetMsgColor(LoggerLevel::trace, color::foreground::grey);
                    
  • Like with the previous two Targets though, the ConsoleTarget's third constructor offers the most control, allowing you to:
    • Set the Logger's name that it will be identified by
    • Set the format pattern that will be used to format the log message itself (the user flags for this can be found HERE-[PLACE_HOLDER]).

  • The second constructor only allows setting the Logger's name.
    • The default pattern used will be:
      " [Short_Level] DDD ddmmmyy HH:MM:SS [Logger_Name]: Log_Message + Platform_Line_Ending. "

    • The above listed defaults apply here as well

  • The first constructor applies all the defaults listed in points one and two as well as:
    • The default name used will be "Console_Logger
    • The default pattern used will be:
      " [Short_Level] DDD ddmmmyy HH:MM:SS [Logger_Name]: Log_Message + Platform_Line_Ending. "

Setting A User Defined Log Output Pattern

If you created a default logger object from one of the targets listed in the section above, then you will need to call that object's SetPattern(std::string_view pattern) function.

In either case, whether you're using the SetPattern(std::string_view pattern) function or if you're using a target constructor that takes in a format pattern parameter, the format string itself can include any of the flags listed below, any strftime flag (with or without modifiers), as well as any additional characters you may want to add (these will be printed as-is).

IMPORTANT NOTE: In Order To See Your Log Message In The Final Log Output, You Must Include '%+' Somewhere In Your User Pattern

The Below Table Lists The Flags Available For Use. For Time Related Flags, Please Refer To The 'strftime' Flags And Modifiers HERE.
With The Exception Of '%T' Due To The Added Sub-Second Precision Field, All 'strftime' Flags Have The Same Behavior In Serenity.
Flag Flag Function Flag Modifiers Example
%l Prints the message level as a short string representation None Trace = 'T', Info = 'I'
Debug = 'D', Warning = 'W'
Error 'E', Fatal = 'F'
%L Prints the message level as a long string representation None Trace = 'Trace', Info = 'Info'
Debug = 'Debug', Warning = 'Warn'
Error 'Error', Fatal = 'Fatal'
%s Formats the source location of the log site ' : ' and any combination of 'a', 'c', 'f', 'l', 'F' %s:c => output the column number of the log site

%s:f => output the file of where the log site is

%s:l => output the line number of the log site

%s:F => output the function name of where the log site is called

%s:a => format source location containing all fields

%s => same as %s:a
%t Formats the current thread ID of the logger ' : ' and any digit between 0-10
default length is 10 digits
If thread ID was 123456789123456789:
%t:6 => 123456
%N Prints the Logger's Name None If logger's name was 'Critical_Logger', prints 'Critical_Logger'
%+ Prints the actual log message itself None If you called:

someLogger.Trace("This Is A Log Message With Value: {}", 42);

Then this flag would output:

"This Is A Log Message With Value: 42"
%T Prints the 24 hour time format as HH:MM:SS ' : ' and any digits for sub-second precision of time If the time was 6:30 PM and the flag used was
'%T:3'
then a possible output might be: 18:30:24.345


Making A Log Call

Once you decide what logger(s) you would like to use, all that's left to do to start using them is to call their respective log functions:


  template <typename... Args> void Trace(MsgWithLoc formatString, Args&&... args);
 template <typename... Args> void Info(MsgWithLoc formatString, Args&&... args);
  template <typename... Args> void Debug(MsgWithLoc formatString, Args&&... args);
 template <typename... Args> void Warn(MsgWithLoc formatString, Args&&... args);
  template <typename... Args> void Error(MsgWithLoc formatString, Args&&... args);
  template <typename... Args> void Fatal(MsgWithLoc formatString, Args&&... args);
                

Where the format string is your message to log. The loggers use the active backend formatter to format any log message with the arguments provided so long as there are substitution brackets present in the form of "{}". If unfamiliar with what the message formatting grammar is, I would highly recommend checking out the grammar specification outlined HERE. "ArgFormatter" and "<format>" are modeled after "fmtlib" and thus use Victor's grammar spec.

As An Example:


 #include <serenity/Targets/FileTarget.h>

 using namespace serenity::targets;
 FileTarget basicFileLogger("My_File_Logger", "[%L] %c [%N]: %+", "Sandbox/Logs/My_Log.txt", true);
 basicFileLogger.Trace("This is how {0} log message might look with an integer formatted to hex: [{1:X}]", "a simple", 42);
                
  • The above constructor call:
    • Sets the logger name to "My_File_Logger"
    • Sets the file path (as it is now) to the current running process's directory + "Sandbox/Logs/My_Log.txt"
    • Enables truncation on open if the file already exists on disk
    • Sets the log format to:

[Long_Logger_Level] ddd mmm dd HH:MM:SS YYYY [My_File_Logger]: Log_Message + Platform_Line_Ending
                                            
which, at the time of writing, would be something like:

[Info] Mon Nov 28 20:35:32 2022 [My_File_Logger]: This is how a simple log message might look with an integer formatted to hex: [0X2A] \r\n
                                
Where the line ending used here is assuming a windows environment is being run.
  • For linux, the line ending is '\n'
  • For Mac systems, the line ending is '\r'

Current Timings (As Of 27Nov22):


Benched Environment
  • Benched with an i7-10750H CPU locked at 4.00 GHz
  • Each log call used a 400 byte c-style string.
  • Benched against spdlog version 1.11.0 with Serenity.
  • All timings were averaged Over 2,000,000 Iterations
  • NOTE: For Serenity's console target and spdlog's console sink, the level was set to 'off'.

Serenity is using `ArgFormatter` and spdlog is using its bundled `fmtlib` (the respective project defaults).
Logging Sink/Target Logging Speed Logging Throughput
Serenity Console Logger 9.36 ns 40738.34 MB/s
Spdlog Console Sink 12.47 ns 30589.40 MB/s
Serenity File Target 350.15 ns 1089.42 MB/s
Spdlog Basic File Sink 1146.80 ns 332.64 MB/s
Serenity Rotating Target 1397.58 ns 272.95 MB/s
Spdlog Rotating Sink 2852.14 ns 133.75 MB/s

This table illustrates the timings of Serenity vs. spdlog with both using "fmtlib" v9.1.0
Logging Sink/Target Logging Speed Logging Throughput
Serenity Console Logger 9.47 ns 40263.85 MB/s
Spdlog Console Sink 12.74 ns 29931.17 MB/s
Serenity File Target 1617.41 ns 235.85 MB/s
Spdlog Basic File Sink 1145.90 ns 332.90 MB/s
Serenity Rotating Target 2205.76 ns 172.94 MB/s
Spdlog Rotating Sink 2677.81 ns 142.45 MB/s

This table illustrates the timings of Serenity vs. spdlog with spdlog using its bundled "fmtlib" and Serenity using "<format>"
Logging Sink/Target Logging Speed Logging Throughput
Serenity Console Logger 8.51 ns 44806.05 MB/s
Spdlog Console Sink 12.74 ns 29951.26 MB/
Serenity File Target 2315.97 ns 164.71 MB/
Spdlog Basic File Sink 1027.15 ns 371.39 MB/
Serenity Rotating Target 2857.12 ns 133.52 MB/s
Spdlog Rotating Sink 2654.13 n 143.73 MB/s