Metadata-Version: 2.4
Name: logballoon
Version: 0.1.8
Summary: Offline-first logging and operations SDK for desktop apps
Project-URL: Homepage, https://logballoon.github.io/logballoon-python/
Project-URL: Repository, https://github.com/logballoon/logballoon-python
Project-URL: Documentation, https://logballoon.github.io/logballoon-python/protocol.html
Project-URL: Issues, https://github.com/logballoon/logballoon-python/issues
Author: logballoon
License-Expression: MIT
License-File: LICENSE
Keywords: crash-reporting,desktop,logging,offline-first,telemetry
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: System :: Logging
Requires-Python: >=3.10
Provides-Extra: dev
Requires-Dist: pytest>=8.0; extra == 'dev'
Description-Content-Type: text/markdown

# LogBalloon

Offline-first logging and operations SDK for desktop apps.

**Buffer locally. Deliver reliably.**

Your app keeps working when the network does not. Startup, events, and crashes
go into a local SQLite queue and are delivered to **your own** server when the
link comes back.

- Site: https://logballoon.github.io/logballoon-python/
- Protocol: https://logballoon.github.io/logballoon-python/protocol.html
- PyPI: https://pypi.org/project/logballoon/
- Repo: https://github.com/logballoon/logballoon-python

```bash
pip install logballoon
```

No third-party runtime dependencies — Python stdlib only (`urllib` + `sqlite3`).

---

## Try it in 30 seconds

**No server to set up.** Clone the repo and run one file. It starts a throwaway
receiver on a free port, sends real traffic through the SDK, and prints exactly
what the server received.

```bash
git clone https://github.com/logballoon/logballoon-python
cd logballoon-python
python examples/try_local.py
```

```
receiver listening on http://127.0.0.1:54097 (api key required)

queue pending after flush: 0

--- /startup
{
  "app": "Try LogBalloon",
  "version": "0.0.1",
  "installation_id": "581a1fd5-...",
  "os": "Windows",
  ...
}
--- /event
{ "event": "export_complete", "payload": {"rows": 120, "format": "csv"}, ... }

received: /startup, /event
```

Add `--crash` to also deliver an uncaught exception:

```bash
python examples/try_local.py --crash
```

### Two-terminal version

Closer to real life: a standalone receiver plus a client.

```bash
python examples/demo_server.py      # terminal 1
python examples/demo_client.py      # terminal 2
```

**See the offline queue work:** stop the server, run the client again (items
queue locally), start the server, run the client once more — the backlog flushes.

Other things to try:

| Command | Shows |
|---|---|
| `python examples/demo_client.py --crash` | crash capture via `sys.excepthook` |
| `python examples/demo_client.py --contact` | opt-in Tk contact prompt |
| `python examples/demo_server.py --api-key secret` | endpoint auth (pair with `--api-key secret` on the client) |
| `pip install fastapi uvicorn && python examples/fastapi_server.py` | FastAPI receiver on port 8765 |

---

## Quick start

```python
from logballoon import LogBalloon

lb = LogBalloon(
    app_name="logballoon_test_app",
    version="1.0.0",
    endpoint="http://127.0.0.1:8765",  # your self-hosted server
)
lb.start()
lb.event("export_complete", {"rows": 120, "format": "csv"})
```

That gives you:

- `installation_id` creation and persistence
- startup reporting with an environment snapshot
- custom events
- uncaught exception / crash capture
- SQLite offline queue with retry
- a stable `message_id` (UUID) on every outbound item for server-side idempotency

`event()` only enqueues — network I/O stays on a background thread, so your UI
never blocks. (The optional contact dialog is the exception: it is modal on the
calling thread.)

## Custom payloads

The **envelope is fixed** for interoperability (`app`, `version`,
`installation_id`, `message_id`, `event`, `timestamp`, …). The **`payload` dict
is yours**:

```python
lb.event("job_done", {
    "duration_ms": 842,
    "operator": "A12",
    "batch_id": "2026-07-22-03",
})
```

Full contract: [Protocol page](https://logballoon.github.io/logballoon-python/protocol.html).

## Optional endpoint auth

Auth is **off by default**. When your receiver sits behind a gateway or needs a
shared secret, pass a key:

```python
lb = LogBalloon(
    app_name="logballoon_test_app",
    version="1.0.0",
    endpoint="https://ops.example.com",
    api_key="...",  # sends Authorization: Bearer ...
)
```

Or arbitrary headers (Basic auth, gateway keys, tenant IDs, …):

```python
lb = LogBalloon(
    app_name="logballoon_test_app",
    version="1.0.0",
    endpoint="https://ops.example.com",
    headers={
        "Authorization": "Basic ...",
        "X-Tenant": "lab-a",
    },
)
```

Read secrets from env vars or config files rather than hard-coding them. If both
`api_key` and `headers` set `Authorization`, **`headers` wins**.

## Contact API: bring your own UI

All frameworks use the same headless API. Your UI only decides **when to ask**
and **which button the user clicked**; LogBalloon owns local state and queues
the matching `POST /user`.

```python
# Before showing your own dialog / form:
if lb.should_prompt_contact():
    state = lb.contact_state()
    # state["status"]: "unset", "skipped", or "registered"
    # state.get("email"): saved address, when registered

# User entered or changed an email:
message_id = lb.submit_contact(
    "user@example.com",
    skip_days=14,
    consent_version=1,
)

# User approved the already-saved address:
message_id = lb.confirm_contact(skip_days=14, consent_version=1)

# User clicked Skip (no email is sent):
lb.skip_contact(skip_days=14)

# User clicked Not now (keep saved email, send nothing):
lb.defer_contact(skip_days=14)
```

`submit_contact()` automatically chooses `action=register` or `action=update`.
`confirm_contact()` uses `action=confirm`. Both save locally first, enqueue a
`user` item, and return its stable `message_id`; they do not wait for the
network. Skip and defer only update local state and do not call `/user`.

| Public method | Local state | Queued request |
|---|---|---|
| `contact_state()` | Read | None |
| `should_prompt_contact()` | Read | None |
| `submit_contact(email)` | Save email + quiet period | `/user`, register/update |
| `confirm_contact()` | Refresh quiet period | `/user`, confirm |
| `skip_contact()` | No email + quiet period | None |
| `defer_contact()` | Keep email + quiet period | None |

This is the recommended integration point for Qt, Streamlit, Flask, FastAPI,
Django, or any other UI. LogBalloon intentionally does not detect or import
those frameworks. A complete terminal-based mapping is in
[`examples/custom_contact_ui.py`](examples/custom_contact_ui.py); replace its
`input()` calls with your framework's widgets or form handlers.

## Optional built-in Tk contact prompt

Sometimes you need to reach the person running your app — `installation_id`
alone cannot tell you who they are. LogBalloon can ask for an email, remember
the answer, and deliver it over the same offline queue.

**Nothing happens on import.** The prompt exists only if you turn it on:

```python
lb.start()
lb.enable_contact_prompt(
    ui="tk",             # stdlib Tkinter, imported only when used
    on=("startup",),     # startup only for now
    skip_days=14,        # quiet after OK / register / Skip / Not now
    message=None,        # optional; default body follows OS language
    lang=None,           # auto from OS UI language (en / ja / zh); or "ja"
    consent_version=1,
)
```

Behaviour:

- **First run:** enter an email, or Skip
- **Later runs (after quiet period):** confirm the saved address (OK / Change / Not now)
- **OK, register, Skip, or Not now:** stays quiet for `skip_days` (default 14)
- **Language:** default body and buttons follow the OS UI language (`en` / `ja` /
  `zh`). Override with `lang="ja"` or a custom `message=`
- Call `enable_contact_prompt` on the **UI / main thread** when using `ui="tk"`
  (the dialog is modal)
- Email is stored in plain text as `contact.json` next to `installation_id` and
  sent to `POST /user` — never mixed into event payloads
- Local state updates immediately; delivery is offline-capable, so the server
  may lag behind what the user just confirmed

The Tk helper is only a UI adapter; internally it calls the same public Contact
API shown above.

Design notes and rationale: [`docs/contact-prompt-spec.md`](docs/contact-prompt-spec.md).

## Self-hosted REST API

LogBalloon does **not** require a SaaS backend. You run the server; it accepts
JSON on four simple routes.

| Method | Path | Purpose |
|---|---|---|
| `POST` | `/startup` | Boot + environment |
| `POST` | `/event` | Named event + free-form payload |
| `POST` | `/crash` | Exception + stack trace |
| `POST` | `/user` | Contact email (`register` / `update` / `confirm`) |

Success is any HTTP 2xx. Delivery is **at-least-once**: every item carries a
`message_id` so your server can de-duplicate. Transient failures (network, 5xx,
408, 429) keep the item queued. **Permanent 4xx** (400, 401, 403, 404, …) and
items that exceed `max_attempts` are dropped so a bad key or poison payload
cannot clog the queue forever.

Receivers in this repo:

- `examples/demo_server.py` — stdlib only, optional `--api-key` / `LOGBALLOON_API_KEY`
- `examples/fastapi_server.py` — FastAPI version of the same routes

### Production receiver checklist

Before pointing real users at an endpoint:

1. **TLS** — use HTTPS; do not put shared secrets on plain HTTP
2. **Auth** — require `Authorization: Bearer …` or your gateway equivalent
3. **Idempotency** — key on `message_id` (and optionally `installation_id`)
4. **Retention** — decide how long you keep events, crashes, and emails
5. **PII** — treat `/user` email and crash stack traces as sensitive
6. **Access** — do not expose a write-open receiver on the public internet

Full envelope examples: [Protocol page](https://logballoon.github.io/logballoon-python/protocol.html).

## Lightweight defaults

Built for weak PCs and flaky networks:

- small flush batches (`batch_size=20`)
- bounded queue (`max_queue=1000`); when full, drop `event` / `startup` before
  `user` / `crash`
- exponential backoff on transient failure (capped by `max_backoff`)
- drop permanent 4xx and items past `max_attempts` (default 40)
- background delivery only — never on the calling thread (except the optional
  contact dialog)

## Client API

| Method | Description |
|---|---|
| `start()` | Enqueue startup and begin background delivery |
| `event(name, payload=None)` | Enqueue a custom event |
| `contact_state()` | Read local contact status/email |
| `should_prompt_contact()` | Check whether your own UI should ask now |
| `submit_contact(email, ...)` | Save and queue register/update to `/user` |
| `confirm_contact(...)` | Queue confirmation for the saved email |
| `skip_contact(...)` | Skip locally; no `/user` request |
| `defer_contact(...)` | Keep email and defer locally; no `/user` request |
| `enable_contact_prompt(...)` | Opt in to the contact (email) dialog |
| `flush(timeout=None)` | Send pending queue items now |
| `stop(flush=True)` | Stop the worker |
| `pending()` | Items still waiting locally |

Constructor options: `app_name`, `version`, `endpoint`, `api_key`, `headers`,
`flush_interval`, `batch_size`, `max_queue`, `max_backoff`, `max_attempts`,
`timeout`, `install_excepthook`, `data_root`.

## Design

```
App → LogBalloon → SQLite queue → HTTP (urllib) → Your server
                 ↖ retry on recovery ↗
```

## Requirements

- Python 3.10+
- Windows / Linux / macOS (including Raspberry Pi)
- Tkinter only if you enable the contact prompt (`sudo apt install python3-tk` on some Linux distros)

## Development

```bash
pip install -e ".[dev]"
python -m pytest -q
```

CI runs pytest on push/PR via free GitHub Actions (Python 3.10 and 3.12).
