Logging Performance

The following vignette presents benchmarks for log4r against all general-purpose logging packages available on CRAN:

Each logging package features slightly different capabilities, but these benchmarks are focused on the two situations common to using all of them:

  1. Logging simple messages to the console; and
  2. Deciding not to log a message because it is below the threshold.

The first of these is likely the most common kind of logging done by end users, although some may chose to log to files, over HTTP, or to the system log (among others). Yet a benchmark of these other scenarios would largely show the relative expense of these operations, instead of the overhead of the logic performed by the logging packages themselves.

The second measures the performance impact of leaving logging messages in running code, even if they are below the current threshold of visibility. This is another measure of overhead for each logging package.

Using cat()

As a reference point, we can measure how long it takes R itself to write a simple message to the console:

cat_debug <- function() {
  cat() # Print nothing.
}

cat_info <- function() cat(
  "INFO  [", format(Sys.time(), "%Y-%m-%d %H:%M:%S", usetz = FALSE),
  "] Info message.", sep = ""
)

The log4r Package

The following is a typical log4r setup:

log4r_logger <- log4r::logger(threshold = "INFO")

log4r_info <- function() {
  log4r::log_info(log4r_logger, "Info message.")
}

log4r_debug <- function() {
  log4r::log_debug(log4r_logger, "Debug message.")
}

The futile.logger Package

The following is a typical futile.logger setup:

requireNamespace("futile.logger")
#> Loading required namespace: futile.logger

futile.logger::flog.logger()

fl_info <- function() {
  futile.logger::flog.info("Info message.")
}

fl_debug <- function() {
  futile.logger::flog.debug("Debug message.")
}

The logging Package

The following is what I believe to be a typical logging setup:

requireNamespace("logging")
#> Loading required namespace: logging

logging::basicConfig()

logging_info <- function() {
  logging::loginfo("Info message.")
}

logging_debug <- function() {
  logging::logdebug("Debug message.")
}

The logger Package

The following is what I believe to be a typical logger setup:

requireNamespace("logger")
#> Loading required namespace: logger
#> Registered S3 method overwritten by 'logger':
#>   method         from 
#>   print.loglevel log4r

# Match the behaviour of other logging packages and write to the console.
logger::log_appender(logger::appender_stdout)

logger_info <- function() {
  logger::log_info("Info message.")
}

logger_debug <- function() {
  logger::log_debug("Debug message.")
}

The lgr Package

The following is what I believe to be a typical lgr setup:

requireNamespace("lgr")
#> Loading required namespace: lgr

lgr_logger <- lgr::get_logger("perf-test")
lgr_logger$set_appenders(list(cons = lgr::AppenderConsole$new()))
lgr_logger$set_propagate(FALSE)

lgr_info <- function() {
  lgr_logger$info("Info message.")
}

lgr_debug <- function() {
  lgr_logger$debug("Debug message.")
}

The loggit Package

The following is what I believe to be a typical loggit setup. Since we only want to log to the console, set the output file to /dev/null. In addition, loggit does not have a notion of thresholds, so there is no “do nothing” operation to test.

requireNamespace("loggit")
#> Loading required namespace: loggit

if (.Platform$OS.type == "unix") {
  loggit::set_logfile("/dev/null")
} else {
  loggit::set_logfile("nul")
}

loggit_info <- function() {
  loggit::loggit("INFO", "Info message.")
}

The rlog Package

The rlog package currently has no configuration options other than the threshold, which is controlled via an environment variable and defaults to hiding debug-level messages:

requireNamespace("rlog")
#> Loading required namespace: rlog

rlog_info <- function() {
  rlog::log_info("Info message.")
}

rlog_debug <- function() {
  rlog::log_debug("Debug message.")
}

Test All Loggers

Debug messages should print nothing.

log4r_debug()
cat_debug()
logging_debug()
fl_debug()
logger_debug()
lgr_debug()
rlog_debug()

Info messages should print to the console. Small differences in output format are to be expected.

log4r_info()
#> INFO  [2025-02-15 02:54:15] Info message.
cat_info()
#> INFO  [2025-02-15 02:54:15] Info message.
logging_info()
#> 2025-02-15 02:54:15.873582 INFO::Info message.
fl_info()
#> INFO [2025-02-15 02:54:15] Info message.
logger_info()
#> INFO [2025-02-15 02:54:15] Info message.
lgr_info()
#> INFO  [02:54:15.879] Info message.
loggit_info()
#> {"timestamp": "2025-02-15T02:54:15+0000", "log_lvl": "INFO", "log_msg": "Info message."}
rlog_info()
#> 2025-02-15 02:54:15.881994 [INFO] Info message.

Benchmarks

The following benchmarks all loggers defined above:

info_bench <- microbenchmark::microbenchmark(
  cat = cat_info(),
  log4r = log4r_info(),
  futile.logger = fl_info(),
  logging = logging_info(),
  logger = logger_info(),
  lgr = lgr_info(),
  loggit = loggit_info(),
  rlog = rlog_info(),
  times = 500,
  control = list(warmups = 50)
)

debug_bench <- microbenchmark::microbenchmark(
  cat = cat_debug(),
  log4r = log4r_debug(),
  futile.logger = fl_debug(),
  logging = logging_debug(),
  logger = logger_debug(),
  lgr = lgr_debug(),
  rlog = rlog_debug(),
  times = 500,
  control = list(warmups = 50)
)

How long does it take to print messages?

print(info_bench, order = "median")
#> Unit: microseconds
#>           expr      min        lq       mean    median        uq       max
#>          log4r   25.968   36.9540   40.03603   38.4570   40.1650   843.416
#>            cat   41.397   49.3875   61.32011   53.2995   55.6090  2720.411
#>           rlog   80.620   97.1010  102.94900  101.9510  105.4120   802.148
#>         logger  196.447  228.5020  248.45544  237.7795  242.5635  2769.183
#>        logging  274.963  311.4165  337.40939  319.6820  328.0130  6397.409
#>            lgr  448.477  493.4515  581.81961  506.2710  514.9320 30054.172
#>         loggit  610.521  653.6055  707.23426  667.2010  685.3050  5462.803
#>  futile.logger 1915.488 1983.7100 2086.20225 2011.1265 2049.8285  4905.612
#>  neval
#>    500
#>    500
#>    500
#>    500
#>    500
#>    500
#>    500
#>    500

How long does it take to decide to do nothing?

print(debug_bench, order = "median")
#> Unit: microseconds
#>           expr     min       lq       mean   median       uq      max neval
#>            cat   1.863   2.5700   4.750894   3.7675   4.1980  659.051   500
#>          log4r   2.555   3.4515   6.024676   4.5535   5.5400  751.814   500
#>           rlog   6.392   7.8550  11.835056   9.8235  11.8320  900.482   500
#>            lgr  12.613  14.8630  19.474534  19.4465  20.4430  749.440   500
#>         logger  15.488  18.8350  24.654118  23.2635  27.2105  708.734   500
#>        logging  16.210  20.1030  26.706826  27.5415  29.6750  726.067   500
#>  futile.logger 558.123 574.8330 611.293130 583.3400 598.5430 3220.936   500