logtree renders nested process execution as a live, colored tree in the console: tree connectors, status glyphs, and elapsed time per step. Nesting depth is tracked via frame-exit handlers, so it never desynchronizes, even when a step errors partway through.
Basic nesting
log_step() opens a step and automatically closes it when
the calling function returns – normally, via an early
return(), or because an error propagated through it.
Nesting composes for free: a step opened inside another step is
automatically indented one level deeper, with no coordination needed
between the two functions.
logtree_reset()
load_config <- function() {
log_step("Load config")
log_info("Reading config.yml")
log_success("Validated 12 parameters")
}
pipeline <- function() {
log_step("Pipeline")
load_config()
}
pipeline()
#> ▶ Pipeline
#> ├─ ▶ Load config
#> │ ├─ ℹ Reading config.yml
#> │ ├─ ✔ Validated 12 parameters
#> │ └─ ✔ Done 0.00s
#> └─ ✔ Done 0.01sLeaf lines and status elevation
log_info() and log_success() are plain leaf
lines. log_warn() and log_error() additionally
elevate the enclosing step’s status, so its close line renders
the elevated glyph even though the function that opened it returns
normally.
logtree_reset()
fetch_articles <- function() {
log_step("Fetching articles")
log_info("Connecting to API")
log_warn("Retry 1/3 due to timeout")
log_success("Fetched 1,204 articles")
}
fetch_articles()
#> ▶ Fetching articles
#> ├─ ℹ Connecting to API
#> ├─ ⚠ Retry 1/3 due to timeout
#> ├─ ✔ Fetched 1,204 articles
#> └─ ⚠ Done 0.00sHandling uncaught errors with with_logging()
If a step’s code actually throws, wrap the run in
with_logging(). Every step still open at the moment of the
error is flagged as failed before the stack unwinds, the error is logged
as a leaf line, a run summary prints, and the error is rethrown –
with_logging() never silently swallows it.
logtree_reset()
run_classifier <- function() {
with_logging({
log_step("Classifying")
stop("model timeout after 30s")
})
}
tryCatch(run_classifier(), error = function(e) invisible(NULL))
#> ▶ Classifying
#> ├─ ✖ model timeout after 30s
#> ✖ Run failed in 0.00s
#> └─ ✖ Done 0.00sEven without with_logging(), depth tracking
still unwinds correctly on an uncaught error – the step just renders as
dimmed/interrupted rather than retroactively painted red, since there
was no handler installed to catch the condition and flag it before the
stack unwound.
logtree_reset()
risky <- function() {
log_step("risky")
stop("boom")
}
tryCatch(risky(), error = function(e) invisible(NULL))
#> ▶ risky
#> └─ ◌ Done 0.00sGrouping
log_step(label, group = value) collapses adjacent steps
that share the same value under one synthetic header,
instead of nesting each step on its own. Here several files belong to
the same dataset, so each dataset becomes a header and its files sit
underneath. The group stays open across calls with a matching value and
only closes – as a sibling, with its own status aggregated from its
members – once a step with a different value (or a plain ungrouped step)
appears at the same level, or the enclosing step finishes. Grouping is
strictly adjacency-based: the same value recurring later, after
something else has closed the group, opens a fresh one rather than
reusing the old header.
Pass a bare value to use it as both the match key and
the header, or c(name = value) to show a fixed
name while still grouping on value.
logtree_reset()
load_file <- function(dataset, file) {
log_step(file, group = dataset)
log_info("Reading rows")
log_success("Merged into dataset")
}
import_datasets <- function() {
log_step("Import datasets")
load_file("sales", "2023.csv")
load_file("sales", "2024.csv")
load_file("returns", "2024.csv")
}
import_datasets()
#> ▶ Import datasets
#> ├─ ▣ sales
#> │ ├─ ▶ 2023.csv
#> │ │ ├─ ℹ Reading rows
#> │ │ ├─ ✔ Merged into dataset
#> │ │ └─ ✔ Done 0.00s
#> │ ├─ ▶ 2024.csv
#> │ │ ├─ ℹ Reading rows
#> │ │ ├─ ✔ Merged into dataset
#> │ │ └─ ✔ Done 0.00s
#> │ └─ ✔ Done 0.02s
#> ├─ ▣ returns
#> │ ├─ ▶ 2024.csv
#> │ │ ├─ ℹ Reading rows
#> │ │ ├─ ✔ Merged into dataset
#> │ │ └─ ✔ Done 0.00s
#> │ └─ ✔ Done 0.00s
#> └─ ✔ Done 0.02sThemes
Three built-in presets are available: "unicode"
(default), "ascii" (safe for log files, CI, and non-UTF-8
terminals), and "emoji". Every glyph is overridable, and
switching themes never breaks column alignment, since each glyph
declares its own rendered width rather than having that width measured
from the string.
logtree_theme("ascii")
pipeline()
#> > Pipeline
#> |- > Load config
#> | |- i Reading config.yml
#> | |- + Validated 12 parameters
#> | |- + Done 0.00s
#> |- + Done 0.00s
logtree_theme("unicode")Individual glyph slots can be overridden with overrides
– a named list keyed by slot, each element 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)
))
pipeline()
#> ▶ Pipeline
#> ├─ ▶ Load config
#> │ ├─ ℹ Reading config.yml
#> │ ├─ * Validated 12 parameters
#> │ └─ * Done 0.00s
#> └─ * Done 0.00s
logtree_theme("unicode")Accepted slots (valid names in an
overrides list):
| Slot | Applies to | Fields it accepts |
|---|---|---|
step |
open / running step glyph |
glyph, width, color
|
info |
log_info() leaf |
glyph, width, color
|
debug |
log_debug() leaf |
glyph, width, color
|
success |
success glyph (clean close, log_success()) |
glyph, width, color
|
warning |
log_warn() / elevated step glyph |
glyph, width, color
|
error |
log_error() / elevated step glyph |
glyph, width, color
|
interrupted |
abnormal-exit (dimmed) glyph |
glyph, width, color
|
group |
group header marker |
glyph, color, bracket
|
branch |
child connector (├─) |
glyph, color
|
corner |
close-line connector (└─) |
glyph, color
|
pipe |
vertical rail (│) |
glyph, color
|
Accepted fields (valid names inside a slot):
| Field | Type | Accepted values |
|---|---|---|
glyph |
character(1) |
Any string, including "". |
width |
integer(1) |
Rendered display width of the glyph (1 normal,
2 emoji/wide). Sets column alignment; status slots
only. |
color |
character / NULL
|
One or more cli styles, or NULL. Named
("red", "cyan", …), bright
("br_red"), backgrounds ("bg_blue"), styles
("bold", "dim", "italic"), or hex
("#ff8800"). A vector combines them,
e.g. c("red", "bold"). |
bracket |
logical(1) |
group slot only. TRUE wraps the header
name in < >. |
Output sinks
The console sink is always on. logtree_sink_file() adds
a plain-text or NDJSON file sink; every logged event fans out to all
active sinks simultaneously.
log_path <- tempfile(fileext = ".log")
logtree_sink_file(log_path, format = "text")
logtree_reset()
pipeline()
#> ▶ Pipeline
#> ├─ ▶ Load config
#> │ ├─ ℹ Reading config.yml
#> │ ├─ ✔ Validated 12 parameters
#> │ └─ ✔ Done 0.00s
#> └─ ✔ Done 0.00s
writeLines(readLines(log_path))
#> > Pipeline
#> |- > Load config
#> | |- i Reading config.yml
#> | |- + Validated 12 parameters
#> | |- + Done 0.00s
#> |- + Done 0.00sVerbosity
logtree_threshold() sets the minimum leaf-line level to
render ("debug", "info", "warn",
"error"). Step open/close lines always render regardless of
verbosity, since hiding them would break the tree structure; a
suppressed log_warn()/log_error() still
elevates the enclosing step’s close glyph.
logtree_threshold("warn")
fetch_articles()
#> ▶ Fetching articles
#> ├─ ⚠ Retry 1/3 due to timeout
#> └─ ⚠ Done 0.00s
logtree_threshold("info")log_debug() is the most verbose level, for fine-grained
diagnostic detail. It’s hidden by default (verbosity is
"info") and only shown when
logtree_threshold("debug") is called. Like
log_info() and log_success(), it does not
elevate the enclosing step’s status.
logtree_reset()
fetch_verbose <- function() {
log_step("Fetching")
log_debug("cache miss for key user:42")
log_info("connecting to API")
log_debug("request took 84ms")
log_success("fetched 12 records")
}
# At default verbosity, debug lines are hidden
fetch_verbose()
#> ▶ Fetching
#> ├─ ℹ connecting to API
#> ├─ ✔ fetched 12 records
#> └─ ✔ Done 0.00s
# Raise verbosity to show debug lines
logtree_threshold("debug")
logtree_reset()
fetch_verbose()
#> ▶ Fetching
#> ├─ ⚙ cache miss for key user:42
#> ├─ ℹ connecting to API
#> ├─ ⚙ request took 84ms
#> ├─ ✔ fetched 12 records
#> └─ ✔ Done 0.00s
logtree_threshold("info")Integrating with the logger package
If your codebase already uses the CRAN logger package
(https://daroczig.github.io/logger/) for logging, you can
route those calls through logtree without rewriting them.
Call logtree_logger() once near the top of your script: it
registers logtree’s custom layout, pairs it with
logger::appender_void (a no-op appender) so
logtree does the rendering, and opens logger’s
own threshold so that logtree_threshold() becomes the
single gate on what is shown.
logtree_reset()
logtree_threshold("debug") # the only gate, now that logtree_logger() opened logger's
ns <- "my_app"
logtree_logger(namespace = ns)
process_data <- function() {
log_step("Processing data")
logger::log_info("reading input file", namespace = ns)
logger::log_debug("parsed 5,000 rows", namespace = ns)
logger::log_success("transformation complete", namespace = ns)
}
process_data()
#> ▶ Processing data
#> ├─ ℹ reading input file
#> ├─ ⚙ parsed 5,000 rows
#> ├─ ✔ transformation complete
#> └─ ✔ Done 0.00slogger severities map onto logtree leaf levels:
FATAL/ERROR become [log_error()],
WARN becomes [log_warn()], SUCCESS becomes
[log_success()], INFO becomes [log_info()], and
DEBUG/TRACE both become [log_debug()] (logger
has two debug-ish tiers, logtree has one). Note that
logger’s own log_threshold() and
logtree_threshold() are two independent gates applied on
top of each other – both apply simultaneously, which is intentional.
