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.00slog_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.00sA 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.00sRun 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.emailGrouping
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.00sThemes
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.00sOr 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.
