Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
0c2313b
feat: replace logger with lightweight console logging + ZIP export
Gero1999 Apr 9, 2026
1a1afd3
Merge remote-tracking branch 'origin/main' into 1210-enhancement/repl…
Gero1999 Apr 14, 2026
41b9403
feat: add log-points for NCA settings changes
Gero1999 Apr 14, 2026
5922af8
feat: add debug log-point for selected parameter names
Gero1999 Apr 14, 2026
7ce0384
feat: add log-points for slope selector user actions
Gero1999 Apr 14, 2026
ef5b7de
feat: add log-points for general exclusion changes
Gero1999 Apr 14, 2026
4a4b92d
feat: add log-points for profile selection and data imputation
Gero1999 Apr 14, 2026
91ca90b
fix: use paste-style logging to avoid glue parent.frame issues
Gero1999 Apr 14, 2026
47f533b
feat: add log-points for NCA settings changes
Gero1999 Apr 14, 2026
550e0bb
feat: add debug log-point for selected parameter names
Gero1999 Apr 14, 2026
b378504
feat: add log-points for slope selector user actions
Gero1999 Apr 14, 2026
1227387
feat: add log-points for general exclusion changes
Gero1999 Apr 14, 2026
dacc0c2
feat: add log-points for profile selection and data imputation
Gero1999 Apr 14, 2026
893084b
fix: use paste-style logging to avoid glue parent.frame issues
Gero1999 Apr 14, 2026
0eb099f
feat: add header block to exported session_log.txt
Gero1999 Apr 14, 2026
a121573
docs: add session log reference vignette
Gero1999 Apr 14, 2026
6400c05
docs: add session log page to pkgdown navbar
Gero1999 Apr 14, 2026
f62eaf2
docs: add scope and reference link to logging.R header
Gero1999 Apr 14, 2026
2f0b9d8
Merge branch '1219-enhancement/nca-input-log-points' of https://githu…
Gero1999 Apr 14, 2026
c686fc5
Merge pull request #1222 from pharmaverse/1220-enhancement/document-s…
Gero1999 Apr 16, 2026
b26843b
refactor lintr
Gero1999 Apr 20, 2026
2a347e1
refactor: unify slope selector logging into single table-change message
Gero1999 Apr 20, 2026
1cb6244
fix: address review feedback on log-points
Gero1999 Apr 23, 2026
07a2d6e
fix: consistent paste-style logging and init guards
Gero1999 Apr 24, 2026
0d78d95
fix: suppress slope log on programmatic updates
Gero1999 Apr 27, 2026
bc1bc80
Merge pull request #1221 from pharmaverse/1219-enhancement/nca-input-…
Gero1999 Apr 29, 2026
db751f7
Merge origin/main into 1210-enhancement/replace-logger-with-console
Gero1999 Apr 29, 2026
c0fe8c3
fix: remove stale logger import and dead sub-module code
Gero1999 Apr 30, 2026
08a7117
Merge main into branch
Gero1999 May 4, 2026
a8fcdc4
Merge accept incoming with req() in manual_slopes_table.R and changin…
Gero1999 May 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion DESCRIPTION
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ Imports:
glue,
htmltools,
htmlwidgets,
logger,
magrittr,
PKNCA (>= 0.12.1),
plotly (>= 4.11.0),
Expand Down
2 changes: 1 addition & 1 deletion NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ importFrom(glue,glue)
importFrom(grid,convertUnit)
importFrom(htmltools,tags)
importFrom(htmlwidgets,onRender)
importFrom(logger,log_info)

importFrom(magrittr,`%>%`)
importFrom(plotly,add_lines)
importFrom(plotly,add_trace)
Expand Down
1 change: 0 additions & 1 deletion R/imports-shiny.R
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
#' @importFrom bslib page_sidebar
#' @importFrom htmltools tags
#' @importFrom htmlwidgets onRender
#' @importFrom logger log_info
#' @importFrom reactable reactable
#' @importFrom reactable.extras reactable_extras_dependency
#' @importFrom shiny runApp
Expand Down
1 change: 0 additions & 1 deletion inst/shiny/app.R
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ require(aNCA)
require(bslib)
require(dplyr)
require(htmlwidgets)
require(logger)
require(formatters)
require(magrittr)
require(plotly)
Expand Down
84 changes: 84 additions & 0 deletions inst/shiny/functions/logging.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# Lightweight logging system for the aNCA Shiny app.
#
# Replaces the `logger` package with console-only output and in-memory
# log capture for ZIP export. Supports glue-style interpolation and
# paste-style multi-argument calls.
#
# Log levels: TRACE < DEBUG < INFO < SUCCESS < WARN < ERROR
# Default threshold: INFO (configurable via aNCA_LOG_LEVEL env var).
#
# The log captures application-level events only — not raw R console
# output. Warnings and errors from third-party packages appear only
# when explicitly caught by tryCatch blocks with log_warn/log_error.
#
# The in-memory buffer is exported as session_log.txt in the ZIP
# download. For a full reference of logged events, see:
# https://pharmaverse.github.io/aNCA/articles/session_log.html

.log_env <- new.env(parent = emptyenv())
.log_env$threshold <- "INFO"
.log_env$buffer <- character(0)

.LOG_LEVELS <- c(TRACE = 1L, DEBUG = 2L, INFO = 3L, SUCCESS = 4L, WARN = 5L, ERROR = 6L)

#' Initialise the logging system.
#'
#' Reads the threshold from the `aNCA_LOG_LEVEL` environment variable
#' (default `"INFO"`) and clears the in-memory log buffer.
setup_logger <- function() {
level <- toupper(Sys.getenv("aNCA_LOG_LEVEL", "INFO"))
if (!level %in% names(.LOG_LEVELS)) level <- "INFO"
.log_env$threshold <- level
.log_env$buffer <- character(0)
}

#' Core logging function.
#' @param level Character: one of the log level names.
#' @param ... Message parts. If the first argument contains `{`, it is
#' evaluated as a glue string in the caller's environment. Otherwise
#' all arguments are pasted together.
#' @noRd
.log_msg <- function(level, ...) {
if (.LOG_LEVELS[[level]] < .LOG_LEVELS[[.log_env$threshold]]) return(invisible(NULL))

args <- list(...)
if (length(args) == 0L) {
msg <- ""
} else if (length(args) == 1L && grepl("\\{", args[[1]])) {
msg <- tryCatch(
glue::glue(args[[1]], .envir = parent.frame(2)),
error = function(e) paste0(args, collapse = "")
)
} else {
msg <- paste0(args, collapse = "")
}

timestamp <- format(Sys.time(), "%Y-%m-%d %H:%M:%S")
line <- paste0("[", timestamp, "] ", level, ": ", msg)

.log_env$buffer <- c(.log_env$buffer, line)
message(line)
invisible(NULL)
}

# Public log functions matching the logger API
log_trace <- function(...) .log_msg("TRACE", ...)
log_debug <- function(...) .log_msg("DEBUG", ...)
log_info <- function(...) .log_msg("INFO", ...)
log_success <- function(...) .log_msg("SUCCESS", ...)
log_warn <- function(...) .log_msg("WARN", ...)
log_error <- function(...) .log_msg("ERROR", ...)

#' Logs a list or data frame at DEBUG level.
#'
#' @param title Title for the log entry.
#' @param l List or data.frame to log.
log_debug_list <- function(title, l) {
log_debug(aNCA:::.concatenate_list(title, l))
}

#' Return the in-memory log buffer as a character vector.
#' @noRd
get_log_buffer <- function() {
.log_env$buffer
}
13 changes: 12 additions & 1 deletion inst/shiny/functions/utils-exclusions.R
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,12 @@ EXCL_COLOR_PARAM <- "#FFF3CD" # yellow — parameter exclusion
exclusion_list(append(current, list_new_reason))
updateTextInput(session, "exclusion_reason", value = "")
updateReactable(table_id, selected = NA)

type_label <- .exclusion_type_label(nca_checked, tlg_checked)
log_info(
"Exclusion added: ", length(rows_sel), " rows, type=", type_label,
", reason='", reason, "'"
)
}
}

Expand Down Expand Up @@ -211,7 +217,8 @@ EXCL_COLOR_PARAM <- "#FFF3CD" # yellow — parameter exclusion
#' @param registered_ids reactiveVal holding character vector of registered IDs.
#' @param input Shiny input object.
#' @noRd
.register_remove_observers <- function(exclusion_list, registered_ids, input) {
.register_remove_observers <- function(exclusion_list, registered_ids, input,
on_remove = NULL) {
lst <- exclusion_list()
already <- registered_ids()
new_ids <- setdiff(
Expand All @@ -223,6 +230,10 @@ EXCL_COLOR_PARAM <- "#FFF3CD" # yellow — parameter exclusion
local_id <- xbtn_id
observeEvent(input[[local_id]], {
current <- exclusion_list()
removed <- Filter(function(x) x$xbtn_id == local_id, current)
if (is.function(on_remove) && length(removed) > 0) {
on_remove(removed[[1]])
}
exclusion_list(Filter(function(x) x$xbtn_id != local_id, current))
}, ignoreInit = TRUE, once = TRUE)
})
Expand Down
42 changes: 0 additions & 42 deletions inst/shiny/functions/utils.R
Original file line number Diff line number Diff line change
@@ -1,45 +1,3 @@
#' Sets up the logger package for the application.
#'
#' @details
#' The application logs everything to a log file located in `/log` directory. If such folder
#' does not exist, it will be created. Logfile for each session will be separate. The application
#' will keep 5 log files at any given time - if this number is exceeded, the oldest log file
#' will be deleted.
#'
#' In addition, information of the level specified by the user will be logged to console.
#' As a default, this level is INFO - this is so that the user has good information on what is
#' happening inside the app, but is not overwhelmed with tracing and debugging information. This
#' level can be changed using `aNCA_LOG_LEVEL` environmental variable, set for example in
#' `.Renviron` file.
setup_logger <- function() {
log_layout(layout_glue_colors)
log_formatter(formatter_glue)
log_threshold(TRACE)
log_threshold(Sys.getenv("aNCA_LOG_LEVEL", "INFO"), index = 2)

log_dir <- "./log"
if (!dir.exists(log_dir)) dir.create(log_dir)
existing_logs <- list.files(log_dir, full.names = TRUE)
if (length(existing_logs) >= 5) file.remove(sort(existing_logs)[1]) # keep only five log files
logfile_name <- paste0(log_dir, "/aNCA_app_", format(Sys.time(), "%y%m%d-%H%M%S-"), ".log")

log_appender(appender_file(logfile_name))
log_appender(appender_console, index = 2)
}

#' Logs a list and data frame objects.
#'
#' @details
#' Utilitary function for logging a list object (like mapping list or used settings) in
#' a nice format. Parses a list into nice string and logs at DEBUG level. Can also process
#' data frames, which will be converted into a list of rows.
#'
#' @param title Title for the logs.
#' @param l List object to be parsed into log. Can also be a data.frame.
log_debug_list <- function(title, l) {
log_debug(aNCA:::.concatenate_list(title, l))
}

#' Needed to properly reset reactable.extras widgets
#'
#' @details
Expand Down
49 changes: 49 additions & 0 deletions inst/shiny/functions/zip-utils.R
Original file line number Diff line number Diff line change
Expand Up @@ -454,6 +454,12 @@ prepare_export_files <- function(target_dir,
detail = "Saving session info...")
.export_session_info(target_dir)
}

if ("session_log" %in% input$res_tree) {
progress$set(message = "Creating exports...",
detail = "Saving session log...")
.export_session_log(target_dir)
}
progress$inc(0.8)

.clean_export_dir(target_dir, input, custom_names)
Expand Down Expand Up @@ -685,6 +691,45 @@ prepare_export_files <- function(target_dir,
writeLines(lines, file.path(target_dir, "session_info.txt"))
}

#' Export the in-memory session log to a text file.
#' @param target_dir Target directory for the export.
#' @keywords internal
.export_session_log <- function(target_dir) {
log_buffer <- get_log_buffer()

threshold <- .log_env$threshold
header <- c(
"# aNCA Session Log",
"#",
"# This file contains application events captured during your session.",
"# It records data upload, mapping, NCA settings, parameter selection,",
"# slope adjustments, exclusions, calculation results, and exports.",
"#",
"# Warnings and errors from these operations are included when caught",
"# by the application. Unexpected R errors or warnings from third-party",
"# packages that are not explicitly handled will NOT appear here.",
"#",
paste0("# Log level: ", threshold,
" (configurable via aNCA_LOG_LEVEL env var)"),
paste0("# Levels shown at current threshold: ",
paste(names(.LOG_LEVELS)[.LOG_LEVELS >= .LOG_LEVELS[[threshold]]],
collapse = ", ")),
"#",
"# For a full reference of logged events, see:",
"# https://pharmaverse.github.io/aNCA/articles/session_log.html",
"#",
paste0("# Generated: ", format(Sys.time(), "%Y-%m-%d %H:%M:%S %Z")),
"# -------------------------------------------------------------------",
""
)

if (length(log_buffer) == 0L) {
log_buffer <- "(No log entries captured during this session.)"
}

writeLines(c(header, log_buffer), file.path(target_dir, "session_log.txt"))
}

#' Clean Export Directory
#' @param target_dir Target directory to clean
#' @param input Shiny input object
Expand Down Expand Up @@ -722,6 +767,10 @@ prepare_export_files <- function(target_dir,
files_req <- c(files_req, grep("session_info\\.txt$", all_files,
value = TRUE))
}
if ("session_log" %in% fnames) {
files_req <- c(files_req, grep("session_log\\.txt$", all_files,
value = TRUE))
}
file.remove(all_files[!all_files %in% files_req])

# Recursive directory cleanup — remove dirs that contain no files at any depth
Expand Down
9 changes: 9 additions & 0 deletions inst/shiny/modules/tab_nca/setup/data_imputation.R
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,15 @@ data_imputation_server <- function(id, settings_override) {
}
})

observeEvent(input$select_blq_strategy, {
log_info("BLQ imputation strategy changed: ", input$select_blq_strategy)
}, ignoreInit = TRUE)

observeEvent(input$should_impute_c0, {
state <- if (isTRUE(input$should_impute_c0)) "enabled" else "disabled"
log_info("Start concentration imputation ", state)
}, ignoreInit = TRUE)

blq_imputation_rule <- reactive({
req(input$select_blq_strategy)
rule_list <- switch(input$select_blq_strategy,
Expand Down
8 changes: 7 additions & 1 deletion inst/shiny/modules/tab_nca/setup/general_exclusions.R
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ general_exclusions_server <- function(

xbtn_counter(max(new_ids))
exclusion_list(rehydrated_list)
log_info("Exclusions restored from settings: ", length(overrides), " rules loaded")
}
})

Expand Down Expand Up @@ -166,7 +167,12 @@ general_exclusions_server <- function(

# Register observers for new remove buttons (shared helper)
observe({
.register_remove_observers(exclusion_list, registered_xbtns, input)
.register_remove_observers(
exclusion_list, registered_xbtns, input,
on_remove = function(item) {
log_info("Exclusion removed: reason='", item$reason, "'")
}
)
})

# Render the exclusions table (not shown if empty)
Expand Down
6 changes: 3 additions & 3 deletions inst/shiny/modules/tab_nca/setup/manual_slopes_table.R
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ manual_slopes_table_server <- function(

# Add a new row to the table when the user clicks the add button
observeEvent(input$add_rule, {
log_trace("{id}: adding manual slopes row")
log_trace("Slope selector: adding manual slope rule")
first_group <- slopes_pknca_groups()[1, ]
time_col <- pknca_data()$conc$columns$time
new_row <- cbind(
Expand Down Expand Up @@ -121,7 +121,7 @@ manual_slopes_table_server <- function(

# Remove selected rows from the table when the user clicks the remove button
observeEvent(input$remove_rule, {
log_trace("{id}: removing manual slopes row")
log_trace("Slope selector: removing manual slope rule")
req(manual_slopes())
selected <- getReactableState("manual_slopes", "selected")
req(selected)
Expand All @@ -135,7 +135,7 @@ manual_slopes_table_server <- function(
# Render the manual slopes table (reactable)
output$manual_slopes <- renderReactable({
req(manual_slopes())
log_trace("{id}: rendering slope edit data table")
log_trace(id, ": rendering slope edit data table")
isolate({
data <- manual_slopes()
})
Expand Down
12 changes: 12 additions & 0 deletions inst/shiny/modules/tab_nca/setup/parameter_selection.R
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,18 @@ parameter_selection_server <- function(id, processed_pknca_data, parameter_overr
)
})

# Log parameter selection changes (debounced to avoid noise from rapid clicks)
selections_debounced <- debounce(selections_state, 2000)
observeEvent(selections_debounced(), {
state <- selections_debounced()
req(state)
study_types <- study_types_list()
for (st in study_types) {
n_params <- sum(state[[st]], na.rm = TRUE)
log_info("Parameter selection for '", st, "': ", n_params, " parameters selected.")
}
}, ignoreInit = TRUE)

observeEvent(input$clear_all, {
state <- selections_state()
req(state)
Expand Down
Loading
Loading