lgr/0000755000176200001440000000000015137120762011042 5ustar liggesuserslgr/tests/0000755000176200001440000000000014131760166012205 5ustar liggesuserslgr/tests/testthat/0000755000176200001440000000000015137120762014044 5ustar liggesuserslgr/tests/testthat/test_logger_index.R0000644000176200001440000000042614131760166017677 0ustar liggesuserscontext("logger_index") test_that("logger_index works as expected", { remove_all_loggers() on.exit(remove_all_loggers()) get_logger("tree/leaf") get_logger("shrub/leaf") get_logger("plant/shrub/leaf") expect_identical(nrow(logger_index()), 8L) }) lgr/tests/testthat/test_logger_config.R0000644000176200001440000001043515035130575020035 0ustar liggesuserscontext("logger_config") test_that("logger_config() works as expected", { cfg <- logger_config( appenders = list("Appender" = list()), propagate = FALSE, exception_handler = default_exception_handler, threshold = NA, filters = list("FilterForceLevel" = list(level = "info")) ) expect_s3_class(cfg, "logger_config") tl <- get_logger("test")$config(cfg) expect_true(inherits(tl$appenders[[1]], "Appender")) expect_false(is_virgin_Logger(tl)) expect_silent(tl$fatal("test")) # propagate expect_equal(tl$last_event$level, 400) # force level filter tl <- get_logger("test")$config(logger_config()) expect_true(is_virgin_Logger(tl)) }) test_that("as_logger_config() works as expected with YAML and JSON files", { skip_if_not_installed("yaml") # files work... cy <- as_logger_config(rprojroot::find_testthat_root_file("testdata", "lg_full.yaml")) cj <- as_logger_config(rprojroot::find_testthat_root_file("testdata", "lg_full.json")) expect_identical(cj, cy) expect_s3_class(cj, "logger_config") expect_identical(cy$appenders[[1]]$layout$LayoutFormat$fmt, "%L %t - %m") # so do text vectors (and scalars with newlines) cy2 <- as_logger_config( readLines(rprojroot::find_testthat_root_file("testdata", "lg_full.yaml")) ) cj2 <- as_logger_config( paste(readLines(rprojroot::find_testthat_root_file("testdata", "lg_full.json")), collapse = "\n") ) expect_identical(cj2, cj) expect_identical(cy2, cy) }) test_that("as_logger_config() works for simplified yaml logger config", { skip_if_not_installed("yaml") cy <- as_logger_config(rprojroot::find_testthat_root_file("testdata", "lg_simple.yaml")) cj <- as_logger_config(rprojroot::find_testthat_root_file("testdata", "lg_simple.json")) expect_identical(cj, cy) expect_s3_class(cj, "logger_config") expect_identical(cy$appenders[[1]]$layout$LayoutFormat$fmt, "%L %t - %m") expect_s3_class(cy, "logger_config") }) test_that("logger$config() fails if yaml file is passed to `text` instead of `file`", { ty <- rprojroot::find_testthat_root_file("testdata", "lg_full.yaml") lg <- get_logger("test") expect_error(lg$config(text = ty), "YAML") lg$config(NULL) }) test_that("resolve_r6_ctors() works as expected", { tf <- tempfile() on.exit(unlink(tf)) x <- logger_config( appenders = list("AppenderFile" = list(file = tf)) ) res <- resolve_r6_ctors(x) expect_s3_class(as_logger_config(res), "logger_config") expect_identical(res$appenders[[1]]$file, tf) tf2 <- tempfile() tf3 <- tempfile() on.exit(unlink(c(tf2, tf3)), add = TRUE) x <- list( "Logger" = list( name = "test2", appenders = list( "AppenderBuffer" = list( threshold = NA, appenders = list( "AppenderJson" = list(threshold = 100, file = tf2), "AppenderFile" = list(file = tf3), "Appender" = list() ) ) ) ) ) res <- resolve_r6_ctors(x)[[1]] expect_true(is_Logger(res)) expect_identical(res$appenders[[1]]$appenders[[1]]$file, tf2) expect_s3_class(res$appenders[[1]]$appenders[[2]], "AppenderFile") expect_identical(res$appenders[[1]]$appenders[[2]]$file, tf3) expect_s3_class(res$appenders[[1]]$appenders[[3]], "Appender") res$config(NULL) }) test_that("parse_logger_config() works", { skip_if_not_installed("yaml") # parse_logger_config turns logger_configs into lists of R6 objects # that cann direclty be applied to a logger full <- as_logger_config(rprojroot::find_testthat_root_file("testdata", "lg_full.yaml")) simple <- as_logger_config(rprojroot::find_testthat_root_file("testdata", "lg_simple.json")) pf <- parse_logger_config(full) ps <- parse_logger_config(simple) lg <- get_logger("test")$config(full) on.exit(lg$config(NULL)) expect_length(lg$appenders, 2) expect_identical(lg$appenders$AppenderConsole$threshold, 200L) expect_identical(lg$appenders$AppenderBuffer$appenders[[1]]$threshold, 123L) expect_identical(lg$threshold, 400L) lg <- get_logger("test")$config(simple) expect_identical(lg$appenders$AppenderConsole$threshold, 200L) expect_identical(lg$propagate, FALSE) expect_identical(lg$threshold, 400L) }) lgr/tests/testthat/test_Appender.R0000644000176200001440000003336215035401127016765 0ustar liggesuserscontext("Appender") event <- LogEvent$new( logger = Logger$new("dummy"), level = 200L, timestamp = structure(1541175573.9308, class = c("POSIXct", "POSIXt")), caller = NA_character_, msg = "foo bar", rawMsg = "foo raw" ) # Appender -------------------------------------------------------------------- test_that("Appender: $append() works", { app <- Appender$new() expect_match(app$append(event), "foo bar") }) test_that("Appender: $set_threshold() works", { app <- Appender$new() app$set_threshold(200) expect_identical(app$threshold, 200L) app$set_threshold("info") expect_identical(app$threshold, 400L) app$set_threshold(NA) expect_identical(app$threshold, NA_integer_) expect_error(app$set_threshold("blubb"), "log levels") }) # AppenderFile --------------------------------------------------------- test_that("AppenderFile: logging with LayoutFormat", { tf <- tempfile() on.exit(unlink(tf)) app <- AppenderFile$new(file = tf) app$append(event) app$append(event) res <- readLines(tf) expect_true(grepl("foo", res[[1]])) expect_true(grepl("bar", res[[2]])) }) test_that("AppenderFile: logging with LayoutJson", { tf <- tempfile() on.exit(unlink(tf)) app <- AppenderFile$new(file = tf, layout = LayoutJson$new()) app$append(event) app$append(event) tres <- read_json_lines(tf) eres <- data.table::rbindlist(list(event$values, event$values)) expect_identical(tres[["level"]], eres[["level"]]) expect_identical(tres[["msg"]], eres[["msg"]]) expect_true(all(is.na(tres[["caller"]])) && all(is.na(eres[["caller"]]))) expect_equal(as.POSIXct(tres[["timestamp"]]), eres[["timestamp"]], tolerance = 1) }) test_that("AppenderFile: creates empty log file on init", { tf <- tempfile() on.exit(unlink(tf)) expect_error(AppenderFile$new( file = file.path(tempdir(), "non", "existing", "directory" ) )) AppenderFile$new(file = file.path(tf)) expect_true(file.exists(tf)) }) test_that("AppenderFile: $show() works", { tf <- tempfile() on.exit(unlink(tf)) tf2 <- tempfile() on.exit(unlink(tf2), add = TRUE) lg <- get_logger("test")$ set_propagate(FALSE)$ set_threshold(NA)$ set_appenders(list( char = AppenderFile$new(file = tf), num = AppenderFile$new(file = tf2, layout = LayoutFormat$new("%n %m")) )) on.exit(get_logger("test", reset = TRUE), add = TRUE) lg$info("foo bar") lg$info("blah blubb") lg$warn("warnwarn") lg$debug("bugbug") expect_output({ expect_length(lg$appenders$char$show("warn"), 1) expect_length(lg$appenders$char$show("info"), 3) expect_length(lg$appenders$char$show("debug"), 4) expect_length(lg$appenders$num$show("warn"), 1) expect_length(lg$appenders$num$show("info"), 3) expect_length(lg$appenders$num$show("debug"), 4) }) }) test_that("AppenderFile$data throws an error", { tf <- tempfile() on.exit(unlink(tf)) lg <- get_logger("test")$ set_propagate(FALSE)$ set_threshold(NA)$ set_appenders(list(file = AppenderFile$new(file = tf))) on.exit(get_logger("test", reset = TRUE), add = TRUE) lg$info("foo bar") lg$info("blah blubb") expect_error(lg$appenders$file$data, class = "CannotParseLogError") }) test_that("AppenderFile: creates empty log file on init", { tf <- tempfile() on.exit(unlink(tf)) app <- AppenderFile$new(file = tf) lg <- get_logger("lgr/testAppenderFileCreatesEmptyLogFile")$ set_appenders(app)$ set_propagate(FALSE) lg$fatal("foo", foo = "bar") res <- readLines(tf) expect_match(res, '"bar"}') }) # AppenderJson ------------------------------------------------------------ test_that("AppenderJson: AppenderFile with LayoutJson$show() and $data() work", { tf <- tempfile() on.exit(unlink(tf)) # with default format app <- AppenderFile$new(file = tf, layout = LayoutJson$new()) for (i in 1:10) app$append(event) # show shows the correct number of lines r <- utils::capture.output(app$show(n = 3)) expect_true(grepl( "(level.*){3}", paste(r, collapse = "\n"))) expect_false(grepl("(level.*){4}", paste(r, collapse = "\n"))) r <- utils::capture.output(app$show(threshold = 100)) expect_identical(r, "") expect_identical(nrow(app$data), 10L) }) # AppenderConsole --------------------------------------------------------- test_that("AppenderConsole: $append() works", { app <- AppenderConsole$new() expect_match( capture.output(app$append(event)), "ERROR.*:19:33.*foo.*bar" ) }) test_that("AppenderConsole: $filter() works", { app1 <- AppenderConsole$new() expect_true(app1$filter(event)) app1$set_filters(list(function(event) FALSE)) expect_false(app1$filter(event)) }) test_that("AppenderConsole - with connection = stderr() - outputs to stderr", { app <- AppenderConsole$new(connection = stderr()) std_out <- textConnection("msg_out", open = "w", local = TRUE) sink(std_out, append = TRUE, type = "message") on.exit(sink(NULL, type = "message"), add = TRUE) expect_silent( app$append(event) ) expect_match( msg_out, "ERROR.*:19:33.*foo.*bar" ) }) test_that("AppenderConsole: chooses stderr by default when in knitr", { opts <- options(knitr.in.progress = TRUE) on.exit(options(opts), add = TRUE) app <- AppenderConsole$new() std_out <- textConnection("msg_out", open = "w", local = TRUE) sink(std_out, append = TRUE, type = "message") on.exit(sink(NULL, type = "message"), add = TRUE) expect_silent( app$append(event) ) expect_match( msg_out, "ERROR.*:19:33.*foo.*bar" ) }) test_that("AppenderConsole: rawMessage visible when set in layout", { lo <- LayoutFormat$new("%m -- %r") app <- AppenderConsole$new(layout = lo) expect_match( capture.output(app$append(event)), "foo bar -- foo raw") }) # AppenderBuffer ---------------------------------------------------- # Tests must be executed in sequence # setup buffer_log <- tempfile() teardown(unlink(buffer_log)) l <- Logger$new( "dummy", appenders = list( buffer = AppenderBuffer$new( appenders = list(file = AppenderFile$new(file = buffer_log)), buffer_size = 10, flush_threshold = "fatal" ) ), propagate = FALSE ) test_that("AppenderBuffer: FATAL log level triggers flush", { l$info(LETTERS[1:3]) expect_length(l$appenders$buffer$buffer_events, 1) l$info(LETTERS[4:7]) expect_identical(length(l$appenders$buffer$buffer_events), 2L) # FATAL triggers flush with default filters l$fatal(letters[1:3]) expect_identical(l$appenders$buffer$buffer_events, event_list()) expect_match( paste(readLines(buffer_log), collapse = "#"), "INFO.*A#INFO.*B#INFO.*C#INFO.*D#INFO.*E#INFO.*F#INFO.*G#FATAL.*a#FATAL.*b#FATAL.*c" ) # Does the next flush flush the correct event? l$fatal("x") expect_identical(length(readLines(buffer_log)), 11L) expect_identical(l$appenders$buffer$buffer_events, event_list()) expect_match(paste(readLines(buffer_log), collapse = "#"), ".*A#.*B#.*C#.*a#.*b#.*c#.*x") }) test_that("AppenderBuffer: buffer cycling triggers flush", { replicate(10, l$info("z")) expect_identical(length(l$appenders$buffer$buffer_events), 10L) l$info(c("y", "y", "y")) expect_identical(length(readLines(buffer_log)), 24L) expect_identical(l$appenders$buffer$buffer_events, event_list()) expect_match( paste(readLines(buffer_log), collapse = "#"), ".*A#.*B#.*C#.*a#.*b#.*c#.*x(.*z.*){10}(.*y.*){3}" ) }) test_that("AppenderBuffer: manual flush trigger works", { l$info(c("y", "y", "y")) l$appenders$buffer$flush() expect_identical(length(readLines(buffer_log)), 27L) eres <- readLines(buffer_log) expect_match(paste(eres, collapse = "#"), "(.*z.*){10}(.*y.*){6}") }) test_that("AppenderBuffer: flush on buffer cycling can be suppressed", { eres <- readLines(buffer_log) l$appenders$buffer$set_flush_on_rotate(FALSE) for (i in 1:15) l$info(i) # theres a 10% tolerance for flushing if no flush on rotate is set expect_true(length(l$appenders$buffer$buffer_events) >= 10L) # Nothing should have been flushed to the log file expect_identical(readLines(buffer_log), eres) }) test_that("AppenderBuffer: object destruction triggers flush", { # this test also cleans up behind the logger created at the beginning # of this section l$appenders$buffer$flush() # ensure empty appender l$info(c("destruction", "destruction")) expect_identical(length(l$appenders$buffer$buffer_events), 1L) rm(l, inherits = TRUE) gc() expect_match( paste(readLines(buffer_log), collapse = "#"), "(.*destruction){2}" ) try(file.remove(buffer_log), silent = TRUE) }) # the following AppenderBuffer tests are self contained again test_that("AppenderBuffer: flush on object destruction can be suppressed", { # must re-initilaize logger buffer_log <- tempfile() l <- Logger$new( "dummy", appenders = list( buffer = AppenderBuffer$new( appenders = list(file = AppenderFile$new(file = buffer_log)), buffer_size = 10 ) ), propagate = FALSE ) l$info(LETTERS[1:3]) l$appenders$buffer$set_flush_on_exit(FALSE) rm(l) gc() expect_true(file.exists(buffer_log)) expect_equal(file.size(buffer_log), 0) expect_true(file.remove(buffer_log)) }) test_that("AppenderBuffer: $add_appender()/$remove_appender()", { sapp <- AppenderBuffer$new() app1 <- AppenderConsole$new(threshold = 100) app2 <- AppenderConsole$new(threshold = 300) # add expect_silent({ sapp$add_appender(app1) sapp$add_appender(app2, "blah") sapp$add_appender(AppenderBuffer$new(), "blubb") }) expect_identical(sapp$appenders[[1]], app1) expect_identical(sapp$appenders$blah, app2) # remove sapp$remove_appender(1) expect_length(sapp$appenders, 2L) sapp$remove_appender(c("blah", "blubb")) # set appenders sapp$set_appenders(list(app1, app2)) expect_length(sapp$appenders, 2) sapp$set_appenders(list(app1, app2)) expect_length(sapp$appenders, 2) expect_identical(sapp$appenders[[1]], app1) expect_identical(sapp$appenders[[2]], app2) }) test_that("AppenderBuffer: $show()", { l <- Logger$new( "buffer test", appenders = AppenderBuffer$new(should_flush = NULL), propagate = FALSE ) l$fatal("foo") l$warn("foo", bar = "foo") l$info(1:3) l$error("and a list column", df = head(iris), env = environment()) expect_identical(nrow(l$appenders[[1]]$buffer_df), 6L) expect_identical(nrow(l$appenders[[1]]$buffer_dt), 6L) expect_length(capture.output(l$appenders[[1]]$show(n = 5, threshold = "warn")), 3L) }) test_that("AppenderBuffer: cycling works", { l <- Logger$new( "buffer test", appenders = AppenderBuffer$new(buffer_size = 3L, flush_on_rotate = FALSE), propagate = FALSE ) l$info("test1") l$info("test2") l$info("test3") l$info("test4") l$info("test5") expect_identical( sapply(l$appenders[[1]]$buffer_events, `[[`, "msg"), paste0("test", 2:5) ) }) test_that("AppenderBuffer: Custom $should_flush works", { l <- Logger$new( "buffer test", appenders = AppenderBuffer$new(), propagate = FALSE ) # FALSE l$appenders[[1]]$set_should_flush(function(event) FALSE) l$fatal("test") expect_length(l$appenders[[1]]$buffer_events, 1L) # TRUE l$appenders[[1]]$set_should_flush( function(event) { inherits(event, "LogEvent") && inherits(.obj(), "AppenderBuffer") } ) l$fatal("test") expect_length(l$appenders[[1]]$buffer_events, 0L) # Undefined l$appenders[[1]]$set_should_flush(function(event) NA) expect_warning(l$fatal("test")) expect_length(l$appenders[[1]]$buffer_events, 1L) l$appenders[[1]]$set_should_flush(function(event) iris) expect_warning(l$fatal("test")) expect_length(l$appenders[[1]]$buffer_events, 2L) # illegal filter expect_error(l$appenders[[1]]$set_should_flush(mean)) }) # self contained buffer tests test_that("AppenderBuffer: buffer_size 0 works as expected", { l <- Logger$new( "0 buffer test", appenders = list(buffer = AppenderBuffer$new()$ set_appenders(list(file = AppenderFile$new(file = tempfile())))), propagate = FALSE ) on.exit(unlink(l$appenders$buffer$appenders$file$file)) l$appenders$buffer$set_buffer_size(0) expect_silent(l$info(LETTERS[1:3])) expect_length(readLines(l$appenders$buffer$appenders$file$file), 3L) expect_silent({ expect_identical(nrow(l$appenders$buffer$buffer_df), 0L) expect_identical(nrow(l$appenders$buffer$buffer_dt), 0L) expect_length(l$appenders$buffer$buffer_events, 0L) }) }) # utils ------------------------------------------------------------------- test_that("default_file_reader() works", { tf <- tempfile() on.exit(unlink(tf)) expect_error(expect_warning(default_file_reader(tf, threshold = NA, n = 0))) writeLines(LETTERS, tf) expect_identical(default_file_reader(tf, threshold = NA, n = 3), c("X", "Y", "Z")) writeLines(LETTERS, tf) expect_warning(default_file_reader(tf, threshold = 4, n = 3)) }) test_that("standardize_should_flush_output() works", { expect_identical(standardize_should_flush_output(TRUE), TRUE) expect_identical(standardize_should_flush_output(FALSE), FALSE) expect_warning( expect_identical(standardize_should_flush_output(NA), FALSE), class = "ValueIsNotBoolError" ) expect_warning( expect_identical(standardize_should_flush_output(iris), FALSE), class = "ValueIsNotBoolError" ) }) lgr/tests/testthat/test_string_repr.R0000644000176200001440000000127614131760166017573 0ustar liggesuserscontext("string_repr") test_that("string_repr works as expected", { string_repr(mean) string_repr(function(x) print(x)) string_repr(iris) string_repr(iris, 16) string_repr(as.matrix(iris), 32) string_repr(data.table::as.data.table(iris), 36) string_repr(data.table::as.data.table(iris), 22) expect_equal(nchar(string_repr(data.table::as.data.table(iris), 22)), 22) expect_equal(nchar(string_repr(data.table::as.data.table(iris), 8)), 8) expect_equal(nchar(string_repr(as.matrix(iris), 8)), 8) expect_lte(nchar(string_repr(letters, 16)), 16) string_repr(letters, 16) string_repr(1:100, 16) expect_lte(nchar(string_repr(iris, 16)), 16) }) lgr/tests/testthat/test_Layout.R0000644000176200001440000000435515036460612016511 0ustar liggesuserscontext("Layout") tevent <- LogEvent$new( logger = Logger$new("dummy"), level = 200L, timestamp = as.POSIXct(1541175573.9308, origin = "1970-01-01", tz = "UTC"), caller = NA_character_, msg = "foo bar", rawMsg = "foo raw", customField = "hashbaz", customField2 = "barfoo" ) test_that("Layouts works as expected", { lo <- Layout$new() expect_match(lo$format_event(tevent), "19:33") expect_true(is_scalar_character(lo$format_event(tevent))) }) test_that("LayoutFormat works as expected", { lo <- LayoutFormat$new() expect_match(lo$format_event(tevent), "ERROR .*2018-11-02 .*:19:33.*\\.* foo bar") lo$set_timestamp_fmt("%Y-%m-%d") lo$set_fmt("[%t]") expect_identical(lo$format_event(tevent), "[2018-11-02]") }) test_that("LayoutFormat works as expected with custom fields", { lo <- LayoutFormat$new("%r %j") expect_identical(lo$format_event(tevent), "foo raw {\"customField\":\"hashbaz\",\"customField2\":\"barfoo\"}") lo$set_timestamp_fmt("%Y-%m-%d") lo$set_fmt("[%t]") expect_identical(lo$format_event(tevent), "[2018-11-02]") }) test_that("LayoutFormat works as expected with custom fields", { lo <- LayoutFormat$new("%r %j", excluded_fields = "customField2") expect_identical(lo$format_event(tevent), "foo raw {\"customField\":\"hashbaz\"}") lo$set_timestamp_fmt("%Y-%m-%d") lo$set_fmt("[%t]") expect_identical(lo$format_event(tevent), "[2018-11-02]") }) test_that("LayoutGlue works as expected", { # basic formatting works lo <- LayoutGlue$new(fmt = "{level} [{timestamp}] {msg}") expect_match(lo$format_event(tevent), "^200.*foo bar$") # active bindings work lo <- LayoutGlue$new(fmt = "{level_name} [{timestamp}] {msg}") expect_match(lo$format_event(tevent), "^error.*foo bar$") # functions work lo <- LayoutGlue$new(fmt = "{toupper(level_name)} [{timestamp}] {msg}") expect_match(lo$format_event(tevent), "^ERROR.*foo bar$") # default format works lo <- LayoutGlue$new(fmt = "{pad_right(colorize_levels(toupper(level_name)), 5)} [{timestamp}] {msg}") expect_match(lo$format_event(tevent), "ERROR.*foo bar$") if (crayon::has_color()){ expect_true(crayon::has_style(lo$format_event(tevent))) } }) lgr/tests/testthat/test_utils-rd.R0000644000176200001440000000042214131760166016770 0ustar liggesuserscontext("utils-rd") test_that("utils-rd works as expected", { res <- r6_usage(AppenderFileRotating) res <- r6_usage(list( AppenderFileRotating, AppenderFileRotatingDate, AppenderFileRotatingTime )) expect_true(is.character(res)) }) lgr/tests/testthat/test_print_Logger.R0000644000176200001440000000153614131760166017667 0ustar liggesuserscontext("format") test_that("print.Logger() works as expected", { tf1 <- tempfile() tf2 <- tempfile() tf_long <- tempfile(pattern = paste(letters, LETTERS, sep = "-", collapse = "-")) on.exit(file.remove(tf1, tf2, tf_long)) l <- Logger$new( "test_logger", appenders = c( AppenderFile$new(file = tf1), AppenderConsole$new(), AppenderFile$new(threshold = 100, file = tf_long), AppenderBuffer$new( appenders = list(AppenderBuffer$new(), AppenderFile$new(file = tf2)) ) ) ) # ensure that print doesn't raise exceptions expect_output(print(l)) expect_output(print(Logger$new("blubb", propagate = FALSE))) expect_output(print(Logger$new("blubb", propagate = FALSE))) expect_output(print(Logger$new("blubb", propagate = FALSE, appenders = Appender$new()))) }) lgr/tests/testthat/test_read_json_log.R0000644000176200001440000000113115035405167020031 0ustar liggesuserscontext("read_json_lines") test_that("read_json_lines() works as expected", { tf <- tempfile() lo <- LayoutJson$new() lgr <- Logger$new( "test", appenders = AppenderFile$new(layout = lo, file = tf), threshold = NA, propagate = FALSE ) lgr$fatal("test") lgr$error("test") lgr$warn("test") lgr$info("test") lgr$debug("test") lgr$trace("test") tres <- read_json_lines(tf) expect_identical(names(tres), c("level", "timestamp", "logger", "caller", "msg")) expect_true(all(tres$level == seq(100, 600, by = 100))) file.remove(tf) }) lgr/tests/testthat/test_Filterable.R0000644000176200001440000000271014131760166017300 0ustar liggesuserscontext("Filterable") test_that("is_filter() works", { fil <- function(event) { blubb } expect_true(is_filter(fil)) expect_false(is_filter(mean)) expect_false(is_filter("blubb")) assert_filter(fil) expect_error( assert_filter(mean), regexp = "mean", class = "ObjectIsNoFilterError" ) }) test_that("Filterable: $add_filter(), $remove_filter(), $filters", { app <- Appender$new() fil <- function(event) { FALSE } expect_error(app$add_filter(mean)) app$add_filter(fil) expect_identical(app$filters[[1]], fil) expect_false(app$filter()) app$remove_filter(1) expect_length(app$filters, 0) expect_true(app$filter()) app$add_filter(fil, "foo") expect_identical(app$filters[[1]], fil) app$remove_filter("foo") expect_length(app$filters, 0) fil <- function(event) { NA } app$add_filter(fil, "foo") app$add_filter(fil, "foo") expect_warning(expect_true(app$filter())) }) test_that("Filterable: $set_filter works with functions", { app <- Appender$new() fil <- function(event) { FALSE } app$set_filters(fil) expect_identical(app$filters[[1]], fil) expect_length(app$filters, 1L) app$set_filters(list(fil)) expect_identical(app$filters[[1]], fil) expect_length(app$filters, 1L) app$set_filters(list(fil, fil)) expect_identical(app$filters[[1]], fil) expect_identical(app$filters[[2]], fil) expect_length(app$filters, 2L) }) lgr/tests/testthat/test_logger_tree.R0000644000176200001440000000302314131760166017523 0ustar liggesuserscontext("logger_tree") testthat::setup(remove_all_loggers()) testthat::teardown(remove_all_loggers()) test_that("logger_tree() works as expected", { on.exit(remove_all_loggers()) get_logger("blah/blubb/foo") get_logger("blah/blubb/bar") expect_setequal(logger_tree()$parent, c("root", "blah", "blubb", "foo", "bar")) remove_all_loggers() expect_identical(logger_tree()$parent, "root") }) test_that("logger_tree() detects logger properties", { on.exit(remove_all_loggers()) get_logger("blah/blubb/foo") get_logger("blah/blubb/bar") get_logger("blah/blubb")$set_propagate(FALSE) get_logger("blah/blubb")$set_appenders( list(AppenderConsole$new(), AppenderConsole$new()) ) res <- logger_tree() expect_false(res$propagate[res$parent == "blubb"]) expect_identical(res$n_appenders[res$parent == "blubb"], 2L) expect_true(all(res$propagate[res$parent != "blubb"])) expect_true(all(res$n_appenders[!res$parent %in% c("blubb", "root")] == 0)) }) test_that("logger_tree() doc examples", { on.exit(remove_all_loggers()) get_logger("fancymodel") get_logger("fancymodel/shiny")$ set_propagate(FALSE) get_logger("fancymodel/shiny/ui")$ set_appenders(AppenderConsole$new()) get_logger("fancymodel/shiny/server")$ set_appenders(list(AppenderConsole$new(), AppenderConsole$new()))$ set_threshold("trace") get_logger("fancymodel/plumber") out <- utils::capture.output(logger_tree()) expect_identical(length(out), 6L) }) lgr/tests/testthat/test_print_LogEvent.R0000644000176200001440000000575215036467161020203 0ustar liggesuserscontext("print_LogEvent") test_that("format.LogEvent works as expected", { x <- LogEvent$new( logger = lgr::lgr, msg = "lorem skjdghsad akjsgh asdgjh asdgjshadk gklsd.", blah = "blubb", numbers = 1:100, large_number = c(23.525325235213525235525, 930.824687923409867298367293406), iris = iris, logg = lgr::lgr, letters = letters ) expect_output(print(x)) expect_true(!crayon::has_style(format(x, colors = NULL))) expect_output(expect_identical(x, print(x))) }) test_that("format.LogEvent works with colored %k and %K paramters", { old <- Sys.getenv("R_CLI_NUM_COLORS") Sys.setenv(R_CLI_NUM_COLORS = "256") on.exit(Sys.setenv(R_CLI_NUM_COLORS = as.character(old))) x <- LogEvent$new( level = 200, logger = lgr::lgr, msg = "lorem skjdghsad akjsgh asdgjh asdgjshadk gklsd.", blah = "blubb", numbers = 1:100, large_number = c(23.525325235213525235525, 930.824687923409867298367293406), iris = iris, logg = lgr::lgr, letters = letters ) expect_silent( expect_true( crayon::has_style(format(x, fmt = "%k %m", colors = list(error = crayon::bgRed))) ) ) expect_silent( expect_true( crayon::has_style(format(x, fmt = "%K %m", colors = list(error = crayon::bgRed))) ) ) }) test_that("format.LogEvent - with excluded_fields - excludes those fields", { # Arrange x <- LogEvent$new( level = 200, logger = lgr::lgr, msg = "lorem skjdghsad akjsgh asdgjh asdgjshadk gklsd.", included_custom_field = "123", excluded_custom_field = "abc" ) # Act res <- format(x, fmt = "%k %m %j %f", excluded_fields = "excluded_custom_field") # Assert expect_match(res, "included_custom_field") expect_no_match(res, "excluded_custom_field") }) test_that("toString.LogEvent works as expected", { x <- LogEvent$new( logger = lgr::lgr, msg = "lorem skjdghsad akjsgh asdgjh asdgjshadk gklsd.", blah = "blubb", numbers = 1:100, large_number = c(23.525325235213525235525, 930.824687923409867298367293406), iris = iris, logg = lgr::lgr, letters = letters ) expect_length(toString(x), 1L) # just a rudimentary check because exact representation depends on console width expect_match(toString(x), "level.*letters") }) test_that("toString.LogEvent without custom fields does not end in whitespace", { event_without_field <- LogEvent$new( logger = lgr::lgr, msg = "lorem skjdghsad akjsgh asdgjh asdgjshadk gklsd." ) res <- format(event_without_field, fmt = "%m %f", colors = NULL) expect_identical( substr(res, nchar(res), nchar(res)), "." ) event_with_field <- LogEvent$new( logger = lgr::lgr, msg = "lorem skjdghsad akjsgh asdgjh asdgjshadk gklsd.", foo = "bar" ) res <- format(event_with_field, fmt = "msg %f", colors = NULL) expect_identical( substr(res, nchar(res), nchar(res)), "}" ) }) lgr/tests/testthat/test_simple_logging.R0000644000176200001440000000652214131760166020233 0ustar liggesuserscontext("simple_logging") setup({ lgr$add_appender(AppenderBuffer$new(threshold = NA), "memory") get_logger("test")$config(NULL) }) teardown({ basic_config() }) test_that("threshold(), console_threshold() and log_exception() work as expected", { expect_identical(threshold(), lgr$threshold) expect_identical(console_threshold(), lgr$appenders$console$threshold) yth <- lgr$threshold cth <- lgr$appenders$console$threshold on.exit({ lgr$set_threshold(yth) lgr$appenders$console$set_threshold(cth) }) threshold(NA) console_threshold(NA) expect_output(lgr$fatal("test"), "FATAL") expect_output(lgr$trace("test"), "TRACE") expect_error( expect_output(log_exception(stop("oops")), "FATAL.*oops$"), "oops" ) }) test_that("show_log()", { expect_output(show_log()) expect_true(nrow(show_data()) > 2) expect_output( expect_equal(show_log(), show_log(target = lgr$appenders$memory)) ) expect_error(show_log(target = lgr$appenders$console), "no method") lg <- Logger$new("test", propagate = FALSE) expect_error(show_log(target = lg), "has no Appender") expect_error(show_log(target = iris), "not a Logger or Appender") }) test_that("add_appender() and remove_appender() work", { tlg <- Logger$new("test", propagate = FALSE) add_appender(AppenderConsole$new(), target = tlg) expect_output(tlg$warn("test"), "WARN.*test\\s*$") remove_appender(1, target = tlg) expect_silent(tlg$warn("test")) expect_error(show_log(tlg)) }) test_that("Option 'lgr.log_file' works", { old <- getOption("lgr.log_file") on.exit(options("lgr.log_file" = old)) options("lgr.log_file" = tempfile()) res <- default_appenders() expect_identical(length(res), 1L) expect_s3_class(res[[1]], "AppenderFile") expect_s3_class(res[[1]]$layout, "LayoutFormat") expect_true(is.na(res[[1]]$threshold)) unlink(getOption("lgr.log_file")) options("lgr.log_file" = tempfile(pattern = ".json")) res <- default_appenders() expect_identical(length(res), 1L) expect_s3_class(res[[1]], "AppenderFile") expect_s3_class(res[[1]]$layout, "LayoutJson") expect_true(is.na(res[[1]]$threshold)) unlink(getOption("lgr.log_file")) options("lgr.log_file" = c(trace = tempfile(pattern = ".json"))) res <- default_appenders() expect_identical(length(res), 1L) expect_s3_class(res[[1]], "AppenderFile") expect_s3_class(res[[1]]$layout, "LayoutJson") expect_identical(res[[1]]$threshold, 600L) unlink(getOption("lgr.log_file")) options("lgr.log_file" = c(blubb = tempfile('_fail'))) expect_warning(res <- default_appenders(), "_fail") unlink(getOption("lgr.log_file")) }) test_that("show_data() works", { basic_config(memory = TRUE) on.exit(basic_config()) expect_output({ lgr$info("a log message") lgr$info("another message with data", data = 1:10) lgr$info(c("a vectorized message", "blubb")) }) expect_output(show_log()) expect_s3_class(show_data(), "data.frame") expect_identical(nrow(show_data()), 4L) expect_s3_class(show_dt(), "data.table") expect_identical(nrow(show_dt()), 4L) }) test_that("basic_config can set the console output format", { basic_config(console_fmt = "foobar %L [%t] %c: %m") expect_output(lgr$info("baz"), "foobar") }) lgr/tests/testthat/test_log_levels.R0000644000176200001440000000606214131760166017366 0ustar liggesuserscontext("simple_logging") test_that("add_log_levels(), get_log_levels() works as expected", { ml <- Logger$new("dummy") expect_error(ml$set_threshold("blubb")) expect_silent(add_log_levels(c(blubb = 250, schwupp = 341))) expect_silent(ml$set_threshold("blubb")) expect_identical(ml$threshold, 250L) expect_setequal( names(get_log_levels()), c("fatal", "error", "warn", "info", "debug", "trace", "blubb", "schwupp") ) expect_error( add_log_levels(c("blubb", "schwupp")) ) expect_message(add_log_levels(c(blubb = 250, schwupp = 341))) expect_true(all_are_distinct(get_log_levels())) expect_true(all_are_distinct(names(get_log_levels()))) remove_log_levels(c("blubb", "schwupp")) # cleanup expect_error(ml$set_threshold("blubb")) }) test_that("label_levels() & unlabel_levels() are symetirc", { tdat <- sample(c(seq(0, 600, by = 100), NA)) expect_silent({ x <- label_levels(tdat) expect_equal(names(x), as.character(tdat)) y <- unlabel_levels(x) expect_equal(unname(y), tdat) expect_equal(names(y), unname(x)) }) expect_silent(label_levels(NA_integer_)) expect_warning(label_levels(700)) expect_warning(unlabel_levels(NA_character_)) expect_warning(unlabel_levels("foo")) num <- c(93L, 200L) expect_warning(chr <- label_levels(num)) expect_warning( expect_identical(unname(unlabel_levels(chr)), num) ) }) test_that("colorize_levels() works as expected", { if (!crayon::has_color()) skip("Terminal does not support colors") # set colors manualy so that we also check on systems without real color support colors <- list( "fatal" = crayon::red, "error" = crayon::blue, "warn" = crayon::yellow, "debug" = crayon::silver, "trace" = crayon::white ) tdat <- setdiff(sample(seq(100, 600, by = 100)), 400) # 400/info is not colored tr1 <- colorize_levels(tdat, colors = colors) walk(tr1, function(.x) expect_true(crayon::has_style(.x), info = .x)) tr2 <- colorize_levels(label_levels(tdat), colors = colors) walk(tr2, function(.x) expect_true(crayon::has_style(.x), info = .x)) expect_true(is.null(colorize_levels(NULL))) expect_identical(colorize_levels(integer()), integer()) expect_identical(colorize_levels(character()), character()) expect_identical(colorize_levels(tdat, NULL), tdat) }) test_that("named_union() works as expected", { x <- c("blah" = "blubb", "fizz" = "buzz") y <- c("foo" = "bar", "blah2" = "blubb") r <- named_union(x, y) expect_identical( r, setNames(c("blubb", "buzz", "bar"), c("foo", "fizz", "blah2")) ) }) test_that("standardize_* doc examples", { expect_identical(standardize_threshold("info"), 400L) expect_true(is.na(standardize_threshold("all"))) expect_identical(standardize_log_level("info"), 400L) expect_error(is.na(standardize_log_level("all"))) expect_error(standardize_log_level(c("info", "fatal"))) expect_identical(standardize_log_levels(c("info", "fatal")), c(400L, 100L)) }) lgr/tests/testthat/test_print_Appender.R0000644000176200001440000000151615035130575020203 0ustar liggesuserscontext("print_Appender") test_that("all Appenders print() without failure", { skip_if_not_installed("rotor") tf <- tempfile() on.exit(unlink(tf)) expect_output({ print(Appender$new()) print(AppenderConsole$new()) print(AppenderConsole$new(layout = LayoutGlue$new())) print(AppenderBuffer$new(appenders = list( AppenderConsole$new(), blah = AppenderBuffer$new() ))) print(AppenderFileRotating$new(tf)) print(AppenderFileRotating$new(tf, size = "1kb")) app <- AppenderFileRotatingDate$new(tf, layout = LayoutJson$new()) app$rotate(force = TRUE) print(app) app$prune(0) print(AppenderFileRotatingTime$new( tf, layout = LayoutJson$new(), age = "2 years", size = 90000) ) print(AppenderFile$new(tf)) }) }) lgr/tests/testthat/test_get_logger.R0000644000176200001440000000745614131760166017361 0ustar liggesuserscontext("get_logger") test_that("get_logger(): works as expected", { lg <- get_logger("blubb") expect_identical(lg$name, "blubb") expect_identical(lg$name, "blubb") expect_identical(lg$parent, lgr) lg1 <- get_logger("fizz") lg2 <- get_logger("fizz/buzz") lg3 <- get_logger("fizz/buzz/wuzz") expect_identical(lg3$parent, lg2) expect_identical(lg2$parent, lg1) expect_identical(lg1$parent, lgr::lgr) lg2$set_propagate(FALSE) expect_equal( unname(unclass(lg3$ancestry)), c(TRUE, FALSE, TRUE) ) expect_equal( names(lg3$ancestry), c("fizz", "buzz", "wuzz") ) }) test_that("logger names can be specified using scalars or vectors", { lg1 <- get_logger("fizz/buzz/wuzz") lg2 <- get_logger(c("fizz", "buzz", "wuzz")) expect_identical(lg1, lg2) }) test_that("get_logger is not confused by existing objects with the same name as logger", { lg1 <- get_logger("log/ger/test") lg2 <- get_logger("log/ger") expect_identical(lg1$parent, lg2) }) test_that("is_virgin_Logger() identifies loggers without settings", { lg1 <- get_logger("foo/bar") expect_true(is_virgin_Logger("foo/bar")) lg1$set_threshold("off") expect_false(is_virgin_Logger("foo/bar")) lg1$set_threshold(NULL) expect_true(is_virgin_Logger(get_logger("foo/bar"))) }) test_that("get_logger_glue() works as expected", { lg <- get_logger("log/ger/test") expect_true(is_Logger(lg)) expect_s3_class(get_logger_glue("log/ger"), "LoggerGlue") expect_s3_class(get_logger("log/ger"), "LoggerGlue") expect_identical(lg$parent, get_logger("log/ger")) lg$config(NULL) }) test_that("get_logger_glue() succeedes to get preconfigured glue loggers", { lg <- get_logger_glue("log/ger/test") lg$set_threshold("fatal") lg <- get_logger_glue("log/ger/test") expect_s3_class(lg, "LoggerGlue") get_logger("log/ger/test", reset = TRUE) }) test_that("get_logger_glue() fails to get preconfigured loggers that are not glue loggers", { lg <- get_logger("log/ger/test2") lg$set_threshold("fatal") expect_error(get_logger_glue("log/ger/test2"), "LoggerGlue") get_logger("log/ger/test2", reset = TRUE) }) test_that("get_logger(reset == TRUE) completely resets logger", { lg <- get_logger_glue("log/ger/reset") lg$set_threshold("fatal") lg$add_appender(AppenderConsole$new()) expect_s3_class(lg, "LoggerGlue") expect_identical(lg$threshold, 100L) expect_identical(lg, get_logger("log/ger/reset")) lg2 <- get_logger("log/ger/reset", reset = TRUE) lg1 <- get_logger("log/ger/reset") expect_identical( data.table::address(lg1), data.table::address(lg2) ) lg_g1 <- get_logger_glue("log/ger/reset") lg_g2 <- get_logger_glue("log/ger/reset") expect_identical(lg1, lg2) expect_identical( data.table::address(lg_g1), data.table::address(lg_g2) ) expect_false(inherits(lg2, "LoggerGlue")) expect_identical(lg2$threshold, 400L) }) test_that("get_logger(reset == TRUE) invalidates old Logger", { lg1 <- get_logger("log/ger/reset", reset = TRUE) lg1$set_threshold("fatal") lg2 <- get_logger("log/ger/reset", reset = TRUE)$ set_threshold("info")$ add_appender(AppenderConsole$new())$ set_propagate(FALSE) expect_true(!identical(lg1, lg2)) expect_warning(lg1$fatal("test"), "log/ger/reset") expect_output(lg2$info("test")) # invalidation works with logger glue get_logger("log/ger/reset", reset = TRUE) lg1 <- get_logger_glue("log/ger/reset") lg1$set_threshold("fatal") lg2 <- get_logger("log/ger/reset", reset = TRUE) expect_true(!identical(lg1, lg2)) expect_warning(lg1$fatal("test"), "log/ger/reset") expect_output(lg2$info("test")) get_logger("log/ger/reset", reset = TRUE) }) lgr/tests/testthat/test_Logger.R0000644000176200001440000003443415035130575016455 0ustar liggesuserscontext("Logger") # Logger ------------------------------------------------------------------ test_that("logging conditions works", { e <- error("blahblah") ln <- get_logger("test")$ config(NULL)$ set_propagate(FALSE) lg <- get_logger_glue("test_glue")$ config(NULL)$ set_propagate(FALSE) expect_match(ln$fatal(e), "blahblah") expect_match(lg$fatal(e, "this is <{it}>", e, it = 1, conditon = e), "blahblahthis is <1>blahblah") }) test_that("set_threshold()", { ml <- Logger$new("test_logger") expect_silent(ml$set_threshold(5)) expect_identical(ml$threshold, 5L) expect_silent(ml$set_threshold("fatal")) expect_identical(ml$threshold, 100L) expect_error(ml$set_threshold("blubb"), "fatal.*trace") walk(ml$appenders, function(.x) expect_true(inherits(.x, "Appender"))) }) test_that("$log() & co work", { ml <- Logger$new("test_logger", appenders = list(memory = AppenderBuffer$new())) ts <- structure(1540486764.41946, class = c("POSIXct", "POSIXt")) testfun <- function(){ ml$fatal("blubb") ml$log(msg = "testfun", level = 500) } expect_output({ testfun() testfun() }) expect_true( all(ml$appenders$memory$data$caller == "testfun"), info = ml$appenders$memory$data$caller ) }) test_that("string formatting works", { l <- Logger$new("test_logger", propagate = FALSE) expect_identical(l$fatal("test"), "test") expect_identical(l$fatal("te%s", "st"), "test") expect_identical(l$fatal("te%s", "st", blah = "blubb"), "test") }) test_that("string formatting is only used if unnamed arguments exist", { lg <- Logger$new( "test", appenders = list(AppenderConsole$new()), propagate = FALSE ) # unnamed arguments trigger sprintf expect_output(lg$fatal("foo %s", "bar"), "foo bar") expect_output(lg$fatal("foo %s", "bar", fizz = "buzz"), "foo bar") # otherwise msg is processed as-is expect_output(lg$fatal("foo %s"), "foo %s") expect_output(lg$fatal("foo %s", fizz = "buzz"), "foo %s") }) test_that("set_threshold() works for Loggers and Appenders", { lg <- Logger$new("dummy", appenders = AppenderConsole$new()) lg$set_threshold(200) expect_identical(lg$threshold, 200L) lg$set_threshold("info") expect_identical(lg$threshold, 400L) lg$set_threshold(NA) expect_identical(lg$threshold, NA_integer_) expect_error(lg$set_threshold("blubb"), "log levels") # test if setting for appenders of a logger also works as this is somewhat tricky lg$appenders[[1]]$set_threshold(NA) expect_identical(lg$appenders[[1]]$threshold, NA_integer_) lg$appenders[[1]]$set_threshold(300) expect_identical(lg$appenders[[1]]$threshold, 300L) lg$appenders[[1]]$set_threshold("info") expect_identical(lg$appenders[[1]]$threshold, 400L) expect_error(lg$appenders[[1]]$set_threshold("blubb"), "log levels") }) test_that("threshold of `0` suspends logging", { ml <- Logger$new("test_logger") expect_output(ml$info("blubb"), "blubb") expect_output(ml$fatal("blubb"), "FATAL") x <- capture.output(ml$fatal("blubb")) ml$set_threshold(0) expect_identical(ml$fatal("blubb %s", "blah"), NULL) ml$set_threshold("error") y <- capture.output(ml$fatal("blubb")) expect_output(ml$log(100, "test"), "FATAL") expect_output(ml$log("error", "test"), "ERROR") expect_silent(ml$warn("blubb")) expect_silent(ml$log(300, "blubb")) # ignore timestamp for comparison x <- gsub("[.*]", x, "[time]") y <- gsub("[.*]", x, "[time]") expect_identical(x, y) }) test_that("add_appenders()/remove_appenders()", { tf <- tempfile() on.exit(unlink(tf)) ml <- Logger$new("test_logger", appenders = AppenderFile$new(file = tf)) app1 <- AppenderConsole$new(threshold = 100) app2 <- AppenderConsole$new(threshold = 300) # add ml$add_appender(app1) ml$add_appender(app2, "blah") ml$add_appender(AppenderBuffer$new(), "blubb") expect_identical(ml$appenders[[2]], app1) expect_identical(ml$appenders$blah, app2) # remove ml$remove_appender(2) expect_length(ml$appenders, 3) ml$remove_appender(c("blah", "blubb")) expect_length(ml$appenders, 1L) # set ml$set_appenders(list(app1, app2)) expect_length(ml$appenders, 2) ml$set_appenders(list(app1, app2)) expect_length(ml$appenders, 2) expect_identical(ml$appenders[[1]], app1) expect_identical(ml$appenders[[2]], app2) }) test_that("setting Appender properties works", { ml <- Logger$new("test_logger", appenders = list(AppenderConsole$new()), propagate = FALSE) tf <- tempfile() on.exit(unlink(tf)) # Add a new appender to a logger. We don't have to supply a name, but that # mak1es it easier to remove later. ml$add_appender(AppenderFile$new(file = tf), name = "file") # configure lgr so that it logs everything to the file, but only info and above # to the console ml$set_threshold(NA) ml$appenders[[1]]$set_threshold("info") ml$appenders$file$set_threshold(NA) expect_output(ml$info("Another informational message")) expect_silent(ml$debug("A debug message that the console appender doesn't show.")) expect_identical(length(readLines(tf)), 2L) expect_match(paste(readLines(tf), collapse = "---"), "INFO.*---DEBUG.*") }) test_that("Exceptions are cought and turned into warnings", { tf <- tempfile() on.exit(unlink(tf)) ml <- Logger$new("test_logger", appenders = list( AppenderFile$new(file = tf), AppenderConsole$new() ) ) expect_warning(ml$fatal(stop("blubb")), "error.*blubb") expect_warning(ml$fatal(), "error") }) test_that("Inheritance and event propagation works", { on.exit({ c1$config(NULL) c2$config(NULL) c3$config(NULL) unlink(c(tf1, tf2, tf3)) }) tf1 <- tempfile() tf2 <- tempfile() tf3 <- tempfile() c1 <- get_logger("c1") c1$add_appender(AppenderFile$new(tf1)) c2 <- get_logger("c1/c2") c2$add_appender(AppenderFile$new(tf2)) c3 <- Logger$new("c1/c2/c3") c3$add_appender(AppenderFile$new(tf3)) expect_output(c3$fatal("blubb"), "FATAL.*blubb") expect_match(readLines(tf1), "FATAL.*blubb") expect_identical(readLines(tf1), readLines(tf2)) expect_identical(readLines(tf1), readLines(tf3)) c2$set_propagate(FALSE) expect_silent(c3$error("blubb")) expect_identical(readLines(tf2), readLines(tf3)) expect_lt(length(readLines(tf1)), length(readLines(tf3))) }) test_that("Named inherited appenders are displayed correctly", { on.exit({ c1$config(NULL) c2$config(NULL) c3$config(NULL) }) c1 <- get_logger("c1")$ set_propagate(FALSE)$ add_appender(AppenderConsole$new(), "console") expect_null(c1$inherited_appenders) c2 <- get_logger("c1/c2")$ add_appender(AppenderConsole$new(), "console") expect_length(c2$inherited_appenders, 1L) c3 <- Logger$new("c1/c2/c3")$ add_appender(AppenderConsole$new(), "console") expect_length(c3$inherited_appenders, 2L) expect_identical(names(c3$inherited_appenders), c("console", "console")) expect_output(print(c3), "\\sconsole:.*\\sconsole:.*") }) test_that("Unnamed inherited appenders are displayed correctly", { on.exit({ c1$config(NULL) c2$config(NULL) c3$config(NULL) }) c1 <- get_logger("c1")$ set_propagate(FALSE)$ add_appender(AppenderConsole$new()) c2 <- get_logger("c1/c2")$ add_appender(AppenderConsole$new()) c3 <- Logger$new("c1/c2/c3")$ add_appender(AppenderConsole$new()) expect_null(names(c3$inherited_appenders)) expect_output(print(c3), "\\[\\[1\\]\\]\\:.*\\[\\[2\\]\\]\\:.*") }) test_that("threshold works", { c1 <- Logger$new("c1") expect_output(c1$error("blubb"), "ERROR") c1$set_threshold(100) expect_silent(c1$error("blubb")) }) test_that("$ancestry works", { l4 <- get_logger("l1/l2/l3/l4") l2 <- get_logger("l1/l2")$set_propagate(FALSE) expect_equal( unname(unclass(l4$ancestry)), c(TRUE, FALSE, TRUE, TRUE) ) }) test_that("Logger handles null values", { l <- Logger$new("test", propagate = FALSE) log_null <- l$info("foo %s %s", NULL, character()) expect_equal(log_null, "foo ") }) test_that("Logger handles null values along with named parameters", { l <- Logger$new("test", propagate = FALSE) log_null <- l$info("foo %s %s", NULL, character(), valbert = "hashbaz", nullbert = NULL) expect_equal(log_null, "foo ") expect_true(l$last_event$valbert == "hashbaz") expect_true("nullbert" %in% names(l$last_event$values)) expect_null(l$last_event$nullbert) }) # LoggerGlue -------------------------------------------------------------- test_that("LoggerGlue supports custom fields", { l <- LoggerGlue$new("glue") expect_output(l$fatal("test", "test"), glue::glue("test", "test")) expect_output( l$fatal("blah", "blubb", foo = "bar"), ".*blahblubb.*bar.*\\}" ) expect_output( l$fatal("blah", "blubb {fizz}", foo = "bar", fizz = "buzz"), "buzz.*foo.*bar.*fizz.*buzz.*" ) expect_output(l$fatal("a", .open = "{", .foo = "bar", fizz = "buzz")) expect_true("fizz" %in% names(l$last_event)) expect_false(".open" %in% names(l$last_event)) expect_false(".foo" %in% names(l$last_event)) }) test_that("LoggerGlue uses the correct evaluation environment", { l <- LoggerGlue$new("glue", propagate = FALSE) expect_match(l$fatal("{iris[['Species']][[1]]}"), "setosa") expect_match(l$log(100, "{iris[['Species']][[1]]}"), "setosa") expect_match(l$fatal(100, "{x}", x = iris[['Species']][[1]]), "setosa") expect_match(l$log(100, "{x}", x = iris[['Species']][[1]]), "setosa") }) test_that("$config works with lists", { l <- LoggerGlue$new("glue", propagate = FALSE) cfg <- list( appenders = list(blubb = AppenderConsole$new()), propagate = FALSE ) expect_identical(names(l$config(cfg)$appenders), "blubb") expect_identical(names(l$config(list = cfg)$appenders), "blubb") expect_identical(l$config(cfg)$propagate, FALSE) expect_error(l$config(cfg = cfg, list = cfg)) l$config(NULL) expect_true(is_virgin_Logger(l, allow_subclass = TRUE)) expect_false(is_virgin_Logger(l)) }) test_that("LoggerGlue handles null values", { l <- LoggerGlue$new("glue", propagate = FALSE) log_null <- l$info("foo", NULL) expect_equal(log_null, "foo") log_null_parameter <- l$info("foo {bar} {baz}", bar = NULL, baz = character()) expect_equal(log_null_parameter, "foo ") }) test_that("LoggerGlue handles null values along with named parameters", { l <- LoggerGlue$new("test", propagate = FALSE) log_null <- l$info("foo ", NULL, character(), valbert = "hashbaz", nullbert = NULL) expect_equal(log_null, "foo ") expect_true(l$last_event$valbert == "hashbaz") expect_true("nullbert" %in% names(l$last_event$values)) expect_null(l$last_event$nullbert) }) test_that("Logger glue can use custom transformers", { transformer <- function(text, envir) { "transformed text" } l <- LoggerGlue$new("glue", propagate = FALSE, transformer = transformer) e <- log_null <- l$info("foo {bar}", foo = "bar") expect_equal(e, "foo transformed text") }) # Multi-Logger tests ------------------------------------------------------------- test_that("Logger$log() dispatches to all appenders, even if some throw an error", { ln <- Logger$new("normal", propagate = FALSE) lg <- LoggerGlue$new("glue", propagate = FALSE) on.exit({ ln$config(NULL) lg$config(NULL) }) AppErr <- R6::R6Class( inherit = AppenderConsole, public = list( append = function(...) stop("error") ) ) tf <- tempfile() on.exit(unlink(tf)) ln$set_appenders(list( err = AppErr$new(), file = AppenderFile$new(file = tf) )) lg$set_appenders(list( err = AppErr$new(), file = AppenderFile$new(file = tf) )) expect_warning(ln$info("test_normal"), class = "appendingFailedWarning") expect_warning(ln$info("test_glue"), class = "appendingFailedWarning") expect_true(any(grepl("test_normal", readLines(tf)))) expect_true(any(grepl("test_glue", readLines(tf)))) }) test_that("Logger error contains useful call object", { l <- get_logger("test") g <- get_logger_glue("testglue") on.exit({ l$config(NULL) g$config(NULL) }) expect_warning(l$info("this will fail", e = stop()), "l\\$info") expect_warning(g$info("this will fail", e = stop()), "g\\$info") }) test_that("Appender error contains useful call object", { l <- get_logger("test")$set_propagate(FALSE) g <- get_logger_glue("testglue")$set_propagate(FALSE) on.exit({ l$config(NULL) g$config(NULL) }) AppenderFail <- R6::R6Class( "AppenderFail", inherit = Appender, public = list( append = function(e) stop("bummer") ) ) a <- AppenderFail$new() l$add_appender(a, "fail") g$add_appender(a, "fail") expect_warning(l$info("this will fail"), ".*AppenderFail.*l\\$info") expect_warning(g$info("this will fail"), ".*AppenderFail.*g\\$info") }) test_that("Logger$log - with 'msg' argument - logs the message", { l <- get_logger("test")$set_propagate(FALSE) on.exit({ l$config(NULL) }) expect_silent(l$log(level = "fatal", msg = "test")) }) test_that("LoggerGlue$log - with 'msg' argument - throws a warning", { l <- get_logger_glue("testglue")$set_propagate(FALSE) on.exit({ l$config(NULL) }) expect_warning(l$log(level = "fatal", msg = "test"), "does not support") }) test_that("Logger$log - with string interpolation - adds rawMsg to event", { l <- get_logger("test")$set_propagate(FALSE) on.exit({ l$config(NULL) }) l$info("foo %s", "bar") expect_identical(l$last_event$msg, "foo bar") expect_identical(l$last_event$rawMsg, "foo %s") }) test_that("LoggerGlue$log - with string interpolation - adds rawMsg to event", { l <- get_logger_glue("testglue")$set_propagate(FALSE) on.exit({ l$config(NULL) }) l$fatal("hash {x}", x = "baz") expect_identical(as.character(l$last_event$msg), "hash baz") expect_identical(l$last_event$rawMsg, "hash {x}") }) lgr/tests/testthat/test_LayoutJson.R0000644000176200001440000000736015037354643017351 0ustar liggesusers# LayoutJson -------------------------------------------------------------- test_that("LayoutJson works as expected", { lo <- LayoutJson$new() event <- LogEvent$new( logger = Logger$new("dummy"), level = 200L, timestamp = as.POSIXct(1541175573.9308, origin = "1970-01-01", tz = "UTC"), caller = NA_character_, msg = "foo bar", rawMsg = "foo raw", customField = "hashbaz", customField2 = "barfoo", foo = "bar" ) eres <- event$values json <- lo$format_event(event) tres <- jsonlite::fromJSON(json) expected_values <- c("level", "timestamp", "logger", "caller", "msg", "foo", "customField", "customField2") # rawMsg is excluded by default tres[sapply(tres, is.null)] <- NA_character_ expect_setequal(expected_values, names(tres)) expect_identical(tres[["level"]], eres[["level"]]) expect_identical(tres[["msg"]], eres[["msg"]]) expect_identical(tres[["caller"]], eres[["caller"]]) expect_identical(tres[["customField"]], eres[["customField"]]) expect_identical(tres[["customField"]], eres[["customField"]]) expect_equal(as.POSIXct(tres[["timestamp"]], tz = "UTC"), eres[["timestamp"]], tolerance = 1) expect_equal(tres[["foo"]], "bar") }) test_that("LayoutJson `excluded_fields` works as expected", { lo <- LayoutJson$new(excluded_fields = c("customField", "customField2")) event <- LogEvent$new( logger = Logger$new("dummy"), level = 200L, timestamp = as.POSIXct(1541175573.9308, origin = "1970-01-01", tz = "UTC"), caller = NA_character_, msg = "foo bar", rawMsg = "foo raw", customField = "hashbaz", customField2 = "barfoo", foo = "bar" ) eres <- event$values json <- lo$format_event(event) tres <- jsonlite::fromJSON(json) expected_values <- c("level", "timestamp", "logger", "caller", "msg", "foo", "rawMsg") tres[sapply(tres, is.null)] <- NA_character_ expect_setequal(expected_values, names(tres)) expect_identical(tres[["level"]], eres[["level"]]) expect_identical(tres[["msg"]], eres[["msg"]]) expect_identical(tres[["caller"]], eres[["caller"]]) expect_equal(as.POSIXct(tres[["timestamp"]], tz = "UTC"), eres[["timestamp"]], tolerance = 1) expect_equal(tres[["foo"]], "bar") expect_equal(tres[["rawMsg"]], "foo raw") }) test_that("formatting timestamps with LayoutJson works", { lo <- LayoutJson$new() event <- LogEvent$new( logger = Logger$new("dummy"), level = 200L, timestamp = as.POSIXct(1541175573.9308, origin = "1970-01-01", tz = "UTC"), caller = NA_character_, msg = "foo bar", rawMsg = "foo raw", customField = "hashbaz", customField2 = "barfoo" ) eres <- event$values json <- lo$format_event(event) lo$set_timestamp_fmt(function(event) "fmt timestamp with function") expect_match(lo$format_event(event), "fmt timestamp with function") lo$set_timestamp_fmt("%H:%M:%S..%OS") expect_match(lo$format_event(event), format(event$timestamp, "%H:%M:%S..%OS")) }) test_that("LayoutJson.format_event() - with field rename function - renames fields correctly", { # Arrange event <- LogEvent$new( logger = Logger$new("dum/my"), level = 200L, timestamp = structure(1541175573.9308, class = c("POSIXct", "POSIXt")), caller = NA_character_, msg = "foo bar", rawMsg = "foobar-raw" ) lo <- LayoutJson$new( transform_event_names = toupper, excluded_fields = c("RAWMSG", "CALLER") ) # Act res <- jsonlite::fromJSON(lo$format_event(event)) # Assert expect_setequal( names(res), c("LOGGER", "LEVEL", "TIMESTAMP", "MSG")) expect_identical(res$MSG, "foo bar") expect_identical(res$LEVEL, 200L) expect_identical(res$LOGGER, "dum/my") }) lgr/tests/testthat/test_default_functions.R0000644000176200001440000000072414131760166020746 0ustar liggesuserscontext("default_functions") test_that("default_functions works as expected", { lg <- get_logger("a/test/logger") lg$set_exception_handler(function(e) e) x <- lg$info(stop("blahblah")) expect_warning(default_exception_handler(x), class = "LoggerWarning") x$appender <- AppenderConsole$new() expect_warning(default_exception_handler(x), class = "AppenderWarning") expect_warning(default_exception_handler(x), class = "LoggerWarning") }) lgr/tests/testthat/test_Filter.R0000644000176200001440000000336414131760166016462 0ustar liggesuserscontext("Filter") test_that("FilterForceLevel and FilterInject work as expected", { l <- Logger$new("foo") l$add_filter(FilterForceLevel$new("fatal")) l$add_filter(FilterInject$new(iris = iris, .list = list(cars = "foo"))) expect_output(l$info("blubb"), "FATAL.*cars.*data\\.frame") l$remove_filter(1) expect_output(l$info("blubb"), "INFO.*cars.*data\\.frame") l$remove_filter(1) expect_output(l$info("test"), "test\\s*$") }) test_that(".obj() works as expected", { l <- Logger$new("foo") f <- function(event) { cat(class_fmt(.obj())) TRUE } l$add_filter(f) expect_output(l$fatal("test"), "Logger/Filterable/R6") l$remove_filter(1) l$add_filter(EventFilter$new(f)) expect_output(l$fatal("test"), "Logger/Filterable/R6") }) test_that("FilterForceLevel and FilterInject work inside function", { lg <- get_logger("test") lg$set_threshold(NA) lg$set_propagate(FALSE) lg$add_appender(AppenderConsole$new(threshold = NA)) analyse <- function(){ lg$add_filter(FilterForceLevel$new("info"), "force") lg$add_filter(FilterInject$new(type = "analysis"), "inject") on.exit(lg$remove_filter(c("force", "inject"))) lg$debug("a debug message") lg$error("an error") } expect_output(analyse(), "INFO.*debug.*type.*INFO.*error.*type.*") expect_length(lgr$filters, 0) lg$config(logger_config()) }) test_that("standardize_filters_list works as expected", { f1 <- function(event) TRUE expect_identical(standardize_filters_list(f1), list(f1)) expect_identical( standardize_filters_list(list(f1, f1)), list(f1, f1) ) expect_error( standardize_filters_list(list(f1, mean)), class = "ObjectIsNoFilterError" ) }) lgr/tests/testthat/test_parallel.R0000644000176200001440000000300415035130575017017 0ustar liggesusersfor (strategy in c( "sequential", "multicore" # "multisession", # "cluster" )){ skip_if_not_installed("future") context(sprintf("future plan = '%s'", strategy)) if (!future::availableCores("multicore") > 1L){ skip("'multicore' not supported on system") } test_that(paste0(strategy, ": Logging works"), { skip_if_not_installed("future") skip_if_not_installed("future.apply") future::plan(strategy) tf <- tempfile() on.exit(unlink(tf)) lr <- get_logger("par_root") lr$ set_appenders(AppenderFile$new(tf))$ set_propagate(FALSE) lg <- get_logger("par_root/par_child") x <- future::future(lr$info("root_logger")) expect_silent(x <- future::value(x)) expect_match(readLines(tf)[[1]], "root_logger") y <- future::future(lg$info("child_logger")) expect_silent(y <- future::value(y)) expect_match(readLines(tf)[[2]], "child_logger") future.apply::future_lapply( c("flapply 1", "flapply 2"), function(.x) lr$info("root %s", .x, pid = Sys.getpid()) ) res <- readLines(tf)[3:4] expect_true(any(grepl("root.*flapply 1", res))) expect_true(any(grepl("root.*flapply 2", res))) future.apply::future_lapply( c("flapply 1", "flapply 2"), function(.x) lr$info("child %s", .x) ) res <- readLines(tf)[5:6] expect_true(any(grepl("child.*flapply 1", res))) expect_true(any(grepl("child.*flapply 2", res))) unlink(tf) }) } lgr/tests/testthat/manual_tests/0000755000176200001440000000000014131760166016544 5ustar liggesuserslgr/tests/testthat/manual_tests/test_AppenderDigest.R0000644000176200001440000000373414131760166022633 0ustar liggesusers# AppenderPushbullet ---------------------------------------------------- email <- whoami::email_address() test_that("AppenderPushbullet: ERROR log level triggers push", { skip_if_not_installed("RPushbullet") tryCatch( RPushbullet::pbGetUser(), error = function(e) skip(as.character(e)) ) l <- Logger$new( "dummy", threshold = NA_integer_, appenders = list( pb = AppenderPushbullet$new( buffer_size = 3 ) ), propagate = FALSE ) l$info("1") l$info("2") l$trace("3") l$debug("4") l$debug("something something 5ish has occurred") l$fatal("pushbullet wörks ÄÖÜßÄ", foo = "bar") }) # AppenderSendmail -------------------------------------------------------- test_that("AppenderSendmail: ERROR log level triggers push", { skip_if_not_installed("sendmailR") smtp <- getOption("stat.smtp_server") if (is.null(smtp)) skip("Please set the 'stat.smtp_server' option") if (is.null(email)) skip("Cannot determine email addresse") l <- Logger$new( "dummy", threshold = NA_integer_, appenders = AppenderSendmail$new( to = email, control = list(smtpServer = smtp, verboseShow = TRUE), buffer_size = 3, html = FALSE), propagate = FALSE ) l$info("1") l$info("2") l$trace("3") l$debug("4") l$debug("something something 5ish has occurred ÄÖÜ") l$fatal("sendmailR wörks", foo = "bar") }) # AppenderGmail -------------------------------------------------------- test_that("AppenderGmail: ERROR log level triggers push", { skip_if_not_installed("gmailr") l <- Logger$new( "dummy", threshold = NA_integer_, appenders = AppenderGmail$new( to = email, html = TRUE, buffer_size = 3), propagate = FALSE ) l$info("1") l$info("2") l$trace("3") l$debug("4") l$debug("ÄÖÜßÄ") l$fatal("gmailr wörks ÄÖÜßÄ", foo = "bar") }) lgr/tests/testthat/test_event_list.R0000644000176200001440000000170514232437202017400 0ustar liggesuserscontext("event_list") test_that("event_list works as expected", { x <- LogEvent$new(msg = LETTERS, logger = lgr) expect_length(as_event_list(x), 1L) expect_length(as_event_list(x, scalarize = TRUE), 26L) l <- event_list( LogEvent$new(level = 100, msg = 1:5, logger = lgr), LogEvent$new(level = 300, msg = c("A", "B"), logger = lgr) ) expect_length(as_event_list(l, scalarize = TRUE), 7L) expect_length(as_event_list(list(l, c(l, list(l))), scalarize = TRUE), 21L) }) test_that("as_event_list works for data.frames", { dd <- data.frame( timestamp = as.POSIXct("2022-04-28 00:00:01") + 1:6, level = c(100, 200, 300, 400, 500, 600), message = paste("test", 1:6) ) res <- as_event_list(dd) expect_s3_class(res, "event_list") expect_true(is.list(res)) expect_identical(length(res), nrow(dd)) for (event in res){ expect_true(inherits(event, "LogEvent")) } }) lgr/tests/testthat/test_utils-logging.R0000644000176200001440000000314614131760166020017 0ustar liggesuserscontext("utils-logging") test_that("log suppression", { lg <- get_logger("test") # without_logging() suppresses log messages temporarily expect_output(lg$fatal("test"), "FATAL") expect_silent(without_logging(lg$fatal("test"))) expect_output(lg$fatal("test"), "FATAL") # suspending works and can be ignored with with_logging() suspend_logging() expect_silent(lg$fatal("test")) expect_output(with_logging(lg$fatal("test"))) # unsuspending logging works unsuspend_logging() expect_output(lg$fatal("test"), "FATAL") }) test_that("without_logging does not conflict with suspend/unsuspend logging", { suspend_logging() without_logging("blah") expect_identical(getOption("lgr.logging_suspended"), TRUE) unsuspend_logging() }) test_that("with_log_level works", { lg <- get_logger("test") lg$ set_appenders(AppenderConsole$new(threshold = "info"))$ set_threshold(NA)$ set_propagate(FALSE) expect_output( with_log_level("warn", lg$info("blubb"), logger = lg), "WARN" ) foo <- function(){ with_log_level("trace", lg$info("blubb"), logger = lg) } expect_silent(foo()) }) test_that("with_log_level works", { lg <- Logger$new( "test", appenders = AppenderConsole$new(layout = LayoutJson$new()), propagate = FALSE ) expect_output( with_log_value(list(foo = "bar", a = 1:2), lg$info("blubb"), logger = lg), '"a":\\[1,2\\],"foo":"bar"' ) foo <- function(){ with_log_value(list(foo = "bar"), lg$info("blubb"), logger = lg) } expect_output(foo()) }) lgr/tests/testthat/test_utils.R0000644000176200001440000000611514131760166016372 0ustar liggesusers context("utils") test_that("utils works as expected", { foo <- function() get_caller(-1L) expect_identical(foo(), "foo") lg <- Logger$new( "test logger", appenders = AppenderConsole$new(layout = LayoutFormat$new(fmt = "%c")), propagate = FALSE ) foobar <- function() lg$error("test") expect_output(foobar(), "foobar") fizzbuzz <- function() foobar() expect_output(fizzbuzz(), "foobar") blahblubb <- function() lg$log(200, "test") expect_output(blahblubb(), "blahblubb") expect_match(get_caller(-99), "shell") }) test_that("standardize log_levels / threshold works", { expect_error(standardize_log_level(NA)) expect_identical(standardize_threshold(NA), NA_integer_) blubb <- 100 expect_identical(standardize_log_levels(blubb), 100L) expect_identical(standardize_log_level(blubb), 100L) expect_identical(standardize_threshold(blubb), 100L) blubb <- NA expect_error(standardize_log_levels(blubb), "blubb.*trace") expect_error(standardize_log_level(blubb), "blubb.*trace") expect_identical(standardize_threshold(blubb), NA_integer_) blubb <- "all" expect_error(standardize_log_levels(blubb), "blubb.*trace") expect_error(standardize_log_level(blubb), "blubb.*trace") expect_identical(standardize_threshold(blubb), NA_integer_) blubb <- "off" expect_error(standardize_log_levels(blubb), "blubb.*trace") expect_error(standardize_log_level(blubb), "blubb.*trace") expect_identical(standardize_threshold(blubb), 0L) blubb <- 0 expect_error(standardize_log_levels(blubb), "blubb.*trace") expect_error(standardize_log_level(blubb), "blubb.*trace") expect_identical(standardize_threshold(blubb), 0L) blubb <- "info" expect_identical(standardize_log_levels(blubb), 400L) expect_identical(standardize_log_level(blubb), 400L) expect_identical(standardize_threshold(blubb), 400L) blubb <- c("info", "warn") expect_identical(standardize_log_levels(blubb), c(400L, 300L)) expect_error(standardize_log_level(blubb), "blubb.*scalar") expect_error(standardize_threshold(blubb), "threshold.*scalar") blubb <- c(400, 300) expect_identical(standardize_log_levels(blubb), c(400L, 300L)) expect_error(standardize_log_level(blubb), "blubb.*scalar") expect_error(standardize_threshold(blubb), "threshold.*scalar") blubb <- c(0, 300) expect_error(standardize_log_levels(blubb), "blubb.*trace") expect_error(standardize_log_level(blubb), "blubb.*scalar") expect_error(standardize_threshold(blubb), "threshold.*scalar") blubb <- "bar" expect_error(standardize_log_levels(blubb), "blubb.*trace") expect_error(standardize_log_level(blubb), "blubb.*trace") expect_error(standardize_threshold(blubb), "blubb.*trace") }) test_that("get_caller() does something useful for corner cases", { foo <- function(arg){get_caller()} expect_identical(foo(), "foo") r <- {function(arg){get_caller()}}() expect_identical(r, "{...}") r <- (function(arg){"sgnasdkgshdkghsakdghskdjghsdkag"; get_caller()})() expect_length(r, 1) }) lgr/tests/testthat/test_AppenderFileRotating.R0000644000176200001440000003027115035130575021277 0ustar liggesuserscontext("AppenderFileRotating") setup({ td <- file.path(tempdir(), "lgr") assign("td", td, parent.env(environment())) dir.create(td, recursive = TRUE) }) teardown({ unlink(td, recursive = TRUE) }) # AppenderFileRotating ----------------------------------------------------- test_that("AppenderFileRotating: works as expected", { skip_if_not_installed("rotor", "0.3.0") if (!is_zipcmd_available()){ skip("Test requires a workings system zip command") } tf <- file.path(td, "test.log") app <- AppenderFileRotating$new(file = tf, size = "1tb") lg <- lgr::get_logger("test")$ set_propagate(FALSE)$ set_appenders(app) on.exit({ lg$config(NULL) file.remove(tf) app$prune(0) }) # appender logs to file expect_identical(app, lg$appenders[[1]]) lg$fatal("test973") expect_true(file.exists(app$file)) expect_length(readLines(app$file), 1) expect_match(readLines(app$file), "test973") # first rotate rotates a file with content app$rotate(force = TRUE) expect_gt(lg$appenders[[1]]$backups[1, ]$size, 0L) expect_identical(nrow(lg$appenders[[1]]$backups), 1L) expect_length(readLines(app$file), 0) expect_match(readLines(app$backups$path[[1]]), "test973") # second rotate only has a file of size 0 to rotate app$rotate(force = TRUE) expect_equal(app$backups[1, ]$size, 0) expect_identical(nrow(lg$appenders[[1]]$backups), 2L) # compression works lg$fatal("test987") lg$appenders[[1]]$set_compression(TRUE) lg$appenders[[1]]$rotate(force = TRUE) expect_identical(lg$appenders[[1]]$backups$ext, c("log.zip", "log", "log")) expect_identical(lg$appenders[[1]]$backups$sfx, as.character(1:3)) con <- unz(app$backups$path[[1]], filename = "test.log") on.exit(close(con), add = TRUE) expect_match(readLines(con), "test987") # pruning works expect_identical(nrow(app$prune(0)$backups), 0L) }) test_that("AppenderFileRotating: works with different backup_dir", { if (!is_zipcmd_available()) { skip("Test requires a workings system zip command") } skip_if_not_installed("rotor", "0.3.0") tf <- file.path(td, "test.log") bu_dir <- file.path(td, "backups") # backup_dir does not exist expect_error( app <- AppenderFileRotating$new(file = tf, backup_dir = bu_dir), class = "DirDoesNotExistError" ) # rotating to different dir works dir.create(bu_dir) app <- AppenderFileRotating$new( file = tf, backup_dir = bu_dir, size = 10 ) lg <- get_logger("test")$ set_propagate(FALSE)$ add_appender(app) on.exit({ app$prune(0) lg$config(NULL) unlink(c(tf, bu_dir), recursive = TRUE) }) # triggers rotation because resulting file will is bigger than 100 byte lg$info(paste(LETTERS, collapse = "-")) app$set_compression(TRUE) lg$info(paste(letters, collapse = "-")) # paths are as expected expect_equal(file.size(tf), 0) expect_equal(list.files(bu_dir), basename(app$backups$path)) expect_setequal(app$backups$ext, c("log.zip", "log")) expect_match(app$backups$path[[1]], "lgr/backups") expect_match(app$backups$path[[2]], "lgr/backups") # file contents are as expected con <- unz(app$backups$path[[1]], filename = "test.log") on.exit(close(con), add = TRUE) expect_match(readLines(con), "a-b-.*-y-z") expect_match(readLines(app$backups$path[[2]]), "A-B-.*-Y-Z") file.remove(app$backups$path) }) test_that("AppenderFileRotating: `size` argument works as expected", { skip_if_not_installed("rotor", "0.3.0") #setup tf <- file.path(td, "test.log") app <- AppenderFileRotating$new(file = tf)$set_size(-1) saveRDS(iris, app$file) on.exit({ unlink(tf) app$prune(0) }) # logic app$set_size("3 KiB") app$rotate() expect_identical(nrow(app$backups), 0L) app$set_size("0.5 KiB") app$rotate() expect_identical(nrow(app$backups), 1L) expect_gt(app$backups$size[[1]], 10) expect_equal(file.size(app$file), 0) }) # AppenderFileRotatingDate ---------------------------------------------------- test_that("AppenderFileRotatingDate: works as expected", { if (!is_zipcmd_available()) skip("Test requires a workings system zip command") skip_if_not_installed("rotor", "0.3.0") tf <- file.path(td, "test.log") app <- AppenderFileRotatingDate$new(file = tf, size = "1tb") lg <- lgr::get_logger("test")$ set_propagate(FALSE)$ set_appenders(app) on.exit({ lg$config(NULL) file.remove(tf) app$prune(0) }) # appender logs to file expect_identical(app, lg$appenders[[1]]) lg$fatal("test341") expect_true(file.exists(app$file)) expect_length(readLines(app$file), 1) expect_match(readLines(app$file), "test341") expect_identical(nrow(app$backups), 0L) # first rotate rotates a file with content app$set_size(-1)$set_age("1 day") app$rotate(now = as.Date("2019-01-01"), force = TRUE) expect_identical(nrow(app$backups), 1L) expect_gt(lg$appenders[[1]]$backups[1, ]$size, 0L) expect_length(readLines(app$file), 0) expect_match(app$backups$path[[1]], "2019-01-01") expect_match(readLines(app$backups$path[[1]]), "test341") # second rotate only has a file of size 0 to rotate app$rotate(now = "2019-01-02") expect_identical(nrow(app$backups), 2L) expect_equal(app$backups[1, ]$size, 0) expect_match(app$backups$path[[1]], "2019-01-02") # compression works app$set_age("10000 years") # prevent rotation on log lg$fatal("test938") app$set_age("1 day") lg$appenders[[1]]$set_compression(TRUE) lg$appenders[[1]]$rotate(now = as.Date("2019-01-03")) expect_identical(nrow(app$backups), 3L) expect_identical(app$backups$ext, c("log.zip", "log", "log")) expect_identical(lg$appenders[[1]]$backups$sfx, c("2019-01-03", "2019-01-02", "2019-01-01")) con <- unz(app$backups$path[[1]], filename = "test.log") on.exit(close(con), add = TRUE) expect_match(readLines(con), "test938") # pruning works expect_identical(nrow(app$prune(0)$backups), 0L) }) test_that("AppenderFileRotatingDate: works with different backup_dir", { skip_if_not(is_zipcmd_available(), "Test requires a workings system zip command") skip_if_not_installed("rotor", "0.3.0") tf <- file.path(td, "test.log") bu_dir <- file.path(td, "backups") # backup_dir does not exist expect_error( app <- AppenderFileRotatingDate$new(file = tf, backup_dir = bu_dir) ) # setup dir.create(bu_dir) app <- AppenderFileRotatingDate$new( file = tf, backup_dir = bu_dir, size = 100 ) lg <- get_logger("test")$set_propagate(FALSE) lg$add_appender(app) on.exit({ app$prune(0) unlink(c(tf, bu_dir), recursive = TRUE) lg$config(NULL) }) # rotating to different dir works app$set_age(-1)$set_size(-1) lg$info(paste(LETTERS)) app$set_compression(TRUE) lg$info(paste(LETTERS)) expect_equal(file.size(tf), 0) expect_equal(list.files(bu_dir), basename(app$backups$path)) expect_setequal(app$backups$ext, c("log.zip", "log")) file.remove(app$backups$path) }) test_that("AppenderFileRotatingDate: `size` and `age` arguments work as expected", { skip_if_not_installed("rotor", "0.3.0") #setup tf <- file.path(td, "test.log") app <- AppenderFileRotatingDate$new(file = tf)$set_age(-1) saveRDS(iris, app$file) on.exit({ unlink(tf) app$prune(0) }) # file size to small app$set_size("3 KiB") app$rotate() expect_identical(nrow(app$backups), 0L) app$set_size("0.5 KiB") app$rotate(now = "2999-01-01") expect_identical(nrow(app$backups), 1L) # age to small app$set_size(-1) app$set_age("1 day") app$rotate(now = "2999-01-01") expect_identical(nrow(app$backups), 1L) app$rotate(now = "2999-01-02") expect_identical(nrow(app$backups), 2L) }) # AppenderFileRotatingTime ---------------------------------------------------- test_that("AppenderFileRotatingTime: works as expected", { skip_if_not(is_zipcmd_available(), "Test requires a workings system zip command") skip_if_not_installed("rotor", "0.3.0") tf <- file.path(td, "test.log") app <- AppenderFileRotatingTime$new(file = tf) lg <- lgr::get_logger("test")$ set_propagate(FALSE)$ set_appenders(app) on.exit({ app$prune(0) lg$config(NULL) unlink(tf) }) lg$fatal("test") # first rotate roates a file with content app$set_size(-1) app$set_age(-1) app$rotate(now = "2999-01-03--12-01") expect_gt(app$backups[1, ]$size, 0) expect_match(app$backups[1, ]$path, "2999-01-03--12-01-00") # second rotate only has a file of size 0 to rotate lg$appenders[[1]]$rotate(now = "2999-01-03--12-02") expect_equal(lg$appenders[[1]]$backups[1, ]$size, 0) expect_match(app$backups[1, ]$path, "2999-01-03--12-02-00") # compression is possible lg$appenders[[1]]$set_compression(TRUE) lg$appenders[[1]]$rotate(now = "2999-01-03--12-03") expect_identical(lg$appenders[[1]]$backups$ext, c("log.zip", "log", "log")) expect_match(app$backups[1, ]$path, "2999-01-03--12-03-00.log.zip") # cleanup app$prune(0) expect_identical(nrow(app$backups), 0L) lg$config(NULL) }) test_that("AppenderFileRotatingTime: works with different backup_dir", { skip_if_not(is_zipcmd_available(), "Test requires a workings system zip command") skip_if_not_installed("rotor", "0.3.0") tf <- file.path(td, "test.log") bu_dir <- file.path(td, "backups") # backup_dir does not exist expect_error( app <- AppenderFileRotatingTime$new(file = tf, backup_dir = bu_dir) ) # setup dir.create(bu_dir) app <- AppenderFileRotatingTime$new( file = tf, backup_dir = bu_dir, size = 100, age = -1 ) lg <- get_logger("test")$ set_propagate(FALSE)$ add_appender(app) on.exit({ app$prune(0) unlink(c(tf, bu_dir), recursive = TRUE) lg$config(NULL) }) # rotating to different dir works lg$info(paste(LETTERS)) app$set_compression(TRUE) Sys.sleep(1) # for sort order of backups lg$info(paste(LETTERS)) expect_equal(file.size(tf), 0) expect_equal(rev(list.files(bu_dir)), basename(app$backups$path)) expect_equal(app$backups$ext, c("log.zip", "log")) file.remove(app$backups$path) }) test_that("AppenderFileRotatingTime: `size` and `age` arguments work as expected", { skip_if_not_installed("rotor", "0.3.0") #setup tf <- file.path(td, "test.log") app <- AppenderFileRotatingTime$new(file = tf)$set_age(-1) saveRDS(iris, app$file) on.exit({ unlink(tf) app$prune(0) }) # file size to small app$set_size(file.size(tf) + 2) app$rotate() expect_identical(nrow(app$backups), 0L) app$set_size(floor(file.size(tf) / 2)) app$rotate(now = "2999-01-01") expect_identical(nrow(app$backups), 1L) # age to small app$set_size(-1) app$set_age("1 day") app$rotate(now = "2999-01-01") expect_identical(nrow(app$backups), 1L) app$rotate(now = "2999-01-02") expect_identical(nrow(app$backups), 2L) }) # Issues ------------------------------------------------------------------ test_that("AppenderFileRotatingTime: `size` and `age` arguments work as expected #39", { skip_if_not_installed("rotor", "0.3.0") #setup tf <- file.path(td, "test.log") log_dir <- file.path(td, "backups") dir.create(log_dir) app <- AppenderFileRotatingTime$new( file = tf, layout = LayoutJson$new(), age = -1, size = "0.5 kb", max_backups = 5, backup_dir = log_dir, overwrite = FALSE, compression = FALSE, threshold = "info" ) on.exit({ unlink(tf) unlink(log_dir, recursive = TRUE) app$prune(0) }) lg <- get_logger("test_issue_39")$ set_propagate(FALSE)$ set_appenders(list(rotating = app)) # push log messages until rotation is triggered, should only take a few iterations for (i in 1:100){ lg$info("test") if (nrow(lg$appenders$rotating$backups) >= 1) break } expect_identical(nrow(lg$appenders$rotating$backups), 1L) expect_length(list.files(log_dir), 1) }) lgr/tests/testthat/test_LogEvent.R0000644000176200001440000000521614232440314016746 0ustar liggesuserscontext("LogEvent") test_that("LogEvent can have custom fields", { l <- Logger$new("l") expect_output(l$log(100, "blubb", user_agent = "007")) expect_identical(l$last_event$values$user_agent, "007") expect_output( l$info("blubb %s -", "blah", user_agent = "008"), "blubb blah" ) expect_identical(l$last_event$values$user_agent, "008") expect_identical(l$last_event$values$msg, "blubb blah -") }) test_that("LogEvents preserves field order", { l <- Logger$new("l", propagate = FALSE) l$fatal("test", c = "1", a = "2", b = "3") # Order depends on the internal implementation of environments I guess... # let's see if this will break one day. expect_identical(names(l$last_event)[1:3], c("c", "a", "b")) }) # as.data.frame/table/tibble --------------------------------------------- as_funs <- list( as.data.frame = as.data.frame.LogEvent, as.data.table = as.data.table.LogEvent, as_tibble = as_tibble.LogEvent ) # nm <- "as.data.table" for (nm in names(as_funs)){ l <- Logger$new("l", propagate = FALSE) test_that(paste0(nm, "() works as expected"), { l$fatal("test", df = iris, root_logger = lgr) res <- as_funs[[nm]](l$last_event) expect_identical(nrow(res), 1L) expect_true(is.data.frame(res$df[[1]])) expect_identical(res$root_logger[[1]], lgr) expect_identical(nrow(res$df[[1]]), 150L) }) test_that(paste0(nm, "() vectorizes over msg"), { l <- Logger$new("l", propagate = FALSE) l$fatal(c("test", "test2"), letters = letters, df = iris, root_logger = lgr) res <- as_funs[[nm]](l$last_event) expect_identical(nrow(res), 2L) expect_true(is.data.frame(res$df[[1]])) expect_true(is.data.frame(res$df[[2]])) expect_true(is.character(res$letters[[1]])) expect_true(is.character(res$letters[[2]])) expect_identical(nrow(res$df[[1]]), 150L) expect_identical(nrow(res$df[[2]]), 150L) }) } test_that("as_LogEvent.list and as_LogEvent.data.frame work", { ts <- Sys.time() l <- list( level = 100, msg = "blah", timestamp = ts, caller = NA, foo = "bar" ) Sys.sleep(1) expect_identical(as_LogEvent(l)$timestamp, l$timestamp) expect_s3_class(as_LogEvent(l), "LogEvent") expect_s3_class(as_LogEvent(as.data.frame(l)), "LogEvent") l <- list( level = 100, msg = "blah", `@timestamp` = ts, caller = NA, foo = "bar" ) expect_identical(as_LogEvent(l)[["timestamp"]], l[["@timestamp"]]) expect_s3_class(as_LogEvent(l), "LogEvent") expect_s3_class(as_LogEvent(as.data.frame(l)), "LogEvent") }) lgr/tests/testthat/test_lgr-package.R0000644000176200001440000000360014131760166017403 0ustar liggesuserscontext("lgr-package") ev_suspend_logging <- Sys.getenv("LGR_SUSPEND_LOGGING") ev_default_config <- Sys.getenv("LGR_DEFAULT_CONFIG") ev_default_threshold <- Sys.getenv("LGR_DEFAULT_THRESHOLD") teardown({ Sys.setenv( LGR_SUSPEND_LOGGING = ev_suspend_logging, LGR_DEFAULT_CONFIG = ev_default_config, LGR_DEFAULT_THRESHOLD = ev_default_threshold ) }) test_that("get_envar_suspend_logging works as expected", { Sys.setenv("LGR_SUSPEND_LOGGING" = TRUE) expect_true(get_envar_suspend_logging()) Sys.setenv("LGR_SUSPEND_LOGGING" = FALSE) expect_false(get_envar_suspend_logging()) Sys.setenv("LGR_SUSPEND_LOGGING" = "foo") expect_false(expect_warning(get_envar_suspend_logging())) Sys.setenv("LGR_SUSPEND_LOGGING" = "") expect_false(get_envar_suspend_logging()) }) test_that("get_envar_default_config works as expected", { Sys.setenv("LGR_DEFAULT_CONFIG" = system.file("configs/recommended.yaml", package = "lgr")) expect_s3_class(get_envar_default_config(), "logger_config") Sys.setenv("LGR_DEFAULT_CONFIG" = FALSE) expect_warning(expect_null(get_envar_default_config())) Sys.setenv("LGR_DEFAULT_CONFIG" = "") expect_null(get_envar_default_config()) }) test_that("get_envar_default_threshold works as expected", { Sys.setenv("LGR_DEFAULT_THRESHOLD" = "100") expect_identical(get_envar_default_threshold(), 100L) Sys.setenv("LGR_DEFAULT_THRESHOLD" = "iNfO") expect_identical(get_envar_default_threshold(), 400L) Sys.setenv("LGR_DEFAULT_THRESHOLD" = "-100") expect_warning(expect_equal(get_envar_default_threshold(), 400L)) # default Sys.setenv("LGR_DEFAULT_THRESHOLD" = FALSE) expect_warning(expect_equal(get_envar_default_threshold(), 400L)) # default Sys.setenv("LGR_DEFAULT_THRESHOLD" = "") expect_equal(get_envar_default_threshold(), 400L) # default }) lgr/tests/testthat/testdata/0000755000176200001440000000000014131760166015656 5ustar liggesuserslgr/tests/testthat/testdata/lg_full.json0000644000176200001440000000105214131760166020173 0ustar liggesusers{ "Logger": { "threshold": "info", "propagate": false, "appenders": { "AppenderConsole": { "threshold": "error", "layout": { "LayoutFormat": { "fmt": "%L %t - %m" } } }, "AppenderBuffer": { "appenders": { "AppenderConsole": { "threshold": 123, "layout": { "LayoutFormat": { "fmt": "%L %t - %m" } } } } } } } } lgr/tests/testthat/testdata/lg_simple.yaml0000644000176200001440000000025714131760166020521 0ustar liggesusersLogger: threshold: info propagate: false appenders: AppenderConsole: threshold: error layout: LayoutFormat: fmt: "%L %t - %m" lgr/tests/testthat/testdata/lg_simple.json0000644000176200001440000000024014131760166020520 0ustar liggesusers{"Logger":{"threshold":["info"],"propagate":[false],"appenders":{"AppenderConsole":{"threshold":["error"],"layout":{"LayoutFormat":{"fmt":["%L %t - %m"]}}}}}} lgr/tests/testthat/testdata/lg_full.yaml0000644000176200001440000000053114131760166020165 0ustar liggesusersLogger: threshold: info propagate: false appenders: AppenderConsole: threshold: error layout: LayoutFormat: fmt: "%L %t - %m" AppenderBuffer: appenders: AppenderConsole: threshold: 123 layout: LayoutFormat: fmt: "%L %t - %m" lgr/tests/testthat.R0000644000176200001440000000006614131760166014172 0ustar liggesuserslibrary(testthat) library(lgr) test_check("lgr") lgr/MD50000644000176200001440000001634115137120762011357 0ustar liggesuserscccadde2a3d0504500b7cac06010d117 *DESCRIPTION ef947e0640b50df3f445463e23efc20c *LICENSE 09ced783a6d57ef873f411d5c4d8b054 *NAMESPACE 7d5fda6e0f7ef4835dbcc13f48ba7107 *NEWS.md 81d6cbf3f30483719ae80d3b229ed15f *R/Appender.R ca8c0d0483dbc0d257224a4c7f7de6ed *R/Filter.R f6d93126b73f113fdb7931903984ca01 *R/Filterable.R 2bfe4fa9571f3830d09dc20bdb1d1356 *R/Layout.R 4eed86528064593ec96bc1542ebf4635 *R/LayoutJson.R 3e6f778efb9dcb8a362384682fb2ef49 *R/LogEvent.R a043533f7b7413fd761bace23a4eee27 *R/Logger.R e58b4a6211d0cf5f02551d4e5a2ae440 *R/basic_config.R 3ab6939dc66095ed021693a86776f8df *R/default_functions.R dc75cc4b3766dd49486f05f630bcbbc8 *R/event_list.R d016e51ffa70f28fa3142e043d80938a *R/get_logger.R 5d7b39079428ddb23dcdcd2604e49b34 *R/lgr-package.R a87d19984da698f7c0097c6460dd2059 *R/log_levels.R 9a1c69048153c956a3c8f2ee55d33a4b *R/logger_config.R 6292d7d465522de7e5ad3e30fc3e96c4 *R/logger_index.R 5af23ae6a8d5d92576e5d188c90088a7 *R/logger_tree.R 556426b629e7be727091ffc78b3f2617 *R/read_json_lines.R cdbe0dba93bd7e1f8e8cdf89a59481a8 *R/simple_logging.R 79522ff4a9c149f209ff0bfc23bde218 *R/string_repr.R e559f96c9543f6407f4c8e68197e4f3b *R/use_logger.R fb773b67501096555e676ba9cd9ebf60 *R/utils-formatting.R a00f86896c5e6b0e57752b9963bb608a *R/utils-logging.R 55ff87e303f13b6ab3f1c0749834c367 *R/utils-rd.R 672d48864846f350b9cfcde606d79b7d *R/utils-rotor.R 71d2f20011aa0edc0884819954300845 *R/utils-sfmisc.R 47cfb08925cb517873c91b318c6f695d *R/utils.R 9c5a7f58929da727f01bdc0050ecdad4 *README.md 99dd77ad3e929c886d96efe4e2eb4136 *build/vignette.rds a163ffb5a48b9c38e865b830c9c2d7cb *inst/WORDLIST b5d8202710f6ed20508b07c0b1c70267 *inst/benchmarks/benchmarks.Rmd 03a03ca18d826283a1a7af32d05b109d *inst/configs/minimal.yaml ff42a08c0083deb98dd05016e6a92aaa *inst/configs/recommended.yaml 5e1a43b22081318d266ca5d296d5b373 *inst/doc/lgr.R 94f440a1948200dc25fd8d6c85123f10 *inst/doc/lgr.Rmd ecf30bc4aaa4176b3662066455a79414 *inst/doc/lgr.html 738fc0370f3b8133542cc0b8899ce773 *inst/draft_vignettes/lgr-configs.Rmd 8bb2d5e49791074baee654baea977729 *inst/draft_vignettes/lgr-faq.Rmd aed1e49532dc6fe8d50cc7b3759223bf *inst/logger_comparison/logger_comparison.Rmd 25a06640f1a284f90eca6ca1ba6996ab *inst/logger_comparison/logger_comparison.html 3e079b910bc1b8d727a8cb194020eb71 *man/Appender.Rd 651d06acfdc48de99b6e5afc714b3973 *man/AppenderBuffer.Rd a5cd0997f22a5c69afcb9ab685895f35 *man/AppenderConsole.Rd a25857fc6b7d2492b0f775f7fd05ca14 *man/AppenderFile.Rd 80f35ce86da3d80e21642be07f6c45f8 *man/AppenderFileRotating.Rd 41b57994daa3b9bf082f34a17e7f97d7 *man/AppenderFileRotatingDate.Rd 959ca1505356a187d8b70b6a77066f85 *man/AppenderFileRotatingTime.Rd 14fb9cca413e23505924ee2fc9d4731f *man/AppenderMemory.Rd 833ff7952ea197e9f6f00249d44f9a39 *man/AppenderTable.Rd 98d4d945264ae78e96da378c3be08a0a *man/CannotInitializeAbstractClassError.Rd 909e6f4fdb2b49374c1f772d97996cf9 *man/EventFilter.Rd e9c64cafe55fb3a0e5891e6bcd674717 *man/FilterForceLevel.Rd cc4141c963ca6b4d840d4252183a35e3 *man/FilterInject.Rd e6dd06b77309776ff4bb0726146ed414 *man/Filterable.Rd f8c27559ae7d51dca0f9997b1637060a *man/Layout.Rd 06f8ecc2a9cf6b6ac6252d1be071d1a2 *man/LayoutFormat.Rd 19f9d274b5fff65ddb848e39770f18ec *man/LayoutGlue.Rd e68be7b70bbd3f6abe1526850ff5221f *man/LayoutJson.Rd aa61021905ffe6e2ad7644101d95d6e2 *man/LogEvent.Rd 58005a41e442220672027a5b80443ed4 *man/Logger.Rd b3326a5d891eee24ec790bf64dfc265d *man/LoggerGlue.Rd 955665289bf821616989899a5866b9a9 *man/as.data.frame.LogEvent.Rd 0ffc76a75d1465791d2f0b5bd1b22ccc *man/as_LogEvent.Rd 9bcaca9bd0578ccdefac8830271a44a0 *man/basic_config.Rd 724f8c169cb7b45c4e55e47e145e4ac8 *man/colorize_levels.Rd 4d0eb99020d1ba2476269d85196c760d *man/default_exception_handler.Rd 3fee6199a4ed33c55f0221e4b1db72e6 *man/event_list.Rd cf3e157f92d29657c2f04a2ca0394518 *man/figures/yog-logo-plain.svg f2d46bfcf911afbd270299fd233d5dfe *man/figures/yog-logo.svg 39de80981f480e27dcecb0c165c96097 *man/get_log_levels.Rd 0688f4b72377208ecfbcac65d28ca081 *man/get_logger.Rd 0f07dc6a35eec48345145acfb53e33fb *man/is_filter.Rd 552750cb71295472ee07fa64032893ea *man/label_levels.Rd fd92dd1ec2353e881f5a9a4740e423ed *man/lgr-package.Rd 7bf301e445a6dde8d92d44ae543ab877 *man/logger_config.Rd 4fe767a973dcd5876380ec2ba5e5baff *man/logger_index.Rd dfb56d87c5fa85255fcef6be52bf1660 *man/logger_tree.Rd 559523ef17a075830c2ffd55ae6dc5ed *man/pad_right.Rd 2f4dc9067e63d4e55d5375e37fd3b1b0 *man/print.Appender.Rd 341607058c2849690b2f2c6ea2e3d373 *man/print.LogEvent.Rd 1be28ece3165983943fcc4f4d7a20952 *man/print.Logger.Rd 71dd458c0f2ee1d996f0b6d058a7c1ea *man/print.logger_tree.Rd 097f28b1220598f2a18e41cfbfeed285 *man/read_json_lines.Rd 0cbad537a079718fd88bb6446641eefe *man/simple_logging.Rd 650d98e4ad21524c85d1f33b478b06de *man/standardize_threshold.Rd dd1ced8b79186210f9643d47f1b4eee8 *man/string_repr.Rd 0ed704471235a89b203cc4c37776815c *man/suspend_logging.Rd 12f02e69de11dd8c6dc84e8cc9ff0109 *man/system_infos.Rd 2c34bcf304ea303b2b43b90c6e610be6 *man/toString.LogEvent.Rd a0a49931979ffdbd29acceef04cf33cd *man/use_logger.Rd 702bc5268e77f5ed50cc55f97c02ac97 *man/with_log_level.Rd 9d999f42e8e43a63855cffe0e6d0ed1a *tests/testthat.R 6d36f6c903de9134de0c441456788852 *tests/testthat/manual_tests/test_AppenderDigest.R 6ceaa23b0032eb5e0594b06c8245aa4d *tests/testthat/test_Appender.R 165026ae476da1142dc5617f9055c0db *tests/testthat/test_AppenderFileRotating.R a236a5589d33409c20fd6ea3531468cb *tests/testthat/test_Filter.R 5084a3ee6f308fa12fdaefc0ba507004 *tests/testthat/test_Filterable.R ff04271ab43b76d82cc0a97c16d1eab1 *tests/testthat/test_Layout.R 231d06542ecc4af21423c0a2c6e14067 *tests/testthat/test_LayoutJson.R 20068bae2291b33fcc923dd40051a5c3 *tests/testthat/test_LogEvent.R 04a2b0dbf11a4d79ffedf24c2aaf4e54 *tests/testthat/test_Logger.R a55f81b3f9dc8dc0ec0a68c1f74b2572 *tests/testthat/test_default_functions.R 94d19c2ecafab2dcc6bd334c7ef3903e *tests/testthat/test_event_list.R cb6efef5f45e67393e52e0248f7bdfea *tests/testthat/test_get_logger.R 758651a9fd174e2696fe8911cbf599f3 *tests/testthat/test_lgr-package.R 8ee557be99875d4f8d3d467ddf2d1fa5 *tests/testthat/test_log_levels.R f34c0108a38d4dd958e8fe09316a6289 *tests/testthat/test_logger_config.R 124b2413481b460d54cbf3f07bc3ffad *tests/testthat/test_logger_index.R 15afe4e4f889e6430b8a60db793dee61 *tests/testthat/test_logger_tree.R 9b03023f4a7f37620710f23f36264259 *tests/testthat/test_parallel.R c89991949e376aa34bad8f3128b1d5e5 *tests/testthat/test_print_Appender.R 1ab3abc901333cd99d27af55d148d4ec *tests/testthat/test_print_LogEvent.R f7682442b4356c9f3036c53bd46eaaf2 *tests/testthat/test_print_Logger.R cae939b82a2659e77843c71acc65052a *tests/testthat/test_read_json_log.R 47f06ab98af7a5dba50a1ed34d982a77 *tests/testthat/test_simple_logging.R 11e767f0c1749cba2bdcb6e190d2c726 *tests/testthat/test_string_repr.R ed654525bfbc10a761ef57d9d1244207 *tests/testthat/test_utils-logging.R 27ba01eb7365161a557ac90d9b6b7186 *tests/testthat/test_utils-rd.R 819f628ce68e447c33fb8e353b037380 *tests/testthat/test_utils.R ed8d85e29afb88aa140c91d03ea7e333 *tests/testthat/testdata/lg_full.json fbf0f9cbcfe4aa9199a5fcd2f00f41a1 *tests/testthat/testdata/lg_full.yaml 7df4edbf977683216f7918160285341d *tests/testthat/testdata/lg_simple.json 1e2380dc3a572c94e7ef65720f9b364f *tests/testthat/testdata/lg_simple.yaml 94f440a1948200dc25fd8d6c85123f10 *vignettes/lgr.Rmd 5778346479651348196de711ddb92cc1 *vignettes/log_flow.svg lgr/R/0000755000176200001440000000000015137111075011240 5ustar liggesuserslgr/R/log_levels.R0000644000176200001440000002502215035130575013522 0ustar liggesusersas_log_levels <- function(x){ assert(is_integerish(x) && identical(length(names(x)), length(x))) assert(all_are_distinct(x)) assert(all_are_distinct(names(x))) assert( !anyNA(x) & all (x > 0), "Log levels must be positive integers. The log levels `0` (off) and `NA` ", "(all) are reserved." ) x <- setNames(as.integer(x), names(x)) structure(sort(x), class = c("log_levels", "integer")) } DEFAULT_LOG_LEVELS <- c("fatal", "error", "warn", "info", "debug", "trace") # manage options ---------------------------------------------------------- #' Manage Log Levels #' #' Display, add and remove character labels for log levels. #' #' @return a named `character` vector of the globally available log levels #' (`add_log_levels()` and `remove_log_levels()` return invisibly). #' @export #' #' #' @section Default Log Levels: #' #' lgr comes with the following predefined log levels that are identical to #' the log levels of log4j. #' #'\tabular{rll}{ #' Level \tab Name \tab Description \cr #' `0` \tab off \tab A log level of 0/off tells a Logger or Appender to suspend all logging \cr #' `100` \tab fatal \tab Critical error that leads to program abort. Should always indicate a `stop()` or similar \cr #' `200` \tab error \tab A severe error that does not trigger program abort\cr #' `300` \tab warn \tab A potentially harmful situation, like `warning()`\cr #' `400` \tab info \tab An informational message on the progress of the application\cr #' `500` \tab debug \tab Finer grained informational messages that are mostly useful for debugging\cr #' `600` \tab trace \tab An even finer grained message than debug\cr #' `NA` \tab all \tab A log level of NA/all tells a Logger or Appender to process all log events #' } #' @aliases log_levels log_level #' @examples #' get_log_levels() #' add_log_levels(c(errorish = 250)) #' get_log_levels() #' remove_log_levels("errorish") #' get_log_levels() get_log_levels <- function(){ getOption("lgr.log_levels") } #' @param levels a named `character` vector (see examples) #' @rdname get_log_levels #' @export add_log_levels <- function( levels ){ levels_cur <- getOption("lgr.log_levels") assert( !is.null(levels_cur), "lgr.log_levels option is not set. something is very wrong with lgr, please file a bug report" ) assert( is_integerish(levels), "`levels` must be a named integer vector" ) assert(length(names(levels)) && all_are_distinct(names(levels))) levels_new <- setNames(as.integer(levels), names(levels)) levels_upd <- levels_new[levels_new %in% levels_cur] if (length(levels_upd)){ message( "Replacing existing log levels '", fmt_log_levels(levels_new[levels_new %in% levels_cur]), "' with '", fmt_log_levels(levels_upd), "'" ) } res <- as_log_levels(named_union(levels_cur, levels_new)) options(lgr.log_levels = res) invisible(get_log_levels()) } #' union for named vectors. names of `y` override names of `x` #' @noRd named_union <- function(x, y){ assert(identical(length(names(x)), length(x))) assert(identical(length(names(y)), length(y))) r <- union(x, y) names(r)[r %in% x] <- names(x[x %in% r]) names(r)[r %in% y] <- names(y[y %in% r]) r } #' @param level_names a `character` vector of the names of the levels to remove #' @rdname get_log_levels #' @export remove_log_levels <- function( level_names ){ assert(is.character(level_names)) assert( !any(DEFAULT_LOG_LEVELS %in% level_names), "Cannot remove default log levels" ) current_lvls <- getOption("lgr.log_levels") assert(all(level_names %in% names(current_lvls))) res <- current_lvls[!names(current_lvls) %in% level_names] res <- as_log_levels(res) options(lgr.log_levels = res) invisible(get_log_levels()) } # format ------------------------------------------------------------------ fmt_log_levels <- function( x ){ paste0(names(sort(x)), " (", sort(x), ")", collapse = ", ") } #' Colorize Levels #' #' @param x `numeric` or `character` levels to be colored. Unlike in many other #' functions in lgr, `character` levels are *not* case sensitive in this #' function and leading/trailing whitespace is ignored to make it more #' comfortable to use `colorize_levels()` inside formatting functions. #' @param transform a `function` to transform `x` (for example `toupper()`) #' @inheritParams format.LogEvent #' #' @return a `character` vector wit color ANSI codes #' @family formatting utils #' @export #' #' @examples #' cat(colorize_levels(c(100, 200))) #' cat(colorize_levels(c("trace", "warn ", "DEBUG"))) #' cat(colorize_levels(c("trace", "warn ", "DEBUG"), transform = function(x) strtrim(x, 1) )) colorize_levels <- function( x, colors = getOption("lgr.colors", NULL), transform = identity ){ if (length(x) && length(colors)){ if (is.character(x)) dd <- standardize_log_levels(trimws(tolower(x))) else dd <- standardize_log_levels(x) cn <- standardize_log_levels( tolower(names(colors))) for (i in seq_along(colors)){ sel <- dd == cn[[i]] x[sel] <- colors[[i]](transform(x[sel])) } } x } # standardize ------------------------------------------------------------ #' Standardize User-Input Log Levels to Their Integer Representation #' #' These are helper functions for verifying log levels and converting them from #' their character to their integer representations. This is primarily useful #' if you want to build your own [Loggers], [Appenders] or [Layouts] and need #' to handle log levels in a way that is consistent with \pkg{lgr} . #' #' @param x a `character` or `integer` scalar, or vector for #' standardize_log_levels #' @param log_levels named `integer` vector of valid log levels #' #' @return An unnamed `integer` vector #' @family docs relevant for extending lgr #' @export #' @examples #' #' standardize_threshold("info") #' standardize_threshold("all") #' is_threshold("all") #' is_threshold("foobar") #' #' standardize_log_level("info") #' # all is a valid threshold, but not a valid log level #' try(is.na(standardize_log_level("all"))) #' is_log_level("all") #' #' # standardized_log_level intentionally only works with scalars, because many #' # functions require scalar log level inputs #' try(standardize_log_level(c("info", "fatal"))) #' #' # You can still use standardize_log_levels() (plural) to work with vectors #' standardize_log_levels(c("info", "fatal")) standardize_threshold <- function( x, log_levels = c(getOption("lgr.log_levels"), c("all" = NA_integer_, "off" = 0L)) ){ assert(is_scalar(x), "A threshold must be a scalar (a vector of length 1)" ) if (is.na(x)){ return(NA_integer_) } if (is_integerish(x) && x >= 0){ return(as.integer(x)) } if (is.character(x) && (x %in% names(log_levels)) ){ return(unname(log_levels[match(x, names(log_levels))])) } stop(error_msg_log_levels(deparse(substitute(x)), log_levels)) } #' @rdname standardize_threshold #' @export is_threshold <- function(x){ tryCatch( {standardize_threshold(x); TRUE}, error = function(...) FALSE ) } #' @rdname standardize_threshold #' @export standardize_log_level <- function( x, log_levels = getOption("lgr.log_levels") ){ assert(is_scalar(x), "'", deparse(substitute(x)), "' must be a scalar log level") if (is_integerish(x) && x > 0){ return(as.integer(x)) } if (is.character(x) && (x %in% names(log_levels)) ){ return(unname(log_levels[match(x, names(log_levels))])) } stop(error_msg_log_levels(deparse(substitute(x)), log_levels)) } #' @rdname standardize_threshold #' @export is_log_level <- function(x){ tryCatch( {standardize_log_level(x); TRUE}, error = function(...) FALSE ) } #' @rdname standardize_threshold #' @export standardize_log_levels <- function( x, log_levels = getOption("lgr.log_levels") ){ if (is_integerish(x) && all(x > 0)){ return(as.integer(x)) } if (is.character(x) && all(x %in% names(log_levels)) ){ return(unname(log_levels[match(x, names(log_levels))])) } stop(error_msg_log_levels(deparse(substitute(x)), log_levels)) } #' @rdname standardize_threshold #' @export is_log_levels <- function(x){ tryCatch( {standardize_log_levels(x); TRUE}, error = function(...) FALSE ) } error_msg_log_levels <- function(varname, log_levels){ ll_text <- paste(sprintf("%s (%s)", names(log_levels), log_levels), collapse = ", ") paste0( "'", varname, "' must either the numeric or character representation ", "of one of the following log levels: ", ll_text ) } # label/unlabel ----------------------------------------------------------- #' Label/Unlabel Log Levels #' #' @param levels an `integer` vector of log levels #' @param labels a `character` vector of log level labels. Please note that #' log levels are lowercase by default, even if many appenders print them #' in uppercase. #' @inheritParams standardize_threshold #' #' @return a `character` vector for `label_levels()` and an integer vector for #' `unlabel_levels` #' #' @seealso [get_log_levels()] #' @family formatting utils #' @export #' @examples #' x <- label_levels(c(seq(0, 600, by = 100), NA)) #' print(x) #' unlabel_levels(x) label_levels <- function( levels, log_levels = getOption("lgr.log_levels") ){ if (!is.numeric(levels)) stop("Expected numeric 'levels'") res <- names(log_levels)[match(levels, log_levels)] res[is.na(levels)] <- "all" res[levels == 0] <- "off" names(res) <- levels if (anyNA(res)) warning("Some `levels` were not valid numeric log levels, coercing to `NA`") res } #' @rdname label_levels #' @export unlabel_levels <- function( labels, log_levels = getOption("lgr.log_levels") ){ if (!is.character(labels)) stop("Expected character 'labels'") res <- log_levels[match(labels, names(log_levels))] if (!is.null(names(labels)) && anyNA(res)){ res[is.na(res)] <- as.integer(names(labels)[is.na(res)]) } if (anyNA(labels) || any(!labels %in% c(names(log_levels), "all", "off"))) warning( "Some `labels` were not valid character log levels. Please", "consider adding them to the global log levels with `?add_log_levels`" ) res[labels == "off"] <- 0L names(res) <- labels res } lgr/R/utils-formatting.R0000644000176200001440000000260414131760166014701 0ustar liggesusers#' Pad Character Vectors #' #' @param x a `character` vector #' @param width `integer` scalar. target string width #' @param pad `character` scalar. the symbol to pad with #' #' @export pad_right #' @name pad_right #' #' @examples #' pad_left("foo", 5) #' pad_right("foo", 5, ".") #' pad_left(c("foo", "foooooo"), pad = ".") NULL #' @export pad_left #' @rdname pad_right #' @name pad_left NULL # internal ---------------------------------------------------------------- fmt_function_signature <- function(x){ paste0("function(", paste(names(formals(x)), collapse = ", "), ")") } fmt_threshold <- function( x, type = "both", log_levels = getOption("lgr.log_levels") ){ assert(all(is.na(x)) || is_integerish(x[!is.na(x)]) || is.character(x)) log_levels = c("off" = 0L, log_levels) if (is.character(x)){ assert(all(x %in% names(log_levels))) x <- unlabel_levels(x, log_levels = log_levels) } impl <- function(.x){ assert((length(.x) == 1L) && (is.na(.x)) || is_integerish(.x)) if (.x %in% log_levels){ .r <- log_levels[which(log_levels == .x)] } else if (is.na(.x)){ .r <- c("all" = NA) } else { return(format(.x)) } if (identical(type, "character")){ return(names(.r)) } paste0(names(.r), " (", .r, ")") } vapply(x, impl, character(1)) } lgr/R/lgr-package.R0000644000176200001440000001722414131760166013552 0ustar liggesusers#' A Fully Featured Logging Framework for R #' #' For details please refer to `vignette("lgr", package = "lgr")`. #' #' @section Options: #' #' You can also set these options in your `.Rprofile` to make them permanent. #' Some options can also be set via environment variables (The environment #' variables are only used if the option is not set manually from R). #' #' \describe{ #' \item{`lgr.colors`}{a `list` of `functions` used for coloring the log #' levels in console output. Usually these will be functions from the #' package **crayon**} #' \item{`lgr.log_levels`}{A named `integer` vector of log levels that are #' known to lgr for labeling, setting thresholds, etc... . Instead of #' modifying this option manually use [add_log_levels()] and #' [remove_log_levels()]} #' \item{`lgr.default_threshold`}{ #' `character` or `integer` scalar. The minimum [log level][log_levels] that #' should be processed by the root logger. Defaults to `400` (`"info"`), #' or to the value of the environment variable `LGR_DEFAULT_THRESHOLD` #' if it is set. This option overrides the threshold specified in #' `lgr.default_config` if both are set. #' } #' \item{`lgr.default_config`}{ #' Default configuration for the root logger. Can either be a special list #' object, a path to a YAML file, or a character scalar containing YAML #' code. See [logger_config] for details. Defaults to the value of the #' environment variable `LGR_DEFAULT_CONFIG` if it is set. #' } #' \item{`lgr.suspend_logging`}{`TRUE` or `FALSE`. Suspend all logging for #' all loggers. Defaults to the `TRUE` if the environment variable #' `LGR_SUSPEND_LOGGING` is set to `"TRUE"`. Instead of modifying this #' option manually use [suspend_logging()] and [unsuspend_logging()]} #' \item{`lgr.user`}{a `character` scalar. The default username for #' `lgr::get_user()`. #' } #' } #' #' @keywords internal #' @importFrom stats setNames "_PACKAGE" #' @export lgr NULL .onLoad <- function(...){ # dyn s3 methods ------------------------------------------------------ dyn_register_s3_method("data.table", "as.data.table", "LogEvent") dyn_register_s3_method("data.table", "as.data.table", "event_list") dyn_register_s3_method("tibble", "as_tibble", "LogEvent") # options ------------------------------------------------------------- op <- options() op.this <- list() # +- colors -------------------------------------------------------------- if (requireNamespace("crayon", quietly = TRUE) && crayon::has_color()){ style_error <- crayon::make_style("#BB3333", colors = 256) style_fatal <- function(...) style_error(crayon::bold(...)) style_warning <- crayon::make_style("#EEBB50", colors = 256) style_subtle <- crayon::make_style(grDevices::grey(0.5), grey = TRUE) style_accent <- crayon::make_style("#ca2c92", colors = 256) col_nchar <- crayon::col_nchar op.this[["lgr.colors"]] <- list( "fatal" = style_fatal, "error" = style_error, "warn" = style_warning, "debug" = style_subtle, "trace" = style_subtle ) } else { style_fatal <- function(...) paste(...) style_error <- style_fatal style_warning <- style_fatal style_subtle <- style_fatal style_accent <- style_fatal col_nchar <- function(...) nchar(...) op.this[["lgr.colors"]] = list() } # make color functions available inside the package assign("style_fatal", style_fatal, envir = parent.env(environment())) assign("style_error", style_error, envir = parent.env(environment())) assign("style_warning", style_warning, envir = parent.env(environment())) assign("style_subtle", style_subtle, envir = parent.env(environment())) assign("style_accent", style_accent, envir = parent.env(environment())) assign("col_nchar", col_nchar, envir = parent.env(environment())) # +- log_levels -------------------------------------------------------------- op.this[["lgr.log_levels"]] <- c( "fatal" = 100L, "error" = 200L, "warn" = 300L, "info" = 400L, "debug" = 500L, "trace" = 600L ) # +- options from envars ------------------------------------------------- op.this[["lgr.suspend_logging"]] <- get_envar_suspend_logging() op.this[["lgr.default_config"]] <- get_envar_default_config() op.this[["lgr.default_threshold"]] <- get_envar_default_threshold(fallback = 400) # +- set options (if they were not set manually before) ------------------ toset <- !(names(op.this) %in% names(op)) if(any(toset)) options(op.this[toset]) # root looger ------------------------------------------------------------- assign( "root", LoggerRoot$new("root", threshold = NA), # threshold cannot be null envir = loggers ) assign( "lgr", get_logger("root"), envir = parent.env(environment()) ) # config default_config <- getOption("lgr.default_config") if (is.null(default_config)){ basic_config() } else { tryCatch( lgr$config(default_config), error = function(e) { warning( "The option 'lgr.default_config' is set to an invalid logger_config: ", e$message, call. = FALSE ) } ) } lgr$set_threshold(getOption("lgr.default_threshold", 400L)) } # get environment variables ----------------------------------------------- get_envar_suspend_logging <- function(){ envar_suspend_logging <- toupper(Sys.getenv("LGR_SUSPEND_LOGGING")) if (identical(envar_suspend_logging, "TRUE")){ TRUE } else if ( identical(envar_suspend_logging, "FALSE") || is_blank(envar_suspend_logging) ){ FALSE } else { warning( "Environment variable 'LGR_SUSPEND_LOGGING' is set to '", envar_suspend_logging, "' but must be either 'TRUE' or 'FALSE'", call. = FALSE) FALSE } } get_envar_default_config <- function(){ envar_default_config <- Sys.getenv("LGR_DEFAULT_CONFIG") if (is_blank(envar_default_config)){ NULL } else { if (!is_scalar_character(envar_default_config)){ warning("Environment variable 'LGR_DEFAULT_CONFIG' must be a path to a YAML or JSON config file", call. = FALSE) NULL } else { tryCatch( as_logger_config(envar_default_config), error = function(e){ warning( "Environment variable 'LGR_DEFAULT_CONFIG' is set but '", envar_default_config, "' is not a path to a valid YAML file", call. = FALSE ) NULL } ) } } } get_envar_default_threshold <- function(fallback = 400){ envvar_default_threshold <- tolower(Sys.getenv("LGR_DEFAULT_THRESHOLD")) if (!is_scalar(envvar_default_threshold)){ warning( "Environment variable 'LGR_DEFAULT_THRESHOLD' bust be a single", "numeric or character value.", call. = FALSE ) return(fallback) } int_threshold <- suppressWarnings(as.integer(envvar_default_threshold)) if (!is.na(int_threshold)) envvar_default_threshold <- int_threshold if (is_blank(envvar_default_threshold)){ fallback } else { tryCatch( standardize_threshold(envvar_default_threshold), error = function(e){ warning( "Environment variable 'LGR_DEFAULT_THRESHOLD' is set but '", envvar_default_threshold, "' is not a valid threshold", call. = FALSE ) fallback } ) } } lgr/R/Logger.R0000644000176200001440000011203615035130575012610 0ustar liggesusers#' Loggers #' #' A Logger produces a [LogEvent] that contains a log message along with #' metadata (timestamp, calling function, ...) and dispatches it to one or #' more [Appenders] which are responsible for the output (console, file, ...) #' of the event. **lgr** comes with a single pre-configured Logger called the #' `root Logger` that can be accessed via `lgr$<...>`. Instantiation of new #' Loggers is done with [get_logger()]. It is advisable to instantiate a #' separate Logger with a descriptive name for each package/script in which #' you use \pkg{lgr}. #' #' @name Logger #' @aliases Loggers #' @include Filterable.R #' @include log_levels.R #' @seealso [glue](https://glue.tidyverse.org/) #' @examples #' # lgr::lgr is the root logger that is always available #' lgr$info("Today is a good day") #' lgr$fatal("This is a serious error") #' #' # Loggers use sprintf() for string formatting by default #' lgr$info("Today is %s", Sys.Date() ) #' #' # If no unnamed `...` are present, msg is not passed through sprintf() #' lgr$fatal("100% bad") # so this works #' lgr$fatal("%s%% bad", 100) # if you use unnamed arguments, you must escape % #' #' # You can create new loggers with get_logger() #' tf <- tempfile() #' lg <- get_logger("mylogger")$set_appenders(AppenderFile$new(tf)) #' #' # The new logger passes the log message on to the appenders of its parent #' # logger, which is by default the root logger. This is why the following #' # writes not only the file 'tf', but also to the console. #' lg$fatal("blubb") #' readLines(tf) #' #' # This logger's print() method depicts this relationship. #' child <- get_logger("lg/child") #' print(child) #' print(child$name) #' #' # use formatting strings and custom fields #' tf2 <- tempfile() #' lg$add_appender(AppenderFile$new(tf2, layout = LayoutJson$new())) #' lg$info("Not all %s support custom fields", "appenders", type = "test") #' cat(readLines(tf), sep = "\n") #' cat(readLines(tf2), sep = "\n") #' #' # cleanup #' unlink(c(tf, tf2)) #' lg$config(NULL) # reset logger config #' #' # LoggerGlue #' # You can also create a new logger that uses the awesome glue library for #' # string formatting instead of sprintf #' #' if (requireNamespace("glue")){ #' #' lg <- get_logger_glue("glue") #' lg$fatal("blah ", "fizz is set to: {fizz}", foo = "bar", fizz = "buzz") #' # prevent creation of custom fields with prefixing a dot #' lg$fatal("blah ", "fizz is set to: {.fizz}", foo = "bar", .fizz = "buzz") #' #' #' # completely reset 'glue' to an unconfigured vanilla Logger #' get_logger("glue", reset = TRUE) #' #' } #' #' #' # Configuring a Logger #' lg <- get_logger("test") #' lg$config(NULL) # resets logger to unconfigured state #' #' # With setters #' lg$ #' set_threshold("error")$ #' set_propagate(FALSE)$ #' set_appenders(AppenderConsole$new(threshold = "info")) #' #' lg$config(NULL) #' #' # With a list #' lg$config(list( #' threshold = "error", #' propagate = FALSE, #' appenders = list(AppenderConsole$new(threshold = "info")) #' )) #' #' lg$config(NULL) # resets logger to unconfigured state #' #' # Via YAML #' cfg <- " #' Logger: #' threshold: error #' propagate: false #' appenders: #' AppenderConsole: #' threshold: info #' " #' #' lg$config(cfg) #' lg$config(NULL) NULL # Logger ------------------------------------------------------------------ #' @export Logger <- R6::R6Class( "Logger", inherit = Filterable, cloneable = FALSE, # public -------------------------------------------------------------------- public = list( # +- methods -------------------------------------------------------------- #' @description #' #' **Loggers should never be instantiated directly with `Logger$new()`** but #' rather via [`get_logger("name")`][get_logger]. This way new Loggers are #' registered in a global namespace which ensures uniqueness and #' facilitates inheritance between Loggers. If `"name"` does not exist, a #' new Logger with that name will be created, otherwise the function returns #' a Reference to the existing Logger. #' #' `name` is potentially a `"/"` separated hierarchical value like #' `foo/bar/baz`. Loggers further down the hierarchy are descendants of the #' loggers above and (by default) inherit `threshold` and `Appenders` from #' their ancestors. #' #' @note #' If you are a package developer you should define a new Logger for each #' package, but you do not need to configure it. The user of the package #' should decide how and where to output logging, usually by configuring the #' root Logger (new Appenders added/removed, Layouts modified, etc...). #' #' @param name,appenders,threshold,filters,exception_handler,propagate #' See section Active bindings. #' #' @seealso [get_logger()] #' initialize = function( name = "(unnamed logger)", appenders = list(), threshold = NULL, filters = list(), exception_handler = default_exception_handler, propagate = TRUE, replace_empty = "" ){ # fields # threshold must be set *after* the logging functions have been initalized if (identical(name, "(unnamed logger)")){ warning( "When creating a new Logger, you should assign it a unique `name`. ", "Please see ?Logger for more infos.", call. = FALSE ) } private$set_name(name) private$.last_event <- LogEvent$new(self) self$set_threshold(threshold) self$set_appenders(appenders) self$set_propagate(propagate) self$set_filters(filters) self$set_exception_handler(exception_handler) self$set_replace_empty(replace_empty) invisible(self) }, #' @description Log an event. #' #' If `level` passes the Logger's `threshold` a new [LogEvent] with `level`, #' `msg`, `timestamp` and `caller` is created. If the new LogEvent also #' passes the Loggers [Filters][EventFilter], it is be dispatched to the #' relevant [Appenders]. #' #' @param level a `character` or `integer` scalar. See [log_levels]. #' #' @param msg `character`. A log message. If unnamed arguments are supplied #' in `...`, `msg` is passed on to [base::sprintf()] (which means `"%"` have #' to be escaped), otherwise `msg` is left as-is. #' #' @param ... *unnamed* arguments in `...` must be `character` scalars and #' are passed to [base::sprintf()]. *Named* arguments must have unique names #' but can be arbitrary \R objects that are passed to [`LogEvent$new()`][LogEvent] and #' will be turned into custom fields. #' #' @param timestamp [POSIXct]. Timestamp of the event. #' @param caller a `character` scalar. The name of the calling function. log = function( level, msg, ..., timestamp = Sys.time(), caller = get_caller(-7) ){ if (identical(get("threshold", envir = self), 0L)) return(invisible()) LOG_CALL <- sys.call(-1L) # for error reporting tryCatch({ # preconditions level <- standardize_log_levels(level) assert( identical(length(unique(level)), 1L), "Can only utilize vectorized logging if log level is the same for all entries" ) # Check if LogEvent should be created if ( isTRUE(level[[1]] > get("threshold", envir = self)) || isTRUE(getOption("lgr.logging_suspended")) ){ return(invisible(msg)) } # init if (inherits(msg, "condition")){ msg <- conditionMessage(msg) } else { msg <- as.character(msg) } force(caller) rawMsg <- msg if (missing(...)){ vals <- list( logger = self, level = level, timestamp = timestamp, caller = caller, msg = msg, rawMsg = rawMsg ) } else { dots <- list(...) if (is.null(names(dots))){ dots <- replace_empty(dots, get("replace_empty", self)) msg <- do.call(sprintf, args = c(list(msg), dots)) vals <- list( logger = self, level = level, timestamp = timestamp, caller = caller, msg = msg, rawMsg = rawMsg ) } else { not_named <- vapply(names(dots), is_blank, TRUE, USE.NAMES = FALSE) unnamed_dots <- dots[not_named] unnamed_dots <- replace_empty(unnamed_dots, get("replace_empty", self)) if (any(not_named)){ msg <- do.call(sprintf, c(list(msg), unnamed_dots)) } vals <- c( list( logger = self, level = level, timestamp = timestamp, caller = caller, msg = msg, rawMsg = rawMsg ), dots[!not_named] ) } } # This code looks really weird, but it really is just replacing all # instances of [[ with get() for minimal overhead. We want event # dispatch to be as quick as possible. event <- do.call(get("new", envir = LogEvent), vals) assign(".last_event", event, private) if (get("filter", envir = self)(event)){ for (app in unlist(mget(c("appenders", "inherited_appenders"), self), recursive = FALSE)){ tryCatch({ app_thresh <- get("threshold", envir = app) if ( (is.na(app_thresh) || get("level", envir = event) <= app_thresh) && get("filter", envir = app)(event) ){ get("append", envir = app)(event) } }, error = function(e) { e$call <- LOG_CALL e$appender <- app e$logger <- self get("handle_exception", envir = self)(e) } ) } } invisible(msg) }, error = function(e) { e$logger <- self e$call <- LOG_CALL get("handle_exception", envir = self)(e) } ) }, #' @description Log an Event fatal priority #' @param msg,...,caller see `$log()` fatal = function(msg, ..., caller = get_caller(-8L)){ if (isTRUE(get("threshold", envir = self) < 100L)) return(invisible()) get("log", envir = self)( msg = msg, caller = caller, level = 100L, timestamp = Sys.time(), ... ) }, #' @description Log an Event error priority #' @param msg,...,caller see `$log()` error = function(msg, ..., caller = get_caller(-8L)){ if (isTRUE(get("threshold", envir = self) < 200L)) return(invisible()) get("log", envir = self)( msg = msg, caller = caller, level = 200L, timestamp = Sys.time(), ... ) }, #' @description Log an Event warn priority #' @param msg,...,caller see `$log()` warn = function(msg, ..., caller = get_caller(-8L)){ if (isTRUE(get("threshold", envir = self) < 300L)) return(invisible()) get("log", envir = self)( msg = msg, caller = caller, level = 300L, timestamp = Sys.time(), ... ) }, #' @description Log an Event info priority #' @param msg,...,caller see `$log()` info = function(msg, ..., caller = get_caller(-8L)){ if (isTRUE(get("threshold", envir = self) < 400L)) return(invisible()) get("log", envir = self)( msg = msg, caller = caller, level = 400L, timestamp = Sys.time(), ... ) }, #' @description Log an Event debug priority #' @param msg,...,caller see `$log()` debug = function(msg, ..., caller = get_caller(-8L)){ if (isTRUE(get("threshold", envir = self) < 500L)) return(invisible()) get("log", envir = self)( msg = msg, caller = caller, level = 500L, timestamp = Sys.time(), ... ) }, #' @description Log an Event trace priority #' @param msg,...,caller see `$log()` trace = function(msg, ..., caller = get_caller(-8L)){ if (isTRUE(get("threshold", envir = self) < 600L)) return(invisible()) get("log", envir = self)( msg = msg, caller = caller, level = 600L, timestamp = Sys.time(), ... ) }, #' @description #' `list_log()` is a shortcut for `do.call(Logger$log, x)`. #' See \url{https://github.com/s-fleck/joblog} for an R package that #' leverages this feature to create custom log event types for tracking #' the status of cron jobs. #' #' @param x a named `list` that must at least contain the named elements #' `level` and `timestamp` #' #' @examples #' lg <- get_logger("test") #' lg$list_log(list(level = 400, msg = "example")) list_log = function(x){ do.call(self$log, x) }, #' @description Load a Logger configuration. #' #' @param cfg #' * a special `list` object with any or all of the the following elements: #' `appenders`, `threshold`, `filters`, `propagate`, `exception_handler`, #' #' * the path to a `YAML`/`JSON` config file, #' #' * a `character` scalar containing `YAML/JSON`, #' #' * `NULL` (to reset the logger config to the default/unconfigured state) #' #' @param file,text,list can be used as an alternative to #' `cfg` that enforces that the supplied argument is of the specified #' type. See [logger_config] for details. config = function( cfg, file, text, list ){ assert( missing(cfg) + missing(file) + missing(text) + missing(list) >= 3, "You can only specify one of `cfg`, `file`, `text` and `list`." ) if (!missing(cfg)){ if (is.list(cfg)){ cfg <- parse_logger_config(cfg) } else { cfg <- as_logger_config(cfg) } } else if (!missing(file)){ assert( is_scalar_character(file) && !grepl("\n", file) && file.exists(file), "`file` is not a valid path to a readable file" ) cfg <- as_logger_config(file) } else if (!missing(text)){ assert( is_scalar_character(text) && grepl("\n", text), "`text` must be a character scalar containing valid YAML" ) cfg <- as_logger_config(text) } else if (!missing(list)){ cfg <- parse_logger_config(list) } else { cfg <- as_logger_config() } if (is_logger_config(cfg)){ cfg <- parse_logger_config(cfg) } assert(is_parsed_logger_config(cfg)) self$set_threshold(cfg$threshold) self$set_appenders(cfg$appenders) self$set_propagate(cfg$propagate) self$set_filters(cfg$filters) self$set_exception_handler(cfg$exception_handler) self }, #' @description Add an Appender to the Logger #' #' @param appender a single [Appender] #' @param name a `character` scalar. Optional but recommended. #' #' @examples #' lg <- get_logger("test") #' lg$add_appender(AppenderConsole$new(), name = "myconsole") #' lg$appenders[[1]] #' lg$appenders$myconsole #' lg$remove_appender("myconsole") #' lg$config(NULL) # reset config add_appender = function( appender, name = NULL ){ assert(inherits(appender, "Appender")) private$.appenders[[length(private$.appenders) + 1L]] <- appender if (!is.null(name)) names(private$.appenders)[length(private$.appenders)] <- name invisible(self) }, #' @param pos `integer` index or `character` name of the Appender(s) to #' remove #' #' @description remove an appender remove_appender = function( pos ){ if (is.numeric(pos)){ assert( all(pos %in% seq_along(private$.appenders)), "'pos' is out of range of the length of appenders (1:", length(private$.appenders), ")" ) pos <- as.integer(pos) } else if (is.character(pos)) { assert( all(pos %in% names(private$.appenders)), "'pos' is not names of appenders (", paste(names(private$.appenders), collapse = ", "), ")" ) } for (nm in pos){ private$.appenders[[nm]] <- NULL } invisible(self) }, #' @description #' To prevent errors in the logging logic from crashing the whole script, #' Loggers pass errors they encounter to an exception handler. The default #' behaviour is to demote errors to [warnings]. See also #' `set_exception_handler()`. #' #' @param expr expression to be evaluated. handle_exception = function(expr){ private$.exception_handler(expr) }, # .. setters -------------------------------------------------------------- #' @description Set the exception handler of a logger #' #' @param fun a `function` with the single argument `e` (an error [condition]) #' #' @examples #' lgr$info(stop("this produces a warning instead of an error")) set_exception_handler = function(fun){ assert(is.function(fun)) private$.exception_handler <- fun invisible(self) }, #' @description Should a Logger propagate events to the Appenders of its ancestors? #' @param x `TRUE` or `FALSE`. Should [LogEvents] be passed on to the appenders #' of the ancestral Loggers? set_propagate = function(x){ assert(is_scalar_bool(x)) private$.propagate <- x invisible(self) }, #' @description Set the minimum log level of events that a Logger should process #' #' @param level `character` or `integer` scalar. The minimum #' [log level][log_levels] that triggers this Logger set_threshold = function(level){ if (!is.null(level)) level <- standardize_threshold(level) private[[".threshold"]] <- level invisible(self) }, #' @description Set the Logger's Appenders #' #' @param x single [Appender] or a `list` thereof. Appenders control the #' output of a Logger. Be aware that a Logger also inherits the Appenders #' of its ancestors (see `vignette("lgr", package = "lgr")` for more info #' about Logger inheritance). set_appenders = function(x){ x <- standardize_appenders_list(x) private[[".appenders"]] <- list() for (i in seq_along(x)) self$add_appender(x[[i]], name = names(x)[[i]]) invisible(self) }, #' @description Set the replacement for empty values (`NULL` or empty #' vectors) #' #' @param x should be a `character` vector, but other types of values are #' supported. use wisely. set_replace_empty = function(x){ private[[".replace_empty"]] <- x invisible(self) }, #' @description Spawn a child Logger. #' This is very similar to using [get_logger()], but #' can be useful in some cases where Loggers are created programmatically #' #' @param name `character` vector. Name of the child logger #' `get_logger("foo/bar")$spawn("baz")` is equivalent #' to `get_logger("foo/bar/baz")` spawn = function(name){ get_logger(c(private[[".name"]], name)) } ), # .. active bindings --------------------------------------------------------- active = list( #' @field name A `character` scalar. The unique name of each logger, #' which also includes the names of its ancestors (separated by `/`). name = function(){ paste(get(".name", envir = private), collapse = "/") }, #' @field threshold `integer` scalar. The threshold of the `Logger`, or if it #' `NULL` the threshold it inherits from its closest ancestor with a #' non-`NULL` threshold threshold = function() { res <- get(".threshold", envir = private) if (is.null(res)){ get("threshold", envir = get("parent", envir = self)) } else { res } }, #' @field propagate A `TRUE` or `FALSE`. Should a Logger propagate events #' to the Appenders of its ancestors? propagate = function(){ get(".propagate", envir = private) }, #' @field ancestry A named `logical` vector of containing the propagate value #' of each Logger upper the inheritance tree. The names are the names of #' the appenders. `ancestry` is an S3 class with a custom #' `format()`/`print()` method, so if you want to use the plain logical #' vector use `unclass(lg$ancestry)` ancestry = function(){ nm <- get(".name", envir = private) res <- logical(length(nm)) for (i in seq_along(nm)){ res[[i]] <- get("propagate", envir = get_logger(nm[1:i])) } structure( setNames(res, nm), class = c("ancestry", class(res)) ) }, #' @field parent a `Logger`. The direct ancestor of the `Logger`. parent = function() { nm <- get(".name", envir = private) if (identical(nm, "root")){ return(NULL) } else { get_logger(nm[-length(nm)]) } }, #' @field last_event The last LogEvent produced by the current Logger last_event = function() { get(".last_event", envir = private) }, #' @field appenders a `list` of all [Appenders] of the Logger appenders = function() { get(".appenders", envir = private) }, #' @field inherited_appenders A `list` of all appenders that the Logger #' inherits from its ancestors inherited_appenders = function(){ if (get(".propagate", envir = private)){ p <- get("parent", envir = self) if (is.null(p)) return(NULL) unlist( unname(mget(c("appenders", "inherited_appenders"), envir = p)), recursive = FALSE ) } else { NULL } }, replace_empty = function() { get(".replace_empty", envir = private) }, #' @field exception_handler a `function`. See `$set_exception_handler` and #' `$handle_exception` exception_handler = function() { get(".exception_handler", envir = private) } ), # .. private ----------------------------------------------------------------- private = list( set_name = function(x){ assert(is_scalar_character(x)) private$.name <- unlist(strsplit(x, "/")) invisible(self) }, #' @description #' Ensure all Appenders attached to a Logger are destroyed before the #' Logger, so that the Appender's finalize method can access the logger if #' it wants to finalize = function(){ for (i in rev(seq_along(self$appenders))){ self$remove_appender(i) gc() } invisible() }, .propagate = NULL, .exception_handler = NULL, .name = NULL, .appenders = NULL, .threshold = NULL, .last_event = NULL, .replace_empty = NULL ) ) # LoggerGlue -------------------------------------------------------------- #' LoggerGlue #' #' `LoggerGlue` uses [glue::glue()] instead of [base::sprintf()] to construct #' log messages. **glue** is a very well designed package for #' string interpolation. It makes composing log messages #' more flexible and comfortable at the price of an additional dependency and #' slightly less performance than `sprintf()`. #' #' `glue()` lets you define temporary named variables inside the call. #' As with the normal Logger, these named arguments get turned into custom #' fields; however, you can suppress this behaviour by making named argument #' start with a `"."`. Please refer to `vignette("lgr", package = "lgr")` for #' examples. #' #' @export LoggerGlue <- R6::R6Class( "LoggerGlue", inherit = Logger, cloneable = FALSE, public = list( initialize = function( name = "(unnamed logger)", appenders = list(), threshold = NULL, filters = list(), exception_handler = default_exception_handler, propagate = TRUE, replace_empty = "", transformer = NULL ){ # fields # threshold must be set *after* the logging functions have been initalized if (identical(name, "(unnamed logger)")){ warning( "When creating a new Logger, you should assign it a unique `name`. ", "Please see ?Logger for more infos.", call. = FALSE ) } private$set_name(name) private$.last_event <- LogEvent$new(self) self$set_threshold(threshold) self$set_appenders(appenders) self$set_propagate(propagate) self$set_filters(filters) self$set_exception_handler(exception_handler) self$set_replace_empty(replace_empty) self$set_transformer(transformer) invisible(self) }, # .. methods -------------------------------------------------------------- fatal = function(..., caller = get_caller(-8L), .envir = parent.frame()){ if (isTRUE(get("threshold", envir = self) < 100L)) return(invisible()) force(.envir) get("log", envir = self)( ..., caller = caller, level = 100L, timestamp = Sys.time(), .envir = .envir ) }, error = function(..., caller = get_caller(-8L), .envir = parent.frame()){ if (isTRUE(get("threshold", envir = self) < 200L)) return(invisible()) get("log", envir = self)( ..., caller = caller, level = 200L, timestamp = Sys.time(), .envir = .envir ) }, warn = function(..., caller = get_caller(-8L), .envir = parent.frame()){ if (isTRUE(get("threshold", envir = self) < 300L)) return(invisible()) get("log", envir = self)( ..., caller = caller, level = 300L, timestamp = Sys.time(), .envir = .envir ) }, info = function(..., caller = get_caller(-8L), .envir = parent.frame()){ if (isTRUE(get("threshold", envir = self) < 400L)) return(invisible()) get("log", envir = self)( ..., caller = caller, level = 400L, timestamp = Sys.time(), .envir = .envir ) }, debug = function(..., caller = get_caller(-8L), .envir = parent.frame()){ if (isTRUE(get("threshold", envir = self) < 500L)) return(invisible()) force(.envir) get("log", envir = self)( ..., caller = caller, level = 500L, timestamp = Sys.time(), .envir = .envir ) }, trace = function(..., caller = get_caller(-8L), .envir = parent.frame()){ if (isTRUE(get("threshold", envir = self) < 600L)) return(invisible()) force(.envir) get("log", envir = self)( ..., caller = caller, level = 600L, timestamp = Sys.time(), .envir = .envir ) }, log = function( level, ..., timestamp = Sys.time(), caller = get_caller(-7), .envir = parent.frame() ){ if (identical(get("threshold", envir = self), 0L)) return(invisible()) LOG_CALL <- sys.call(-1L) # for error reporting force(.envir) tryCatch({ # preconditions level <- standardize_log_levels(level) assert( identical(length(unique(level)), 1L), "Can only utilize vectorized logging if log level is the same for all entries" ) assert( !missing(...), "No log message or structured logging fields supplied" ) dots <- list(...) if ("msg" %in% names(dots)){ warning( "LoggerGlue does not support a `$msg` argument. Please use unnamed ", "text strings for construction your message like in `glue::glue()`.", call. = FALSE ) names(dots)[names(dots) == "msg"] <- "" } # construct msg dots_msg <- dots # because we need the original dots later again sel <- names(dots_msg) == "" if (!length(sel)){ sel <- seq_along(dots_msg) } dots_msg[sel] <- lapply(dots_msg[sel], function(e){ if (inherits(e, "condition")){ conditionMessage(e) } else { as.character(e) } }) dots_msg <- replace_empty(dots_msg, get("replace_empty", self)) rawMsg <- dots[[1]] glue_args <- list(.envir = .envir) transformer <- get("transformer", self) if (!is.null(transformer)){ glue_args[[".transformer"]] <- transformer } msg <- do.call(glue::glue, args = c(dots_msg, glue_args)) # Check if LogEvent should be created if ( isTRUE(level[[1]] > get("threshold", envir = self)) || isTRUE(getOption("lgr.logging_suspended")) ){ return(invisible(msg)) } force(caller) # Create list that contains all values equired for the log event custom_fields <- !(grepl("^\\.", names(dots)) | is_blank(names(dots))) vals <- c( list( logger = self, level = level, timestamp = timestamp, caller = caller, msg = msg, rawMsg = rawMsg ), dots[custom_fields] ) # This code looks really weird, but it really is just replacing all # instances of [[ with get() for minimal overhead. We want event # dispatch to be as quick as possible. event <- do.call(get("new", envir = LogEvent), vals) assign(".last_event", event, private) if (get("filter", envir = self)(event)){ for (app in unlist(mget(c("appenders", "inherited_appenders"), self), recursive = FALSE)){ tryCatch({ app_thresh <- get("threshold", envir = app) if ( (is.na(app_thresh) || get("level", envir = event) <= app_thresh) && get("filter", envir = app)(event) ){ get("append", envir = app)(event) } }, error = function(e) { e$call <- LOG_CALL e$appender <- app e$logger <- self get("handle_exception", envir = self)(e) } ) } } invisible(msg) }, error = function(e){ e$call <- LOG_CALL e$logger <- self get("handle_exception", envir = self)(e) } ) }, list_log = function(x){ # TODO: Workaround until we have a better implementation of list_log names(x)[names(x) == "msg"] <- "" do.call(self$log, x) }, spawn = function(name){ get_logger_glue(c(private[[".name"]], name)) }, # .. setters ----------------------------------------------------------------- #' @description Set the transformer for glue string interpolation #' #' @param x single [function] taking two arguments. See [glue::glue()]. set_transformer = function(x){ private[[".transformer"]] <- x invisible(self) } ), # .. active bindings --------------------------------------------------- active = list( transformer = function() { get(".transformer", envir = private) } ), # . private ------------------------------------ private = list( .transformer = NULL ) ) # LoggerRoot -------------------------------------------------------------- #' Special logger subclass for the root logger. Currently exactly like a #' normal Logger, but prevents the threshold to be set to `NULL` #' @noRd LoggerRoot <- R6::R6Class( "LoggerRoot", inherit = Logger, cloneable = FALSE, public <- list( config = function( cfg, file, text ){ if (is.null(cfg)){ cfg <- logger_config() cfg$threshold <- getOption("lgr.default_threshold", 400L) } super$config( cfg, file, text ) }, set_threshold = function(level){ if (is.null(level)){ warning("Cannot set `threshold` to `NULL` for the root Logger") level <- NA_integer_ } level <- standardize_threshold(level) private[[".threshold"]] <- level invisible(self) } ) ) # utils ------------------------------------------------------------------- is_Logger <- function(x){ inherits(x, "Logger") } # print.Logger ------------------------------------------------------------ #' Print a Logger Object #' #' The `print()` method for Loggers displays the most important aspects of #' the Logger. #' #' #' @param x any \R Object #' @param color `TRUE` or `FALSE`: Output with color? Requires the Package #' **crayon** #' @param ... ignored #' #' @return #' `print()` returns `x` (invisibly), `format()` returns a `character` vector. #' @export #' #' @examples #' # print most important details of logger #' print(lgr) print.Logger <- function( x, color = requireNamespace("crayon", quietly = TRUE), ... ){ assert(is_scalar_bool(color)) cat(format(x, color = color), sep = "\n") invisible(x) } #' @export #' @rdname print.Logger format.Logger = function( x, color = FALSE, ... ){ assert(is_scalar_bool(color)) assert(is_scalar_bool(color)) if (!color) style_subtle <- identity thr <- fmt_threshold(x$threshold, type = "character") if (is_threshold_inherited(x)){ thr <- style_subtle(thr) } header <- paste( paste0("<", class(x)[[1]], "> [", thr, "]"), format(x$ancestry, color = color) ) appenders <- appender_summary(x$appenders) inherited_appenders <- appender_summary(x$inherited_appenders) ind <- " " res <- header if (!is.null(appenders)){ res <-c( res, "", "appenders:", paste0(ind, appenders) ) } if (!is.null(inherited_appenders)){ res <- c( res, "", "inherited appenders:", paste0(ind, inherited_appenders) ) } res } appender_summary <- function(x){ dd <- lapply(x, srs_appender) if (!length(x)){ return(NULL) } if (is.null(names(x))){ names(x) <- "" } dd <- do.call(rbind, dd) if (is.null(dd)) return(NULL) dd$name <- ifelse( is.na(names(x)) | is_blank(names(x)), paste0("[[", seq_len(length(x)), "]]"), names(x) ) dd$destination <- ifelse( !is_blank(dd$destination), paste("->", dd$destination), "" ) with( dd, paste0( pad_right(name), ": ", pad_right(class), " [", pad_left(fmt_threshold(threshold, type = "character")), "] ", destination ) ) } # single-row-summary srs_appender <- function(x){ data.frame( class = fmt_class(class(x)[[1]]), threshold = x$threshold, destination = x$destination ) } #' @description #' You can also print just the `ancestry` of a Logger which can be accessed with #' with `logger$ancestry()`. This returns a named `character` vector whose #' names correspond to the names of the Loggers `logger` inherits from. The #' `TRUE`/`FALSE` status of its elements correspond to the `propagate` values of #' these Loggers. #' #' @rdname print.Logger #' @export #' #' @examples #' # print only the ancestry of a logger #' lg <- get_logger("AegonV/Aerys/Rheagar/Aegon") #' get_logger("AegonV/Aerys/Rheagar")$set_propagate(FALSE) #' #' print(lg$ancestry) #' unclass(lg$ancestry) print.ancestry <- function( x, color = requireNamespace("crayon", quietly = TRUE), ... ){ assert(is_scalar_bool(color)) cat(format(x, color = color), "\n") invisible(x) } #' @export #' @rdname print.Logger format.ancestry <- function( x, color = FALSE, ... ){ assert(is_scalar_bool(color)) sps <- rep("/", length(x)) nms <- names(x) style <- identity if (color){ for (i in rev(seq_along(x))){ nms[[i]] <- style(nms[[i]]) sps[[i]] <- style(sps[[i]]) if (!x[[i]]){ style <- style_subtle } } } sps[length(sps)] <- "" paste0(nms, sps, collapse = "") } is_threshold_inherited <- function(x){ is.null(x[[".__enclos_env__"]][["private"]][[".threshold"]]) } lgr/R/use_logger.R0000644000176200001440000000201414131760166013517 0ustar liggesusers#' Setup a Simple Logger for a Package #' #' This gives you a minimal logger with no appenders that you can use inside #' your package under the name `lg` (e.g. lg$fatal("test")). `use_logger()` #' does not modify any files but only prints code for you to copy and paste. #' #' #' @param pkg `character` scalar. Name of the package. The default is to try to #' get the Package name automatically using the packages **rprojroot** and #' **desc** #' #' @return a `character` scalar containing \R code. #' @export #' #' @examples #' use_logger("testpkg") #' use_logger <- function( pkg = desc::desc_get("Package", rprojroot::find_package_root_file("DESCRIPTION"))[[1]] ){ code <- sprintf( '.onLoad <- function(...){ assign( "lg", lgr::get_logger("%s"), envir = parent.env(environment()) ) }', pkg ) msg <- sprintf( "Add the following to any R file in your package (usually '%s-package.R' or 'zzz.R'):", pkg ) message("\n", msg, "\n") cat(code) invisible(code) } lgr/R/logger_index.R0000644000176200001440000000321014131760166014031 0ustar liggesusers#' Return a data.frame of all registered loggers #' #' @return a `logger_index` `data.frame` #' @export #' @seealso [logger_tree()] for a more visual representation of registered #' loggers #' #' @examples #' get_logger("tree/leaf") #' get_logger("shrub/leaf") #' get_logger("plant/shrub/leaf") #' logger_index() logger_index <- function(){ initialize_implicit_loggers() names <- sort(ls(envir = loggers)) names <- union("root", names) # ensure root logger is sorted first res <- lapply(names, function(logger_name){ cur_logger <- get_logger(logger_name) data.frame( name = logger_name, configured = !is_virgin_Logger(logger_name), threshold = cur_logger$threshold, threshold_inherited = is_threshold_inherited(cur_logger), propagate = cur_logger$propagate, n_appenders = length(cur_logger$appenders) ) }) res <- do.call(rbind, res) structure( res, class = union("logger_index", class(res)) ) } #' Initialize all loggers along all logger paths #' #' This is usually not necessary because all loggers along the path are #' initialized the first time they are needed (even when printing a #' logger!); however, it is sometimes necessary to do this explicitely #' when calling `logger_index()`. #' @noRd initialize_implicit_loggers <- function(){ names <- sort(ls(envir = loggers)) for (n in names){ x <- unlist(strsplit(n, "/", fixed = TRUE)) for (i in seq_along(x)){ get_logger(paste(x[1:i], collapse = "/")) } } } lgr/R/default_functions.R0000644000176200001440000000274614131760166015114 0ustar liggesusers# utils ------------------------------------------------------------------- #' Demote an exception to a warning #' #' Throws a timestamped warning instead of stopping the program. This is #' the default exception handler used by [Loggers]. #' #' @param e an `error condition` object #' #' @return The warning as `character` vector #' @export #' #' @examples #' tryCatch(stop("an error has occurred"), error = default_exception_handler) #' default_exception_handler <- function(e){ ts <- format(Sys.time(), format = "%Y-%m-%d %H:%M:%OS3") call <- paste(trimws(format(e$call)), collapse = " ") logger <- format(e$logger$name) if ("appender" %in% names(e)){ appender <- paste0(class_fmt(e$appender, ignore = c("Filterable", "R6", "Appender"))) message <- sprintf("[%s] %s %s ~ error in `%s`: %s", ts, logger, appender, call, e$message) res <- AppenderWarning(message = message, error = e) } else { message <- sprintf("[%s] %s ~ error in `%s`: %s", ts, logger, call, e$message) res <- LoggerWarning(message = message, error = e) } warning(res) } AppenderWarning <- function(message, class = NULL, call = NULL, ...){ LoggerWarning( message = message, class = union(class, "AppenderWarning"), call = call, ... ) } LoggerWarning <- function(message, class = NULL, call = NULL, ...){ warning_condition( message = message, class = union(class, "LoggerWarning"), call = call, ... ) } lgr/R/simple_logging.R0000644000176200001440000001777114131760166014403 0ustar liggesusers#' Simple Logging #' #' lgr provides convenience functions managing the root Logger. These are #' designed chiefly for interactive use and are less verbose than their #' R6 method counterparts. #' #' @name simple_logging #' NULL # logging ----------------------------------------------------------------- #' @inheritParams with_log_value #' @param logfun a `function` for processing the log request, usually #' `lgr$info()`, `lgr$debug()`, etc... . #' @param caller a `character` scalar. The name of the calling function #' #' @export #' @rdname simple_logging log_exception <- function( code, logfun = lgr$fatal, caller = get_caller(-3) ){ force(caller) force(logfun) tryCatch( force(code), error = function(e){ logfun(unlist(strsplit(e$message, split = "\n", fixed = TRUE)), caller = caller) stop(e) } ) } # managment --------------------------------------------------------------- #' @param target a [Logger] or [Appender] or the name of a Logger as `character` #' scalar #' #' @description #' `threshold()` sets or retrieves the threshold for an [Appender] or [Logger] #' (the minimum level of log messages it processes). It's `target` defaults to #' the root logger. (equivalent to `lgr::lgr$threshold` and #' `lgr::lgr$set_threshold`) #' #' `console_threshold()` is a shortcut to set the threshold of the root #' loggers [AppenderConsole], which is usually the only Appender that manages #' console output for a given \R session. (equivalent to #' `lgr::lgr$appenders$console$threshold` and #' `lgr::lgr$appenders$console$set_threshold`) #' #' `add_appender()` and `remove_appender()` add Appenders to Loggers and other #' Appenders. (equivalent to `lgr::lgr$add_appender` and #' `lgr::lgr$remove_appender`) #' #' @rdname simple_logging #' @return #' `threshold()` and `console_threshold()` return the [log_level] of `target` #' as `integer` (invisibly) #' #' @export #' #' #' @examples #' # Get and set the threshold of the root logger #' threshold("error") #' threshold() #' lgr$info("this will be supressed") #' lgr$error("an important error message") #' #' # you can also specify a target to modify other loggers #' lg <- get_logger("test") #' threshold("fatal", target = lg) #' threshold(target = lg) #' #' # If a Logger's threshold is not set, the threshold is inherited from #' # its parent, in this case the root logger (that we set to error/200 before) #' threshold(NULL, target = lg) #' threshold(target = lg) #' #' # Alternative R6 API for getting/setting thresholds #' lg$set_threshold("info") #' lg$threshold #' lg$set_threshold(300) #' lg$threshold #' lg$set_threshold(NULL) #' lg$threshold #' #' # cleanup #' lgr$config(NULL) #' lg$config(NULL) threshold <- function( level, target = lgr::lgr ){ if (missing(level)) return(target[["threshold"]]) else target$set_threshold(level) invisible(target$threshold) } #' @rdname simple_logging #' @export console_threshold <- function( level, target = lgr::lgr$appenders$console ){ assert(inherits(target, "AppenderConsole")) if (missing(level)) target$threshold else target$set_threshold(level) invisible(target$threshold) } #' @rdname simple_logging #' @param appender an `Appender` #' @param name `character` scalar. An optional name for the new Appender. #' @return `add_appender()` and `remove_appender()` return `target`. #' @export #' @examples #' #' #' # add Appenders to a Logger #' add_appender(AppenderConsole$new(), "second_console_appender") #' lgr$fatal("Multiple console appenders are a bad idea") #' remove_appender("second_console_appender") #' lgr$info("Good that we defined an appender name, so it's easy to remove") add_appender <- function( appender, name = NULL, target = lgr::lgr ){ target$add_appender(appender, name = name) } #' @param pos `integer` index or `character` names of the appenders to remove #' @rdname simple_logging #' @export remove_appender <- function( pos, target = lgr::lgr ){ target$remove_appender(pos) } #' @description #' `show_log()` displays the last `n` log entries of an Appender (or a Logger #' with such an Appender attached) with a `$show()` method. Most, but not all #' Appenders support this function (try [AppenderFile] or [AppenderBuffer]). #' #' `show_data()` and `show_dt()` work similar to `show_log()`, except that #' they return the log as `data.frame` or `data.table` respectively. Only #' Appenders that log to formats that can easily be converted to `data.frames` #' are supported (try [AppenderJson] or [AppenderBuffer]). #' #' The easiest way to try out this features is by adding an AppenderBuffer #' to the root logger with [`basic_config(memory = TRUE)`][basic_config()]. #' #' @param n `integer` scalar. Show only the last `n` log entries that match #' `threshold` #' #' @inheritParams basic_config #' #' @return `show_log()` prints to the console and returns whatever the target #' Appender's `$show()` method returns, usually a `character` vector, #' `data.frame` or `data.table` (invisibly). #' #' `show_data()` always returns a `data.frame` and `show_dt()` always returns #' a `data.table`. #' @rdname simple_logging #' @export #' #' @examples #' #' # Reconfigure the root logger #' basic_config(memory = TRUE) #' #' # log some messages #' lgr$info("a log message") #' lgr$info("another message with data", data = 1:3) #' #' show_log() #' show_data() #' #' # cleanup #' lgr$config(NULL) show_log <- function( threshold = NA_integer_, n = 20L, target = lgr::lgr ){ dd <- find_target(target, "show") dd$show(n = n, threshold = threshold) } #' @rdname simple_logging #' @export show_dt <- function( target = lgr::lgr ){ tryCatch( find_target(target, "dt")[["dt"]], error = function(e){ data.table::as.data.table(find_target(target, "data")[["data"]]) } ) } #' @rdname simple_logging #' @export show_data <- function( target = lgr::lgr ){ dd <- find_target(target, "data") as.data.frame(dd$data) } #' Return the first Appender that has a method or field called `name` #' #' @param x a [Logger] or [Appender] #' @param name #' #' @return an Appender #' @noRd find_target <- function( x, name ){ assert(is_scalar_character(name)) if (is_scalar_character(x)) { x <- get_logger(x) } if (inherits(x, "Appender")){ if (name %in% names(x)){ return(x) } else { stop( "This ", class_fmt(x, c("R6", "Filterable", "Appender")), " has no method or field called `", name, "`" ) } } else if (inherits(x, "Logger")){ for (app in x$appenders){ res <- try(find_target(app, name), silent = TRUE) if (inherits(res, "Appender")) return(res) } stop( "This ", class_fmt(x, c("R6", "Filterable")), " has no Appender with a field or method called `", name, "`" ) } else { stop("`x` is not a Logger or Appender") } } # simple setup ------------------------------------------------------------ default_appenders <- function( ){ log_files <- getOption("lgr.log_file", NULL) thresholds <- names(log_files) if (is.null(thresholds)){ thresholds <- rep(NA, length(log_files)) } lf <- lapply(seq_along(log_files), function(i){ tryCatch( setup_file_appender(log_files[[i]], thresholds[[i]]), error = function(e){ warning( sprintf(paste( "Cannot setup logfile '%s' as specified in the global options:", "%s\nSee '?lgr' for help."), log_files[[i]], e) ) NULL } ) }) } setup_file_appender <- function(path, threshold = NA){ if (grepl("\\.json.*", path)){ lo <- LayoutJson$new() } else { lo <- LayoutFormat$new() } AppenderFile$new(file = path, threshold = threshold, layout = lo) } lgr/R/logger_config.R0000644000176200001440000001454714131760166014206 0ustar liggesusers#' Logger Configuration Objects #' #' `logger_config()` is an S3 constructor for `logger_config` objects #' that can be passed to the `$config` method of a [Logger]. You #' can just pass a normal `list` instead, but using this constructor is #' a more formal way that includes additional argument checking. #' #' #' @param appenders see [Logger] #' @param threshold see [Logger] #' @param filters see [Logger] #' @param exception_handler see [Logger] #' @param propagate see [Logger] #' #' @return a `list` with the subclass `"logger_config"` #' logger_config <- function( appenders = NULL, threshold = NULL, filters = NULL, exception_handler = NULL, propagate = TRUE ){ # init/preconditions if (is.function(exception_handler)){ exception_handler <- deparse(exception_handler) } else if (!is.null(exception_handler)){ assert( is.character(exception_handler), "'exception_handler' must be a function, a character scalar giving the", "name of a function, or a character vector containing arbitrary R code." ) } assert(is.null(threshold) || is_threshold(threshold)) assert(is.null(appenders) || all(vapply(appenders, is.list, logical(1)))) assert(is.null(filters) || all(vapply(filters, is.list, logical(1)))) if (!is.null(propagate)){ assert(is_scalar(propagate)) if (is.character(propagate)) propagate <- toupper(propagate) propagate <- as.logical(propagate) assert(is_scalar_bool(propagate)) } cfg <- compact(list( appenders = appenders, threshold = threshold, filters = filters, exception_handler = exception_handler, propagate = propagate )) class(cfg) <- c("logger_config", "list") cfg } is_logger_config <- function(x){ inherits(x, "logger_config") } parsed_logger_config <- function( appenders = list(), threshold = NULL, filters = list(), exception_handler = default_exception_handler, propagate = TRUE ){ structure( list( appenders = standardize_appenders_list(appenders), threshold = threshold, filters = standardize_filters_list(filters), exception_handler = exception_handler, propagate = propagate ), class = c("parsed_logger_config", "list") ) } is_parsed_logger_config <- function(x){ inherits(x, "parsed_logger_config") } parse_logger_config <- function( x, defaults = parsed_logger_config() ){ if (is_parsed_logger_config(x)){ return(x) } else { assert(all(names(x) %in% names(defaults))) } if (is_logger_config(x)){ objects <- resolve_r6_ctors(x) res <- defaults if ("appenders" %in% names(x)) res$appenders <- standardize_appenders_list(objects$appenders) if ("exception_handler" %in% names(x)) res$exception_handler <- eval(parse(text = x[["exception_handler"]])) if ("propagate" %in% names(x)) res$propagate <- as.logical(toupper(x[["propagate"]])) if ("threshold" %in% names(x)) res$threshold <- standardize_threshold(x$threshold) if ("filters" %in% names(x)) res$filters <- standardize_filters_list(objects$filters) class(res) <- c("parsed_logger_config", "list") } else { res <- do.call(parsed_logger_config, x) } res } #' `as_logger_config()` coerces any supported \R object to a `logger_config` #' object. You usually do not have to call this function directly, as it is #' automatically invoked by the `config()` method of Loggers (see examples) #' #' @param x any \R object. Especially: #' * A `character` scalar. This can either be the path to a #' YAML file or a character scalar containing valid YAML #' * a list containing the elements `appenders`, `threshold`, `exception_handler`, #' `propagate` and `filters`. See the section *Fields* in [Logger] for #' details. #' * a Logger object, to clone its configuration. #' #' @rdname logger_config #' @seealso \url{https://yaml.org/} #' @return a logger_config object #' @export #' as_logger_config <- function(x){ UseMethod("as_logger_config") } #' @export as_logger_config.NULL <- function(x){ as_logger_config(list()) } #' @rdname logger_config #' @export as_logger_config.list <- function(x){ if (identical(names(x), "Logger")) x <- x[["Logger"]] assert(all( names(x) %in% c("exception_handler", "propagate", "threshold", "appenders", "filters") )) class(x) <- c("logger_config", class(x)) x } #' @rdname logger_config #' @export as_logger_config.character <- function( x ){ if (identical(length(x), 1L) && !grepl("\n", x)){ if ( identical(tolower(tools::file_ext(x)), "json") && requireNamespace("jsonlite", quietly = TRUE) ){ # if jsonliste is installed use jsonlite for yaml, otherwiese use the # yaml package (since JSON is also valid YAML) dd <- jsonlite::read_json(x, simplifyVector = TRUE) } else { assert(file.exists(x), "The file '", x, "' does not exist.") assert_namespace("yaml") dd <- yaml::read_yaml(file = x) } } else { dd <- yaml::read_yaml(text = x) } assert( identical(names(dd), "Logger"), "If 'x' is a YAML file, it must contain a single logger object" ) as_logger_config(dd) } resolve_r6_ctors <- function(x){ ctors <- lapply(names(x), get0_R6Class) for (i in seq_along(x)){ if (length(ctors) && !is.null(ctors[[i]])){ args <- resolve_r6_ctors(x[[i]]) if (is.null(args)) args <- list() # prevent logger not named warning suppressWarnings(x[[i]] <- do.call(ctors[[i]]$new, args)) } else { if (is.recursive(x[[i]])){ x[[i]] <- resolve_r6_ctors(x[[i]]) } else { x[[i]] <- x[[i]] } } } x } get0_R6Class <- function(x){ assert(is_scalar_character(x)) res <- get0(x, envir = parent.env(environment())) if (R6::is.R6Class(res)){ res } else { NULL } } standardize_appenders_list <- function(x){ if (is.null(x)) return(list()) if (inherits(x, "Appender")) x <- list(x) assert( is.list(x) && all(vapply(x, inherits, TRUE, "Appender")), "'appenders' must either be a single Appender, a list thereof, or ", "NULL for no appenders." ) x } lgr/R/logger_tree.R0000644000176200001440000001063714131760166013674 0ustar liggesusers#' Logger Tree #' #' Displays a tree structure of all registered Loggers. #' #' @section Symbology: #' #' * unconfigured Loggers are displayed in gray (if your terminal supports #' colors and you have the package \pkg{crayon} installed). #' * If a logger's `threshold` is set, it is displayed in square brackets next #' to its name (reminder: if the threshold is not set, it is inherited from #' next logger up the logger tree). #' * If a logger's `propagate` field is set to `FALSE` an red hash (`#`) sign #' is displayed in front of the logger name, to imply that it does not pass #' LogEvents up the tree. #' #' @return `data.frame` with subclass `"logger_tree"` #' #' @seealso [logger_index()] for a tidy `data.frame` representation of #' all registered loggers #' #' @export #' #' @examples #' get_logger("fancymodel") #' get_logger("fancymodel/shiny")$ #' set_propagate(FALSE) #' #' get_logger("fancymodel/shiny/ui")$ #' set_appenders(AppenderConsole$new()) #' #' get_logger("fancymodel/shiny/server")$ #' set_appenders(list(AppenderConsole$new(), AppenderConsole$new()))$ #' set_threshold("trace") #' #' get_logger("fancymodel/plumber") #' #' if (requireNamespace("cli")){ #' print(logger_tree()) #' } #' logger_tree <- function( ){ names <- ls(envir = loggers) nodes <- lapply(names, function(.x) unlist(strsplit(.x, "/"))) max_depth <- seq_len(max(vapply(nodes, length, integer(1)))) second_tier <- setdiff(vapply(nodes, `[[`, character(1), 1L), "root") res <- data.frame( parent = "root", children = I(list(unique(second_tier))), configured = TRUE, threshold = get_logger()$threshold, threshold_inherited = FALSE, propagate = TRUE, n_appenders = length(get_logger()$appenders), stringsAsFactors = FALSE ) for (i in seq_along(nodes)){ for (j in seq_along(nodes[[i]])){ parent_cur <- nodes[[i]][[j]] if (j < length(nodes[[i]])){ child_cur <- nodes[[i]][[j + 1]] } else { child_cur <- NULL } if (parent_cur %in% res$parent){ sel <- res$parent == parent_cur res$children[sel] <- I(list(unique(unlist(compact(c(res$children[sel], child_cur)))))) } else { logger_name <- paste(nodes[[i]][seq_len(j)], collapse = "/") cur_logger <- get_logger(logger_name) res <- rbind( res, data.frame( parent = parent_cur, children = I(list(child_cur)), configured = !is_virgin_Logger(logger_name), threshold = get_logger(logger_name)$threshold, threshold_inherited = is_threshold_inherited(cur_logger), propagate = cur_logger$propagate, n_appenders = length(cur_logger$appenders) ) ) } } } assert(all_are_distinct(res$parent)) structure( res, class = union("logger_tree", class(res)) ) } #' Print Logger Trees #' #' @param x a [logger_tree][logger_tree()] #' @param color `logical` scalar. If `TRUE` terminal output is colorized via #' the package \pkg{crayon}? #' @param ... passed on to [cli::tree()] #' @return `x` (invisibly) #' @export print.logger_tree <- function( x, color = requireNamespace("crayon", quietly = TRUE), ... ){ if (requireNamespace("cli", quietly = TRUE)){ cat(format(x, color = color, ...), sep = "\n") } else { warning( "Console output of logger trees requires the package 'cli'. You can", "install it with `install.packages(\"cli\")`." ) print(as.data.frame(x)) } invisible(x) } #' @rdname print.logger_tree #' @export format.logger_tree <- function( x, color = FALSE, ... ){ assert_namespace("cli") if (!color){ style_fatal <- identity style_color <- identity } label <- ifelse(x$configured, x$parent, style_subtle(x$parent)) label <- ifelse( x$threshold_inherited, label, paste0(label, " [", label_levels(x$threshold), "]") ) label <- ifelse( x$n_appenders == 0, label, paste0(label, " -> ", x$n_appenders, " appender", ifelse(x$n_appenders > 1, "s", "")) ) label <- ifelse( x$propagate, label, paste0(style_fatal("#"), label) ) x_print <- data.frame( parent = x$parent, children = x$children, label = label ) format(cli::tree(x_print, root = "root", ...)) } lgr/R/LayoutJson.R0000644000176200001440000002023715102074054013473 0ustar liggesusers# LayoutJson -------------------------------------------------------------- #' Format LogEvents as JSON #' #' @description #' A format for formatting LogEvents as #' [jsonlines](https://jsonlines.org/) log files. This provides a #' nice balance between human- an machine-readability. #' #' @family Layouts #' @seealso [read_json_lines()], [https://jsonlines.org/](https://jsonlines.org/) #' @export #' @examples #' # setup a dummy LogEvent #' event <- LogEvent$new( #' logger = Logger$new("dummy logger"), #' level = 200, #' timestamp = Sys.time(), #' caller = NA_character_, #' msg = "a test message", #' custom_field = "LayoutJson can handle arbitrary fields" #' ) #' #' lo <- LayoutJson$new() #' lo$format_event(event) #' #' lo <- LayoutJson$new( #' transform_event_names = toupper, #' excluded_fields = c("RAWMSG", "CALLER")) #' #' lo$format_event(event) #' #' lo <- LayoutJson$new( #' transform_event = function(e) { #' values <- e$values #' values$msg <- toupper(values$msg) #' values #' }, #' timestamp_fmt = "%a %b %d %H:%M:%S %Y", #' excluded_fields = c("RAWMSG", "CALLER")) #' #' lo$format_event(event) LayoutJson <- R6::R6Class( "LayoutJson", inherit = Layout, public = list( #' @description #' Creates a new instance of this [R6][R6::R6Class] class. #' #' @param toJSON_args a list of arguments passed to [jsonlite::toJSON()], #' #' @param transform_event a `function` with a single argument that #' takes a [LogEvent] object and returns a `list` of values. #' #' @param timestamp_fmt Format to be applied to the timestamp. This is #' applied after `transform_event` but `before transform_event_names` #' * `NULL`: formatting of the timestamp is left to [jsonlite::toJSON()], #' * a `character` scalar as for [format.POSIXct()], or #' * a `function` that returns a vector of the same length as its #' ([POSIXct]) input. The returned vector can be of any type #' supported by [jsonlite::toJSON()]. #' #' @param transform_event_names #' * `NULL`: don't process names #' * a named `character` vector of the format `new_name = old_name` #' * or a `function` with a single mandatory argument that accepts a #' `character` vector of field names. Applied after `transform_event`. #' #' @param excluded_fields A `character` vector of field names to exclude #' from the final output. Applied after `transform_event_names`. initialize = function( toJSON_args = list(auto_unbox = TRUE), timestamp_fmt = NULL, transform_event = function(event) event[["values"]], transform_event_names = NULL, excluded_fields = "rawMsg" ){ self$set_toJSON_args(toJSON_args) self$set_timestamp_fmt(timestamp_fmt) self$set_transform_event(transform_event) self$set_transform_event_names(transform_event_names) self$set_excluded_fields(excluded_fields) }, format_event = function(event) { values <- get(".transform_event", private)(event) values[["timestamp"]] <- apply_timestamp_formatter(values[["timestamp"]], get(".timestamp_fmt", private)) names(values) <- apply_event_name_transformer(names(values), get(".transform_event_names", private)) values <- apply_field_exclusion(values, self$excluded_fields) do.call( jsonlite::toJSON, args = c(list(x = values), get(".toJSON_args", private)) ) }, # . . setters ------------------------------------------------------------- #' @param x a `list` set_toJSON_args = function(x){ assert(is.list(x)) assert(identical(length(names(x)), length(x))) private$.toJSON_args <- x invisible(self) }, #' @param x a `character` scalar or a `function` that accepts a `POSIXct` #' as its single argument set_timestamp_fmt = function(x){ assert(is.null(x) || is_scalar_character(x) || is.function(x)) private[[".timestamp_fmt"]] <- x invisible(self) }, #' @param x a `function` that accepts a `LogEvent` as its single argument set_transform_event = function(x){ assert( is.function(x) && length(formals(x)) >= 1L, "`transform_event` must be a function a single argument (optional arguments are OK)") private[[".transform_event"]] <- x invisible(self) }, #' @param x a named `character` vector or a function that accepts a #' `character` vector of field names as its single argument. set_transform_event_names = function(x){ assert( is.null(x) || is_field_name_map(x) || (is.function(x) && length(formals(x)) >= 1L), "`transform_event_names` must be a named character vector or function with a single mandatory argument (optional arguments are OK)") private[[".transform_event_names"]] <- x }, # . . methods ---------------------------------------------------------------- #' @description Represent the `LayoutJson` class as a string toString = function() { fmt_class(class(self)[[1]]) }, #' @description Read and parse file written using this Layout #' #' This can be used by the `$data` active binding of an [Appender] #' #' @param file `character` scalar: path to a file parse = function(file){ read_json_lines(file) }, #' @description Read a file written using this Layout (without parsing) #' #' This can be used by the `$show()` method of an [Appender] #' #' @param file `character` scalar: path to a file #' @param threshold `character` Minimum log level to show. Requires parsing #' of the log file (but will still display unparsed output) #' @param n `integer` number of lines to show read = function( file, threshold = NA_integer_, n = 20L ){ assert(is_scalar_integerish(n)) threshold <- standardize_threshold(threshold) dd <- readLines(file) if (!is.na(threshold)){ sel <- self$parse(file)$level <= threshold } else { sel <- TRUE } dd <- tail(dd[sel], n) dd } ), # . . active fields ------------------------------------------------------ active = list( #' @field toJSON_args a `list` toJSON_args = function() { get(".toJSON_args", private) }, #' @field timestamp_fmt a `character` scalar or a `function` that accepts a `POSIXct` #' as its single argument timestamp_fmt = function() { get(".timestamp_fmt", private) }, #' @field transform_event a `function` that accepts a `LogEvent` as its single argument transform_event = function(){ get(".transform_event", private) }, #' @field transform_event_names a named `character` vector or a function that accepts a #' `character` vector of field names as its single argument. transform_event_names = function() { get(".transform_event_names", private) } ), # . . private -------------------------------------------------------------- private = list( .toJSON_args = NULL, .timestamp_fmt = NULL, .transform_event = NULL, .transform_event_names = NULL ) ) # utils ------------------------------------------------------------------- apply_timestamp_formatter = function(x, f){ if (is.null(f)){ return(x) } if (is.character(f)){ return(format(x, f)) } if (is.function(f)){ return(f(x)) } warning("`f` must be a character scalar or a function") } apply_event_name_transformer = function(x, f){ if (is.null(f)){ return(x) } if (is.character(f)){ rename_idx <- match(x, f, nomatch = 0L) x[rename_idx > 0L] <- names(f)[rename_idx[rename_idx > 0L]] return(x) } if(is.function(f)){ return(f(x)) } warning("`f` must be a named character vector or a function") x } apply_field_exclusion <- function(x, f){ if (is.null(f)){ return(x) } x[!names(x) %in% f] } is_field_name_map <- function(x){ is.character(x) && !is.null(names(x)) && all(nzchar(names(x))) } lgr/R/utils-sfmisc.R0000644000176200001440000003545615035130575014025 0ustar liggesusers# sfmisc utils 1.0.0.9034 # utils ------------------------------------------------------------------- # nocov start # commonly used utility functions included from the package sfmisc #' Paste and Truncate #' #' @param x a vector #' @param width (maximum) width of result #' @param dots `character` scalar. String to use for ellipses #' @inheritParams paste #' #' @return a `character` scalar #' @noRd #' #' @examples #' ptrunc(month.abb) #' ptrunc(month.abb, month.name) #' ptrunc <- function( ..., width = 40L, sep = ", ", collapse = ", ", dots = " ..." ){ assert(width > 7L, "The minimum supported width is 8") x <- paste(..., sep = sep, collapse = collapse) sel <- vapply(x, nchar, integer(1), USE.NAMES = FALSE) > width x[sel] <- strtrim(x[sel], width = width - 4L) x[sel] <- paste0(gsub(",{0,1}\\s*$", "", x[sel]), dots) x } fmt_class <- function(x, open = "<", close = ">"){ paste0(open, paste(x, collapse = "/"), close) } #' @param x any \R object #' @param ignore subclasses to ignore #' @noRd class_fmt <- function(x, ignore = NULL){ fmt_class(setdiff(class(x), ignore)) } compact <- function(x){ x[!vapply(x, is_empty, FALSE)] } replace_empty <- function(x, fallback = ""){ for (i in seq_along(x)){ if (is_empty(x[[i]])){ x[[i]] <- fallback } } x } walk <- function(.x, .f, ...){ for (i in seq_along(.x)){ .f(.x[[i]], ...) } invisible(.x) } # assertions -------------------------------------------------------------- #' Assert a condition #' #' A simpler and more efficient for [base::stopifnot()] that has an easy #' mechanism for supplying custom error messages. As opposed to `stopifnot()`, #' `assert()` only works with a single (scalar) assertions. #' #' @param cond `TRUE` or `FALSE` (without any attributes). `FALSE` will throw #' an exception with an automatically constructed error message (if `...` #' was not supplied). Anything else will throw an exception stating that #' `cond` was not valid. #' @param ... passed on to [stop()] #' @param call. passed on to [stop()] #' @param domain passed on to [stop()] #' #' @noRd #' #' @return TRUE on success #' #' @examples #' #' \dontrun{ #' assert(1 == 1) #' assert(1 == 2) #' } #' #' assert <- function( cond, ..., call. = FALSE, domain = NULL ){ if (isTRUE(cond)){ return(TRUE) } else if (isFALSE(cond)){ if (identical(length(list(...)), 0L)){ msg <- paste0("`", deparse(match.call()[[2]]), "`", " is not 'TRUE'") stop(msg, call. = call., domain = domain) } else { suppressWarnings( stop(..., call. = call., domain = domain) ) } } else { stop("Assertion must be either 'TRUE' or 'FALSE'") } } assert_namespace <- function(...){ pkgs <- c(...) res <- vapply(pkgs, requireNamespace, logical(1), quietly = TRUE) if (all(res)){ return(invisible(TRUE)) } else { miss <- pkgs[!res] if (identical(length(miss), 1L)){ msg <- sprintf(paste( "This function requires the package '%s'. You can install it with", '`install.packages("%s")`.'), miss, miss ) } else { msg <- sprintf( paste( "This function requires the packages %s. You can install them with", "`install.packages(%s)`." ), paste(miss, collapse = ", "), paste0("c(", paste(paste0('\"', miss, '\"'), collapse = ", "), ")") ) } } stop(msg, call. = FALSE) } # predicates -------------------------------------------------------------- is_error <- function(x){ inherits(x, "error") } is_try_error <- function(x){ inherits(x, "try-error") } is_scalar <- function(x){ identical(length(x), 1L) } is_POSIXct <- function(x){ inherits(x, "POSIXct") } is_scalar_POSIXct <- function(x){ is_POSIXct(x) && is_scalar(x) } is_POSIXlt <- function(x){ inherits(x, "POSIXlt") } is_scalar_POSIXlt <- function(x){ is_POSIXlt(x) && is_scalar(x) } is_POSIXt <- function(x){ inherits(x, "POSIXt") } is_scalar_POSIXt <- function(x){ is_POSIXt(x) && is_scalar(x) } is_Date <- function(x){ inherits(x, "Date") } is_scalar_Date <- function(x){ is_Date(x) && is_scalar(x) } is_scalar_list <- function(x){ is_list(x) && is_scalar(x) } is_scalar_atomic <- function(x){ is.atomic(x) && is_scalar(x) } is_scalar_logical <- function(x){ is.logical(x) && is_scalar(x) } is_scalar_integer <- function(x){ is.integer(x) && is_scalar(x) } is_scalar_factor <- function(x){ is.factor(x) && is_scalar(x) } is_scalar_list <- function(x){ is.list(x) && is_scalar(x) } is_scalar_numeric <- function(x){ is.numeric(x) && is_scalar(x) } is_scalar_character <- function(x){ is.character(x) && is_scalar(x) } is_vector <- function(x){ is.atomic(x) || is.list(x) } is_bool <- function(x){ is.logical(x) && !anyNA(x) } #' Check if Object is a Boolean #' #' Check wheter an object is either `TRUE` or `FALSE`. #' #' @param x Any \R Object. #' @return either `TRUE` or `FALSE` #' @noRd #' is_scalar_bool <- function(x){ is.logical(x) && length(x) == 1L && !is.na(x) } #' Check if Object is Integer-like #' #' Check wheter an object is either `TRUE` or `FALSE`. #' #' @param x Any \R Object. #' @return either `TRUE` or `FALSE` #' @noRd #' is_integerish <- function(x){ if (!is.numeric(x)){ FALSE } else { all(as.integer(x) == x) } } is_scalar_integerish <- function(x){ is_scalar(x) && is_integerish(x) } is_n <- function(x){ is_scalar_integerish(x) && isTRUE(x > 0) } is_n0 <- function(x){ is_scalar_integerish(x) && isTRUE(x >= 0) } #' Check if Objects have the same length #' #' @param ... Any number of \R Objects. #' #' @return either `TRUE` or `FALSE` #' @noRd is_equal_length <- function(...){ lengths <- vapply(list(...), length, 1L) identical(length(unique(lengths)), 1L) } #' Check if Object has length 0 #' #' Check whether an object is either `TRUE` or `FALSE`. #' #' @param x Any \R Object. #' @return either `TRUE` or `FALSE` #' @noRd #' is_empty <- function(x){ !length(x) } #' Check if a String is Blank #' #' Check wheter a character vector contains only of spaces #' #' @param x Any \R Object. #' @return either `TRUE` or `FALSE` #' @noRd #' is_blank <- function(x){ trimws(x) == "" } #' Test if a Vector or Combination of Vectors is a Candidate Key #' #' Checks if all elements of the atomic vector `x`, or the combination of #' all elements of `x` if `x` is a `list`, are unique and neither `NA` or #' `infinite`. #' #' @param x a atomic vector or a list of atomic vectors #' #' @return `TRUE/FALSE` #' @noRd #' #' @examples #' #' is_candidate_key(c(1, 2, 3)) #' is_candidate_key(c(1, 2, NA)) #' is_candidate_key(c(1, 2, Inf)) #' #' td <- data.frame( #' x = 1:10, #' y = 1:2, #' z = 1:5 #' ) #' #' is_candidate_key(list(td$x, td$z)) #' # a data.frame is just a special list #' is_candidate_key(td[, c("y", "z")]) is_candidate_key <- function(x){ if (is.atomic(x)){ # !is.infinite instead of is.finite because x can be a character vector length(x) > 1 && all(!is.infinite(x)) && !anyNA(x) && identical(length(unique(x)), length(x)) } else if (is.list(x)){ length(x) > 0 && length(x[[1]] > 0) && do.call(is_equal_length, x) && all(vapply(x, function(.x) all(!is.infinite(.x)), logical(1))) && all(vapply(x, function(.x) !anyNA(.x), logical(1))) && !anyDuplicated(as.data.frame(x)) > 0 } } # https://modern-sql.com/feature/is-distinct-from is_not_distinct_from <- function(x, y){ ((x == y) & !is.na(x) & !is.na(y)) | (is.na(x) & is.na(y)) } is_distinct_from <- function(x, y){ ((x != y) & !is.na(x) & !is.na(y)) | (is.na(x) != is.na(y)) } is_windows_path <- function(x){ nchar(x) >= 2 & grepl("^[A-Za-z].*", x) & substr(x, 2, 2) == ":" } # equalish ---------------------------------------------------------------- #' Check for equality within a tolerance level #' #' #' #' @param x,y `numeric` vectors #' @param tolerance `numeric` scalar. tolerance level (absolute value). Defaults #' to `.Machine$double.eps^0.5` which is a sensible default for comparing #' floating point numbers. #' #' @return `equalish()` returns TRUE if the absolute difference between `x` and #' `y` is less than `tolerance`. #' @noRd #' @seealso [.Machine] #' #' #' @examples #' a <- 0.7 #' b <- 0.2 #' a - b == 0.5 #' equalish(a - b, 0.5) #' equalish <- function(x, y, tolerance = .Machine$double.eps ^ 0.5){ assert(is_scalar_numeric(tolerance) && tolerance >= 0) abs(x - y) < tolerance } #' @return `equalish_frac()` returns `TRUE` if the relative difference between #' `x` and `y` is smaller than `tolerance`. The relative difference is #' defined as `abs(x - y) / pmax(abs(x), abs(y))`. If both `x` and `y` are #' `0` the relative difference is not defined, but this function will still #' return `TRUE`. #' #' @noRd #' @examples #' #' equalish_frac(1000, 1010, tolerance = 0.01) #' equalish_frac(1000, 1010, tolerance = 0.009) #' equalish_frac(0, 0) #' equalish_frac <- function(x, y, tolerance = .Machine$double.eps ^ 0.5){ assert(is_scalar_numeric(tolerance) && tolerance >= 0) res <- abs(x - y) / pmax(abs(x), abs(y)) < tolerance res[x == 0 & y == 0] <- TRUE res } # all_are ----------------------------------------------------------------- #' Convert vector if identical elements to scalar #' #' Returns `unique(x)` if all elements of `x` are identical, throws an error if #' not. #' #' @inheritParams all_are_identical #' #' @return A scalar of the same type as `x` #' @noRd as_scalar <- function(x){ res <- unique(x) if (is_scalar(res)){ return(res) } else { stop("Not all elements of x are identical") } } #' Test if all elements of a vector are identical #' #' @param x any object that can be handled by [unique()] (usually a vector or #' list) #' @param empty_value Value to return if function is called on a vector of #' length 0 (e.g. `NULL`, `numeric()`, ...) #' #' @noRd #' @family special equality checks #' @return `TRUE/FALSE` #' #' @examples #' #' all_are_identical(c(1,2,3)) #' all_are_identical(c(1,1,1)) #' all_are_identical <- function(x, empty_value = FALSE) { assert(length(empty_value) <= 1) if (length(x) > 0L) { return(identical(length(unique(x)), 1L)) } else { if (is.null(x)){ warning("'x' is NULL") } else { warning("'x' is an empty vector") } return(empty_value) } } #' Test if all elements of a vector are unique #' #' @inheritParams all_are_identical #' #' @return TRUE/FALSE #' #' @noRd #' @family special equality checks #' #' @examples #' #' all_are_identical(c(1,2,3)) #' all_are_identical(c(1,1,1)) #' all_are_distinct <- function( x, empty_value = FALSE ){ assert(length(empty_value) <= 1) if (identical(length(x), 1L)) { return(TRUE) } else if (length(x) > 1L) { return(identical(length(unique(x)), length(x))) } else { if (is.null(x)){ warning("'x' is NULL") } else { warning("'x' is an empty vector") } return(empty_value) } } n_distinct <- function(x){ length(unique(x)) } # misc -------------------------------------------------------------------- pad_left <- function( x, width = max(nchar(paste(x))), pad = " " ){ diff <- pmax(width - nchar(paste(x)), 0L) padding <- vapply(diff, function(i) paste(rep.int(pad, i), collapse = ""), character(1)) paste0(padding, x) } pad_right <- function( x, width = max(nchar(paste(x))), pad = " " ){ diff <- pmax(width - nchar(paste(x)), 0L) padding <- vapply(diff, function(i) paste(rep.int(pad, i), collapse = ""), character(1)) paste0(x, padding) } `%||%` <- function(x, y){ if (is.null(x)) y else (x) } preview_object <- function( x, width = 32, brackets = c("(", ")"), quotes = c("`", "`"), dots = ".." ){ if (is.function(x)){ fmls <- names(formals(x)) len_fmls <- length(fmls) if (len_fmls > 4){ fmls <- fmls[1:4] fmls_fmt <- paste(fmls, collapse = ", ") fmls_fmt <- paste0(fmls_fmt, ", +", len_fmls - length(fmls), "") } else { fmls_fmt <- paste(fmls, collapse = ", ") } return(fmt_class(paste( fmt_class(class(x), open = "", close = ""), "(", fmls_fmt, ")", sep = "" ))) } if (!is.atomic(x)) return(class_fmt(x)) if (is.numeric(x)) x <- format(x, justify = "none", drop0trailing = TRUE, trim = TRUE) res <- ptrunc(x, collapse = ", ", width = width, dots = dots) if (length(x) > 1) res <- paste0(brackets[[1]], res, brackets[[2]]) else res <- paste0(quotes[[1]], res, quotes[[2]]) res } #' Collapse text vectors with a comma #' #' @param x `character` vector #' #' @return a `character` scalar #' @noRd comma <- function(..., collapse = ", "){ paste(unlist(c(...)), collapse = collapse) } #' Collapse text vectors with a comma (no duplicates) #' #' @param x `character` vector #' #' @return a `character` scalar #' @noRd commaset <- function(..., collapse = ", "){ paste(sort(unique(unlist(c(...)))), collapse = collapse) } #' Clean up paths to make them comparable, inspired by fs::path_tidy #' #' @param x `character` vector #' #' @return a `character` vector #' @noRd path_tidy <- function(x){ x <- gsub("\\\\", "/", x) x <- gsub("(?!^)/+", "/", x, perl = TRUE) sel <- x != "/" x[sel] <- gsub("/$", "", x[sel]) sel <- is_windows_path(x) if (any(sel)){ clean_win <- function(.x){ substr(.x, 1, 1) <- toupper(substr(.x, 1 ,1)) .sel <- nchar(.x) == 2 .x[.sel] <- paste0(.x[.sel], "/") .x } x[sel] <- clean_win(x[sel]) } x } #' Return (unique) duplicated elements of a vector or rows of a data.frame #' #' For every element/row of `x` that has at least one duplicate, return one #' instance of that element. #' #' @param x an [atomic] vector or [data.frame] #' @param ... passed on to [duplicated()] #' #' @noRd #' #' @examples #' dupes(c(1, 1, 1, 2)) #' dupes(cars[c(1, 1, 1, 2), ]) dupes <- function(x, ...){ if (is.atomic(x)){ sort(unique(x[duplicated(x, ...)])) } else if (is.data.frame(x)){ res <- unique(x[duplicated(x, ...), ]) row.names(res) <- NULL res } } # nocov end lgr/R/Filterable.R0000644000176200001440000000574515035130575013452 0ustar liggesusers#' Abstract Class for Filterables #' #' @description Superclass for classes that have a `$filter()` method such as #' [Appenders] and [Loggers]. See [EventFilter] for details. #' #' @template abstract_class #' #' @export Filterable <- R6::R6Class( "Filterable", cloneable = FALSE, public = list( #' @description Determine whether the LogEvent `x` should be passed on to #' Appenders (`TRUE`) or not (`FALSE`). See also the active binding #' `filters`. #' #' @param event a [LogEvent] filter = function(event){ for (f in get(".filters", private)) { # we can assume f() is a valid Filter since it is aleady verrified by # $set_filters() if (is.function(f)){ r <- f(event) } else { r <- f[["filter"]](event) } if (isTRUE(r)){ # do nothing } else if (isFALSE(r)){ return(FALSE) } else { warning( "`$filter()` of ", class_fmt(self, c("R6", "Filterable")), " object did not return `TRUE` or `FALSE` but ", string_repr(r), ". Please check its `$filters`", call. = FALSE ) } } TRUE }, #' @description Attach a filter #' @param filter #' * a function with the single argument `event` that returns `TRUE` #' or `FALSE`; #' * an [EventFilter] [R6::R6] object; or #' * any \R object with a `$filter()` method. #' #' If a Filter returns a non-`FALSE` value, will be interpreted as `TRUE` #' (= no filtering takes place) and a warning will be thrown. #' #' @param name `character` scalar or `NULL`. An optional #' name which makes it easier to access (or remove) the filter add_filter = function(filter, name = NULL){ assert_filter(filter) assert(is.null(name) || is_scalar_character(name)) pos <- name %||% (length(private$.filters) + 1L) private[[".filters"]][[pos]] <- filter invisible(self) }, #' @description Remove a filter #' @param pos `character` or `integer` scalar. The name or index of the #' Filter to be removed. remove_filter = function(pos){ if (is.numeric(pos)) sort(pos, decreasing = TRUE) for (p in pos){ private[[".filters"]][[p]] <- NULL } invisible(self) }, #' @description Set or replace (all) Filters of parent object. See #' [EventFilter] for how Filters work. #' #' @param filters a `list` (named or unnamed) of [EventFilters][EventFilter] #' or predicate functions. See [is_filter()]. set_filters = function(filters){ filters <- standardize_filters_list(filters) private[[".filters"]] <- filters invisible(self) } ), active = list( #' @field filters a `list` of all attached Filters. filters = function(){ get(".filters", private) } ), private = list( .filters = list() ) ) lgr/R/event_list.R0000644000176200001440000000712414305461165013547 0ustar liggesusers#' A List of LogEvents #' #' An event_list is a class for `list()`s whose only elements are [LogEvents]. #' This structure is occasionally used internally in lgr (for example by #' [AppenderBuffer]) and can be useful for developers that want to write #' their own Appenders. #' #' For convenience, `as.data.frame()` and `as.data.table()` methods #' exist for event lists. #' #' @param x any `R` object #' @param ... for `event` elements to be added to the list, for the `as_*()` #' functions parameters passed on to methods. #' #' @family docs relevant for extending lgr #' @export event_list <- function(...){ as_event_list(list(...)) } #' @rdname event_list #' @return #' an `event_list()` and `as_event_list()` return a flat `list` #' of [LogEvents]. Nested lists get automatically flattened. #' #' `as.data.frame` and `as.data.table` return a `data.frame` or `data.table` #' respectively #' @export #' @examples #' e <- LogEvent$new(level = 300, msg = "a", logger = lgr) #' as_event_list(e) #' as_event_list(c(e, e)) #' # nested lists get automatically unnested #' as_event_list(c(e, list(nested_event = e))) #' as_event_list <- function(x, ...){ UseMethod("as_event_list") } #' @rdname event_list #' @export as_event_list.list <- function( x, ..., scalarize = FALSE ){ if (length(x)){ res <- unlist(x) assert(all(vapply(res, inherits, logical(1), "LogEvent"))) if (scalarize){ res <- unlist(lapply(res, as_event_list, scalarize = TRUE)) } } else { res <- list() } structure(res, class = c("event_list", "list")) } #' @param scalarize `logical` scalar. Turn [LogEvents] with non-scalar `msg` #' field into separate log events #' @export #' @rdname event_list #' @examples #' # scalarize = TRUE "unpacks" events with vector log messages #' e <- LogEvent$new(level = 300, msg = c("A", "B"), logger = lgr) #' as_event_list(e, scalarize = FALSE) #' as_event_list(e, scalarize = TRUE) #' as_event_list.LogEvent <- function( x, ..., scalarize = FALSE ){ if (scalarize && length(msgs <- get("msg", envir = x)) > 1){ vals <- x$values vals <- vals[!names(vals) %in% c("msg", "logger")] as_event_list.list(lapply( msgs, function(m) do.call(LogEvent$new, c(list(msg = m, logger = get(".logger", envir = x)), vals)) )) } else { event_list(x) } } #' @rdname event_list #' @param na.rm remove `NA` values before coercing a data.frame to an `event_list()`. #' @export as_event_list.data.frame <- function( x, na.rm = TRUE, ... ){ structure(lapply( seq_len(nrow(x)), # the hardcoded .id and .fields columns are used by lgrExtra::AppenderDt function(i){ dd <- as.list(x[i, !names(x) %in% c(".id", ".fields")]) if (na.rm){ for (j in rev(seq_along(dd))) if (is.na(dd[[j]])) dd[[j]] <- NULL } r <- as_LogEvent(c(dd, x[i, ][[".fields"]][[1]])) r } ), class = c("event_list", "list") ) } #' @rdname event_list #' @export as.data.table.event_list <- function(x, na.rm = TRUE){ data.table::rbindlist( lapply( x, data.table::as.data.table, box_if = Negate(is_scalar_atomic) ), fill = TRUE, use.names = TRUE) } #' @inheritParams as.data.frame.LogEvent #' @rdname event_list #' @export as.data.frame.event_list <- function( x, row.names = NULL, optional = FALSE, stringsAsFactors = FALSE, na.rm = TRUE, ... ){ as.data.frame(as.data.table.event_list(x), stringsAsFactors = stringsAsFactors) } lgr/R/read_json_lines.R0000644000176200001440000000103314131760166014522 0ustar liggesusers#' Read a JSON logfile #' #' @param file `character` scalar. path to a JSON logfile (one JSON object per line) #' @param ... passed on to [jsonlite::stream_in()] #' @return a `data.frame` #' @seealso [LayoutJson] #' @export #' read_json_lines <- function(file, ...){ assert_namespace("data.table", "jsonlite") con <- file(file, open = "r") on.exit(close(con)) res <- jsonlite::stream_in(con, verbose = FALSE, ...) if ("timestamp" %in% names(res)){ res$timestamp <- as.POSIXct(res$timestamp) } res } lgr/R/Layout.R0000644000176200001440000002143315036522456012652 0ustar liggesusers# Layout ------------------------------------------------------------------ #' Abstract Class for Layouts #' #' [Appenders] pass [LogEvents][LogEvent] to a Layout which formats it for #' output. For the Layouts included in lgr that means turning the LogEvent #' into a `character` string. #' #' For each Appender exist one more more possible Layouts, but not every Layout #' will work with every Appender. See the package \pkg{lgrExtra} for examples #' for Layouts that return different data types (such as `data.frames`) and #' Appenders that can handle them. #' #' @section Notes for developers: #' Layouts may have an additional `$read(file, threshold, n)` method that returns #' a `character` vector, and/or an `$parse(file)` method that #' returns a `data.frame`. These can be used by Appenders to `$show()` methods #' and `$data` active bindings respectively (see source code of [AppenderFile]). #' #' @aliases Layouts #' @family Layouts #' @include LogEvent.R #' @include utils.R #' @include utils-sfmisc.R #' @include Filterable.R #' @include log_levels.R #' #' @export Layout <- R6::R6Class( "Layout", public = list( #' @description Format a log event #' #' Function that the Layout uses to transform a [LogEvent] into something #' that an [Appender] can write to an output destination. #' #' @param event a [LogEvent] format_event = function(event){ toString(event) }, toString = function() fmt_class(class(self)[[1]]), # . . setters ----------------------------------------------------------------- set_excluded_fields = function(x){ assert(is.null(x) || is.character(x)) private$.excluded_fields <- x invisible(self) } ), # . . active -------------------------------------------------------------- active = list( #' @field excluded_fields fields to exclude from the final log excluded_fields = function() { get(".excluded_fields", private) } ), # . . private ------------------------------------------------------------- private = list( .excluded_fields = NULL ) ) # LayoutFormat ------------------------------------------------------------ #' Format Log Events as Text #' #' Format a [LogEvent] as human readable text using [format.LogEvent()], which #' provides a quick and easy way to customize log messages. If you need #' more control and flexibility, consider using [LayoutGlue] instead. #' #' @inheritSection print.LogEvent Format Tokens #' #' @section Format Tokens: #' This is the same list of format tokens as for [format.LogEvent()] #' #' #' @export #' @family Layouts #' @include Filterable.R #' @include log_levels.R #' @examples #' # setup a dummy LogEvent #' event <- LogEvent$new( #' logger = Logger$new("dummy logger"), #' level = 200, #' timestamp = Sys.time(), #' caller = NA_character_, #' msg = "a test message" #' ) #' lo <- LayoutFormat$new() #' lo$format_event(event) LayoutFormat <- R6::R6Class( "LayoutFormat", inherit = Layout, public = list( initialize = function( fmt = "%L [%t] %m %j", timestamp_fmt = "%Y-%m-%d %H:%M:%OS3", colors = NULL, pad_levels = "right", excluded_fields = NULL ){ self$set_fmt(fmt) self$set_timestamp_fmt(timestamp_fmt) self$set_colors(colors) self$set_pad_levels(pad_levels) self$set_excluded_fields(excluded_fields) }, #' @description Format a LogEvent #' @param event a [LogEvent] format_event = function( event ){ format.LogEvent( event, fmt = private$.fmt, timestamp_fmt = private$.timestamp_fmt, colors = private$.colors, pad_levels = private$.pad_levels, excluded_fields = private$.excluded_fields ) }, #' @details see Fields set_fmt = function(x){ assert(is_scalar_character(x)) private$.fmt <- x invisible(self) }, #' @details see Fields set_timestamp_fmt = function(x){ assert(is_scalar_character(x)) private$.timestamp_fmt <- x invisible(self) }, #' @details see Fields set_colors = function(x){ assert( is.null(x) || is.list(x), "'colors' must either be NULL or a list of functions, not ", class_fmt(x) ) private$.colors <- x invisible(self) }, #' @details see Fields set_pad_levels = function(x){ assert(is_scalar_character(x)) private$.pad_levels <- x invisible(self) }, #' @details #' Convert Layout to a `character` string toString = function(){ paste(fmt_class(class(self)[[1]]), self$fmt) }, #' Read a log file written using LayoutFormat #' @param threshold a `character` or `integer` threshold #' @param n number of log entries to display read = function( file, threshold = NA_integer_, n = 20L ){ assert(is_scalar_integerish(n)) threshold_ori <- threshold threshold <- standardize_threshold(threshold) dd <- readLines(file) sel <- TRUE if (!is.na(threshold)){ lvls_keep <- get_log_levels()[get_log_levels() <= threshold] if (grepl("%L", self$fmt, ignore.case = TRUE)){ sel <- grep( paste0("(", names(lvls_keep), ")", collapse = "|"), dd, ignore.case = TRUE ) } else if (grepl("%n", self$fmt)){ sel <- grep(paste0("(", lvls_keep, ")", collapse = "|"), dd) } else { warning(sprintf(paste( "A threshold of `%s` was but the Layout's format specification", "('%s') does not support filtering by log level." )), threshold_ori, self$fmt ) } } dd <- tail(dd[sel], n) dd } ), active = list( #' @field fmt a `character` scalar containing format tokens. See [format.LogEvent()]. fmt = function() private$.fmt, #' @field timestamp_fmt a `character` scalar. See [base::format.POSIXct()]. timestamp_fmt = function() private$.timestamp_fmt, #' @field colors a named `list` of functions (like the ones provided by #' the package \pkg{crayon}) passed on on [format.LogEvent()]. colors = function() private$.colors, #' @field pad_levels `"right"`, `"left"` or `NULL`. See [format.LogEvent()]. pad_levels = function() private$.pad_levels ), private = list( .fmt = NULL, .timestamp_fmt = NULL, .colors = NULL, .pad_levels = NULL ) ) # LayoutGlue ------------------------------------------------------------ #' Format Log Events as Text via glue #' #' Format a [LogEvent] as human readable text using [glue::glue]. The function #' is evaluated in an environment in which it has access to all elements of #' the [LogEvent] (see examples). This is more flexible than [LayoutFormat], #' but also more complex and slightly less performant. #' #' @export #' @family Layouts #' @seealso lgr exports a number of formatting utility functions that are #' useful for layout glue: [colorize_levels()], [pad_left()], [pad_right()]. #' @examples #' lg <- get_logger("test")$ #' set_appenders(AppenderConsole$new())$ #' set_propagate(FALSE) #' #' lg$appenders[[1]]$set_layout(LayoutGlue$new()) #' lg$fatal("test") #' #' #' # All fields of the LogEvent are available, even custom ones #' lg$appenders[[1]]$layout$set_fmt( #' "{logger} {level_name}({level}) {caller}: {toupper(msg)} {{custom: {custom}}}" #' ) #' lg$fatal("test", custom = "foobar") #' lg$config(NULL) # reset logger config LayoutGlue <- R6::R6Class( "LayoutGlue", inherit = Layout, public = list( initialize = function( fmt = "{pad_right(colorize_levels(toupper(level_name)), 5)} [{timestamp}] {msg}" ){ assert_namespace("glue") self$set_fmt(fmt) }, format_event = function( event ){ op <- parent.env(event) on.exit(parent.env(event) <- op) parent.env(event) <- environment() unclass(glue::glue(get(".fmt", private), .envir = event)) }, set_fmt = function(x){ assert(is_scalar_character(x)) private$.fmt <- x invisible(self) }, set_colors = function(x){ assert( is.null(x) || is.list(x), "'colors' must either be NULL or a list of functions, not ", class_fmt(x) ) private$.colors <- x invisible(self) }, toString = function() { paste(fmt_class(class(self)[[1]]), self$fmt) } ), active = list( #' @field fmt A string that will be interpreted by [glue::glue()] fmt = function() private$.fmt ), private = list( .fmt = NULL ) ) lgr/R/utils.R0000644000176200001440000001357515035130575012541 0ustar liggesusers#' Information About the System #' #' `get_caller()` Tries to determine the calling functions based on `where`. #' #' @param where `integer` scalar (usually negative). Look up that many frames #' up the call stack #' #' @seealso [base::sys.call()] #' @export #' #' @rdname system_infos #' @export #' #' @examples #' foo <- function() get_caller(-1L) #' foo() get_caller <- function( where = -1L ){ res <- tryCatch( sys.call(where)[[1]], error = function(e) NULL ) if (is.symbol(res)){ deparse(res) } else if (is.null(res)){ "(shell)" } else if (inherits(res, "{")){ "{...}" } else if (is.function(res)){ # rare, but can happen f.e. through plumber fmt_function_signature(res) } else { ptrunc(deparse(res)) } } #' `get_user()` tries to determine the current user. Defaults to #' `getOption("lgr.user")`. If the option is not set, `Sys.info()[["user"]]` #' is used. If the option is not set and the package **whoami** is available, #' the user name is guessed based on whichever of the following is available: #' `email_address`, `fullname`, `gh_username`, `username`. #' #' @return a `character` scalar. #' #' @seealso [whoami::whoami()] #' @name system_infos #' @param fallback A fallback in case the user name could not be determined #' @rdname system_infos #' @export #' @examples #' get_user() get_user <- function(fallback = "unknown user"){ guess_user <- function(){ if (requireNamespace("whoami", quietly = TRUE)){ res <- try({ whoami::email_address( whoami::fullname( whoami::gh_username( whoami::username( fallback )))) }, silent = TRUE) } else { res <- try(Sys.info()[["user"]], silent = TRUE) } if (inherits(res, "try-error") || is.null(res)) res <- fallback res } getOption("lgr.user", guess_user()) } # internal -------------------------------------------------------------- #' Paste and Truncate #' #' color aware version of ptrunc from sfmisc #' @noRd ptrunc_col <- function( ..., width = 40L, sep = ", ", collapse = ", ", dots = " ..." ){ assert(width > 7L, "The minimum supported width is 8") x <- paste(..., sep = sep, collapse = collapse) width <- width + nchar(x) - col_nchar(x) sel <- vapply(x, nchar, integer(1), USE.NAMES = FALSE) > width x[sel] <- strtrim(x[sel], width = width - 4L) x[sel] <- paste0(gsub(",{0,1}\\s*$", "", x[sel]), dots) x } # misc -------------------------------------------------------------------- # nocov start dyn_register_s3_method <- function( pkg, generic, class, fun = NULL ){ stopifnot( is_scalar_character(pkg), is_scalar_character(generic), is_scalar_character(class) ) if (is.null(fun)) { fun <- get(paste0(generic, ".", class), envir = parent.frame()) } else { stopifnot(is.function(fun)) } if (pkg %in% loadedNamespaces()) { registerS3method(generic, class, fun, envir = asNamespace(pkg)) } # Always register hook in case package is later unloaded & reloaded setHook( packageEvent(pkg, "onLoad"), function(...) { registerS3method(generic, class, fun, envir = asNamespace(pkg)) } ) } last <- function(x){ x[length(x)] } is_Id = function(x){ inherits(x, "Id") } is_zipcmd_available <- function(cmd = Sys.getenv("R_ZIPCMD", "zip")){ if (is_blank(cmd)){ return(FALSE) } if (.Platform$OS.type == "windows"){ suppressWarnings(res <- system2("where", cmd, stderr = NULL, stdout = NULL)) } else { res <- tryCatch( system2("command", paste("-v", cmd), stderr = NULL, stdout = NULL), warning = function(w) {99} ) } assert(is_scalar(res)) res == 0 } fmt_bytes <- function( x ){ x <- as.numeric(x) readablifiy <- function(.x){ for (unit in c("B", "KiB", "MiB", "GiB", "TiB")){ if (max(abs(.x)) < 1024 || unit == "TiB") break else .x <- .x / 1024 } return(paste(round(.x, 1), unit)) } vapply(x, readablifiy, character(1)) } #' Logger Error Conditions #' #' @param class `character` scalar. The abstract class that was mistakenly #' tried to initialize. The default is to discover the class name #' automatically if called inside `$initialize(){...}` in an [R6::R6] class #' definition #' #' @return a condition object #' @export #' @family developer tools CannotInitializeAbstractClassError <- function( class = parent.frame(2)[["classes"]] ){ error( paste(fmt_class(class), "is an abstract class and cannot be initlized"), class = c("CannotInitializeAbstractClassError", "NotImplementedError"), call = NULL ) } # stricter & faster thant base R version & support for R < 3.5 isFALSE <- function(x){ is.logical(x) && length(x) == 1L && !is.na(x) && !x } # like utils::tail.default, but saves you from importing utils. last_n <- function(x, n){ assert( is_n0(n), "`n` must be a postive integer >= 0, not ", string_repr(n) ) len <- length(x) n <- min(n, len) x[seq.int(to = len, length.out = n)] } # conditions -------------------------------------------------------------- condition <- function(message, class, call = NULL, ...) { structure( class = union(class, "condition"), list(message = message, call = call, ...) ) } error <- function(message, class = NULL, call = NULL, ...) { structure( class = union(class, c("error", "condition")), list(message = message, call = call, ...) ) } warning_condition <- function(message, class, call = NULL, ...) { structure( class = union(class, c("warning", "condition")), list(message = message, call = call, ...) ) } # nocov end lgr/R/basic_config.R0000644000176200001440000001231315035130575013774 0ustar liggesusers#' Basic Setup for the Logging System #' #' A quick and easy way to configure the root logger. This is less powerful #' then using `lgr$config()` or `lgr$set_*()` (see [Logger]), but reduces the #' most common configurations to a single line of code. #' #' @param file `character` scalar: If not `NULL` a [AppenderFile] will be #' created that logs to this file. If the filename ends in `.jsonl`, the #' Appender will be set up to use the [JSON Lines](https://jsonlines.org/) #' format instead of plain text (see [AppenderFile] and [AppenderJson]). #' @param fmt `character` scalar: Format to use if `file` is supplied and not a #' `.jsonl` file. If `NULL` it defaults to `"%L [%t] %m"` (see #' [format.LogEvent]) #' @param console_fmt `character` scalar: like `fmt` but used for console output #' @param console_timestamp_fmt `character` scalar: like `timestamp_fmt` but #' used for console output #' @param console_connection see [cat()] `file` argument. #' @inheritParams print.LogEvent #' @inheritParams Logger #' @param appenders a single [Appender] or a list thereof. #' @param threshold `character` or `integer` scalar. The minimum [log #' level][log_levels] that should be processed by the root logger. #' @param memory `logical` scalar. or a `threshold` (see above). Add an Appender #' that logs to a memory buffer, see also [show_log()] and [AppenderBuffer] #' @param console `logical` scalar or a `threshold` (see above). Add an appender #' logs to the console (i.e. displays messages in an interactive R session) #' #' @return the `root` Logger (lgr) #' @export #' #' @examples #' # log to a file #' basic_config(file = tempfile()) #' unlink(lgr$appenders$file$file) # cleanup #' # # log to a JSON file #' basic_config(file = tempfile(fileext = "jsonl")) #' unlink(lgr$appenders$file$file) # cleanup #' #' # log debug messages to a memory buffer #' basic_config(threshold = "all", memory = "all", console = "info") #' lgr$info("an info message") #' lgr$debug("a hidden message") #' show_log() #' #' # reset to default config #' basic_config() basic_config <- function( file = NULL, fmt = "%L [%t] %m", timestamp_fmt = "%Y-%m-%d %H:%M:%OS3", threshold = "info", appenders = NULL, console = if (is.null(appenders)) "all" else FALSE, console_fmt = "%L [%t] %m %f", console_timestamp_fmt = "%H:%M:%OS3", console_connection = NULL, memory = FALSE ){ default_fmt = "%L [%t] %m" # only relevant for triggering warning when logging to json stopifnot( is.null(file) || is_scalar_character(file), is.null(fmt) || is_scalar_character(fmt), is_scalar_character(console_fmt), is_scalar_character(timestamp_fmt), is_threshold(threshold), is_scalar_bool(console) || is_threshold(console), is_scalar_bool(memory) || is_threshold(console), is.null(appenders) || is.list(appenders) || inherits(appenders, "Appender") ) l <- get_logger()$ config(NULL)$ set_threshold(threshold) if (length(appenders)){ assert( is.null(file) || !"file" %in% names(appenders), "If `appenders` contains an appender named `file`, the `file` argument to basic_config() must be `NULL`" ) assert( isFALSE(console) || !"console" %in% names(appenders), "If `appenders` contains an appender named `console`, the `console` argument to basic_config() must be `FALSE`" ) assert( isFALSE(memory) || !"memory" %in% names(appenders), "If `appenders` contains an appender named `memory`, the `memory` argument to basic_config() must be `FALSE`" ) l$set_appenders(appenders) } if (!is.null(file)){ ext <- tools::file_ext(file) if (tolower(ext) %in% c("jsonl", "json")){ if (identical(tolower(ext), "json")){ warning( "Please use `.jsonl` and not `.json` as file extension for JSON log ", "files. The reason is that that JSON files created ", "by lgr are not true JSON files but JSONlines files. ", "See https://jsonlines.org/ for more infos." ) } if (!is.null(fmt) && !identical(fmt, default_fmt)){ warning("`fmt` is ignored if `file` is a '.jsonl' file") } l$add_appender( name = "file", AppenderJson$new(file = file, threshold = NA) ) } else { l$add_appender( name = "file", AppenderFile$new( file = file, threshold = NA, layout = LayoutFormat$new( fmt = fmt, timestamp_fmt = timestamp_fmt ) ) ) } } if (!isFALSE(console)){ if (isTRUE(console)) console <- 400 l$add_appender( name = "console", AppenderConsole$new( threshold = console, connection = console_connection, layout = LayoutFormat$new( colors = getOption("lgr.colors"), fmt = console_fmt, timestamp_fmt = console_timestamp_fmt ) ) ) } if (!isFALSE(memory)){ if (isTRUE(memory)) memory <- NA l$add_appender(name = "memory", AppenderBuffer$new( threshold = memory, should_flush = NULL )) } lgr } lgr/R/Filter.R0000644000176200001440000001742015137110753012616 0ustar liggesusers# EventFilter ------------------------------------------------------------- #' Event Filters #' #' @description EventFilters specify arbitrarily complex logic for whether or #' not a LogEvent should be processed by a [Logger] or [Appender]. They are #' attached to Loggers/Appenders via their `$set_filter()` or `$add_filter()` #' methods. If any EventFilter evaluates to `FALSE` for a given event, that #' event is ignored - similarly to when it does not pass the objects' #' threshold. #' #' Usually you do not need to instantiate a formal `EventFilter` object as you #' can just use any `function` that has the single argument `event` instead. #' If you need to implement more complex filter logic - for example a filter #' that is dependent on a dataset - it might be desirable to subclass #' EventFilter, as [R6::R6] objects can store data and functions together. #' #' #' @section Modifying LogEvents with EventFilters: #' #' Since LogEvents are R6 objects with reference semantics, EventFilters can be #' abused to modify events before passing them on. lgr comes with a few #' preset filters that use this property: [FilterInject] (similar to #' [with_log_level()]) and [FilterForceLevel] (similar to [with_log_value()]). #' #' **NOTE:** The base class for Filters is called `EventFilter` so that it #' doesn't conflict with [base::Filter()]. The recommended convention for #' Filter subclasses is to call them `FilterSomething` and leave out the #' `Event` prefix. #' #' @aliases Filter #' @seealso [is_filter()] #' @export EventFilter <- R6::R6Class( "EventFilter", public = list( #' @description Initialize a new EventFilter #' @param fun a `function` with a single argument `event` that must return #' either `TRUE` or `FALSE`. Any non-`FALSE` will be interpreted as #' `TRUE` (= no filtering takes place) and a warning will be thrown. initialize = function(fun = function(event) TRUE){ self$filter <- fun }, #' @description filter The `filter` method of an event must return either #' `TRUE` or `FALSE`. If it returns `TRUE`, [Filterable]s such as #' [Appenders] or [Loggers] will continue processing the event, if `FALSE` #' the event will be discarded. filter = NULL ) ) # FilterInject ------------------------------------------------------------ #' Inject values into all events processed by a Logger/Appender #' #' @description Inject arbitrary values into all [LogEvents][LogEvent] processed #' by a Logger/Appender. It is recommended to use filters that modify LogEvents #' only with Loggers, but they will also work with Appenders. #' #' @export #' @examples #' lg <- get_logger("test") #' #' analyse <- function(){ #' lg$add_filter(FilterInject$new(type = "analysis"), "inject") #' on.exit(lg$remove_filter("inject")) #' lg$error("an error with forced custom 'type'-field") #' } #' #' analyse() #' lg$error("an normal error") #' lg$config(NULL) # reset config FilterInject <- R6::R6Class( "FilterInject", inherit = EventFilter, public = list( #' @description Initialize a new FilterInject #' @param ...,.list any number of named \R objects that will be injected as #' custom fields into all [LogEvents][LogEvent] processed by the #' Appender/Logger that this filter is attached to. See also #' [with_log_value()]. initialize = function(..., .list = list()){ vals <- list(...) assert(!is.null(names(vals)) && is_equal_length(names(vals)), vals) if (length(.list) > 0){ assert(!is.null(names(.list)) && is_equal_length(names(.list)), .list) } vals <- c(vals, .list) assert(all_are_distinct(names(vals))) self[["values"]] <- vals self$filter <- function(event){ vals <- get("values", envir = self) for (i in seq_along(vals)){ event[[names(vals)[[i]] ]] <- vals[[i]] } TRUE } }, #' @field values a named `list` of values to be injected into each #' [LogEvent] processed by this filter values = NULL ) ) # FilterForceLevel -------------------------------------------------------- #' Override the log level of all events processed by a Logger/Appender #' #' @description Overrides the log level of the Appender/Logger that this filter #' is attached to to with `level`. See also [with_log_level()]. It is #' recommended to use filters that modify LogEvents only with Loggers, but #' they will also work with Appenders. #' #' @export #' @examples #' lg <- get_logger("test") #' #' analyse <- function(){ #' lg$add_filter(FilterForceLevel$new("info"), "force") #' on.exit(lg$remove_filter("force")) #' lg$error("an error with forced log level INFO") #' } #' #' analyse() #' lg$error("an normal error") #' lg$config(NULL) # reset config FilterForceLevel <- R6::R6Class( "FilterForceLevel", inherit = EventFilter, public = list( #' @description Initialize a new FilterForceLevel #' @param level an `integer` or `character` [log level][log_level] initialize = function(level){ self[["level"]] <- standardize_log_level(level) self[["filter"]] <- function(event){ event[["level"]] <- get("level", self) TRUE } }, #' @field level an `integer` [log level][log_level] used to override the log #' levels of each [LogEvent] processed by this filter. level = NULL ) ) # utils ------------------------------------------------------------------- #' @description `.obj()` is a special function that can only be used within the #' `$filter()` methods of [EventFilters][EventFilter]. It returns the [Logger] #' or [Appender] that the EventFilter is attached to. #' #' @rdname EventFilter #' @export #' @examples #' lg <- get_logger("test") #' f <- function(event) { #' cat("via event$.logger:", event$.logger$threshold, "\n") # works for loggers only #' cat("via .obj(): ",.obj()$threshold, "\n") # works for loggers and appenders #' TRUE #' } #' lg$add_filter(f) #' lg$fatal("test") #' lg$config(NULL) .obj <- function(){ get("self", parent.env(parent.frame(2))) } #' Check if an R Object is a Filter #' #' @description Returns `TRUE` for any \R object that can be used as a Filter #' for [Loggers] or, [Appenders]: #' * a `function` with the single argument `event`; #' * an [EventFilter] [R6::R6] object; or #' * any object with a `$filter(event)` method. #' #' **Note:** A Filter **must** return a scalar `TRUE` or `FALSE`, but this #' property cannot be checked by [is_filter()]. #' #' @param x any \R Object #' @seealso [EventFilter], [Filterable] #' @return `TRUE` or `FALSE` #' #' @export is_filter <- function( x ){ if (is.function(x)){ identical(names(formals(x)), "event") } else { # the extra is.function is to prevent infinite recursions "filter" %in% names(x) && is.function(x[["filter"]]) && identical(names(formals(x[["filter"]])), c("event")) } } standardize_filters_list <- function(x){ if (is.null(x)) return(list()) if (is_filter(x)) return(list(x)) assert( is.list(x), "'filters' must be a list Filters or a single Filter (see ?is_filter)" ) for (f in x) assert_filter(f) x } assert_filter <- function(x){ if (is_filter(x)) TRUE else stop(ObjectIsNoFilterError(paste0( "`", deparse(substitute(x)), "` ", "is not a function with the single argument `event` or an EventFilter, ", "but ", string_repr(x), ". See ?is_filter." ) )) } ObjectIsNoFilterError <- function(message = "Object is not an EventFilter. See ?is_filter."){ error(message, "ObjectIsNoFilterError") } lgr/R/utils-rotor.R0000644000176200001440000000063214131760166013673 0ustar liggesusersassert_valid_compression <- function(compression){ assert( is_scalar_atomic(compression) && ( compression %in% c("base::zip", "zip::zipr") || compression %in% 1:9 || is_bool(compression) ), '`compression` must be `TRUE`, `FALSE`, or an integer between 1 and 9', 'or the character scalers "base::zip" or "zip::zipr" not: ', string_repr(compression) ) } lgr/R/LogEvent.R0000644000176200001440000004145715036467073013133 0ustar liggesusers# LogEvent ---------------------------------------------------------------- #' LogEvents - The atomic unit of logging #' #' @description #' A `LogEvent` is a single unit of data that should be logged. `LogEvents` are #' usually created by a [Logger], and then processed by one more [Appenders]. #' They do not need to be instantiated manually except for testing and #' experimentation; however, if you plan on writing your own Appenders or #' Layouts you need to understand LogEvents. #' #' @seealso [as.data.frame.LogEvent()] #' @family docs relevant for extending lgr #' @aliases LogEvents #' @examples #' lg <- get_logger("test") #' lg$error("foo bar") #' #' # The last LogEvent produced by a Logger is stored in its `last_event` field #' lg$last_event # formatted console output #' lg$last_event$values # values stored in the event #' #' # Also contains the Logger that created it as .logger #' lg$last_event$logger #' # equivalent to #' lg$last_event$.logger$name #' #' # This is really a reference to the complete Logger, so the following is #' # possible (though nonsensical) #' lg$last_event$.logger$last_event$msg #' identical(lg, lg$last_event$.logger) #' lg$config(NULL) # reset logger config #' @export LogEvent <- R6::R6Class( "LogEvent", lock_objects = FALSE, public = list( #' @description #' The arguments to `LogEvent$new()` directly translate to the fields stored #' in the `LogEvent`. Usually these values will be scalars, but (except for #' `"logger"`) they can also be vectors if they are all of the same length (or #' scalars that will be recycled). In this case the event will be treated by #' the [Appenders] and [Layouts] as if several separate events. #' #' @param ... All named arguments in `...` will be added to the LogEvent #' as **custom fields**. You can store arbitrary \R objects in LogEvents #' this way, but not all Appenders will support them. See [AppenderJson] for #' @param logger,level,timestamp,caller,msg see **Public fields**. initialize = function( logger, level = 400, timestamp = Sys.time(), caller = NA, msg = NA, rawMsg = msg, ... ){ assert(inherits(logger, "Logger"), "Logger must be a object, not a ", class_fmt(logger)) # assign has less overhead than [[ and event creation needs to be as fast # as possible assign(".logger", logger, self) assign("level", level, self) assign("timestamp", timestamp, self) assign("caller", caller, self) assign("msg", msg, self) assign("rawMsg", rawMsg, self) # custom values if (!missing(...)){ dots <- list(...) assert(identical(length(names(dots)), length(dots))) # the rev() ensures that the values get added int eh same order as # the user entered them for (nm in rev(names(dots))){ assign(nm, dots[[nm]], self) } } }, #' @field level `integer`. The [log_level] / priority of the LogEvent. Use the #' active binding `level_name` to get the `character` representation #' instead. level = NULL, #' @field timestamp [`POSIXct`][base::POSIXct]. The time when then the #' LogEvent was created. timestamp = NULL, #' @field caller `character`. The name of the calling function. caller = NULL, #' @field msg `character`. The log message. msg = NULL, #' @field .logger [Logger]. A reference to the Logger that created the #' event (equivalent to `get_logger(event$logger)`). .logger = NULL, #' @field rawMsg `character`. The raw log message without string #' interpolation. rawMsg = NULL ), active = list( #' @field values `list`. All values stored in the `LogEvent`, including #' all *custom fields*, but not including `event$.logger`. values = function(){ fixed_vals <- c("level", "timestamp", "logger", "caller", "msg", "rawMsg") custom_vals <- setdiff( names(get(".__enclos_env__", self)[["self"]]), c(".__enclos_env__", "level_name", "initialize", "clone", "values", ".logger") ) valnames <- union(fixed_vals, custom_vals) # to enforce order of fixed_vals mget(valnames, envir = self) }, #' @field level_name `character`. The [log_level] / priority of the LogEvent labelled #' according to `getOption("lgr.log_levels")` level_name = function(){ label_levels(get("level", envir = self)) }, #' @field logger `character` scalar. The name of the Logger that #' created this event, equivalent to `event$.logger$name`) logger = function(){ get("name", envir = get(".logger", envir = self)) } ) ) # coercion --------------------------------------------------- #' Coerce objects to LogEvent #' #' Smartly coerce \R objects that look like LogEvents to LogEvents. Mainly #' useful for developing Appenders. #' #' **Note**: `as_LogEvent.data.frame()` only supports single-row `data.frames` #' #' @param x any supported \R object #' @param ... currently ignored #' #' @return a [LogEvent] #' @family docs relevant for extending lgr #' @export as_LogEvent <- function(x, ...){ UseMethod("as_LogEvent") } #' @rdname as_LogEvent #' @export as_LogEvent.list <- function(x, ...){ if (is.null(x[["logger"]])){ x[["logger"]] <- get_logger() } else if (is.character(x[["logger"]])){ x[["logger"]] <- get_logger(x[["logger"]]) } # smartly rename timestamp fields from ElasticSearch/Logstash if (!"timestamp" %in% names(x) && "@timestamp" %in% names(x)){ names(x)[names(x) == "@timestamp"] <- "timestamp" } x[["level"]] <- standardize_log_level(x[["level"]]) do.call(LogEvent$new, x) } #' @rdname as_LogEvent #' @export as_LogEvent.data.frame <- function( x, ... ){ assert( identical(nrow(x), 1L), "`as_LogEvent()` only supports single-row data.frames. Try `as_event_list()` instead" ) as_LogEvent(unclass(x)) } #' Coerce LogEvents to Data Frames #' #' Coerce LogEvents to `data.frames`, [`data.tables`][data.table::data.table], #' or [`tibbles`][tibble::tibble]. #' #' @inheritParams base::as.data.frame #' @param stringsAsFactors `logical` scalar: should `character` vectors be #' converted to factors? Defaults to `FALSE` (as opposed to #' [base::as.data.frame()]) and is only included for compatibility. #' @param ... passed on to `data.frame()` #' @param optional currently ignored and only included for compatibility. #' @param box_if a `function` that returns `TRUE` or `FALSE` to determine #' which values are to be boxed (i.e. placed as single elements in a list #' column). See example #' @param cols_expand `character` vector. Columns to *not* box (even if #' `box_if()` returns `TRUE`). Vectors in these columns will result in multiple #' rows in the result (rather than a single list-column row). This defaults to #' `"msg"` for vectorized logging over the log message. #' @export #' @seealso [data.table::data.table], [tibble::tibble] #' #' @examples #' lg <- get_logger("test") #' lg$info("lorem ipsum") #' as.data.frame(lg$last_event) #' #' lg$info("LogEvents can store any custom log values", df = iris) #' as.data.frame(lg$last_event) #' head(as.data.frame(lg$last_event)$df[[1]]) #' #' # how boxing works #' #' # by default non-scalars are boxed #' lg$info("letters", letters = letters) #' as.data.frame(lg$last_event) #' #' # this behaviour can be modified by supplying a custom boxing function #' as.data.frame(lg$last_event, box_if = function(.) FALSE) #' as.data.frame(lg$last_event, cols_expand = "letters") #' #' # The `msg` argument of a log event is always vectorized #' lg$info(c("a vectorized", "log message")) #' as.data.frame(lg$last_event) #' #' lg$config(NULL) as.data.frame.LogEvent <- function( x, row.names = NULL, optional = FALSE, stringsAsFactors = FALSE, ..., box_if = function(.) !(is.atomic(.) && identical(length(.), 1L)), cols_expand = NULL ){ assert(is.null(cols_expand) || is.character(cols_expand)) boxer <- function(.) I(list(.)) values <- do_box_if(x$values, box_if, except = cols_expand, boxer = boxer) msg_len <- length(x[["msg"]]) cols_expand <- c("msg", cols_expand) # as.data.frame requires manual recycling if (msg_len > 1){ for (i in seq_along(values)){ if (identical(length(values[[i]]), 1L)) values[[i]] <- rep(values[[i]], msg_len) } values <- do_box_if(values, box_if, except = cols_expand, boxer = I) } do.call( data.frame, c(values, stringsAsFactors = stringsAsFactors, row.names = row.names, ... ) ) } #' @rdname as.data.frame.LogEvent as.data.table.LogEvent <- function( x, ..., box_if = function(.) !(is.atomic(.) && identical(length(.), 1L)), cols_expand = "msg" ){ values <- do_box_if(x$values, box_if, cols_expand) data.table::as.data.table(values) } #' @rdname as.data.frame.LogEvent as_tibble.LogEvent <- function( x, ..., box_if = function(.) !(is.atomic(.) && identical(length(.), 1L)), cols_expand = "msg" ){ values <- do_box_if(x$values, box_if, cols_expand) tibble::as_tibble(values) } # printing --------------------------------------------------- #' Print or Format Logging Data #' #' @param x a [LogEvent] #' @param timestamp_fmt see [format.POSIXct()] #' @param fmt A `character` scalar that may contain any of the tokens listed #' bellow in the section Format Tokens. #' @param colors A `list` of `functions` that will be used to color the #' log levels (likely from [crayon::crayon]). #' @inheritParams standardize_threshold #' @param pad_levels `right`, `left` or `NULL`. Whether or not to pad the log #' level names to the same width on the left or right side, or not at all. #' @param excluded_fields a `character` vector of fields to exclude from `%j` and #' `%f` #' @param ... ignored #' #' @section Format Tokens: #' \describe{ #' \item{`%t`}{The timestamp of the message, formatted according to #' `timestamp_fmt`)} #' \item{`%l`}{the log level, lowercase `character` representation} #' \item{`%L`}{the log level, uppercase `character` representation} #' \item{`%k`}{the log level, first letter of lowercase `character` representation} #' \item{`%K`}{the log level, first letter of uppercase `character` representation} #' \item{`%n`}{the log level, `integer` representation} #' \item{`%g`}{the name of the logger} #' \item{`%p`}{the PID (process ID). Useful when logging code that uses #' multiple threads.} #' \item{`%c`}{the calling function} #' \item{`%m`}{the log message} #' \item{`%r`}{the raw log message (without string interpolation)} #' \item{`%f`}{all custom fields of `x` in a pseudo-JSON like format that is #' optimized for human readability and console output} #' \item{`%j`}{all custom fields of `x` in proper JSON. This requires that you #' have **jsonlite** installed and does not support colors as opposed to #' `%f` #' } #' } #' #' @return `x` for `print()` and a `character` scalar for `format()` #' @export #' #' @examples #' # standard fields can be printed using special tokens #' x <- LogEvent$new( #' level = 300, msg = "a test event", caller = "testfun()", logger = lgr #' ) #' print(x) #' print(x, fmt = c("%t (%p) %c: %n - %m")) #' print(x, colors = NULL) #' #' # custom values #' y <- LogEvent$new( #' level = 300, msg = "a gps track", logger = lgr, #' waypoints = 10, location = "Austria" #' ) #' #' # default output with %f #' print(y) #' #' # proper JSON output with %j #' if (requireNamespace("jsonlite")){ #' print(y, fmt = "%L [%t] %m %j") #' } #' print.LogEvent <- function( x, fmt = "%L [%t] %m %f", timestamp_fmt = "%Y-%m-%d %H:%M:%S", colors = getOption("lgr.colors"), log_levels = getOption("lgr.log_levels"), pad_levels = "right", excluded_fields = NULL, ... ){ cat(format( x, fmt = fmt, timestamp_fmt = timestamp_fmt, colors = colors, log_levels = log_levels, pad_levels = pad_levels, excluded_fields = excluded_fields ), sep = "\n") invisible(x) } #' @rdname print.LogEvent #' @export format.LogEvent <- function( x, fmt = "%L [%t] %m %f", timestamp_fmt = "%Y-%m-%d %H:%M:%S", colors = NULL, log_levels = getOption("lgr.log_levels"), pad_levels = "right", excluded_fields = NULL, ... ){ stopifnot( is_scalar_character(fmt), is_scalar_character(timestamp_fmt), is_scalar_character(pad_levels) || is.null(pad_levels) ) # init lvls <- label_levels(x$level, log_levels = log_levels) lvls[is.na(lvls)] <- x$level[is.na(lvls)] if (!is.null(pad_levels)){ nchar_max <- max(nchar(names(log_levels))) diff <- nchar_max - nchar(lvls) pad <- vapply(diff, function(i) paste(rep.int(" ", i), collapse = ""), character(1)) if (pad_levels == "right"){ lvls <- paste0(lvls, pad) } else { lvls <- paste0(pad, lvls) } } else { lvls <- x$level } # tokenize tokens <- tokenize_format( fmt, valid_tokens = paste0( "%", c("t", "p", "c", "m", "r", "l", "L", "n", "f", "j", "k", "K", "g")) ) # format len <- length(tokens) res <- vector("list", length(tokens)) for(i in seq_len(len)){ res[[i]] <- switch( tokens[[i]], "%n" = colorize_levels(x$level, colors), "%l" = colorize_levels(lvls, colors), "%L" = colorize_levels(toupper(lvls), colors), "%k" = colorize_levels(lvls, colors, transform = function(.) strtrim(., 1)), "%K" = colorize_levels(lvls, colors, transform = function(.) toupper(strtrim(., 1))), "%t" = format(get("timestamp", envir = x), format = timestamp_fmt), "%m" = get("msg", envir = x), "%r" = get("rawMsg", envir = x), "%c" = get("caller", envir = x), "%g" = get("logger", envir = x), "%p" = Sys.getpid(), "%f" = format_custom_fields(get_custom_fields(x, excluded_fields), color = length(colors)), "%j" = format_custom_fields_json(get_custom_fields(x, excluded_fields)), tokens[[i]] ) } sub("[ \t\r]*$", "", do.call(paste0, res)) } format_custom_fields_json <- function(x){ if (length(x)){ jsonlite::toJSON(x, auto_unbox = TRUE) } else { "" } } #' Convert a LogEvent to a character string #' #' @param x a [LogEvent] #' @param ... ignored #' #' @return a `character` scalar #' @export #' #' @examples #' toString(LogEvent$new(logger = lgr::lgr)) toString.LogEvent <- function(x, ...){ paste( paste0("$", names(x$values), ": ", vapply(x$values, function(.) string_repr(.), character(1L))), collapse = ", " ) } # utils ------------------------------------------------------------------- do_box_if <- function(x, predicate, except, boxer = list){ sel <- vapply(x, predicate, TRUE, USE.NAMES = FALSE) if (length(except)){ sel[(names(x) %in% except)] <- FALSE } for (i in seq_along(x)) if (sel[[i]]) x[[i]] <- boxer(x[[i]]) x } get_custom_fields <- function(x, excluded_fields = NULL){ x$values[!names(x$values) %in% c(DEFAULT_FIELDS, excluded_fields)] } format_custom_fields <- function( x, color = TRUE ){ if (!length(x)) return("") max_len <- floor(max(512 / length(x) - sum(nchar(names(x))), 16)) braces <- c("{", "}") brackets <- c("[", "]") colon <- ": " comma <- ", " dots <- ".." if (!color){ style_subtle <- identity style_accent <- identity } else { braces <- style_subtle(braces) colon <- style_subtle(colon) comma <- style_subtle(comma) } res <- lapply( x, string_repr, width = max_len ) paste0( braces[[1L]], paste( style_accent(names(res)), colon, res, sep = "", collapse = comma ), braces[[2L]] ) } tokenize_format <- function( x, valid_tokens = NULL ){ pos <- unlist(gregexpr("%.", x)) if (identical(pos, -1L)) return(x) pos <- sort(unique(c(1L, pos, pos + 2L, nchar(x) + 1L))) res <- vector("character", length(x)) for(i in seq_len(length(pos) - 1L)) { res[[i]] <- substr(x, pos[[i]], pos[[i + 1L]] - 1L) } if (!is.null(valid_tokens)){ placeholders <- grep("%", res, value = TRUE, fixed = TRUE) assert( all(placeholders %in% valid_tokens), "'format' contains unrecognised format specifications: ", paste(sort(setdiff(placeholders, valid_tokens)), collapse = ", ") ) } res } # globals -------------------------------------------------------- DEFAULT_FIELDS <- c("level", "timestamp", "logger", "caller", "msg", "rawMsg") lgr/R/utils-logging.R0000644000176200001440000000770314131760166014162 0ustar liggesusers#' Suspend All Logging #' #' Completely disable logging for all loggers. This is for example useful for #' automated test code. `suspend_logging()` globally disables all logging with #' lgr until `unsuspend_logging()` is invoked, while `without_logging()` and #' `with_logging()` temporarily disable/enable logging. #' #' @return #' `suspend_logging()` and `unsuspend_logging()` return `NULL` (invisibly), #' `without_logging()` and `with_logging()` returns whatever `code` returns. #' @export #' @examples #' lg <- get_logger("test") #' #' # temporarily disable logging #' lg$fatal("foo") #' without_logging({ #' lg$info("everything in this codeblock will be suppressed") #' lg$fatal("bar") #' }) #' #' # globally disable logging #' suspend_logging() #' lg$fatal("bar") #' with_logging(lg$fatal("foo")) # log anyways #' #' # globally enable logging again #' unsuspend_logging() #' lg$fatal("foo") suspend_logging <- function(){ options("lgr.logging_suspended" = TRUE) invisible() } #' @rdname suspend_logging #' @export unsuspend_logging <- function(){ options("lgr.logging_suspended" = FALSE) invisible() } #' @rdname suspend_logging #' @param code Any \R code #' @export without_logging <- function(code){ old <- getOption("lgr.logging_suspended") on.exit(options(lgr.logging_suspended = old)) suspend_logging() force(code) } #' @rdname suspend_logging #' @export with_logging <- function(code){ old <- getOption("lgr.logging_suspended") on.exit(options(lgr.logging_suspended = old)) unsuspend_logging() force(code) } #' Inject Values into Logging Calls #' #' `with_log_level` temporarily overrides the log level of all [LogEvents] #' created by target [Logger]. #' #' These functions abuses lgr's filter mechanic to modify LogEvents in-place #' before they passed on the Appenders. Use with care as they can #' produce hard to reason about code. #' #' @param level `integer` or `character` scalar: the desired log level #' @param code Any \R code #' @param logger a [Logger] or the name of one (see [get_logger()]). Defaults #' to the root logger (`lgr::lgr`). #' #' @return whatever `code` would return #' @export #' @examples #' with_log_level("warn", { #' lgr$info("More important than it seems") #' lgr$fatal("Really not so bad") #' }) with_log_level <- function( level, code, logger = lgr::lgr ){ if (is_scalar_character(logger)){ logger <- get_logger(logger) } assert(is_Logger(logger)) level <- standardize_log_level(level) force(level) set_level <- function(event){ event[["level"]] <- level TRUE } # to make it unlikely something goes wrong if people do funky stuff with # filters inside with_log_level calls tn <- paste0("...WITH_LOG_LEVEL_TEMP_FILTER", sample.int(1e9, size = 1)) logger$add_filter(set_level, name = tn) on.exit(logger$remove_filter(tn)) force(code) } #' `with_log_value()` injects arbitrary values into all [LogEvents] (overriding #' existing ones). This is especially powerful in combination with Appenders #' that support arbitrary log fields, like [AppenderJson]. #' #' @param values a named `list` of values to be injected into the logging calls #' @rdname with_log_level #' @export #' @examples #' with_log_value( #' list(msg = "overriden msg"), { #' lgr$info("bar") #' lgr$fatal("FOO") #' }) with_log_value <- function( values, code, logger = lgr::lgr ){ if (is_scalar_character(logger)){ logger <- get_logger(logger) } assert(is_Logger(logger)) assert(is_equal_length(names(values), values)) set_level <- function(event){ for (i in seq_along(values)){ event[[names(values)[[i]] ]] <- values[[i]] } TRUE } tn <- paste0("...WITH_LOG_VALUE_TEMP_FILTER", sample.int(1e9, size = 1)) logger$add_filter(set_level, name = tn) on.exit(logger$remove_filter(tn)) force(code) } lgr/R/Appender.R0000644000176200001440000012105515137111272013124 0ustar liggesusers# Appender ---------------------------------------------------------------- #' Appenders #' #' @description Appenders are attached to [Loggers] and manage the output of the #' [LogEvents] to a destination - such as the console or a text file. An #' Appender has a single [Layout] that tells it how to format the LogEvent. For #' details please refer to the documentations of the specific Appenders. #' #' Additional Appenders that support a wide range of output destinations - #' such as databases, email, push-notifications or Linux syslog - are available #' from the package [lgrExtra](https://github.com/s-fleck/lgrExtra). #' #' @template abstract_class #' #' @aliases Appenders #' @family Appenders #' @include utils.R #' @include utils-sfmisc.R #' @include Filterable.R #' @keywords internal #' @export Appender <- R6::R6Class( "Appender", inherit = Filterable, cloneable = FALSE, # +- public -------------------------------------------------------------- public = list( initialize = function( layout = Layout$new(), threshold = NA_integer_ ){ self$set_layout(layout) self$set_threshold(threshold) }, #' @description Process a [LogEvent] `event`. This method is usually not #' called by the user, but invoked by a [Logger] append = function(event){ private$.layout$format_event(event) }, #' @description Set the minimum log level that triggers this Appender. See #' [threshold()] for examples #' @param level `character` or `integer` scalar log level. See [log_levels]. set_threshold = function(level){ level <- standardize_threshold(level) private$.threshold <- as.integer(level) invisible(self) }, #' @description Set the `Layout` that this Appender will use for formatting #' `LogEvents` set_layout = function(layout){ if (is_scalar_list(layout)){ # set_layout also accepts a list of length 1 containing a single Layout # object. This is a workaround for resolve_r6_ctors. This behaviour is # intentionally not documented and you should not rely on it. layout <- layout[[1]] } assert( inherits(layout, "Layout"), "`layout` must a `Layout` object, but is ", string_repr(layout) ) private$.layout <- layout invisible(self) }, format = function( color = FALSE, ... ){ assert(is_scalar_bool(color)) if (!color) style_subtle <- identity thr <- fmt_threshold(self$threshold, type = "character") header <- paste( paste0("<", class(self)[[1]], "> [", thr, "]") ) if (!is.null(self$layout)){ res <- c( header, paste0(" layout: ", self$layout$toString()) ) } if (!is.null(self$destination)){ res <- c( res, paste(" destination:", self$destination) ) } if (!color) style_error <- identity if (class(self)[[1]] == "Appender"){ paste(res[[1]], style_error("[abstract class]")) } else { res } } ), # +- active --------------------------------------------------------------- active = list( threshold = function() private$.threshold, layout = function() private$.layout, #' @field destination #' The output destination of the `Appender` in human-readable form. This #' is mainly used when printing information about the Appender itself. destination = function() "" ), private = list( .threshold = NA, .layout = NULL ) ) # AppenderConsole --------------------------------------------------------- #' Log to the console #' #' An Appender that outputs to the \R console. If you have the package #' **crayon** installed log levels will be coloured by default #' (but you can modify this behaviour by passing a custom [Layout]). #' #' @param connection A connection or a `character` scalar. See the `file` #' argument of [cat()] for more details. Defaults to [stdout()], except #' inside \pkg{knitr} rendering processes where it defaults to [stderr()]. #' #' @family Appenders #' @seealso [LayoutFormat] #' @export #' #' @examples #' # create a new logger with propagate = FALSE to prevent routing to the root #' # logger. Please look at the section "Logger Hirarchies" in the package #' # vignette for more info. #' lg <- get_logger("test")$set_propagate(FALSE) #' #' lg$add_appender(AppenderConsole$new()) #' lg$add_appender(AppenderConsole$new( #' layout = LayoutFormat$new("[%t] %c(): [%n] %m", colors = getOption("lgr.colors")))) #' #' # Will output the message twice because we attached two console appenders #' lg$warn("A test message") #' lg$config(NULL) # reset config AppenderConsole <- R6::R6Class( "AppenderConsole", inherit = Appender, cloneable = FALSE, public = list( initialize = function( threshold = NA_integer_, layout = LayoutFormat$new( fmt = "%L [%t] %m %f", timestamp_fmt = "%H:%M:%OS3", colors = getOption("lgr.colors", list()) ), filters = NULL, connection = NULL ){ self$set_threshold(threshold) self$set_layout(layout) self$set_filters(filters) if (is.null(connection)) { connection <- default_console_connection() } self$set_connection(connection) }, append = function(event){ cat(private$.layout$format_event(event), sep = "\n", file = get(".connection", private)) }, set_connection = function(connection){ assert( inherits(connection, "connection") || is_scalar_character(connection) ) private[[".connection"]] <- connection } ), active = list( destination = function() "console", connection = function() private[[".connection"]] ), private = list( .connection = NULL ) ) default_console_connection <- function() { # In knitr, default to using stderr where log messages can be better handled if (isTRUE(getOption("knitr.in.progress", FALSE))) { return(stderr()) } # has to be this, not a call to `stdout()` "" } # AppenderFile ------------------------------------------------------------ #' Log to a file #' #' @description #' A simple Appender that outputs to a file in the file system. If you plan #' to log to text files, consider logging to JSON files and take a look at #' [AppenderJson], which is a shortcut for `AppenderFile` preconfigured with #' [`LayoutJson`]. #' #' #' @export #' @seealso [LayoutFormat], [LayoutJson] #' @family Appenders #' @name AppenderFile #' #' @examples #' lg <- get_logger("test") #' default <- tempfile() #' fancy <- tempfile() #' json <- tempfile() #' #' lg$add_appender(AppenderFile$new(default), "default") #' lg$add_appender( #' AppenderFile$new(fancy, layout = LayoutFormat$new("[%t] %c(): %L %m")), "fancy" #' ) #' lg$add_appender( #' AppenderFile$new(json, layout = LayoutJson$new()), "json" #' ) #' #' lg$info("A test message") #' #' readLines(default) #' readLines(fancy) #' readLines(json) #' #' # cleanup #' lg$config(NULL) #' unlink(default) #' unlink(fancy) #' unlink(json) AppenderFile <- R6::R6Class( "AppenderFile", inherit = Appender, cloneable = FALSE, public = list( initialize = function( file, threshold = NA_integer_, layout = LayoutFormat$new(), filters = NULL ){ self$set_file(file) self$set_threshold(threshold) self$set_layout(layout) self$set_filters(filters) }, append = function(event){ cat( private$.layout$format_event(event), sep = "\n", file = get(".file", envir = private), append = TRUE ) }, #' @description Set a log file #' @param file `character` scalar. Path to the log file. If `file` does not #' exist it will be created. set_file = function(file){ assert( is_scalar_character(file), "`file` must be character scalar, not: ", string_repr(file) ) assert( dir.exists(dirname(file)), "Cannot create file: directory '", dirname(file), "' does not exist." ) private$.file <- file if (!file.exists(file)) { assert( file.create(file, showWarnings = FALSE), "Cannot create file '", file , "'" )} invisible(self) }, #' @description Display the contents of the log file. #' #' @param threshold `character` or `integer` scalar. The minimum log level #' that should be displayed. #' @param n `integer` scalar. Show only the last `n` log entries that match #' `threshold`. show = function( threshold = NA_integer_, n = 20L ){ assert(is_scalar_integerish(n)) threshold <- standardize_threshold(threshold) reader <- self$layout$read %||% default_file_reader dd <- reader(self$file, threshold = threshold, n = n) cat(dd, sep = "\n") invisible(dd) } ), # +- active --------------------------------------------------------------- active = list( #' @field file `character` scalar. path to the log file file = function() private$.file, #' @field data `data.frame`. Contents of `file` parsed to a #' `data.frame` if used with a [Layout] that supports parsing of log #' file data (notably [LayoutJson]). Will throw an error if `Layout` does #' not support parsing. data = function(){ parser <- self$layout$parse assert( !is.null(parser), CannotParseLogError(paste( "Neither", fmt_class(class(self)[[1]]), "nor", fmt_class(class(self$layout)[[1]]), "support parsing log files to data.frames. Consider using ", "instead." )) ) parser(self$file) }, #' @field data `character` scalar. Like `$data`, but returns a `data.table` #' instead (requires the **data.table** package). dt = function(){ data.table::as.data.table(self$data) }, destination = function() self$file ), private = list( .file = NULL ) ) # AppenderJson ------------------------------------------------------------ #' Log to a JSON file #' #' @rdname AppenderFile #' #' @family Appenders #' @export #' @seealso [LayoutFormat], [LayoutJson] #' @examples #' tf <- tempfile() #' lg <- get_logger("test")$ #' set_appenders(AppenderJson$new(tf))$ #' set_propagate(FALSE) #' #' lg$info("A test message") #' lg$info("A test message %s strings", "with format strings", and = "custom_fields") #' #' lg$appenders[[1]]$show() #' lg$appenders[[1]]$data #' #' # cleanup #' lg$config(NULL) #' unlink(tf) AppenderJson <- R6::R6Class( "AppenderJson", inherit = AppenderFile, cloneable = FALSE, public = list( initialize = function( file, threshold = NA_integer_, layout = LayoutJson$new(), filters = NULL ){ assert_namespace("jsonlite") self$set_file(file) self$set_threshold(threshold) self$set_layout(layout) self$set_filters(filters) } ) ) # AppenderTable ----------------------------------------------------------- # nocov start #' Abstract class for logging to tabular structures #' #' @template abstract_class #' #' @description AppenderTable is extended by Appenders that write to a data #' source that can be interpreted as tables, (usually a `data.frame`). Examples #' are `AppenderDbi`, `AppenderRjdbc` and `AppenderDt` from the #' [lgrExtra](https://github.com/s-fleck/lgrExtra) package. #' #' @family Appenders #' @export AppenderTable <- R6::R6Class( "AppenderTable", inherit = Appender, cloneable = FALSE, public = list( initialize = function(...){ stop(CannotInitializeAbstractClassError()) }, #' @description Show recent log entries #' @param n a positive `integer` scalar. Show at most that many entries #' @param threshold an `integer` or `character` [threshold][log_level]. #' Only show events with a log level at or below this threshold. show = function(threshold = NA_integer_, n = 20L) NULL, format = function( color = FALSE, ... ){ res <- super$format(color = color, ...) if (!color) style_error <- identity if (class(self)[[1]] == "AppenderTable"){ paste(res[[1]], style_error("[abstract class]")) } else { res } } ), active = list( #' @field data `character` scalar. Contents of the table, parsed to a #' `data.frame`. data = function() as.data.frame(self$dt), #' @field data `character` scalar. Like `$data`, but returns a `data.table` #' instead (requires the **data.table** package). dt = function() data.table::as.data.table(self$data) ) ) # nocov end # AppenderMemory -------------------------------------------------- #' Abstract class for logging to memory buffers #' #' @template abstract_class #' #' @description #' AppenderMemory is extended by Appenders that retain an in-memory event #' buffer, such as [AppenderBuffer] and `AppenderPushbullet` from the #' [lgrExtra](https://github.com/s-fleck/lgrExtra) package. #' #' @export #' @seealso [LayoutFormat] #' #' @export AppenderMemory <- R6::R6Class( "AppenderMemory", inherit = Appender, cloneable = FALSE, public = list( initialize = function(...){ stop(CannotInitializeAbstractClassError()) }, append = function(event){ i <- get("insert_pos", envir = private) + 1L assign("insert_pos", i, envir = private, inherits = TRUE) private[["last_event"]] <- private[["last_event"]] + 1L private[["event_order"]][[i]] <- private[["last_event"]] private[[".buffer_events"]][[i]] <- event # check if buffer should be flushed if ({ # buffer is rotated i > get(".buffer_size", envir = private) && get(".flush_on_rotate", envir = private) } || { # flush threshold is exceeded thr <- get(".flush_threshold", envir = private) !is.null(thr) && (is.na(thr) || all(event[["level"]] <= thr)) } || { # should_flush() function triggers standardize_should_flush_output( get(".should_flush", envir = private)(event) ) }){ self$flush() } else if (i > get(".buffer_size", envir = private)){ # check if flush should be triggered if buffer size is exceeded # sepparetly from other conditions. Must be below all other flush # conditions in case this causes the buffer to be cleared but not flushed. if (get(".flush_on_rotate", envir = private) ){ self[["flush"]]() } else { # does not clear the buffer. this is intentional assign("insert_pos", 0L, envir = private) } } }, #' @description Sends the buffer's contents to all attached Appenders and #' then clears the Buffer flush = function(){self}, #' @description Clears the buffer, discarding all buffered Events clear = function(){self}, #' @description Set the maximum size of the buffer #' @param x an `integer` scalar `>= 0`. Number of [LogEvents] to buffer. set_buffer_size = function(x){ assert(is_n0(x)) private$.buffer_size <- x invisible(self) }, #' @description Set function that can trigger flushing the buffer #' @param x A `function` with the single argument `event`. Setting `x` to #' `NULL` is a shortcut for `function(event) FALSE`. See active bindings. set_should_flush = function(x){ if (is.null(x)) x <- function(event) FALSE assert_filter(x) private$.should_flush <- x invisible(self) }, #' @description Should the buffer be flushed when the Appender is destroyed? #' @param x A `logical` scalar. See active bindings. set_flush_on_exit = function(x){ assert(is_scalar_bool(x)) private$.flush_on_exit <- x invisible(self) }, #' @description Should the buffer be flushed if `buffer_size` is exceeded? #' @param x A `logical` scalar. See active bindings. set_flush_on_rotate = function(x){ assert(is_scalar_bool(x)) private$.flush_on_rotate <- x invisible(self) }, #' @description Set threshold that triggers flushing #' @param level A `numeric` or `character` [threshold][log_level]. See #' active bindings. set_flush_threshold = function(level){ if (!is.null(level)) level <- standardize_threshold(level) private$.flush_threshold <- level invisible(self) }, #' @description Display the contents of the log table. Relies on the #' `$format_event` method of the [Layout] attached to this Appender. #' #' @param threshold `character` or `integer` scalar. The minimum log level #' that should be displayed. #' @param n `integer` scalar. Show only the last `n` log entries that match #' `threshold`. show = function(threshold = NA_integer_, n = 20L){ assert(is_scalar_integerish(n)) threshold <- standardize_threshold(threshold) if (is.na(threshold)) threshold <- Inf dd <- get("buffer_dt", envir = self) if (identical(nrow(dd), 0L)){ cat("[empty log]") return(invisible(NULL)) } dd <- tail(dd[dd$level <= threshold, ], n) dd <- as.environment(dd) assign("logger", self[[".logger"]], dd) cat(self$layout$format_event(dd), sep = "\n") invisible(dd) }, format = function( color = FALSE, ... ){ res <- super$format(color = color, ...) if (!color) style_error <- identity if (class(self)[[1]] == "AppenderMemory"){ paste(res[[1]], style_error("[abstract class]")) } else { res } } ), # +- active --------------------------------------------------------------- active = list( #' @field flush_on_exit A `logical` scalar. Should the buffer be flushed if #' the Appender is destroyed (e.g. because the \R session is terminated)? flush_on_exit = function() { get(".flush_on_exit", private) }, #' @field flush_on_rotate A `logical` scalar. Should the buffer be flushed when it is #' rotated because `$buffer_size` is exceeded? flush_on_rotate = function() { get(".flush_on_rotate", private) }, #' @field should_flush A `function` with exactly one arguments: `event`. #' `$append()` calls this function internally on the current [LogEvent] #' and flushes the buffer if it evaluates to `TRUE`. should_flush = function(){ get(".should_flush", private) }, #' @field buffer_size `integer` scalar `>= 0`. Maximum number of [LogEvents] #' to buffer. buffer_size = function() { get(".buffer_size", private) }, #' @field flush_threshold A `numeric` or `character` threshold. [LogEvents] #' with a [log_level] equal to or lower than this threshold trigger #' flushing the buffer. flush_threshold = function(){ get(".flush_threshold", private) }, #' @field buffer_events A `list` of [LogEvents]. Contents of the buffer. buffer_events = function() { res <- get(".buffer_events", envir = private) if (length(res)){ ord <- get("event_order", envir = private) ord <- ord - min(ord) + 1L ord <- order(ord) res <- res[ord] } as_event_list( res[!vapply(res, is.null, FALSE)] ) }, data = function(){ self$buffer_df }, dt = function(){ self$buffer_dt }, #' @field buffer_events A `data.frame`. Contents of the buffer converted #' to a `data.frame`. buffer_df = function() { as.data.frame(self[["buffer_dt"]]) }, #' @field buffer_events A `data.frame`. Contents of the buffer converted #' to a `data.table`. buffer_dt = function(){ assert_namespace("data.table") as.data.table.event_list(get("buffer_events", self)) } ), # +- private --------------------------------------------------------- private = list( initialize_buffer = function(buffer_size){ assert(is_n0(buffer_size)) private[["insert_pos"]] <- 0L private[["last_event"]] <- 0L private[["event_order"]] <- seq_len(buffer_size) private[[".buffer_size"]] <- buffer_size private[[".buffer_events"]] <- list() }, insert_pos = NULL, last_event = NULL, event_order = NULL, .flush_threshold = NULL, .should_flush = NULL, .flush_on_exit = NULL, .flush_on_rotate = NULL, .buffer_size = NULL, .buffer_events = NULL ) ) # AppenderBuffer -------------------------------------------------- #' Log to a memory buffer #' #' @description #' An Appender that Buffers LogEvents in-memory and and redirects them to other #' Appenders once certain conditions are met. #' #' @section Fields: #' #' \describe{ #' \item{`appenders`, `set_appenders()`}{Like for a [Logger]. Buffered events #' will be passed on to these Appenders once a flush is triggered} #' \item{`flush_on_exit, set_flush_on_exit(x)`}{`TRUE` or `FALSE`: Whether the #' buffer should be flushed when the Appender is garbage collected (f.e when #' you close \R)} #' \item{`flush_on_rotate, set_flush_on_rotate`}{`TRUE` or `FALSE`: Whether #' the buffer should be flushed when the Buffer is full (f.e when you close #' \R). Setting this to off can have slightly negative performance impacts.} #' } #' #' @export #' @seealso [LayoutFormat] #' @family Appenders #' @export AppenderBuffer <- R6::R6Class( "AppenderBuffer", inherit = AppenderMemory, cloneable = FALSE, public = list( #' @description #' The [Layout] for this Appender is used only to format console output of #' its `$show()` method. initialize = function( threshold = NA_integer_, layout = LayoutFormat$new( fmt = "%L [%t] %m", timestamp_fmt = "%H:%M:%S", colors = getOption("lgr.colors") ), appenders = NULL, buffer_size = 1e3, flush_threshold = NULL, flush_on_exit = TRUE, flush_on_rotate = TRUE, should_flush = NULL, filters = NULL ){ self$set_threshold(threshold) private$initialize_buffer(buffer_size) self$set_should_flush(should_flush) self$set_flush_threshold(flush_threshold) self$set_flush_on_exit(flush_on_exit) self$set_flush_on_rotate(flush_on_rotate) self$set_filters(filters) self$set_appenders(appenders) self$set_layout(layout) invisible(self) }, #' @description Sends the buffer's contents to all attached Appenders and #' then clears the Buffer flush = function(){ for (event in get("buffer_events", envir = self)){ for (app in self$appenders) { if (app$filter(event)) app$append(event) } } self$clear() }, #' @description Clears the buffer, discarding all buffered Events clear = function(){ assign("insert_pos", 0L, envir = private) private$.buffer_events <- list() invisible(self) }, #' @description Exactly like A [Logger], an [AppenderBuffer] can have an #' arbitrary amount of Appenders attached. When the buffer is flushed, the #' buffered events are dispatched to these Appenders. #' #' @param x single [Appender] or a `list` thereof. Appenders control the #' output of a Logger. Be aware that a Logger also inherits the Appenders #' of its ancestors (see `vignette("lgr", package = "lgr")` for more info #' about Logger inheritance). set_appenders = function(x){ if (is.null(x)){ private$.appenders <- list() return(invisible()) } if (inherits(x, "Appender")) x <- list(x) assert( is.list(x) && all(vapply(x, inherits, TRUE, "Appender")), "'appenders' must either be a single Appender, a list thereof, or ", "NULL for no appenders." ) private$.appenders <- list() for (i in seq_along(x)) self$add_appender(x[[i]], name = names(x)[[i]]) invisible(self) }, #' @description Add an Appender to the AppenderBuffer #' @param appender a single [Appender] #' @param name a `character` scalar. Optional but recommended. #' #' @description Add or remove an [Appender]. Supplying a `name` is optional but #' recommended. After adding an Appender with #' `appender$add_appender(AppenderConsole$new(), name = "console")` you can #' refer to it via `appender$appenders$console`. `remove_appender()` can #' remove an Appender by position or name. add_appender = function( appender, name = NULL ){ assert(inherits(appender, "Appender")) private$.appenders[[length(private$.appenders) + 1L]] <- appender if (!is.null(name)) names(private$.appenders)[length(private$.appenders)] <- name invisible(self) }, #' @description remove an appender #' @param pos `integer` index or `character` name of the Appender(s) to #' remove #' remove_appender = function( pos ){ if (is.numeric(pos)){ assert( all(pos %in% seq_along(private$.appenders)), "'pos' is out of range of the length of appenders (1:", length(private$.appenders), ")" ) pos <- as.integer(pos) } else if (is.character(pos)) { assert( all(pos %in% names(private$.appenders)), "'pos' is not names of appenders (", paste(names(private$.appenders), collapse = ", "), ")" ) } for (nm in pos){ private$.appenders[[nm]] <- NULL } invisible(self) }, format = function( ... ){ res <- super$format() appenders <- appender_summary(self$appenders) if (!is.null(appenders)){ res <-c( res, paste0(" ", appenders) ) } res } ), # +- active --------------------------------------------------------------- active = list( appenders = function(value){ if (missing(value)) return(c(private$.appenders)) if (is.null(value)){ private$.appenders <- NULL return(invisible()) } if (inherits(value, "Appender")) value <- list(value) assert( is.list(value) && all(vapply(value, inherits, TRUE, "Appender")), "'appenders' must either be a single Appender, a list thereof, or NULL for no appenders." ) private$.appenders <- value invisible() }, destination = function() paste(length(self$appenders), "child Appenders") ), # +- private --------------------------------------------------------- private = list( finalize = function(){ if (self$flush_on_exit) self$flush() # Ensure child appenders are gc'ed first. This ensures more predictable # behaviour when destroying an AppenderBuffer. for (i in rev(seq_along(self$appenders))){ self$remove_appender(i) gc() } invisible() }, .appenders = list() ) ) # AppenderFileRotating ---------------------------------------------------- #' Log to a rotating file #' #' An extension of [AppenderFile] that rotates logfiles based on certain #' conditions. Please refer to the documentation of [rotor::rotate()] for #' the meanings of the extra arguments #' #' @export #' @seealso [AppenderFileRotatingDate], [AppenderFileRotatingTime], [rotor::rotate()] #' @family Appenders #' @export AppenderFileRotating <- R6::R6Class( "AppenderFileRotating", inherit = AppenderFile, public = list( #' @description #' @param size,max_backups,compression,backup_dir,fmt #' see [rotor::rotate()] for the meaning of these arguments. Note that #' `fmt` corresponds to `format` and `backup_dir` to `dir`. initialize = function( file, threshold = NA_integer_, layout = LayoutFormat$new(), filters = NULL, size = Inf, max_backups = Inf, compression = FALSE, backup_dir = dirname(file), create_file = NULL ){ assert_namespace("rotor") if (!file.exists(file)) file.create(file) if (length(create_file)){ .Deprecated(msg = "The create_file argument is defunct and will be removed from future versions of lgr.") } private$bq <- rotor::BackupQueueIndex$new(file) self$set_file(file) self$set_threshold(threshold) self$set_layout(layout) self$set_filters(filters) self$set_size(size) self$set_max_backups(max_backups) self$set_compression(compression) self$set_backup_dir(backup_dir) self$set_create_file(TRUE) self }, append = function(event){ super$append(event) self$rotate() }, rotate = function( force = FALSE ){ assert(is_scalar_bool(force)) bq <- get("bq", private) if (force || bq$should_rotate(size = self$size)){ bq$push() bq$prune() file.remove(self$file) file.create(self$file) } self }, prune = function(max_backups = self$max_backups){ get("bq", envir = private)$prune(max_backups) self }, set_file = function( file ){ super$set_file(file) private$bq$set_origin(self$file) self }, set_size = function( x ){ private[[".size"]] <- x self }, set_max_backups = function( x ){ private[["bq"]]$set_max_backups(x) self }, set_compression = function( x ){ private[["bq"]]$set_compression(x) self }, set_create_file = function( x ){ assert(is_scalar_bool(x)) private[[".create_file"]] <- x self }, set_backup_dir = function( x ){ private$bq$set_dir(x, create = FALSE) self }, format = function( color = false, ... ){ res <- super$format(color = color, ...) if (!color) style_subtle <- identity cs <- style_subtle( sprintf("(current size: %s)", fmt_bytes(file.size(self$file))) ) size <- { if (is.infinite(self$size)) "Inf" else if (is.numeric(self$size)) fmt_bytes(self$size) else self$size } size <- paste(size, cs) c( res, sprintf(" backups: %s/%s", private$bq$n_backups, self$max_backups), paste(" size:", size) ) } ), active = list( size = function() get(".size", private), create_file = function() get(".create_file", private), compression = function() get("bq", private)$compression, max_backups = function() get("bq", private)$max_backups, #' @field backups A `data.frame` containing information on path, file size, #' etc... on the available backups of `file`. backups = function() get("bq", private)$files, backup_dir = function() get("bq", private)$dir ), private = list( .size = NULL, .create_file = NULL, bq = NULL ) ) # AppenderFileRotatingTime ------------------------------------------------ #' Log to a time-stamped rotating file #' #' An extension of [AppenderFile] that rotates logfiles based on certain #' conditions. Please refer to the documentation of [rotor::rotate_time()] for #' the meanings of the extra arguments #' #' @family Appenders #' @seealso [AppenderFileRotatingDate], [AppenderFileRotating], [rotor::rotate()] #' @export AppenderFileRotatingTime <- R6::R6Class( "AppenderFileRotatingTime", inherit = AppenderFileRotating, public = list( #' @description #' @param size,age,max_backups,compression,backup_dir,fmt,overwrite,cache_backups #' see [rotor::rotate_time()] for the meaning of these arguments. Note that #' `fmt` corresponds to `format` and `backup_dir` to `dir`. initialize = function( file, threshold = NA_integer_, layout = LayoutFormat$new(), filters = NULL, age = Inf, size = -1, max_backups = Inf, compression = FALSE, backup_dir = dirname(file), fmt = "%Y-%m-%d--%H-%M-%S", overwrite = FALSE, cache_backups = TRUE, create_file = NULL ){ assert_namespace("rotor") if (length(create_file)){ .Deprecated(msg = "The create_file argument is defunct and will be removed from future versions of lgr.") } if (!file.exists(file)) file.create(file) private$bq <- rotor::BackupQueueDateTime$new(file) self$set_file(file) self$set_threshold(threshold) self$set_layout(layout) self$set_filters(filters) self$set_fmt(fmt) self$set_age(age) self$set_size(size) self$set_max_backups(max_backups) self$set_compression(compression) self$set_overwrite(overwrite) self$set_backup_dir(backup_dir) self$set_create_file(TRUE) self$set_cache_backups(cache_backups) self }, rotate = function( force = FALSE, now = Sys.time() ){ assert(is_scalar_bool(force)) bq <- get("bq", private) if ( force || bq$should_rotate( size = self$size, age = self$age, now = now ) ){ bq$push( overwrite = self$overwrite, now = now ) bq$prune() file.remove(self$file) file.create(self$file) } self }, set_age = function( x ){ private[[".age"]] <- x self }, set_fmt = function( x ){ get("bq", private)$set_fmt(x) self }, set_overwrite = function( x ){ assert(is_scalar_bool(x)) private[[".overwrite"]] <- x self }, #' @description set the `cache_backups` flag. #' @param x a `logical` scalar set_cache_backups = function( x ){ private$bq$set_cache_backups(x) self }, format = function( color = FALSE, ... ){ res <- super$format(color = color, ...) if (!color) style_subtle <- identity lr <- private$bq$last_rotation if (!is.null(lr)) lr <- style_subtle(sprintf(" (last rotation: %s)", format(lr))) else lr <- "" c( res, sprintf(" age: %s%s", self$age, lr) ) } ), active = list( age = function() get(".age", private), overwrite = function() get(".overwrite", private), fmt = function() get("bq", private)$fmt, #' @field cache_backups #' `TRUE` or `FALSE`. If `TRUE` (the default) the list of backups is cached, #' if `FALSE` it is read from disk every time this appender triggers. #' Caching brings a significant speedup for checking whether to rotate or #' not based on the `age` of the last backup, but is only safe if #' there are no other programs/functions (except this appender) interacting #' with the backups. cache_backups = function() get("bq", private)$cache_backups ), private = list( .age = NULL, .overwrite = NULL ) ) # AppenderFileRotatingDate ---------------------------------------------------- #' Log to a date-stamped rotating file #' #' This is a simpler version of AppenderFileRotatingTime when the timestamps #' do not need to include sub-day accuracy. #' #' @family Appenders #' @seealso [AppenderFileRotatingTime], [AppenderFileRotating], [rotor::rotate()] #' @export AppenderFileRotatingDate <- R6::R6Class( "AppenderFileRotatingDate", inherit = AppenderFileRotatingTime, public = list( #' @description #' @param size,age,max_backups,compression,backup_dir,fmt,overwrite,cache_backups #' see [rotor::rotate_date()] for the meaning of these arguments. Note that #' `fmt` corresponds to `format` (because `$format` has a special meaning #' for R6 classes). initialize = function( file, threshold = NA_integer_, layout = LayoutFormat$new(), filters = NULL, age = Inf, size = -1, max_backups = Inf, compression = FALSE, backup_dir = dirname(file), fmt = "%Y-%m-%d", overwrite = FALSE, cache_backups = TRUE, create_file = NULL ){ assert_namespace("rotor") if (!file.exists(file)) file.create(file) if (length(create_file)){ .Deprecated(msg = "The create_file argument is defunct and will be removed from future versions of lgr.") } private$bq <- rotor::BackupQueueDate$new(file) self$set_file(file) self$set_threshold(threshold) self$set_layout(layout) self$set_filters(filters) self$set_fmt(fmt) self$set_age(age) self$set_size(size) self$set_max_backups(max_backups) self$set_compression(compression) self$set_overwrite(overwrite) self$set_create_file(TRUE) self$set_backup_dir(backup_dir) self$set_cache_backups(cache_backups) self } ) ) # print.Appender ---------------------------------------------------------- #' Print an Appender object #' #' The `print()` method for Loggers displays the most important aspects of #' the Appender. #' #' #' @inheritParams print.Logger #' #' @return #' `print()` returns `x` (invisibly), `format()` returns a `character` vector. #' @export #' #' @examples #' # print most important details of logger #' print(lgr$console) print.Appender <- function( x, color = requireNamespace("crayon", quietly = TRUE), ... ){ assert(is_scalar_bool(color)) cat(format(x, color = color), sep = "\n") invisible(x) } appender_summary <- function(x){ dd <- lapply(x, srs_appender) if (!length(x)){ return(NULL) } if (is.null(names(x))){ names(x) <- "" } names(dd) <- ifelse( is.na(names(x)) | is_blank(names(x)), paste0("[[", seq_len(length(x)), "]]"), names(x) ) dd <- do.call(rbind, dd) if (is.null(dd)) return(NULL) dd$name <- rownames(dd) dd$destination <- ifelse( !is_blank(dd$destination), paste("->", dd$destination), "" ) with( dd, paste0( pad_right(name), ": ", pad_right(class), " [", pad_left(fmt_threshold(threshold, type = "character")), "] ", destination ) ) } # helpers ----------------------------------------------------------------- #' Default reader for files created with AppenderFile #' #' @param file a `character` scalar: path to a file #' @param threshold only `NA` is supported for the default reader, other #' values cause a warning. See `LayoutFormat$read()` for an example where #' that supports thresholds. #' @param n an `integer` scalar > 0 #' #' @return #' @noRd default_file_reader <- function(file, threshold, n){ assert( is_scalar_character(file), "`file` must be the path to a file, not ", string_repr(file) ) if (isFALSE(is.na(threshold))){ warning("A threshold was supplied to AppenderFile$show(), but the ", "Appenders' Layout does not support filtering by log level") } last_n(readLines(file), n) } standardize_should_flush_output <- function( x ){ if (isTRUE(x)){ TRUE } else if (isFALSE(x)){ FALSE } else { warning(ValueIsNotBoolWarning(paste0( "`$should_flush()` did not return `TRUE` or `FALSE` but ", string_repr(x), ". ", "Please set a valid filter function with `$set_should_flush()`." ))) FALSE } } # conditions -------------------------------------------------------------- CannotParseLogError <- function(message){ error(message = message, class = "CannotParseLogError") } ValueIsNotBoolWarning <- function(message){ condition(message, class = c("ValueIsNotBoolWarning", "warning")) } lgr/R/utils-rd.R0000644000176200001440000001002614131760166013131 0ustar liggesusersr6_usage <- function( x, name = "x", ignore = NULL, header = "", show_methods = TRUE ){ if (is.list(x)){ classname <- deparse(substitute(x)) classname <- gsub("(list\\()|\\)$", "", classname) classname <- unlist(strsplit(classname, ", ", fixed = TRUE)) res <- lapply( seq_along(x), function(i){ collect_usage.R6( x = x[[i]], classname = classname[[i]], ignore = ignore ) } ) res <- list( ctor = unlist(lapply(res, `[[`, "ctor")), fields = unique(unlist(lapply(res, `[[`, "fields"))), methods = unique(unlist(lapply(res, `[[`, "methods"))) ) } else if (R6::is.R6Class(x)){ res <- collect_usage.R6( x, classname = deparse(substitute(x)), ignore = ignore ) } else { stop("Object ", string_repr(x), "not supported") } fmt_r6_usage( res, name = name, header = header, show_methods = show_methods ) } #' Format R6 usage #' #' @param x an `R6ClassGenerator` #' @param classname `character` scalar. The name of the R6 class #' @param ignore `character` vector. methods/fields to ignore when generating #' usage #' #' @return a `list` with the components `ctor`, `fields` and `methods` #' @noRd collect_usage.R6 <- function( x, classname = deparse(substitute(x)), ignore = TRUE ){ public_methods <- vapply( setdiff(names(x$public_methods), ignore), function(nm) make_function_usage(nm, formals(x$public_methods[[nm]])), character(1) ) ctor <- get_public_method_recursively(x, "initialize") if (!is.null(ctor)){ ctor <- make_function_usage(paste0(classname, "$new"), formals(ctor)) } fields <- c(names(x$public_fields), names(x$active)) if (!is.null(fields)) fields <- sort(fields) fields <- setdiff(fields, ignore) els <- list( ctor = ctor, methods = public_methods[!names(public_methods) %in% c("initialize", "finalize")], fields = fields ) els <- els[!vapply(els, is_empty, FALSE)] if ("get_inherit" %in% names(x)){ els <- c(els, collect_usage.R6(x$get_inherit(), ignore = ignore)) list( ctor = els$ctor, fields = unique(unlist(els[names(els) == "fields"])), methods = unique(unlist(els[names(els) == "methods"])) ) } else { els } } #' Format R6 usage #' #' @param x output of collect_usage.R6 #' @param header an optional `character` vector for a heading #' @param show_methods `logical` scalar: Show methods #' #' @return a `character` vector #' @noRd fmt_r6_usage <- function( x, name = x, header = "", show_methods = TRUE ){ assert(is_scalar_bool(show_methods)) res <- c() res <- c("@section Usage:", "") ctors <- unlist(lapply( x$ctor, function(.x) c(strwrap(paste0(name, " <- ", .x), width = 80, exdent = 2), "") )) res <- c( res, "```", header, ctors ) if (show_methods){ res <- c( res, paste0(name, "$", sort(x$methods)), "", paste0(name, "$", sort(x$fields)), "", "```" ) } res } get_public_method_recursively = function(ctor, method){ if (is.function(ctor)) return(ctor) else if (is.null(ctor)) return(NULL) if (method %in% names(ctor$public_methods)){ return(ctor$public_methods[[method]]) } else { get_public_method_recursively(ctor$get_inherit(), method) } } make_function_usage <- function(name, arglist){ paste0(name, "(", fmt_formals(arglist), ")") } fmt_formals <- function(fmls){ arg_to_text <- function(.x) { if (is.symbol(.x) && deparse(.x) == "") return("") text <- enc2utf8(deparse(.x, backtick = TRUE, width.cutoff = 500L)) text <- paste0(text, collapse = "\n") Encoding(text) <- "UTF-8" text } res <- vapply(fmls, arg_to_text, character(1)) sep <- ifelse(res == "", "", " = ") paste0(names(res), sep, res, collapse = ", ") } lgr/R/string_repr.R0000644000176200001440000000512715036470240013726 0ustar liggesusers#' Short string representation for R objects #' #' This is inspired by the python function `repr` and produces a short #' string representation of any \R object that is suitable for logging and error #' messages. It is a generic so you can implement methods for custom S3 objects. #' #' @param x Any \R object. #' @param width a scalar integer #' @param ... passed on to methods #' #' @return a `scalar` character #' @export #' #' @examples #' string_repr(iris) #' string_repr(LETTERS) #' string_repr(LETTERS, 10) string_repr <- function( x, width = 32, ... ){ assert(is_scalar_integerish(width)) if (width < 8){ warning("string_repr() does not support width < 8") width <- 8L } UseMethod("string_repr") } #' @rdname string_repr #' @export string_repr.function <- function( x, width = 32L, ... ){ fmls <- names(formals(x)) len_fmls <- length(fmls) if (len_fmls > 4){ fmls <- fmls[1:4] fmls_fmt <- paste(fmls, collapse = ", ") fmls_fmt <- paste0(fmls_fmt, ", +", len_fmls - length(fmls), "") } else { fmls_fmt <- paste(fmls, collapse = ", ") } fmt_class(paste( fmt_class(class(x), open = "", close = ""), "(", fmls_fmt, ")", sep = "" )) } #' @rdname string_repr #' @export string_repr.data.frame <- function( x, width = 32L, ... ){ x_class <- fmt_class(class(x), open = "", close ="") x_shape <- paste0(nrow(x),"x", ncol(x)) if (nchar(x_class) + nchar(x_shape) + 3L <= width){ res <- paste0("<", x_class, " ", x_shape, ">") } else { if (nchar(x_class) >= width - 2L){ x_class <- paste0(strtrim(x_class, width - 4L), "..") } res <- paste0("<", strtrim(x_class, width = width - 2L), ">") } res } #' @rdname string_repr #' @export string_repr.matrix <- string_repr.data.frame #' @rdname string_repr #' @export string_repr.numeric <- function( x, width = 32L, ... ){ string_repr(format(x, justify = "none", drop0trailing = TRUE, trim = TRUE)) } #' @rdname string_repr #' @export string_repr.default <- function( x, width = 32L, ... ){ if (is.recursive(x)){ x_class <- fmt_class(class(x), open = "", close ="") if (nchar(x_class) >= width - 2L){ x_class <- paste0(strtrim(x_class, width - 4L), "..") } res <- paste0("<", strtrim(x_class, width = width - 2L), ">") } else { res <- ptrunc(x, collapse = ", ", width = width, dots = "..") if (is_scalar(x)){ res <- paste0("`", res, "`") } else { res <- paste0("(", res, ")") } } res } lgr/R/get_logger.R0000644000176200001440000000662114232440604013504 0ustar liggesusersloggers <- new.env() remove_all_loggers <- function(){ rm(list = setdiff(ls(envir = loggers), "root"), pos = loggers) } #' Get/Create a Logger #' #' @param name a `character` scalar or vector: The qualified name of the Logger #' as a hierarchical value. #' @param class An [R6ClassGenerator][R6::R6] object. Usually `Logger` or `LoggerGlue` #' are the only valid choices. #' @param reset a `logical` scalar. If `TRUE` the logger is reset to an #' unconfigured state. Unlike `$config(NULL)` this also replaces a #' `LoggerGlue` with vanilla `Logger`. Please note that this will invalidate #' Logger references created before the reset call (see examples). #' #' @return a [Logger] #' @export #' #' @examples #' lg <- get_logger("log/ger/test") #' # equivalent to #' lg <- get_logger(c("log", "ger", "test")) #' lg$warn("a %s message", "warning") #' lg #' lg$parent #' #' if (requireNamespace('glue')){ #' lg <- get_logger_glue("log/ger") #' } #' lg$warn("a {.text} message", .text = "warning") #' #' # completely reset 'glue' to an unconfigured vanilla Logger #' get_logger("log/ger", reset = TRUE) #' # WARNING: this invalidates existing references to the Logger #' try(lg$info("lg has been invalidated an no longer works")) #' #' lg <- get_logger("log/ger") #' lg$info("now all is well again") get_logger <- function( name, class = Logger, reset = FALSE ){ if (missing(name) || !length(name) || all(is_blank(name))){ return(lgr) } assert(is.character(name)) assert(is_scalar_bool(reset)) nm_cur <- unlist(strsplit(name, "/", fixed = TRUE)) name <- paste(nm_cur, collapse = "/") res <- get0(name, envir = loggers, inherits = FALSE) if (reset && is_Logger(res)){ res$set_filters(function(event) stop(call. = FALSE, sprintf(paste( "Trying to log via a Logger reference that is no longer valid.", "Logger references become invalid when you reset a when you reset a", "Logger with `get_logger(reset = TRUE)`. Please", "re-create the Logger reference with with `get_logger(%s)`"), name ))) res <- NULL } if (is.null(res)){ assert(R6::is.R6Class(class), "`class` must be an R6ClassGenerator") assign(name, class$new(name), envir = loggers, inherits = FALSE) return(get_logger(name, class = class)) } res } #' @rdname get_logger #' @export get_logger_glue <- function( name ){ if (is_virgin_Logger(name)){ res <- get_logger(name = name, class = LoggerGlue, reset = TRUE) } else { res <- get_logger(name = name) } assert( "LoggerGlue" %in% class(res), sprintf( "'%s' must be an unconfigured or a . You can use `get_logger('%s')$config(NULL)` to reset its configuration.", name, name ) ) res } exists_logger <- function( name ){ inherits(get0(name, envir = loggers), "Logger") } is_virgin_Logger <- function( x, allow_subclass = FALSE ){ assert(is_scalar_bool(allow_subclass)) if (is.character(x)){ x <- get_logger(x) } else { assert(is_Logger(x)) } (identical(class(x), c("Logger", "Filterable", "R6")) || allow_subclass) && !length(x$appenders) && !length(x$filters) && isTRUE(x$propagate) && identical(x$exception_handler, default_exception_handler) && is.null(x$.__enclos_env__$private$.threshold) } lgr/vignettes/0000755000176200001440000000000015137115052013046 5ustar liggesuserslgr/vignettes/log_flow.svg0000644000176200001440000007371714131760166015423 0ustar liggesusers
event level
<= logger
$threshold
[Not supported by viewer]
Logging Call
logger.info
logger.error
...
[Not supported by viewer]
Create
LogEvent
[Not supported by viewer]
passes
logger$filter()

[Not supported by viewer]
pass to
Appenders of
current logger
[Not supported by viewer]
logger
$propagate
== TRUE
[Not supported by viewer]
pass to
Appenders of
parent logger(s)
[Not supported by viewer]
Stop
Stop
Yes
Yes
Yes
Yes
 No 
[Not supported by viewer]
 No 
[Not supported by viewer]
 No 
[Not supported by viewer]
File
File
Console
Console
other
other
Memory
Memory
event level
<= appender
$threshold

[Not supported by viewer]
passes
appender
$filter()
[Not supported by viewer]
Yes
Yes
 Yes 
[Not supported by viewer]
Stop
Stop
No
No
 No 
[Not supported by viewer]
appender$append()
appender$append()
Logger
Logger
Appender
Appender
lgr/vignettes/lgr.Rmd0000644000176200001440000006772414402036437014321 0ustar liggesusers--- title: "lgr: A fully featured logging framework for R" author: "Stefan Fleck" date: "`r Sys.Date()`" output: rmarkdown::html_vignette: toc: true number_sections: true vignette: > %\VignetteIndexEntry{lgr} %\VignetteEncoding{UTF-8} %\VignetteEngine{knitr::rmarkdown} editor_options: chunk_output_type: console --- ```{r setup, include = FALSE} library(lgr) knitr::opts_chunk$set( collapse = TRUE, comment = "#>" ) ``` # Introduction lgr is a logging framework for R inspired by [Apache Log4j](https://logging.apache.org/log4j/2.x/) and [Python logging](https://docs.python.org/3/library/logging.html). It follows an object oriented design implemented through [R6 classes](https://github.com/r-lib/R6). This enables lgr to have a larger set of features than other logging packages for R, and makes it flexible and easy to extend. If you are not sure if lgr is the right package for you, take a look [examples](#examples) to see what lgr can do for you. ## Quickstart lgr comes with a so-called **root logger** that is ready to go after you install it. If you are a package developer you can (and should) set up a new Logger for your package, but more on that later. For many use cases the root Logger will suffice. ### Logging to the console ```{r} # the root logger is called "lgr" lgr$info("Vampire stories are generally located in Styria.") ``` You can use formatting strings that are passed on to `sprintf()` in lgr. ```{r} lgr$error("Vampires generally arrive in carriages drawn by %i black horses.", 2) ``` ### Logging to plaintext files Usually you don't (only) want to log to the console, you want to log to files. Output of Loggers is managed by Appenders. The root Logger is preconfigured with a console Appender (that is why we see output in the console). Let's add a file Appender: ```{r} tf <- tempfile(fileext = ".info") lgr$add_appender(AppenderFile$new(tf), name = "file") lgr$info("You must think I am joking") readLines(tf) ``` The various Appenders available in lgr are R6 classes. To instantiate an object of that class (i.e. create a new appender) you have to use the `$new()` function as in the example above. Whenever you see something in lgr that `IsNamedLikeThis`, you can be sure that it is such an R6 class. If you look at the output, you see that the timestamp format of the file appender is slightly different to the timestamp format of the console Appender. Formatting is handled by **Layouts**, and each Appender has exactly one: ```{r} lgr$appenders$file$set_layout(LayoutFormat$new(timestamp_fmt = "%B %d %T")) lgr$info("No, I am quite serious") readLines(tf) #cleanup unlink(tf) ``` ### Logging to JSON files If you log to files, you should not log normal text. If you want to analyse your logs later, it's much better to log to a format like JSON: ```{r} # cleanup behind the old Appender unlink(tf) lgr$remove_appender("file") # setup a JSON appender lgr$add_appender(AppenderJson$new(tf), name = "json") lgr$info("We lived in Styria") ``` JSON is still somewhat human readable ```{r} cat(readLines(tf)) ``` and easy for machines to parse ```{r} read_json_lines(tf) ``` Many Appenders provide either a `$show()` method and a `$data` active binding convenience, and so you do not have to call `readLines()` & co manually. ```{r} # show is a method and takes some extra arguments, like maximum number of lines # to show lgr$appenders$json$show() # $data always returns a data.frame if available. It is an active binding # rather than a method, so no extra arguments are possible lgr$appenders$json$data ``` Please note that under the hood, `AppenderJson` is just an `AppenderFile` with `LayoutJson`. The only difference is AppenderJson provides a `$data()` method while AppenderFile does not. ### Structured Logging (custom fields) lgr treats a *LogEvent* as a unit of data, not just a message with a timestamp. A log event can contain arbitrary data, though not all Appenders can handle that well. The JSON appender we added above is particularly good at handling most R objects. ```{r} # The default console appender displays custom fields as pseudo-json after the message lgr$info("Styria has", poultry = c("capons", "turkeys")) # JSON can store most R objects quite naturally read_json_lines(tf) read_json_lines(tf)$poultry[[2]] # works because poultry is a list column ``` ```{r echo = FALSE} lgr$remove_appender("json") unlink(tf) ``` ### What Else If the examples above have piqued your interest, the rest of this vignette will provide more details on the workings of lgr. Discussing all Appenders and configuration options is beyond the scope of this vignette, please refer to the [function reference](https://s-fleck.github.io/lgr/reference/index.html) for that. # Usage ## Structure of the logging system If you want custom logging configurations, you have to understand the structure of the logging process. * A **Logger** collects information and dispatches it to its *Appenders*, and also the *Appenders* of its *Parent Loggers* (also see the section on hierarchical logging) * An **Appender** writes the log message to destination (the console, a file, a database, etc...). * A **Layout** is used by an Appender to format LogEvents. For example, `AppenderFile` uses `LayoutFormat` by default to write human readable log events to a text file, but can also use `LayoutJson` produce machine readable JSON lines logfiles. * **LogEvents** are produced by the Logger and dispatched to Appenders. They contain all the information that is being logged (think of it as a row in table). LogEvents usually contain the log level, a timestamp, a message, the name of the calling function, and a [reference](https://adv-r.hadley.nz/r6.html#r6-semantics]) to the Logger that created it. In addition, a LogEvent can contain any number of custom fields. See [examples 1 & 2](#examples) ### On R6 classes The elements described above are R6 classes. R6 is an object orientation system for R that is used by many popular packages such as shiny, dplyr, plumber, roxygen2, and testthat but often behind the scenes and not as exposed as in lgr. You recognize R6 classes in this package because they are named following the `UpperCamelCase` naming convention. While there is only one kind of Logger and one kind of LogEvent, there are several subclasses of Appenders and Layouts. An introduction to R6 classes is beyond the scope of this document, but you can find the official documentation [here](https://r6.r-lib.org/) and there is also this [talk on Youtube](https://www.youtube.com/watch?v=3GEFd8rZQgY). In short R6 classes store data (fields) together with functions (methods) and have to be instantiated with `$new()`. So if you want to create a new `AppenderFile`, you do this by calling `AppenderFile$new(file = tempfile())`. Please not that Loggers should never be instantiated directly with `$new()` but always with `get_logger()`. ## Log levels lgr supports the standard log4j Log Levels outlined bellow. The Log Level of an event represents its severity. The named log levels are really just nicknames for integer values, and you can use the `character` or `integer` representations interchangeably. You can also use arbitrary integer values (greater than `0`), but you are encouraged to stick to the ones bellow. ```{r, echo = FALSE} ll <- data.frame( `Level` = c(0, seq(100, 600, by = 100), NA), `Name` = c("off", "fatal", "error", "warn", "info", "debug", "trace", "all"), `Description` = c( "Tells a Logger or Appender to suspend all logging", "Critical error that leads to program abort. Should always indicate a `stop()` or similar", "A severe error that does not trigger program abort", "A potentially harmful situation, like `warning()`", "An informational message on the progress of the application", "Finer grained informational messages that are mostly useful for debugging", "An even finer grained message than debug ([more info](https://softwareengineering.stackexchange.com/questions/279690/why-does-the-trace-level-exist-and-when-should-i-use-it-rather-than-debug))", "Tells a Logger or Appender to process all log events" ) ) knitr::kable(ll) ``` `off` and `all` are valid thresholds for Appenders and Loggers, but not valid levels for LogEvents; e.g. `lgr$set_threshold(NA)` makes sense, but `lgr$log("all", "an example message")` does not. The list of named log levels is stored as a global option (`getOption("lgr.log_levels")`) and you can use `add_log_levels()` and `remove_log_levels()` to define your own named levels if you want to. There are only predefined logging methods (`lgr$fatal()`, etc..) for the standard log levels and you have to use `lgr$log(level, message)` to create a LogEvent with a custom log level. ## Logging with the Root Logger lgr comes with a pre-configured root Logger. It is called *root* because you can [set up a tree of Loggers](#hierarchy) that descent from it, but for basic use you will not have to worry about that. ### Logging syntax lgr Loggers are R6 objects with *methods* (functions) for logging. You can refer to the *root* logger with `lgr`. ```{r} lgr$fatal("This is an important message about %s going wrong", "->something<-") lgr$trace("Trace messages are still hidden") lgr$set_threshold("trace") lgr$trace("Unless we lower the threshold") ``` ### Formatting strings You can use `sprintf()` style formatting strings directly in log messages. ```{r} lgr$info("The sky was the color of %s, tuned to a dead chanel", "television") ``` ## LogEvents: The atomic unit of logging LogEvents are objects that store all information collected by the Logger. They are passed on to Appenders that output them, but Appenders usually don't utilize all the information present in a log event. The last event produced by a Logger is stored in its `last_event` field. ```{r} lgr$info("Vampire stories are generally located in Styria") lgr$last_event # a summary output of the event lgr$last_event$values # all values stored in the event as a list ``` LogEvents can contain not only these standard values, but an arbitrary number of extra values. These extra values are passed as named arguments to the logging function (as opposed as to parameters to `sprintf()`, which are passed as unnamed arguments). It is up to the Appender whether to process them further or not. You should consider making use of structured logging liberally and using output formats that support them (such as JSON), rather than producing elaborately formatted but hard to parse log messages. ```{r} # bad lgr$info("Processing track '%s' with %s waypoints", "track.gpx", 32) # Good tf <- tempfile() lgr$add_appender(AppenderJson$new(tf), "json") lgr$info("Processing track", file = "track.gpx", waypoints = 32) lgr$appenders$json$data ``` ```{r echo = FALSE} lgr$remove_appender("json") unlink(tf) ``` ## Thresholds & Filters: controlling output detail {#thresholds} To control the level of detail of the log output, you can set **thresholds** for Loggers and Appenders. A Logger with a threshold of `warn` will only create LogEvents of the priorities `warn`, `error` and `fatal` and dispatch them to its Appenders. > A **threshold** of a Logger or Appender is the minimum **log level** a > LogEvent must have so that that Logger/Appender processes it. If you require more complex logic to decide whether a LogEvent should be created/processed you can also assign **filters** to Loggers/Appenders. Filters are just functions that have exactly one argument, `event` (the LogEvent to be filtered), and return `TRUE` or `FALSE`. They will be applied after the threshold is checked. Alternatively there is also a formal R6 class for Filters (?EventFilter) that you can use, but this is usually not necessary. examples: ```{r} f1 <- function(event) { grepl("bird", event$msg) } lgr$set_filters(list(f1)) lgr$info("is it a plane?") lgr$info("no! is it a bird?") # since this is not a very useful filter, we better remove it again lgr$set_filters(NULL) ``` ## Appenders: Managing log destinations The root logger only logs to the console by default. If you want to redirect the output to a file you can just add a file appender to lgr. ```{r} tf <- tempfile() # Add a new appender to a logger. We don't have to supply a name, but that # makes it easier to remove later. lgr$add_appender(AppenderFile$new(file = tf), name = "file") # configure lgr so that it logs everything to the file, but only info and above # to the console lgr$set_threshold(NA) lgr$appenders$console$set_threshold("info") lgr$appenders$file$set_threshold(NA) lgr$info("Another informational message") lgr$debug("A debug message not shown by the console appender") readLines(tf) # Remove the appender again lgr$remove_appender("file") unlink(tf) ``` ## Inheritance: Hierarchical Loggers {#hierarchy} Logger hierarchies are a powerful concept to organize logging for different parts of a larger system. This is mainly relevant for package developers. It is good practice to have a separate Logger for each package. Since it is not common in R to build complex systems of hierarchically organised packages, hierarchies will usually be pretty flat (i.e. most Loggers will only inherit from the root logger). Each newly created Logger is child to a parent Logger, derived from its name. So `lg <- get_logger("foo/bar")` creates the logger with the qualified name `foo/bar` whose parent is the logger `foo` whose parent is (implicitly) the `root` logger. If the logger `foo` does not exist in that scenario, it is created automatically. This behaviour might sound strange at first, but it mimics tried and tested behaviour of python logging. This way logging is decoupled from the business logic and your program will not abort if you forgot to initialize some logger up the hierarchy for whatever reason. A logger dispatches the LogEvents it creates not only to its own Appenders, but also to the Appenders of all its ancestral Loggers (ignoring the threshold and Filters of the ancestral Loggers, but not of the Appenders). When you define Loggers for your package, you should *not* configure them (with custom Appenders or thresholds); that should be left to the user of the package. If all this sounds confusing to you, take a look at the [examples](#examples-hierarchy) and `?logger_tree`. The common use cases are pretty easy to understand and illustrate the *how* and *why* pretty well. Example hierarchy for the package **fancymodel** that provides a model along with a plumber API and a shiny web-interface to the package. ``` r # prints a tree structure of all registered loggers logger_tree() ``` ``` root [info] -> 1 appenders └─fancymodel ├─plumber └─#shiny ├─server [trace] -> 2 appenders └─ui -> 1 appenders ``` ### Log flow The graph bellow outlines the flow of LogEvents through the logging system. This is an important reference if you want to work with Filters and Logger hierarchies. ![](log_flow.svg) ## Logging with LoggerGlue [glue](https://glue.tidyverse.org/) is very nicely designed package for string interpolation. It makes composing log messages more flexible and comfortable at the price of an additional dependency and slightly less performance than `sprintf()` (which is used by normal Loggers). To take advantage of glue, simply create a new LoggerGlue like this: ```{r} # install.packages("glue") lg <- get_logger_glue("glue/logger") lg$info( "glue automatically ", "pastes together unnamed arguments ", "and evaluates arbitray expressions inside braces {Sys.Date()}" ) ``` Glue lets you define temporary variables inside the `glue()` call. As with the normal Logger, named arguments get turned into custom fields. ```{r} lg$info("For more info on glue see {website}", website = "https://glue.tidyverse.org/") ``` You can suppress this behaviour by making named argument start with a `"."`. ```{r} lg$info("Glue is available from {.cran}", .cran = "https://CRAN.R-project.org/package=glue") ``` # Configuration ## With setters There are several different ways to configure loggers. The most straight forward one is to use *setters* to specify the Loggers properties. ```{r} lg <- get_logger("test") lg$config(NULL) # resets logger to unconfigured state lg$set_threshold("fatal") ``` lgr sets up Loggers in a way so that R6 piping with `$` is possible. This works similar to the magrittr (`#%>#`) pipes. ```{r} lg$ set_threshold("info")$ set_appenders(AppenderConsole$new(threshold = "info"))$ set_propagate(FALSE) ``` ## With a list object ```{r} lg$config(list( threshold = "info", propagate = FALSE, appenders = AppenderConsole$new(threshold = "info") )) ``` ## With YAML or JSON You can use YAML and JSON config files with lgr. ```{r eval = FALSE} lg$config("path/to/config.yaml") lg$config("path/to/config.json") ``` You can also pass in YAML/JSON directly as a character string (or vector with one element per line) ```{r} # Via YAML cfg <- " Logger: threshold: info propagate: false appenders: AppenderConsole: threshold: info " lg$config(cfg) ``` # Examples {#examples} ## Logging to the console lgr comes with simple but powerful formatting syntax for LogEvents. Please refer to `?format.LogEvent` for the full list of available placeholders. ```{r} lg <- get_logger("test") lg$set_appenders(list(cons = AppenderConsole$new())) lg$set_propagate(FALSE) lg$info("the default format") lg$appenders$cons$layout$set_fmt("%L (%n) [%t] %c(): !! %m !!") lg$info("A more involved custom format") ``` If this is not enough for you, you can use `LayoutGlue` based on the awesome [glue](https://github.com/tidyverse/glue) package. The syntax is a bit more verbose, and `AppenderGlue` is a bit less performant than `AppenderFormat`, but the possibilities are endless. ```{r} # install.packages("glue") library(glue) lg$appenders$cons$set_layout(LayoutGlue$new( fmt = "{.logger$name} {level_name} {caller}: {toupper(msg)}" )) lg$info("with glue") ``` All fields of the [LogEvent] object are exposed through LayoutGlue, so please refer to `?LogEvent` for a list of all available Fields. ## Logging to JSON files JavaScript Object Notation (JSON) is an open-standard file format that uses human-readable text to transmit data objects consisting of attribute–value pairs and array data types ([Wikipedia](https://en.wikipedia.org/wiki/JSON)). JSON is the **recommended text-based logging format when logging to files** ^[Technically, the logger does not produce standard JSON files but [JSON lines](https://jsonlines.org/)], as it is human- as well as machine readable. You should only log to a different format if you have very good reasons for it. The easiest way to log to JSON files is with AppenderJson^[AppenderJson is just an AppenderFile with LayotJson as default Layout and a few extra features] ```{r} # install.packages("jsonlite") tf <- tempfile() lg <- get_logger("test") lg$set_appenders(list(json = AppenderJson$new(file = tf))) lg$set_propagate(FALSE) lg$info("JSON naturally ", field = "custom") lg$info("supports custom", numbers = 1:3) lg$info("log fields", use = "JSON") ``` JSON is easy to parse and analyse with R. lgr provides the function `read_json_lines()` that can be used to read JSON log files, but you can also use AppenderJson's `$data` binding for an even more convenient method to read the logfile. ```{r eval = FALSE} lg$appenders$json$data # same as read_json_lines(tf) ``` ```{r echo = FALSE} lg$appenders$json$data ``` JSON is also human readable, though this vignette does not transport that fact very well because of the lack of horizontal space. ```{r eval = FALSE} lg$appenders$json$show() # same as cat(readLines(tf), sep = "\n") ``` ```{r echo = FALSE} lg$appenders$json$show() ``` ```{r} # cleanup lg$config(NULL) unlink(tf) ``` ## Logging to rotating files lgr can also log to rotating files. The following example logs to a file that is reset and backed-up once it reaches a size of 10kb. Only the last 5 backups of the logfile are kept. ```{r} # install.packages("rotor") tf <- tempfile(fileext = ".log") lg <- get_logger("test")$ set_propagate(FALSE)$ set_appenders(list(rotating = AppenderFileRotating$new( file = tf, size = "10 kb", max_backups = 5)) ) for (i in 1:100) lg$info(paste(LETTERS, sep = "-")) # display info on the backups of tf lg$appenders$rotating$backups # manually delete all backups invisible(lg$appenders$rotating$prune(0)) lg$appenders$rotating$backups #cleanup unlink(tf) ``` ## Logger hierarchies {#examples-hierarchy} The most common use cases for creating a new Logger rather than just using the root Logger is if you create a Package that should contain logging. This way you can have separate Appenders (e.g logfiles) and thresholds for each package. ```{r} # The logger name should be the same as the package name tf <- tempfile() lg <- get_logger("mypackage") lg$add_appender(AppenderFile$new(tf)) ``` The `print()` method for Loggers gives a nice overview of the newly created Logger: ```{r} print(lg) ``` This tells us that `lg` logs all events of at least level `info`. It does have a single (unnamed) Appender that logs to a temporary file, and dispatches all LogEvents it creates to the Appenders of the root Logger (ignoring the threshold and filters of the root Logger, but not of its Appenders). We can use `lg$fatal()`, `lg$info()`, etc.. to log messages with this Logger: ```{r} lg$info("A test message for lg") ``` If we do not want `lg` to dispatch to the root Logger, we can set `propagate` to `FALSE`. ```{r} lg$set_propagate(FALSE) ``` When we take a look at the Logger again, we now see that it does not inherit any Appenders anymore ```{r} print(lg) ``` Consequently, `lg` no longer outputs log messages to he console ```{r} lg$info("Nothing to see here") ``` ```{r} # cleanup lg$config(NULL) unlink(tf) ``` ## Buffered logging The main purpose of AppenderBuffer is to retain LogEvents in memory and write them to destinations at a later point in time, e.g. when the Buffer is full and needs to be flushed. For example, if you log to a remote database you can postpone this costly operation until after your analysis is finished. By setting a [filter](#thresholds) as a custom `$should_flush()` method for an AppenderBuffer, you can define more complex conditions to trigger flushing. For example, the will output the last 5 LogEvents that happened before an `error` occurred. ```{r} lg <- get_logger("buffer") lg$ set_threshold(NA)$ set_propagate(FALSE)$ set_appenders( AppenderBuffer$new( threshold = NA, buffer_size = 5, # can hold 5 events, the 6th will trigger flushing flush_on_exit = FALSE, flush_on_rotate = FALSE, flush_threshold = "error", appenders = AppenderConsole$new(threshold = NA) )) # The for loop below stores 8 log events in the Buffer for (nm in month.name[1:8]) lg$debug("%s", nm) # An event of level 'error' or 'fatal' triggers flushing of the buffer lg$error("But the days grow short when you reach September") ``` ## Logging to databases Logging to databases is simple, though a few aspects can be tricky to configure based on the backend used. For performance reasons database inserts are buffered by default. This works exactly identical as described above for AppenderBuffer. If you want to write each LogEvent directly to the database, just set the buffer size to `0`. As of lgr 0.4.0, database appenders are part of the **lgrExtra** package that has to be installed separately. **Database logging is still somewhat experimental**. ```{r, eval = FALSE} # install.packages("RSQLite") # install.packages("lgrExtra") lg <- get_logger("db_logger") lg$ set_propagate(FALSE)$ add_appender( name = "db", lgrExtra::AppenderDbi$new( conn = DBI::dbConnect(RSQLite::SQLite()), table = "log", buffer_size = 2L ) ) lg$info("Logging to databases uses a buffer") lg$info("As the buffer size is 2, no insert took place till now") lg$appenders$db$show() lg$info("Now as the buffer is rotated, all events are output to the db") lg$appenders$db$show() ``` ## Adding default extra fields to messages By abusing Filters, lgr can modify LogEvents as they are processed. One example for when this is useful is assigning a grouping identifier to a series of log calls. ```{r} # setup an example function clean <- function() lgr$info("cleaning data") process <- function() lgr$info("processing data") output <- function() lgr$info("outputing data") analyze <- function(){ clean() process() output() } ``` `with_log_value()` provides a convenient wrapper to inject values into log calls. ```{r eval = FALSE} with_log_value( list(dataset_id = "dataset1"), analyze() ) ``` An alternative way to achieve the same is to use one of the preconfigured Filters that come with lgr. This approach is more more comfortable for use within functions. ```{r eval = FALSE} analyze <- function(id = "dataset1"){ lgr$add_filter(FilterInject$new(dataset_id = id), name = "inject") on.exit(lgr$remove_filter("inject")) clean() process() output() } analyze() ``` The result is the same in both cases: ```{r, echo = FALSE} with_log_value( list(dataset_id = "dataset1"), analyze() ) ``` You can use `with_log_level()` and `FilterForceLevel` in a similar fashion to modify the log level of events conveniently. ## Temporarily disable logging Temporary disabling logging for portions of code is straight forward and easy with lgr: ```{r} without_logging({ lgr$warn("Oh Yeah?") lgr$fatal("Oh No") }) ``` ## Adding a custom logger to a package If you are a package author, it is good practice to define a separate logger for your package. This gives users the ability to easily enable/disable logging on a per-package basis. Loggers must be initialized in the packages .onLoad hook. You can do this by adding the following code to any `.R` file inside the `R/` directory of your package: ```{r} # mypackage/R/mypackage-package.R .onLoad <- function(...){ assign( "lg", # the recommended name for a logger object lgr::get_logger(name = "mypackage"), # should be the same as the package name envir = parent.env(environment()) ) } ``` You can also just use `lgr::use_logger()` to generate the appropriate code for your package automatically. After you set this up you can use `lg$fatal()`, `lg$info()`, etc... inside your package. You do not have to define any appenders, since all log events will get redirected to the root Logger (see [Inheritance](#hierarchy)). ## Creating a Layout that mimics NodeJS Winston logstash see https://github.com/s-fleck/lgr/issues/29 ## Adding the source file / R-script to the log event Getting the source file is sadly non-trivial in R, otherwise it would have been included in the core logging functions. Luckily, this is easy to add with filters and the awesome [this.path](https://CRAN.R-project.org/package=this.path) package. This solution works in most scenarios, but not in all (for example, building this vignette). ```{r, eval = FALSE} # install.packages("this.path") lg <- get_logger("srcfile") lg$add_filter(function(event){ tryCatch({ event$srcfile <- this.path::this.path() }, error = function(e) NULL) TRUE }) ``` # References [Python Logging](https://docs.python.org/3/library/logging.html) [Eric Stenbock: The True Story of A Vampire](https://gutenberg.net.au/ebooks06/0606601h.html) lgr/NAMESPACE0000644000176200001440000000461415036470265012272 0ustar liggesusers# Generated by roxygen2: do not edit by hand S3method(as.data.frame,LogEvent) S3method(as.data.frame,event_list) S3method(as_LogEvent,data.frame) S3method(as_LogEvent,list) S3method(as_event_list,LogEvent) S3method(as_event_list,data.frame) S3method(as_event_list,list) S3method(as_logger_config,"NULL") S3method(as_logger_config,character) S3method(as_logger_config,list) S3method(format,LogEvent) S3method(format,Logger) S3method(format,ancestry) S3method(format,logger_tree) S3method(print,Appender) S3method(print,LogEvent) S3method(print,Logger) S3method(print,ancestry) S3method(print,logger_tree) S3method(string_repr,"function") S3method(string_repr,data.frame) S3method(string_repr,default) S3method(string_repr,matrix) S3method(string_repr,numeric) S3method(toString,LogEvent) export(.obj) export(Appender) export(AppenderBuffer) export(AppenderConsole) export(AppenderFile) export(AppenderFileRotating) export(AppenderFileRotatingDate) export(AppenderFileRotatingTime) export(AppenderJson) export(AppenderMemory) export(AppenderTable) export(CannotInitializeAbstractClassError) export(EventFilter) export(FilterForceLevel) export(FilterInject) export(Filterable) export(Layout) export(LayoutFormat) export(LayoutGlue) export(LayoutJson) export(LogEvent) export(Logger) export(LoggerGlue) export(add_appender) export(add_log_levels) export(as.data.table.event_list) export(as_LogEvent) export(as_event_list) export(as_logger_config) export(basic_config) export(colorize_levels) export(console_threshold) export(default_exception_handler) export(event_list) export(get_caller) export(get_log_levels) export(get_logger) export(get_logger_glue) export(get_user) export(is_filter) export(is_log_level) export(is_log_levels) export(is_threshold) export(label_levels) export(lgr) export(log_exception) export(logger_index) export(logger_tree) export(pad_left) export(pad_right) export(read_json_lines) export(remove_appender) export(remove_log_levels) export(show_data) export(show_dt) export(show_log) export(standardize_log_level) export(standardize_log_levels) export(standardize_threshold) export(string_repr) export(suspend_logging) export(threshold) export(unlabel_levels) export(unsuspend_logging) export(use_logger) export(with_log_level) export(with_log_value) export(with_logging) export(without_logging) importFrom(stats,setNames) lgr/LICENSE0000644000176200001440000000005414131760166012047 0ustar liggesusersYEAR: 2018 COPYRIGHT HOLDER: Stefan Fleck lgr/NEWS.md0000644000176200001440000002110215137114736012140 0ustar liggesusers# lgr 0.5.2 - Quote 'Dynatrace' and 'lgrExtra' in DESCRIPTION # lgr 0.5.1 - Fix doc problem for CRAN # lgr 0.5.0 - Added `rawMsg` property to LogEvents to store message without string interpolation (i.e. the original string that still contains the placeholders from either `sprintf()` or `glue()`) (#60) - Updated `AppenderConsole` to accept a `connection` argument. If called from a `{knitr}` rendering process, log messages are now output to `stderr` instead of `stdout` by default. This avoids polluting markdown documents (#62, thx @gadenbuie). - Replace `NULL` values and empty characters in logging by the string `""`. Before, `NULL` values would have resulted in empty log messages. (#51) - Support transformers for `LoggerGlue` (see `?glue::glue`) (#51) - Add `excluded_fields` to `Layout` for excluding fields from logging. - Add `transform_event` and `transform_event_names` to `LayoutJson` to make it more flexible. Exclude `rawMsg` by default for backwards compatibility. # lgr 0.4.4 - `%k` and `%K` parameters in `format.LogEvent` now work as expected when using **crayon** terminal colours (#52). - Fix default format string for `LayoutGlue` which was using `msg` instead of `{msg}` (#54, thx @mmuurr) - Update docs to use the more common term "structured logging" instead of "custom fields" where appropriate - `as_event_list.data.frame` now really returns a list of `LogEvents` - added `as_LogEvent()` to coerce various event-like objects to `LogEvents` - rebuild docs for R 4.2.0 # lgr 0.4.3 - `logger_index()` returns a `data.frame` with metadata on all registered loggers (#47) (thanks @Fuco1) - export new `string_repr()` generic that is used to layout R objects for formatted log message output (#48, thanks @mmuurr) - The `$log()` method of Logger and LoggerGlue now unpacks conditions (except if they are supplied as a named argument) (#45, thanks @mmuurr) - Fix some timezone related tests for CRAN # lgr 0.4.2 - Deprecated the `create_file` argument of `AppenderFileRotating*`. This is now hardcoded to `TRUE` (because `FALSE` doesn't really make sense here). - `default_exception_handler()` now throws more informative warnings if an error is encountered during logging. - drop tests for deprecated [future](https://cran.r-project.org/package=future) plans to ensure compatibility with upcoming versions of future (#43) # lgr 0.4.1 - Moved more complex Appenders to package [lgrExtra](https://github.com/s-fleck/lgrExtra). This includes database Appenders, email and push notifications and AppenderDt (in-memory `data.tables`). - `AppenderFile$show()` can now filter log files formatted by LayoutFormat by log level. Be aware that this just `greps` through the file and therefore will return false positives on lines where the log message contains strings that can be interpreted as log levels. - `AppenderFile$show()` and `AppenderFile$data` now dispatches to `Layout$read()` and `Layout$parse()`. This makes it possible to tie reading/parsing of log files to Layouts. - Loggers gain a `list_log()` method. See https://github.com/s-fleck/joblog for an R package that leverages this feature to create custom log event types for tracking the status of cron jobs. - Export more utility functions that are useful for creating custom Appenders; such as `standardize_threshold()` and `event_list()`. - AppenderBuffer now defaults to `flush_threshold = NULL` (never flush because of the log level of an event) - `basic_config()` now works as documented for .jsonl files - AppenderMemory gains a `$clear()` method that clears the buffer without sending the events to it's attached appenders - LayoutJson gains a `timestamp_fmt` field that can be used for custom timestamp formats (#34) - added `toString.LogEvent()` for compact representations of LogEvents - lgr is now automatically tested for all major R version >= 3.2.0 - AppenderMemory/AppenderBuffer: `flush_threshold` is now independent of `should_flush` function. `default_should_flush()` is no longer necessary and has been removed. - Updated AppenderFileRotating and co for compatibility with [rotor](https://github.com/s-fleck/rotor) 0.3.0 - Most errors now have appropriate subclasses - `Logger$log()` dispatches to all appenders - even if some throw an error - instead of aborting after the first Appender that throws an error - complete rewrite of the documentation to use the new roxygen2 features for R6 classes. # lgr 0.3.4 - Hotfix for compatibility with R < 3.6.0 (#32) # lgr 0.3.3 - Fixed a performance regression when looking up the parent of a Logger. This notably affected the performance of "do-nothing" logging (e.g. when a log message is discarded because it is below a loggers' threshold) # lgr 0.3.2 - Added AppenderSyslog for logging to syslog via [rsyslog](https://github.com/atheriel/rsyslog) (thanks to atheriel) # lgr 0.3.1 - Added `logger_tree()` which provides an overview of all registered loggers - Added `print()` and `format()` methods for Appenders - `AppenderMemory`: added `data` and `dt` active fields (which return the log as a data.frame or data.table) - Removed deprecated functions `FATAL()`, `ERROR()`. Use `lgr$fatal()`, `lgr$error()`, ... instead. - `AppenderMemory`: `$buffer_dt()` and `$show()` now handle custom fields containing atomic vectors correctly # lgr 0.3.0 - Added support for rotating log files via `AppenderFileRotating`, `AppenderFileRotatingDate` and `AppenderFileRotatingTime`. Requires the package [rotor](https://github.com/s-fleck/rotor). - functions like `show_log()`, `show_data()`,... now accept logger names as well as Logger or Appender objects as `target`. - `AppenderFile$new()` now creates an empty file, or fails if it can't - Improved support for RMariaDB and dropped support for RMySQL - Improved support for RPostgres and dropped support for RPostgreSQL - added `reset` argument to `get_logger()`. This completely resets the configuration of the logger and also replaces special loggers (such as `LoggerGlue`) with vanilla ones. # lgr 0.2.2 - The root logger can now be configured via `options()` and/or environment variables (see `?lgr`) - `basic_config()` now accepts thresholds ("info", "fatal") as arguments to `console` and `memory`. - The default config of the root logger has changed. It now only has a console appender and a default threshold of `"info"`. To get back the old behaviour run `basic_config(threshold = "all", console = "info", memory = "all")`. - `$config(NULL)` now resets a Logger to its default/unconfigured state - `$config()` now accepts YAML as well as JSON files (or YAML/JSON as a character string) - `with_log_level()` and `with_log_value()` now accept logger names as well as Logger objects as the `logger` argument - `get_logger_glue()` now works as intended - Deprecated `FATAL()`, `ERROR()`. Use `lgr$fatal()`, `lgr$error()`, ... instead. # lgr 0.2.1 - Emergency fix that ensures test suite cleans up temporary files - Removed .rd file for the unexported LoggerRoot class # lgr 0.2.0 - `get_loggers()` registers new loggers in the lgr::loggers namespace, this is a more global and decoupled approach similar to how python logging handles loggers. - removed `full_name` active binding for loggers. Loggers now only have qualified names and `name` is now identical to what `full_name` was before. For consistency the format method of `ancestry` has also been revised. - Logger inheritance is now derived from the qualified name of a logger. Consequently `lg$parent` is now derived from `lg$name`, `lg$set_parent()` is no longer possible. - If no threshold is set for a new Logger, it now inherits the threshold of its parent - Depend on R6 >= 2.4.0 which includes relevant fixes to finalizers. finalize methods are now private. - Logger now have a `config` method that allows configuring Loggers with config objects and YAML files (experimental) - added `with_logging()`, the opposite of `without_logging()`. This can be handy for automated tests where you might want so switch logging off/on only for specific unit tests. # lgr 0.1.1 - Added `show_data()` and `show_dt()` for quick access to the root loggers in memory log as `data.frame` or `data.table` respectively - numerous small fixes - removed non-breaking-spaces from .RD files. This caused unforeseen problems with the compiling the .pdf manual during the CRAN submission process. lgr/inst/0000755000176200001440000000000015137115052012013 5ustar liggesuserslgr/inst/draft_vignettes/0000755000176200001440000000000014131760166015210 5ustar liggesuserslgr/inst/draft_vignettes/lgr-configs.Rmd0000644000176200001440000000160714131760166020072 0ustar liggesusers--- title: "Example Configurations" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{lgr-simple} %\VignetteEncoding{UTF-8} %\VignetteEngine{knitr::rmarkdown} editor_options: chunk_output_type: console --- ```{r, include = FALSE} knitr::opts_chunk$set( collapse = TRUE, comment = "", echo = FALSE ) ``` ```{r setup} library(lgr) ``` # minimal ```{r} cfg <- system.file("configs/minimal.yaml", package = "lgr") cat(readLines(cfg), sep = "\n") ``` ```{r} print(lgr::lgr$config(cfg)) ``` ```{r} lgr$info("test") ``` # recommended ```{r} cfg <- system.file("configs/recommended.yaml", package = "lgr") cat(readLines(cfg), sep = "\n") ``` ```{r} print(lgr::lgr$config(cfg)) ``` ```{r echo = TRUE, comment="#>"} lgr$trace("trace messages get ignored by console appender") lgr$info("test") show_log() ``` lgr/inst/draft_vignettes/lgr-faq.Rmd0000644000176200001440000000206514131760166017210 0ustar liggesusers--- title: "Frequently Asked Questions" author: "Stefan Fleck" date: "`r Sys.Date()`" output: rmarkdown::html_vignette: toc: true number_sections: true vignette: > %\VignetteIndexEntry{lgr-faq} %\VignetteEncoding{UTF-8} %\VignetteEngine{knitr::rmarkdown} editor_options: chunk_output_type: console --- # Frequently Asked Questions https://github.com/s-fleck/lgr/issues/17 Sometimes I would like to have structured logging in order to be able to have msg in a log not as a plain string, but an easy to parse object. So it would be nice to have ability to specify on how to "translate" R's message into logging record. For example with JSON layout we can serialize R objects into json objects (and have fully machine-readable logs) AppenderJson looks a little bit weird - it mixes both file sink and JSON formatting. https://github.com/s-fleck/lgr/issues/15 How can i have custom formatting for a package logger When should i use get_logger(), when should i use Logger$new Why is Logger (Appender, AppenderDigest, etc) exported lgr/inst/configs/0000755000176200001440000000000014131760166013450 5ustar liggesuserslgr/inst/configs/minimal.yaml0000644000176200001440000000010014131760166015751 0ustar liggesusersLogger: threshold: info appenders: AppenderConsole: lgr/inst/configs/recommended.yaml0000644000176200001440000000020314131760166016611 0ustar liggesusersLogger: threshold: all appenders: AppenderConsole: threshold: info AppenderBuffer: threshold: all lgr/inst/logger_comparison/0000755000176200001440000000000014131760166015531 5ustar liggesuserslgr/inst/logger_comparison/logger_comparison.Rmd0000644000176200001440000000651514131760166021715 0ustar liggesusers--- title: "Logger Comparison" author: "Stefan Fleck" date: "`r Sys.Date()`" output: html_document: toc: true number_sections: true --- ```{r setup, include = FALSE} knitr::opts_chunk$set( collapse = TRUE, comment = "#>" ) library(devtools) library(bench) library(dplyr) library(forcats) ``` ```{r eval = FALSE} install_github("daroczig/logger") install.packages("log4r") install.packages("futile.logger") install.packages("logging") ``` ```{r} library(lgr) library(logger) library(log4r) library(futile.logger) ``` Setup packages that do not work out-of-the-box ```{r} logging::addHandler(logging::writeToConsole, logger="logging") lgr::lgr$info("an informative message") logging::loginfo("an informative message") futile.logger::flog.info("an informative message") ``` # Syntax Examples wip # Feature Matrix wip # Performance This section reviews the performance of available logging packages against `cat()`. ## Simple Console Logging Call Log a simple log message ```{r, results='hide'} # By default lgr is configured for color output, so we disable that for a fair # comparison lgr$set_appenders(AppenderConsole$new(layout = LayoutFormat$new())) sink("/dev/null") res <- bench::mark( cat = cat("FATAL", " [", format(Sys.time()), "] ", "test", sep = ""), logger = logger::log_info("test"), futile.logger = futile.logger::flog.info("test"), logging = logging::logerror("test", logger = "logging"), lgr = lgr::lgr$info("test"), check = FALSE ) sink() ``` ```{r} res %>% select(expression, min, mean, median, mem_alloc) %>% arrange(median) %>% knitr::kable() res$expression <- fct_reorder(res$expression, res$median) plot(res) ``` For simple log messages **logging* and *logger** are in the clear lead performance wise # Log a simple log message (with colors) The only packages that support color output are **lgr** and **logger**. Both do not allow much configuration of the color output. Here lgr is in the lead, mainly because **logger** relies on **glue** for the color formatting. Both ```{r, results='hide'} lgr$set_appenders(AppenderConsole$new()) log_layout(layout_glue_colors) library(crayon) sink("/dev/null") res <- bench::mark( cat = cat(red("FATAL"), " [", format(Sys.time()), "] ", "test", sep = ""), logger = logger::log_fatal("test"), lgr = lgr::lgr$info("test"), check = FALSE ) sink() ``` ```{r} res %>% select(expression, min, mean, median, mem_alloc) %>% arrange(median) %>% knitr::kable() res$expression <- fct_reorder(res$expression, res$median) plot(res) ``` # Log a suppressed message ```{r} lgr$set_threshold("info") res <- bench::mark( cat = cat(), logger = logger::log_debug("test"), futile.logger = futile.logger::flog.debug("test"), logging = logging::logdebug("test", logger = "logging"), lgr = lgr::lgr$debug("test"), check = FALSE ) ``` ```{r} plot(res) ``` Here lgr is pretty much in the lead, because it completely disables the logging functions that are under the loggers threshold. ### Conclusion While there are subtle differences in the performance of the available logging packages, all perform pretty well. Nothing can compare with a simple `cat()`, but that was to be expected. lgr/inst/doc/0000755000176200001440000000000015137115052012560 5ustar liggesuserslgr/inst/doc/lgr.R0000644000176200001440000003176615137115051013503 0ustar liggesusers## ----setup, include = FALSE--------------------------------------------------- library(lgr) knitr::opts_chunk$set( collapse = TRUE, comment = "#>" ) ## ----------------------------------------------------------------------------- # the root logger is called "lgr" lgr$info("Vampire stories are generally located in Styria.") ## ----------------------------------------------------------------------------- lgr$error("Vampires generally arrive in carriages drawn by %i black horses.", 2) ## ----------------------------------------------------------------------------- tf <- tempfile(fileext = ".info") lgr$add_appender(AppenderFile$new(tf), name = "file") lgr$info("You must think I am joking") readLines(tf) ## ----------------------------------------------------------------------------- lgr$appenders$file$set_layout(LayoutFormat$new(timestamp_fmt = "%B %d %T")) lgr$info("No, I am quite serious") readLines(tf) #cleanup unlink(tf) ## ----------------------------------------------------------------------------- # cleanup behind the old Appender unlink(tf) lgr$remove_appender("file") # setup a JSON appender lgr$add_appender(AppenderJson$new(tf), name = "json") lgr$info("We lived in Styria") ## ----------------------------------------------------------------------------- cat(readLines(tf)) ## ----------------------------------------------------------------------------- read_json_lines(tf) ## ----------------------------------------------------------------------------- # show is a method and takes some extra arguments, like maximum number of lines # to show lgr$appenders$json$show() # $data always returns a data.frame if available. It is an active binding # rather than a method, so no extra arguments are possible lgr$appenders$json$data ## ----------------------------------------------------------------------------- # The default console appender displays custom fields as pseudo-json after the message lgr$info("Styria has", poultry = c("capons", "turkeys")) # JSON can store most R objects quite naturally read_json_lines(tf) read_json_lines(tf)$poultry[[2]] # works because poultry is a list column ## ----echo = FALSE------------------------------------------------------------- lgr$remove_appender("json") unlink(tf) ## ----echo = FALSE------------------------------------------------------------- ll <- data.frame( `Level` = c(0, seq(100, 600, by = 100), NA), `Name` = c("off", "fatal", "error", "warn", "info", "debug", "trace", "all"), `Description` = c( "Tells a Logger or Appender to suspend all logging", "Critical error that leads to program abort. Should always indicate a `stop()` or similar", "A severe error that does not trigger program abort", "A potentially harmful situation, like `warning()`", "An informational message on the progress of the application", "Finer grained informational messages that are mostly useful for debugging", "An even finer grained message than debug ([more info](https://softwareengineering.stackexchange.com/questions/279690/why-does-the-trace-level-exist-and-when-should-i-use-it-rather-than-debug))", "Tells a Logger or Appender to process all log events" ) ) knitr::kable(ll) ## ----------------------------------------------------------------------------- lgr$fatal("This is an important message about %s going wrong", "->something<-") lgr$trace("Trace messages are still hidden") lgr$set_threshold("trace") lgr$trace("Unless we lower the threshold") ## ----------------------------------------------------------------------------- lgr$info("The sky was the color of %s, tuned to a dead chanel", "television") ## ----------------------------------------------------------------------------- lgr$info("Vampire stories are generally located in Styria") lgr$last_event # a summary output of the event lgr$last_event$values # all values stored in the event as a list ## ----------------------------------------------------------------------------- # bad lgr$info("Processing track '%s' with %s waypoints", "track.gpx", 32) # Good tf <- tempfile() lgr$add_appender(AppenderJson$new(tf), "json") lgr$info("Processing track", file = "track.gpx", waypoints = 32) lgr$appenders$json$data ## ----echo = FALSE------------------------------------------------------------- lgr$remove_appender("json") unlink(tf) ## ----------------------------------------------------------------------------- f1 <- function(event) { grepl("bird", event$msg) } lgr$set_filters(list(f1)) lgr$info("is it a plane?") lgr$info("no! is it a bird?") # since this is not a very useful filter, we better remove it again lgr$set_filters(NULL) ## ----------------------------------------------------------------------------- tf <- tempfile() # Add a new appender to a logger. We don't have to supply a name, but that # makes it easier to remove later. lgr$add_appender(AppenderFile$new(file = tf), name = "file") # configure lgr so that it logs everything to the file, but only info and above # to the console lgr$set_threshold(NA) lgr$appenders$console$set_threshold("info") lgr$appenders$file$set_threshold(NA) lgr$info("Another informational message") lgr$debug("A debug message not shown by the console appender") readLines(tf) # Remove the appender again lgr$remove_appender("file") unlink(tf) ## ----------------------------------------------------------------------------- # install.packages("glue") lg <- get_logger_glue("glue/logger") lg$info( "glue automatically ", "pastes together unnamed arguments ", "and evaluates arbitray expressions inside braces {Sys.Date()}" ) ## ----------------------------------------------------------------------------- lg$info("For more info on glue see {website}", website = "https://glue.tidyverse.org/") ## ----------------------------------------------------------------------------- lg$info("Glue is available from {.cran}", .cran = "https://CRAN.R-project.org/package=glue") ## ----------------------------------------------------------------------------- lg <- get_logger("test") lg$config(NULL) # resets logger to unconfigured state lg$set_threshold("fatal") ## ----------------------------------------------------------------------------- lg$ set_threshold("info")$ set_appenders(AppenderConsole$new(threshold = "info"))$ set_propagate(FALSE) ## ----------------------------------------------------------------------------- lg$config(list( threshold = "info", propagate = FALSE, appenders = AppenderConsole$new(threshold = "info") )) ## ----eval = FALSE------------------------------------------------------------- # lg$config("path/to/config.yaml") # lg$config("path/to/config.json") ## ----------------------------------------------------------------------------- # Via YAML cfg <- " Logger: threshold: info propagate: false appenders: AppenderConsole: threshold: info " lg$config(cfg) ## ----------------------------------------------------------------------------- lg <- get_logger("test") lg$set_appenders(list(cons = AppenderConsole$new())) lg$set_propagate(FALSE) lg$info("the default format") lg$appenders$cons$layout$set_fmt("%L (%n) [%t] %c(): !! %m !!") lg$info("A more involved custom format") ## ----------------------------------------------------------------------------- # install.packages("glue") library(glue) lg$appenders$cons$set_layout(LayoutGlue$new( fmt = "{.logger$name} {level_name} {caller}: {toupper(msg)}" )) lg$info("with glue") ## ----------------------------------------------------------------------------- # install.packages("jsonlite") tf <- tempfile() lg <- get_logger("test") lg$set_appenders(list(json = AppenderJson$new(file = tf))) lg$set_propagate(FALSE) lg$info("JSON naturally ", field = "custom") lg$info("supports custom", numbers = 1:3) lg$info("log fields", use = "JSON") ## ----eval = FALSE------------------------------------------------------------- # lg$appenders$json$data # # same as # read_json_lines(tf) ## ----echo = FALSE------------------------------------------------------------- lg$appenders$json$data ## ----eval = FALSE------------------------------------------------------------- # lg$appenders$json$show() # # same as # cat(readLines(tf), sep = "\n") ## ----echo = FALSE------------------------------------------------------------- lg$appenders$json$show() ## ----------------------------------------------------------------------------- # cleanup lg$config(NULL) unlink(tf) ## ----------------------------------------------------------------------------- # install.packages("rotor") tf <- tempfile(fileext = ".log") lg <- get_logger("test")$ set_propagate(FALSE)$ set_appenders(list(rotating = AppenderFileRotating$new( file = tf, size = "10 kb", max_backups = 5)) ) for (i in 1:100) lg$info(paste(LETTERS, sep = "-")) # display info on the backups of tf lg$appenders$rotating$backups # manually delete all backups invisible(lg$appenders$rotating$prune(0)) lg$appenders$rotating$backups #cleanup unlink(tf) ## ----------------------------------------------------------------------------- # The logger name should be the same as the package name tf <- tempfile() lg <- get_logger("mypackage") lg$add_appender(AppenderFile$new(tf)) ## ----------------------------------------------------------------------------- print(lg) ## ----------------------------------------------------------------------------- lg$info("A test message for lg") ## ----------------------------------------------------------------------------- lg$set_propagate(FALSE) ## ----------------------------------------------------------------------------- print(lg) ## ----------------------------------------------------------------------------- lg$info("Nothing to see here") ## ----------------------------------------------------------------------------- # cleanup lg$config(NULL) unlink(tf) ## ----------------------------------------------------------------------------- lg <- get_logger("buffer") lg$ set_threshold(NA)$ set_propagate(FALSE)$ set_appenders( AppenderBuffer$new( threshold = NA, buffer_size = 5, # can hold 5 events, the 6th will trigger flushing flush_on_exit = FALSE, flush_on_rotate = FALSE, flush_threshold = "error", appenders = AppenderConsole$new(threshold = NA) )) # The for loop below stores 8 log events in the Buffer for (nm in month.name[1:8]) lg$debug("%s", nm) # An event of level 'error' or 'fatal' triggers flushing of the buffer lg$error("But the days grow short when you reach September") ## ----eval = FALSE------------------------------------------------------------- # # install.packages("RSQLite") # # install.packages("lgrExtra") # lg <- get_logger("db_logger") # lg$ # set_propagate(FALSE)$ # add_appender( # name = "db", # lgrExtra::AppenderDbi$new( # conn = DBI::dbConnect(RSQLite::SQLite()), # table = "log", # buffer_size = 2L # ) # ) # # lg$info("Logging to databases uses a buffer") # lg$info("As the buffer size is 2, no insert took place till now") # lg$appenders$db$show() # # lg$info("Now as the buffer is rotated, all events are output to the db") # lg$appenders$db$show() ## ----------------------------------------------------------------------------- # setup an example function clean <- function() lgr$info("cleaning data") process <- function() lgr$info("processing data") output <- function() lgr$info("outputing data") analyze <- function(){ clean() process() output() } ## ----eval = FALSE------------------------------------------------------------- # with_log_value( # list(dataset_id = "dataset1"), # analyze() # ) ## ----eval = FALSE------------------------------------------------------------- # analyze <- function(id = "dataset1"){ # lgr$add_filter(FilterInject$new(dataset_id = id), name = "inject") # on.exit(lgr$remove_filter("inject")) # # clean() # process() # output() # } # analyze() ## ----echo = FALSE------------------------------------------------------------- with_log_value( list(dataset_id = "dataset1"), analyze() ) ## ----------------------------------------------------------------------------- without_logging({ lgr$warn("Oh Yeah?") lgr$fatal("Oh No") }) ## ----------------------------------------------------------------------------- # mypackage/R/mypackage-package.R .onLoad <- function(...){ assign( "lg", # the recommended name for a logger object lgr::get_logger(name = "mypackage"), # should be the same as the package name envir = parent.env(environment()) ) } ## ----eval = FALSE------------------------------------------------------------- # # install.packages("this.path") # # lg <- get_logger("srcfile") # lg$add_filter(function(event){ # tryCatch({ # event$srcfile <- this.path::this.path() # }, error = function(e) NULL) # TRUE # }) lgr/inst/doc/lgr.Rmd0000644000176200001440000006772414402036437014033 0ustar liggesusers--- title: "lgr: A fully featured logging framework for R" author: "Stefan Fleck" date: "`r Sys.Date()`" output: rmarkdown::html_vignette: toc: true number_sections: true vignette: > %\VignetteIndexEntry{lgr} %\VignetteEncoding{UTF-8} %\VignetteEngine{knitr::rmarkdown} editor_options: chunk_output_type: console --- ```{r setup, include = FALSE} library(lgr) knitr::opts_chunk$set( collapse = TRUE, comment = "#>" ) ``` # Introduction lgr is a logging framework for R inspired by [Apache Log4j](https://logging.apache.org/log4j/2.x/) and [Python logging](https://docs.python.org/3/library/logging.html). It follows an object oriented design implemented through [R6 classes](https://github.com/r-lib/R6). This enables lgr to have a larger set of features than other logging packages for R, and makes it flexible and easy to extend. If you are not sure if lgr is the right package for you, take a look [examples](#examples) to see what lgr can do for you. ## Quickstart lgr comes with a so-called **root logger** that is ready to go after you install it. If you are a package developer you can (and should) set up a new Logger for your package, but more on that later. For many use cases the root Logger will suffice. ### Logging to the console ```{r} # the root logger is called "lgr" lgr$info("Vampire stories are generally located in Styria.") ``` You can use formatting strings that are passed on to `sprintf()` in lgr. ```{r} lgr$error("Vampires generally arrive in carriages drawn by %i black horses.", 2) ``` ### Logging to plaintext files Usually you don't (only) want to log to the console, you want to log to files. Output of Loggers is managed by Appenders. The root Logger is preconfigured with a console Appender (that is why we see output in the console). Let's add a file Appender: ```{r} tf <- tempfile(fileext = ".info") lgr$add_appender(AppenderFile$new(tf), name = "file") lgr$info("You must think I am joking") readLines(tf) ``` The various Appenders available in lgr are R6 classes. To instantiate an object of that class (i.e. create a new appender) you have to use the `$new()` function as in the example above. Whenever you see something in lgr that `IsNamedLikeThis`, you can be sure that it is such an R6 class. If you look at the output, you see that the timestamp format of the file appender is slightly different to the timestamp format of the console Appender. Formatting is handled by **Layouts**, and each Appender has exactly one: ```{r} lgr$appenders$file$set_layout(LayoutFormat$new(timestamp_fmt = "%B %d %T")) lgr$info("No, I am quite serious") readLines(tf) #cleanup unlink(tf) ``` ### Logging to JSON files If you log to files, you should not log normal text. If you want to analyse your logs later, it's much better to log to a format like JSON: ```{r} # cleanup behind the old Appender unlink(tf) lgr$remove_appender("file") # setup a JSON appender lgr$add_appender(AppenderJson$new(tf), name = "json") lgr$info("We lived in Styria") ``` JSON is still somewhat human readable ```{r} cat(readLines(tf)) ``` and easy for machines to parse ```{r} read_json_lines(tf) ``` Many Appenders provide either a `$show()` method and a `$data` active binding convenience, and so you do not have to call `readLines()` & co manually. ```{r} # show is a method and takes some extra arguments, like maximum number of lines # to show lgr$appenders$json$show() # $data always returns a data.frame if available. It is an active binding # rather than a method, so no extra arguments are possible lgr$appenders$json$data ``` Please note that under the hood, `AppenderJson` is just an `AppenderFile` with `LayoutJson`. The only difference is AppenderJson provides a `$data()` method while AppenderFile does not. ### Structured Logging (custom fields) lgr treats a *LogEvent* as a unit of data, not just a message with a timestamp. A log event can contain arbitrary data, though not all Appenders can handle that well. The JSON appender we added above is particularly good at handling most R objects. ```{r} # The default console appender displays custom fields as pseudo-json after the message lgr$info("Styria has", poultry = c("capons", "turkeys")) # JSON can store most R objects quite naturally read_json_lines(tf) read_json_lines(tf)$poultry[[2]] # works because poultry is a list column ``` ```{r echo = FALSE} lgr$remove_appender("json") unlink(tf) ``` ### What Else If the examples above have piqued your interest, the rest of this vignette will provide more details on the workings of lgr. Discussing all Appenders and configuration options is beyond the scope of this vignette, please refer to the [function reference](https://s-fleck.github.io/lgr/reference/index.html) for that. # Usage ## Structure of the logging system If you want custom logging configurations, you have to understand the structure of the logging process. * A **Logger** collects information and dispatches it to its *Appenders*, and also the *Appenders* of its *Parent Loggers* (also see the section on hierarchical logging) * An **Appender** writes the log message to destination (the console, a file, a database, etc...). * A **Layout** is used by an Appender to format LogEvents. For example, `AppenderFile` uses `LayoutFormat` by default to write human readable log events to a text file, but can also use `LayoutJson` produce machine readable JSON lines logfiles. * **LogEvents** are produced by the Logger and dispatched to Appenders. They contain all the information that is being logged (think of it as a row in table). LogEvents usually contain the log level, a timestamp, a message, the name of the calling function, and a [reference](https://adv-r.hadley.nz/r6.html#r6-semantics]) to the Logger that created it. In addition, a LogEvent can contain any number of custom fields. See [examples 1 & 2](#examples) ### On R6 classes The elements described above are R6 classes. R6 is an object orientation system for R that is used by many popular packages such as shiny, dplyr, plumber, roxygen2, and testthat but often behind the scenes and not as exposed as in lgr. You recognize R6 classes in this package because they are named following the `UpperCamelCase` naming convention. While there is only one kind of Logger and one kind of LogEvent, there are several subclasses of Appenders and Layouts. An introduction to R6 classes is beyond the scope of this document, but you can find the official documentation [here](https://r6.r-lib.org/) and there is also this [talk on Youtube](https://www.youtube.com/watch?v=3GEFd8rZQgY). In short R6 classes store data (fields) together with functions (methods) and have to be instantiated with `$new()`. So if you want to create a new `AppenderFile`, you do this by calling `AppenderFile$new(file = tempfile())`. Please not that Loggers should never be instantiated directly with `$new()` but always with `get_logger()`. ## Log levels lgr supports the standard log4j Log Levels outlined bellow. The Log Level of an event represents its severity. The named log levels are really just nicknames for integer values, and you can use the `character` or `integer` representations interchangeably. You can also use arbitrary integer values (greater than `0`), but you are encouraged to stick to the ones bellow. ```{r, echo = FALSE} ll <- data.frame( `Level` = c(0, seq(100, 600, by = 100), NA), `Name` = c("off", "fatal", "error", "warn", "info", "debug", "trace", "all"), `Description` = c( "Tells a Logger or Appender to suspend all logging", "Critical error that leads to program abort. Should always indicate a `stop()` or similar", "A severe error that does not trigger program abort", "A potentially harmful situation, like `warning()`", "An informational message on the progress of the application", "Finer grained informational messages that are mostly useful for debugging", "An even finer grained message than debug ([more info](https://softwareengineering.stackexchange.com/questions/279690/why-does-the-trace-level-exist-and-when-should-i-use-it-rather-than-debug))", "Tells a Logger or Appender to process all log events" ) ) knitr::kable(ll) ``` `off` and `all` are valid thresholds for Appenders and Loggers, but not valid levels for LogEvents; e.g. `lgr$set_threshold(NA)` makes sense, but `lgr$log("all", "an example message")` does not. The list of named log levels is stored as a global option (`getOption("lgr.log_levels")`) and you can use `add_log_levels()` and `remove_log_levels()` to define your own named levels if you want to. There are only predefined logging methods (`lgr$fatal()`, etc..) for the standard log levels and you have to use `lgr$log(level, message)` to create a LogEvent with a custom log level. ## Logging with the Root Logger lgr comes with a pre-configured root Logger. It is called *root* because you can [set up a tree of Loggers](#hierarchy) that descent from it, but for basic use you will not have to worry about that. ### Logging syntax lgr Loggers are R6 objects with *methods* (functions) for logging. You can refer to the *root* logger with `lgr`. ```{r} lgr$fatal("This is an important message about %s going wrong", "->something<-") lgr$trace("Trace messages are still hidden") lgr$set_threshold("trace") lgr$trace("Unless we lower the threshold") ``` ### Formatting strings You can use `sprintf()` style formatting strings directly in log messages. ```{r} lgr$info("The sky was the color of %s, tuned to a dead chanel", "television") ``` ## LogEvents: The atomic unit of logging LogEvents are objects that store all information collected by the Logger. They are passed on to Appenders that output them, but Appenders usually don't utilize all the information present in a log event. The last event produced by a Logger is stored in its `last_event` field. ```{r} lgr$info("Vampire stories are generally located in Styria") lgr$last_event # a summary output of the event lgr$last_event$values # all values stored in the event as a list ``` LogEvents can contain not only these standard values, but an arbitrary number of extra values. These extra values are passed as named arguments to the logging function (as opposed as to parameters to `sprintf()`, which are passed as unnamed arguments). It is up to the Appender whether to process them further or not. You should consider making use of structured logging liberally and using output formats that support them (such as JSON), rather than producing elaborately formatted but hard to parse log messages. ```{r} # bad lgr$info("Processing track '%s' with %s waypoints", "track.gpx", 32) # Good tf <- tempfile() lgr$add_appender(AppenderJson$new(tf), "json") lgr$info("Processing track", file = "track.gpx", waypoints = 32) lgr$appenders$json$data ``` ```{r echo = FALSE} lgr$remove_appender("json") unlink(tf) ``` ## Thresholds & Filters: controlling output detail {#thresholds} To control the level of detail of the log output, you can set **thresholds** for Loggers and Appenders. A Logger with a threshold of `warn` will only create LogEvents of the priorities `warn`, `error` and `fatal` and dispatch them to its Appenders. > A **threshold** of a Logger or Appender is the minimum **log level** a > LogEvent must have so that that Logger/Appender processes it. If you require more complex logic to decide whether a LogEvent should be created/processed you can also assign **filters** to Loggers/Appenders. Filters are just functions that have exactly one argument, `event` (the LogEvent to be filtered), and return `TRUE` or `FALSE`. They will be applied after the threshold is checked. Alternatively there is also a formal R6 class for Filters (?EventFilter) that you can use, but this is usually not necessary. examples: ```{r} f1 <- function(event) { grepl("bird", event$msg) } lgr$set_filters(list(f1)) lgr$info("is it a plane?") lgr$info("no! is it a bird?") # since this is not a very useful filter, we better remove it again lgr$set_filters(NULL) ``` ## Appenders: Managing log destinations The root logger only logs to the console by default. If you want to redirect the output to a file you can just add a file appender to lgr. ```{r} tf <- tempfile() # Add a new appender to a logger. We don't have to supply a name, but that # makes it easier to remove later. lgr$add_appender(AppenderFile$new(file = tf), name = "file") # configure lgr so that it logs everything to the file, but only info and above # to the console lgr$set_threshold(NA) lgr$appenders$console$set_threshold("info") lgr$appenders$file$set_threshold(NA) lgr$info("Another informational message") lgr$debug("A debug message not shown by the console appender") readLines(tf) # Remove the appender again lgr$remove_appender("file") unlink(tf) ``` ## Inheritance: Hierarchical Loggers {#hierarchy} Logger hierarchies are a powerful concept to organize logging for different parts of a larger system. This is mainly relevant for package developers. It is good practice to have a separate Logger for each package. Since it is not common in R to build complex systems of hierarchically organised packages, hierarchies will usually be pretty flat (i.e. most Loggers will only inherit from the root logger). Each newly created Logger is child to a parent Logger, derived from its name. So `lg <- get_logger("foo/bar")` creates the logger with the qualified name `foo/bar` whose parent is the logger `foo` whose parent is (implicitly) the `root` logger. If the logger `foo` does not exist in that scenario, it is created automatically. This behaviour might sound strange at first, but it mimics tried and tested behaviour of python logging. This way logging is decoupled from the business logic and your program will not abort if you forgot to initialize some logger up the hierarchy for whatever reason. A logger dispatches the LogEvents it creates not only to its own Appenders, but also to the Appenders of all its ancestral Loggers (ignoring the threshold and Filters of the ancestral Loggers, but not of the Appenders). When you define Loggers for your package, you should *not* configure them (with custom Appenders or thresholds); that should be left to the user of the package. If all this sounds confusing to you, take a look at the [examples](#examples-hierarchy) and `?logger_tree`. The common use cases are pretty easy to understand and illustrate the *how* and *why* pretty well. Example hierarchy for the package **fancymodel** that provides a model along with a plumber API and a shiny web-interface to the package. ``` r # prints a tree structure of all registered loggers logger_tree() ``` ``` root [info] -> 1 appenders └─fancymodel ├─plumber └─#shiny ├─server [trace] -> 2 appenders └─ui -> 1 appenders ``` ### Log flow The graph bellow outlines the flow of LogEvents through the logging system. This is an important reference if you want to work with Filters and Logger hierarchies. ![](log_flow.svg) ## Logging with LoggerGlue [glue](https://glue.tidyverse.org/) is very nicely designed package for string interpolation. It makes composing log messages more flexible and comfortable at the price of an additional dependency and slightly less performance than `sprintf()` (which is used by normal Loggers). To take advantage of glue, simply create a new LoggerGlue like this: ```{r} # install.packages("glue") lg <- get_logger_glue("glue/logger") lg$info( "glue automatically ", "pastes together unnamed arguments ", "and evaluates arbitray expressions inside braces {Sys.Date()}" ) ``` Glue lets you define temporary variables inside the `glue()` call. As with the normal Logger, named arguments get turned into custom fields. ```{r} lg$info("For more info on glue see {website}", website = "https://glue.tidyverse.org/") ``` You can suppress this behaviour by making named argument start with a `"."`. ```{r} lg$info("Glue is available from {.cran}", .cran = "https://CRAN.R-project.org/package=glue") ``` # Configuration ## With setters There are several different ways to configure loggers. The most straight forward one is to use *setters* to specify the Loggers properties. ```{r} lg <- get_logger("test") lg$config(NULL) # resets logger to unconfigured state lg$set_threshold("fatal") ``` lgr sets up Loggers in a way so that R6 piping with `$` is possible. This works similar to the magrittr (`#%>#`) pipes. ```{r} lg$ set_threshold("info")$ set_appenders(AppenderConsole$new(threshold = "info"))$ set_propagate(FALSE) ``` ## With a list object ```{r} lg$config(list( threshold = "info", propagate = FALSE, appenders = AppenderConsole$new(threshold = "info") )) ``` ## With YAML or JSON You can use YAML and JSON config files with lgr. ```{r eval = FALSE} lg$config("path/to/config.yaml") lg$config("path/to/config.json") ``` You can also pass in YAML/JSON directly as a character string (or vector with one element per line) ```{r} # Via YAML cfg <- " Logger: threshold: info propagate: false appenders: AppenderConsole: threshold: info " lg$config(cfg) ``` # Examples {#examples} ## Logging to the console lgr comes with simple but powerful formatting syntax for LogEvents. Please refer to `?format.LogEvent` for the full list of available placeholders. ```{r} lg <- get_logger("test") lg$set_appenders(list(cons = AppenderConsole$new())) lg$set_propagate(FALSE) lg$info("the default format") lg$appenders$cons$layout$set_fmt("%L (%n) [%t] %c(): !! %m !!") lg$info("A more involved custom format") ``` If this is not enough for you, you can use `LayoutGlue` based on the awesome [glue](https://github.com/tidyverse/glue) package. The syntax is a bit more verbose, and `AppenderGlue` is a bit less performant than `AppenderFormat`, but the possibilities are endless. ```{r} # install.packages("glue") library(glue) lg$appenders$cons$set_layout(LayoutGlue$new( fmt = "{.logger$name} {level_name} {caller}: {toupper(msg)}" )) lg$info("with glue") ``` All fields of the [LogEvent] object are exposed through LayoutGlue, so please refer to `?LogEvent` for a list of all available Fields. ## Logging to JSON files JavaScript Object Notation (JSON) is an open-standard file format that uses human-readable text to transmit data objects consisting of attribute–value pairs and array data types ([Wikipedia](https://en.wikipedia.org/wiki/JSON)). JSON is the **recommended text-based logging format when logging to files** ^[Technically, the logger does not produce standard JSON files but [JSON lines](https://jsonlines.org/)], as it is human- as well as machine readable. You should only log to a different format if you have very good reasons for it. The easiest way to log to JSON files is with AppenderJson^[AppenderJson is just an AppenderFile with LayotJson as default Layout and a few extra features] ```{r} # install.packages("jsonlite") tf <- tempfile() lg <- get_logger("test") lg$set_appenders(list(json = AppenderJson$new(file = tf))) lg$set_propagate(FALSE) lg$info("JSON naturally ", field = "custom") lg$info("supports custom", numbers = 1:3) lg$info("log fields", use = "JSON") ``` JSON is easy to parse and analyse with R. lgr provides the function `read_json_lines()` that can be used to read JSON log files, but you can also use AppenderJson's `$data` binding for an even more convenient method to read the logfile. ```{r eval = FALSE} lg$appenders$json$data # same as read_json_lines(tf) ``` ```{r echo = FALSE} lg$appenders$json$data ``` JSON is also human readable, though this vignette does not transport that fact very well because of the lack of horizontal space. ```{r eval = FALSE} lg$appenders$json$show() # same as cat(readLines(tf), sep = "\n") ``` ```{r echo = FALSE} lg$appenders$json$show() ``` ```{r} # cleanup lg$config(NULL) unlink(tf) ``` ## Logging to rotating files lgr can also log to rotating files. The following example logs to a file that is reset and backed-up once it reaches a size of 10kb. Only the last 5 backups of the logfile are kept. ```{r} # install.packages("rotor") tf <- tempfile(fileext = ".log") lg <- get_logger("test")$ set_propagate(FALSE)$ set_appenders(list(rotating = AppenderFileRotating$new( file = tf, size = "10 kb", max_backups = 5)) ) for (i in 1:100) lg$info(paste(LETTERS, sep = "-")) # display info on the backups of tf lg$appenders$rotating$backups # manually delete all backups invisible(lg$appenders$rotating$prune(0)) lg$appenders$rotating$backups #cleanup unlink(tf) ``` ## Logger hierarchies {#examples-hierarchy} The most common use cases for creating a new Logger rather than just using the root Logger is if you create a Package that should contain logging. This way you can have separate Appenders (e.g logfiles) and thresholds for each package. ```{r} # The logger name should be the same as the package name tf <- tempfile() lg <- get_logger("mypackage") lg$add_appender(AppenderFile$new(tf)) ``` The `print()` method for Loggers gives a nice overview of the newly created Logger: ```{r} print(lg) ``` This tells us that `lg` logs all events of at least level `info`. It does have a single (unnamed) Appender that logs to a temporary file, and dispatches all LogEvents it creates to the Appenders of the root Logger (ignoring the threshold and filters of the root Logger, but not of its Appenders). We can use `lg$fatal()`, `lg$info()`, etc.. to log messages with this Logger: ```{r} lg$info("A test message for lg") ``` If we do not want `lg` to dispatch to the root Logger, we can set `propagate` to `FALSE`. ```{r} lg$set_propagate(FALSE) ``` When we take a look at the Logger again, we now see that it does not inherit any Appenders anymore ```{r} print(lg) ``` Consequently, `lg` no longer outputs log messages to he console ```{r} lg$info("Nothing to see here") ``` ```{r} # cleanup lg$config(NULL) unlink(tf) ``` ## Buffered logging The main purpose of AppenderBuffer is to retain LogEvents in memory and write them to destinations at a later point in time, e.g. when the Buffer is full and needs to be flushed. For example, if you log to a remote database you can postpone this costly operation until after your analysis is finished. By setting a [filter](#thresholds) as a custom `$should_flush()` method for an AppenderBuffer, you can define more complex conditions to trigger flushing. For example, the will output the last 5 LogEvents that happened before an `error` occurred. ```{r} lg <- get_logger("buffer") lg$ set_threshold(NA)$ set_propagate(FALSE)$ set_appenders( AppenderBuffer$new( threshold = NA, buffer_size = 5, # can hold 5 events, the 6th will trigger flushing flush_on_exit = FALSE, flush_on_rotate = FALSE, flush_threshold = "error", appenders = AppenderConsole$new(threshold = NA) )) # The for loop below stores 8 log events in the Buffer for (nm in month.name[1:8]) lg$debug("%s", nm) # An event of level 'error' or 'fatal' triggers flushing of the buffer lg$error("But the days grow short when you reach September") ``` ## Logging to databases Logging to databases is simple, though a few aspects can be tricky to configure based on the backend used. For performance reasons database inserts are buffered by default. This works exactly identical as described above for AppenderBuffer. If you want to write each LogEvent directly to the database, just set the buffer size to `0`. As of lgr 0.4.0, database appenders are part of the **lgrExtra** package that has to be installed separately. **Database logging is still somewhat experimental**. ```{r, eval = FALSE} # install.packages("RSQLite") # install.packages("lgrExtra") lg <- get_logger("db_logger") lg$ set_propagate(FALSE)$ add_appender( name = "db", lgrExtra::AppenderDbi$new( conn = DBI::dbConnect(RSQLite::SQLite()), table = "log", buffer_size = 2L ) ) lg$info("Logging to databases uses a buffer") lg$info("As the buffer size is 2, no insert took place till now") lg$appenders$db$show() lg$info("Now as the buffer is rotated, all events are output to the db") lg$appenders$db$show() ``` ## Adding default extra fields to messages By abusing Filters, lgr can modify LogEvents as they are processed. One example for when this is useful is assigning a grouping identifier to a series of log calls. ```{r} # setup an example function clean <- function() lgr$info("cleaning data") process <- function() lgr$info("processing data") output <- function() lgr$info("outputing data") analyze <- function(){ clean() process() output() } ``` `with_log_value()` provides a convenient wrapper to inject values into log calls. ```{r eval = FALSE} with_log_value( list(dataset_id = "dataset1"), analyze() ) ``` An alternative way to achieve the same is to use one of the preconfigured Filters that come with lgr. This approach is more more comfortable for use within functions. ```{r eval = FALSE} analyze <- function(id = "dataset1"){ lgr$add_filter(FilterInject$new(dataset_id = id), name = "inject") on.exit(lgr$remove_filter("inject")) clean() process() output() } analyze() ``` The result is the same in both cases: ```{r, echo = FALSE} with_log_value( list(dataset_id = "dataset1"), analyze() ) ``` You can use `with_log_level()` and `FilterForceLevel` in a similar fashion to modify the log level of events conveniently. ## Temporarily disable logging Temporary disabling logging for portions of code is straight forward and easy with lgr: ```{r} without_logging({ lgr$warn("Oh Yeah?") lgr$fatal("Oh No") }) ``` ## Adding a custom logger to a package If you are a package author, it is good practice to define a separate logger for your package. This gives users the ability to easily enable/disable logging on a per-package basis. Loggers must be initialized in the packages .onLoad hook. You can do this by adding the following code to any `.R` file inside the `R/` directory of your package: ```{r} # mypackage/R/mypackage-package.R .onLoad <- function(...){ assign( "lg", # the recommended name for a logger object lgr::get_logger(name = "mypackage"), # should be the same as the package name envir = parent.env(environment()) ) } ``` You can also just use `lgr::use_logger()` to generate the appropriate code for your package automatically. After you set this up you can use `lg$fatal()`, `lg$info()`, etc... inside your package. You do not have to define any appenders, since all log events will get redirected to the root Logger (see [Inheritance](#hierarchy)). ## Creating a Layout that mimics NodeJS Winston logstash see https://github.com/s-fleck/lgr/issues/29 ## Adding the source file / R-script to the log event Getting the source file is sadly non-trivial in R, otherwise it would have been included in the core logging functions. Luckily, this is easy to add with filters and the awesome [this.path](https://CRAN.R-project.org/package=this.path) package. This solution works in most scenarios, but not in all (for example, building this vignette). ```{r, eval = FALSE} # install.packages("this.path") lg <- get_logger("srcfile") lg$add_filter(function(event){ tryCatch({ event$srcfile <- this.path::this.path() }, error = function(e) NULL) TRUE }) ``` # References [Python Logging](https://docs.python.org/3/library/logging.html) [Eric Stenbock: The True Story of A Vampire](https://gutenberg.net.au/ebooks06/0606601h.html) lgr/inst/benchmarks/0000755000176200001440000000000015137115053014131 5ustar liggesuserslgr/inst/benchmarks/benchmarks.Rmd0000644000176200001440000000652014131760166016721 0ustar liggesusers--- title: "benchmarks" author: "Stefan Fleck" date: "`r Sys.Date()`" output: html_document: toc: true number_sections: true editor_options: chunk_output_type: console --- ```{r setup, include = FALSE} knitr::opts_chunk$set( collapse = TRUE, comment = "#>" ) library(devtools) library(bench) library(dplyr) library(lgr) ``` ```{r} ITS <- seq_len(1e2) ``` A simple comparison of the perfromance of various Appenders # Setup loggers ```{r, results="hide"} app <- list( disabled = AppenderFile$new(tempfile()), plain = AppenderFile$new(tempfile()), rotating = AppenderFileRotating$new(tempfile()), rotating_date = AppenderFileRotatingDate$new(tempfile(), cache_backups = FALSE), rotating_date_cached = AppenderFileRotatingDate$new(tempfile(), cache_backups = TRUE), json = AppenderJson$new(tempfile()) ) # configure AppenderRotatingDate so that it is forced to # check the date stamp on each log (the wors-case scenario) td <- list() td$rotating <- file.path(tempdir(), "rotating") td$rotating_date <- file.path(tempdir(), "rotating_date") td$rotating_date_cached <- file.path(tempdir(), "rotating_date_cached") lapply(td, dir.create) # set backup dirs so that they don't conflict app$rotating$ set_backup_dir(td$rotating) app$rotating_date$ set_size(-1)$ set_age("1 year")$ set_backup_dir(td$rotating_date) app$rotating_date_cached$ set_size(-1)$ set_age("1 year")$ set_backup_dir(td$rotating_date_cached) app$rotating$rotate() app$rotating_date$rotate() app$rotating_date$rotate() app$rotating_date_cached$rotate() app$rotating_date_cached$rotate() stopifnot(isTRUE(app$rotating_date_cached$.__enclos_env__$private$bq$cache_backups)) stopifnot(isFALSE(app$rotating_date$.__enclos_env__$private$bq$cache_backups)) stopifnot(nrow(app$rotating$backups) == 0) stopifnot(nrow(app$rotating_date$backups) == 1) stopifnot(nrow(app$rotating_date_cached$backups) == 1) loggers = list( disabled = get_logger("disabled")$set_propagate(FALSE)$add_appender(app$disabled)$set_threshold(0), plain = get_logger("plain")$set_propagate(FALSE)$add_appender(app$plain), rotating = get_logger("rotating")$set_propagate(FALSE)$add_appender(app$rotating), rotating_date = get_logger("rotating_date")$set_propagate(FALSE)$add_appender(app$rotating_date), rotating_date_cached = get_logger("rotating_date_cached")$set_propagate(FALSE)$add_appender(app$rotating_date_cached), json = get_logger("json")$set_propagate(FALSE)$add_appender(app$json) ) loggers$rotating$info("test") loggers$rotating$info("test") loggers$rotating_date$info("test") ``` ```{r eval = FALSE, echo = FALSE} profvis::profvis({ for (i in 1:10) loggers$rotating_date$fatal("test") }) profvis::profvis({ for (i in 1:10) loggers$rotating_date_cached$fatal("test") }) ``` ```{r} exprs <- lapply( loggers, function(.x) bquote(for (i in ITS) .(.x)$fatal("a test message")) ) ``` ```{r} res <- mark(exprs = exprs, iterations = 100) res %>% dplyr::transmute( expression = format(expression), median = format(median), mem_alloc = format(mem_alloc) ) %>% knitr::kable() ``` ```{r} plot(res) ``` ```{r} lapply(app, function(a) unlink(a$file)) lapply(td, function(d) unlink(d, recursive = TRUE)) ``` lgr/inst/WORDLIST0000644000176200001440000000246015037365445013223 0ustar liggesuserstestthat subclasses Stenbock SMTP sendmailR RPushbullet RPostgreSQL RMariaDB RJDBC pushbullet Postgres plaintext PID monospace LoggerGlue LogEvents LogEvent lgr's LayoutGlue LayoutFormat LayoutDbi LayotJson labelled jsonlite JSON gmailr Filterables Filterable Filterable's EventFilter AppenderSendmail AppenderTable Appenders AppenderRjdbc AppenderPushbullet AppenderMemory AppenderJson AppenderFile AppenderDbi AppenderDt AppenderConsole AppenderBuffer Appender's Appender Vectorized whoami Youtube RSQLite Unlabel rprojroot roxygen Quickstart preconfigured pre logfile performant odbc organised onLoad Lifecycle Inkscape coloured LogEvents Codecov Instantiation dplyr behaviour Acknowledgement api appender AppenderJson's analyse instantiation io ANSI desc JDBC yaml YAML RPostgres RMySQL config csv finalizers github appenders logfiles LoggerRoot magrittr unconfigured AppenderFileRotating LayoutJson kb cli fancymodel AppenderSyslog atheriel rsyslog syslog AppenderFileRotatingDate AppenderFileRotatingTime cloneable vectorized POSIXct programmatically ORCID NodeJS logstash lgrExtra jsonl FilterInject Hotfix FilterForceLevel cron ElasticSearch json jsonlines colours hardcoded thx Dynatrace unparsed lgr/README.md0000644000176200001440000002173115035130575012325 0ustar liggesusers # lgr [![CRAN status](https://www.r-pkg.org/badges/version/lgr)](https://cran.r-project.org/package=lgr) [![Lifecycle: maturing](https://img.shields.io/badge/lifecycle-stable-green.svg)](https://lifecycle.r-lib.org/articles/stages.html) lgr is a logging package for R built on the back of [R6](https://github.com/r-lib/R6) classes. It is designed to be flexible, performant and extensible. The package [vignette](https://s-fleck.github.io/lgr/articles/lgr.html) contains a comprehensive description of the features of lgr (some of them unique among R logging packages) along with many code examples. Users that have not worked with R6 classes before, will find configuring Loggers a bit strange and verbose, but care was taken to keep the syntax for common logging tasks and interactive usage simple and concise. User that have experience with [shiny](https://github.com/rstudio/shiny), [plumber](https://github.com/rstudio/plumber), [python logging](https://docs.python.org/3/library/logging.html) or [Apache Log4j](https://logging.apache.org/log4j/2.x/) will feel at home. User that are proficient with R6 classes will also find it easy to extend and customize lgr, for example with their own appenders Loggers or Appenders. ## Features - *Hierarchical loggers* like in log4j and python logging. This is useful if you want to be able to configure logging on a per-package basis. - An *arbitrary number of appenders* for each logger. A single logger can write to the console, a logfile, a database, etc… . - Support for structured logging. As opposed to many other logging packages for R a log event is not just a message with a timestamp, but an object that can contain arbitrary data fields. This is useful for producing machine readable logs. - *Vectorized* logging (so `lgr$fatal(capture.output(iris))` works) - Lightning fast *in-memory logs* for interactive use. - Appenders that write logs to a wide range of destinations: - databases (buffered or directly) - email or pushbullet - plaintext files (with a powerful formatting syntax) - JSON files with arbitrary data fields - Rotating files that are reset and backed-up after they reach a certain file size or age - memory buffers - (colored) console output - Optional support to use [glue](https://glue.tidyverse.org/) instead of `sprintf()` for composing log messages. ## Usage To log an *event* with with lgr we call `lgr$()`. Unnamed arguments to the logging function are interpreted by `sprintf()`. For a way to create loggers that [glue](https://glue.tidyverse.org/) instead please refer to the vignette. ``` r lgr$fatal("A critical error") #> FATAL [11:32:48.843] A critical error lgr$error("A less severe error") #> ERROR [11:32:48.864] A less severe error lgr$warn("A potentially bad situation") #> WARN [11:32:48.868] A potentially bad situation lgr$info("iris has %s rows", nrow(iris)) #> INFO [11:32:48.869] iris has 150 rows # the following log levels are hidden by default lgr$debug("A debug message") lgr$trace("A finer grained debug message") ``` A Logger can have several Appenders. For example, we can add a JSON appender to log to a file with little effort. ``` r tf <- tempfile() lgr$add_appender(AppenderFile$new(tf, layout = LayoutJson$new())) lgr$info("cars has %s rows", nrow(cars)) #> INFO [11:32:48.878] cars has 50 rows cat(readLines(tf)) #> {"level":400,"timestamp":"2025-07-13 11:32:48","logger":"root","caller":"eval","msg":"cars has 50 rows","rawMsg":"cars has %s rows"} ``` By passing a named argument to the log function, you can log not only text but arbitrary R objects. Not all appenders support structured logging perfectly, but JSON does. This way you can create logfiles that are machine as well as (somewhat) human readable. ``` r lgr$info("loading %s", "cars", rows = nrow(cars), cols = ncol(cars), vector = c(1, 2, 3)) #> INFO [11:32:48.893] loading cars {rows: `50`, cols: `2`, vector: (1, 2, 3)} cat(readLines(tf), sep = "\n") #> {"level":400,"timestamp":"2025-07-13 11:32:48","logger":"root","caller":"eval","msg":"cars has 50 rows","rawMsg":"cars has %s rows"} #> {"level":400,"timestamp":"2025-07-13 11:32:48","logger":"root","caller":"eval","msg":"loading cars","rawMsg":"loading %s","rows":50,"cols":2,"vector":[1,2,3]} ``` For more examples please see the package [vignette](https://s-fleck.github.io/lgr/articles/lgr.html) and [documentation](https://s-fleck.github.io/lgr/) ## See lgr in action lgr is used to govern console output in my shiny based csv editor [shed](https://github.com/s-fleck/shed) ``` r # install.packages("remotes") remotes::install_github("s-fleck/shed") library(shed) # log only output from the "shed" logger to a file logfile <- tempfile() lgr::get_logger("shed")$add_appender(AppenderFile$new(logfile)) lgr::threshold("all") # edit away and watch the rstudio console! lgr$info("starting shed") shed(iris) lgr$info("this will not end up in the log file") readLines(logfile) # cleanup file.remove(logfile) ``` ## Development status lgr is stable and safe for use. I’ve been using it in production code for several years myself. There has been very little recent development because it’s pretty stable and contains (nearly) all planned features. Notable points that are still planned (without specific ETA): - Support for config files is heavily experimental and incomplete. This is an important basic feature, but I have not yet found a great way to support this in a generic way. For now, I recommend you come up with your own solution if you need to lgr to work in a production environment that relies on config files. - Improve the documentation. The documentation should be mostly complete, but is not perfect. If there’s something missing or something you don’t understand, please ask (for example via a github issue). ## Dependencies [R6](https://github.com/r-lib/R6): The R6 class system provides the framework on which lgr is built and the **only Package lgr will ever depend on**. If you are a **package developer** and want to add logging to your package, this is the only transitive dependency you have to worry about, as configuring of the loggers should be left to the user of your package. ### Optional dependencies lgr comes with a long list of optional dependencies that make a wide range of appenders possible. You only need the dependencies for the Appenders you actually want to use. Care was taken to choose packages that are slim, stable, have minimal dependencies, and are well maintained : Extra appenders (in the main package): - [jsonlite](https://github.com/jeroen/jsonlite) for JSON logging via `LayoutJson`. JSON is a popular plaintext based file format that is easy to read for humans and machines alike. - [rotor](https://github.com/s-fleck/rotor) for log rotation via AppenderFileRotating and co. - [data.table](https://github.com/Rdatatable/) for fast in-memory logging with `AppenderDt`, and also by all database / DBI Appenders. - [glue](https://glue.tidyverse.org/) for a more flexible formatting syntax via LoggerGlue and LayoutGlue. Extra appenders via lgrExtra: - For support for Elasticsearch, Dynatrace, Push- and Email notifications, etc… as well as the relevant dependencies please refer to the documentation of [lgrExtra](https://github.com/s-fleck/lgrExtra) Other extra features: - [yaml](https://CRAN.R-project.org/package=yaml) for configuring loggers via YAML files (experimental) - [crayon](https://github.com/r-lib/crayon) for colored console output. - [whoami](https://github.com/r-lib/whoami/blob/master/DESCRIPTION) for guessing the user name from various sources. You can also set the user name manually if you want to use it for logging. - [desc](https://CRAN.R-project.org/package=desc) for the package development convenience function `use_logger()` - [cli](https://CRAN.R-project.org/package=cli) for printing the tree structure of registered loggers with `logger_tree()` Other `Suggests` ([future](https://CRAN.R-project.org/package=future), [future.apply](https://CRAN.R-project.org/package=future.apply)) do not provide extra functionality but had to be included for some of the automated unit tests run by lgr. ## Installation You can install lgr from CRAN ``` r install.packages("lgr") ``` Or you can install the current development version directly from github ``` r #install.packages("remotes") remotes::install_github("s-fleck/lgr") ``` ## Outlook The long term goal is to support (nearly) all features of the python logging module. If you have experience with python logging or Log4j and are missing features/appenders that you’d like to see, please feel free to post a feature request on the issue tracker. ## Acknowledgement - [diagrams.net](https://app.diagrams.net/) for the flow chart in the vignette lgr/build/0000755000176200001440000000000015137115052012135 5ustar liggesuserslgr/build/vignette.rds0000644000176200001440000000027515137115052014500 0ustar liggesusers‹‹àb```b`aed`b2™… 1# 'fÏI/Ò ÊMAf £ q€Tf”äæ ‰³‚MÚ$ „H ²¢¼ÄÜÔbt»]R RóR@Âÿ°ëgüîïÔÊòü"˜5lP5,n™9©0{C2Kàæ7(“1Ý 棸Ÿ³(¿\æ^P¸6‰ÿ@€îÑäœÄbtr¥$–$ê¥õƒÜ $[™lgr/man/0000755000176200001440000000000015035130575011615 5ustar liggesuserslgr/man/AppenderMemory.Rd0000644000176200001440000002413114643514316015037 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/Appender.R \name{AppenderMemory} \alias{AppenderMemory} \title{Abstract class for logging to memory buffers} \description{ \strong{NOTE}: This is an \emph{abstract class}. Abstract classes cannot be instantiated directly, but are exported for package developers that want to extend lgr - for example by creating their own \link[=Appender]{Appenders} or \link[=Layout]{Layouts}. Please refer to the \emph{see also} section for actual implementations of this class. AppenderMemory is extended by Appenders that retain an in-memory event buffer, such as \link{AppenderBuffer} and \code{AppenderPushbullet} from the \href{https://github.com/s-fleck/lgrExtra}{lgrExtra} package. } \seealso{ \link{LayoutFormat} Other abstract classes: \code{\link{Appender}}, \code{\link{AppenderTable}}, \code{\link{Filterable}} } \concept{abstract classes} \section{Super classes}{ \code{\link[lgr:Filterable]{lgr::Filterable}} -> \code{\link[lgr:Appender]{lgr::Appender}} -> \code{AppenderMemory} } \section{Active bindings}{ \if{html}{\out{
}} \describe{ \item{\code{flush_on_exit}}{A \code{logical} scalar. Should the buffer be flushed if the Appender is destroyed (e.g. because the \R session is terminated)?} \item{\code{flush_on_rotate}}{A \code{logical} scalar. Should the buffer be flushed when it is rotated because \verb{$buffer_size} is exceeded?} \item{\code{should_flush}}{A \code{function} with exactly one arguments: \code{event}. \verb{$append()} calls this function internally on the current \link{LogEvent} and flushes the buffer if it evaluates to \code{TRUE}.} \item{\code{buffer_size}}{\code{integer} scalar \verb{>= 0}. Maximum number of \link{LogEvents} to buffer.} \item{\code{flush_threshold}}{A \code{numeric} or \code{character} threshold. \link{LogEvents} with a \link{log_level} equal to or lower than this threshold trigger flushing the buffer.} \item{\code{buffer_events}}{A \code{list} of \link{LogEvents}. Contents of the buffer.} \item{\code{buffer_events}}{A \code{data.frame}. Contents of the buffer converted to a \code{data.frame}.} \item{\code{buffer_events}}{A \code{data.frame}. Contents of the buffer converted to a \code{data.table}.} } \if{html}{\out{
}} } \section{Methods}{ \subsection{Public methods}{ \itemize{ \item \href{#method-AppenderMemory-new}{\code{AppenderMemory$new()}} \item \href{#method-AppenderMemory-append}{\code{AppenderMemory$append()}} \item \href{#method-AppenderMemory-flush}{\code{AppenderMemory$flush()}} \item \href{#method-AppenderMemory-clear}{\code{AppenderMemory$clear()}} \item \href{#method-AppenderMemory-set_buffer_size}{\code{AppenderMemory$set_buffer_size()}} \item \href{#method-AppenderMemory-set_should_flush}{\code{AppenderMemory$set_should_flush()}} \item \href{#method-AppenderMemory-set_flush_on_exit}{\code{AppenderMemory$set_flush_on_exit()}} \item \href{#method-AppenderMemory-set_flush_on_rotate}{\code{AppenderMemory$set_flush_on_rotate()}} \item \href{#method-AppenderMemory-set_flush_threshold}{\code{AppenderMemory$set_flush_threshold()}} \item \href{#method-AppenderMemory-show}{\code{AppenderMemory$show()}} \item \href{#method-AppenderMemory-format}{\code{AppenderMemory$format()}} } } \if{html}{\out{
Inherited methods
}} \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-AppenderMemory-new}{}}} \subsection{Method \code{new()}}{ \subsection{Usage}{ \if{html}{\out{
}}\preformatted{AppenderMemory$new(...)}\if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-AppenderMemory-append}{}}} \subsection{Method \code{append()}}{ \subsection{Usage}{ \if{html}{\out{
}}\preformatted{AppenderMemory$append(event)}\if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-AppenderMemory-flush}{}}} \subsection{Method \code{flush()}}{ Sends the buffer's contents to all attached Appenders and then clears the Buffer \subsection{Usage}{ \if{html}{\out{
}}\preformatted{AppenderMemory$flush()}\if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-AppenderMemory-clear}{}}} \subsection{Method \code{clear()}}{ Clears the buffer, discarding all buffered Events \subsection{Usage}{ \if{html}{\out{
}}\preformatted{AppenderMemory$clear()}\if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-AppenderMemory-set_buffer_size}{}}} \subsection{Method \code{set_buffer_size()}}{ Set the maximum size of the buffer \subsection{Usage}{ \if{html}{\out{
}}\preformatted{AppenderMemory$set_buffer_size(x)}\if{html}{\out{
}} } \subsection{Arguments}{ \if{html}{\out{
}} \describe{ \item{\code{x}}{an \code{integer} scalar \verb{>= 0}. Number of \link{LogEvents} to buffer.} } \if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-AppenderMemory-set_should_flush}{}}} \subsection{Method \code{set_should_flush()}}{ Set function that can trigger flushing the buffer \subsection{Usage}{ \if{html}{\out{
}}\preformatted{AppenderMemory$set_should_flush(x)}\if{html}{\out{
}} } \subsection{Arguments}{ \if{html}{\out{
}} \describe{ \item{\code{x}}{A \code{function} with the single argument \code{event}. Setting \code{x} to \code{NULL} is a shortcut for \code{function(event) FALSE}. See active bindings.} } \if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-AppenderMemory-set_flush_on_exit}{}}} \subsection{Method \code{set_flush_on_exit()}}{ Should the buffer be flushed when the Appender is destroyed? \subsection{Usage}{ \if{html}{\out{
}}\preformatted{AppenderMemory$set_flush_on_exit(x)}\if{html}{\out{
}} } \subsection{Arguments}{ \if{html}{\out{
}} \describe{ \item{\code{x}}{A \code{logical} scalar. See active bindings.} } \if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-AppenderMemory-set_flush_on_rotate}{}}} \subsection{Method \code{set_flush_on_rotate()}}{ Should the buffer be flushed if \code{buffer_size} is exceeded? \subsection{Usage}{ \if{html}{\out{
}}\preformatted{AppenderMemory$set_flush_on_rotate(x)}\if{html}{\out{
}} } \subsection{Arguments}{ \if{html}{\out{
}} \describe{ \item{\code{x}}{A \code{logical} scalar. See active bindings.} } \if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-AppenderMemory-set_flush_threshold}{}}} \subsection{Method \code{set_flush_threshold()}}{ Set threshold that triggers flushing \subsection{Usage}{ \if{html}{\out{
}}\preformatted{AppenderMemory$set_flush_threshold(level)}\if{html}{\out{
}} } \subsection{Arguments}{ \if{html}{\out{
}} \describe{ \item{\code{level}}{A \code{numeric} or \code{character} \link[=log_level]{threshold}. See active bindings.} } \if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-AppenderMemory-show}{}}} \subsection{Method \code{show()}}{ Display the contents of the log table. Relies on the \verb{$format_event} method of the \link{Layout} attached to this Appender. \subsection{Usage}{ \if{html}{\out{
}}\preformatted{AppenderMemory$show(threshold = NA_integer_, n = 20L)}\if{html}{\out{
}} } \subsection{Arguments}{ \if{html}{\out{
}} \describe{ \item{\code{threshold}}{\code{character} or \code{integer} scalar. The minimum log level that should be displayed.} \item{\code{n}}{\code{integer} scalar. Show only the last \code{n} log entries that match \code{threshold}.} } \if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-AppenderMemory-format}{}}} \subsection{Method \code{format()}}{ \subsection{Usage}{ \if{html}{\out{
}}\preformatted{AppenderMemory$format(color = FALSE, ...)}\if{html}{\out{
}} } } } lgr/man/as_LogEvent.Rd0000644000176200001440000000151214232437611014310 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/LogEvent.R \name{as_LogEvent} \alias{as_LogEvent} \alias{as_LogEvent.list} \alias{as_LogEvent.data.frame} \title{Coerce objects to LogEvent} \usage{ as_LogEvent(x, ...) \method{as_LogEvent}{list}(x, ...) \method{as_LogEvent}{data.frame}(x, ...) } \arguments{ \item{x}{any supported \R object} \item{...}{currently ignored} } \value{ a \link{LogEvent} } \description{ Smartly coerce \R objects that look like LogEvents to LogEvents. Mainly useful for developing Appenders. } \details{ \strong{Note}: \code{as_LogEvent.data.frame()} only supports single-row \code{data.frames} } \seealso{ Other docs relevant for extending lgr: \code{\link{LogEvent}}, \code{\link{event_list}()}, \code{\link{standardize_threshold}()} } \concept{docs relevant for extending lgr} lgr/man/as.data.frame.LogEvent.Rd0000644000176200001440000000527314131760166016242 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/LogEvent.R \name{as.data.frame.LogEvent} \alias{as.data.frame.LogEvent} \alias{as.data.table.LogEvent} \alias{as_tibble.LogEvent} \title{Coerce LogEvents to Data Frames} \usage{ \method{as.data.frame}{LogEvent}( x, row.names = NULL, optional = FALSE, stringsAsFactors = FALSE, ..., box_if = function(.) !(is.atomic(.) && identical(length(.), 1L)), cols_expand = NULL ) as.data.table.LogEvent( x, ..., box_if = function(.) !(is.atomic(.) && identical(length(.), 1L)), cols_expand = "msg" ) as_tibble.LogEvent( x, ..., box_if = function(.) !(is.atomic(.) && identical(length(.), 1L)), cols_expand = "msg" ) } \arguments{ \item{x}{any \R object.} \item{row.names}{\code{NULL} or a character vector giving the row names for the data frame. Missing values are not allowed.} \item{optional}{currently ignored and only included for compatibility.} \item{stringsAsFactors}{\code{logical} scalar: should \code{character} vectors be converted to factors? Defaults to \code{FALSE} (as opposed to \code{\link[base:as.data.frame]{base::as.data.frame()}}) and is only included for compatibility.} \item{...}{passed on to \code{data.frame()}} \item{box_if}{a \code{function} that returns \code{TRUE} or \code{FALSE} to determine which values are to be boxed (i.e. placed as single elements in a list column). See example} \item{cols_expand}{\code{character} vector. Columns to \emph{not} box (even if \code{box_if()} returns \code{TRUE}). Vectors in these columns will result in multiple rows in the result (rather than a single list-column row). This defaults to \code{"msg"} for vectorized logging over the log message.} } \description{ Coerce LogEvents to \code{data.frames}, \code{\link[data.table:data.table]{data.tables}}, or \code{\link[tibble:tibble]{tibbles}}. } \examples{ lg <- get_logger("test") lg$info("lorem ipsum") as.data.frame(lg$last_event) lg$info("LogEvents can store any custom log values", df = iris) as.data.frame(lg$last_event) head(as.data.frame(lg$last_event)$df[[1]]) # how boxing works # by default non-scalars are boxed lg$info("letters", letters = letters) as.data.frame(lg$last_event) # this behaviour can be modified by supplying a custom boxing function as.data.frame(lg$last_event, box_if = function(.) FALSE) as.data.frame(lg$last_event, cols_expand = "letters") # The `msg` argument of a log event is always vectorized lg$info(c("a vectorized", "log message")) as.data.frame(lg$last_event) lg$config(NULL) } \seealso{ \link[data.table:data.table]{data.table::data.table}, \link[tibble:tibble]{tibble::tibble} } lgr/man/AppenderFileRotatingTime.Rd0000644000176200001440000002372415137111310016766 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/Appender.R \name{AppenderFileRotatingTime} \alias{AppenderFileRotatingTime} \title{Log to a time-stamped rotating file} \description{ Log to a time-stamped rotating file Log to a time-stamped rotating file } \details{ An extension of \link{AppenderFile} that rotates logfiles based on certain conditions. Please refer to the documentation of \code{\link[rotor:rotate]{rotor::rotate_time()}} for the meanings of the extra arguments } \seealso{ \link{AppenderFileRotatingDate}, \link{AppenderFileRotating}, \code{\link[rotor:rotate]{rotor::rotate()}} Other Appenders: \code{\link{Appender}}, \code{\link{AppenderBuffer}}, \code{\link{AppenderConsole}}, \code{\link{AppenderFile}}, \code{\link{AppenderFileRotating}}, \code{\link{AppenderFileRotatingDate}}, \code{\link{AppenderTable}} } \concept{Appenders} \section{Super classes}{ \code{\link[lgr:Filterable]{lgr::Filterable}} -> \code{\link[lgr:Appender]{lgr::Appender}} -> \code{\link[lgr:AppenderFile]{lgr::AppenderFile}} -> \code{\link[lgr:AppenderFileRotating]{lgr::AppenderFileRotating}} -> \code{AppenderFileRotatingTime} } \section{Active bindings}{ \if{html}{\out{
}} \describe{ \item{\code{cache_backups}}{\code{TRUE} or \code{FALSE}. If \code{TRUE} (the default) the list of backups is cached, if \code{FALSE} it is read from disk every time this appender triggers. Caching brings a significant speedup for checking whether to rotate or not based on the \code{age} of the last backup, but is only safe if there are no other programs/functions (except this appender) interacting with the backups.} } \if{html}{\out{
}} } \section{Methods}{ \subsection{Public methods}{ \itemize{ \item \href{#method-AppenderFileRotatingTime-new}{\code{AppenderFileRotatingTime$new()}} \item \href{#method-AppenderFileRotatingTime-rotate}{\code{AppenderFileRotatingTime$rotate()}} \item \href{#method-AppenderFileRotatingTime-set_age}{\code{AppenderFileRotatingTime$set_age()}} \item \href{#method-AppenderFileRotatingTime-set_fmt}{\code{AppenderFileRotatingTime$set_fmt()}} \item \href{#method-AppenderFileRotatingTime-set_overwrite}{\code{AppenderFileRotatingTime$set_overwrite()}} \item \href{#method-AppenderFileRotatingTime-set_cache_backups}{\code{AppenderFileRotatingTime$set_cache_backups()}} \item \href{#method-AppenderFileRotatingTime-format}{\code{AppenderFileRotatingTime$format()}} \item \href{#method-AppenderFileRotatingTime-clone}{\code{AppenderFileRotatingTime$clone()}} } } \if{html}{\out{
Inherited methods
}} \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-AppenderFileRotatingTime-new}{}}} \subsection{Method \code{new()}}{ \subsection{Usage}{ \if{html}{\out{
}}\preformatted{AppenderFileRotatingTime$new( file, threshold = NA_integer_, layout = LayoutFormat$new(), filters = NULL, age = Inf, size = -1, max_backups = Inf, compression = FALSE, backup_dir = dirname(file), fmt = "\%Y-\%m-\%d--\%H-\%M-\%S", overwrite = FALSE, cache_backups = TRUE, create_file = NULL )}\if{html}{\out{
}} } \subsection{Arguments}{ \if{html}{\out{
}} \describe{ \item{\code{size, age, max_backups, compression, backup_dir, fmt, overwrite, cache_backups}}{see \code{\link[rotor:rotate]{rotor::rotate_time()}} for the meaning of these arguments. Note that \code{fmt} corresponds to \code{format} and \code{backup_dir} to \code{dir}.} } \if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-AppenderFileRotatingTime-rotate}{}}} \subsection{Method \code{rotate()}}{ \subsection{Usage}{ \if{html}{\out{
}}\preformatted{AppenderFileRotatingTime$rotate(force = FALSE, now = Sys.time())}\if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-AppenderFileRotatingTime-set_age}{}}} \subsection{Method \code{set_age()}}{ \subsection{Usage}{ \if{html}{\out{
}}\preformatted{AppenderFileRotatingTime$set_age(x)}\if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-AppenderFileRotatingTime-set_fmt}{}}} \subsection{Method \code{set_fmt()}}{ \subsection{Usage}{ \if{html}{\out{
}}\preformatted{AppenderFileRotatingTime$set_fmt(x)}\if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-AppenderFileRotatingTime-set_overwrite}{}}} \subsection{Method \code{set_overwrite()}}{ \subsection{Usage}{ \if{html}{\out{
}}\preformatted{AppenderFileRotatingTime$set_overwrite(x)}\if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-AppenderFileRotatingTime-set_cache_backups}{}}} \subsection{Method \code{set_cache_backups()}}{ set the \code{cache_backups} flag. \subsection{Usage}{ \if{html}{\out{
}}\preformatted{AppenderFileRotatingTime$set_cache_backups(x)}\if{html}{\out{
}} } \subsection{Arguments}{ \if{html}{\out{
}} \describe{ \item{\code{x}}{a \code{logical} scalar} } \if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-AppenderFileRotatingTime-format}{}}} \subsection{Method \code{format()}}{ \subsection{Usage}{ \if{html}{\out{
}}\preformatted{AppenderFileRotatingTime$format(color = FALSE, ...)}\if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-AppenderFileRotatingTime-clone}{}}} \subsection{Method \code{clone()}}{ The objects of this class are cloneable with this method. \subsection{Usage}{ \if{html}{\out{
}}\preformatted{AppenderFileRotatingTime$clone(deep = FALSE)}\if{html}{\out{
}} } \subsection{Arguments}{ \if{html}{\out{
}} \describe{ \item{\code{deep}}{Whether to make a deep clone.} } \if{html}{\out{
}} } } } lgr/man/lgr-package.Rd0000644000176200001440000000476414131760166014275 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/lgr-package.R \docType{package} \name{lgr-package} \alias{lgr} \alias{lgr-package} \title{A Fully Featured Logging Framework for R} \description{ For details please refer to \code{vignette("lgr", package = "lgr")}. } \section{Options}{ You can also set these options in your \code{.Rprofile} to make them permanent. Some options can also be set via environment variables (The environment variables are only used if the option is not set manually from R). \describe{ \item{\code{lgr.colors}}{a \code{list} of \code{functions} used for coloring the log levels in console output. Usually these will be functions from the package \strong{crayon}} \item{\code{lgr.log_levels}}{A named \code{integer} vector of log levels that are known to lgr for labeling, setting thresholds, etc... . Instead of modifying this option manually use \code{\link[=add_log_levels]{add_log_levels()}} and \code{\link[=remove_log_levels]{remove_log_levels()}}} \item{\code{lgr.default_threshold}}{ \code{character} or \code{integer} scalar. The minimum \link[=log_levels]{log level} that should be processed by the root logger. Defaults to \code{400} (\code{"info"}), or to the value of the environment variable \code{LGR_DEFAULT_THRESHOLD} if it is set. This option overrides the threshold specified in \code{lgr.default_config} if both are set. } \item{\code{lgr.default_config}}{ Default configuration for the root logger. Can either be a special list object, a path to a YAML file, or a character scalar containing YAML code. See \link{logger_config} for details. Defaults to the value of the environment variable \code{LGR_DEFAULT_CONFIG} if it is set. } \item{\code{lgr.suspend_logging}}{\code{TRUE} or \code{FALSE}. Suspend all logging for all loggers. Defaults to the \code{TRUE} if the environment variable \code{LGR_SUSPEND_LOGGING} is set to \code{"TRUE"}. Instead of modifying this option manually use \code{\link[=suspend_logging]{suspend_logging()}} and \code{\link[=unsuspend_logging]{unsuspend_logging()}}} \item{\code{lgr.user}}{a \code{character} scalar. The default username for \code{lgr::get_user()}. } } } \seealso{ Useful links: \itemize{ \item \url{https://s-fleck.github.io/lgr/} \item Report bugs at \url{https://github.com/s-fleck/lgr/issues/} } } \author{ \strong{Maintainer}: Stefan Fleck \email{stefan.b.fleck@gmail.com} (\href{https://orcid.org/0000-0003-3344-9851}{ORCID}) } \keyword{internal} lgr/man/Layout.Rd0000644000176200001440000000671515035407137013373 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/Layout.R \name{Layout} \alias{Layout} \alias{Layouts} \title{Abstract Class for Layouts} \description{ Abstract Class for Layouts Abstract Class for Layouts } \details{ \link{Appenders} pass \link[=LogEvent]{LogEvents} to a Layout which formats it for output. For the Layouts included in lgr that means turning the LogEvent into a \code{character} string. For each Appender exist one more more possible Layouts, but not every Layout will work with every Appender. See the package \pkg{lgrExtra} for examples for Layouts that return different data types (such as \code{data.frames}) and Appenders that can handle them. } \section{Notes for developers}{ Layouts may have an additional \verb{$read(file, threshold, n)} method that returns a \code{character} vector, and/or an \verb{$parse(file)} method that returns a \code{data.frame}. These can be used by Appenders to \verb{$show()} methods and \verb{$data} active bindings respectively (see source code of \link{AppenderFile}). } \seealso{ Other Layouts: \code{\link{LayoutFormat}}, \code{\link{LayoutGlue}}, \code{\link{LayoutJson}} } \concept{Layouts} \section{Active bindings}{ \if{html}{\out{
}} \describe{ \item{\code{excluded_fields}}{fields to exclude from the final log} } \if{html}{\out{
}} } \section{Methods}{ \subsection{Public methods}{ \itemize{ \item \href{#method-Layout-format_event}{\code{Layout$format_event()}} \item \href{#method-Layout-toString}{\code{Layout$toString()}} \item \href{#method-Layout-set_excluded_fields}{\code{Layout$set_excluded_fields()}} \item \href{#method-Layout-clone}{\code{Layout$clone()}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-Layout-format_event}{}}} \subsection{Method \code{format_event()}}{ Format a log event Function that the Layout uses to transform a \link{LogEvent} into something that an \link{Appender} can write to an output destination. \subsection{Usage}{ \if{html}{\out{
}}\preformatted{Layout$format_event(event)}\if{html}{\out{
}} } \subsection{Arguments}{ \if{html}{\out{
}} \describe{ \item{\code{event}}{a \link{LogEvent}} } \if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-Layout-toString}{}}} \subsection{Method \code{toString()}}{ \subsection{Usage}{ \if{html}{\out{
}}\preformatted{Layout$toString()}\if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-Layout-set_excluded_fields}{}}} \subsection{Method \code{set_excluded_fields()}}{ \subsection{Usage}{ \if{html}{\out{
}}\preformatted{Layout$set_excluded_fields(x)}\if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-Layout-clone}{}}} \subsection{Method \code{clone()}}{ The objects of this class are cloneable with this method. \subsection{Usage}{ \if{html}{\out{
}}\preformatted{Layout$clone(deep = FALSE)}\if{html}{\out{
}} } \subsection{Arguments}{ \if{html}{\out{
}} \describe{ \item{\code{deep}}{Whether to make a deep clone.} } \if{html}{\out{
}} } } } lgr/man/label_levels.Rd0000644000176200001440000000203314131760166014534 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/log_levels.R \name{label_levels} \alias{label_levels} \alias{unlabel_levels} \title{Label/Unlabel Log Levels} \usage{ label_levels(levels, log_levels = getOption("lgr.log_levels")) unlabel_levels(labels, log_levels = getOption("lgr.log_levels")) } \arguments{ \item{levels}{an \code{integer} vector of log levels} \item{log_levels}{named \code{integer} vector of valid log levels} \item{labels}{a \code{character} vector of log level labels. Please note that log levels are lowercase by default, even if many appenders print them in uppercase.} } \value{ a \code{character} vector for \code{label_levels()} and an integer vector for \code{unlabel_levels} } \description{ Label/Unlabel Log Levels } \examples{ x <- label_levels(c(seq(0, 600, by = 100), NA)) print(x) unlabel_levels(x) } \seealso{ \code{\link[=get_log_levels]{get_log_levels()}} Other formatting utils: \code{\link{colorize_levels}()} } \concept{formatting utils} lgr/man/colorize_levels.Rd0000644000176200001440000000231414131762445015307 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/log_levels.R \name{colorize_levels} \alias{colorize_levels} \title{Colorize Levels} \usage{ colorize_levels( x, colors = getOption("lgr.colors", NULL), transform = identity ) } \arguments{ \item{x}{\code{numeric} or \code{character} levels to be colored. Unlike in many other functions in lgr, \code{character} levels are \emph{not} case sensitive in this function and leading/trailing whitespace is ignored to make it more comfortable to use \code{colorize_levels()} inside formatting functions.} \item{colors}{A \code{list} of \code{functions} that will be used to color the log levels (likely from \link[crayon:crayon]{crayon::crayon}).} \item{transform}{a \code{function} to transform \code{x} (for example \code{toupper()})} } \value{ a \code{character} vector wit color ANSI codes } \description{ Colorize Levels } \examples{ cat(colorize_levels(c(100, 200))) cat(colorize_levels(c("trace", "warn ", "DEBUG"))) cat(colorize_levels(c("trace", "warn ", "DEBUG"), transform = function(x) strtrim(x, 1) )) } \seealso{ Other formatting utils: \code{\link{label_levels}()} } \concept{formatting utils} lgr/man/string_repr.Rd0000644000176200001440000000216115036470265014446 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/string_repr.R \name{string_repr} \alias{string_repr} \alias{string_repr.function} \alias{string_repr.data.frame} \alias{string_repr.matrix} \alias{string_repr.numeric} \alias{string_repr.default} \title{Short string representation for R objects} \usage{ string_repr(x, width = 32, ...) \method{string_repr}{`function`}(x, width = 32L, ...) \method{string_repr}{data.frame}(x, width = 32L, ...) \method{string_repr}{matrix}(x, width = 32L, ...) \method{string_repr}{numeric}(x, width = 32L, ...) \method{string_repr}{default}(x, width = 32L, ...) } \arguments{ \item{x}{Any \R object.} \item{width}{a scalar integer} \item{...}{passed on to methods} } \value{ a \code{scalar} character } \description{ This is inspired by the python function \code{repr} and produces a short string representation of any \R object that is suitable for logging and error messages. It is a generic so you can implement methods for custom S3 objects. } \examples{ string_repr(iris) string_repr(LETTERS) string_repr(LETTERS, 10) } lgr/man/suspend_logging.Rd0000644000176200001440000000240214131760166015272 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/utils-logging.R \name{suspend_logging} \alias{suspend_logging} \alias{unsuspend_logging} \alias{without_logging} \alias{with_logging} \title{Suspend All Logging} \usage{ suspend_logging() unsuspend_logging() without_logging(code) with_logging(code) } \arguments{ \item{code}{Any \R code} } \value{ \code{suspend_logging()} and \code{unsuspend_logging()} return \code{NULL} (invisibly), \code{without_logging()} and \code{with_logging()} returns whatever \code{code} returns. } \description{ Completely disable logging for all loggers. This is for example useful for automated test code. \code{suspend_logging()} globally disables all logging with lgr until \code{unsuspend_logging()} is invoked, while \code{without_logging()} and \code{with_logging()} temporarily disable/enable logging. } \examples{ lg <- get_logger("test") # temporarily disable logging lg$fatal("foo") without_logging({ lg$info("everything in this codeblock will be suppressed") lg$fatal("bar") }) # globally disable logging suspend_logging() lg$fatal("bar") with_logging(lg$fatal("foo")) # log anyways # globally enable logging again unsuspend_logging() lg$fatal("foo") } lgr/man/with_log_level.Rd0000644000176200001440000000236314131760166015114 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/utils-logging.R \name{with_log_level} \alias{with_log_level} \alias{with_log_value} \title{Inject Values into Logging Calls} \usage{ with_log_level(level, code, logger = lgr::lgr) with_log_value(values, code, logger = lgr::lgr) } \arguments{ \item{level}{\code{integer} or \code{character} scalar: the desired log level} \item{code}{Any \R code} \item{logger}{a \link{Logger} or the name of one (see \code{\link[=get_logger]{get_logger()}}). Defaults to the root logger (\code{lgr::lgr}).} \item{values}{a named \code{list} of values to be injected into the logging calls} } \value{ whatever \code{code} would return } \description{ \code{with_log_level} temporarily overrides the log level of all \link{LogEvents} created by target \link{Logger}. } \details{ These functions abuses lgr's filter mechanic to modify LogEvents in-place before they passed on the Appenders. Use with care as they can produce hard to reason about code. } \examples{ with_log_level("warn", { lgr$info("More important than it seems") lgr$fatal("Really not so bad") }) with_log_value( list(msg = "overriden msg"), { lgr$info("bar") lgr$fatal("FOO") }) } lgr/man/EventFilter.Rd0000644000176200001440000000721014402036437014332 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/Filter.R \name{EventFilter} \alias{EventFilter} \alias{Filter} \alias{.obj} \title{Event Filters} \usage{ .obj() } \description{ EventFilters specify arbitrarily complex logic for whether or not a LogEvent should be processed by a \link{Logger} or \link{Appender}. They are attached to Loggers/Appenders via their \verb{$set_filter()} or \verb{$add_filter()} methods. If any EventFilter evaluates to \code{FALSE} for a given event, that event is ignored - similarly to when it does not pass the objects' threshold. Usually you do not need to instantiate a formal \code{EventFilter} object as you can just use any \code{function} that has the single argument \code{event} instead. If you need to implement more complex filter logic - for example a filter that is dependent on a dataset - it might be desirable to subclass EventFilter, as \link[R6:R6Class]{R6::R6} objects can store data and functions together. \code{.obj()} is a special function that can only be used within the \verb{$filter()} methods of \link[=EventFilter]{EventFilters}. It returns the \link{Logger} or \link{Appender} that the EventFilter is attached to. } \section{Modifying LogEvents with EventFilters}{ Since LogEvents are R6 objects with reference semantics, EventFilters can be abused to modify events before passing them on. lgr comes with a few preset filters that use this property: \link{FilterInject} (similar to \code{\link[=with_log_level]{with_log_level()}}) and \link{FilterForceLevel} (similar to \code{\link[=with_log_value]{with_log_value()}}). \strong{NOTE:} The base class for Filters is called \code{EventFilter} so that it doesn't conflict with \code{\link[base:funprog]{base::Filter()}}. The recommended convention for Filter subclasses is to call them \code{FilterSomething} and leave out the \code{Event} prefix. } \examples{ lg <- get_logger("test") f <- function(event) { cat("via event$.logger:", event$.logger$threshold, "\n") # works for loggers only cat("via .obj(): ",.obj()$threshold, "\n") # works for loggers and appenders TRUE } lg$add_filter(f) lg$fatal("test") lg$config(NULL) } \seealso{ \code{\link[=is_filter]{is_filter()}} } \section{Methods}{ \subsection{Public methods}{ \itemize{ \item \href{#method-EventFilter-new}{\code{EventFilter$new()}} \item \href{#method-EventFilter-clone}{\code{EventFilter$clone()}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-EventFilter-new}{}}} \subsection{Method \code{new()}}{ Initialize a new EventFilter \subsection{Usage}{ \if{html}{\out{
}}\preformatted{EventFilter$new(fun = function(event) TRUE)}\if{html}{\out{
}} } \subsection{Arguments}{ \if{html}{\out{
}} \describe{ \item{\code{fun}}{a \code{function} with a single argument \code{event} that must return either \code{TRUE} or \code{FALSE}. Any non-\code{FALSE} will be interpreted as \code{TRUE} (= no filtering takes place) and a warning will be thrown.} } \if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-EventFilter-clone}{}}} \subsection{Method \code{clone()}}{ The objects of this class are cloneable with this method. \subsection{Usage}{ \if{html}{\out{
}}\preformatted{EventFilter$clone(deep = FALSE)}\if{html}{\out{
}} } \subsection{Arguments}{ \if{html}{\out{
}} \describe{ \item{\code{deep}}{Whether to make a deep clone.} } \if{html}{\out{
}} } } } lgr/man/LayoutJson.Rd0000644000176200001440000002327515037360137014225 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/LayoutJson.R \name{LayoutJson} \alias{LayoutJson} \title{Format LogEvents as JSON} \description{ A format for formatting LogEvents as \href{https://jsonlines.org/}{jsonlines} log files. This provides a nice balance between human- an machine-readability. } \examples{ # setup a dummy LogEvent event <- LogEvent$new( logger = Logger$new("dummy logger"), level = 200, timestamp = Sys.time(), caller = NA_character_, msg = "a test message", custom_field = "LayoutJson can handle arbitrary fields" ) lo <- LayoutJson$new() lo$format_event(event) lo <- LayoutJson$new( transform_event_names = toupper, excluded_fields = c("RAWMSG", "CALLER")) lo$format_event(event) lo <- LayoutJson$new( transform_event = function(e) { values <- e$values values$msg <- toupper(values$msg) values }, timestamp_fmt = "\%a \%b \%d \%H:\%M:\%S \%Y", excluded_fields = c("RAWMSG", "CALLER")) lo$format_event(event) } \seealso{ \code{\link[=read_json_lines]{read_json_lines()}}, \url{https://jsonlines.org/} Other Layouts: \code{\link{Layout}}, \code{\link{LayoutFormat}}, \code{\link{LayoutGlue}} } \concept{Layouts} \section{Super class}{ \code{\link[lgr:Layout]{lgr::Layout}} -> \code{LayoutJson} } \section{Active bindings}{ \if{html}{\out{
}} \describe{ \item{\code{toJSON_args}}{a \code{list}} \item{\code{timestamp_fmt}}{a \code{character} scalar or a \code{function} that accepts a \code{POSIXct} as its single argument} \item{\code{transform_event}}{a \code{function} that accepts a \code{LogEvent} as its single argument} \item{\code{transform_event_names}}{a named \code{character} vector or a function that accepts a \code{character} vector of field names as its single argument.} } \if{html}{\out{
}} } \section{Methods}{ \subsection{Public methods}{ \itemize{ \item \href{#method-LayoutJson-new}{\code{LayoutJson$new()}} \item \href{#method-LayoutJson-format_event}{\code{LayoutJson$format_event()}} \item \href{#method-LayoutJson-set_toJSON_args}{\code{LayoutJson$set_toJSON_args()}} \item \href{#method-LayoutJson-set_timestamp_fmt}{\code{LayoutJson$set_timestamp_fmt()}} \item \href{#method-LayoutJson-set_transform_event}{\code{LayoutJson$set_transform_event()}} \item \href{#method-LayoutJson-set_transform_event_names}{\code{LayoutJson$set_transform_event_names()}} \item \href{#method-LayoutJson-toString}{\code{LayoutJson$toString()}} \item \href{#method-LayoutJson-parse}{\code{LayoutJson$parse()}} \item \href{#method-LayoutJson-read}{\code{LayoutJson$read()}} \item \href{#method-LayoutJson-clone}{\code{LayoutJson$clone()}} } } \if{html}{\out{
Inherited methods
}} \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-LayoutJson-new}{}}} \subsection{Method \code{new()}}{ Creates a new instance of this \link[R6:R6Class]{R6} class. \subsection{Usage}{ \if{html}{\out{
}}\preformatted{LayoutJson$new( toJSON_args = list(auto_unbox = TRUE), timestamp_fmt = NULL, transform_event = function(event) event[["values"]], transform_event_names = NULL, excluded_fields = "rawMsg" )}\if{html}{\out{
}} } \subsection{Arguments}{ \if{html}{\out{
}} \describe{ \item{\code{toJSON_args}}{a list of arguments passed to \code{\link[jsonlite:fromJSON]{jsonlite::toJSON()}},} \item{\code{timestamp_fmt}}{Format to be applied to the timestamp. This is applied after \code{transform_event} but \verb{before transform_event_names} \itemize{ \item \code{NULL}: formatting of the timestamp is left to \code{\link[jsonlite:fromJSON]{jsonlite::toJSON()}}, \item a \code{character} scalar as for \code{\link[=format.POSIXct]{format.POSIXct()}}, or \item a \code{function} that returns a vector of the same length as its (\link{POSIXct}) input. The returned vector can be of any type supported by \code{\link[jsonlite:fromJSON]{jsonlite::toJSON()}}. }} \item{\code{transform_event}}{a \code{function} with a single argument that takes a \link{LogEvent} object and returns a \code{list} of values.} \item{\code{transform_event_names}}{\itemize{ \item \code{NULL}: don't process names \item a named \code{character} vector of the format \code{new_name = old_name} \item or a \code{function} with a single mandatory argument that accepts a \code{character} vector of field names. Applied after \code{transform_event}. }} \item{\code{excluded_fields}}{A \code{character} vector of field names to exclude from the final output. Applied after \code{transform_event_names}.} } \if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-LayoutJson-format_event}{}}} \subsection{Method \code{format_event()}}{ \subsection{Usage}{ \if{html}{\out{
}}\preformatted{LayoutJson$format_event(event)}\if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-LayoutJson-set_toJSON_args}{}}} \subsection{Method \code{set_toJSON_args()}}{ \subsection{Usage}{ \if{html}{\out{
}}\preformatted{LayoutJson$set_toJSON_args(x)}\if{html}{\out{
}} } \subsection{Arguments}{ \if{html}{\out{
}} \describe{ \item{\code{x}}{a \code{list}} } \if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-LayoutJson-set_timestamp_fmt}{}}} \subsection{Method \code{set_timestamp_fmt()}}{ \subsection{Usage}{ \if{html}{\out{
}}\preformatted{LayoutJson$set_timestamp_fmt(x)}\if{html}{\out{
}} } \subsection{Arguments}{ \if{html}{\out{
}} \describe{ \item{\code{x}}{a \code{character} scalar or a \code{function} that accepts a \code{POSIXct} as its single argument} } \if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-LayoutJson-set_transform_event}{}}} \subsection{Method \code{set_transform_event()}}{ \subsection{Usage}{ \if{html}{\out{
}}\preformatted{LayoutJson$set_transform_event(x)}\if{html}{\out{
}} } \subsection{Arguments}{ \if{html}{\out{
}} \describe{ \item{\code{x}}{a \code{function} that accepts a \code{LogEvent} as its single argument} } \if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-LayoutJson-set_transform_event_names}{}}} \subsection{Method \code{set_transform_event_names()}}{ \subsection{Usage}{ \if{html}{\out{
}}\preformatted{LayoutJson$set_transform_event_names(x)}\if{html}{\out{
}} } \subsection{Arguments}{ \if{html}{\out{
}} \describe{ \item{\code{x}}{a named \code{character} vector or a function that accepts a \code{character} vector of field names as its single argument.} } \if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-LayoutJson-toString}{}}} \subsection{Method \code{toString()}}{ Represent the \code{LayoutJson} class as a string \subsection{Usage}{ \if{html}{\out{
}}\preformatted{LayoutJson$toString()}\if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-LayoutJson-parse}{}}} \subsection{Method \code{parse()}}{ Read and parse file written using this Layout This can be used by the \verb{$data} active binding of an \link{Appender} \subsection{Usage}{ \if{html}{\out{
}}\preformatted{LayoutJson$parse(file)}\if{html}{\out{
}} } \subsection{Arguments}{ \if{html}{\out{
}} \describe{ \item{\code{file}}{\code{character} scalar: path to a file} } \if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-LayoutJson-read}{}}} \subsection{Method \code{read()}}{ Read a file written using this Layout (without parsing) This can be used by the \verb{$show()} method of an \link{Appender} \subsection{Usage}{ \if{html}{\out{
}}\preformatted{LayoutJson$read(file, threshold = NA_integer_, n = 20L)}\if{html}{\out{
}} } \subsection{Arguments}{ \if{html}{\out{
}} \describe{ \item{\code{file}}{\code{character} scalar: path to a file} \item{\code{threshold}}{\code{character} Minimum log level to show. Requires parsing of the log file (but will still display unparsed output)} \item{\code{n}}{\code{integer} number of lines to show} } \if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-LayoutJson-clone}{}}} \subsection{Method \code{clone()}}{ The objects of this class are cloneable with this method. \subsection{Usage}{ \if{html}{\out{
}}\preformatted{LayoutJson$clone(deep = FALSE)}\if{html}{\out{
}} } \subsection{Arguments}{ \if{html}{\out{
}} \describe{ \item{\code{deep}}{Whether to make a deep clone.} } \if{html}{\out{
}} } } } lgr/man/print.logger_tree.Rd0000644000176200001440000000124014131760166015533 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/logger_tree.R \name{print.logger_tree} \alias{print.logger_tree} \alias{format.logger_tree} \title{Print Logger Trees} \usage{ \method{print}{logger_tree}(x, color = requireNamespace("crayon", quietly = TRUE), ...) \method{format}{logger_tree}(x, color = FALSE, ...) } \arguments{ \item{x}{a \link[=logger_tree]{logger_tree}} \item{color}{\code{logical} scalar. If \code{TRUE} terminal output is colorized via the package \pkg{crayon}?} \item{...}{passed on to \code{\link[cli:tree]{cli::tree()}}} } \value{ \code{x} (invisibly) } \description{ Print Logger Trees } lgr/man/AppenderConsole.Rd0000644000176200001440000001223114643514315015166 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/Appender.R \name{AppenderConsole} \alias{AppenderConsole} \title{Log to the console} \description{ An Appender that outputs to the \R console. If you have the package \strong{crayon} installed log levels will be coloured by default (but you can modify this behaviour by passing a custom \link{Layout}). } \examples{ # create a new logger with propagate = FALSE to prevent routing to the root # logger. Please look at the section "Logger Hirarchies" in the package # vignette for more info. lg <- get_logger("test")$set_propagate(FALSE) lg$add_appender(AppenderConsole$new()) lg$add_appender(AppenderConsole$new( layout = LayoutFormat$new("[\%t] \%c(): [\%n] \%m", colors = getOption("lgr.colors")))) # Will output the message twice because we attached two console appenders lg$warn("A test message") lg$config(NULL) # reset config } \seealso{ \link{LayoutFormat} Other Appenders: \code{\link{Appender}}, \code{\link{AppenderBuffer}}, \code{\link{AppenderFile}}, \code{\link{AppenderFileRotating}}, \code{\link{AppenderFileRotatingDate}}, \code{\link{AppenderFileRotatingTime}}, \code{\link{AppenderTable}} } \concept{Appenders} \section{Super classes}{ \code{\link[lgr:Filterable]{lgr::Filterable}} -> \code{\link[lgr:Appender]{lgr::Appender}} -> \code{AppenderConsole} } \section{Methods}{ \subsection{Public methods}{ \itemize{ \item \href{#method-AppenderConsole-new}{\code{AppenderConsole$new()}} \item \href{#method-AppenderConsole-append}{\code{AppenderConsole$append()}} \item \href{#method-AppenderConsole-set_connection}{\code{AppenderConsole$set_connection()}} } } \if{html}{\out{
Inherited methods
}} \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-AppenderConsole-new}{}}} \subsection{Method \code{new()}}{ \subsection{Usage}{ \if{html}{\out{
}}\preformatted{AppenderConsole$new( threshold = NA_integer_, layout = LayoutFormat$new(fmt = "\%L [\%t] \%m \%f", timestamp_fmt = "\%H:\%M:\%OS3", colors = getOption("lgr.colors", list())), filters = NULL, connection = NULL )}\if{html}{\out{
}} } \subsection{Arguments}{ \if{html}{\out{
}} \describe{ \item{\code{connection}}{A connection or a \code{character} scalar. See the \code{file} argument of \code{\link[=cat]{cat()}} for more details. Defaults to \code{\link[=stdout]{stdout()}}, except inside \pkg{knitr} rendering processes where it defaults to \code{\link[=stderr]{stderr()}}.} } \if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-AppenderConsole-append}{}}} \subsection{Method \code{append()}}{ \subsection{Usage}{ \if{html}{\out{
}}\preformatted{AppenderConsole$append(event)}\if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-AppenderConsole-set_connection}{}}} \subsection{Method \code{set_connection()}}{ \subsection{Usage}{ \if{html}{\out{
}}\preformatted{AppenderConsole$set_connection(connection)}\if{html}{\out{
}} } \subsection{Arguments}{ \if{html}{\out{
}} \describe{ \item{\code{connection}}{A connection or a \code{character} scalar. See the \code{file} argument of \code{\link[=cat]{cat()}} for more details. Defaults to \code{\link[=stdout]{stdout()}}, except inside \pkg{knitr} rendering processes where it defaults to \code{\link[=stderr]{stderr()}}.} } \if{html}{\out{
}} } } } lgr/man/system_infos.Rd0000644000176200001440000000143714131760166014634 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/utils.R \name{get_caller} \alias{get_caller} \alias{system_infos} \alias{get_user} \title{Information About the System} \usage{ get_caller(where = -1L) get_user(fallback = "unknown user") } \arguments{ \item{where}{\code{integer} scalar (usually negative). Look up that many frames up the call stack} \item{fallback}{A fallback in case the user name could not be determined} } \value{ a \code{character} scalar. } \description{ \code{get_caller()} Tries to determine the calling functions based on \code{where}. } \examples{ foo <- function() get_caller(-1L) foo() get_user() } \seealso{ \code{\link[base:sys.parent]{base::sys.call()}} \code{\link[whoami:whoami]{whoami::whoami()}} } lgr/man/logger_index.Rd0000644000176200001440000000106614131760166014556 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/logger_index.R \name{logger_index} \alias{logger_index} \title{Return a data.frame of all registered loggers} \usage{ logger_index() } \value{ a \code{logger_index} \code{data.frame} } \description{ Return a data.frame of all registered loggers } \examples{ get_logger("tree/leaf") get_logger("shrub/leaf") get_logger("plant/shrub/leaf") logger_index() } \seealso{ \code{\link[=logger_tree]{logger_tree()}} for a more visual representation of registered loggers } lgr/man/FilterForceLevel.Rd0000644000176200001440000000465414643514316015314 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/Filter.R \name{FilterForceLevel} \alias{FilterForceLevel} \title{Override the log level of all events processed by a Logger/Appender} \description{ Overrides the log level of the Appender/Logger that this filter is attached to to with \code{level}. See also \code{\link[=with_log_level]{with_log_level()}}. It is recommended to use filters that modify LogEvents only with Loggers, but they will also work with Appenders. } \examples{ lg <- get_logger("test") analyse <- function(){ lg$add_filter(FilterForceLevel$new("info"), "force") on.exit(lg$remove_filter("force")) lg$error("an error with forced log level INFO") } analyse() lg$error("an normal error") lg$config(NULL) # reset config } \section{Super class}{ \code{\link[lgr:EventFilter]{lgr::EventFilter}} -> \code{FilterForceLevel} } \section{Public fields}{ \if{html}{\out{
}} \describe{ \item{\code{level}}{an \code{integer} \link[=log_level]{log level} used to override the log levels of each \link{LogEvent} processed by this filter.} } \if{html}{\out{
}} } \section{Methods}{ \subsection{Public methods}{ \itemize{ \item \href{#method-FilterForceLevel-new}{\code{FilterForceLevel$new()}} \item \href{#method-FilterForceLevel-clone}{\code{FilterForceLevel$clone()}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-FilterForceLevel-new}{}}} \subsection{Method \code{new()}}{ Initialize a new FilterForceLevel \subsection{Usage}{ \if{html}{\out{
}}\preformatted{FilterForceLevel$new(level)}\if{html}{\out{
}} } \subsection{Arguments}{ \if{html}{\out{
}} \describe{ \item{\code{level}}{an \code{integer} or \code{character} \link[=log_level]{log level}} } \if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-FilterForceLevel-clone}{}}} \subsection{Method \code{clone()}}{ The objects of this class are cloneable with this method. \subsection{Usage}{ \if{html}{\out{
}}\preformatted{FilterForceLevel$clone(deep = FALSE)}\if{html}{\out{
}} } \subsection{Arguments}{ \if{html}{\out{
}} \describe{ \item{\code{deep}}{Whether to make a deep clone.} } \if{html}{\out{
}} } } } lgr/man/Filterable.Rd0000644000176200001440000001023214643514315014155 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/Filterable.R \name{Filterable} \alias{Filterable} \title{Abstract Class for Filterables} \description{ Superclass for classes that have a \verb{$filter()} method such as \link{Appenders} and \link{Loggers}. See \link{EventFilter} for details. \strong{NOTE}: This is an \emph{abstract class}. Abstract classes cannot be instantiated directly, but are exported for package developers that want to extend lgr - for example by creating their own \link[=Appender]{Appenders} or \link[=Layout]{Layouts}. Please refer to the \emph{see also} section for actual implementations of this class. } \seealso{ Other abstract classes: \code{\link{Appender}}, \code{\link{AppenderMemory}}, \code{\link{AppenderTable}} } \concept{abstract classes} \section{Active bindings}{ \if{html}{\out{
}} \describe{ \item{\code{filters}}{a \code{list} of all attached Filters.} } \if{html}{\out{
}} } \section{Methods}{ \subsection{Public methods}{ \itemize{ \item \href{#method-Filterable-filter}{\code{Filterable$filter()}} \item \href{#method-Filterable-add_filter}{\code{Filterable$add_filter()}} \item \href{#method-Filterable-remove_filter}{\code{Filterable$remove_filter()}} \item \href{#method-Filterable-set_filters}{\code{Filterable$set_filters()}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-Filterable-filter}{}}} \subsection{Method \code{filter()}}{ Determine whether the LogEvent \code{x} should be passed on to Appenders (\code{TRUE}) or not (\code{FALSE}). See also the active binding \code{filters}. \subsection{Usage}{ \if{html}{\out{
}}\preformatted{Filterable$filter(event)}\if{html}{\out{
}} } \subsection{Arguments}{ \if{html}{\out{
}} \describe{ \item{\code{event}}{a \link{LogEvent}} } \if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-Filterable-add_filter}{}}} \subsection{Method \code{add_filter()}}{ Attach a filter \subsection{Usage}{ \if{html}{\out{
}}\preformatted{Filterable$add_filter(filter, name = NULL)}\if{html}{\out{
}} } \subsection{Arguments}{ \if{html}{\out{
}} \describe{ \item{\code{filter}}{\itemize{ \item a function with the single argument \code{event} that returns \code{TRUE} or \code{FALSE}; \item an \link{EventFilter} \link[R6:R6Class]{R6::R6} object; or \item any \R object with a \verb{$filter()} method. } If a Filter returns a non-\code{FALSE} value, will be interpreted as \code{TRUE} (= no filtering takes place) and a warning will be thrown.} \item{\code{name}}{\code{character} scalar or \code{NULL}. An optional name which makes it easier to access (or remove) the filter} } \if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-Filterable-remove_filter}{}}} \subsection{Method \code{remove_filter()}}{ Remove a filter \subsection{Usage}{ \if{html}{\out{
}}\preformatted{Filterable$remove_filter(pos)}\if{html}{\out{
}} } \subsection{Arguments}{ \if{html}{\out{
}} \describe{ \item{\code{pos}}{\code{character} or \code{integer} scalar. The name or index of the Filter to be removed.} } \if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-Filterable-set_filters}{}}} \subsection{Method \code{set_filters()}}{ Set or replace (all) Filters of parent object. See \link{EventFilter} for how Filters work. \subsection{Usage}{ \if{html}{\out{
}}\preformatted{Filterable$set_filters(filters)}\if{html}{\out{
}} } \subsection{Arguments}{ \if{html}{\out{
}} \describe{ \item{\code{filters}}{a \code{list} (named or unnamed) of \link[=EventFilter]{EventFilters} or predicate functions. See \code{\link[=is_filter]{is_filter()}}.} } \if{html}{\out{
}} } } } lgr/man/print.LogEvent.Rd0000644000176200001440000000632615036467176015004 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/LogEvent.R \name{print.LogEvent} \alias{print.LogEvent} \alias{format.LogEvent} \title{Print or Format Logging Data} \usage{ \method{print}{LogEvent}( x, fmt = "\%L [\%t] \%m \%f", timestamp_fmt = "\%Y-\%m-\%d \%H:\%M:\%S", colors = getOption("lgr.colors"), log_levels = getOption("lgr.log_levels"), pad_levels = "right", excluded_fields = NULL, ... ) \method{format}{LogEvent}( x, fmt = "\%L [\%t] \%m \%f", timestamp_fmt = "\%Y-\%m-\%d \%H:\%M:\%S", colors = NULL, log_levels = getOption("lgr.log_levels"), pad_levels = "right", excluded_fields = NULL, ... ) } \arguments{ \item{x}{a \link{LogEvent}} \item{fmt}{A \code{character} scalar that may contain any of the tokens listed bellow in the section Format Tokens.} \item{timestamp_fmt}{see \code{\link[=format.POSIXct]{format.POSIXct()}}} \item{colors}{A \code{list} of \code{functions} that will be used to color the log levels (likely from \link[crayon:crayon]{crayon::crayon}).} \item{log_levels}{named \code{integer} vector of valid log levels} \item{pad_levels}{\code{right}, \code{left} or \code{NULL}. Whether or not to pad the log level names to the same width on the left or right side, or not at all.} \item{excluded_fields}{a \code{character} vector of fields to exclude from \verb{\%j} and \verb{\%f}} \item{...}{ignored} } \value{ \code{x} for \code{print()} and a \code{character} scalar for \code{format()} } \description{ Print or Format Logging Data } \section{Format Tokens}{ \describe{ \item{\verb{\%t}}{The timestamp of the message, formatted according to \code{timestamp_fmt})} \item{\verb{\%l}}{the log level, lowercase \code{character} representation} \item{\verb{\%L}}{the log level, uppercase \code{character} representation} \item{\verb{\%k}}{the log level, first letter of lowercase \code{character} representation} \item{\verb{\%K}}{the log level, first letter of uppercase \code{character} representation} \item{\verb{\%n}}{the log level, \code{integer} representation} \item{\verb{\%g}}{the name of the logger} \item{\verb{\%p}}{the PID (process ID). Useful when logging code that uses multiple threads.} \item{\verb{\%c}}{the calling function} \item{\verb{\%m}}{the log message} \item{\verb{\%r}}{the raw log message (without string interpolation)} \item{\verb{\%f}}{all custom fields of \code{x} in a pseudo-JSON like format that is optimized for human readability and console output} \item{\verb{\%j}}{all custom fields of \code{x} in proper JSON. This requires that you have \strong{jsonlite} installed and does not support colors as opposed to \verb{\%f} } } } \examples{ # standard fields can be printed using special tokens x <- LogEvent$new( level = 300, msg = "a test event", caller = "testfun()", logger = lgr ) print(x) print(x, fmt = c("\%t (\%p) \%c: \%n - \%m")) print(x, colors = NULL) # custom values y <- LogEvent$new( level = 300, msg = "a gps track", logger = lgr, waypoints = 10, location = "Austria" ) # default output with \%f print(y) # proper JSON output with \%j if (requireNamespace("jsonlite")){ print(y, fmt = "\%L [\%t] \%m \%j") } } lgr/man/get_log_levels.Rd0000644000176200001440000000346614131760166015110 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/log_levels.R \name{get_log_levels} \alias{get_log_levels} \alias{log_levels} \alias{log_level} \alias{add_log_levels} \alias{remove_log_levels} \title{Manage Log Levels} \usage{ get_log_levels() add_log_levels(levels) remove_log_levels(level_names) } \arguments{ \item{levels}{a named \code{character} vector (see examples)} \item{level_names}{a \code{character} vector of the names of the levels to remove} } \value{ a named \code{character} vector of the globally available log levels (\code{add_log_levels()} and \code{remove_log_levels()} return invisibly). } \description{ Display, add and remove character labels for log levels. } \section{Default Log Levels}{ lgr comes with the following predefined log levels that are identical to the log levels of log4j. \tabular{rll}{ Level \tab Name \tab Description \cr \code{0} \tab off \tab A log level of 0/off tells a Logger or Appender to suspend all logging \cr \code{100} \tab fatal \tab Critical error that leads to program abort. Should always indicate a \code{stop()} or similar \cr \code{200} \tab error \tab A severe error that does not trigger program abort\cr \code{300} \tab warn \tab A potentially harmful situation, like \code{warning()}\cr \code{400} \tab info \tab An informational message on the progress of the application\cr \code{500} \tab debug \tab Finer grained informational messages that are mostly useful for debugging\cr \code{600} \tab trace \tab An even finer grained message than debug\cr \code{NA} \tab all \tab A log level of NA/all tells a Logger or Appender to process all log events } } \examples{ get_log_levels() add_log_levels(c(errorish = 250)) get_log_levels() remove_log_levels("errorish") get_log_levels() } lgr/man/simple_logging.Rd0000644000176200001440000001174714131760166015116 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/simple_logging.R \name{simple_logging} \alias{simple_logging} \alias{log_exception} \alias{threshold} \alias{console_threshold} \alias{add_appender} \alias{remove_appender} \alias{show_log} \alias{show_dt} \alias{show_data} \title{Simple Logging} \usage{ log_exception(code, logfun = lgr$fatal, caller = get_caller(-3)) threshold(level, target = lgr::lgr) console_threshold(level, target = lgr::lgr$appenders$console) add_appender(appender, name = NULL, target = lgr::lgr) remove_appender(pos, target = lgr::lgr) show_log(threshold = NA_integer_, n = 20L, target = lgr::lgr) show_dt(target = lgr::lgr) show_data(target = lgr::lgr) } \arguments{ \item{code}{Any \R code} \item{logfun}{a \code{function} for processing the log request, usually \code{lgr$info()}, \code{lgr$debug()}, etc... .} \item{caller}{a \code{character} scalar. The name of the calling function} \item{level}{\code{integer} or \code{character} scalar: the desired log level} \item{target}{a \link{Logger} or \link{Appender} or the name of a Logger as \code{character} scalar} \item{appender}{an \code{Appender}} \item{name}{\code{character} scalar. An optional name for the new Appender.} \item{pos}{\code{integer} index or \code{character} names of the appenders to remove} \item{threshold}{\code{character} or \code{integer} scalar. The minimum \link[=log_levels]{log level} that should be processed by the root logger.} \item{n}{\code{integer} scalar. Show only the last \code{n} log entries that match \code{threshold}} } \value{ \code{threshold()} and \code{console_threshold()} return the \link{log_level} of \code{target} as \code{integer} (invisibly) \code{add_appender()} and \code{remove_appender()} return \code{target}. \code{show_log()} prints to the console and returns whatever the target Appender's \verb{$show()} method returns, usually a \code{character} vector, \code{data.frame} or \code{data.table} (invisibly). \code{show_data()} always returns a \code{data.frame} and \code{show_dt()} always returns a \code{data.table}. } \description{ lgr provides convenience functions managing the root Logger. These are designed chiefly for interactive use and are less verbose than their R6 method counterparts. \code{threshold()} sets or retrieves the threshold for an \link{Appender} or \link{Logger} (the minimum level of log messages it processes). It's \code{target} defaults to the root logger. (equivalent to \code{lgr::lgr$threshold} and \code{lgr::lgr$set_threshold}) \code{console_threshold()} is a shortcut to set the threshold of the root loggers \link{AppenderConsole}, which is usually the only Appender that manages console output for a given \R session. (equivalent to \code{lgr::lgr$appenders$console$threshold} and \code{lgr::lgr$appenders$console$set_threshold}) \code{add_appender()} and \code{remove_appender()} add Appenders to Loggers and other Appenders. (equivalent to \code{lgr::lgr$add_appender} and \code{lgr::lgr$remove_appender}) \code{show_log()} displays the last \code{n} log entries of an Appender (or a Logger with such an Appender attached) with a \verb{$show()} method. Most, but not all Appenders support this function (try \link{AppenderFile} or \link{AppenderBuffer}). \code{show_data()} and \code{show_dt()} work similar to \code{show_log()}, except that they return the log as \code{data.frame} or \code{data.table} respectively. Only Appenders that log to formats that can easily be converted to \code{data.frames} are supported (try \link{AppenderJson} or \link{AppenderBuffer}). The easiest way to try out this features is by adding an AppenderBuffer to the root logger with \code{\link[=basic_config]{basic_config(memory = TRUE)}}. } \examples{ # Get and set the threshold of the root logger threshold("error") threshold() lgr$info("this will be supressed") lgr$error("an important error message") # you can also specify a target to modify other loggers lg <- get_logger("test") threshold("fatal", target = lg) threshold(target = lg) # If a Logger's threshold is not set, the threshold is inherited from # its parent, in this case the root logger (that we set to error/200 before) threshold(NULL, target = lg) threshold(target = lg) # Alternative R6 API for getting/setting thresholds lg$set_threshold("info") lg$threshold lg$set_threshold(300) lg$threshold lg$set_threshold(NULL) lg$threshold # cleanup lgr$config(NULL) lg$config(NULL) # add Appenders to a Logger add_appender(AppenderConsole$new(), "second_console_appender") lgr$fatal("Multiple console appenders are a bad idea") remove_appender("second_console_appender") lgr$info("Good that we defined an appender name, so it's easy to remove") # Reconfigure the root logger basic_config(memory = TRUE) # log some messages lgr$info("a log message") lgr$info("another message with data", data = 1:3) show_log() show_data() # cleanup lgr$config(NULL) } lgr/man/is_filter.Rd0000644000176200001440000000146514131760166014073 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/Filter.R \name{is_filter} \alias{is_filter} \title{Check if an R Object is a Filter} \usage{ is_filter(x) } \arguments{ \item{x}{any \R Object} } \value{ \code{TRUE} or \code{FALSE} } \description{ Returns \code{TRUE} for any \R object that can be used as a Filter for \link{Loggers} or, \link{Appenders}: \itemize{ \item a \code{function} with the single argument \code{event}; \item an \link{EventFilter} \link[R6:R6Class]{R6::R6} object; or \item any object with a \verb{$filter(event)} method. } \strong{Note:} A Filter \strong{must} return a scalar \code{TRUE} or \code{FALSE}, but this property cannot be checked by \code{\link[=is_filter]{is_filter()}}. } \seealso{ \link{EventFilter}, \link{Filterable} } lgr/man/Logger.Rd0000644000176200001440000005363015035407137013333 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/Logger.R \name{Logger} \alias{Logger} \alias{Loggers} \title{Loggers} \description{ A Logger produces a \link{LogEvent} that contains a log message along with metadata (timestamp, calling function, ...) and dispatches it to one or more \link{Appenders} which are responsible for the output (console, file, ...) of the event. \strong{lgr} comes with a single pre-configured Logger called the \verb{root Logger} that can be accessed via \verb{lgr$<...>}. Instantiation of new Loggers is done with \code{\link[=get_logger]{get_logger()}}. It is advisable to instantiate a separate Logger with a descriptive name for each package/script in which you use \pkg{lgr}. } \note{ If you are a package developer you should define a new Logger for each package, but you do not need to configure it. The user of the package should decide how and where to output logging, usually by configuring the root Logger (new Appenders added/removed, Layouts modified, etc...). } \examples{ # lgr::lgr is the root logger that is always available lgr$info("Today is a good day") lgr$fatal("This is a serious error") # Loggers use sprintf() for string formatting by default lgr$info("Today is \%s", Sys.Date() ) # If no unnamed `...` are present, msg is not passed through sprintf() lgr$fatal("100\% bad") # so this works lgr$fatal("\%s\%\% bad", 100) # if you use unnamed arguments, you must escape \% # You can create new loggers with get_logger() tf <- tempfile() lg <- get_logger("mylogger")$set_appenders(AppenderFile$new(tf)) # The new logger passes the log message on to the appenders of its parent # logger, which is by default the root logger. This is why the following # writes not only the file 'tf', but also to the console. lg$fatal("blubb") readLines(tf) # This logger's print() method depicts this relationship. child <- get_logger("lg/child") print(child) print(child$name) # use formatting strings and custom fields tf2 <- tempfile() lg$add_appender(AppenderFile$new(tf2, layout = LayoutJson$new())) lg$info("Not all \%s support custom fields", "appenders", type = "test") cat(readLines(tf), sep = "\n") cat(readLines(tf2), sep = "\n") # cleanup unlink(c(tf, tf2)) lg$config(NULL) # reset logger config # LoggerGlue # You can also create a new logger that uses the awesome glue library for # string formatting instead of sprintf if (requireNamespace("glue")){ lg <- get_logger_glue("glue") lg$fatal("blah ", "fizz is set to: {fizz}", foo = "bar", fizz = "buzz") # prevent creation of custom fields with prefixing a dot lg$fatal("blah ", "fizz is set to: {.fizz}", foo = "bar", .fizz = "buzz") #' # completely reset 'glue' to an unconfigured vanilla Logger get_logger("glue", reset = TRUE) } # Configuring a Logger lg <- get_logger("test") lg$config(NULL) # resets logger to unconfigured state # With setters lg$ set_threshold("error")$ set_propagate(FALSE)$ set_appenders(AppenderConsole$new(threshold = "info")) lg$config(NULL) # With a list lg$config(list( threshold = "error", propagate = FALSE, appenders = list(AppenderConsole$new(threshold = "info")) )) lg$config(NULL) # resets logger to unconfigured state # Via YAML cfg <- " Logger: threshold: error propagate: false appenders: AppenderConsole: threshold: info " lg$config(cfg) lg$config(NULL) ## ------------------------------------------------ ## Method `Logger$list_log` ## ------------------------------------------------ lg <- get_logger("test") lg$list_log(list(level = 400, msg = "example")) ## ------------------------------------------------ ## Method `Logger$add_appender` ## ------------------------------------------------ lg <- get_logger("test") lg$add_appender(AppenderConsole$new(), name = "myconsole") lg$appenders[[1]] lg$appenders$myconsole lg$remove_appender("myconsole") lg$config(NULL) # reset config ## ------------------------------------------------ ## Method `Logger$set_exception_handler` ## ------------------------------------------------ lgr$info(stop("this produces a warning instead of an error")) } \seealso{ \href{https://glue.tidyverse.org/}{glue} \code{\link[=get_logger]{get_logger()}} } \section{Super class}{ \code{\link[lgr:Filterable]{lgr::Filterable}} -> \code{Logger} } \section{Active bindings}{ \if{html}{\out{
}} \describe{ \item{\code{name}}{A \code{character} scalar. The unique name of each logger, which also includes the names of its ancestors (separated by \code{/}).} \item{\code{threshold}}{\code{integer} scalar. The threshold of the \code{Logger}, or if it \code{NULL} the threshold it inherits from its closest ancestor with a non-\code{NULL} threshold} \item{\code{propagate}}{A \code{TRUE} or \code{FALSE}. Should a Logger propagate events to the Appenders of its ancestors?} \item{\code{ancestry}}{A named \code{logical} vector of containing the propagate value of each Logger upper the inheritance tree. The names are the names of the appenders. \code{ancestry} is an S3 class with a custom \code{format()}/\code{print()} method, so if you want to use the plain logical vector use \code{unclass(lg$ancestry)}} \item{\code{parent}}{a \code{Logger}. The direct ancestor of the \code{Logger}.} \item{\code{last_event}}{The last LogEvent produced by the current Logger} \item{\code{appenders}}{a \code{list} of all \link{Appenders} of the Logger} \item{\code{inherited_appenders}}{A \code{list} of all appenders that the Logger inherits from its ancestors} \item{\code{exception_handler}}{a \code{function}. See \verb{$set_exception_handler} and \verb{$handle_exception}} } \if{html}{\out{
}} } \section{Methods}{ \subsection{Public methods}{ \itemize{ \item \href{#method-Logger-new}{\code{Logger$new()}} \item \href{#method-Logger-log}{\code{Logger$log()}} \item \href{#method-Logger-fatal}{\code{Logger$fatal()}} \item \href{#method-Logger-error}{\code{Logger$error()}} \item \href{#method-Logger-warn}{\code{Logger$warn()}} \item \href{#method-Logger-info}{\code{Logger$info()}} \item \href{#method-Logger-debug}{\code{Logger$debug()}} \item \href{#method-Logger-trace}{\code{Logger$trace()}} \item \href{#method-Logger-list_log}{\code{Logger$list_log()}} \item \href{#method-Logger-config}{\code{Logger$config()}} \item \href{#method-Logger-add_appender}{\code{Logger$add_appender()}} \item \href{#method-Logger-remove_appender}{\code{Logger$remove_appender()}} \item \href{#method-Logger-handle_exception}{\code{Logger$handle_exception()}} \item \href{#method-Logger-set_exception_handler}{\code{Logger$set_exception_handler()}} \item \href{#method-Logger-set_propagate}{\code{Logger$set_propagate()}} \item \href{#method-Logger-set_threshold}{\code{Logger$set_threshold()}} \item \href{#method-Logger-set_appenders}{\code{Logger$set_appenders()}} \item \href{#method-Logger-set_replace_empty}{\code{Logger$set_replace_empty()}} \item \href{#method-Logger-spawn}{\code{Logger$spawn()}} } } \if{html}{\out{
Inherited methods
}} \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-Logger-new}{}}} \subsection{Method \code{new()}}{ \strong{Loggers should never be instantiated directly with \code{Logger$new()}} but rather via \code{\link[=get_logger]{get_logger("name")}}. This way new Loggers are registered in a global namespace which ensures uniqueness and facilitates inheritance between Loggers. If \code{"name"} does not exist, a new Logger with that name will be created, otherwise the function returns a Reference to the existing Logger. \code{name} is potentially a \code{"/"} separated hierarchical value like \code{foo/bar/baz}. Loggers further down the hierarchy are descendants of the loggers above and (by default) inherit \code{threshold} and \code{Appenders} from their ancestors. \subsection{Usage}{ \if{html}{\out{
}}\preformatted{Logger$new( name = "(unnamed logger)", appenders = list(), threshold = NULL, filters = list(), exception_handler = default_exception_handler, propagate = TRUE, replace_empty = "" )}\if{html}{\out{
}} } \subsection{Arguments}{ \if{html}{\out{
}} \describe{ \item{\code{name, appenders, threshold, filters, exception_handler, propagate}}{See section Active bindings.} } \if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-Logger-log}{}}} \subsection{Method \code{log()}}{ Log an event. If \code{level} passes the Logger's \code{threshold} a new \link{LogEvent} with \code{level}, \code{msg}, \code{timestamp} and \code{caller} is created. If the new LogEvent also passes the Loggers \link[=EventFilter]{Filters}, it is be dispatched to the relevant \link{Appenders}. \subsection{Usage}{ \if{html}{\out{
}}\preformatted{Logger$log(level, msg, ..., timestamp = Sys.time(), caller = get_caller(-7))}\if{html}{\out{
}} } \subsection{Arguments}{ \if{html}{\out{
}} \describe{ \item{\code{level}}{a \code{character} or \code{integer} scalar. See \link{log_levels}.} \item{\code{msg}}{\code{character}. A log message. If unnamed arguments are supplied in \code{...}, \code{msg} is passed on to \code{\link[base:sprintf]{base::sprintf()}} (which means \code{"\%"} have to be escaped), otherwise \code{msg} is left as-is.} \item{\code{...}}{\emph{unnamed} arguments in \code{...} must be \code{character} scalars and are passed to \code{\link[base:sprintf]{base::sprintf()}}. \emph{Named} arguments must have unique names but can be arbitrary \R objects that are passed to \code{\link[=LogEvent]{LogEvent$new()}} and will be turned into custom fields.} \item{\code{timestamp}}{\link{POSIXct}. Timestamp of the event.} \item{\code{caller}}{a \code{character} scalar. The name of the calling function.} } \if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-Logger-fatal}{}}} \subsection{Method \code{fatal()}}{ Log an Event fatal priority \subsection{Usage}{ \if{html}{\out{
}}\preformatted{Logger$fatal(msg, ..., caller = get_caller(-8L))}\if{html}{\out{
}} } \subsection{Arguments}{ \if{html}{\out{
}} \describe{ \item{\code{msg, ..., caller}}{see \verb{$log()}} } \if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-Logger-error}{}}} \subsection{Method \code{error()}}{ Log an Event error priority \subsection{Usage}{ \if{html}{\out{
}}\preformatted{Logger$error(msg, ..., caller = get_caller(-8L))}\if{html}{\out{
}} } \subsection{Arguments}{ \if{html}{\out{
}} \describe{ \item{\code{msg, ..., caller}}{see \verb{$log()}} } \if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-Logger-warn}{}}} \subsection{Method \code{warn()}}{ Log an Event warn priority \subsection{Usage}{ \if{html}{\out{
}}\preformatted{Logger$warn(msg, ..., caller = get_caller(-8L))}\if{html}{\out{
}} } \subsection{Arguments}{ \if{html}{\out{
}} \describe{ \item{\code{msg, ..., caller}}{see \verb{$log()}} } \if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-Logger-info}{}}} \subsection{Method \code{info()}}{ Log an Event info priority \subsection{Usage}{ \if{html}{\out{
}}\preformatted{Logger$info(msg, ..., caller = get_caller(-8L))}\if{html}{\out{
}} } \subsection{Arguments}{ \if{html}{\out{
}} \describe{ \item{\code{msg, ..., caller}}{see \verb{$log()}} } \if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-Logger-debug}{}}} \subsection{Method \code{debug()}}{ Log an Event debug priority \subsection{Usage}{ \if{html}{\out{
}}\preformatted{Logger$debug(msg, ..., caller = get_caller(-8L))}\if{html}{\out{
}} } \subsection{Arguments}{ \if{html}{\out{
}} \describe{ \item{\code{msg, ..., caller}}{see \verb{$log()}} } \if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-Logger-trace}{}}} \subsection{Method \code{trace()}}{ Log an Event trace priority \subsection{Usage}{ \if{html}{\out{
}}\preformatted{Logger$trace(msg, ..., caller = get_caller(-8L))}\if{html}{\out{
}} } \subsection{Arguments}{ \if{html}{\out{
}} \describe{ \item{\code{msg, ..., caller}}{see \verb{$log()}} } \if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-Logger-list_log}{}}} \subsection{Method \code{list_log()}}{ \code{list_log()} is a shortcut for \code{do.call(Logger$log, x)}. See \url{https://github.com/s-fleck/joblog} for an R package that leverages this feature to create custom log event types for tracking the status of cron jobs. \subsection{Usage}{ \if{html}{\out{
}}\preformatted{Logger$list_log(x)}\if{html}{\out{
}} } \subsection{Arguments}{ \if{html}{\out{
}} \describe{ \item{\code{x}}{a named \code{list} that must at least contain the named elements \code{level} and \code{timestamp}} } \if{html}{\out{
}} } \subsection{Examples}{ \if{html}{\out{
}} \preformatted{lg <- get_logger("test") lg$list_log(list(level = 400, msg = "example")) } \if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-Logger-config}{}}} \subsection{Method \code{config()}}{ Load a Logger configuration. \subsection{Usage}{ \if{html}{\out{
}}\preformatted{Logger$config(cfg, file, text, list)}\if{html}{\out{
}} } \subsection{Arguments}{ \if{html}{\out{
}} \describe{ \item{\code{cfg}}{\itemize{ \item a special \code{list} object with any or all of the the following elements: \code{appenders}, \code{threshold}, \code{filters}, \code{propagate}, \code{exception_handler}, \item the path to a \code{YAML}/\code{JSON} config file, \item a \code{character} scalar containing \code{YAML/JSON}, \item \code{NULL} (to reset the logger config to the default/unconfigured state) }} \item{\code{file, text, list}}{can be used as an alternative to \code{cfg} that enforces that the supplied argument is of the specified type. See \link{logger_config} for details.} } \if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-Logger-add_appender}{}}} \subsection{Method \code{add_appender()}}{ Add an Appender to the Logger \subsection{Usage}{ \if{html}{\out{
}}\preformatted{Logger$add_appender(appender, name = NULL)}\if{html}{\out{
}} } \subsection{Arguments}{ \if{html}{\out{
}} \describe{ \item{\code{appender}}{a single \link{Appender}} \item{\code{name}}{a \code{character} scalar. Optional but recommended.} } \if{html}{\out{
}} } \subsection{Examples}{ \if{html}{\out{
}} \preformatted{lg <- get_logger("test") lg$add_appender(AppenderConsole$new(), name = "myconsole") lg$appenders[[1]] lg$appenders$myconsole lg$remove_appender("myconsole") lg$config(NULL) # reset config } \if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-Logger-remove_appender}{}}} \subsection{Method \code{remove_appender()}}{ remove an appender \subsection{Usage}{ \if{html}{\out{
}}\preformatted{Logger$remove_appender(pos)}\if{html}{\out{
}} } \subsection{Arguments}{ \if{html}{\out{
}} \describe{ \item{\code{pos}}{\code{integer} index or \code{character} name of the Appender(s) to remove} } \if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-Logger-handle_exception}{}}} \subsection{Method \code{handle_exception()}}{ To prevent errors in the logging logic from crashing the whole script, Loggers pass errors they encounter to an exception handler. The default behaviour is to demote errors to \link{warnings}. See also \code{set_exception_handler()}. \subsection{Usage}{ \if{html}{\out{
}}\preformatted{Logger$handle_exception(expr)}\if{html}{\out{
}} } \subsection{Arguments}{ \if{html}{\out{
}} \describe{ \item{\code{expr}}{expression to be evaluated.} } \if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-Logger-set_exception_handler}{}}} \subsection{Method \code{set_exception_handler()}}{ Set the exception handler of a logger \subsection{Usage}{ \if{html}{\out{
}}\preformatted{Logger$set_exception_handler(fun)}\if{html}{\out{
}} } \subsection{Arguments}{ \if{html}{\out{
}} \describe{ \item{\code{fun}}{a \code{function} with the single argument \code{e} (an error \link{condition})} } \if{html}{\out{
}} } \subsection{Examples}{ \if{html}{\out{
}} \preformatted{lgr$info(stop("this produces a warning instead of an error")) } \if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-Logger-set_propagate}{}}} \subsection{Method \code{set_propagate()}}{ Should a Logger propagate events to the Appenders of its ancestors? \subsection{Usage}{ \if{html}{\out{
}}\preformatted{Logger$set_propagate(x)}\if{html}{\out{
}} } \subsection{Arguments}{ \if{html}{\out{
}} \describe{ \item{\code{x}}{\code{TRUE} or \code{FALSE}. Should \link{LogEvents} be passed on to the appenders of the ancestral Loggers?} } \if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-Logger-set_threshold}{}}} \subsection{Method \code{set_threshold()}}{ Set the minimum log level of events that a Logger should process \subsection{Usage}{ \if{html}{\out{
}}\preformatted{Logger$set_threshold(level)}\if{html}{\out{
}} } \subsection{Arguments}{ \if{html}{\out{
}} \describe{ \item{\code{level}}{\code{character} or \code{integer} scalar. The minimum \link[=log_levels]{log level} that triggers this Logger} } \if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-Logger-set_appenders}{}}} \subsection{Method \code{set_appenders()}}{ Set the Logger's Appenders \subsection{Usage}{ \if{html}{\out{
}}\preformatted{Logger$set_appenders(x)}\if{html}{\out{
}} } \subsection{Arguments}{ \if{html}{\out{
}} \describe{ \item{\code{x}}{single \link{Appender} or a \code{list} thereof. Appenders control the output of a Logger. Be aware that a Logger also inherits the Appenders of its ancestors (see \code{vignette("lgr", package = "lgr")} for more info about Logger inheritance).} } \if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-Logger-set_replace_empty}{}}} \subsection{Method \code{set_replace_empty()}}{ Set the replacement for empty values (\code{NULL} or empty vectors) \subsection{Usage}{ \if{html}{\out{
}}\preformatted{Logger$set_replace_empty(x)}\if{html}{\out{
}} } \subsection{Arguments}{ \if{html}{\out{
}} \describe{ \item{\code{x}}{should be a \code{character} vector, but other types of values are supported. use wisely.} } \if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-Logger-spawn}{}}} \subsection{Method \code{spawn()}}{ Spawn a child Logger. This is very similar to using \code{\link[=get_logger]{get_logger()}}, but can be useful in some cases where Loggers are created programmatically \subsection{Usage}{ \if{html}{\out{
}}\preformatted{Logger$spawn(name)}\if{html}{\out{
}} } \subsection{Arguments}{ \if{html}{\out{
}} \describe{ \item{\code{name}}{\code{character} vector. Name of the child logger \code{get_logger("foo/bar")$spawn("baz")} is equivalent to \code{get_logger("foo/bar/baz")}} } \if{html}{\out{
}} } } } lgr/man/AppenderBuffer.Rd0000644000176200001440000002223714643514316015005 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/Appender.R \name{AppenderBuffer} \alias{AppenderBuffer} \title{Log to a memory buffer} \description{ An Appender that Buffers LogEvents in-memory and and redirects them to other Appenders once certain conditions are met. } \section{Fields}{ \describe{ \item{\code{appenders}, \code{set_appenders()}}{Like for a \link{Logger}. Buffered events will be passed on to these Appenders once a flush is triggered} \item{\verb{flush_on_exit, set_flush_on_exit(x)}}{\code{TRUE} or \code{FALSE}: Whether the buffer should be flushed when the Appender is garbage collected (f.e when you close \R)} \item{\verb{flush_on_rotate, set_flush_on_rotate}}{\code{TRUE} or \code{FALSE}: Whether the buffer should be flushed when the Buffer is full (f.e when you close \R). Setting this to off can have slightly negative performance impacts.} } } \seealso{ \link{LayoutFormat} Other Appenders: \code{\link{Appender}}, \code{\link{AppenderConsole}}, \code{\link{AppenderFile}}, \code{\link{AppenderFileRotating}}, \code{\link{AppenderFileRotatingDate}}, \code{\link{AppenderFileRotatingTime}}, \code{\link{AppenderTable}} } \concept{Appenders} \section{Super classes}{ \code{\link[lgr:Filterable]{lgr::Filterable}} -> \code{\link[lgr:Appender]{lgr::Appender}} -> \code{\link[lgr:AppenderMemory]{lgr::AppenderMemory}} -> \code{AppenderBuffer} } \section{Methods}{ \subsection{Public methods}{ \itemize{ \item \href{#method-AppenderBuffer-new}{\code{AppenderBuffer$new()}} \item \href{#method-AppenderBuffer-flush}{\code{AppenderBuffer$flush()}} \item \href{#method-AppenderBuffer-clear}{\code{AppenderBuffer$clear()}} \item \href{#method-AppenderBuffer-set_appenders}{\code{AppenderBuffer$set_appenders()}} \item \href{#method-AppenderBuffer-add_appender}{\code{AppenderBuffer$add_appender()}} \item \href{#method-AppenderBuffer-remove_appender}{\code{AppenderBuffer$remove_appender()}} \item \href{#method-AppenderBuffer-format}{\code{AppenderBuffer$format()}} } } \if{html}{\out{
Inherited methods
}} \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-AppenderBuffer-new}{}}} \subsection{Method \code{new()}}{ The \link{Layout} for this Appender is used only to format console output of its \verb{$show()} method. \subsection{Usage}{ \if{html}{\out{
}}\preformatted{AppenderBuffer$new( threshold = NA_integer_, layout = LayoutFormat$new(fmt = "\%L [\%t] \%m", timestamp_fmt = "\%H:\%M:\%S", colors = getOption("lgr.colors")), appenders = NULL, buffer_size = 1000, flush_threshold = NULL, flush_on_exit = TRUE, flush_on_rotate = TRUE, should_flush = NULL, filters = NULL )}\if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-AppenderBuffer-flush}{}}} \subsection{Method \code{flush()}}{ Sends the buffer's contents to all attached Appenders and then clears the Buffer \subsection{Usage}{ \if{html}{\out{
}}\preformatted{AppenderBuffer$flush()}\if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-AppenderBuffer-clear}{}}} \subsection{Method \code{clear()}}{ Clears the buffer, discarding all buffered Events \subsection{Usage}{ \if{html}{\out{
}}\preformatted{AppenderBuffer$clear()}\if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-AppenderBuffer-set_appenders}{}}} \subsection{Method \code{set_appenders()}}{ Exactly like A \link{Logger}, an \link{AppenderBuffer} can have an arbitrary amount of Appenders attached. When the buffer is flushed, the buffered events are dispatched to these Appenders. \subsection{Usage}{ \if{html}{\out{
}}\preformatted{AppenderBuffer$set_appenders(x)}\if{html}{\out{
}} } \subsection{Arguments}{ \if{html}{\out{
}} \describe{ \item{\code{x}}{single \link{Appender} or a \code{list} thereof. Appenders control the output of a Logger. Be aware that a Logger also inherits the Appenders of its ancestors (see \code{vignette("lgr", package = "lgr")} for more info about Logger inheritance).} } \if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-AppenderBuffer-add_appender}{}}} \subsection{Method \code{add_appender()}}{ Add an Appender to the AppenderBuffer Add or remove an \link{Appender}. Supplying a \code{name} is optional but recommended. After adding an Appender with \code{appender$add_appender(AppenderConsole$new(), name = "console")} you can refer to it via \code{appender$appenders$console}. \code{remove_appender()} can remove an Appender by position or name. \subsection{Usage}{ \if{html}{\out{
}}\preformatted{AppenderBuffer$add_appender(appender, name = NULL)}\if{html}{\out{
}} } \subsection{Arguments}{ \if{html}{\out{
}} \describe{ \item{\code{appender}}{a single \link{Appender}} \item{\code{name}}{a \code{character} scalar. Optional but recommended.} } \if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-AppenderBuffer-remove_appender}{}}} \subsection{Method \code{remove_appender()}}{ remove an appender \subsection{Usage}{ \if{html}{\out{
}}\preformatted{AppenderBuffer$remove_appender(pos)}\if{html}{\out{
}} } \subsection{Arguments}{ \if{html}{\out{
}} \describe{ \item{\code{pos}}{\code{integer} index or \code{character} name of the Appender(s) to remove} } \if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-AppenderBuffer-format}{}}} \subsection{Method \code{format()}}{ \subsection{Usage}{ \if{html}{\out{
}}\preformatted{AppenderBuffer$format(...)}\if{html}{\out{
}} } } } lgr/man/AppenderFileRotatingDate.Rd0000644000176200001440000002014115137111310016733 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/Appender.R \name{AppenderFileRotatingDate} \alias{AppenderFileRotatingDate} \title{Log to a date-stamped rotating file} \description{ Log to a date-stamped rotating file Log to a date-stamped rotating file } \details{ This is a simpler version of AppenderFileRotatingTime when the timestamps do not need to include sub-day accuracy. } \seealso{ \link{AppenderFileRotatingTime}, \link{AppenderFileRotating}, \code{\link[rotor:rotate]{rotor::rotate()}} Other Appenders: \code{\link{Appender}}, \code{\link{AppenderBuffer}}, \code{\link{AppenderConsole}}, \code{\link{AppenderFile}}, \code{\link{AppenderFileRotating}}, \code{\link{AppenderFileRotatingTime}}, \code{\link{AppenderTable}} } \concept{Appenders} \section{Super classes}{ \code{\link[lgr:Filterable]{lgr::Filterable}} -> \code{\link[lgr:Appender]{lgr::Appender}} -> \code{\link[lgr:AppenderFile]{lgr::AppenderFile}} -> \code{\link[lgr:AppenderFileRotating]{lgr::AppenderFileRotating}} -> \code{\link[lgr:AppenderFileRotatingTime]{lgr::AppenderFileRotatingTime}} -> \code{AppenderFileRotatingDate} } \section{Methods}{ \subsection{Public methods}{ \itemize{ \item \href{#method-AppenderFileRotatingDate-new}{\code{AppenderFileRotatingDate$new()}} \item \href{#method-AppenderFileRotatingDate-clone}{\code{AppenderFileRotatingDate$clone()}} } } \if{html}{\out{
Inherited methods
}} \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-AppenderFileRotatingDate-new}{}}} \subsection{Method \code{new()}}{ \subsection{Usage}{ \if{html}{\out{
}}\preformatted{AppenderFileRotatingDate$new( file, threshold = NA_integer_, layout = LayoutFormat$new(), filters = NULL, age = Inf, size = -1, max_backups = Inf, compression = FALSE, backup_dir = dirname(file), fmt = "\%Y-\%m-\%d", overwrite = FALSE, cache_backups = TRUE, create_file = NULL )}\if{html}{\out{
}} } \subsection{Arguments}{ \if{html}{\out{
}} \describe{ \item{\code{size, age, max_backups, compression, backup_dir, fmt, overwrite, cache_backups}}{see \code{\link[rotor:rotate]{rotor::rotate_date()}} for the meaning of these arguments. Note that \code{fmt} corresponds to \code{format} (because \verb{$format} has a special meaning for R6 classes).} } \if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-AppenderFileRotatingDate-clone}{}}} \subsection{Method \code{clone()}}{ The objects of this class are cloneable with this method. \subsection{Usage}{ \if{html}{\out{
}}\preformatted{AppenderFileRotatingDate$clone(deep = FALSE)}\if{html}{\out{
}} } \subsection{Arguments}{ \if{html}{\out{
}} \describe{ \item{\code{deep}}{Whether to make a deep clone.} } \if{html}{\out{
}} } } } lgr/man/LayoutGlue.Rd0000644000176200001440000001070715035407137014204 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/Layout.R \name{LayoutGlue} \alias{LayoutGlue} \title{Format Log Events as Text via glue} \description{ Format a \link{LogEvent} as human readable text using \link[glue:glue]{glue::glue}. The function is evaluated in an environment in which it has access to all elements of the \link{LogEvent} (see examples). This is more flexible than \link{LayoutFormat}, but also more complex and slightly less performant. } \examples{ lg <- get_logger("test")$ set_appenders(AppenderConsole$new())$ set_propagate(FALSE) lg$appenders[[1]]$set_layout(LayoutGlue$new()) lg$fatal("test") # All fields of the LogEvent are available, even custom ones lg$appenders[[1]]$layout$set_fmt( "{logger} {level_name}({level}) {caller}: {toupper(msg)} {{custom: {custom}}}" ) lg$fatal("test", custom = "foobar") lg$config(NULL) # reset logger config } \seealso{ lgr exports a number of formatting utility functions that are useful for layout glue: \code{\link[=colorize_levels]{colorize_levels()}}, \code{\link[=pad_left]{pad_left()}}, \code{\link[=pad_right]{pad_right()}}. Other Layouts: \code{\link{Layout}}, \code{\link{LayoutFormat}}, \code{\link{LayoutJson}} } \concept{Layouts} \section{Super class}{ \code{\link[lgr:Layout]{lgr::Layout}} -> \code{LayoutGlue} } \section{Active bindings}{ \if{html}{\out{
}} \describe{ \item{\code{fmt}}{A string that will be interpreted by \code{\link[glue:glue]{glue::glue()}}} } \if{html}{\out{
}} } \section{Methods}{ \subsection{Public methods}{ \itemize{ \item \href{#method-LayoutGlue-new}{\code{LayoutGlue$new()}} \item \href{#method-LayoutGlue-format_event}{\code{LayoutGlue$format_event()}} \item \href{#method-LayoutGlue-set_fmt}{\code{LayoutGlue$set_fmt()}} \item \href{#method-LayoutGlue-set_colors}{\code{LayoutGlue$set_colors()}} \item \href{#method-LayoutGlue-toString}{\code{LayoutGlue$toString()}} \item \href{#method-LayoutGlue-clone}{\code{LayoutGlue$clone()}} } } \if{html}{\out{
Inherited methods
}} \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-LayoutGlue-new}{}}} \subsection{Method \code{new()}}{ \subsection{Usage}{ \if{html}{\out{
}}\preformatted{LayoutGlue$new( fmt = "{pad_right(colorize_levels(toupper(level_name)), 5)} [{timestamp}] {msg}" )}\if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-LayoutGlue-format_event}{}}} \subsection{Method \code{format_event()}}{ \subsection{Usage}{ \if{html}{\out{
}}\preformatted{LayoutGlue$format_event(event)}\if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-LayoutGlue-set_fmt}{}}} \subsection{Method \code{set_fmt()}}{ \subsection{Usage}{ \if{html}{\out{
}}\preformatted{LayoutGlue$set_fmt(x)}\if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-LayoutGlue-set_colors}{}}} \subsection{Method \code{set_colors()}}{ \subsection{Usage}{ \if{html}{\out{
}}\preformatted{LayoutGlue$set_colors(x)}\if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-LayoutGlue-toString}{}}} \subsection{Method \code{toString()}}{ \subsection{Usage}{ \if{html}{\out{
}}\preformatted{LayoutGlue$toString()}\if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-LayoutGlue-clone}{}}} \subsection{Method \code{clone()}}{ The objects of this class are cloneable with this method. \subsection{Usage}{ \if{html}{\out{
}}\preformatted{LayoutGlue$clone(deep = FALSE)}\if{html}{\out{
}} } \subsection{Arguments}{ \if{html}{\out{
}} \describe{ \item{\code{deep}}{Whether to make a deep clone.} } \if{html}{\out{
}} } } } lgr/man/get_logger.Rd0000644000176200001440000000270414131760166014226 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/get_logger.R \name{get_logger} \alias{get_logger} \alias{get_logger_glue} \title{Get/Create a Logger} \usage{ get_logger(name, class = Logger, reset = FALSE) get_logger_glue(name) } \arguments{ \item{name}{a \code{character} scalar or vector: The qualified name of the Logger as a hierarchical value.} \item{class}{An \link[R6:R6Class]{R6ClassGenerator} object. Usually \code{Logger} or \code{LoggerGlue} are the only valid choices.} \item{reset}{a \code{logical} scalar. If \code{TRUE} the logger is reset to an unconfigured state. Unlike \verb{$config(NULL)} this also replaces a \code{LoggerGlue} with vanilla \code{Logger}. Please note that this will invalidate Logger references created before the reset call (see examples).} } \value{ a \link{Logger} } \description{ Get/Create a Logger } \examples{ lg <- get_logger("log/ger/test") # equivalent to lg <- get_logger(c("log", "ger", "test")) lg$warn("a \%s message", "warning") lg lg$parent if (requireNamespace('glue')){ lg <- get_logger_glue("log/ger") } lg$warn("a {.text} message", .text = "warning") # completely reset 'glue' to an unconfigured vanilla Logger get_logger("log/ger", reset = TRUE) # WARNING: this invalidates existing references to the Logger try(lg$info("lg has been invalidated an no longer works")) lg <- get_logger("log/ger") lg$info("now all is well again") } lgr/man/print.Logger.Rd0000644000176200001440000000276114131760166014465 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/Logger.R \name{print.Logger} \alias{print.Logger} \alias{format.Logger} \alias{print.ancestry} \alias{format.ancestry} \title{Print a Logger Object} \usage{ \method{print}{Logger}(x, color = requireNamespace("crayon", quietly = TRUE), ...) \method{format}{Logger}(x, color = FALSE, ...) \method{print}{ancestry}(x, color = requireNamespace("crayon", quietly = TRUE), ...) \method{format}{ancestry}(x, color = FALSE, ...) } \arguments{ \item{x}{any \R Object} \item{color}{\code{TRUE} or \code{FALSE}: Output with color? Requires the Package \strong{crayon}} \item{...}{ignored} } \value{ \code{print()} returns \code{x} (invisibly), \code{format()} returns a \code{character} vector. } \description{ The \code{print()} method for Loggers displays the most important aspects of the Logger. You can also print just the \code{ancestry} of a Logger which can be accessed with with \code{logger$ancestry()}. This returns a named \code{character} vector whose names correspond to the names of the Loggers \code{logger} inherits from. The \code{TRUE}/\code{FALSE} status of its elements correspond to the \code{propagate} values of these Loggers. } \examples{ # print most important details of logger print(lgr) # print only the ancestry of a logger lg <- get_logger("AegonV/Aerys/Rheagar/Aegon") get_logger("AegonV/Aerys/Rheagar")$set_propagate(FALSE) print(lg$ancestry) unclass(lg$ancestry) } lgr/man/basic_config.Rd0000644000176200001440000000523414402036437014515 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/basic_config.R \name{basic_config} \alias{basic_config} \title{Basic Setup for the Logging System} \usage{ basic_config( file = NULL, fmt = "\%L [\%t] \%m", timestamp_fmt = "\%Y-\%m-\%d \%H:\%M:\%OS3", threshold = "info", appenders = NULL, console = if (is.null(appenders)) "all" else FALSE, console_fmt = "\%L [\%t] \%m \%f", console_timestamp_fmt = "\%H:\%M:\%OS3", console_connection = NULL, memory = FALSE ) } \arguments{ \item{file}{\code{character} scalar: If not \code{NULL} a \link{AppenderFile} will be created that logs to this file. If the filename ends in \code{.jsonl}, the Appender will be set up to use the \href{https://jsonlines.org/}{JSON Lines} format instead of plain text (see \link{AppenderFile} and \link{AppenderJson}).} \item{fmt}{\code{character} scalar: Format to use if \code{file} is supplied and not a \code{.jsonl} file. If \code{NULL} it defaults to \code{"\%L [\%t] \%m"} (see \link{format.LogEvent})} \item{timestamp_fmt}{see \code{\link[=format.POSIXct]{format.POSIXct()}}} \item{threshold}{\code{character} or \code{integer} scalar. The minimum \link[=log_levels]{log level} that should be processed by the root logger.} \item{appenders}{a single \link{Appender} or a list thereof.} \item{console}{\code{logical} scalar or a \code{threshold} (see above). Add an appender logs to the console (i.e. displays messages in an interactive R session)} \item{console_fmt}{\code{character} scalar: like \code{fmt} but used for console output} \item{console_timestamp_fmt}{\code{character} scalar: like \code{timestamp_fmt} but used for console output} \item{console_connection}{see \code{\link[=cat]{cat()}} \code{file} argument.} \item{memory}{\code{logical} scalar. or a \code{threshold} (see above). Add an Appender that logs to a memory buffer, see also \code{\link[=show_log]{show_log()}} and \link{AppenderBuffer}} } \value{ the \code{root} Logger (lgr) } \description{ A quick and easy way to configure the root logger. This is less powerful then using \code{lgr$config()} or \verb{lgr$set_*()} (see \link{Logger}), but reduces the most common configurations to a single line of code. } \examples{ # log to a file basic_config(file = tempfile()) unlink(lgr$appenders$file$file) # cleanup basic_config(file = tempfile(fileext = "jsonl")) unlink(lgr$appenders$file$file) # cleanup # log debug messages to a memory buffer basic_config(threshold = "all", memory = "all", console = "info") lgr$info("an info message") lgr$debug("a hidden message") show_log() # reset to default config basic_config() } lgr/man/pad_right.Rd0000644000176200001440000000077114131760166014053 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/utils-formatting.R \name{pad_right} \alias{pad_right} \alias{pad_left} \title{Pad Character Vectors} \arguments{ \item{x}{a \code{character} vector} \item{width}{\code{integer} scalar. target string width} \item{pad}{\code{character} scalar. the symbol to pad with} } \description{ Pad Character Vectors } \examples{ pad_left("foo", 5) pad_right("foo", 5, ".") pad_left(c("foo", "foooooo"), pad = ".") } lgr/man/logger_config.Rd0000644000176200001440000000307714131760166014720 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/logger_config.R \name{logger_config} \alias{logger_config} \alias{as_logger_config} \alias{as_logger_config.list} \alias{as_logger_config.character} \title{Logger Configuration Objects} \usage{ logger_config( appenders = NULL, threshold = NULL, filters = NULL, exception_handler = NULL, propagate = TRUE ) as_logger_config(x) \method{as_logger_config}{list}(x) \method{as_logger_config}{character}(x) } \arguments{ \item{appenders}{see \link{Logger}} \item{threshold}{see \link{Logger}} \item{filters}{see \link{Logger}} \item{exception_handler}{see \link{Logger}} \item{propagate}{see \link{Logger}} \item{x}{any \R object. Especially: \itemize{ \item A \code{character} scalar. This can either be the path to a YAML file or a character scalar containing valid YAML \item a list containing the elements \code{appenders}, \code{threshold}, \code{exception_handler}, \code{propagate} and \code{filters}. See the section \emph{Fields} in \link{Logger} for details. \item a Logger object, to clone its configuration. }} } \value{ a \code{list} with the subclass \code{"logger_config"} a logger_config object } \description{ \code{logger_config()} is an S3 constructor for \code{logger_config} objects that can be passed to the \verb{$config} method of a \link{Logger}. You can just pass a normal \code{list} instead, but using this constructor is a more formal way that includes additional argument checking. } \seealso{ \url{https://yaml.org/} } lgr/man/LoggerGlue.Rd0000644000176200001440000002126515035407137014147 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/Logger.R \name{LoggerGlue} \alias{LoggerGlue} \title{LoggerGlue} \description{ LoggerGlue LoggerGlue } \details{ \code{LoggerGlue} uses \code{\link[glue:glue]{glue::glue()}} instead of \code{\link[base:sprintf]{base::sprintf()}} to construct log messages. \strong{glue} is a very well designed package for string interpolation. It makes composing log messages more flexible and comfortable at the price of an additional dependency and slightly less performance than \code{sprintf()}. \code{glue()} lets you define temporary named variables inside the call. As with the normal Logger, these named arguments get turned into custom fields; however, you can suppress this behaviour by making named argument start with a \code{"."}. Please refer to \code{vignette("lgr", package = "lgr")} for examples. } \section{Super classes}{ \code{\link[lgr:Filterable]{lgr::Filterable}} -> \code{\link[lgr:Logger]{lgr::Logger}} -> \code{LoggerGlue} } \section{Methods}{ \subsection{Public methods}{ \itemize{ \item \href{#method-LoggerGlue-new}{\code{LoggerGlue$new()}} \item \href{#method-LoggerGlue-fatal}{\code{LoggerGlue$fatal()}} \item \href{#method-LoggerGlue-error}{\code{LoggerGlue$error()}} \item \href{#method-LoggerGlue-warn}{\code{LoggerGlue$warn()}} \item \href{#method-LoggerGlue-info}{\code{LoggerGlue$info()}} \item \href{#method-LoggerGlue-debug}{\code{LoggerGlue$debug()}} \item \href{#method-LoggerGlue-trace}{\code{LoggerGlue$trace()}} \item \href{#method-LoggerGlue-log}{\code{LoggerGlue$log()}} \item \href{#method-LoggerGlue-list_log}{\code{LoggerGlue$list_log()}} \item \href{#method-LoggerGlue-spawn}{\code{LoggerGlue$spawn()}} \item \href{#method-LoggerGlue-set_transformer}{\code{LoggerGlue$set_transformer()}} } } \if{html}{\out{
Inherited methods
}} \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-LoggerGlue-new}{}}} \subsection{Method \code{new()}}{ \subsection{Usage}{ \if{html}{\out{
}}\preformatted{LoggerGlue$new( name = "(unnamed logger)", appenders = list(), threshold = NULL, filters = list(), exception_handler = default_exception_handler, propagate = TRUE, replace_empty = "", transformer = NULL )}\if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-LoggerGlue-fatal}{}}} \subsection{Method \code{fatal()}}{ \subsection{Usage}{ \if{html}{\out{
}}\preformatted{LoggerGlue$fatal(..., caller = get_caller(-8L), .envir = parent.frame())}\if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-LoggerGlue-error}{}}} \subsection{Method \code{error()}}{ \subsection{Usage}{ \if{html}{\out{
}}\preformatted{LoggerGlue$error(..., caller = get_caller(-8L), .envir = parent.frame())}\if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-LoggerGlue-warn}{}}} \subsection{Method \code{warn()}}{ \subsection{Usage}{ \if{html}{\out{
}}\preformatted{LoggerGlue$warn(..., caller = get_caller(-8L), .envir = parent.frame())}\if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-LoggerGlue-info}{}}} \subsection{Method \code{info()}}{ \subsection{Usage}{ \if{html}{\out{
}}\preformatted{LoggerGlue$info(..., caller = get_caller(-8L), .envir = parent.frame())}\if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-LoggerGlue-debug}{}}} \subsection{Method \code{debug()}}{ \subsection{Usage}{ \if{html}{\out{
}}\preformatted{LoggerGlue$debug(..., caller = get_caller(-8L), .envir = parent.frame())}\if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-LoggerGlue-trace}{}}} \subsection{Method \code{trace()}}{ \subsection{Usage}{ \if{html}{\out{
}}\preformatted{LoggerGlue$trace(..., caller = get_caller(-8L), .envir = parent.frame())}\if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-LoggerGlue-log}{}}} \subsection{Method \code{log()}}{ \subsection{Usage}{ \if{html}{\out{
}}\preformatted{LoggerGlue$log( level, ..., timestamp = Sys.time(), caller = get_caller(-7), .envir = parent.frame() )}\if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-LoggerGlue-list_log}{}}} \subsection{Method \code{list_log()}}{ \subsection{Usage}{ \if{html}{\out{
}}\preformatted{LoggerGlue$list_log(x)}\if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-LoggerGlue-spawn}{}}} \subsection{Method \code{spawn()}}{ \subsection{Usage}{ \if{html}{\out{
}}\preformatted{LoggerGlue$spawn(name)}\if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-LoggerGlue-set_transformer}{}}} \subsection{Method \code{set_transformer()}}{ Set the transformer for glue string interpolation \subsection{Usage}{ \if{html}{\out{
}}\preformatted{LoggerGlue$set_transformer(x)}\if{html}{\out{
}} } \subsection{Arguments}{ \if{html}{\out{
}} \describe{ \item{\code{x}}{single \link{function} taking two arguments. See \code{\link[glue:glue]{glue::glue()}}.} } \if{html}{\out{
}} } } } lgr/man/AppenderFileRotating.Rd0000644000176200001440000002151014643514316016154 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/Appender.R \name{AppenderFileRotating} \alias{AppenderFileRotating} \title{Log to a rotating file} \description{ Log to a rotating file Log to a rotating file } \details{ An extension of \link{AppenderFile} that rotates logfiles based on certain conditions. Please refer to the documentation of \code{\link[rotor:rotate]{rotor::rotate()}} for the meanings of the extra arguments } \seealso{ \link{AppenderFileRotatingDate}, \link{AppenderFileRotatingTime}, \code{\link[rotor:rotate]{rotor::rotate()}} Other Appenders: \code{\link{Appender}}, \code{\link{AppenderBuffer}}, \code{\link{AppenderConsole}}, \code{\link{AppenderFile}}, \code{\link{AppenderFileRotatingDate}}, \code{\link{AppenderFileRotatingTime}}, \code{\link{AppenderTable}} } \concept{Appenders} \section{Super classes}{ \code{\link[lgr:Filterable]{lgr::Filterable}} -> \code{\link[lgr:Appender]{lgr::Appender}} -> \code{\link[lgr:AppenderFile]{lgr::AppenderFile}} -> \code{AppenderFileRotating} } \section{Active bindings}{ \if{html}{\out{
}} \describe{ \item{\code{backups}}{A \code{data.frame} containing information on path, file size, etc... on the available backups of \code{file}.} } \if{html}{\out{
}} } \section{Methods}{ \subsection{Public methods}{ \itemize{ \item \href{#method-AppenderFileRotating-new}{\code{AppenderFileRotating$new()}} \item \href{#method-AppenderFileRotating-append}{\code{AppenderFileRotating$append()}} \item \href{#method-AppenderFileRotating-rotate}{\code{AppenderFileRotating$rotate()}} \item \href{#method-AppenderFileRotating-prune}{\code{AppenderFileRotating$prune()}} \item \href{#method-AppenderFileRotating-set_file}{\code{AppenderFileRotating$set_file()}} \item \href{#method-AppenderFileRotating-set_size}{\code{AppenderFileRotating$set_size()}} \item \href{#method-AppenderFileRotating-set_max_backups}{\code{AppenderFileRotating$set_max_backups()}} \item \href{#method-AppenderFileRotating-set_compression}{\code{AppenderFileRotating$set_compression()}} \item \href{#method-AppenderFileRotating-set_create_file}{\code{AppenderFileRotating$set_create_file()}} \item \href{#method-AppenderFileRotating-set_backup_dir}{\code{AppenderFileRotating$set_backup_dir()}} \item \href{#method-AppenderFileRotating-format}{\code{AppenderFileRotating$format()}} \item \href{#method-AppenderFileRotating-clone}{\code{AppenderFileRotating$clone()}} } } \if{html}{\out{
Inherited methods
}} \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-AppenderFileRotating-new}{}}} \subsection{Method \code{new()}}{ \subsection{Usage}{ \if{html}{\out{
}}\preformatted{AppenderFileRotating$new( file, threshold = NA_integer_, layout = LayoutFormat$new(), filters = NULL, size = Inf, max_backups = Inf, compression = FALSE, backup_dir = dirname(file), create_file = NULL )}\if{html}{\out{
}} } \subsection{Arguments}{ \if{html}{\out{
}} \describe{ \item{\code{size, max_backups, compression, backup_dir, fmt}}{see \code{\link[rotor:rotate]{rotor::rotate()}} for the meaning of these arguments. Note that \code{fmt} corresponds to \code{format} and \code{backup_dir} to \code{dir}.} } \if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-AppenderFileRotating-append}{}}} \subsection{Method \code{append()}}{ \subsection{Usage}{ \if{html}{\out{
}}\preformatted{AppenderFileRotating$append(event)}\if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-AppenderFileRotating-rotate}{}}} \subsection{Method \code{rotate()}}{ \subsection{Usage}{ \if{html}{\out{
}}\preformatted{AppenderFileRotating$rotate(force = FALSE)}\if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-AppenderFileRotating-prune}{}}} \subsection{Method \code{prune()}}{ \subsection{Usage}{ \if{html}{\out{
}}\preformatted{AppenderFileRotating$prune(max_backups = self$max_backups)}\if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-AppenderFileRotating-set_file}{}}} \subsection{Method \code{set_file()}}{ \subsection{Usage}{ \if{html}{\out{
}}\preformatted{AppenderFileRotating$set_file(file)}\if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-AppenderFileRotating-set_size}{}}} \subsection{Method \code{set_size()}}{ \subsection{Usage}{ \if{html}{\out{
}}\preformatted{AppenderFileRotating$set_size(x)}\if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-AppenderFileRotating-set_max_backups}{}}} \subsection{Method \code{set_max_backups()}}{ \subsection{Usage}{ \if{html}{\out{
}}\preformatted{AppenderFileRotating$set_max_backups(x)}\if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-AppenderFileRotating-set_compression}{}}} \subsection{Method \code{set_compression()}}{ \subsection{Usage}{ \if{html}{\out{
}}\preformatted{AppenderFileRotating$set_compression(x)}\if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-AppenderFileRotating-set_create_file}{}}} \subsection{Method \code{set_create_file()}}{ \subsection{Usage}{ \if{html}{\out{
}}\preformatted{AppenderFileRotating$set_create_file(x)}\if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-AppenderFileRotating-set_backup_dir}{}}} \subsection{Method \code{set_backup_dir()}}{ \subsection{Usage}{ \if{html}{\out{
}}\preformatted{AppenderFileRotating$set_backup_dir(x)}\if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-AppenderFileRotating-format}{}}} \subsection{Method \code{format()}}{ \subsection{Usage}{ \if{html}{\out{
}}\preformatted{AppenderFileRotating$format(color = false, ...)}\if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-AppenderFileRotating-clone}{}}} \subsection{Method \code{clone()}}{ The objects of this class are cloneable with this method. \subsection{Usage}{ \if{html}{\out{
}}\preformatted{AppenderFileRotating$clone(deep = FALSE)}\if{html}{\out{
}} } \subsection{Arguments}{ \if{html}{\out{
}} \describe{ \item{\code{deep}}{Whether to make a deep clone.} } \if{html}{\out{
}} } } } lgr/man/figures/0000755000176200001440000000000014131760166013262 5ustar liggesuserslgr/man/figures/yog-logo-plain.svg0000644000176200001440000001267014131760166016646 0ustar liggesusers image/svg+xml lgr/man/figures/yog-logo.svg0000644000176200001440000001552314131760166015545 0ustar liggesusers image/svg+xml YOG lgr/man/Appender.Rd0000644000176200001440000001235714643514315013654 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/Appender.R \name{Appender} \alias{Appender} \alias{Appenders} \title{Appenders} \description{ Appenders are attached to \link{Loggers} and manage the output of the \link{LogEvents} to a destination - such as the console or a text file. An Appender has a single \link{Layout} that tells it how to format the LogEvent. For details please refer to the documentations of the specific Appenders. Additional Appenders that support a wide range of output destinations - such as databases, email, push-notifications or Linux syslog - are available from the package \href{https://github.com/s-fleck/lgrExtra}{lgrExtra}. \strong{NOTE}: This is an \emph{abstract class}. Abstract classes cannot be instantiated directly, but are exported for package developers that want to extend lgr - for example by creating their own \link[=Appender]{Appenders} or \link[=Layout]{Layouts}. Please refer to the \emph{see also} section for actual implementations of this class. } \seealso{ Other abstract classes: \code{\link{AppenderMemory}}, \code{\link{AppenderTable}}, \code{\link{Filterable}} Other Appenders: \code{\link{AppenderBuffer}}, \code{\link{AppenderConsole}}, \code{\link{AppenderFile}}, \code{\link{AppenderFileRotating}}, \code{\link{AppenderFileRotatingDate}}, \code{\link{AppenderFileRotatingTime}}, \code{\link{AppenderTable}} } \concept{Appenders} \concept{abstract classes} \keyword{internal} \section{Super class}{ \code{\link[lgr:Filterable]{lgr::Filterable}} -> \code{Appender} } \section{Active bindings}{ \if{html}{\out{
}} \describe{ \item{\code{destination}}{The output destination of the \code{Appender} in human-readable form. This is mainly used when printing information about the Appender itself.} } \if{html}{\out{
}} } \section{Methods}{ \subsection{Public methods}{ \itemize{ \item \href{#method-Appender-new}{\code{Appender$new()}} \item \href{#method-Appender-append}{\code{Appender$append()}} \item \href{#method-Appender-set_threshold}{\code{Appender$set_threshold()}} \item \href{#method-Appender-set_layout}{\code{Appender$set_layout()}} \item \href{#method-Appender-format}{\code{Appender$format()}} } } \if{html}{\out{
Inherited methods
}} \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-Appender-new}{}}} \subsection{Method \code{new()}}{ \subsection{Usage}{ \if{html}{\out{
}}\preformatted{Appender$new(layout = Layout$new(), threshold = NA_integer_)}\if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-Appender-append}{}}} \subsection{Method \code{append()}}{ Process a \link{LogEvent} \code{event}. This method is usually not called by the user, but invoked by a \link{Logger} \subsection{Usage}{ \if{html}{\out{
}}\preformatted{Appender$append(event)}\if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-Appender-set_threshold}{}}} \subsection{Method \code{set_threshold()}}{ Set the minimum log level that triggers this Appender. See \code{\link[=threshold]{threshold()}} for examples \subsection{Usage}{ \if{html}{\out{
}}\preformatted{Appender$set_threshold(level)}\if{html}{\out{
}} } \subsection{Arguments}{ \if{html}{\out{
}} \describe{ \item{\code{level}}{\code{character} or \code{integer} scalar log level. See \link{log_levels}.} } \if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-Appender-set_layout}{}}} \subsection{Method \code{set_layout()}}{ Set the \code{Layout} that this Appender will use for formatting \code{LogEvents} \subsection{Usage}{ \if{html}{\out{
}}\preformatted{Appender$set_layout(layout)}\if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-Appender-format}{}}} \subsection{Method \code{format()}}{ \subsection{Usage}{ \if{html}{\out{
}}\preformatted{Appender$format(color = FALSE, ...)}\if{html}{\out{
}} } } } lgr/man/print.Appender.Rd0000644000176200001440000000133614131760166015001 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/Appender.R \name{print.Appender} \alias{print.Appender} \title{Print an Appender object} \usage{ \method{print}{Appender}(x, color = requireNamespace("crayon", quietly = TRUE), ...) } \arguments{ \item{x}{any \R Object} \item{color}{\code{TRUE} or \code{FALSE}: Output with color? Requires the Package \strong{crayon}} \item{...}{ignored} } \value{ \code{print()} returns \code{x} (invisibly), \code{format()} returns a \code{character} vector. } \description{ The \code{print()} method for Loggers displays the most important aspects of the Appender. } \examples{ # print most important details of logger print(lgr$console) } lgr/man/standardize_threshold.Rd0000644000176200001440000000370414232437514016475 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/log_levels.R \name{standardize_threshold} \alias{standardize_threshold} \alias{is_threshold} \alias{standardize_log_level} \alias{is_log_level} \alias{standardize_log_levels} \alias{is_log_levels} \title{Standardize User-Input Log Levels to Their Integer Representation} \usage{ standardize_threshold( x, log_levels = c(getOption("lgr.log_levels"), c(all = NA_integer_, off = 0L)) ) is_threshold(x) standardize_log_level(x, log_levels = getOption("lgr.log_levels")) is_log_level(x) standardize_log_levels(x, log_levels = getOption("lgr.log_levels")) is_log_levels(x) } \arguments{ \item{x}{a \code{character} or \code{integer} scalar, or vector for standardize_log_levels} \item{log_levels}{named \code{integer} vector of valid log levels} } \value{ An unnamed \code{integer} vector } \description{ These are helper functions for verifying log levels and converting them from their character to their integer representations. This is primarily useful if you want to build your own \link{Loggers}, \link{Appenders} or \link{Layouts} and need to handle log levels in a way that is consistent with \pkg{lgr} . } \examples{ standardize_threshold("info") standardize_threshold("all") is_threshold("all") is_threshold("foobar") standardize_log_level("info") # all is a valid threshold, but not a valid log level try(is.na(standardize_log_level("all"))) is_log_level("all") # standardized_log_level intentionally only works with scalars, because many # functions require scalar log level inputs try(standardize_log_level(c("info", "fatal"))) # You can still use standardize_log_levels() (plural) to work with vectors standardize_log_levels(c("info", "fatal")) } \seealso{ Other docs relevant for extending lgr: \code{\link{LogEvent}}, \code{\link{as_LogEvent}()}, \code{\link{event_list}()} } \concept{docs relevant for extending lgr} lgr/man/AppenderTable.Rd0000644000176200001440000001156314643514316014623 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/Appender.R \name{AppenderTable} \alias{AppenderTable} \title{Abstract class for logging to tabular structures} \description{ \strong{NOTE}: This is an \emph{abstract class}. Abstract classes cannot be instantiated directly, but are exported for package developers that want to extend lgr - for example by creating their own \link[=Appender]{Appenders} or \link[=Layout]{Layouts}. Please refer to the \emph{see also} section for actual implementations of this class. AppenderTable is extended by Appenders that write to a data source that can be interpreted as tables, (usually a \code{data.frame}). Examples are \code{AppenderDbi}, \code{AppenderRjdbc} and \code{AppenderDt} from the \href{https://github.com/s-fleck/lgrExtra}{lgrExtra} package. } \seealso{ Other abstract classes: \code{\link{Appender}}, \code{\link{AppenderMemory}}, \code{\link{Filterable}} Other Appenders: \code{\link{Appender}}, \code{\link{AppenderBuffer}}, \code{\link{AppenderConsole}}, \code{\link{AppenderFile}}, \code{\link{AppenderFileRotating}}, \code{\link{AppenderFileRotatingDate}}, \code{\link{AppenderFileRotatingTime}} } \concept{Appenders} \concept{abstract classes} \section{Super classes}{ \code{\link[lgr:Filterable]{lgr::Filterable}} -> \code{\link[lgr:Appender]{lgr::Appender}} -> \code{AppenderTable} } \section{Active bindings}{ \if{html}{\out{
}} \describe{ \item{\code{data}}{\code{character} scalar. Contents of the table, parsed to a \code{data.frame}.} \item{\code{data}}{\code{character} scalar. Like \verb{$data}, but returns a \code{data.table} instead (requires the \strong{data.table} package).} } \if{html}{\out{
}} } \section{Methods}{ \subsection{Public methods}{ \itemize{ \item \href{#method-AppenderTable-new}{\code{AppenderTable$new()}} \item \href{#method-AppenderTable-show}{\code{AppenderTable$show()}} \item \href{#method-AppenderTable-format}{\code{AppenderTable$format()}} } } \if{html}{\out{
Inherited methods
}} \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-AppenderTable-new}{}}} \subsection{Method \code{new()}}{ \subsection{Usage}{ \if{html}{\out{
}}\preformatted{AppenderTable$new(...)}\if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-AppenderTable-show}{}}} \subsection{Method \code{show()}}{ Show recent log entries \subsection{Usage}{ \if{html}{\out{
}}\preformatted{AppenderTable$show(threshold = NA_integer_, n = 20L)}\if{html}{\out{
}} } \subsection{Arguments}{ \if{html}{\out{
}} \describe{ \item{\code{threshold}}{an \code{integer} or \code{character} \link[=log_level]{threshold}. Only show events with a log level at or below this threshold.} \item{\code{n}}{a positive \code{integer} scalar. Show at most that many entries} } \if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-AppenderTable-format}{}}} \subsection{Method \code{format()}}{ \subsection{Usage}{ \if{html}{\out{
}}\preformatted{AppenderTable$format(color = FALSE, ...)}\if{html}{\out{
}} } } } lgr/man/default_exception_handler.Rd0000644000176200001440000000113214131760166017301 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/default_functions.R \name{default_exception_handler} \alias{default_exception_handler} \title{Demote an exception to a warning} \usage{ default_exception_handler(e) } \arguments{ \item{e}{an \verb{error condition} object} } \value{ The warning as \code{character} vector } \description{ Throws a timestamped warning instead of stopping the program. This is the default exception handler used by \link{Loggers}. } \examples{ tryCatch(stop("an error has occurred"), error = default_exception_handler) } lgr/man/CannotInitializeAbstractClassError.Rd0000644000176200001440000000123114131760166021032 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/utils.R \name{CannotInitializeAbstractClassError} \alias{CannotInitializeAbstractClassError} \title{Logger Error Conditions} \usage{ CannotInitializeAbstractClassError(class = parent.frame(2)[["classes"]]) } \arguments{ \item{class}{\code{character} scalar. The abstract class that was mistakenly tried to initialize. The default is to discover the class name automatically if called inside \verb{$initialize()\{...\}} in an \link[R6:R6Class]{R6::R6} class definition} } \value{ a condition object } \description{ Logger Error Conditions } \concept{developer tools} lgr/man/LayoutFormat.Rd0000644000176200001440000001720315036467176014550 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/Layout.R \name{LayoutFormat} \alias{LayoutFormat} \title{Format Log Events as Text} \description{ Format Log Events as Text Format Log Events as Text } \details{ Format a \link{LogEvent} as human readable text using \code{\link[=format.LogEvent]{format.LogEvent()}}, which provides a quick and easy way to customize log messages. If you need more control and flexibility, consider using \link{LayoutGlue} instead. see Fields see Fields see Fields see Fields Convert Layout to a \code{character} string Read a log file written using LayoutFormat } \section{Format Tokens}{ This is the same list of format tokens as for \code{\link[=format.LogEvent]{format.LogEvent()}} \describe{ \item{\verb{\%t}}{The timestamp of the message, formatted according to \code{timestamp_fmt})} \item{\verb{\%l}}{the log level, lowercase \code{character} representation} \item{\verb{\%L}}{the log level, uppercase \code{character} representation} \item{\verb{\%k}}{the log level, first letter of lowercase \code{character} representation} \item{\verb{\%K}}{the log level, first letter of uppercase \code{character} representation} \item{\verb{\%n}}{the log level, \code{integer} representation} \item{\verb{\%g}}{the name of the logger} \item{\verb{\%p}}{the PID (process ID). Useful when logging code that uses multiple threads.} \item{\verb{\%c}}{the calling function} \item{\verb{\%m}}{the log message} \item{\verb{\%r}}{the raw log message (without string interpolation)} \item{\verb{\%f}}{all custom fields of \code{x} in a pseudo-JSON like format that is optimized for human readability and console output} \item{\verb{\%j}}{all custom fields of \code{x} in proper JSON. This requires that you have \strong{jsonlite} installed and does not support colors as opposed to \verb{\%f} } } } \examples{ # setup a dummy LogEvent event <- LogEvent$new( logger = Logger$new("dummy logger"), level = 200, timestamp = Sys.time(), caller = NA_character_, msg = "a test message" ) lo <- LayoutFormat$new() lo$format_event(event) } \seealso{ Other Layouts: \code{\link{Layout}}, \code{\link{LayoutGlue}}, \code{\link{LayoutJson}} } \concept{Layouts} \section{Super class}{ \code{\link[lgr:Layout]{lgr::Layout}} -> \code{LayoutFormat} } \section{Active bindings}{ \if{html}{\out{
}} \describe{ \item{\code{fmt}}{a \code{character} scalar containing format tokens. See \code{\link[=format.LogEvent]{format.LogEvent()}}.} \item{\code{timestamp_fmt}}{a \code{character} scalar. See \code{\link[base:strptime]{base::format.POSIXct()}}.} \item{\code{colors}}{a named \code{list} of functions (like the ones provided by the package \pkg{crayon}) passed on on \code{\link[=format.LogEvent]{format.LogEvent()}}.} \item{\code{pad_levels}}{\code{"right"}, \code{"left"} or \code{NULL}. See \code{\link[=format.LogEvent]{format.LogEvent()}}.} } \if{html}{\out{
}} } \section{Methods}{ \subsection{Public methods}{ \itemize{ \item \href{#method-LayoutFormat-new}{\code{LayoutFormat$new()}} \item \href{#method-LayoutFormat-format_event}{\code{LayoutFormat$format_event()}} \item \href{#method-LayoutFormat-set_fmt}{\code{LayoutFormat$set_fmt()}} \item \href{#method-LayoutFormat-set_timestamp_fmt}{\code{LayoutFormat$set_timestamp_fmt()}} \item \href{#method-LayoutFormat-set_colors}{\code{LayoutFormat$set_colors()}} \item \href{#method-LayoutFormat-set_pad_levels}{\code{LayoutFormat$set_pad_levels()}} \item \href{#method-LayoutFormat-toString}{\code{LayoutFormat$toString()}} \item \href{#method-LayoutFormat-read}{\code{LayoutFormat$read()}} \item \href{#method-LayoutFormat-clone}{\code{LayoutFormat$clone()}} } } \if{html}{\out{
Inherited methods
}} \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-LayoutFormat-new}{}}} \subsection{Method \code{new()}}{ \subsection{Usage}{ \if{html}{\out{
}}\preformatted{LayoutFormat$new( fmt = "\%L [\%t] \%m \%j", timestamp_fmt = "\%Y-\%m-\%d \%H:\%M:\%OS3", colors = NULL, pad_levels = "right", excluded_fields = NULL )}\if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-LayoutFormat-format_event}{}}} \subsection{Method \code{format_event()}}{ Format a LogEvent \subsection{Usage}{ \if{html}{\out{
}}\preformatted{LayoutFormat$format_event(event)}\if{html}{\out{
}} } \subsection{Arguments}{ \if{html}{\out{
}} \describe{ \item{\code{event}}{a \link{LogEvent}} } \if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-LayoutFormat-set_fmt}{}}} \subsection{Method \code{set_fmt()}}{ \subsection{Usage}{ \if{html}{\out{
}}\preformatted{LayoutFormat$set_fmt(x)}\if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-LayoutFormat-set_timestamp_fmt}{}}} \subsection{Method \code{set_timestamp_fmt()}}{ \subsection{Usage}{ \if{html}{\out{
}}\preformatted{LayoutFormat$set_timestamp_fmt(x)}\if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-LayoutFormat-set_colors}{}}} \subsection{Method \code{set_colors()}}{ \subsection{Usage}{ \if{html}{\out{
}}\preformatted{LayoutFormat$set_colors(x)}\if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-LayoutFormat-set_pad_levels}{}}} \subsection{Method \code{set_pad_levels()}}{ \subsection{Usage}{ \if{html}{\out{
}}\preformatted{LayoutFormat$set_pad_levels(x)}\if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-LayoutFormat-toString}{}}} \subsection{Method \code{toString()}}{ \subsection{Usage}{ \if{html}{\out{
}}\preformatted{LayoutFormat$toString()}\if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-LayoutFormat-read}{}}} \subsection{Method \code{read()}}{ \subsection{Usage}{ \if{html}{\out{
}}\preformatted{LayoutFormat$read(file, threshold = NA_integer_, n = 20L)}\if{html}{\out{
}} } \subsection{Arguments}{ \if{html}{\out{
}} \describe{ \item{\code{threshold}}{a \code{character} or \code{integer} threshold} \item{\code{n}}{number of log entries to display} } \if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-LayoutFormat-clone}{}}} \subsection{Method \code{clone()}}{ The objects of this class are cloneable with this method. \subsection{Usage}{ \if{html}{\out{
}}\preformatted{LayoutFormat$clone(deep = FALSE)}\if{html}{\out{
}} } \subsection{Arguments}{ \if{html}{\out{
}} \describe{ \item{\code{deep}}{Whether to make a deep clone.} } \if{html}{\out{
}} } } } lgr/man/FilterInject.Rd0000644000176200001440000000472114643514316014475 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/Filter.R \name{FilterInject} \alias{FilterInject} \title{Inject values into all events processed by a Logger/Appender} \description{ Inject arbitrary values into all \link[=LogEvent]{LogEvents} processed by a Logger/Appender. It is recommended to use filters that modify LogEvents only with Loggers, but they will also work with Appenders. } \examples{ lg <- get_logger("test") analyse <- function(){ lg$add_filter(FilterInject$new(type = "analysis"), "inject") on.exit(lg$remove_filter("inject")) lg$error("an error with forced custom 'type'-field") } analyse() lg$error("an normal error") lg$config(NULL) # reset config } \section{Super class}{ \code{\link[lgr:EventFilter]{lgr::EventFilter}} -> \code{FilterInject} } \section{Public fields}{ \if{html}{\out{
}} \describe{ \item{\code{values}}{a named \code{list} of values to be injected into each \link{LogEvent} processed by this filter} } \if{html}{\out{
}} } \section{Methods}{ \subsection{Public methods}{ \itemize{ \item \href{#method-FilterInject-new}{\code{FilterInject$new()}} \item \href{#method-FilterInject-clone}{\code{FilterInject$clone()}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-FilterInject-new}{}}} \subsection{Method \code{new()}}{ Initialize a new FilterInject \subsection{Usage}{ \if{html}{\out{
}}\preformatted{FilterInject$new(..., .list = list())}\if{html}{\out{
}} } \subsection{Arguments}{ \if{html}{\out{
}} \describe{ \item{\code{..., .list}}{any number of named \R objects that will be injected as custom fields into all \link[=LogEvent]{LogEvents} processed by the Appender/Logger that this filter is attached to. See also \code{\link[=with_log_value]{with_log_value()}}.} } \if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-FilterInject-clone}{}}} \subsection{Method \code{clone()}}{ The objects of this class are cloneable with this method. \subsection{Usage}{ \if{html}{\out{
}}\preformatted{FilterInject$clone(deep = FALSE)}\if{html}{\out{
}} } \subsection{Arguments}{ \if{html}{\out{
}} \describe{ \item{\code{deep}}{Whether to make a deep clone.} } \if{html}{\out{
}} } } } lgr/man/read_json_lines.Rd0000644000176200001440000000077614131760166015255 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/read_json_lines.R \name{read_json_lines} \alias{read_json_lines} \title{Read a JSON logfile} \usage{ read_json_lines(file, ...) } \arguments{ \item{file}{\code{character} scalar. path to a JSON logfile (one JSON object per line)} \item{...}{passed on to \code{\link[jsonlite:stream_in]{jsonlite::stream_in()}}} } \value{ a \code{data.frame} } \description{ Read a JSON logfile } \seealso{ \link{LayoutJson} } lgr/man/logger_tree.Rd0000644000176200001440000000262414131760166014407 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/logger_tree.R \name{logger_tree} \alias{logger_tree} \title{Logger Tree} \usage{ logger_tree() } \value{ \code{data.frame} with subclass \code{"logger_tree"} } \description{ Displays a tree structure of all registered Loggers. } \section{Symbology}{ \itemize{ \item unconfigured Loggers are displayed in gray (if your terminal supports colors and you have the package \pkg{crayon} installed). \item If a logger's \code{threshold} is set, it is displayed in square brackets next to its name (reminder: if the threshold is not set, it is inherited from next logger up the logger tree). \item If a logger's \code{propagate} field is set to \code{FALSE} an red hash (\verb{#}) sign is displayed in front of the logger name, to imply that it does not pass LogEvents up the tree. } } \examples{ get_logger("fancymodel") get_logger("fancymodel/shiny")$ set_propagate(FALSE) get_logger("fancymodel/shiny/ui")$ set_appenders(AppenderConsole$new()) get_logger("fancymodel/shiny/server")$ set_appenders(list(AppenderConsole$new(), AppenderConsole$new()))$ set_threshold("trace") get_logger("fancymodel/plumber") if (requireNamespace("cli")){ print(logger_tree()) } } \seealso{ \code{\link[=logger_index]{logger_index()}} for a tidy \code{data.frame} representation of all registered loggers } lgr/man/event_list.Rd0000644000176200001440000000547514232437514014274 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/event_list.R \name{event_list} \alias{event_list} \alias{as_event_list} \alias{as_event_list.list} \alias{as_event_list.LogEvent} \alias{as_event_list.data.frame} \alias{as.data.table.event_list} \alias{as.data.frame.event_list} \title{A List of LogEvents} \usage{ event_list(...) as_event_list(x, ...) \method{as_event_list}{list}(x, ..., scalarize = FALSE) \method{as_event_list}{LogEvent}(x, ..., scalarize = FALSE) \method{as_event_list}{data.frame}(x, na.rm = TRUE, ...) as.data.table.event_list(x, na.rm = TRUE) \method{as.data.frame}{event_list}( x, row.names = NULL, optional = FALSE, stringsAsFactors = FALSE, na.rm = TRUE, ... ) } \arguments{ \item{...}{for \code{event} elements to be added to the list, for the \verb{as_*()} functions parameters passed on to methods.} \item{x}{any \code{R} object} \item{scalarize}{\code{logical} scalar. Turn \link{LogEvents} with non-scalar \code{msg} field into separate log events} \item{na.rm}{remove \code{NA} values before coercing a data.frame to an \code{event_list()}.} \item{row.names}{\code{NULL} or a character vector giving the row names for the data frame. Missing values are not allowed.} \item{optional}{currently ignored and only included for compatibility.} \item{stringsAsFactors}{\code{logical} scalar: should \code{character} vectors be converted to factors? Defaults to \code{FALSE} (as opposed to \code{\link[base:as.data.frame]{base::as.data.frame()}}) and is only included for compatibility.} } \value{ an \code{event_list()} and \code{as_event_list()} return a flat \code{list} of \link{LogEvents}. Nested lists get automatically flattened. \code{as.data.frame} and \code{as.data.table} return a \code{data.frame} or \code{data.table} respectively } \description{ An event_list is a class for \code{list()}s whose only elements are \link{LogEvents}. This structure is occasionally used internally in lgr (for example by \link{AppenderBuffer}) and can be useful for developers that want to write their own Appenders. } \details{ For convenience, \code{as.data.frame()} and \code{as.data.table()} methods exist for event lists. } \examples{ e <- LogEvent$new(level = 300, msg = "a", logger = lgr) as_event_list(e) as_event_list(c(e, e)) # nested lists get automatically unnested as_event_list(c(e, list(nested_event = e))) # scalarize = TRUE "unpacks" events with vector log messages e <- LogEvent$new(level = 300, msg = c("A", "B"), logger = lgr) as_event_list(e, scalarize = FALSE) as_event_list(e, scalarize = TRUE) } \seealso{ Other docs relevant for extending lgr: \code{\link{LogEvent}}, \code{\link{as_LogEvent}()}, \code{\link{standardize_threshold}()} } \concept{docs relevant for extending lgr} lgr/man/use_logger.Rd0000644000176200001440000000150714643514317014246 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/use_logger.R \name{use_logger} \alias{use_logger} \title{Setup a Simple Logger for a Package} \usage{ use_logger( pkg = desc::desc_get("Package", rprojroot::find_package_root_file("DESCRIPTION"))[[1]] ) } \arguments{ \item{pkg}{\code{character} scalar. Name of the package. The default is to try to get the Package name automatically using the packages \strong{rprojroot} and \strong{desc}} } \value{ a \code{character} scalar containing \R code. } \description{ This gives you a minimal logger with no appenders that you can use inside your package under the name \code{lg} (e.g. lg$fatal("test")). \code{use_logger()} does not modify any files but only prints code for you to copy and paste. } \examples{ use_logger("testpkg") } lgr/man/LogEvent.Rd0000644000176200001440000001111715035407136013630 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/LogEvent.R \name{LogEvent} \alias{LogEvent} \alias{LogEvents} \title{LogEvents - The atomic unit of logging} \description{ A \code{LogEvent} is a single unit of data that should be logged. \code{LogEvents} are usually created by a \link{Logger}, and then processed by one more \link{Appenders}. They do not need to be instantiated manually except for testing and experimentation; however, if you plan on writing your own Appenders or Layouts you need to understand LogEvents. } \examples{ lg <- get_logger("test") lg$error("foo bar") # The last LogEvent produced by a Logger is stored in its `last_event` field lg$last_event # formatted console output lg$last_event$values # values stored in the event # Also contains the Logger that created it as .logger lg$last_event$logger # equivalent to lg$last_event$.logger$name # This is really a reference to the complete Logger, so the following is # possible (though nonsensical) lg$last_event$.logger$last_event$msg identical(lg, lg$last_event$.logger) lg$config(NULL) # reset logger config } \seealso{ \code{\link[=as.data.frame.LogEvent]{as.data.frame.LogEvent()}} Other docs relevant for extending lgr: \code{\link{as_LogEvent}()}, \code{\link{event_list}()}, \code{\link{standardize_threshold}()} } \concept{docs relevant for extending lgr} \section{Public fields}{ \if{html}{\out{
}} \describe{ \item{\code{level}}{\code{integer}. The \link{log_level} / priority of the LogEvent. Use the active binding \code{level_name} to get the \code{character} representation instead.} \item{\code{timestamp}}{\code{\link[base:DateTimeClasses]{POSIXct}}. The time when then the LogEvent was created.} \item{\code{caller}}{\code{character}. The name of the calling function.} \item{\code{msg}}{\code{character}. The log message.} \item{\code{.logger}}{\link{Logger}. A reference to the Logger that created the event (equivalent to \code{get_logger(event$logger)}).} \item{\code{rawMsg}}{\code{character}. The raw log message without string interpolation.} } \if{html}{\out{
}} } \section{Active bindings}{ \if{html}{\out{
}} \describe{ \item{\code{values}}{\code{list}. All values stored in the \code{LogEvent}, including all \emph{custom fields}, but not including \code{event$.logger}.} \item{\code{level_name}}{\code{character}. The \link{log_level} / priority of the LogEvent labelled according to \code{getOption("lgr.log_levels")}} \item{\code{logger}}{\code{character} scalar. The name of the Logger that created this event, equivalent to \code{event$.logger$name})} } \if{html}{\out{
}} } \section{Methods}{ \subsection{Public methods}{ \itemize{ \item \href{#method-LogEvent-new}{\code{LogEvent$new()}} \item \href{#method-LogEvent-clone}{\code{LogEvent$clone()}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-LogEvent-new}{}}} \subsection{Method \code{new()}}{ The arguments to \code{LogEvent$new()} directly translate to the fields stored in the \code{LogEvent}. Usually these values will be scalars, but (except for \code{"logger"}) they can also be vectors if they are all of the same length (or scalars that will be recycled). In this case the event will be treated by the \link{Appenders} and \link{Layouts} as if several separate events. \subsection{Usage}{ \if{html}{\out{
}}\preformatted{LogEvent$new( logger, level = 400, timestamp = Sys.time(), caller = NA, msg = NA, rawMsg = msg, ... )}\if{html}{\out{
}} } \subsection{Arguments}{ \if{html}{\out{
}} \describe{ \item{\code{logger, level, timestamp, caller, msg}}{see \strong{Public fields}.} \item{\code{...}}{All named arguments in \code{...} will be added to the LogEvent as \strong{custom fields}. You can store arbitrary \R objects in LogEvents this way, but not all Appenders will support them. See \link{AppenderJson} for} } \if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-LogEvent-clone}{}}} \subsection{Method \code{clone()}}{ The objects of this class are cloneable with this method. \subsection{Usage}{ \if{html}{\out{
}}\preformatted{LogEvent$clone(deep = FALSE)}\if{html}{\out{
}} } \subsection{Arguments}{ \if{html}{\out{
}} \describe{ \item{\code{deep}}{Whether to make a deep clone.} } \if{html}{\out{
}} } } } lgr/man/toString.LogEvent.Rd0000644000176200001440000000072414131760166015443 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/LogEvent.R \name{toString.LogEvent} \alias{toString.LogEvent} \title{Convert a LogEvent to a character string} \usage{ \method{toString}{LogEvent}(x, ...) } \arguments{ \item{x}{a \link{LogEvent}} \item{...}{ignored} } \value{ a \code{character} scalar } \description{ Convert a LogEvent to a character string } \examples{ toString(LogEvent$new(logger = lgr::lgr)) } lgr/man/AppenderFile.Rd0000644000176200001440000002240014643514316014443 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/Appender.R \name{AppenderFile} \alias{AppenderFile} \alias{AppenderJson} \title{Log to a file} \description{ A simple Appender that outputs to a file in the file system. If you plan to log to text files, consider logging to JSON files and take a look at \link{AppenderJson}, which is a shortcut for \code{AppenderFile} preconfigured with \code{\link{LayoutJson}}. } \examples{ lg <- get_logger("test") default <- tempfile() fancy <- tempfile() json <- tempfile() lg$add_appender(AppenderFile$new(default), "default") lg$add_appender( AppenderFile$new(fancy, layout = LayoutFormat$new("[\%t] \%c(): \%L \%m")), "fancy" ) lg$add_appender( AppenderFile$new(json, layout = LayoutJson$new()), "json" ) lg$info("A test message") readLines(default) readLines(fancy) readLines(json) # cleanup lg$config(NULL) unlink(default) unlink(fancy) unlink(json) tf <- tempfile() lg <- get_logger("test")$ set_appenders(AppenderJson$new(tf))$ set_propagate(FALSE) lg$info("A test message") lg$info("A test message \%s strings", "with format strings", and = "custom_fields") lg$appenders[[1]]$show() lg$appenders[[1]]$data # cleanup lg$config(NULL) unlink(tf) } \seealso{ \link{LayoutFormat}, \link{LayoutJson} \link{LayoutFormat}, \link{LayoutJson} Other Appenders: \code{\link{Appender}}, \code{\link{AppenderBuffer}}, \code{\link{AppenderConsole}}, \code{\link{AppenderFileRotating}}, \code{\link{AppenderFileRotatingDate}}, \code{\link{AppenderFileRotatingTime}}, \code{\link{AppenderTable}} Other Appenders: \code{\link{Appender}}, \code{\link{AppenderBuffer}}, \code{\link{AppenderConsole}}, \code{\link{AppenderFileRotating}}, \code{\link{AppenderFileRotatingDate}}, \code{\link{AppenderFileRotatingTime}}, \code{\link{AppenderTable}} } \concept{Appenders} \section{Super classes}{ \code{\link[lgr:Filterable]{lgr::Filterable}} -> \code{\link[lgr:Appender]{lgr::Appender}} -> \code{AppenderFile} } \section{Active bindings}{ \if{html}{\out{
}} \describe{ \item{\code{file}}{\code{character} scalar. path to the log file} \item{\code{data}}{\code{data.frame}. Contents of \code{file} parsed to a \code{data.frame} if used with a \link{Layout} that supports parsing of log file data (notably \link{LayoutJson}). Will throw an error if \code{Layout} does not support parsing.} \item{\code{data}}{\code{character} scalar. Like \verb{$data}, but returns a \code{data.table} instead (requires the \strong{data.table} package).} } \if{html}{\out{
}} } \section{Methods}{ \subsection{Public methods}{ \itemize{ \item \href{#method-AppenderFile-new}{\code{AppenderFile$new()}} \item \href{#method-AppenderFile-append}{\code{AppenderFile$append()}} \item \href{#method-AppenderFile-set_file}{\code{AppenderFile$set_file()}} \item \href{#method-AppenderFile-show}{\code{AppenderFile$show()}} } } \if{html}{\out{
Inherited methods
}} \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-AppenderFile-new}{}}} \subsection{Method \code{new()}}{ \subsection{Usage}{ \if{html}{\out{
}}\preformatted{AppenderFile$new( file, threshold = NA_integer_, layout = LayoutFormat$new(), filters = NULL )}\if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-AppenderFile-append}{}}} \subsection{Method \code{append()}}{ \subsection{Usage}{ \if{html}{\out{
}}\preformatted{AppenderFile$append(event)}\if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-AppenderFile-set_file}{}}} \subsection{Method \code{set_file()}}{ Set a log file \subsection{Usage}{ \if{html}{\out{
}}\preformatted{AppenderFile$set_file(file)}\if{html}{\out{
}} } \subsection{Arguments}{ \if{html}{\out{
}} \describe{ \item{\code{file}}{\code{character} scalar. Path to the log file. If \code{file} does not exist it will be created.} } \if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-AppenderFile-show}{}}} \subsection{Method \code{show()}}{ Display the contents of the log file. \subsection{Usage}{ \if{html}{\out{
}}\preformatted{AppenderFile$show(threshold = NA_integer_, n = 20L)}\if{html}{\out{
}} } \subsection{Arguments}{ \if{html}{\out{
}} \describe{ \item{\code{threshold}}{\code{character} or \code{integer} scalar. The minimum log level that should be displayed.} \item{\code{n}}{\code{integer} scalar. Show only the last \code{n} log entries that match \code{threshold}.} } \if{html}{\out{
}} } } } \section{Super classes}{ \code{\link[lgr:Filterable]{lgr::Filterable}} -> \code{\link[lgr:Appender]{lgr::Appender}} -> \code{\link[lgr:AppenderFile]{lgr::AppenderFile}} -> \code{AppenderJson} } \section{Methods}{ \subsection{Public methods}{ \itemize{ \item \href{#method-AppenderJson-new}{\code{AppenderJson$new()}} } } \if{html}{\out{
Inherited methods
}} \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-AppenderJson-new}{}}} \subsection{Method \code{new()}}{ \subsection{Usage}{ \if{html}{\out{
}}\preformatted{AppenderJson$new( file, threshold = NA_integer_, layout = LayoutJson$new(), filters = NULL )}\if{html}{\out{
}} } } } lgr/DESCRIPTION0000644000176200001440000000341515137120762012553 0ustar liggesusersType: Package Package: lgr Title: A Fully Featured Logging Framework Version: 0.5.2 Authors@R: person(given = "Stefan", family = "Fleck", role = c("aut", "cre"), email = "stefan.b.fleck@gmail.com", comment = c(ORCID = "0000-0003-3344-9851")) Maintainer: Stefan Fleck Description: A flexible, light-weight logging framework based on 'R6' classes. It supports hierarchical loggers, structured logging, output to plaintext, 'JSON', (rotating) files, and memory buffers. Additional appenders for 'Elasticsearch', 'Dynatrace', databases, email and push notifications, and more are available in the 'lgrExtra' package. License: MIT + file LICENSE URL: https://s-fleck.github.io/lgr/ BugReports: https://github.com/s-fleck/lgr/issues/ Depends: R (>= 3.2.0) Imports: R6 (>= 2.4.0) Suggests: cli, covr, crayon, data.table, desc, future, future.apply, glue, jsonlite, knitr, rmarkdown, rotor (>= 0.3.5), rprojroot, testthat, tibble, tools, utils, whoami, yaml VignetteBuilder: knitr Encoding: UTF-8 RoxygenNote: 7.3.3 Collate: 'Filterable.R' 'utils-sfmisc.R' 'utils.R' 'Appender.R' 'Filter.R' 'log_levels.R' 'LogEvent.R' 'Layout.R' 'LayoutJson.R' 'Logger.R' 'basic_config.R' 'default_functions.R' 'event_list.R' 'get_logger.R' 'lgr-package.R' 'logger_config.R' 'logger_index.R' 'logger_tree.R' 'read_json_lines.R' 'simple_logging.R' 'string_repr.R' 'use_logger.R' 'utils-formatting.R' 'utils-logging.R' 'utils-rd.R' 'utils-rotor.R' NeedsCompilation: no Packaged: 2026-01-30 11:46:52 UTC; stefan.fleck Author: Stefan Fleck [aut, cre] (ORCID: ) Repository: CRAN Date/Publication: 2026-01-30 12:20:02 UTC