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