This is an automated email from the git hooks/post-receive script. tille pushed a commit to branch master in repository r-cran-futile.logger.
commit e2b1ae41fd4fe25ae71908d87ad1fa4abbc08884 Author: Andreas Tille <[email protected]> Date: Fri Sep 29 21:02:34 2017 +0200 New upstream version 1.4.3 --- DESCRIPTION | 23 +++ MD5 | 26 +++ NAMESPACE | 6 + R/appender.R | 99 ++++++++++ R/constants.R | 14 ++ R/futile.logger-package.R | 113 +++++++++++ R/layout.R | 183 ++++++++++++++++++ R/logger.R | 379 +++++++++++++++++++++++++++++++++++++ R/options.R | 18 ++ R/scat.R | 24 +++ README.md | 133 +++++++++++++ debian/README.test | 9 - debian/changelog | 26 --- debian/compat | 1 - debian/control | 25 --- debian/copyright | 18 -- debian/docs | 3 - debian/rules | 4 - debian/source/format | 1 - debian/tests/control | 3 - debian/tests/run-unit-test | 11 -- debian/watch | 2 - man/flog.appender.Rd | 80 ++++++++ man/flog.carp.Rd | 41 ++++ man/flog.layout.Rd | 95 ++++++++++ man/flog.logger.Rd | 152 +++++++++++++++ man/flog.remove.Rd | 29 +++ man/flog.threshold.Rd | 38 ++++ man/ftry.Rd | 28 +++ man/futile.logger-package.Rd | 123 ++++++++++++ man/logger.options.Rd | 36 ++++ man/scat.Rd | 34 ++++ tests/testthat.R | 3 + tests/testthat/test_debug.R | 20 ++ tests/testthat/test_json.R | 44 +++++ tests/testthat/test_layout.R | 55 ++++++ tests/testthat/test_logger.R | 86 +++++++++ tests/testthat/test_stringconfig.R | 49 +++++ 38 files changed, 1931 insertions(+), 103 deletions(-) diff --git a/DESCRIPTION b/DESCRIPTION new file mode 100644 index 0000000..047fb59 --- /dev/null +++ b/DESCRIPTION @@ -0,0 +1,23 @@ +Package: futile.logger +Type: Package +Title: A Logging Utility for R +Version: 1.4.3 +Date: 2016-07-10 +Author: Brian Lee Yung Rowe +Maintainer: Brian Lee Yung Rowe <[email protected]> +Depends: R (>= 3.0.0) +Imports: utils, lambda.r (>= 1.1.0), futile.options +Suggests: testthat, jsonlite +Description: Provides a simple yet powerful logging utility. Based loosely on + log4j, futile.logger takes advantage of R idioms to make logging a + convenient and easy to use replacement for cat and print statements. +License: LGPL-3 +LazyLoad: yes +NeedsCompilation: no +ByteCompile: yes +Collate: 'options.R' 'appender.R' 'constants.R' 'layout.R' 'logger.R' + 'scat.R' 'futile.logger-package.R' +RoxygenNote: 5.0.1 +Packaged: 2016-07-10 13:44:30 UTC; brian +Repository: CRAN +Date/Publication: 2016-07-10 16:57:47 diff --git a/MD5 b/MD5 new file mode 100644 index 0000000..eb3a3be --- /dev/null +++ b/MD5 @@ -0,0 +1,26 @@ +d614067d8a6f9b6c793d9a3cdb6b0540 *DESCRIPTION +d61ed2adf506a7b7909f59c852f347ed *NAMESPACE +1969ced6e73dcd0fa77a863596c898e5 *R/appender.R +adc433b28d3a7d3af71dd39a1b9db833 *R/constants.R +0ea1553b425022a30df57b7c1e121fb5 *R/futile.logger-package.R +c78a7f26382a3350abee1f71c9209a22 *R/layout.R +0f41f8079924d220d49c59e0e99b90e3 *R/logger.R +eb3624430fe77aa17a803acf9e383c2f *R/options.R +fce29de8b2fb63d0315b07bec6bd5e5c *R/scat.R +5af3301ce507c00309be63d5f2c8bad4 *README.md +6921ef82af355463a353c91dfc3f120d *man/flog.appender.Rd +746d421add2129919aea5cf3c7d6a369 *man/flog.carp.Rd +a5d5136f9eb63de9e4387d9827beb70c *man/flog.layout.Rd +f021d78c3269a680b6b89d0660507bb7 *man/flog.logger.Rd +8ff226e5e61a04f2d2d9b58e07c82cd4 *man/flog.remove.Rd +0c3e06e2c7ae3db202e5f35b9df70a3e *man/flog.threshold.Rd +df02150ddb649d54e0f82ae9343edf84 *man/ftry.Rd +1213f19c2224957247267af28edbd73c *man/futile.logger-package.Rd +b0c25a5578011d180f1766d8c0ba5904 *man/logger.options.Rd +89f313302a032d9ca6be6f422e509d6f *man/scat.Rd +b2d6ad4073eda6f23f39f2faf921f2dc *tests/testthat.R +3962c833c285a6c1dca810336022f000 *tests/testthat/test_debug.R +25119c8e5a5da1695a9987e7a6ba31b9 *tests/testthat/test_json.R +b77c63046eae36f5cb9928dc28d015b2 *tests/testthat/test_layout.R +32c7e9a789e9fc1a107817f03539c01d *tests/testthat/test_logger.R +dd364c3763eb235f19ea637a7ed304b7 *tests/testthat/test_stringconfig.R diff --git a/NAMESPACE b/NAMESPACE new file mode 100644 index 0000000..4ead997 --- /dev/null +++ b/NAMESPACE @@ -0,0 +1,6 @@ +# Generated by roxygen2: do not edit by hand + +exportPattern("^[^\\.]") +import(futile.options) +import(lambda.r) +import(utils) diff --git a/R/appender.R b/R/appender.R new file mode 100644 index 0000000..afe4fd1 --- /dev/null +++ b/R/appender.R @@ -0,0 +1,99 @@ +#' Manage appenders for loggers +#' +#' Provides functions for adding and removing appenders. +#' +#' @section Usage: +#' # Get the appender for the given logger\cr +#' flog.appender(name) \%::\% character : Function\cr +#' flog.appender(name='ROOT') +#' +#' # Set the appender for the given logger\cr +#' flog.appender(fn, name='ROOT') +#' +#' # Print log messages to the console\cr +#' appender.console() +#' +#' # Write log messages to a file\cr +#' appender.file(file) +#' +#' # Write log messages to console and a file\cr +#' appender.tee(file) +#' +#' +#' @section Details: +#' Appenders do the actual work of writing log messages to some target. +#' To use an appender in a logger, you must register it to a given logger. +#' Use \code{flog.appender} to both access and set appenders. +#' +#' The ROOT logger by default uses \code{appender.console}. +#' +#' \code{appender.console} is a function that writes to the console. +#' No additional arguments are necessary when registering the appender +#' via flog.appender. +#' +#' +#' \code{appender.file} writes to a file, so you must pass an additional file +#' argument to the function. To change the file name, just call +#' \code{flog.appender(appender.file(file))} again with a new file name. +#' +#' To use your own appender create a function that takes a single argument, +#' which represents the log message. You need to pass a function reference to +#' \code{flog.appender}. +#' +#' \code{appender.tee} writes to both the console and file. +#' +#' @section Value: +#' When getting the appender, \code{flog.appender} returns the appender +#' function. When setting an appender, \code{flog.appender} has no +#' return value. +#' +#' @name flog.appender +#' @aliases appender.console appender.file appender.tee +#' @param \dots Used internally by lambda.r +#' @author Brian Lee Yung Rowe +#' @seealso \code{\link{flog.logger}} \code{\link{flog.layout}} +#' @keywords data +#' @examples +#' \dontrun{ +#' flog.appender(appender.console(), name='my.logger') +#' +#' # Set an appender to the logger named 'my.package'. Any log operations from +#' # this package will now use this appender. +#' flog.appender(appender.file('my.package.out'), 'my.package') +#' } + +# Get appenders associated with the given logger +flog.appender(name) %::% character : Function +flog.appender(name='ROOT') %as% +{ + logger <- flog.logger(name) + logger$appender +} + +# Set the appender for the given logger +flog.appender(fn, name='ROOT') %as% +{ + flog.logger(name, appender=fn) + invisible() +} + +# Some default handlers for use in futile.logger. All handlers need to conform +# to the below signature: function(line) +appender.console <- function() +{ + function(line) cat(line, sep='') +} + +# Write to a file. +appender.file <- function(file) +{ + function(line) cat(line, file=file, append=TRUE, sep='') +} + +# Write to a file and to console +appender.tee <- function(file){ + function(line) { + cat(line, sep='') + cat(line, file=file, append=TRUE, sep='') + } +} diff --git a/R/constants.R b/R/constants.R new file mode 100644 index 0000000..f6c6cdb --- /dev/null +++ b/R/constants.R @@ -0,0 +1,14 @@ +FATAL <- 1L +names(FATAL) <- "FATAL" +ERROR <- 2L +names(ERROR) <- "ERROR" +WARN <- 4L +names(WARN) <- "WARN" +INFO <- 6L +names(INFO) <- "INFO" +DEBUG <- 8L +names(DEBUG) <- "DEBUG" +TRACE <- 9L +names(TRACE) <- "TRACE" + + diff --git a/R/futile.logger-package.R b/R/futile.logger-package.R new file mode 100644 index 0000000..7c58135 --- /dev/null +++ b/R/futile.logger-package.R @@ -0,0 +1,113 @@ +#' A Logging Utility for R +#' +#' This package implements a logging system inspired by log4j. The basic idea +#' of layouts, appenders, and loggers is faithful to log4j, while the +#' implementation and idiom is all R. This means that support for hierarchical +#' loggers, custom appenders, custom layouts is coupled with a simple and +#' intuitive functional syntax. +#' +#' \tabular{ll}{ +#' Package: \tab futile.logger\cr +#' Type: \tab Package\cr +#' Version: \tab 1.4.3\cr +#' Date: \tab 2016-07-10\cr +#' License: \tab LGPL-3\cr +#' LazyLoad: \tab yes\cr +#' } +#' +#' The latest version of futile.logger introduces zero-configuration semantics +#' out of the box. This means that you can use the default configuration as is. +#' It is also easy to interactively change the configuration of the ROOT +#' logger, as well as create new loggers. Since loggers form a hierarchy based +#' on their name, the ROOT logger is the starting point of the hierarchy and +#' always exists. By default the ROOT logger is defined with a simple layout, +#' printing to the console, with an INFO threshold. This means that writing to +#' any logger with a threshold of INFO or higher will write to the console. +#' +#' All of the logging functions take a format string so it is easy to add +#' arbitrary values to log messages. +#' +#' > flog.info("This song is just \%s words \%s", 7, "long") +#' +#' Thresholds range from most verbose to least verbose: TRACE, DEBUG, INFO, +#' WARN, ERROR, FATAL. You can easily change the threshold of the ROOT logger +#' by calling > flog.threshold(TRACE) which changes will print all log messages +#' from every package. To suppress most logging by default but turn on all +#' debugging for a logger 'my.logger', you would execute +#' +#' > flog.threshold(ERROR)\cr +#' > flog.threshold(TRACE, name='my.logger') +#' +#' Any arbitrary logger can be defined simply by specifying it in any +#' futile.logger write operation (futile.threshold, futile.appender, +#' futile.layout). If the logger hasn't been defined, then it will be defined +#' dynamically. Any unspecified options will be copied from the parent logger. +#' +#' When writing log messages, futile.logger will search the hierarchy based on +#' the logger name. In our example, if 'my.logger' hasn't been defined then +#' futile.logger will look for a logger named 'my' and finally the ROOT logger. +#' +#' Functions calling futile.logger from a package are automatically assigned a +#' logger that has the name of the package. Suppose we have log messages in a +#' package called 'my.package'. Then any function that calls futile.logger from +#' within the package will automatically be assigned a default logger of +#' 'my.package' instead of ROOT. This means that it is easy to change the log +#' setting of any package that uses futile.logger for logging by just updating +#' the logger for the given package. For instance suppose you want to output +#' log message for my.package to a file instead. +#' +#' > flog.appender(appender.file('my.package.log'), name='my.package') +#' +#' Now all log statements in the package my.package will be written to a file +#' instead of the console. All other log messages will continue to be written +#' to the console. +#' +#' Appenders do the actual work of writing log messages to a writeable target, +#' whether that is a console, a file, a URL, database, etc. When creating an +#' appender, the implementation-specific options are passed to the appender at +#' instantiation. The package defines two appender generator functions: +#' +#' \describe{ +#' \item{appender.file}{Write to a file} +#' \item{appender.console}{Write to the console} +#' } +#' +#' Each of these functions returns the actual appender function, so be sure to +#' actually call the function! +#' +#' Layouts are responsible for formatting messages. This operation usually +#' consists of adding the log level, a timestamp, plus some pretty-printing to +#' make the log messages easy on the eyes. The package supplies several layouts: +#' +#' \describe{ +#' \item{layout.simple}{Writes messages with a default format} +#' \item{layout.json}{Generates messages in a JSON format} +#' \item{layout.format}{Define your own format} +#' \item{layout.tracearg}{Print a variable name along with its value} +#' } +#' +#' @name futile.logger-package +#' @aliases futile.logger-package futile.logger flog.namespace +#' @docType package +#' @exportPattern "^[^\\.]" +#' @import utils lambda.r futile.options +#' @author Brian Lee Yung Rowe <r@@zatonovo.com> +#' @seealso \code{\link{flog.logger}}, \code{\link{flog.threshold}}, +#' \code{\link{flog.layout}}, \code{\link{flog.appender}} +#' @keywords package attribute logic +#' @examples +#' +#' flog.debug("This %s print", "won't") +#' flog.warn("This %s print", "will") +#' +#' flog.info("This inherits from the ROOT logger", name='logger.a') +#' flog.threshold(DEBUG, name='logger.a') +#' flog.debug("logger.a has now been set to DEBUG", name='logger.a') +#' flog.debug("But the ROOT logger is still at INFO (so this won't print)") +#' +#' \dontrun{ +#' flog.appender(appender.file("other.log"), name='logger.b') +#' flog.info("This writes to a %s", "file", name='logger.b') +#' } +#' +NULL diff --git a/R/layout.R b/R/layout.R new file mode 100644 index 0000000..cce6916 --- /dev/null +++ b/R/layout.R @@ -0,0 +1,183 @@ +#' Manage layouts within the 'futile.logger' sub-system +#' +#' Provides functions for managing layouts. Typically 'flog.layout' is only +#' used when manually creating a logging configuration. +#' +#' @section Usage: +#' # Get the layout function for the given logger\cr +#' flog.layout(name) \%::\% character : Function\cr +#' flog.layout(name='ROOT') +#' +#' # Set the layout function for the given logger\cr +#' flog.layout(fn, name='ROOT') +#' +#' # Decorate log messages with a standard format\cr +#' layout.simple(level, msg, ...) +#' +#' # Generate log messages as JSON\cr +#' layout.json(level, msg, ...) +#' +#' # Decorate log messages using a custom format\cr +#' layout.format(format, datetime.fmt="%Y-%m-%d %H:%M:%S") +#' +#' # Show the value of a single variable +#' layout.tracearg(level, msg, ...) +#' +#' @section Details: +#' Layouts are responsible for formatting messages so they are human-readable. +#' Similar to an appender, a layout is assigned to a logger by calling +#' \code{flog.layout}. The \code{flog.layout} function is used internally +#' to get the registered layout function. It is kept visible so +#' user-level introspection is possible. +#' +#' \code{layout.simple} is a pre-defined layout function that +#' prints messages in the following format:\cr +#' LEVEL [timestamp] message +#' +#' This is the default layout for the ROOT logger. +#' +#' \code{layout.format} allows you to specify the format string to use +#' in printing a message. The following tokens are available. +#' \describe{ +#' \item{~l}{Log level} +#' \item{~t}{Timestamp} +#' \item{~n}{Namespace} +#' \item{~f}{The calling function} +#' \item{~m}{The message} +#' } +#' +#' \code{layout.json} converts the message and any additional objects provided +#' to a JSON structure. E.g.: +#' +#' flog.info("Hello, world", cat='asdf') +#' +#' yields something like +#' +#' \{"level":"INFO","timestamp":"2015-03-06 19:16:02 EST","message":"Hello, world","func":"(shell)","cat":["asdf"]\} +#' +#' \code{layout.tracearg} is a special layout that takes a variable +#' and prints its name and contents. +#' +#' @name flog.layout +#' @aliases layout.simple layout.format layout.tracearg layout.json +#' @param \dots Used internally by lambda.r +#' @author Brian Lee Yung Rowe +#' @seealso \code{\link{flog.logger}} \code{\link{flog.appender}} +#' @keywords data +#' @examples +#' # Set the layout for 'my.package' +#' flog.layout(layout.simple, name='my.package') +#' +#' # Update the ROOT logger to use a custom layout +#' layout <- layout.format('[~l] [~t] [~n.~f] ~m') +#' flog.layout(layout) +#' +#' # Create a custom logger to trace variables +#' flog.layout(layout.tracearg, name='tracer') +#' x <- 5 +#' flog.info(x, name='tracer') +NULL + +# Get the layout for the given logger +flog.layout(name) %::% character : Function +flog.layout(name='ROOT') %as% +{ + logger <- flog.logger(name) + logger$layout +} + +# Set the layout +flog.layout(fn, name='ROOT') %as% +{ + flog.logger(name, layout=fn) + invisible() +} + +# This file provides some standard formatters +# This prints out a string in the following format: +# LEVEL [timestamp] message +layout.simple <- function(level, msg, ...) +{ + the.time <- format(Sys.time(), "%Y-%m-%d %H:%M:%S") + if (length(list(...)) > 0) { + parsed <- lapply(list(...), function(x) ifelse(is.null(x), 'NULL', x)) + msg <- do.call(sprintf, c(msg, parsed)) + } + sprintf("%s [%s] %s\n", names(level),the.time, msg) +} + +# Generates a list object, then converts it to JSON and outputs it +layout.json <- function(level, msg, ...) { + if (!requireNamespace("jsonlite", quietly=TRUE)) + stop("layout.json requires jsonlite. Please install it.", call.=FALSE) + + where <- 1 # to avoid R CMD CHECK issue + the.function <- tryCatch(deparse(sys.call(where)[[1]]), + error=function(e) "(shell)") + the.function <- ifelse( + length(grep('flog\\.',the.function)) == 0, the.function, '(shell)') + + output_list <- list( + level=jsonlite::unbox(names(level)), + timestamp=jsonlite::unbox(format(Sys.time(), "%Y-%m-%d %H:%M:%S %z")), + message=jsonlite::unbox(msg), + func=jsonlite::unbox(the.function), + additional=... + ) + jsonlite::toJSON(output_list, simplifyVector=TRUE) +} + +# This parses and prints a user-defined format string. Available tokens are +# ~l - Log level +# ~t - Timestamp +# ~n - Namespace +# ~f - Calling function +# ~m - Message +# +# layout <- layout.format('[~l] [~t] [~n.~f] ~m') +# flog.layout(layout) +layout.format <- function(format, datetime.fmt="%Y-%m-%d %H:%M:%S") +{ + where <- 1 + function(level, msg, ...) { + if (! is.null(substitute(...))) msg <- sprintf(msg, ...) + the.level <- names(level) + the.time <- format(Sys.time(), datetime.fmt) + the.namespace <- ifelse(flog.namespace() == 'futile.logger','ROOT',flog.namespace()) + #print(sys.calls()) + the.function <- tryCatch(deparse(sys.call(where)[[1]]), error=function(e) "(shell)") + the.function <- ifelse(length(grep('flog\\.',the.function)) == 0, the.function, '(shell)') + #pattern <- c('~l','~t','~n','~f','~m') + #replace <- c(the.level, the.time, the.namespace, the.function, msg) + message <- gsub('~l',the.level, format, fixed=TRUE) + message <- gsub('~t',the.time, message, fixed=TRUE) + message <- gsub('~n',the.namespace, message, fixed=TRUE) + message <- gsub('~f',the.function, message, fixed=TRUE) + message <- gsub('~m',msg, message, fixed=TRUE) + sprintf("%s\n", message) + } +} + +layout.tracearg <- function(level, msg, ...) +{ + the.time <- format(Sys.time(), "%Y-%m-%d %H:%M:%S") + if (is.character(msg)) { + if (! is.null(substitute(...))) msg <- sprintf(msg, ...) + } else { + external.call <- sys.call(-2) + external.fn <- eval(external.call[[1]]) + matched.call <- match.call(external.fn, external.call) + matched.call <- matched.call[-1] + matched.call.names <- names(matched.call) + + ## We are interested only in the msg and ... parameters, + ## i.e. in msg and all parameters not explicitly declared + ## with the function + is.output.param <- matched.call.names == "msg" | + !(matched.call.names %in% c(setdiff(names(formals(external.fn)), "..."))) + + label <- lapply(matched.call[is.output.param], deparse) + msg <- sprintf("%s: %s", label, c(msg, list(...))) + } + sprintf("%s [%s] %s\n", names(level),the.time, msg) +} diff --git a/R/logger.R b/R/logger.R new file mode 100644 index 0000000..a9bb354 --- /dev/null +++ b/R/logger.R @@ -0,0 +1,379 @@ +#' Manage loggers +#' +#' Provides functions for writing log messages and managing loggers. Typically +#' only the flog.[trace|debug|info|warn|error|fatal] functions need to be used +#' in conjunction with flog.threshold to interactively change the log level. +#' +#' @section Usage: +#' # Conditionally print a log statement at TRACE log level\cr +#' flog.trace(msg, ..., name=flog.namespace(), capture=FALSE) +#' +#' # Conditionally print a log statement at DEBUG log level\cr +#' flog.debug(msg, ..., name=flog.namespace(), capture=FALSE) +#' +#' # Conditionally print a log statement at INFO log level\cr +#' flog.info(msg, ..., name=flog.namespace(), capture=FALSE) +#' +#' # Conditionally print a log statement at WARN log level\cr +#' flog.warn(msg, ..., name=flog.namespace(), capture=FALSE) +#' +#' # Conditionally print a log statement at ERROR log level\cr +#' flog.error(msg, ..., name=flog.namespace(), capture=FALSE) +#' +#' # Print a log statement at FATAL log level\cr +#' flog.fatal(msg, ..., name=flog.namespace(), capture=FALSE) +#' +#' # Execute an expression and capture any warnings or errors +#' ftry(expr, error=stop, finally=NULL) +#' +#' @section Additional Usage: +#' These functions generally do not need to be called by an end user. +#' +#' # Get the ROOT logger\cr +#' flog.logger() +#' +#' # Get the logger with the specified name\cr +#' flog.logger(name) +#' +#' # Set options for the given logger\cr +#' flog.logger(name, threshold=NULL, appender=NULL, layout=NULL, carp=NULL) +#' +#' @section Details: +#' These functions represent the high level interface to futile.logger. +#' +#' The primary use case for futile.logger is to write out log messages. There +#' are log writers associated with all the predefined log levels: TRACE, DEBUG, +#' INFO, WARN, ERROR, FATAL. Log messages will only be written if the log level +#' is equal to or more urgent than the current threshold. By default the ROOT +#' logger is set to INFO. +#' +#' > flog.debug("This won't print") \cr +#' > flog.info("But this \%s", 'will') \cr +#' > flog.warn("As will \%s", 'this') +#' +#' Typically, the built in log level constants are used in the call, which +#' conform to the log4j levels (from least severe to most severe): TRACE, +#' DEBUG, INFO, WARN, ERROR, FATAL. It is not a strict requirement to use these +#' constants (any numeric value will work), though most users should find this +#' level of granularity sufficient. +#' +#' Loggers are hierarchical in the sense that any requested logger that is +#' undefined will fall back to its most immediate defined parent logger. The +#' absolute parent is ROOT, which is guaranteed to be defined for the system +#' and cannot be deleted. This means that you can specify a new logger +#' directly. +#' +#' > flog.info("This will fall back to 'my', then 'ROOT'", name='my.logger') +#' +#' You can also change the threshold or any other setting associated with a +#' logger. This will create an explicit logger where any unspecified options +#' are copied from the parent logger. +#' +#' > flog.appender(appender.file("foo.log"), name='my') \cr +#' > flog.threshold(ERROR, name='my.logger') \cr +#' > flog.info("This won't print", name='my.logger') \cr +#' > flog.error("This %s print to a file", 'will', name='my.logger') \cr +#' +#' If you define a logger that you later want to remove, use flog.remove. +#' +#' The option 'capture' allows you to print out more complicated data +#' structures without a lot of ceremony. This variant doesn't accept format +#' strings and instead appends the value to the next line of output. Consider +#' +#' > m <- matrix(rnorm(12), nrow=3) \cr +#' > flog.info("Matrix:",m, capture=TRUE) +#' +#' which preserves the formatting, whereas using capture=FALSE will have +#' a cluttered output due to recycling. +#' +#' @name flog.logger +#' @aliases flog.trace flog.debug flog.info flog.warn flog.error flog.fatal +#' @param msg The message to log +#' @param name The logger name to use +#' @param capture Capture print output of variables instead of interpolate +#' @param \dots Optional arguments to populate the format string +#' @param expr An expression to evaluate +#' @param finally An optional expression to evaluate at the end +#' @author Brian Lee Yung Rowe +#' @seealso \code{\link{flog.threshold}} \code{\link{flog.remove}} +#' \code{\link{flog.carp}} \code{\link{flog.appender}} \code{\link{flog.layout}} +#' @keywords data +#' @examples +#' +#' flog.threshold(DEBUG) +#' flog.debug("This debug message will print") +#' +#' flog.threshold(WARN) +#' flog.debug("This one won't") +#' +#' m <- matrix(rnorm(12), nrow=3) +#' flog.info("Matrix:",m, capture=TRUE) +#' +#' ftry(log(-1)) +#' +#' \dontrun{ +#' s <- c('FCX','AAPL','JPM','AMZN') +#' p <- TawnyPortfolio(s) +#' +#' flog.threshold(TRACE,'tawny') +#' ws <- optimizePortfolio(p, RandomMatrixDenoiser()) +#' z <- getIndexComposition() +#' +#' flog.threshold(WARN,'tawny') +#' ws <- optimizePortfolio(p, RandomMatrixDenoiser()) +#' z <- getIndexComposition() +#' +#' } +NULL + +.log_level <- function(msg, ..., level, name, capture) +{ + logger <- flog.logger(name) + if (level > logger$threshold && (is.null(logger$carp) || !logger$carp)) { + return(invisible()) + } + + appender <- flog.appender(name) + layout <- flog.layout(name) + if (capture) { + values <- paste(capture.output(print(...)), collapse='\n') + message <- c(layout(level, msg), "\n", values, "\n") + } else { + message <- layout(level, msg, ...) + } + if (level <= logger$threshold) appender(message) + invisible(message) +} + +# Get the namespace that a function resides in. If no namespace exists, then +# return NULL. +# <environment: namespace:lambda.r> +flog.namespace <- function(where=1) +{ + s <- capture.output(str(topenv(environment(sys.function(where))), give.attr=FALSE)) + if (length(grep('lambda.r',s)) > 0) + s <- attr(sys.function(-5), 'topenv') + + if (length(grep('namespace', s)) < 1) return('ROOT') + + ns <- sub('.*namespace:([^>]+)>.*','\\1', s) + ifelse(is.null(ns), 'ROOT', ns) +} + + +flog.trace <- function(msg, ..., name=flog.namespace(), capture=FALSE) { + .log_level(msg, ..., level=TRACE,name=name, capture=capture) +} + +flog.debug <- function(msg, ..., name=flog.namespace(), capture=FALSE) { + .log_level(msg, ..., level=DEBUG,name=name, capture=capture) +} + +flog.info <- function(msg, ..., name=flog.namespace(), capture=FALSE) { + .log_level(msg, ..., level=INFO,name=name, capture=capture) +} + +flog.warn <- function(msg, ..., name=flog.namespace(), capture=FALSE) { + .log_level(msg, ..., level=WARN,name=name, capture=capture) +} + +flog.error <- function(msg, ..., name=flog.namespace(), capture=FALSE) { + .log_level(msg, ..., level=ERROR,name=name, capture=capture) +} + +flog.fatal <- function(msg, ..., name=flog.namespace(), capture=FALSE) { + .log_level(msg, ..., level=FATAL,name=name, capture=capture) +} + +#' Wrap a try block in futile.logger +#' +#' This function integrates futile.logger with the error and warning system +#' so problems can be caught both in the standard R warning system, while +#' also being emitted via futile.logger. +#' +#' @name ftry +#' @param expr The expression to evaluate in a try block +#' @param error An error handler +#' @param finally Pass-through to tryCatch finally +#' @author Brian Lee Yung Rowe +#' @keywords data +#' @examples +#' ftry(log(-1)) +ftry <- function(expr, error=stop, finally=NULL) { + w.handler <- function(e) flog.warn("%s", e) + e.handler <- function(e) { flog.error("%s", e); error(e) } + tryCatch(expr, warning=w.handler, error=e.handler, finally) +} + +# By default, use the package namespace or use the 'ROOT' logger. +flog.logger() %as% +{ + flog.logger(flog.namespace()) +} + +flog.logger(name) %as% +{ + if (nchar(name) < 1) name <- 'ROOT' + #cat(sprintf("Searching for logger %s\n", name)) + + key <- paste("logger", name, sep='.') + # TODO: Search hierarchy + os <- logger.options(key) + if (! is.null(os)) return(os) + if (name == 'ROOT') { + logger <- list(name=name, + threshold=INFO, + appender=appender.console(), + layout=layout.simple) + logger.options(update=list(key, logger)) + return(logger) + } + + parts <- strsplit(name, '.', fixed=TRUE)[[1]] + parent <- paste(parts[1:length(parts)-1], collapse='.') + flog.logger(parent) +} + +flog.logger(name, threshold=NULL, appender=NULL, layout=NULL, carp=NULL) %as% +{ + logger <- flog.logger(name) + if (!is.null(threshold)) logger$threshold <- threshold + if (!is.null(appender)) logger$appender <- appender + if (!is.null(layout)) logger$layout <- layout + if (!is.null(carp)) logger$carp <- carp + + key <- paste("logger", name, sep='.') + logger.options(update=list(key, logger)) + invisible() +} + + +#' Remove a logger +#' +#' In the event that you no longer wish to have a logger registered, +#' use this function to remove it. Then any references to this +#' logger will inherit the next available logger in the hierarchy. +#' +#' @section Usage: +#' # Remove a logger\cr +#' flog.remove(name) +#' +#' @name flog.remove +#' @param name The logger name to use +#' @author Brian Lee Yung Rowe +#' @keywords data +#' @examples +#' flog.threshold(ERROR, name='my.logger') +#' flog.info("Won't print", name='my.logger') +#' flog.remove('my.logger') +#' flog.info("Will print", name='my.logger') +flog.remove('ROOT') %as% { invisible() } +flog.remove(name) %as% +{ + key <- paste("logger", name, sep='.') + logger.options(update=list(key, NULL)) + invisible() +} + +#' Get and set the threshold for a logger +#' +#' The threshold affects the visibility of a given logger. When a log +#' statement is called, e.g. \code{flog.debug('foo')}, futile.logger +#' compares the threshold of the logger with the level implied in the +#' log command (in this case DEBUG). If the log level is at or higher +#' in priority than the logger threshold, a message will print. +#' Otherwise the command will silently return. +#' +#' @section Usage: +#' # Get the threshold for the given logger\cr +#' flog.threshold(name) \%::\% character : character \cr +#' flog.threshold(name=ROOT) +#' +#' # Set the threshold for the given logger\cr +#' flog.threshold(threshold, name=ROOT) +#' +#' @name flog.threshold +#' @param threshold integer The new threshold for the given logger +#' @param name character The name of the logger +#' @author Brian Lee Yung Rowe +#' @keywords data +#' @examples +#' flog.threshold(ERROR) +#' flog.info("Won't print") +#' flog.threshold(INFO) +#' flog.info("Will print") +# Set the threshold +flog.threshold('TRACE', name='ROOT') %as% flog.threshold(TRACE, name) +flog.threshold('trace', name='ROOT') %as% flog.threshold(TRACE, name) +flog.threshold('DEBUG', name='ROOT') %as% flog.threshold(DEBUG, name) +flog.threshold('debug', name='ROOT') %as% flog.threshold(DEBUG, name) +flog.threshold('INFO', name='ROOT') %as% flog.threshold(INFO, name) +flog.threshold('info', name='ROOT') %as% flog.threshold(INFO, name) +flog.threshold('WARN', name='ROOT') %as% flog.threshold(WARN, name) +flog.threshold('warn', name='ROOT') %as% flog.threshold(WARN, name) +flog.threshold('ERROR', name='ROOT') %as% flog.threshold(ERROR, name) +flog.threshold('error', name='ROOT') %as% flog.threshold(ERROR, name) +flog.threshold('FATAL', name='ROOT') %as% flog.threshold(FATAL, name) +flog.threshold('fatal', name='ROOT') %as% flog.threshold(FATAL, name) + +flog.threshold(threshold, name='ROOT') %as% +{ + flog.logger(name, threshold=threshold) + invisible() +} + +# Get the threshold +flog.threshold(name) %::% character : character +flog.threshold(name='ROOT') %as% +{ + logger <- flog.logger(name) + names(logger$threshold) +} + + +#' Always return the log message +#' +#' Indicate whether the logger will always return the log message +#' despite the threshold. +#' +#' This is a special option to allow the return value of the flog.* +#' logging functions to return the generated log message even if +#' the log level does not exceed the threshold. Note that this +#' minorly impacts performance when enabled. This functionality +#' is separate from the appender, which is still bound to the +#' value of the logger threshold. +#' +#' @section Usage: +#' # Indicate whether the given logger should carp\cr +#' flog.carp(name=ROOT) +#' +#' # Set whether the given logger should carp\cr +#' flog.carp(carp, name=ROOT) +#' +#' @name flog.carp +#' @param carp logical Whether to carp output or not +#' @param name character The name of the logger +#' @author Brian Lee Yung Rowe +#' @keywords data +#' @examples +#' flog.carp(TRUE) +#' x <- flog.debug("Returns this message but won't print") +#' flog.carp(FALSE) +#' y <- flog.debug("Returns nothing and prints nothing") +flog.carp(name) %::% character : logical +flog.carp(name='ROOT') %as% +{ + logger <- flog.logger(name) + if (is.null(logger$carp)) FALSE + else logger$carp +} + +# Set whether to carp +flog.carp(carp, name='ROOT') %as% +{ + flog.logger(name, carp=carp) + invisible() +} + + + diff --git a/R/options.R b/R/options.R new file mode 100644 index 0000000..14d4b89 --- /dev/null +++ b/R/options.R @@ -0,0 +1,18 @@ +#' Constants for 'futile.logger' +#' +#' Log level constants and the logger options. +#' +#' The logging configuration is managed by 'logger.options', a function +#' generated by OptionsManager within 'futile.options'. +#' +#' @name logger.options +#' @aliases FATAL ERROR WARN INFO DEBUG TRACE +#' @usage logger.options(..., simplify = FALSE, update = list()) +#' @param ... TODO +#' @param simplify TODO +#' @param update TODO +#' @author Brian Lee Yung Rowe +#' @seealso \code{futile.options} +#' @keywords data +logger.options <- OptionsManager('logger.options') + diff --git a/R/scat.R b/R/scat.R new file mode 100644 index 0000000..46dab88 --- /dev/null +++ b/R/scat.R @@ -0,0 +1,24 @@ +#' Print formatted messages +#' +#' A replacement for \code{cat} that has built-in sprintf formatting +#' +#' Like \code{cat} but you can use format strings. +#' +#' @param format A format string passed to sprintf +#' @param use.newline Whether to append a new line at the end +#' @param \dots Arguments to pass to sprintf for dereferencing +#' @return A formatted string printed to the console +#' @author Brian Lee Yung Rowe +#' @keywords data +#' @examples +#' +#' apply(array(2:5),1, function(x) scat('This has happened %s times', x) ) +#' +scat <- function(format, ..., use.newline=TRUE) +{ + if (use.newline) newline = '\n' + else newline = '' + + cat(paste(sprintf(format, ...), newline, sep='')) +} + diff --git a/README.md b/README.md new file mode 100644 index 0000000..7637e73 --- /dev/null +++ b/README.md @@ -0,0 +1,133 @@ +[](https://travis-ci.org/zatonovo/futile.logger) + +Overview +======== +futile.logger is a logging utility for R. Originally built based on log4j, +the latest version introduces a new API that is more consistent with R idioms. +In practice this means an interface that works equally well in the shell for +interactive use and also in scripts for system use. + +The underlying concepts of log4j still exist, e.g. loggers, appenders, and +formatters. There continues to be a hierarchical system for logger. In +addition, there is now automatic package scoping, which means that packages +are given their own logger namespace so you can interactively turn on and +off logging for specific packages. + +Also included is formatting logic to log list objects (which includes +data.frames) in a smart way. + +Usage +===== +Out of the box, the default `ROOT` logger logs to the console with threshold +set to INFO. + +```R +flog.info("Hello, %s", "world") + +# This won't print by default +flog.debug("Goodbye, %s", "world") + +# Change the log level to debug and try again +flog.threshold(DEBUG) +flog.debug("Goodbye, %s", "world") + +# Keep an alternate logger at WARN +flog.threshold(WARN, name='quiet') + +# This won't print since it's using the logger named 'quiet'! +flog.debug("Goodbye, %s", "world", name='quiet') + +``` + +Loggers +------- +A logger is simply a namespace bound to a threshold, an appender, and a +formatter. Loggers are configured automatically whenever they are +referenced (for example when changing the threshold) inheriting the settings +of the root logger. To explicitly create a logger call `log.logger()`. + +```R +flog.logger("tawny", WARN, appender=appender.file('tawny.log')) +``` + +To remove a logger, use `log.remove()`. If no such logger exists, +the command is safely ignored. + +```R +flog.remove("tawny") +``` + +Thresholds +---------- +The logger threshold determines what will be logged for a given logger. Use +this function to retrieve and also change this threshold. + +```R +# Get the logging threshold for the ROOT logger +flog.threshold() +``` + +The default logger is ROOT. To change the threshold of a different logger, +specify the logger argument with a string that represents the logger. Note +that a log.(debug|info|warn|error) command running from a package will +automatically be associated with a logger with the name of the package. This +structure means you can change the log level for a specific package as +necessary. + +```R +# Set root logger to DEBUG level to see all log messages +flog.threshold(DEBUG) +# Suppress log messages below WARN for logger 'quiet' +flog.threshold(WARN, name="quiet") +``` + +Appenders +--------- +An appender defines where output is directed. Typically only one appender is +used per logger, but multiple can be assigned. The package provides the +following appenders: + ++ `appender.console` ++ `appender.file` ++ `appender.tee` + +To change the appenders assigned to a logger, use `flog.appender()`: +```R +# Change the 'quiet' logger to write to a file +flog.appender(appender.file('quiet.log'), 'quiet') +flog.warn("Goodbye, %s", "world", name='quiet') +``` + +You can create your own appender by defining a function that accepts a single +character argument. It is up to you to define the behavior. For example, +an appender that logs to a URL might look like the following. + +```R +url_appender.gen <- function(url) { + conn <- url(url) + function(line) { + file.write() + } +} +``` + +flog.format("futile.matrix", fn) + +Layouts +------- +A layout defines how a log message is printed. The default layout.simple +prints log messages using the following format: + LEVEL [datetime] Message + +The layouts included in the package are: ++ layout.simple - Use a default format ++ layout.format - Provide a customizable format string ++ layout.tracearg - Dump a variable with its name + + +What's New +========== ++ Function to wrap a try/catch with logging (ftry) ++ Capture output for print statements (for more complex objects) ++ New layout.tracearg + diff --git a/debian/README.test b/debian/README.test deleted file mode 100644 index 8d70ca3..0000000 --- a/debian/README.test +++ /dev/null @@ -1,9 +0,0 @@ -Notes on how this package can be tested. -──────────────────────────────────────── - -This package can be tested by running the provided test: - -cd tests -LC_ALL=C R --no-save < testthat.R - -in order to confirm its integrity. diff --git a/debian/changelog b/debian/changelog deleted file mode 100644 index d32aad6..0000000 --- a/debian/changelog +++ /dev/null @@ -1,26 +0,0 @@ -r-cran-futile.logger (1.4.3-1) unstable; urgency=medium - - * New upstream version - * Convert to dh-r - * Canonical homepage for CRAN - - -- Andreas Tille <[email protected]> Mon, 07 Nov 2016 21:25:47 +0100 - -r-cran-futile.logger (1.4.1-3) unstable; urgency=medium - - * Add missing test dependency r-cran-testthat - - -- Andreas Tille <[email protected]> Fri, 29 Apr 2016 09:43:55 +0200 - -r-cran-futile.logger (1.4.1-2) unstable; urgency=medium - - * Fix autopkgtest - * cme fix dpkg-control - - -- Andreas Tille <[email protected]> Thu, 28 Apr 2016 11:38:00 +0200 - -r-cran-futile.logger (1.4.1-1) unstable; urgency=medium - - * Initial upload (Closes: #787097) - - -- Andreas Tille <[email protected]> Thu, 28 May 2015 17:22:13 +0200 diff --git a/debian/compat b/debian/compat deleted file mode 100644 index ec63514..0000000 --- a/debian/compat +++ /dev/null @@ -1 +0,0 @@ -9 diff --git a/debian/control b/debian/control deleted file mode 100644 index 85aa260..0000000 --- a/debian/control +++ /dev/null @@ -1,25 +0,0 @@ -Source: r-cran-futile.logger -Maintainer: Debian Med Packaging Team <[email protected]> -Uploaders: Andreas Tille <[email protected]> -Section: gnu-r -Priority: optional -Build-Depends: debhelper (>= 9), - dh-r, - r-base-dev, - r-cran-lambda.r, - r-cran-futile.options -Standards-Version: 3.9.8 -Vcs-Browser: https://anonscm.debian.org/viewvc/debian-med/trunk/packages/R/r-cran-futile.logger/ -Vcs-Svn: svn://anonscm.debian.org/debian-med/trunk/packages/R/r-cran-futile.logger/ -Homepage: https://cran.r-project.org/package=futile.logger - -Package: r-cran-futile.logger -Architecture: all -Depends: ${misc:Depends}, - ${R:Depends} -Recommends: ${R:Recommends} -Suggests: ${R:Suggests} -Description: logging utility for GNU R - This GNU R package provides a simple yet powerful logging utility. Based - loosely on log4j, futile.logger takes advantage of R idioms to make logging - a convenient and easy to use replacement for cat and print statements. diff --git a/debian/copyright b/debian/copyright deleted file mode 100644 index 0e3735c..0000000 --- a/debian/copyright +++ /dev/null @@ -1,18 +0,0 @@ -Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ -Upstream-Contact: Brian Lee Yung Rowe <[email protected]> -Upstream-Name: futile.logger -Source: https://cran.r-project.org/package=futile.logger - -Files: * -Copyright: 2012-2016 Brian Lee Yung Rowe <[email protected]> -License: LGPL-3 -Comment: License is mentioned in the metadata of the file DESCRIPTION - and at the download page mentioned above. - -Files: debian/* -Copyright: 2015-2016 Andreas Tille <[email protected]> -License: LGPL-3 - -License: LGPL-3 - On Debian systems, the complete text of the GNU Lesser General Public - License version 3 can be found in ‘/usr/share/common-licenses/LGPL-3’. diff --git a/debian/docs b/debian/docs deleted file mode 100644 index 960011c..0000000 --- a/debian/docs +++ /dev/null @@ -1,3 +0,0 @@ -tests -debian/README.test -debian/tests/run-unit-test diff --git a/debian/rules b/debian/rules deleted file mode 100755 index 68d9a36..0000000 --- a/debian/rules +++ /dev/null @@ -1,4 +0,0 @@ -#!/usr/bin/make -f - -%: - dh $@ --buildsystem R diff --git a/debian/source/format b/debian/source/format deleted file mode 100644 index 163aaf8..0000000 --- a/debian/source/format +++ /dev/null @@ -1 +0,0 @@ -3.0 (quilt) diff --git a/debian/tests/control b/debian/tests/control deleted file mode 100644 index 051fd72..0000000 --- a/debian/tests/control +++ /dev/null @@ -1,3 +0,0 @@ -Tests: run-unit-test -Depends: @, r-cran-survival, r-cran-testthat -Restrictions: allow-stderr diff --git a/debian/tests/run-unit-test b/debian/tests/run-unit-test deleted file mode 100644 index e13b6b6..0000000 --- a/debian/tests/run-unit-test +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/sh -e - -pkg=r-cran-futile.logger -if [ "$ADTTMP" = "" ] ; then - ADTTMP=`mktemp -d /tmp/${pkg}-test.XXXXXX` -fi -cd $ADTTMP -cp -a /usr/share/doc/${pkg}/tests/* $ADTTMP -find . -name "*.gz" -exec gunzip \{\} \; -LC_ALL=C R --no-save < testthat.R -rm -fr $ADTTMP/* diff --git a/debian/watch b/debian/watch deleted file mode 100644 index bcbb45d..0000000 --- a/debian/watch +++ /dev/null @@ -1,2 +0,0 @@ -version=3 -http://cran.r-project.org/src/contrib/futile.logger_([\d.-]*)\.tar.gz diff --git a/man/flog.appender.Rd b/man/flog.appender.Rd new file mode 100644 index 0000000..9761d11 --- /dev/null +++ b/man/flog.appender.Rd @@ -0,0 +1,80 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/appender.R +\name{flog.appender} +\alias{appender.console} +\alias{appender.file} +\alias{appender.tee} +\alias{flog.appender} +\title{Manage appenders for loggers} +\arguments{ +\item{\dots}{Used internally by lambda.r} +} +\description{ +Provides functions for adding and removing appenders. +} +\section{Usage}{ + +# Get the appender for the given logger\cr +flog.appender(name) \%::\% character : Function\cr +flog.appender(name='ROOT') + +# Set the appender for the given logger\cr +flog.appender(fn, name='ROOT') + +# Print log messages to the console\cr +appender.console() + +# Write log messages to a file\cr +appender.file(file) + +# Write log messages to console and a file\cr +appender.tee(file) +} + +\section{Details}{ + +Appenders do the actual work of writing log messages to some target. +To use an appender in a logger, you must register it to a given logger. +Use \code{flog.appender} to both access and set appenders. + +The ROOT logger by default uses \code{appender.console}. + +\code{appender.console} is a function that writes to the console. +No additional arguments are necessary when registering the appender +via flog.appender. + + +\code{appender.file} writes to a file, so you must pass an additional file +argument to the function. To change the file name, just call +\code{flog.appender(appender.file(file))} again with a new file name. + +To use your own appender create a function that takes a single argument, +which represents the log message. You need to pass a function reference to +\code{flog.appender}. + +\code{appender.tee} writes to both the console and file. +} + +\section{Value}{ + +When getting the appender, \code{flog.appender} returns the appender +function. When setting an appender, \code{flog.appender} has no +return value. +} +\examples{ +\dontrun{ +flog.appender(appender.console(), name='my.logger') + +# Set an appender to the logger named 'my.package'. Any log operations from +# this package will now use this appender. +flog.appender(appender.file('my.package.out'), 'my.package') +} +} +\author{ +Brian Lee Yung Rowe +} +\seealso{ +\code{\link{flog.logger}} \code{\link{flog.layout}} +} +\keyword{data} + diff --git a/man/flog.carp.Rd b/man/flog.carp.Rd new file mode 100644 index 0000000..80b0676 --- /dev/null +++ b/man/flog.carp.Rd @@ -0,0 +1,41 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/logger.R +\name{flog.carp} +\alias{flog.carp} +\title{Always return the log message} +\arguments{ +\item{carp}{logical Whether to carp output or not} + +\item{name}{character The name of the logger} +} +\description{ +Indicate whether the logger will always return the log message +despite the threshold. +} +\details{ +This is a special option to allow the return value of the flog.* +logging functions to return the generated log message even if +the log level does not exceed the threshold. Note that this +minorly impacts performance when enabled. This functionality +is separate from the appender, which is still bound to the +value of the logger threshold. +} +\section{Usage}{ + +# Indicate whether the given logger should carp\cr +flog.carp(name=ROOT) + +# Set whether the given logger should carp\cr +flog.carp(carp, name=ROOT) +} +\examples{ +flog.carp(TRUE) +x <- flog.debug("Returns this message but won't print") +flog.carp(FALSE) +y <- flog.debug("Returns nothing and prints nothing") +} +\author{ +Brian Lee Yung Rowe +} +\keyword{data} + diff --git a/man/flog.layout.Rd b/man/flog.layout.Rd new file mode 100644 index 0000000..91a2ac8 --- /dev/null +++ b/man/flog.layout.Rd @@ -0,0 +1,95 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/layout.R +\name{flog.layout} +\alias{flog.layout} +\alias{layout.format} +\alias{layout.json} +\alias{layout.simple} +\alias{layout.tracearg} +\title{Manage layouts within the 'futile.logger' sub-system} +\arguments{ +\item{\dots}{Used internally by lambda.r} +} +\description{ +Provides functions for managing layouts. Typically 'flog.layout' is only +used when manually creating a logging configuration. +} +\section{Usage}{ + +# Get the layout function for the given logger\cr +flog.layout(name) \%::\% character : Function\cr +flog.layout(name='ROOT') + +# Set the layout function for the given logger\cr +flog.layout(fn, name='ROOT') + +# Decorate log messages with a standard format\cr +layout.simple(level, msg, ...) + +# Generate log messages as JSON\cr +layout.json(level, msg, ...) + +# Decorate log messages using a custom format\cr +layout.format(format, datetime.fmt="%Y-%m-%d %H:%M:%S") + +# Show the value of a single variable +layout.tracearg(level, msg, ...) +} + +\section{Details}{ + +Layouts are responsible for formatting messages so they are human-readable. +Similar to an appender, a layout is assigned to a logger by calling +\code{flog.layout}. The \code{flog.layout} function is used internally +to get the registered layout function. It is kept visible so +user-level introspection is possible. + +\code{layout.simple} is a pre-defined layout function that +prints messages in the following format:\cr + LEVEL [timestamp] message + +This is the default layout for the ROOT logger. + +\code{layout.format} allows you to specify the format string to use +in printing a message. The following tokens are available. +\describe{ +\item{~l}{Log level} +\item{~t}{Timestamp} +\item{~n}{Namespace} +\item{~f}{The calling function} +\item{~m}{The message} +} + +\code{layout.json} converts the message and any additional objects provided +to a JSON structure. E.g.: + +flog.info("Hello, world", cat='asdf') + +yields something like + +\{"level":"INFO","timestamp":"2015-03-06 19:16:02 EST","message":"Hello, world","func":"(shell)","cat":["asdf"]\} + +\code{layout.tracearg} is a special layout that takes a variable +and prints its name and contents. +} +\examples{ +# Set the layout for 'my.package' +flog.layout(layout.simple, name='my.package') + +# Update the ROOT logger to use a custom layout +layout <- layout.format('[~l] [~t] [~n.~f] ~m') +flog.layout(layout) + +# Create a custom logger to trace variables +flog.layout(layout.tracearg, name='tracer') +x <- 5 +flog.info(x, name='tracer') +} +\author{ +Brian Lee Yung Rowe +} +\seealso{ +\code{\link{flog.logger}} \code{\link{flog.appender}} +} +\keyword{data} + diff --git a/man/flog.logger.Rd b/man/flog.logger.Rd new file mode 100644 index 0000000..3254ec8 --- /dev/null +++ b/man/flog.logger.Rd @@ -0,0 +1,152 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/logger.R +\name{flog.logger} +\alias{flog.debug} +\alias{flog.error} +\alias{flog.fatal} +\alias{flog.info} +\alias{flog.logger} +\alias{flog.trace} +\alias{flog.warn} +\title{Manage loggers} +\arguments{ +\item{msg}{The message to log} + +\item{name}{The logger name to use} + +\item{capture}{Capture print output of variables instead of interpolate} + +\item{\dots}{Optional arguments to populate the format string} + +\item{expr}{An expression to evaluate} + +\item{finally}{An optional expression to evaluate at the end} +} +\description{ +Provides functions for writing log messages and managing loggers. Typically +only the flog.[trace|debug|info|warn|error|fatal] functions need to be used +in conjunction with flog.threshold to interactively change the log level. +} +\section{Usage}{ + +# Conditionally print a log statement at TRACE log level\cr +flog.trace(msg, ..., name=flog.namespace(), capture=FALSE) + +# Conditionally print a log statement at DEBUG log level\cr +flog.debug(msg, ..., name=flog.namespace(), capture=FALSE) + +# Conditionally print a log statement at INFO log level\cr +flog.info(msg, ..., name=flog.namespace(), capture=FALSE) + +# Conditionally print a log statement at WARN log level\cr +flog.warn(msg, ..., name=flog.namespace(), capture=FALSE) + +# Conditionally print a log statement at ERROR log level\cr +flog.error(msg, ..., name=flog.namespace(), capture=FALSE) + +# Print a log statement at FATAL log level\cr +flog.fatal(msg, ..., name=flog.namespace(), capture=FALSE) + +# Execute an expression and capture any warnings or errors +ftry(expr, error=stop, finally=NULL) +} + +\section{Additional Usage}{ + +These functions generally do not need to be called by an end user. + +# Get the ROOT logger\cr +flog.logger() + +# Get the logger with the specified name\cr +flog.logger(name) + +# Set options for the given logger\cr +flog.logger(name, threshold=NULL, appender=NULL, layout=NULL, carp=NULL) +} + +\section{Details}{ + +These functions represent the high level interface to futile.logger. + +The primary use case for futile.logger is to write out log messages. There +are log writers associated with all the predefined log levels: TRACE, DEBUG, +INFO, WARN, ERROR, FATAL. Log messages will only be written if the log level +is equal to or more urgent than the current threshold. By default the ROOT +logger is set to INFO. + +> flog.debug("This won't print") \cr +> flog.info("But this \%s", 'will') \cr +> flog.warn("As will \%s", 'this') + +Typically, the built in log level constants are used in the call, which +conform to the log4j levels (from least severe to most severe): TRACE, +DEBUG, INFO, WARN, ERROR, FATAL. It is not a strict requirement to use these +constants (any numeric value will work), though most users should find this +level of granularity sufficient. + +Loggers are hierarchical in the sense that any requested logger that is +undefined will fall back to its most immediate defined parent logger. The +absolute parent is ROOT, which is guaranteed to be defined for the system +and cannot be deleted. This means that you can specify a new logger +directly. + +> flog.info("This will fall back to 'my', then 'ROOT'", name='my.logger') + +You can also change the threshold or any other setting associated with a +logger. This will create an explicit logger where any unspecified options +are copied from the parent logger. + +> flog.appender(appender.file("foo.log"), name='my') \cr +> flog.threshold(ERROR, name='my.logger') \cr +> flog.info("This won't print", name='my.logger') \cr +> flog.error("This %s print to a file", 'will', name='my.logger') \cr + +If you define a logger that you later want to remove, use flog.remove. + +The option 'capture' allows you to print out more complicated data +structures without a lot of ceremony. This variant doesn't accept format +strings and instead appends the value to the next line of output. Consider + +> m <- matrix(rnorm(12), nrow=3) \cr +> flog.info("Matrix:",m, capture=TRUE) + +which preserves the formatting, whereas using capture=FALSE will have +a cluttered output due to recycling. +} +\examples{ + +flog.threshold(DEBUG) +flog.debug("This debug message will print") + +flog.threshold(WARN) +flog.debug("This one won't") + +m <- matrix(rnorm(12), nrow=3) +flog.info("Matrix:",m, capture=TRUE) + +ftry(log(-1)) + +\dontrun{ +s <- c('FCX','AAPL','JPM','AMZN') +p <- TawnyPortfolio(s) + +flog.threshold(TRACE,'tawny') +ws <- optimizePortfolio(p, RandomMatrixDenoiser()) +z <- getIndexComposition() + +flog.threshold(WARN,'tawny') +ws <- optimizePortfolio(p, RandomMatrixDenoiser()) +z <- getIndexComposition() + +} +} +\author{ +Brian Lee Yung Rowe +} +\seealso{ +\code{\link{flog.threshold}} \code{\link{flog.remove}} +\code{\link{flog.carp}} \code{\link{flog.appender}} \code{\link{flog.layout}} +} +\keyword{data} + diff --git a/man/flog.remove.Rd b/man/flog.remove.Rd new file mode 100644 index 0000000..a699970 --- /dev/null +++ b/man/flog.remove.Rd @@ -0,0 +1,29 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/logger.R +\name{flog.remove} +\alias{flog.remove} +\title{Remove a logger} +\arguments{ +\item{name}{The logger name to use} +} +\description{ +In the event that you no longer wish to have a logger registered, +use this function to remove it. Then any references to this +logger will inherit the next available logger in the hierarchy. +} +\section{Usage}{ + +# Remove a logger\cr +flog.remove(name) +} +\examples{ +flog.threshold(ERROR, name='my.logger') +flog.info("Won't print", name='my.logger') +flog.remove('my.logger') +flog.info("Will print", name='my.logger') +} +\author{ +Brian Lee Yung Rowe +} +\keyword{data} + diff --git a/man/flog.threshold.Rd b/man/flog.threshold.Rd new file mode 100644 index 0000000..4af73f3 --- /dev/null +++ b/man/flog.threshold.Rd @@ -0,0 +1,38 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/logger.R +\name{flog.threshold} +\alias{flog.threshold} +\title{Get and set the threshold for a logger} +\arguments{ +\item{threshold}{integer The new threshold for the given logger} + +\item{name}{character The name of the logger} +} +\description{ +The threshold affects the visibility of a given logger. When a log +statement is called, e.g. \code{flog.debug('foo')}, futile.logger +compares the threshold of the logger with the level implied in the +log command (in this case DEBUG). If the log level is at or higher +in priority than the logger threshold, a message will print. +Otherwise the command will silently return. +} +\section{Usage}{ + +# Get the threshold for the given logger\cr +flog.threshold(name) \%::\% character : character \cr +flog.threshold(name=ROOT) + +# Set the threshold for the given logger\cr +flog.threshold(threshold, name=ROOT) +} +\examples{ +flog.threshold(ERROR) +flog.info("Won't print") +flog.threshold(INFO) +flog.info("Will print") +} +\author{ +Brian Lee Yung Rowe +} +\keyword{data} + diff --git a/man/ftry.Rd b/man/ftry.Rd new file mode 100644 index 0000000..2aa1031 --- /dev/null +++ b/man/ftry.Rd @@ -0,0 +1,28 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/logger.R +\name{ftry} +\alias{ftry} +\title{Wrap a try block in futile.logger} +\usage{ +ftry(expr, error = stop, finally = NULL) +} +\arguments{ +\item{expr}{The expression to evaluate in a try block} + +\item{error}{An error handler} + +\item{finally}{Pass-through to tryCatch finally} +} +\description{ +This function integrates futile.logger with the error and warning system +so problems can be caught both in the standard R warning system, while +also being emitted via futile.logger. +} +\examples{ +ftry(log(-1)) +} +\author{ +Brian Lee Yung Rowe +} +\keyword{data} + diff --git a/man/futile.logger-package.Rd b/man/futile.logger-package.Rd new file mode 100644 index 0000000..ff3900b --- /dev/null +++ b/man/futile.logger-package.Rd @@ -0,0 +1,123 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/futile.logger-package.R +\docType{package} +\name{futile.logger-package} +\alias{flog.namespace} +\alias{futile.logger} +\alias{futile.logger-package} +\title{A Logging Utility for R} +\description{ +This package implements a logging system inspired by log4j. The basic idea +of layouts, appenders, and loggers is faithful to log4j, while the +implementation and idiom is all R. This means that support for hierarchical +loggers, custom appenders, custom layouts is coupled with a simple and +intuitive functional syntax. +} +\details{ +\tabular{ll}{ +Package: \tab futile.logger\cr +Type: \tab Package\cr +Version: \tab 1.4.3\cr +Date: \tab 2016-07-10\cr +License: \tab LGPL-3\cr +LazyLoad: \tab yes\cr +} + +The latest version of futile.logger introduces zero-configuration semantics +out of the box. This means that you can use the default configuration as is. +It is also easy to interactively change the configuration of the ROOT +logger, as well as create new loggers. Since loggers form a hierarchy based +on their name, the ROOT logger is the starting point of the hierarchy and +always exists. By default the ROOT logger is defined with a simple layout, +printing to the console, with an INFO threshold. This means that writing to +any logger with a threshold of INFO or higher will write to the console. + +All of the logging functions take a format string so it is easy to add +arbitrary values to log messages. + +> flog.info("This song is just \%s words \%s", 7, "long") + +Thresholds range from most verbose to least verbose: TRACE, DEBUG, INFO, +WARN, ERROR, FATAL. You can easily change the threshold of the ROOT logger +by calling > flog.threshold(TRACE) which changes will print all log messages +from every package. To suppress most logging by default but turn on all +debugging for a logger 'my.logger', you would execute + +> flog.threshold(ERROR)\cr +> flog.threshold(TRACE, name='my.logger') + +Any arbitrary logger can be defined simply by specifying it in any +futile.logger write operation (futile.threshold, futile.appender, +futile.layout). If the logger hasn't been defined, then it will be defined +dynamically. Any unspecified options will be copied from the parent logger. + +When writing log messages, futile.logger will search the hierarchy based on +the logger name. In our example, if 'my.logger' hasn't been defined then +futile.logger will look for a logger named 'my' and finally the ROOT logger. + +Functions calling futile.logger from a package are automatically assigned a +logger that has the name of the package. Suppose we have log messages in a +package called 'my.package'. Then any function that calls futile.logger from +within the package will automatically be assigned a default logger of +'my.package' instead of ROOT. This means that it is easy to change the log +setting of any package that uses futile.logger for logging by just updating +the logger for the given package. For instance suppose you want to output +log message for my.package to a file instead. + +> flog.appender(appender.file('my.package.log'), name='my.package') + +Now all log statements in the package my.package will be written to a file +instead of the console. All other log messages will continue to be written +to the console. + +Appenders do the actual work of writing log messages to a writeable target, +whether that is a console, a file, a URL, database, etc. When creating an +appender, the implementation-specific options are passed to the appender at +instantiation. The package defines two appender generator functions: + +\describe{ + \item{appender.file}{Write to a file} + \item{appender.console}{Write to the console} +} + +Each of these functions returns the actual appender function, so be sure to +actually call the function! + +Layouts are responsible for formatting messages. This operation usually +consists of adding the log level, a timestamp, plus some pretty-printing to +make the log messages easy on the eyes. The package supplies several layouts: + +\describe{ + \item{layout.simple}{Writes messages with a default format} + \item{layout.json}{Generates messages in a JSON format} + \item{layout.format}{Define your own format} + \item{layout.tracearg}{Print a variable name along with its value} +} +} +\examples{ + +flog.debug("This \%s print", "won't") +flog.warn("This \%s print", "will") + +flog.info("This inherits from the ROOT logger", name='logger.a') +flog.threshold(DEBUG, name='logger.a') +flog.debug("logger.a has now been set to DEBUG", name='logger.a') +flog.debug("But the ROOT logger is still at INFO (so this won't print)") + +\dontrun{ +flog.appender(appender.file("other.log"), name='logger.b') +flog.info("This writes to a \%s", "file", name='logger.b') +} + +} +\author{ +Brian Lee Yung Rowe <[email protected]> +} +\seealso{ +\code{\link{flog.logger}}, \code{\link{flog.threshold}}, +\code{\link{flog.layout}}, \code{\link{flog.appender}} +} +\keyword{attribute} +\keyword{logic} +\keyword{package} + diff --git a/man/logger.options.Rd b/man/logger.options.Rd new file mode 100644 index 0000000..fb7c31e --- /dev/null +++ b/man/logger.options.Rd @@ -0,0 +1,36 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/options.R +\name{logger.options} +\alias{DEBUG} +\alias{ERROR} +\alias{FATAL} +\alias{INFO} +\alias{TRACE} +\alias{WARN} +\alias{logger.options} +\title{Constants for 'futile.logger'} +\usage{ +logger.options(..., simplify = FALSE, update = list()) +} +\arguments{ +\item{...}{TODO} + +\item{simplify}{TODO} + +\item{update}{TODO} +} +\description{ +Log level constants and the logger options. +} +\details{ +The logging configuration is managed by 'logger.options', a function +generated by OptionsManager within 'futile.options'. +} +\author{ +Brian Lee Yung Rowe +} +\seealso{ +\code{futile.options} +} +\keyword{data} + diff --git a/man/scat.Rd b/man/scat.Rd new file mode 100644 index 0000000..a7d8472 --- /dev/null +++ b/man/scat.Rd @@ -0,0 +1,34 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/scat.R +\name{scat} +\alias{scat} +\title{Print formatted messages} +\usage{ +scat(format, ..., use.newline = TRUE) +} +\arguments{ +\item{format}{A format string passed to sprintf} + +\item{use.newline}{Whether to append a new line at the end} + +\item{\dots}{Arguments to pass to sprintf for dereferencing} +} +\value{ +A formatted string printed to the console +} +\description{ +A replacement for \code{cat} that has built-in sprintf formatting +} +\details{ +Like \code{cat} but you can use format strings. +} +\examples{ + + apply(array(2:5),1, function(x) scat('This has happened \%s times', x) ) + +} +\author{ +Brian Lee Yung Rowe +} +\keyword{data} + diff --git a/tests/testthat.R b/tests/testthat.R new file mode 100644 index 0000000..48aab7f --- /dev/null +++ b/tests/testthat.R @@ -0,0 +1,3 @@ +library(testthat) +library(futile.logger) +test_check("futile.logger") diff --git a/tests/testthat/test_debug.R b/tests/testthat/test_debug.R new file mode 100644 index 0000000..b90495b --- /dev/null +++ b/tests/testthat/test_debug.R @@ -0,0 +1,20 @@ +context("debug level logging") + +test_that("debug level is logged", { + flog.threshold(DEBUG) + expect_output(flog.debug("testlog"), "testlog") +}) + +test_that("higher levels are logged", { + testlog = paste0("testlog ", sample(100, 1)) + flog.threshold(DEBUG) + expect_output(flog.info(testlog), testlog) + expect_output(flog.warn(testlog), testlog) + expect_output(flog.error(testlog), testlog) + expect_output(flog.fatal(testlog), testlog) +}) + +test_that("lower levels are not logged", { + flog.threshold(DEBUG) + expect_silent(flog.trace("testlog")) +}) diff --git a/tests/testthat/test_json.R b/tests/testthat/test_json.R new file mode 100644 index 0000000..f8051c7 --- /dev/null +++ b/tests/testthat/test_json.R @@ -0,0 +1,44 @@ +if (requireNamespace("jsonlite", quietly=TRUE)) { +context("JSON: typical usage") +flog.threshold(INFO) +flog.layout(layout.json) + +test_that("simple string", { + raw <- capture.output(flog.info("log message")) + aslist <- jsonlite::fromJSON(raw) + expect_equal(aslist$level, "INFO") + expect_equal(aslist$message, "log message") + + ts <- strptime(aslist$timestamp, "%Y-%m-%d %H:%M:%S %z") + expect_true('POSIXt' %in% class(ts)) +}) + +test_that("additional objects", { + raw <- capture.output( + flog.info("log message", pet="hamster", weight=12, stuff=c("a", "b"))) + aslist <- jsonlite::fromJSON(raw) + expect_equal(aslist$level, "INFO") + expect_equal(aslist$message, "log message") + expect_equal(aslist$pet, "hamster") + expect_equal(aslist$weight, 12) + expect_equal(aslist$stuff, c("a", "b")) +}) + +context("JSON: NULL values") + +#test_that("NULL message", { +# raw <- capture.output(flog.info(NULL)) +# aslist <- fromJSON(raw) +# expect_equal(length(aslist$message), 0) +#}) + +test_that("NULL additional objects", { + raw <- capture.output(flog.info("log message", nullthing=NULL)) + aslist <- jsonlite::fromJSON(raw) + expect_equal(length(aslist$nullthing), 0) +}) + +# knockdown +flog.layout(layout.simple) + +} diff --git a/tests/testthat/test_layout.R b/tests/testthat/test_layout.R new file mode 100644 index 0000000..d708ce0 --- /dev/null +++ b/tests/testthat/test_layout.R @@ -0,0 +1,55 @@ +# :vim set ff=R +context("format string") +test_that("Embedded format string", { + flog.threshold(INFO) + raw <- capture.output(flog.info("This is a %s message", "log")) + #cat("\n[test.default] Raw:",raw,"\n") + expect_that(length(grep('INFO', raw)) > 0, is_true()) + expect_that(length(grep('log message', raw)) > 0, is_true()) +}) + +test_that("Custom layout dereferences level field", { + flog.threshold(INFO) + flog.layout(layout.format('xxx[~l]xxx')) + raw <- capture.output(flog.info("log message")) + flog.layout(layout.simple) + expect_that('xxx[INFO]xxx' == raw, is_true()) + expect_that(length(grep('log message', raw)) == 0, is_true()) +}) + +context("null values") +test_that("Raw null value is printed", { + raw <- capture.output(flog.info('xxx[%s]xxx', NULL)) + expect_that(length(grep('xxx[NULL]xxx', raw, fixed=TRUE)) == 1, is_true()) +}) + +test_that("Single null value is printed", { + opt <- list() + raw <- capture.output(flog.info('xxx[%s]xxx', opt$noexist)) + expect_that(length(grep('xxx[NULL]xxx', raw, fixed=TRUE)) == 1, is_true()) +}) + +test_that("Null is printed amongst variables", { + opt <- list() + raw <- capture.output(flog.info('aaa[%s]aaa xxx[%s]xxx', 3, opt$noexist)) + expect_that(length(grep('aaa[3]aaa', raw, fixed=TRUE)) == 1, is_true()) + expect_that(length(grep('xxx[NULL]xxx', raw, fixed=TRUE)) == 1, is_true()) +}) + +context("sprintf arguments") +test_that("no extra arguments are passed", { + expect_true(grepl('foobar$', capture.output(flog.info('foobar')))) + expect_true(grepl('foobar %s$', capture.output(flog.info('foobar %s')))) + expect_true(grepl('10%$', capture.output(flog.info('10%')))) +}) +test_that("some extra arguments are passed", { + expect_true(grepl('foobar$', capture.output(flog.info('foobar', pi)))) + expect_true(grepl( + 'foobar foo$', + capture.output(flog.info('foobar %s', 'foo')))) + expect_true(grepl('100$', capture.output(flog.info('10%d', 0)))) + expect_true( + grepl('foo and bar equals to foobar', + capture.output( + flog.info('%s and %s equals to %s', 'foo', 'bar', 'foobar')))) +}) diff --git a/tests/testthat/test_logger.R b/tests/testthat/test_logger.R new file mode 100644 index 0000000..c024aa9 --- /dev/null +++ b/tests/testthat/test_logger.R @@ -0,0 +1,86 @@ +context("root logger") +test_that("Default settings", { + flog.threshold(INFO) + raw <- capture.output(flog.info("log message")) + #cat("\n[test.default] Raw:",raw,"\n") + expect_that(length(grep('INFO', raw)) > 0, is_true()) + expect_that(length(grep('log message', raw)) > 0, is_true()) +}) + +test_that("Change root threshold", { + flog.threshold(ERROR) + raw <- capture.output(flog.info("log message")) + #cat("\n[test.change_threshold] Raw:",raw,"\n") + expect_that(length(raw) == 0, is_true()) +}) + + +context("capture output") +test_that("Capture works as expected", { + flog.threshold(INFO) + raw <- capture.output(flog.info("log message", head(cars), capture = TRUE)) + expect_that(length(raw) == 9, is_true()) + expect_that(grepl('^INFO',raw[1]), is_true()) + expect_that(nchar(raw[2]) == 0, is_true()) + expect_that(grepl('dist$',raw[3]), is_true()) + }) + +context("new logger") +test_that("Create new logger", { + flog.threshold(ERROR) + flog.threshold(DEBUG, name='my.package') + raw.root <- capture.output(flog.info("log message")) + raw.mine <- capture.output(flog.info("log message", name='my.package')) + expect_that(length(raw.root) == 0, is_true()) + expect_that(length(grep('INFO', raw.mine)) > 0, is_true()) + expect_that(length(grep('log message', raw.mine)) > 0, is_true()) +}) + +context("logger hierarchy") +test_that("Hierarchy is honored", { + flog.threshold(ERROR) + flog.threshold(TRACE, name='my') + flog.threshold(DEBUG, name='my.package') + raw.root <- capture.output(flog.info("log message")) + raw.l1 <- capture.output(flog.trace("log message", name='my')) + raw.l2 <- capture.output(flog.trace("log message", name='my.package')) + expect_that(length(raw.root) == 0, is_true()) + expect_that(length(grep('TRACE', raw.l1)) > 0, is_true()) + expect_that(length(grep('log message', raw.l1)) > 0, is_true()) + expect_that(length(raw.l2) == 0, is_true()) +}) + +test_that("Hierarchy inheritance", { + flog.remove('my.package') + flog.remove('my') + flog.threshold(ERROR) + flog.threshold(TRACE, name='my') + raw.root <- capture.output(flog.info("log message")) + raw.l1 <- capture.output(flog.trace("log message", name='my')) + raw.l2 <- capture.output(flog.trace("log message", name='my.package')) + expect_that(length(raw.root) == 0, is_true()) + expect_that(length(grep('TRACE', raw.l1)) > 0, is_true()) + expect_that(length(grep('log message', raw.l1)) > 0, is_true()) + expect_that(length(grep('TRACE', raw.l2)) > 0, is_true()) + expect_that(length(grep('log message', raw.l2)) > 0, is_true()) +}) + + +# Can't test this since test_that calls suppressMessages +#context("package loggers") +#test_that("ftry captures warnings", { + #raw <- capture.output(ftry(log(-1))) + #cat("raw =",raw,'\n') + #expect_that(length(grep('WARN', raw)) > 0, is_true()) + #expect_that(length(grep('simpleWarning', raw)) > 0, is_true()) +#}) + +context("carp") +test_that("carp returns output", { + expect_that(flog.carp(), is_false()) + flog.carp(TRUE) + flog.threshold(WARN) + raw <- flog.debug("foo") + expect_that(length(grep('DEBUG', raw)) > 0, is_true()) +}) + diff --git a/tests/testthat/test_stringconfig.R b/tests/testthat/test_stringconfig.R new file mode 100644 index 0000000..f41b807 --- /dev/null +++ b/tests/testthat/test_stringconfig.R @@ -0,0 +1,49 @@ +context("String configuration") + +test_that("trace threshold is set via string", { + flog.threshold("trace") + expect_equal(flog.threshold(), "TRACE") + expect_equivalent(flog.logger()$threshold, 9) + flog.threshold("TRACE") + expect_equal(flog.threshold(), "TRACE") +}) + +test_that("debug threshold is set via string", { + flog.threshold("debug") + expect_equal(flog.threshold(), "DEBUG") + expect_equivalent(flog.logger()$threshold, 8) + flog.threshold("DEBUG") + expect_equal(flog.threshold(), "DEBUG") +}) + +test_that("info threshold is set via string", { + flog.threshold("info") + expect_equal(flog.threshold(), "INFO") + expect_equivalent(flog.logger()$threshold, 6) + flog.threshold("INFO") + expect_equal(flog.threshold(), "INFO") +}) + +test_that("warn threshold is set via string", { + flog.threshold("warn") + expect_equal(flog.threshold(), "WARN") + expect_equivalent(flog.logger()$threshold, 4) + flog.threshold("WARN") + expect_equal(flog.threshold(), "WARN") +}) + +test_that("error threshold is set via string", { + flog.threshold("error") + expect_equal(flog.threshold(), "ERROR") + expect_equivalent(flog.logger()$threshold, 2) + flog.threshold("ERROR") + expect_equal(flog.threshold(), "ERROR") +}) + +test_that("fatal threshold is set via string", { + flog.threshold("fatal") + expect_equal(flog.threshold(), "FATAL") + expect_equivalent(flog.logger()$threshold, 1) + flog.threshold("FATAL") + expect_equal(flog.threshold(), "FATAL") +}) \ No newline at end of file -- Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/debian-med/r-cran-futile.logger.git _______________________________________________ debian-med-commit mailing list [email protected] http://lists.alioth.debian.org/cgi-bin/mailman/listinfo/debian-med-commit
