logger_tree merges different loggers with same "basename"
Fuco1 opened this issue · 5 comments
Let me illustrate with an example:
> lgr::get_logger("app/backend/api")
> lgr::get_logger("app/frontend/api")
> x <- lgr::logger_tree()
> x
root [info] -> 1 appender
└─app
├─backend
│ └─api
└─frontend
└─api
> x %>% as_tibble
parent children configured threshold threshold_inherited propagate n_appenders
<chr> <I<list>> <lgl> <int> <lgl> <lgl> <int>
1 root <chr [2]> TRUE 400 FALSE TRUE 1
2 app <chr [2]> FALSE 400 TRUE TRUE 0
3 backend <chr [1]> FALSE 400 TRUE TRUE 0
4 api <NULL> FALSE 400 TRUE TRUE 0
5 frontend <chr [1]> FALSE 400 TRUE TRUE 0
Now x
only contains one row with api
logger even tough there are two of them.
I'm building an interactive Shiny application for debugging of our R software and I want to have a dynamic control there with all the loggers and give the user the ability to change log-levels of various components (the logs are rendered in the shiny app). The logger_tree
function looked like what I'd like, but it doesn't return complete data.
So far I've figured I can use ls(envir = lgr:::loggers)
but this uses "private" access to the package which I'd rather avoid if possible.
I'd fix the function myself but I'm not sure what the expected solution is. Should we disambiguate similarly named loggers by the "shortest unique prefix" for example? Maybe inverting the relation, storing a child and its (single) parent in another column. This way the key would be the combination of (child, parent)
. Right now for two leaf loggers with the same name the children
column is NULL
and there's no way to know which one is which.
Maybe adding another function for introspection into existing loggers if you care about backward compatibility on this one.
Thanks!
The structure of the data.frame
is determined by the input that cli::tree()
expects (+ a few extra columns) . See lgr:::format.logger_tree
for details.
I'm not really sure what the best way is right now. Probably a new exported function that returns a "flatter" data.frame
with the qualified logger name (eg. app/backend/api
) would really be more sensible than one that transforms the somewhat convoluted logger_tree
structure (maybe logger_summary()
, logger_toc()
, logger_index()
?).
something like this would probably just be a simplified version of the logger_tree()
function, but right now I sadly have little time to work on my R packages... do you think you could cook something like this up?
I came up with this:
logger_ls <- function() {
## using lgr::: since I don't know how to work with packages :(
names <- ls(envir = lgr:::loggers)
## Is this necessary to initialize an empty data frame?
res <- data.frame(
name = c(),
configured = c(),
threshold = c(),
threshold_inherited = c(),
propagate = c(),
n_appenders = c(),
stringsAsFactors = FALSE
)
for (logger_name in names) {
cur_logger <- get_logger(logger_name)
res <- rbind(
res,
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)
)
)
}
## This is probably also unnecessary since there's no special formatting or anything.
structure(
res,
class = union("logger_ls", class(res))
)
}
The output is very similar to the logger_tree except it only contains the name instead of parent and children. This is the output from our application.
name configured threshold threshold_inherited propagate n_appenders
1 root TRUE 400 FALSE TRUE 1
2 ydistri TRUE 400 FALSE FALSE 1
3 ydistri/cutoff FALSE 400 TRUE TRUE 0
4 ydistri/db FALSE 400 TRUE TRUE 0
5 ydistri/expert FALSE 400 TRUE TRUE 0
6 ydistri/features TRUE 400 FALSE TRUE 0
7 ydistri/features/ratio TRUE 600 FALSE TRUE 0
8 ydistri/forecast TRUE 400 FALSE TRUE 0
9 ydistri/graphics FALSE 400 TRUE TRUE 0
10 ydistri/ratio FALSE 400 TRUE TRUE 0
11 ydistri/seasonality TRUE 400 FALSE TRUE 0
12 ydistri/sink FALSE 400 TRUE TRUE 0
13 ydistri/source FALSE 400 TRUE TRUE 0
14 ydistri/source/stub FALSE 400 TRUE TRUE 0
15 ydistri/stub FALSE 400 TRUE TRUE 0
16 ydistri/yd_yearly FALSE 400 TRUE TRUE 0
17 ydistri/yd_yearly/outliers FALSE 400 TRUE TRUE 0
18 ydistri/yd_yearly/superseason FALSE 400 TRUE TRUE 0
If I place this into its own file and just add it to the package, will it work or is there something special I need to do? I'm completely ignorant about R package development.
Looks good! I'll add it to the package myself with a few modifications and documentation over the weekend!
to answer your questions:
- you don't need
:::
for unexported objects in the package itself (so justenvir = loggers
would have been fine) - you can just add regular functions to a package, but you need to add a special roxygen comment before the function to export it (= make it available to someone loading the package with)
'# @export
.
btw, making packages is really not that hard! There's a great free ebook by hadley on it if you want to learn more: https://r-pkgs.org/
Rewrote the code a bit and added documentation:
Some notes:
- I decided for
logger_index()
as the name. initially I wanted to go for justloggers()
but that name was already taken by the environment for storing loggers. - The weird
for
loop with therbind()
inlogger_tree
was mainly because of the node structure required forcli::tree()
. Because of the "flatter" structure forlogger_index()
we could implement it cleaner with alapply()
and without having to initialize an emptydata.frame
at the beginning of the function. - assigning an
s3
class at the end of the function does little, but it also does not hurt
Tell me if you encounter any problems. Might be a while till the next lgr version hits CRAN though, i hope that is not an issue.
I think I'm already using a "git" version with renv::install
so I'll just fetch the latest commit. Thanks!