Skip to content

Instantly share code, notes, and snippets.

@jonathancallahan
Created March 15, 2017 23:07
Show Gist options
  • Save jonathancallahan/3ed51265d3c6d56818458de95567d3ae to your computer and use it in GitHub Desktop.
Save jonathancallahan/3ed51265d3c6d56818458de95567d3ae to your computer and use it in GitHub Desktop.
R script that wraps futile.logger to emulate python style logging for six levels: TRACE, DEBUG, INFO, WARN, ERROR, FATAL.
#' @name logger.setup
#' @export
#' @title Set Up Python-Style Logging
#' @param traceLog file name or full path where \code{logger.trace()} messages will be sent
#' @param debugLog file name or full path where \code{logger.debug()} messages will be sent
#' @param infoLog file name or full path where \code{logger.info()} messages will be sent
#' @param warnLog file name or full path where \code{logger.warn()} messages will be sent
#' @param errorLog file name or full path where \code{logger.error()} messages will be sent
#' @param fatalLog file name or full path where \code{logger.fatal()} messages will be sent
#' @return No return value.
#' @description Good logging allows package developers and users to create log files at different
#' levels to track and debug lengthy or complex calculations. "Python-style" logging is intended
#' to suggest that users should set up multiple log files for different log severities
#' so that the \code{errorLog} will contain only log messages at or above the \code{ERROR} level while
#' a \code{debugLog} will contain log messages at the \code{DEBUG} level as well as all higher
#' levels.
#'
#' Python-style log files are set up with \code{logger.setup()}. Logs can be set up for any
#' combination of log levels. Accepting the default \code{NULL} setting for any log file
#' simply means that log file will not be created.
#'
#' Python-style logging requires the use of \code{logger.debug()} style logging statements as seen
#' in the example below.
#' @note All functionality is built on top of the excellent \pkg{futile.logger} package.
#' @examples
#' \dontrun{
#' # Only save three log files
#' logger.setup(debugLog='debug.log', infoLog='info.log', errorLog='error.log')
#'
#' # But allow lot statements at all levels within the code
#' logger.trace('trace statement #%d', 1)
#' logger.debug('debug statement')
#' logger.info('info statement %s %s', "with", "arguments")
#' logger.warn('warn statement %s', "about to try something dumb")
#' result <- try(1/"a", silent=TRUE)
#' logger.error('error message: %s', geterrmessage())
#' logger.fatal('fatal statement %s', "THE END")
#' }
#' @seealso \code{\link{logger.trace}} \code{\link{logger.debug}} \code{\link{logger.info}}
#' \code{\link{logger.warn}} \code{\link{logger.error}} \code{\link{logger.fatal}}
# Set up logging namespaces
logger.setup <- function(traceLog=NULL,
debugLog=NULL,
infoLog=NULL,
warnLog=NULL,
errorLog=NULL,
fatalLog=NULL) {
if ( ! 'futile.logger' %in% loadedNamespaces() ) requireNamespace('futile.logger', quietly=TRUE)
# By default, the console receives only FATAL messages.
futile.logger::flog.threshold(futile.logger::FATAL)
# Set up TRACE logging
if ( is.null(traceLog) ) {
invisible( futile.logger::flog.logger("trace", futile.logger::TRACE, appender.null()) )
} else {
if ( file.exists(traceLog) ) result <- file.remove(traceLog)
invisible( futile.logger::flog.logger("trace", futile.logger::TRACE, futile.logger::appender.file(traceLog)) )
}
# Set up DEBUG logging
if ( is.null(debugLog) ) {
invisible( futile.logger::flog.logger("debug", futile.logger::DEBUG, appender.null()) )
} else {
if ( file.exists(debugLog) ) result <- file.remove(debugLog)
invisible( futile.logger::flog.logger("debug", futile.logger::DEBUG, futile.logger::appender.file(debugLog)) )
}
# Set up INFO logging
if ( is.null(infoLog) ) {
invisible( futile.logger::flog.logger("info", futile.logger::INFO, appender.null()) )
} else {
if ( file.exists(infoLog) ) result <- file.remove(infoLog)
invisible( futile.logger::flog.logger("info", futile.logger::INFO, futile.logger::appender.file(infoLog)) )
}
# Set up WARN logging
if ( is.null(warnLog) ) {
invisible( futile.logger::flog.logger("warn", futile.logger::WARN, appender.null()) )
} else {
if ( file.exists(warnLog) ) result <- file.remove(warnLog)
invisible( futile.logger::flog.logger("warn", futile.logger::WARN, futile.logger::appender.file(warnLog)) )
}
# Set up ERROR logging
if ( is.null(errorLog) ) {
invisible( futile.logger::flog.logger("error", futile.logger::ERROR, appender.null()) )
} else {
if ( file.exists(errorLog) ) result <- file.remove(errorLog)
invisible( futile.logger::flog.logger("error", futile.logger::ERROR, futile.logger::appender.file(errorLog)) )
}
# Set up FATAL logging
if ( is.null(fatalLog) ) {
invisible( futile.logger::flog.appender(futile.logger::appender.console(), name='ROOT') )
} else {
if ( file.exists(fatalLog) ) result <- file.remove(fatalLog)
invisible( futile.logger::flog.appender(futile.logger::appender.tee(fatalLog)) )
}
}
#' @name logger.setLevel
#' @export
#' @title Set Console Log Level
#' @param level threshold level
#' @return No return value.
#' @description By default, the looger threshold is set to \code{FATAL} so that the console
#' will typically receive no log messages. By setting the level to one of the other log levels:
#' \code{TRACE, DEBUG, INFO, WARN, ERROR} users can see logging messages while running
#' commands at the command line.
#' @note All functionality is built on top of the excellent \pkg{futile.logger} package.
#' @seealso \code{\link{logger.setup}}
#' @examples
#' \dontrun{
#' # Set up console logging only
#' logger.setup()
#' logger.setLevel(DEBUG)
#' }
logger.setLevel <- function(level) {
invisible( futile.logger::flog.threshold(level) )
}
#' @name logger.trace
#' @export
#' @title Python-Style Logging Statements
#' @param msg message with format strings applied to additional arguments
#' @param \dots additional arguments to be formatted
#' @return No return value.
#' @description After initializing the level-specific log files with \code{logger.setup(...)},
#' this function will generate \code{TRACE} level log statements.
#' @note All functionality is built on top of the excellent \pkg{futile.logger} package.
#' @examples
#' \dontrun{
#' # Only save three log files
#' logger.setup(debugLog='debug.log', infoLog='info.log', errorLog='error.log')
#'
#' # But allow log statements at all levels within the code
#' logger.trace('trace statement #%d', 1)
#' logger.debug('debug statement')
#' logger.info('info statement %s %s', "with", "arguments")
#' logger.warn('warn statement %s', "about to try something dumb")
#' result <- try(1/"a", silent=TRUE)
#' logger.error('error message: %s', geterrmessage())
#' logger.fatal('fatal statement %s', "THE END")
#' }
#' @seealso \code{\link{logger.setup}}
# Log at the TRACE level
logger.trace <- function(msg, ...) {
stopIfNotInitialized()
futile.logger::flog.trace(msg, ..., name='ROOT')
futile.logger::flog.trace(msg, ..., name='trace')
}
#' @name logger.debug
#' @export
#' @title Python-Style Logging Statements
#' @param msg message with format strings applied to additional arguments
#' @param \dots additional arguments to be formatted
#' @return No return value.
#' @description After initializing the level-specific log files with \code{logger.setup(...)},
#' this function will generate \code{DEBUG} level log statements.
#' @note All functionality is built on top of the excellent \pkg{futile.logger} package.
#' @examples
#' \dontrun{
#' # Only save three log files
#' logger.setup(debugLog='debug.log', infoLog='info.log', errorLog='error.log')
#'
#' # But allow log statements at all levels within the code
#' logger.trace('trace statement #%d', 1)
#' logger.debug('debug statement')
#' logger.info('info statement %s %s', "with", "arguments")
#' logger.warn('warn statement %s', "about to try something dumb")
#' result <- try(1/"a", silent=TRUE)
#' logger.error('error message: %s', geterrmessage())
#' logger.fatal('fatal statement %s', "THE END")
#' }
#' @seealso \code{\link{logger.setup}}
# Log at the DEBUG level
logger.debug <- function(msg, ...) {
stopIfNotInitialized()
futile.logger::flog.debug(msg, ..., name='ROOT')
futile.logger::flog.debug(msg, ..., name='trace')
futile.logger::flog.debug(msg, ..., name='debug')
}
#' @name logger.info
#' @export
#' @title Python-Style Logging Statements
#' @param msg message with format strings applied to additional arguments
#' @param \dots additional arguments to be formatted
#' @return No return value.
#' @description After initializing the level-specific log files with \code{logger.setup(...)},
#' this function will generate \code{INFO} level log statements.
#' @note All functionality is built on top of the excellent \pkg{futile.logger} package.
#' @examples
#' \dontrun{
#' # Only save three log files
#' logger.setup(debugLog='debug.log', infoLog='info.log', errorLog='error.log')
#'
#' # But allow log statements at all levels within the code
#' logger.trace('trace statement #%d', 1)
#' logger.debug('debug statement')
#' logger.info('info statement %s %s', "with", "arguments")
#' logger.warn('warn statement %s', "about to try something dumb")
#' result <- try(1/"a", silent=TRUE)
#' logger.error('error message: %s', geterrmessage())
#' logger.fatal('fatal statement %s', "THE END")
#' }
#' @seealso \code{\link{logger.setup}}
# Log at the INFO level
logger.info <- function(msg, ...) {
stopIfNotInitialized()
futile.logger::flog.info(msg, ..., name='ROOT')
futile.logger::flog.info(msg, ..., name='trace')
futile.logger::flog.info(msg, ..., name='debug')
futile.logger::flog.info(msg, ..., name='info')
}
#' @name logger.warn
#' @export
#' @title Python-Style Logging Statements
#' @param msg message with format strings applied to additional arguments
#' @param \dots additional arguments to be formatted
#' @return No return value.
#' @description After initializing the level-specific log files with \code{logger.setup(...)},
#' this function will generate \code{WARN} level log statements.
#' @note All functionality is built on top of the excellent \pkg{futile.logger} package.
#' @examples
#' \dontrun{
#' # Only save three log files
#' logger.setup(debugLog='debug.log', infoLog='info.log', errorLog='error.log')
#'
#' # But allow log statements at all levels within the code
#' logger.trace('trace statement #%d', 1)
#' logger.debug('debug statement')
#' logger.info('info statement %s %s', "with", "arguments")
#' logger.warn('warn statement %s', "about to try something dumb")
#' result <- try(1/"a", silent=TRUE)
#' logger.error('error message: %s', geterrmessage())
#' logger.fatal('fatal statement %s', "THE END")
#' }
#' @seealso \code{\link{logger.setup}}
# Log at the WARN level
logger.warn <- function(msg, ...) {
stopIfNotInitialized()
futile.logger::flog.warn(msg, ..., name='ROOT')
futile.logger::flog.warn(msg, ..., name='trace')
futile.logger::flog.warn(msg, ..., name='debug')
futile.logger::flog.warn(msg, ..., name='info')
futile.logger::flog.warn(msg, ..., name='warn')
}
#' @name logger.error
#' @export
#' @title Python-Style Logging Statements
#' @param msg message with format strings applied to additional arguments
#' @param \dots additional arguments to be formatted
#' @return No return value.
#' @description After initializing the level-specific log files with \code{logger.setup(...)},
#' this function will generate \code{ERROR} level log statements.
#' @note All functionality is built on top of the excellent \pkg{futile.logger} package.
#' @examples
#' \dontrun{
#' # Only save three log files
#' logger.setup(debugLog='debug.log', infoLog='info.log', errorLog='error.log')
#'
#' # But allow log statements at all levels within the code
#' logger.trace('trace statement #%d', 1)
#' logger.debug('debug statement')
#' logger.info('info statement %s %s', "with", "arguments")
#' logger.warn('warn statement %s', "about to try something dumb")
#' result <- try(1/"a", silent=TRUE)
#' logger.error('error message: %s', geterrmessage())
#' logger.fatal('fatal statement %s', "THE END")
#' }
#' @seealso \code{\link{logger.setup}}
# Log at the ERROR level
logger.error <- function(msg, ...) {
stopIfNotInitialized()
futile.logger::flog.error(msg, ..., name='ROOT')
futile.logger::flog.error(msg, ..., name='trace')
futile.logger::flog.error(msg, ..., name='debug')
futile.logger::flog.error(msg, ..., name='info')
futile.logger::flog.error(msg, ..., name='warn')
futile.logger::flog.error(msg, ..., name='error')
}
#' @name logger.fatal
#' @export
#' @title Python-Style Logging Statements
#' @param msg message with format strings applied to additional arguments
#' @param \dots additional arguments to be formatted
#' @return No return value.
#' @description After initializing the level-specific log files with \code{logger.setup(...)},
#' this function will generate \code{FATAL} level log statements.
#' @note All functionality is built on top of the excellent \pkg{futile.logger} package.
#' @examples
#' \dontrun{
#' # Only save three log files
#' logger.setup(debugLog='debug.log', infoLog='info.log', errorLog='error.log')
#'
#' # But allow log statements at all levels within the code
#' logger.trace('trace statement #%d', 1)
#' logger.debug('debug statement')
#' logger.info('info statement %s %s', "with", "arguments")
#' logger.warn('warn statement %s', "about to try something dumb")
#' result <- try(1/"a", silent=TRUE)
#' logger.error('error message: %s', geterrmessage())
#' logger.fatal('fatal statement %s', "THE END")
#' }
#' @seealso \code{\link{logger.setup}}
# Log at the fatal level
logger.fatal <- function(msg, ...) {
stopIfNotInitialized()
futile.logger::flog.fatal(msg, ..., name='ROOT')
futile.logger::flog.fatal(msg, ..., name='trace')
futile.logger::flog.fatal(msg, ..., name='debug')
futile.logger::flog.fatal(msg, ..., name='info')
futile.logger::flog.fatal(msg, ..., name='warn')
futile.logger::flog.fatal(msg, ..., name='error')
}
# ----- Utility functions -----------------------------------------------------
# This function is missing from futile.logger
appender.null <- function() {
function(line) invisible(NULL)
}
# Quick test if futile.logger namespace is loaded
stopIfNotInitialized <- function() {
if ( ! 'futile.logger' %in% loadedNamespaces() ) {
stop("You must initialize with 'logger.setup()' before issuing logger statements.", call.=FALSE)
}
}
# ----- Constants -------------------------------------------------------------
# Verbatim values from futile.logger::constants
#' @docType data
#' @name logLevels
#' @aliases FATAL ERROR WARN INFO DEBUG TRACE
#' @title Log Levels
#' @description Log levels matching those found in \pkg{futile.logger}.
#' Available levels include:
#'
#' \code{FATAL ERROR WARN INFO DEBUG TRACE}
#' @export
FATAL <- 1L
names(FATAL) <- "FATAL"
#' @export
ERROR <- 2L
names(ERROR) <- "ERROR"
#' @export
WARN <- 4L
names(WARN) <- "WARN"
#' @export
INFO <- 6L
names(INFO) <- "INFO"
#' @export
DEBUG <- 8L
names(DEBUG) <- "DEBUG"
#' @export
TRACE <- 9L
names(TRACE) <- "TRACE"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment