logtree renders nested process execution as a live, colored tree in the console – tree connectors, status glyphs, and elapsed time per step – while keeping nesting depth correct even when a step errors partway through.
Installation
logtree isn’t on CRAN yet. Install the development version from GitHub:
# install.packages("pak") pak::pak("IvanSortino/logtree")
Once released to CRAN:
install.packages("logtree")Quick start
library(logtree) pipeline <- function() { log_step("Pipeline") load_config() } load_config <- function() { log_step("Load config") log_info("Reading config.yml") log_success("Validated 12 parameters") } pipeline() #> ▶ Pipeline #> ├─ ▶ Load config #> │ ├─ ℹ Reading config.yml #> │ ├─ ✔ Validated 12 parameters #> │ └─ ✔ Done 0.00s #> └─ ✔ Done 0.00s
log_step() is meant to be called from inside a function: the step
auto-closes when the function that opened it returns – normally, early,
or via an uncaught error – so nesting depth never gets stuck out of
sync. (At top level, with no function frame to close on, reach for
log_open() / log_close() instead.)
Status levels & verbosity
Five leaf levels – log_debug(), log_info(), log_success(),
log_warn(), log_error() – plus logtree_threshold() to filter them.
log_warn()/ log_error() also elevate the enclosing step’s glyph,
even when suppressed by verbosity; step lines always render regardless
of threshold.
fetch <- function() { log_step("Fetch") log_debug("cache miss for key user:42") log_info("requesting from API") log_warn("rate limit at 80%") log_success("fetched 128 rows") } with_logging(fetch(), summary = FALSE) # default verbosity ("info"): debug hidden #> ▶ Fetch #> ├─ ℹ requesting from API #> ├─ ⚠ rate limit at 80% #> ├─ ✔ fetched 128 rows #> └─ ⚠ Done 0.00s logtree_threshold("debug") with_logging(fetch(), summary = FALSE) # verbosity raised: debug shown #> ▶ Fetch #> ├─ ⚙ cache miss for key user:42 #> ├─ ℹ requesting from API #> ├─ ⚠ rate limit at 80% #> ├─ ✔ fetched 128 rows #> └─ ⚠ Done 0.00s logtree_threshold("info")
Error handling
log_error() from code that itself returns normally elevates the
enclosing step’s glyph but lets the run continue – pass
status = "success" to log_close() once you know recovery actually
worked:
connect_db <- function() { log_step("Connect primary") log_error("primary unreachable") log_info("failing over to replica") log_success("connected to replica") log_close(status = "success") # recovered: override the elevated glyph } with_logging(connect_db(), summary = FALSE) #> ▶ Connect primary #> ├─ ✖ primary unreachable #> ├─ ℹ failing over to replica #> ├─ ✔ connected to replica #> └─ ✔ Done 0.00s
A step whose code actually throws is different: with_logging() marks
every currently-open step failed, logs the condition as a leaf, prints a
run summary, then rethrows – it never silently swallows errors.
apply_migration <- function() { log_step("Apply migration") log_info("adding column users.tier") stop("constraint violation on users.email") } try(with_logging(apply_migration()), silent = TRUE) #> ▶ Apply migration #> ├─ ℹ adding column users.tier #> ├─ ✖ constraint violation on users.email #> └─ ✖ Done 0.00s #> ✖ Run failed in 0.00s
Run summary
When a run ends, logtree_summary() prints a breadcrumb digest of every
error, warning, and pinned leaf since the last reset – so you see what
went wrong without scrolling back through the tree. filter restricts
by status; depth trims each breadcrumb to its N deepest nodes.
logtree_reset() migrate <- function() { log_step("Apply migration") log_warn("table lock held 800ms") log_error("constraint violation on users.email") } release <- function() { log_step("Release v2.1") migrate() log_step("Smoke test") log_success("all endpoints 200") } with_logging(release(), summary = FALSE) #> ▶ Release v2.1 #> ├─ ▶ Apply migration #> │ ├─ ⚠ table lock held 800ms #> │ ├─ ✖ constraint violation on users.email #> │ └─ ✖ Done 0.00s #> ├─ ▶ Smoke test #> │ ├─ ✔ all endpoints 200 #> │ └─ ✔ Done 0.00s #> └─ ✔ Done 0.00s logtree_summary() #> Summary: 1 error, 1 warning #> ⚠ Release v2.1 > Apply migration > table lock held 800ms #> ✖ Release v2.1 > Apply migration > constraint violation on users.email
Grouping
Adjacent log_step() calls that share a group = c(name = value) value
collapse under one < name > header instead of stacking as siblings:
check <- function(item, label) { log_step(label, group = stats::setNames(item, paste0("Item ", item))) log_info(paste0(label, " running")) log_success(paste0(label, " ok")) } process_item <- function(item) { check(item, "validate schema") check(item, "check bounds") } run_pipeline <- function() { log_step("Pipeline run") for (i in 1:2) process_item(i) } with_logging(run_pipeline(), summary = FALSE) #> ▶ Pipeline run #> ├─ ▣ Item 1 #> │ ├─ ▶ validate schema #> │ │ ├─ ℹ validate schema running #> │ │ ├─ ✔ validate schema ok #> │ │ └─ ✔ Done 0.00s #> │ ├─ ▶ check bounds #> │ │ ├─ ℹ check bounds running #> │ │ ├─ ✔ check bounds ok #> │ │ └─ ✔ Done 0.00s #> │ └─ ✔ Done 0.00s #> ├─ ▣ Item 2 #> │ ├─ ▶ validate schema #> │ │ ├─ ℹ validate schema running #> │ │ ├─ ✔ validate schema ok #> │ │ └─ ✔ Done 0.00s #> │ ├─ ▶ check bounds #> │ │ ├─ ℹ check bounds running #> │ │ ├─ ✔ check bounds ok #> │ │ └─ ✔ Done 0.00s #> │ └─ ✔ Done 0.00s #> └─ ✔ Done 0.00s
Themes
logtree_theme() swaps the whole glyph/color preset ("unicode",
"ascii", "emoji") or merges per-glyph overrides onto the active one:
demo_build <- function() { with_logging({ log_step("Build") log_info("compiling") log_warn("3 deprecation warnings") log_success("build ok") }, summary = FALSE) } logtree_theme("ascii") demo_build() #> > Build #> |- i compiling #> |- ! 3 deprecation warnings #> |- + build ok #> |- ! Done 0.00s logtree_theme("emoji") demo_build() #> 🔹 Build #> ├─ 💡 compiling #> ├─ ⚠️ 3 deprecation warnings #> ├─ ✅ build ok #> └─ ⚠️ Done 0.00s logtree_theme("unicode") demo_build() #> ▶ Build #> ├─ ℹ compiling #> ├─ ⚠ 3 deprecation warnings #> ├─ ✔ build ok #> └─ ⚠ Done 0.00s
Or override individual slots with overrides – a list keyed by slot,
each holding only the fields to change (everything else is kept from the
active theme):
logtree_theme("unicode", overrides = list( success = list(glyph = "*", color = c("green", "bold")), group = list(bracket = TRUE) )) demo_build() #> ▶ Build #> ├─ ℹ compiling #> ├─ ⚠ 3 deprecation warnings #> ├─ * build ok #> └─ ⚠ Done 0.00s logtree_theme("unicode")
Pass compact to tighten the tree’s per-level indentation – "medium"
drops the trailing gap after each connector, "tight" also slims the
connectors down to a single character:
logtree_theme("unicode", compact = "medium") demo_build() #> ▶ Build #> ├─ℹ compiling #> ├─⚠ 3 deprecation warnings #> ├─✔ build ok #> └─⚠ Done 0.00s logtree_theme("unicode")
The full list of overridable slots (step, info, success, group,
branch, corner, …) and their fields (glyph, width, color,
bracket) is in the Themes section of the
vignette
and ?logtree_theme.
More
- Output sinks –
logtree_sink_file(path, format = c("text", "json"))mirrors console output to a plain-text or NDJSON file; every registered sink runs alongside the console sink. loggerintegration –logtree_logger()routes the logger package through logtree in one call, sologger::log_info()and friends render as logtree leaves.- Manual step control –
log_open()/log_close()open and close steps by hand (with an explicitparent) instead of relying on frame exit, useful at top level or across script blocks. Opening a step at the same depth as an open one (e.g. sharing aparent) retires the earlier sibling automatically, so you can stream siblings without an explicitlog_close()on each. - Re-running top-level lines – at top level (e.g. re-running
selected lines in RStudio/Positron while iterating),
log_step()/log_open()key each step on its label and call site, so re-running the same line re-anchors to that node instead of nesting deeper on every run. Pass an explicitkeyto opt out, or to keep two same-label steps open at once distinct.
See vignette("logtree") and the documentation
site for full details on error
handling semantics, manual step control, and the design philosophy
behind the tree renderer.