s-fleck/lgr

logger_tree merges different loggers with same "basename"

Fuco1 opened this issue · 5 comments

Fuco1 commented

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?

Fuco1 commented

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 just envir = 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 just loggers() but that name was already taken by the environment for storing loggers.
  • The weird for loop with the rbind() in logger_tree was mainly because of the node structure required for cli::tree(). Because of the "flatter" structure for logger_index() we could implement it cleaner with a lapply() and without having to initialize an empty data.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.

Fuco1 commented

I think I'm already using a "git" version with renv::install so I'll just fetch the latest commit. Thanks!