Metadata-Version: 2.5
Name: cphtech-app-monitor
Version: 0.1.0
Summary: Tiny, dependency-free error-monitoring SDK for the App Monitor collector — Python services, ASGI apps, workers and CLIs.
Project-URL: Repository, https://github.com/outatimedev/app-monitor
License-Expression: LicenseRef-Proprietary
Classifier: Framework :: AsyncIO
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: System :: Monitoring
Classifier: Typing :: Typed
Requires-Python: >=3.10
Description-Content-Type: text/markdown

# cphtech-app-monitor

Python SDK for the App Monitor collector. Reports errors from services, ASGI
apps (Starlette, FastAPI), worker loops and CLIs. Standard library only, no
dependencies, Python 3.10+.

```sh
uv add cphtech-app-monitor
```

## Setup

```python
import app_monitor

app_monitor.init(
    environment="production",
    urls="path",  # drop query strings from request URLs
)
```

`init()` reads `APP_MONITOR_ENDPOINT` and `APP_MONITOR_KEY` (the project's
public ingest key) unless you pass `endpoint` / `api_key`, and the release from
`APP_MONITOR_RELEASE` or the usual CI commit-SHA variables, and the environment from
`APP_MONITOR_ENVIRONMENT`. Without an endpoint
and key it returns `False` and everything is a no-op, so you can call it
unconditionally. `APP_MONITOR_DISABLED=1` turns monitoring off without a code
change.

Once initialised, the SDK reports:

- **Uncaught exceptions**: `sys.excepthook` (flushed before the process exits)
  and `threading.excepthook`. Both pass on to the hook that was there before.
- **Logged errors**: records at `ERROR` and above that reach your logging
  handlers, so `logger.exception(...)` in a worker loop and asyncio's "Task
  exception was never retrieved" arrive without code changes
  (`logging_level=None` turns this off). The SDK adds no handler, so your
  logging setup (`basicConfig()` included) works exactly as before.
- **`capture_error(exc, context)`**, for errors you handle yourself.

Records below the error level (from `INFO`, `breadcrumb_level`) become
breadcrumbs on the next report.

### ASGI (Starlette, FastAPI)

```python
from starlette.middleware import Middleware
from app_monitor.asgi import AppMonitorMiddleware

app = Starlette(routes=routes, middleware=[Middleware(AppMonitorMiddleware)])
# FastAPI: app.add_middleware(AppMonitorMiddleware)
```

Reports exceptions that escape a request, with the request URL and method, and
re-raises them so your server's error handling still runs. Each request gets
its own scope: breadcrumbs, `set_user()` and the URL never leak between
concurrent requests. The same exception logged again by the server is not
reported twice.

## Privacy

The SDK **never sends local variables**, request bodies or headers. Traceback
paths are shortened to the package or project-relative path
(`httpx/_client.py`, `src/app/api.py`), which also keeps home-directory
usernames out. It sends what is in the exception message, log messages you
log at the error level, breadcrumbs, and the request URL (with its query
string unless `urls="path"`).

`before_send` is the last look at every report:

```python
def scrub(payload: dict) -> dict | None:
    payload["message"] = CVR.sub("[cvr]", payload["message"])
    return payload  # or None to drop it


app_monitor.init(before_send=scrub)
```

A hook that raises drops that report rather than sending it unscrubbed.

## Behaviour under load

Reports go out from one background thread, in batches of up to 10, at most one
request a second. Up to 50 reports are buffered: beyond that, new ones are
dropped. A failed upload is dropped, never retried, and a `429` from the
collector pauses uploads for a minute. The same exception object is reported
once per 10 seconds by the automatic capture points (`capture_error` always
reports). Forked children (gunicorn, multiprocessing) start clean. `flush()`
sends now, blocking for at most its timeout in wall-clock time and still at
most one request a second; `shutdown()` runs automatically at exit.

## Development

```sh
uv run pytest          # includes drift guards against the TypeScript wire contract
UPDATE_GOLDEN=1 uv run pytest tests/test_wire_contract.py   # after a payload change
uv run ruff check . && uv run ruff format --check .
```

Versioning follows the JS SDK's 0.x policy: additive or fix → patch, breaking
→ minor. Changing `src/app_monitor/_version.py` and merging to master publishes
to PyPI (`.github/workflows/sdk-python-release.yml`).
