## Registered S3 method overwritten by 'log4r':
##   method         from  
##   print.loglevel logger

In this vignette I suppose that you are already familiar with at least one of the similar logging R packages and you are looking for suggestions on how to switch to logger. Before moving forward, please make sure that you have read the Introduction to logger, The Anatomy of a Log Request and Customizing the Format and the Destination of a Log Record vignettes for a decent background on logger, and use this vignette as a quick-reference sheet to help you migrate from another package.

futile.logger

The logger package has been very heavily inspired by futile.logger and have been using it for many years, also opened multiple pull requests to extend futile.logger before I decided to revamp my ideas into a new R package – but there are still many common things between futile.logger and logger.

Initialize

Both packages comes with a default log engine / config, so it’s enough to load the packages and those are ready to be used right away:

futile.logger

library(futile.logger)
#> 
#> Attaching package: 'futile.logger'
#> The following objects are masked from 'package:logger':
#> 
#>     DEBUG, ERROR, FATAL, INFO, TRACE, WARN

logger

library(logger)

Logging functions

The most important change is that function names are by snake_case in logger, while futile.logger uses dot.separated expressions, and futile.logger prefixes function names by flog while logger uses log for that:

futile.logger

flog.info('hi there')
#> INFO [2024-03-05 15:10:46] hi there
flog.warn('watch out')
#> WARN [2024-03-05 15:10:46] watch out

logger

log_info('hi there')
#> INFO [2024-03-05 15:10:46] hi there
log_warn('watch out')
#> WARN [2024-03-05 15:10:46] watch out

As you can see above, the default layout of the messages is exactly the same.

Log levels

Regarding log levels, futile.logger bundles the default log4j levels (TRACE, DEBUG, INFO, WARN, ERROR and FATAL) that is extended by SUCCESS in logger as sometimes it’s worth logging with a higher than INFO level that something succeeded.

Log record layout

Changing layouts is easy in both package, as you simply pass a layout function:

futile.logger

flog.layout(layout.json)
#> NULL
flog.info('hi again')
#> {"level":"INFO","timestamp":"2024-03-05 15:10:46 +0100","message":"hi again","func":"tools::buildVignettes"}

logger

log_layout(layout_json())

log_info('hi again')
#> {"time":"2024-03-05 15:10:46","level":"INFO","ns":"global","ans":"global","topenv":"R_GlobalEnv","fn":"eval","node":"nevermind","arch":"x86_64","os_name":"Linux","os_release":"6.7.6-arch1-1","os_version":"#1 SMP PREEMPT_DYNAMIC Fri, 23 Feb 2024 16:31:48 +0000","pid":157305,"user":"daroczig","msg":"hi again"}

As you can see, logger provided a bit more information about the log request compared to futile.logger, but it’s easy to change the list of fields to be used in the JSON – see ?get_logger_meta_variables for a complete list of variable names to be passed to ?layout_json. logger also ships a lot more layouts, eg ?layout_glue_colors or roll out your own via the ?layout_glue_generator factory function.

Log message formatting

By default, futile.logger uses an sprintf formatter, while logger passes the objects to be logged to glue:

futile.logger

flog.info('hi')
#> INFO [2024-03-05 15:10:46] hi
flog.info('hi %s', 84/2)
#> INFO [2024-03-05 15:10:46] hi 42
flog.info(paste('hi', 84/2))
#> INFO [2024-03-05 15:10:46] hi 42
flog.info(glue::glue('hi {84/2}'))
#> INFO [2024-03-05 15:10:46] hi 42

logger

log_info('hi')
#> INFO [2024-03-05 15:10:46] hi
log_info('hi {84/2}')
#> INFO [2024-03-05 15:10:46] hi 42
log_formatter(formatter_sprintf)
log_info('hi %s', 84/2)
#> INFO [2024-03-05 15:10:46] hi 42
log_formatter(formatter_paste)
log_info('hi', 84/2)
#> INFO [2024-03-05 15:10:46] hi 42

It’s easy to change this default formatter in both packages: use flog.layout handles this as well in futile.logger, while the formatter is separated from the layout function in logger, so check ?log_formatter instead. logger ships with a bit more formatter functions, eg the default ?formatter_glue and ?formatter_glue_or_sprintf that tries to combine the best from both words.

Log record destination

Setting the destination of the log records works similarly in both packages, although he logger packages bundles a lot more options:

logging

t <- tempfile()
flog.appender(appender.file(t))
#> NULL
flog.appender(appender.tee(t))
#> NULL

logger

t <- tempfile()
log_appender(appender_file(t))
log_appender(appender_tee(t))

Hierarchical logging and performance

Both packages support using different logging namespaces and stacking loggers within the same namespace. Performance-wise, there’s logger seems to be faster than futile.logger, but for more details, check the Simple Benchmarks on Performance vignette.

Using logger as a drop-in-replacement of futile.logger

logger has no hard requirements, so it’s a very lightweight alternative of futile.logger. Although the function names are a bit different, and the message formatter also differs, but with some simple tweaks, logger can become an almost perfect drop-in-replacement of futile.logger:

library(logger)
log_formatter(formatter_sprintf)
flog.trace <- log_trace
flog.debug <- log_debug
flog.info <- log_info
flog.warn <- log_warn
flog.error <- log_error

flog.info('Hello from logger in a futile.logger theme ...')
flog.warn('... where the default log message formatter is %s', 'sprintf')

logging

The logging package behaves very similarly to the Python logging module and so thus being pretty Pythonic, while logger tries to accommodate native R users’ expectations – so there are some minor nuances between the usage of the two packages.

Initialize

In logging, you have to initialize a logger first via addHandler or simply by calling basicConfig, which is not required in logger as it already comes with a default log config:

logging

library(logging)
basicConfig()

logger

library(logger)

Logging functions

After initializing the logging engine, actual logging works similarly in the two packages – with a bit different function names:

  • although logging uses mostly camelCase function names (eg basicConfig), but the logging functions are all lowercase without any separator, such as loginfo or logwarn
  • logger uses snake_case for the function names, such as log_info and log_warn

logging

loginfo('hi there')
#> 2024-03-05 15:10:46.327209 INFO::hi there
logwarn('watch out')
#> 2024-03-05 15:10:46.338784 WARNING::watch out

logger

log_info('hi there')
log_warn('watch out')

As you can see above, the default layout of the log messages is somewhat different:

  • logging starts with the timestamp that is followed by the log level, optional namespace and the message separated by colons
  • logger starts with the log level, followed by the timestamp between brackets and then the message

Log levels

For the available log levels in logging, check ?loglevels, and ?log_levels for the same in logger:

logging

str(as.list(loglevels))
#> List of 11
#>  $ NOTSET  : num 0
#>  $ FINEST  : num 1
#>  $ FINER   : num 4
#>  $ FINE    : num 7
#>  $ DEBUG   : num 10
#>  $ INFO    : num 20
#>  $ WARNING : num 30
#>  $ WARN    : num 30
#>  $ ERROR   : num 40
#>  $ CRITICAL: num 50
#>  $ FATAL   : num 50

logger

levels <- mget(ls(
    envir = environment(logger), pattern = '^[A-Z]'),
    envir = environment(logger))
str(levels[order(-as.numeric(levels))], give.attr = FALSE)
#>  Named list()

Performance

Performance-wise, there’s no big difference between the two packages, but for more details, check the Simple Benchmarks on Performance vignette.

Log record layout

Getting and setting the layout of the log record should happen up-front in both packages:

logging

getLogger()[['handlers']]$basic.stdout$formatter
#> function (record) 
#> {
#>     msg <- trimws(record$msg)
#>     text <- paste(record$timestamp, paste(record$levelname, record$logger, 
#>         msg, sep = ":"))
#>     return(text)
#> }
#> <bytecode: 0x595477b73b38>
#> <environment: namespace:logging>

logger

log_layout()
#> layout_simple

logger provides multiple configurable layouts to fit the user’s need, eg easily show the calling function of the lof request, the pid of the R process, name of the machine etc. or colorized outputs. See Customizing the Format and the Destination of a Log Record vignette for more details.

Log message formatting

If you want to pass dynamic log messages to the log engines, you can do that via the hard-coded sprintf in the logging package, while you can set that on a namespaces basis in logger, which is by default using glue:

logging

loginfo('hi')
#> 2024-03-05 15:10:46.423546 INFO::hi
loginfo('hi %s', 84/2)
#> 2024-03-05 15:10:46.425129 INFO::hi 42
loginfo(paste('hi', 84/2))
#> 2024-03-05 15:10:46.426593 INFO::hi 42
loginfo(glue::glue('hi {84/2}'))
#> 2024-03-05 15:10:46.428261 INFO::hi 42

logger

log_info('hi')
log_info('hi {84/2}')
log_formatter(formatter_sprintf)
log_info('hi %s', 84/2)
log_formatter(formatter_paste)
log_info('hi', 84/2)

For even better compatibility, there’s also ?formatter_logging that not only relies on sprintf when the first argument is a string, but will log the call and the result as well when the log object is an R expression:

log_formatter(formatter_logging)
log_info('42')
log_info(42)
log_info(4+2)
log_info('foo %s', 'bar')
log_info(12, 1+1, 2 * 2)

Log record destination

Setting the destination of the log records works similarly in both packages, although he logger packages bundles a lot more options:

logging

?addHandler
?writeToConsole
?writeToFile

logger

?log_appender
?appender_console
?appender_file
?appender_tee
?appender_slack
?appender_pushbullet

Hierarchical logging

Both packages support using different logging namespaces and stacking loggers within the same namespace.

Using logger as a drop-in-replacement of logging

logger has no hard requirements, so it’s an adequate alternative of logging. Although the function names are a bit different, and the message formatter also differs, but with some simple tweaks, logger can become an almost perfect drop-in-replacement of logging – although not all log levels (eg and ) are supported:

library(logger)
log_formatter(formatter_logging)
log_layout(layout_logging)
logdebug <- log_debug
loginfo <- log_info
logwarn <- log_warn
logerror <- log_error

loginfo('Hello from logger in a logging theme ...')
logwarn('... where the default log message formatter is %s', 'sprintf', namespace = 'foobar')

log4r

The log4r package provides an object-oriented approach for logging in R, so the logger object is to be passed to the log calls – unlike in the logger package.

Initialize

So thus it’s important to create a logging object in log4r before being able to log messages, while that’s automatically done in `logger:

log4r

library(log4r)
#> 
#> Attaching package: 'log4r'
#> The following object is masked _by_ '.GlobalEnv':
#> 
#>     logger
#> The following object is masked from 'package:logging':
#> 
#>     levellog
#> The following objects are masked from 'package:logger':
#> 
#>     as.loglevel, logger
#> The following object is masked from 'package:base':
#> 
#>     debug
logger <- create.logger(logfile = stdout(), level = "INFO")

logger

library(logger)

Please note that in the background, logger does have a concept of logger objects, but that’s behind the scene and the user does not have to specify / reference it. On the other hand, if you wish, you can do that via the namespace concept of logger – more on that later.

Logging functions

While logger has a log_ prefix for all logging functions, log4r has lowercase functions names referring to the log level, which takes a logging object and the log message:

log4r

info(logger, 'hi there')
#> INFO  [2024-03-05 15:10:46] hi there
warn(logger, 'watch out')
#> WARN  [2024-03-05 15:10:46] watch out

logger

log_info('hi there')
log_warn('watch out')

As you can see the default layout of the messages is a bit different in the two packages.

Log levels

Both packages are based on log4j, and log4r provides DEBUG, INFO, WARN, ERROR and FATAL, while logger also adds TRACE and SUCCESS on the top of these.

To change the log level threshold, use the level function on the logging object in log4r, while it’s log_level in logger.

Log record layout and formatter functions

The log4r provides a logformat argument in create.logger that can be used to override the default formatting, while logger provides formatter and layout functions for a flexible log record design.

Log record destination

By default, log4r logs to a file that can be set to stoud to write to the console, while logger writes to the console by default, but logging to files via the appender_file functions is also possible – besides a number of other log record destinations as well.

Hierarchical logging and performance

Creating objects is the log4r way of handling multiple log environments, while logger handles that via namespaces.

loggit

Sorry, no direct replacement for loggit – capturing message, warning and stop function messages, but it’s on the roadmap to provide helper functions to be used as message hooks feed logger.

#> Warning: 'logger' namespace cannot be unloaded:
#>   namespace 'logger' is imported by 'logger.tester' so cannot be unloaded