Introducing RcppSpdlog
spdlog is a widely-used and very capable header-only C++ library for logging. The RcppSpdlog package provides R users with easy-to-use customized access to the spdlog logging library by including its headers in an R package which permit other R packages to deploy it via a simple LinkingTo: RcppSpdlog
as described in Section 1.1.3 of WRE.
spdlog is mature and widely deployed. It also has a very rich set of features described at the repository wiki. This vignette will highlight a few first use cases.
Note that in order to use RcppSpdlog in an R package that might get distributed to CRAN, the code should follow the example R and C++ code in function exampleRsink()
as described below.
We will however start with some simpler examples. Do not copy those into your R package. The package checks used by R test for use of stdout
and stderr
which is why the customized setup described later is preferable.
Initial example: Basics
This example follows the simplest and initial example in the spdlog. It is also included in the RcppSpdlog package as examples/exampleOne.cpp. As discussed above, do not use this example as a starting point in an R package.
// based on the 'basic usage' example in the README.md at https://github.com/gabime/spdlog
#include "spdlog/spdlog.h"
#include <Rcpp.h>
// [[Rcpp::depends(RcppSpdlog)]]
// [[Rcpp::export]]
void exampleOne() {
// change log pattern (changed from [%H:%M:%S %z] [%n] [%^---%L---%$] )
"[%H:%M:%S.%f] [%L] [thread %t] %v");
spdlog::set_pattern(
"Welcome to spdlog!");
spdlog::info("Some error message with arg: {}", 1);
spdlog::error(
"Easy padding in numbers like {:08d}", 12);
spdlog::warn("Support for int: {0:d}; hex: {0:x}; oct: {0:o}; bin: {0:b}", 42);
spdlog::critical("Support for floats {:03.2f}", 1.23456);
spdlog::info("Positional args are {1} {0}..", "too", "supported");
spdlog::info("{:<30}", "left aligned");
spdlog::info(
// Set global log level to debug
spdlog::set_level(spdlog::level::debug); "This message should be displayed..");
spdlog::debug(
// Compile time log levels
// define SPDLOG_ACTIVE_LEVEL to desired level
"Some trace message with param {}", {});
SPDLOG_TRACE("Some debug message");
SPDLOG_DEBUG(
}
/*** R
exampleOne()
*/
When built, which is easiest via Rcpp::sourceCpp()
, the final block ensures that the created function exampleOne()
is executed. In one previous run, the following output was produded:
R> exampleOne()
14:25:03.362024] [I] [thread 2453030] Welcome to spdlog!
[14:25:03.362047] [E] [thread 2453030] Some error message with arg: 1
[14:25:03.362051] [W] [thread 2453030] Easy padding in numbers like 00000012
[14:25:03.362053] [C] [thread 2453030] Support for int: 42; hex: 2a; oct: 52; bin: 101010
[14:25:03.362056] [I] [thread 2453030] Support for floats 1.23
[14:25:03.362058] [I] [thread 2453030] Positional args are supported too..
[14:25:03.362060] [I] [thread 2453030] left aligned
[14:25:03.362061] [D] [thread 2453030] This message should be displayed..
[R>
We note the easy-to-formatting in the source which benefits from the embedded fmt package for easy-to-use variable expansion. We also notice the different logging âlevelsâ indicated by single letters: info, errror, warning, critical and debug. More on this below.
Second example: Showcase
This second example follows a more complete example in the spdlog documention and highlights numerous features of the library. As before, this example is also included in the RcppSpdlog package as examples/exampleTwo.cpp. And as before, do not use this example as a starting point in an R package.
#include "spdlog/spdlog.h"
#include <Rcpp.h>
void stdout_logger_example();
void basic_example();
void rotating_example();
void daily_example();
void async_example();
void binary_example();
void trace_example();
void multi_sink_example();
void user_defined_example();
void err_handler_example();
void syslog_example();
void clone_example();
#include "spdlog/spdlog.h"
// [[Rcpp::depends(RcppSpdlog)]]
// [[Rcpp::export]]
void exampleTwo() {
"Welcome to spdlog version {}.{}.{} !",
spdlog::info(
SPDLOG_VER_MAJOR, SPDLOG_VER_MINOR, SPDLOG_VER_PATCH);"Easy padding in numbers like {:08d}", 12);
spdlog::warn("Support for int: {0:d}; hex: {0:x}; oct: {0:o}; bin: {0:b}", 42);
spdlog::critical("Support for floats {:03.2f}", 1.23456);
spdlog::info("Positional args are {1} {0}..", "too", "supported");
spdlog::info("{:>8} aligned, {:<8} aligned", "right", "left");
spdlog::info(
// Runtime log levels
// Set global log level to info
spdlog::set_level(spdlog::level::info); "This message should not be displayed!");
spdlog::debug(// Set specific logger's log level
spdlog::set_level(spdlog::level::trace); "This message should be displayed..");
spdlog::debug(
// Customize msg format for all loggers
"[%H:%M:%S %z] [%^%L%$] [thread %t] %v");
spdlog::set_pattern("This an info message with custom format");
spdlog::info("%+"); // back to default format
spdlog::set_pattern(
try
{
stdout_logger_example();
basic_example();
rotating_example();
daily_example();
clone_example();
async_example();
binary_example();
multi_sink_example();
user_defined_example();
err_handler_example();
trace_example();
// Flush all *registered* loggers using a worker thread every 3 seconds.
// note: registered loggers *must* be thread safe for this to work correctly!
std::chrono::seconds(3));
spdlog::flush_every(
// Apply some function on all registered loggers
std::shared_ptr<spdlog::logger> l) { l->info("End of example."); });
spdlog::apply_all([&](
// Release all spdlog resources, and drop all loggers in the registry.
// This is optional (only mandatory if using windows + async log).
//spdlog::shutdown();
}
// Exceptions will only be thrown upon failed logger or sink construction (not during logging).
catch (const spdlog::spdlog_ex &ex)
{std::printf("Log initialization failed: %s\n", ex.what());
return;
}
// added to this example file allow multiple runs of function
"console");
spdlog::drop("file_logger");
spdlog::drop("some_logger_name");
spdlog::drop("daily_logger");
spdlog::drop("async_file_logger");
spdlog::drop(
}
#include "spdlog/sinks/stdout_color_sinks.h"
// or #include "spdlog/sinks/stdout_sinks.h" if no colors needed.
void stdout_logger_example()
{// Create color multi threaded logger.
auto console = spdlog::stdout_color_mt("console");
// or for stderr:
// auto console = spdlog::stderr_color_mt("error-logger");
}
#include "spdlog/sinks/basic_file_sink.h"
void basic_example()
{// Create basic file logger (not rotated).
auto my_logger = spdlog::basic_logger_mt("file_logger", "logs/basic-log.txt");
}
#include "spdlog/sinks/rotating_file_sink.h"
void rotating_example()
{// Create a file rotating logger with 5mb size max and 3 rotated files.
auto rotating_logger =
"some_logger_name", "logs/rotating.txt", 1048576 * 5, 3);
spdlog::rotating_logger_mt(
}
#include "spdlog/sinks/daily_file_sink.h"
void daily_example()
{// Create a daily logger - a new file is created every day on 2:30am.
auto daily_logger = spdlog::daily_logger_mt("daily_logger", "logs/daily.txt", 2, 30);
}
// Clone a logger and give it new name.
// Useful for creating component/subsystem loggers from some "root" logger.
void clone_example()
{auto network_logger = spdlog::default_logger()->clone("network");
"Logging network stuff..");
network_logger->info(
}
#include "spdlog/async.h"
void async_example()
{// Default thread pool settings can be modified *before* creating the async logger:
// spdlog::init_thread_pool(32768, 1); // queue with max 32k items 1 backing thread.
auto async_file =
"async_file_logger", "logs/async_log.txt");
spdlog::basic_logger_mt<spdlog::async_factory>(// alternatively:
// auto async_file =
// spdlog::create_async<spdlog::sinks::basic_file_sink_mt>("async_file_logger",
// "logs/async_log.txt");
for (int i = 1; i < 101; ++i)
{"Async message #{}", i);
async_file->info(
}
}
// Log binary data as hex.
// Many types of std::container<char> types can be used.
// Iterator ranges are supported too.
// Format flags:
// {:X} - print in uppercase.
// {:s} - don't separate each byte with space.
// {:p} - don't print the position on each line start.
// {:n} - don't split the output to lines.
#include "spdlog/fmt/bin_to_hex.h"
void binary_example()
{std::vector<char> buf;
for (int i = 0; i < 80; i++)
{static_cast<char>(i & 0xff));
buf.push_back(
}"Binary example: {}", spdlog::to_hex(buf));
spdlog::info("Another binary example:{:n}",
spdlog::info(std::begin(buf), std::begin(buf) + 10));
spdlog::to_hex(// more examples:
// logger->info("uppercase: {:X}", spdlog::to_hex(buf));
// logger->info("uppercase, no delimiters: {:Xs}", spdlog::to_hex(buf));
// logger->info("uppercase, no delimiters, no position info: {:Xsp}", spdlog::to_hex(buf));
}
// Compile time log levels.
// define SPDLOG_ACTIVE_LEVEL to required level (e.g. SPDLOG_LEVEL_TRACE)
void trace_example()
{// trace from default logger
"Some trace message.. {} ,{}", 1, 3.23);
SPDLOG_TRACE(// debug from default logger
"Some debug message.. {} ,{}", 1, 3.23);
SPDLOG_DEBUG(
// trace from logger object
auto logger = spdlog::get("file_logger");
"another trace message");
SPDLOG_LOGGER_TRACE(logger,
}
// A logger with multiple sinks (stdout and file) - each with a different format and log level.
void multi_sink_example()
{auto console_sink = std::make_shared<spdlog::sinks::stdout_color_sink_mt>();
console_sink->set_level(spdlog::level::warn);"[multi_sink_example] [%^%l%$] %v");
console_sink->set_pattern(
auto file_sink =
std::make_shared<spdlog::sinks::basic_file_sink_mt>("logs/multisink.txt", true);
file_sink->set_level(spdlog::level::trace);
"multi_sink", {console_sink, file_sink});
spdlog::logger logger(
logger.set_level(spdlog::level::debug);"this should appear in both console and file");
logger.warn("this message should not appear in the console, only in the file");
logger.info(
}
// User defined types logging by implementing operator<<
#include "spdlog/fmt/ostr.h" // must be included
struct my_type
{int i;
template<typename OStream>
friend OStream &operator<<(OStream &os, const my_type &c)
{return os << "[my_type i=" << c.i << "]";
}
};
void user_defined_example()
{"user defined type: {}", my_type{14});
spdlog::info(
}
// Custom error handler. Will be triggered on log failure.
void err_handler_example()
{// can be set globally or per logger(logger->set_error_handler(..))
const std::string &msg) {
spdlog::set_error_handler([]("*** Custom log error handler: %s ***\n", msg.c_str()); });
printf(
}
// syslog example (linux/osx/freebsd)
#ifndef _WIN32
#include "spdlog/sinks/syslog_sink.h"
void syslog_example()
{std::string ident = "spdlog-example";
auto syslog_logger = spdlog::syslog_logger_mt("syslog", ident, LOG_PID);
"This is warning that will end up in syslog.");
syslog_logger->warn(
}#endif
// Android example.
#if defined(__ANDROID__)
#include "spdlog/sinks/android_sink.h"
void android_example()
{std::string tag = "spdlog-android";
auto android_logger = spdlog::android_logger_mt("android", tag);
"Use \"adb shell logcat\" to view this message.");
android_logger->critical(
}
#endif
/*** R
exampleTwo()
*/
We are not showing the output here; it can be compiled, linked, loaded and run just as above by simply passing the filename to Rcpp::sourceCpp()
. Note that is will create a few demonstration logfiles so you may want to run the example from a temporary directory.
Third example: Colour
The next example highlights a colour âsinkâ for the logger. Again, do not use this as a starting point for your package as R CMD check
will protest about use of stdout
.
#include "spdlog/spdlog.h"
#include "spdlog/sinks/stdout_color_sinks.h"
#include <Rcpp.h>
// [[Rcpp::depends(RcppSpdlog)]]
// [[Rcpp::export]]
void exampleThree() {
auto console = spdlog::stdout_color_mt("console");
// change log pattern (changed from [%H:%M:%S %z] [%n] [%^---%L---%$] )
"[%H:%M:%S.%f] [%^%L%$] [thread %t] %v");
spdlog::set_pattern("This an info message with custom format");
spdlog::info(//spdlog::set_pattern("%+"); // back to default format
"Welcome to spdlog!");
spdlog::info("Some error message with arg: {}", 1);
spdlog::error(
"Easy padding in numbers like {:08d}", 12);
spdlog::warn("Support for int: {0:d}; hex: {0:x}; oct: {0:o}; bin: {0:b}", 42);
spdlog::critical("Support for floats {:03.2f}", 1.23456);
spdlog::info("Positional args are {1} {0}..", "too", "supported");
spdlog::info("{:<30}", "left aligned");
spdlog::info(
// added to this example file allow multiple runs of different package functions
"console");
spdlog::drop(
}
/*** R
exampleThree()
*/
When running this example in a terminal capable of displaying colour escape sequence, the logging levels are distinguished by colour. This ranges from green (âinfoâ) to yellow (âwarningâ) to red (âerrorâ) and white-on-red (âcriticalâ). Not that in this vignette color from standard output does how not show (in the keep-it-simple-mode we are using here).
R> exampleThree()
14:47:52.260692] [I] [thread 2502026] This an info message with custom format
[14:47:52.260715] [I] [thread 2502026] Welcome to spdlog!
[14:47:52.260732] [E] [thread 2502026] Some error message with arg: 1
[14:47:52.260734] [W] [thread 2502026] Easy padding in numbers like 00000012
[14:47:52.260736] [C] [thread 2502026] Support for int: 42; hex: 2a; oct: 52; bin: 101010
[14:47:52.260739] [I] [thread 2502026] Support for floats 1.23
[14:47:52.260741] [I] [thread 2502026] Positional args are supported too..
[14:47:52.260743] [I] [thread 2502026] left aligned
[R>
Fourth Example: Dedicated R Logger
The next example is suitable for use in R packages, and in fact included as an example in the package. We include the source file src/exampleRsink.cpp.
// this portmanteau include also defines the r_sink we use below, and which
// diverts all logging to R via the Rcpp::Rcout replacement for std::cout
#include <RcppSpdlog>
#include <spdlog/stopwatch.h> // also support stopwatch feature
//' spdlog Example using a sink for R
//'
//' A simple example invoking a derived R/Rcpp logger. Also demonstrates the
//' stopwatch feature. For more features see the 'spdlog' documnetation.
//'
//' Note that this no longer triggers R warnings thanks to excellent help by
//' Gabi Melman.
//' @return None
//' @examples
//' exampleRsink()
// [[Rcpp::export]]
void exampleRsink() {
std::string logname = "fromR"; // fix a name for this logger
auto sp = spdlog::get(logname); // retrieve existing one
if (sp == nullptr) sp = spdlog::r_sink_mt(logname); // or create new one if needed
// and set as default
spdlog::set_default_logger(sp);
// instantiate a stop watch
spdlog::stopwatch sw;
// change log pattern (changed from [%H:%M:%S %z] [%n] [%^---%L---%$] )
"[%H:%M:%S.%f] [%n] [%^%L%$] [thread %t] %v");
spdlog::set_pattern(
"Welcome to spdlog!");
spdlog::info("Some error message with arg: {}", 1);
spdlog::error("Elapsed time: {}", sw);
spdlog::info(
"Easy padding in numbers like {:08d}", 12);
spdlog::warn("Support for int: {0:d}; hex: {0:x}; oct: {0:o}; bin: {0:b}", 42);
spdlog::critical("Support for floats {:03.2f}", 1.23456);
spdlog::info("Positional args are {1} {0}..", "too", "supported");
spdlog::info("{:<30}", "left aligned");
spdlog::info("Elapsed time: {}", sw);
spdlog::info(
}
//' spdlog Logging Lever Setter
//'
//' A helper function to turn a logging level given as string
//' into the current logging level
//'
//' @param name A string with the logging level. Value understood are,
//' in decreasing verbosity \sQuote{trace}, \sQuote{debug}, \sQuote{info},
//' \sQuote{warning}, \sQuote{error}, \sQuote{critical}, and \sQuote{off}.
//' Unrecognised names are equivalent to \sQuote{off}.
//' @return Nothing is returned.
// [[Rcpp::export]]
void setLogLevel(const std::string &name) {
spdlog::set_level(spdlog::level::from_str(name)); }
The example file contains three key aspects to highlight: - use of the r_sink_mt()
class for R-specific logger sink - use of the very convenient stopwatch
object - use of logging levels
We highlight these below after first showing the relevant output:
R> exampleRsink()
16:52:12.076751] [fromR] [I] [thread 2453030] Welcome to spdlog!
[16:52:12.076809] [fromR] [E] [thread 2453030] Some error message with arg: 1
[16:52:12.076823] [fromR] [I] [thread 2453030] Elapsed time: 9.6104e-05
[16:52:12.076833] [fromR] [W] [thread 2453030] Easy padding in numbers like 00000012
[16:52:12.076844] [fromR] [C] [thread 2453030] Support for int: 42; hex: 2a; oct: 52; bin: 101010
[16:52:12.076853] [fromR] [I] [thread 2453030] Support for floats 1.23
[16:52:12.076871] [fromR] [I] [thread 2453030] Positional args are supported too..
[16:52:12.076879] [fromR] [I] [thread 2453030] left aligned
[16:52:12.076892] [fromR] [I] [thread 2453030] Elapsed time: 0.000167057
[R>
R-specific sink
spdlog has the ability to derive and sub-class sinks for logger. The RcppSpdlog package uses this feature to implement a sink using the Rcpp conduit Rcpp::Rcout
instead of std::cout
as it conveniently redirects to the R output stream. This class should be the default one for any R packages wanting to use spdlog while also passing R CMD check
.
A second important aspect of the initial code in function exampleRsink()
is how a named logging instance is requested. If none is found, a new one is instantiated. Next, this logger is made the default logger permitting convenient programmatic access via spdlog::
.
Stopwatch
A powerful (recent) feature of the include fmt library is the automatic formatting of timestamps and interval. As the code example shows, simply instantianting an object, here called sw
, and referring to it later is all that takes.
Log-Level
A second utility function setLogLevel()
is also provided. Usage is simple: after calling it with a given level, only message equal to it or higher are shown as the next example shows.
R> setLogLevel("error")
R> exampleRsink()
16:54:12.666261] [fromR] [E] [thread 2453030] Some error message with arg: 1
[16:54:12.666286] [fromR] [C] [thread 2453030] Support for int: 42; hex: 2a; oct: 52; bin: 101010
[R>
By requestion level âerrorâ, message of level âinfoâ, âwarningâ or âdebugâ are suppressed but messages levels âerrorâ or âcriticalâ as shown as desired.
Fifth Example: Initialization
Package desiring to use RcppSpdlog can initialize its facilities during startup.
For that we first define a setup function
// this portmanteau include also defines the r_sink we use below, and which
// diverts all logging to R via the Rcpp::Rcout replacement for std::cout
#include <RcppSpdlog>
//' Set a new default logger for R
//'
// [[Rcpp::export]]
void setDefault() {
std::string logname = "fromR"; // fix a name for this logger
auto sp = spdlog::get(logname); // retrieve existing one
if (sp == nullptr) sp = spdlog::r_sink_mt(logname); // or create new one if needed
"[%H:%M:%S.%f] [%^%L%$] %v");
sp->set_pattern(
spdlog::set_default_logger(sp); }
We can then call this function during startup:
<- function(libname, pkgname) {
.onLoad setDefault();
}
Sixth Example: Compile-time Selection
Of course, spdlog also supports a common usage paradigm with loggers in which the decision of whether to log or not is compile-time rather than run time. As this is typically implemented via macros, usage is via upper-case macros as well.
The following example shows a function with three different logging-level statements as well as a #define
set such one and only one is shown. Similarly, code can contain debug or trace or info or ⌠statements which would not appear in the actually loaded âproduction codeâ (or CRAN version) if the compile-time logging level define is set high enough.
// this portmanteau include also defines the r_sink we use below, and which
// diverts all logging to R via the Rcpp::Rcout replacement for std::cout
#include <RcppSpdlog>
// A define such as this could also be set in src/Makevars via a -D flag
#define SPDLOG_LOG_LEVEL SPDLOG_LEVEL_CRITICAL
// [[Rcpp::export]]
void demoInvisible() {
"Hello from demoInvisible, just to show we're being called...\n";
Rcpp::Rcout <<
// trace message via default logger
"Some trace message.. {} ,{}", 1, 3.23);
SPDLOG_TRACE(// debug message via default logger
"Some debug message.. {} ,{}", 1, 3.23);
SPDLOG_DEBUG(// debug message via default logger
"Some critical message.. {} ,{}", 1, 3.23);
SPDLOG_CRITICAL( }
When a piece of code with such compile-time defines is used, we see the expected outcome. The following example uses default logger, and as the preceding section showed this can be set up to be the custom R sink:
> Rcpp::sourceCpp("/tmp/rcppspdlog.cpp") # plus a '// [[Rcpp:depends("RcppSpdlog")'
R> demoInvisible()
R're being called...
Hello from demoInvisible, just to show we[08:44:48.198075] [fromR] [C] [thread 2453030] Some critical message.. 1 ,3.23
R>
Conclusion
spdlog and the included fmt are two very powerful and widely used C++ libraries. The RcppSpdlog package adds to them to the set of packages R users can deploy. It is our hope that the examples shown here are of interest to R users who are looking for effortless, performant and flexible logging solutions for their compiled R extension packages.