Skip to contents

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.

Annotated logtree console output

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 sinkslogtree_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.
  • logger integrationlogtree_logger() routes the logger package through logtree in one call, so logger::log_info() and friends render as logtree leaves.
  • Manual step controllog_open()/log_close() open and close steps by hand (with an explicit parent) 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 a parent) retires the earlier sibling automatically, so you can stream siblings without an explicit log_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 explicit key to 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.