---
title: "logtree"
output: rmarkdown::html_vignette
vignette: >
  %\VignetteIndexEntry{logtree}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

```{r, include = FALSE}
knitr::opts_chunk$set(
  collapse = TRUE,
  comment = "#>"
)
```

```{r setup}
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.

```{r}
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()
```

## 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.

```{r}
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()
```

## 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.

```{r}
logtree_reset()

run_classifier <- function() {
  with_logging({
    log_step("Classifying")
    stop("model timeout after 30s")
  })
}

tryCatch(run_classifier(), error = function(e) invisible(NULL))
```

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.

```{r}
logtree_reset()

risky <- function() {
  log_step("risky")
  stop("boom")
}

tryCatch(risky(), error = function(e) invisible(NULL))
```

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

```{r}
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()
```

## 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.

```{r}
logtree_theme("ascii")
pipeline()

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):

```{r}
logtree_theme("unicode", overrides = list(
  success = list(glyph = "*", color = c("green", "bold")),
  group   = list(bracket = TRUE)
))
pipeline()
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.

```{r}
log_path <- tempfile(fileext = ".log")
logtree_sink_file(log_path, format = "text")

logtree_reset()
pipeline()

writeLines(readLines(log_path))
```

## 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.

```{r}
logtree_threshold("warn")
fetch_articles()
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.

```{r}
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()

# Raise verbosity to show debug lines
logtree_threshold("debug")
logtree_reset()
fetch_verbose()

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.

```{r, eval = requireNamespace("logger", quietly = TRUE)}
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()
```

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