Metadata-Version: 2.4
Name: hannah-logging
Version: 0.2.1
Summary: Ships the logs of a Hannah component to the Hannah log collector
Requires-Python: >=3.9
Description-Content-Type: text/markdown
Requires-Dist: grpcio>=1.84.0
Requires-Dist: hannah-proto>=4.5.0
Provides-Extra: test
Requires-Dist: pytest>=8; extra == "test"

# hannah-logging (Python)

Ships the logs of a Hannah component to the Hannah log collector, so a user can download
one archive of all components' logs from the WebUI.

The library adds one `logging.Handler`. Your existing handlers (stdout, journald, syslog)
keep working unchanged.

## Installation

```sh
pip install hannah-logging
```

The library relies on the `hannah-proto` version the component already uses, because
every call to Hannah Core has to carry the matching protocol version.

## Usage

```python
import logging
import hannah_logging

logging.basicConfig(level=logging.INFO)

# As early as possible: everything logged from here on is buffered.
shipping = hannah_logging.install(
    "telegram",
    version=__version__,
    secrets=[config.bot_token],                         # masked wherever they appear
    logger_categories={"hannah.stt": hannah_logging.TRANSCRIPT},
)

# Once the config is loaded:
shipping.connect(
    hannah_address="localhost:50051",                   # discovery via Hannah Core
    collector_address=None,                             # optional static fallback
)
```

### Setting the collector address directly

A component that knows the collector address itself (Hannah Core, which announces it)
passes it in instead of subscribing to discovery:

```python
shipping.set_collector_address("192.168.1.10:50061")   # None withdraws it
```

It starts shipping if `connect` hasn't been called yet, takes precedence over the static
`collector_address`, and a new address moves the stream.

### Categories

Each line is tagged `GENERAL`, `TRANSCRIPT` (speech transcripts, user utterances) or
`METADATA` (room and device names, presence). Exports can leave out the last two, and
the WebUI does so by default.

- By logger name: `logger_categories={"hannah.stt": TRANSCRIPT}` also covers child
  loggers such as `hannah.stt.whisper`.
- Per call: `log.info("heard: %s", text, extra={hannah_logging.CATEGORY_ATTR: hannah_logging.TRANSCRIPT})`

### Secrets

Before a line leaves the process, the library masks:

- values of keys such as `password`, `token`, `api_key`, `secret`, `psk`
- `Bearer` tokens, JWTs, credentials in URLs (`mqtt://user:pw@host`), PEM private keys
- Telegram bot tokens and well-known token prefixes (`glpat-`, `ghp_`, …)
- every value passed as `secrets=[…]` or later through `shipping.add_secret(…)`

Extra regexes go in `secret_patterns=[…]`. The component's own handlers still see the
unmasked line.

## Behaviour

- **Buffer:** 4 MiB by default (`max_buffer_bytes`). When it is full, the oldest lines are
  dropped and reported to the collector as a gap. While no collector is known, the
  buffer simply keeps running as a ring.
- **Timestamps** are taken when a line is logged, not when it is sent.
- **Discovery:** subscribes to Hannah Core's infrastructure announcements and follows the
  log collector when it moves. If Core is unreachable, the last known collector is kept.
  The static `collector_address` is used while none is announced.
- **Never blocks the component:** sending runs on its own threads. Connection problems
  are logged once to the `hannah_logging` logger (not shipped) and retried with backoff.
- **Shutdown:** at exit, the library tries for up to 2 seconds to send what is still
  buffered (`shipping.close(timeout=…)`).
