logger/0000755000176200001440000000000014705655607011550 5ustar liggesuserslogger/tests/0000755000176200001440000000000014654522610012700 5ustar liggesuserslogger/tests/testthat/0000755000176200001440000000000014705655607014552 5ustar liggesuserslogger/tests/testthat/test-appenders.R0000644000176200001440000000370014705270541017621 0ustar liggesuserstest_that("append to file", { t <- withr::local_tempfile() local_test_logger( appender = appender_file(t), layout = layout_glue_generator("{level} {msg}"), threshold = TRACE ) 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") }) test_that("overwrite file", { t <- withr::local_tempfile() local_test_logger( appender = appender_file(t, append = FALSE), layout = layout_glue_generator("{level} {msg}"), threshold = TRACE ) 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") }) test_that("append to file + print to console", { t <- withr::local_tempfile() local_test_logger( appender = appender_tee(t), layout = layout_glue_generator("{level} {msg}"), ) expect_output(log_info("foobar"), "INFO foobar") capture.output(log_info("{1:2}")) expect_equal(length(readLines(t)), 3) expect_equal(readLines(t)[1], "INFO foobar") }) test_that("logrotate", { t <- withr::local_tempdir() f <- file.path(t, "log") local_test_logger( appender = appender_file(f, max_lines = 2, max_files = 5L), layout = layout_glue_generator("{msg}"), threshold = TRACE ) 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") }) test_that("async logging", { skip_on_cran() t <- withr::local_tempfile() local_test_logger( layout = layout_blank, appender = appender_async(appender_file(file = t)) ) for (i in 1:5) log_info(i) Sys.sleep(1) expect_equal(readLines(t)[1], "1") expect_equal(length(readLines(t)), 5) }) logger/tests/testthat/test-utils.R0000644000176200001440000000173314705270032016777 0ustar liggesuserstest_that("fail_on_missing_package", { expect_no_error(fail_on_missing_package("logger")) expect_snapshot(error = TRUE, { fail_on_missing_package("logger", "9.9.9", call = quote(f())) fail_on_missing_package("an.R.package-that-doesNotExists", call = quote(f())) }) }) test_that("validate_log_level", { expect_equal(validate_log_level(ERROR), ERROR) expect_equal(validate_log_level("ERROR"), ERROR) expect_snapshot(validate_log_level("FOOBAR"), error = TRUE) }) test_that("catch_base_log", { expect_true(nchar(catch_base_log(ERROR, NA_character_)) == 28) expect_true(nchar(catch_base_log(INFO, NA_character_)) == 27) local_test_logger(layout = layout_blank) expect_true(nchar(catch_base_log(INFO, NA_character_)) == 0) local_test_logger(layout = layout_glue_generator(format = "{namespace}/{fn}")) expect_equal(catch_base_log(INFO, "TEMP", .topcall = NA), "global/NA") expect_equal(catch_base_log(INFO, "TEMP", .topcall = quote(f())), "global/f") }) logger/tests/testthat/test-try.R0000644000176200001440000000057314654755544016501 0ustar liggesuserstest_that("%except% logs errors and returns default value", { local_test_logger(layout = layout_glue_generator("{ns} / {ans} / {topenv} / {fn} / {call}\n{level} {msg}")) f <- function() { FunDoesNotExist(1:10) %except% 1 } expect_snapshot(out <- f()) expect_equal(out, 1) }) test_that("%except% returns value when no error", { expect_equal(5 %except% 1, 5) }) logger/tests/testthat/test-layouts.R0000644000176200001440000000541514705271034017343 0ustar liggesuserstest_that("blank layout", { local_test_logger(layout = layout_blank) expect_output(log_info("foobar"), "foobar") expect_equal(capture.output(log_info("foobar")), "foobar") }) test_that("colorized layout", { local_test_logger(layout = layout_glue_colors) 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", { local_test_logger(layout = layout_glue_generator("{level} {ans} {fn}")) f_info <- function() log_info() expect_output(f_info(), "INFO global f_info()") f_warn <- function() log_warn() expect_output(f_warn(), "WARN global f_warn()") }) test_that("JSON layout", { local_test_logger(layout = layout_json(fields = "level")) out <- jsonlite::fromJSON(capture.output(log_info("foobar"))) expect_equal(out, list(level = "INFO", msg = "foobar")) }) test_that("JSON layout warns if you include msg", { expect_snapshot(layout <- layout_json(fields = "msg")) local_test_logger(layout = layout) out <- jsonlite::fromJSON(capture.output(log_info("foobar"))) expect_equal(out, list(msg = "foobar")) }) test_that("JSON parser layout", { local_test_logger(layout = layout_json_parser(fields = character())) expect_output(log_info(skip_formatter('{"x": 4}')), '{"x":4}', fixed = TRUE) }) test_that("must throw errors", { skip_if_not(getRversion() >= "4.3") # error call changed expect_snapshot(error = TRUE, { layout_simple(FOOBAR) layout_simple(42) layout_simple(msg = "foobar") }) expect_snapshot(error = TRUE, { layout_glue(FOOBAR) layout_glue(42) layout_glue(msg = "foobar") layout_glue(level = 53, msg = "foobar") }) }) test_that("logging layout", { local_test_logger(layout = layout_logging) 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") }) test_that("log_info() captures local info", { local_test_logger( layout = layout_glue_generator("{ns} / {ans} / {topenv} / {fn} / {call}") ) f <- function() log_info("foobar") g <- function() f() expect_snapshot({ log_info("foobar") f() g() }) }) test_that("log_info() captures package info", { devtools::load_all( system.file("demo-packages/logger-tester-package", package = "logger"), quiet = TRUE ) withr::defer(devtools::unload("logger.tester")) local_test_logger(layout = layout_glue_generator("{ns} {level} {msg}")) expect_snapshot({ logger_tester_function(INFO, "x = ") logger_info_tester_function("everything = ") }) }) logger/tests/testthat/test-helpers.R0000644000176200001440000000434514705272633017314 0ustar liggesuserstest_that("separator", { local_test_logger(layout = layout_blank) expect_output(log_separator(), "={80,80}") local_test_logger() expect_output(log_separator(separator = "-"), "---") expect_output(log_separator(), "INFO") expect_output(log_separator(WARN), "WARN") }) test_that("tictoc", { local_test_logger() local_mocked_bindings(Sys.time = function() as.POSIXct("2024-01-01 00:00:00")) expect_output(log_tictoc(), "timer tic 0 secs") ## simulate time passing local_mocked_bindings(Sys.time = function() as.POSIXct("2024-01-01 00:01:00")) expect_output(log_tictoc(), "timer toc 1 mins") }) test_that("log with separator", { local_test_logger() expect_output(log_with_separator(42), "===") expect_output(log_with_separator("Boo!", level = FATAL, width = 120), width = 120) }) test_that("log failure", { skip_if_not(getRversion() >= "4.3") # error call changed local_test_logger() expect_output(log_failure("foobar"), NA) expect_output(try(log_failure(foobar), silent = TRUE), "ERROR.*foobar") expect_no_error(log_failure("foobar")) expect_snapshot(capture.output(log_failure(foobar)), error = TRUE) }) test_that("log with separator", { local_test_logger(layout = layout_glue_generator("{level} {msg}")) expect_snapshot({ log_with_separator(42) log_with_separator(42, separator = "|") }) }) test_that("single line", { local_test_logger(layout = layout_glue_generator("{level} {msg}")) expect_output(log_eval(4, INFO), sprintf("INFO %s => %s", shQuote(4), shQuote(4))) }) test_that("multi line", { local_test_logger(layout = layout_glue_generator("{level} {msg}")) 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", { local_test_logger(layout = layout_glue_generator("{level} {msg}")) expect_output(log_eval(require(logger), INFO), sprintf( "INFO %s => %s", shQuote("require\\(logger\\)"), shQuote(TRUE) )) }) test_that("lower log level", { local_test_logger(TRACE, layout = layout_glue_generator("{level} {msg}")) expect_output(log_eval(4), sprintf("TRACE %s => %s", shQuote(4), shQuote(4))) }) logger/tests/testthat/helper.R0000644000176200001440000000231414670303260016136 0ustar liggesuserslocal_test_logger <- function(threshold = INFO, formatter = formatter_glue, layout = layout_simple, appender = appender_stdout, namespace = "global", frame = parent.frame()) { old <- namespaces[[namespace]] namespaces[[namespace]] <- list( default = list( threshold = as.loglevel(threshold), layout = layout, formatter = formatter, appender = appender ) ) withr::defer(namespaces[[namespace]] <- old, frame) invisible() } eval_outside <- function(...) { input <- normalizePath(withr::local_tempfile(lines = character()), winslash = "/") output <- normalizePath(withr::local_tempfile(lines = character()), winslash = "/") writeLines(con = input, c( "library(logger)", "log_layout(layout_glue_generator('{level} {msg}'))", paste0("log_appender(appender_file('", output, "'))"), ... )) path <- file.path(R.home("bin"), "Rscript") if (Sys.info()[["sysname"]] == "Windows") { path <- paste0(path, ".exe") } suppressWarnings(system2(path, input, stdout = TRUE, stderr = TRUE)) readLines(output) } logger/tests/testthat/test-eval.R0000644000176200001440000000163214705270032016564 0ustar liggesuserstest_that("single line", { local_test_logger(layout = layout_glue_generator("{level} {msg}")) expect_output(log_eval(4, INFO), sprintf("INFO %s => %s", shQuote(4), shQuote(4))) }) test_that("multi line", { local_test_logger(layout = layout_glue_generator("{level} {msg}")) 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", { local_test_logger(layout = layout_glue_generator("{level} {msg}")) expect_output(log_eval(require(logger), INFO), sprintf( "INFO %s => %s", shQuote("require\\(logger\\)"), shQuote(TRUE) )) }) test_that("lower log level", { local_test_logger(TRACE, layout = layout_glue_generator("{level} {msg}")) expect_output(log_eval(4), sprintf("TRACE %s => %s", shQuote(4), shQuote(4))) }) logger/tests/testthat/test-return.R0000644000176200001440000000117414654755544017200 0ustar liggesusersglue_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", { local_test_logger(appender = appender_file(withr::local_tempfile())) 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)}") }) logger/tests/testthat/test-formatters.R0000644000176200001440000001376114705270032020031 0ustar liggesuserstest_that("glue works", { local_test_logger(formatter = formatter_glue) a <- 43 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 {1:2}"), paste("Hi", 1:2)) expect_output(do.call(logger, namespaces$global[[1]])(INFO, 42), "42") expect_output(do.call(logger, namespaces$global[[1]])(INFO, "Hi {a}"), "43") expect_equal(formatter_glue("Hi {a}"), "Hi 43") expect_output(log_info("Hi {a}"), "43") expect_output(log_warn("Hi {a}"), "43") f <- function() log_info("Hi {a}") expect_output(f(), "43") local_test_logger( formatter = formatter_glue, appender = appender_void, ) expect_snapshot(formatter_glue("malformed {"), error = TRUE) expect_no_error(formatter_glue("malformed {{")) ## nolint start ## 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")) ## nolint end }) test_that("glue gives informative error if message contains curlies", { local_test_logger(formatter = formatter_glue) expect_snapshot(log_info("hi{"), error = TRUE) }) test_that("glue_safe works", { local_test_logger(formatter = formatter_glue_safe) 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") a <- 43 expect_equal(formatter_glue_safe("Hi {a}"), "Hi 43") expect_output(log_info("Hi {a}"), "43") expect_output(log_warn("Hi {a}"), "43") f <- function() log_info("Hi {a}") expect_output(f(), "43") expect_snapshot(error = TRUE, { formatter_glue_safe("Hi {42}") formatter_glue_safe("malformed {") }) expect_no_error(formatter_glue_safe("malformed {{")) }) test_that("sprintf works", { local_test_logger(formatter = formatter_sprintf) expect_equal(formatter_sprintf("Hi"), "Hi") expect_equal(formatter_sprintf("Hi %s", 42), "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_snapshot(formatter_sprintf("%s and %i", 1), error = TRUE) expect_equal(formatter_sprintf("%s and %i", 1, 2), "1 and 2") a <- 43 expect_output(log_info("Hi %s", a), "43") expect_equal(formatter_sprintf("Hi %s", a), "Hi 43") f <- function() log_info("Hi %s", a) expect_output(f(), "43") }) test_that("glue+sprintf works", { result <- c( "Hi foo, did you know that 2*4=8?", "Hi bar, did you know that 2*4=8?" ) 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)) { local_test_logger(formatter = fn, appender = appender_void) expect_no_error(log_info(character(0))) local_test_logger(formatter = fn) expect_output(log_info(character(0)), "INFO") } }) test_that("formatter_logging works", { local_test_logger(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", { array <- "[1, 2, 3, 4]" object <- '{"x": 1, "y": 2}' expect_equal(formatter_glue("JSON: {array}"), paste0("JSON: ", array)) expect_equal(formatter_glue("JSON: {object}"), paste0("JSON: ", object)) local_test_logger() expect_output(log_info("JSON: {array}"), paste0("JSON: ", array), fixed = TRUE) expect_output(log_info("JSON: {object}"), paste0("JSON: ", object), fixed = TRUE) }) test_that("pander formatter", { local_test_logger(formatter = formatter_pander) # pander partially matches coef to coefficient withr::local_options(warnPartialMatchDollar = FALSE) 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") }) test_that("paste formatter in actual logs", { local_test_logger(formatter = formatter_paste) expect_output(log_info("hi", 5), "hi 5") }) test_that("skip formatter", { local_test_logger(formatter = formatter_glue) expect_output(log_info(skip_formatter("hi {pi}")), "hi \\{pi\\}") expect_snapshot(log_info(skip_formatter("hi {x}", x = 4)), error = TRUE) }) test_that("skip formatter", { local_test_logger(formatter = formatter_json) expect_output(log_info(skip_formatter("hi {pi}")), "hi \\{pi\\}") expect_output(log_info(x = 1), '\\{"x":1\\}') }) logger/tests/testthat/_snaps/0000755000176200001440000000000014705273044016024 5ustar liggesuserslogger/tests/testthat/_snaps/helpers.md0000644000176200001440000000142414705273042020007 0ustar liggesusers# log failure Code capture.output(log_failure(foobar)) Condition Error: ! object 'foobar' not found # log with separator Code log_with_separator(42) Output INFO =========================================================================== INFO = 42 = INFO =========================================================================== Code log_with_separator(42, separator = "|") Output INFO ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||| INFO | 42 | INFO ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||| logger/tests/testthat/_snaps/logger.md0000644000176200001440000000074314705273044017631 0ustar liggesusers# setters check inputs Code log_appender(1) Condition Error in `log_appender()`: ! `appender` must be a function Code log_formatter(1) Condition Error in `log_formatter()`: ! `formatter` must be a function Code log_layout(1) Condition Error in `log_layout()`: ! `layout` must be a function Code log_threshold("x") Condition Error in `validate_log_level()`: ! Invalid log level logger/tests/testthat/_snaps/hooks.md0000644000176200001440000000257514705273050017477 0ustar liggesusers# log_messages Code writeLines(eval_outside("log_messages()", "message(42)")) Output INFO 42 # log_warnings Code writeLines(eval_outside("log_warnings(TRUE)", "warning(42)", "log(-1)")) Output WARN 42 WARN NaNs produced # log_errors Code writeLines(eval_outside("log_errors()", "stop(42)")) Output ERROR 42 Code writeLines(eval_outside("log_errors()", "foobar")) Output ERROR object 'foobar' not found Code writeLines(eval_outside("log_errors()", "f<-function(x) {42 * \"foobar\"}; f()")) Output ERROR non-numeric argument to binary operator # shiny input initialization is detected Code writeLines(obs) Output INFO mock-session Default Shiny inputs initialized: {} # shiny input initialization is detected with different log-level Code writeLines(obs) Output ERROR mock-session Default Shiny inputs initialized: {} # shiny input change is detected Code writeLines(obs) Output INFO mock-session Default Shiny inputs initialized: {} INFO mock-session Shiny input change detected in a: NULL -> 2 # shiny input change is logged with different level Code writeLines(obs) Output ERROR mock-session Default Shiny inputs initialized: {} ERROR mock-session Shiny input change detected in a: NULL -> 2 logger/tests/testthat/_snaps/formatters.md0000644000176200001440000000342114705273042020532 0ustar liggesusers# glue works Code formatter_glue("malformed {") Condition Error in `h()`: ! `glue` failed in `formatter_glue` on: chr "malformed {" Raw error message: Expecting '}' Please consider using another `log_formatter` or `skip_formatter` on strings with curly braces. # glue gives informative error if message contains curlies Code log_info("hi{") Condition Error in `h()`: ! `glue` failed in `formatter_glue` on: chr "hi{" Raw error message: Expecting '}' Please consider using another `log_formatter` or `skip_formatter` on strings with curly braces. # glue_safe works Code formatter_glue_safe("Hi {42}") Condition Error in `value[[3L]]()`: ! `glue_safe` failed in `formatter_glue_safe` on: chr "Hi {42}" Raw error message: object '42' not found Please consider using another `log_formatter` or `skip_formatter` on strings with curly braces. Code formatter_glue_safe("malformed {") Condition Error in `value[[3L]]()`: ! `glue_safe` failed in `formatter_glue_safe` on: chr "malformed {" Raw error message: Expecting '}' Please consider using another `log_formatter` or `skip_formatter` on strings with curly braces. # sprintf works Code formatter_sprintf("%s and %i", 1) Condition Error in `sprintf()`: ! too few arguments # skip formatter Code log_info(skip_formatter("hi {x}", x = 4)) Condition Error in `skip_formatter()`: ! Cannot skip the formatter function if further arguments are passed besides the actual log message(s) logger/tests/testthat/_snaps/try.md0000644000176200001440000000034714705273044017170 0ustar liggesusers# %except% logs errors and returns default value Code out <- f() Output except / global / logger / f / f() WARN Running '1' as 'FunDoesNotExist(1:10)' failed: 'could not find function "FunDoesNotExist"' logger/tests/testthat/_snaps/layouts.md0000644000176200001440000000314314705273044020047 0ustar liggesusers# JSON layout warns if you include msg Code layout <- layout_json(fields = "msg") Condition Warning in `layout_json()`: 'msg' is always automatically included # must throw errors Code layout_simple(FOOBAR) Condition Error: ! object 'FOOBAR' not found Code layout_simple(42) Condition Error in `layout_simple()`: ! argument "msg" is missing, with no default Code layout_simple(msg = "foobar") Condition Error in `layout_simple()`: ! argument "level" is missing, with no default --- Code layout_glue(FOOBAR) Condition Error: ! object 'FOOBAR' not found Code layout_glue(42) Condition Error in `layout_glue()`: ! Invalid log level, see ?log_levels Code layout_glue(msg = "foobar") Condition Error in `layout_glue()`: ! argument "level" is missing, with no default Code layout_glue(level = 53, msg = "foobar") Condition Error in `layout_glue()`: ! Invalid log level, see ?log_levels # log_info() captures local info Code log_info("foobar") Output logger / global / logger / eval / eval(expr, envir) Code f() Output logger / global / logger / f / f() Code g() Output logger / global / logger / f / f() # log_info() captures package info Code logger_tester_function(INFO, "x = ") Output logger.tester INFO x = 0.0807501375675201 Code logger_info_tester_function("everything = ") Output logger.tester INFO everything = 42 logger/tests/testthat/_snaps/utils.md0000644000176200001440000000103114705273045017502 0ustar liggesusers# fail_on_missing_package Code fail_on_missing_package("logger", "9.9.9", call = quote(f())) Condition Error: ! Please install min. 9.9.9 version of logger to use f Code fail_on_missing_package("an.R.package-that-doesNotExists", call = quote(f())) Condition Error: ! Please install the 'an.R.package-that-doesNotExists' package to use f # validate_log_level Code validate_log_level("FOOBAR") Condition Error in `validate_log_level()`: ! Invalid log level logger/tests/testthat/test-logger-meta.R0000644000176200001440000000207314705270032020040 0ustar liggesuserstest_that("captures call/environment info", { f <- function(...) logger_meta_env() env <- f(x = 1) # values are computed lazily expect_type(substitute(fn, env), "language") expect_type(substitute(call, env), "language") # and give correct values expect_equal(env$fn, "f") expect_equal(env$call, "f(x = 1)") expect_equal(env$topenv, "logger") }) test_that("captures namespace info", { env <- logger_meta_env(namespace = "testthat") expect_equal(env$ns, "testthat") expect_equal(env$ans, "global") expect_equal(env$ns_pkg_version, as.character(packageVersion("testthat"))) }) test_that("captures other environmental metadata", { env <- logger_meta_env() expect_equal(env$pid, Sys.getpid()) expect_equal(env$r_version, as.character(getRversion())) sysinfo <- as.list(Sys.info()) expect_equal(env$node, sysinfo$nodename) expect_equal(env$arch, sysinfo$machine) expect_equal(env$os_name, sysinfo$sysname) expect_equal(env$os_release, sysinfo$release) expect_equal(env$os_version, sysinfo$version) expect_equal(env$user, sysinfo$user) }) logger/tests/testthat/test-logger.R0000644000176200001440000001417214705270032017117 0ustar liggesuserstest_that("log levels", { local_test_logger(WARN) 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) }) test_that("log levels - OFF", { local_test_logger(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) }) test_that("log thresholds", { local_test_logger(TRACE) 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") }) test_that("with log thresholds", { local_test_logger(WARN) 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) }) test_that("simple glue layout with no threshold", { local_test_logger(TRACE, layout = layout_glue_generator("{level} {msg}")) 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") }) test_that("simple glue layout with threshold", { local_test_logger(INFO, layout = layout_glue_generator("{level} {msg}")) 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", { local_test_logger(ERROR, namespace = "custom", layout = layout_glue_generator("{level} {msg}")) 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) local_test_logger(INFO, namespace = "custom", layout = layout_glue_generator("{level} {msg}")) 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") }) test_that("simple glue layout with threshold directly calling log", { local_test_logger(layout = layout_glue_generator("{level} {msg}")) 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", { local_test_logger(layout = layout_glue_generator("{pid}")) expect_equal(capture.output(log_info("foobar")), as.character(Sys.getpid())) }) test_that("built in variables: fn and call", { local_test_logger(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", { local_test_logger(layout = layout_glue_generator("{ns}")) expect_output(log_info("bar", namespace = "foo"), "foo") local_test_logger(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", { local_test_logger() t <- withr::local_tempfile() expect_no_error(do.call(log_appender, list(appender_file(t)))) log_info(42) expect_length(readLines(t), 1) expect_no_error(do.call(log_threshold, list(ERROR))) log_info(42) expect_length(readLines(t), 1) expect_no_error(do.call(log_threshold, list(INFO))) log_info(42) expect_length(readLines(t), 2) expect_no_error(do.call(log_layout, list(formatter_paste))) log_info(42) expect_length(readLines(t), 3) }) test_that("providing log_level() args to wrappers diretly is OK", { local_test_logger(WARN) expect_silent(log_info("{Sepal.Length}", .topenv = list2env(iris))) }) test_that("setters check inputs", { expect_snapshot(error = TRUE, { log_appender(1) log_formatter(1) log_layout(1) log_threshold("x") }) }) logger/tests/testthat/test-hooks.R0000644000176200001440000000407514705270032016764 0ustar liggesuserstest_that("log_messages", { expect_snapshot({ writeLines(eval_outside("log_messages()", "message(42)")) }) }) test_that("log_warnings", { expect_snapshot({ writeLines(eval_outside("log_warnings(TRUE)", "warning(42)", "log(-1)")) }) }) test_that("log_errors", { expect_snapshot({ writeLines(eval_outside("log_errors()", "stop(42)")) writeLines(eval_outside("log_errors()", "foobar")) writeLines(eval_outside("log_errors()", 'f<-function(x) {42 * "foobar"}; f()')) }) }) 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, {}) ") expect_snapshot(writeLines(obs)) }) 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, {}) ") expect_snapshot(writeLines(obs)) }) 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) }) ") expect_snapshot(writeLines(obs)) }) 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) }) ") expect_snapshot(writeLines(obs)) }) logger/tests/testthat.R0000644000176200001440000000061014654522610014660 0ustar liggesusers# This file is part of the standard setup for testthat. # It is recommended that you do not modify it. # # Where should you do additional test configuration? # Learn more about the roles of various files in: # * https://r-pkgs.org/testing-design.html#sec-tests-files-overview # * https://testthat.r-lib.org/articles/special-files.html library(testthat) library(logger) test_check("logger") logger/MD50000644000176200001440000001715414705655607012070 0ustar liggesusersc9e9a9d9bd27a3d9908be1e579537bb8 *DESCRIPTION c489782d0c47e9ff57164b8835ecc9cc *LICENSE 0c4be63cc9c42e17147afb27ef83e68b *NAMESPACE 3a297022036a94e485501add58f5ddce *NEWS.md ba9185b11948e6061dcece02fd2c497d *R/appenders.R 235805dc13249c4680851916904f4235 *R/color.R 4c4b2ea961e6398285f8e56af54b6573 *R/formatters.R 9cd9e3d883cbf9d4dc2c7aaf126ec097 *R/helpers.R 132ff6dbe17b4727061bd12bcf024c2d *R/hooks.R b64fbc53c6ce6018a1814fc92de7faa5 *R/layouts.R d7fd5d78101996455492891157add2b9 *R/levels.R 0984ad2552a1c9dc4c8a50692dd2d073 *R/logger-meta.R df6cc46bc7fae1a55b713f3d5065b35a *R/logger-package.R 9c779815a538544e490d8026a59e2cb1 *R/logger.R 6e250041ff86547393071123c5bb01e7 *R/try.R deaa9cdd6ee1a1605d5d644742bed0c1 *R/utils.R 257b0f7f1f0ed6b214608e33cc975307 *R/zzz.R 86613ac6e08eda9befdd89e05f9d34e6 *README.md 4ed8ff561b5d3adae108629c1349d455 *build/vignette.rds 52fb78365daf6d7911008696ce6f7f9e *inst/demo-packages/logger-tester-package/DESCRIPTION c489782d0c47e9ff57164b8835ecc9cc *inst/demo-packages/logger-tester-package/LICENSE 6c90a0c8b5882b9fddddd084fb5a2310 *inst/demo-packages/logger-tester-package/NAMESPACE 36acf8e04639b46ebe2a5e433d46874f *inst/demo-packages/logger-tester-package/R/tester.R 88b182ac9be0f95da8e6c4320f6bec24 *inst/demo-packages/logger-tester-package/man/logger_info_tester_function.Rd cb802d58a70408eebdb8ab9155ff0212 *inst/demo-packages/logger-tester-package/man/logger_tester_function.Rd d1bfd99d6c6b7a62a91e7bfe53905dda *inst/doc/Intro.R a41c1b76a20268eaa0d0b25a64a66d4f *inst/doc/Intro.Rmd 3fff70a5e66067403337a54cf4398862 *inst/doc/Intro.html ec383de3a16b618a3ee666c5f4751cdf *inst/doc/anatomy.R 10fbc94e4d6aea8632912679213ebbd4 *inst/doc/anatomy.Rmd 45def17f505f838908641c0a78035edb *inst/doc/anatomy.html 58ced1f1aea139042c04a5aa16f13ba5 *inst/doc/customize_logger.R aa66cf085b05c7833b8a572d9f1939f8 *inst/doc/customize_logger.Rmd 742dc32fc0378842d151291747d40c88 *inst/doc/customize_logger.html 149b5de688aff2b01317b4d994195af7 *inst/doc/migration.R 3d15dc4dc6956e78dcbb39b8b5eba4d9 *inst/doc/migration.Rmd 40455f6ddd7479e53e49d8a82a49b4a0 *inst/doc/migration.html ce2ccef9271d2cd4a3de94fa8d583e4e *inst/doc/performance.R 3feb9303315029439851aecb6e9243b7 *inst/doc/performance.Rmd 280ffe31794fb3616ce436ca6fbaaff8 *inst/doc/performance.html 7a0fe597844c8f2e02a47b25a66fe065 *inst/doc/r_packages.R 81c1ed76b9e2f500cf887df6e4aef467 *inst/doc/r_packages.Rmd 739d2cb8adf23ffb619315a88a827e2c *inst/doc/r_packages.html 0f6541c0558e9c08c354bd1e460b5a9d *inst/doc/write_custom_extensions.R 9e1801ac59b57839dfd3ba7c8eacf898 *inst/doc/write_custom_extensions.Rmd d52adf1af177397484a005edeb04b40c *inst/doc/write_custom_extensions.html f1655286723385a3f596d5d62c295d21 *man/appender_async.Rd 85416a34c54020c8e06d394c73d4b2b5 *man/appender_console.Rd f624d5af35caa5fff8f778d193e13132 *man/appender_file.Rd cc8a497a90bdd05c276d25f6cc47f0ba *man/appender_kinesis.Rd 3a5f724b3a7e8c2f4d2c87b9e2d5afe7 *man/appender_pushbullet.Rd e21a7c4d076526678b1537bdb9971c28 *man/appender_slack.Rd 330e033ebfddde6a43ba14ace1826cb5 *man/appender_stdout.Rd 56701398bfb18c0223c9eedf8d897954 *man/appender_syslog.Rd ee505e8c4f37848ebf3e34c4033918af *man/appender_syslognet.Rd 1f032763b2b9e77f64c6af5de6932840 *man/appender_tee.Rd 664c8d6c937f2b3260caee62caab1d29 *man/appender_telegram.Rd 4bf0af4c9573a5f3459189b0c147463d *man/appender_void.Rd f219d75c9f054dedb013081eac67e8ca *man/as.loglevel.Rd d081e65fd526583d7dcc8d75f1d159a8 *man/colorize_by_log_level.Rd b53cd4955f12f2c69141c0e7593cc3ac *man/delete_logger_index.Rd 1d71ec136036580e8ecd34ee1afbbf0c *man/deparse_to_one_line.Rd b3ef6ac842bd1ec0078e23c5d39e0939 *man/fail_on_missing_package.Rd 5ac80bf744bb1b3953f462627e58bb00 *man/figures/colors.png 5bbbbb9f92b66004ffcebf2818cf2a08 *man/figures/logo.png d9ac5554751d850c4fae64e62aa8419c *man/formatter_glue.Rd a07aa0a3799f983dc254221ee3685514 *man/formatter_glue_or_sprintf.Rd bb5c2399e1eb91eb45d7f29613d7b0ea *man/formatter_glue_safe.Rd 33f2a6bb8f66e1eb3dfb39f86e067b5a *man/formatter_json.Rd 48f556f8d9675a208728f35de574db1c *man/formatter_logging.Rd 950510ec9caf1b04003bb99504f6f39a *man/formatter_pander.Rd 58f580c7d308c5fd9aeee6a8023db397 *man/formatter_paste.Rd 16d176abfcd95b3fc94e0eb1417415db *man/formatter_sprintf.Rd 750c33aad8ecc84f0cf60014b6627761 *man/get_logger_meta_variables.Rd 79455dc9dd4071bf3e4d357ccdbed868 *man/grapes-except-grapes.Rd feaff5cd2f2ccde3cadea38aa1c605a0 *man/layout_blank.Rd dbc191fe5486e6b4cbe9e14fbd6968f7 *man/layout_glue.Rd f94264d46253ae22dfeaacb0c43f78b3 *man/layout_glue_colors.Rd 73133c0b1d3934473e2c985542ba93fe *man/layout_glue_generator.Rd c5e5b1aa5b227786089116b34522ad6f *man/layout_json.Rd 2fca7265c31a30c01d720fb6b46f59d1 *man/layout_json_parser.Rd fd6b429f17c8aebe772945b674e4f32b *man/layout_logging.Rd 73c4b5c1184e9a57029ac25adce0db0d *man/layout_simple.Rd 841fc526e58ef4b0924bf8132c760463 *man/layout_syslognet.Rd 1ca4ba0008329de8d2709b7427c35735 *man/log_appender.Rd 81523664b4755e52fa7852a78732d545 *man/log_errors.Rd d7c58a5dd50a7c9d7203c058efcffd29 *man/log_eval.Rd b32a1621eab88508222536443f0253f6 *man/log_failure.Rd 1b6232a1749de944a65ef1b1c1d4e78b *man/log_formatter.Rd f0d134ae6c99c51fb4a6f47ef52b32a5 *man/log_indices.Rd 8046332a3e4261841bc04d11a1131699 *man/log_layout.Rd 8ffbdafe8fbc39cb355a4a0302aaa0fc *man/log_level.Rd 11283cbc05020dfe5f5297223fdb9a24 *man/log_levels.Rd cfbbcff7ead75b5d1b0fb20787c0977f *man/log_messages.Rd e552f79f09fa298ef361123d946cbb53 *man/log_namespaces.Rd 76dafd7700546736af82a33a24e0ac19 *man/log_separator.Rd 8fa6ecd224449344446a95e2d35a549d *man/log_shiny_input_changes.Rd 7501e5c09cd9927100d507a40ba85ef4 *man/log_threshold.Rd 264fcaee72797bfa2afc0c523aa40b8f *man/log_tictoc.Rd 6808523c154ea51ea1cbfc986f1ce61e *man/log_warnings.Rd 9546a8008734408363bc8a30d95c88f4 *man/log_with_separator.Rd 508ca1d6d494bfc675b3a3a68885e2ed *man/logger-package.Rd 1f88668b57bd75e0a80c6f429fa166e8 *man/logger.Rd 0c5dffdd192b31b88030ff46d155cef2 *man/skip_formatter.Rd cdb6e0f9b33a7233915abde412984e28 *man/with_log_threshold.Rd 1304119bd8a871ab9f1f4e8e52ca5c22 *tests/testthat.R 04881a6b819e7599d5c0e7a7c1fba63e *tests/testthat/_snaps/formatters.md 1046fed8bcc533b3f155eb9f62b82451 *tests/testthat/_snaps/helpers.md e3f81da25e7229b79758dcc805fd6a4f *tests/testthat/_snaps/hooks.md d40834a27807c643ef722df878898a5a *tests/testthat/_snaps/layouts.md e7fcd8da615834d85c3136006d610008 *tests/testthat/_snaps/logger.md cbb0c8cd736aabd4efcf20bb5196584f *tests/testthat/_snaps/try.md b9b2499e9bf394e4d0670a16ab91edba *tests/testthat/_snaps/utils.md 7715ca2431f9d38ad46aed8a5f4b80bb *tests/testthat/helper.R 0e40d8fc8bd9534fdb2c23ba39624edf *tests/testthat/test-appenders.R b776b3f81998e84616ed12950475dfa6 *tests/testthat/test-eval.R 2d6623b8af8d2c9d4d103e3efedc14a1 *tests/testthat/test-formatters.R cae95282641d41aee00ac3cfe0214d16 *tests/testthat/test-helpers.R 617cd8534eace647abc2f4697f5190e4 *tests/testthat/test-hooks.R 20055425e8faab3cb10ce43b7eb43d7a *tests/testthat/test-layouts.R f78687c0542d92d8ae6af0ef14dcf328 *tests/testthat/test-logger-meta.R dbd582f45890a87c754cd89d21b2f135 *tests/testthat/test-logger.R f3130fca569675223a4149c20d15cef3 *tests/testthat/test-return.R ad98fbf3de1d7cd2fa0ecf72b4895a76 *tests/testthat/test-try.R d0a13ee9111377d6b4581f0114430edf *tests/testthat/test-utils.R a41c1b76a20268eaa0d0b25a64a66d4f *vignettes/Intro.Rmd 10fbc94e4d6aea8632912679213ebbd4 *vignettes/anatomy.Rmd aa66cf085b05c7833b8a572d9f1939f8 *vignettes/customize_logger.Rmd e4d0b90c553ae4fcb97db1ceac8bb7ae *vignettes/logger_structure.svg 3d15dc4dc6956e78dcbb39b8b5eba4d9 *vignettes/migration.Rmd 3feb9303315029439851aecb6e9243b7 *vignettes/performance.Rmd 81c1ed76b9e2f500cf887df6e4aef467 *vignettes/r_packages.Rmd 9e1801ac59b57839dfd3ba7c8eacf898 *vignettes/write_custom_extensions.Rmd logger/R/0000755000176200001440000000000014705270032011732 5ustar liggesuserslogger/R/hooks.R0000644000176200001440000001225014654755544013223 0ustar liggesusers#' Injects a logger call to standard messages #' #' This function uses `trace` to add a `log_info` function call when #' `message` is called to log the informative messages with the #' `logger` layout and appender. #' @export #' @examples \dontrun{ #' log_messages() #' message("hi there") #' } log_messages <- function() { 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") ) } } #' Injects a logger call to standard warnings #' #' This function uses `trace` to add a `log_warn` function call when #' `warning` is called to log the warning messages with the `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 (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") ) } } #' Injects a logger call to standard errors #' #' This function uses `trace` to add a `log_error` function call when #' `stop` is called to log the error messages with the `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 (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") ) } } #' Auto logging input changes in Shiny app #' #' This is to be called in the `server` section of the Shiny app. #' @export #' @param input passed from Shiny's `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") session <- shiny::getDefaultReactiveDomain() ns <- ifelse(!is.null(session), session$ns(character(0)), "") if (!(shiny::isRunning() || inherits(session, "MockShinySession") || inherits(session, "session_proxy"))) { 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(trimws(paste( ns, "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)) { message <- trimws("{ns} Shiny input change detected in {name}: {old} -> {new}") log_level(level, message, namespace = namespace) } } assignInNamespace("shiny_input_values", new_input_values, ns = "logger") }) } shiny_input_values <- NULL logger/R/try.R0000644000176200001440000000241714670303260012700 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 `try` fails #' @export #' @note Suppress log messages in the `except` namespace if you don't #' want to throw a `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) { ## Need to capture these in the evaluation frame of `%except%` but only want ## to do the work if there's an error delayedAssign("call", sys.call(-1)) delayedAssign("env", parent.frame()) delayedAssign("except_text", deparse(substitute(except))) delayedAssign("try_text", deparse(substitute(try))) tryCatch(try, error = function(e) { log_level( WARN, paste0("Running '", except_text, "' as '", try_text, "' failed: '", e$message, "'"), namespace = "except", .topcall = call, .topenv = env ) except } ) } logger/R/appenders.R0000644000176200001440000003221214705276464014055 0ustar liggesusers#' Dummy appender not delivering the log record to anywhere #' @param lines character vector #' @export appender_void <- function(lines) {} attr(appender_void, "generator") <- quote(appender_void()) #' Append log record to stderr #' @param lines character vector #' @export #' @family log_appenders appender_console <- function(lines) { cat(lines, file = stderr(), sep = "\n") } attr(appender_console, "generator") <- quote(appender_console()) #' @export #' @rdname appender_console appender_stderr <- appender_console attr(appender_stderr, "generator") <- quote(appender_stderr()) #' Append log record to stdout #' @param lines character vector #' @export #' @family log_appenders appender_stdout <- function(lines) { cat(lines, sep = "\n") } attr(appender_stdout, "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 `Inf`, #' then the log file is renamed with a `.1` suffix and a new log file #' is created. The renaming happens recursively (eg `logfile.1` #' renamed to `logfile.2`) until the specified `max_files`, then the #' oldest file (\code{logfile.{max_files-1}}) is deleted. #' @param file path #' @param append boolean passed to `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 `lines` argument #' @family log_appenders #' @examples #' \dontshow{old <- logger:::namespaces_set()} #' ## ########################################################################## #' ## 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)) #' #' ## enable internal logging to see what's actually happening in the logrotate steps #' log_threshold(TRACE, namespace = ".logger") #' ## 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") #' }) #' #' \dontshow{logger:::namespaces_set(old)} appender_file <- function(file, append = TRUE, max_lines = Inf, max_bytes = Inf, max_files = 1L) { # nolint 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 [appender_file()]. #' @inheritParams appender_file #' @export #' @return function taking `lines` argument #' @family log_appenders 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) { if (needs_stdout()) appender_stdout(lines) else 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 `lines` argument #' @export #' @note This functionality depends on the \pkg{slackr} package. #' @family log_appenders 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 `pbPost`, such as `recipients` or #' `apikey`, although it's probably much better to set all these #' in the `~/.rpushbullet.json` as per package docs at #' #' @export #' @note This functionality depends on the \pkg{RPushbullet} package. #' @family log_appenders #' @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 `lines` argument #' @export #' @note This functionality depends on the \pkg{telegram} package. #' @family log_appenders 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 [rsyslog::open_syslog()]. #' @return function taking `lines` argument #' @export #' @note This functionality depends on the \pkg{rsyslog} package. #' @family log_appenders #' @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 `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 `lines` and optional `partition_key` #' argument #' @export #' @note This functionality depends on the \pkg{botor} package. #' @family log_appenders 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 [log_appender()] function with a `generator` #' attribute (TODO note not required, all fn will be passed if #' not) #' @param namespace `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 `appender` function requires some extra packages to be #' loaded or some environment variables to be set etc #' @return function taking `lines` argument #' @export #' @note This functionality depends on the \pkg{mirai} package. #' @family log_appenders #' @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}") #' #' ## use async appender #' log_appender(appender_async(appender_file_slow(file = t))) #' log_info("Was this slow?") #' system.time(for (i in 1:25) log_info(i)) #' #' readLines(t) #' Sys.sleep(10) #' readLines(t) #' #' } appender_async <- function(appender, namespace = "async_logger", init = function() log_info("Background process started")) { fail_on_missing_package("mirai") force(appender) # Start one background process (hence dispatcher not required) # force = FALSE allows multiple appenders to use same namespace logger mirai::daemons(1L, dispatcher = "none", force = FALSE, .compute = namespace) mirai::everywhere( { library(logger) init() }, appender = appender, # remains in .GlobalEnv on daemon .args = list(init = init), .compute = namespace ) structure( function(lines) { mirai::mirai( for (line in lines) { appender(line) }, .args = list(lines = lines), .compute = namespace ) }, generator = deparse(match.call()) ) } ## TODO other appenders: graylog, datadog, cloudwatch, email via sendmailR, ES etc logger/R/helpers.R0000644000176200001440000001662714705270032013533 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 [log_levels()] #' @param multiline setting to `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 `=>`, while setting to #' `TRUE` will log the expression and the result in separate #' sections reserving line-breaks and rendering the printed results #' @examples #' \dontshow{old <- logger:::namespaces_set()} #' 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) #' \dontshow{logger:::namespaces_set(old)} #' @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 #' \dontshow{old <- logger:::namespaces_set()} #' 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) #' \dontshow{logger:::namespaces_set(old)} #' @seealso [log_with_separator()] log_separator <- function(level = INFO, namespace = NA_character_, separator = "=", width = 80, .logcall = sys.call(), .topcall = sys.call(-1), .topenv = parent.frame()) { stopifnot(length(separator) == 1, nchar(separator) == 1) base_info_chars <- nchar(catch_base_log(level, namespace, .topcall = .topcall, .topenv = .topenv)) log_level( paste(rep(separator, max(0, width - base_info_chars)), collapse = ""), level = level, namespace = namespace, .logcall = .logcall, .topcall = .topcall, .topenv = .topenv ) } #' Logs a message in a very visible way #' @inheritParams log_level #' @inheritParams log_separator #' @export #' @examples #' \dontshow{old <- logger:::namespaces_set()} #' 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) #' \dontshow{logger:::namespaces_set(old)} #' @seealso [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(-1))) log_separator( level = level, separator = separator, width = width, namespace = namespace, .logcall = sys.call(), .topcall = sys.call(-1), .topenv = parent.frame() ) 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, .topenv = parent.frame()) log_separator( level = level, separator = separator, width = width, namespace = namespace, .logcall = sys.call(), .topcall = sys.call(-1), .topenv = parent.frame() ) } #' Tic-toc logging #' @param ... passed to `log_level` #' @param level see [log_levels()] #' @param namespace x #' @export #' @examples #' 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) }) 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, .logcall = sys.call(), .topcall = sys.call(-1), .topenv = parent.frame() ) } tictocs <- new.env(parent = emptyenv()) #' Logs the error message to console before failing #' @param expression call #' @export #' @examples #' log_failure("foobar") #' try(log_failure(foobar)) log_failure <- function(expression) { withCallingHandlers( expression, error = function(e) { log_error(conditionMessage(e)) } ) } logger/R/formatters.R0000644000176200001440000002422314705277304014257 0ustar liggesusers#' Concatenate R objects into a character vector via `paste` #' @param ... passed to `paste` #' @inheritParams log_level #' @return character vector #' @export #' @family log_formatters formatter_paste <- function(..., .logcall = sys.call(), .topcall = sys.call(-1), .topenv = parent.frame()) { paste(...) } attr(formatter_paste, "generator") <- quote(formatter_paste()) #' Apply `sprintf` to convert R objects into a character vector #' @param fmt passed to `sprintf` #' @param ... passed to `sprintf` #' @inheritParams log_level #' @return character vector #' @export #' @family log_formatters formatter_sprintf <- function(fmt, ..., .logcall = sys.call(), .topcall = sys.call(-1), .topenv = parent.frame()) { sprintf(fmt, ...) } attr(formatter_sprintf, "generator") <- quote(formatter_sprintf()) #' Apply `glue` to convert R objects into a character vector #' @param ... passed to `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, [formatter_sprintf()] #' will be used as a fallback. #' @family log_formatters #' @importFrom utils str formatter_glue <- function(..., .logcall = sys.call(), .topcall = sys.call(-1), .topenv = parent.frame()) { fail_on_missing_package("glue") withCallingHandlers( glue::glue(..., .envir = .topenv), error = function(e) { args <- paste0(capture.output(str(...)), collapse = "\n") stop(paste0( "`glue` failed in `formatter_glue` on:\n\n", args, "\n\nRaw error message:\n\n", conditionMessage(e), "\n\nPlease consider using another `log_formatter` or ", "`skip_formatter` on strings with curly braces." )) } ) } attr(formatter_glue, "generator") <- quote(formatter_glue()) #' Apply `glue_safe` to convert R objects into a character vector #' @param ... passed to `glue_safe` for the text interpolation #' @inheritParams log_level #' @return character vector #' @export #' @family log_formatters #' @importFrom utils str formatter_glue_safe <- 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." )) } ) ) } attr(formatter_glue_safe, "generator") <- quote(formatter_glue_safe()) #' Apply `glue` and `sprintf` #' #' The best of both words: using both formatter functions in your log #' messages, which can be useful eg if you are migrating from #' `sprintf` formatted log messages to `glue` or similar. #' #' Note that this function tries to be smart when passing arguments to #' `glue` and `sprintf`, but might fail with some edge cases, and #' returns an unformatted string. #' @param msg passed to `sprintf` as `fmt` or handled as part of `...` #' in `glue` #' @param ... passed to `glue` for the text interpolation #' @inheritParams log_level #' @return character vector #' @family log_formatters #' @export #' @examples #' 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) formatter_glue_or_sprintf <- 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 } attr(formatter_glue_or_sprintf, "generator") <- quote(formatter_glue_or_sprintf()) #' Transforms all passed R objects into a JSON list #' @param ... passed to `toJSON` wrapped into a `list` #' @inheritParams log_level #' @return character vector #' @export #' @note This functionality depends on the \pkg{jsonlite} package. #' @family log_formatters #' @examples #' \dontshow{old <- logger:::namespaces_set()} #' log_formatter(formatter_json) #' log_layout(layout_json_parser()) #' log_info(everything = 42) #' log_info(mtcars = mtcars, species = iris$Species) #' \dontshow{logger:::namespaces_set(old)} formatter_json <- 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) } attr(formatter_json, "generator") <- quote(formatter_json()) #' Skip the formatter function #' #' Adds the `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 `message` should #' be a string, and `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 [logger()] #' @param ... should be never set #' @return character vector with `skip_formatter` attribute set to #' `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 [sprintf()] is being called -- otherwise it does #' something like [log_eval()] and logs the R expression(s) and the #' result(s) as well. #' @param ... string and further params passed to `sprintf` or R #' expressions to be evaluated #' @inheritParams log_level #' @return character vector #' @export #' @family log_formatters #' @examples #' \dontshow{old <- logger:::namespaces_set()} #' 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) #' \dontshow{logger:::namespaces_set(old)} formatter_logging <- 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(seq_along(params), function(i) { paste(deparse(as.list(.logcall)[-1][[i]]), params[[i]], sep = ": ") }) } attr(formatter_logging, "generator") <- quote(formatter_logging()) #' Formats R objects with pander #' @param x object to be logged #' @param ... optional parameters passed to `pander` #' @inheritParams log_level #' @return character vector #' @note This functionality depends on the \pkg{pander} package. #' @export #' @family log_formatters #' @examples #' \dontshow{old <- logger:::namespaces_set()} #' 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)) #' \dontshow{logger:::namespaces_set(old)} formatter_pander <- function(x, ..., .logcall = sys.call(), .topcall = sys.call(-1), .topenv = parent.frame()) { fail_on_missing_package("pander") pander::pander_return(x, ...) } attr(formatter_pander, "generator") <- quote(formatter_pander()) logger/R/zzz.R0000644000176200001440000000274614705270032012723 0ustar liggesusers## init storage for all logger settings namespaces <- new.env(parent = emptyenv()) .onLoad <- function(libname, pkgname) { namespaces_set(namespaces_default()) } namespaces_reset <- function() { rm(list = ls(namespaces), envir = namespaces) namespaces_set(namespaces_default()) } namespaces_default <- function() { has_glue <- requireNamespace("glue", quietly = TRUE) list( global = list( default = list( threshold = as.loglevel(Sys.getenv("LOGGER_LOG_LEVEL", unset = "INFO")), layout = layout_simple, formatter = if (has_glue) formatter_glue else formatter_sprintf, appender = if (needs_stdout()) appender_stdout else appender_console ) ), .logger = list( default = list( threshold = ERROR, layout = layout_simple, formatter = formatter_sprintf, appender = if (needs_stdout()) appender_stdout else appender_console ) ) ) } namespaces_set <- function(new = namespaces_default()) { old <- as.list(namespaces) rm(list = ls(namespaces), envir = namespaces) list2env(new, namespaces) invisible(old) } .onAttach <- function(libname, pkgname) { ## warn user about using sprintf instead of glue due to missing dependency if (!requireNamespace("glue", quietly = TRUE)) { packageStartupMessage( paste( 'logger: As the "glue" R package is not installed,', 'using "sprintf" as the default log message formatter instead of "glue".' ) ) } } logger/R/levels.R0000644000176200001440000000570114654755544013375 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 #' `SUCCESS`. For the full list of these log levels and suggested #' usage, check the below Details. #' #' List of supported log levels: #' #' * `OFF` No events will be logged #' * `FATAL` Severe error that will prevent the application from continuing #' * `ERROR` An error in the application, possibly recoverable #' * `WARN` An event that might possible lead to an error #' * `SUCCESS` An explicit success event above the INFO level that you want to log #' * `INFO` An event for informational purposes #' * `DEBUG` A general debugging event #' * `TRACE` A fine-grained debug message, typically capturing the flow through the application. #' @references , #' #' @name log_levels NULL #' @rdname log_levels #' @export #' @format NULL OFF <- structure(0L, level = "OFF", class = c("loglevel", "integer")) #' @export #' @rdname log_levels #' @format NULL FATAL <- structure(100L, level = "FATAL", class = c("loglevel", "integer")) #' @export #' @rdname log_levels #' @format NULL ERROR <- structure(200L, level = "ERROR", class = c("loglevel", "integer")) #' @export #' @rdname log_levels #' @format NULL WARN <- structure(300L, level = "WARN", class = c("loglevel", "integer")) #' @export #' @rdname log_levels #' @format NULL SUCCESS <- structure(350L, level = "SUCCESS", class = c("loglevel", "integer")) #' @export #' @rdname log_levels #' @format NULL INFO <- structure(400L, level = "INFO", class = c("loglevel", "integer")) #' @export #' @rdname log_levels #' @format NULL DEBUG <- structure(500L, level = "DEBUG", class = c("loglevel", "integer")) #' @export #' @rdname log_levels #' @format NULL 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. `INFO` #' @export #' @examples #' as.loglevel(INFO) #' as.loglevel(400L) #' as.loglevel(400) as.loglevel <- function(x) { # nolint 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/logger-package.R0000644000176200001440000000013514703307621014727 0ustar liggesusers#' @keywords internal "_PACKAGE" ## usethis namespace: start ## usethis namespace: end NULL logger/R/color.R0000644000176200001440000000437114654755544013223 0ustar liggesusers#' Color string by the related log level #' #' Color log messages according to their severity with either a rainbow #' or grayscale color scheme. The greyscale theme assumes a dark background on #' the terminal. #' #' @param msg String to color. #' @param level see [log_levels()] #' @return A string with ANSI escape codes. #' @export #' @examplesIf requireNamespace("crayon") #' cat(colorize_by_log_level("foobar", FATAL), "\n") #' cat(colorize_by_log_level("foobar", ERROR), "\n") #' cat(colorize_by_log_level("foobar", WARN), "\n") #' cat(colorize_by_log_level("foobar", SUCCESS), "\n") #' cat(colorize_by_log_level("foobar", INFO), "\n") #' cat(colorize_by_log_level("foobar", DEBUG), "\n") #' cat(colorize_by_log_level("foobar", TRACE), "\n") #' #' cat(grayscale_by_log_level("foobar", FATAL), "\n") #' cat(grayscale_by_log_level("foobar", ERROR), "\n") #' cat(grayscale_by_log_level("foobar", WARN), "\n") #' cat(grayscale_by_log_level("foobar", SUCCESS), "\n") #' cat(grayscale_by_log_level("foobar", INFO), "\n") #' cat(grayscale_by_log_level("foobar", DEBUG), "\n") #' cat(grayscale_by_log_level("foobar", TRACE), "\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("")) } #' @export #' @rdname colorize_by_log_level 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.R0000644000176200001440000000623214705270032013220 0ustar liggesusers#' Check if R package can be loaded and fails loudly otherwise #' @param pkg string #' @param min_version optional minimum version needed #' @param call Call to include in error message. #' @export #' @importFrom utils packageVersion compareVersion #' @examples #' f <- function() fail_on_missing_package("foobar") #' try(f()) #' g <- function() fail_on_missing_package("stats") #' g() fail_on_missing_package <- function(pkg, min_version, call = NULL) { pc <- call %||% sys.call(which = 1) if (!requireNamespace(pkg, quietly = TRUE)) { stop( sprintf( "Please install the '%s' package to use %s", 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 #' @noRd #' @param .topenv call environment top_env_name <- function(.topenv = parent.frame()) { environmentName(topenv(.topenv)) } #' Deparse and join all lines into a single line #' #' Calling `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 `deparse` #' @return string #' @export deparse_to_one_line <- function(x) { gsub('\\s+(?=(?:[^\\\'"]*[\\\'"][^\\\'"]*[\\\'"])*[^\\\'"]*$)', " ", paste(deparse(x), collapse = " "), perl = TRUE ) } #' Catch the log header #' @return string #' @param level see [log_levels()] #' @param namespace string #' @noRd #' @examples #' \dontshow{old <- logger:::namespaces_set()} #' 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")) #' \dontshow{logger:::namespaces_set(old)} catch_base_log <- function(level, namespace, .topcall = sys.call(-1), .topenv = parent.frame()) { namespace <- fallback_namespace(namespace) old <- log_appender(appender_console, namespace = namespace) on.exit(log_appender(old, namespace = namespace)) # catch error, warning or message capture.output( log_level( level = level, "", namespace = namespace, .topcall = .topcall, .topenv = .topenv ), type = "message" ) } `%||%` <- function(x, y) { if (is.null(x)) y else x } in_pkgdown <- function() { identical(Sys.getenv("IN_PKGDOWN"), "true") } is_testing <- function() { identical(Sys.getenv("TESTTHAT"), "true") } is_checking_logger <- function() { Sys.getenv("_R_CHECK_PACKAGE_NAME_", "") == "logger" } needs_stdout <- function() { in_pkgdown() || is_testing() || is_checking_logger() } # allow mocking Sys.time <- NULL # nolint logger/R/layouts.R0000644000176200001440000003022514705277343013573 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 [layout_glue_generator()]: #' #' * `levelr`: log level as an R object, eg [INFO()] #' * `level`: log level as a string, eg [INFO()] #' * `time`: current time as `POSIXct` #' * `node`: name by which the machine is known on the network as reported by `Sys.info` #' * `arch`: machine type, typically the CPU architecture #' * `os_name`: Operating System's name #' * `os_release`: Operating System's release #' * `os_version`: Operating System's version #' * `user`: name of the real user id as reported by `Sys.info` #' * `pid`: the process identification number of the R session #' * `node`: name by which the machine is known on the network as reported by `Sys.info` #' * `r_version`: R's major and minor version as a string #' * `ns`: namespace usually defaults to `global` or the name of the holding R package #' of the calling the logging function #' * `ns_pkg_version`: the version of `ns` when it's a package #' * `ans`: same as `ns` if there's a defined [logger()] for the namespace, #' otherwise a fallback namespace (eg usually `global`) #' * `topenv`: the name of the top environment from which the parent call was called #' (eg R package name or `GlobalEnv`) #' * `call`: parent call (if any) calling the logging function #' * `fn`: function's (if any) name calling the logging function #' #' @param log_level log level as per [log_levels()] #' @inheritParams log_level #' @return list #' @export #' @importFrom utils packageVersion #' @seealso [layout_glue_generator()] #' @family log_layouts 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 #' #' `format` is passed to `glue` with access to the below variables: #' \itemize{ \item msg: the actual log message \item further variables #' set by [get_logger_meta_variables()] } #' @param format `glue`-flavored layout of the message using the above #' variables #' @return function taking `level` and `msg` arguments - keeping the #' original call creating the generator in the `generator` attribute #' that is returned when calling [log_layout()] for the currently #' used layout #' @export #' @examples #' \dontshow{old <- logger:::namespaces_set()} #' 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)}") #' \dontshow{logger:::namespaces_set(old)} #' @seealso See example calls from [layout_glue()] and [layout_glue_colors()]. #' @family log_layouts 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") } meta <- logger_meta_env( log_level = level, namespace = namespace, .logcall = .logcall, .topcall = .topcall, .topenv = .topenv, parent = environment() ) glue::glue(format, .envir = meta) }, 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 #' @family log_layouts layout_blank <- function(level, msg, namespace = NA_character_, .logcall = sys.call(), .topcall = sys.call(-1), .topenv = parent.frame()) { msg } attr(layout_blank, "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 #' @family log_layouts layout_simple <- 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) } attr(layout_simple, "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 #' @family log_layouts #' @examples #' \dontshow{old <- logger:::namespaces_set()} #' log_layout(layout_logging) #' log_info(42) #' log_info(42, namespace = "everything") #' #' \dontrun{ #' devtools::load_all(system.file("demo-packages/logger-tester-package", package = "logger")) #' logger_tester_function(INFO, 42) #' } #' \dontshow{logger:::namespaces_set(old)} layout_logging <- function(level, msg, namespace = NA_character_, .logcall = sys.call(), .topcall = sys.call(-1), .topenv = parent.frame()) { meta <- logger_meta_env( 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 ) } attr(layout_logging, "generator") <- quote(layout_logging()) #' Format a log message with `glue` #' #' By default, this layout includes the log level of the log record as #' per [log_levels()], the current timestamp and the actual log #' message -- that you can override via calling #' [layout_glue_generator()] directly. For colorized output, see #' [layout_glue_colors()]. #' @inheritParams layout_simple #' @return character vector #' @export #' @family log_layouts layout_glue <- layout_glue_generator() attr(layout_glue, "generator") <- quote(layout_glue()) #' Format a log message with `glue` and ANSI escape codes to add colors #' #' Colour log levels based on their severity. Log levels are coloured #' with [colorize_by_log_level()] and the messages are coloured with #' [grayscale_by_log_level()]. #' #' @inheritParams layout_simple #' @return character vector #' @export #' @family log_layouts #' @note This functionality depends on the \pkg{crayon} package. #' @examplesIf requireNamespace("crayon") #' 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.") 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)}" ) ) attr(layout_glue_colors, "generator") <- quote(layout_glue_colors()) #' Generate log layout function rendering JSON #' @param fields character vector of field names to be included in the #' JSON #' @return character vector #' @export #' @note This functionality depends on the \pkg{jsonlite} package. #' @family log_layouts #' @examples #' \dontshow{old <- logger:::namespaces_set()} #' log_layout(layout_json()) #' log_info(42) #' log_info("ok {1:3} + {1:3} = {2*(1:3)}") #' \dontshow{logger:::namespaces_set(old)} layout_json <- function(fields = default_fields()) { force(fields) if ("msg" %in% fields) { warning("'msg' is always automatically included") fields <- setdiff(fields, "msg") } structure(function(level, msg, namespace = NA_character_, .logcall = sys.call(), .topcall = sys.call(-1), .topenv = parent.frame()) { fail_on_missing_package("jsonlite") meta <- logger_meta_env( log_level = level, namespace = namespace, .logcall = .logcall, .topcall = .topcall, .topenv = .topenv ) json <- mget(fields, meta) sapply(msg, function(msg) jsonlite::toJSON(c(json, list(msg = msg)), 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 #' @note This functionality depends on the \pkg{jsonlite} package. #' @family log_layouts #' @examples #' \dontshow{old <- logger:::namespaces_set()} #' 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)) #' \dontshow{logger:::namespaces_set(old)} layout_json_parser <- function(fields = default_fields()) { 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 <- logger_meta_env( log_level = level, namespace = namespace, .logcall = .logcall, .topcall = .topcall, .topenv = .topenv ) meta <- mget(fields, meta) msg <- jsonlite::fromJSON(msg) jsonlite::toJSON(c(meta, msg), auto_unbox = TRUE, null = "null") }, generator = deparse(match.call())) } default_fields <- function() { c( "time", "level", "ns", "ans", "topenv", "fn", "node", "arch", "os_name", "os_release", "os_version", "pid", "user" ) } # 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/logger.R0000644000176200001440000003723514705270032013346 0ustar liggesusers#' Generate logging utility #' #' A logger consists of a log level `threshold`, a log message #' `formatter` function, a log record `layout` formatting function and #' the `appender` function deciding on the destination of the log #' record. For more details, see the package `README.md`. #' #' By default, a general logger definition is created when loading the `logger` package, that uses #' #' * [INFO()] (or as per the `LOGGER_LOG_LEVEL` environment variable override) as the log level threshold #' * [layout_simple()] as the layout function showing the log level, timestamp and log message #' * [formatter_glue()] (or [formatter_sprintf()] if \pkg{glue} is not installed) as the #' default formatter function transforming the R objects to be logged to a character vector #' * [appender_console()] as the default log record destination #' #' @param threshold omit log messages below this [log_levels()] #' @param formatter function pre-processing the message of the log #' record when it's not wrapped in a [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 `level` to compare with the set #' threshold, all the `...` arguments passed to the formatter #' function, besides the standard `namespace`, `.logcall`, #' `.topcall` and `.topenv` arguments (see [log_level()] for more #' details). The function invisibly returns a list including the #' original `level`, `namespace`, all `...` transformed to a list as #' `params`, the log `message` (after calling the `formatter` #' function) and the log `record` (after calling the `layout` #' function), and a list of `handlers` with the `formatter`, #' `layout` and `appender` functions. #' @export #' @references For more details, see the Anatomy of a Log Request #' vignette at #' . #' @note It's quite unlikely that you need to call this function #' directly, but instead set the logger parameters and functions at #' [log_threshold()], [log_formatter()], [log_layout()] and #' [log_appender()] and then call [log_levels()] and its #' derivatives, such as [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 #' @noRd fallback_namespace <- function(namespace) { if (!exists(namespace, envir = namespaces, inherits = FALSE)) { namespace <- "global" } namespace } #' @param namespace logger namespace #' @param index index of the logger within the namespace #' @return If `value` is `NULL`, will return the currently set value. #' If `value` is not `NULL`, will return the previously set value. #' @noRd log_config_setter <- function(name, value, namespace = "global", index = 1) { if (length(namespace) > 1) { for (ns in namespace) { log_config_setter(name, value, ns, index) } return(invisible()) } configs <- get(fallback_namespace(namespace), envir = namespaces) config <- configs[[min(index, length(configs))]] old <- config[[name]] if (name == "threshold") { if (is.null(value)) { return(config[[name]]) } config[[name]] <- validate_log_level(value) } else { if (is.null(value)) { res <- config[[name]] if (!is.null(attr(res, "generator"))) { res <- parse(text = attr(res, "generator"))[[1]] } return(res) } config[[name]] <- value } configs[[min(index, length(config) + 1)]] <- config assign(namespace, configs, envir = namespaces) invisible(old) } #' 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 [log_levels()] #' @param namespace logger namespace #' @param index index of the logger within the namespace #' @return currently set log level threshold #' @export #' @examples #' \dontshow{old <- logger:::namespaces_set()} #' ## 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()) #' \dontshow{logger:::namespaces_set(old)} #' @family log configutation functions log_threshold <- function(level = NULL, namespace = "global", index = 1) { log_config_setter("threshold", level, namespace = namespace, index = index) } #' Get or set log record layout #' @param layout function defining the structure of a log record, eg #' [layout_simple()], [layout_glue()] or [layout_glue_colors()], #' [layout_json()], or generator functions such as #' [layout_glue_generator()], default NULL #' @inheritParams log_threshold #' @export #' @examples #' \dontshow{old <- logger:::namespaces_set()} #' log_layout(layout_json()) #' log_info(42) #' \dontshow{logger:::namespaces_set(old)} #' @family log configutation functions log_layout <- function(layout = NULL, namespace = "global", index = 1) { if (!is.null(layout) && !is.function(layout)) { stop("`layout` must be a function") } log_config_setter("layout", 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 [formatter_paste()], [formatter_sprintf()], #' [formatter_glue()], [formatter_glue_or_sprintf()], #' [formatter_logging()], default NULL #' @inheritParams log_threshold #' @export #' @family log configutation functions log_formatter <- function(formatter = NULL, namespace = "global", index = 1) { if (!is.null(formatter) && !is.function(formatter)) { stop("`formatter` must be a function") } log_config_setter("formatter", formatter, namespace = namespace, index = index) } #' Get or set log record appender function #' @param appender function delivering a log record to the #' destination, eg [appender_console()], [appender_file()] or #' [appender_tee()], default NULL #' @inheritParams log_threshold #' @export #' @examples #' \dontshow{old <- logger:::namespaces_set()} #' ## 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(43) #' log_info(44) #' readLines(t) #' #' ## poor man's tee by stacking loggers in the namespace #' t <- tempfile() #' log_appender(appender_stdout) #' log_appender(appender_file(t), index = 2) #' log_info(42) #' readLines(t) #' \dontshow{logger:::namespaces_set(old)} #' @family log configutation functions log_appender <- function(appender = NULL, namespace = "global", index = 1) { if (!is.null(appender) && !is.function(appender)) { stop("`appender` must be a function") } log_config_setter("appender", 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) #' @noRd #' @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) } #' Returns number of currently active indices #' @param namespace override the default / auto-picked namespace with #' a custom string #' @return number of indices #' @export log_indices <- function(namespace = "global") { length(get(fallback_namespace(namespace), envir = namespaces)) } #' Log a message with given log level #' @param level log level, see [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 `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 `.topcall` calling function #' where the formatter function will be evaluated and that is used #' to look up the `namespace` as well via `logger:::top_env_name` #' @export #' @examples #' \dontshow{old <- logger:::namespaces_set()} #' 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)}") #' #' ## use json layout #' log_layout(layout_json(c("time", "level"))) #' log_info("ok {1:3} + {1:3} = {2*(1:3)}") #' \dontshow{logger:::namespaces_set(old)} #' @return Invisible `list` of `logger` objects. See [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 [log_levels()] object or string representation #' @return [log_levels()] object #' @noRd 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 #' @rdname log_level 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 #' @rdname log_level 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 #' @rdname log_level 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 #' @rdname log_level 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 #' @rdname log_level 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 #' @rdname log_level 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 #' @rdname log_level 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 [log_levels()] #' @inheritParams log_threshold #' @export #' @examples #' \dontshow{old <- logger:::namespaces_set()} #' 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") #' \dontshow{logger:::namespaces_set(old)} with_log_threshold <- function(expression, threshold = ERROR, namespace = "global", index = 1) { old <- log_threshold(threshold, namespace = namespace, index = index) on.exit(log_threshold(old, namespace = namespace, index = index)) expression } logger/R/logger-meta.R0000644000176200001440000000304514705270032014262 0ustar liggesuserslogger_meta_env <- function(log_level = NULL, namespace = NA_character_, .logcall = sys.call(), .topcall = sys.call(-1), .topenv = parent.frame(), parent = emptyenv()) { timestamp <- Sys.time() env <- new.env(parent = parent) env$ns <- namespace env$ans <- fallback_namespace(namespace) force(.topcall) force(.topenv) delayedAssign("fn", deparse_to_one_line(.topcall[[1]]), assign.env = env) delayedAssign("call", deparse_to_one_line(.topcall), assign.env = env) delayedAssign("topenv", top_env_name(.topenv), assign.env = env) env$time <- timestamp env$levelr <- log_level env$level <- attr(log_level, "level") delayedAssign("pid", Sys.getpid(), assign.env = env) # R and ns package versions delayedAssign( "ns_pkg_version", tryCatch(as.character(packageVersion(namespace)), error = function(e) NA_character_), assign.env = env ) delayedAssign("r_version", as.character(getRversion()), assign.env = env) # stuff from Sys.info delayedAssign(".sysinfo", Sys.info()) delayedAssign("node", .sysinfo[["nodename"]], assign.env = env) delayedAssign("arch", .sysinfo[["machine"]], assign.env = env) delayedAssign("os_name", .sysinfo[["sysname"]], assign.env = env) delayedAssign("os_release", .sysinfo[["release"]], assign.env = env) delayedAssign("os_version", .sysinfo[["version"]], assign.env = env) delayedAssign("user", .sysinfo[["user"]], assign.env = env) env } logger/vignettes/0000755000176200001440000000000014705277432013554 5ustar liggesuserslogger/vignettes/r_packages.Rmd0000644000176200001440000000447414654755544016340 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. ```{r cleanup, include = FALSE} logger:::namespaces_reset() ``` 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/write_custom_extensions.Rmd0000644000176200001440000001153514654751446021235 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) }) ``` ```{r cleanup, include = FALSE} logger:::namespaces_reset() ``` logger/vignettes/customize_logger.Rmd0000644000176200001440000002667014654755544017624 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) 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_stdout) ``` 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} logger:::namespaces_reset() ``` logger/vignettes/migration.Rmd0000644000176200001440000003505714705270032016210 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) 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_stdout) ``` ### 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(rev(logger:::log_levels_supported), envir = asNamespace("logger")) str(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} logger:::namespaces_reset() detach("package:logger", unload = TRUE) detach("package:futile.logger", unload = TRUE) detach("package:logging", unload = TRUE) ``` logger/vignettes/Intro.Rmd0000644000176200001440000000575614654755544015340 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) 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} logger:::namespaces_reset() ``` logger/vignettes/performance.Rmd0000644000176200001440000001203214705270032016504 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() 298.275 315.5565 329.6235 322.219 333.371 894.579 1000 ``` Please note that although this ~0.3 ms is 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. ```{r cleanup, include = FALSE} logger:::namespaces_reset() ``` logger/vignettes/anatomy.Rmd0000644000176200001440000000721614654755544015706 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} resource_files: - logger_structure.svg --- ```{r setup, include = FALSE} knitr::opts_chunk$set( collapse = TRUE, comment = "#>" ) library(logger) 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_stdout) 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} logger:::namespaces_reset() ``` logger/NAMESPACE0000644000176200001440000000375314670303260012761 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_indices) 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/LICENSE0000644000176200001440000000005414705270032012535 0ustar liggesusersYEAR: 2024 COPYRIGHT HOLDER: logger authors logger/NEWS.md0000644000176200001440000001430614705270032012633 0ustar liggesusers# logger (development version) ## New features * logo 😻 (#196, @hadley) * computing metadata lazily, so various expensive computations are only performed if you actually add them to the log (#105, @hadley) * `log_appender()`, `log_layout()` and `log_formatter()` now check that you are calling them with a function, and return the previously set value (#170, @hadley) * new function to return number of log indices (#194, @WurmPeter) * `appender_async` is now using `mirai` instead of a custom background process and queue system (#214, @hadley @shikokuchuo) ## Fixes * `eval` scoping and lazy eval (#178, @hadley) ## Housekeeping * update `pkgdown` site to Bootstrap 5 and related revamp, e.g. reference index and run/show examples (#159 #165 #193, @hadley) * roxygen updated to use markdown, general cleanup (#160 #161 #191 #201, @hadley) * testing improvements, e.g. move to `testthat` v3 and snapshots, syntactic sugar (#163 #167 #168 #169 #171 #192, @hadley) * README tweaks (#162 #176, @hadley) * modernize GitHub Actions (#171, @hadley) * drop support for R versions below 4.0.0 (#177, @hadley) * internal function tweaks (#181 #187 #197, @hadley) * restyle sources (#185 #186 #191 #199, @daroczig and @hadley) # 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/inst/0000755000176200001440000000000014705277432012521 5ustar liggesuserslogger/inst/demo-packages/0000755000176200001440000000000013406263046015212 5ustar liggesuserslogger/inst/demo-packages/logger-tester-package/0000755000176200001440000000000014705270032021362 5ustar liggesuserslogger/inst/demo-packages/logger-tester-package/R/0000755000176200001440000000000014654755544021606 5ustar liggesuserslogger/inst/demo-packages/logger-tester-package/R/tester.R0000644000176200001440000000065414654755544023244 0ustar liggesusers#' Testing logging from package #' @param level foo #' @param msg bar #' @export #' @importFrom logger log_level logger_tester_function <- function(level, msg) { set.seed(1014) 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/demo-packages/logger-tester-package/NAMESPACE0000644000176200001440000000025214654755544022623 0ustar liggesusers# Generated by roxygen2: do not edit by hand export(logger_info_tester_function) export(logger_tester_function) importFrom(logger,log_info) importFrom(logger,log_level) logger/inst/demo-packages/logger-tester-package/LICENSE0000644000176200001440000000005414705270032022366 0ustar liggesusersYEAR: 2024 COPYRIGHT HOLDER: logger authors logger/inst/demo-packages/logger-tester-package/man/0000755000176200001440000000000014654755544022160 5ustar liggesuserslogger/inst/demo-packages/logger-tester-package/man/logger_info_tester_function.Rd0000644000176200001440000000050314654755544030232 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/tester.R \name{logger_info_tester_function} \alias{logger_info_tester_function} \title{Testing logging INFO from package} \usage{ logger_info_tester_function(msg) } \arguments{ \item{msg}{bar} } \description{ Testing logging INFO from package } logger/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/DESCRIPTION0000644000176200001440000000067314705270032023076 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: 7.3.2 License: MIT + file LICENSE Imports: logger logger/inst/doc/0000755000176200001440000000000014705277432013266 5ustar liggesuserslogger/inst/doc/r_packages.Rmd0000644000176200001440000000447414654755544016052 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. ```{r cleanup, include = FALSE} logger:::namespaces_reset() ``` logger/inst/doc/performance.R0000644000176200001440000000015614705277430015712 0ustar liggesusers## ----cleanup, include = FALSE------------------------------------------------- logger:::namespaces_reset() logger/inst/doc/migration.R0000644000176200001440000001305114705277427015406 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) 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_stdout) ## ----------------------------------------------------------------------------- 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(rev(logger:::log_levels_supported), envir = asNamespace("logger")) str(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------------------------------------------------- logger:::namespaces_reset() detach("package:logger", unload = TRUE) detach("package:futile.logger", unload = TRUE) detach("package:logging", unload = TRUE) logger/inst/doc/Intro.html0000644000176200001440000004162614705277423015260 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-10-21 00:07:14] Loading data
data(mtcars)
log_info("The dataset includes {nrow(mtcars)} rows")
#> INFO [2024-10-21 00:07:14] 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-10-21 00:07:14] 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-10-21 00:07:14] Loading data
data(mtcars)
log_info("The dataset includes {nrow(mtcars)} rows")
#> INFO [2024-10-21 00:07:14] 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-10-21 00:07:14] Oh, no! There are no cars with more than 1K horsepower in the dataset :/
#> DEBUG [2024-10-21 00:07:14] 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-10-21 00:07:14] '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-10-21 00:07:15] 
#> INFO [2024-10-21 00:07:15] 
#> INFO [2024-10-21 00:07:15]  Sepal.Length   Sepal.Width   Petal.Length   Petal.Width   Species 
#> INFO [2024-10-21 00:07:15] -------------- ------------- -------------- ------------- ---------
#> INFO [2024-10-21 00:07:15]      5.1            3.5           1.4            0.2       setosa  
#> INFO [2024-10-21 00:07:15]      4.9             3            1.4            0.2       setosa  
#> INFO [2024-10-21 00:07:15]      4.7            3.2           1.3            0.2       setosa  
#> INFO [2024-10-21 00:07:15]      4.6            3.1           1.5            0.2       setosa  
#> INFO [2024-10-21 00:07:15]       5             3.6           1.4            0.2       setosa  
#> INFO [2024-10-21 00:07:15]      5.4            3.9           1.7            0.4       setosa  
#> INFO [2024-10-21 00:07:15]

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.html0000644000176200001440000002633214705277431016260 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/write_custom_extensions.R0000644000176200001440000000037514705277431020420 0ustar liggesusers## ----setup, include = FALSE--------------------------------------------------- knitr::opts_chunk$set( collapse = TRUE, comment = "#>" ) ## ----cleanup, include = FALSE------------------------------------------------- logger:::namespaces_reset() logger/inst/doc/anatomy.R0000644000176200001440000000302114705277423015055 0ustar liggesusers## ----setup, include = FALSE--------------------------------------------------- knitr::opts_chunk$set( collapse = TRUE, comment = "#>" ) library(logger) 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_stdout) 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------------------------------------------------- logger:::namespaces_reset() logger/inst/doc/performance.html0000644000176200001440000005440614705277430016464 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() 298.275 315.5565 329.6235 322.219 333.371 894.579  1000

Please note that although this ~0.3 ms is 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.

logger/inst/doc/anatomy.html0000644000176200001440000017046714705277424015644 0ustar liggesusers The Anatomy of a Log Request

The Anatomy of a Log Request

The structure of a logger and the flow of a log record request

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-10-21 00:07:15 CEST"
    #> 
    #> $levelr
    #> Log level: INFO
    #> 
    #> $level
    #> [1] "INFO"
    #> 
    #> $pid
    #> [1] 2486861
    #> 
    #> $r_version
    #> [1] "4.4.1"
    #> 
    #> $ns_pkg_version
    #> [1] NA
    #> 
    #> $node
    #> [1] "nevermind"
    #> 
    #> $arch
    #> [1] "x86_64"
    #> 
    #> $os_name
    #> [1] "Linux"
    #> 
    #> $os_release
    #> [1] "6.9.6-arch1-1"
    #> 
    #> $os_version
    #> [1] "#1 SMP PREEMPT_DYNAMIC Fri, 21 Jun 2024 19:49:19 +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-10-21 00:07:15] 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_stdout)
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")
#> WARN [2024-10-21 00:07:15] 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/write_custom_extensions.html0000644000176200001440000231357614705277431021177 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/Intro.R0000644000176200001440000000377514705277423014520 0ustar liggesusers## ----setup, include = FALSE--------------------------------------------------- knitr::opts_chunk$set( collapse = TRUE, comment = "#>" ) library(logger) 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------------------------------------------------- logger:::namespaces_reset() logger/inst/doc/write_custom_extensions.Rmd0000644000176200001440000001153514654751446020747 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) }) ``` ```{r cleanup, include = FALSE} logger:::namespaces_reset() ``` logger/inst/doc/r_packages.R0000644000176200001440000000147514705277431015516 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") ## ----cleanup, include = FALSE------------------------------------------------- logger:::namespaces_reset() logger/inst/doc/customize_logger.R0000644000176200001440000001107614705277425017001 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) 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_stdout) ## ----------------------------------------------------------------------------- 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------------------------------------------------- logger:::namespaces_reset() logger/inst/doc/customize_logger.Rmd0000644000176200001440000002667014654755544017336 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) 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_stdout) ``` 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} logger:::namespaces_reset() ``` logger/inst/doc/migration.Rmd0000644000176200001440000003505714705270032015722 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) 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_stdout) ``` ### 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(rev(logger:::log_levels_supported), envir = asNamespace("logger")) str(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} logger:::namespaces_reset() detach("package:logger", unload = TRUE) detach("package:futile.logger", unload = TRUE) detach("package:logging", unload = TRUE) ``` logger/inst/doc/migration.html0000644000176200001440000235572614705277430016167 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-10-21 00:07:18] hi there
flog.warn("watch out")
#> WARN [2024-10-21 00:07:18] watch out

logger

log_info("hi there")
#> INFO [2024-10-21 00:07:18] hi there
log_warn("watch out")
#> WARN [2024-10-21 00:07:18] 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-10-21 00:07:18 +0200","message":"hi again","func":"tools::buildVignettes"}

logger

log_layout(layout_json())

log_info("hi again")
#> {"time":"2024-10-21 00:07:18","level":"INFO","ns":"global","ans":"global","topenv":"R_GlobalEnv","fn":"eval","node":"nevermind","arch":"x86_64","os_name":"Linux","os_release":"6.9.6-arch1-1","os_version":"#1 SMP PREEMPT_DYNAMIC Fri, 21 Jun 2024 19:49:19 +0000","pid":2486861,"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-10-21 00:07:18] hi
flog.info("hi %s", 84 / 2)
#> INFO [2024-10-21 00:07:18] hi 42
flog.info(paste("hi", 84 / 2))
#> INFO [2024-10-21 00:07:18] hi 42
flog.info(glue::glue("hi {84/2}"))
#> INFO [2024-10-21 00:07:18] hi 42

logger

log_info("hi")
#> INFO [2024-10-21 00:07:18] hi
log_info("hi {84/2}")
#> INFO [2024-10-21 00:07:18] hi 42
log_formatter(formatter_sprintf)
log_info("hi %s", 84 / 2)
#> INFO [2024-10-21 00:07:18] hi 42
log_formatter(formatter_paste)
log_info("hi", 84 / 2)
#> INFO [2024-10-21 00:07:18] 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 ...")
#> INFO [2024-10-21 00:07:19] Hello from logger in a futile.logger theme ...
flog.warn("... where the default log message formatter is %s", "sprintf")
#> WARN [2024-10-21 00:07:19] ... where the default log message formatter is 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-10-21 00:07:19.260898 INFO::hi there
logwarn("watch out")
#> 2024-10-21 00:07:19.275246 WARNING::watch out

logger

log_info("hi there")
#> INFO [2024-10-21 00:07:19] hi there
log_warn("watch out")
#> WARN [2024-10-21 00:07:19] 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(rev(logger:::log_levels_supported), envir = asNamespace("logger"))
str(levels, give.attr = FALSE)
#> List of 8
#>  $ TRACE  : 'loglevel' int 600
#>  $ DEBUG  : 'loglevel' int 500
#>  $ INFO   : 'loglevel' int 400
#>  $ SUCCESS: 'loglevel' int 350
#>  $ WARN   : 'loglevel' int 300
#>  $ ERROR  : 'loglevel' int 200
#>  $ FATAL  : 'loglevel' int 100
#>  $ OFF    : 'loglevel' int 0

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: 0x59cb13ca0720>
#> <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-10-21 00:07:19.47244 INFO::hi
loginfo("hi %s", 84 / 2)
#> 2024-10-21 00:07:19.473808 INFO::hi 42
loginfo(paste("hi", 84 / 2))
#> 2024-10-21 00:07:19.474968 INFO::hi 42
loginfo(glue::glue("hi {84/2}"))
#> 2024-10-21 00:07:19.476365 INFO::hi 42

logger

log_info("hi")
#> INFO [2024-10-21 00:07:19] hi
log_info("hi {84/2}")
#> INFO [2024-10-21 00:07:19] hi {84/2}
log_formatter(formatter_sprintf)
log_info("hi %s", 84 / 2)
#> INFO [2024-10-21 00:07:19] hi 42
log_formatter(formatter_paste)
log_info("hi", 84 / 2)
#> INFO [2024-10-21 00:07:19] hi 42

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")
#> INFO [2024-10-21 00:07:19] 42
log_info(42)
#> INFO [2024-10-21 00:07:19] 42: 42
log_info(4 + 2)
#> INFO [2024-10-21 00:07:19] 4 + 2: 6
log_info("foo %s", "bar")
#> INFO [2024-10-21 00:07:19] foo bar
log_info(12, 1 + 1, 2 * 2)
#> INFO [2024-10-21 00:07:19] 12: 12
#> INFO [2024-10-21 00:07:19] 1 + 1: 2
#> INFO [2024-10-21 00:07:19] 2 * 2: 4

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 ...")
#> 2024-10-21 00:07:19 INFO::Hello from logger in a logging theme ...
logwarn("... where the default log message formatter is %s", "sprintf", namespace = "foobar")
#> 2024-10-21 00:07:19 WARN:foobar:... where the default log message formatter is sprintf

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-10-21 00:07:19] hi there
warn(logger, "watch out")
#> WARN  [2024-10-21 00:07:19] watch out

logger

log_info("hi there")
#> 2024-10-21 00:07:19 INFO::hi there
log_warn("watch out")
#> 2024-10-21 00:07:19 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.

logger/inst/doc/customize_logger.html0000644000176200001440000013210214705277426017537 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-10-21 00:07:17] 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-10-21 00:07:17] 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-10-21 00:07:17] 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-10-21 00:07:17] no

with_log_threshold(log_debug("pst, can you hear me?"), threshold = TRACE)
#> DEBUG [2024-10-21 00:07:17] pst, can you hear me?
log_info("yes")
#> INFO [2024-10-21 00:07:17] yes

with_log_threshold(
  {
    log_debug("pst, can you hear me?")
    log_info("yes")
  },
  threshold = TRACE
)
#> DEBUG [2024-10-21 00:07:17] pst, can you hear me?
#> INFO [2024-10-21 00:07:17] 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-10-21 00:07:17] ping
log_info("pong")
#> INFO [2024-10-21 00:07:17] 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-10-21 00:07:17] 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-10-21 00:07:17] Hi, there!
log_info("Hi, there!", namespace = "kitchensink")
#> INFO [2024-10-21 00:07:17] 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-10-21 00:07:17] There are 32 cars in the mtcars dataset
log_info("2 + 2 = {2+2}")
#> INFO [2024-10-21 00:07:17] 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-10-21 00:07:17] 42
log_info("The answer is {42}")
#> INFO [2024-10-21 00:07:17] The answer is 42
log_info("The answers are {1:5}")
#> INFO [2024-10-21 00:07:17] The answers are 1
#> INFO [2024-10-21 00:07:17] The answers are 2
#> INFO [2024-10-21 00:07:17] The answers are 3
#> INFO [2024-10-21 00:07:17] The answers are 4
#> INFO [2024-10-21 00:07:17] 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-10-21 00:07:17","level":"INFO","ns":"global","ans":"global","topenv":"R_GlobalEnv","fn":"eval","node":"nevermind","arch":"x86_64","os_name":"Linux","os_release":"6.9.6-arch1-1","os_version":"#1 SMP PREEMPT_DYNAMIC Fri, 21 Jun 2024 19:49:19 +0000","pid":2486861,"user":"daroczig","msg":"42"}
log_info("The answer is {42}")
#> {"time":"2024-10-21 00:07:17","level":"INFO","ns":"global","ans":"global","topenv":"R_GlobalEnv","fn":"eval","node":"nevermind","arch":"x86_64","os_name":"Linux","os_release":"6.9.6-arch1-1","os_version":"#1 SMP PREEMPT_DYNAMIC Fri, 21 Jun 2024 19:49:19 +0000","pid":2486861,"user":"daroczig","msg":"The answer is 42"}
log_info("The answers are {1:5}")
#> {"time":"2024-10-21 00:07:17","level":"INFO","ns":"global","ans":"global","topenv":"R_GlobalEnv","fn":"eval","node":"nevermind","arch":"x86_64","os_name":"Linux","os_release":"6.9.6-arch1-1","os_version":"#1 SMP PREEMPT_DYNAMIC Fri, 21 Jun 2024 19:49:19 +0000","pid":2486861,"user":"daroczig","msg":"The answers are 1"}
#> {"time":"2024-10-21 00:07:17","level":"INFO","ns":"global","ans":"global","topenv":"R_GlobalEnv","fn":"eval","node":"nevermind","arch":"x86_64","os_name":"Linux","os_release":"6.9.6-arch1-1","os_version":"#1 SMP PREEMPT_DYNAMIC Fri, 21 Jun 2024 19:49:19 +0000","pid":2486861,"user":"daroczig","msg":"The answers are 2"}
#> {"time":"2024-10-21 00:07:17","level":"INFO","ns":"global","ans":"global","topenv":"R_GlobalEnv","fn":"eval","node":"nevermind","arch":"x86_64","os_name":"Linux","os_release":"6.9.6-arch1-1","os_version":"#1 SMP PREEMPT_DYNAMIC Fri, 21 Jun 2024 19:49:19 +0000","pid":2486861,"user":"daroczig","msg":"The answers are 3"}
#> {"time":"2024-10-21 00:07:17","level":"INFO","ns":"global","ans":"global","topenv":"R_GlobalEnv","fn":"eval","node":"nevermind","arch":"x86_64","os_name":"Linux","os_release":"6.9.6-arch1-1","os_version":"#1 SMP PREEMPT_DYNAMIC Fri, 21 Jun 2024 19:49:19 +0000","pid":2486861,"user":"daroczig","msg":"The answers are 4"}
#> {"time":"2024-10-21 00:07:17","level":"INFO","ns":"global","ans":"global","topenv":"R_GlobalEnv","fn":"eval","node":"nevermind","arch":"x86_64","os_name":"Linux","os_release":"6.9.6-arch1-1","os_version":"#1 SMP PREEMPT_DYNAMIC Fri, 21 Jun 2024 19:49:19 +0000","pid":2486861,"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/2486861/global/eval 2024-10-21 00:07:17.470625 INFO: foo
  • check what’s being logged when called from a custom function:

    f <- function() log_info("foo")
    f()
    #> nevermind/2486861/global/f 2024-10-21 00:07:17.498178 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/2486861/logger.tester/logger_tester_function 2024-10-21 00:07:17.733193 INFO: hi from tester package 0.0807501375675201
  • 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/2486861/logger.tester/logger_tester_function 2024-10-21 00:07:17.764441 WARN: hi from tester package 0.0807501375675201
    log_info("I am still working in the global namespace")
    #> nevermind/2486861/global/eval 2024-10-21 00:07:17.766201 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-10-21 00:07:17] 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_stdout)

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")
#> INFO [2024-10-21 00:07:17] info msg
log_debug("info msg")

readLines(t)
#> [1] "INFO [2024-10-21 00:07:17] info msg" 
#> [2] "DEBUG [2024-10-21 00:07:17] info msg"
unlink(t)
logger/inst/doc/Intro.Rmd0000644000176200001440000000575614654755544015052 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) 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} logger:::namespaces_reset() ``` logger/inst/doc/performance.Rmd0000644000176200001440000001203214705270032016216 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() 298.275 315.5565 329.6235 322.219 333.371 894.579 1000 ``` Please note that although this ~0.3 ms is 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. ```{r cleanup, include = FALSE} logger:::namespaces_reset() ``` logger/inst/doc/anatomy.Rmd0000644000176200001440000000721614654755544015420 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} resource_files: - logger_structure.svg --- ```{r setup, include = FALSE} knitr::opts_chunk$set( collapse = TRUE, comment = "#>" ) library(logger) 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_stdout) 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} logger:::namespaces_reset() ``` logger/README.md0000644000176200001440000001575214705275622013034 0ustar liggesusers # logger logger website [![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 flexible logging utility for R – heavily inspired by the `futile.logger` R package and `logging` Python module. ## Installation [![CRAN version](https://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 # install.packages("pak") pak::pak("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 [2024-08-15 11:59:27] Script starting up... pkgs <- available.packages() log_info("There are {nrow(pkgs)} R packages hosted on CRAN!") #> INFO [2024-08-15 11:59:28] There are 21131 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 [2024-08-15 11:59:28] 10193 R packages including the 'a' letter #> DEBUG [2024-08-15 11:59:28] 7016 R packages including the 'c' letter #> DEBUG [2024-08-15 11:59:28] 5751 R packages including the 'd' letter #> DEBUG [2024-08-15 11:59:28] 10907 R packages including the 'e' letter #> DEBUG [2024-08-15 11:59:28] 8825 R packages including the 'i' letter #> DEBUG [2024-08-15 11:59:28] 7059 R packages including the 'l' letter #> DEBUG [2024-08-15 11:59:28] 7045 R packages including the 'm' letter #> DEBUG [2024-08-15 11:59:28] 6665 R packages including the 'n' letter #> DEBUG [2024-08-15 11:59:28] 7863 R packages including the 'o' letter #> DEBUG [2024-08-15 11:59:28] 6581 R packages including the 'p' letter #> DEBUG [2024-08-15 11:59:28] 11229 R packages including the 'r' letter #> DEBUG [2024-08-15 11:59:28] 10296 R packages including the 's' letter #> DEBUG [2024-08-15 11:59:28] 9531 R packages including the 't' letter log_warn("There might be many, like {1:2} or more warnings!!!") #> WARN [2024-08-15 11:59:28] There might be many, like 1 or more warnings!!! #> WARN [2024-08-15 11:59:28] There might be many, like 2 or more warnings!!! ``` You can even use a custom log layout to render the log records with colors, as you can see in `layout_glue_colors()`: 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 `vignette("write_custom_extensions")`. ## Related work There are many other logging packages available on CRAN: - [`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 - [`lgr`](https://cran.r-project.org/package=lgr): built on top of R6. - [`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 Why use logger? I decided to write the `n+1`th extensible `log4j` logger that fits my liking — and hopefully yours as well — with the aim to: - Keep it close to `log4j`. - Respect the modern 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()` (e.g. 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 (e.g. 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)! If you already use any of the above packages for logging, you might find `vignette("migration")` useful. ## Interested in more details?
Check out the main documentation site at 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)
If you prefer visual content, you can watch the video recording of the “Getting things logged” talk at RStudio::conf(2020): [![Gergely Daroczi presenting “Getting things logged” on using the `logger` R package at the RStudio conference in 2020](https://img.youtube.com/vi/_rUuBbml9dU/0.jpg)](https://www.youtube.com/watch?v=_rUuBbml9dU) logger/build/0000755000176200001440000000000014705277432012643 5ustar liggesuserslogger/build/vignette.rds0000644000176200001440000000071214705277432015202 0ustar liggesusersRMO1]`Agz"ƨ AfwX-vKPOm/nEz3y3^j=/x^KmU|gVh%[=` %w` >(Ej&6ƠRq*p`D#Ht~oDrEGp$cͧ!=BɽH^'hs~eJa"";)MMg]qCRk{ gc !jo;1ƍR%9鑮1gIj,8,5Oqylm;F;\ct3Jneȹbg8םVUg%hpAq vVmh4?q+eA99oJQ;ZV0ZMmu,%*9mdu\Of2 edZH5m ќtlogger/man/0000755000176200001440000000000014705270032012304 5ustar liggesuserslogger/man/layout_logging.Rd0000644000176200001440000000377114705277361015642 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]{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{ \dontshow{old <- logger:::namespaces_set()} log_layout(layout_logging) log_info(42) log_info(42, namespace = "everything") \dontrun{ devtools::load_all(system.file("demo-packages/logger-tester-package", package = "logger")) logger_tester_function(INFO, 42) } \dontshow{logger:::namespaces_set(old)} } \seealso{ Other log_layouts: \code{\link{get_logger_meta_variables}()}, \code{\link{layout_blank}()}, \code{\link{layout_glue}()}, \code{\link{layout_glue_colors}()}, \code{\link{layout_glue_generator}()}, \code{\link{layout_json}()}, \code{\link{layout_json_parser}()}, \code{\link{layout_simple}()} } \concept{log_layouts} logger/man/appender_file.Rd0000644000176200001440000000500114705276510015373 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{ \dontshow{old <- logger:::namespaces_set()} ## ########################################################################## ## 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)) ## enable internal logging to see what's actually happening in the logrotate steps log_threshold(TRACE, namespace = ".logger") ## 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") }) \dontshow{logger:::namespaces_set(old)} } \seealso{ Other log_appenders: \code{\link{appender_async}()}, \code{\link{appender_console}()}, \code{\link{appender_kinesis}()}, \code{\link{appender_pushbullet}()}, \code{\link{appender_slack}()}, \code{\link{appender_stdout}()}, \code{\link{appender_syslog}()}, \code{\link{appender_tee}()}, \code{\link{appender_telegram}()} } \concept{log_appenders} logger/man/appender_console.Rd0000644000176200001440000000127414705276510016126 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{ Other log_appenders: \code{\link{appender_async}()}, \code{\link{appender_file}()}, \code{\link{appender_kinesis}()}, \code{\link{appender_pushbullet}()}, \code{\link{appender_slack}()}, \code{\link{appender_stdout}()}, \code{\link{appender_syslog}()}, \code{\link{appender_tee}()}, \code{\link{appender_telegram}()} } \concept{log_appenders} 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_json_parser.Rd0000644000176200001440000000234014705277361016530 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 = default_fields()) } \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{ \dontshow{old <- logger:::namespaces_set()} 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)) \dontshow{logger:::namespaces_set(old)} } \seealso{ Other log_layouts: \code{\link{get_logger_meta_variables}()}, \code{\link{layout_blank}()}, \code{\link{layout_glue}()}, \code{\link{layout_glue_colors}()}, \code{\link{layout_glue_generator}()}, \code{\link{layout_json}()}, \code{\link{layout_logging}()}, \code{\link{layout_simple}()} } \concept{log_layouts} logger/man/appender_slack.Rd0000644000176200001440000000232614705276510015560 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{ Other log_appenders: \code{\link{appender_async}()}, \code{\link{appender_console}()}, \code{\link{appender_file}()}, \code{\link{appender_kinesis}()}, \code{\link{appender_pushbullet}()}, \code{\link{appender_stdout}()}, \code{\link{appender_syslog}()}, \code{\link{appender_tee}()}, \code{\link{appender_telegram}()} } \concept{log_appenders} logger/man/log_appender.Rd0000644000176200001440000000235214705270032015234 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]{appender_console()}}, \code{\link[=appender_file]{appender_file()}} or \code{\link[=appender_tee]{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{ \dontshow{old <- logger:::namespaces_set()} ## 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(43) log_info(44) readLines(t) ## poor man's tee by stacking loggers in the namespace t <- tempfile() log_appender(appender_stdout) log_appender(appender_file(t), index = 2) log_info(42) readLines(t) \dontshow{logger:::namespaces_set(old)} } \seealso{ Other log configutation functions: \code{\link{log_formatter}()}, \code{\link{log_layout}()}, \code{\link{log_threshold}()} } \concept{log configutation functions} logger/man/appender_kinesis.Rd0000644000176200001440000000151414705276510016126 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{ Other log_appenders: \code{\link{appender_async}()}, \code{\link{appender_console}()}, \code{\link{appender_file}()}, \code{\link{appender_pushbullet}()}, \code{\link{appender_slack}()}, \code{\link{appender_stdout}()}, \code{\link{appender_syslog}()}, \code{\link{appender_tee}()}, \code{\link{appender_telegram}()} } \concept{log_appenders} logger/man/log_separator.Rd0000644000176200001440000000374214705270032015442 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, .logcall = sys.call(), .topcall = sys.call(-1), .topenv = parent.frame() ) } \arguments{ \item{level}{log level, see \code{\link[=log_levels]{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{.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}} } \description{ Logs a long line to stand out from the console } \examples{ \dontshow{old <- logger:::namespaces_set()} 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) \dontshow{logger:::namespaces_set(old)} } \seealso{ \code{\link[=log_with_separator]{log_with_separator()}} } logger/man/layout_glue_generator.Rd0000644000176200001440000000322114705277361017204 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]{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]{get_logger_meta_variables()}} } } \examples{ \dontshow{old <- logger:::namespaces_set()} 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)}") \dontshow{logger:::namespaces_set(old)} } \seealso{ See example calls from \code{\link[=layout_glue]{layout_glue()}} and \code{\link[=layout_glue_colors]{layout_glue_colors()}}. Other log_layouts: \code{\link{get_logger_meta_variables}()}, \code{\link{layout_blank}()}, \code{\link{layout_glue}()}, \code{\link{layout_glue_colors}()}, \code{\link{layout_json}()}, \code{\link{layout_json_parser}()}, \code{\link{layout_logging}()}, \code{\link{layout_simple}()} } \concept{log_layouts} logger/man/appender_stdout.Rd0000644000176200001440000000121214705276510015776 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{ Other log_appenders: \code{\link{appender_async}()}, \code{\link{appender_console}()}, \code{\link{appender_file}()}, \code{\link{appender_kinesis}()}, \code{\link{appender_pushbullet}()}, \code{\link{appender_slack}()}, \code{\link{appender_syslog}()}, \code{\link{appender_tee}()}, \code{\link{appender_telegram}()} } \concept{log_appenders} logger/man/with_log_threshold.Rd0000644000176200001440000000176214705270032016471 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]{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{ \dontshow{old <- logger:::namespaces_set()} 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") \dontshow{logger:::namespaces_set(old)} } logger/man/appender_syslog.Rd0000644000176200001440000000205414705276510016001 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:syslog]{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{ Other log_appenders: \code{\link{appender_async}()}, \code{\link{appender_console}()}, \code{\link{appender_file}()}, \code{\link{appender_kinesis}()}, \code{\link{appender_pushbullet}()}, \code{\link{appender_slack}()}, \code{\link{appender_stdout}()}, \code{\link{appender_tee}()}, \code{\link{appender_telegram}()} } \concept{log_appenders} 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/formatter_sprintf.Rd0000644000176200001440000000252514705277361016363 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{ Other log_formatters: \code{\link{formatter_glue}()}, \code{\link{formatter_glue_or_sprintf}()}, \code{\link{formatter_glue_safe}()}, \code{\link{formatter_json}()}, \code{\link{formatter_logging}()}, \code{\link{formatter_pander}()}, \code{\link{formatter_paste}()} } \concept{log_formatters} logger/man/appender_telegram.Rd0000644000176200001440000000212214705276510016255 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{ Other log_appenders: \code{\link{appender_async}()}, \code{\link{appender_console}()}, \code{\link{appender_file}()}, \code{\link{appender_kinesis}()}, \code{\link{appender_pushbullet}()}, \code{\link{appender_slack}()}, \code{\link{appender_stdout}()}, \code{\link{appender_syslog}()}, \code{\link{appender_tee}()} } \concept{log_appenders} logger/man/log_threshold.Rd0000644000176200001440000000223114705270032015426 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]{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{ \dontshow{old <- logger:::namespaces_set()} ## 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()) \dontshow{logger:::namespaces_set(old)} } \seealso{ Other log configutation functions: \code{\link{log_appender}()}, \code{\link{log_formatter}()}, \code{\link{log_layout}()} } \concept{log configutation functions} logger/man/log_failure.Rd0000644000176200001440000000057514705270032015072 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{ log_failure("foobar") try(log_failure(foobar)) } logger/man/formatter_glue_or_sprintf.Rd0000644000176200001440000000444514705277361020102 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{ 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{ Other log_formatters: \code{\link{formatter_glue}()}, \code{\link{formatter_glue_safe}()}, \code{\link{formatter_json}()}, \code{\link{formatter_logging}()}, \code{\link{formatter_pander}()}, \code{\link{formatter_paste}()}, \code{\link{formatter_sprintf}()} } \concept{log_formatters} logger/man/log_warnings.Rd0000644000176200001440000000115314654755544015307 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_formatter.Rd0000644000176200001440000000167614670303260015452 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]{formatter_paste()}}, \code{\link[=formatter_sprintf]{formatter_sprintf()}}, \code{\link[=formatter_glue]{formatter_glue()}}, \code{\link[=formatter_glue_or_sprintf]{formatter_glue_or_sprintf()}}, \code{\link[=formatter_logging]{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{ Other log configutation functions: \code{\link{log_appender}()}, \code{\link{log_layout}()}, \code{\link{log_threshold}()} } \concept{log configutation functions} logger/man/appender_async.Rd0000644000176200001440000000413014705276510015573 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, namespace = "async_logger", init = function() log_info("Background process started") ) } \arguments{ \item{appender}{a \code{\link[=log_appender]{log_appender()}} function with a \code{generator} attribute (TODO note not required, all fn will be passed if not)} \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{mirai} package. } \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}") ## use async appender log_appender(appender_async(appender_file_slow(file = t))) log_info("Was this slow?") system.time(for (i in 1:25) log_info(i)) readLines(t) Sys.sleep(10) readLines(t) } } \seealso{ Other log_appenders: \code{\link{appender_console}()}, \code{\link{appender_file}()}, \code{\link{appender_kinesis}()}, \code{\link{appender_pushbullet}()}, \code{\link{appender_slack}()}, \code{\link{appender_stdout}()}, \code{\link{appender_syslog}()}, \code{\link{appender_tee}()}, \code{\link{appender_telegram}()} } \concept{log_appenders} logger/man/formatter_glue_safe.Rd0000644000176200001440000000251514705277361016627 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{ Other log_formatters: \code{\link{formatter_glue}()}, \code{\link{formatter_glue_or_sprintf}()}, \code{\link{formatter_json}()}, \code{\link{formatter_logging}()}, \code{\link{formatter_pander}()}, \code{\link{formatter_paste}()}, \code{\link{formatter_sprintf}()} } \concept{log_formatters} logger/man/appender_tee.Rd0000644000176200001440000000245614705276510015244 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]{appender_file()}}. } \seealso{ Other log_appenders: \code{\link{appender_async}()}, \code{\link{appender_console}()}, \code{\link{appender_file}()}, \code{\link{appender_kinesis}()}, \code{\link{appender_pushbullet}()}, \code{\link{appender_slack}()}, \code{\link{appender_stdout}()}, \code{\link{appender_syslog}()}, \code{\link{appender_telegram}()} } \concept{log_appenders} logger/man/layout_syslognet.Rd0000644000176200001440000000273514654755544016251 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]{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/log_level.Rd0000644000176200001440000000563414705270032014553 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_fatal( ..., 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_warn( ..., 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_info( ..., 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_trace( ..., namespace = NA_character_, .logcall = sys.call(), .topcall = sys.call(-1), .topenv = parent.frame() ) } \arguments{ \item{level}{log level, see \code{\link[=log_levels]{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]{logger()}} for more details on the format. } \description{ Log a message with given log level } \examples{ \dontshow{old <- logger:::namespaces_set()} 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)}") ## use json layout log_layout(layout_json(c("time", "level"))) log_info("ok {1:3} + {1:3} = {2*(1:3)}") \dontshow{logger:::namespaces_set(old)} } logger/man/formatter_glue.Rd0000644000176200001440000000276214705277361015635 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]{formatter_sprintf()}} will be used as a fallback. } \seealso{ Other log_formatters: \code{\link{formatter_glue_or_sprintf}()}, \code{\link{formatter_glue_safe}()}, \code{\link{formatter_json}()}, \code{\link{formatter_logging}()}, \code{\link{formatter_pander}()}, \code{\link{formatter_paste}()}, \code{\link{formatter_sprintf}()} } \concept{log_formatters} logger/man/log_eval.Rd0000644000176200001440000000315114705270032014363 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]{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 \verb{=>}, 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{ \dontshow{old <- logger:::namespaces_set()} 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) \dontshow{logger:::namespaces_set(old)} } logger/man/appender_syslognet.Rd0000644000176200001440000000144314654755544016525 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/logger-package.Rd0000644000176200001440000000175714705270032015455 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/logger-package.R \docType{package} \name{logger-package} \alias{logger-package} \title{logger: A Lightweight, Modern and Flexible Logging Utility} \description{ \if{html}{\figure{logo.png}{options: style='float: right' alt='logo' width='120'}} 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. } \seealso{ Useful links: \itemize{ \item \url{https://daroczig.github.io/logger/} \item Report bugs at \url{https://github.com/daroczig/logger/issues} } } \author{ \strong{Maintainer}: Gergely Daróczi \email{daroczig@rapporter.net} (\href{https://orcid.org/0000-0003-3149-8537}{ORCID}) Authors: \itemize{ \item Hadley Wickham \email{hadley@posit.co} (\href{https://orcid.org/0000-0003-4757-117X}{ORCID}) } Other contributors: \itemize{ \item System1 [funder] } } \keyword{internal} 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/layout_json.Rd0000644000176200001440000000172014705277361015155 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 = default_fields()) } \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{ \dontshow{old <- logger:::namespaces_set()} log_layout(layout_json()) log_info(42) log_info("ok {1:3} + {1:3} = {2*(1:3)}") \dontshow{logger:::namespaces_set(old)} } \seealso{ Other log_layouts: \code{\link{get_logger_meta_variables}()}, \code{\link{layout_blank}()}, \code{\link{layout_glue}()}, \code{\link{layout_glue_colors}()}, \code{\link{layout_glue_generator}()}, \code{\link{layout_json_parser}()}, \code{\link{layout_logging}()}, \code{\link{layout_simple}()} } \concept{log_layouts} logger/man/log_messages.Rd0000644000176200001440000000071014654755544015264 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/colorize_by_log_level.Rd0000644000176200001440000000271514654755544017173 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} \alias{grayscale_by_log_level} \title{Color string by the related log level} \usage{ colorize_by_log_level(msg, level) grayscale_by_log_level(msg, level) } \arguments{ \item{msg}{String to color.} \item{level}{see \code{\link[=log_levels]{log_levels()}}} } \value{ A string with ANSI escape codes. } \description{ Color log messages according to their severity with either a rainbow or grayscale color scheme. The greyscale theme assumes a dark background on the terminal. } \examples{ \dontshow{if (requireNamespace("crayon")) (if (getRversion() >= "3.4") withAutoprint else force)(\{ # examplesIf} cat(colorize_by_log_level("foobar", FATAL), "\n") cat(colorize_by_log_level("foobar", ERROR), "\n") cat(colorize_by_log_level("foobar", WARN), "\n") cat(colorize_by_log_level("foobar", SUCCESS), "\n") cat(colorize_by_log_level("foobar", INFO), "\n") cat(colorize_by_log_level("foobar", DEBUG), "\n") cat(colorize_by_log_level("foobar", TRACE), "\n") cat(grayscale_by_log_level("foobar", FATAL), "\n") cat(grayscale_by_log_level("foobar", ERROR), "\n") cat(grayscale_by_log_level("foobar", WARN), "\n") cat(grayscale_by_log_level("foobar", SUCCESS), "\n") cat(grayscale_by_log_level("foobar", INFO), "\n") cat(grayscale_by_log_level("foobar", DEBUG), "\n") cat(grayscale_by_log_level("foobar", TRACE), "\n") \dontshow{\}) # examplesIf} } logger/man/appender_pushbullet.Rd0000644000176200001440000000170114705276510016646 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 \verb{~/.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{ Other log_appenders: \code{\link{appender_async}()}, \code{\link{appender_console}()}, \code{\link{appender_file}()}, \code{\link{appender_kinesis}()}, \code{\link{appender_slack}()}, \code{\link{appender_stdout}()}, \code{\link{appender_syslog}()}, \code{\link{appender_tee}()}, \code{\link{appender_telegram}()} } \concept{log_appenders} logger/man/formatter_logging.Rd0000644000176200001440000000355114705277361016324 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]{sprintf()}} is being called -- otherwise it does something like \code{\link[=log_eval]{log_eval()}} and logs the R expression(s) and the result(s) as well. } \examples{ \dontshow{old <- logger:::namespaces_set()} 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) \dontshow{logger:::namespaces_set(old)} } \seealso{ Other log_formatters: \code{\link{formatter_glue}()}, \code{\link{formatter_glue_or_sprintf}()}, \code{\link{formatter_glue_safe}()}, \code{\link{formatter_json}()}, \code{\link{formatter_pander}()}, \code{\link{formatter_paste}()}, \code{\link{formatter_sprintf}()} } \concept{log_formatters} logger/man/figures/0000755000176200001440000000000014670303260013751 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/figures/logo.png0000644000176200001440000024147414670303260015433 0ustar liggesusersPNG  IHDRޫh cHRMz&u0`:pQ<bKGD pHYs.#.#x?vtIME;y)IDATxwUUy'0 9,AY0Q5q3 "Ar93arUzf+@uuխ{{{ `?u p g&~*ovc.."CGsxG_%\%vqfg'@p  \ ,V}B`@\p ;@Zrb6Eͦsr992xKK/!x 8Yh4{u3 hӺe\|xHo. 0` qClܸ`!%՗Gsl?/0s/g1(Z`P;ECBغGq]׉P z.~$10(\b)!.vwP};ۡoǵU4ѷO'a4e%@5]EKCg*L-y|P6MB I 0HBH$!+2%Lnc;#Y) g ApЖD327s뭃 XBw]%`:˙ [n~4M}e$8%V2iR+Őp/ MPc𲄦 6mɘ1_E8]}O> X +qzCg:b1ѯoW9Za0Zͪ+KPј'I^Ctv6Ÿ 6\ːeE+5z"Vm&gB }A=%θd [3$[7a;4GHmwY*XzŨ,>'t>TvE0(ʣ#hlEɆ\XP7goطe>zD=5\2 twg J}aذkHL1\twY;󼼿*(uH4PڄsopF@e$?̸W3),,9)*`>K@Z#",Ѥi&2$71~])')&f B:KD諻$Kƚ5[5z? {:&td}wg:d2ҳG{lݻd2C23Wp܃&8|iVCwiN.f^1XaGu >> pQtRiZ$5d#suw N#YYTaƃry@&S4z/ESk`©Dn>Çuڝ`PG?E\2 :qg"&&r]r7P'5Q_q5Hk`Bi U$ bT=mj[ Yxo\JKvb?}O Aף vfi٪1$-|[bb+}$iqs\\%TACeexwD.Z?iˁX_$w)g:`Pҹ5F>Zh4&X2ibUUd=>v1ď!o'eeL>1cd붽3(3%dVe<4Hn曮$*:| 6>̷2ŒSP.#WHEpxi3VcL{>gLW-FZWgE0E0}9KKH}0#"@U瓵%|IO s[ M\/oa 5l.@e4۶a{_TT2~VrqMAu gi gZr@w{ڵoMr]*Svy?!@%ױ0s$ 1ɿWx>-ZŻ&lf1:}tf%Zy".bћ? Yi׮=:+AH7 Xt,=¯O4j3\.Kȧv\kv>x3D?(8u\Ÿfwn|s4FOOKo}2n#aR"Kڇ B0p[w% IeL֡c|<[&MRU*wѫ羒.sn曮dCҰQ: TBW—%VwZG}s{\\%ʠavFYr2B;^JzBbtlON<6r(]h4B,W%fvN!Z6J6?EeakeEtӼ=5kgM wNT ܌jx$IeˆqW_ݛPi٬rƒJ^!! BKeqe49u_W_bG9p{^]% <tA?-kIN6\$)8IaGa|J.w;C'UnpGdV]$QED{pdfgL̅i4jא F3uc#ѼyÓN p &aoE)%}Q.G7KVmbI <޳R+ %eW :nt*ia429^:`2NigSӘ1kX_.iԆAx:G-݆INRQdƌE6BΚ&ѕ%r+p(z@AOOOsmE 膾iͰap߽C[7!N-;P5| !\7}"&PCM ,[ܱcӾGYY)>twee+̸*@ؙ 2^qm6E 2̴Bmu'R0-t ehlfD˧-|~,Y˻'xZ2Ko#U_7xV69-EcG]_Ȁ[ć6ָriCVIpCngTWع\eeXOYTm8KL)3#[oө62r Y- D=&ìV8z8ߓ{N5 lYIMC|P9V] *5c>IV.Pl.A@z S s2- M"*ί^PL `8pzb1sE<6r(;MBO D?˃nZ94qaa 'asR_fRrБ&r72l$A` /.ʴ oKjs4hhӨIH$I'nOQ%sl$ in(~ U,bCDU&vTά&q/jYYu]轫B.j\njN{|yfX:zRRUe~R9w igS?kn 04MQcE*ĵauPRUbȔR syKPe)&#!Ѩj#2B=_3۹SMb):-.b5ՀeB*Ǚm\suq;-[6:A~PZ*羈 N0Hn!~PAcki뗃C*$a4AUhSB85\=̨RhTx57j W=w;/iDA2ti#ѧw',TtsiTFQ&Lhe `[=rJeACK<!(hׇbF"H ӹ|2~uׯ 1ёuyx(`b|$T=7$eU2%[9-z 現ϓq1yo&Matp^yy2UG5$6| v.!~P1N 0 pGOQ?[@&۶C4v8#e7V7ԺɤU*Mx4E?,,Q5sLI#ʠ!Ka6hժ1WNhP^~FeP]OtS[}1莾x}=4oݷo.J-3&˹v; zih@hU$#b\_I Q$T ruNv?V(ؼe7/>M7^f]—_O~4Zn¦;YpyK% /RA:˵>?:F4T5:F ]t\h0ݺz}hWBI jHzHh.ƞ?dJz`% bnoém:|<[&NN^Y$4t5Q ~b@7gT9曯dCаa:A`ag9Z?x5c>FY!p6+Vn<%Wo`4I&t^7F bz=g. ,i֬)dbۉMgX5'atb%9ELnnI/JR5:Y-f^—_`=٣=IȊ4$$rrӼYZlDjj"$FSe!'_L\Q$Q]nk6nHD)Wظa~YqJv:~Zۀ%t!9A>um0FLUf"+zoYfX(F"Pd|{a43fBii91 zY$$$`1[a4(..0?Ȥ~yO&Ϧ5kpe탢X̄tOرy?2#ۙ73:wnrzK:etw))dȍ#Yh5J+2xP/ڴnJQq) ?-}qLLZ6oݑ [p+&>)҄O4l0H1σA5 $\[ƨѓXf˹$ &_{%7gJ5* xDV9MhhiUVUVFb@vhEу!Bp88|I^"1)OZj*Fھc;tJOccʧG*w ʋaX:6-{<17_'@qa۹?M{X[.sp*տ^:) 7&U`6L80 PUF-PUKWȊ9t0EiwP4J-*q,QUEqTIDia;DA~1_}=?Ω&=5]B㿑Fɣg:0%%'7_.]`4|.R5i!ogLjR#+ Ry"K@,_u붱f&}#|%$.sqڷkCll,^?_N ,Zx@%ot2~;䢖Beh*F e4nC8vh VEР5Go"..nߕYP2`M@Q㵄JR̂' y78vwu+|.ڷjW֭:sxx{6fZielڲg.bժML&R&j3⣥5f, d BfJ4JB ѡcK,8xF͠(;HDFfc߲Bb(ȅ&2z~l$Aș sum4k2 R+OR9O7{\Ql|_TVHXX%%%tӇg3:UNV|6=KQ KfGWߧd`>ɐZwFmDFmšg#4l {6ӡ]3pZy*Qy.^DQdFcONrJgC9z?_wf획TVVb4i֬17t ,}Ileaѯoڶi$4V:Mw,lRkƖE% aNQXI_df%^&#4B .PB͓:}]h4У{;9c2F u& Fn<>?_~9>v{B DJ$?[2R~06DjCڐ@<~\رς;tdfgt<.xF8~d4W_=O/ZL*ߦAkA{ ^}i$ r}D[.0:ټaLΜ9 ֛Kvj`g;ʙ{ȉ/[7c=E6^ȵVUI¢I)QƠ0/D8h2'Pe ` t#9n; :JM:~T n@/kzI4i# 7\ADD)l`7ח3@% :0#_\Fh bjxq ΰxfW6.@h), E@?ިdx $Vσ$1`? ^հa:f|HFF*OxjV9VK%gZH4lT9s&=N$ R2ݢL Jrʏd1ǹ|;uw&Чn3ChidUWWs<@zZ2SM&5#aV* l. B6YTG}SMtQXT'ط0u&ѷOgn?kOԨICE0sC@燞/>>;]} !5- icrp6SP.x5!ா1tX&(8QȼpA4(΃!fy .\HS膙M;@kaPY ZaˊjezUc2-kffnM CevϤI?p}CIO h*!7`S@{h`W2>>̍neL#K⋝|Vqֳ3K9u1}_#h䪨IKOE ~?.J K-\uGr8[@Ii9.x~WtRZ|QGo@O;-WV&+ C`^<:ڴ Z04`2>\][بH4M,v5f_|9C)t77s†0uUm| |#Ю7M"귂Ȼ7BH(xo!k7fWCnAW/cĊ"q_HR#M:#Gsh߾ 7|- (iI[ ^^,N&tțdfΘY{?+ 0h.\%HOOAi<^.r'%_ĉBN($?¢RJt_Z-Ȳ`PHOOIAL}:R&ߐv-ǽ}{ЩcK9~` hcU$p E2cֻXyX#` ÔzoA0X,f.ՑCa|WEn,fOclh +J>"z]c ~'x$~ؿ *+ԬgmӔvsSPT!:E(*c  lX)Sb4xh!O9;i?0?K ͹ؼv3fgT9y 1f|~K %IiLO#%%:uIINN|;Q'pHUC8rrȴ C~-}><$g؁VM 3-\ELLiU+S i ٤26v gNɌ(·IouN.PWZ]f1bwpeaH h/sH &975%6´Mypf25 QzhB /m 6Is/W3wE/vaV~C99].$+̭V:J ؼi++Wge ljb!,AXX(D'b im#$$F> cNat-;~O'L~ 'i (f{gUt;ۨN~mNn*|f23VI**L9 ~CLH)f8x8ɶnw"OΆWI6=zQs`&[zY*]E MPPjRpX 2a4nٔO>~fM3)ÍGY4cD͖Ť8|RNIb0QT |)G+}{f"a83}b-4 Ϊ&h*+"N *T(@w݉<'f2176I$YF-w1/Ws\h (V L`rСC+ WG]`z'(Z`cm.׵&hvmS>h0o2kpÛQ[UXG3IP ̭x>O z۽$4!zg&ʎT3B@,7MAPQM7_wT&0/o#$i-e~sIU!ݦh1 .UP}8USxTW4 !s`FE(+p`ҸqVY#KT7hӦ) _׏Q'b3IX ?@oH_˯Ǡsi\dYm&|t(W]v>WIJJ©sO.A Ph`w;Y!-պ;< &(ӭ^4cd /_CCbzQ!X2,a3hVӨד*)=X1ft w@#<ЍT+*ls1uJ).b-̼yyJbby{Zl.2:+uoV*8!Ъ&U2lXYLd Pxvdzhެ2VFb64;f9{F5(t#$ evR-˧kSiҧw'/>I,DMbB 11Bli5 \& E/z۹.GyP0KKn,M;9GciΐO>ǭ+Ynw>x aWjv > 6 /EpZWh{8F"Ym~<%EHE؄O7j_eήtE(L̏sYp fIzuо9={u&GrCE3ƮKJYzЉ_TO!d7 311ϬKvv~={g0Nth ` z"?3J(-<(6%PZ1+Ǎ0w n6ZlHDx(*XK#I|]"JMBjȒDέhѲ!s,aԘI[wF5 z_x,$ѢEC^{Q^xફWdTU쐋3zyGKP]'$]Ir? ML0Ǯٱcy=Ehu1! Fl4C#rM\ڃl:\@;Q!z P)2\0u#Ɉ aږc\qU/ЫW7nj<|F~>KOX( +[sxQf5AN݆@0gΒZ4J @{N cA*o2*ϨE}H?zP1pc26H":Ef=3 ˃Lv(vy{i$I'*1{O%|s1 =m4hڬ>W]كHG;LiDt}|w\pmsj{n`O(+Пйelш{b.FY~ ys:Q!FɆ2wD2XXtO> 7lElTiq2UH1sw C'ׯ 뮻 \.'c͚5Y-[r!VȪUl<0XP/ \/,BjB^IwM&]qV < شh\"z~,IPVnͮu2ն rAq(Wrݭt_$%&ٲA۶MJ8jcLպ?6d\,,Z8f͚SSq^KQ~.f鲕̞=M6#6x4Uczr源P彔 ̊דg/ʋNڷk1ٺuɮ= Zv^Y;9l]Q Gޭpn ?F~.gE0uDD2{'t%Nk΂+֭-~GƇV9cHz]{]\u5 M?rSV`EIBӪV]T903ʆʁ-!! z9|wȀSs]Xֿ_8 ( 0{8,6DFKdIxv65d6)<#*=]>l&EoP #͌L_NR>6#G={vЀe~E(ٰ灵e%fgyhC,\c ߱oaƎypM\GYJ3z8pTVxm Cǎtn'`IqzTxϰn)4in6]U/Iң٢f+4n!?GPWFzWgŽٰqݻ]FhiB3y,;eD?]b4}sERE}![TK!Cy;`aywՏPz,?P@Ox f$d|;|߆w^^i7w K\d(uHexdYF{=m1w6rYJ8bQ&Om!%-GS^E$Ibr4I!ַ!tޝ^{q>λe§e pԧv "!KUI1w^Etڑ-*[owgakۯ+ahۃrEn1 u L'{h%Xx-3wv5/B0gbJJʰm$8S6Eļ 3\Fnd@s N?YcXTU8FIH7}>B d2f^̸n6xMel!d|n7Ht6Giγ볧i7+6`ٸs;iӦ lX#陱bfd]yr8F7*Қ`)GߘJoi޵=w6v/6 \ITrM\v6=Vjӫ U3%MTx>~|U%X’xᙿ7_?^ø_""c1N6~X9@cKdU7a5̤g.1oҞUs)ݿ:]иV#""CYGf}wAF8~ƽ 1Ц+꬧6 !HMi`  EعS~ ߝ0ƂJ3~!*s8Kja9yC>_Te 2MM  FIQp,Y}âQ5A~}x|#كs~wFbR'%' qj2a6)!:Ą 0P0/|PtS3H#z5-V޺ oT;q H*+s ]27-U} 6$Qϊ'߇_l/+vc=.c}Cptxٱ}SK.mic 0Н{]GХhܟi}{ĉ%?N]/ ~rMEbߛfЬa=>~1z׋*Chd6!trߢWә+u;[Y^_$л`^9;HϒGz%{( Fn8'u< r޽b kj֖Bf٠ ֮ȇa%TVhִ ?>뮻Gh( p`4P5tYD8]q75p+q8,F$3pԢUj%d&I(qlژGUav֤ oq1{buM^~9ī"h:ۛdkij=;_IMh? )j'e5J K@9ze6`07h~ VD\׫kؚ )ݱžֈp`/$!$Fi Y{“oA!~x:HV;-t"#xaCCjGֱ|.QOjswsd[޿=R1G{ EfPctmAAU5nGOaw(*.#** 5.U켄n,&6ȇp}/y{~­F+ZpŐ밙iI)^~(Ք -9};&, cd>Vnâzxin@SWpEs "eǼL5._l(ThFEy.5\Ô)9{1VV]*.GH=lzɦufnU%;|^sd,}n`̩tmD>}()gʔi%(jk#|<< 뤐BX^rq5tzY3$t1a4N7!!6nuf2/o+)wpA]hل!&$#t9)>CY34i:dz0ׇz.]`Pd@C.uHN݈=}3Iv+syX>=7a_A%vVHܝW}<קa6)%E֥BAo 6ƿh ~AM⹫}*v))-&`w^mf<=f۸JnB7J}73!$q 2t >Q*Vᗝ6\H`<4V=oe[5kҧ_D@E54dYfì\ Bքؒrt'6 ǵs&BN=('#i4 3 qI0 ݈[놽iFՆ3,?^xiɧβLIQ cӍ߄n'VA]`A/SφB` jAeWׄy&@ϿZ! 3v3HC@]a0(de1PUX2 :ՃV_عQ'XC@4?~7rjͯunFRNLWi&aN7_dH*$7Dc@;Qȧ}GMxv&#o{@E*VN_ Kgx^ f6 p lHfAM[Lp ؝WyUdnӌ'E%!IQdԫKDGEUgctP0?+ #~UsQ)F D x*^VxTpmK/W?M{,|pʳ3xQ̙W^zn.C1*&N ;٣3_$ w ̤^ pl槔ZO/ceڗYj5Yנyz\sMٽ5Sӵn)MI]w:XM4^I"ksyY( >r;-[4D*SJmwdxDS5^$Ms28̋Y41VoA4"X}9dӢQ;V/e޽G$:!"7Y:o7`.7ӷmyprJ\Z)9ܱpÐhР!V5<5@q1c|^Ǜ&8zMˣqJYCX8/"V\gsy?|iVhm-BNv>FOd(+ 9Đ1/u;+p w [NRd AȊBGǶt#a1RݔE2E,.,؛϶㥴M9ϕd[0oKKqdfi?˺ޱ[ѻAwMXM *aW$zpaq7 6h؂ArH|ASF EbJv.WXyP- Q_~XčCwiN"״>3*$ ٠.eeW89tO><۷C\9t@\8WoԽ~8h>&m&ՠUF|ӕބPUY_ ؃>Fw=N >?{2w]:6?̜xO TիK/>LDD(o,/eOn!@6Hhf+ȡVّ_9^0wǑ"!bz}~4+Wm"6ȈvU<ڪyiIub2ޛNFiXTK<،M #2Lw-Xqc>JTR  8mDPSHDd͢\ٹMQ&E&aƤp֒ӷ{ 67@b궊0 a%RJ ^>%1n veM 34FZP2kWs^y5f'$%c6~%b-Iހҧ'، T5+71{b6nű㹔Vtp<\@ iXl4o<Ӿ/ ,?"`clbĈR@MLkKiDP\z`"f/jnNm6ڴaO>EE8!ôjiLXwK4u(&}֋"EcőjW3yc(.,`HhFܘDvEB@U!2G"ĸ1a] \yޝ^gn 3MAE=3_̯PWw2ݜͽox>nʗѿQCZ%4>PY҃] +ywߖCvYGf,a2K2+XŞ#>:4 H+eF/QX!QjbsSFIOt&"#Cۻw`1t}ki ;N1ʼsUSx}57Jˆ 2"?C|B,aa=fl_EDw)X;גv:|g' ".56?]Ӄr')GKAMff]v%zoJ=8ؕ3uuK\}쯃Vёwo0r A߯rB m9/LFSi+RMB}B|?ϵ=Ydԓ jЪ@[N}`͡ 4m`arҳ:nʳٚ#GPPyS.r#^/çn %n`j'K@L:k * զQs \tcf:V<e ^Vd4lNll$.kεAtCFtXƷj6 dD@i2^8ԩرЪuk"""0رI嚫ЫȑƑr]DN9ݯ/Qg~[xdԫó܏nCUS~MGLt$C^K慌~6~ 1xUV 'h4pR5.e3(˿? [PB94A"&5JB Yx A\'n )]DyV)h,˪S#e*~ZDfd#-꙰[%6Y^v&+Gu2؄wǠ,d牊ǀ&ȫW=SbX4ݚ[iT\<00 "v܍G j@lC*Qd8Xa2Scw53!QpFNFv맰ISe<=q$38qrS5CՍc9\^9nZ׫W/ ;ˆM0}=Oxv$ $ߎ!MO kϽƌ鳸.c*61- **:u:|&H8Ƽ4vr1<̙zZa(3" $˨9Gъ 04l)K!2xh~.G7ӱ$=ɂդN$Yx{~u$U 5L̼F倏7'0vD i FvKiFC!L#KԚiQdi[PŅWH@nJbA 8>BᚮvޚR;*hfcrfDb/Yn^-ѹ^%e K2ǻWíxWWyY^!Fn2M;{>;f~,]ubқj'>*0лl xUQ!|_nA*'!0pc;1Ra}n4R9w{ Ga$3kb^~C(duha~iI$4F<:*ćHF^7*ILB 3Y'?g_#h!n9ڑ٤6Ogf4 L_QIe3|L3V:+ pm7;!2-*cOF4A&jX|IDܡ=͌,~3}4U@#sc4t:9xݳqb'0UlG(ڿha !g*$QB#㉼(MQ7@O݋d<{[dB&RiZ\;`U5CrWoέK˨C@_de\QIDB(G--XĿmJiQ6&ԌĆctt*?Ϥ+hbw}ڸ٫])t3+[ lq]^ !h(=SS2 pzԁ_z|6pd3bs|7ߚ@MٳWyikcQLaQ @MӘ:m ;BY07DQV"d4&|:g8Y*J4ڴn[ox 4*F~MWUq$OX6Ik(VCGblҊ@1gmFYnC c5rc {ߞ]H19¥nVE4 & & Mj <@t GL٦ ".kan>NOZ7 +hbƤVHM|Z`3*JcvLZՇWPb׿3$հ(>QaKJ~C>2F9-o7nH; fH h%U]Om0ǝ6^x)5h-Z1Cio=NsSRz"y]ƌz6md=~:7\]gudFz^ AbHS_Y شm7W!u3#?W.&i,QW/1aZ)7M#3388ܟүoQYOΞ=$j`OF,iɠyN Ub gT/IrJBukYXdO*E[p`36Of[iڢ!Uo ԉgG41O*[7}E0u<r |Dz;&؉0pUpCy)g$6;ٙSJX MB%[ MTe@U;=|8emՠP$kˢ ,Ub19xG i!ؿƪbnzKݿyL$ =\fh#S <_~I2 _@ ~GSyBd-psWYXY"<0F .cO9~xm]%6C%9N귪OlL N{vV]]ۇ[nHff*{E yoh~w' (ٿ JrHBtZaaRTT޽q>T/s\c;s発BE8p(~YMqV:?[QPPdaK(DENsIVM_ѢuhruXٰaM[4DQ]$g@.Tm|x^[<\;t0:MHNOItKpcU4lۚxjW!ju bTdeD3f}+Qh ]K 99M3oR-/v22VD6QN1p 74b{J6CJ4)rU ޞQ\«zXTCH2/wFx` PY{cSC P0~6c( 4jof&kA:kG]E$ڶbl;^HxX? JO@zK^/۴+*e-lڴwEQQkN9s0g09V3`Lw5kҺ_)E{`ZjI*ˈh@ ~%ZuC !t޹ %dEF4n/٨64dֺ^@"nD7A3=3B@Yf K;ifblƓu{{4WnIϷE|B"~1w9!I$%%CYl9kh=8 ,G%1AsE7@(h "D$!4JiLoO߾|Q:g{b{l6]<|WaMH岶͘=g l{゙FbbK,aE6SLVQQdj#BOu[,fn!/ʕaBB%Po#|DIusҝȲJvvAn0I %${ \x+VI 9RvzbDzA"7Gc,?S&5!U0V? &Ǩd4Cwծ7ŔLW*:/a!͌FvmJnUd#%GD%lrwUJ f1A36 ^TϦ92)АxC%bnf@"&77͛7Cލ$+yxA4!NƒET5:ڬl.C$##P(/F]ͷޢ;a4x0vlV9K$[ܳӷo \($JMԓWŀے{N}}ޚ5[􉯑_̸O1ko!%R¢%:Cppa9O1Fv޹K-U0Ȁڴi|P̨Yv?'JPixTXzD Q/BVY(-,+ #SAW+hꝵWrOAp;6c0/Ê&lQՍV(XzڀƵO`)UM8[rܹ-ݺ4ڴiu h&LW`>_@L%8Z!(p "5263|S5 !!2Ffhcvy94S2瀋 LD[p[n7DL{ QPNScp# $I;qu@̸Y}1v QՃf:]{;7mb%,۵yeQhNȌqLFW "g/"*:)|:a> N;~Nir3o1>wgю{﹡ZJd jFjl1(2d"GV2^Niq)9-qx?a 9‚I>@EU&ϡ H4&۶ҳNjwXb(ULXL$z5BNX9,TY1O%l"6o2r/XR;ps/kvzj~Mb Ĩxfi#H5p,^Ư>V/r4!&vCXmV}N"u JA0SƯOm*>HԋxsOjxM 1֍oFdD[d˯_gR;&|Dq. m &0&7]Q=N*J̊C|^oN0y"Ceo Fn&پ},Z…Xu eŅ.CYy%DzcjԜ(ь-ܯ'Ntv1BfL+  yj%^KAɯ PXҶ jw3R:5ʔzmDlx4&DemrD̪,Qv4yd*IJXb*gG!%XvTpeD7(0̂~, \_, >lfF/N?4 i!{CJc(/K`D9 7Is ;Gb\|"aQddIG`ֻGXJmI.]xs-\9sq|Lo%? GyY7{*a |U+ȰjI UE$$1dT†ˍDDa3*!4 OWk9rk"Dž0 *z(K͞N%1hn(Y hz;c>T#۲ /ݭyJa[$I|;ǫ8^HSӣ±rAxbiq0_(\/6V&+4~ڝǂ} nTpY.>^y"U\}~f*6M&|~py5=u/ZKͧWY9~^Vʖ#R;멯n$J:M.&]p]l: ѣ¤p}d)* u.qƐw>U9N|{YxP'q!0#tN|*-$Y1B {^thߞ:0ᇙ6u*/NƊ%<=T|! hFQQo'[4R$2nddK|n/O=y d2VttB9]xV==t )IA4|>?2`>rJ0e`HֺP>/IѨȮ no'QB @6 i2kWؽSWc$~Xd!=瘟88?zt,?Yxf O"3Gy{~>Ɖr/;rYscL&#t411\5TUթ[5)RAn1e3!=u\AV>4j:.bNA:9j$ `Ǻlu la`KK[ˡbܒ?B7nŁy衇xQ6f|L:lV)"4Nff*Nr$EMӈBe4@fB5/MӉG +(`.!4A*EuvdRoMmdꔴ'LH?@ OQZM"i(&:jc@tʚU*%{j>Ad#[ZxGnEI@jΎZqjrիwunQ[S]҅Bu *)/h#h/;˕ާ(Y╩*B"OƄEsT$j6zƒ$!|(8JLHB$H ͛5:'q\MkѢG];01aAG2}5kPQnkxX̿oJ!_Vm7no$YW84d3&8(jX] XzLe)bV$Cw!쯖ߨJ-T$etL4*V!Yd.)M4@Y#8jtijᣙeH0#03k-`7zjPn1j t>UzCStd ,D7\h QVm*?f!$FAA1W5n8W3~M~ 0nȩ,6T]zxn|Og.|df@όX7N@r V`mڃ-{#c?acWdő߈il]C ޢʂk0[dfX|\=V'3B掦&Zeuʖ|/YFO7eIVC০]n>UpU*6Q3! v3Eݻ2/NL0hBonyb\pux(<95,:Nii9q!r7%Yc̫0)$[_%IZI[y 5:$r/ h@TJ$AGFi݄ WUS/‚ j/ T@bº5p,O2 +6#p mjdkʶ`t:JyDH.Q|j K(RNfoI\.6m@t!_Hm^p漱Q\Eh}0,&ooh5>s;N_#zGˡBy~[6$6673\8!IvE&'_ 5>'5 +k6ax9}cfQ6c$ yoovݶ#''dI/jZ+c_YN9TA&i,^(صCekpً,P].A!5T`V_-X%$YwmVHէŌt,rDa!2|S9 9:̪}.;@ E~vw'؊" 1 Lw޾33y 7g^뻾ߎPT]3΁Z@äNkFZLt5N>ruv IQKZX> Ƙv$;-Qw 6|-!SnѣPgr\Pr`7}5ŀ>wX1y2/s/fYfw`ZM563:0Ƭ^_u?q#.ŗ1ȡl>!$rq/ X,4m֌^z1a8mPDm΄^if+.*J#U:6~jh'/YWIǐd-# m 6*PBn Y&nXVgGt*};muiLEEE|H)^_>|vwu-I1R[Wkha!=W_#,`UB&; îjR" c$yfΨCcꍨ&3ޒ|~=B\G2knj܂kRUowy'yzx~DlH]bQy|X+4 fUWуݻ3ѧԅt쏁(-*_dvX&-?z#)рU]áDͧt5 GߡSg3I0JUǯG|Fŵ[R[`u3* ^F$%x!Je^'j1EN?Kzj|/d,tW@Nͼ\8# lWlҦZ:H Wt# m6є9ilڴ'Zw]4ƍv64IMb|.yp CSLtUifJg[s4I9!DbHMy6AYz\z+\ޜ)SM,a-XNhLIvܞGQcڒdQg@3(f_ F3b@d<kHV1i%U%_"fJػ?7]Hǎy7wHG.A ˘#\P)E8 ArUXk麤SC5̢xݯ;6q$_R*D~ųt[XB:Z!Gd%U5*G-˲ShSuj)J~@]$ ڦHi ѡ'q`,&[Jsujn%>a'e)e;_)))?\su'E 37p .XIQ~CSLT$Ϭl:d=>zb5g!)ymu&(#wѥ['? Zw#kFʟxϩorG&Odi6R0 Ţ%\6<=;RNqhl8% ӫ=g[FR3DJܛh+߆5Nk)ߑVb6Ph)$ld#¡q{yoڵ'wBSDX1 S\U^slic H‚thu7yvwp FF1б0(?hE} "YN|:BCeG#ḠR\\_ěo~aԫYjAhk5kۢ:nxY掮Vz$c!#2؜[ńn7a3),>X[+0B@Po _B+@Μh_'zXyn}~_J^F mbŽ9w7?.o2E eNVf ӮԤ)i{3C9V(ȊB䆟&ƛ嬨KEX>8ˏ6QMHk"B, R&sSx2#)_@ɜW!AQѽQ%э4Lԏ*&Ijq>2,wb+!\DF3IؖOLL$mg\s_3!j}xF͢s8O3lP0MoC׮m:&>0vwVw8X:Y9@)Q" pZP(B\L8blρ=<IDATE ւ¬xp^?WԠ5B,0 *vb$9$!6AePUIйH __lnA|QHI8 ,&Q/sRW $NdOfsv[eIqq1s.aoXn+ɉLW^9JLy{ n(@[ch@HpPǯ;#3o6喉t[o,+g$Q`)ɉ'DaN*):FRC4^zq _|:^O˩Ɓ[^̢<;4V87Y<ˮ`wyvIV&|Ɲ?ᡄ [{Fy`/yi۾q|{ ڴk E%H]C=[bMxY ,&>FHAM@z̈́R*d(vhuƂF,t[Y~54#!. Rby - ZD)8]-4 OGeXT٦a2$a̪FR]-TU²nfqSdW4mJ橤sرc/vɓs]7вes.]ڵ[0+ps' =T-x#ĴiWӱcӆַ A~]޽!} FLt$M4"55А .}̮]F)$% ڷE: "oWK w)fQy?4#@ژYzULz 5ml #qcIm~rIE BՎB&`$$Đw3Xqw)%1]n@:P#ku%x$ A?LB2>Mbfy,3Ҥz4s+Ҩ*ܚΆbҨQ#:oOj&8 WTϑGر3Օ( D94Dx9'D$,)B[w¤ſj$\7.}oyY7x?;v쥠̰ak2deee< ޗF+–fJGNّ/#̴lMFMD( ks|\ 4$e[qh+DvOk%SV\LJT~_&~ 0ׯ+˲Yv+/=hh: n8՞ݳiycϧrCG2'iﮣʱL@:0/&UJyY%jY8̪ B&P^ȭ`%{JXXXol6ѿwn ;o8v~[ʓOێiRLD>M^?Nszu$""}ܳڇ*^oz}2W@j'iڎV7?8t4+-y*#4| ؃fOi{!J4\5J"0uMHߡ""B0'^:#yՙ|OAF]":s}UUs6m~g/s+ Kg߾LbyէqEɾwH[_@*EK#זs7ya&6!J]~v[ IFf$R4L~ in 0>sl_?r(ʇ~ϋ7 Ԇ(RQtvÅ%* IAQ8t(Ӟ:wj|:3ؔQr yW߂H@Glxey$4$+stVdHT$6AO+vP:R`]5)G~@'v# }\PZZsfx>}aQZZ曟VJeq$hkKK>N$ qt=\ǙcϜ9K}de咔=ͅg8o!upZ4Fez6VxՏ)<;:-/mqFbڋxۈטjI`*5V ]&:ğ>y ׎ c ,]y% Zd]HJXD6Z~89@^Tke4! Hwt)4kڸށm&ADJh|ǜO7Q*DL2BM@C s U "&D)nWAMB" F 9!Nʡe]LFDD(ݺgƝuq&*=| {{7r!p^Wjk .*Bɒ͒$ɯpחN9$0"0XhW@;i+-M2p1)f^%ˆYp1o) 2H.d SLO v jn]{#**kj>=Vz1Kz*+ڥo, ;ג1qBUc!p?.|B|Llec75hִ1>3 .IU(>P7`dT^_6X''L }EV#ˋ,lsC8̈́TTD=C8ѻE*<9̦S:H)^ĥnWώ,$a"Ģr˷eLv~)na9}~٫j,)a)&55qΠ :~d^ɧ+un2PT3!E+|N^)115b֙CûYf%6V1~ȋM#5G4{}QScDP.`78n:Q]{S@jo}S ;yՉ2pۭӿ_n3UUz LjskO:R Pj^!HR/̺5k'YfUF4cfŹ Э{{(sϯ`8O><]"(-L*B?&$d~BϠG&0Z]ٴ%͆,5޸JcB~fY'?1:'hH)_s ~NiFQPX®"-"mӏ>”bʼ=bl12KZI VPl^|NR5Ӯ};&^{&%5`E]>J~4>J7+.N2yT(p r~~E!?95pyN$&q9<"zb#''{s0iD(Z$3fWrǝwLBU3]\i6{{ٳ*LSO?HBb"gkw-I¯|:S}% )Lr`.?:+.?F㑚JsL), 2)QB l{M,C{#FѬENBдY [x͂04pr,bB7.6cZ"&@Ll$C:0eN io;oZ­,NR1)E1k}+%t܅)}b _^#(OedX&wṃ2jg~oM͙'c3+mS-uB[b/UAL3f(#F yVK/yؽRJR\uN 6fiUUz< >+΋,]y[҉neʔ  S͡_.Z\1=G3ϼgJܒ(믻 ހ7v- "WXO/JϞ 5:q *l?W+LyjF9J{2m>Jj4;Ƒt:qIs#KA(!hěCUy\Wv+;7GHBUՓ7݅'Fޝ sp}mJ Z iJWy;(N̸"Pja$DQ</|O<ŴmӒg~cF rPA Rڰb*6D,,Xס{1*V9ea efD/5 sXz3V7elL'ELJňNdN_׷LYM|"rTCR܈4r*=,*;aAfHۊ%iӂ{vG̙šO v2aH0(jo9[UU~/ä+qޘ$&&vNX|^6;;yO>@@uh^9 !gGDFEf^}MibM3a*}d7FēVmA_@>*Cs}SUyv*<\4  BZA&v,BH]Wp܂)sv9nkR['>X%(媫s9Yj ~;^j[⪫."$$_~Y>[K+U:*d.7}Ѭvk~=}F%gןN&QuKK*xݯxm3h`x^ʶ.#װFិƛ+PÄ&&CL`gݏB.mHqP2DV*rrs Y QGTԸor7>1P**ZYuqݗ0~JCD`nhX5Hd$ T#5*6ȡ\^xZk]È`ѧm\aG  1-.ek|ۏ.eJsM) hFcK2X]m 0 1w:܄}Ħ BR; 6̆ dUTk3@Mi :sj Qɤr饷ftDLLddm嫯~Wҳ|VFߛ eS0u)-f͡O\qя(.Y_>Z'~Eƃɔ)OYAO(\ C/&ᜉlھ'|~[FdUI1qUt"#uG<[1<ڦk_h9ꝷ&9EYh<)w 3vӉ~>LfQu:_9A,A* үt@Rȡ5IH@b^RY1JK*Y|qqTgQQ5*}p 9z'3wpkQ=1_ ^D;^fnʦETSz( 2K\d#%LHLUXsUrgNί+9P~T_YY͛o~-Ry)5o ټy'Nq3gb2E7ozo6t:wNc `0b֘_{}[̢~}{#w1xT]d~Eڭn UZĄ&&p%Fп+=ztGUldWJ8{r[@b"lw\rԿa_}3I5Mӑ (g! , c G8|8Wc$| pX-Tp+;~S2͛1v@(EݽmI 2׽<8`5ݒV-"+K?=Gpv UdY.Qٻx&V n 7B9kvʠo-}mZu^ Y[,`9@ܟʆ;ټe73?nsyř߰Hyhz-q ^˾'YDu@ҹWv{r-˗Cdl' uQ54na~%HMgO*b: )8sMI-qƴ%Y`EA]jՄ607TZEԩzƚRɐj >IсX$.Ɛ)ډ{iP tieOMw0<áyH@v߆yxQ.Gĥk#EQ  V ,! DuU|:~][wr$;ֶMv9^c=yYV. s^"'R3ys:Nyf}Bvv>nwҷ_oT W~"ZLot3~u&oI-L4 U9+b)zvoa4WYy䰃=T(O,)zE΄eK4n1H/&6[N\9?r ;@@~bς}jd!pH @JLj=CoVN<)(⁅xּ"C%Unj5W> *i^C-PJdY m!7E6  1Xff5jDF5\n7Gر}֯gӦM߿4M2&l1߾ aEi7Ӷ};[Ȟ1R {p$ċ2폱ʹFA ?fXdDݞ=C7s90&c$gdK^y^c-ajJ1g'wcB(FPXWW7qQ*C5ZHMM"<<2RTpșTӮ &#O~Fb61\v[so *=GX!ͻI {70lu57@px'ޅCt#HllbR!,,ZUִj՚.INn.'cSTTDUu@h|>?SSS2V58|YSk2߯' js+'oWT'a-<lZ 1iF]m>Jk["Jwc¼gՅyO9#tA͏r߼oQ D/)%"wBlw>9R }}VBK x 4n@Ӧ9T]ƴ^`NQ/fq4H &)JH/jÔ9<>MA Z,""@ EmFh9{!s+"g/TjP46*{r 6  ٸ-$z!AjՊVZ1u)4M ԃ.t]G4.̙3 \@N//LQ:O㖛'sEcATS# iEݓ9$TEWqԁ4J5 v"H!VpzuJb2WT, OLyJjXNp= # ?9<~U - aЧw'ߎ*43l&b"U?T';Ƈb7H :&Q p=~MX?u!ꍰ1g*hL J(<Yۡwdjmt(tV@Md#6σXh~$47տ_E٘aC!8Lzxx8 0Rwb˖-ر ).)9iX/֭s51$6jT!g'hϿXb-<<7lddcs[Zn?^ .[/'!!AQWq(&}$/~tNq*VW**qiܗ!GCΚ'5EAD|<1]B<'\ǵA0o3XyW ci8ci9$6((FaA&q`(`Ʊ*O|\AMmB ݢ(Bp]d\>5ha- )B{v.FvM:ApBEJpȦ]S 9{&xϱ ֌ssY.bJh;m "2U_Ѡ-q[Hh!QՄPbbb4x0F%Ng EE8pWgS ZhI2qxGϨܻؾI:g">OŒ7gpW >gsPU ΃X[U4N g\uJ`Z]G kfPO!( p"{_&$YsEEQXv+Ӟ-QtI~8gI `9'[ Z$! ?p@CE+3]\agώ Ѝ_2*hp0&biYT;O@ɥ. '"XhG!2R#Cl DŁPj=Z09w/bwط>쪀 ?#lBv?A $Ѭ+2C[.ؾ6υp;YWHngDt-$!i֜f͚3bHڷkǵ]{o'*䒿; WBH6"6lذ~VraWy!A/8o08qQκtddISitO:[go-9&XCpBvѤe63o@^ٻ!%IcbA9PH3wa֎joBhh0w1wp}L/ %=T\bi ffQTk_<oBdž"f2X@a`n`8Mpw "%D؄h.(K 7 ^ mQ pTB~ c[ u3ƴgrvAĤ0۷-uZKR6_YH- jeS~5xi:o)fl\?fV~q!W_}!&उ:rqS/'6$GϦ 8*nX*[n kpݴW;%zayENkzl.I2..N)0 Gl~}Xo,\d 0dH/|F~d:.CPhp/aYf?H\;ݓ<¹w`3މ k|<1%-!US{k To96ŦU+* n83H#"GwS;Bah.|(˅ڇ 1P\bO&C ~% ڷEЋ8i x78e-R-­98ͣ:+,*w?OAa wv[@{ bĨ/3a;?x,GQZZ-; >ajïs36oJ^&KXZEvePlC$ZcH%-lr96g},bZS. u '_dUV?siDׯ)hݤ|3ULشxB(1ېc@~]R",v0Ú'6kC͈i :̃5Ii<Ѵ3~'F` ewn^~Mjd!R>ԜADQϼCYY=:lO6v]U  C 2c=j1VA~Q)<bCxE>FTLY(rgq']JR5,&nqzv䭷gR)MU]'.`+iЦM*"گ)!6^E5LΌ4^#h aaV1 JmMJUQb5aUJ[ ~rUBi]77xnIK?AYeh;\ flw.1ɻW"" %Ig# |vP8`/YDbV43WNxĥ/ɇQfl& dYWsbMPԎ}h|ٍ|2+[ˣ>o~&"< 'B#䥟˯VLK

CYgtсҒrɣOGGG`YyݯWmRլҵQ8ȶJ)grfDYI!j".JJQA4pd*'40Ϋ˨޺[r)p~8;@5!ՈA!+QIARk 7fc[da#o@[H1U,iːQjwlnnҧzv=KRR[aB( @W2Hk&QS烙pxꉩ8 ?jhzc`A>~^$uo]د.>ٌ(*t W)Yrhf;;{,pQ_5a3SQ55kQ™rTWW/0/ r7a n> X^4k@z- a6iܷ`/_OB݌! ?!{^`$:l ylh&bdФ!XtmMiQ)|-%JhL.țɾCG+$ ۅas%r}L=}ou;*xc8}'555ʛʇ8o՘T@9c4G|tfNK^QB:tC40/xeȩX?S@MGzg 03p˧[*l}o@9CAfyVyȭk r ڲC|3{Z >ΐH! bL y( ٬+I wSywl{L&ZŶ\dsuXM uKU䦚仍-P%YT!S`Jc<杷?frۭz"/k9u i8m:A,Ss"rBK[va=tDCC0P[!ц `FdK8}Lgrmp$1Dnt(3CUpܸ=3^[gOq #)̎ԏ ! N=pj#qռ+[a݌9E$M"0)Dp[ H/q`+Kp9C)wz(r$ZlF_қQB#j4(T*ʣN Oi|9>>nFǀ֡|o|@/Cٕ&nVcSMJIU! RpӨ IcǖE\8Hg%3ԺQa3@wkK ciO =~zƕ"Xb#Wlsҥ['@`Yq4ms_0[-=Tz<4R"kό.".ػqp $Qh3Uk+3N oIl7Yr-b@bxyAەOZL0d !دf R;5C!l(qrw嗟EQ>3yl:%%suDF3¹i*V[fjM&5Y?|ě5}9ХdH#3 cAY@/^aB߬DbiLjbr]$VJI)A CD$>˸#6/>XKc=q1yEp(jf(GU]Vw(5Жe-3ÛpAxz4 ')Ԋլ~#R!n&fp[ZcA/F($eL}6zrJtmڵ$(/NUrSL NYY=v&eFӬY 3V 4p韲dz}܂bƧIRؐ]'qց&eͯeB5aJmpÉ36WI"mFV"T(t(u6vW>ߧt&a5)[LnƥlnRI/amv9V[yLeſsaSY:ӥO(KNq`&6iNtM'6&А ,=~;-iJB@I@UOdR lġH~hEUpo6tY}.!/]7Y'iS\"HY=$[4RQشi's-?a +#hX (G;׸G`B`Dxq>z!j3QpH]G(i'?L&z”)1z(l(Z l@S]p#8.J\>ZFd1Ej뼟la͑2ć6 y~M]]QP5C//FVWL}Vv$Lm!̖L9r]$^Wc2myÇӳgw-Zʬ/e2g]I6͹}ʕ\uXR-:.vD&0x0>;9 6400O__K ]PM.Q(Ӄ^Fjjnb@/ǹ(_XMv:ve؟IF گ~Yᷞ%a Jt<[sq>Klrz1Ux|VߪбC[nq]<`;%pdbS[6Ux߭D/iyL옄͢dk/΍XTlUwVӱck=R-@ffNs[C/0}F8o\[+cnˀ1;F1vu5]U~vUy7EDhjKK%A ^f 51[,sF5 &. . C1lH* JZb+r5,}xXud*&cit *PB:CCay G@BH7*46СCqe6Bd oG k@؅sƣbtn]ws;/o^h<6ED8ÛޣhrTr2sG""I},zhOd; 2@òujnOԅ%]CِY^ e)csz۱XnuIHC*Kj,VIH]ǢH@)WGU@Iy۷_;@Weof.oԼ4 U܁0fBBA 4v^)(wAzW!O>?cH--,@Vz1w#۶'b--d2ѢEnRBBCjSq\|H+e2mۋm c!㰎9fڃ,+F//!}-JH8jf'!cas]oH&)lڴ+7ѣGZZ uxUak(0_ uᓱvSj8wz6Z_Wht,Hic }$ù6Z?<; o)@%"$a!~ޘ1mӹ ?g'މ.F G4^l).*bڴ﹤ʨd3wX#4"Gnˆֵw ҚQ)  bctԚo[v3w2~[N~fx|o\L{`4s ٱ`Ө+ag2fMiV~Ibls](|c_vH !!8􆪸Zi?q* 3I-F?랽(+$-m7Me<|p$@cYm&`UL\~}{puW0jPB&{C40GrKRZR´ism+1*oPQR/x]4U@uB΀A=:.1rQ%|Qn-ya^wꄆ3thoAaA kmg,_\ZUW{8n $aT) PQki!%F+Z MrҪ\B 䄒S#-h(s~ӿY'pp$2⢏ӏ+STB ʪQlv#atkRj+iFtiOLLHIҝL^.:lAH 9X6fXr%W\G>=r53I\Ey9?~=7h6{+$-8眾)S%y2׆E!^ZX)#>5VY TE%01!hk70cs8!nv.Z͚5[]Rv0tA>KLReNp'p M۲."{Ȉ9P9~O-clպ#_[Á}Rc, !WVkAzzl]c[NVTwBC,UsmaL=`L\Q#pmҷo\n}571$xaAڶmsy1 nn_¦`EbU$)D}rDۡ7hYIP q))I%ܑ5[X~;{8)WY]A`*[WɌ A؃d1?̘a0%*^XR"T+qf{YY%wQ ғ>pT3eI.AI-M1&ΟOb.`Pnr]֗dNt^wף$1::wKb>ZŔ*j8ox'ѣRYdd#eL?5I "t2gNL%))#r-dlٺm?08n4MYYf$v$Lb5 SΈ`tF L&|?Z9Mb]v|3сY(=pek]n_ cHtG)n<E@T}RB2~l Eo/+kp$ޤ8+bh4fMS@$+ N:>/w8ۭ\DZ蟨f+BZ“۵{wWsυzP|̲nѲe*Z7eܸx=^*.˯λ_I0Eܹa1z>2GT9gΧm3A-D=Kv-GAH6V0]? K"M$'s ߬A_(JpTgshۦ/#eMkg2 -A L:-BpP65tІȈ0@F喧''@Bz ֝ +JH/yj~ *#PxWvTK>gWNyk#j:?0 |ڎ.uiXcfuuhGBMna@U^ Y^^RV^%-Kl)Pa9?ɓn`59Y>9cwP{M,*WiD!Cefn ;kr K) *0(uAx87by5_]þ5]f%Ҡk#<[K# ~G r4k(Oڥ rIu@γYdyx(_GX2OMB8*e: @LL$/t/vEj[BiJ$.$#e@3ZCv5/g5^f zaz^5 7׉Fb6"(y8Ubyxu>|f˯a< ^y~b-<>#aTJM f ֎B]XJSƸ7Z{i ,Rxj1%eE<kaU@ jaL=ѩi>/iNH5.aŊML8Y_fҮm+ЀFEEb0y!ٰIM h==DDSS9?ejfVBø\n7Kk/b(vMܗB3o/?6!ft> mìط ۸"_dy1&u_fqXbL )|WgD cڴyGy֭]ebN cü(|.7oVLvf>EPl`Cb! )i+8ӓ#V%pEUQ9|$zl&))|b둔NU׃e4ȧķnU M7N@ɴhUWkٱXnBHj gh*B:6#X:BND;-h snM&Ԧm_r#6݌DzوHQpMײb*~O?.} aqeKE@*+7G|6.<)jh.'Gn0Brk>*Ee=&721ƒs[n}+6Ҷm+ 6W#!! 咸N%Vrlx#&c>JL&~usMJ77m_0yx%ts\+ɟ4 _`40(O_U $QPӧq/v 3L@ E:{NLL$' @ }ՆSX) BQAaq W9ۛo~ƫOM嶶VEr75l!VJG5.L "鿀Β04DpUnӏ:P2|L,lG~>S΋$ĦrhR"-^|i&}Qsv{(+`K VNWYHؿ=w/CUzxvI N1=ξ+}"44Ԕ$vgDH86]05m G @DĠ_x"Q7E!15m\}<_aށh%xph ΣƧ4M%`GvT2 KBm*`!*+?f&qBˆ/9 lψ\@¨9- _ʌ$..GJl\ s&Q#q+kl\9@W?=F帝hYFcsPFt^1?&{߼пa4[ÊkV[/KaQ O! ~,|o^SM[z\Hw JdA!"%^:aKчoo73Q-;0o"hd%^Q{†ҐYw /r0]"5.7 &#u ڠh@bmLh\⥼צJM/;0{l:?״[]c2QZ^Z%8)sp|#7'0tH/4U٘m Q3]դ#l !ѽ= Ǎ(|~FVW BQS[Ide)MvmD8Q`0Pե>f纸*1ƝϽLiYeGՉPah̀fWUss_XjQixswR,߈XVY' %`)% 3rH#KH4kt85knvl6+c?b~"cvV?*%XG]V%s {i;{Ue=iwNƬdy;p¨6۩2::&%5 .lyWj-CA DXQ.谋QAu3|ZǭLτp7FFS6qC7a3YD4rJzH %,()3(&k8x7]rFt؆E<:ٓ[ʪQ6w JXMˍbMf'ؽtGn#::&y(nsƤ$%1xPObK iY_ Q1eG?u:SU~u$5(#9s tЖ7f|GPUH7-G c$!&yASK?FB0!3gM &#0v{_i:M-7Dd/GY;. EgU~̵ }|^ǖ68-k Z~(8Kk~so>}V@8:M^|[W-w$WƷn!R g0U}Vgg>t(+/'"<Ə@Qr| -x|i_ߙ~אo hNޝ<]:5 Ѥ,+; Kw[58^LZ̿RAzgu7> 66̗_etPU&F1oR+6aU ?.s=kgb g , K3O"LAG9{^~#^oۙr7* XhG`jxg0w1#\FF 6ISVhRUni{ujv.9HkRݡ'D= GoIrD/HEe5^Db?L-C{IeM%U[u'ԖQ5EE"lC\;{g3xWyɗx9'|8i vjj<_E wֆM*珏oԅ - 00)Qr&G$%l\j2A7PUMw˫B+uE}(;Mb=Z>[1ߊ9ӿNuQ>nDp(wǻ{"CLz=S đr*[IjD UoX1X]4']k?E/}.9Ҿ -s/MhG߾ s[=ojQH ?4+ pݵs紇X`ccŊ{'GlٰۙR:n 55õUowzZua<-/*`[wWnض!O>6Kg^Ő'6Ԏ\5G?; TS>;Dd}Վelތm%vo6W1ajvҚ+bƹ֤Eaڀ(Ƥ)dRة5w#qؾ(Gc7 qXzԦ+jRSXDPQ6Dz\)^a6_o\Rc#x{e=\xRS39bKa{GOӴic:ujCR=E>vzRPP`LZ06`NpM,(`7~tb.FjAE%(-Ny 66vPef8v֬u[8r8{aq'5gڇ_s/Aڃ Վ?A92HNI-Ac\XZc*$w3*N[T$;b+N`vMu::` 1Jv}Uee[5ޭ~s`y&suS7oмY*-\-騵pŷޞŨQdBH~q9p뢾OOE 7X;,Ib6,ǿiY=KdD܋O6vPZj_j`jj3+Hq?5Fܳc,Y*u|9P`:cZc|.Z^zm}2.[k=M7Nb(rJFTYv(?,daDMi.+@OVn 'Lv\T*NۨÏI*6oI $${Np XW_B:Sf8gx_*),uPSov݄9Qvڤ ޗ>}ycA={,А`f3/Ofpp62ɲek;1={v劉y7xٺmqc6]wc8YAk[{c4LjXtֿ 8k CDӚlKuRLRknCyyRJcGкu3/_:+Ł{*3ހ@#t]Һc-Co^ \zylټW_뮽CG3wD&OEU.d!aHf;G05ւnWǭ$''7qKo̓xJ>nnJޜ!Æg\wۓ}ssOӡ]ky3r ֩_ߎ5h!6751gA:/ WNMӦt p+VV#nD]ow E/x0OMqnJ]Q649"# rfۋ?޽UUeQAиq]iēwbz-^u8iXJJ"t]:]U[.%K0s(FidxOI2 UCL=SN⥗?C6xG :EWs⳷˘%mF’;A(*R8om=)(*=kaBU~Ӊ Y RJ>\gS!"uDlK՟$`VKI@+ Mtmda:Z&(((b/ٸi;~0YtF6-TW;OyB;viì*6[kahD%U  qlb+aa& "9t׮H8kz>p;[ XZyfEa/pcEH:S@8;H 9EQa1cC#f~u Ox^|[oi~0^~ n~>t*#F R7v16o|97좯SRǡSu-~Q8:F"#0 !*gO) ˓X4/q~½}9jiԌoMwS\\J5bL6-xXaS)**fŊ=jE0ZylV 4YW.8?JU(/]V~ƒxQS~uGoƧNeȻ;xwڶiIӏ[v&N y G)nN(5no2t0dPGtx\NBC4i܀p׷62lT(k sLǗg'11{4iˆqj{uxY ]sS%F@TNL畮#hl:e'Kǀj:yf`.\v O^a,qM@ -=n&r` Nge^BKJ n4X'.FЃ(+-ͷ>SǶՍ'1LG[n˔)?0cBƏEE%lذ UqߝtJ0 8x!3Tv\!`RbSRmp=7SJ,#|<<0"##1s>$7/Utl'IJL '(=36|Š)9)j^ϘcO igH V0{3DEwK X&TAtz4v&M8I: 1.T[i4Sl}&rw>|_EQ䓩DEcaٻ>8{v&//Von<جN>ȕٿ#*#?:򫼚PJcN>](O>7mc혦@լ~qK2zSv)xta`OM~T neLoxrTZiIh䣎`ǂva 5"40MP:Pl*/@}O}_#/i2πp7 &qmh`*0 %j4#pAĩ3ٹs7t7Y~') mv_k".!qc>jrキr:>bZn[opt7RnK6Pa(l*Պ tnKLt9DEE1b@WRߙKmq`E L?p{MD+Z#<0 B-N~Qf֬e\ף"ܼDb3˳Z}t ЉTETUv{,,-F.ǾQUG!d]Ey X B7BM{UK<J-瞝 T p ~Z(\ u ܡp( {Yz=}0mZЫ5:pͿ8)x͏(,,z*Ԯ]087vLrhn5Ljx)*.AcB vw:G=E RKo[ T *\_G2h̚?];nb݌Bq|-vAv-{Jxum6_&yBèeBT3Wu9%A15S /:f ~5 [lpvbvpX )Tо,HKOQj͂McUNUN9M 4UD!7q1N΢JBB4))iҸQ Iq0m rVP-<*qU TT '33=86&{ՓVTS`fs}|iۋ)ү DXg8r$cP`Qam:"^ab 1A Lgp(PUBB(--jIvV~Nέ5}BY[>|_ fOLn0sRyn,G$<"~G4]La)P ^ѯ\}nFgiU+NIᖁӵkjתFpH>k„)|[ΝQ: |"<,ͅ|>N1"Ei iN0KpU{>Ʒ-"@ii9kneُZ4jժ+)[ (D[N]zMKtMz|y+h0FU;6)Bn9D:Rb:$tR3BHIf f-++J5WܫyYl89j2~#`oc~ZVqlakۜ;p4.hcE x22] vB$I*WTUؕ%7hLk2aIZ S&X=%K2,[={0L˘8q?2Oxx5V:18N ],^ÇDEӼMB0` s}x~چ . w cLxnj*E!(̌>+{ /Po $\'V ]:GХKl6[%ZQdcdsUDYٸq'nIff.wR`[bRieRk{ WPPMU<7 sHF7UU3yrm㨒~ ^O)uW;rU@4ow<ξGйBB0 + 5r2h= r.HM&76P(vB8-`Ӡ,Ohh0QQᨪʉ ӷo_7iLTdV 0(--Tz:7oeYҲrukHZըȦ2_]Uzڠ`C1VtP\-i*%̙&u^\-pt-CCпFʮӬ|kTO]w?÷-$%ZaJ? gd5OT1?<`r8_HEr?B[3LQ'KD0u\{UtfRW]̿'ULj?k'~3P3gb+TUy{(l'!.Z4uTzw~C^W+M)*WUSdɶ K]4R v8N:mɮ]/FQڕ{+۵j=BAH[O\e6 ZjLNp9߃ krOmEĵpe+!MC]@EA;QbЪDM)U5* X"TÇN0v$f>ee6kg>RZ<`v Pbʺ4_3~<=l(_}؟Oq| V"sQ28Z7}+ת]7^E~Pfak$*V= td j #~1>5S%/bVKϿtp+0{Bf)<Н{5!A pHv<@۪ ᐫEMneo kL&l58Qa($W9Úiܿ@g[`}[Os1d<7ahT `a:(-4*qc8m=Ƒ`Z,S4.ԤX[^h8]^WBPCK4.9F嬓 5lW){MUu.]˨oy~ .M)m`y Lpm{63s8OfKn'<<#3S17O@~_(N z/G``pS9&{#? ' 7x=+Uq; D\aDN?"W|}5?GN\*[w70v\ӞQ M&&l6~YӦ̘15ϐi@n͟`(E !G-2bnwP{ BIDATÂөQ3E?N(0D8S>"hst9]}8UqVYHnyŋVI_b9d# wGI`(p=^sbth߂6W`gID`Le2}ZK k8V&5vg o2j0]5isXjӦNgժu8HrNˉ(gĻk3&biM`dE4 OWy+;%6^RʕA~:wjE5X 'Y\UL5g[&h\7OX KR{F 4zx0!涉A+j3!؛#+|`NiST^(lMffGHڣ@ L0+̀pQY5unUS0Mƍ7~2s,EcpKd z! PGUd|;/9Y QN(AO*4W R  "gLULzj\ toHPCAn?`OHH0ڵc+iԸ lV (((A|?Dbb4mHMiܸ4MJK(.)׋1l6k+UU8xo&MRV2e+Y`)iiYP6Z-!<,DτZs,>&cM".:Pt0l l(*c+pgn6[,NRycS 6i%G> srUH@S D4$1Taa6 W_aB6"oRYcLYJ 2#G`0LF0z4H)..!.6hTU%;;ݻrI\.'-Z4Ez9u*Çr1NQ0Jdl0wؤ}fk*cL^xT+1tRs. UvaĎpxvEK~@3k ߣ^tܞBmI^nUӴiCԭiU+׳k^<^/11$''QZ"qDFnch2xLS^8̊k)).gk*NJg9:tf8~\ンɡG^e8.+ C"t}BJJC-uK>, 8Khj ,%]+0E) \q2\!d}vT>j,Uz5WyF\\:͛5`Y#2?S/N`mB>7؉\_SR|LRW~%Xǀn񞳠)^ <j?Fz*7~'΂ە,ֹy^!O{~Ve'1V|{~jx]kT'5[$ZU 1 RSO`Z_.5jTG?Ȣ?r:JJJiڴ!-䢨ͧbJ=b&bZp8X5MRhР ~\O!Cqٻ }̺uV<:jU&0ݏ/]@~ *; AOcC/S+RFN %88+\KېT ҩxLK|/DaQ*esLٝ Y2er.`"H`. uk{_Ը*:! "S`X`}oI-d h. J M6SlC||΁AxRTT Vw zLٚ!L~,{M~:&hЯʊc&o3)Axh\W[I}`΢#rE};xǰZ-gVY*zϸ˧Ne39u*bOvc<9ZϪw键ui}-dgDF^g3BFol'q_Y*&N~CǏQ#ݫzz~Zܜ|֫E.WѬY#, bُظq+R+hFHOp !eeQXXDiiY%JLL4QQ(G5s>,r1tMU;o};)uyp佘䕗of#.,?b]vloA>LjrjEs1vJFF6nиFW/G>NU&M7wX49Nlq†Eo\E3él γώ4a/ѦMKGHNAfV_MO+P-*]ߍv}? oFQUڴnA׮WҰ>!!axRScnvK6M=@ƽe唕# 666mWkhܨ!yyy̘1iӦr9y{qmw/ض}O>0-[4O'+кMK~>4Na%~*n#iT 2Ky4R7`T>rfnUG2A0c0Š&?3ٙ)1E%T04:Q ' ~ZUU蒬3`!ͧ.+2JD*9RM`gb0t ~Q1 rYEY1c'tٺ 5Tv3S?a vj$-J ]?0 .7T48_ E˦d`c⤏[6oLdd8'}DaaCϭcftrF~*}{2oB}ir>|/ǸQlڴ QKJ.OϫaT{~:PXx 'Uwٚ.W;jh mO]&AȍbjF(z[e#V\}5. !('[LYgS!u&G6Xgo=w3YGf͞JJ 8*vӾO︙gEHp(y,^ƐhPn W aհ٬dddq :%PP ]i@viٲ9NS'<-[G}޿F=G:5y rz:{5>[n7#Oy6^|){q6n8q{T+mIV5 ]}GPPP]5yV wh:N+Dj< lڂ%G:eCv)EYӪpXjr݂E>7˿^z^=jJ *+3Sf3oH=vRO^=[?6A֒/ +1tYUSe^;vS$׀2?FC_=atҗ[o3},~fΜLÆ Y~=oµ^￉2jsٳ7|֭Z2kԭIPԣ9zΝ: عs/;vS8vQA]bcx>-;Xr.#{hfs v;¾ew`.TfΚǓOqmkػw/}܊jeYTR/ڴᯁvSV67vKL40kpS&[O5ģcQe[* zB_|Eh-vXRc&=AANғ'ui nYW0f$6m}6,KcMq$-sp] V]שBj2(Ƥck/xtS!' ҝ Y ~nLҭ['dyj՜zkc>&~d%=.dnذU+1ilSmmGU23y׸;Y-BډC66""Swj߿/-7Wkw>dyQ17ɬϾJJJ}:oCJ'>#:::36~ΝS~m5kʕk(..jukKSx=0a 5 3!\Nql( r!Lꦩ $@0 Rw2d]9©唤!UAUaGɳMKLac={vnaJPb&nc̸_#pˁH6.@=G0 Cr[.жX\\_{2xp?boDU 1n ~w EvC@^^j׮IpP099INN"7/]q}Ch6JJػ:nژDU4֯L5h808y_~OCKΡUxXG6+WwDvmi߮N~~!< ];7|^zǿ^~_ա-͚5_1 ,C6hބ/>JNN1W6{vds9]N'.˥@QެҍVTtԭE<)h߮9ǎbIBGVU:VWI."AZfB !@7%W*6/ +M`_Nu֟;[GO0oR 6d/90MMw3gګxm%i µiS#Y6>s#O|uؼy;]:wZDrrr b'!!Ұv;KPfu֬HIiaaΥHزES>K/7߈0;`tSoēveDGGr~8NE%K~VdRR4niԭE|| (,*fs-V 1LۃA Y͛7e(Eq6AQL2K@]p_L}M7u/ 3't9* +_P1S&^NBTtqELxx1TKIԮ]\N0E@:4MtZƌȚzyHٽЌ,&p‡tnu:9x̜իr1|u$V1C(Ya0)WE#0d%##'x)S> ##&MbZ())fѱc;>t2ǏQ\a۷oo9 Y:m۶b{6n;ӻw}UV}hޔ>7䡇YI({ЦuK4UѯͲe+;U%%b f̥z$zPYf--grʗЂKȢGl+#77ӧ3H˓4PTbT 6aV'e4hP;˯|SDSGM RPfǶс_ERRNk&]bQRaԍJ)[a)̚ҋ6ynH$%#@6#c qO:w)3 aoн{+L{-GO*۳E5GDl}·f [[ M$$JKOT_pTkJQfuuOBBaѠA])y"=}oQn-ϗ%(+3U9sSw #(' zHL">|ay>k3fLB{֊+Z5C eiB|kQ~qcח){ qk{ֈ6ٳR>=?mZnS~_®{ջ; WLJJ}ù[G>>GCTT$aTVzQz2!!!n<Ȅ^j27ӧ'YY'|<:~ DzeyǩV-wMdd4 1|>cƎ&.6/ɓ4?zMƍ7$7W&~ {[Pb~֭T`8#2" wU`*p%%дI=vmK=<8v|6v}\MM gHTs鏚EG_e%G}x)rtŸ]|q5n ~׷wW~NZl4d *: f^H~ O˖xǧckcxo~/}Lf湭Jx!F"΂)B9t ظq'aDLHHբ) (& '|*i> *h'L3 iYrO+6]$>z\Ѕ-G3t{H4 gi UC|1oN/PFmlʝw G4M:uM6#DEE`ڨj4n܀k_)tU>WwѴiCTG޽Yv#!';ۃvІƍSfdeдi#ڴiIYY9~;Y3?}Sc옏Y37z͛sAyv43ģgkq2ݏSYԃS`Ԥm54ܒ`0 3Uit) A$mQXy~NzgIdzs>w/:ߔCAVL.Jr3W7 .t܊))yh pg1jtS񄛱k Xpϔ UTsvu|CMq&)x{ջ'N'⭷_՝Ya3tժ!5>رx'Zv8ìY3gRO "287Nz\ +#l޲m[wޟvWfLoZ$  7DUU]O"##aw¬x7()){s}Caɒ'PBp=?{12QnyqpC\FR)(td*V'D'ܠclW$E8hWiKOfę|w8~;)a8dEI] I]\bDNyt$J}&2nM>Oy0:.b!~-~GK߲෿Ŀ~ijuզiF?G]&7ԋMJV-1t3no1)] 2V[Xnrrr8~<;vc1tܼʲP@~F g| rrr]knZjNNNsf/撗_@^pcIi‘#Gx\߳;>) R(//cW ڸ~E |b_Xv'ﴩPkH4 6BAi?~TRDd M=xh4nRg$T2Jt&o-s+m&!mƘV z3JG݁y !ݎlԤxy2Fܹ=~6S&KYy9nн{g_SM~?[n盯gqV\N' 5IիMRRUrPU+Bx})((4 bbvFy 8^ 3{!;v!2*>}e@>ԩS¢Bv&!G2a mⓏ'`R9MEAKLsҀ 7Qj\T7 ـ(lMCwhrn;%x."EcNm~\Y?EjIIs NuXIEʶų\b~7z5J߈IȤ}q0AaeO݃()"&&'Ѿ}[VZ×_~ŪHiX^zбԪU3Sdeei6V\HODAXX(qTOBB<1QI'77Gk>rsп֟0obvڇUt3-Z4nqp*Kgcn8^}oaUڵ@ {يsS=?$Pk5Ůo*" |f*:x'|Ŵ)ܥm~\HLwU7k Is:N/`ms],5aۢPx#q IɰbB&.7k*-k xfV:%z! !(Ucx'ګYmرYrZ ^=Ю}kRRYq{HOȑc9JZZ:yy`pEZɴjՂؘh>iij՜$~ݻլ_\"##h$6mz$n6}6Ÿ][O LOy@2`6jC .SG" }]gڴ9|8k9y]@}YS3Xt& !ض}?,GС}K,V ”j]}p[<'{ԣY~3of޼!HJJFiܨԢYt~F%=h1R!m:9Y?qfdiW#+k"];f8]N4NE! 330s|.XFnk^/gAFQ}D^X`Ү:)dHS׮swr`&7d̙嗞r0.W0+bh4Q@k3M">V3[>EW-(T<Wu^>u1NV7u3?,q@r6 Xۍpci׳oYKRL6Ntl UIZĀ^k?!==L23#+3'u]J`Zs4oބf|]cnj&!!B/_ٽk/{#T'nQ Fߵ9 & k3Ȳa*ZL馦S@QTKS&$++R&.So3 #D5ϷajjO=3f.Gs r3ԩ3&%^?l!T@o.9PBȟ4,Z݆zҴ5t?É +b058JZZ:LO^Ԫ]famEQ0 uo:!|5k!>C0{x|oyg-:ZrE=O&a, V&P.~B87ئҫ phJE|B`*3l/rwW ɦǙia߰CЫg'yx0[7AmBʡ^uoe.>v*w[J8q5> ԛfL ܅m(/B VxM)Tos rt8X-rЛiQ !0 /tЬY"HnQQ.x'_6q/G4H'P130OVV` )@7O\20ASuG:DҳJM=+ΕXh=Ȫ[.5 9}d>ν  <OGݑŬ\;nfjT>wY~e=B&o-"XqJpJH8mԆ%Ř'4<.\|֐[FFqR -w3DY)AOAcY(uɗՊy"'%>[[03OX` ;hHQEp9z40?XY5ppEhRZTic|?(zA2'o^ Bk$ Ҷf~a*.?l"DI (ih8e;7܎F {BNJm: Z$*հu#߹eOQV(/gsK~008Xh֨%ZF١+ ˎD;pp NNQ[E,^~#I3/f?uwޮxcw & Cs6_huad[4S'Ƒu_Y]bcPB0|W7kPQ>/BQlvmۀN7ĸiE@J$US).*eƬ?;/9 IW40l[Fnb&l CwҬY?룬,'8q0F-Q"з~p/b"~DAbE@fR,<d (P\Ah50vlBx܁8DƙEժo'ɣX4Y+!ah~i3򥠪r.ph8Y>(/A:Zaf5)D*E(4{tH/C!2Z ܇ԱuD&pc;[o[`rףYR- //L4@|Tnk$6i@UQ㪂^/ݎLKQ"QDaky9uW&HZa2ӎo߀VF *_&xܘKDV3MJXJpXm ΄f(+-g7͛/9 ~py>?N}ϛz}lڼri9\(U.1>PkUϞϔn(+FRM0 | +stwa85*ZUR$[{ٙRԄD֔(ȕq`b˙czpߍ#dH(8Xq `0@rY& _!(Op-ZX,'aiF֓D_W1P, jF&HwWUQ2!EGKO0d (!(è $ j(}ΐMDkRO;FSK]BMO䘒?7~H׃HFB.-˅A~ԨX)jreӴ350 DyjRMh\QcjF$/9ÏT Gr/'S1 -(-FQ '3#MvIiHVTj5QN̴cO&E`fU|Xj$(A!R$*jC{*%EfLFrINI;A6? 9_" U\hc߂9([>%ELpGiF!rj<}WEr-]aUC XEQq}sJX,!lUW'1ϦLUq7}F=[/%{3SN7 ?SKH2#o߇n9h,[ f@x='yx\D M&*d`F)J>lt8ۀ#嘙1s6DT6W@ε3S;\vH{O\lbLG_㙿sGV.H`?*u1M0Q"P"C>@S#EvMK_:r-Hyec7!'-%_%bPԄD&Zrm*1M8Yo;\UwPUQ"dwۍu,~qi?g7QwesP|,%dOӔ< )2fUT`wg1QT)vFSZXQB1 riΚK!Y8Kf.σ@j7> \?4$/fG1^aHZQR wƱCX7Atf+f<bKy alc_:>?ʁHqـ|xHCn}^|'Jq^O"\ B,)Pk&>T9R6ފ|Y,cpم[9那Sˢ.,.[+# Вe) ڢi\XZBˣZ|c;l-\6޿.= WyH}[#T ϴ1N<'klއs,%Kc9P,V9".ƚ}مuAu+`:!o=#&V~lnaw 5"cqȏYe\Ȥɥ P "Ure%&^(qU. 82r?>$iv/yT )+WN"KZO,z 4FRn!RqlH%Ķg|H1k˸?anDwq 72.28TCkƛ2.2bЀdk_#Gr{Iُ/n{!IENDB`logger/man/layout_glue.Rd0000644000176200001440000000371414705277361015145 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]{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]{log_levels()}}, the current timestamp and the actual log message -- that you can override via calling \code{\link[=layout_glue_generator]{layout_glue_generator()}} directly. For colorized output, see \code{\link[=layout_glue_colors]{layout_glue_colors()}}. } \seealso{ Other log_layouts: \code{\link{get_logger_meta_variables}()}, \code{\link{layout_blank}()}, \code{\link{layout_glue_colors}()}, \code{\link{layout_glue_generator}()}, \code{\link{layout_json}()}, \code{\link{layout_json_parser}()}, \code{\link{layout_logging}()}, \code{\link{layout_simple}()} } \concept{log_layouts} logger/man/log_shiny_input_changes.Rd0000644000176200001440000000225214654755544017521 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/formatter_pander.Rd0000644000176200001440000000313514705277361016145 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{ \dontshow{old <- logger:::namespaces_set()} 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)) \dontshow{logger:::namespaces_set(old)} } \seealso{ Other log_formatters: \code{\link{formatter_glue}()}, \code{\link{formatter_glue_or_sprintf}()}, \code{\link{formatter_glue_safe}()}, \code{\link{formatter_json}()}, \code{\link{formatter_logging}()}, \code{\link{formatter_paste}()}, \code{\link{formatter_sprintf}()} } \concept{log_formatters} logger/man/grapes-except-grapes.Rd0000644000176200001440000000163014654755544016644 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/layout_blank.Rd0000644000176200001440000000335614705277361015302 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]{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{ Other log_layouts: \code{\link{get_logger_meta_variables}()}, \code{\link{layout_glue}()}, \code{\link{layout_glue_colors}()}, \code{\link{layout_glue_generator}()}, \code{\link{layout_json}()}, \code{\link{layout_json_parser}()}, \code{\link{layout_logging}()}, \code{\link{layout_simple}()} } \concept{log_layouts} logger/man/log_levels.Rd0000644000176200001440000000244114654755544014752 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/levels.R \docType{data} \name{log_levels} \alias{log_levels} \alias{OFF} \alias{FATAL} \alias{ERROR} \alias{WARN} \alias{SUCCESS} \alias{INFO} \alias{DEBUG} \alias{TRACE} \title{Log levels} \usage{ OFF FATAL ERROR WARN SUCCESS INFO DEBUG TRACE } \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: \itemize{ \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/log_with_separator.Rd0000644000176200001440000000366614705270032016502 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]{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{ \dontshow{old <- logger:::namespaces_set()} 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) \dontshow{logger:::namespaces_set(old)} } \seealso{ \code{\link[=log_separator]{log_separator()}} } logger/man/log_errors.Rd0000644000176200001440000000106314654755544014773 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/fail_on_missing_package.Rd0000644000176200001440000000115114705270032017404 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, call = NULL) } \arguments{ \item{pkg}{string} \item{min_version}{optional minimum version needed} \item{call}{Call to include in error message.} } \description{ Check if R package can be loaded and fails loudly otherwise } \examples{ f <- function() fail_on_missing_package("foobar") try(f()) g <- function() fail_on_missing_package("stats") g() } logger/man/log_layout.Rd0000644000176200001440000000204414705270032014751 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]{layout_simple()}}, \code{\link[=layout_glue]{layout_glue()}} or \code{\link[=layout_glue_colors]{layout_glue_colors()}}, \code{\link[=layout_json]{layout_json()}}, or generator functions such as \code{\link[=layout_glue_generator]{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{ \dontshow{old <- logger:::namespaces_set()} log_layout(layout_json()) log_info(42) \dontshow{logger:::namespaces_set(old)} } \seealso{ Other log configutation functions: \code{\link{log_appender}()}, \code{\link{log_formatter}()}, \code{\link{log_threshold}()} } \concept{log configutation functions} logger/man/skip_formatter.Rd0000644000176200001440000000152114654755544015646 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]{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/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/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/layout_simple.Rd0000644000176200001440000000333414705277361015500 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]{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{ Other log_layouts: \code{\link{get_logger_meta_variables}()}, \code{\link{layout_blank}()}, \code{\link{layout_glue}()}, \code{\link{layout_glue_colors}()}, \code{\link{layout_glue_generator}()}, \code{\link{layout_json}()}, \code{\link{layout_json_parser}()}, \code{\link{layout_logging}()} } \concept{log_layouts} logger/man/logger.Rd0000644000176200001440000000602414654755544014077 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]{log_levels()}}} \item{formatter}{function pre-processing the message of the log record when it's not wrapped in a \code{\link[=skip_formatter]{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]{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 \itemize{ \item \code{\link[=INFO]{INFO()}} (or as per the \code{LOGGER_LOG_LEVEL} environment variable override) as the log level threshold \item \code{\link[=layout_simple]{layout_simple()}} as the layout function showing the log level, timestamp and log message \item \code{\link[=formatter_glue]{formatter_glue()}} (or \code{\link[=formatter_sprintf]{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]{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]{log_threshold()}}, \code{\link[=log_formatter]{log_formatter()}}, \code{\link[=log_layout]{log_layout()}} and \code{\link[=log_appender]{log_appender()}} and then call \code{\link[=log_levels]{log_levels()}} and its derivatives, such as \code{\link[=log_info]{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_tictoc.Rd0000644000176200001440000000114514705270032014722 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]{log_levels()}}} \item{namespace}{x} } \description{ Tic-toc logging } \examples{ 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/layout_glue_colors.Rd0000644000176200001440000000471714705277361016532 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]{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{ Colour log levels based on their severity. Log levels are coloured with \code{\link[=colorize_by_log_level]{colorize_by_log_level()}} and the messages are coloured with \code{\link[=grayscale_by_log_level]{grayscale_by_log_level()}}. } \note{ This functionality depends on the \pkg{crayon} package. } \examples{ \dontshow{if (requireNamespace("crayon")) (if (getRversion() >= "3.4") withAutoprint else force)(\{ # examplesIf} 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.") \dontshow{\}) # examplesIf} } \seealso{ Other log_layouts: \code{\link{get_logger_meta_variables}()}, \code{\link{layout_blank}()}, \code{\link{layout_glue}()}, \code{\link{layout_glue_generator}()}, \code{\link{layout_json}()}, \code{\link{layout_json_parser}()}, \code{\link{layout_logging}()}, \code{\link{layout_simple}()} } \concept{log_layouts} logger/man/get_logger_meta_variables.Rd0000644000176200001440000000643514705277361017773 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]{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]{layout_glue_generator()}}: } \details{ \itemize{ \item \code{levelr}: log level as an R object, eg \code{\link[=INFO]{INFO()}} \item \code{level}: log level as a string, eg \code{\link[=INFO]{INFO()}} \item \code{time}: current time as \code{POSIXct} \item \code{node}: name by which the machine is known on the network as reported by \code{Sys.info} \item \code{arch}: machine type, typically the CPU architecture \item \code{os_name}: Operating System's name \item \code{os_release}: Operating System's release \item \code{os_version}: Operating System's version \item \code{user}: name of the real user id as reported by \code{Sys.info} \item \code{pid}: the process identification number of the R session \item \code{node}: name by which the machine is known on the network as reported by \code{Sys.info} \item \code{r_version}: R's major and minor version as a string \item \code{ns}: namespace usually defaults to \code{global} or the name of the holding R package of the calling the logging function \item \code{ns_pkg_version}: the version of \code{ns} when it's a package \item \code{ans}: same as \code{ns} if there's a defined \code{\link[=logger]{logger()}} for the namespace, otherwise a fallback namespace (eg usually \code{global}) \item \code{topenv}: the name of the top environment from which the parent call was called (eg R package name or \code{GlobalEnv}) \item \code{call}: parent call (if any) calling the logging function \item \code{fn}: function's (if any) name calling the logging function } } \seealso{ \code{\link[=layout_glue_generator]{layout_glue_generator()}} Other log_layouts: \code{\link{layout_blank}()}, \code{\link{layout_glue}()}, \code{\link{layout_glue_colors}()}, \code{\link{layout_glue_generator}()}, \code{\link{layout_json}()}, \code{\link{layout_json_parser}()}, \code{\link{layout_logging}()}, \code{\link{layout_simple}()} } \concept{log_layouts} logger/man/log_indices.Rd0000644000176200001440000000062614670303260015057 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/logger.R \name{log_indices} \alias{log_indices} \title{Returns number of currently active indices} \usage{ log_indices(namespace = "global") } \arguments{ \item{namespace}{override the default / auto-picked namespace with a custom string} } \value{ number of indices } \description{ Returns number of currently active indices } logger/man/formatter_paste.Rd0000644000176200001440000000243414705277361016011 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{ Other log_formatters: \code{\link{formatter_glue}()}, \code{\link{formatter_glue_or_sprintf}()}, \code{\link{formatter_glue_safe}()}, \code{\link{formatter_json}()}, \code{\link{formatter_logging}()}, \code{\link{formatter_pander}()}, \code{\link{formatter_sprintf}()} } \concept{log_formatters} logger/man/formatter_json.Rd0000644000176200001440000000311114705277361015637 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{ \dontshow{old <- logger:::namespaces_set()} log_formatter(formatter_json) log_layout(layout_json_parser()) log_info(everything = 42) log_info(mtcars = mtcars, species = iris$Species) \dontshow{logger:::namespaces_set(old)} } \seealso{ Other log_formatters: \code{\link{formatter_glue}()}, \code{\link{formatter_glue_or_sprintf}()}, \code{\link{formatter_glue_safe}()}, \code{\link{formatter_logging}()}, \code{\link{formatter_pander}()}, \code{\link{formatter_paste}()}, \code{\link{formatter_sprintf}()} } \concept{log_formatters} logger/DESCRIPTION0000644000176200001440000000302014705655607013251 0ustar liggesusersType: Package Package: logger Title: A Lightweight, Modern and Flexible Logging Utility Version: 0.4.0 Date: 2024-10-19 Authors@R: c( person("Gergely", "Daróczi", , "daroczig@rapporter.net", role = c("aut", "cre"), comment = c(ORCID = "0000-0003-3149-8537")), person("Hadley", "Wickham", , "hadley@posit.co", role = "aut", comment = c(ORCID = "0000-0003-4757-117X")), person("System1", role = "fnd") ) 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. License: MIT + file LICENSE URL: https://daroczig.github.io/logger/ BugReports: https://github.com/daroczig/logger/issues Depends: R (>= 4.0.0) Imports: utils Suggests: botor, covr, crayon, devtools, glue, jsonlite, knitr, mirai (>= 1.3.0), pander, parallel, R.utils, rmarkdown, roxygen2, RPushbullet, rsyslog, shiny, slackr (>= 1.4.1), syslognet, telegram, testthat (>= 3.0.0), withr Enhances: futile.logger, log4r, logging VignetteBuilder: knitr Config/testthat/edition: 3 Config/testthat/parallel: TRUE Encoding: UTF-8 RoxygenNote: 7.3.2 NeedsCompilation: no Packaged: 2024-10-20 22:07:23 UTC; daroczig Author: Gergely Daróczi [aut, cre] (), Hadley Wickham [aut] (), System1 [fnd] Maintainer: Gergely Daróczi Repository: CRAN Date/Publication: 2024-10-22 08:00:07 UTC