Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
26 changes: 13 additions & 13 deletions DESCRIPTION
Original file line number Diff line number Diff line change
Expand Up @@ -26,22 +26,33 @@ Description: An interactive 'shiny' application for performing non-compartmental
Noncompartmental Analysis: The PKNCA R Package" <doi:10.1007/s10928-015-9432-2>.
License: Apache License (>= 2)
Imports:
bslib,
dplyr,
formatters,
ggplot2,
glue,
htmltools,
htmlwidgets,
magrittr,
PKNCA (>= 0.12.1),
plotly (>= 4.11.0),
purrr,
reactable,
reactable.extras,
rlang,
shiny,
shinycssloaders,
shinyjs,
shinyjqui,
shinyWidgets,
stats,
tern,
tidyr,
units,
utils,
writexl,
yaml
yaml,
zip
Encoding: UTF-8
Roxygen: list(markdown = TRUE)
RoxygenNote: 7.3.3
Expand All @@ -52,8 +63,6 @@ Suggests:
flextable,
ggh4x,
haven,
htmltools,
htmlwidgets,
jsonlite,
knitr,
lintr (>= 3.2.0),
Expand All @@ -64,25 +73,16 @@ Suggests:
officer,
pak,
readxl,
reactable,
reactable.extras,
rlistings,
rmarkdown,
sass,
scales,
shiny,
shinycssloaders,
shinyjs,
shinyjqui,
shinytest2,
shinyWidgets,
stringi,
testthat (>= 3.0.0),
tools,
quarto,
vdiffr,
withr,
zip
withr
Config/testthat/edition: 3
Language: en-US
URL: https://pharmaverse.github.io/aNCA/, https://github.com/pharmaverse/aNCA
Expand Down
49 changes: 15 additions & 34 deletions R/run_app.R
Original file line number Diff line number Diff line change
Expand Up @@ -86,43 +86,24 @@ run_app <- function(datapath = NULL, settings = NULL,
}

#' Check if all dependencies required to run shiny application are installed.
#' If not, install them.
#' This list of packages should also be provided as `Suggests` in the DESCRIPTION file.
#'
#' Reads the Imports field from DESCRIPTION and verifies each package is
#' available. This keeps the check in sync with DESCRIPTION automatically.
#' @noRd
check_app_dependencies <- function() {
deps <- c(
"bslib",
"dplyr",
"htmlwidgets",
"logger",
"formatters",
"magrittr",
"plotly",
"purrr",
"reactable",
"reactable.extras",
"shiny",
"shinycssloaders",
"shinyjs",
"shinyjqui",
"shinyWidgets",
"stats",
"stringi",
"tidyr",
"tools",
"utils",
"rlang",
"yaml"
)
desc <- read.dcf(system.file("DESCRIPTION", package = "aNCA"), fields = "Imports")
deps <- trimws(unlist(strsplit(desc[1, "Imports"], ",")))
deps <- gsub("\\s*\\(.*\\)", "", deps) # strip version constraints
deps <- deps[nzchar(deps)]

missing_packages <- purrr::keep(deps, function(dep) !requireNamespace(dep, quietly = TRUE))
missing_packages <- deps[!vapply(deps, requireNamespace, logical(1), quietly = TRUE)]

if (length(missing_packages) != 0) {
stop(paste0(
"Some packages required for Shiny application are missing. ",
"You can install them by running `install.packages(c(",
paste0("'", missing_packages, "'", collapse = ", "),
"))`"
))
if (length(missing_packages) > 0) {
stop(
"Some packages required for the Shiny application are missing. ",
"You can install them by running:\n",
" install.packages(c(", paste0("'", missing_packages, "'", collapse = ", "), "))",
call. = FALSE
)
}
}
3 changes: 1 addition & 2 deletions 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 All @@ -16,7 +15,7 @@ require(shinyjs)
require(shinyjqui)
require(shinyWidgets)
require(stats)
require(stringi)

require(tidyr)
require(tools)
require(utils)
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
}
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 @@ -428,6 +428,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 @@ -640,6 +646,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 @@ -677,6 +722,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
5 changes: 3 additions & 2 deletions inst/shiny/modules/tab_nca/zip.R
Original file line number Diff line number Diff line change
Expand Up @@ -678,7 +678,7 @@ zip_server <- function(id, res_nca, adnca_data, settings, grouping_vars) {
}
items$extras <- TREE_LIST$extras
} else {
items$extras <- TREE_LIST$extras[c("settings_file", "session_info")]
items$extras <- TREE_LIST$extras[c("settings_file", "session_info", "session_log")]
}

items
Expand Down Expand Up @@ -713,7 +713,8 @@ TREE_LIST <- list(
results_slides = "",
r_script = "",
settings_file = "",
session_info = ""
session_info = "",
session_log = ""
)
)

Expand Down
12 changes: 10 additions & 2 deletions inst/shiny/modules/tab_tlg.R
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,11 @@ tab_tlg_server <- function(id, data) {
panels <- lapply(tlg_order_graphs, function(g_id) {
graph_ui <- {
g_def <- .TLG_DEFINITIONS[[g_id]]
module_id <- paste0(g_id, stringi::stri_rand_strings(1, 5))
module_id <- paste0(
g_id,
paste0(sample(c(letters, 0:9), 5, replace = TRUE),
collapse = "")
)

if (exists(g_def$fun)) {
tlg_module_server(module_id, data, "graph", get(g_def$fun), g_def$options)
Expand Down Expand Up @@ -288,7 +292,11 @@ tab_tlg_server <- function(id, data) {
panels <- lapply(tlg_order_listings, function(g_id) {
list_ui <- {
g_def <- .TLG_DEFINITIONS[[g_id]]
module_id <- paste0(g_id, stringi::stri_rand_strings(1, 5))
module_id <- paste0(
g_id,
paste0(sample(c(letters, 0:9), 5, replace = TRUE),
collapse = "")
)

if (exists(g_def$fun)) {
tlg_module_server(module_id, data, "listing", get(g_def$fun), g_def$options)
Expand Down
Loading
Loading