12

Trying to log all errors and warnings with futile.logger.

Somewhat satisfied with this for dealing with errors:

library(futile.logger)
options(error = function() { flog.error(geterrmessage()) ; traceback() ; stop() })
log("a")
# Error in log("a") : argument non numérique pour une fonction mathématique
# ERROR [2016-12-01 21:12:07] Error in log("a") : argument non numérique pour une fonction mathématique
# 
# No traceback available 
# Erreur pendant l'emballage (wrapup) : 

There is redundancy, but I can easily separate between stderr, stdout and log file, so it's not a problem. It's certainly not pretty, there is an additional "wrapup" error message somehow caused by the final stop() that I don't understand, so I'm open to suggestions.

I cannot find a similar solution for warnings. What I tried:

options(warn = 1L)
options(warning.expression = expression(flog.warn(last.warning)))
log(- 1)
# [1] NaN

But to no avail.

Follow-up question: Are there best practices that I am unknowingly ignoring?

Aurèle
  • 12,545
  • 1
  • 31
  • 49
  • Concerning [warnings](http://stackoverflow.com/a/37455516/5784831). Does that help? – Christoph Dec 13 '16 at 11:32
  • You can capture warnings with `sink`. If you want a regular log file with code, output, and errors/warnings then my advice is to R CMD BATCH your file, which will create a .ROut file. – Carl Dec 13 '16 at 22:24
  • Thx both for relevant pointers, but my question is more specifically about intercepting error and warnings to deal with them my way (namely: logging them with futile.logger) – Aurèle Dec 17 '16 at 13:13
  • Do you only want to log "unhandled" warnings or also log handled warnings (catched with `withCallingHandlers` and `tryCatch`)? Then it will become very difficult... – R Yoda Jun 07 '17 at 17:50
  • @RYoda I'm afraid my understanding of exception handling is not firm enough yet to properly answer. Still trying to wrap my head around `help("withCallingHandlers")`. When a warning is emitted by a function, how can I tell which it is? (apart from looking at source, or entering debug)? For instance with `log(-1)`? – Aurèle Jun 08 '17 at 08:01
  • 2
    @Aurèle I have written (a really long) answer that also shows my personal best practice to wrap original unchanged code to inject logging for R script batch jobs... HIH – R Yoda Jun 08 '17 at 21:34

3 Answers3

9

How about:

options(warning.expression = 
        quote({
            if(exists("last.warning",baseenv()) && !is.null(last.warning)){
                txt = paste0(names(last.warning),collapse=" ")
                try(suppressWarnings(flog.warn(txt)))
                cat("Warning message:\n",txt,'\n',sep = "")
            } 
        }))
Jthorpe
  • 9,756
  • 2
  • 49
  • 64
  • Thanks, you made me realize my confusion between `expression()` and `quote()` (though it's not 100% clear yet). Plus, your version seems more robust than `options(warning.expression = quote(flog.warn(last.warning)))` – Aurèle Jun 08 '17 at 07:56
  • Naive question: how does your answer address @RYoda's first comment: "Do you only want to log "unhandled" warnings or also log handled warnings (catched with withCallingHandlers and tryCatch)? Then it will become very difficult..." – Aurèle Jun 08 '17 at 08:05
  • 1
    `options(warning.expression=...)` won't handle warnings that are handled by other means such as `tryCatch(log(-1),warning=function(w){cat('Hi\n')})` or [withCallingHandlers](https://stackoverflow.com/a/35905561/1519199), and personally, I wouldn't want to log those anyway b/c you would never see them when coding interactively. – Jthorpe Jun 08 '17 at 16:15
  • Did you fully test this? Seems to me `is.null(last.warning)` will error unless a warning has been issued because the object will simply not exist. Should that be `exists('last.warning')`? – RoyalTS Jun 08 '17 at 23:18
  • 1
    Good point @RoyalTS. Tested this on a windows machine earlier (though maybe a warning was raised in my .Rprofile...), but I get the error you mention on my Mac (which doesn't have an .Rprofile...). Adjusted accordlingly... – Jthorpe Jun 09 '17 at 00:58
  • This does not seem to do nothing for me at the moment, with up-to-date version of R and `futile.logger`. Can you explain why? Warnings are just ignored. – AF7 Mar 30 '18 at 21:41
  • @AF7 Not sure, especially b/c I don't have your computer to work with. However, consider using something like `options(warning.expression = quote({browser()}))` and then call `warning("Hello world")` and use the interactive debugger to see what's going on. – Jthorpe Apr 11 '18 at 21:26
  • @Jthorpe Thanks for the answer and sorry for the delay. Apparently if I set the option in the answer I never have a `last.warning` object, in any environment. Any idea? – AF7 Apr 25 '18 at 09:46
  • Check the value of `options("warn")`. According to the docs (`?options`), If it's negative, the `last.warning` will not be created. In my experience the handling of warnings is implementation specific (Cran / MRO / Rstudio), which can be a pain. If `options("warn")` is not negative and calling `warning('Hello'); exists("last.warning",baseenv())` is not `TRUE`, I encourage you to create a new SO question with details on your platform (Version / Soruce) and include this simple example. – Jthorpe Apr 25 '18 at 18:09
7

In can contribute two options to log R conditions like warnings with futile.logger and catch all warnings no matter how deep the function call stack is:

  1. Wrap your code with withCallingHandlers for a basic solution
  2. Use my package tryCatchLog for an advanced solution (for compliance reason: I am the author)

To explain the solutions I have created a simple R script that produces warnings and errors:

# Store this using the file name "your_code_file.R"
# This could be your code...

f1 <- function(value) {
  print("f1() called")
  f2(value)                 # call another function to show what happens
  print("f1() returns")
}

f2 <- function(value) {
  print("f2() called")
  a <- log(-1)           # This throws a warning: "NaNs produced"
  print(paste("log(-1) =", a))
  b <- log(value)        # This throws an error if you pass a string as value
  print("f2() returns")
}

f1(1)  # produces a warning
f1("not a number") # produces a warning and an error

Executed "as is" (without logging) this code produces this output:

[1] "f1() called"
[1] "f2() called"
[1] "log(-1) = NaN"
[1] "f2() returns"
[1] "f1() returns"
[1] "f1() called"
[1] "f2() called"
[1] "log(-1) = NaN"
Error in log(value) : non-numeric argument to mathematical function
Calls: source -> withVisible -> eval -> eval -> f1 -> f2
In addition: Warning messages:
1: In log(-1) : NaNs produced
2: In log(-1) : NaNs produced

Solution 1 (withCallingHandlers)

Create a new R file that is called by R and sources your unchanged (!) original R script:

# Store this using the file name "logging_injector_withCallingHandlers.R"
# Main function to inject logging of warnings without changing your original source code

library(futile.logger)

flog.threshold(INFO)

# Injecting the logging of errors and warnings:
tryCatch(withCallingHandlers({
  source("your_code_file.R")     # call your unchanged code by sourcing it!
}, error = function(e) {
     call.stack <- sys.calls()   # "sys.calls" within "withCallingHandlers" is like a traceback!
     log.message <- e$message
     flog.error(log.message)     # let's ignore the call.stack for now since it blows-up the output
}, warning = function(w) {
     call.stack <- sys.calls()   # "sys.calls" within "withCallingHandlers" is like a traceback!
     log.message <- w$message
     flog.warn(log.message)      # let's ignore the call.stack for now since it blows-up the output
     invokeRestart("muffleWarning")  # avoid warnings to "bubble up" to being printed at the end by the R runtime
})
 , error = function(e) {
  flog.info("Logging injector: The called user code had errors...")
})

If you execute this wrapper code the R output is:

$ Rscript logging_injector_withCallingHandlers.R
NULL
[1] "f1() called"
[1] "f2() called"
WARN [2017-06-08 22:35:53] NaNs produced
[1] "log(-1) = NaN"
[1] "f2() returns"
[1] "f1() returns"
[1] "f1() called"
[1] "f2() called"
WARN [2017-06-08 22:35:53] NaNs produced
[1] "log(-1) = NaN"
ERROR [2017-06-08 22:35:53] non-numeric argument to mathematical function
INFO [2017-06-08 22:35:53] Logging injector: The called user code had errors...

As you can see

  • warnings are logged now
  • the call stack could be output too (I have disabled this to avoid flooding this answer)

References: https://stackoverflow.com/a/19446931/4468078

Solution 2 - package tryCatchLog (I am the author)

Solution 1 has some drawbacks, mainly:

  • The stack trace ("traceback") does not contain file names and line numbers
  • The stack trace is flooded with internal function calls you don't want to see (believe me or try it with your non-trival R scripts ;-)

Instead of copying&pasting the above code snippet again and again I have developed a package that encapsulates the above withCallingHandlers logic in a function and adds additional features like

  • logging of errors, warnings and messages
  • identifying the origin of errors and warnings by logging a stack trace with a reference to the source file name and line number
  • support post-mortem analysis after errors by creating a dump file with all variables of the global environment (workspace) and each function called (via dump.frames) - very helpful for batch jobs that you cannot debug on the server directly to reproduce the error!

To wrap the above R script file using tryCatchLog create a wrapper file

# Store this using the file name "logging_injector_tryCatchLog.R"
# Main function to inject logging of warnings without changing your original source code

# install.packages("devtools")
# library(devtools)
# install_github("aryoda/tryCatchLog")
library(tryCatchLog)
library(futile.logger)

flog.threshold(INFO)

tryCatchLog({
  source("your_code_file.R")     # call your unchanged code by sourcing it!
  #, dump.errors.to.file = TRUE  # Saves a dump of the workspace and the call stack named dump_<YYYYMMDD_HHMMSS>.rda
})

and execute it via Rscript to get this (shortened!) result:

# $ Rscript -e "options(keep.source = TRUE); source('logging_injector_tryCatchLog.R')" > log.txt
[1] "f1() called"
[1] "f2() called"
WARN [2017-06-08 23:13:31] NaNs produced
Compact call stack:
  1 source("logging_injector_tryCatchLog.R")
  2 logging_injector_tryCatchLog.R#12: tryCatchLog({
  3 logging_injector_tryCatchLog.R#13: source("your_code_file.R")
  4 your_code_file.R#18: f1(1)
  5 your_code_file.R#6: f2(value)
  6 your_code_file.R#12: .signalSimpleWarning("NaNs produced", quote(log(-1)))
Full call stack:
  1 source("logging_injector_tryCatchLog.R")
  2 withVisible(eval(ei, envir))

  ...
<a lot of logging output omitted here...>

As you can see clearly at the call stack level 6 the source code file name and line number (#12) is logged as the source of the warning together with the source code snippet throwing the warning:

6 your_code_file.R#12 .signalSimpleWarning("NaNs produced", quote(log(-1)))

R Yoda
  • 8,358
  • 2
  • 50
  • 87
1

The way you should use futile.logger is shown in its documentation. Here us a quick example, of how it typically is used, and how to set the threshold.

# set log file to write to
flog.appender (appender.file ("mylog.log"))
# set log threshold, this ensures only logs at or above this threshold are written.
flog.threshold(WARN)

flog.info("DOES NOT LOG")
flog.warn ("Logged!")
calder-ty
  • 431
  • 3
  • 9
  • 2
    Thx @calder.ty. I understand the typical use of futile.logger as examplified in the doc. But my question is more about intercepting errors and warnings that are triggered by other functions (not my own functions), so that I can deal with them my way (that is logging them with futile.logger). – Aurèle Dec 17 '16 at 13:16