The `ttlog-macros` crate provides the user-facing logging API. The procedural macros
push as much work as possible to compile time.

## Available Macros

One macro per standard log level:

* `trace!`
* `debug!`
* `info!`
* `warn!`
* `error!`
* `fatal!`

## Usage

### 1. Message Only

Equivalent to `println!`.

```rust
use ttlog_macros::info;

info!("User successfully authenticated.");
```

### 2. Message with Structured Data (Key-Value Pairs)

Add context with `key = value`. The message is the last string literal in the
argument list.

```rust
use ttlog_macros::warn;

let user_id = 123;
let attempt_count = 3;
warn!(user_id = user_id, attempts = attempt_count, "Failed login attempt");
```

### 3. Structured Data Only

Key-value pairs without a message.

```rust
use ttlog_macros::debug;

let db_latency_ms = 55;
let query = "SELECT * FROM users";
debug!(latency = db_latency_ms, query = query);
```

### 4. Event-Only Logging

Log an occurrence with no message or data.

```rust
use ttlog_macros::trace;

trace!("Entering critical section");
// ... some code ...
trace!(); // Log that we've entered the section
```

## How It Works

The macros push work from runtime to compile time.

1. **Static string interning**: arguments parse at compile time. The message and key
   names are known, so the macro emits code that interns them once per application
   lifetime via `std::sync::OnceLock`. Later calls to the same site are integer
   lookups.

2. **Static metadata**: file path, module path, and line number come from `file!()`,
   `module_path!()`, and `line!()` — no runtime gathering.

3. **Value serialization**: key-value pairs serialize to a compact binary format.
   Primitive integers (`i64`, `u64`) and floats (`f32`, `f64`) convert to strings
   directly, not through `serde`.

4. **Direct logger call**: expanded code calls into the `ttlog` crate's
   `GLOBAL_LOGGER` with no intermediate abstraction.

Each `info!` call expands to code tailored to the arguments passed.