# Create a log file and a file connection
logfile <- tempfile(fileext = ".log")
logfile_con <- file(logfile, open = "a")
# Write things to the logger
writeLines("Start analyses", con = logfile_con)
res <- 40 + 2
writeLines(paste("res is", res), con = logfile_con)
writeLines("End analyses", con = logfile_con)
close(logfile_con)This post was originally published on the author’s blog.
Introduction
What is logging?
In computer science, logging is the practice of keeping track of some events occurring while the code runs1.
These outputs (called logs) are most often written to a text file (logfile), which has the .log extension by convention.
Why to log
Logging allows to keep track of events in a permanent way that’s not going to be erased (like console outputs) or forgotten (like your memory).
I find logging particularly useful to keep track of:
- Errors/warnings. Sometimes, code can run in an unexpected manner, and logs are useful to determine why and how this happened.
- Metadata about your environment/setup. For example, I use it to record how long a bit of code ran, or which arguments I used.
When to log
Logging is useful primarily for scripts that you don’t monitor when they run, like scripts that are long to run, or a series of short, but numerous numerous scripts.
Logging in base R
To write a logfile, the simplest option is just to use base R functions:
Let’s inspect the logfile:
Start analyses
res is 42
End analyses
This approach is sometimes enough, but there are packages out there that allow to make things more flexible and easy for us on the user end.
The {logger} package
One of them is {logger}. It is very complete and flexible, but at the same time I find it simple to use. This package allows you to:
Basic logger
The first step is to initialize the logger using the log_appender function. Here, we set the appender argument with an appender_file() function. The {logger} package also comes with many other appenders allowing to log to console, Slack channel, Telegram group chat…
We also define a logging level with log_threshold(DEBUG). Here, all messages above the DEBUG level will be logged (see the list and order of levels here).
Then, we can log messages with logging functions. Here, we use log_debug and log_info function to write to our logfile. Note that by default, {logger} uses the {glue} syntax to concatenate text and expressions (exemplified in "res is {res}", where {res} is replaced with the variable value in the log).
Here is our logfile:
DEBUG [2026-03-25 09:14:36] Start script
INFO [2026-03-25 09:14:36] res is 42
DEBUG [2026-03-25 09:14:36] End analyses
That’s it for a basic logger!
Setting logger level
Now imagine we want a detailed logger in the testing phase, but when launching our final script we want to print only important stuff. This can be achieved by changing the log level.
# Create a log file and a file connection
logfile <- tempfile(fileext = ".log")
log_appender(appender = appender_file(file = logfile))
# Change log level to INFO
log_threshold(INFO)
# Write things to the logger
log_debug("Start script")
res <- 40 + 2
log_info("res is {res}")
log_debug("End analyses")In the code above with a log level set to INFO, all DEBUG messages are omitted.
INFO [2026-03-25 09:14:36] res is 42
Logging warnings, errors and messages
Now a useful thing to log are errors, warnings and messages occurring during computations. Consider the code below:
# Create a log file and a file connection
logfile <- tempfile(fileext = ".log")
log_appender(appender = appender_file(file = logfile))
log_threshold(DEBUG)
# Write things to the logger
log_debug("Start script")
res <- "forty-two"
log_info("res is {res}")
res <- as.numeric(res) # This produces a warningWarning: NAs introduced by coercion
Now, the certainly the logfile should show the warning?
DEBUG [2026-03-25 09:14:36] Start script
INFO [2026-03-25 09:14:36] res is forty-two
DEBUG [2026-03-25 09:14:36] End analyses
… except it doesn’t. The logger records only what we tell it to, so we need to explicitly ask to record warnings.
To record warnings, we need to use log_warnings()2:
# Create a file connection (assuming "logfile" is a valid path)
log_appender(appender = appender_file(file = logfile))
log_threshold(DEBUG)
# Record errors in logger
log_warnings()
# Write things to the logger
log_debug("Start script")
res <- "forty-two"
log_info("res is {res}")
res <- as.numeric(res)
log_debug("End analyses")And now, our logger records the warnings.
DEBUG [2026-03-25 09:00:18] Start script
INFO [2026-03-25 09:00:18] res is forty-two
WARN [2026-03-25 09:00:18] NAs introduced by coercion
DEBUG [2026-03-25 09:00:18] End analyses
The same is true for errors and messages, which can be recorded with log_messages() and log_errors().
Logging with parallel computing
Another thing I find interesting with {logger} is that it’s handy to keep track of what’s happening in different parallel processes (see this great blogpost for more explanations on parallel computing in R). Consider the parallel code below: starting from a list of species sightings, it computes the total number of sightings per species:
# Parallel computing libraries
library(parallel)
library(foreach)
library(doParallel)
# Parallel computing setup
n_cores <- 4
cluster <- makeCluster(spec = n_cores)
registerDoParallel(cluster)
# Generate dummy dataset of species sightings
species_counts <- lapply(1:4,
function(i) rbinom(10, size = 1, prob = 0.5))
names(species_counts) <- c("Aeshna cyanea", "Anax imperator",
"Calopteryx virgo", "Crocothemis erythraea")
# Parallel loop
res <- foreach(i = 1:4) %dopar% {
# Get species name
sp <- names(species_counts)[i]
# Get total count
res <- sum(species_counts[[i]])
# Return values
return(paste(sp, res))
}
stopCluster(cluster)
# print the total count per species
res[[1]]
[1] "Aeshna cyanea 8"
[[2]]
[1] "Anax imperator 5"
[[3]]
[1] "Calopteryx virgo 6"
[[4]]
[1] "Crocothemis erythraea 6"
We can log parallel events by defining a logger in each parallel process:
# Parallel computing setup
cluster <- makeCluster(spec = n_cores)
clusterEvalQ(cl = cluster,
expr = {library(logger)})
clusterExport(cl = cluster,
varlist = c("log_appender", "appender_file", "log_info"))
registerDoParallel(cluster)
# Get temporary directory for logfiles
# This ensures all logs are written to the same temp folder
tmpdir <- tempdir()
# Parallel loop
res <- foreach(i = 1:4) %dopar% {
# Get species name
sp <- names(species_counts)[i]
# Create species logger
logfile <- file.path(tmpdir, paste0(sp, ".log"))
log_appender(appender = appender_file(file = logfile))
# Get and log total count
log_info("Logger for species {sp}")
res <- sum(species_counts[[i]])
log_info("Species count: {res}")
# Return values
return(paste(sp, res))
}
stopCluster(cluster)Let’s see what’s in the logfiles:
[1] "File Aeshna cyanea.log -----"
INFO [2026-03-25 09:14:37] Logger for species Aeshna cyanea
INFO [2026-03-25 09:14:37] Species count: 8
[1] "File Anax imperator.log -----"
INFO [2026-03-25 09:14:37] Logger for species Anax imperator
INFO [2026-03-25 09:14:37] Species count: 5
[1] "File Calopteryx virgo.log -----"
INFO [2026-03-25 09:14:37] Logger for species Calopteryx virgo
INFO [2026-03-25 09:14:37] Species count: 6
[1] "File Crocothemis erythraea.log -----"
INFO [2026-03-25 09:14:37] Logger for species Crocothemis erythraea
INFO [2026-03-25 09:14:37] Species count: 6
Amazing! Our outputs got copied to the files!
Conclusion
Logging is a great way to improve the reproducibility of analyses, and the {logger} packages can really make logging easy. This post showcased some applications of the package, including logging warnings and using it with parallel computing, but many other uses are possible: check out the resources below to learn more!
Resources
- {logger} package documentation
- {logger} presentation at RStudio::conf 2020
