lumonox-sdk 0.2.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- lumonox_sdk-0.2.3/.gitignore +73 -0
- lumonox_sdk-0.2.3/CHANGELOG.md +77 -0
- lumonox_sdk-0.2.3/LICENSE +21 -0
- lumonox_sdk-0.2.3/PKG-INFO +126 -0
- lumonox_sdk-0.2.3/README.md +96 -0
- lumonox_sdk-0.2.3/pyproject.toml +43 -0
- lumonox_sdk-0.2.3/src/lumonox/__init__.py +55 -0
- lumonox_sdk-0.2.3/src/lumonox/_infrastructure.py +65 -0
- lumonox_sdk-0.2.3/src/lumonox/_jobs.py +58 -0
- lumonox_sdk-0.2.3/src/lumonox/_monitor.py +863 -0
- lumonox_sdk-0.2.3/src/lumonox/fixtures/README.md +58 -0
- lumonox_sdk-0.2.3/src/lumonox/fixtures/__init__.py +1 -0
- lumonox_sdk-0.2.3/src/lumonox/fixtures/synthetic_load.py +479 -0
- lumonox_sdk-0.2.3/src/lumonox/fixtures/synthetic_lumonox_config.py +119 -0
- lumonox_sdk-0.2.3/src/lumonox/fixtures/synthetic_test_app.py +759 -0
- lumonox_sdk-0.2.3/src/lumonox/widgets.py +267 -0
- lumonox_sdk-0.2.3/tests/client_lifespan.py +22 -0
- lumonox_sdk-0.2.3/tests/conftest.py +18 -0
- lumonox_sdk-0.2.3/tests/test_benchmarks.py +69 -0
- lumonox_sdk-0.2.3/tests/test_init.py +9 -0
- lumonox_sdk-0.2.3/tests/test_jobs.py +48 -0
- lumonox_sdk-0.2.3/tests/test_monitor.py +739 -0
- lumonox_sdk-0.2.3/tests/test_synthetic_test_app.py +60 -0
- lumonox_sdk-0.2.3/tests/test_widgets.py +64 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
*.so
|
|
6
|
+
.Python
|
|
7
|
+
build/
|
|
8
|
+
develop-eggs/
|
|
9
|
+
dist/
|
|
10
|
+
downloads/
|
|
11
|
+
eggs/
|
|
12
|
+
.eggs/
|
|
13
|
+
/lib/
|
|
14
|
+
lib64/
|
|
15
|
+
parts/
|
|
16
|
+
sdist/
|
|
17
|
+
var/
|
|
18
|
+
wheels/
|
|
19
|
+
*.egg-info/
|
|
20
|
+
.installed.cfg
|
|
21
|
+
*.egg
|
|
22
|
+
MANIFEST
|
|
23
|
+
.venv/
|
|
24
|
+
venv/
|
|
25
|
+
ENV/
|
|
26
|
+
|
|
27
|
+
# uv
|
|
28
|
+
.uv/
|
|
29
|
+
|
|
30
|
+
# Testing / coverage
|
|
31
|
+
.pytest_cache/
|
|
32
|
+
.coverage
|
|
33
|
+
htmlcov/
|
|
34
|
+
.mypy_cache/
|
|
35
|
+
.ruff_cache/
|
|
36
|
+
|
|
37
|
+
# Node / Next.js (frontend)
|
|
38
|
+
node_modules/
|
|
39
|
+
.next/
|
|
40
|
+
out/
|
|
41
|
+
.turbo/
|
|
42
|
+
*.tsbuildinfo
|
|
43
|
+
|
|
44
|
+
# OS / editors
|
|
45
|
+
.DS_Store
|
|
46
|
+
.idea/
|
|
47
|
+
.vscode/
|
|
48
|
+
*.swp
|
|
49
|
+
*.swo
|
|
50
|
+
|
|
51
|
+
# Environment
|
|
52
|
+
.env
|
|
53
|
+
.env.*
|
|
54
|
+
!.env.example
|
|
55
|
+
!.env.lumonox.example
|
|
56
|
+
|
|
57
|
+
# Local SQLite (dev)
|
|
58
|
+
*.db
|
|
59
|
+
|
|
60
|
+
# Logs
|
|
61
|
+
*.log
|
|
62
|
+
|
|
63
|
+
.lumonox/
|
|
64
|
+
# Legacy local data dir name (pre-rename checkouts); never commit.
|
|
65
|
+
.autopulse/
|
|
66
|
+
backend/.autopulse/
|
|
67
|
+
sdk/.autopulse/
|
|
68
|
+
|
|
69
|
+
# Local stray copies of dashboard static export (canonical build is frontend/out)
|
|
70
|
+
sdk/src/lumonox/ui/
|
|
71
|
+
|
|
72
|
+
# Hatch sdist staging for shipped wheels (never commit baked export under src)
|
|
73
|
+
backend/src/lumonox_backend/dashboard_static/
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to the **Lumonox** Python SDK are documented here.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html)
|
|
7
|
+
for public API and packaging.
|
|
8
|
+
|
|
9
|
+
## [Unreleased]
|
|
10
|
+
|
|
11
|
+
## [0.2.3] - 2026-05-11
|
|
12
|
+
|
|
13
|
+
### Packaging
|
|
14
|
+
|
|
15
|
+
- **`[stack]`** extra depends on **`lumonox>=0.2.5`** (aligned with the **0.2.5** API wheel on PyPI).
|
|
16
|
+
|
|
17
|
+
## [0.2.2] - 2026-05-11
|
|
18
|
+
|
|
19
|
+
### Packaging
|
|
20
|
+
|
|
21
|
+
- **`[stack]`** extra depends on **`lumonox>=0.2.1`** (PyPI project **`lumonox`** for the API + bundled dashboard; replaces the prior **`lumonox-api`** distribution name).
|
|
22
|
+
|
|
23
|
+
## [0.2.1] - 2026-05-10
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
|
|
27
|
+
- **Dashboard (bundled in `lumonox-api`):** settings composition and hooks, shared session-scoped dashboard fetches, stricter JSON guards for dashboard query responses, chart and query-toolbar accessibility improvements, extended Vitest/Playwright smoke coverage, and frontend README/ESLint contributor guardrails (see `docs/FRONTEND_MULTI_LANE_REVIEW_TASK_PLAN.md`).
|
|
28
|
+
- **Release tooling:** `/dashboard` first-load uncompressed JS bundle budget headroom updated to match current Next.js output (`frontend/scripts/checkRouteBundleBudgets.mjs`).
|
|
29
|
+
|
|
30
|
+
### Packaging
|
|
31
|
+
|
|
32
|
+
- **`[stack]`** extra requires **`lumonox-api>=0.2.1`**.
|
|
33
|
+
|
|
34
|
+
## [0.2.0] - 2026-05-09
|
|
35
|
+
|
|
36
|
+
### Changed
|
|
37
|
+
|
|
38
|
+
- **Branding and packaging:** project and PyPI distributions are **Lumonox** (`lumonox-sdk`, `lumonox-api`); Python packages are **`lumonox`** (SDK) and **`lumonox_backend`** (API). Environment variables use the **`LUMONOX_`** prefix; dashboard static export mounts at **`/lumonox/ui/`**.
|
|
39
|
+
|
|
40
|
+
### Breaking
|
|
41
|
+
|
|
42
|
+
- **Database migrations:** Alembic history is replaced by a **single `initial` revision** that creates the full schema from current ORM models. Existing SQLite dev databases with stale `alembic_version` rows may be **recreated** on startup when migrations cannot resolve the old revision (see `upgrade_to_head` in `lumonox_backend.database.migrations`). Plan Postgres upgrades explicitly (`alembic stamp` / dump-restore) before deploying.
|
|
43
|
+
|
|
44
|
+
### Packaging
|
|
45
|
+
|
|
46
|
+
- **`[stack]`** extra requires **`lumonox-api>=0.2.0`**.
|
|
47
|
+
|
|
48
|
+
## [0.1.4] - 2026-05-08
|
|
49
|
+
|
|
50
|
+
### Packaging
|
|
51
|
+
|
|
52
|
+
- **`[stack]`** extra requires **`lumonox-api>=0.1.5`** (aligned with the current API wheel release train).
|
|
53
|
+
|
|
54
|
+
## [0.1.3] - 2026-05-08
|
|
55
|
+
|
|
56
|
+
### Packaging
|
|
57
|
+
|
|
58
|
+
- **`[stack]`** extra now depends on **`lumonox-api>=0.1.4`** (PyPI name for the API + bundled dashboard).
|
|
59
|
+
|
|
60
|
+
## [0.1.2] - 2026-05-08
|
|
61
|
+
|
|
62
|
+
### Added
|
|
63
|
+
|
|
64
|
+
- Optional extra **`[stack]`**: depends on the API distribution so `pip install "lumonox-sdk[stack]"` installs the API (with bundled dashboard) plus this SDK.
|
|
65
|
+
|
|
66
|
+
### Packaging
|
|
67
|
+
|
|
68
|
+
- **PyPI distribution name** for the SDK remains **`lumonox-sdk`** (import package **`lumonox`**).
|
|
69
|
+
|
|
70
|
+
### Security
|
|
71
|
+
|
|
72
|
+
- **Breaking / privacy:** `monitor()` now defaults `capture_headers` and `capture_query_params` to **off** unless enabled via kwargs or `LUMONOX_CAPTURE_HEADERS` / `LUMONOX_CAPTURE_QUERY_PARAMS`. Reduces accidental PII in events.
|
|
73
|
+
- **Embedded:** if `.env.lumonox` cannot be written, the SDK no longer falls back to a repo-known API key; it uses the generated key from the failed write attempt for that process and logs remediation steps.
|
|
74
|
+
|
|
75
|
+
### Fixed
|
|
76
|
+
|
|
77
|
+
- Middleware tests using a stub dispatcher no longer assume a private `_send_enabled` attribute on arbitrary dispatcher objects (`getattr` fallback).
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Lumonox contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: lumonox-sdk
|
|
3
|
+
Version: 0.2.3
|
|
4
|
+
Summary: FastAPI observability SDK for Lumonox
|
|
5
|
+
Project-URL: Homepage, https://github.com/sintimaski/lumonox
|
|
6
|
+
Project-URL: Repository, https://github.com/sintimaski/lumonox
|
|
7
|
+
Project-URL: Documentation, https://github.com/sintimaski/lumonox/blob/main/sdk/README.md
|
|
8
|
+
Project-URL: Changelog, https://github.com/sintimaski/lumonox/blob/main/sdk/CHANGELOG.md
|
|
9
|
+
Project-URL: Issues, https://github.com/sintimaski/lumonox/issues
|
|
10
|
+
Author: Lumonox maintainers
|
|
11
|
+
License-Expression: MIT
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Keywords: apm,errors,fastapi,monitoring,observability,telemetry
|
|
14
|
+
Classifier: Development Status :: 4 - Beta
|
|
15
|
+
Classifier: Framework :: FastAPI
|
|
16
|
+
Classifier: Intended Audience :: Developers
|
|
17
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Typing :: Typed
|
|
23
|
+
Requires-Python: >=3.11
|
|
24
|
+
Requires-Dist: fastapi>=0.115.0
|
|
25
|
+
Requires-Dist: httpx>=0.27.0
|
|
26
|
+
Requires-Dist: psutil>=6.0.0
|
|
27
|
+
Provides-Extra: stack
|
|
28
|
+
Requires-Dist: lumonox>=0.2.5; extra == 'stack'
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
|
|
31
|
+
# Lumonox SDK (Python)
|
|
32
|
+
|
|
33
|
+
Lumonox SDK instruments a FastAPI app and sends request/error events to Lumonox with safe defaults.
|
|
34
|
+
|
|
35
|
+
## Install
|
|
36
|
+
|
|
37
|
+
| Goal | PyPI one-liner | Import / process |
|
|
38
|
+
|------|-----------------|------------------|
|
|
39
|
+
| **API + bundled dashboard UI** (ingest, dashboard, static export under `/lumonox/ui/`) | `pip install lumonox` | `import lumonox_backend` · run `uvicorn lumonox_backend.main:app` |
|
|
40
|
+
| **Instrument your FastAPI app** (send-only SDK) | `pip install lumonox-sdk` | `from lumonox import lumonox` |
|
|
41
|
+
| **API + UI + SDK in one environment** | `pip install "lumonox-sdk[stack]"` | both of the above |
|
|
42
|
+
|
|
43
|
+
`uv add` works the same (`uv add lumonox`, `uv add "lumonox-sdk[stack]"`, …).
|
|
44
|
+
|
|
45
|
+
**From a git checkout** (offline wheels from repo root):
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
./scripts/build_sdk_release_wheels.sh # writes dist/wheels/*.whl
|
|
49
|
+
pip install dist/wheels/lumonox-*.whl dist/wheels/lumonox_sdk-*.whl
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Publish to PyPI
|
|
53
|
+
|
|
54
|
+
Artifacts are standard **wheel + sdist** from the workspace root (`hatchling` via `uv build`):
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
uv build --package lumonox-sdk -o dist/pypi-sdk
|
|
58
|
+
python -m pip install twine # once
|
|
59
|
+
twine check dist/pypi-sdk/*
|
|
60
|
+
twine upload dist/pypi-sdk/*
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
**Recommended:** use [trusted publishing](https://docs.pypi.org/trusted-publishers/) from GitHub Actions instead of storing a long-lived PyPI token in CI secrets:
|
|
64
|
+
|
|
65
|
+
- **SDK:** `.github/workflows/publish-lumonox-sdk-pypi.yml` → project **`lumonox-sdk`**
|
|
66
|
+
- **API + bundled UI:** `.github/workflows/publish-lumonox-pypi.yml` → project **`lumonox`**
|
|
67
|
+
|
|
68
|
+
**On `main`:** the SDK workflow runs when `sdk/pyproject.toml`, `sdk/src/**`, `sdk/README.md`, or `sdk/LICENSE` change. The **`lumonox`** publish workflow runs when `backend/pyproject.toml`, `backend/src/**`, or `frontend/**` change. Both upload **only when** the corresponding `[project] version` is **not already** on PyPI.
|
|
69
|
+
|
|
70
|
+
**`lumonox-sdk[stack]` on PyPI:** publish **`lumonox`** first (or same release train) so the extra can resolve **`lumonox>=0.2.5`**.
|
|
71
|
+
|
|
72
|
+
**One-time on PyPI:** create projects **`lumonox-sdk`** and **`lumonox`**, add trusted publishers for each workflow, then merge version bumps or run workflows manually.
|
|
73
|
+
|
|
74
|
+
## Integration
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
from fastapi import FastAPI
|
|
78
|
+
from lumonox import lumonox, monitor
|
|
79
|
+
|
|
80
|
+
app = FastAPI()
|
|
81
|
+
lumonox(app) # recommended default
|
|
82
|
+
# monitor(app) # backwards-compatible alias for existing integrations
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
`lumonox()` defaults **`environment` to `development`** so request events match common dashboard server-scope filters. Use `lumonox(app, environment="production")` when you need production labels.
|
|
86
|
+
|
|
87
|
+
By default, `lumonox()` (and `monitor()` for existing integrations) target a remote Lumonox project (set `LUMONOX_API_KEY` / `LUMONOX_INGEST_URL`). It uses bounded in-memory buffering, async background sending, and silent failure behavior so host apps stay healthy if Lumonox is unavailable.
|
|
88
|
+
|
|
89
|
+
If **either** variable is missing, the sender stays off: middleware still runs, but **no events are enqueued** (minimal overhead). A **one-time `WARNING`** is emitted on process startup so misconfiguration is obvious without breaking the host app.
|
|
90
|
+
|
|
91
|
+
## Key runtime controls
|
|
92
|
+
|
|
93
|
+
- `LUMONOX_API_KEY`
|
|
94
|
+
- `LUMONOX_INGEST_URL` (or `LUMONOX_ENDPOINT`)
|
|
95
|
+
- `LUMONOX_FLUSH_INTERVAL_SECONDS`
|
|
96
|
+
- `LUMONOX_BATCH_MAX_EVENTS`
|
|
97
|
+
- `LUMONOX_MAX_QUEUE_SIZE`
|
|
98
|
+
- `LUMONOX_DEBUG`
|
|
99
|
+
- `LUMONOX_REQUEST_SAMPLE_RATE` (`0.0`-`1.0`; default `1.0`)
|
|
100
|
+
- `LUMONOX_IGNORE_PATH_PREFIXES` (comma-separated, default `/health,/ready`)
|
|
101
|
+
- `LUMONOX_CAPTURE_HEADERS` — when `true`/`1`/`yes`, capture request headers in events (default **off** for production-safe privacy).
|
|
102
|
+
- `LUMONOX_CAPTURE_QUERY_PARAMS` — when truthy, capture query parameters (default **off**).
|
|
103
|
+
|
|
104
|
+
`lumonox()` and `monitor()` support explicit kwargs for runtime behavior:
|
|
105
|
+
|
|
106
|
+
- `capture_headers` (default follows `LUMONOX_CAPTURE_HEADERS`, else **False**)
|
|
107
|
+
- `capture_query_params` (default follows `LUMONOX_CAPTURE_QUERY_PARAMS`, else **False**)
|
|
108
|
+
- `request_sample_rate` (float `0.0`-`1.0`, keeps 5xx/error capture unsampled)
|
|
109
|
+
- `ignore_path_prefixes` (tuple/list of path prefixes to skip, e.g. `("/health", "/ready")`)
|
|
110
|
+
- `scrub_keys` (additional sensitive keys to redact)
|
|
111
|
+
- `queue_maxsize`, `batch_size`, `flush_interval_s`, `max_retries`, `retry_backoff_s`
|
|
112
|
+
|
|
113
|
+
## Security defaults
|
|
114
|
+
|
|
115
|
+
- Sensitive keys are scrubbed before send.
|
|
116
|
+
- **Headers and query strings are not captured unless explicitly enabled** (kwargs or env vars above)—opt in when debugging, not in production, unless you accept the PII risk.
|
|
117
|
+
- Middleware captures error context and re-raises original exceptions.
|
|
118
|
+
|
|
119
|
+
## Troubleshooting (“no events”)
|
|
120
|
+
|
|
121
|
+
1. Confirm `LUMONOX_API_KEY` and `LUMONOX_INGEST_URL` are both set for `lumonox()` (see startup `WARNING` when remote send is disabled).
|
|
122
|
+
2. Enable `LUMONOX_DEBUG=1` temporarily to surface queue drops or send failures on stderr.
|
|
123
|
+
3. Check dashboard server-scope filters (environment / service) vs the `environment` and `service_name` fields you send from the SDK.
|
|
124
|
+
4. Dashboard shows traffic but **requests/errors look empty** while DuckDB tools show rows: you likely opened a **different** DuckDB file than the API. Set `LUMONOX_DATA_DIR` or an absolute `LUMONOX_DUCKDB_PATH` on the backend and confirm startup logs (`Startup settings [event_store]: … duckdb_path=…`).
|
|
125
|
+
|
|
126
|
+
Canonical behavior and constraints are defined in `DEVELOPMENT.md`.
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# Lumonox SDK (Python)
|
|
2
|
+
|
|
3
|
+
Lumonox SDK instruments a FastAPI app and sends request/error events to Lumonox with safe defaults.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
| Goal | PyPI one-liner | Import / process |
|
|
8
|
+
|------|-----------------|------------------|
|
|
9
|
+
| **API + bundled dashboard UI** (ingest, dashboard, static export under `/lumonox/ui/`) | `pip install lumonox` | `import lumonox_backend` · run `uvicorn lumonox_backend.main:app` |
|
|
10
|
+
| **Instrument your FastAPI app** (send-only SDK) | `pip install lumonox-sdk` | `from lumonox import lumonox` |
|
|
11
|
+
| **API + UI + SDK in one environment** | `pip install "lumonox-sdk[stack]"` | both of the above |
|
|
12
|
+
|
|
13
|
+
`uv add` works the same (`uv add lumonox`, `uv add "lumonox-sdk[stack]"`, …).
|
|
14
|
+
|
|
15
|
+
**From a git checkout** (offline wheels from repo root):
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
./scripts/build_sdk_release_wheels.sh # writes dist/wheels/*.whl
|
|
19
|
+
pip install dist/wheels/lumonox-*.whl dist/wheels/lumonox_sdk-*.whl
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Publish to PyPI
|
|
23
|
+
|
|
24
|
+
Artifacts are standard **wheel + sdist** from the workspace root (`hatchling` via `uv build`):
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
uv build --package lumonox-sdk -o dist/pypi-sdk
|
|
28
|
+
python -m pip install twine # once
|
|
29
|
+
twine check dist/pypi-sdk/*
|
|
30
|
+
twine upload dist/pypi-sdk/*
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
**Recommended:** use [trusted publishing](https://docs.pypi.org/trusted-publishers/) from GitHub Actions instead of storing a long-lived PyPI token in CI secrets:
|
|
34
|
+
|
|
35
|
+
- **SDK:** `.github/workflows/publish-lumonox-sdk-pypi.yml` → project **`lumonox-sdk`**
|
|
36
|
+
- **API + bundled UI:** `.github/workflows/publish-lumonox-pypi.yml` → project **`lumonox`**
|
|
37
|
+
|
|
38
|
+
**On `main`:** the SDK workflow runs when `sdk/pyproject.toml`, `sdk/src/**`, `sdk/README.md`, or `sdk/LICENSE` change. The **`lumonox`** publish workflow runs when `backend/pyproject.toml`, `backend/src/**`, or `frontend/**` change. Both upload **only when** the corresponding `[project] version` is **not already** on PyPI.
|
|
39
|
+
|
|
40
|
+
**`lumonox-sdk[stack]` on PyPI:** publish **`lumonox`** first (or same release train) so the extra can resolve **`lumonox>=0.2.5`**.
|
|
41
|
+
|
|
42
|
+
**One-time on PyPI:** create projects **`lumonox-sdk`** and **`lumonox`**, add trusted publishers for each workflow, then merge version bumps or run workflows manually.
|
|
43
|
+
|
|
44
|
+
## Integration
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
from fastapi import FastAPI
|
|
48
|
+
from lumonox import lumonox, monitor
|
|
49
|
+
|
|
50
|
+
app = FastAPI()
|
|
51
|
+
lumonox(app) # recommended default
|
|
52
|
+
# monitor(app) # backwards-compatible alias for existing integrations
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
`lumonox()` defaults **`environment` to `development`** so request events match common dashboard server-scope filters. Use `lumonox(app, environment="production")` when you need production labels.
|
|
56
|
+
|
|
57
|
+
By default, `lumonox()` (and `monitor()` for existing integrations) target a remote Lumonox project (set `LUMONOX_API_KEY` / `LUMONOX_INGEST_URL`). It uses bounded in-memory buffering, async background sending, and silent failure behavior so host apps stay healthy if Lumonox is unavailable.
|
|
58
|
+
|
|
59
|
+
If **either** variable is missing, the sender stays off: middleware still runs, but **no events are enqueued** (minimal overhead). A **one-time `WARNING`** is emitted on process startup so misconfiguration is obvious without breaking the host app.
|
|
60
|
+
|
|
61
|
+
## Key runtime controls
|
|
62
|
+
|
|
63
|
+
- `LUMONOX_API_KEY`
|
|
64
|
+
- `LUMONOX_INGEST_URL` (or `LUMONOX_ENDPOINT`)
|
|
65
|
+
- `LUMONOX_FLUSH_INTERVAL_SECONDS`
|
|
66
|
+
- `LUMONOX_BATCH_MAX_EVENTS`
|
|
67
|
+
- `LUMONOX_MAX_QUEUE_SIZE`
|
|
68
|
+
- `LUMONOX_DEBUG`
|
|
69
|
+
- `LUMONOX_REQUEST_SAMPLE_RATE` (`0.0`-`1.0`; default `1.0`)
|
|
70
|
+
- `LUMONOX_IGNORE_PATH_PREFIXES` (comma-separated, default `/health,/ready`)
|
|
71
|
+
- `LUMONOX_CAPTURE_HEADERS` — when `true`/`1`/`yes`, capture request headers in events (default **off** for production-safe privacy).
|
|
72
|
+
- `LUMONOX_CAPTURE_QUERY_PARAMS` — when truthy, capture query parameters (default **off**).
|
|
73
|
+
|
|
74
|
+
`lumonox()` and `monitor()` support explicit kwargs for runtime behavior:
|
|
75
|
+
|
|
76
|
+
- `capture_headers` (default follows `LUMONOX_CAPTURE_HEADERS`, else **False**)
|
|
77
|
+
- `capture_query_params` (default follows `LUMONOX_CAPTURE_QUERY_PARAMS`, else **False**)
|
|
78
|
+
- `request_sample_rate` (float `0.0`-`1.0`, keeps 5xx/error capture unsampled)
|
|
79
|
+
- `ignore_path_prefixes` (tuple/list of path prefixes to skip, e.g. `("/health", "/ready")`)
|
|
80
|
+
- `scrub_keys` (additional sensitive keys to redact)
|
|
81
|
+
- `queue_maxsize`, `batch_size`, `flush_interval_s`, `max_retries`, `retry_backoff_s`
|
|
82
|
+
|
|
83
|
+
## Security defaults
|
|
84
|
+
|
|
85
|
+
- Sensitive keys are scrubbed before send.
|
|
86
|
+
- **Headers and query strings are not captured unless explicitly enabled** (kwargs or env vars above)—opt in when debugging, not in production, unless you accept the PII risk.
|
|
87
|
+
- Middleware captures error context and re-raises original exceptions.
|
|
88
|
+
|
|
89
|
+
## Troubleshooting (“no events”)
|
|
90
|
+
|
|
91
|
+
1. Confirm `LUMONOX_API_KEY` and `LUMONOX_INGEST_URL` are both set for `lumonox()` (see startup `WARNING` when remote send is disabled).
|
|
92
|
+
2. Enable `LUMONOX_DEBUG=1` temporarily to surface queue drops or send failures on stderr.
|
|
93
|
+
3. Check dashboard server-scope filters (environment / service) vs the `environment` and `service_name` fields you send from the SDK.
|
|
94
|
+
4. Dashboard shows traffic but **requests/errors look empty** while DuckDB tools show rows: you likely opened a **different** DuckDB file than the API. Set `LUMONOX_DATA_DIR` or an absolute `LUMONOX_DUCKDB_PATH` on the backend and confirm startup logs (`Startup settings [event_store]: … duckdb_path=…`).
|
|
95
|
+
|
|
96
|
+
Canonical behavior and constraints are defined in `DEVELOPMENT.md`.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "lumonox-sdk"
|
|
3
|
+
version = "0.2.3"
|
|
4
|
+
description = "FastAPI observability SDK for Lumonox"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
requires-python = ">=3.11"
|
|
8
|
+
authors = [{ name = "Lumonox maintainers" }]
|
|
9
|
+
keywords = ["fastapi", "observability", "monitoring", "apm", "errors", "telemetry"]
|
|
10
|
+
classifiers = [
|
|
11
|
+
"Development Status :: 4 - Beta",
|
|
12
|
+
"Intended Audience :: Developers",
|
|
13
|
+
"License :: OSI Approved :: MIT License",
|
|
14
|
+
"Programming Language :: Python :: 3",
|
|
15
|
+
"Programming Language :: Python :: 3.11",
|
|
16
|
+
"Programming Language :: Python :: 3.12",
|
|
17
|
+
"Programming Language :: Python :: 3.13",
|
|
18
|
+
"Framework :: FastAPI",
|
|
19
|
+
"Typing :: Typed",
|
|
20
|
+
]
|
|
21
|
+
dependencies = [
|
|
22
|
+
"fastapi>=0.115.0",
|
|
23
|
+
"httpx>=0.27.0",
|
|
24
|
+
"psutil>=6.0.0",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
# ``pip install "lumonox-sdk[stack]"`` pulls the API + bundled dashboard (PyPI: ``lumonox``) alongside this SDK.
|
|
28
|
+
[project.optional-dependencies]
|
|
29
|
+
stack = ["lumonox>=0.2.5"]
|
|
30
|
+
|
|
31
|
+
[project.urls]
|
|
32
|
+
Homepage = "https://github.com/sintimaski/lumonox"
|
|
33
|
+
Repository = "https://github.com/sintimaski/lumonox"
|
|
34
|
+
Documentation = "https://github.com/sintimaski/lumonox/blob/main/sdk/README.md"
|
|
35
|
+
Changelog = "https://github.com/sintimaski/lumonox/blob/main/sdk/CHANGELOG.md"
|
|
36
|
+
Issues = "https://github.com/sintimaski/lumonox/issues"
|
|
37
|
+
|
|
38
|
+
[build-system]
|
|
39
|
+
requires = ["hatchling>=1.26.0"]
|
|
40
|
+
build-backend = "hatchling.build"
|
|
41
|
+
|
|
42
|
+
[tool.hatch.build.targets.wheel]
|
|
43
|
+
packages = ["src/lumonox"]
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""Lumonox SDK: FastAPI observability integration."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
from lumonox._jobs import capture_background_job
|
|
6
|
+
from lumonox._monitor import monitor
|
|
7
|
+
from lumonox.widgets import (
|
|
8
|
+
BarChartWidget,
|
|
9
|
+
BaseDashboardWidget,
|
|
10
|
+
CardWidget,
|
|
11
|
+
DonutChartWidget,
|
|
12
|
+
HistogramWidget,
|
|
13
|
+
LineChartWidget,
|
|
14
|
+
ScatterPlotWidget,
|
|
15
|
+
StackedAreaWidget,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def lumonox(app: object, **kwargs: object) -> None:
|
|
20
|
+
"""One-line FastAPI setup: remote ingest from env, or ``kwargs``.
|
|
21
|
+
|
|
22
|
+
Reads ``LUMONOX_MODE`` when ``mode`` is not passed: ``remote`` (default), or
|
|
23
|
+
``off`` / ``false`` / ``0`` / ``no`` / ``none`` / ``disabled`` to skip instrumentation.
|
|
24
|
+
``embedded`` is treated as ``remote`` (embedded mode was removed).
|
|
25
|
+
Sets ``environment`` to ``development`` when omitted (matches common dashboard filters).
|
|
26
|
+
Honors ``LUMONOX_SERVICE_NAME`` when ``service_name`` is not passed.
|
|
27
|
+
"""
|
|
28
|
+
options = dict(kwargs)
|
|
29
|
+
if "mode" not in options:
|
|
30
|
+
raw = os.getenv("LUMONOX_MODE", "remote").strip().lower()
|
|
31
|
+
if raw in {"off", "false", "0", "no", "none", "disabled"}:
|
|
32
|
+
return
|
|
33
|
+
options["mode"] = "remote"
|
|
34
|
+
if "environment" not in options:
|
|
35
|
+
options["environment"] = "development"
|
|
36
|
+
if "service_name" not in options:
|
|
37
|
+
service_name = os.getenv("LUMONOX_SERVICE_NAME", "").strip()
|
|
38
|
+
if service_name:
|
|
39
|
+
options["service_name"] = service_name
|
|
40
|
+
monitor(app, **options)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
__all__ = [
|
|
44
|
+
"monitor",
|
|
45
|
+
"lumonox",
|
|
46
|
+
"capture_background_job",
|
|
47
|
+
"BaseDashboardWidget",
|
|
48
|
+
"CardWidget",
|
|
49
|
+
"LineChartWidget",
|
|
50
|
+
"BarChartWidget",
|
|
51
|
+
"DonutChartWidget",
|
|
52
|
+
"HistogramWidget",
|
|
53
|
+
"ScatterPlotWidget",
|
|
54
|
+
"StackedAreaWidget",
|
|
55
|
+
]
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from time import monotonic
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
_LOG = logging.getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
psutil: Any
|
|
11
|
+
try:
|
|
12
|
+
import psutil as _psutil
|
|
13
|
+
except Exception: # pragma: no cover - optional runtime dependency
|
|
14
|
+
psutil = None
|
|
15
|
+
else:
|
|
16
|
+
psutil = _psutil
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(slots=True)
|
|
20
|
+
class InfrastructureSampler:
|
|
21
|
+
ttl_seconds: float = 2.0
|
|
22
|
+
_last_sample: dict[str, Any] = field(default_factory=dict)
|
|
23
|
+
_last_sampled_at: float = 0.0
|
|
24
|
+
_warned_sampling_error: bool = False
|
|
25
|
+
|
|
26
|
+
def sample(self) -> dict[str, Any]:
|
|
27
|
+
if psutil is None:
|
|
28
|
+
return {}
|
|
29
|
+
now = monotonic()
|
|
30
|
+
if self._last_sample and (now - self._last_sampled_at) < self.ttl_seconds:
|
|
31
|
+
return dict(self._last_sample)
|
|
32
|
+
|
|
33
|
+
try:
|
|
34
|
+
process = psutil.Process()
|
|
35
|
+
vm = psutil.virtual_memory()
|
|
36
|
+
disk = psutil.disk_usage("/")
|
|
37
|
+
disk_io = psutil.disk_io_counters()
|
|
38
|
+
net = psutil.net_io_counters()
|
|
39
|
+
payload: dict[str, Any] = {
|
|
40
|
+
"host_cpu_percent": float(psutil.cpu_percent(interval=None)),
|
|
41
|
+
"host_memory_used_percent": float(vm.percent),
|
|
42
|
+
"host_memory_total_bytes": float(vm.total),
|
|
43
|
+
"host_memory_used_bytes": float(vm.used),
|
|
44
|
+
"process_cpu_percent": float(process.cpu_percent(interval=None)),
|
|
45
|
+
"process_memory_percent": float(process.memory_percent()),
|
|
46
|
+
"process_memory_rss_bytes": float(process.memory_info().rss),
|
|
47
|
+
"disk_used_percent": float(disk.percent),
|
|
48
|
+
"disk_total_bytes": float(disk.total),
|
|
49
|
+
"disk_used_bytes": float(disk.used),
|
|
50
|
+
"disk_io_read_bytes": float(disk_io.read_bytes if disk_io is not None else 0.0),
|
|
51
|
+
"disk_io_write_bytes": float(disk_io.write_bytes if disk_io is not None else 0.0),
|
|
52
|
+
"network_bytes_sent": float(net.bytes_sent),
|
|
53
|
+
"network_bytes_recv": float(net.bytes_recv),
|
|
54
|
+
}
|
|
55
|
+
except Exception as exc:
|
|
56
|
+
if not self._warned_sampling_error:
|
|
57
|
+
_LOG.warning(
|
|
58
|
+
"lumonox infrastructure sampling disabled for this process: %s",
|
|
59
|
+
exc,
|
|
60
|
+
)
|
|
61
|
+
self._warned_sampling_error = True
|
|
62
|
+
return {}
|
|
63
|
+
self._last_sample = payload
|
|
64
|
+
self._last_sampled_at = now
|
|
65
|
+
return dict(payload)
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Optional background job / cron outcome capture (non-blocking)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import traceback
|
|
6
|
+
from datetime import UTC, datetime
|
|
7
|
+
from typing import Any, Literal
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def capture_background_job(
|
|
11
|
+
app: Any,
|
|
12
|
+
*,
|
|
13
|
+
name: str,
|
|
14
|
+
success: bool,
|
|
15
|
+
latency_ms: float = 0.0,
|
|
16
|
+
trigger: Literal["job", "cron"] = "job",
|
|
17
|
+
correlated_request_id: str | None = None,
|
|
18
|
+
exception: BaseException | None = None,
|
|
19
|
+
) -> None:
|
|
20
|
+
"""Record a completed background task as a ``type=job`` ingest row (silent if not configured).
|
|
21
|
+
|
|
22
|
+
Requires ``monitor()`` / ``lumonox()`` to have been applied on ``app`` so
|
|
23
|
+
``app.state._lumonox_dispatcher`` exists. Never raises to the caller.
|
|
24
|
+
"""
|
|
25
|
+
try:
|
|
26
|
+
dispatcher = getattr(app.state, "_lumonox_dispatcher", None)
|
|
27
|
+
config = getattr(app.state, "_lumonox_config", None)
|
|
28
|
+
if dispatcher is None or config is None:
|
|
29
|
+
return
|
|
30
|
+
raw_name = str(name or "").strip() or "background_task"
|
|
31
|
+
path = raw_name[:2048]
|
|
32
|
+
method = "CRON" if trigger == "cron" else "JOB"
|
|
33
|
+
ok = bool(success) and exception is None
|
|
34
|
+
status_code = 200 if ok else 500
|
|
35
|
+
payload: dict[str, Any] = {
|
|
36
|
+
"type": "job",
|
|
37
|
+
"timestamp": datetime.now(tz=UTC).isoformat(),
|
|
38
|
+
"service_name": str(getattr(config, "service_name", "api") or "api")[:120],
|
|
39
|
+
"environment": str(getattr(config, "environment", "production") or "production")[:120],
|
|
40
|
+
"method": method,
|
|
41
|
+
"path": path,
|
|
42
|
+
"status_code": status_code,
|
|
43
|
+
"latency_ms": max(0.0, float(latency_ms)),
|
|
44
|
+
"job_trigger": trigger,
|
|
45
|
+
}
|
|
46
|
+
if correlated_request_id and str(correlated_request_id).strip():
|
|
47
|
+
payload["correlated_request_id"] = str(correlated_request_id).strip()[:128]
|
|
48
|
+
payload["request_id"] = str(correlated_request_id).strip()[:128]
|
|
49
|
+
if exception is not None:
|
|
50
|
+
payload["exception_type"] = type(exception).__name__
|
|
51
|
+
msg = str(exception).strip()
|
|
52
|
+
if msg:
|
|
53
|
+
payload["exception_message"] = msg[:4000]
|
|
54
|
+
stack = traceback.format_exception(type(exception), exception, exception.__traceback__)
|
|
55
|
+
payload["stack_trace"] = "".join(stack)[-16_000:]
|
|
56
|
+
dispatcher.enqueue(payload)
|
|
57
|
+
except Exception:
|
|
58
|
+
return
|