docs/logging.md

# Python Logging and Tracing Integration

This guide covers integrating Python's `logging` module with Erlang's `logger`, and distributed tracing support for Python code.

## Overview

erlang_python provides:
- **Logging**: Python `logging` forwarded to Erlang `logger`
- **Tracing**: Span-based distributed tracing from Python

Both features use fire-and-forget NIFs, meaning Python execution is never blocked waiting for Erlang.

## Logging

### Quick Start

```erlang
%% Configure Python logging
ok = py:configure_logging().

%% Run Python code that logs
{ok, _} = py:eval(<<"
import logging
logging.info('Hello from Python!')
logging.warning('Something happened')
">>).
```

Log messages appear in your Erlang logger output with domain `[python]`.

### Python API

After `py:configure_logging()`, these functions are available on the `erlang` module:

```python
import erlang

# ErlangHandler - logging.Handler subclass
handler = erlang.ErlangHandler()
logging.getLogger().addHandler(handler)

# Or use the setup helper
erlang.setup_logging(level=20)  # INFO level
erlang.setup_logging(level=10, format='%(name)s: %(message)s')
```

### Erlang API

```erlang
%% Configure with defaults (debug level)
ok = py:configure_logging().

%% Configure with options
ok = py:configure_logging(#{
    level => info,  % debug | info | warning | error | critical
    format => <<"%(name)s - %(message)s">>  % Optional Python format string
}).
```

### Log Level Mapping

| Python Level | Python levelno | Erlang Level |
|--------------|---------------|--------------|
| DEBUG        | 10            | debug        |
| INFO         | 20            | info         |
| WARNING      | 30            | warning      |
| ERROR        | 40            | error        |
| CRITICAL     | 50            | critical     |

### Metadata

Each log message includes Python metadata:

```erlang
%% In your Erlang logger handler, you'll receive:
#{
    domain => [python],
    py_logger => <<"root">>,  % Logger name
    py_meta => #{
        <<"module">> => <<"mymodule">>,
        <<"lineno">> => 42,
        <<"funcName">> => <<"my_function">>
    }
}
```

## Distributed Tracing

### Quick Start

```erlang
%% Enable tracing
ok = py:enable_tracing().

%% Run Python code with spans
{ok, _} = py:eval(<<"
import erlang

with erlang.Span('process-request', user_id=123):
    do_work()
">>).

%% Retrieve collected spans
{ok, Spans} = py:get_traces().
%% Spans = [#{name => <<"process-request">>, status => ok, ...}]

%% Clean up
ok = py:clear_traces().
ok = py:disable_tracing().
```

### Python API

#### Context Manager

```python
import erlang

with erlang.Span('operation-name', key='value', count=42) as span:
    # Your code here
    span.event('checkpoint', items_processed=10)

    # Nested spans
    with erlang.Span('sub-operation'):
        pass
```

#### Decorator

```python
import erlang

@erlang.trace()
def my_function():
    return 42

@erlang.trace(name='custom-name')
def another_function():
    pass
```

### Erlang API

```erlang
%% Enable/disable tracing
ok = py:enable_tracing().
ok = py:disable_tracing().

%% Get all collected spans
{ok, Spans} = py:get_traces().

%% Clear collected spans
ok = py:clear_traces().
```

### Span Structure

Each completed span is a map with these keys:

```erlang
#{
    name => <<"operation-name">>,
    span_id => 12345678901234567890,  % Unique 64-bit ID
    parent_id => 9876543210987654321, % Parent span ID (or 'undefined')
    start_time => 1234567890123,      % Microseconds (monotonic)
    end_time => 1234567890456,
    duration_us => 333,               % Duration in microseconds
    status => ok | error,
    attributes => #{<<"key">> => <<"value">>},
    end_attrs => #{},                 % Attributes added at span end
    events => [                       % Events within the span
        #{
            name => <<"checkpoint">>,
            attrs => #{<<"items_processed">> => 10},
            time => 1234567890200
        }
    ]
}
```

### Error Handling

Spans automatically capture exceptions:

```python
import erlang

try:
    with erlang.Span('risky-operation'):
        raise ValueError('something went wrong')
except ValueError:
    pass

# The span will have:
# - status: 'error'
# - end_attrs: {'exception': 'something went wrong'}
```

### Thread Safety

Both logging and tracing are thread-safe:
- Span context is stored in thread-local storage
- Each thread maintains its own span stack for proper parent-child relationships
- NIFs use atomic operations for receiver registration

## Architecture

```
Python                           NIF                          Erlang
───────                         ─────                        ────────
logging.info(msg)                 │                             │
       │                          │                             │
       ▼                          │                             │
ErlangHandler.emit()              │                             │
       │                          │                             │
       ▼                          │                             │
erlang._log(...)  ───────►  nif_py_log()  ──► enif_send() ──►  py_logger
       │                          │                          (gen_server)
       │ (returns immediately)    │                             │
       ▼                          │                     logger:log(...)
   continue execution             │                             │
```

Key design decisions:
- **Fire-and-forget**: `enif_send()` is non-blocking
- **Level filtering**: Done in NIF before message creation
- **No Python blocking**: Python never waits for Erlang

## Performance Considerations

- Log messages below the configured level are filtered at the NIF level
- No heap allocation occurs for filtered messages
- Tracing disabled by default; enable only when needed
- Span data is accumulated in memory until retrieved with `get_traces()`

## Configuration Options

### Logger

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `level` | atom | `debug` | Minimum log level |
| `format` | binary | `%(message)s` | Python format string |

### Tracer

The tracer has no configuration options. Enable/disable with `py:enable_tracing()`/`py:disable_tracing()`.

## Examples

See `examples/logging_example.erl` for a complete working example.

```erlang
%% Basic usage
{ok, _} = application:ensure_all_started(erlang_python).

%% Logging
ok = py:configure_logging(#{level => info}).
{ok, _} = py:eval(<<"import logging; logging.info('hello')">>).

%% Tracing
ok = py:enable_tracing().
{ok, _} = py:eval(<<"
import erlang
with erlang.Span('work'):
    pass
">>).
{ok, Spans} = py:get_traces().
io:format("Collected ~p spans~n", [length(Spans)]).
```