logger/0000755000176200001440000000000014571643063011542 5ustar liggesuserslogger/NAMESPACE0000644000176200001440000000372714571077771013001 0ustar liggesusers# Generated by roxygen2: do not edit by hand S3method(as.loglevel,character) S3method(as.loglevel,default) S3method(as.loglevel,integer) S3method(as.loglevel,numeric) S3method(print,loglevel) export("%except%") export(DEBUG) export(ERROR) export(FATAL) export(INFO) export(OFF) export(SUCCESS) export(TRACE) export(WARN) export(appender_async) export(appender_console) export(appender_file) export(appender_kinesis) export(appender_pushbullet) export(appender_slack) export(appender_stderr) export(appender_stdout) export(appender_syslog) export(appender_syslognet) export(appender_tee) export(appender_telegram) export(appender_void) export(as.loglevel) export(colorize_by_log_level) export(delete_logger_index) export(deparse_to_one_line) export(fail_on_missing_package) export(formatter_glue) export(formatter_glue_or_sprintf) export(formatter_glue_safe) export(formatter_json) export(formatter_logging) export(formatter_pander) export(formatter_paste) export(formatter_sprintf) export(get_logger_meta_variables) export(grayscale_by_log_level) export(layout_blank) export(layout_glue) export(layout_glue_colors) export(layout_glue_generator) export(layout_json) export(layout_json_parser) export(layout_logging) export(layout_simple) export(layout_syslognet) export(log_appender) export(log_debug) export(log_error) export(log_errors) export(log_eval) export(log_failure) export(log_fatal) export(log_formatter) export(log_info) export(log_layout) export(log_level) export(log_messages) export(log_namespaces) export(log_separator) export(log_shiny_input_changes) export(log_success) export(log_threshold) export(log_tictoc) export(log_trace) export(log_warn) export(log_warnings) export(log_with_separator) export(logger) export(skip_formatter) export(with_log_threshold) importFrom(utils,assignInMyNamespace) importFrom(utils,assignInNamespace) importFrom(utils,capture.output) importFrom(utils,compareVersion) importFrom(utils,getFromNamespace) importFrom(utils,packageVersion) importFrom(utils,str) logger/demo/0000755000176200001440000000000013375516546012474 5ustar liggesuserslogger/demo/colors.R0000644000176200001440000000064013375124527014112 0ustar liggesuserssuppressWarnings(suppressMessages(library(logger))) log_layout(layout_glue_colors) log_threshold(TRACE) log_info('Starting the script...') log_debug('This is the second line') log_trace('That is being placed right after the first one.') log_warn('Some errors might come!') log_error('This is a problem') log_debug('Getting an error is usually bad') log_error('This is another problem') log_fatal('The last problem.')logger/demo/00Index0000644000176200001440000000004013375516546013620 0ustar liggesuserscolors Using a colored layout logger/README.md0000644000176200001440000001604514567717130013031 0ustar liggesusers# logger [![Project Status: Active – The project has reached a stable, usable state and is being actively developed.](https://www.repostatus.org/badges/latest/active.svg)](https://www.repostatus.org/#active) [![CRAN](https://www.r-pkg.org/badges/version/logger)](https://cran.r-project.org/package=logger) [![Build Status](https://github.com/daroczig/logger/workflows/R-CMD-check/badge.svg)](https://github.com/daroczig/logger/actions) [![Code Coverage](https://codecov.io/gh/daroczig/logger/branch/master/graph/badge.svg)](https://app.codecov.io/gh/daroczig/logger) [![A Mikata Project](https://mikata.dev/img/badge.svg)](https://mikata.dev) A lightweight, modern and flexibly logging utility for R -- heavily inspired by the `futile.logger` R package and `logging` Python module. ## Installation [![CRAN version](http://www.r-pkg.org/badges/version-ago/logger)](https://cran.r-project.org/package=logger) ```r install.packages('logger') ``` The most recent, development version of `logger` can also be installed from GitHub: ```r remotes::install_github('daroczig/logger') ``` ## Quick example Setting the log level threshold to something low and logging various messages in ad-hoc and programmatic ways: ```r library(logger) log_threshold(DEBUG) log_info('Script starting up...') #> INFO [2018-20-11 22:49:36] Script starting up... pkgs <- available.packages() log_info('There are {nrow(pkgs)} R packages hosted on CRAN!') #> INFO [2018-20-11 22:49:37] There are 13433 R packages hosted on CRAN! for (letter in letters) { lpkgs <- sum(grepl(letter, pkgs[, 'Package'], ignore.case = TRUE)) log_level(if (lpkgs < 5000) TRACE else DEBUG, '{lpkgs} R packages including the {shQuote(letter)} letter') } #> DEBUG [2018-20-11 22:49:38] 6300 R packages including the 'a' letter #> DEBUG [2018-20-11 22:49:38] 6772 R packages including the 'e' letter #> DEBUG [2018-20-11 22:49:38] 5412 R packages including the 'i' letter #> DEBUG [2018-20-11 22:49:38] 7014 R packages including the 'r' letter #> DEBUG [2018-20-11 22:49:38] 6402 R packages including the 's' letter #> DEBUG [2018-20-11 22:49:38] 5864 R packages including the 't' letter log_warn('There might be many, like {1:2} or more warnings!!!') #> WARN [2018-20-11 22:49:39] There might be many, like 1 or more warnings!!! #> WARN [2018-20-11 22:49:39] There might be many, like 2 or more warnings!!! ``` Setting a custom log layout to render the log records with colors: ```r library(logger) log_layout(layout_glue_colors) log_threshold(TRACE) log_info('Starting the script...') log_debug('This is the second log line') log_trace('Note that the 2nd line is being placed right after the 1st one.') log_success('Doing pretty well so far!') log_warn('But beware, as some errors might come :/') log_error('This is a problem') log_debug('Note that getting an error is usually bad') log_error('This is another problem') log_fatal('The last problem') ``` Or simply run the related demo: ```r demo(colors, package = 'logger', echo = FALSE) ``` colored log output But you could set up any custom colors and layout, eg using custom colors only for the log levels, make it grayscale, include the calling function or R package namespace with specific colors etc. For more details, see the related vignettes. ## Why yet another logging R package? Although there are multiple pretty good options already hosted on CRAN when it comes to logging in R, such as - [`futile.logger`](https://cran.r-project.org/package=futile.logger): probably the most popular `log4j` variant (and I'm a big fan) - [`logging`](https://cran.r-project.org/package=logging): just like Python's `logging` package - [`loggit`](https://cran.r-project.org/package=loggit): capture `message`, `warning` and `stop` function messages in a JSON file - [`log4r`](https://cran.r-project.org/package=log4r): `log4j`-based, object-oriented logger - [`rsyslog`](https://cran.r-project.org/package=rsyslog): logging to `syslog` on 'POSIX'-compatible operating systems - [`lumberjack`](https://cran.r-project.org/package=lumberjack): provides a special operator to log changes in data Also many more work-in-progress R packages hosted on eg GitHub, such as - https://github.com/smbache/loggr - https://github.com/nfultz/tron - https://github.com/metrumresearchgroup/logrrr - https://github.com/lorenzwalthert/drogger - https://github.com/s-fleck/yog But some/most of these packages are - not actively maintained any more, and/or maintainers are not being open for new features / patches - not being modular enough for extensions - prone to scoping issues - using strange syntax elements, eg dots in function names or object-oriented approaches not being very familiar to most R users - requires a lot of typing and code repetitions So based on all the above subjective opinions, I decided to write the `n+1`th extensible `log4j` logger that fits my liking -- and hopefully yours as well -- with the focus being on: - keep it close to `log4j` - respect the most recent function / variable naming conventions and general R coding style - by default, rely on `glue` when it comes to formatting / rendering log messages, but keep it flexible if others prefer `sprintf` (eg for performance reasons) or other functions - support vectorization (eg passing a vector to be logged on multiple lines) - make it easy to extend with new features (eg custom layouts, message formats and output) - prepare for writing to various services, streams etc - provide support for namespaces, preferably automatically finding and creating a custom namespace for all R packages writing log messages, each with optionally configurable log level threshold, message and output formats - allow stacking loggers to implement logger hierarchy -- even within a namespace, so that the very same `log` call can write all the `TRACE` log messages to the console, while only pushing `ERROR`s to DataDog and eg `INFO` messages to CloudWatch - optionally colorize log message based on the log level - make logging fun Welcome to the [Bazaar](https://en.wikipedia.org/wiki/The_Cathedral_and_the_Bazaar), and if you have happened to already use any of the above mentioned R packages for logging, you might find useful the [Migration Guide](https://daroczig.github.io/logger/articles/migration.html). ## Interested in more details? Check out the main documentation site at https://daroczig.github.io/logger or the vignettes on the below topics: * [Introduction to logger](https://daroczig.github.io/logger/articles/Intro.html) * [The Anatomy of a Log Request](https://daroczig.github.io/logger/articles/anatomy.html) * [Customizing the Format and the Destination of a Log Record](https://daroczig.github.io/logger/articles/customize_logger.html) * [Writing Custom Logger Extensions](https://daroczig.github.io/logger/articles/write_custom_extensions.html) * [Migration Guide from other logging packages](https://daroczig.github.io/logger/articles/migration.html) * [Logging from R Packages](https://daroczig.github.io/logger/articles/r_packages.html) * [Simple Benchmarks on Performance](https://daroczig.github.io/logger/articles/performance.html) logger/man/0000755000176200001440000000000014571101564012310 5ustar liggesuserslogger/man/layout_logging.Rd0000644000176200001440000000355713770625564015647 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/layouts.R \name{layout_logging} \alias{layout_logging} \title{Format a log record as the logging package does by default} \usage{ layout_logging( level, msg, namespace = NA_character_, .logcall = sys.call(), .topcall = sys.call(-1), .topenv = parent.frame() ) } \arguments{ \item{level}{log level, see \code{\link{log_levels}} for more details} \item{msg}{string message} \item{namespace}{string referring to the \code{logger} environment / config to be used to override the target of the message record to be used instead of the default namespace, which is defined by the R package name from which the logger was called, and falls back to a common, global namespace.} \item{.logcall}{the logging call being evaluated (useful in formatters and layouts when you want to have access to the raw, unevaluated R expression)} \item{.topcall}{R expression from which the logging function was called (useful in formatters and layouts to extract the calling function's name or arguments)} \item{.topenv}{original frame of the \code{.topcall} calling function where the formatter function will be evaluated and that is used to look up the \code{namespace} as well via \code{logger:::top_env_name}} } \value{ character vector } \description{ Format a log record as the logging package does by default } \examples{ \dontrun{ log_layout(layout_logging) log_info(42) log_info(42, namespace = 'everything') devtools::load_all(system.file('demo-packages/logger-tester-package', package = 'logger')) logger_tester_function(INFO, 42) } } \seealso{ This is a \code{\link{log_layout}}, for alternatives, see \code{\link{layout_blank}}, \code{\link{layout_glue}}, \code{\link{layout_glue_colors}}, \code{\link{layout_json}}, \code{\link{layout_json_parser}}, or generator functions such as \code{\link{layout_glue_generator}} } logger/man/log_shiny_input_changes.Rd0000644000176200001440000000230214567717130017505 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/hooks.R \name{log_shiny_input_changes} \alias{log_shiny_input_changes} \title{Auto logging input changes in Shiny app} \usage{ log_shiny_input_changes( input, level = INFO, namespace = NA_character_, excluded_inputs = character() ) } \arguments{ \item{input}{passed from Shiny's \code{server}} \item{level}{log level} \item{namespace}{the name of the namespace} \item{excluded_inputs}{character vector of input names to exclude from logging} } \description{ This is to be called in the \code{server} section of the Shiny app. } \examples{ \dontrun{ library(shiny) ui <- bootstrapPage( numericInput('mean', 'mean', 0), numericInput('sd', 'sd', 1), textInput('title', 'title', 'title'), textInput('foo', 'This is not used at all, still gets logged', 'foo'), passwordInput('password', 'Password not to be logged', 'secret'), plotOutput('plot') ) server <- function(input, output) { logger::log_shiny_input_changes(input, excluded_inputs = 'password') output$plot <- renderPlot({ hist(rnorm(1e3, input$mean, input$sd), main = input$title) }) } shinyApp(ui = ui, server = server) } } logger/man/layout_blank.Rd0000644000176200001440000000317213770625564015301 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/layouts.R \name{layout_blank} \alias{layout_blank} \title{Format a log record by including the raw message without anything added or modified} \usage{ layout_blank( level, msg, namespace = NA_character_, .logcall = sys.call(), .topcall = sys.call(-1), .topenv = parent.frame() ) } \arguments{ \item{level}{log level, see \code{\link{log_levels}} for more details} \item{msg}{string message} \item{namespace}{string referring to the \code{logger} environment / config to be used to override the target of the message record to be used instead of the default namespace, which is defined by the R package name from which the logger was called, and falls back to a common, global namespace.} \item{.logcall}{the logging call being evaluated (useful in formatters and layouts when you want to have access to the raw, unevaluated R expression)} \item{.topcall}{R expression from which the logging function was called (useful in formatters and layouts to extract the calling function's name or arguments)} \item{.topenv}{original frame of the \code{.topcall} calling function where the formatter function will be evaluated and that is used to look up the \code{namespace} as well via \code{logger:::top_env_name}} } \value{ character vector } \description{ Format a log record by including the raw message without anything added or modified } \seealso{ This is a \code{\link{log_layout}}, for alternatives, see \code{\link{layout_simple}}, \code{\link{layout_glue_colors}}, \code{\link{layout_json}}, or generator functions such as \code{\link{layout_glue_generator}} } logger/man/appender_void.Rd0000644000176200001440000000053314571077771015433 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/appenders.R \name{appender_void} \alias{appender_void} \title{Dummy appender not delivering the log record to anywhere} \usage{ appender_void(lines) } \arguments{ \item{lines}{character vector} } \description{ Dummy appender not delivering the log record to anywhere } logger/man/layout_glue_generator.Rd0000644000176200001440000000230213770625564017206 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/layouts.R \name{layout_glue_generator} \alias{layout_glue_generator} \title{Generate log layout function using common variables available via glue syntax} \usage{ layout_glue_generator( format = "{level} [{format(time, \\"\%Y-\%m-\%d \%H:\%M:\%S\\")}] {msg}" ) } \arguments{ \item{format}{\code{glue}-flavored layout of the message using the above variables} } \value{ function taking \code{level} and \code{msg} arguments - keeping the original call creating the generator in the \code{generator} attribute that is returned when calling \code{\link{log_layout}} for the currently used layout } \description{ \code{format} is passed to \code{glue} with access to the below variables: \itemize{ \item msg: the actual log message \item further variables set by \code{\link{get_logger_meta_variables}} } } \examples{ \dontrun{ example_layout <- layout_glue_generator( format = '{node}/{pid}/{ns}/{ans}/{topenv}/{fn} {time} {level}: {msg}') example_layout(INFO, 'try {runif(1)}') log_layout(example_layout) log_info('try {runif(1)}') } } \seealso{ See example calls from \code{\link{layout_glue}} and \code{\link{layout_glue_colors}}. } logger/man/appender_syslognet.Rd0000644000176200001440000000144314020000764016473 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/appenders.R \name{appender_syslognet} \alias{appender_syslognet} \title{Send log messages to a network syslog server} \usage{ appender_syslognet(identifier, server, port = 601L) } \arguments{ \item{identifier}{program/function identification (string).} \item{server}{machine where syslog daemon runs (string).} \item{port}{port where syslog daemon listens (integer).} } \value{ A function taking a \code{lines} argument. } \description{ Send log messages to a network syslog server } \note{ This functionality depends on the \pkg{syslognet} package. } \examples{ \dontrun{ if (requireNamespace("syslognet", quietly = TRUE)) { log_appender(appender_syslognet("test_app", 'remoteserver')) log_info("Test message.") } } } logger/man/warn_if_globalCallingHandlers_is_not_available.Rd0000644000176200001440000000052713770625564024111 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/hooks.R \name{warn_if_globalCallingHandlers_is_not_available} \alias{warn_if_globalCallingHandlers_is_not_available} \title{Warn to update R to 4+} \usage{ warn_if_globalCallingHandlers_is_not_available() } \description{ Warn to update R to 4+ } \keyword{internal} logger/man/log_warnings.Rd0000644000176200001440000000115014570721542015270 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/hooks.R \name{log_warnings} \alias{log_warnings} \title{Injects a logger call to standard warnings} \usage{ log_warnings(muffle = getOption("logger_muffle_warnings", FALSE)) } \arguments{ \item{muffle}{if TRUE, the warning is not shown after being logged} } \description{ This function uses \code{trace} to add a \code{log_warn} function call when \code{warning} is called to log the warning messages with the \code{logger} layout and appender. } \examples{ \dontrun{ log_warnings() for (i in 1:5) { Sys.sleep(runif(1)); warning(i) } } } logger/man/log_eval.Rd0000644000176200001440000000302113400221416014350 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/helpers.R \name{log_eval} \alias{log_eval} \title{Evaluate an expression and log results} \usage{ log_eval(expr, level = TRACE, multiline = FALSE) } \arguments{ \item{expr}{R expression to be evaluated while logging the expression itself along with the result} \item{level}{\code{\link{log_levels}}} \item{multiline}{setting to \code{FALSE} will print both the expression (enforced to be on one line by removing line-breaks if any) and its result on a single line separated by \code{=>}, while setting to \code{TRUE} will log the expression and the result in separate sections reserving line-breaks and rendering the printed results} } \description{ Evaluate an expression and log results } \examples{ \dontrun{ log_eval(pi * 2, level = INFO) ## lowering the log level threshold so that we don't have to set a higher level in log_eval log_threshold(TRACE) log_eval(x <- 4) log_eval(sqrt(x)) ## log_eval can be called in-line as well as returning the return value of the expression x <- log_eval(mean(runif(1e3))) x ## https://twitter.com/krlmlr/status/1067864829547999232 f <- sqrt g <- mean x <- 1:31 log_eval(f(g(x)), level = INFO) log_eval(y <- f(g(x)), level = INFO) ## returning a function log_eval(f <- sqrt) log_eval(f) ## evaluating something returning a wall of "text" log_eval(f <- log_eval) log_eval(f <- log_eval, multiline = TRUE) ## doing something computationally intensive log_eval(system.time(for(i in 1:100) mad(runif(1000))), multiline = TRUE) } } logger/man/catch_base_log.Rd0000644000176200001440000000136414567717130015527 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/utils.R \name{catch_base_log} \alias{catch_base_log} \title{Catch the log header} \usage{ catch_base_log( level, namespace, .topcall = sys.call(-1), .topenv = parent.frame() ) } \arguments{ \item{level}{see \code{\link{log_levels}}} \item{namespace}{string} } \value{ string } \description{ Catch the log header } \examples{ \dontrun{ catch_base_log(INFO, NA_character_) logger <- layout_glue_generator(format = '{node}/{pid}/{namespace}/{fn} {time} {level}: {msg}') log_layout(logger) catch_base_log(INFO, NA_character_) fun <- function() catch_base_log(INFO, NA_character_) fun() catch_base_log(INFO, NA_character_, .topcall = call('funLONG')) } } \keyword{internal} logger/man/formatter_pander.Rd0000644000176200001440000000276414570721542016147 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/formatters.R \name{formatter_pander} \alias{formatter_pander} \title{Formats R objects with pander} \usage{ formatter_pander( x, ..., .logcall = sys.call(), .topcall = sys.call(-1), .topenv = parent.frame() ) } \arguments{ \item{x}{object to be logged} \item{...}{optional parameters passed to \code{pander}} \item{.logcall}{the logging call being evaluated (useful in formatters and layouts when you want to have access to the raw, unevaluated R expression)} \item{.topcall}{R expression from which the logging function was called (useful in formatters and layouts to extract the calling function's name or arguments)} \item{.topenv}{original frame of the \code{.topcall} calling function where the formatter function will be evaluated and that is used to look up the \code{namespace} as well via \code{logger:::top_env_name}} } \value{ character vector } \description{ Formats R objects with pander } \note{ This functionality depends on the \pkg{pander} package. } \examples{ \dontrun{ log_formatter(formatter_pander) log_info('42') log_info(42) log_info(4+2) log_info(head(iris)) log_info(head(iris), style = 'simple') log_info(lm(hp ~ wt, mtcars)) } } \seealso{ This is a \code{\link{log_formatter}}, for alternatives, see \code{\link{formatter_paste}}, \code{\link{formatter_sprintf}}, \code{\link{formatter_glue}}, \code{\link{formatter_glue_safe}}, \code{\link{formatter_glue_or_sprintf}}, \code{\link{formatter_logging}} } logger/man/fallback_namespace.Rd0000644000176200001440000000063713406041521016350 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/logger.R \name{fallback_namespace} \alias{fallback_namespace} \title{Checks if provided namespace exists and falls back to global if not} \usage{ fallback_namespace(namespace) } \arguments{ \item{namespace}{string} } \value{ string } \description{ Checks if provided namespace exists and falls back to global if not } \keyword{internal} logger/man/log_level.Rd0000644000176200001440000000553114571077771014567 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/logger.R \name{log_level} \alias{log_level} \alias{log_fatal} \alias{log_error} \alias{log_warn} \alias{log_success} \alias{log_info} \alias{log_debug} \alias{log_trace} \title{Log a message with given log level} \usage{ log_level(level, ..., namespace = NA_character_, .logcall = sys.call(), .topcall = sys.call(-1), .topenv = parent.frame()) log_trace(..., namespace = NA_character_, .logcall = sys.call(), .topcall = sys.call(-1), .topenv = parent.frame()) log_debug(..., namespace = NA_character_, .logcall = sys.call(), .topcall = sys.call(-1), .topenv = parent.frame()) log_info(..., namespace = NA_character_, .logcall = sys.call(), .topcall = sys.call(-1), .topenv = parent.frame()) log_success(..., namespace = NA_character_, .logcall = sys.call(), .topcall = sys.call(-1), .topenv = parent.frame()) log_warn(..., namespace = NA_character_, .logcall = sys.call(), .topcall = sys.call(-1), .topenv = parent.frame()) log_error(..., namespace = NA_character_, .logcall = sys.call(), .topcall = sys.call(-1), .topenv = parent.frame()) log_fatal(..., namespace = NA_character_, .logcall = sys.call(), .topcall = sys.call(-1), .topenv = parent.frame()) } \arguments{ \item{level}{log level, see \code{\link{log_levels}} for more details} \item{...}{R objects that can be converted to a character vector via the active message formatter function} \item{namespace}{string referring to the \code{logger} environment / config to be used to override the target of the message record to be used instead of the default namespace, which is defined by the R package name from which the logger was called, and falls back to a common, global namespace.} \item{.logcall}{the logging call being evaluated (useful in formatters and layouts when you want to have access to the raw, unevaluated R expression)} \item{.topcall}{R expression from which the logging function was called (useful in formatters and layouts to extract the calling function's name or arguments)} \item{.topenv}{original frame of the \code{.topcall} calling function where the formatter function will be evaluated and that is used to look up the \code{namespace} as well via \code{logger:::top_env_name}} } \value{ Invisible \code{list} of \code{logger} objects. See \code{\link{logger}} for more details on the format/ } \description{ Log a message with given log level } \examples{ \dontrun{ log_level(INFO, 'hi there') log_info('hi there') ## output omitted log_debug('hi there') ## lower threshold and retry log_threshold(TRACE) log_debug('hi there') ## multiple lines log_info('ok {1:3} + {1:3} = {2*(1:3)}') log_layout(layout_json()) log_info('ok {1:3} + {1:3} = {2*(1:3)}') ## note for the JSON output, glue is not automatically applied log_info(glue::glue('ok {1:3} + {1:3} = {2*(1:3)}')) } } \seealso{ \code{\link{logger}} } logger/man/log_with_separator.Rd0000644000176200001440000000346514567717130016512 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/helpers.R \name{log_with_separator} \alias{log_with_separator} \title{Logs a message in a very visible way} \usage{ log_with_separator( ..., level = INFO, namespace = NA_character_, separator = "=", width = 80 ) } \arguments{ \item{...}{R objects that can be converted to a character vector via the active message formatter function} \item{level}{log level, see \code{\link{log_levels}} for more details} \item{namespace}{string referring to the \code{logger} environment / config to be used to override the target of the message record to be used instead of the default namespace, which is defined by the R package name from which the logger was called, and falls back to a common, global namespace.} \item{separator}{character to be used as a separator} \item{width}{max width of message -- longer text will be wrapped into multiple lines} } \description{ Logs a message in a very visible way } \examples{ log_with_separator('An important message') log_with_separator('Some critical KPI down!!!', separator = '$') log_with_separator('This message is worth a {1e3} words') log_with_separator(paste( 'A very important message with a bunch of extra words that will', 'eventually wrap into a multi-line message for our quite nice demo :wow:')) log_with_separator(paste( 'A very important message with a bunch of extra words that will', 'eventually wrap into a multi-line message for our quite nice demo :wow:'), width = 60) log_with_separator('Boo!', level = FATAL) log_layout(layout_blank) log_with_separator('Boo!', level = FATAL) logger <- layout_glue_generator(format = '{node}/{pid}/{namespace}/{fn} {time} {level}: {msg}') log_layout(logger) log_with_separator('Boo!', level = FATAL, width = 120) } \seealso{ \code{\link{log_separator}} } logger/man/formatter_paste.Rd0000644000176200001440000000256314570721542016007 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/formatters.R \name{formatter_paste} \alias{formatter_paste} \title{Concatenate R objects into a character vector via \code{paste}} \usage{ formatter_paste( ..., .logcall = sys.call(), .topcall = sys.call(-1), .topenv = parent.frame() ) } \arguments{ \item{...}{passed to \code{paste}} \item{.logcall}{the logging call being evaluated (useful in formatters and layouts when you want to have access to the raw, unevaluated R expression)} \item{.topcall}{R expression from which the logging function was called (useful in formatters and layouts to extract the calling function's name or arguments)} \item{.topenv}{original frame of the \code{.topcall} calling function where the formatter function will be evaluated and that is used to look up the \code{namespace} as well via \code{logger:::top_env_name}} } \value{ character vector } \description{ Concatenate R objects into a character vector via \code{paste} } \seealso{ This is a \code{\link{log_formatter}}, for alternatives, see \code{\link{formatter_sprintf}}, \code{\link{formatter_glue}}, \code{\link{formatter_glue_safe}}, \code{\link{formatter_glue_or_sprintf}}, \code{\link{formatter_logging}}, \code{\link{formatter_json}}, \code{\link{formatter_pander}} and \code{\link{skip_formatter}} for marking a string not to apply the formatter on it. } logger/man/appender_tee.Rd0000644000176200001440000000254214567717130015244 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/appenders.R \name{appender_tee} \alias{appender_tee} \title{Append log messages to a file and stdout as well} \usage{ appender_tee( file, append = TRUE, max_lines = Inf, max_bytes = Inf, max_files = 1L ) } \arguments{ \item{file}{path} \item{append}{boolean passed to \code{cat} defining if the file should be overwritten with the most recent log message instead of appending} \item{max_lines}{numeric specifying the maximum number of lines allowed in a file before rotating} \item{max_bytes}{numeric specifying the maximum number of bytes allowed in a file before rotating} \item{max_files}{integer specifying the maximum number of files to be used in rotation} } \value{ function taking \code{lines} argument } \description{ This appends log messages to both console and a file. The same rotation options are available as in \code{\link{appender_file}}. } \seealso{ This is generator function for \code{\link{log_appender}}, for alternatives, see eg \code{\link{appender_console}}, \code{\link{appender_file}}, \code{\link{appender_slack}}, \code{\link{appender_pushbullet}}, \code{\link{appender_telegram}}, \code{\link{appender_syslog}}, \code{\link{appender_kinesis}} and \code{\link{appender_async}} for evaluate any \code{\link{log_appender}} function in a background process. } logger/man/formatter_logging.Rd0000644000176200001440000000347214570721542016321 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/formatters.R \name{formatter_logging} \alias{formatter_logging} \title{Mimic the default formatter used in the \pkg{logging} package} \usage{ formatter_logging( ..., .logcall = sys.call(), .topcall = sys.call(-1), .topenv = parent.frame() ) } \arguments{ \item{...}{string and further params passed to \code{sprintf} or R expressions to be evaluated} \item{.logcall}{the logging call being evaluated (useful in formatters and layouts when you want to have access to the raw, unevaluated R expression)} \item{.topcall}{R expression from which the logging function was called (useful in formatters and layouts to extract the calling function's name or arguments)} \item{.topenv}{original frame of the \code{.topcall} calling function where the formatter function will be evaluated and that is used to look up the \code{namespace} as well via \code{logger:::top_env_name}} } \value{ character vector } \description{ The \pkg{logging} package uses a formatter that behaves differently when the input is a string or other R object. If the first argument is a string, then \code{\link{sprintf}} is being called -- otherwise it does something like \code{\link{log_eval}} and logs the R expression(s) and the result(s) as well. } \examples{ \dontrun{ log_formatter(formatter_logging) log_info('42') log_info(42) log_info(4+2) log_info('foo \%s', 'bar') log_info('vector \%s', 1:3) log_info(12, 1+1, 2 * 2) } } \seealso{ This is a \code{\link{log_formatter}}, for alternatives, see \code{\link{formatter_paste}}, \code{\link{formatter_glue}}, \code{\link{formatter_glue_safe}}, \code{\link{formatter_glue_or_sprintf}}, \code{\link{formatter_json}}, \code{\link{formatter_pander}} and \code{\link{skip_formatter}} for marking a string not to apply the formatter on it. } logger/man/formatter_glue.Rd0000644000176200001440000000306314570721542015623 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/formatters.R \name{formatter_glue} \alias{formatter_glue} \title{Apply \code{glue} to convert R objects into a character vector} \usage{ formatter_glue( ..., .logcall = sys.call(), .topcall = sys.call(-1), .topenv = parent.frame() ) } \arguments{ \item{...}{passed to \code{glue} for the text interpolation} \item{.logcall}{the logging call being evaluated (useful in formatters and layouts when you want to have access to the raw, unevaluated R expression)} \item{.topcall}{R expression from which the logging function was called (useful in formatters and layouts to extract the calling function's name or arguments)} \item{.topenv}{original frame of the \code{.topcall} calling function where the formatter function will be evaluated and that is used to look up the \code{namespace} as well via \code{logger:::top_env_name}} } \value{ character vector } \description{ Apply \code{glue} to convert R objects into a character vector } \note{ Although this is the default log message formatter function, but when \pkg{glue} is not installed, \code{\link{formatter_sprintf}} will be used as a fallback. } \seealso{ This is a \code{\link{log_formatter}}, for alternatives, see \code{\link{formatter_paste}}, \code{\link{formatter_sprintf}}, \code{\link{formatter_glue_or_sprintf}}, \code{\link{formatter_glue_safe}}, \code{\link{formatter_logging}}, \code{\link{formatter_json}}, \code{\link{formatter_pander}} and \code{\link{skip_formatter}} for marking a string not to apply the formatter on it. } logger/man/layout_json_parser.Rd0000644000176200001440000000236713770625564016544 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/layouts.R \name{layout_json_parser} \alias{layout_json_parser} \title{Generate log layout function rendering JSON after merging meta fields with parsed list from JSON message} \usage{ layout_json_parser( fields = c("time", "level", "ns", "ans", "topenv", "fn", "node", "arch", "os_name", "os_release", "os_version", "pid", "user") ) } \arguments{ \item{fields}{character vector of field names to be included in the JSON} } \description{ Generate log layout function rendering JSON after merging meta fields with parsed list from JSON message } \note{ This functionality depends on the \pkg{jsonlite} package. } \examples{ \dontrun{ log_formatter(formatter_json) log_info(everything = 42) log_layout(layout_json_parser()) log_info(everything = 42) log_layout(layout_json_parser(fields = c('time', 'node'))) log_info(cars = row.names(mtcars), species = unique(iris$Species)) } } \seealso{ This is a \code{\link{log_layout}} potentially to be used with \code{\link{formatter_json}}, for alternatives, see \code{\link{layout_simple}}, \code{\link{layout_glue}}, \code{\link{layout_glue_colors}}, \code{\link{layout_json}} or generator functions such as \code{\link{layout_glue_generator}} } logger/man/log_config_setter.Rd0000644000176200001440000000103614567717130016302 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/logger.R \name{log_config_setter} \alias{log_config_setter} \title{Base Logging Function} \usage{ log_config_setter(fun_name, arg, namespace, index) } \arguments{ \item{fun_name}{string a full name of log function} \item{arg}{see \code{\link{log_levels}}} \item{namespace}{logger namespace} \item{index}{index of the logger within the namespace} } \value{ currently set or return log function property } \description{ Base Logging Function } \keyword{internal} logger/man/layout_glue_colors.Rd0000644000176200001440000000416613770625564016533 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/layouts.R \name{layout_glue_colors} \alias{layout_glue_colors} \title{Format a log message with \code{glue} and ANSI escape codes to add colors} \usage{ layout_glue_colors( level, msg, namespace = NA_character_, .logcall = sys.call(), .topcall = sys.call(-1), .topenv = parent.frame() ) } \arguments{ \item{level}{log level, see \code{\link{log_levels}} for more details} \item{msg}{string message} \item{namespace}{string referring to the \code{logger} environment / config to be used to override the target of the message record to be used instead of the default namespace, which is defined by the R package name from which the logger was called, and falls back to a common, global namespace.} \item{.logcall}{the logging call being evaluated (useful in formatters and layouts when you want to have access to the raw, unevaluated R expression)} \item{.topcall}{R expression from which the logging function was called (useful in formatters and layouts to extract the calling function's name or arguments)} \item{.topenv}{original frame of the \code{.topcall} calling function where the formatter function will be evaluated and that is used to look up the \code{namespace} as well via \code{logger:::top_env_name}} } \value{ character vector } \description{ Format a log message with \code{glue} and ANSI escape codes to add colors } \note{ This functionality depends on the \pkg{crayon} package. } \examples{ \dontrun{ log_layout(layout_glue_colors) log_threshold(TRACE) log_info('Starting the script...') log_debug('This is the second line') log_trace('That is being placed right after the first one.') log_warn('Some errors might come!') log_error('This is a problem') log_debug('Getting an error is usually bad') log_error('This is another problem') log_fatal('The last problem.') } } \seealso{ This is a \code{\link{log_layout}}, for alternatives, see \code{\link{layout_blank}}, \code{\link{layout_simple}}, \code{\link{layout_glue}}, \code{\link{layout_json}}, \code{\link{layout_json_parser}}, or generator functions such as \code{\link{layout_glue_generator}} } logger/man/formatter_glue_safe.Rd0000644000176200001440000000270214570721542016620 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/formatters.R \name{formatter_glue_safe} \alias{formatter_glue_safe} \title{Apply \code{glue_safe} to convert R objects into a character vector} \usage{ formatter_glue_safe( ..., .logcall = sys.call(), .topcall = sys.call(-1), .topenv = parent.frame() ) } \arguments{ \item{...}{passed to \code{glue_safe} for the text interpolation} \item{.logcall}{the logging call being evaluated (useful in formatters and layouts when you want to have access to the raw, unevaluated R expression)} \item{.topcall}{R expression from which the logging function was called (useful in formatters and layouts to extract the calling function's name or arguments)} \item{.topenv}{original frame of the \code{.topcall} calling function where the formatter function will be evaluated and that is used to look up the \code{namespace} as well via \code{logger:::top_env_name}} } \value{ character vector } \description{ Apply \code{glue_safe} to convert R objects into a character vector } \seealso{ This is a \code{\link{log_formatter}}, for alternatives, see \code{\link{formatter_glue}}, \code{\link{formatter_paste}}, \code{\link{formatter_sprintf}}, \code{\link{formatter_glue}}, \code{\link{formatter_glue_or_sprintf}}, \code{\link{formatter_logging}}, \code{\link{formatter_json}}, \code{\link{formatter_pander}} and \code{\link{skip_formatter}} for marking a string not to apply the formatter on it. } logger/man/log_errors.Rd0000644000176200001440000000106314570721542014757 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/hooks.R \name{log_errors} \alias{log_errors} \title{Injects a logger call to standard errors} \usage{ log_errors(muffle = getOption("logger_muffle_errors", FALSE)) } \arguments{ \item{muffle}{if TRUE, the error is not thrown after being logged} } \description{ This function uses \code{trace} to add a \code{log_error} function call when \code{stop} is called to log the error messages with the \code{logger} layout and appender. } \examples{ \dontrun{ log_errors() stop('foobar') } } logger/man/log_levels.Rd0000644000176200001440000000260614571101712014731 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/levels.R \docType{data} \name{OFF} \alias{OFF} \alias{log_levels} \alias{FATAL} \alias{ERROR} \alias{WARN} \alias{SUCCESS} \alias{INFO} \alias{DEBUG} \alias{TRACE} \title{Log levels} \format{ An object of class \code{loglevel} (inherits from \code{integer}) of length 1. } \usage{ TRACE DEBUG INFO SUCCESS WARN ERROR FATAL OFF } \description{ The standard Apache logj4 log levels plus a custom level for \code{SUCCESS}. For the full list of these log levels and suggested usage, check the below Details. } \details{ List of supported log levels: \enumerate{ \item \code{OFF} No events will be logged \item \code{FATAL} Severe error that will prevent the application from continuing \item \code{ERROR} An error in the application, possibly recoverable \item \code{WARN} An event that might possible lead to an error \item \code{SUCCESS} An explicit success event above the INFO level that you want to log \item \code{INFO} An event for informational purposes \item \code{DEBUG} A general debugging event \item \code{TRACE} A fine-grained debug message, typically capturing the flow through the application. } } \references{ \url{https://logging.apache.org/log4j/2.x/javadoc/log4j-api/org/apache/logging/log4j/Level.html}, \url{https://logging.apache.org/log4j/2.x/manual/customloglevels.html} } \keyword{datasets} logger/man/appender_slack.Rd0000644000176200001440000000243413770625564015570 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/appenders.R \name{appender_slack} \alias{appender_slack} \title{Send log messages to a Slack channel} \usage{ appender_slack( channel = Sys.getenv("SLACK_CHANNEL"), username = Sys.getenv("SLACK_USERNAME"), icon_emoji = Sys.getenv("SLACK_ICON_EMOJI"), api_token = Sys.getenv("SLACK_API_TOKEN"), preformatted = TRUE ) } \arguments{ \item{channel}{Slack channel name with a hashtag prefix for public channel and no prefix for private channels} \item{username}{Slack (bot) username} \item{icon_emoji}{optional override for the bot icon} \item{api_token}{Slack API token} \item{preformatted}{use code tags around the message?} } \value{ function taking \code{lines} argument } \description{ Send log messages to a Slack channel } \note{ This functionality depends on the \pkg{slackr} package. } \seealso{ This is generator function for \code{\link{log_appender}}, for alternatives, see eg \code{\link{appender_console}}, \code{\link{appender_file}}, \code{\link{appender_tee}}, \code{\link{appender_pushbullet}}, \code{\link{appender_telegram}}, \code{\link{appender_syslog}}, \code{\link{appender_kinesis}} and \code{\link{appender_async}} for evaluate any \code{\link{log_appender}} function in a background process. } logger/man/deparse_to_one_line.Rd0000644000176200001440000000103613770625564016607 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/utils.R \name{deparse_to_one_line} \alias{deparse_to_one_line} \title{Deparse and join all lines into a single line} \usage{ deparse_to_one_line(x) } \arguments{ \item{x}{object to \code{deparse}} } \value{ string } \description{ Calling \code{deparse} and joining all the returned lines into a single line, separated by whitespace, and then cleaning up all the duplicated whitespace (except for excessive whitespace in strings between single or double quotes). } logger/man/appender_pushbullet.Rd0000644000176200001440000000200713571735644016656 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/appenders.R \name{appender_pushbullet} \alias{appender_pushbullet} \title{Send log messages to Pushbullet} \usage{ appender_pushbullet(...) } \arguments{ \item{...}{parameters passed to \code{pbPost}, such as \code{recipients} or \code{apikey}, although it's probably much better to set all these in the \code{~/.rpushbullet.json} as per package docs at \url{http://dirk.eddelbuettel.com/code/rpushbullet.html}} } \description{ Send log messages to Pushbullet } \note{ This functionality depends on the \pkg{RPushbullet} package. } \seealso{ This is generator function for \code{\link{log_appender}}, for alternatives, see eg \code{\link{appender_console}}, \code{\link{appender_file}}, \code{\link{appender_tee}}, \code{\link{appender_slack}}, \code{\link{appender_telegram}}, \code{\link{appender_syslog}}, \code{\link{appender_kinesis}} and \code{\link{appender_async}} for evaluate any \code{\link{log_appender}} function in a background process. } logger/man/appender_telegram.Rd0000644000176200001440000000217213770625564016272 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/appenders.R \name{appender_telegram} \alias{appender_telegram} \title{Send log messages to a Telegram chat} \usage{ appender_telegram( chat_id = Sys.getenv("TELEGRAM_CHAT_ID"), bot_token = Sys.getenv("TELEGRAM_BOT_TOKEN"), parse_mode = NULL ) } \arguments{ \item{chat_id}{Unique identifier for the target chat or username of the target channel (in the format @channelusername)} \item{bot_token}{Telegram Authorization token} \item{parse_mode}{Message parse mode. Allowed values: Markdown or HTML} } \value{ function taking \code{lines} argument } \description{ Send log messages to a Telegram chat } \note{ This functionality depends on the \pkg{telegram} package. } \seealso{ This is generator function for \code{\link{log_appender}}, for alternatives, see eg \code{\link{appender_console}}, \code{\link{appender_file}}, \code{\link{appender_tee}}, \code{\link{appender_pushbullet}}, \code{\link{appender_syslog}}, \code{\link{appender_kinesis}} and \code{\link{appender_async}} for evaluate any \code{\link{log_appender}} function in a background process. } logger/man/log_threshold.Rd0000644000176200001440000000201114567717130015435 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/logger.R \name{log_threshold} \alias{log_threshold} \title{Get or set log level threshold} \usage{ log_threshold(level = NULL, namespace = "global", index = 1) } \arguments{ \item{level}{see \code{\link{log_levels}}} \item{namespace}{logger namespace} \item{index}{index of the logger within the namespace} } \value{ currently set log level threshold } \description{ Get or set log level threshold } \examples{ \dontrun{ ## check the currently set log level threshold log_threshold() ## change the log level threshold to WARN log_threshold(WARN) log_info(1) log_warn(2) ## add another logger with a lower log level threshold and check the number of logged messages log_threshold(INFO, index = 2) log_info(1) log_warn(2) ## set the log level threshold in all namespaces to ERROR log_threshold(ERROR, namespace = log_namespaces()) } } \seealso{ \code{\link{logger}}, \code{\link{log_layout}}, \code{\link{log_formatter}}, \code{\link{log_appender}} } logger/man/formatter_glue_or_sprintf.Rd0000644000176200001440000000460214570721542020070 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/formatters.R \name{formatter_glue_or_sprintf} \alias{formatter_glue_or_sprintf} \title{Apply \code{glue} and \code{sprintf}} \usage{ formatter_glue_or_sprintf( msg, ..., .logcall = sys.call(), .topcall = sys.call(-1), .topenv = parent.frame() ) } \arguments{ \item{msg}{passed to \code{sprintf} as \code{fmt} or handled as part of \code{...} in \code{glue}} \item{...}{passed to \code{glue} for the text interpolation} \item{.logcall}{the logging call being evaluated (useful in formatters and layouts when you want to have access to the raw, unevaluated R expression)} \item{.topcall}{R expression from which the logging function was called (useful in formatters and layouts to extract the calling function's name or arguments)} \item{.topenv}{original frame of the \code{.topcall} calling function where the formatter function will be evaluated and that is used to look up the \code{namespace} as well via \code{logger:::top_env_name}} } \value{ character vector } \description{ The best of both words: using both formatter functions in your log messages, which can be useful eg if you are migrating from \code{sprintf} formatted log messages to \code{glue} or similar. } \details{ Note that this function tries to be smart when passing arguments to \code{glue} and \code{sprintf}, but might fail with some edge cases, and returns an unformatted string. } \examples{ \dontrun{ formatter_glue_or_sprintf("{a} + {b} = \%s", a = 2, b = 3, 5) formatter_glue_or_sprintf("{pi} * {2} = \%s", pi*2) formatter_glue_or_sprintf("{pi} * {2} = {pi*2}") formatter_glue_or_sprintf("Hi ", "{c('foo', 'bar')}, did you know that 2*4={2*4}") formatter_glue_or_sprintf("Hi {c('foo', 'bar')}, did you know that 2*4={2*4}") formatter_glue_or_sprintf("Hi {c('foo', 'bar')}, did you know that 2*4=\%s", 2*4) formatter_glue_or_sprintf("Hi \%s, did you know that 2*4={2*4}", c('foo', 'bar')) formatter_glue_or_sprintf("Hi \%s, did you know that 2*4=\%s", c('foo', 'bar'), 2*4) } } \seealso{ This is a \code{\link{log_formatter}}, for alternatives, see \code{\link{formatter_paste}}, \code{\link{formatter_sprintf}}, \code{\link{formatter_glue}}, \code{\link{formatter_glue_safe}}, \code{\link{formatter_logging}}, \code{\link{formatter_json}}, \code{\link{formatter_pander}} and \code{\link{skip_formatter}} for marking a string not to apply the formatter on it. } logger/man/appender_kinesis.Rd0000644000176200001440000000156413571735644016143 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/appenders.R \name{appender_kinesis} \alias{appender_kinesis} \title{Send log messages to a Amazon Kinesis stream} \usage{ appender_kinesis(stream) } \arguments{ \item{stream}{name of the Kinesis stream} } \value{ function taking \code{lines} and optional \code{partition_key} argument } \description{ Send log messages to a Amazon Kinesis stream } \note{ This functionality depends on the \pkg{botor} package. } \seealso{ This is generator function for \code{\link{log_appender}}, for alternatives, see eg \code{\link{appender_console}}, \code{\link{appender_file}}, \code{\link{appender_tee}}, \code{\link{appender_pushbullet}}, \code{\link{appender_telegram}}, \code{\link{appender_syslog}} and \code{\link{appender_async}} for evaluate any \code{\link{log_appender}} function in a background process. } logger/man/appender_syslog.Rd0000644000176200001440000000210213571735644016003 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/appenders.R \name{appender_syslog} \alias{appender_syslog} \title{Send log messages to the POSIX system log} \usage{ appender_syslog(identifier, ...) } \arguments{ \item{identifier}{A string identifying the process.} \item{...}{Further arguments passed on to \code{\link[rsyslog]{open_syslog}}.} } \value{ function taking \code{lines} argument } \description{ Send log messages to the POSIX system log } \note{ This functionality depends on the \pkg{rsyslog} package. } \examples{ \dontrun{ if (requireNamespace("rsyslog", quietly = TRUE)) { log_appender(appender_syslog("test")) log_info("Test message.") } } } \seealso{ This is generator function for \code{\link{log_appender}}, for alternatives, see eg \code{\link{appender_console}}, \code{\link{appender_file}}, \code{\link{appender_tee}}, \code{\link{appender_pushbullet}}, \code{\link{appender_telegram}}, \code{\link{appender_kinesis}} and \code{\link{appender_async}} for evaluate any \code{\link{log_appender}} function in a background process. } logger/man/fail_on_missing_package.Rd0000644000176200001440000000106314567717130017421 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/utils.R \name{fail_on_missing_package} \alias{fail_on_missing_package} \title{Check if R package can be loaded and fails loudly otherwise} \usage{ fail_on_missing_package(pkg, min_version) } \arguments{ \item{pkg}{string} \item{min_version}{optional minimum version needed} } \description{ Check if R package can be loaded and fails loudly otherwise } \examples{ \dontrun{ f <- function() fail_on_missing_package('foobar') f() g <- function() fail_on_missing_package('stats') g() } } logger/man/layout_json.Rd0000644000176200001440000000172513770625564015165 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/layouts.R \name{layout_json} \alias{layout_json} \title{Generate log layout function rendering JSON} \usage{ layout_json( fields = c("time", "level", "ns", "ans", "topenv", "fn", "node", "arch", "os_name", "os_release", "os_version", "pid", "user", "msg") ) } \arguments{ \item{fields}{character vector of field names to be included in the JSON} } \value{ character vector } \description{ Generate log layout function rendering JSON } \note{ This functionality depends on the \pkg{jsonlite} package. } \examples{ \dontrun{ log_layout(layout_json()) log_info(42) log_info('ok {1:3} + {1:3} = {2*(1:3)}') } } \seealso{ This is a \code{\link{log_layout}}, for alternatives, see \code{\link{layout_blank}}, \code{\link{layout_simple}}, \code{\link{layout_glue}}, \code{\link{layout_glue_colors}}, \code{\link{layout_json_parser}}, or generator functions such as \code{\link{layout_glue_generator}} } logger/man/appender_console.Rd0000644000176200001440000000141413571735644016132 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/appenders.R \name{appender_console} \alias{appender_console} \alias{appender_stderr} \title{Append log record to stderr} \usage{ appender_console(lines) appender_stderr(lines) } \arguments{ \item{lines}{character vector} } \description{ Append log record to stderr } \seealso{ This is a \code{\link{log_appender}}, for alternatives, see eg \code{\link{appender_stdout}}, \code{\link{appender_file}}, \code{\link{appender_tee}}, \code{\link{appender_slack}}, \code{\link{appender_pushbullet}}, \code{\link{appender_telegram}}, \code{\link{appender_syslog}}, \code{\link{appender_kinesis}} and \code{\link{appender_async}} for evaluate any \code{\link{log_appender}} function in a background process. } logger/man/figures/0000755000176200001440000000000013406263114013750 5ustar liggesuserslogger/man/figures/colors.png0000644000176200001440000012224413375502204015765 0ustar liggesusersPNG  IHDRk־[sBIT|dtEXtSoftwaregnome-screenshot> IDATxw|հҪ.B Qۘv=N\8Η:kv)nqLPTP]3B ] h̙sϜss–t(((((((((;9sT*f@MAAAAAAADljZ Q5u_?wrdY0AHLH8,)(((((((|3X++-㲅aPdYfâw }3qI%ҹV_:+~v#c+>768ї33s2c bomٶY#b7K7~Ίg$K'2Ng{+ Y0 \.xᇱ^ tziNװ*p "24{wWh&G7`gk)G rDJXf/dμ>~e֕.``6ּ5kKƐ=-M_= ի,;,tvv]wEJJWr V++o&W_ˍ$umۍpKn$+SPP:l2| OntH;~ܤp `iȖ`}!P}Q >wY=뢙v,H"EKQv| ;^P&.˧&lizۼ\wd"Gxaxz_&>SFE'Qۘل{e+ξ;I{3I#>TKg]5ր$?yo𛻧qI?{THwyBL2>d_w=&&2Q}d+"S x4\!yGof{\vHǮ.ƒ==j ۨ7LOm3zy M"r;_c'Leje^-r?o;ǫFep1 555$''j*~⨫JFAݒtrr`EtT~f:N^g@c5%XSP8w,d.珪1#[> e?f$/M=^DfN:H n[0um1e?9A%_rU 5a󗟳~O1-f+|g|g| gY{JFe1!IJ3.ȧ,$^?1 -|gd,,?~,' S/Wh&]} 3·iz {ZSq-3CKp ᤥ#XҒԸJr8'݇]S:8[p .e*%՗i˹1;tfzy6Bb z: >ĦM`LxL$>s XO?EP !*i W]6*^5wygΖFbbbxy+{,f wFƿ\'zԤ^6ATu)à ьKΛ<^:ePt%Dgkw70遹 uVє*{,Hc^N : 7b?.>^k_}~1J׮a{ z/ϻ'~ݥGLqZ7ФOgXwkhA dڌew55nĸhEh;q /xM~%Ƚ@dd eNwUu &&6YKӃT"ZGNJh4*dO)X(p:7U9J9hAn"̈́Sq_Vs xҖ]g wzyS5_U^E"h6ןT9 9}yIɘƨϊW5!<@TTHxVZE{{G}k=r:ٟ'g3C%uk 鬩$%&yT36c]1>0@&c]K|Z*~w!&>d_pWRZ-XL! Bg/X{>-S$ NY$$,HRhjZeP hZ ]SI~ZSX*i -J?)r:*L!no֦玣mev|AdfA6Md<^4klz^z뮳iV`Nj |A"##=o$gl6=tGϿ%GYSPW6T暬ǡ Y:hon-ēu003{.†Y,O`_.CT˺0:z|(jˆ0?Tr0h@x;-'sAK|f&~K)wc}w.94I9Q7JG2'Dg< UlgӱKqn{Wwxlohˀ$g2)6.32}_8·`?QYY /pmժUw}y%c`MbCSZeTAkU|s쉗qmT%W7P$j77$cGQ_H%ȁJulӘ.c᤹,࠹rvv8 eq\zuS2_HH>G| .RfukJ;ISsE.|/b[YslL{_ʓYz4eԒ }v?,,catAgC5pDc'LDUzCA7+<6jjijhu%pR9{J0Vd>[WʤӸ4)sc湖UZ7… x^z i=z:~x5Uhf̻Cem&e.xw*_Dq٤NShkr|VJOKr|nTUPPPQzcTAAAAAAAA@ c mRPPPPPPPPv=xuĕ9_@8.G:casA oL\UPPPPPP8;$T j "XCsq\L,~~O?̯2Q}v107'8݅VnnO,ˎ. 2iysODEp0k~Vg;D<oP#jc|09g9B~wes:yl bR|4_}OrcNGp,I5__T2jDs ځS4KˈLClIBЅGPtjo/6% /zXN.H NbLWeXO f/*F3o y Ʌ C_ܔD<%A,]8};m&O-fk_Ѩ`~f\_cϭFH^ɏL?>̌1YN~|I:=Vq)edkBǗ 6RF5z$DYm:d(t/;2IMyqEJXn+]%گ)\:Du 6fN+c)m P^D1Gx(tf[$kس5G8X?\F/_voN ! p)peB+G 4xvjl6&kh0!rm̂-\PS%E$iH.JAٷ׫^k%cnjGLo|!z_h˸*6c$2초DncÃ]ł}RBkNj6RK*>'9>'$IY@cє`ORpX+K0v2F sIJ@7Bd.M~԰,yd c59M_OT~_|Hmc\VWZɊаkc%/Üc!d_B*"@pԴY|14]epp8Td$3<_=6xIGEOW)M_M곔?X_?tO,_"PWa # =81bI(/us{bA'u_A9v&ל Nu,{)uu\}9mZ4_ ‘?j<ӟUrߎ1'st$LJ-"&fE뭣0Z*YA||z4WhpzY}?"#jo]ZkT aT*XkQ%F _}t]MuMb{ uJUtWO7;g-Ђ?4n6j!Drb h*[?;gMdO'փٟQˤU"5`tUj1:1w _/ajV~xɪv '\](Cha" ɄVqfT2ueʁ׍ND!1 r,^$wi}٪Λ}ų~qjH- B;\fTL;x\ )֐mg b 6!368?&pr@95t\ ޠm`J*3!|N;_!5 Tqѽ*j!Q9(:S `E`шv2jh >ɃIC[\3N!iKH|'V9}(;r@ЛxE!Yf#tȨZE vTFwXFD NuT?~}Imm;ϞcC\C~z@hp#0l`WcC0 `N.N_,=#uRj":@fcO+鰉11[H2jq`hĢ9\g5rpʓ1OJǥ"kV,9)<[e솈|Ł׍>7w75Ie`_/S`omt#lƳ~WqPB?o?;H ;Q%̄ ;/|,;9?pAAB{FZkQÆo5sԣ&`L eme4%g):J ԐhZf|2*JF>H}a*&ShIMoeַ L%.Jof̄6UA4dLh{X[r&?%7aL>ɋG  ڭdm%H-ZSuAk=xATt-7Li$ț;(hp@VFƼNj^n? dFGXNs|1U\gW۹dl+/ ;,D E-Ș%\g&P#VV.]?^ZGLTb-n:33Ǵ /Jh]]14pibe<دI-9vh IDATt,gFg96+Y-%|C#,h^3GOÚ6TX <Y>#VR|)ٿNg`+>^X4I6jt4H uՁ . d3M/pz; QxsCFa` 7odJ%0>4J0NwMB|mNj")j#54Uƥ;k᪎c˖j2&yn-ݑcrLҎ<dk[D T%~!\ɮ݁\Nn 0Q *Ѯ;P{m|o`D3RPiOZrX*46%?6ϓPd-9F4ۮ@/i#zAUxZ3G|)hoY2Oscodˁ,/`?Ni+Jك~֯?ʃIXƝS-eU |ZvzF.fIi>rhÁ]m am^gw0vVcwӏu{#}n 7Y{ Ϊ侥eX_%a}^7SY=#DbL;h z̄q6S`/d|E]ߚ"TYǎ|2+ 're!w-!@_9kydN@&{Pr/$d+n 4jx4bi9M>͘w>AD' *ڭ.6R'$^}kd<„qU>ZM䓆At 6.OR$^/cԬC\ny56udэ\>) _W E?n b/bYBs1[{w5N@7o!h:+?ymWcWG} µiWsӵ3IR¶5o^ Y?{A;=_M`KY<-( Eokri@Vէ!m"5 {|@"YIChx{y`n)|\GAKWt&w{,3?qUy'_"B5v.swIu-<32v\e\3n7[:Uòϲz,ԳgxI"f7Pvt5;eLx:^Un]JWcv6߮L3I1 hJxg>!2uLbjw*̢q5[MAAAAax~Ta>9 |Ә^H |iO$*J!Zsl= -=VI: ԫ% VPlD>U1$ɔ ؀ʂ2ڮOu1$39l?PAI%,t{$ŒU7FDu9;2 T_&\Ć'%T[6ꄍUI̜=1ZMczo }EG¸L&%rZY]y(d\?G;#IUǧ3#c4͡ ongҒoJLGUvQg> /YsSe~w#B"bXb}}IE1EN:KP}O̘ywy/CpP<H5a>^Cl̘14֫7Yj9`K$j(IJ|!̳Y~G=#غw;I>؋7bkLW-"()LlP+v؃Ƴp;x<{>S5GFq) uJ/ ?VرIȉO9+DRsw F1>!}}% 75ͺd̟ʚ8̠&mY9Wc_<Z͝A*;8@6Ab?ṼvdPō%շ("\jw'QɧZC2ܿ*P<#z }}ʗ@Ŧ1!$߰&^zpy0ER^{_l/2~rJn m}]Ϟu[ʬ3-Wj`'{HY'KÂQc{sI+;%72- (p"j3i~"cI nȶ2ڜ24WFu}xRc|Gu QWJ"U{4$ɀ$Iף\ML(4eyU)gh)ʣÉL#yULGXM?Y' !7Ta/+,/gMeaGѠŁ>69+eEA^:m #@ms-E؛oF[c֞DcQWmG>e)wEo==PoƂx9DΑ69Fd{^|vE?yU~#,k w_.Ev(w"sScGYkcKi6cw{X>$g[%9h"H9._W;̜HTZU6B*5j赗ba1q2$ AAg@Sj#:^L[yqR|`ŀ'̱4Y ?#*GlkA N{Tp3fo,%uf:oO1@KA>kRkCK~u!c83qVWSoTtd=6f{k$+Ձ>qi:@_zr7xI?{M|D勌Z\2UcFZzP&]w+KƊ^b}WŲ,-?c{3S/0!kn"j0Fnk6v}^dAF-J8mOX SFݏE%4*iCOs6"|ը4>čgd$;2B1jUZ4*Y~]TJŒ@*_4&ġ|-!8y5 '̼V.MiqgYp{:E7n"5_{VПv ^_R&E7dC!*{&ysJCX뢹x?k=uO|%7hTS~qq8/eF:Jkq6m;F)RkJq 4~>%#"+Z Ø~L:.) [jN<@n'.r7RXi&pS4ӉmPgNaՙUJ9S3D]I)Sq =l/:W3gkqVph^NOD+Q_c&y2e: ظ-C/tLֿsUP8KhRz ?"AE|2aNLxQgmP.TnJSm;j(|9Z U.mE ˄;۔@MAAAa5kM%,BiV4*ppA *(((((((\Lx3 :٠ JpsQk6k5o0/?bWEzc5oZNje FZLvreIz=#-z[ b$Y/p,59 &yϰQIS91[tmysLˑz^W:}>I\rI*WA|E Sr|,ඡ1WJ2 &Qd-vg^JeArÛnw \KTԅo06-*n=TLh,-;N,+ɍ7_Ό4l\`#sx5*~_<1iWsӵ3IR¶5oւG=k/&0,Luo5zc U+%DS۶oEC|2#ː1`[b.f#g/]WϚ_RwH1Cr^.qA 3=(E IټTgٰ͋mTRnt%0&քQ؛rCí51{^nY?<| ]N!(;WxtU9[wUX Oy?e Z^g Yb~'WV xSGߋs,c3c^j*d]?C`&.>bZ*В8RR{o~5p\HAAAAaP >IS1&`P%ɨl .9XS%4&ɑKD~:$H`.< ߥ*TАzյ~*CM|ȞPuYǂ*33/xc* h>k#*_痳xWc//hs9pb Zv8;gk@"cVZmz{W&6+3Wý!tNGR/j*ie91,3ݘ^tbSA@G@R8DrQ_ʡ4{ʼnG=11ո:(/[~/FkM pӕ7[a ~*c )jqUQi^wWPPsF`5t FE/ T&Z *x#rdЦ0;ȑ5Ly)TmҠAB:M!$&r˭=v>hý0 s2,aO_Noзޗ|!X_*;d.@Mn=}3cBTtegC1~ Cu4Rq ~OW@ 8܏}[u3nzi(4!Ϊ}|\%<f OmK|H%B|_yLƩطmN2mHC!1wD22:*]K=vAO|zIcCPgf0mGwy~F3q {qfzƛ;ߑ' \n R#t^}SvoW rDsR,QyT@n$b?h> 1͊\^yb;M2ƒ[B lQDF.5Qڸxb|Zw2ܿ*P<#FCA{o/Qr_ށiL 75Ůx{'zm_>|j3~Ŀ^f'd5\>CGj V$}0y4J HaȰj7?v (ǎHBUz8G*Q&!DSNCGGZIHXmNz.K(nH o j$UϞ./s0$P7{d_jziʋSkd![87Yy/+Ÿ>ЉnuXjk9//j|3ڲ j3=>)Kս(z9Ԅ*~>w?x 7<9.hӹ{c@Gxn^WG=16:pW5y2PDNa" ͸)Y1ݣֱ4=&p}y20% A|#VQL6 (an*'ӰHGZ~_ыͽ'HTy=נש5[Ѣz;Xs楻!M'?rO/頭B ~4VaJ=tQą٩\;/ls.qXH ա傅m,{[rۺ),~FEWX;?5O3بKW^c}LW>I~9_;~PBa~J/g#?{f콬{3+ ]>1n\]Lx;w|:i, ތRs!?oMrQUTb&{alԔsP$߆d)pqɗ\AN 6S NdԥqXJ~%qC2*y(ptzϖ 0y(#s> gk٬yd)*I*oMɷ3mL!a-$L%g}R=G8'1L[ӏWwP dY[7Pi\|?тFw7 s!;R5ڕF%UЦK>R8 B #%̦Z{R>L#xC#qT6Hf'ўʶcy4 Gg-`LJoΊV.!;?呧7[0RGEp̱$urek'k~e;<4kW^4PF6u( &KڰRvZ}`?&XξE%lGI/S9GJ/"s~nFN3ŧ/?hABWê1Է`@v"Z\p5M$_cPaF[SQjCj<BnH ۈڋQ 'fk c8~(m%L 7yRVVr'@J{u p0' jv7R[_VI/N,^/>|6΋Ky#Jy<>FrGCre~3_cЬ/Yx~54v)Fo=/ƧKTSA6V~s3FEFDva;Ps&9 2:HlB& ñxkC){E%;| HJͻp<tnGVswB(u:Irz* jdP|xEvk!ԴV1W|ό (ȣvV ZO^N_kr$b턤{E"fSmR~~sŰHJnƧzowl&/F7QIpHtK֍cIpk{*h]͈HVk<'rm_o^__w(A}]!<}I dƦ1I\P3e_٠fDE %TfN%zp,m( jG8w0Oui37ϩ#T[IM jA\Vo.Ά%lRQۂi%IH.si5JMWԃ&y頼.Z^x*c3؜. [zZMWZ=PTr'烦La $lPZ^:O|6*iv7TbvԘ)Wh [P|bOؕ?Ac䤺FBIe#/uBzԲ&4pl&, 6FM)(eG`U(5#$ ݷ5Lفx/Iik6RNiU))?2hy[8^HhLڢm䷛5`L v)T}Or[3>{o<ʲ6:Ѕ}Jwar aa92xw巤I@3AIΖ/9Zo3s39qWC/Ƨw8/$! !`&2OZIvVhVQbw's8pF":-:Hx;\:@5Z-Z؏;H=qh h[ Hi֖[բ=?O%~J/?DAK1NV!:ףdM'(7-f`1 0U1SGI2#&wj|[a%]7 rb|["v0\p':ƏڼN>qdSW֙9)Ψ(cZAw/O>%r9]kRtm\%&ړ"OC(n6>և/%]d3.eET֝M?#ZքGjѺ] %+CyTûNr8pN+/jzXMZmкgIRk94 e#:pV p*}_8ǩ\O˞s;eTN''5Z- %8[o+Q@pϸK~TAGFlj+U1@UE4ju8QT 4 7M~뷉i &zacm71v+vt9%Z[45`LRއab$rۯN_~#uXB ka5=eFWСUSoW$a ʌsY/6*(A@$'.I@K98nx^+8p7AԴQ*ADԸ ֱ)z*F0DQW1teϱ[( hq_QFP'.K}afF+^saL ћ lږBH`QD@rT8]2 Si֪/w$4K=MJ(܉LtM6Niм\HI@6AH\R-ncoQra Mh[-h,rNG~ekjfI#N\/YD3 a?R^{8Y=kerBQ6-m]]Bp# Ǣu+͚EKTioxilT+>WA.g$o/O~H?JNIldqLpPu/~ƶwYAZ=ƭ:~b28Z<fk5u6|k(19`0"gS D$b8p!P1ShFi+'} |mfK%GK-mI|b`45a.Y}rn\X!"!8c9d~1ѷڟ{c4PVRK|F 4W.ѢV3YEAD0,Ri\\2 sa PE-o/_ǩ__&})CYq;*.DhV9fNO!$,x xYvZ 돯Te Ҫ1?-\[xf:2Ϫ7xG&a#̦xSN㫢r%{y :`BzlHFb|i˦HɠG5YWUQ9Q{PѸlԔR&}_ޡ!%TTTTTTTTTzŜSɚ5GY5Yq0z)#sSov ּt# FC̿[{Z/uڶRQQܛ`f#KTS9|lJWicT'G|Gδ****'ESx.[O1d05S*or&ewJI6Á%|xW"HIOfn o ctyl|3mc~w7kK#qT6Hf'ўʶ)cy4 Gg-`LJoΊV.!Z)<PGEp̱$urek'k~e;<4kW^4PF6u(Z-A%qm؉dv;y-i>_]̘55~JjbmM<{S +sqKyYѳk-.uNl"85J4詖`hРkflWK:lԒ|whO$G lO=m%L 7yRVVr'@J{u p0' jv7R[_VI/N,N/>|6΋Ky#Jy<>FrG W>9,گ@~oc֢%+ُѳƒ.ˆŷUt&o&1^z/?q#.e>S. -Z$b]HX^Nߺʸh GCʿFov)8Kʴj.0HϾ6Q ^PExZp;ʿsN=RU@G3I.]_eR#A oOTv)uEQ F+c>gfQ;+Q'/WL@ޯ c ]k WvBҽ"Sg3)?׶}I)?bbX$a|%{o w flKqh'jWW4KpwԒ[>WJZ@|K+1x\TK E2~ WDi|(\J|7W4lHFuYu!+?%< A?|%z$?sE pM#k8ҝƂ]IbShw41Q4Ě!"!.b˓߽S_)l|%OEb 36mh8 ?***DJkH-52x\BsP-y$jĀ[Cxڠ \ fqZ mкBF>ÓuRe/GQIj~}\ᜒЀ9#th\>`gT ۯ\ޯ#YR8}8<Sn*No**JkD; Dx%5݂^Ā:%7I7ѨrIt'5"jp5h*r|How"/Jl\3w#_F>f;@9D"3E]H(AJU+{}d-vx ޷M#yWYW|kӚz$sUTɆgpwu/"DV{z_t0wYxXMwJ}0-`v^{KRښMSZUJ_ 3}4W hf fĂd])h&sCqsӬZq铹wL0 ƣ,yRޅ}Jwar aa92xw巤I@3A7-_rtfgr⮆vPO+ظ60V35xI4p7a:K88pE|Fp/qM:i،[/Q_[MprAwQ]>r!-TdWQ] m{'&ꩵ́ NQR^:j3;̤(}r8itrhhadrѳAPk0nA 7ݚ1u lɉo tCwś!</H%Pw*>]HW,y~W0 st;8@;1~pȈ+'gvV9)Ψ(cZAw/O>%r9]kRtl"KL'E]|a%2Uި7 IDATG.lY|aΉ"T$ޞ"T:7//͝z<O[u4 q̸n$PիV%u`|D-n= ޓ2Nlo۳IڿSUTTz7P0h69RKBj4P^ѵ dZ!t1TǸo8qŌ8iz >J<'nǛ}ZΪM8PrTε]{ s}l^K۲;|Ip <<; O;adj>{cAk?&i?&ky4/5K=t~ .a^{Am_>W?uGEEG]t/e~R yj5yux5||#\՗} ٬yd)*ԡx*oMɷ3esV9}y gdMڃGE$M#VEJ !`1{]dGu 6u/[XPȅME=žal ,*;nD\<맍&)|mQJcXn31Y ۼU.xsKcpd#Oo|<'z5:s,I8si|\Zpr/_~Yδ~yh&͚ Խky}]*5JfǸRvZ}`3Y8IW"*****0Z$] kc9˿c:ʼ"n MF(ٸh6Ll360#Y1h >yb:~կeKx,^[ݶ: OY>Z.?W_ $>vpm|z&b-&r9P/>{/@D/rrr]!9&ZeͲtùY\ퟲ_1+ُJ aSrAz,C22EYFVuw&>*Z7011in^@ wɡdkݮƘ)<\ _p{=)=3._HѾ[%tgv3@Ɲ:.2 wx}Qr.s =:uMMJ}kV4%`&JO`h~*d/[1up:,GQIn~S;]HdhoƝ Lh{%[~?'1wDj8\t7wJ~^ג4q]oĂoQ:l> Ғ8"]/|ݧ-p;ΨQI4lXrwnLSxn׵'̌o5%`, s.{Tt(LJ>$_0RY}y6Hm#h;_Й|V;iY{&Od:YAEEdMWp}OIm j"]oJNK:]N)7wN'y-/pRy֯VM =HlDB@t!*1Wu|5#o)$]T<ҫ^+>hz$sUTɆgpwu/"DV{z_t0wYxQ@.$@#^~Z @CKMW`&[엤ԴLSZUJ_ 3}4uDhLڢm䷛5`L v)T}OrckOe3$dclIua"]د\.G?}XA ]-iuz{bE6uKLCnGo$ ['YNrX=jqdSWnޜUMgT±d ﻗ`\~ߵzj ):6>I%&ړ"OC(nA$[!?~&?A"o;7KQX/kQ?Ò4xx!`i0dŪ`̜h"yb7t:qCwRꪫ֞ _`s'7=ns!:"ƨb\r[yө=~O/a-,w0c|10= :u_Uኤ`<:LaC`#F.e ylI'܂TɝeL/g_U BSÆHݨݝM4|z8 g7s\.X(JƲ%Kn$X{Yh)*m{={/4{5*Z>I~9_;~PBa~J/g#?{f콬{3+ ]>1n\]Lx;w 3G2 "$|;OϋUMXCV~/`ǃyϽslSȰj/ZXA M'Tö [kspV=^<2S*$a>e8&~/;-m]Efr3<7@CuS!+5B΂M xe:LU JozUg#j~;wC?O<4UQQQA>^ǜWuROpOg EڶLۡrޡ5RQQQQQQQ99AUTTTTTTT%t-k********g1jrs^M0Pg1΋٠~/e.M+NF*r#0zH"}[%"3pgG.bAYN*g*Y͚GBri )v^{9U$mcÙ6C q qKz.WQ@ .&4ӆRg)Rtl '-mO#曯Ich$G##𙤏IDAQ=n!EAly ?4z- qT6Hf'ўʶcy4 Gg-`LJoΊV.!Z)<PGEp̱$ure{r/_~Yδ~yh&͚ Խky}]*5JfǸ6v;y-i>_?#A$Emh)Ha ,ʛLd:nPrvqs-!/f0 n؈IR0x_..MHܷA 8:4CNJMt͕Ţ)f6ZJSd;V ww/U9,گ@~oc֢%+ُѳƒ.ˆŷe2OhH!HŅ'?J,{yoA<2ww Ub9\w:%4uy7M@|r`8Tӳ08{! {rnql9'~)Ī]#$2@ŷ'*Pdw~hZYn,F13c 3V퓗+S{ WZܱ.ⵅm;!`^(?W_ $>vhm|:JBiֶf֪BV7ݛؤ ]ޜKJQ x 9鱀5g'[ ].bG!"\nď#X|ccgØa$D" v+qd`@=1*2\ʮ/K7kEG lɥ=p_7]꒕  &6FMI2w*=ݖ~ZsaRr)hoh x'cgӯn˷l?yvsa=K/ ǝd5(xEshBܡ2ٔ7+gNrr>zEԯ,1kR5F i2xD+FX]Q(h4\Y07ġu|./|'ס{Q QD{Tn[Ee8$24 i&]7׶] /'Sy7tnry|@*-q'e)IMbs诡>w._u#v_+"?(b 2[өQTCW'_09b5yiUv7"^ECr>-볺攓=|Ytl`tFII{1 .<ӂ$x?b(A i|LW,vC(U#J!ŊQlacGus}9rkW?mE]]#Q#%`0d.wk%Ur]?}KAptjr[JԑAn pj ݨxA{ZL_0QI/E~ĀC?8[^:Ҏ.5@^7A~D٤ƶĆ,B !0ꌭQj}j@ x|їQAznh"QĖFq( %UQUY޷n}?8n\W.WR%Y/"xV#u=ܙi]yY"~b.>;~-792m|:CJV[]ƸhXLL`3G:JyX()a4fJOˬS\HPTD e%@)չdWrb.3P~gcLBCC[Eח{I4!Ek`r?a wS"AK9j H-':.I$\BI桳HRE$Ņ}4Z w2&D}QV53`2J]t"B~3  r.fDwSQ!'kM'I5ܢ)&J_fgYaqIidi `&[엤5hk)h4g-ij@$4&m60&ZS*DM澇n挧Y5>d]v?LH6v(v@)+߅r.ׇݕߒV'YϬgj'Zd+l'9[X/kl?] &p6>#8#:-%rKrKԣYYAVH7'B&ŧ<dHH'I{JC,VK(6*bn/}^TTDbbb%ͻ7>-d+or]l\t&xl AY0:BOHc .& "9(RNw+V}~:}?cOtJHkF@l>I:db̊UYqyltYgHaa z X9Ѵzz&sdKY_1_yW0|kVMAt’2yqd:9M<>A^[lQJ}xth^λ&-l:ZI\+;%PKj䢫\IY.z5*Gڷ#J2K[E.L%1f^iTX97&M$QK2R &zg?ZՙJwIL' ;-)LIVJVk m vFB֢QIȲ//!//5*xZߒ+rIxzؠFR0xi$!>ӫ>iP dM SIʱǿ;j5za5Aͨ,Q3/Jvù=%x;<.W*Ԭqc5fM2K%3L)v&1ceؘ/' IMr4HljYݡnDgSao=לi 0Nύ Iğ~!|Kr͌yOZ&-bKO[RN_Oc2჏pPs}–h KɖDEd Gg^EmcFW=7>/e 6*TN1t؛rR@ ( Fe9,Оܧ0dxemE\8*(Qȅqjrx%4 k lgrɉVtO\ǟNYKN#ilҫOkJ2cR ?(\hצ'F|5OE/P=M@Fk*$#)61HO50CL:É8`LF#}0iJaZ,)xnȫ_}{ה3ItjݝUF37~x b"2)4l leSc8YĒr"NcҒjVRW?_@ xl)1 5mUavϩ-$=?O~'~ww'_|J@Bݜf\LEoDh_tG8[A-eߚ@p7$Y^%?8Z9-! ΉDMp; э$= AȚ@ BZ]ms#@ D&@J$zk×lbkjT<6s k× WQԭ"茕xcj\ !TZ{"3`JCiCתp|(w53%__TQ'y ly)#\5k GX4~ 1{^ȝF TBY&g3yߴ)n7mXxʏ)Hvp7_o.kAUX@UM8WHpf*ekdO> ׁ`_'HĞ#iK}~tn6/߬erɑޘøe.Y;6 qw!CF}7Rf>[o~ilA/.X˧N$@F &o 6Y{ZN*k2]e?t#k%@zKrTko}C=K?|2]0 ͭOsPA|$ko|X%m| Ʊ@״ Kǔ7pɵ.H w)5Td8A`^.+1i7R؀jnDZ=#>DZhJ( Q"ǑF %$mȔ!D$ #eC^9ovmq ǒ)"cFaU`Y+Rk WZÛ]/ɔ*!>}#y=3>V~c~Q~/9WlƧ;6Qz6#?j'K}ljV>Zdr~'+(9gGpK/ 5IazZ6 |jhL%rzV~de5eoϣkV 9bظ ®4 6r>Ah({:L\y6/}}zR\u|AiL#Ա m[9w#`C>f`v.4Bfni x" Y8Ns_r[Xgz޼2a{@ʙ_|RxxiukC_`HW.Tsm7\:56@pGEgYJW6%(ҁd*ģ2MtgɠkNvG7QuZ0F\hdFsL_HV蝛0[9fj*W.W|lI4$3eBXgMUUpisW^])dڰ8Wrw6>|\&,`ÌS~#2JH5?c6Driۿk ϙ>y"Ͼ-I'0%gzHn>2q^˕&h 2f4O}Ϙ 4/%jw578ŲOfs*?+ca[#/4 x1l,Oo9Q3A`[w~2c.dM557ŹR}Y*c,wM^%m2ߟņv:ČғPgXA *zw{J[&p$Db%rɈ%U N 󶤿 }fOjUYOۂ@x\(@FBzznJ#9IGÒ8n"._ ?f-sמ%oJ쫑ۯ\^4Ξ3PWv|iꈟ[?Ͽi2vpKtpQE2טY1V6>}8pҜy2 ?`4"&&YܰUNKP,Y% S#q0fVtWu0Zǝ\+(8~[hiZJLSdz?kۑ #2i{ݱ$ 5\ )͹֭Ѷ YB+Dܞ-K,47[B*[Q)5+)&r%Dspv65YdGh@"ɏp?'eP˦򗟌FPSb0}Gd 8.%)$'1u~ECѣ|aHxU惘>9kBDj^Lze OEbU_r7yW}JWar%Y뼽q7D_8-9~!>XIPbi$j\eM:=`KmSd FT*­&D_'M1gv͆/ܺk8gB36'IPj%[{lf_d )^qgj*'5<5o O7)=f";#Kقa";ơ,mP69!]KJNt,--Q_BgeYq_odOڅx n+/|݊ۑ^qʯEq3jxsgF:έ~ɡ+&ŇL=ONi,Ӓ rɕFq#*IULYd)Qo^e[' S)_#Ȁ\rPFhVR j1j7R ,M 8fBF"#v9C5_s$p>{S@]3:`eM1O!kgV|ѨʊczF K Ή3'S[ϲUXj*VؔӒ2yqd:9M<>A^[lQJ}xth^λ&-l:Z hhd7 Z+kZ wJFNm0/"Ké CtЋv0OsR%Bɹ:䣯Q&3>  ]h.=q2 5z}}-GM*|{TYZ_gF&k1j \YR|EC[4CGc<,HhV_ ӨյWZ7ԍ x m/!Ƹ 1aS?!)SY/DOuI1 _֤Epi ؞ViP&|"cNۢOjKɖDEd Gg^EmcFW=7>/e 6*TN1t؛rR@ ( FeqM3η;M{ ɔCkXGQz/ggwA<|ؔ|wBLv#'5fpnǡ1n7b;a%jЪg{Bt#F#_wnm"2>m1/6I>_GPhB \H+L,xŚҳ~Wa8 /BƂ?)$?mS7Hy}B42y{?委mH+xMԘRw2{:.]o;ʓ3#hKݓW ;U/_|ou}N!_@ ku@ SӠ@ wJAȚ@ ? @ 5ɡ;o^,Zl(}\ ]?n_ǖ?yœm+z|J0\"I<'NWw2{#<2N0">Su\3L,-#9nl,9bl"۲G9T2mS~@]UA&46)&48CUƧS@_b_푴M1%]xjt?:7uEoV2Coa\K `g zȴxu]žu_́eI%,oI-w[_mCѷcSl c-;E1|Λ 0嵜6ԁUbGY_Vg]%2#q߾3j%"f>,@ PD}TLoӌ-N J&XL">$Yx"yIYہ&+&ch{<& #2HΡL8vVX{L8}r% 9]s'K04%Kشa^%gp[aO}f { Xϒ"گ@~[_i,b: BsT_(C~Ik*@ry:n:qj^_$k"EI<5}-1D⋨>G-6_ S$莟CWRJuWě%j!;6K!k?gYJ^kh9nov'SVp膤zUN92@~Q~/9WlƧ=g¡mnO?{l1`Dwo.YNќΣFhhc ɜ%V'hwslTP|'qq^HC׋IY,?^P6 /NCK`<>Er%_ u޸"r/ΖC{$(fY`Otwz:o 2yG*ri oey#8%(=2ybU!IPzHN"o'yŸI8QavLϺiV&y q9[p#9 eqJ JL;ʘA  ~@`(F &&5E ܳ(YSХ<k[9ٴ8ba$s0C_E̙{Di0Ikd@`M'Pk.߅Xn>i۷nV0PBϨΦ' ZVrg-اDnI@nz_:++Ȋ#BF]V׭jz”NF1֍73ja- ӃLy+Y71U,0> <ʅ1\_vjǖyW3ucRbIupvuB"89c_CyT9/ W'5eEvv..r/}ݸk3 d'URy7IDAT@ Y2I\.j9omtS6m!a߼NϘ҄>+[oyYX?FUV\h{7RXX?VhtN45?ݒ}WL@^U ŚUSyB%ets3zy|,γ bW<쁽Nw+MM[t@ȩvܟ'{ Z7ia1/5Xs+v+tj9y5=޾f c"xjmILd$۩o!IQӤ<c <މ-?=:0jP0}D5 : ~C9m6ZU|*hZ2yV~4> F֌ZsZ]򒈲H΢sNT50a\ofkvUՍ x m},gKk01wnLxH%n jɽ73=Akњ.>mJ9~?ʄ>A]Dzi~[ [I,'[/[ .Ay],Htu7S9}ao2UKqF6$wǑeLtφg?5[iXzo>S>c )m!W0ٶml3~8c7ǿ/lJX=BkxuU$W;NIn"F[y|EI:~vi=gb)g$/%xt-?H]_+_r/#vmɡ;/CF&o識(N1.T<6F4QcJiO~Q5dĢ#1LߑwW A@@@@˗/[>9gf)?n;m@ 9ĺq@ ;HQQb\@ pwr7bdM 7gIENDB`logger/man/formatter_sprintf.Rd0000644000176200001440000000265414570721542016361 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/formatters.R \name{formatter_sprintf} \alias{formatter_sprintf} \title{Apply \code{sprintf} to convert R objects into a character vector} \usage{ formatter_sprintf( fmt, ..., .logcall = sys.call(), .topcall = sys.call(-1), .topenv = parent.frame() ) } \arguments{ \item{fmt}{passed to \code{sprintf}} \item{...}{passed to \code{sprintf}} \item{.logcall}{the logging call being evaluated (useful in formatters and layouts when you want to have access to the raw, unevaluated R expression)} \item{.topcall}{R expression from which the logging function was called (useful in formatters and layouts to extract the calling function's name or arguments)} \item{.topenv}{original frame of the \code{.topcall} calling function where the formatter function will be evaluated and that is used to look up the \code{namespace} as well via \code{logger:::top_env_name}} } \value{ character vector } \description{ Apply \code{sprintf} to convert R objects into a character vector } \seealso{ This is a \code{\link{log_formatter}}, for alternatives, see \code{\link{formatter_paste}}, \code{\link{formatter_glue}}, \code{\link{formatter_glue_safe}}, \code{\link{formatter_glue_or_sprintf}}, \code{\link{formatter_logging}}, \code{\link{formatter_json}}, \code{\link{formatter_pander}} and \code{\link{skip_formatter}} for marking a string not to apply the formatter on it. } logger/man/validate_log_level.Rd0000644000176200001440000000061013455067767016435 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/logger.R \name{validate_log_level} \alias{validate_log_level} \title{Assure valid log level} \usage{ validate_log_level(level) } \arguments{ \item{level}{\code{\link{log_levels}} object or string representation} } \value{ \code{\link{log_levels}} object } \description{ Assure valid log level } \keyword{internal} logger/man/colorize_by_log_level.Rd0000644000176200001440000000151113770625564017160 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/color.R \name{colorize_by_log_level} \alias{colorize_by_log_level} \title{Colorize string by the related log level} \usage{ colorize_by_log_level(msg, level) } \arguments{ \item{msg}{string} \item{level}{see \code{\link{log_levels}}} } \value{ string with ANSI escape code } \description{ Adding color to a string to be used in terminal output. Supports ANSI standard colors 8 or 256. } \examples{ \dontrun{ cat(colorize_by_log_level(FATAL, 'foobar'), '\n') cat(colorize_by_log_level(ERROR, 'foobar'), '\n') cat(colorize_by_log_level(WARN, 'foobar'), '\n') cat(colorize_by_log_level(SUCCESS, 'foobar'), '\n') cat(colorize_by_log_level(INFO, 'foobar'), '\n') cat(colorize_by_log_level(DEBUG, 'foobar'), '\n') cat(colorize_by_log_level(TRACE, 'foobar'), '\n') } } logger/man/logger.Rd0000644000176200001440000000546614571077771014105 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/logger.R \name{logger} \alias{logger} \title{Generate logging utility} \usage{ logger(threshold, formatter, layout, appender) } \arguments{ \item{threshold}{omit log messages below this \code{\link{log_levels}}} \item{formatter}{function pre-processing the message of the log record when it's not wrapped in a \code{\link{skip_formatter}} call} \item{layout}{function rendering the layout of the actual log record} \item{appender}{function writing the log record} } \value{ A function taking the log \code{level} to compare with the set threshold, all the \code{...} arguments passed to the formatter function, besides the standard \code{namespace}, \code{.logcall}, \code{.topcall} and \code{.topenv} arguments (see \code{\link{log_level}} for more details). The function invisibly returns a list including the original \code{level}, \code{namespace}, all \code{...} transformed to a list as \code{params}, the log \code{message} (after calling the \code{formatter} function) and the log \code{record} (after calling the \code{layout} function), and a list of \code{handlers} with the \code{formatter}, \code{layout} and \code{appender} functions. } \description{ A logger consists of a log level \code{threshold}, a log message \code{formatter} function, a log record \code{layout} formatting function and the \code{appender} function deciding on the destination of the log record. For more details, see the package \code{README.md}. } \details{ By default, a general logger definition is created when loading the \code{logger} package, that uses \enumerate{ \item \code{\link{INFO}} (or as per the \code{LOGGER_LOG_LEVEL} environment variable override) as the log level threshold \item \code{\link{layout_simple}} as the layout function showing the log level, timestamp and log message \item \code{\link{formatter_glue}} (or \code{\link{formatter_sprintf}} if \pkg{glue} is not installed) as the default formatter function transforming the R objects to be logged to a character vector \item \code{\link{appender_console}} as the default log record destination } } \note{ It's quite unlikely that you need to call this function directly, but instead set the logger parameters and functions at \code{\link{log_threshold}}, \code{\link{log_formatter}}, \code{\link{log_layout}} and \code{\link{log_appender}} and then call \code{\link{log_levels}} and its derivatives, such as \code{\link{log_info}} directly. } \examples{ \dontrun{ do.call(logger, logger:::namespaces$global[[1]])(INFO, 42) do.call(logger, logger:::namespaces$global[[1]])(INFO, '{pi}') x <- 42 do.call(logger, logger:::namespaces$global[[1]])(INFO, '{x}^2 = {x^2}') } } \references{ For more details, see the Anatomy of a Log Request vignette at \url{https://daroczig.github.io/logger/articles/anatomy.html}. } logger/man/log_namespaces.Rd0000644000176200001440000000043313770625564015572 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/logger.R \name{log_namespaces} \alias{log_namespaces} \title{Looks up logger namespaces} \usage{ log_namespaces() } \value{ character vector of namespace names } \description{ Looks up logger namespaces } logger/man/layout_syslognet.Rd0000644000176200001440000000271614567717130016240 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/layouts.R \name{layout_syslognet} \alias{layout_syslognet} \title{Format a log record for syslognet} \usage{ layout_syslognet( level, msg, namespace = NA_character_, .logcall = sys.call(), .topcall = sys.call(-1), .topenv = parent.frame() ) } \arguments{ \item{level}{log level, see \code{\link{log_levels}} for more details} \item{msg}{string message} \item{namespace}{string referring to the \code{logger} environment / config to be used to override the target of the message record to be used instead of the default namespace, which is defined by the R package name from which the logger was called, and falls back to a common, global namespace.} \item{.logcall}{the logging call being evaluated (useful in formatters and layouts when you want to have access to the raw, unevaluated R expression)} \item{.topcall}{R expression from which the logging function was called (useful in formatters and layouts to extract the calling function's name or arguments)} \item{.topenv}{original frame of the \code{.topcall} calling function where the formatter function will be evaluated and that is used to look up the \code{namespace} as well via \code{logger:::top_env_name}} } \value{ A character vector with a severity attribute. } \description{ Format a log record for syslognet. This function converts the logger log level to a log severity level according to RFC 5424 "The Syslog Protocol". } logger/man/formatter_json.Rd0000644000176200001440000000322514571077771015651 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/formatters.R \name{formatter_json} \alias{formatter_json} \title{Transforms all passed R objects into a JSON list} \usage{ formatter_json( ..., .logcall = sys.call(), .topcall = sys.call(-1), .topenv = parent.frame() ) } \arguments{ \item{...}{passed to \code{toJSON} wrapped into a \code{list}} \item{.logcall}{the logging call being evaluated (useful in formatters and layouts when you want to have access to the raw, unevaluated R expression)} \item{.topcall}{R expression from which the logging function was called (useful in formatters and layouts to extract the calling function's name or arguments)} \item{.topenv}{original frame of the \code{.topcall} calling function where the formatter function will be evaluated and that is used to look up the \code{namespace} as well via \code{logger:::top_env_name}} } \value{ character vector } \description{ Transforms all passed R objects into a JSON list } \note{ This functionality depends on the \pkg{jsonlite} package. } \examples{ \dontrun{ log_formatter(formatter_json) log_layout(layout_json_parser()) log_info(everything = 42) log_info(mtcars = mtcars, species = iris$Species) } } \seealso{ This is a \code{\link{log_formatter}} potentially to be used with \code{\link{layout_json_parser}}, for alternatives, see \code{\link{formatter_paste}}, \code{\link{formatter_sprintf}}, \code{\link{formatter_glue}}, \code{\link{formatter_glue_safe}}, \code{\link{formatter_glue_or_sprintf}}, \code{\link{formatter_logging}}, \code{\link{formatter_pander}} and \code{\link{skip_formatter}} for marking a string not to apply the formatter on it. } logger/man/grayscale_by_log_level.Rd0000644000176200001440000000155613770625564017315 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/color.R \name{grayscale_by_log_level} \alias{grayscale_by_log_level} \title{Render a string with light/dark gray based on the related log level} \usage{ grayscale_by_log_level(msg, level) } \arguments{ \item{msg}{string} \item{level}{see \code{\link{log_levels}}} } \value{ string with ANSI escape code } \description{ Adding color to a string to be used in terminal output. Supports ANSI standard colors 8 or 256. } \examples{ \dontrun{ cat(grayscale_by_log_level(FATAL, 'foobar'), '\n') cat(grayscale_by_log_level(ERROR, 'foobar'), '\n') cat(grayscale_by_log_level(WARN, 'foobar'), '\n') cat(grayscale_by_log_level(SUCCESS, 'foobar'), '\n') cat(grayscale_by_log_level(INFO, 'foobar'), '\n') cat(grayscale_by_log_level(DEBUG, 'foobar'), '\n') cat(grayscale_by_log_level(TRACE, 'foobar'), '\n') } } logger/man/log_tictoc.Rd0000644000176200001440000000114214567717130014732 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/helpers.R \name{log_tictoc} \alias{log_tictoc} \title{Tic-toc logging} \usage{ log_tictoc(..., level = INFO, namespace = NA_character_) } \arguments{ \item{...}{passed to \code{log_level}} \item{level}{see \code{\link{log_levels}}} \item{namespace}{x} } \description{ Tic-toc logging } \examples{ \dontrun{ log_tictoc('warming up') Sys.sleep(0.1) log_tictoc('running') Sys.sleep(0.1) log_tictoc('running') Sys.sleep(runif(1)) log_tictoc('and running') } } \author{ Thanks to Neal Fultz for the idea and original implementation! } logger/man/get_logger_definitions.Rd0000644000176200001440000000115113770625564017321 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/logger.R \name{get_logger_definitions} \alias{get_logger_definitions} \title{Find the logger definition(s) specified for the current namespace with a fallback to the global namespace} \usage{ get_logger_definitions(namespace = NA_character_, .topenv = parent.frame()) } \arguments{ \item{namespace}{override the default / auto-picked namespace with a custom string} } \value{ list of function(s) } \description{ Find the logger definition(s) specified for the current namespace with a fallback to the global namespace } \keyword{internal} logger/man/with_log_threshold.Rd0000644000176200001440000000161113770625564016501 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/logger.R \name{with_log_threshold} \alias{with_log_threshold} \title{Evaluate R expression with a temporarily updated log level threshold} \usage{ with_log_threshold( expression, threshold = ERROR, namespace = "global", index = 1 ) } \arguments{ \item{expression}{R command} \item{threshold}{\code{\link{log_levels}}} \item{namespace}{logger namespace} \item{index}{index of the logger within the namespace} } \description{ Evaluate R expression with a temporarily updated log level threshold } \examples{ \dontrun{ log_threshold(TRACE) log_trace('Logging everything!') x <- with_log_threshold({ log_info('Now we are temporarily suppressing eg INFO messages') log_warn('WARN') log_debug('Debug messages are suppressed as well') log_error('ERROR') invisible(42) }, threshold = WARN) x log_trace('DONE') } } logger/man/log_layout.Rd0000644000176200001440000000150214567717130014762 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/logger.R \name{log_layout} \alias{log_layout} \title{Get or set log record layout} \usage{ log_layout(layout = NULL, namespace = "global", index = 1) } \arguments{ \item{layout}{function defining the structure of a log record, eg \code{\link{layout_simple}}, \code{\link{layout_glue}} or \code{\link{layout_glue_colors}}, \code{\link{layout_json}}, or generator functions such as \code{\link{layout_glue_generator}}, default NULL} \item{namespace}{logger namespace} \item{index}{index of the logger within the namespace} } \description{ Get or set log record layout } \examples{ \dontrun{ log_layout(layout_json()) log_info(42) } } \seealso{ \code{\link{logger}}, \code{\link{log_threshold}}, \code{\link{log_appender}} and \code{\link{log_formatter}} } logger/man/appender_file.Rd0000644000176200001440000000504013770625564015406 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/appenders.R \name{appender_file} \alias{appender_file} \title{Append log messages to a file} \usage{ appender_file( file, append = TRUE, max_lines = Inf, max_bytes = Inf, max_files = 1L ) } \arguments{ \item{file}{path} \item{append}{boolean passed to \code{cat} defining if the file should be overwritten with the most recent log message instead of appending} \item{max_lines}{numeric specifying the maximum number of lines allowed in a file before rotating} \item{max_bytes}{numeric specifying the maximum number of bytes allowed in a file before rotating} \item{max_files}{integer specifying the maximum number of files to be used in rotation} } \value{ function taking \code{lines} argument } \description{ Log messages are written to a file with basic log rotation: when max number of lines or bytes is defined to be other than \code{Inf}, then the log file is renamed with a \code{.1} suffix and a new log file is created. The renaming happens recursively (eg \code{logfile.1} renamed to \code{logfile.2}) until the specified \code{max_files}, then the oldest file (\code{logfile.{max_files-1}}) is deleted. } \examples{ \dontrun{ ## ########################################################################## ## simple example logging to a file t <- tempfile() log_appender(appender_file(t)) for (i in 1:25) log_info(i) readLines(t) ## ########################################################################## ## more complex example of logging to file ## rotated after every 3rd line up to max 5 files ## create a folder storing the log files t <- tempfile(); dir.create(t) f <- file.path(t, 'log') ## define the file logger with log rotation enabled log_appender(appender_file(f, max_lines = 3, max_files = 5L)) ## log 25 messages for (i in 1:25) log_info(i) ## see what was logged lapply(list.files(t, full.names = TRUE), function(t) { cat('\n##', t, '\n') cat(readLines(t), sep = '\n') }) ## enable internal logging to see what's actually happening in the logrotate steps log_threshold(TRACE, namespace = '.logger') ## run the above commands again } } \seealso{ This is generator function for \code{\link{log_appender}}, for alternatives, see eg \code{\link{appender_console}}, \code{\link{appender_tee}}, \code{\link{appender_slack}}, \code{\link{appender_pushbullet}}, \code{\link{appender_telegram}}, \code{\link{appender_syslog}}, \code{\link{appender_kinesis}} and \code{\link{appender_async}} for evaluate any \code{\link{log_appender}} function in a background process. } logger/man/layout_simple.Rd0000644000176200001440000000324513770625564015504 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/layouts.R \name{layout_simple} \alias{layout_simple} \title{Format a log record by concatenating the log level, timestamp and message} \usage{ layout_simple( level, msg, namespace = NA_character_, .logcall = sys.call(), .topcall = sys.call(-1), .topenv = parent.frame() ) } \arguments{ \item{level}{log level, see \code{\link{log_levels}} for more details} \item{msg}{string message} \item{namespace}{string referring to the \code{logger} environment / config to be used to override the target of the message record to be used instead of the default namespace, which is defined by the R package name from which the logger was called, and falls back to a common, global namespace.} \item{.logcall}{the logging call being evaluated (useful in formatters and layouts when you want to have access to the raw, unevaluated R expression)} \item{.topcall}{R expression from which the logging function was called (useful in formatters and layouts to extract the calling function's name or arguments)} \item{.topenv}{original frame of the \code{.topcall} calling function where the formatter function will be evaluated and that is used to look up the \code{namespace} as well via \code{logger:::top_env_name}} } \value{ character vector } \description{ Format a log record by concatenating the log level, timestamp and message } \seealso{ This is a \code{\link{log_layout}}, for alternatives, see \code{\link{layout_blank}}, \code{\link{layout_glue}}, \code{\link{layout_glue_colors}}, \code{\link{layout_json}}, \code{\link{layout_json_parser}}, or generator functions such as \code{\link{layout_glue_generator}} } logger/man/log_messages.Rd0000644000176200001440000000071013455067437015257 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/hooks.R \name{log_messages} \alias{log_messages} \title{Injects a logger call to standard messages} \usage{ log_messages() } \description{ This function uses \code{trace} to add a \code{log_info} function call when \code{message} is called to log the informative messages with the \code{logger} layout and appender. } \examples{ \dontrun{ log_messages() message('hi there') } } logger/man/log_formatter.Rd0000644000176200001440000000142614567717130015455 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/logger.R \name{log_formatter} \alias{log_formatter} \title{Get or set log message formatter} \usage{ log_formatter(formatter = NULL, namespace = "global", index = 1) } \arguments{ \item{formatter}{function defining how R objects are converted into a single string, eg \code{\link{formatter_paste}}, \code{\link{formatter_sprintf}}, \code{\link{formatter_glue}}, \code{\link{formatter_glue_or_sprintf}}, \code{\link{formatter_logging}}, default NULL} \item{namespace}{logger namespace} \item{index}{index of the logger within the namespace} } \description{ Get or set log message formatter } \seealso{ \code{\link{logger}}, \code{\link{log_threshold}}, \code{\link{log_appender}} and \code{\link{log_layout}} } logger/man/log_separator.Rd0000644000176200001440000000271014567717130015447 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/helpers.R \name{log_separator} \alias{log_separator} \title{Logs a long line to stand out from the console} \usage{ log_separator( level = INFO, namespace = NA_character_, separator = "=", width = 80, .topcall = sys.call() ) } \arguments{ \item{level}{log level, see \code{\link{log_levels}} for more details} \item{namespace}{string referring to the \code{logger} environment / config to be used to override the target of the message record to be used instead of the default namespace, which is defined by the R package name from which the logger was called, and falls back to a common, global namespace.} \item{separator}{character to be used as a separator} \item{width}{max width of message -- longer text will be wrapped into multiple lines} \item{.topcall}{R expression from which the logging function was called (useful in formatters and layouts to extract the calling function's name or arguments)} } \description{ Logs a long line to stand out from the console } \examples{ log_separator() log_separator(ERROR, separator = '!', width = 60) log_separator(ERROR, separator = '!', width = 100) logger <- layout_glue_generator(format = '{node}/{pid}/{namespace}/{fn} {time} {level}: {msg}') log_layout(logger) log_separator(ERROR, separator = '!', width = 100) log_layout(layout_blank) log_separator(ERROR, separator = '!', width = 80) } \seealso{ \code{\link{log_with_separator}} } logger/man/delete_logger_index.Rd0000644000176200001440000000063214571077771016604 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/logger.R \name{delete_logger_index} \alias{delete_logger_index} \title{Delete an index from a logger namespace} \usage{ delete_logger_index(namespace = "global", index) } \arguments{ \item{namespace}{logger namespace} \item{index}{index of the logger within the namespace} } \description{ Delete an index from a logger namespace } logger/man/grapes-except-grapes.Rd0000644000176200001440000000163014570721542016630 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/try.R \name{\%except\%} \alias{\%except\%} \title{Try to evaluate an expressions and evaluate another expression on exception} \usage{ try \%except\% except } \arguments{ \item{try}{R expression} \item{except}{fallback R expression to be evaluated if \code{try} fails} } \description{ Try to evaluate an expressions and evaluate another expression on exception } \note{ Suppress log messages in the \code{except} namespace if you don't want to throw a \code{WARN} log message on the exception branch. } \examples{ everything \%except\% 42 everything <- '640kb' everything \%except\% 42 FunDoesNotExist(1:10) \%except\% sum(1:10) / length(1:10) FunDoesNotExist(1:10) \%except\% (sum(1:10) / length(1:10)) FunDoesNotExist(1:10) \%except\% MEAN(1:10) \%except\% mean(1:10) FunDoesNotExist(1:10) \%except\% (MEAN(1:10) \%except\% mean(1:10)) } logger/man/get_logger_meta_variables.Rd0000644000176200001440000000544714571164177017776 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/layouts.R \name{get_logger_meta_variables} \alias{get_logger_meta_variables} \title{Collect useful information about the logging environment to be used in log messages} \usage{ get_logger_meta_variables( log_level = NULL, namespace = NA_character_, .logcall = sys.call(), .topcall = sys.call(-1), .topenv = parent.frame() ) } \arguments{ \item{log_level}{log level as per \code{\link{log_levels}}} \item{namespace}{string referring to the \code{logger} environment / config to be used to override the target of the message record to be used instead of the default namespace, which is defined by the R package name from which the logger was called, and falls back to a common, global namespace.} \item{.logcall}{the logging call being evaluated (useful in formatters and layouts when you want to have access to the raw, unevaluated R expression)} \item{.topcall}{R expression from which the logging function was called (useful in formatters and layouts to extract the calling function's name or arguments)} \item{.topenv}{original frame of the \code{.topcall} calling function where the formatter function will be evaluated and that is used to look up the \code{namespace} as well via \code{logger:::top_env_name}} } \value{ list } \description{ Available variables to be used in the log formatter functions, eg in \code{\link{layout_glue_generator}}: \itemize{ \item levelr: log level as an R object, eg \code{\link{INFO}} \item level: log level as a string, eg \code{\link{INFO}} \item time: current time as \code{POSIXct} \item node: name by which the machine is known on the network as reported by \code{Sys.info} \item arch: machine type, typically the CPU architecture \item os_name: Operating System's name \item os_release: Operating System's release \item os_version: Operating System's version \item user: name of the real user id as reported by \code{Sys.info} \item pid: the process identification number of the R session \item node: name by which the machine is known on the network as reported by \code{Sys.info} \item r_version: R's major and minor version as a string \item ns: namespace usually defaults to \code{global} or the name of the holding R package of the calling the logging function \item ns_pkg_version: the version of \code{ns} when it's a package \item ans: same as \code{ns} if there's a defined \code{\link{logger}} for the namespace, otherwise a fallback namespace (eg usually \code{global}) \item topenv: the name of the top environment from which the parent call was called (eg R package name or \code{GlobalEnv}) \item call: parent call (if any) calling the logging function \item fn: function's (if any) name calling the logging function } } \seealso{ \code{\link{layout_glue_generator}} } logger/man/layout_glue.Rd0000644000176200001440000000352513770625564015150 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/layouts.R \name{layout_glue} \alias{layout_glue} \title{Format a log message with \code{glue}} \usage{ layout_glue( level, msg, namespace = NA_character_, .logcall = sys.call(), .topcall = sys.call(-1), .topenv = parent.frame() ) } \arguments{ \item{level}{log level, see \code{\link{log_levels}} for more details} \item{msg}{string message} \item{namespace}{string referring to the \code{logger} environment / config to be used to override the target of the message record to be used instead of the default namespace, which is defined by the R package name from which the logger was called, and falls back to a common, global namespace.} \item{.logcall}{the logging call being evaluated (useful in formatters and layouts when you want to have access to the raw, unevaluated R expression)} \item{.topcall}{R expression from which the logging function was called (useful in formatters and layouts to extract the calling function's name or arguments)} \item{.topenv}{original frame of the \code{.topcall} calling function where the formatter function will be evaluated and that is used to look up the \code{namespace} as well via \code{logger:::top_env_name}} } \value{ character vector } \description{ By default, this layout includes the log level of the log record as per \code{\link{log_levels}}, the current timestamp and the actual log message -- that you can override via calling \code{\link{layout_glue_generator}} directly. For colorized output, see \code{\link{layout_glue_colors}}. } \seealso{ This is a \code{\link{log_layout}}, for alternatives, see \code{\link{layout_blank}}, \code{\link{layout_simple}}, \code{\link{layout_glue_colors}}, \code{\link{layout_json}}, \code{\link{layout_json_parser}}, or generator functions such as \code{\link{layout_glue_generator}} } logger/man/log_appender.Rd0000644000176200001440000000205214567717130015244 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/logger.R \name{log_appender} \alias{log_appender} \title{Get or set log record appender function} \usage{ log_appender(appender = NULL, namespace = "global", index = 1) } \arguments{ \item{appender}{function delivering a log record to the destination, eg \code{\link{appender_console}}, \code{\link{appender_file}} or \code{\link{appender_tee}}, default NULL} \item{namespace}{logger namespace} \item{index}{index of the logger within the namespace} } \description{ Get or set log record appender function } \examples{ \dontrun{ ## change appender to "tee" that writes to the console and a file as well t <- tempfile() log_appender(appender_tee(t)) log_info(42) log_info(42:44) readLines(t) ## poor man's tee by stacking loggers in the namespace t <- tempfile() log_appender(appender_console) log_appender(appender_file(t), index = 2) log_info(42) readLines(t) } } \seealso{ \code{\link{logger}}, \code{\link{log_threshold}}, \code{\link{log_layout}} and \code{\link{log_formatter}} } logger/man/appender_stdout.Rd0000644000176200001440000000101313543310366015772 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/appenders.R \name{appender_stdout} \alias{appender_stdout} \title{Append log record to stdout} \usage{ appender_stdout(lines) } \arguments{ \item{lines}{character vector} } \description{ Append log record to stdout } \seealso{ This is a \code{\link{log_appender}}, for alternatives, see eg \code{\link{appender_console}}, \code{\link{appender_file}}, \code{\link{appender_tee}}, \code{\link{appender_slack}}, \code{\link{appender_pushbullet}} } logger/man/log_failure.Rd0000644000176200001440000000060413455067767015107 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/helpers.R \name{log_failure} \alias{log_failure} \title{Logs the error message to console before failing} \usage{ log_failure(expression) } \arguments{ \item{expression}{call} } \description{ Logs the error message to console before failing } \examples{ \dontrun{ log_failure('foobar') log_failure(foobar) } } logger/man/appender_async.Rd0000644000176200001440000000543214567717130015605 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/appenders.R \name{appender_async} \alias{appender_async} \title{Delays executing the actual appender function to the future in a background process to avoid blocking the main R session} \usage{ appender_async( appender, batch = 1, namespace = "async_logger", init = function() log_info("Background process started") ) } \arguments{ \item{appender}{a \code{\link{log_appender}} function with a \code{generator} attribute (TODO note not required, all fn will be passed if not)} \item{batch}{number of records to process from the queue at once} \item{namespace}{\code{logger} namespace to use for logging messages on starting up the background process} \item{init}{optional function to run in the background process that is useful to set up the environment required for logging, eg if the \code{appender} function requires some extra packages to be loaded or some environment variables to be set etc} } \value{ function taking \code{lines} argument } \description{ Delays executing the actual appender function to the future in a background process to avoid blocking the main R session } \note{ This functionality depends on the \pkg{txtq} and \pkg{callr} packages. The R session's temp folder is used for staging files (message queue and other forms of communication between the parent and child processes). } \examples{ \dontrun{ appender_file_slow <- function(file) { force(file) function(lines) { Sys.sleep(1) cat(lines, sep = '\n', file = file, append = TRUE) } } ## log what's happening in the background log_threshold(TRACE, namespace = 'async_logger') log_appender(appender_console, namespace = 'async_logger') ## start async appender t <- tempfile() log_info('Logging in the background to {t}') my_appender <- appender_async(appender_file_slow(file = t)) ## use async appender log_appender(my_appender) log_info('Was this slow?') system.time(for (i in 1:25) log_info(i)) readLines(t) Sys.sleep(10) readLines(t) ## check on the async appender (debugging, you will probably never need this) attr(my_appender, 'async_writer_queue')$count() attr(my_appender, 'async_writer_queue')$log() attr(my_appender, 'async_writer_process')$get_pid() attr(my_appender, 'async_writer_process')$get_state() attr(my_appender, 'async_writer_process')$poll_process(1) attr(my_appender, 'async_writer_process')$read() attr(my_appender, 'async_writer_process')$is_alive() attr(my_appender, 'async_writer_process')$read_error() } } \seealso{ This function is to be used with an actual \code{\link{log_appender}}, for example \code{\link{appender_console}}, \code{\link{appender_file}}, \code{\link{appender_tee}}, \code{\link{appender_pushbullet}}, \code{\link{appender_telegram}}, \code{\link{appender_syslog}} or \code{\link{appender_kinesis}}. } logger/man/as.loglevel.Rd0000644000176200001440000000063214570721542015016 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/levels.R \name{as.loglevel} \alias{as.loglevel} \title{Convert R object into a logger log-level} \usage{ as.loglevel(x) } \arguments{ \item{x}{string or integer} } \value{ pander log-level, e.g. \code{INFO} } \description{ Convert R object into a logger log-level } \examples{ as.loglevel(INFO) as.loglevel(400L) as.loglevel(400) } logger/man/skip_formatter.Rd0000644000176200001440000000150714570721542015636 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/formatters.R \name{skip_formatter} \alias{skip_formatter} \title{Skip the formatter function} \usage{ skip_formatter(message, ...) } \arguments{ \item{message}{character vector directly passed to the appender function in \code{\link{logger}}} \item{...}{should be never set} } \value{ character vector with \code{skip_formatter} attribute set to \code{TRUE} } \description{ Adds the \code{skip_formatter} attribute to an object so that logger will skip calling the formatter function(s). This is useful if you want to preprocess the log message with a custom function instead of the active formatter function(s). Note that the \code{message} should be a string, and \code{skip_formatter} should be the only input for the logging function to make this work. } logger/man/top_env_name.Rd0000644000176200001440000000067113406213410015243 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/utils.R \name{top_env_name} \alias{top_env_name} \title{Returns the name of the top level environment from which the logger was called} \usage{ top_env_name(.topenv = parent.frame()) } \arguments{ \item{.topenv}{call environment} } \value{ string } \description{ Returns the name of the top level environment from which the logger was called } \keyword{internal} logger/DESCRIPTION0000644000176200001440000000235514571643062013254 0ustar liggesusersType: Package Package: logger Authors@R: c( person("Gergely", "Daróczi", , "daroczig@rapporter.net", role = c("aut", "cre"), comment = c(ORCID = "0000-0003-3149-8537")), person("System1", role = c("fnd")) ) Title: A Lightweight, Modern and Flexible Logging Utility Description: Inspired by the the 'futile.logger' R package and 'logging' Python module, this utility provides a flexible and extensible way of formatting and delivering log messages with low overhead. Version: 0.3.0 Date: 2024-03-03 URL: https://daroczig.github.io/logger/ BugReports: https://github.com/daroczig/logger/issues Encoding: UTF-8 RoxygenNote: 7.3.1 License: AGPL-3 Imports: utils Suggests: glue, pander, jsonlite, crayon, slackr (>= 1.4.1), RPushbullet, telegram, testthat, covr, knitr, rmarkdown, devtools, roxygen2, parallel, rsyslog, shiny, callr, txtq, botor, R.utils, syslognet Enhances: logging, futile.logger, log4r VignetteBuilder: knitr Config/testthat/edition: 2 NeedsCompilation: no Packaged: 2024-03-05 14:10:49 UTC; daroczig Author: Gergely Daróczi [aut, cre] (), System1 [fnd] Maintainer: Gergely Daróczi Repository: CRAN Date/Publication: 2024-03-05 16:20:02 UTC logger/build/0000755000176200001440000000000014571623750012642 5ustar liggesuserslogger/build/vignette.rds0000644000176200001440000000071014571623750015177 0ustar liggesusersRKO1 3FyxScԄH;,%'qvŊᢇ|3=!)Zm WWI4Zz"(1p"ߠ8ٶ53\I@Lۺ?b3!E>)+\~=F0Gkm> PFoUL{2X [ 15hd?e^"Tz^۝+^y>b=)E~úb80JھX3}Ğdn"s9oBcjF$tc_s߃І?I{Y>?9W纳o9zqeRq7gT0#8bJ&v͂OjV?Jv^W\6@FNF70tӅDU&-l Wl6Xd&,uXaCglogger/tests/0000755000176200001440000000000014571623750012705 5ustar liggesuserslogger/tests/testthat/0000755000176200001440000000000014571643062014543 5ustar liggesuserslogger/tests/testthat/test-formatters.R0000644000176200001440000001446314571556264020050 0ustar liggesuserslibrary(logger) library(testthat) library(jsonlite) ## save current settings so that we can reset later formatter <- log_formatter() appender <- log_appender() context('formatters') everything <- 42 g <- function() { log_info("Hi {everything}") } f <- function() { log_info("Hi %s", everything) } log_formatter(formatter_glue) log_appender(appender_stdout) test_that('glue works', { expect_equal(formatter_glue("Hi"), "Hi") expect_equal(formatter_glue(" Hi"), " Hi") expect_equal(formatter_glue('1 + {1}'), '1 + 1') expect_equal(formatter_glue('{1:2}'), as.character(1:2)) expect_equal(formatter_glue('pi is {round(pi, 2)}'), 'pi is 3.14') expect_equal(formatter_glue("Hi {42}"), "Hi 42") expect_equal(formatter_glue("Hi {a}", a = 42), "Hi 42") expect_equal(formatter_glue("Hi {everything}"), "Hi 42") expect_equal(formatter_glue("Hi {1:2}"), paste("Hi", 1:2)) expect_output(do.call(logger, logger:::namespaces$global[[1]])(INFO, 42), '42') expect_output(do.call(logger, logger:::namespaces$global[[1]])(INFO, "Hi {everything}"), '42') expect_output(log_info("Hi {everything}"), '42') expect_output(log_warn("Hi {everything}"), '42') expect_output(g(), '42') log_appender(appender_void) expect_error(formatter_glue('malformed {')) expect_error(formatter_glue('malformed {{'), NA) ## disabled for https://github.com/atalv/azlogr/issues/35 ## expect_warning(formatter_glue(NULL)) ## expect_warning(log_info(NULL)) ## expect_warning(log_info(a = 42, b = "foobar")) log_appender(appender_stdout) }) log_formatter(formatter_glue_safe) test_that('glue_safe works', { expect_equal(formatter_glue_safe("Hi"), "Hi") expect_equal(formatter_glue_safe(" Hi"), " Hi") expect_equal(formatter_glue_safe("Hi {a}", a = 42), "Hi 42") expect_equal(formatter_glue_safe("Hi {everything}"), "Hi 42") expect_output(log_info("Hi {everything}"), '42') expect_output(log_warn("Hi {everything}"), '42') expect_output(g(), '42') expect_error(formatter_glue_safe("Hi {42}")) expect_error(formatter_glue_safe('malformed {')) expect_error(formatter_glue_safe('malformed {{'), NA) }) log_formatter(formatter_sprintf) test_that('sprintf works', { expect_equal(formatter_sprintf("Hi"), "Hi") expect_equal(formatter_sprintf("Hi %s", 42), "Hi 42") expect_equal(formatter_sprintf("Hi %s", everything), "Hi 42") expect_equal(formatter_sprintf("Hi %s", 1:2), paste("Hi", 1:2)) expect_equal(formatter_sprintf('1 + %s', 1), '1 + 1') expect_equal(formatter_sprintf('=>%2i', 2), '=> 2') expect_equal(formatter_sprintf('%s', 1:2), as.character(1:2)) expect_equal(formatter_sprintf('pi is %s', round(pi, 2)), 'pi is 3.14') expect_equal(formatter_sprintf('pi is %1.2f', pi), 'pi is 3.14') expect_error(formatter_sprintf('%s and %i', 1)) expect_equal(formatter_sprintf('%s and %i', 1, 2), '1 and 2') expect_output(log_info("Hi %s", everything), '42') expect_output(f(), '42') }) result <- c( "Hi foo, did you know that 2*4=8?", "Hi bar, did you know that 2*4=8?") test_that('glue+sprintf works', { expect_equal(formatter_glue_or_sprintf("Hi ", "{c('foo', 'bar')}, did you know that 2*4={2*4}?"), result) expect_equal(formatter_glue_or_sprintf("Hi {c('foo', 'bar')}, did you know that 2*4={2*4}?"), result) expect_equal(formatter_glue_or_sprintf("Hi {c('foo', 'bar')}, did you know that 2*4=%s?", 2*4), result) expect_equal(formatter_glue_or_sprintf("Hi %s, did you know that 2*4={2*4}?", c('foo', 'bar')), result) expect_equal(formatter_glue_or_sprintf("Hi %s, did you know that 2*4=%s?", c('foo', 'bar'), 2*4), result) expect_equal(formatter_glue_or_sprintf('%s and %i'), '%s and %i') expect_equal(formatter_glue_or_sprintf('%s and %i', 1), '%s and %i') expect_equal(formatter_glue_or_sprintf('fun{fun}'), 'fun{fun}') for (fn in c(formatter_sprintf, formatter_glue_or_sprintf)) { log_formatter(fn) log_appender(appender_void) expect_error(log_info(character(0)), NA) log_appender(appender_stdout) expect_output(log_info(character(0)), 'INFO') } }) test_that('formatter_logging works', { log_formatter(formatter_logging) expect_output(log_info('42'), '42') expect_output(log_info(42), '42') expect_output(log_info(4+2), '4 \\+ 2') expect_output(log_info(4+2), '6') expect_output(log_info('foo %s', 'bar'), 'foo bar') expect_output(log_info(12, 100+100, 2*2), '12') expect_output(log_info(12, 100+100, 2*2), '100 \\+ 100') expect_output(log_info(12, 100+100, 2*2), '200') expect_output(log_info(12, 100+100, 2*2), '2 \\* 2') expect_output(log_info(12, 100+100, 2*2), '4') }) test_that('special chars in the text work', { expect_equal(formatter_glue('JSON: {toJSON(1:4)}'), 'JSON: [1,2,3,4]') expect_equal(formatter_glue('JSON: {toJSON(iris[1:2, ], auto_unbox = TRUE)}'), 'JSON: [{"Sepal.Length":5.1,"Sepal.Width":3.5,"Petal.Length":1.4,"Petal.Width":0.2,"Species":"setosa"},{"Sepal.Length":4.9,"Sepal.Width":3,"Petal.Length":1.4,"Petal.Width":0.2,"Species":"setosa"}]') # nolint expect_output(log_info('JSON: {toJSON(1:4)}'), '[1,2,3,4]') expect_output(log_info('JSON: {toJSON(iris[1:2, ], auto_unbox = TRUE)}'), '[{"Sepal.Length":5.1,"Sepal.Width":3.5,"Petal.Length":1.4,"Petal.Width":0.2,"Species":"setosa"},{"Sepal.Length":4.9,"Sepal.Width":3,"Petal.Length":1.4,"Petal.Width":0.2,"Species":"setosa"}]') # nolint }) log_formatter(formatter_pander) test_that('pander formatter', { expect_output(log_info(42), '_42_') expect_output(log_info('42'), '42') expect_output(log_info(head(iris)), 'Sepal.Length') expect_output(log_info(lm(hp ~ wt, mtcars)), 'Fitting linear model') }) ## cleanup rm(everything) rm(f) log_formatter(formatter_paste) test_that('paste formatter in actual logs', { expect_output(log_info('hi', 5), 'hi 5') }) log_formatter(formatter_glue) test_that('skip formatter', { expect_output(log_info(skip_formatter('hi {pi}')), 'hi \\{pi\\}') expect_error(log_info(skip_formatter(mtcars))) expect_error(log_info(skip_formatter('hi {x}', x = 4))) }) log_formatter(formatter_json) test_that('skip formatter', { expect_output(log_info(skip_formatter('hi {pi}')), 'hi \\{pi\\}') expect_output(log_info(x = 1), '\\{"x":1\\}') }) log_formatter(formatter) log_appender(appender) logger/tests/testthat/test-CRANSKIP-hooks.R0000644000176200001440000000631414571556260020205 0ustar liggesuserslibrary(logger) library(testthat) context('hooks') eval_outside <- function(expr) { t <- tempfile() on.exit(unlink(t)) cat('library(logger); log_messages(); log_warnings(); log_errors();', file = t) cat(expr, file = t, append = TRUE, sep = '\n') paste( suppressWarnings(system(paste('$R_HOME/bin/Rscript', t, '2>&1'), intern = TRUE)), collapse = '\n') } test_that('log_messages', { expect_match(eval_outside('message(42)'), 'INFO') if (R.Version()$os == 'linux-gnu') { expect_match(eval_outside('system("echo 42", invisible = TRUE)'), 'INFO') } }) test_that('log_warnings', { expect_match(eval_outside('warning(42)'), 'WARN') if (R.Version()$major >= 4) { expect_match(eval_outside('log(-1)'), 'WARN') } }) test_that('log_errors', { expect_match(eval_outside('stop(42)'), 'ERROR') if (R.Version()$major >= 4) { expect_match(eval_outside('foobar'), 'ERROR') expect_match(eval_outside('f<-function(x) {42 * "foobar"}; f()'), 'ERROR') } }) test_that('shiny input initialization is detected', { obs <- eval_outside(" .globals <- shiny:::.globals .globals$appState <- new.env(parent = emptyenv()) server <- function(input, output, session) { logger::log_shiny_input_changes(input) } shiny::testServer(server, {}) " ) exp <- "INFO \\[[0-9: \\-]+\\] Default Shiny inputs initialized" expect_match(obs, exp) }) test_that('shiny input initialization is detected with different log-level', { obs <- eval_outside(" .globals <- shiny:::.globals .globals$appState <- new.env(parent = emptyenv()) server <- function(input, output, session) { logger::log_shiny_input_changes(input, level = logger::ERROR) } shiny::testServer(server, {}) " ) exp <- "ERROR \\[[0-9: \\-]+\\] Default Shiny inputs initialized" expect_match(obs, exp) }) test_that('shiny input change is detected', { obs <- eval_outside(" .globals <- shiny:::.globals .globals$appState <- new.env(parent = emptyenv()) server <- function(input, output, session) { logger::log_shiny_input_changes(input) x <- shiny::reactive(input$a) } shiny::testServer(server, { session$setInputs(a = 2) }) " ) exp <- "INFO \\[[0-9: \\-]+\\] Shiny input change detected on a: NULL -> 2" expect_match(obs, exp) }) test_that('shiny input change is logged with different level', { obs <- eval_outside(" .globals <- shiny:::.globals .globals$appState <- new.env(parent = emptyenv()) server <- function(input, output, session) { logger::log_shiny_input_changes(input, level = logger::ERROR) x <- shiny::reactive(input$a) } shiny::testServer(server, { session$setInputs(a = 2) }) " ) exp <- "ERROR \\[[0-9: \\-]+\\] Shiny input change detected on a: NULL -> 2" expect_match(obs, exp) }) logger/tests/testthat/test-logger.R0000644000176200001440000001414014571556260017125 0ustar liggesuserslibrary(logger) library(testthat) ## save current settings so that we can reset later threshold <- log_threshold() appender <- log_appender() layout <- log_layout() context('loggers') log_appender(appender_stdout) log_threshold(WARN) test_that('log levels', { expect_output(log_fatal('foo'), 'FATAL.*foo') expect_output(log_error('foo'), 'ERROR.*foo') expect_output(log_warn('foo'), 'WARN.*foo') expect_output(log_success('foo'), NA) expect_output(log_info('foo'), NA) expect_output(log_debug('foo'), NA) expect_output(log_trace('foo'), NA) expect_output(log_level('ERROR', 'foo'), 'ERROR.*foo') expect_output(log_level(ERROR, 'foo'), 'ERROR.*foo') expect_output(log_level(as.loglevel(ERROR), 'foo'), 'ERROR.*foo') expect_output(log_level(as.loglevel('ERROR'), 'foo'), 'ERROR.*foo') expect_output(log_level(as.loglevel(200L), 'foo'), 'ERROR.*foo') expect_output(log_level('TRACE', 'foo'), NA) expect_output(log_level(TRACE, 'foo'), NA) expect_output(log_level(as.loglevel(TRACE), 'foo'), NA) expect_output(log_level(as.loglevel('TRACE'), 'foo'), NA) expect_output(log_level(as.loglevel(600L), 'foo'), NA) }) log_threshold(OFF) test_that('log levels - OFF', { expect_output(log_fatal('foo'), NA) expect_output(log_error('foo'), NA) expect_output(log_warn('foo'), NA) expect_output(log_success('foo'), NA) expect_output(log_info('foo'), NA) expect_output(log_debug('foo'), NA) expect_output(log_trace('foo'), NA) }) log_threshold(TRACE) test_that('log thresholds', { expect_output(log_fatal('foo'), 'FATAL.*foo') expect_output(log_error('foo'), 'ERROR.*foo') expect_output(log_warn('foo'), 'WARN.*foo') expect_output(log_success('foo'), 'SUCCESS.*foo') expect_output(log_info('foo'), 'INFO.*foo') expect_output(log_debug('foo'), 'DEBUG.*foo') expect_output(log_trace('foo'), 'TRACE.*foo') }) log_threshold(WARN) test_that('with log thresholds', { expect_output(with_log_threshold(log_fatal('foo'), threshold = TRACE), 'FATAL.*foo') expect_output(with_log_threshold(log_error('foo'), threshold = TRACE), 'ERROR.*foo') expect_output(with_log_threshold(log_error('foo'), threshold = FATAL), NA) expect_output(with_log_threshold(log_error('foo'), threshold = INFO), 'ERROR.*foo') expect_output(with_log_threshold(log_debug('foo'), threshold = INFO), NA) }) log_layout(layout_glue_generator('{level} {msg}')) log_threshold(TRACE) test_that('simple glue layout with no threshold', { expect_equal(capture.output(log_fatal('foobar')), 'FATAL foobar') expect_equal(capture.output(log_error('foobar')), 'ERROR foobar') expect_equal(capture.output(log_warn('foobar')), 'WARN foobar') expect_equal(capture.output(log_info('foobar')), 'INFO foobar') expect_equal(capture.output(log_debug('foobar')), 'DEBUG foobar') expect_equal(capture.output(log_trace('foobar')), 'TRACE foobar') }) log_threshold(INFO) test_that('simple glue layout with threshold', { expect_equal(capture.output(log_fatal('foobar')), 'FATAL foobar') expect_equal(capture.output(log_error('foobar')), 'ERROR foobar') expect_equal(capture.output(log_warn('foobar')), 'WARN foobar') expect_equal(capture.output(log_info('foobar')), 'INFO foobar') expect_equal(capture.output(log_debug('foobar')), character()) expect_equal(capture.output(log_trace('foobar')), character()) }) test_that('namespaces', { log_threshold(ERROR, namespace = 'custom') expect_output(log_fatal('foobar', namespace = 'custom'), 'FATAL foobar') expect_output(log_error('foobar', namespace = 'custom'), 'ERROR foobar') expect_output(log_info('foobar', namespace = 'custom'), NA) expect_output(log_debug('foobar', namespace = 'custom'), NA) log_threshold(INFO, namespace = 'custom') expect_output(log_info('foobar', namespace = 'custom'), 'INFO foobar') expect_output(log_debug('foobar', namespace = 'custom'), NA) log_threshold(TRACE, namespace = log_namespaces()) expect_output(log_debug('foobar', namespace = 'custom'), 'DEBUG foobar') log_threshold(INFO) }) test_that('simple glue layout with threshold directly calling log', { expect_equal(capture.output(log_level(FATAL, 'foobar')), 'FATAL foobar') expect_equal(capture.output(log_level(ERROR, 'foobar')), 'ERROR foobar') expect_equal(capture.output(log_level(WARN, 'foobar')), 'WARN foobar') expect_equal(capture.output(log_level(INFO, 'foobar')), 'INFO foobar') expect_equal(capture.output(log_level(DEBUG, 'foobar')), character()) expect_equal(capture.output(log_level(TRACE, 'foobar')), character()) }) test_that('built in variables: pid', { log_layout(layout_glue_generator('{pid}')) expect_equal(capture.output(log_info('foobar')), as.character(Sys.getpid())) }) test_that('built in variables: fn and call', { log_layout(layout_glue_generator('{fn} / {call}')) f <- function() log_info('foobar') expect_output(f(), 'f / f()') g <- function() f() expect_output(g(), 'f / f()') g <- f expect_output(g(), 'g / g()') }) test_that('built in variables: namespace', { log_layout(layout_glue_generator('{ns}')) expect_output(log_info('bar', namespace = 'foo'), 'foo') log_layout(layout_glue_generator('{ans}')) expect_output(log_info('bar', namespace = 'foo'), 'global') }) test_that('print.level', { expect_equal(capture.output(print(INFO)), 'Log level: INFO') }) test_that('config setter called from do.call', { t <- tempfile() expect_error(do.call(log_appender, list(appender_file(t))), NA) log_info(42) expect_length(readLines(t), 1) expect_error(do.call(log_threshold, list(ERROR)), NA) log_info(42) expect_length(readLines(t), 1) expect_error(do.call(log_threshold, list(INFO)), NA) log_info(42) expect_length(readLines(t), 2) expect_error(do.call(log_layout, list(formatter_paste)), NA) log_info(42) expect_length(readLines(t), 3) unlink(t) }) test_that('providing log_level() args to wrappers diretly is OK', { expect_silent(log_info('{Sepal.Length}', .topenv = iris)) }) ## reset settings log_threshold(threshold) log_layout(layout) log_appender(appender) logger/tests/testthat/test-layout.R0000644000176200001440000000433214571162532017160 0ustar liggesuserslibrary(logger) library(testthat) library(jsonlite) ## save current settings so that we can reset later layout <- log_layout() appender <- log_appender() log_appender(appender_stdout) context('layouts') log_layout(layout_blank) test_that('blank layout', { expect_output(log_info('foobar'), 'foobar') expect_equal(capture.output(log_info('foobar')), 'foobar') }) log_layout(layout_glue_colors) test_that('colorized layout', { expect_output(log_info('foobar'), 'INFO') expect_output(log_info('foobar'), 'foobar') expect_output(log_error('foobar'), 'ERROR') expect_output(log_error('foobar'), 'foobar') }) test_that('metavars', { log_layout(layout_glue_generator(format = '{level} {ans} {fn} {msg}')) expect_output((function(){log_info(42)})(), 'INFO') expect_output((function(){log_warn(42)})(), 'WARN') expect_output((function(){log_info(42)})(), 'log_info') log_layout(layout_glue_generator(format = '{fn}')) expect_output({fun42<-function(){log_info(42)};fun42();rm(fun42)}, 'fun42') }) log_layout(layout_json()) test_that('JSON layout', { expect_equal(fromJSON(capture.output(log_info('foobar')))$level, 'INFO') expect_equal(fromJSON(capture.output(log_info('foobar')))$msg, 'foobar') }) log_layout(layout_json_parser(fields = c())) test_that('JSON parser layout', { expect_output(log_info(skip_formatter('{"x": 4}')), '\\{"x":4\\}') expect_equal(capture.output(log_info(skip_formatter('{"x": 4}'))), '{"x":4}') }) test_that('must throw errors', { expect_error(layout_simple(FOOBAR)) expect_error(layout_simple(42)) expect_error(layout_simple(msg = 'foobar')) expect_error(layout_glue(FOOBAR)) expect_error(layout_glue(42)) expect_error(layout_glue(msg = 'foobar')) expect_error(layout_glue(level = 53, msg = 'foobar')) }) log_layout(layout_logging) test_that('logging layout', { expect_output(log_level(INFO, 'foo', namespace = 'bar'), 'INFO:bar:foo') expect_output(log_info('foobar'), 'INFO') expect_output(log_info('foo', namespace = 'bar'), 'foo') expect_output(log_info('foo', namespace = 'bar'), 'bar') expect_output(log_info('foo', namespace = 'bar'), 'INFO:bar:foo') }) ## reset settings log_layout(layout) log_appender(appender) logger/tests/testthat/test-helpers.R0000644000176200001440000000232414567717130017311 0ustar liggesuserslibrary(logger) library(testthat) ## save current settings so that we can reset later appender <- log_appender() log_appender(appender_stdout) context('helpers') test_that('separator', { original_layout <- log_layout() log_layout(layout_blank) expect_output(log_separator(), '={80,80}') log_layout(original_layout) expect_output(log_separator(separator = '-'), '---') expect_output(log_separator(), 'INFO') expect_output(log_separator(WARN), 'WARN') }) test_that('tictoc', { expect_output(log_tictoc(), 'timer') }) test_that('log with separator', { expect_output(log_with_separator(42), '===') logger <- layout_glue_generator(format = '{node}/{pid}/{namespace}/{fn} {time} {level}: {msg}') layout_original <- log_layout() log_layout(logger) expect_output(log_with_separator('Boo!', level = FATAL, width = 120), width = 120) log_layout(layout_original) }) test_that('log failure', { expect_output(log_failure("foobar"), NA) expect_output(try(log_failure(foobar), silent = TRUE), 'ERROR.*foobar') expect_error(log_failure('foobar'), NA) expect_match(capture.output(expect_error(log_failure(foobar))), 'not found') }) ## reset settings log_appender(appender) logger/tests/testthat/test-eval.R0000644000176200001440000000215313543310366016567 0ustar liggesuserslibrary(logger) library(testthat) ## save current settings so that we can reset later layout <- log_layout() threshold <- log_threshold() appender <- log_appender() log_layout(layout_glue_generator('{level} {msg}')) log_appender(appender_stdout) context('log_eval') test_that('single line', { expect_output(log_eval(4, INFO), sprintf("INFO %s => %s", shQuote(4), shQuote(4))) }) test_that('multi line', { expect_output(log_eval(4, INFO, multiline = TRUE), "Running expression") expect_output(log_eval(4, INFO, multiline = TRUE), "Results:") expect_output(log_eval(4, INFO, multiline = TRUE), "INFO 4") }) test_that('invisible return', { expect_output(log_eval(require(logger), INFO), sprintf("INFO %s => %s", shQuote('require\\(logger\\)'), shQuote(TRUE))) }) log_threshold(TRACE) test_that('lower log level', { expect_output(log_eval(4), sprintf("TRACE %s => %s", shQuote(4), shQuote(4))) }) ## reset settings log_threshold(threshold) log_layout(layout) log_appender(appender) logger/tests/testthat/test-CRANSKIP-appenders.R0000644000176200001440000000115514567717130021041 0ustar liggesuserslibrary(logger) library(testthat) ## save current settings so that we can reset later threshold <- log_threshold() layout <- log_layout() appender <- log_appender() context('CRAN skip: async appender') test_that('async logging', { t <- tempfile() my_appender <- appender_async(appender_file(file = t)) log_appender(my_appender) log_layout(layout_blank) for (i in 1:5) log_info(i) Sys.sleep(0.25) expect_equal(readLines(t)[1], '1') expect_equal(length(readLines(t)), 5) unlink(t) rm(t) }) ## reset settings log_threshold(threshold) log_layout(layout) log_appender(appender) logger/tests/testthat/test-appender.R0000644000176200001440000000371613571726503017451 0ustar liggesuserslibrary(logger) library(testthat) ## save current settings so that we can reset later threshold <- log_threshold() layout <- log_layout() appender <- log_appender() context('appenders') test_that('append to file', { log_layout(layout_glue_generator('{level} {msg}')) log_threshold(TRACE) t <- tempfile() log_appender(appender_file(t)) log_info('foobar') log_info('{1:2}') expect_equal(length(readLines(t)), 3) expect_equal(readLines(t)[1], 'INFO foobar') expect_equal(readLines(t)[3], 'INFO 2') unlink(t) rm(t) }) test_that('overwrite file', { log_layout(layout_glue_generator('{level} {msg}')) log_threshold(TRACE) t <- tempfile() log_appender(appender_file(t, append = FALSE)) log_info('foobar') log_info('{1:2}') expect_equal(length(readLines(t)), 2) expect_equal(readLines(t), c('INFO 1', 'INFO 2')) log_info('42') expect_equal(length(readLines(t)), 1) expect_equal(readLines(t), 'INFO 42') unlink(t) rm(t) }) test_that('append to file + print to console', { t <- tempfile() log_appender(appender_tee(t)) expect_equal(capture.output(log_info('foobar'), type = 'message'), 'INFO foobar') devnull <- capture.output(log_info('{1:2}'), type = 'message') expect_equal(length(readLines(t)), 3) expect_equal(readLines(t)[1], 'INFO foobar') unlink(t) rm(t) }) test_that('logrotate', { log_layout(layout_glue_generator('{msg}')) log_threshold(TRACE) t <- tempfile() dir.create(t) f <- file.path(t, 'log') log_appender(appender_file(f, max_lines = 2, max_files = 5L)) for (i in 1:24) log_info(i) expect_equal(length(readLines(f)), 2) expect_equal(length(list.files(t)), 5) expect_equal(readLines(f), c('23', '24')) log_info('42') expect_equal(length(readLines(f)), 1) expect_equal(readLines(f), '42') unlink(t) rm(t) }) ## reset settings log_threshold(threshold) log_layout(layout) log_appender(appender) logger/tests/testthat/test-return.R0000644000176200001440000000157414571077771017201 0ustar liggesuserslibrary(logger) library(testthat) ## save current settings so that we can reset later formatter <- log_formatter() appender <- log_appender() context('return value') t <- tempfile() log_appender(appender_file(t)) glue_or_sprintf_result <- c( "Hi foo, did you know that 2*4=8?", "Hi bar, did you know that 2*4=8?") test_that("return value is formatted string", { log_formatter(formatter_glue) expect_equal(log_info('pi is {round(pi, 2)}')[[1]]$message, 'pi is 3.14') expect_match(log_info('pi is {round(pi, 2)}')[[1]]$record, 'INFO [[0-9: -]*] pi is 3.14') log_formatter(formatter_paste, index = 2) expect_equal(log_info('pi is {round(pi, 2)}')[[1]]$message, 'pi is 3.14') expect_equal(log_info('pi is {round(pi, 2)}')[[2]]$message, 'pi is {round(pi, 2)}') }) ## reset settings unlink(t) delete_logger_index(index = 2) log_formatter(formatter) log_appender(appender) logger/tests/testthat/test-CRANSKIP-helpers.R0000644000176200001440000000147313571735644020531 0ustar liggesuserslibrary(logger) library(testthat) context('CRAN skip: helpers') test_that('tictoc', { expect_match(capture.output(log_tictoc(), type = 'message'), 'timer tic 0 secs') ## let time pass a bit Sys.sleep(0.01) expect_match(capture.output(log_tictoc(), type = 'message'), 'timer toc') capture.output(expect_silent(log_tictoc()), type = 'message') }) test_that('log with separator', { expect_output( cat(system("$R_HOME/bin/Rscript -e 'logger::log_with_separator(42)' 2>&1", intern = TRUE)), '===') expect_output( cat(system("$R_HOME/bin/Rscript -e 'logger::log_with_separator(42)' 2>&1", intern = TRUE)), '42') expect_output( cat(system("$R_HOME/bin/Rscript -e 'logger::log_with_separator(42, separator = \"|\")' 2>&1", intern = TRUE)), '|||||') }) logger/tests/testthat/test-utils.R0000644000176200001440000000310714570721542017003 0ustar liggesuserslibrary(logger) library(testthat) ## save current settings so that we can reset later appender <- log_appender() log_appender(appender_stdout) context('utils') test_that('fail_on_missing_package', { expect_error(fail_on_missing_package('logger'), NA) expect_error(fail_on_missing_package('logger', '9.9.9')) expect_error(fail_on_missing_package('an.R.package-that-doesNotExists')) }) test_that('except helper', { expect_equal(FunDoesNotExist(1:10) %except% sum(1:10) / length(1:10), 5.5) expect_output(FunDoesNotExist(1:10) %except% sum(1:10) / length(1:10), 'WARN') }) test_that('validate_log_level', { expect_equal(logger:::validate_log_level(ERROR), ERROR) expect_equal(logger:::validate_log_level('ERROR'), ERROR) expect_error(logger:::validate_log_level('FOOBAR'), 'log level') }) test_that('catch_base_log', { expect_true(nchar(logger:::catch_base_log(ERROR, NA_character_)) == 28) expect_true(nchar(logger:::catch_base_log(INFO, NA_character_)) == 27) layout_original <- log_layout() log_layout(layout_blank) expect_true(nchar(logger:::catch_base_log(INFO, NA_character_)) == 0) log_layout(layout_original) layout_original <- log_layout(namespace = 'TEMP') logger <- layout_glue_generator(format = '{namespace}/{fn} {level}: {msg}') log_layout(logger, namespace = 'TEMP') expect_true(nchar(logger:::catch_base_log(INFO, 'TEMP', .topcall = NA)) == 14) expect_true(nchar(logger:::catch_base_log(INFO, 'TEMP', .topcall = call('5char'))) == 17) log_layout(layout_original) }) ## reset settings log_appender(appender) logger/tests/testthat/test-CRANSKIP-logger-namespaces.R0000644000176200001440000000635013571735644022462 0ustar liggesuserslibrary(logger) library(testthat) ## save current settings so that we can reset later layout <- log_layout() appender <- log_appender() context('CRAN skip: logging from packages') test_that('called from package', { devtools::load_all(system.file('demo-packages/logger-tester-package', package = 'logger')) log_layout(layout_simple) log_appender(appender_stdout) expect_output(logger_tester_function(INFO, 'x = '), 'INFO') expect_output(logger_info_tester_function('everything = '), 'INFO') }) test_that('namespace in a remote R session to avoid calling from testthat', { t <- tempfile() cat(' library(logger) log_layout(layout_glue_generator("{ns} / {ans} / {topenv} / {fn} / {call}")) log_info("foobar")', file = t) expect_equal( system(paste('$R_HOME/bin/Rscript', t, '2>&1'), intern = TRUE), 'global / global / R_GlobalEnv / NA / NA') unlink(t) t <- tempfile() cat(' library(logger) log_layout(layout_glue_generator("{ns} / {ans} / {topenv} / {fn} / {call}")) f <- function() log_info("foobar") f()', file = t) expect_equal( system(paste('$R_HOME/bin/Rscript', t, '2>&1'), intern = TRUE), 'global / global / R_GlobalEnv / f / f()') unlink(t) t <- tempfile() cat(' library(logger) log_layout(layout_glue_generator("{ns} / {ans} / {topenv} / {fn} / {call}")) f <- function() log_info("foobar") g <- function() f() g()', file = t) expect_equal( system(paste('$R_HOME/bin/Rscript', t, '2>&1'), intern = TRUE), 'global / global / R_GlobalEnv / f / f()') unlink(t) t <- tempfile() cat(' library(logger) log_layout(layout_glue_generator("{ns} / {ans} / {topenv} / {fn} / {call}")) f <- function() log_info("foobar") g <- f g()', file = t) expect_equal( system(paste('$R_HOME/bin/Rscript', t, '2>&1'), intern = TRUE), 'global / global / R_GlobalEnv / g / g()') unlink(t) t <- tempfile() cat(' library(logger) log_layout(layout_glue_generator("{ns} / {ans} / {topenv} / {fn} / {call}")) log_appender(appender_stdout) devtools::load_all(system.file("demo-packages/logger-tester-package", package = "logger"), quiet = TRUE) logger_info_tester_function("foobar")', file = t) expect_equal( system(paste('$R_HOME/bin/Rscript', t), intern = TRUE), 'logger.tester / global / logger.tester / logger_info_tester_function / logger_info_tester_function("foobar")') unlink(t) t <- tempfile() cat(' library(logger) log_layout(layout_glue_generator("{ns} / {ans} / {topenv} / {fn} / {call}")) devtools::load_all(system.file("demo-packages/logger-tester-package", package = "logger"), quiet = TRUE) log_threshold(INFO, namespace = "logger.tester") log_appender(appender_stdout, namespace = "logger.tester") logger_info_tester_function("foobar")', file = t) expect_equal( system(paste('$R_HOME/bin/Rscript', t), intern = TRUE), 'logger.tester / logger.tester / logger.tester / logger_info_tester_function / logger_info_tester_function("foobar")') unlink(t) }) ## reset settings log_layout(layout) log_appender(appender) logger/tests/testthat.R0000644000176200001440000000024613571735644014677 0ustar liggesuserslibrary(testthat) library(logger) if (identical(Sys.getenv("NOT_CRAN"), "true")) { test_check('logger') } else { test_check('logger', filter = '^[a-z]*$') } logger/vignettes/0000755000176200001440000000000014571623750013553 5ustar liggesuserslogger/vignettes/anatomy.Rmd0000644000176200001440000000773514020006627015666 0ustar liggesusers--- title: "The Anatomy of a Log Request" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{The Anatomy of a Log Request} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r setup, include = FALSE} knitr::opts_chunk$set( collapse = TRUE, comment = "#>" ) library(logger) ## backup settings oldconf <- list( threshold = log_threshold(), layout = log_layout(), formatter = log_formatter(), appender = log_appender()) ## knitr not picking up stderr log_appender(appender_stdout) ``` ```{r loggerStructureImage, echo=FALSE, out.extra='style="width: 100%;" title="The structure of a logger and the flow of a log record request" alt="The structure of a logger and the flow of a log record request"'} knitr::include_graphics("logger_structure.svg") ``` To make a successful log record, `logger` requires the below components: - a **log request**, eg ```r log_error('Oops') ``` - including the log level (importance) of the record, which will be later used to decide if the log record is to be delivered or not: `ERROR` in this case - R objects to be logged: a simple string in this case, although it could be a character vector or any R object(s) that can be converted into a character vector by the `formatter` function - the **environment** and meta-information of the log request, eg actual timestamp, hostname of the computer, the name of the user running the R script, the pid of the R process, calling function and the actual call etc. ```{r} f <- function() get_logger_meta_variables(log_level = INFO) f() ``` - a **logger definition** to process the log request, including - log level `threshold`, eg `INFO`, which defines the minimum log level required for actual logging -- all log requests with lower log level will be thrown away ```{r} log_threshold() ERROR <= INFO log_error('Oops') ``` - `formatter` function, which takes R objects and converts those into actual log message(s) to be then passed to the `layout` function for the log record rendering -- such as `paste`, `sprintf`, `glue` or eg the below custom example: ```{r} formatter <- function(...) paste(..., collapse = ' ', sep = ' ') formatter(1:3, c('foo', 'bar')) ``` - `layout` function, which takes log message(s) and further information on the log request (such as timestamp, hostname, username, calling function etc) to render the actual log records eg human-readable text, JSON etc ```r library(jsonlite) layout <- function(level, msg) toJSON(level = level, timestamp = time, hostname = node, message = msg) layout(INFO, 'Happy Thursday!') #> {'level': 'INFO', 'timestamp': '1970-01-01 00:00:00', 'hostname': 'foobar', 'message': 'Happy Thursday!'} ``` - `appender` function, which takes fully-rendered log record(s) and delivers to somewhere, eg `stdout`, a file or a streaming service, eg ```{r} appender <- function(line) cat(line, '\n') appender('INFO [now] I am a log message') ``` Putting all these together (by explicitly setting the default config in the `global` namespace): ```{r} log_threshold(INFO) log_formatter(formatter_glue) log_layout(layout_simple) log_appender(appender_console) log_debug('I am a low level log message that will not be printed with a high log level threshold') log_warn('I am a higher level log message that is very likely to be printed') ``` Note, that all `logger` definitions and requests are tied to a logging namespace, and one log request might trigger multiple `logger` definitions as well (stacking). Find more information on these in the [Customizing the format and destination of log records](https://daroczig.github.io/logger/articles/customize_logger.html) vignette. ```{r cleanup, include = FALSE} ## restore settings log_threshold(oldconf$threshold) log_layout(oldconf$layout) log_formatter(oldconf$formatter) log_appender(oldconf$appender) ``` logger/vignettes/performance.Rmd0000644000176200001440000001224514020007524016504 0ustar liggesusers--- title: "Simple Benchmarks on logger Performance" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Simple Benchmarks on logger Performance} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- Although this has not been an important feature in the early development and overall design of this `logger` implementation, but with the default `?layout_simple` and `?formatter_glue`, it seems to perform pretty well when comparing with `futile.logger` and `logging` packages: ```r library(microbenchmark) ## fl library(futile.logger) t1 <- tempfile() flog.appender(appender.file(t1)) #> NULL ## lg library(logging) t2 <- tempfile() addHandler(writeToFile, file = t2) ## lr library(logger) #> The following objects are masked from ‘package:futile.logger’: DEBUG, ERROR, FATAL, INFO, TRACE, WARN t3 <- tempfile() log_appender(appender_file(t3)) string_fl <- function() flog.info('hi') string_lg <- function() loginfo('hi') string_lr <- function() log_info('hi') dynamic_fl <- function() flog.info('hi %s', 42) dynamic_lg <- function() loginfo('hi %s', 42) dynamic_lr <- function() log_info('hi {42}') vector_fl <- function() flog.info(paste('hi', 1:5)) vector_lg <- function() loginfo(paste('hi', 1:5)) vector_lr <- function() log_info('hi {1:5}') microbenchmark( string_fl(), string_lg(), string_lr(), vector_fl(), vector_lg(), vector_lr(), dynamic_fl(), dynamic_lg(), dynamic_lr(), times = 1e3) #> Unit: microseconds #> expr min lq mean median uq max neval #> string_fl() 1533.379 1650.7915 2510.5517 1759.9345 2885.4465 20835.425 1000 #> string_lg() 172.963 206.7615 315.6177 237.3150 335.3010 12738.735 1000 #> string_lr() 227.981 263.4715 390.7139 301.9045 409.0400 11926.974 1000 #> vector_fl() 1552.706 1661.7030 2434.0460 1766.7485 2819.5525 40892.197 1000 #> vector_lg() 198.338 234.2355 330.3268 266.7695 358.2510 9969.333 1000 #> vector_lr() 290.169 337.4730 592.0041 382.4335 537.5485 101946.435 1000 #> dynamic_fl() 1538.985 1663.7890 2564.6668 1782.1160 2932.7555 46039.686 1000 #> dynamic_lg() 188.213 226.5370 387.2470 255.1745 350.2015 60737.562 1000 #> dynamic_lr() 271.478 317.3350 486.1123 360.5815 483.5830 12070.936 1000 paste(t1, length(readLines(t1))) #> [1] "/tmp/Rtmp3Fp6qa/file7a8919485a36 7000" paste(t2, length(readLines(t2))) #> [1] "/tmp/Rtmp3Fp6qa/file7a89b17929f 7000" paste(t3, length(readLines(t3))) #> [1] "/tmp/Rtmp3Fp6qa/file289f24c88c41 7000" ``` So based on the above, non-comprehensive benchmark, it seems that when it comes to using the very base functionality of a logging engine, `logging` comes first, then `logger` performs with a bit of overhead due to using `glue` by default, then comes a bit slower `futile.logger`. On the other hand, there are some low-hanging fruits to improve performance, eg caching the `logger` function in the namespace, or using much faster message formatters (eg `paste0` or `sprintf` instead of `glue`) if needed -- like what `futile.logger` and `logging` are using instead of `glue`, so a quick `logger` comparison: ```r log_formatter(formatter_sprintf) string <- function() log_info('hi') dynamic <- function() log_info('hi %s', 42) vector <- function() log_info(paste('hi', 1:5)) microbenchmark(string(), vector(), dynamic(), times = 1e3) #> Unit: microseconds #> expr min lq mean median uq max neval cld #> string() 110.192 118.4850 148.5848 137.1825 152.7275 1312.903 1000 a #> vector() 129.111 136.8245 168.9274 155.5840 172.6795 3230.528 1000 b #> dynamic() 116.347 124.7620 159.1570 143.2140 160.5040 4397.640 1000 ab ``` Which suggests that `logger` is a pretty well-performing log framework. If you need even more performance with slower appenders, then asynchronous logging is your friend: passing the log messages to a reliable message queue, and a background process delivering those to the actual log destination in the background -- without blocking the main R process. This can be easily achieved in `logger` by wrapping any appender function in the `appender_async` function, such as: ```r ## demo log appender that's pretty slow appender_file_slow <- function(file) { force(file) function(lines) { Sys.sleep(1) cat(lines, sep = '\n', file = file, append = TRUE) } } ## create an async appender and start using it right away log_appender(appender_async(appender_file_slow(file = tempfile()))) async <- function() log_info('Was this slow?') microbenchmark(async(), times = 1e3) #> Unit: microseconds #> expr min lq mean median uq max neval #> async() 511.329 528.247 614.8694 558.2535 616.14 5018.731 1000 ``` Please note that although this ~0.6 ms is significantly higher than the ~0.15 ms we achieved above with the `sprintf` formatter, but this time we are calling an appender that would take 1 full second to deliver the log message (and not just printing to the console), so bringing that down to less than 1 millisecond is not too bad. If you need even higher throughput, then a custom `appender_async` without checking on the background process and potentially a faster message queue can bring this even below to 200 µs. logger/vignettes/write_custom_extensions.Rmd0000644000176200001440000001143413406213410021205 0ustar liggesusers--- title: "Writing Custom Logger Extensions" vignette: > %\VignetteIndexEntry{Writing Custom Logger Extensions} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r setup, include = FALSE} knitr::opts_chunk$set( collapse = TRUE, comment = "#>" ) ``` In this vignette I suppose that you are already familiar with [Customizing the format and destination of log records](https://daroczig.github.io/logger/articles/customize_logger.html) vignette. ## Custom log message formatter functions The log message formatter function should be able to take any number of R objects and convert those into a character vector that is safe to pass to the layout function. This transformer function can be as simple as calling `paste`, `glue` or `sprintf` or something complex as well, eg looking up user attributes of a user id mentioned in a log record etc. When writing a custom formatter function, it should also accept the original logging function call as `.logcall`, the parent call as `.topcall` and the log request's environment as `.topenv`, which can be used to find the relevant variables in your formatter. If you are writing such a function or a generator function returning a log message formatter function, please keep the actual call resulting in the formatter function (eg `match.call()` in the generator function or the quoted function call) recorded in the `generator` attribute of the function so that `?log_formatter` can pretty-print that instead of the unnamed function body. See the [`formatters.R`](https://github.com/daroczig/logger/blob/master/R/formatters.R) file for more examples. ## Custom log layout rendering functions The layout functions return the log record and take at least two arguments: - the log level and - a message already formatted as a string by a log message formatter function - the namespace (as `namespace`), calling function (as `.topcall`) and its environment (as `.topenv`) of the log request, and the actual log call (as `.logcall`) automatically collected by `?log_level` Such layout rendering function can be as simple as the default `?layout_simple`: ```r layout_simple <- function(level, msg, ...) { paste0(attr(level, 'level'), ' [', format(Sys.time(), "%Y-%m-%d %H:%M:%S"), '] ', msg) } ``` Or much more complex, eg looking up the hostname of the machine, public IP address etc and logging all these automatically with the message of the log request. Your easiest option to set up a custom layout is calling `?layout_glue_generator` that comes with a nice API being able to access a bunch of meta-information on the log request via `?get_logger_meta_variables`. For such example, see the [Customizing the format and destination of log records](https://daroczig.github.io/logger/articles/customize_logger.html) vignette. If you are writing such a function or a generator function returning a log message formatter function, please keep the actual call resulting in the formatter function (eg `match.call()` in the generator function or the quoted function call) recorded in the `generator` attribute of the function so that `?log_layout` can pretty-print that instead of the unnamed function body. See the [`layouts.R`](https://github.com/daroczig/logger/blob/master/R/layouts.R) file for more examples. ## Custom log record appenders The appender functions take log records and delivers those to the desired destination. This can be as simple as writing to the console (`?appender_console`) or to a local file (`?appender_file`), or delivering the log record via an API request to a remote service, streaming somewhere or sending a Slack message (`?appender_slack`). If you are writing such a function or a generator function returning a log message formatter function, please keep the actual call resulting in the formatter function (eg `match.call()` in the generator function or the quoted function call) recorded in the `generator` attribute of the function so that `?log_appender` can pretty-print that instead of the unnamed function body. See the [`appenders.R`](https://github.com/daroczig/logger/blob/master/R/appenders.R) file for more examples. An example for a custom appender delivering log messages to a database table: ```r ## the dbr package provides and easy and secure way of connecting to databased from R ## although if you want to minimize the dependencies, feel free to stick with DBI etc. library(dbr) ## init a persistent connection to the database using a yaml config in the background thanks to dbr ## NOTE that this is optional and a temporarily connection could be also used ## for higher reliability but lower throughput con <- db_connect('mydb') ## define custom function writing the log message to a table log_appender(function(lines) { db_append( df = data.frame(timestamp = Sys.time(), message = lines), table = 'logs', db = con) }) ``` logger/vignettes/logger_structure.svg0000644000176200001440000007352513770625564017714 0ustar liggesusers log_info('Hello, {name}') /dev/null logger log_threshold Throws away low level log requests log_formatter Converts the R objects to be logged into a character vector log_layout Render actual log record(s) from the message log_appender Deliver log record to destination message <- "Hello, world!" line <- "INFO [2019-09-19 04:58:13] {message}" cat(line, file = stderr()) get_logger_meta_variables Collect useful information about the environment of the log record request. INFO [2019-09-19 04:58:13] Hello, world! formatter_glue layout_simple appender_console logger/vignettes/migration.Rmd0000644000176200001440000003567014567717130016224 0ustar liggesusers--- title: "Migration Guide" vignette: > %\VignetteIndexEntry{Migration Guide} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{css, echo=FALSE} div.comparison { width: 49%; display: inline-block; vertical-align: top; } div.comparison p package { font-family: monospace; color: brown; } div#tocnav ul.nav li ul { padding-left: 10px; } ``` ```{r pkgchecks, echo = FALSE} ## check if other logger packages are available and exit if not for (pkg in c('futile.logger', 'logging', 'log4r')) { if (!requireNamespace(pkg, quietly = TRUE)) { warning(paste(pkg, 'package not available, so cannot build this vignette')) knitr::knit_exit() } } ``` ```{r setup, include = FALSE} knitr::opts_chunk$set( collapse = TRUE, comment = "#>" ) ## load the main package first library(logger) ## backup settings oldconf <- list( threshold = log_threshold(), layout = log_layout(), formatter = log_formatter(), appender = log_appender()) ## knitr not picking up stderr log_appender(appender_stdout) ``` In this vignette I suppose that you are already familiar with at least one of the [similar logging R packages](https://daroczig.github.io/logger/index.html#why-another-logging-r-package) and you are looking for suggestions on how to switch to `logger`. Before moving forward, please make sure that you have read the [Introduction to logger](https://daroczig.github.io/logger/articles/Intro.html), [The Anatomy of a Log Request](https://daroczig.github.io/logger/articles/anatomy.html) and [Customizing the Format and the Destination of a Log Record](https://daroczig.github.io/logger/articles/customize_logger.html) vignettes for a decent background on `logger`, and use this vignette as a quick-reference sheet to help you migrate from another package. ## futile.logger The `logger` package has been very heavily inspired by [`futile.logger`](https://cran.r-project.org/package=futile.logger) and have been using it for many years, also opened multiple pull requests to extend `futile.logger` before I decided to revamp my ideas into a new R package -- but there are still many common things between `futile.logger` and `logger`. ### Initialize Both packages comes with a default log engine / config, so it's enough to load the packages and those are ready to be used right away:
futile.logger ```{r} library(futile.logger) ```
logger ```{r} library(logger) ```
### Logging functions The most important change is that function names are by snake_case in `logger`, while `futile.logger` uses dot.separated expressions, and `futile.logger` prefixes function names by `flog` while `logger` uses `log` for that:
futile.logger ```{r} flog.info('hi there') flog.warn('watch out') ```
logger ```{r} log_info('hi there') log_warn('watch out') ```
As you can see above, the default layout of the messages is exactly the same. ### Log levels Regarding log levels, `futile.logger` bundles the default `log4j` levels (`TRACE`, `DEBUG`, `INFO`, `WARN`, `ERROR` and `FATAL`) that is extended by `SUCCESS` in `logger` as sometimes it's worth logging with a higher than `INFO` level that something succeeded. ### Log record layout Changing layouts is easy in both package, as you simply pass a layout function:
futile.logger ```{r} flog.layout(layout.json) flog.info('hi again') ```
logger ```{r} log_layout(layout_json()) log_info('hi again') ```
As you can see, `logger` provided a bit more information about the log request compared to `futile.logger`, but it's easy to change the list of fields to be used in the JSON -- see `?get_logger_meta_variables` for a complete list of variable names to be passed to `?layout_json`. `logger` also ships a lot more layouts, eg `?layout_glue_colors` or roll out your own via the `?layout_glue_generator` factory function. ```{r echo=FALSE, results='hide'} flog.layout(layout.simple) log_layout(layout_simple) ``` ### Log message formatting By default, `futile.logger` uses an `sprintf` formatter, while `logger` passes the objects to be logged to `glue`:
futile.logger ```{r} flog.info('hi') flog.info('hi %s', 84/2) flog.info(paste('hi', 84/2)) flog.info(glue::glue('hi {84/2}')) ```
logger ```{r} log_info('hi') log_info('hi {84/2}') log_formatter(formatter_sprintf) log_info('hi %s', 84/2) log_formatter(formatter_paste) log_info('hi', 84/2) ```
```{r echo=FALSE, results='hide'} log_formatter(formatter_glue) ``` It's easy to change this default formatter in both packages: use `flog.layout` handles this as well in `futile.logger`, while the formatter is separated from the layout function in `logger`, so check `?log_formatter` instead. `logger` ships with a bit more formatter functions, eg the default `?formatter_glue` and `?formatter_glue_or_sprintf` that tries to combine the best from both words. ### Log record destination Setting the destination of the log records works similarly in both packages, although he `logger` packages bundles a lot more options:
logging ```{r} t <- tempfile() flog.appender(appender.file(t)) flog.appender(appender.tee(t)) ```
logger ```{r} t <- tempfile() log_appender(appender_file(t)) log_appender(appender_tee(t)) ```
```{r echo=FALSE, results='hide'} flog.appender(appender.console) log_appender(appender_console) ``` ### Hierarchical logging and performance Both packages support using different logging namespaces and stacking loggers within the same namespace. Performance-wise, there's `logger` seems to be faster than `futile.logger`, but for more details, check the [Simple Benchmarks on Performance](https://daroczig.github.io/logger/articles/performance.html) vignette. ### Using `logger` as a drop-in-replacement of `futile.logger` `logger` has no hard requirements, so it's a very lightweight alternative of `futile.logger`. Although the function names are a bit different, and the message formatter also differs, but with some simple tweaks, `logger` can become an almost perfect drop-in-replacement of `futile.logger`: ```{r} library(logger) log_formatter(formatter_sprintf) flog.trace <- log_trace flog.debug <- log_debug flog.info <- log_info flog.warn <- log_warn flog.error <- log_error flog.info('Hello from logger in a futile.logger theme ...') flog.warn('... where the default log message formatter is %s', 'sprintf') ``` ## logging The [`logging`](https://cran.r-project.org/package=logging) package behaves very similarly to the Python logging module and so thus being pretty Pythonic, while `logger` tries to accommodate native R users' expectations -- so there are some minor nuances between the usage of the two packages. ### Initialize In `logging`, you have to initialize a logger first via `addHandler` or simply by calling `basicConfig`, which is not required in `logger` as it already comes with a default log config:
logging ```{r} library(logging) basicConfig() ```
logger ```{r} library(logger) ```
### Logging functions After initializing the logging engine, actual logging works similarly in the two packages -- with a bit different function names: * although `logging` uses mostly camelCase function names (eg `basicConfig`), but the logging functions are all lowercase without any separator, such as `loginfo` or `logwarn` * `logger` uses snake_case for the function names, such as `log_info` and `log_warn`
logging ```{r} loginfo('hi there') logwarn('watch out') ```
logger ```{r} log_info('hi there') log_warn('watch out') ```
As you can see above, the default layout of the log messages is somewhat different: * `logging` starts with the timestamp that is followed by the log level, optional namespace and the message separated by colons * `logger` starts with the log level, followed by the timestamp between brackets and then the message ### Log levels For the available log levels in `logging`, check `?loglevels`, and `?log_levels` for the same in `logger`:
logging ```{r} str(as.list(loglevels)) ```
logger ```{r} levels <- mget(ls( envir = environment(logger), pattern = '^[A-Z]'), envir = environment(logger)) str(levels[order(-as.numeric(levels))], give.attr = FALSE) ```
### Performance Performance-wise, there's no big difference between the two packages, but for more details, check the [Simple Benchmarks on Performance](https://daroczig.github.io/logger/articles/performance.html) vignette. ### Log record layout Getting and setting the layout of the log record should happen up-front in both packages:
logging ```{r} getLogger()[['handlers']]$basic.stdout$formatter ```
logger ```{r} log_layout() ```
`logger` provides multiple configurable layouts to fit the user's need, eg easily show the calling function of the lof request, the `pid` of the R process, name of the machine etc. or colorized outputs. See [Customizing the Format and the Destination of a Log Record](https://daroczig.github.io/logger/articles/customize_logger.html) vignette for more details. ### Log message formatting If you want to pass dynamic log messages to the log engines, you can do that via the hard-coded `sprintf` in the `logging` package, while you can set that on a namespaces basis in `logger`, which is by default using `glue`:
logging ```{r} loginfo('hi') loginfo('hi %s', 84/2) loginfo(paste('hi', 84/2)) loginfo(glue::glue('hi {84/2}')) ```
logger ```{r} log_info('hi') log_info('hi {84/2}') log_formatter(formatter_sprintf) log_info('hi %s', 84/2) log_formatter(formatter_paste) log_info('hi', 84/2) ```
For even better compatibility, there's also `?formatter_logging` that not only relies on `sprintf` when the first argument is a string, but will log the call and the result as well when the log object is an R expression: ```{r} log_formatter(formatter_logging) log_info('42') log_info(42) log_info(4+2) log_info('foo %s', 'bar') log_info(12, 1+1, 2 * 2) ``` ```{r echo=FALSE, results='hide'} log_formatter(formatter_glue) ``` ### Log record destination Setting the destination of the log records works similarly in both packages, although he `logger` packages bundles a lot more options:
logging ```r ?addHandler ?writeToConsole ?writeToFile ```
logger ```r ?log_appender ?appender_console ?appender_file ?appender_tee ?appender_slack ?appender_pushbullet ```
### Hierarchical logging Both packages support using different logging namespaces and stacking loggers within the same namespace. ### Using `logger` as a drop-in-replacement of `logging` `logger` has no hard requirements, so it's an adequate alternative of `logging`. Although the function names are a bit different, and the message formatter also differs, but with some simple tweaks, `logger` can become an almost perfect drop-in-replacement of `logging` -- although not all log levels (eg \code{FINE} and \code{CRITICAL}) are supported: ```{r} library(logger) log_formatter(formatter_logging) log_layout(layout_logging) logdebug <- log_debug loginfo <- log_info logwarn <- log_warn logerror <- log_error loginfo('Hello from logger in a logging theme ...') logwarn('... where the default log message formatter is %s', 'sprintf', namespace = 'foobar') ``` ## log4r The [`log4r`](https://cran.r-project.org/package=log4r) package provides an object-oriented approach for logging in R, so the logger object is to be passed to the log calls -- unlike in the `logger` package. ### Initialize So thus it's important to create a logging object in `log4r` before being able to log messages, while that's automatically done in `logger:
log4r ```{r} library(log4r) logger <- create.logger(logfile = stdout(), level = "INFO") ```
logger ```{r} library(logger) ```
Please note that in the background, `logger` does have a concept of logger objects, but that's behind the scene and the user does not have to specify / reference it. On the other hand, if you wish, you can do that via the `namespace` concept of `logger` -- more on that later. ### Logging functions While `logger` has a `log_` prefix for all logging functions, `log4r` has lowercase functions names referring to the log level, which takes a logging object and the log message:
log4r ```{r} info(logger, 'hi there') warn(logger, 'watch out') ```
logger ```{r} log_info('hi there') log_warn('watch out') ```
As you can see the default layout of the messages is a bit different in the two packages. ### Log levels Both packages are based on `log4j`, and `log4r` provides `DEBUG`, `INFO`, `WARN`, `ERROR` and `FATAL`, while `logger` also adds `TRACE` and `SUCCESS` on the top of these. To change the log level threshold, use the `level` function on the logging object in `log4r`, while it's `log_level` in `logger`. ### Log record layout and formatter functions The `log4r` provides a `logformat` argument in `create.logger` that can be used to override the default formatting, while `logger` provides formatter and layout functions for a flexible log record design. ### Log record destination By default, `log4r` logs to a file that can be set to `stoud` to write to the console, while `logger` writes to the console by default, but logging to files via the `appender_file` functions is also possible -- besides a number of other log record destinations as well. ### Hierarchical logging and performance Creating objects is the `log4r` way of handling multiple log environments, while `logger` handles that via `namespace`s. ## loggit Sorry, no direct replacement for [`loggit`](https://cran.r-project.org/package=loggit) -- capturing `message`, `warning` and `stop` function messages, but it's on the [roadmap](https://github.com/daroczig/logger/issues/6) to provide helper functions to be used as message hooks feed `logger`. ```{r cleanup, include = FALSE} ## restore settings log_threshold(oldconf$threshold) log_layout(oldconf$layout) log_formatter(oldconf$formatter) log_appender(oldconf$appender) ``` ```{r echo=FALSE, results='hide'} detach('package:logger', unload = TRUE) detach('package:futile.logger', unload = TRUE) detach('package:logging', unload = TRUE) ``` logger/vignettes/r_packages.Rmd0000644000176200001440000000440713406424353016315 0ustar liggesusers--- title: "Logging from R Packages" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Logging from R Packages} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r pkgchecks, echo = FALSE} ## check if other logger packages are available and exit if not for (pkg in c('devtools')) { if (!requireNamespace(pkg, quietly = TRUE)) { warning(paste(pkg, 'package not available, so cannot build this vignette')) knitr::knit_exit() } } ``` ```{r setup, include = FALSE} knitr::opts_chunk$set( collapse = TRUE, comment = "#>" ) ``` In this vignette I suppose that you are already familiar with [Customizing the format and destination of log records](https://daroczig.github.io/logger/articles/customize_logger.html) vignette, especially with the [Log namespaces](https://daroczig.github.io/logger/articles/customize_logger.html#log-namespaces) section. So that your R package's users can suppress (or render with custom layout) the log messages triggered by your R package, it's wise to record all those log messages in a custom namespace. By default, if you are calling the `?log_level` function family from an R package after importing from the `logger` package, then `logger` will try to auto-guess the calling R package name and use that as the default namespace, see eg: ```{r} library(logger) devtools::load_all(system.file('demo-packages/logger-tester-package', package = 'logger')) logger_tester_function(INFO, 'hi from tester package') ``` But if auto-guessing is not your style, then feel free to set your custom namespace (eg the name of your package) in all `?log_info` etc function calls and let your users know about how to suppress / reformat / redirect your log messages via `?log_threshold`, `?log_layout`, `?log_appender`. Please note that setting the formatter function via `?log_formatter` should not be left to the R package end-users, as the log message formatter is specific to your logging calls, so that should be decided by the R package author. Feel free to pick any formatter function (eg `glue`, `sprintf`, `paste` or something else), and set that via `?log_formatter` when your R package is loaded. All other parameters of your `logger` will inherit from the `global` namespace -- set by your R package's end user. logger/vignettes/customize_logger.Rmd0000644000176200001440000002744014567717130017610 0ustar liggesusers--- title: "Customizing the Format and the Destination of a Log Record" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Customizing the Format and the Destination of a Log Record} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r pkgchecks, echo = FALSE} ## check if other logger packages are available and exit if not for (pkg in c('devtools', 'parallel')) { if (!requireNamespace(pkg, quietly = TRUE)) { warning(paste(pkg, 'package not available, so cannot build this vignette')) knitr::knit_exit() } } ``` ```{r setup, include = FALSE} knitr::opts_chunk$set( collapse = TRUE, comment = "#>" ) library(logger) ## backup settings oldconf <- list( threshold = log_threshold(), layout = log_layout(), formatter = log_formatter(), appender = log_appender()) ## knitr not picking up stderr log_appender(appender_stdout) ``` In this vignette I suppose that you are already familiar with [The Anatomy of a Log Request](https://daroczig.github.io/logger/articles/anatomy.html) vignette. ## What gets logged? `logger` mostly relies on and uses the default `log4j` log levels and supports suppressing log messages with a lower log level compared to the currently set threshold in the logging namespace: ```{r} log_info('Hi, there!') log_debug('How are you doing today?') log_threshold() log_threshold(TRACE) log_debug('How are you doing today?') ``` So the `?log_threshold` function can both get and set the log level threshold for all future log requests. For the full list of all supported log levels and so thus the possible log level thresholds, see `?log_levels`. If you want to define the log level in a programmatic way, check out `?log_level`, eg ```{r} log_level(INFO, 'Hi, there!') ``` To temporarily update the log level threshold, you may also find the `?with_log_threshold` function useful: ```{r} log_threshold(INFO) log_debug('pst, can you hear me?') log_info('no') with_log_threshold(log_debug('pst, can you hear me?'), threshold = TRACE) log_info('yes') with_log_threshold({ log_debug('pst, can you hear me?') log_info('yes') }, threshold = TRACE) ``` You can also define your own log level(s) if needed, for example introducing an extra level between `DEBUG` and `INFO`: ```{r} FYI <- structure(450L, level = 'FYI', class = c('loglevel', 'integer')) log_threshold(FYI) log_debug('ping') log_level(FYI, 'ping') log_info('pong') ``` ## Log namespaces By default, all log messages will be processed by the global `logger` definition, but you may also use custom namespaces (eg to deliver specific log records to a special destination or to apply a custom log level threshold) and even multiple loggers as well within the very same namespace (eg to deliver all `INFO` and above log levels in the console and everything below that to a trace log file). If you specify an unknown `namespace` in a log request, it will fall back to the global settings: ```{r} log_threshold(INFO) log_trace('Hi, there!', namespace = 'kitchensink') log_info('Hi, there!', namespace = 'kitchensink') ``` But once you start customizing that namespace, it gets forked from the global settings and live on its own without modifying the original namespace: ```{r} log_threshold(TRACE, namespace = 'kitchensink') log_trace('Hi, there!', namespace = 'kitchensink') log_info('Hi, there!', namespace = 'kitchensink') log_trace('Hi, there!') ``` ## Log message formatter functions In the above example, we logged strings without any dynamic parameter, so the task of the logger was quite easy. But in most cases you want to log a parameterized string and the formatter function's task to transform that to a regular character vector. By default, `logger` uses `glue` in the background: ```{r} log_formatter(formatter_glue) log_info('There are {nrow(mtcars)} cars in the mtcars dataset') log_info('2 + 2 = {2+2}') ``` If you don't like this syntax, or want to save a dependency, you can use other formatter functions as well, such as `?formatter_sprintf` (being the default in eg the [`logging` and `futile.logger` packages](https://daroczig.github.io/logger/articles/migration.html)) or `?formatter_paste`, or [write your own formatter function](https://daroczig.github.io/logger/articles/write_custom_extensions.html) converting R objects into string. ## Log message layouts By default, `?log_level` and its derivative functions (eg `?log_info`) will simply record the log-level, the current timestamp and the message after being processed by `glue`: ```{r} log_info(42) log_info('The answer is {42}') log_info('The answers are {1:5}') ``` In the above example, first, `42` was converted to a string by the `?formatter_glue` message formatter, then the message was passed to the `?layout_simple` layout function to generate the actual log record. An example of another layout function writing the same log messages in JSON: ```{r} log_layout(layout_json()) log_info(42) log_info('The answer is {42}') log_info('The answers are {1:5}') ``` If you need colorized logs highlighting the important log messages, check out `?layout_glue_colors`, and for other formatter and layout functions, see the manual of the above mentioned functions that have references to all the other functions and generator functions bundled with the package. ## Custom log record layout To define a custom format on how the log messages should be rendered, you may write your own `formatter` and `layout` function(s) or rely on the function generator functions bundled with the `logger` package, such as `?layout_glue_generator`. This function returns a `layout` function that you can define by `glue`-ing together variables describing the log request via `?get_logger_meta_variables`, so having easy access to (package) namespace, calling function's name, hostname, user running the R process etc. A quick example: * define custom logger: ```{r} logger <- layout_glue_generator(format = '{node}/{pid}/{namespace}/{fn} {time} {level}: {msg}') log_layout(logger) ``` * check what's being logged when called from the global environment: ```{r} log_info('foo') ``` * check what's being logged when called from a custom function: ```{r} f <- function() log_info('foo') f() ``` * check what's being logged when called from a package: ```{r} devtools::load_all(system.file('demo-packages/logger-tester-package', package = 'logger')) logger_tester_function(INFO, 'hi from tester package') ``` * suppress messages in a namespace: ```{r} log_threshold(namespace = 'logger.tester') log_threshold(WARN, namespace = 'logger.tester') logger_tester_function(INFO, 'hi from tester package') logger_tester_function(WARN, 'hi from tester package') log_info('I am still working in the global namespace') ``` Another example of making use of the generator function is to update the layout to include the Process ID that might be very useful eg when forking, see for example the below code chunk still using the above defined log layout: ```r f <- function(x) { log_info('received {length(x)} values') log_success('with the mean of {mean(x)}') mean(x) } library(parallel) mclapply(split(runif(100), 1:10), f, mc.cores = 5) #> nevermind/26448/R_GlobalEnv/FUN 2018-12-02 21:54:11 INFO: received 10 values #> nevermind/26448/R_GlobalEnv/FUN 2018-12-02 21:54:11 SUCCESS: with the mean of 0.403173440974206 #> nevermind/26449/R_GlobalEnv/FUN 2018-12-02 21:54:11 INFO: received 10 values #> nevermind/26448/R_GlobalEnv/FUN 2018-12-02 21:54:11 INFO: received 10 values #> nevermind/26449/R_GlobalEnv/FUN 2018-12-02 21:54:11 SUCCESS: with the mean of 0.538581100990996 #> nevermind/26448/R_GlobalEnv/FUN 2018-12-02 21:54:11 SUCCESS: with the mean of 0.485734378430061 #> nevermind/26450/R_GlobalEnv/FUN 2018-12-02 21:54:11 INFO: received 10 values #> nevermind/26449/R_GlobalEnv/FUN 2018-12-02 21:54:11 INFO: received 10 values #> nevermind/26450/R_GlobalEnv/FUN 2018-12-02 21:54:11 SUCCESS: with the mean of 0.580483326432295 #> nevermind/26452/R_GlobalEnv/FUN 2018-12-02 21:54:11 INFO: received 10 values #> nevermind/26449/R_GlobalEnv/FUN 2018-12-02 21:54:11 SUCCESS: with the mean of 0.461282140854746 #> nevermind/26450/R_GlobalEnv/FUN 2018-12-02 21:54:11 INFO: received 10 values #> nevermind/26451/R_GlobalEnv/FUN 2018-12-02 21:54:11 INFO: received 10 values #> nevermind/26450/R_GlobalEnv/FUN 2018-12-02 21:54:11 SUCCESS: with the mean of 0.465152264293283 #> nevermind/26452/R_GlobalEnv/FUN 2018-12-02 21:54:11 SUCCESS: with the mean of 0.618332817289047 #> nevermind/26451/R_GlobalEnv/FUN 2018-12-02 21:54:11 SUCCESS: with the mean of 0.493527933699079 #> nevermind/26452/R_GlobalEnv/FUN 2018-12-02 21:54:11 INFO: received 10 values #> nevermind/26452/R_GlobalEnv/FUN 2018-12-02 21:54:11 SUCCESS: with the mean of 0.606248055002652 #> nevermind/26451/R_GlobalEnv/FUN 2018-12-02 21:54:11 INFO: received 10 values #> nevermind/26451/R_GlobalEnv/FUN 2018-12-02 21:54:11 SUCCESS: with the mean of 0.537314630229957 ``` *Note that the `layout_glue_generator` functions also adds a special attribute to the resulting formatting function so that when printing the layout function to the console, the user can easily interpret what's being used instead of just showing the actual functions's body:* ```{r} log_layout() ``` For more details on this, see the [Writing custom logger extensions](https://daroczig.github.io/logger/articles/write_custom_extensions.html) vignette. ```{r} ## reset layout log_layout(layout_simple) ``` ## Delivering log records to their destination By default, `logger` will write to the console or `stdout` via the `?appender_console` function: ```{r} log_appender() ``` To write to a logfile instead, use the `?appender_file` generator function, that returns a function that can be used in any namespace: ```{r} t <- tempfile() log_appender(appender_file(t)) log_info('where is this message going?') log_appender() readLines(t) unlink(t) ``` There's a similar generator function that returns an appender function delivering log messages to Slack channels: ```r ## load Slack configuration, API token etc from a (hopefully encrypted) yaml file or similar slack_config <- config::config(...) ## redirect log messages to Slack log_appender(appender_slack( channel = '#gergely-test', username = 'logger', api_token = slack_config$token ), namespace = 'slack') log_info('Excited about sending logs to Slack!') #> INFO [2018-11-28 00:21:13] Excited about sending logs to Slack! log_info('Hi there from logger@R!', namespace = 'slack') ``` You may find `?appender_tee` also useful, that writes the log messages to both `stdout` and a file. ```{r} ## reset appender log_appender(appender_console) ``` And the are many other appender functions bundled with `logger` as well, eg some writing to Syslog, Telegram, Pushbullet, a database table or an Amazon Kinesis stream -- even doing that asynchronously via `appender_async` -- see [Simple Benchmarks on Performance](https://daroczig.github.io/logger/articles/performance.html) for more details. ## Stacking loggers Note that the `?appender_tee` functionality can be implemented by stacking loggers as well, eg setting two loggers for the global namespace: `?appender_console` and `?appender_file`. The advantage of this approach is that you can set different log level thresholds for each logger, for example: ```{r} log_threshold() ## create a new logger with index 2 log_threshold(TRACE, index = 2) ## note that the original logger still have the same log level threshold log_threshold() log_threshold(index = 2) ## update the appender of the new logger t <- tempfile() log_appender(appender_file(t), index = 2) ## test both loggers log_info('info msg') log_debug('info msg') readLines(t) unlink(t) ``` ```{r cleanup, include = FALSE} ## restore settings log_threshold(oldconf$threshold) log_layout(oldconf$layout) log_formatter(oldconf$formatter) log_appender(oldconf$appender) ``` logger/vignettes/Intro.Rmd0000644000176200001440000000645114570721542015315 0ustar liggesusers--- title: "Introduction to logger" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Introduction to logger} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r setup, include = FALSE} knitr::opts_chunk$set( collapse = TRUE, comment = "#>" ) library(logger) ## backup settings oldconf <- list( threshold = log_threshold(), layout = log_layout(), formatter = log_formatter(), appender = log_appender()) ## knitr not picking up stderr log_appender(appender_stdout) ``` If you are not only using R in the interactive console for ad-hoc data analysis, but running eg batch jobs (for ETL, reporting, modeling, forecasting etc) as well, then logging the status(changes) of your script is a must so that later on you can review / debug what have happened. For most cases, it's enough to load the package and use the functions with the `log` prefix to log important and not so important messages, for example: ```{r} library(logger) log_info('Loading data') data(mtcars) log_info('The dataset includes {nrow(mtcars)} rows') if (max(mtcars$hp) < 1000) { log_warn('Oh, no! There are no cars with more than 1K horsepower in the dataset :/') log_debug('The most powerful car is {rownames(mtcars)[which.max(mtcars$hp)]} with {max(mtcars$hp)} hp') } ``` Interestingly, the most powerful car was not being logged -- because by default the `logger` prints messages with at least the `INFO` log level: ```{r} log_threshold() ``` To change that, specify the new log level threshold, eg `TRACE` to log everything: ```{r} log_threshold(TRACE) ``` The rerunning the above code chunk: ```{r} log_info('Loading data') data(mtcars) log_info('The dataset includes {nrow(mtcars)} rows') if (max(mtcars$hp) < 1000) { log_warn('Oh, no! There are no cars with more than 1K horsepower in the dataset :/') log_debug('The most powerful car is {rownames(mtcars)[which.max(mtcars$hp)]} with {max(mtcars$hp)} hp') } ``` You may also find the `?log_eval` function useful to log both an R expression and its result in the same log record: ```{r} f <- sqrt g <- mean x <- 1:31 log_eval(y <- f(g(x)), level = INFO) str(y) ``` Sometimes, it may be reasonable to log R objects as markdown, e.g. a smallish `data.frame` or `data.table`, e.g. `mtcars` or `iris`. Calling the formatter using `pander` instead of `glue` can help: ```{r knitr-pander-setup, include = FALSE} ppo1 <- pander::panderOptions('knitr.auto.asis') ppo2 <- pander::panderOptions('table.style') pander::panderOptions('knitr.auto.asis', FALSE) pander::panderOptions('table.style', 'simple') ``` ```{r} log_formatter(formatter_pander) log_info(head(iris)) ``` ```{r knitr-pander-revert, include = FALSE} pander::panderOptions('knitr.auto.asis', ppo1) pander::panderOptions('table.style', ppo2) ``` For more details, check the [function reference in the manual](https://daroczig.github.io/logger/reference/index.html), or start with the [The Anatomy of a Log Request](https://daroczig.github.io/logger/articles/anatomy.html) and [Customizing the Format and the Destination of a Log Record](https://daroczig.github.io/logger/articles/customize_logger.html) vignettes. ```{r cleanup, include = FALSE} ## restore settings log_threshold(oldconf$threshold) log_layout(oldconf$layout) log_formatter(oldconf$formatter) log_appender(oldconf$appender) ``` logger/R/0000755000176200001440000000000014571623750011744 5ustar liggesuserslogger/R/color.R0000644000176200001440000000503013377353570013206 0ustar liggesusers#' Colorize string by the related log level #' #' Adding color to a string to be used in terminal output. Supports ANSI standard colors 8 or 256. #' @param msg string #' @param level see \code{\link{log_levels}} #' @return string with ANSI escape code #' @export #' @examples \dontrun{ #' cat(colorize_by_log_level(FATAL, 'foobar'), '\n') #' cat(colorize_by_log_level(ERROR, 'foobar'), '\n') #' cat(colorize_by_log_level(WARN, 'foobar'), '\n') #' cat(colorize_by_log_level(SUCCESS, 'foobar'), '\n') #' cat(colorize_by_log_level(INFO, 'foobar'), '\n') #' cat(colorize_by_log_level(DEBUG, 'foobar'), '\n') #' cat(colorize_by_log_level(TRACE, 'foobar'), '\n') #' } colorize_by_log_level <- function(msg, level) { fail_on_missing_package('crayon') color <- switch( attr(level, 'level'), 'FATAL' = crayon::combine_styles(crayon::bold, crayon::make_style('red1')), 'ERROR' = crayon::make_style('red4'), 'WARN' = crayon::make_style('darkorange'), 'SUCCESS' = crayon::combine_styles(crayon::bold, crayon::make_style('green4')), 'INFO' = crayon::reset, 'DEBUG' = crayon::make_style('deepskyblue4'), 'TRACE' = crayon::make_style('dodgerblue4'), stop('Unknown log level') ) paste0(color(msg), crayon::reset('')) } #' Render a string with light/dark gray based on the related log level #' #' Adding color to a string to be used in terminal output. Supports ANSI standard colors 8 or 256. #' @param msg string #' @param level see \code{\link{log_levels}} #' @return string with ANSI escape code #' @export #' @examples \dontrun{ #' cat(grayscale_by_log_level(FATAL, 'foobar'), '\n') #' cat(grayscale_by_log_level(ERROR, 'foobar'), '\n') #' cat(grayscale_by_log_level(WARN, 'foobar'), '\n') #' cat(grayscale_by_log_level(SUCCESS, 'foobar'), '\n') #' cat(grayscale_by_log_level(INFO, 'foobar'), '\n') #' cat(grayscale_by_log_level(DEBUG, 'foobar'), '\n') #' cat(grayscale_by_log_level(TRACE, 'foobar'), '\n') #' } grayscale_by_log_level <- function(msg, level) { fail_on_missing_package('crayon') color <- switch( attr(level, 'level'), 'FATAL' = crayon::make_style('gray100'), 'ERROR' = crayon::make_style('gray90'), 'WARN' = crayon::make_style('gray80'), 'SUCCESS' = crayon::make_style('gray70'), 'INFO' = crayon::make_style('gray60'), 'DEBUG' = crayon::make_style('gray50'), 'TRACE' = crayon::make_style('gray40'), stop('Unknown log level') ) paste0(color(msg), crayon::reset('')) } logger/R/utils.R0000644000176200001440000000551114567717130013232 0ustar liggesusers#' Check if R package can be loaded and fails loudly otherwise #' @param pkg string #' @param min_version optional minimum version needed #' @export #' @importFrom utils packageVersion compareVersion #' @examples \dontrun{ #' f <- function() fail_on_missing_package('foobar') #' f() #' g <- function() fail_on_missing_package('stats') #' g() #' } fail_on_missing_package <- function(pkg, min_version) { pc <- sys.call(which = 1) if (!requireNamespace(pkg, quietly = TRUE)) { stop(sprintf( 'Please install the %s package to use %s', shQuote(pkg), deparse(pc[[1]])), call. = FALSE) } if (!missing(min_version)) { if (compareVersion(min_version, as.character(packageVersion(pkg))) == 1) { stop(sprintf( 'Please install min. %s version of %s to use %s', min_version, pkg, deparse(pc[[1]])), call. = FALSE) } } } #' Returns the name of the top level environment from which the logger was called #' @return string #' @keywords internal #' @param .topenv call environment top_env_name <- function(.topenv = parent.frame()) { environmentName(topenv(.topenv)) } #' Deparse and join all lines into a single line #' #' Calling \code{deparse} and joining all the returned lines into a #' single line, separated by whitespace, and then cleaning up all the #' duplicated whitespace (except for excessive whitespace in strings #' between single or double quotes). #' @param x object to \code{deparse} #' @return string #' @export deparse_to_one_line <- function(x) { gsub('\\s+(?=(?:[^\\\'"]*[\\\'"][^\\\'"]*[\\\'"])*[^\\\'"]*$)', ' ', paste(deparse(x), collapse = ' '), perl = TRUE) } #' Catch the log header #' @return string #' @keywords internal #' @param level see \code{\link{log_levels}} #' @param namespace string #' @examples #' \dontrun{ #' catch_base_log(INFO, NA_character_) #' logger <- layout_glue_generator(format = '{node}/{pid}/{namespace}/{fn} {time} {level}: {msg}') #' log_layout(logger) #' catch_base_log(INFO, NA_character_) #' fun <- function() catch_base_log(INFO, NA_character_) #' fun() #' catch_base_log(INFO, NA_character_, .topcall = call('funLONG')) #' } catch_base_log <- function( level, namespace, .topcall = sys.call(-1), .topenv = parent.frame() ) { namespace <- fallback_namespace(namespace) orginal_appender <- log_appender(namespace = namespace) log_appender(appender_console, namespace = namespace) # catch error, warning or message res <- capture.output( log_level(level = level, "", namespace = namespace, .topcall = .topcall, .topenv = .topenv), type = 'message' ) log_appender(orginal_appender, namespace = namespace) res } logger/R/zzz.R0000644000176200001440000000226414571555517012735 0ustar liggesusers## init storage for all logger settings namespaces <- new.env() .onLoad <- function(libname, pkgname) { ## default namespace's logger settings namespaces$global <- list( ## there can be multiple loggers for a namespace default = list( threshold = as.loglevel(Sys.getenv('LOGGER_LOG_LEVEL', unset = 'INFO')), layout = layout_simple, formatter = formatter_sprintf, appender = appender_console)) if (requireNamespace('glue', quietly = TRUE)) { log_formatter(formatter_glue, namespace = 'global', index = 1) } ## internal namespace for debugging logger namespaces$.logger <- list( default = list( threshold = ERROR, layout = layout_simple, formatter = formatter_sprintf, appender = appender_console)) } .onAttach <- function(libname, pkgname) { ## warn user about using sprintf instead of glue due to missing dependency if (!requireNamespace('glue', quietly = TRUE)) { packageStartupMessage('logger: As the "glue" R package is not installed, using "sprintf" as the default log message formatter instead of "glue".') } } logger/R/helpers.R0000644000176200001440000001563014567717130013537 0ustar liggesusers#' Evaluate an expression and log results #' @param expr R expression to be evaluated while logging the expression itself along with the result #' @param level \code{\link{log_levels}} #' @param multiline setting to \code{FALSE} will print both the expression (enforced to be on one line by removing line-breaks if any) and its result on a single line separated by \code{=>}, while setting to \code{TRUE} will log the expression and the result in separate sections reserving line-breaks and rendering the printed results #' @examples \dontrun{ #' log_eval(pi * 2, level = INFO) #' #' ## lowering the log level threshold so that we don't have to set a higher level in log_eval #' log_threshold(TRACE) #' log_eval(x <- 4) #' log_eval(sqrt(x)) #' #' ## log_eval can be called in-line as well as returning the return value of the expression #' x <- log_eval(mean(runif(1e3))) #' x #' #' ## https://twitter.com/krlmlr/status/1067864829547999232 #' f <- sqrt #' g <- mean #' x <- 1:31 #' log_eval(f(g(x)), level = INFO) #' log_eval(y <- f(g(x)), level = INFO) #' #' ## returning a function #' log_eval(f <- sqrt) #' log_eval(f) #' #' ## evaluating something returning a wall of "text" #' log_eval(f <- log_eval) #' log_eval(f <- log_eval, multiline = TRUE) #' #' ## doing something computationally intensive #' log_eval(system.time(for(i in 1:100) mad(runif(1000))), multiline = TRUE) #' } #' @importFrom utils capture.output #' @export log_eval <- function(expr, level = TRACE, multiline = FALSE) { ## capture call expr <- substitute(expr) exprs <- gsub('\n', ' ', deparse(expr), fixed = TRUE) ## evaluate call and store results timer <- Sys.time() res <- withVisible(eval.parent(expr)) ## log expression and results if (multiline == FALSE) { log_level(level, skip_formatter( paste( shQuote(paste(exprs, collapse = ' ')), '=>', shQuote(paste(gsub('\n', ' ', deparse(res$value)), collapse = ' '))))) } else { log_level(level, 'Running expression: ====================') log_level(level, skip_formatter(exprs)) log_level(level, 'Results: ===============================') log_level(level, skip_formatter(capture.output(res$value))) log_level(level, paste( 'Elapsed time:', round(difftime(Sys.time(), timer, units = 'secs'), 2), 'sec')) } ## return the results of the call if (res$visible == TRUE) { return(res$value) } else { return(invisible(res$value)) } } #' Logs a long line to stand out from the console #' @inheritParams log_level #' @param separator character to be used as a separator #' @param width max width of message -- longer text will be wrapped into multiple lines #' @export #' @examples #' log_separator() #' log_separator(ERROR, separator = '!', width = 60) #' log_separator(ERROR, separator = '!', width = 100) #' logger <- layout_glue_generator(format = '{node}/{pid}/{namespace}/{fn} {time} {level}: {msg}') #' log_layout(logger) #' log_separator(ERROR, separator = '!', width = 100) #' log_layout(layout_blank) #' log_separator(ERROR, separator = '!', width = 80) #' @seealso \code{\link{log_with_separator}} log_separator <- function(level = INFO, namespace = NA_character_, separator = '=', width = 80, .topcall = sys.call()) { stopifnot(length(separator) == 1, nchar(separator) == 1) base_info_chars <- nchar(catch_base_log(level, namespace, .topcall = .topcall)) log_level( paste(rep(separator, max(0, width - base_info_chars)), collapse = ''), level = level, namespace = namespace) } #' Logs a message in a very visible way #' @inheritParams log_level #' @inheritParams log_separator #' @export #' @examples #' log_with_separator('An important message') #' log_with_separator('Some critical KPI down!!!', separator = '$') #' log_with_separator('This message is worth a {1e3} words') #' log_with_separator(paste( #' 'A very important message with a bunch of extra words that will', #' 'eventually wrap into a multi-line message for our quite nice demo :wow:')) #' log_with_separator(paste( #' 'A very important message with a bunch of extra words that will', #' 'eventually wrap into a multi-line message for our quite nice demo :wow:'), #' width = 60) #' log_with_separator('Boo!', level = FATAL) #' log_layout(layout_blank) #' log_with_separator('Boo!', level = FATAL) #' logger <- layout_glue_generator(format = '{node}/{pid}/{namespace}/{fn} {time} {level}: {msg}') #' log_layout(logger) #' log_with_separator('Boo!', level = FATAL, width = 120) #' @seealso \code{\link{log_separator}} log_with_separator <- function(..., level = INFO, namespace = NA_character_, separator = '=', width = 80) { base_info_chars <- nchar(catch_base_log(level, namespace, .topcall = sys.call())) log_separator( level = level, separator = separator, width = width, namespace = namespace, .topcall = call('log_separator') ) message <- do.call(eval(log_formatter()), list(...)) message <- strwrap(message, max(0, width - base_info_chars - 4)) message <- sapply(message, function(m) { paste0( separator, ' ', m, paste(rep(' ', max(0, width - base_info_chars - 4 - nchar(m))), collapse = ''), ' ', separator) }) log_level(skip_formatter(message), level = level, namespace = namespace) log_separator( level = level, separator = separator, width = width, namespace = namespace, .topcall = call('log_separator') ) } #' Tic-toc logging #' @param ... passed to \code{log_level} #' @param level see \code{\link{log_levels}} #' @param namespace x #' @export #' @examples \dontrun{ #' log_tictoc('warming up') #' Sys.sleep(0.1) #' log_tictoc('running') #' Sys.sleep(0.1) #' log_tictoc('running') #' Sys.sleep(runif(1)) #' log_tictoc('and running') #' } #' @author Thanks to Neal Fultz for the idea and original implementation! log_tictoc <- function(..., level = INFO, namespace = NA_character_) { ns <- fallback_namespace(namespace) on.exit({ assign(ns, toc, envir = tictocs) }) nsenv <- get(fallback_namespace(namespace), envir = namespaces) tic <- get0(ns, envir = tictocs, ifnotfound = Sys.time()) toc <- Sys.time() tictoc <- difftime(toc, tic) log_level(paste(ns, 'timer', ifelse(round(tictoc, 2) == 0, 'tic', 'toc'), round(tictoc, 2), attr(tictoc, 'units') , '-- '), ..., level = level, namespace = namespace) } tictocs <- new.env() #' Logs the error message to console before failing #' @param expression call #' @export #' @examples \dontrun{ #' log_failure('foobar') #' log_failure(foobar) #' } log_failure <- function(expression) { tryCatch(expression, error = function(e) { log_error(e$message) stop(e) }) } logger/R/levels.R0000644000176200001440000000571214571101707013357 0ustar liggesuserslog_levels_supported <- c('OFF', 'FATAL', 'ERROR', 'WARN', 'SUCCESS', 'INFO', 'DEBUG', 'TRACE') #' Log levels #' #' The standard Apache logj4 log levels plus a custom level for \code{SUCCESS}. For the full list of these log levels and suggested usage, check the below Details. #' #' List of supported log levels: #' \enumerate{ #' \item \code{OFF} No events will be logged #' \item \code{FATAL} Severe error that will prevent the application from continuing #' \item \code{ERROR} An error in the application, possibly recoverable #' \item \code{WARN} An event that might possible lead to an error #' \item \code{SUCCESS} An explicit success event above the INFO level that you want to log #' \item \code{INFO} An event for informational purposes #' \item \code{DEBUG} A general debugging event #' \item \code{TRACE} A fine-grained debug message, typically capturing the flow through the application. #' } #' @references \url{https://logging.apache.org/log4j/2.x/javadoc/log4j-api/org/apache/logging/log4j/Level.html}, \url{https://logging.apache.org/log4j/2.x/manual/customloglevels.html} #' @aliases log_levels OFF FATAL ERROR WARN SUCCESS INFO DEBUG TRACE #' @rdname log_levels #' @usage #' TRACE #' #' DEBUG #' #' INFO #' #' SUCCESS #' #' WARN #' #' ERROR #' #' FATAL #' #' OFF #' @export OFF <- structure(0L, level = 'OFF', class = c('loglevel', 'integer')) #' @export FATAL <- structure(100L, level = 'FATAL', class = c('loglevel', 'integer')) #' @export ERROR <- structure(200L, level = 'ERROR', class = c('loglevel', 'integer')) #' @export WARN <- structure(300L, level = 'WARN', class = c('loglevel', 'integer')) #' @export SUCCESS <- structure(350L, level = 'SUCCESS', class = c('loglevel', 'integer')) #' @export INFO <- structure(400L, level = 'INFO', class = c('loglevel', 'integer')) #' @export DEBUG <- structure(500L, level = 'DEBUG', class = c('loglevel', 'integer')) #' @export TRACE <- structure(600L, level = 'TRACE', class = c('loglevel', 'integer')) #' @export print.loglevel <- function(x, ...) { cat('Log level: ', attr(x, 'level'), '\n', sep = '') } #' Convert R object into a logger log-level #' @param x string or integer #' @return pander log-level, e.g. \code{INFO} #' @export #' @examples #' as.loglevel(INFO) #' as.loglevel(400L) #' as.loglevel(400) as.loglevel <- function(x) { UseMethod('as.loglevel', x) } #' @export as.loglevel.default <- function(x) { stop(paste( 'Do not know how to convert', shQuote(class(x)[1]), 'to a logger log-level.' )) } #' @export as.loglevel.character <- function(x) { stopifnot( length(x) == 1, x %in% log_levels_supported ) getFromNamespace(x, 'logger') } #' @export as.loglevel.integer <- function(x) { loglevels <- mget(log_levels_supported, envir = asNamespace('logger')) stopifnot( length(x) == 1, x %in% as.integer(loglevels) ) loglevels[[which(loglevels == x)]] } #' @export as.loglevel.numeric <- as.loglevel.integer logger/R/hooks.R0000644000176200001440000001527714571077771013234 0ustar liggesusers#' Warn to update R to 4+ #' @keywords internal warn_if_globalCallingHandlers_is_not_available <- function() { log_warn( 'Using legacy version of global message/warning/error hook, ', 'please update your R installation to at least 4.0.0 ', 'to make use of the much more elegant globalCallingHandlers approach.') } #' Injects a logger call to standard messages #' #' This function uses \code{trace} to add a \code{log_info} function call when \code{message} is called to log the informative messages with the \code{logger} layout and appender. #' @export #' @examples \dontrun{ #' log_messages() #' message('hi there') #' } log_messages <- function() { if (R.Version()$major >= 4) { if (any(sapply(globalCallingHandlers()[names(globalCallingHandlers()) == 'message'], attr, which = 'implements') == 'log_messages')) { warning('Ignoring this call to log_messages as it was registered previously.') } else { globalCallingHandlers( message = structure(function(m) { logger::log_level(logger::INFO, m$message, .topcall = m$call) }, implements = 'log_messages')) } } else { warn_if_globalCallingHandlers_is_not_available() invisible(suppressMessages(trace( what = 'message', exit = substitute(logger::log_info(logger::skip_formatter(cond$message))), print = FALSE, where = baseenv()))) } } #' Injects a logger call to standard warnings #' #' This function uses \code{trace} to add a \code{log_warn} function call when \code{warning} is called to log the warning messages with the \code{logger} layout and appender. #' @param muffle if TRUE, the warning is not shown after being logged #' @export #' @examples \dontrun{ #' log_warnings() #' for (i in 1:5) { Sys.sleep(runif(1)); warning(i) } #' } log_warnings <- function(muffle = getOption('logger_muffle_warnings', FALSE)) { if (R.Version()$major >= 4) { if (any(sapply(globalCallingHandlers()[names(globalCallingHandlers()) == 'warning'], attr, which = 'implements') == 'log_warnings')) { warning('Ignoring this call to log_warnings as it was registered previously.') } else { globalCallingHandlers( warning = structure(function(m) { logger::log_level(logger::WARN, m$message, .topcall = m$call) if (isTRUE(muffle)) { invokeRestart('muffleWarning') } }, implements = 'log_warnings')) } } else { warn_if_globalCallingHandlers_is_not_available() invisible(suppressMessages(trace( what = 'warning', tracer = substitute(logger::log_warn(logger::skip_formatter(paste(list(...), collapse = '')))), print = FALSE, where = baseenv()))) } } #' Injects a logger call to standard errors #' #' This function uses \code{trace} to add a \code{log_error} function call when \code{stop} is called to log the error messages with the \code{logger} layout and appender. #' @param muffle if TRUE, the error is not thrown after being logged #' @export #' @examples \dontrun{ #' log_errors() #' stop('foobar') #' } log_errors <- function(muffle = getOption('logger_muffle_errors', FALSE)) { if (R.Version()$major >= 4) { if (any(sapply(globalCallingHandlers()[names(globalCallingHandlers()) == 'error'], attr, which = 'implements') == 'log_errors')) { warning('Ignoring this call to log_errors as it was registered previously.') } else { globalCallingHandlers( error = structure(function(m) { logger::log_level(logger::ERROR, m$message, .topcall = m$call) if (isTRUE(muffle)) { invokeRestart('abort') } }, implements = 'log_errors')) } } else { warn_if_globalCallingHandlers_is_not_available() invisible(suppressMessages(trace( what = 'stop', tracer = substitute(logger::log_error(logger::skip_formatter(paste(list(...), collapse = '')))), print = FALSE, where = baseenv()))) } } #' Auto logging input changes in Shiny app #' #' This is to be called in the \code{server} section of the Shiny app. #' @export #' @param input passed from Shiny's \code{server} #' @param level log level #' @param excluded_inputs character vector of input names to exclude from logging #' @param namespace the name of the namespace #' @importFrom utils assignInMyNamespace assignInNamespace #' @examples \dontrun{ #' library(shiny) #' #' ui <- bootstrapPage( #' numericInput('mean', 'mean', 0), #' numericInput('sd', 'sd', 1), #' textInput('title', 'title', 'title'), #' textInput('foo', 'This is not used at all, still gets logged', 'foo'), #' passwordInput('password', 'Password not to be logged', 'secret'), #' plotOutput('plot') #' ) #' #' server <- function(input, output) { #' #' logger::log_shiny_input_changes(input, excluded_inputs = 'password') #' #' output$plot <- renderPlot({ #' hist(rnorm(1e3, input$mean, input$sd), main = input$title) #' }) #' #' } #' #' shinyApp(ui = ui, server = server) #' } log_shiny_input_changes <- function(input, level = INFO, namespace = NA_character_, excluded_inputs = character()) { fail_on_missing_package('shiny') fail_on_missing_package('jsonlite') if (!shiny::isRunning()) { stop('No Shiny app running, it makes no sense to call this function outside of a Shiny app') } input_values <- shiny::isolate(shiny::reactiveValuesToList(input)) assignInMyNamespace('shiny_input_values', input_values) log_level(level, skip_formatter(paste( 'Default Shiny inputs initialized:', as.character(jsonlite::toJSON(input_values, auto_unbox = TRUE)))), namespace = namespace) shiny::observe({ old_input_values <- shiny_input_values new_input_values <- shiny::reactiveValuesToList(input) names <- unique(c(names(old_input_values), names(new_input_values))) names <- setdiff(names, excluded_inputs) for (name in names) { old <- old_input_values[name] new <- new_input_values[name] if (!identical(old, new)) { log_level(level, 'Shiny input change detected on {name}: {old} -> {new}', namespace = namespace) } } assignInNamespace('shiny_input_values', new_input_values, ns = 'logger') }) } shiny_input_values <- NULL logger/R/formatters.R0000644000176200001440000003122714571556264014267 0ustar liggesusers#' Concatenate R objects into a character vector via \code{paste} #' @param ... passed to \code{paste} #' @inheritParams log_level #' @return character vector #' @export #' @seealso This is a \code{\link{log_formatter}}, for alternatives, see \code{\link{formatter_sprintf}}, \code{\link{formatter_glue}}, \code{\link{formatter_glue_safe}}, \code{\link{formatter_glue_or_sprintf}}, \code{\link{formatter_logging}}, \code{\link{formatter_json}}, \code{\link{formatter_pander}} and \code{\link{skip_formatter}} for marking a string not to apply the formatter on it. formatter_paste <- structure(function(..., .logcall = sys.call(), .topcall = sys.call(-1), .topenv = parent.frame()) { eval(paste(...), envir = .topenv) }, generator = quote(formatter_paste())) #' Apply \code{sprintf} to convert R objects into a character vector #' @param fmt passed to \code{sprintf} #' @param ... passed to \code{sprintf} #' @inheritParams log_level #' @return character vector #' @export #' @seealso This is a \code{\link{log_formatter}}, for alternatives, see \code{\link{formatter_paste}}, \code{\link{formatter_glue}}, \code{\link{formatter_glue_safe}}, \code{\link{formatter_glue_or_sprintf}}, \code{\link{formatter_logging}}, \code{\link{formatter_json}}, \code{\link{formatter_pander}} and \code{\link{skip_formatter}} for marking a string not to apply the formatter on it. formatter_sprintf <- structure(function(fmt, ..., .logcall = sys.call(), .topcall = sys.call(-1), .topenv = parent.frame()) { eval(sprintf(fmt, ...), envir = .topenv) }, generator = quote(formatter_sprintf())) #' Apply \code{glue} to convert R objects into a character vector #' @param ... passed to \code{glue} for the text interpolation #' @inheritParams log_level #' @return character vector #' @export #' @note Although this is the default log message formatter function, but when \pkg{glue} is not installed, \code{\link{formatter_sprintf}} will be used as a fallback. #' @seealso This is a \code{\link{log_formatter}}, for alternatives, see \code{\link{formatter_paste}}, \code{\link{formatter_sprintf}}, \code{\link{formatter_glue_or_sprintf}}, \code{\link{formatter_glue_safe}}, \code{\link{formatter_logging}}, \code{\link{formatter_json}}, \code{\link{formatter_pander}} and \code{\link{skip_formatter}} for marking a string not to apply the formatter on it. #' @importFrom utils str formatter_glue <- structure(function(..., .logcall = sys.call(), .topcall = sys.call(-1), .topenv = parent.frame()) { fail_on_missing_package('glue') message <- as.character( tryCatch( glue::glue(..., .envir = .topenv), error = function(e) { stop(paste( '`glue` failed in `formatter_glue` on:\n\n', capture.output(str(...)), '\n\nRaw error message:\n\n', e$message, '\n\nPlease consider using another `log_formatter` or', '`skip_formatter` on strings with curly braces.')) })) ## throw warning with logger inputs on empty response if (length(message) == 0) { ## disabled until azlogr drops test for no warning here: https://github.com/atalv/azlogr/issues/35 ## try(warning(paste( ## 'glue in formatter_glue returned nothing with the following parameters:', ## paste(..., sep = ' | ') ## )), silent = TRUE) } message }, generator = quote(formatter_glue())) #' Apply \code{glue_safe} to convert R objects into a character vector #' @param ... passed to \code{glue_safe} for the text interpolation #' @inheritParams log_level #' @return character vector #' @export #' @seealso This is a \code{\link{log_formatter}}, for alternatives, see \code{\link{formatter_glue}}, \code{\link{formatter_paste}}, \code{\link{formatter_sprintf}}, \code{\link{formatter_glue}}, \code{\link{formatter_glue_or_sprintf}}, \code{\link{formatter_logging}}, \code{\link{formatter_json}}, \code{\link{formatter_pander}} and \code{\link{skip_formatter}} for marking a string not to apply the formatter on it. #' @importFrom utils str formatter_glue_safe <- structure(function(..., .logcall = sys.call(), .topcall = sys.call(-1), .topenv = parent.frame()) { fail_on_missing_package('glue') as.character( tryCatch( glue::glue_safe(..., .envir = .topenv), error = function(e) { stop(paste( '`glue_safe` failed in `formatter_glue_safe` on:\n\n', capture.output(str(...)), '\n\nRaw error message:\n\n', e$message, '\n\nPlease consider using another `log_formatter` or', '`skip_formatter` on strings with curly braces.')) })) }, generator = quote(formatter_glue_safe())) #' Apply \code{glue} and \code{sprintf} #' #' The best of both words: using both formatter functions in your log messages, which can be useful eg if you are migrating from \code{sprintf} formatted log messages to \code{glue} or similar. #' #' Note that this function tries to be smart when passing arguments to \code{glue} and \code{sprintf}, but might fail with some edge cases, and returns an unformatted string. #' @param msg passed to \code{sprintf} as \code{fmt} or handled as part of \code{...} in \code{glue} #' @param ... passed to \code{glue} for the text interpolation #' @inheritParams log_level #' @return character vector #' @export #' @examples \dontrun{ #' formatter_glue_or_sprintf("{a} + {b} = %s", a = 2, b = 3, 5) #' formatter_glue_or_sprintf("{pi} * {2} = %s", pi*2) #' formatter_glue_or_sprintf("{pi} * {2} = {pi*2}") #' #' formatter_glue_or_sprintf("Hi ", "{c('foo', 'bar')}, did you know that 2*4={2*4}") #' formatter_glue_or_sprintf("Hi {c('foo', 'bar')}, did you know that 2*4={2*4}") #' formatter_glue_or_sprintf("Hi {c('foo', 'bar')}, did you know that 2*4=%s", 2*4) #' formatter_glue_or_sprintf("Hi %s, did you know that 2*4={2*4}", c('foo', 'bar')) #' formatter_glue_or_sprintf("Hi %s, did you know that 2*4=%s", c('foo', 'bar'), 2*4) #' } #' @seealso This is a \code{\link{log_formatter}}, for alternatives, see \code{\link{formatter_paste}}, \code{\link{formatter_sprintf}}, \code{\link{formatter_glue}}, \code{\link{formatter_glue_safe}}, \code{\link{formatter_logging}}, \code{\link{formatter_json}}, \code{\link{formatter_pander}} and \code{\link{skip_formatter}} for marking a string not to apply the formatter on it. formatter_glue_or_sprintf <- structure(function(msg, ..., .logcall = sys.call(), .topcall = sys.call(-1), .topenv = parent.frame()) { params <- list(...) ## params without a name are potential sprintf params sprintfparams <- which(names(params) == '') if (length(params) > 0 & length(sprintfparams) == 0) { sprintfparams <- seq_along(params) } if (is.null(msg) || length(msg) == 0) { msg <- '' } ## early return if (is.null(msg) || length(msg) == 0) { return('') } ## but some unnamed params might belong to glue actually, so ## let's look for the max number of first unnamed params sprintf expects sprintftags <- regmatches(msg, gregexpr('%[0-9.+0]*[aAdifeEgGosxX]', msg))[[1]] sprintfparams <- sprintfparams[seq_len(min(length(sprintftags), length(sprintfparams)))] ## get the actual params instead of indexes glueparams <- params[setdiff(seq_along(params), sprintfparams)] sprintfparams <- params[sprintfparams] ## first try to apply sprintf if (length(sprintfparams) > 0) { sprintfparams[vapply(sprintfparams, is.null, logical(1))] <- 'NULL' msg <- tryCatch( do.call(sprintf, c(msg, sprintfparams), envir = .topenv), error = function(e) msg) } ## then try to glue fail_on_missing_package('glue') msg <- tryCatch( as.character(sapply(msg, function(msg) { do.call(glue::glue, c(msg, glueparams), envir = .topenv) }, USE.NAMES = FALSE)), error = function(e) msg) ## return msg }, generator = quote(formatter_glue_or_sprintf())) #' Transforms all passed R objects into a JSON list #' @param ... passed to \code{toJSON} wrapped into a \code{list} #' @inheritParams log_level #' @return character vector #' @export #' @note This functionality depends on the \pkg{jsonlite} package. #' @examples \dontrun{ #' log_formatter(formatter_json) #' log_layout(layout_json_parser()) #' log_info(everything = 42) #' log_info(mtcars = mtcars, species = iris$Species) #' } #' @seealso This is a \code{\link{log_formatter}} potentially to be used with \code{\link{layout_json_parser}}, for alternatives, see \code{\link{formatter_paste}}, \code{\link{formatter_sprintf}}, \code{\link{formatter_glue}}, \code{\link{formatter_glue_safe}}, \code{\link{formatter_glue_or_sprintf}}, \code{\link{formatter_logging}}, \code{\link{formatter_pander}} and \code{\link{skip_formatter}} for marking a string not to apply the formatter on it. formatter_json <- structure(function(..., .logcall = sys.call(), .topcall = sys.call(-1), .topenv = parent.frame()) { fail_on_missing_package('jsonlite') eval(as.character(jsonlite::toJSON(list(...), auto_unbox = TRUE)), envir = .topenv) }, generator = quote(formatter_json())) #' Skip the formatter function #' #' Adds the \code{skip_formatter} attribute to an object so that logger will skip calling the formatter function(s). This is useful if you want to preprocess the log message with a custom function instead of the active formatter function(s). Note that the \code{message} should be a string, and \code{skip_formatter} should be the only input for the logging function to make this work. #' @param message character vector directly passed to the appender function in \code{\link{logger}} #' @param ... should be never set #' @return character vector with \code{skip_formatter} attribute set to \code{TRUE} #' @export skip_formatter <- function(message, ...) { if (!inherits(message, 'character')) { stop('Cannot skip the formatter function if the log message is not already formatter to a character vector') } if (length(list(...)) > 0) { stop('Cannot skip the formatter function if further arguments are passed besides the actual log message(s)') } structure(message, skip_formatter = TRUE) } #' Mimic the default formatter used in the \pkg{logging} package #' #' The \pkg{logging} package uses a formatter that behaves differently when the input is a string or other R object. If the first argument is a string, then \code{\link{sprintf}} is being called -- otherwise it does something like \code{\link{log_eval}} and logs the R expression(s) and the result(s) as well. #' @examples \dontrun{ #' log_formatter(formatter_logging) #' log_info('42') #' log_info(42) #' log_info(4+2) #' log_info('foo %s', 'bar') #' log_info('vector %s', 1:3) #' log_info(12, 1+1, 2 * 2) #' } #' @param ... string and further params passed to \code{sprintf} or R expressions to be evaluated #' @inheritParams log_level #' @return character vector #' @export #' @seealso This is a \code{\link{log_formatter}}, for alternatives, see \code{\link{formatter_paste}}, \code{\link{formatter_glue}}, \code{\link{formatter_glue_safe}}, \code{\link{formatter_glue_or_sprintf}}, \code{\link{formatter_json}}, \code{\link{formatter_pander}} and \code{\link{skip_formatter}} for marking a string not to apply the formatter on it. formatter_logging <- structure(function(..., .logcall = sys.call(), .topcall = sys.call(-1), .topenv = parent.frame()) { params <- list(...) .logcall <- substitute(.logcall) if (is.character(params[[1]])) { return(do.call(sprintf, params, envir = .topenv)) } sapply(1:length(params), function(i) { paste(deparse(as.list(.logcall)[-1][[i]]), params[[i]], sep = ': ') }) }, generator = quote(formatter_logging())) #' Formats R objects with pander #' @param x object to be logged #' @param ... optional parameters passed to \code{pander} #' @inheritParams log_level #' @return character vector #' @examples \dontrun{ #' log_formatter(formatter_pander) #' log_info('42') #' log_info(42) #' log_info(4+2) #' log_info(head(iris)) #' log_info(head(iris), style = 'simple') #' log_info(lm(hp ~ wt, mtcars)) #' } #' @note This functionality depends on the \pkg{pander} package. #' @export #' @seealso This is a \code{\link{log_formatter}}, for alternatives, see \code{\link{formatter_paste}}, \code{\link{formatter_sprintf}}, \code{\link{formatter_glue}}, \code{\link{formatter_glue_safe}}, \code{\link{formatter_glue_or_sprintf}}, \code{\link{formatter_logging}} formatter_pander <- structure(function(x, ..., .logcall = sys.call(), .topcall = sys.call(-1), .topenv = parent.frame()) { fail_on_missing_package('pander') eval(pander::pander_return(x, ...), envir = .topenv) }, generator = quote(formatter_pander())) logger/R/try.R0000644000176200001440000000237214570721542012706 0ustar liggesusers#' Try to evaluate an expressions and evaluate another expression on exception #' @param try R expression #' @param except fallback R expression to be evaluated if \code{try} fails #' @export #' @note Suppress log messages in the \code{except} namespace if you don't want to throw a \code{WARN} log message on the exception branch. #' @examples #' everything %except% 42 #' everything <- '640kb' #' everything %except% 42 #' #' FunDoesNotExist(1:10) %except% sum(1:10) / length(1:10) #' FunDoesNotExist(1:10) %except% (sum(1:10) / length(1:10)) #' FunDoesNotExist(1:10) %except% MEAN(1:10) %except% mean(1:10) #' FunDoesNotExist(1:10) %except% (MEAN(1:10) %except% mean(1:10)) `%except%` <- function(try, except) { call <- sys.call(-1) env <- parent.frame() try <- substitute(try) fallback <- substitute(except) tryCatch( eval(try, envir = env), error = function(e) { log_level( WARN, paste( 'Running', shQuote(deparse(fallback)), 'as', shQuote(deparse(try)), 'failed:', shQuote(e$message)), namespace = 'except', .topcall = call, .topenv = env) eval(fallback, envir = env) }) } logger/R/logger.R0000644000176200001440000004205214571556260013352 0ustar liggesusers#' Generate logging utility #' #' A logger consists of a log level \code{threshold}, a log message \code{formatter} function, a log record \code{layout} formatting function and the \code{appender} function deciding on the destination of the log record. For more details, see the package \code{README.md}. #' #' By default, a general logger definition is created when loading the \code{logger} package, that uses #' #' \enumerate{ #' \item \code{\link{INFO}} (or as per the \code{LOGGER_LOG_LEVEL} environment variable override) as the log level threshold #' \item \code{\link{layout_simple}} as the layout function showing the log level, timestamp and log message #' \item \code{\link{formatter_glue}} (or \code{\link{formatter_sprintf}} if \pkg{glue} is not installed) as the default formatter function transforming the R objects to be logged to a character vector #' \item \code{\link{appender_console}} as the default log record destination #' } #' @param threshold omit log messages below this \code{\link{log_levels}} #' @param formatter function pre-processing the message of the log record when it's not wrapped in a \code{\link{skip_formatter}} call #' @param layout function rendering the layout of the actual log record #' @param appender function writing the log record #' @return A function taking the log \code{level} to compare with the set threshold, all the \code{...} arguments passed to the formatter function, besides the standard \code{namespace}, \code{.logcall}, \code{.topcall} and \code{.topenv} arguments (see \code{\link{log_level}} for more details). The function invisibly returns a list including the original \code{level}, \code{namespace}, all \code{...} transformed to a list as \code{params}, the log \code{message} (after calling the \code{formatter} function) and the log \code{record} (after calling the \code{layout} function), and a list of \code{handlers} with the \code{formatter}, \code{layout} and \code{appender} functions. #' @export #' @references For more details, see the Anatomy of a Log Request vignette at \url{https://daroczig.github.io/logger/articles/anatomy.html}. #' @note It's quite unlikely that you need to call this function directly, but instead set the logger parameters and functions at \code{\link{log_threshold}}, \code{\link{log_formatter}}, \code{\link{log_layout}} and \code{\link{log_appender}} and then call \code{\link{log_levels}} and its derivatives, such as \code{\link{log_info}} directly. #' @examples \dontrun{ #' do.call(logger, logger:::namespaces$global[[1]])(INFO, 42) #' do.call(logger, logger:::namespaces$global[[1]])(INFO, '{pi}') #' x <- 42 #' do.call(logger, logger:::namespaces$global[[1]])(INFO, '{x}^2 = {x^2}') #' } logger <- function(threshold, formatter, layout, appender) { force(threshold) threshold <- validate_log_level(threshold) force(layout) force(appender) function(level, ..., namespace = NA_character_, .logcall = sys.call(), .topcall = sys.call(-1), .topenv = parent.frame()) { res <- list( level = level, namespace = namespace, params = list(...), handlers = list( formatter = formatter, layout = layout, appender = appender ), message = NULL, record = NULL ) if (level > threshold) { return(invisible(res)) } ## workaround to be able to avoid any formatter function, eg when passing in a string if (length(res$params) == 1 && isTRUE(attr(res$params[[1]], 'skip_formatter', exact = TRUE))) { res$message <- res$params[[1]] } else { res$message <- do.call(formatter, c(res$params, list( .logcall = substitute(.logcall), .topcall = substitute(.topcall), .topenv = .topenv))) } res$record <- layout( level, res$message, namespace = namespace, .logcall = substitute(.logcall), .topcall = substitute(.topcall), .topenv = .topenv) appender(res$record) invisible(res) } } #' Checks if provided namespace exists and falls back to global if not #' @param namespace string #' @return string #' @keywords internal fallback_namespace <- function(namespace) { if (!exists(namespace, envir = namespaces, inherits = FALSE)) { namespace <- 'global' } namespace } #' Base Logging Function #' @param fun_name string a full name of log function #' @param arg see \code{\link{log_levels}} #' @param namespace logger namespace #' @param index index of the logger within the namespace #' @return currently set or return log function property #' @keywords internal log_config_setter <- function(fun_name, arg, namespace, index) { if (length(namespace) > 1) { for (ns in namespace) { log_config_setter(fun_name, arg, ns, index) } return(invisible()) } fun_name_base <- strsplit(fun_name, '_')[[1]][2] configs <- get(fallback_namespace(namespace), envir = namespaces) config <- configs[[min(index, length(configs))]] if (fun_name_base == 'threshold') { if (is.null(arg)) { return(config[[fun_name_base]]) } config[[fun_name_base]] <- validate_log_level(arg) } else { if (is.null(arg)) { res <- config[[fun_name_base]] if (!is.null(attr(res, 'generator'))) { res <- parse(text = attr(res, 'generator'))[[1]] } return(res) } config[[fun_name_base]] <- arg } configs[[min(index, length(config) + 1)]] <- config assign(namespace, configs, envir = namespaces) } #' Delete an index from a logger namespace #' @inheritParams log_threshold #' @export delete_logger_index <- function(namespace = 'global', index) { configs <- get(fallback_namespace(namespace), envir = namespaces) if (index > length(configs)) { stop(sprintf('%s namespace has only %i indexes', namespace, length(configs))) } configs[index] <- NULL assign(namespace, configs, envir = namespaces) } #' Get or set log level threshold #' @param level see \code{\link{log_levels}} #' @param namespace logger namespace #' @param index index of the logger within the namespace #' @return currently set log level threshold #' @export #' @examples \dontrun{ #' ## check the currently set log level threshold #' log_threshold() #' #' ## change the log level threshold to WARN #' log_threshold(WARN) #' log_info(1) #' log_warn(2) #' #' ## add another logger with a lower log level threshold and check the number of logged messages #' log_threshold(INFO, index = 2) #' log_info(1) #' log_warn(2) #' #' ## set the log level threshold in all namespaces to ERROR #' log_threshold(ERROR, namespace = log_namespaces()) #' } #' @seealso \code{\link{logger}}, \code{\link{log_layout}}, \code{\link{log_formatter}}, \code{\link{log_appender}} log_threshold <- function(level = NULL, namespace = 'global', index = 1) { log_config_setter(fun_name = 'log_threshold', arg = level, namespace = namespace, index = index) } #' Get or set log record layout #' @param layout function defining the structure of a log record, eg \code{\link{layout_simple}}, \code{\link{layout_glue}} or \code{\link{layout_glue_colors}}, \code{\link{layout_json}}, or generator functions such as \code{\link{layout_glue_generator}}, default NULL #' @inheritParams log_threshold #' @export #' @examples \dontrun{ #' log_layout(layout_json()) #' log_info(42) #' } #' @seealso \code{\link{logger}}, \code{\link{log_threshold}}, \code{\link{log_appender}} and \code{\link{log_formatter}} log_layout <- function(layout = NULL, namespace = 'global', index = 1) { log_config_setter(fun_name = 'log_layout', arg = layout, namespace = namespace, index = index) } #' Get or set log message formatter #' @param formatter function defining how R objects are converted into a single string, eg \code{\link{formatter_paste}}, \code{\link{formatter_sprintf}}, \code{\link{formatter_glue}}, \code{\link{formatter_glue_or_sprintf}}, \code{\link{formatter_logging}}, default NULL #' @inheritParams log_threshold #' @export #' @seealso \code{\link{logger}}, \code{\link{log_threshold}}, \code{\link{log_appender}} and \code{\link{log_layout}} log_formatter <- function(formatter = NULL, namespace = 'global', index = 1) { log_config_setter(fun_name = 'log_formatter', arg = formatter, namespace = namespace, index = index) } #' Get or set log record appender function #' @param appender function delivering a log record to the destination, eg \code{\link{appender_console}}, \code{\link{appender_file}} or \code{\link{appender_tee}}, default NULL #' @inheritParams log_threshold #' @export #' @examples \dontrun{ #' ## change appender to "tee" that writes to the console and a file as well #' t <- tempfile() #' log_appender(appender_tee(t)) #' log_info(42) #' log_info(42:44) #' readLines(t) #' #' ## poor man's tee by stacking loggers in the namespace #' t <- tempfile() #' log_appender(appender_console) #' log_appender(appender_file(t), index = 2) #' log_info(42) #' readLines(t) #' } #' @seealso \code{\link{logger}}, \code{\link{log_threshold}}, \code{\link{log_layout}} and \code{\link{log_formatter}} log_appender <- function(appender = NULL, namespace = 'global', index = 1) { log_config_setter(fun_name = 'log_appender', arg = appender, namespace = namespace, index = index) } #' Find the logger definition(s) specified for the current namespace with a fallback to the global namespace #' @return list of function(s) #' @keywords internal #' @importFrom utils getFromNamespace #' @param namespace override the default / auto-picked namespace with a custom string get_logger_definitions <- function(namespace = NA_character_, .topenv = parent.frame()) { namespace <- ifelse(is.na(namespace), top_env_name(.topenv), namespace) if (!exists(namespace, envir = namespaces, inherits = FALSE)) { namespace <- 'global' } get(namespace, envir = getFromNamespace('namespaces', 'logger')) } #' Looks up logger namespaces #' @return character vector of namespace names #' @export log_namespaces <- function() { ls(envir = namespaces) } #' Log a message with given log level #' @param level log level, see \code{\link{log_levels}} for more details #' @param ... R objects that can be converted to a character vector via the active message formatter function #' @param namespace string referring to the \code{logger} environment / config to be used to override the target of the message record to be used instead of the default namespace, which is defined by the R package name from which the logger was called, and falls back to a common, global namespace. #' @param .logcall the logging call being evaluated (useful in formatters and layouts when you want to have access to the raw, unevaluated R expression) #' @param .topcall R expression from which the logging function was called (useful in formatters and layouts to extract the calling function's name or arguments) #' @param .topenv original frame of the \code{.topcall} calling function where the formatter function will be evaluated and that is used to look up the \code{namespace} as well via \code{logger:::top_env_name} #' @seealso \code{\link{logger}} #' @export #' @aliases log_level log_fatal log_error log_warn log_success log_info log_debug log_trace #' @usage #' log_level(level, ..., namespace = NA_character_, #' .logcall = sys.call(), .topcall = sys.call(-1), .topenv = parent.frame()) #' #' log_trace(..., namespace = NA_character_, #' .logcall = sys.call(), .topcall = sys.call(-1), .topenv = parent.frame()) #' #' log_debug(..., namespace = NA_character_, #' .logcall = sys.call(), .topcall = sys.call(-1), .topenv = parent.frame()) #' #' log_info(..., namespace = NA_character_, #' .logcall = sys.call(), .topcall = sys.call(-1), .topenv = parent.frame()) #' #' log_success(..., namespace = NA_character_, #' .logcall = sys.call(), .topcall = sys.call(-1), .topenv = parent.frame()) #' #' log_warn(..., namespace = NA_character_, #' .logcall = sys.call(), .topcall = sys.call(-1), .topenv = parent.frame()) #' #' log_error(..., namespace = NA_character_, #' .logcall = sys.call(), .topcall = sys.call(-1), .topenv = parent.frame()) #' #' log_fatal(..., namespace = NA_character_, #' .logcall = sys.call(), .topcall = sys.call(-1), .topenv = parent.frame()) #' @examples \dontrun{ #' log_level(INFO, 'hi there') #' log_info('hi there') #' #' ## output omitted #' log_debug('hi there') #' #' ## lower threshold and retry #' log_threshold(TRACE) #' log_debug('hi there') #' #' ## multiple lines #' log_info('ok {1:3} + {1:3} = {2*(1:3)}') #' #' log_layout(layout_json()) #' log_info('ok {1:3} + {1:3} = {2*(1:3)}') #' #' ## note for the JSON output, glue is not automatically applied #' log_info(glue::glue('ok {1:3} + {1:3} = {2*(1:3)}')) #' } #' @return Invisible \code{list} of \code{logger} objects. See \code{\link{logger}} for more details on the format/ log_level <- function(level, ..., namespace = NA_character_, .logcall = sys.call(), .topcall = sys.call(-1), .topenv = parent.frame()) { ## guess namespace if (is.na(namespace)) { topenv <- top_env_name(.topenv) namespace <- ifelse(topenv == 'R_GlobalEnv', 'global', topenv) } definitions <- get_logger_definitions(namespace, .topenv = .topenv) level <- validate_log_level(level) ## super early return (even before evaluating passed parameters) if (length(definitions) == 1 && level > definitions[[1]]$threshold) { return(invisible(NULL)) } log_arg <- list(...) log_arg$level <- level log_arg$.logcall <- .logcall log_arg$.topcall <- if(!is.null(.topcall)) { .topcall } else { ## cannot pass NULL NA } log_arg$.topenv <- .topenv log_arg$namespace <- namespace invisible(lapply(definitions, function(definition) { if (level > definition$threshold) { return(NULL) } log_fun <- do.call(logger, definition) structure(do.call(log_fun, log_arg), class = 'logger') })) } #' Assure valid log level #' @param level \code{\link{log_levels}} object or string representation #' @return \code{\link{log_levels}} object #' @keywords internal validate_log_level <- function(level) { if (inherits(level, 'loglevel')) { return(level) } if (is.character(level) & level %in% log_levels_supported) { return(get(level)) } stop('Invalid log level') } #' @export log_fatal <- function(..., namespace = NA_character_, .logcall = sys.call(), .topcall = sys.call(-1), .topenv = parent.frame()) { log_level(FATAL, ..., namespace = namespace, .logcall = .logcall, .topcall = .topcall, .topenv = .topenv) } #' @export log_error <- function(..., namespace = NA_character_, .logcall = sys.call(), .topcall = sys.call(-1), .topenv = parent.frame()) { log_level(ERROR, ..., namespace = namespace, .logcall = .logcall, .topcall = .topcall, .topenv = .topenv) } #' @export log_warn <- function(..., namespace = NA_character_, .logcall = sys.call(), .topcall = sys.call(-1), .topenv = parent.frame()) { log_level(WARN, ..., namespace = namespace, .logcall = .logcall, .topcall = .topcall, .topenv = .topenv) } #' @export log_success <- function(..., namespace = NA_character_, .logcall = sys.call(), .topcall = sys.call(-1), .topenv = parent.frame()) { log_level(SUCCESS, ..., namespace = namespace, .logcall = .logcall, .topcall = .topcall, .topenv = .topenv) } #' @export log_info <- function(..., namespace = NA_character_, .logcall = sys.call(), .topcall = sys.call(-1), .topenv = parent.frame()) { log_level(INFO, ..., namespace = namespace, .logcall = .logcall, .topcall = .topcall, .topenv = .topenv) } #' @export log_debug <- function(..., namespace = NA_character_, .logcall = sys.call(), .topcall = sys.call(-1), .topenv = parent.frame()) { log_level(DEBUG, ..., namespace = namespace, .logcall = .logcall, .topcall = .topcall, .topenv = .topenv) } #' @export log_trace <- function(..., namespace = NA_character_, .logcall = sys.call(), .topcall = sys.call(-1), .topenv = parent.frame()) { log_level(TRACE, ..., namespace = namespace, .logcall = .logcall, .topcall = .topcall, .topenv = .topenv) } #' Evaluate R expression with a temporarily updated log level threshold #' @param expression R command #' @param threshold \code{\link{log_levels}} #' @inheritParams log_threshold #' @export #' @examples \dontrun{ #' log_threshold(TRACE) #' log_trace('Logging everything!') #' x <- with_log_threshold({ #' log_info('Now we are temporarily suppressing eg INFO messages') #' log_warn('WARN') #' log_debug('Debug messages are suppressed as well') #' log_error('ERROR') #' invisible(42) #' }, threshold = WARN) #' x #' log_trace('DONE') #' } with_log_threshold <- function(expression, threshold = ERROR, namespace = 'global', index = 1) { old <- log_threshold(namespace = namespace, index = index) on.exit({ log_threshold(old, namespace = namespace, index = index) }) log_threshold(threshold, namespace = namespace, index = index) eval(quote(expression)) } logger/R/layouts.R0000644000176200001440000003163414571164655013602 0ustar liggesusers#' Collect useful information about the logging environment to be used in log messages #' #' Available variables to be used in the log formatter functions, eg in \code{\link{layout_glue_generator}}: #' \itemize{ #' \item levelr: log level as an R object, eg \code{\link{INFO}} #' \item level: log level as a string, eg \code{\link{INFO}} #' \item time: current time as \code{POSIXct} #' \item node: name by which the machine is known on the network as reported by \code{Sys.info} #' \item arch: machine type, typically the CPU architecture #' \item os_name: Operating System's name #' \item os_release: Operating System's release #' \item os_version: Operating System's version #' \item user: name of the real user id as reported by \code{Sys.info} #' \item pid: the process identification number of the R session #' \item node: name by which the machine is known on the network as reported by \code{Sys.info} #' \item r_version: R's major and minor version as a string #' \item ns: namespace usually defaults to \code{global} or the name of the holding R package of the calling the logging function #' \item ns_pkg_version: the version of \code{ns} when it's a package #' \item ans: same as \code{ns} if there's a defined \code{\link{logger}} for the namespace, otherwise a fallback namespace (eg usually \code{global}) #' \item topenv: the name of the top environment from which the parent call was called (eg R package name or \code{GlobalEnv}) #' \item call: parent call (if any) calling the logging function #' \item fn: function's (if any) name calling the logging function #' } #' @param log_level log level as per \code{\link{log_levels}} #' @inheritParams log_level #' @return list #' @export #' @importFrom utils packageVersion #' @seealso \code{\link{layout_glue_generator}} get_logger_meta_variables <- function(log_level = NULL, namespace = NA_character_, .logcall = sys.call(), .topcall = sys.call(-1), .topenv = parent.frame()) { sysinfo <- Sys.info() timestamp <- Sys.time() list( ns = namespace, ans = fallback_namespace(namespace), topenv = top_env_name(.topenv), fn = deparse_to_one_line(.topcall[[1]]), call = deparse_to_one_line(.topcall), time = timestamp, levelr = log_level, level = attr(log_level, 'level'), pid = Sys.getpid(), ## R and ns package versions r_version = paste0(R.Version()[c('major', 'minor')], collapse = '.'), ns_pkg_version = tryCatch(as.character(packageVersion(namespace)), error = function(e) NA_character_), ## stuff from Sys.info node = sysinfo[['nodename']], arch = sysinfo[['machine']], os_name = sysinfo[['sysname']], os_release = sysinfo[['release']], os_version = sysinfo[['version']], user = sysinfo[['user']] ## NOTE might be better to rely on the whoami pkg? ## TODO jenkins (or any) env vars => no need to get here, users can write custom layouts ## TODO seed ) } #' Generate log layout function using common variables available via glue syntax #' #' \code{format} is passed to \code{glue} with access to the below variables: #' \itemize{ #' \item msg: the actual log message #' \item further variables set by \code{\link{get_logger_meta_variables}} #' } #' @param format \code{glue}-flavored layout of the message using the above variables #' @return function taking \code{level} and \code{msg} arguments - keeping the original call creating the generator in the \code{generator} attribute that is returned when calling \code{\link{log_layout}} for the currently used layout #' @export #' @examples \dontrun{ #' example_layout <- layout_glue_generator( #' format = '{node}/{pid}/{ns}/{ans}/{topenv}/{fn} {time} {level}: {msg}') #' example_layout(INFO, 'try {runif(1)}') #' #' log_layout(example_layout) #' log_info('try {runif(1)}') #' } #' @seealso See example calls from \code{\link{layout_glue}} and \code{\link{layout_glue_colors}}. layout_glue_generator <- function(format = '{level} [{format(time, "%Y-%m-%d %H:%M:%S")}] {msg}') { force(format) structure(function(level, msg, namespace = NA_character_, .logcall = sys.call(), .topcall = sys.call(-1), .topenv = parent.frame()) { fail_on_missing_package('glue') if (!inherits(level, 'loglevel')) { stop('Invalid log level, see ?log_levels') } with(get_logger_meta_variables( log_level = level, namespace = namespace, .logcall = .logcall, .topcall = .topcall, .topenv = .topenv), glue::glue(format)) }, generator = deparse(match.call())) } #' Format a log record by including the raw message without anything added or modified #' @inheritParams log_level #' @param msg string message #' @return character vector #' @export #' @seealso This is a \code{\link{log_layout}}, for alternatives, see \code{\link{layout_simple}}, \code{\link{layout_glue_colors}}, \code{\link{layout_json}}, or generator functions such as \code{\link{layout_glue_generator}} layout_blank <- structure(function(level, msg, namespace = NA_character_, .logcall = sys.call(), .topcall = sys.call(-1), .topenv = parent.frame()) { msg }, generator = quote(layout_blank())) #' Format a log record by concatenating the log level, timestamp and message #' @inheritParams log_level #' @param msg string message #' @return character vector #' @export #' @seealso This is a \code{\link{log_layout}}, for alternatives, see \code{\link{layout_blank}}, \code{\link{layout_glue}}, \code{\link{layout_glue_colors}}, \code{\link{layout_json}}, \code{\link{layout_json_parser}}, or generator functions such as \code{\link{layout_glue_generator}} layout_simple <- structure(function(level, msg, namespace = NA_character_, .logcall = sys.call(), .topcall = sys.call(-1), .topenv = parent.frame()) { paste0(attr(level, 'level'), ' [', format(Sys.time(), "%Y-%m-%d %H:%M:%S"), '] ', msg) }, generator = quote(layout_simple())) #' Format a log record as the logging package does by default #' @inheritParams layout_simple #' @param msg string message #' @return character vector #' @export #' @seealso This is a \code{\link{log_layout}}, for alternatives, see \code{\link{layout_blank}}, \code{\link{layout_glue}}, \code{\link{layout_glue_colors}}, \code{\link{layout_json}}, \code{\link{layout_json_parser}}, or generator functions such as \code{\link{layout_glue_generator}} #' @examples \dontrun{ #' log_layout(layout_logging) #' log_info(42) #' log_info(42, namespace = 'everything') #' #' devtools::load_all(system.file('demo-packages/logger-tester-package', package = 'logger')) #' logger_tester_function(INFO, 42) #' } layout_logging <- structure(function(level, msg, namespace = NA_character_, .logcall = sys.call(), .topcall = sys.call(-1), .topenv = parent.frame()) { meta <- get_logger_meta_variables( log_level = level, namespace = namespace, .logcall = .logcall, .topcall = .topcall, .topenv = .topenv) paste0(format(Sys.time(), "%Y-%m-%d %H:%M:%S"), ' ', attr(level, 'level'), ':', ifelse(meta$ns == 'global', '', meta$ns), ':', msg) }, generator = quote(layout_logging())) #' Format a log message with \code{glue} #' #' By default, this layout includes the log level of the log record as per \code{\link{log_levels}}, the current timestamp and the actual log message -- that you can override via calling \code{\link{layout_glue_generator}} directly. For colorized output, see \code{\link{layout_glue_colors}}. #' @inheritParams layout_simple #' @return character vector #' @export #' @seealso This is a \code{\link{log_layout}}, for alternatives, see \code{\link{layout_blank}}, \code{\link{layout_simple}}, \code{\link{layout_glue_colors}}, \code{\link{layout_json}}, \code{\link{layout_json_parser}}, or generator functions such as \code{\link{layout_glue_generator}} layout_glue <- layout_glue_generator() #' Format a log message with \code{glue} and ANSI escape codes to add colors #' @inheritParams layout_simple #' @return character vector #' @export #' @examples \dontrun{ #' log_layout(layout_glue_colors) #' log_threshold(TRACE) #' log_info('Starting the script...') #' log_debug('This is the second line') #' log_trace('That is being placed right after the first one.') #' log_warn('Some errors might come!') #' log_error('This is a problem') #' log_debug('Getting an error is usually bad') #' log_error('This is another problem') #' log_fatal('The last problem.') #' } #' @seealso This is a \code{\link{log_layout}}, for alternatives, see \code{\link{layout_blank}}, \code{\link{layout_simple}}, \code{\link{layout_glue}}, \code{\link{layout_json}}, \code{\link{layout_json_parser}}, or generator functions such as \code{\link{layout_glue_generator}} #' @note This functionality depends on the \pkg{crayon} package. layout_glue_colors <- layout_glue_generator( format = paste( '{crayon::bold(colorize_by_log_level(level, levelr))}', '[{crayon::italic(format(time, "%Y-%m-%d %H:%M:%S"))}]', '{grayscale_by_log_level(msg, levelr)}')) #' Generate log layout function rendering JSON #' @param fields character vector of field names to be included in the JSON #' @return character vector #' @export #' @examples \dontrun{ #' log_layout(layout_json()) #' log_info(42) #' log_info('ok {1:3} + {1:3} = {2*(1:3)}') #' } #' @note This functionality depends on the \pkg{jsonlite} package. #' @seealso This is a \code{\link{log_layout}}, for alternatives, see \code{\link{layout_blank}}, \code{\link{layout_simple}}, \code{\link{layout_glue}}, \code{\link{layout_glue_colors}}, \code{\link{layout_json_parser}}, or generator functions such as \code{\link{layout_glue_generator}} layout_json <- function(fields = c('time', 'level', 'ns', 'ans', 'topenv', 'fn', 'node', 'arch', 'os_name', 'os_release', 'os_version', 'pid', 'user', 'msg')) { force(fields) structure(function(level, msg, namespace = NA_character_, .logcall = sys.call(), .topcall = sys.call(-1), .topenv = parent.frame()) { fail_on_missing_package('jsonlite') json <- get_logger_meta_variables( log_level = level, namespace = namespace, .logcall = .logcall, .topcall = .topcall, .topenv = .topenv) sapply(msg, function(msg) jsonlite::toJSON(c(json, list(msg = msg))[fields], auto_unbox = TRUE)) }, generator = deparse(match.call())) } #' Generate log layout function rendering JSON after merging meta fields with parsed list from JSON message #' @param fields character vector of field names to be included in the JSON #' @export #' @examples \dontrun{ #' log_formatter(formatter_json) #' log_info(everything = 42) #' log_layout(layout_json_parser()) #' log_info(everything = 42) #' log_layout(layout_json_parser(fields = c('time', 'node'))) #' log_info(cars = row.names(mtcars), species = unique(iris$Species)) #' } #' @note This functionality depends on the \pkg{jsonlite} package. #' @seealso This is a \code{\link{log_layout}} potentially to be used with \code{\link{formatter_json}}, for alternatives, see \code{\link{layout_simple}}, \code{\link{layout_glue}}, \code{\link{layout_glue_colors}}, \code{\link{layout_json}} or generator functions such as \code{\link{layout_glue_generator}} layout_json_parser <- function(fields = c('time', 'level', 'ns', 'ans', 'topenv', 'fn', 'node', 'arch', 'os_name', 'os_release', 'os_version', 'pid', 'user')) { force(fields) structure(function(level, msg, namespace = NA_character_, .logcall = sys.call(), .topcall = sys.call(-1), .topenv = parent.frame()) { fail_on_missing_package('jsonlite') meta <- get_logger_meta_variables( log_level = level, namespace = namespace, .logcall = .logcall, .topcall = .topcall, .topenv = .topenv)[fields] msg <- jsonlite::fromJSON(msg) jsonlite::toJSON(c(meta, msg), auto_unbox = TRUE, null = 'null') }, generator = deparse(match.call())) } #nocov start #' Format a log record for syslognet #' #' Format a log record for syslognet. #' This function converts the logger log level to a #' log severity level according to RFC 5424 "The Syslog Protocol". #' #' @inheritParams layout_simple #' @return A character vector with a severity attribute. #' @export layout_syslognet <- structure( function(level, msg, namespace = NA_character_, .logcall = sys.call(), .topcall = sys.call(-1), .topenv = parent.frame()) { ret <- paste(attr(level, 'level'), msg) attr(ret, 'severity') <- switch( attr(level, 'level', exact = TRUE), 'FATAL' = 'CRITICAL', 'ERROR' = 'ERR', 'WARN' = 'WARNING', 'SUCCESS' = 'NOTICE', 'INFO' = 'INFO', 'DEBUG' = 'DEBUG', 'TRACE' = 'DEBUG') return(ret) }, generator = quote(layout_syslognet()) ) #nocov end logger/R/appenders.R0000644000176200001440000005156614571077771014073 0ustar liggesusers#' Dummy appender not delivering the log record to anywhere #' @param lines character vector #' @export appender_void <- structure(function(lines) {}, generator = quote(appender_void())) #' Append log record to stderr #' @param lines character vector #' @export #' @aliases appender_stderr #' @usage #' appender_console(lines) #' #' appender_stderr(lines) #' @seealso This is a \code{\link{log_appender}}, for alternatives, see eg \code{\link{appender_stdout}}, \code{\link{appender_file}}, \code{\link{appender_tee}}, \code{\link{appender_slack}}, \code{\link{appender_pushbullet}}, \code{\link{appender_telegram}}, \code{\link{appender_syslog}}, \code{\link{appender_kinesis}} and \code{\link{appender_async}} for evaluate any \code{\link{log_appender}} function in a background process. appender_console <- structure(function(lines) { cat(lines, file = stderr(), sep = '\n') }, generator = quote(appender_console())) #' @export appender_stderr <- appender_console #' Append log record to stdout #' @param lines character vector #' @export #' @seealso This is a \code{\link{log_appender}}, for alternatives, see eg \code{\link{appender_console}}, \code{\link{appender_file}}, \code{\link{appender_tee}}, \code{\link{appender_slack}}, \code{\link{appender_pushbullet}} appender_stdout <- structure(function(lines) { cat(lines, sep = '\n') }, generator = quote(appender_stdout())) #' Append log messages to a file #' #' Log messages are written to a file with basic log rotation: when max number of lines or bytes is defined to be other than \code{Inf}, then the log file is renamed with a \code{.1} suffix and a new log file is created. The renaming happens recursively (eg \code{logfile.1} renamed to \code{logfile.2}) until the specified \code{max_files}, then the oldest file (\code{logfile.{max_files-1}}) is deleted. #' @param file path #' @param append boolean passed to \code{cat} defining if the file should be overwritten with the most recent log message instead of appending #' @param max_lines numeric specifying the maximum number of lines allowed in a file before rotating #' @param max_bytes numeric specifying the maximum number of bytes allowed in a file before rotating #' @param max_files integer specifying the maximum number of files to be used in rotation #' @export #' @return function taking \code{lines} argument #' @seealso This is generator function for \code{\link{log_appender}}, for alternatives, see eg \code{\link{appender_console}}, \code{\link{appender_tee}}, \code{\link{appender_slack}}, \code{\link{appender_pushbullet}}, \code{\link{appender_telegram}}, \code{\link{appender_syslog}}, \code{\link{appender_kinesis}} and \code{\link{appender_async}} for evaluate any \code{\link{log_appender}} function in a background process. #' @examples \dontrun{ #' ## ########################################################################## #' ## simple example logging to a file #' t <- tempfile() #' log_appender(appender_file(t)) #' for (i in 1:25) log_info(i) #' readLines(t) #' #' ## ########################################################################## #' ## more complex example of logging to file #' ## rotated after every 3rd line up to max 5 files #' #' ## create a folder storing the log files #' t <- tempfile(); dir.create(t) #' f <- file.path(t, 'log') #' #' ## define the file logger with log rotation enabled #' log_appender(appender_file(f, max_lines = 3, max_files = 5L)) #' #' ## log 25 messages #' for (i in 1:25) log_info(i) #' #' ## see what was logged #' lapply(list.files(t, full.names = TRUE), function(t) { #' cat('\n##', t, '\n') #' cat(readLines(t), sep = '\n') #' }) #' #' ## enable internal logging to see what's actually happening in the logrotate steps #' log_threshold(TRACE, namespace = '.logger') #' ## run the above commands again #' } appender_file <- function(file, append = TRUE, max_lines = Inf, max_bytes = Inf, max_files = 1L) { force(file) force(append) force(max_lines) force(max_bytes) force(max_files) if (!is.integer(max_files) || max_files < 1) { stop('max_files must be a positive integer') } structure( function(lines) { if (is.finite(max_lines) | is.finite(max_bytes)) { fail_on_missing_package('R.utils') n_lines <- tryCatch( suppressWarnings(R.utils::countLines(file)), error = function(e) 0) n_bytes <- ifelse(file.exists(file), file.info(file)$size, 0) if (n_lines >= max_lines || n_bytes >= max_bytes) { log_trace( 'lines: %s, max_lines: %s, bytes: %s, max_bytes: %s', n_lines, max_lines, n_bytes, max_bytes, namespace = '.logger') log_trace( 'lines >= max_lines || bytes >= max_bytes: %s', n_lines >= max_lines || n_bytes >= max_bytes, namespace = '.logger') for (i in max_files:1) { ## just kill the old file if (i == 1) { log_trace('killing the main file: %s', file, namespace = '.logger') unlink(file) } else { ## rotate the old file new <- paste(file, i - 1, sep = '.') if (i == 2) { old <- file } else { old <- paste(file, i - 2, sep = '.') } if (file.exists(old)) { log_trace('renaming %s to %s', old, new, namespace = '.logger') file.rename(old, new) } ## kill the rotated, but not needed file if (i > max_files) { log_trace('killing the file with too many rotations: %s', new, namespace = '.logger') unlink(new) } } } } } log_trace('logging %s to %s', shQuote(lines), file, namespace = '.logger') cat(lines, sep = '\n', file = file, append = append) }, generator = deparse(match.call())) } #' Append log messages to a file and stdout as well #' #' This appends log messages to both console and a file. The same rotation options are available as in \code{\link{appender_file}}. #' @inheritParams appender_file #' @export #' @return function taking \code{lines} argument #' @seealso This is generator function for \code{\link{log_appender}}, for alternatives, see eg \code{\link{appender_console}}, \code{\link{appender_file}}, \code{\link{appender_slack}}, \code{\link{appender_pushbullet}}, \code{\link{appender_telegram}}, \code{\link{appender_syslog}}, \code{\link{appender_kinesis}} and \code{\link{appender_async}} for evaluate any \code{\link{log_appender}} function in a background process. appender_tee <- function(file, append = TRUE, max_lines = Inf, max_bytes = Inf, max_files = 1L) { force(file) force(append) force(max_lines) force(max_bytes) force(max_files) structure( function(lines) { appender_console(lines) appender_file(file, append, max_lines, max_bytes, max_files)(lines) }, generator = deparse(match.call())) } #' Send log messages to a Slack channel #' @param channel Slack channel name with a hashtag prefix for public channel and no prefix for private channels #' @param username Slack (bot) username #' @param icon_emoji optional override for the bot icon #' @param api_token Slack API token #' @param preformatted use code tags around the message? #' @return function taking \code{lines} argument #' @export #' @note This functionality depends on the \pkg{slackr} package. #' @seealso This is generator function for \code{\link{log_appender}}, for alternatives, see eg \code{\link{appender_console}}, \code{\link{appender_file}}, \code{\link{appender_tee}}, \code{\link{appender_pushbullet}}, \code{\link{appender_telegram}}, \code{\link{appender_syslog}}, \code{\link{appender_kinesis}} and \code{\link{appender_async}} for evaluate any \code{\link{log_appender}} function in a background process. appender_slack <- function(channel = Sys.getenv('SLACK_CHANNEL'), username = Sys.getenv('SLACK_USERNAME'), icon_emoji = Sys.getenv('SLACK_ICON_EMOJI'), api_token = Sys.getenv('SLACK_API_TOKEN'), preformatted = TRUE) { fail_on_missing_package('slackr', '1.4.1') force(channel) force(username) force(icon_emoji) force(api_token) force(preformatted) structure( function(lines) { slackr::slackr_msg( text = lines, channel = channel, username = username, icon_emoji = icon_emoji, token = api_token, preformatted = preformatted) }, generator = deparse(match.call())) } #' Send log messages to Pushbullet #' @param ... parameters passed to \code{pbPost}, such as \code{recipients} or \code{apikey}, although it's probably much better to set all these in the \code{~/.rpushbullet.json} as per package docs at \url{http://dirk.eddelbuettel.com/code/rpushbullet.html} #' @export #' @note This functionality depends on the \pkg{RPushbullet} package. #' @seealso This is generator function for \code{\link{log_appender}}, for alternatives, see eg \code{\link{appender_console}}, \code{\link{appender_file}}, \code{\link{appender_tee}}, \code{\link{appender_slack}}, \code{\link{appender_telegram}}, \code{\link{appender_syslog}}, \code{\link{appender_kinesis}} and \code{\link{appender_async}} for evaluate any \code{\link{log_appender}} function in a background process. #' @export appender_pushbullet <- function(...) { fail_on_missing_package('RPushbullet') structure( function(lines) { RPushbullet::pbPost(type = 'note', body = paste(lines, sep = '\n'), ...) }, generator = deparse(match.call())) } #' Send log messages to a Telegram chat #' @param chat_id Unique identifier for the target chat or username of the target channel (in the format @channelusername) #' @param bot_token Telegram Authorization token #' @param parse_mode Message parse mode. Allowed values: Markdown or HTML #' @return function taking \code{lines} argument #' @export #' @note This functionality depends on the \pkg{telegram} package. #' @seealso This is generator function for \code{\link{log_appender}}, for alternatives, see eg \code{\link{appender_console}}, \code{\link{appender_file}}, \code{\link{appender_tee}}, \code{\link{appender_pushbullet}}, \code{\link{appender_syslog}}, \code{\link{appender_kinesis}} and \code{\link{appender_async}} for evaluate any \code{\link{log_appender}} function in a background process. appender_telegram <- function(chat_id = Sys.getenv('TELEGRAM_CHAT_ID'), bot_token = Sys.getenv('TELEGRAM_BOT_TOKEN'), parse_mode = NULL) { fail_on_missing_package('telegram') force(chat_id) force(bot_token) force(parse_mode) tb <- telegram::TGBot$new(token = bot_token) structure( function(lines) { tb$sendMessage(text = lines, parse_mode = parse_mode, chat_id = chat_id) }, generator = deparse(match.call())) } #' Send log messages to the POSIX system log #' @param identifier A string identifying the process. #' @param ... Further arguments passed on to \code{\link[rsyslog]{open_syslog}}. #' @return function taking \code{lines} argument #' @export #' @note This functionality depends on the \pkg{rsyslog} package. #' @seealso This is generator function for \code{\link{log_appender}}, for alternatives, see eg \code{\link{appender_console}}, \code{\link{appender_file}}, \code{\link{appender_tee}}, \code{\link{appender_pushbullet}}, \code{\link{appender_telegram}}, \code{\link{appender_kinesis}} and \code{\link{appender_async}} for evaluate any \code{\link{log_appender}} function in a background process. #' @examples \dontrun{ #' if (requireNamespace("rsyslog", quietly = TRUE)) { #' log_appender(appender_syslog("test")) #' log_info("Test message.") #' } #' } appender_syslog <- function(identifier, ...) { fail_on_missing_package('rsyslog') rsyslog::open_syslog(identifier = identifier, ...) structure( function(lines) { for (line in lines) { rsyslog::syslog(line) } }, generator = deparse(match.call()) ) } #nocov start #' Send log messages to a network syslog server #' @param identifier program/function identification (string). #' @param server machine where syslog daemon runs (string). #' @param port port where syslog daemon listens (integer). #' #' @return A function taking a \code{lines} argument. #' @export #' @note This functionality depends on the \pkg{syslognet} package. #' @examples \dontrun{ #' if (requireNamespace("syslognet", quietly = TRUE)) { #' log_appender(appender_syslognet("test_app", 'remoteserver')) #' log_info("Test message.") #' } #' } appender_syslognet <- function(identifier, server, port = 601L) { fail_on_missing_package('syslognet') force(identifier) force(server) force(port) structure( function(lines) { sev <- attr(lines, 'severity', exact = TRUE) for (line in lines) { syslognet::syslog(line, sev, app_name = identifier, server = server, port = port) } }, generator = deparse(match.call()) ) } #nocov end #' Send log messages to a Amazon Kinesis stream #' @param stream name of the Kinesis stream #' @return function taking \code{lines} and optional \code{partition_key} argument #' @export #' @note This functionality depends on the \pkg{botor} package. #' @seealso This is generator function for \code{\link{log_appender}}, for alternatives, see eg \code{\link{appender_console}}, \code{\link{appender_file}}, \code{\link{appender_tee}}, \code{\link{appender_pushbullet}}, \code{\link{appender_telegram}}, \code{\link{appender_syslog}} and \code{\link{appender_async}} for evaluate any \code{\link{log_appender}} function in a background process. appender_kinesis <- function(stream) { fail_on_missing_package('botor') force(stream) structure( function(lines, partition_key = NA_character_) { for (line in lines) { botor::kinesis()$put_record(StreamName = stream, Data = line, PartitionKey = partition_key) } }, generator = deparse(match.call()) ) } #' Delays executing the actual appender function to the future in a background process to avoid blocking the main R session #' @param appender a \code{\link{log_appender}} function with a \code{generator} attribute (TODO note not required, all fn will be passed if not) #' @param batch number of records to process from the queue at once #' @param namespace \code{logger} namespace to use for logging messages on starting up the background process #' @param init optional function to run in the background process that is useful to set up the environment required for logging, eg if the \code{appender} function requires some extra packages to be loaded or some environment variables to be set etc #' @return function taking \code{lines} argument #' @export #' @note This functionality depends on the \pkg{txtq} and \pkg{callr} packages. The R session's temp folder is used for staging files (message queue and other forms of communication between the parent and child processes). #' @seealso This function is to be used with an actual \code{\link{log_appender}}, for example \code{\link{appender_console}}, \code{\link{appender_file}}, \code{\link{appender_tee}}, \code{\link{appender_pushbullet}}, \code{\link{appender_telegram}}, \code{\link{appender_syslog}} or \code{\link{appender_kinesis}}. #' @examples \dontrun{ #' appender_file_slow <- function(file) { #' force(file) #' function(lines) { #' Sys.sleep(1) #' cat(lines, sep = '\n', file = file, append = TRUE) #' } #' } #' #' ## log what's happening in the background #' log_threshold(TRACE, namespace = 'async_logger') #' log_appender(appender_console, namespace = 'async_logger') #' #' ## start async appender #' t <- tempfile() #' log_info('Logging in the background to {t}') #' my_appender <- appender_async(appender_file_slow(file = t)) #' #' ## use async appender #' log_appender(my_appender) #' log_info('Was this slow?') #' system.time(for (i in 1:25) log_info(i)) #' #' readLines(t) #' Sys.sleep(10) #' readLines(t) #' #' ## check on the async appender (debugging, you will probably never need this) #' attr(my_appender, 'async_writer_queue')$count() #' attr(my_appender, 'async_writer_queue')$log() #' #' attr(my_appender, 'async_writer_process')$get_pid() #' attr(my_appender, 'async_writer_process')$get_state() #' attr(my_appender, 'async_writer_process')$poll_process(1) #' attr(my_appender, 'async_writer_process')$read() #' #' attr(my_appender, 'async_writer_process')$is_alive() #' attr(my_appender, 'async_writer_process')$read_error() #' } appender_async <- function(appender, batch = 1, namespace = 'async_logger', init = function() log_info('Background process started')) { fail_on_missing_package('txtq') fail_on_missing_package('callr') force(appender) force(batch) ## create a storage for the message queue async_writer_storage <- tempfile() log_trace(paste('Async writer storage:', async_writer_storage), namespace = 'async_logger') ## initialize the message queue async_writer_queue <- txtq::txtq(async_writer_storage) ## start a background process for the async execution of the message queue ## TODO make it easy to create multiple/parallel background processes? async_writer_process <- callr::r_session$new() log_trace(paste('Async writer PID:', async_writer_process$get_pid()), namespace = 'async_logger') ## load minimum required packages async_writer_process$run(function() source(system.file( 'load-packages-in-background-process.R', package = 'logger'))) async_writer_process$run(init) ## connect to the message queue async_writer_process$run(assign, args = list(x = 'async_writer_storage', value = async_writer_storage)) async_writer_process$run(function() async_writer_queue <<- txtq::txtq(async_writer_storage)) ## pass arguments async_writer_process$run(assign, args = list(x = 'batch', value = batch)) ## pass appender async_writer_tempfile <- tempfile() saveRDS(appender, async_writer_tempfile) log_trace(paste('Async appender cached at:', async_writer_tempfile), namespace = 'async_logger') async_writer_process$run(assign, args = list(x = 'async_writer_tempfile', value = async_writer_tempfile)) async_writer_process$run(assign, args = list(x = 'appender', value = readRDS(async_writer_tempfile))) ## start infinite loop processing log records async_writer_process$call(function() { while (TRUE) { items <- async_writer_queue$pop(batch) if (nrow(items) == 0) { ## avoid burning CPU Sys.sleep(.1) } else { ## execute the actual appender for each log item for (i in seq_len(nrow(items))) { appender(items$message[i]) } ## remove processed log records async_writer_queue$clean() } } }) structure( function(lines) { ## check if background process still works if (!isTRUE(async_writer_process$is_alive())) { stop('FATAL: Async writer process not found') } remote_error <- async_writer_process$read_error() if (remote_error != '') { stop(paste('FATAL: Async writer failed with', shQuote(remote_error))) } remote_event <- async_writer_process$read() if (!is.null(remote_event) && !is.null(remote_event$error)) { stop(paste( 'FATAL: Async writer error of', shQuote(remote_event$error$message), 'in', shQuote(paste(deparse(remote_event$error$call), collapse = ' ')))) } ## write to message queue for (line in lines) { async_writer_queue$push(title = as.character(as.numeric(Sys.time())), message = line) } }, generator = deparse(match.call()), ## share remote process and queue with parent for debugging purposes async_writer_storage = async_writer_storage, async_writer_queue = async_writer_queue, async_writer_process = async_writer_process) ## NOTE no need to clean up, all will go away with the current R session's temp folder } ## TODO other appenders: graylog, datadog, cloudwatch, email via sendmailR, ES etc logger/NEWS.md0000644000176200001440000001177214571556264012656 0ustar liggesusers# logger 0.3.0 (2024-03-03) Many unrelated small features, fixes and documentation updates collected over 2+ years. ## New features * update `log_*` functions to invisibly return the formatted log message and record (#26, @r2evans) * add `namespace` argument to `log_shiny_input_changes` (#93, @kpagacz) * optionally suppress messages in `globalCallingHandlers` after being logged (#100, @DanChaltiel) * `as.loglevel` helper to convert string/number to loglevel (requested by @tentacles-from-outer-space) * new formatter function: `formatter_glue_safe` (#126, @terashim) * support `OFF` log level (#138, @pawelru) * override default `INFO` log level via env var (#145, requested by sellorm) ## Fixes * handle zero-length messages in `formatter_glue_or_sprintf` (#74, @deeenes) * generalize `log_separator` to work with all layout functions (#96, @Polkas) * support log levels in `log_shiny_input_changes` (#103, @taekeharkema) * fix fn name lookup/reference with nested calls (#120, reported by @averissimo) * force the `file` argument of `appender_tee` (#124, reported by @dbontemps) * don't allow stacking logger hooks on messages/warnings/errors (reported by @jkeuskamp) * improve fragile test case when `Hmisc` loaded (#131, @r2evans) * pass `index`, `namespace` etc from `log_` functions down to `log_level` (#143, @MichaelChirico) * refer to the caller function in global message logger hooks (#146, reported by @gabesolomon10) # logger 0.2.2 (2021-10-10) Maintenance release: * fix unbalanced code chunk delimiters in vignette (yihui/knitr#2057) # logger 0.2.1 (2021-07-06) Maintenance release: * update `appender_slack` to use `slackr_msg` instead of `text_slackr` # logger 0.2.0 (2021-03-03) ## Breaking changes * `appender_console` writes to `stderr` by default instead of `stdout` (#28) ## Fixes * default date format in `glue` layouts (#44, @burgikukac) * `fn` reference in loggers will not to a Cartesian join on the log lines and message, but merge (and clean up) the `fn` even for large anonymous functions (#20) ## New features * allow defining the log level threshold as a string (#13, @artemklevtsov) * allow updating the log level threshold, formatter, layout and appender in all namespaces with a single call (#50) * new argument to `appender_file` to optionally truncate before appending (#24, @eddelbuettel) * new arguments to `appender_file` to optionally rotate the log files after appending (#42) * new meta variables for logging in custom layouts: R version and calling package's version * improved performance by not evaluating arguments when the log record does not meet the log level threshold (#27, @jozefhajnala) * `logger` in now part of the Mikata Project: https://mikata.dev ## New helper functions * `%except%`: evaluate an expression with fallback * `log_separator`: logging with separator lines (#16) * `log_tictoc`: tic-toc logging (#16, @nfultz) * `log_failure`: log error before failing (#19, @amy17519) * `log_messages`, `log_warnings`, `log_errors`: optionally auto-log messages, warnings and errors using `globalCallingHandlers` on R 4.0.0 and above, and injecting `logger` calls to `message`, `warnings` and `stop` below R 4.0.0 * `log_shiny_input_changes`: auto-log input changes in Shiny apps (#25) ## New formatter functions * `layout_pander`: transform R objects into markdown before logging (#22) ## New layout functions * `layout_blank`: blank log messages without any modification * `layout_json_parser`: render the layout as a JSON blob after merging with requested meta fields ## New appender functions * `appender_telegram`: deliver log records to Telegram (#14, @artemklevtsov) * `appender_syslog`: deliver log records to syslog (#30, @atheriel) * `appender_kinesis`: deliver log records to Amazon Kinesis (#35) * `appender_async`: wrapper function for other appender functions to deliver log records in a background process asynchronously without blocking the master process (#35) # logger 0.1 (2018-12-20) Initial CRAN release after collecting feedback for a month on Twitter at `https://twitter.com/daroczig/status/1067461632677330944`: * finalized design of a log request defined by * a log level `threshold`, * a `formatter` function preparing the log message, * a `layout` function rendering the actual log records and * an `appender` function delivering to the log destination * detailed documentation with 7 vignettes and a lot of examples, even some benchmarks * ~75% code coverage for unit tests * 5 `formatter` functions mostly using `paste`, `sprintf` and `glue` * 6 `layout` functions with convenient wrappers to let users define custom layouts via `glue` or `JSON`, including colorized output * 5 `appender` functions delivering log records to the console, files, Pushbullet and Slack * helper function to evaluate an expressions with auto-logging both the expression and its result * helper function to temporarily update the log level threshold * helper function to skip running the formatter function on a log message * mostly backward compatibly with the `logging` and `futile.logger` packages logger/MD50000644000176200001440000001650714571643063012063 0ustar liggesusers87c0bad1f3fabc5712e3784e03d16db3 *DESCRIPTION 42dae1fc66085beb4e36f7c596006d4b *NAMESPACE 432c7bc6e2cb9b451eb058f213a25a31 *NEWS.md 9737e6447cdcc214bcf78b4659399fc6 *R/appenders.R 01ac0d3df5520bc53fc2fc99ea748256 *R/color.R f66d6ecef7fdeb79f66ae4bf84a5cfc5 *R/formatters.R c888ffaedc0dcea4aaf31c4600e02142 *R/helpers.R 2c05160c4d6ae844c774fc0d55ff2433 *R/hooks.R 0e68d86da55f3450cd2326f616608051 *R/layouts.R a108dc042bfec62e85d50a583b8fb8c1 *R/levels.R f15db21cf1f8ade723ffcbe7e04be98d *R/logger.R fac86ceefee735e7ab35be4b152a999e *R/try.R dd689840b0f9895ff624b2f347bed042 *R/utils.R de746e30c69ba588e954a9eb2943e163 *R/zzz.R c04cfe7cae85d19bab1c62dab56bb935 *README.md 40080c268999cec4bc74bd22b6590517 *build/vignette.rds 7b2a5381086e54f10441934fe3578066 *demo/00Index 46b13f35a75b750c5835f0795bf1ba0a *demo/colors.R fc90df0b1f2faedad2187989f1ac7227 *inst/demo-packages/logger-tester-package/DESCRIPTION 6e6bb5a18a924cd3145a7cdb89002d9c *inst/demo-packages/logger-tester-package/NAMESPACE 9264f73b76a78e39ff86732bc0bab239 *inst/demo-packages/logger-tester-package/R/tester.R cb802d58a70408eebdb8ab9155ff0212 *inst/demo-packages/logger-tester-package/man/logger_tester_function.Rd 659c209b7dbdef346f37e955e8031670 *inst/doc/Intro.R ad4c19505935397ac062a0417329159f *inst/doc/Intro.Rmd e9c61faa4e41d1df57b26832761f487d *inst/doc/Intro.html b0e7799e51882ee553885dab17a648b4 *inst/doc/anatomy.R 9f14b72be6ff636bcf8dd72cb103481c *inst/doc/anatomy.Rmd f51ff540496b6e20877606f979688e95 *inst/doc/anatomy.html ea7a59a421139e6429e37ff39c6699a6 *inst/doc/customize_logger.R 0155de61041fa9418f1dd6a5179305cf *inst/doc/customize_logger.Rmd a7ce0cb1d9092cd597c6106dc63fa2c2 *inst/doc/customize_logger.html 30d35d75bf44ca320ddfb9b75f857869 *inst/doc/migration.R 73582fad1a0f01b77e02cbaa3608a80b *inst/doc/migration.Rmd 38a2d7b2d8e935d5e86dc979ab40277f *inst/doc/migration.html 3e00ce3697cb52add51da4235f4ae09b *inst/doc/performance.Rmd 60493bf83725002189514b0f7ca85c64 *inst/doc/performance.html 64a8f21b9b9444da1cf93238f49882e6 *inst/doc/r_packages.R 2df9a86a723724fb02e595b4be466363 *inst/doc/r_packages.Rmd 309a65c5676c16240352d5fccb4805f3 *inst/doc/r_packages.html 46750e778a5cdb4d65c347f8e43df623 *inst/doc/write_custom_extensions.R b0a92c6cf1ee29fc43907cc9d49ca9e2 *inst/doc/write_custom_extensions.Rmd d52adf1af177397484a005edeb04b40c *inst/doc/write_custom_extensions.html 70877f1152540568dc4cc22e55f3c459 *inst/load-packages-in-background-process.R 70b4ea6fda982e23e917ba29a572bba4 *man/appender_async.Rd 3c5d86341d13808cff6e6a3687e1f23d *man/appender_console.Rd ad3b1886d697d32eabfdbbf945565f79 *man/appender_file.Rd 10410fba31f521cfc4211512d1622320 *man/appender_kinesis.Rd b5fb33f3eb80b1bd309ed97b17968136 *man/appender_pushbullet.Rd 9220928de3936d78b46b3abe626d3a03 *man/appender_slack.Rd 846c6f42f844dfabdab3d946950d9648 *man/appender_stdout.Rd 24d519e48c19c94cd8c004cb87291bf2 *man/appender_syslog.Rd 5f87f74c91875d000be98d1fe47f0a9e *man/appender_syslognet.Rd d6a795b8b6879485aae397e0e9c23592 *man/appender_tee.Rd 2155927f4452c80ac2e05c784ca34749 *man/appender_telegram.Rd 4bf0af4c9573a5f3459189b0c147463d *man/appender_void.Rd f219d75c9f054dedb013081eac67e8ca *man/as.loglevel.Rd e08b3b7143568ee7d053b624b8a8f7ce *man/catch_base_log.Rd 069426befe00f52fa4a9edc43725ec7c *man/colorize_by_log_level.Rd b53cd4955f12f2c69141c0e7593cc3ac *man/delete_logger_index.Rd 1d71ec136036580e8ecd34ee1afbbf0c *man/deparse_to_one_line.Rd aea06d1e05a69b31d7cadc01cb652957 *man/fail_on_missing_package.Rd c9906c4422b8fb9a15a0bac36e5824d5 *man/fallback_namespace.Rd 5ac80bf744bb1b3953f462627e58bb00 *man/figures/colors.png 6811f1d3af4921628cf6d4da67454dcb *man/formatter_glue.Rd 9d1c5f9b43992d3a434c2cea5420e9ab *man/formatter_glue_or_sprintf.Rd d482b4985d90c5ca2caff7bc2e11aa82 *man/formatter_glue_safe.Rd e6196aee1785001e413bf5029980b4de *man/formatter_json.Rd baf4275a588cb53abb574b05c7d82316 *man/formatter_logging.Rd ea426635d9c5fb2d53ac50f30dff5c2d *man/formatter_pander.Rd c9d1a0f2ed7dcc15193a6eabadf6c765 *man/formatter_paste.Rd 8c3525eb63ffdc7f91f2babc20d1c87a *man/formatter_sprintf.Rd 1f4cca46d0b1d4a956c4539ef5b862ff *man/get_logger_definitions.Rd e5fac3ca7d90b9dd52c2fababfbb0f3c *man/get_logger_meta_variables.Rd e9e1ea30d60101b8453d04a3bf59765a *man/grapes-except-grapes.Rd 9d5043a48b273b577f903e8150527e09 *man/grayscale_by_log_level.Rd 56511b4a372ae805354772f850de0611 *man/layout_blank.Rd d45fba26b7ec19fcac2e73c9b6d64545 *man/layout_glue.Rd 60a987679b360917b77df99f9456e788 *man/layout_glue_colors.Rd 9a90a0dd8f207607d7a8290697d0342a *man/layout_glue_generator.Rd f349d9ad627c3f13ba24226934e70b01 *man/layout_json.Rd d487e876af98d6caaea68c4cea27cb3c *man/layout_json_parser.Rd fcfbc26d30d898d18a2ab98acba2987e *man/layout_logging.Rd dbbdccc950dc36ac0c501e9edae25055 *man/layout_simple.Rd c488e6cac93634f5a1afa66630928e61 *man/layout_syslognet.Rd 90905c8028d9c5bb435f7ec1557df4d3 *man/log_appender.Rd 05277ab5c8a1e4e40e94b3cdb2de45f0 *man/log_config_setter.Rd de242e56f86be139d4b6365b32ccbbef *man/log_errors.Rd bd3533d6682db5e30e81567978cfd47e *man/log_eval.Rd be63bba37951de5a5c9f7a1f922e1dfd *man/log_failure.Rd e4e8d400a0282f1bbf781bf0ea94a8c2 *man/log_formatter.Rd ab90b2e0755c7d274ef8be0706c846ba *man/log_layout.Rd 93685610f747d6d3c00a2d46a7a283cf *man/log_level.Rd 8cab15c8309deaa2a278e342b29ae072 *man/log_levels.Rd a852acf9a32df25aaaaad32052aed66f *man/log_messages.Rd e552f79f09fa298ef361123d946cbb53 *man/log_namespaces.Rd 1afb0fdcad0c23bae069615629c028ba *man/log_separator.Rd ffbe22a3ddc945739eb6788872ce9556 *man/log_shiny_input_changes.Rd bae7f4badcc1bbe99d39983e78be207c *man/log_threshold.Rd 2da88845d4546f7d28c72d5e25705ee9 *man/log_tictoc.Rd 2e65cccf9037897db800d128f9452cb7 *man/log_warnings.Rd 099f4e2f794f298a7ca1f6f9d3530933 *man/log_with_separator.Rd fc9512e9043d48c7cda4467ab6f70ba4 *man/logger.Rd 46b2465757f77a81b07e866461dc833f *man/skip_formatter.Rd 9a603c307d470a8e9c8c5ed5e8befa36 *man/top_env_name.Rd f72ba9a37bfac87e8386b776138d0684 *man/validate_log_level.Rd 371331576fcb013d302064eb12d24095 *man/warn_if_globalCallingHandlers_is_not_available.Rd eb8c228b15756c7456527b335e033e9c *man/with_log_threshold.Rd 199f29372639c63929db225131b213eb *tests/testthat.R 121ed42d8ed6eadc6b30da44a9891a6a *tests/testthat/test-CRANSKIP-appenders.R 7ca93ec707f728e9cda052c5f117171f *tests/testthat/test-CRANSKIP-helpers.R 9a742f628a6cac76a9212d87b79c03b9 *tests/testthat/test-CRANSKIP-hooks.R df232e0f2d566ed6e435d9da32f3e923 *tests/testthat/test-CRANSKIP-logger-namespaces.R 4a2f630401e76b7d07aaacd75b29c474 *tests/testthat/test-appender.R 115237a5dcfdc57276b749de84f753f2 *tests/testthat/test-eval.R 1035bfb2979b68ec1240bcc8d93d2dd8 *tests/testthat/test-formatters.R 5ee08bb1bc4a33dad4cd0ecf09f31fda *tests/testthat/test-helpers.R 6c25d3e32b2667962ec1248204564323 *tests/testthat/test-layout.R ce9753293c6f16d05f778fc4b7cb79f9 *tests/testthat/test-logger.R 77869df00a9e2a0727e2851e6079efa5 *tests/testthat/test-return.R 124cb4424e93174dfddcd1e1a7e42bab *tests/testthat/test-utils.R ad4c19505935397ac062a0417329159f *vignettes/Intro.Rmd 9f14b72be6ff636bcf8dd72cb103481c *vignettes/anatomy.Rmd 0155de61041fa9418f1dd6a5179305cf *vignettes/customize_logger.Rmd e4d0b90c553ae4fcb97db1ceac8bb7ae *vignettes/logger_structure.svg 73582fad1a0f01b77e02cbaa3608a80b *vignettes/migration.Rmd 3e00ce3697cb52add51da4235f4ae09b *vignettes/performance.Rmd 2df9a86a723724fb02e595b4be466363 *vignettes/r_packages.Rmd b0a92c6cf1ee29fc43907cc9d49ca9e2 *vignettes/write_custom_extensions.Rmd logger/inst/0000755000176200001440000000000014571623750012520 5ustar liggesuserslogger/inst/demo-packages/0000755000176200001440000000000013406263046015212 5ustar liggesuserslogger/inst/demo-packages/logger-tester-package/0000755000176200001440000000000013404032211021350 5ustar liggesuserslogger/inst/demo-packages/logger-tester-package/NAMESPACE0000644000176200001440000000020613404032211022565 0ustar liggesusers# Generated by roxygen2: do not edit by hand export(logger_tester_function) importFrom(logger,log_level) importFrom(logger,log_info) logger/inst/demo-packages/logger-tester-package/man/0000755000176200001440000000000013375503224022141 5ustar liggesuserslogger/inst/demo-packages/logger-tester-package/man/logger_tester_function.Rd0000644000176200001440000000050413375503224027201 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/tester.R \name{logger_tester_function} \alias{logger_tester_function} \title{Testing logging from package} \usage{ logger_tester_function(level, msg) } \arguments{ \item{level}{foo} \item{msg}{bar} } \description{ Testing logging from package } logger/inst/demo-packages/logger-tester-package/DESCRIPTION0000644000176200001440000000065713373664653023120 0ustar liggesusersType: Package Package: logger.tester Authors@R: c( person("Gergely", "Daroczi", , "daroczig@rapporter.net", role = c("aut", "cre")), person("System1", role = c("fnd")) ) Title: Dummy package providing testing functions for logger Description: Dummy package providing testing functions for logger Version: 0.1 Date: 2018-07-04 URL: https://github.com/daroczig/logger RoxygenNote: 6.1.0 License: AGPL-3 Imports: logger logger/inst/demo-packages/logger-tester-package/R/0000755000176200001440000000000013404033327021562 5ustar liggesuserslogger/inst/demo-packages/logger-tester-package/R/tester.R0000644000176200001440000000064313404033327023216 0ustar liggesusers#' Testing logging from package #' @param level foo #' @param msg bar #' @export #' @importFrom logger log_level logger_tester_function <- function(level, msg) { x <- runif(1) log_level(level, '{msg} {x}') } #' Testing logging INFO from package #' @param msg bar #' @export #' @importFrom logger log_info logger_info_tester_function <- function(msg) { everything <- 42 log_info('{msg} {everything}') } logger/inst/load-packages-in-background-process.R0000644000176200001440000000027714017724077021540 0ustar liggesusers## this is to be called in the background process of appender_async ## because having library/require calls in that function throws false R CMD check alerts require('logger') require('txtq') logger/inst/doc/0000755000176200001440000000000014571623750013265 5ustar liggesuserslogger/inst/doc/anatomy.Rmd0000644000176200001440000000773514020006627015400 0ustar liggesusers--- title: "The Anatomy of a Log Request" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{The Anatomy of a Log Request} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r setup, include = FALSE} knitr::opts_chunk$set( collapse = TRUE, comment = "#>" ) library(logger) ## backup settings oldconf <- list( threshold = log_threshold(), layout = log_layout(), formatter = log_formatter(), appender = log_appender()) ## knitr not picking up stderr log_appender(appender_stdout) ``` ```{r loggerStructureImage, echo=FALSE, out.extra='style="width: 100%;" title="The structure of a logger and the flow of a log record request" alt="The structure of a logger and the flow of a log record request"'} knitr::include_graphics("logger_structure.svg") ``` To make a successful log record, `logger` requires the below components: - a **log request**, eg ```r log_error('Oops') ``` - including the log level (importance) of the record, which will be later used to decide if the log record is to be delivered or not: `ERROR` in this case - R objects to be logged: a simple string in this case, although it could be a character vector or any R object(s) that can be converted into a character vector by the `formatter` function - the **environment** and meta-information of the log request, eg actual timestamp, hostname of the computer, the name of the user running the R script, the pid of the R process, calling function and the actual call etc. ```{r} f <- function() get_logger_meta_variables(log_level = INFO) f() ``` - a **logger definition** to process the log request, including - log level `threshold`, eg `INFO`, which defines the minimum log level required for actual logging -- all log requests with lower log level will be thrown away ```{r} log_threshold() ERROR <= INFO log_error('Oops') ``` - `formatter` function, which takes R objects and converts those into actual log message(s) to be then passed to the `layout` function for the log record rendering -- such as `paste`, `sprintf`, `glue` or eg the below custom example: ```{r} formatter <- function(...) paste(..., collapse = ' ', sep = ' ') formatter(1:3, c('foo', 'bar')) ``` - `layout` function, which takes log message(s) and further information on the log request (such as timestamp, hostname, username, calling function etc) to render the actual log records eg human-readable text, JSON etc ```r library(jsonlite) layout <- function(level, msg) toJSON(level = level, timestamp = time, hostname = node, message = msg) layout(INFO, 'Happy Thursday!') #> {'level': 'INFO', 'timestamp': '1970-01-01 00:00:00', 'hostname': 'foobar', 'message': 'Happy Thursday!'} ``` - `appender` function, which takes fully-rendered log record(s) and delivers to somewhere, eg `stdout`, a file or a streaming service, eg ```{r} appender <- function(line) cat(line, '\n') appender('INFO [now] I am a log message') ``` Putting all these together (by explicitly setting the default config in the `global` namespace): ```{r} log_threshold(INFO) log_formatter(formatter_glue) log_layout(layout_simple) log_appender(appender_console) log_debug('I am a low level log message that will not be printed with a high log level threshold') log_warn('I am a higher level log message that is very likely to be printed') ``` Note, that all `logger` definitions and requests are tied to a logging namespace, and one log request might trigger multiple `logger` definitions as well (stacking). Find more information on these in the [Customizing the format and destination of log records](https://daroczig.github.io/logger/articles/customize_logger.html) vignette. ```{r cleanup, include = FALSE} ## restore settings log_threshold(oldconf$threshold) log_layout(oldconf$layout) log_formatter(oldconf$formatter) log_appender(oldconf$appender) ``` logger/inst/doc/migration.html0000644000176200001440000235351014571623746016162 0ustar liggesusers Migration Guide
## Registered S3 method overwritten by 'log4r':
##   method         from  
##   print.loglevel logger

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

futile.logger

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

Initialize

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

futile.logger

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

logger

library(logger)

Logging functions

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

futile.logger

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

logger

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

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

Log levels

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

Log record layout

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

futile.logger

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

logger

log_layout(layout_json())

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

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

Log message formatting

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

futile.logger

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

logger

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

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

Log record destination

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

logging

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

logger

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

Hierarchical logging and performance

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

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

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

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

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

logging

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

Initialize

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

logging

library(logging)
basicConfig()

logger

library(logger)

Logging functions

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

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

logging

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

logger

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

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

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

Log levels

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

logging

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

logger

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

Performance

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

Log record layout

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

logging

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

logger

log_layout()
#> layout_simple

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

Log message formatting

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

logging

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

logger

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

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

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

Log record destination

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

logging

?addHandler
?writeToConsole
?writeToFile

logger

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

Hierarchical logging

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

Using logger as a drop-in-replacement of logging

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

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

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

log4r

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

Initialize

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

log4r

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

logger

library(logger)

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

Logging functions

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

log4r

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

logger

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

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

Log levels

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

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

Log record layout and formatter functions

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

Log record destination

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

Hierarchical logging and performance

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

loggit

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

#> Warning: 'logger' namespace cannot be unloaded:
#>   namespace 'logger' is imported by 'logger.tester' so cannot be unloaded
logger/inst/doc/r_packages.html0000644000176200001440000002627514571623750016266 0ustar liggesusers Logging from R Packages

Logging from R Packages

In this vignette I suppose that you are already familiar with Customizing the format and destination of log records vignette, especially with the Log namespaces section.

So that your R package’s users can suppress (or render with custom layout) the log messages triggered by your R package, it’s wise to record all those log messages in a custom namespace. By default, if you are calling the ?log_level function family from an R package after importing from the logger package, then logger will try to auto-guess the calling R package name and use that as the default namespace, see eg:

library(logger)
#> 
#> Attaching package: 'logger'
#> The following objects are masked from 'package:log4r':
#> 
#>     as.loglevel, logger
devtools::load_all(system.file('demo-packages/logger-tester-package', package = 'logger'))
#> ℹ Loading logger.tester
logger_tester_function(INFO, 'hi from tester package')

But if auto-guessing is not your style, then feel free to set your custom namespace (eg the name of your package) in all ?log_info etc function calls and let your users know about how to suppress / reformat / redirect your log messages via ?log_threshold, ?log_layout, ?log_appender.

Please note that setting the formatter function via ?log_formatter should not be left to the R package end-users, as the log message formatter is specific to your logging calls, so that should be decided by the R package author. Feel free to pick any formatter function (eg glue, sprintf, paste or something else), and set that via ?log_formatter when your R package is loaded. All other parameters of your logger will inherit from the global namespace – set by your R package’s end user.

logger/inst/doc/performance.Rmd0000644000176200001440000001224514020007524016216 0ustar liggesusers--- title: "Simple Benchmarks on logger Performance" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Simple Benchmarks on logger Performance} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- Although this has not been an important feature in the early development and overall design of this `logger` implementation, but with the default `?layout_simple` and `?formatter_glue`, it seems to perform pretty well when comparing with `futile.logger` and `logging` packages: ```r library(microbenchmark) ## fl library(futile.logger) t1 <- tempfile() flog.appender(appender.file(t1)) #> NULL ## lg library(logging) t2 <- tempfile() addHandler(writeToFile, file = t2) ## lr library(logger) #> The following objects are masked from ‘package:futile.logger’: DEBUG, ERROR, FATAL, INFO, TRACE, WARN t3 <- tempfile() log_appender(appender_file(t3)) string_fl <- function() flog.info('hi') string_lg <- function() loginfo('hi') string_lr <- function() log_info('hi') dynamic_fl <- function() flog.info('hi %s', 42) dynamic_lg <- function() loginfo('hi %s', 42) dynamic_lr <- function() log_info('hi {42}') vector_fl <- function() flog.info(paste('hi', 1:5)) vector_lg <- function() loginfo(paste('hi', 1:5)) vector_lr <- function() log_info('hi {1:5}') microbenchmark( string_fl(), string_lg(), string_lr(), vector_fl(), vector_lg(), vector_lr(), dynamic_fl(), dynamic_lg(), dynamic_lr(), times = 1e3) #> Unit: microseconds #> expr min lq mean median uq max neval #> string_fl() 1533.379 1650.7915 2510.5517 1759.9345 2885.4465 20835.425 1000 #> string_lg() 172.963 206.7615 315.6177 237.3150 335.3010 12738.735 1000 #> string_lr() 227.981 263.4715 390.7139 301.9045 409.0400 11926.974 1000 #> vector_fl() 1552.706 1661.7030 2434.0460 1766.7485 2819.5525 40892.197 1000 #> vector_lg() 198.338 234.2355 330.3268 266.7695 358.2510 9969.333 1000 #> vector_lr() 290.169 337.4730 592.0041 382.4335 537.5485 101946.435 1000 #> dynamic_fl() 1538.985 1663.7890 2564.6668 1782.1160 2932.7555 46039.686 1000 #> dynamic_lg() 188.213 226.5370 387.2470 255.1745 350.2015 60737.562 1000 #> dynamic_lr() 271.478 317.3350 486.1123 360.5815 483.5830 12070.936 1000 paste(t1, length(readLines(t1))) #> [1] "/tmp/Rtmp3Fp6qa/file7a8919485a36 7000" paste(t2, length(readLines(t2))) #> [1] "/tmp/Rtmp3Fp6qa/file7a89b17929f 7000" paste(t3, length(readLines(t3))) #> [1] "/tmp/Rtmp3Fp6qa/file289f24c88c41 7000" ``` So based on the above, non-comprehensive benchmark, it seems that when it comes to using the very base functionality of a logging engine, `logging` comes first, then `logger` performs with a bit of overhead due to using `glue` by default, then comes a bit slower `futile.logger`. On the other hand, there are some low-hanging fruits to improve performance, eg caching the `logger` function in the namespace, or using much faster message formatters (eg `paste0` or `sprintf` instead of `glue`) if needed -- like what `futile.logger` and `logging` are using instead of `glue`, so a quick `logger` comparison: ```r log_formatter(formatter_sprintf) string <- function() log_info('hi') dynamic <- function() log_info('hi %s', 42) vector <- function() log_info(paste('hi', 1:5)) microbenchmark(string(), vector(), dynamic(), times = 1e3) #> Unit: microseconds #> expr min lq mean median uq max neval cld #> string() 110.192 118.4850 148.5848 137.1825 152.7275 1312.903 1000 a #> vector() 129.111 136.8245 168.9274 155.5840 172.6795 3230.528 1000 b #> dynamic() 116.347 124.7620 159.1570 143.2140 160.5040 4397.640 1000 ab ``` Which suggests that `logger` is a pretty well-performing log framework. If you need even more performance with slower appenders, then asynchronous logging is your friend: passing the log messages to a reliable message queue, and a background process delivering those to the actual log destination in the background -- without blocking the main R process. This can be easily achieved in `logger` by wrapping any appender function in the `appender_async` function, such as: ```r ## demo log appender that's pretty slow appender_file_slow <- function(file) { force(file) function(lines) { Sys.sleep(1) cat(lines, sep = '\n', file = file, append = TRUE) } } ## create an async appender and start using it right away log_appender(appender_async(appender_file_slow(file = tempfile()))) async <- function() log_info('Was this slow?') microbenchmark(async(), times = 1e3) #> Unit: microseconds #> expr min lq mean median uq max neval #> async() 511.329 528.247 614.8694 558.2535 616.14 5018.731 1000 ``` Please note that although this ~0.6 ms is significantly higher than the ~0.15 ms we achieved above with the `sprintf` formatter, but this time we are calling an appender that would take 1 full second to deliver the log message (and not just printing to the console), so bringing that down to less than 1 millisecond is not too bad. If you need even higher throughput, then a custom `appender_async` without checking on the background process and potentially a faster message queue can bring this even below to 200 µs. logger/inst/doc/write_custom_extensions.Rmd0000644000176200001440000001143413406213410020717 0ustar liggesusers--- title: "Writing Custom Logger Extensions" vignette: > %\VignetteIndexEntry{Writing Custom Logger Extensions} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r setup, include = FALSE} knitr::opts_chunk$set( collapse = TRUE, comment = "#>" ) ``` In this vignette I suppose that you are already familiar with [Customizing the format and destination of log records](https://daroczig.github.io/logger/articles/customize_logger.html) vignette. ## Custom log message formatter functions The log message formatter function should be able to take any number of R objects and convert those into a character vector that is safe to pass to the layout function. This transformer function can be as simple as calling `paste`, `glue` or `sprintf` or something complex as well, eg looking up user attributes of a user id mentioned in a log record etc. When writing a custom formatter function, it should also accept the original logging function call as `.logcall`, the parent call as `.topcall` and the log request's environment as `.topenv`, which can be used to find the relevant variables in your formatter. If you are writing such a function or a generator function returning a log message formatter function, please keep the actual call resulting in the formatter function (eg `match.call()` in the generator function or the quoted function call) recorded in the `generator` attribute of the function so that `?log_formatter` can pretty-print that instead of the unnamed function body. See the [`formatters.R`](https://github.com/daroczig/logger/blob/master/R/formatters.R) file for more examples. ## Custom log layout rendering functions The layout functions return the log record and take at least two arguments: - the log level and - a message already formatted as a string by a log message formatter function - the namespace (as `namespace`), calling function (as `.topcall`) and its environment (as `.topenv`) of the log request, and the actual log call (as `.logcall`) automatically collected by `?log_level` Such layout rendering function can be as simple as the default `?layout_simple`: ```r layout_simple <- function(level, msg, ...) { paste0(attr(level, 'level'), ' [', format(Sys.time(), "%Y-%m-%d %H:%M:%S"), '] ', msg) } ``` Or much more complex, eg looking up the hostname of the machine, public IP address etc and logging all these automatically with the message of the log request. Your easiest option to set up a custom layout is calling `?layout_glue_generator` that comes with a nice API being able to access a bunch of meta-information on the log request via `?get_logger_meta_variables`. For such example, see the [Customizing the format and destination of log records](https://daroczig.github.io/logger/articles/customize_logger.html) vignette. If you are writing such a function or a generator function returning a log message formatter function, please keep the actual call resulting in the formatter function (eg `match.call()` in the generator function or the quoted function call) recorded in the `generator` attribute of the function so that `?log_layout` can pretty-print that instead of the unnamed function body. See the [`layouts.R`](https://github.com/daroczig/logger/blob/master/R/layouts.R) file for more examples. ## Custom log record appenders The appender functions take log records and delivers those to the desired destination. This can be as simple as writing to the console (`?appender_console`) or to a local file (`?appender_file`), or delivering the log record via an API request to a remote service, streaming somewhere or sending a Slack message (`?appender_slack`). If you are writing such a function or a generator function returning a log message formatter function, please keep the actual call resulting in the formatter function (eg `match.call()` in the generator function or the quoted function call) recorded in the `generator` attribute of the function so that `?log_appender` can pretty-print that instead of the unnamed function body. See the [`appenders.R`](https://github.com/daroczig/logger/blob/master/R/appenders.R) file for more examples. An example for a custom appender delivering log messages to a database table: ```r ## the dbr package provides and easy and secure way of connecting to databased from R ## although if you want to minimize the dependencies, feel free to stick with DBI etc. library(dbr) ## init a persistent connection to the database using a yaml config in the background thanks to dbr ## NOTE that this is optional and a temporarily connection could be also used ## for higher reliability but lower throughput con <- db_connect('mydb') ## define custom function writing the log message to a table log_appender(function(lines) { db_append( df = data.frame(timestamp = Sys.time(), message = lines), table = 'logs', db = con) }) ``` logger/inst/doc/r_packages.R0000644000176200001440000000133314571623747015515 0ustar liggesusers## ----pkgchecks, echo = FALSE-------------------------------------------------- ## check if other logger packages are available and exit if not for (pkg in c('devtools')) { if (!requireNamespace(pkg, quietly = TRUE)) { warning(paste(pkg, 'package not available, so cannot build this vignette')) knitr::knit_exit() } } ## ----setup, include = FALSE--------------------------------------------------- knitr::opts_chunk$set( collapse = TRUE, comment = "#>" ) ## ----------------------------------------------------------------------------- library(logger) devtools::load_all(system.file('demo-packages/logger-tester-package', package = 'logger')) logger_tester_function(INFO, 'hi from tester package') logger/inst/doc/migration.Rmd0000644000176200001440000003567014567717130015736 0ustar liggesusers--- title: "Migration Guide" vignette: > %\VignetteIndexEntry{Migration Guide} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{css, echo=FALSE} div.comparison { width: 49%; display: inline-block; vertical-align: top; } div.comparison p package { font-family: monospace; color: brown; } div#tocnav ul.nav li ul { padding-left: 10px; } ``` ```{r pkgchecks, echo = FALSE} ## check if other logger packages are available and exit if not for (pkg in c('futile.logger', 'logging', 'log4r')) { if (!requireNamespace(pkg, quietly = TRUE)) { warning(paste(pkg, 'package not available, so cannot build this vignette')) knitr::knit_exit() } } ``` ```{r setup, include = FALSE} knitr::opts_chunk$set( collapse = TRUE, comment = "#>" ) ## load the main package first library(logger) ## backup settings oldconf <- list( threshold = log_threshold(), layout = log_layout(), formatter = log_formatter(), appender = log_appender()) ## knitr not picking up stderr log_appender(appender_stdout) ``` In this vignette I suppose that you are already familiar with at least one of the [similar logging R packages](https://daroczig.github.io/logger/index.html#why-another-logging-r-package) and you are looking for suggestions on how to switch to `logger`. Before moving forward, please make sure that you have read the [Introduction to logger](https://daroczig.github.io/logger/articles/Intro.html), [The Anatomy of a Log Request](https://daroczig.github.io/logger/articles/anatomy.html) and [Customizing the Format and the Destination of a Log Record](https://daroczig.github.io/logger/articles/customize_logger.html) vignettes for a decent background on `logger`, and use this vignette as a quick-reference sheet to help you migrate from another package. ## futile.logger The `logger` package has been very heavily inspired by [`futile.logger`](https://cran.r-project.org/package=futile.logger) and have been using it for many years, also opened multiple pull requests to extend `futile.logger` before I decided to revamp my ideas into a new R package -- but there are still many common things between `futile.logger` and `logger`. ### Initialize Both packages comes with a default log engine / config, so it's enough to load the packages and those are ready to be used right away:
futile.logger ```{r} library(futile.logger) ```
logger ```{r} library(logger) ```
### Logging functions The most important change is that function names are by snake_case in `logger`, while `futile.logger` uses dot.separated expressions, and `futile.logger` prefixes function names by `flog` while `logger` uses `log` for that:
futile.logger ```{r} flog.info('hi there') flog.warn('watch out') ```
logger ```{r} log_info('hi there') log_warn('watch out') ```
As you can see above, the default layout of the messages is exactly the same. ### Log levels Regarding log levels, `futile.logger` bundles the default `log4j` levels (`TRACE`, `DEBUG`, `INFO`, `WARN`, `ERROR` and `FATAL`) that is extended by `SUCCESS` in `logger` as sometimes it's worth logging with a higher than `INFO` level that something succeeded. ### Log record layout Changing layouts is easy in both package, as you simply pass a layout function:
futile.logger ```{r} flog.layout(layout.json) flog.info('hi again') ```
logger ```{r} log_layout(layout_json()) log_info('hi again') ```
As you can see, `logger` provided a bit more information about the log request compared to `futile.logger`, but it's easy to change the list of fields to be used in the JSON -- see `?get_logger_meta_variables` for a complete list of variable names to be passed to `?layout_json`. `logger` also ships a lot more layouts, eg `?layout_glue_colors` or roll out your own via the `?layout_glue_generator` factory function. ```{r echo=FALSE, results='hide'} flog.layout(layout.simple) log_layout(layout_simple) ``` ### Log message formatting By default, `futile.logger` uses an `sprintf` formatter, while `logger` passes the objects to be logged to `glue`:
futile.logger ```{r} flog.info('hi') flog.info('hi %s', 84/2) flog.info(paste('hi', 84/2)) flog.info(glue::glue('hi {84/2}')) ```
logger ```{r} log_info('hi') log_info('hi {84/2}') log_formatter(formatter_sprintf) log_info('hi %s', 84/2) log_formatter(formatter_paste) log_info('hi', 84/2) ```
```{r echo=FALSE, results='hide'} log_formatter(formatter_glue) ``` It's easy to change this default formatter in both packages: use `flog.layout` handles this as well in `futile.logger`, while the formatter is separated from the layout function in `logger`, so check `?log_formatter` instead. `logger` ships with a bit more formatter functions, eg the default `?formatter_glue` and `?formatter_glue_or_sprintf` that tries to combine the best from both words. ### Log record destination Setting the destination of the log records works similarly in both packages, although he `logger` packages bundles a lot more options:
logging ```{r} t <- tempfile() flog.appender(appender.file(t)) flog.appender(appender.tee(t)) ```
logger ```{r} t <- tempfile() log_appender(appender_file(t)) log_appender(appender_tee(t)) ```
```{r echo=FALSE, results='hide'} flog.appender(appender.console) log_appender(appender_console) ``` ### Hierarchical logging and performance Both packages support using different logging namespaces and stacking loggers within the same namespace. Performance-wise, there's `logger` seems to be faster than `futile.logger`, but for more details, check the [Simple Benchmarks on Performance](https://daroczig.github.io/logger/articles/performance.html) vignette. ### Using `logger` as a drop-in-replacement of `futile.logger` `logger` has no hard requirements, so it's a very lightweight alternative of `futile.logger`. Although the function names are a bit different, and the message formatter also differs, but with some simple tweaks, `logger` can become an almost perfect drop-in-replacement of `futile.logger`: ```{r} library(logger) log_formatter(formatter_sprintf) flog.trace <- log_trace flog.debug <- log_debug flog.info <- log_info flog.warn <- log_warn flog.error <- log_error flog.info('Hello from logger in a futile.logger theme ...') flog.warn('... where the default log message formatter is %s', 'sprintf') ``` ## logging The [`logging`](https://cran.r-project.org/package=logging) package behaves very similarly to the Python logging module and so thus being pretty Pythonic, while `logger` tries to accommodate native R users' expectations -- so there are some minor nuances between the usage of the two packages. ### Initialize In `logging`, you have to initialize a logger first via `addHandler` or simply by calling `basicConfig`, which is not required in `logger` as it already comes with a default log config:
logging ```{r} library(logging) basicConfig() ```
logger ```{r} library(logger) ```
### Logging functions After initializing the logging engine, actual logging works similarly in the two packages -- with a bit different function names: * although `logging` uses mostly camelCase function names (eg `basicConfig`), but the logging functions are all lowercase without any separator, such as `loginfo` or `logwarn` * `logger` uses snake_case for the function names, such as `log_info` and `log_warn`
logging ```{r} loginfo('hi there') logwarn('watch out') ```
logger ```{r} log_info('hi there') log_warn('watch out') ```
As you can see above, the default layout of the log messages is somewhat different: * `logging` starts with the timestamp that is followed by the log level, optional namespace and the message separated by colons * `logger` starts with the log level, followed by the timestamp between brackets and then the message ### Log levels For the available log levels in `logging`, check `?loglevels`, and `?log_levels` for the same in `logger`:
logging ```{r} str(as.list(loglevels)) ```
logger ```{r} levels <- mget(ls( envir = environment(logger), pattern = '^[A-Z]'), envir = environment(logger)) str(levels[order(-as.numeric(levels))], give.attr = FALSE) ```
### Performance Performance-wise, there's no big difference between the two packages, but for more details, check the [Simple Benchmarks on Performance](https://daroczig.github.io/logger/articles/performance.html) vignette. ### Log record layout Getting and setting the layout of the log record should happen up-front in both packages:
logging ```{r} getLogger()[['handlers']]$basic.stdout$formatter ```
logger ```{r} log_layout() ```
`logger` provides multiple configurable layouts to fit the user's need, eg easily show the calling function of the lof request, the `pid` of the R process, name of the machine etc. or colorized outputs. See [Customizing the Format and the Destination of a Log Record](https://daroczig.github.io/logger/articles/customize_logger.html) vignette for more details. ### Log message formatting If you want to pass dynamic log messages to the log engines, you can do that via the hard-coded `sprintf` in the `logging` package, while you can set that on a namespaces basis in `logger`, which is by default using `glue`:
logging ```{r} loginfo('hi') loginfo('hi %s', 84/2) loginfo(paste('hi', 84/2)) loginfo(glue::glue('hi {84/2}')) ```
logger ```{r} log_info('hi') log_info('hi {84/2}') log_formatter(formatter_sprintf) log_info('hi %s', 84/2) log_formatter(formatter_paste) log_info('hi', 84/2) ```
For even better compatibility, there's also `?formatter_logging` that not only relies on `sprintf` when the first argument is a string, but will log the call and the result as well when the log object is an R expression: ```{r} log_formatter(formatter_logging) log_info('42') log_info(42) log_info(4+2) log_info('foo %s', 'bar') log_info(12, 1+1, 2 * 2) ``` ```{r echo=FALSE, results='hide'} log_formatter(formatter_glue) ``` ### Log record destination Setting the destination of the log records works similarly in both packages, although he `logger` packages bundles a lot more options:
logging ```r ?addHandler ?writeToConsole ?writeToFile ```
logger ```r ?log_appender ?appender_console ?appender_file ?appender_tee ?appender_slack ?appender_pushbullet ```
### Hierarchical logging Both packages support using different logging namespaces and stacking loggers within the same namespace. ### Using `logger` as a drop-in-replacement of `logging` `logger` has no hard requirements, so it's an adequate alternative of `logging`. Although the function names are a bit different, and the message formatter also differs, but with some simple tweaks, `logger` can become an almost perfect drop-in-replacement of `logging` -- although not all log levels (eg \code{FINE} and \code{CRITICAL}) are supported: ```{r} library(logger) log_formatter(formatter_logging) log_layout(layout_logging) logdebug <- log_debug loginfo <- log_info logwarn <- log_warn logerror <- log_error loginfo('Hello from logger in a logging theme ...') logwarn('... where the default log message formatter is %s', 'sprintf', namespace = 'foobar') ``` ## log4r The [`log4r`](https://cran.r-project.org/package=log4r) package provides an object-oriented approach for logging in R, so the logger object is to be passed to the log calls -- unlike in the `logger` package. ### Initialize So thus it's important to create a logging object in `log4r` before being able to log messages, while that's automatically done in `logger:
log4r ```{r} library(log4r) logger <- create.logger(logfile = stdout(), level = "INFO") ```
logger ```{r} library(logger) ```
Please note that in the background, `logger` does have a concept of logger objects, but that's behind the scene and the user does not have to specify / reference it. On the other hand, if you wish, you can do that via the `namespace` concept of `logger` -- more on that later. ### Logging functions While `logger` has a `log_` prefix for all logging functions, `log4r` has lowercase functions names referring to the log level, which takes a logging object and the log message:
log4r ```{r} info(logger, 'hi there') warn(logger, 'watch out') ```
logger ```{r} log_info('hi there') log_warn('watch out') ```
As you can see the default layout of the messages is a bit different in the two packages. ### Log levels Both packages are based on `log4j`, and `log4r` provides `DEBUG`, `INFO`, `WARN`, `ERROR` and `FATAL`, while `logger` also adds `TRACE` and `SUCCESS` on the top of these. To change the log level threshold, use the `level` function on the logging object in `log4r`, while it's `log_level` in `logger`. ### Log record layout and formatter functions The `log4r` provides a `logformat` argument in `create.logger` that can be used to override the default formatting, while `logger` provides formatter and layout functions for a flexible log record design. ### Log record destination By default, `log4r` logs to a file that can be set to `stoud` to write to the console, while `logger` writes to the console by default, but logging to files via the `appender_file` functions is also possible -- besides a number of other log record destinations as well. ### Hierarchical logging and performance Creating objects is the `log4r` way of handling multiple log environments, while `logger` handles that via `namespace`s. ## loggit Sorry, no direct replacement for [`loggit`](https://cran.r-project.org/package=loggit) -- capturing `message`, `warning` and `stop` function messages, but it's on the [roadmap](https://github.com/daroczig/logger/issues/6) to provide helper functions to be used as message hooks feed `logger`. ```{r cleanup, include = FALSE} ## restore settings log_threshold(oldconf$threshold) log_layout(oldconf$layout) log_formatter(oldconf$formatter) log_appender(oldconf$appender) ``` ```{r echo=FALSE, results='hide'} detach('package:logger', unload = TRUE) detach('package:futile.logger', unload = TRUE) detach('package:logging', unload = TRUE) ``` logger/inst/doc/anatomy.html0000644000176200001440000014444114571623743015635 0ustar liggesusers The Anatomy of a Log Request

The Anatomy of a Log Request

log_info('Hello, {name}') /dev/null logger log_threshold Throws away low level log requests log_formatter Converts the R objects to be logged into a character vector log_layout Render actual log record(s) from the message log_appender Deliver log record to destination message <- "Hello, world!" line <- "INFO [2019-09-19 04:58:13] {message}" cat(line, file = stderr()) get_logger_meta_variables Collect useful information about the environment of the log record request. INFO [2019-09-19 04:58:13] Hello, world! formatter_glue layout_simple appender_console

To make a successful log record, logger requires the below components:

  • a log request, eg

    log_error('Oops')
    • including the log level (importance) of the record, which will be later used to decide if the log record is to be delivered or not: ERROR in this case
    • R objects to be logged: a simple string in this case, although it could be a character vector or any R object(s) that can be converted into a character vector by the formatter function
  • the environment and meta-information of the log request, eg actual timestamp, hostname of the computer, the name of the user running the R script, the pid of the R process, calling function and the actual call etc.

    f <- function() get_logger_meta_variables(log_level = INFO)
    f()
    #> $ns
    #> [1] NA
    #> 
    #> $ans
    #> [1] "global"
    #> 
    #> $topenv
    #> [1] "R_GlobalEnv"
    #> 
    #> $fn
    #> [1] "f"
    #> 
    #> $call
    #> [1] "f()"
    #> 
    #> $time
    #> [1] "2024-03-05 15:10:43 CET"
    #> 
    #> $levelr
    #> Log level: INFO
    #> 
    #> $level
    #> [1] "INFO"
    #> 
    #> $pid
    #> [1] 157305
    #> 
    #> $r_version
    #> [1] "4.3.2"
    #> 
    #> $ns_pkg_version
    #> [1] NA
    #> 
    #> $node
    #> [1] "nevermind"
    #> 
    #> $arch
    #> [1] "x86_64"
    #> 
    #> $os_name
    #> [1] "Linux"
    #> 
    #> $os_release
    #> [1] "6.7.6-arch1-1"
    #> 
    #> $os_version
    #> [1] "#1 SMP PREEMPT_DYNAMIC Fri, 23 Feb 2024 16:31:48 +0000"
    #> 
    #> $user
    #> [1] "daroczig"
  • a logger definition to process the log request, including

    • log level threshold, eg INFO, which defines the minimum log level required for actual logging – all log requests with lower log level will be thrown away

      log_threshold()
      #> Log level: INFO
      ERROR <= INFO
      #> [1] TRUE
      log_error('Oops')
      #> ERROR [2024-03-05 15:10:43] Oops
    • formatter function, which takes R objects and converts those into actual log message(s) to be then passed to the layout function for the log record rendering – such as paste, sprintf, glue or eg the below custom example:

      formatter <- function(...) paste(..., collapse = ' ', sep = ' ')
      formatter(1:3, c('foo', 'bar'))
      #> [1] "1 foo 2 bar 3 foo"
    • layout function, which takes log message(s) and further information on the log request (such as timestamp, hostname, username, calling function etc) to render the actual log records eg human-readable text, JSON etc

      library(jsonlite)
      layout <- function(level, msg) toJSON(level = level, timestamp = time, hostname = node, message = msg)
      layout(INFO, 'Happy Thursday!')
      #> {'level': 'INFO', 'timestamp': '1970-01-01 00:00:00', 'hostname': 'foobar', 'message': 'Happy Thursday!'}
    • appender function, which takes fully-rendered log record(s) and delivers to somewhere, eg stdout, a file or a streaming service, eg

      appender <- function(line) cat(line, '\n')
      appender('INFO [now] I am a log message')
      #> INFO [now] I am a log message

Putting all these together (by explicitly setting the default config in the global namespace):

log_threshold(INFO)
log_formatter(formatter_glue)
log_layout(layout_simple)
log_appender(appender_console)
log_debug('I am a low level log message that will not be printed with a high log level threshold')
log_warn('I am a higher level log message that is very likely to be printed')

Note, that all logger definitions and requests are tied to a logging namespace, and one log request might trigger multiple logger definitions as well (stacking). Find more information on these in the Customizing the format and destination of log records vignette.

logger/inst/doc/anatomy.R0000644000176200001440000000351114571623743015062 0ustar liggesusers## ----setup, include = FALSE--------------------------------------------------- knitr::opts_chunk$set( collapse = TRUE, comment = "#>" ) library(logger) ## backup settings oldconf <- list( threshold = log_threshold(), layout = log_layout(), formatter = log_formatter(), appender = log_appender()) ## knitr not picking up stderr log_appender(appender_stdout) ## ----loggerStructureImage, echo=FALSE, out.extra='style="width: 100%;" title="The structure of a logger and the flow of a log record request" alt="The structure of a logger and the flow of a log record request"'---- knitr::include_graphics("logger_structure.svg") ## ----------------------------------------------------------------------------- f <- function() get_logger_meta_variables(log_level = INFO) f() ## ----------------------------------------------------------------------------- log_threshold() ERROR <= INFO log_error('Oops') ## ----------------------------------------------------------------------------- formatter <- function(...) paste(..., collapse = ' ', sep = ' ') formatter(1:3, c('foo', 'bar')) ## ----------------------------------------------------------------------------- appender <- function(line) cat(line, '\n') appender('INFO [now] I am a log message') ## ----------------------------------------------------------------------------- log_threshold(INFO) log_formatter(formatter_glue) log_layout(layout_simple) log_appender(appender_console) log_debug('I am a low level log message that will not be printed with a high log level threshold') log_warn('I am a higher level log message that is very likely to be printed') ## ----cleanup, include = FALSE------------------------------------------------- ## restore settings log_threshold(oldconf$threshold) log_layout(oldconf$layout) log_formatter(oldconf$formatter) log_appender(oldconf$appender) logger/inst/doc/migration.R0000644000176200001440000001373514571623746015417 0ustar liggesusers## ----pkgchecks, echo = FALSE-------------------------------------------------- ## check if other logger packages are available and exit if not for (pkg in c('futile.logger', 'logging', 'log4r')) { if (!requireNamespace(pkg, quietly = TRUE)) { warning(paste(pkg, 'package not available, so cannot build this vignette')) knitr::knit_exit() } } ## ----setup, include = FALSE--------------------------------------------------- knitr::opts_chunk$set( collapse = TRUE, comment = "#>" ) ## load the main package first library(logger) ## backup settings oldconf <- list( threshold = log_threshold(), layout = log_layout(), formatter = log_formatter(), appender = log_appender()) ## knitr not picking up stderr log_appender(appender_stdout) ## ----------------------------------------------------------------------------- library(futile.logger) ## ----------------------------------------------------------------------------- library(logger) ## ----------------------------------------------------------------------------- flog.info('hi there') flog.warn('watch out') ## ----------------------------------------------------------------------------- log_info('hi there') log_warn('watch out') ## ----------------------------------------------------------------------------- flog.layout(layout.json) flog.info('hi again') ## ----------------------------------------------------------------------------- log_layout(layout_json()) log_info('hi again') ## ----echo=FALSE, results='hide'----------------------------------------------- flog.layout(layout.simple) log_layout(layout_simple) ## ----------------------------------------------------------------------------- flog.info('hi') flog.info('hi %s', 84/2) flog.info(paste('hi', 84/2)) flog.info(glue::glue('hi {84/2}')) ## ----------------------------------------------------------------------------- log_info('hi') log_info('hi {84/2}') log_formatter(formatter_sprintf) log_info('hi %s', 84/2) log_formatter(formatter_paste) log_info('hi', 84/2) ## ----echo=FALSE, results='hide'----------------------------------------------- log_formatter(formatter_glue) ## ----------------------------------------------------------------------------- t <- tempfile() flog.appender(appender.file(t)) flog.appender(appender.tee(t)) ## ----------------------------------------------------------------------------- t <- tempfile() log_appender(appender_file(t)) log_appender(appender_tee(t)) ## ----echo=FALSE, results='hide'----------------------------------------------- flog.appender(appender.console) log_appender(appender_console) ## ----------------------------------------------------------------------------- library(logger) log_formatter(formatter_sprintf) flog.trace <- log_trace flog.debug <- log_debug flog.info <- log_info flog.warn <- log_warn flog.error <- log_error flog.info('Hello from logger in a futile.logger theme ...') flog.warn('... where the default log message formatter is %s', 'sprintf') ## ----------------------------------------------------------------------------- library(logging) basicConfig() ## ----------------------------------------------------------------------------- library(logger) ## ----------------------------------------------------------------------------- loginfo('hi there') logwarn('watch out') ## ----------------------------------------------------------------------------- log_info('hi there') log_warn('watch out') ## ----------------------------------------------------------------------------- str(as.list(loglevels)) ## ----------------------------------------------------------------------------- levels <- mget(ls( envir = environment(logger), pattern = '^[A-Z]'), envir = environment(logger)) str(levels[order(-as.numeric(levels))], give.attr = FALSE) ## ----------------------------------------------------------------------------- getLogger()[['handlers']]$basic.stdout$formatter ## ----------------------------------------------------------------------------- log_layout() ## ----------------------------------------------------------------------------- loginfo('hi') loginfo('hi %s', 84/2) loginfo(paste('hi', 84/2)) loginfo(glue::glue('hi {84/2}')) ## ----------------------------------------------------------------------------- log_info('hi') log_info('hi {84/2}') log_formatter(formatter_sprintf) log_info('hi %s', 84/2) log_formatter(formatter_paste) log_info('hi', 84/2) ## ----------------------------------------------------------------------------- log_formatter(formatter_logging) log_info('42') log_info(42) log_info(4+2) log_info('foo %s', 'bar') log_info(12, 1+1, 2 * 2) ## ----echo=FALSE, results='hide'----------------------------------------------- log_formatter(formatter_glue) ## ----------------------------------------------------------------------------- library(logger) log_formatter(formatter_logging) log_layout(layout_logging) logdebug <- log_debug loginfo <- log_info logwarn <- log_warn logerror <- log_error loginfo('Hello from logger in a logging theme ...') logwarn('... where the default log message formatter is %s', 'sprintf', namespace = 'foobar') ## ----------------------------------------------------------------------------- library(log4r) logger <- create.logger(logfile = stdout(), level = "INFO") ## ----------------------------------------------------------------------------- library(logger) ## ----------------------------------------------------------------------------- info(logger, 'hi there') warn(logger, 'watch out') ## ----------------------------------------------------------------------------- log_info('hi there') log_warn('watch out') ## ----cleanup, include = FALSE------------------------------------------------- ## restore settings log_threshold(oldconf$threshold) log_layout(oldconf$layout) log_formatter(oldconf$formatter) log_appender(oldconf$appender) ## ----echo=FALSE, results='hide'----------------------------------------------- detach('package:logger', unload = TRUE) detach('package:futile.logger', unload = TRUE) detach('package:logging', unload = TRUE) logger/inst/doc/Intro.html0000644000176200001440000004156714571623743015265 0ustar liggesusers Introduction to logger

Introduction to logger

If you are not only using R in the interactive console for ad-hoc data analysis, but running eg batch jobs (for ETL, reporting, modeling, forecasting etc) as well, then logging the status(changes) of your script is a must so that later on you can review / debug what have happened.

For most cases, it’s enough to load the package and use the functions with the log prefix to log important and not so important messages, for example:

library(logger)
log_info('Loading data')
#> INFO [2024-03-05 15:10:41] Loading data
data(mtcars)
log_info('The dataset includes {nrow(mtcars)} rows')
#> INFO [2024-03-05 15:10:41] The dataset includes 32 rows
if (max(mtcars$hp) < 1000) {
    log_warn('Oh, no! There are no cars with more than 1K horsepower in the dataset :/')
    log_debug('The most powerful car is {rownames(mtcars)[which.max(mtcars$hp)]} with {max(mtcars$hp)} hp')
}
#> WARN [2024-03-05 15:10:41] Oh, no! There are no cars with more than 1K horsepower in the dataset :/

Interestingly, the most powerful car was not being logged – because by default the logger prints messages with at least the INFO log level:

log_threshold()
#> Log level: INFO

To change that, specify the new log level threshold, eg TRACE to log everything:

log_threshold(TRACE)

The rerunning the above code chunk:

log_info('Loading data')
#> INFO [2024-03-05 15:10:41] Loading data
data(mtcars)
log_info('The dataset includes {nrow(mtcars)} rows')
#> INFO [2024-03-05 15:10:41] The dataset includes 32 rows
if (max(mtcars$hp) < 1000) {
    log_warn('Oh, no! There are no cars with more than 1K horsepower in the dataset :/')
    log_debug('The most powerful car is {rownames(mtcars)[which.max(mtcars$hp)]} with {max(mtcars$hp)} hp')
}
#> WARN [2024-03-05 15:10:41] Oh, no! There are no cars with more than 1K horsepower in the dataset :/
#> DEBUG [2024-03-05 15:10:41] The most powerful car is Maserati Bora with 335 hp

You may also find the ?log_eval function useful to log both an R expression and its result in the same log record:

f <- sqrt
g <- mean
x <- 1:31
log_eval(y <- f(g(x)), level = INFO)
#> INFO [2024-03-05 15:10:41] 'y <- f(g(x))' => '4'
str(y)
#>  num 4

Sometimes, it may be reasonable to log R objects as markdown, e.g. a smallish data.frame or data.table, e.g. mtcars or iris. Calling the formatter using pander instead of glue can help:

log_formatter(formatter_pander)
log_info(head(iris))
#> INFO [2024-03-05 15:10:41] 
#> INFO [2024-03-05 15:10:41] 
#> INFO [2024-03-05 15:10:41]  Sepal.Length   Sepal.Width   Petal.Length   Petal.Width   Species 
#> INFO [2024-03-05 15:10:41] -------------- ------------- -------------- ------------- ---------
#> INFO [2024-03-05 15:10:41]      5.1            3.5           1.4            0.2       setosa  
#> INFO [2024-03-05 15:10:41]      4.9             3            1.4            0.2       setosa  
#> INFO [2024-03-05 15:10:41]      4.7            3.2           1.3            0.2       setosa  
#> INFO [2024-03-05 15:10:41]      4.6            3.1           1.5            0.2       setosa  
#> INFO [2024-03-05 15:10:41]       5             3.6           1.4            0.2       setosa  
#> INFO [2024-03-05 15:10:41]      5.4            3.9           1.7            0.4       setosa  
#> INFO [2024-03-05 15:10:41]

For more details, check the function reference in the manual, or start with the The Anatomy of a Log Request and Customizing the Format and the Destination of a Log Record vignettes.

logger/inst/doc/r_packages.Rmd0000644000176200001440000000440713406424353016027 0ustar liggesusers--- title: "Logging from R Packages" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Logging from R Packages} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r pkgchecks, echo = FALSE} ## check if other logger packages are available and exit if not for (pkg in c('devtools')) { if (!requireNamespace(pkg, quietly = TRUE)) { warning(paste(pkg, 'package not available, so cannot build this vignette')) knitr::knit_exit() } } ``` ```{r setup, include = FALSE} knitr::opts_chunk$set( collapse = TRUE, comment = "#>" ) ``` In this vignette I suppose that you are already familiar with [Customizing the format and destination of log records](https://daroczig.github.io/logger/articles/customize_logger.html) vignette, especially with the [Log namespaces](https://daroczig.github.io/logger/articles/customize_logger.html#log-namespaces) section. So that your R package's users can suppress (or render with custom layout) the log messages triggered by your R package, it's wise to record all those log messages in a custom namespace. By default, if you are calling the `?log_level` function family from an R package after importing from the `logger` package, then `logger` will try to auto-guess the calling R package name and use that as the default namespace, see eg: ```{r} library(logger) devtools::load_all(system.file('demo-packages/logger-tester-package', package = 'logger')) logger_tester_function(INFO, 'hi from tester package') ``` But if auto-guessing is not your style, then feel free to set your custom namespace (eg the name of your package) in all `?log_info` etc function calls and let your users know about how to suppress / reformat / redirect your log messages via `?log_threshold`, `?log_layout`, `?log_appender`. Please note that setting the formatter function via `?log_formatter` should not be left to the R package end-users, as the log message formatter is specific to your logging calls, so that should be decided by the R package author. Feel free to pick any formatter function (eg `glue`, `sprintf`, `paste` or something else), and set that via `?log_formatter` when your R package is loaded. All other parameters of your `logger` will inherit from the `global` namespace -- set by your R package's end user. logger/inst/doc/write_custom_extensions.html0000644000176200001440000231357614571623750021177 0ustar liggesusers Writing Custom Logger Extensions

In this vignette I suppose that you are already familiar with Customizing the format and destination of log records vignette.

Custom log message formatter functions

The log message formatter function should be able to take any number of R objects and convert those into a character vector that is safe to pass to the layout function.

This transformer function can be as simple as calling paste, glue or sprintf or something complex as well, eg looking up user attributes of a user id mentioned in a log record etc.

When writing a custom formatter function, it should also accept the original logging function call as .logcall, the parent call as .topcall and the log request’s environment as .topenv, which can be used to find the relevant variables in your formatter.

If you are writing such a function or a generator function returning a log message formatter function, please keep the actual call resulting in the formatter function (eg match.call() in the generator function or the quoted function call) recorded in the generator attribute of the function so that ?log_formatter can pretty-print that instead of the unnamed function body. See the formatters.R file for more examples.

Custom log layout rendering functions

The layout functions return the log record and take at least two arguments:

  • the log level and
  • a message already formatted as a string by a log message formatter function
  • the namespace (as namespace), calling function (as .topcall) and its environment (as .topenv) of the log request, and the actual log call (as .logcall) automatically collected by ?log_level

Such layout rendering function can be as simple as the default ?layout_simple:

layout_simple <- function(level, msg, ...) {
    paste0(attr(level, 'level'), ' [', format(Sys.time(), "%Y-%m-%d %H:%M:%S"), '] ', msg)
}

Or much more complex, eg looking up the hostname of the machine, public IP address etc and logging all these automatically with the message of the log request.

Your easiest option to set up a custom layout is calling ?layout_glue_generator that comes with a nice API being able to access a bunch of meta-information on the log request via ?get_logger_meta_variables. For such example, see the Customizing the format and destination of log records vignette.

If you are writing such a function or a generator function returning a log message formatter function, please keep the actual call resulting in the formatter function (eg match.call() in the generator function or the quoted function call) recorded in the generator attribute of the function so that ?log_layout can pretty-print that instead of the unnamed function body. See the layouts.R file for more examples.

Custom log record appenders

The appender functions take log records and delivers those to the desired destination. This can be as simple as writing to the console (?appender_console) or to a local file (?appender_file), or delivering the log record via an API request to a remote service, streaming somewhere or sending a Slack message (?appender_slack).

If you are writing such a function or a generator function returning a log message formatter function, please keep the actual call resulting in the formatter function (eg match.call() in the generator function or the quoted function call) recorded in the generator attribute of the function so that ?log_appender can pretty-print that instead of the unnamed function body. See the appenders.R file for more examples.

An example for a custom appender delivering log messages to a database table:

## the dbr package provides and easy and secure way of connecting to databased from R
## although if you want to minimize the dependencies, feel free to stick with DBI etc.
library(dbr)
## init a persistent connection to the database using a yaml config in the background thanks to dbr
## NOTE that this is optional and a temporarily connection could be also used
##      for higher reliability but lower throughput
con <- db_connect('mydb')
## define custom function writing the log message to a table
log_appender(function(lines) {
    db_append(
        df = data.frame(timestamp = Sys.time(), message = lines),
        table = 'logs', db = con)
})
logger/inst/doc/customize_logger.Rmd0000644000176200001440000002744014567717130017322 0ustar liggesusers--- title: "Customizing the Format and the Destination of a Log Record" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Customizing the Format and the Destination of a Log Record} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r pkgchecks, echo = FALSE} ## check if other logger packages are available and exit if not for (pkg in c('devtools', 'parallel')) { if (!requireNamespace(pkg, quietly = TRUE)) { warning(paste(pkg, 'package not available, so cannot build this vignette')) knitr::knit_exit() } } ``` ```{r setup, include = FALSE} knitr::opts_chunk$set( collapse = TRUE, comment = "#>" ) library(logger) ## backup settings oldconf <- list( threshold = log_threshold(), layout = log_layout(), formatter = log_formatter(), appender = log_appender()) ## knitr not picking up stderr log_appender(appender_stdout) ``` In this vignette I suppose that you are already familiar with [The Anatomy of a Log Request](https://daroczig.github.io/logger/articles/anatomy.html) vignette. ## What gets logged? `logger` mostly relies on and uses the default `log4j` log levels and supports suppressing log messages with a lower log level compared to the currently set threshold in the logging namespace: ```{r} log_info('Hi, there!') log_debug('How are you doing today?') log_threshold() log_threshold(TRACE) log_debug('How are you doing today?') ``` So the `?log_threshold` function can both get and set the log level threshold for all future log requests. For the full list of all supported log levels and so thus the possible log level thresholds, see `?log_levels`. If you want to define the log level in a programmatic way, check out `?log_level`, eg ```{r} log_level(INFO, 'Hi, there!') ``` To temporarily update the log level threshold, you may also find the `?with_log_threshold` function useful: ```{r} log_threshold(INFO) log_debug('pst, can you hear me?') log_info('no') with_log_threshold(log_debug('pst, can you hear me?'), threshold = TRACE) log_info('yes') with_log_threshold({ log_debug('pst, can you hear me?') log_info('yes') }, threshold = TRACE) ``` You can also define your own log level(s) if needed, for example introducing an extra level between `DEBUG` and `INFO`: ```{r} FYI <- structure(450L, level = 'FYI', class = c('loglevel', 'integer')) log_threshold(FYI) log_debug('ping') log_level(FYI, 'ping') log_info('pong') ``` ## Log namespaces By default, all log messages will be processed by the global `logger` definition, but you may also use custom namespaces (eg to deliver specific log records to a special destination or to apply a custom log level threshold) and even multiple loggers as well within the very same namespace (eg to deliver all `INFO` and above log levels in the console and everything below that to a trace log file). If you specify an unknown `namespace` in a log request, it will fall back to the global settings: ```{r} log_threshold(INFO) log_trace('Hi, there!', namespace = 'kitchensink') log_info('Hi, there!', namespace = 'kitchensink') ``` But once you start customizing that namespace, it gets forked from the global settings and live on its own without modifying the original namespace: ```{r} log_threshold(TRACE, namespace = 'kitchensink') log_trace('Hi, there!', namespace = 'kitchensink') log_info('Hi, there!', namespace = 'kitchensink') log_trace('Hi, there!') ``` ## Log message formatter functions In the above example, we logged strings without any dynamic parameter, so the task of the logger was quite easy. But in most cases you want to log a parameterized string and the formatter function's task to transform that to a regular character vector. By default, `logger` uses `glue` in the background: ```{r} log_formatter(formatter_glue) log_info('There are {nrow(mtcars)} cars in the mtcars dataset') log_info('2 + 2 = {2+2}') ``` If you don't like this syntax, or want to save a dependency, you can use other formatter functions as well, such as `?formatter_sprintf` (being the default in eg the [`logging` and `futile.logger` packages](https://daroczig.github.io/logger/articles/migration.html)) or `?formatter_paste`, or [write your own formatter function](https://daroczig.github.io/logger/articles/write_custom_extensions.html) converting R objects into string. ## Log message layouts By default, `?log_level` and its derivative functions (eg `?log_info`) will simply record the log-level, the current timestamp and the message after being processed by `glue`: ```{r} log_info(42) log_info('The answer is {42}') log_info('The answers are {1:5}') ``` In the above example, first, `42` was converted to a string by the `?formatter_glue` message formatter, then the message was passed to the `?layout_simple` layout function to generate the actual log record. An example of another layout function writing the same log messages in JSON: ```{r} log_layout(layout_json()) log_info(42) log_info('The answer is {42}') log_info('The answers are {1:5}') ``` If you need colorized logs highlighting the important log messages, check out `?layout_glue_colors`, and for other formatter and layout functions, see the manual of the above mentioned functions that have references to all the other functions and generator functions bundled with the package. ## Custom log record layout To define a custom format on how the log messages should be rendered, you may write your own `formatter` and `layout` function(s) or rely on the function generator functions bundled with the `logger` package, such as `?layout_glue_generator`. This function returns a `layout` function that you can define by `glue`-ing together variables describing the log request via `?get_logger_meta_variables`, so having easy access to (package) namespace, calling function's name, hostname, user running the R process etc. A quick example: * define custom logger: ```{r} logger <- layout_glue_generator(format = '{node}/{pid}/{namespace}/{fn} {time} {level}: {msg}') log_layout(logger) ``` * check what's being logged when called from the global environment: ```{r} log_info('foo') ``` * check what's being logged when called from a custom function: ```{r} f <- function() log_info('foo') f() ``` * check what's being logged when called from a package: ```{r} devtools::load_all(system.file('demo-packages/logger-tester-package', package = 'logger')) logger_tester_function(INFO, 'hi from tester package') ``` * suppress messages in a namespace: ```{r} log_threshold(namespace = 'logger.tester') log_threshold(WARN, namespace = 'logger.tester') logger_tester_function(INFO, 'hi from tester package') logger_tester_function(WARN, 'hi from tester package') log_info('I am still working in the global namespace') ``` Another example of making use of the generator function is to update the layout to include the Process ID that might be very useful eg when forking, see for example the below code chunk still using the above defined log layout: ```r f <- function(x) { log_info('received {length(x)} values') log_success('with the mean of {mean(x)}') mean(x) } library(parallel) mclapply(split(runif(100), 1:10), f, mc.cores = 5) #> nevermind/26448/R_GlobalEnv/FUN 2018-12-02 21:54:11 INFO: received 10 values #> nevermind/26448/R_GlobalEnv/FUN 2018-12-02 21:54:11 SUCCESS: with the mean of 0.403173440974206 #> nevermind/26449/R_GlobalEnv/FUN 2018-12-02 21:54:11 INFO: received 10 values #> nevermind/26448/R_GlobalEnv/FUN 2018-12-02 21:54:11 INFO: received 10 values #> nevermind/26449/R_GlobalEnv/FUN 2018-12-02 21:54:11 SUCCESS: with the mean of 0.538581100990996 #> nevermind/26448/R_GlobalEnv/FUN 2018-12-02 21:54:11 SUCCESS: with the mean of 0.485734378430061 #> nevermind/26450/R_GlobalEnv/FUN 2018-12-02 21:54:11 INFO: received 10 values #> nevermind/26449/R_GlobalEnv/FUN 2018-12-02 21:54:11 INFO: received 10 values #> nevermind/26450/R_GlobalEnv/FUN 2018-12-02 21:54:11 SUCCESS: with the mean of 0.580483326432295 #> nevermind/26452/R_GlobalEnv/FUN 2018-12-02 21:54:11 INFO: received 10 values #> nevermind/26449/R_GlobalEnv/FUN 2018-12-02 21:54:11 SUCCESS: with the mean of 0.461282140854746 #> nevermind/26450/R_GlobalEnv/FUN 2018-12-02 21:54:11 INFO: received 10 values #> nevermind/26451/R_GlobalEnv/FUN 2018-12-02 21:54:11 INFO: received 10 values #> nevermind/26450/R_GlobalEnv/FUN 2018-12-02 21:54:11 SUCCESS: with the mean of 0.465152264293283 #> nevermind/26452/R_GlobalEnv/FUN 2018-12-02 21:54:11 SUCCESS: with the mean of 0.618332817289047 #> nevermind/26451/R_GlobalEnv/FUN 2018-12-02 21:54:11 SUCCESS: with the mean of 0.493527933699079 #> nevermind/26452/R_GlobalEnv/FUN 2018-12-02 21:54:11 INFO: received 10 values #> nevermind/26452/R_GlobalEnv/FUN 2018-12-02 21:54:11 SUCCESS: with the mean of 0.606248055002652 #> nevermind/26451/R_GlobalEnv/FUN 2018-12-02 21:54:11 INFO: received 10 values #> nevermind/26451/R_GlobalEnv/FUN 2018-12-02 21:54:11 SUCCESS: with the mean of 0.537314630229957 ``` *Note that the `layout_glue_generator` functions also adds a special attribute to the resulting formatting function so that when printing the layout function to the console, the user can easily interpret what's being used instead of just showing the actual functions's body:* ```{r} log_layout() ``` For more details on this, see the [Writing custom logger extensions](https://daroczig.github.io/logger/articles/write_custom_extensions.html) vignette. ```{r} ## reset layout log_layout(layout_simple) ``` ## Delivering log records to their destination By default, `logger` will write to the console or `stdout` via the `?appender_console` function: ```{r} log_appender() ``` To write to a logfile instead, use the `?appender_file` generator function, that returns a function that can be used in any namespace: ```{r} t <- tempfile() log_appender(appender_file(t)) log_info('where is this message going?') log_appender() readLines(t) unlink(t) ``` There's a similar generator function that returns an appender function delivering log messages to Slack channels: ```r ## load Slack configuration, API token etc from a (hopefully encrypted) yaml file or similar slack_config <- config::config(...) ## redirect log messages to Slack log_appender(appender_slack( channel = '#gergely-test', username = 'logger', api_token = slack_config$token ), namespace = 'slack') log_info('Excited about sending logs to Slack!') #> INFO [2018-11-28 00:21:13] Excited about sending logs to Slack! log_info('Hi there from logger@R!', namespace = 'slack') ``` You may find `?appender_tee` also useful, that writes the log messages to both `stdout` and a file. ```{r} ## reset appender log_appender(appender_console) ``` And the are many other appender functions bundled with `logger` as well, eg some writing to Syslog, Telegram, Pushbullet, a database table or an Amazon Kinesis stream -- even doing that asynchronously via `appender_async` -- see [Simple Benchmarks on Performance](https://daroczig.github.io/logger/articles/performance.html) for more details. ## Stacking loggers Note that the `?appender_tee` functionality can be implemented by stacking loggers as well, eg setting two loggers for the global namespace: `?appender_console` and `?appender_file`. The advantage of this approach is that you can set different log level thresholds for each logger, for example: ```{r} log_threshold() ## create a new logger with index 2 log_threshold(TRACE, index = 2) ## note that the original logger still have the same log level threshold log_threshold() log_threshold(index = 2) ## update the appender of the new logger t <- tempfile() log_appender(appender_file(t), index = 2) ## test both loggers log_info('info msg') log_debug('info msg') readLines(t) unlink(t) ``` ```{r cleanup, include = FALSE} ## restore settings log_threshold(oldconf$threshold) log_layout(oldconf$layout) log_formatter(oldconf$formatter) log_appender(oldconf$appender) ``` logger/inst/doc/Intro.Rmd0000644000176200001440000000645114570721542015027 0ustar liggesusers--- title: "Introduction to logger" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Introduction to logger} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r setup, include = FALSE} knitr::opts_chunk$set( collapse = TRUE, comment = "#>" ) library(logger) ## backup settings oldconf <- list( threshold = log_threshold(), layout = log_layout(), formatter = log_formatter(), appender = log_appender()) ## knitr not picking up stderr log_appender(appender_stdout) ``` If you are not only using R in the interactive console for ad-hoc data analysis, but running eg batch jobs (for ETL, reporting, modeling, forecasting etc) as well, then logging the status(changes) of your script is a must so that later on you can review / debug what have happened. For most cases, it's enough to load the package and use the functions with the `log` prefix to log important and not so important messages, for example: ```{r} library(logger) log_info('Loading data') data(mtcars) log_info('The dataset includes {nrow(mtcars)} rows') if (max(mtcars$hp) < 1000) { log_warn('Oh, no! There are no cars with more than 1K horsepower in the dataset :/') log_debug('The most powerful car is {rownames(mtcars)[which.max(mtcars$hp)]} with {max(mtcars$hp)} hp') } ``` Interestingly, the most powerful car was not being logged -- because by default the `logger` prints messages with at least the `INFO` log level: ```{r} log_threshold() ``` To change that, specify the new log level threshold, eg `TRACE` to log everything: ```{r} log_threshold(TRACE) ``` The rerunning the above code chunk: ```{r} log_info('Loading data') data(mtcars) log_info('The dataset includes {nrow(mtcars)} rows') if (max(mtcars$hp) < 1000) { log_warn('Oh, no! There are no cars with more than 1K horsepower in the dataset :/') log_debug('The most powerful car is {rownames(mtcars)[which.max(mtcars$hp)]} with {max(mtcars$hp)} hp') } ``` You may also find the `?log_eval` function useful to log both an R expression and its result in the same log record: ```{r} f <- sqrt g <- mean x <- 1:31 log_eval(y <- f(g(x)), level = INFO) str(y) ``` Sometimes, it may be reasonable to log R objects as markdown, e.g. a smallish `data.frame` or `data.table`, e.g. `mtcars` or `iris`. Calling the formatter using `pander` instead of `glue` can help: ```{r knitr-pander-setup, include = FALSE} ppo1 <- pander::panderOptions('knitr.auto.asis') ppo2 <- pander::panderOptions('table.style') pander::panderOptions('knitr.auto.asis', FALSE) pander::panderOptions('table.style', 'simple') ``` ```{r} log_formatter(formatter_pander) log_info(head(iris)) ``` ```{r knitr-pander-revert, include = FALSE} pander::panderOptions('knitr.auto.asis', ppo1) pander::panderOptions('table.style', ppo2) ``` For more details, check the [function reference in the manual](https://daroczig.github.io/logger/reference/index.html), or start with the [The Anatomy of a Log Request](https://daroczig.github.io/logger/articles/anatomy.html) and [Customizing the Format and the Destination of a Log Record](https://daroczig.github.io/logger/articles/customize_logger.html) vignettes. ```{r cleanup, include = FALSE} ## restore settings log_threshold(oldconf$threshold) log_layout(oldconf$layout) log_formatter(oldconf$formatter) log_appender(oldconf$appender) ``` logger/inst/doc/performance.html0000644000176200001440000005471714571623747016500 0ustar liggesusers Simple Benchmarks on logger Performance

Simple Benchmarks on logger Performance

Although this has not been an important feature in the early development and overall design of this logger implementation, but with the default ?layout_simple and ?formatter_glue, it seems to perform pretty well when comparing with futile.logger and logging packages:

library(microbenchmark)

## fl
library(futile.logger)
t1 <- tempfile()
flog.appender(appender.file(t1))
#> NULL

## lg
library(logging)
t2 <- tempfile()
addHandler(writeToFile, file = t2)

## lr
library(logger)
#> The following objects are masked from ‘package:futile.logger’: DEBUG, ERROR, FATAL, INFO, TRACE, WARN
t3 <- tempfile()
log_appender(appender_file(t3))

string_fl <- function() flog.info('hi')
string_lg <- function() loginfo('hi')
string_lr <- function() log_info('hi')
dynamic_fl <- function() flog.info('hi %s', 42)
dynamic_lg <- function() loginfo('hi %s', 42)
dynamic_lr <- function() log_info('hi {42}')
vector_fl <- function() flog.info(paste('hi', 1:5))
vector_lg <- function() loginfo(paste('hi', 1:5))
vector_lr <- function() log_info('hi {1:5}')

microbenchmark(
    string_fl(), string_lg(), string_lr(),
    vector_fl(), vector_lg(), vector_lr(),
    dynamic_fl(), dynamic_lg(), dynamic_lr(),
    times = 1e3)
#> Unit: microseconds
#>          expr      min        lq      mean    median        uq        max neval
#>   string_fl() 1533.379 1650.7915 2510.5517 1759.9345 2885.4465  20835.425  1000
#>   string_lg()  172.963  206.7615  315.6177  237.3150  335.3010  12738.735  1000
#>   string_lr()  227.981  263.4715  390.7139  301.9045  409.0400  11926.974  1000
#>   vector_fl() 1552.706 1661.7030 2434.0460 1766.7485 2819.5525  40892.197  1000
#>   vector_lg()  198.338  234.2355  330.3268  266.7695  358.2510   9969.333  1000
#>   vector_lr()  290.169  337.4730  592.0041  382.4335  537.5485 101946.435  1000
#>  dynamic_fl() 1538.985 1663.7890 2564.6668 1782.1160 2932.7555  46039.686  1000
#>  dynamic_lg()  188.213  226.5370  387.2470  255.1745  350.2015  60737.562  1000
#>  dynamic_lr()  271.478  317.3350  486.1123  360.5815  483.5830  12070.936  1000

paste(t1, length(readLines(t1)))
#> [1] "/tmp/Rtmp3Fp6qa/file7a8919485a36 7000"
paste(t2, length(readLines(t2)))
#> [1] "/tmp/Rtmp3Fp6qa/file7a89b17929f 7000"
paste(t3, length(readLines(t3)))
#> [1] "/tmp/Rtmp3Fp6qa/file289f24c88c41 7000"

So based on the above, non-comprehensive benchmark, it seems that when it comes to using the very base functionality of a logging engine, logging comes first, then logger performs with a bit of overhead due to using glue by default, then comes a bit slower futile.logger.

On the other hand, there are some low-hanging fruits to improve performance, eg caching the logger function in the namespace, or using much faster message formatters (eg paste0 or sprintf instead of glue) if needed – like what futile.logger and logging are using instead of glue, so a quick logger comparison:

log_formatter(formatter_sprintf)
string <- function() log_info('hi')
dynamic <- function() log_info('hi %s', 42)
vector <- function() log_info(paste('hi', 1:5))

microbenchmark(string(), vector(), dynamic(), times = 1e3)
#> Unit: microseconds
#>       expr     min       lq     mean   median       uq      max neval cld
#>   string() 110.192 118.4850 148.5848 137.1825 152.7275 1312.903  1000  a
#>   vector() 129.111 136.8245 168.9274 155.5840 172.6795 3230.528  1000   b
#>  dynamic() 116.347 124.7620 159.1570 143.2140 160.5040 4397.640  1000  ab

Which suggests that logger is a pretty well-performing log framework.

If you need even more performance with slower appenders, then asynchronous logging is your friend: passing the log messages to a reliable message queue, and a background process delivering those to the actual log destination in the background – without blocking the main R process. This can be easily achieved in logger by wrapping any appender function in the appender_async function, such as:

## demo log appender that's pretty slow
appender_file_slow <- function(file) {
  force(file)
  function(lines) {
    Sys.sleep(1)
    cat(lines, sep = '\n', file = file, append = TRUE)
  }
}

## create an async appender and start using it right away
log_appender(appender_async(appender_file_slow(file = tempfile())))

async <- function() log_info('Was this slow?')
microbenchmark(async(), times = 1e3)
#> Unit: microseconds
#>     expr     min      lq     mean   median     uq      max neval
#>  async() 511.329 528.247 614.8694 558.2535 616.14 5018.731  1000

Please note that although this ~0.6 ms is significantly higher than the ~0.15 ms we achieved above with the sprintf formatter, but this time we are calling an appender that would take 1 full second to deliver the log message (and not just printing to the console), so bringing that down to less than 1 millisecond is not too bad. If you need even higher throughput, then a custom appender_async without checking on the background process and potentially a faster message queue can bring this even below to 200 µs.

logger/inst/doc/Intro.R0000644000176200001440000000447014571623741014510 0ustar liggesusers## ----setup, include = FALSE--------------------------------------------------- knitr::opts_chunk$set( collapse = TRUE, comment = "#>" ) library(logger) ## backup settings oldconf <- list( threshold = log_threshold(), layout = log_layout(), formatter = log_formatter(), appender = log_appender()) ## knitr not picking up stderr log_appender(appender_stdout) ## ----------------------------------------------------------------------------- library(logger) log_info('Loading data') data(mtcars) log_info('The dataset includes {nrow(mtcars)} rows') if (max(mtcars$hp) < 1000) { log_warn('Oh, no! There are no cars with more than 1K horsepower in the dataset :/') log_debug('The most powerful car is {rownames(mtcars)[which.max(mtcars$hp)]} with {max(mtcars$hp)} hp') } ## ----------------------------------------------------------------------------- log_threshold() ## ----------------------------------------------------------------------------- log_threshold(TRACE) ## ----------------------------------------------------------------------------- log_info('Loading data') data(mtcars) log_info('The dataset includes {nrow(mtcars)} rows') if (max(mtcars$hp) < 1000) { log_warn('Oh, no! There are no cars with more than 1K horsepower in the dataset :/') log_debug('The most powerful car is {rownames(mtcars)[which.max(mtcars$hp)]} with {max(mtcars$hp)} hp') } ## ----------------------------------------------------------------------------- f <- sqrt g <- mean x <- 1:31 log_eval(y <- f(g(x)), level = INFO) str(y) ## ----knitr-pander-setup, include = FALSE-------------------------------------- ppo1 <- pander::panderOptions('knitr.auto.asis') ppo2 <- pander::panderOptions('table.style') pander::panderOptions('knitr.auto.asis', FALSE) pander::panderOptions('table.style', 'simple') ## ----------------------------------------------------------------------------- log_formatter(formatter_pander) log_info(head(iris)) ## ----knitr-pander-revert, include = FALSE------------------------------------- pander::panderOptions('knitr.auto.asis', ppo1) pander::panderOptions('table.style', ppo2) ## ----cleanup, include = FALSE------------------------------------------------- ## restore settings log_threshold(oldconf$threshold) log_layout(oldconf$layout) log_formatter(oldconf$formatter) log_appender(oldconf$appender) logger/inst/doc/customize_logger.html0000644000176200001440000013120314571623745017540 0ustar liggesusers Customizing the Format and the Destination of a Log Record

Customizing the Format and the Destination of a Log Record

In this vignette I suppose that you are already familiar with The Anatomy of a Log Request vignette.

What gets logged?

logger mostly relies on and uses the default log4j log levels and supports suppressing log messages with a lower log level compared to the currently set threshold in the logging namespace:

log_info('Hi, there!')
#> INFO [2024-03-05 15:10:44] Hi, there!
log_debug('How are you doing today?')
log_threshold()
#> Log level: INFO
log_threshold(TRACE)
log_debug('How are you doing today?')
#> DEBUG [2024-03-05 15:10:44] How are you doing today?

So the ?log_threshold function can both get and set the log level threshold for all future log requests.

For the full list of all supported log levels and so thus the possible log level thresholds, see ?log_levels.

If you want to define the log level in a programmatic way, check out ?log_level, eg

log_level(INFO, 'Hi, there!')
#> INFO [2024-03-05 15:10:44] Hi, there!

To temporarily update the log level threshold, you may also find the ?with_log_threshold function useful:

log_threshold(INFO)
log_debug('pst, can you hear me?')
log_info('no')
#> INFO [2024-03-05 15:10:44] no

with_log_threshold(log_debug('pst, can you hear me?'), threshold = TRACE)
#> DEBUG [2024-03-05 15:10:44] pst, can you hear me?
log_info('yes')
#> INFO [2024-03-05 15:10:44] yes

with_log_threshold({
    log_debug('pst, can you hear me?')
    log_info('yes')
}, threshold = TRACE)
#> DEBUG [2024-03-05 15:10:44] pst, can you hear me?
#> INFO [2024-03-05 15:10:44] yes

You can also define your own log level(s) if needed, for example introducing an extra level between DEBUG and INFO:

FYI <- structure(450L, level = 'FYI', class = c('loglevel', 'integer'))
log_threshold(FYI)
log_debug('ping')
log_level(FYI, 'ping')
#> FYI [2024-03-05 15:10:44] ping
log_info('pong')
#> INFO [2024-03-05 15:10:44] pong

Log namespaces

By default, all log messages will be processed by the global logger definition, but you may also use custom namespaces (eg to deliver specific log records to a special destination or to apply a custom log level threshold) and even multiple loggers as well within the very same namespace (eg to deliver all INFO and above log levels in the console and everything below that to a trace log file).

If you specify an unknown namespace in a log request, it will fall back to the global settings:

log_threshold(INFO)
log_trace('Hi, there!', namespace = 'kitchensink')
log_info('Hi, there!', namespace = 'kitchensink')
#> INFO [2024-03-05 15:10:44] Hi, there!

But once you start customizing that namespace, it gets forked from the global settings and live on its own without modifying the original namespace:

log_threshold(TRACE, namespace = 'kitchensink')
log_trace('Hi, there!', namespace = 'kitchensink')
#> TRACE [2024-03-05 15:10:44] Hi, there!
log_info('Hi, there!', namespace = 'kitchensink')
#> INFO [2024-03-05 15:10:44] Hi, there!
log_trace('Hi, there!')

Log message formatter functions

In the above example, we logged strings without any dynamic parameter, so the task of the logger was quite easy. But in most cases you want to log a parameterized string and the formatter function’s task to transform that to a regular character vector.

By default, logger uses glue in the background:

log_formatter(formatter_glue)
log_info('There are {nrow(mtcars)} cars in the mtcars dataset')
#> INFO [2024-03-05 15:10:44] There are 32 cars in the mtcars dataset
log_info('2 + 2 = {2+2}')
#> INFO [2024-03-05 15:10:44] 2 + 2 = 4

If you don’t like this syntax, or want to save a dependency, you can use other formatter functions as well, such as ?formatter_sprintf (being the default in eg the logging and futile.logger packages) or ?formatter_paste, or write your own formatter function converting R objects into string.

Log message layouts

By default, ?log_level and its derivative functions (eg ?log_info) will simply record the log-level, the current timestamp and the message after being processed by glue:

log_info(42)
#> INFO [2024-03-05 15:10:44] 42
log_info('The answer is {42}')
#> INFO [2024-03-05 15:10:44] The answer is 42
log_info('The answers are {1:5}')
#> INFO [2024-03-05 15:10:44] The answers are 1
#> INFO [2024-03-05 15:10:44] The answers are 2
#> INFO [2024-03-05 15:10:44] The answers are 3
#> INFO [2024-03-05 15:10:44] The answers are 4
#> INFO [2024-03-05 15:10:44] The answers are 5

In the above example, first, 42 was converted to a string by the ?formatter_glue message formatter, then the message was passed to the ?layout_simple layout function to generate the actual log record.

An example of another layout function writing the same log messages in JSON:

log_layout(layout_json())
log_info(42)
#> {"time":"2024-03-05 15:10:44","level":"INFO","ns":"global","ans":"global","topenv":"R_GlobalEnv","fn":"eval","node":"nevermind","arch":"x86_64","os_name":"Linux","os_release":"6.7.6-arch1-1","os_version":"#1 SMP PREEMPT_DYNAMIC Fri, 23 Feb 2024 16:31:48 +0000","pid":157305,"user":"daroczig","msg":"42"}
log_info('The answer is {42}')
#> {"time":"2024-03-05 15:10:44","level":"INFO","ns":"global","ans":"global","topenv":"R_GlobalEnv","fn":"eval","node":"nevermind","arch":"x86_64","os_name":"Linux","os_release":"6.7.6-arch1-1","os_version":"#1 SMP PREEMPT_DYNAMIC Fri, 23 Feb 2024 16:31:48 +0000","pid":157305,"user":"daroczig","msg":"The answer is 42"}
log_info('The answers are {1:5}')
#> {"time":"2024-03-05 15:10:44","level":"INFO","ns":"global","ans":"global","topenv":"R_GlobalEnv","fn":"eval","node":"nevermind","arch":"x86_64","os_name":"Linux","os_release":"6.7.6-arch1-1","os_version":"#1 SMP PREEMPT_DYNAMIC Fri, 23 Feb 2024 16:31:48 +0000","pid":157305,"user":"daroczig","msg":"The answers are 1"}
#> {"time":"2024-03-05 15:10:44","level":"INFO","ns":"global","ans":"global","topenv":"R_GlobalEnv","fn":"eval","node":"nevermind","arch":"x86_64","os_name":"Linux","os_release":"6.7.6-arch1-1","os_version":"#1 SMP PREEMPT_DYNAMIC Fri, 23 Feb 2024 16:31:48 +0000","pid":157305,"user":"daroczig","msg":"The answers are 2"}
#> {"time":"2024-03-05 15:10:44","level":"INFO","ns":"global","ans":"global","topenv":"R_GlobalEnv","fn":"eval","node":"nevermind","arch":"x86_64","os_name":"Linux","os_release":"6.7.6-arch1-1","os_version":"#1 SMP PREEMPT_DYNAMIC Fri, 23 Feb 2024 16:31:48 +0000","pid":157305,"user":"daroczig","msg":"The answers are 3"}
#> {"time":"2024-03-05 15:10:44","level":"INFO","ns":"global","ans":"global","topenv":"R_GlobalEnv","fn":"eval","node":"nevermind","arch":"x86_64","os_name":"Linux","os_release":"6.7.6-arch1-1","os_version":"#1 SMP PREEMPT_DYNAMIC Fri, 23 Feb 2024 16:31:48 +0000","pid":157305,"user":"daroczig","msg":"The answers are 4"}
#> {"time":"2024-03-05 15:10:44","level":"INFO","ns":"global","ans":"global","topenv":"R_GlobalEnv","fn":"eval","node":"nevermind","arch":"x86_64","os_name":"Linux","os_release":"6.7.6-arch1-1","os_version":"#1 SMP PREEMPT_DYNAMIC Fri, 23 Feb 2024 16:31:48 +0000","pid":157305,"user":"daroczig","msg":"The answers are 5"}

If you need colorized logs highlighting the important log messages, check out ?layout_glue_colors, and for other formatter and layout functions, see the manual of the above mentioned functions that have references to all the other functions and generator functions bundled with the package.

Custom log record layout

To define a custom format on how the log messages should be rendered, you may write your own formatter and layout function(s) or rely on the function generator functions bundled with the logger package, such as ?layout_glue_generator.

This function returns a layout function that you can define by glue-ing together variables describing the log request via ?get_logger_meta_variables, so having easy access to (package) namespace, calling function’s name, hostname, user running the R process etc.

A quick example:

  • define custom logger:

    logger <- layout_glue_generator(format = '{node}/{pid}/{namespace}/{fn} {time} {level}: {msg}')
    log_layout(logger)
  • check what’s being logged when called from the global environment:

    log_info('foo')
    #> nevermind/157305/global/eval 2024-03-05 15:10:44.956043 INFO: foo
  • check what’s being logged when called from a custom function:

    f <- function() log_info('foo')
    f()
    #> nevermind/157305/global/f 2024-03-05 15:10:44.973217 INFO: foo
  • check what’s being logged when called from a package:

    devtools::load_all(system.file('demo-packages/logger-tester-package', package = 'logger'))
    #> ℹ Loading logger.tester
    logger_tester_function(INFO, 'hi from tester package')
    #> nevermind/157305/logger.tester/logger_tester_function 2024-03-05 15:10:45.226536 INFO: hi from tester package 0.0112850470468402
  • suppress messages in a namespace:

    log_threshold(namespace = 'logger.tester')
    #> Log level: INFO
    log_threshold(WARN, namespace = 'logger.tester')
    logger_tester_function(INFO, 'hi from tester package')
    logger_tester_function(WARN, 'hi from tester package')
    #> nevermind/157305/logger.tester/logger_tester_function 2024-03-05 15:10:45.245573 WARN: hi from tester package 0.512536549242213
    log_info('I am still working in the global namespace')
    #> nevermind/157305/global/eval 2024-03-05 15:10:45.248455 INFO: I am still working in the global namespace

Another example of making use of the generator function is to update the layout to include the Process ID that might be very useful eg when forking, see for example the below code chunk still using the above defined log layout:

f <- function(x) {
    log_info('received {length(x)} values')
    log_success('with the mean of {mean(x)}')
    mean(x)
}
library(parallel)
mclapply(split(runif(100), 1:10), f, mc.cores = 5)
#> nevermind/26448/R_GlobalEnv/FUN 2018-12-02 21:54:11 INFO: received 10 values
#> nevermind/26448/R_GlobalEnv/FUN 2018-12-02 21:54:11 SUCCESS: with the mean of 0.403173440974206
#> nevermind/26449/R_GlobalEnv/FUN 2018-12-02 21:54:11 INFO: received 10 values
#> nevermind/26448/R_GlobalEnv/FUN 2018-12-02 21:54:11 INFO: received 10 values
#> nevermind/26449/R_GlobalEnv/FUN 2018-12-02 21:54:11 SUCCESS: with the mean of 0.538581100990996
#> nevermind/26448/R_GlobalEnv/FUN 2018-12-02 21:54:11 SUCCESS: with the mean of 0.485734378430061
#> nevermind/26450/R_GlobalEnv/FUN 2018-12-02 21:54:11 INFO: received 10 values
#> nevermind/26449/R_GlobalEnv/FUN 2018-12-02 21:54:11 INFO: received 10 values
#> nevermind/26450/R_GlobalEnv/FUN 2018-12-02 21:54:11 SUCCESS: with the mean of 0.580483326432295
#> nevermind/26452/R_GlobalEnv/FUN 2018-12-02 21:54:11 INFO: received 10 values
#> nevermind/26449/R_GlobalEnv/FUN 2018-12-02 21:54:11 SUCCESS: with the mean of 0.461282140854746
#> nevermind/26450/R_GlobalEnv/FUN 2018-12-02 21:54:11 INFO: received 10 values
#> nevermind/26451/R_GlobalEnv/FUN 2018-12-02 21:54:11 INFO: received 10 values
#> nevermind/26450/R_GlobalEnv/FUN 2018-12-02 21:54:11 SUCCESS: with the mean of 0.465152264293283
#> nevermind/26452/R_GlobalEnv/FUN 2018-12-02 21:54:11 SUCCESS: with the mean of 0.618332817289047
#> nevermind/26451/R_GlobalEnv/FUN 2018-12-02 21:54:11 SUCCESS: with the mean of 0.493527933699079
#> nevermind/26452/R_GlobalEnv/FUN 2018-12-02 21:54:11 INFO: received 10 values
#> nevermind/26452/R_GlobalEnv/FUN 2018-12-02 21:54:11 SUCCESS: with the mean of 0.606248055002652
#> nevermind/26451/R_GlobalEnv/FUN 2018-12-02 21:54:11 INFO: received 10 values
#> nevermind/26451/R_GlobalEnv/FUN 2018-12-02 21:54:11 SUCCESS: with the mean of 0.537314630229957

Note that the layout_glue_generator functions also adds a special attribute to the resulting formatting function so that when printing the layout function to the console, the user can easily interpret what’s being used instead of just showing the actual functions’s body:

log_layout()
#> layout_glue_generator(format = "{node}/{pid}/{namespace}/{fn} {time} {level}: {msg}")

For more details on this, see the Writing custom logger extensions vignette.

## reset layout
log_layout(layout_simple)

Delivering log records to their destination

By default, logger will write to the console or stdout via the ?appender_console function:

log_appender()
#> appender_stdout

To write to a logfile instead, use the ?appender_file generator function, that returns a function that can be used in any namespace:

t <- tempfile()
log_appender(appender_file(t))
log_info('where is this message going?')
log_appender()
#> appender_file(file = t)
readLines(t)
#> [1] "INFO [2024-03-05 15:10:45] where is this message going?"
unlink(t)

There’s a similar generator function that returns an appender function delivering log messages to Slack channels:

## load Slack configuration, API token etc from a (hopefully encrypted) yaml file or similar
slack_config <- config::config(...)
## redirect log messages to Slack
log_appender(appender_slack(
    channel   = '#gergely-test',
    username  = 'logger',
    api_token = slack_config$token
), namespace = 'slack')
log_info('Excited about sending logs to Slack!')
#> INFO [2018-11-28 00:21:13] Excited about sending logs to Slack!
log_info('Hi there from logger@R!', namespace = 'slack')

You may find ?appender_tee also useful, that writes the log messages to both stdout and a file.

## reset appender
log_appender(appender_console)

And the are many other appender functions bundled with logger as well, eg some writing to Syslog, Telegram, Pushbullet, a database table or an Amazon Kinesis stream – even doing that asynchronously via appender_async – see Simple Benchmarks on Performance for more details.

Stacking loggers

Note that the ?appender_tee functionality can be implemented by stacking loggers as well, eg setting two loggers for the global namespace: ?appender_console and ?appender_file. The advantage of this approach is that you can set different log level thresholds for each logger, for example:

log_threshold()
#> Log level: INFO

## create a new logger with index 2
log_threshold(TRACE, index = 2)

## note that the original logger still have the same log level threshold
log_threshold()
#> Log level: INFO
log_threshold(index = 2)
#> Log level: TRACE

## update the appender of the new logger
t <- tempfile()
log_appender(appender_file(t), index = 2)

## test both loggers
log_info('info msg')
log_debug('info msg')

readLines(t)
#> [1] "INFO [2024-03-05 15:10:45] info msg" 
#> [2] "DEBUG [2024-03-05 15:10:45] info msg"
unlink(t)
logger/inst/doc/customize_logger.R0000644000176200001440000001156614571623745017006 0ustar liggesusers## ----pkgchecks, echo = FALSE-------------------------------------------------- ## check if other logger packages are available and exit if not for (pkg in c('devtools', 'parallel')) { if (!requireNamespace(pkg, quietly = TRUE)) { warning(paste(pkg, 'package not available, so cannot build this vignette')) knitr::knit_exit() } } ## ----setup, include = FALSE--------------------------------------------------- knitr::opts_chunk$set( collapse = TRUE, comment = "#>" ) library(logger) ## backup settings oldconf <- list( threshold = log_threshold(), layout = log_layout(), formatter = log_formatter(), appender = log_appender()) ## knitr not picking up stderr log_appender(appender_stdout) ## ----------------------------------------------------------------------------- log_info('Hi, there!') log_debug('How are you doing today?') log_threshold() log_threshold(TRACE) log_debug('How are you doing today?') ## ----------------------------------------------------------------------------- log_level(INFO, 'Hi, there!') ## ----------------------------------------------------------------------------- log_threshold(INFO) log_debug('pst, can you hear me?') log_info('no') with_log_threshold(log_debug('pst, can you hear me?'), threshold = TRACE) log_info('yes') with_log_threshold({ log_debug('pst, can you hear me?') log_info('yes') }, threshold = TRACE) ## ----------------------------------------------------------------------------- FYI <- structure(450L, level = 'FYI', class = c('loglevel', 'integer')) log_threshold(FYI) log_debug('ping') log_level(FYI, 'ping') log_info('pong') ## ----------------------------------------------------------------------------- log_threshold(INFO) log_trace('Hi, there!', namespace = 'kitchensink') log_info('Hi, there!', namespace = 'kitchensink') ## ----------------------------------------------------------------------------- log_threshold(TRACE, namespace = 'kitchensink') log_trace('Hi, there!', namespace = 'kitchensink') log_info('Hi, there!', namespace = 'kitchensink') log_trace('Hi, there!') ## ----------------------------------------------------------------------------- log_formatter(formatter_glue) log_info('There are {nrow(mtcars)} cars in the mtcars dataset') log_info('2 + 2 = {2+2}') ## ----------------------------------------------------------------------------- log_info(42) log_info('The answer is {42}') log_info('The answers are {1:5}') ## ----------------------------------------------------------------------------- log_layout(layout_json()) log_info(42) log_info('The answer is {42}') log_info('The answers are {1:5}') ## ----------------------------------------------------------------------------- logger <- layout_glue_generator(format = '{node}/{pid}/{namespace}/{fn} {time} {level}: {msg}') log_layout(logger) ## ----------------------------------------------------------------------------- log_info('foo') ## ----------------------------------------------------------------------------- f <- function() log_info('foo') f() ## ----------------------------------------------------------------------------- devtools::load_all(system.file('demo-packages/logger-tester-package', package = 'logger')) logger_tester_function(INFO, 'hi from tester package') ## ----------------------------------------------------------------------------- log_threshold(namespace = 'logger.tester') log_threshold(WARN, namespace = 'logger.tester') logger_tester_function(INFO, 'hi from tester package') logger_tester_function(WARN, 'hi from tester package') log_info('I am still working in the global namespace') ## ----------------------------------------------------------------------------- log_layout() ## ----------------------------------------------------------------------------- ## reset layout log_layout(layout_simple) ## ----------------------------------------------------------------------------- log_appender() ## ----------------------------------------------------------------------------- t <- tempfile() log_appender(appender_file(t)) log_info('where is this message going?') log_appender() readLines(t) unlink(t) ## ----------------------------------------------------------------------------- ## reset appender log_appender(appender_console) ## ----------------------------------------------------------------------------- log_threshold() ## create a new logger with index 2 log_threshold(TRACE, index = 2) ## note that the original logger still have the same log level threshold log_threshold() log_threshold(index = 2) ## update the appender of the new logger t <- tempfile() log_appender(appender_file(t), index = 2) ## test both loggers log_info('info msg') log_debug('info msg') readLines(t) unlink(t) ## ----cleanup, include = FALSE------------------------------------------------- ## restore settings log_threshold(oldconf$threshold) log_layout(oldconf$layout) log_formatter(oldconf$formatter) log_appender(oldconf$appender) logger/inst/doc/write_custom_extensions.R0000644000176200001440000000021714571623750020413 0ustar liggesusers## ----setup, include = FALSE--------------------------------------------------- knitr::opts_chunk$set( collapse = TRUE, comment = "#>" )