
## How it works

Log statements are part of Vary's grammar. The compiler parses them, validates field types, checks purity constraints, and generates level-gated bytecode. At runtime, events are dispatched to a configurable sink.

There is nothing to import. You write `log info "event_id" "message"` and the compiler handles the rest.

## Log statements

A log statement has a level, a stable event ID, and an optional message:

```vary
log info "app_started" "Server is running"
log debug "cache_miss" "Key not found"
log error "db_timeout" "Connection timed out"
```

The short form takes the level, event ID (a string literal), and an optional message expression. The event ID is a stable identifier for this log point. It does not change when you reword the message.

## Log levels

Five levels are available, from lowest to highest priority:

| Level | Use |
|-------|-----|
| `trace` | Detailed diagnostic output |
| `debug` | Internal state useful during development |
| `info` | Normal operational events |
| `warn` | Unexpected but recoverable situations |
| `error` | Failures that need attention |

The runtime filters events below the active level. Setting the level to `warn` silences `trace`, `debug`, and `info` events.

## Structured fields

For richer events, use the block form to attach typed fields:

```vary
let name = "alice"
let count = 3
let elapsed = 0.042

log info "user_login" {
    message = "User logged in"
    field "username" = name
    field "attempt" = count
    field "latency_ms" = elapsed * 1000.0
}
```

Field values must be one of four primitive types:

| Type | Example |
|------|---------|
| `Int` | `field "port" = 8080` |
| `Float` | `field "latency" = 3.14` |
| `Bool` | `field "cached" = True` |
| `Str` | `field "user" = name` |

The compiler rejects complex types (lists, dicts, objects) at compile time. This keeps log events flat, avoids serialization overhead, and means any sink can write them without custom encoders.

## Logger blocks

Logger blocks scope a name to all nested log statements:

```vary
logger "auth" {
    log info "login_attempt" "Checking credentials"
    log info "login_success" "Authenticated"
}
```

The compiler propagates the logger name into each nested log statement. Logger blocks can nest, and names are joined with dots:

```vary-snippet
logger "app" {
    logger "auth" {
        log info "check" "Verifying token"
        # logger name: app.auth
    }
}
```

## Context blocks

Context blocks attach fields that propagate to every log statement inside them:

```vary-snippet
logger "api" {
    context {
        field "service" = "orders"
        field "version" = "2.1"
    }
    log info "request_start" "Handling request"
    # event includes service="orders" and version="2.1"
}
```

Context fields are merged into each log event at compile time. If a log statement defines a field with the same name as a context field, the log statement's value takes precedence.

## Sinks

Five sinks are built in:

| Sink | Output | Default |
|------|--------|---------|
| `stdout` | Plain text, one line per event | Yes |
| `color` | ANSI-colored output for terminals | |
| `json` | One JSON object per line | |
| `memory` | In-memory buffer for testing | |
| `none` | Discards all events | |

Switch sinks at runtime:

```vary
import log

log.set_sink("json")
log info "test_event" "Testing JSON output"
log.set_sink("color")
log info "test_event" "Testing color output"
```

## Level configuration

Set the minimum level at runtime:

```vary
import log

log.set_level("debug")   # show debug and above
log debug "test" "This is visible"
log.set_level("warn")    # show warn and error only
log debug "test" "This is hidden"
log warn "test" "This is visible"
```

The default level is `info`.

## Testing with capture

For testing, capture log events in memory instead of printing them:

```vary
import log

test "login logs username" {
    log.set_sink("none")
    log.capture()

    log info "login" {
        message = "User logged in"
        field "username" = "alice"
    }

    let events = log.events()
    observe len(events) == 1
}
```

`log.capture()` starts collecting events on the current thread. `log.events()` returns the collected events as a list and clears the buffer. Events are captured regardless of the active sink, so you can set the sink to `"none"` to suppress output during tests.

## Purity

Log statements are side effects. The compiler rejects them inside `pure` functions:

```vary-snippet
pure def add(a: Int, b: Int) -> Int {
    log info "adding" "Computing sum"  # compile error
    return a + b
}
```

This is enforced by the effect system. A function that logs cannot be `pure`.

## Mutation testing

The compiler treats log statements specially during mutation testing.

Event IDs and message strings are not mutated. Changing `"user_login"` to `"user_logout"` would not test anything meaningful about correctness. Field value expressions are mutated normally: if your code logs `field "count" = len(items)`, the mutator may replace `len(items)` with `0` to see if any test notices.

A library-based logger would be opaque to the mutation engine. Because `log` is grammar, the compiler can tell the difference between a label and a value.

## Built-in functions

| Function | Signature | Purpose |
|----------|-----------|---------|
| `capture_logs()` | `() -> None` | Start capturing log events on the current thread |
| `captured_events()` | `() -> List` | Return captured events and clear the buffer |
| `log_set_level(level)` | `(Str) -> None` | Set minimum log level (`"trace"`, `"debug"`, `"info"`, `"warn"`, `"error"`) |
| `log_set_sink(sink)` | `(Str) -> None` | Set active sink (`"stdout"`, `"color"`, `"json"`, `"memory"`, `"none"`) |
