# logtree

    library(logtree)

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.00s

## Leaf 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.00s

## Handling 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.00s

Even 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.00s

## Grouping

`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.00s
    #> ├─ ▣ returns
    #> │  ├─ ▶ 2024.csv
    #> │  │  ├─ ℹ Reading rows
    #> │  │  ├─ ✔ Merged into dataset
    #> │  │  └─ ✔ Done  0.00s
    #> │  └─ ✔ Done  0.00s
    #> └─ ✔ Done  0.00s

## Themes

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.00s

## Verbosity

`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.00s

`logger` 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.