logsetu 0.3.0__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.
- logsetu-0.3.0/.gitignore +49 -0
- logsetu-0.3.0/LICENSE +21 -0
- logsetu-0.3.0/PKG-INFO +226 -0
- logsetu-0.3.0/README.md +187 -0
- logsetu-0.3.0/pyproject.toml +49 -0
- logsetu-0.3.0/src/logsetu/__init__.py +41 -0
- logsetu-0.3.0/src/logsetu/apps.py +6 -0
- logsetu-0.3.0/src/logsetu/asgi.py +151 -0
- logsetu-0.3.0/src/logsetu/client.py +250 -0
- logsetu-0.3.0/src/logsetu/conf.py +41 -0
- logsetu-0.3.0/src/logsetu/context.py +36 -0
- logsetu-0.3.0/src/logsetu/fastapi.py +11 -0
- logsetu-0.3.0/src/logsetu/flask.py +141 -0
- logsetu-0.3.0/src/logsetu/handler.py +191 -0
- logsetu-0.3.0/src/logsetu/middleware.py +186 -0
- logsetu-0.3.0/src/logsetu/py.typed +0 -0
- logsetu-0.3.0/src/logsetu/web.py +71 -0
- logsetu-0.3.0/tests/__init__.py +0 -0
- logsetu-0.3.0/tests/conftest.py +28 -0
- logsetu-0.3.0/tests/fakeserver.py +47 -0
- logsetu-0.3.0/tests/settings.py +18 -0
- logsetu-0.3.0/tests/test_client.py +95 -0
- logsetu-0.3.0/tests/test_django.py +149 -0
- logsetu-0.3.0/tests/test_flask_asgi.py +154 -0
- logsetu-0.3.0/tests/urls.py +22 -0
logsetu-0.3.0/.gitignore
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# deps
|
|
2
|
+
node_modules/
|
|
3
|
+
.pnpm-store/
|
|
4
|
+
|
|
5
|
+
# builds
|
|
6
|
+
dist/
|
|
7
|
+
.next/
|
|
8
|
+
out/
|
|
9
|
+
.turbo/
|
|
10
|
+
*.tsbuildinfo
|
|
11
|
+
apps/server/generated/
|
|
12
|
+
|
|
13
|
+
# python
|
|
14
|
+
__pycache__/
|
|
15
|
+
*.py[cod]
|
|
16
|
+
*.egg-info/
|
|
17
|
+
build/
|
|
18
|
+
.venv/
|
|
19
|
+
venv/
|
|
20
|
+
.pytest_cache/
|
|
21
|
+
.mypy_cache/
|
|
22
|
+
.ruff_cache/
|
|
23
|
+
|
|
24
|
+
# env / secrets
|
|
25
|
+
.env
|
|
26
|
+
.env.*
|
|
27
|
+
!.env.example
|
|
28
|
+
|
|
29
|
+
# data
|
|
30
|
+
data/
|
|
31
|
+
*.db
|
|
32
|
+
*.db-journal
|
|
33
|
+
*.sqlite
|
|
34
|
+
*.sqlite3
|
|
35
|
+
*.sqlite-*
|
|
36
|
+
|
|
37
|
+
# editors / os
|
|
38
|
+
.idea/
|
|
39
|
+
.vscode/
|
|
40
|
+
.DS_Store
|
|
41
|
+
Thumbs.db
|
|
42
|
+
|
|
43
|
+
# logs
|
|
44
|
+
*.log
|
|
45
|
+
npm-debug.log*
|
|
46
|
+
pnpm-debug.log*
|
|
47
|
+
|
|
48
|
+
# generated by next
|
|
49
|
+
next-env.d.ts
|
logsetu-0.3.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Raj Verma
|
|
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.
|
logsetu-0.3.0/PKG-INFO
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: logsetu
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: LogSetu SDK for Python — Django, DRF, Flask and FastAPI: ship logging records and request errors to your self-hosted LogSetu server
|
|
5
|
+
Project-URL: Homepage, https://github.com/itsrajverma/logsetu
|
|
6
|
+
Project-URL: Documentation, https://github.com/itsrajverma/logsetu/tree/main/docs
|
|
7
|
+
Project-URL: Repository, https://github.com/itsrajverma/logsetu
|
|
8
|
+
Project-URL: Issues, https://github.com/itsrajverma/logsetu/issues
|
|
9
|
+
Author: Raj Verma
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: asgi,django,drf,error-tracking,fastapi,flask,logging,logsetu,python,self-hosted,sentry-alternative
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Framework :: Django
|
|
15
|
+
Classifier: Framework :: FastAPI
|
|
16
|
+
Classifier: Framework :: Flask
|
|
17
|
+
Classifier: Intended Audience :: Developers
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
20
|
+
Classifier: Topic :: System :: Logging
|
|
21
|
+
Classifier: Typing :: Typed
|
|
22
|
+
Requires-Python: >=3.9
|
|
23
|
+
Provides-Extra: dev
|
|
24
|
+
Requires-Dist: build; extra == 'dev'
|
|
25
|
+
Requires-Dist: django>=3.2; extra == 'dev'
|
|
26
|
+
Requires-Dist: fastapi>=0.95; extra == 'dev'
|
|
27
|
+
Requires-Dist: flask>=2.2; extra == 'dev'
|
|
28
|
+
Requires-Dist: httpx; extra == 'dev'
|
|
29
|
+
Requires-Dist: pytest-django>=4.8; extra == 'dev'
|
|
30
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
31
|
+
Requires-Dist: twine; extra == 'dev'
|
|
32
|
+
Provides-Extra: django
|
|
33
|
+
Requires-Dist: django>=3.2; extra == 'django'
|
|
34
|
+
Provides-Extra: fastapi
|
|
35
|
+
Requires-Dist: fastapi>=0.95; extra == 'fastapi'
|
|
36
|
+
Provides-Extra: flask
|
|
37
|
+
Requires-Dist: flask>=2.2; extra == 'flask'
|
|
38
|
+
Description-Content-Type: text/markdown
|
|
39
|
+
|
|
40
|
+
# logsetu (Python)
|
|
41
|
+
|
|
42
|
+
Ship Django / DRF, **Flask** and **FastAPI** logs and unhandled errors to your self-hosted [LogSetu](https://github.com/itsrajverma/logsetu) server.
|
|
43
|
+
Zero runtime dependencies, non-blocking (background thread + batching), and it never crashes your app if the log server is down.
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
pip install "logsetu[django]" # Django / DRF
|
|
47
|
+
pip install "logsetu[flask]" # Flask
|
|
48
|
+
pip install "logsetu[fastapi]" # FastAPI / Starlette / any ASGI app
|
|
49
|
+
pip install logsetu # just the client + logging handler (zero dependencies)
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
The extras only pull in the framework itself; the SDK has no runtime dependencies. Integrations:
|
|
53
|
+
[Django](#setup-2-minutes), [Flask](#flask) and [FastAPI / any ASGI app](#fastapi--starlette--asgi).
|
|
54
|
+
|
|
55
|
+
> **Coming from `logsetu-django`?** The package was renamed to `logsetu` in 0.3.0. The import name (`import logsetu`)
|
|
56
|
+
> and all settings are unchanged. Switch with `pip uninstall -y logsetu-django && pip install "logsetu[django]"` and
|
|
57
|
+
> update your requirements file. Don't keep both installed: they contain the same `logsetu` module. `logsetu-django`
|
|
58
|
+
> is still published with identical code for existing installs.
|
|
59
|
+
|
|
60
|
+
## Setup (2 minutes)
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
# settings.py
|
|
64
|
+
INSTALLED_APPS = [..., "logsetu"] # optional, but nice for discoverability
|
|
65
|
+
|
|
66
|
+
LOGGING = {
|
|
67
|
+
"version": 1,
|
|
68
|
+
"disable_existing_loggers": False,
|
|
69
|
+
"handlers": {
|
|
70
|
+
"logsetu": {
|
|
71
|
+
"class": "logsetu.handler.LogSetuHandler",
|
|
72
|
+
"api_key": env("LOGSETU_API_KEY"), # from the LogSetu dashboard → Projects
|
|
73
|
+
"endpoint": env("LOGSETU_ENDPOINT"), # e.g. https://logs.example.com
|
|
74
|
+
"environment": env("ENVIRONMENT", default="production"),
|
|
75
|
+
"source": "django-backend",
|
|
76
|
+
"level": "WARNING",
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
"root": {"handlers": ["logsetu"], "level": "INFO"},
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
MIDDLEWARE = [
|
|
83
|
+
...,
|
|
84
|
+
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
|
85
|
+
"logsetu.middleware.LogSetuMiddleware", # after auth middleware so user ids are captured
|
|
86
|
+
]
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
That's it. Every `logging` call at or above the handler's level is sent to LogSetu, and the middleware
|
|
90
|
+
reports unhandled exceptions (and 5xx responses) with the request path, method, view, user, redacted headers and full traceback.
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
import logging
|
|
94
|
+
log = logging.getLogger(__name__)
|
|
95
|
+
|
|
96
|
+
log.warning("Checkout is slow", extra={"duration_ms": 1830, "cart_items": 12})
|
|
97
|
+
try:
|
|
98
|
+
charge(card)
|
|
99
|
+
except PaymentError:
|
|
100
|
+
log.exception("Payment failed") # traceback is attached automatically
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Multi-tenant apps
|
|
104
|
+
|
|
105
|
+
Tag every log with the current tenant by pointing the SDK at a callable `(request) -> str | None`:
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
# settings.py
|
|
109
|
+
LOGGING["handlers"]["logsetu"]["get_tenant_id"] = "myapp.tenants.get_tenant_id"
|
|
110
|
+
|
|
111
|
+
# myapp/tenants.py
|
|
112
|
+
def get_tenant_id(request):
|
|
113
|
+
return getattr(request, "tenant", None) and request.tenant.slug
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Or, from anywhere inside a request (e.g. after you resolve the tenant in your own middleware):
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
from logsetu import update_request_context
|
|
120
|
+
update_request_context(tenant_id=tenant.slug, plan=tenant.plan)
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## Handler options
|
|
124
|
+
|
|
125
|
+
| Option | Default | Description |
|
|
126
|
+
|---|---|---|
|
|
127
|
+
| `api_key` / `endpoint` | — | Required (or set `LOGSETU = {"API_KEY": ..., "ENDPOINT": ...}` in settings) |
|
|
128
|
+
| `environment` | `"production"` | Tag on every log |
|
|
129
|
+
| `source` | `"django"` | Tag on every log; override per record with `extra={"source": "celery"}` |
|
|
130
|
+
| `get_tenant_id` | `None` | Dotted path or callable `(request) -> str` |
|
|
131
|
+
| `include_extra` | `True` | Forward `extra={...}` fields as metadata |
|
|
132
|
+
| `include_request_context` | `True` | Attach request id / path / method / tenant set by the middleware |
|
|
133
|
+
| `flush_interval` | `2.0` | Seconds between background flushes |
|
|
134
|
+
| `batch_size` | `10` | Flush as soon as this many logs are queued |
|
|
135
|
+
| `max_queue_size` | `1000` | Drop oldest logs beyond this |
|
|
136
|
+
| `max_retries` | `3` | Retries with exponential backoff on 5xx / network errors |
|
|
137
|
+
| `timeout` | `5.0` | HTTP timeout in seconds |
|
|
138
|
+
| `debug` | `False` | Print every transport failure to stderr (otherwise only the first) |
|
|
139
|
+
|
|
140
|
+
## Middleware options (`settings.LOGSETU`)
|
|
141
|
+
|
|
142
|
+
The middleware reuses the client and options of a configured `LogSetuHandler`. Without a handler it can run standalone:
|
|
143
|
+
|
|
144
|
+
```python
|
|
145
|
+
LOGSETU = {
|
|
146
|
+
"API_KEY": env("LOGSETU_API_KEY"),
|
|
147
|
+
"ENDPOINT": env("LOGSETU_ENDPOINT"),
|
|
148
|
+
"ENVIRONMENT": "production",
|
|
149
|
+
"SOURCE": "django-backend",
|
|
150
|
+
"GET_TENANT_ID": "myapp.tenants.get_tenant_id",
|
|
151
|
+
"REDACT_HEADERS": ["authorization", "cookie", "x-api-key"], # defaults already cover these
|
|
152
|
+
"CAPTURE_5XX": True, # also report 5xx responses that didn't raise
|
|
153
|
+
}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Every request gets a `request_id` (taken from `X-Request-ID` if present, otherwise generated) which is
|
|
157
|
+
attached to all logs emitted during that request and exposed as `request.logsetu_request_id`.
|
|
158
|
+
|
|
159
|
+
## Guarantees
|
|
160
|
+
|
|
161
|
+
- **Never blocks** the request cycle: `emit()` only appends to an in-memory queue.
|
|
162
|
+
- **Never raises** into your app: transport errors are printed once to stderr, then suppressed.
|
|
163
|
+
- **Never leaks secrets**: `Authorization`, `Cookie`, `X-Api-Key`, `X-CSRFToken` and `Proxy-Authorization` are redacted.
|
|
164
|
+
- **Never loses the last logs** on shutdown: the queue is flushed via `atexit`.
|
|
165
|
+
|
|
166
|
+
## Celery / management commands
|
|
167
|
+
|
|
168
|
+
Nothing special — the handler works anywhere Python's `logging` does. Set `extra={"source": "celery"}` to distinguish
|
|
169
|
+
workers, or configure a second handler with a different `source`.
|
|
170
|
+
|
|
171
|
+
## Flask
|
|
172
|
+
|
|
173
|
+
```python
|
|
174
|
+
import os
|
|
175
|
+
from flask import Flask
|
|
176
|
+
from logsetu.flask import LogSetu
|
|
177
|
+
|
|
178
|
+
app = Flask(__name__)
|
|
179
|
+
LogSetu(app, api_key=os.environ["LOGSETU_API_KEY"], endpoint=os.environ["LOGSETU_ENDPOINT"], source="flask-api")
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
App factory? Create `logsetu = LogSetu()` at module level and call `logsetu.init_app(app)`; options you don't pass are
|
|
183
|
+
read from `app.config` (`LOGSETU_API_KEY`, `LOGSETU_ENDPOINT`, `LOGSETU_ENVIRONMENT`, `LOGSETU_SOURCE`).
|
|
184
|
+
|
|
185
|
+
The extension reports unhandled exceptions and 5xx responses (route, endpoint, redacted headers, traceback), and attaches
|
|
186
|
+
a `LogSetuHandler` to the root logger so `logging.warning(...)` and above is shipped with the request id, method and
|
|
187
|
+
path of the current request. Flask's own `"Exception on /path"` log line is de-duplicated.
|
|
188
|
+
|
|
189
|
+
| Option | Default | Description |
|
|
190
|
+
|---|---|---|
|
|
191
|
+
| `api_key` / `endpoint` | `app.config` | Project API key and server URL |
|
|
192
|
+
| `environment` / `source` | `"production"` / `"flask"` | Tags on every log |
|
|
193
|
+
| `get_tenant_id` | — | `callable(request) -> str \| None` or dotted path |
|
|
194
|
+
| `capture_logging` | `True` | Attach a handler to the root logger |
|
|
195
|
+
| `log_level` | `logging.WARNING` | Minimum level forwarded from `logging` |
|
|
196
|
+
| `capture_5xx` | `True` | Report 5xx responses that didn't raise |
|
|
197
|
+
| `log_requests` | `False` | One info/warn log per request |
|
|
198
|
+
| `redact_headers` | auth / cookie / API-key headers | Header names replaced by `[REDACTED]` |
|
|
199
|
+
| `client` | — | Use an existing `LogSetuClient` |
|
|
200
|
+
|
|
201
|
+
## FastAPI / Starlette / ASGI
|
|
202
|
+
|
|
203
|
+
```python
|
|
204
|
+
import os
|
|
205
|
+
from fastapi import FastAPI
|
|
206
|
+
from logsetu.fastapi import LogSetuMiddleware # alias of logsetu.asgi — works with any ASGI app
|
|
207
|
+
|
|
208
|
+
app = FastAPI()
|
|
209
|
+
app.add_middleware(
|
|
210
|
+
LogSetuMiddleware,
|
|
211
|
+
api_key=os.environ["LOGSETU_API_KEY"],
|
|
212
|
+
endpoint=os.environ["LOGSETU_ENDPOINT"],
|
|
213
|
+
source="fastapi",
|
|
214
|
+
ignore_paths=["/healthz"],
|
|
215
|
+
)
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
A pure ASGI middleware (streaming responses aren't buffered). Same options as Flask plus `ignore_paths`;
|
|
219
|
+
`get_tenant_id` receives the ASGI `scope`, and `api_key` / `endpoint` / `environment` / `source` fall back to the
|
|
220
|
+
`LOGSETU_*` environment variables. Request context reaches logs from both `async def` endpoints and sync endpoints
|
|
221
|
+
running in the threadpool. `HTTPException`s with 4xx status codes aren't reported as errors. The request id is
|
|
222
|
+
available as `request.state.logsetu_request_id`.
|
|
223
|
+
|
|
224
|
+
## License
|
|
225
|
+
|
|
226
|
+
MIT
|
logsetu-0.3.0/README.md
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
# logsetu (Python)
|
|
2
|
+
|
|
3
|
+
Ship Django / DRF, **Flask** and **FastAPI** logs and unhandled errors to your self-hosted [LogSetu](https://github.com/itsrajverma/logsetu) server.
|
|
4
|
+
Zero runtime dependencies, non-blocking (background thread + batching), and it never crashes your app if the log server is down.
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
pip install "logsetu[django]" # Django / DRF
|
|
8
|
+
pip install "logsetu[flask]" # Flask
|
|
9
|
+
pip install "logsetu[fastapi]" # FastAPI / Starlette / any ASGI app
|
|
10
|
+
pip install logsetu # just the client + logging handler (zero dependencies)
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
The extras only pull in the framework itself; the SDK has no runtime dependencies. Integrations:
|
|
14
|
+
[Django](#setup-2-minutes), [Flask](#flask) and [FastAPI / any ASGI app](#fastapi--starlette--asgi).
|
|
15
|
+
|
|
16
|
+
> **Coming from `logsetu-django`?** The package was renamed to `logsetu` in 0.3.0. The import name (`import logsetu`)
|
|
17
|
+
> and all settings are unchanged. Switch with `pip uninstall -y logsetu-django && pip install "logsetu[django]"` and
|
|
18
|
+
> update your requirements file. Don't keep both installed: they contain the same `logsetu` module. `logsetu-django`
|
|
19
|
+
> is still published with identical code for existing installs.
|
|
20
|
+
|
|
21
|
+
## Setup (2 minutes)
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
# settings.py
|
|
25
|
+
INSTALLED_APPS = [..., "logsetu"] # optional, but nice for discoverability
|
|
26
|
+
|
|
27
|
+
LOGGING = {
|
|
28
|
+
"version": 1,
|
|
29
|
+
"disable_existing_loggers": False,
|
|
30
|
+
"handlers": {
|
|
31
|
+
"logsetu": {
|
|
32
|
+
"class": "logsetu.handler.LogSetuHandler",
|
|
33
|
+
"api_key": env("LOGSETU_API_KEY"), # from the LogSetu dashboard → Projects
|
|
34
|
+
"endpoint": env("LOGSETU_ENDPOINT"), # e.g. https://logs.example.com
|
|
35
|
+
"environment": env("ENVIRONMENT", default="production"),
|
|
36
|
+
"source": "django-backend",
|
|
37
|
+
"level": "WARNING",
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
"root": {"handlers": ["logsetu"], "level": "INFO"},
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
MIDDLEWARE = [
|
|
44
|
+
...,
|
|
45
|
+
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
|
46
|
+
"logsetu.middleware.LogSetuMiddleware", # after auth middleware so user ids are captured
|
|
47
|
+
]
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
That's it. Every `logging` call at or above the handler's level is sent to LogSetu, and the middleware
|
|
51
|
+
reports unhandled exceptions (and 5xx responses) with the request path, method, view, user, redacted headers and full traceback.
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
import logging
|
|
55
|
+
log = logging.getLogger(__name__)
|
|
56
|
+
|
|
57
|
+
log.warning("Checkout is slow", extra={"duration_ms": 1830, "cart_items": 12})
|
|
58
|
+
try:
|
|
59
|
+
charge(card)
|
|
60
|
+
except PaymentError:
|
|
61
|
+
log.exception("Payment failed") # traceback is attached automatically
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Multi-tenant apps
|
|
65
|
+
|
|
66
|
+
Tag every log with the current tenant by pointing the SDK at a callable `(request) -> str | None`:
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
# settings.py
|
|
70
|
+
LOGGING["handlers"]["logsetu"]["get_tenant_id"] = "myapp.tenants.get_tenant_id"
|
|
71
|
+
|
|
72
|
+
# myapp/tenants.py
|
|
73
|
+
def get_tenant_id(request):
|
|
74
|
+
return getattr(request, "tenant", None) and request.tenant.slug
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Or, from anywhere inside a request (e.g. after you resolve the tenant in your own middleware):
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
from logsetu import update_request_context
|
|
81
|
+
update_request_context(tenant_id=tenant.slug, plan=tenant.plan)
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Handler options
|
|
85
|
+
|
|
86
|
+
| Option | Default | Description |
|
|
87
|
+
|---|---|---|
|
|
88
|
+
| `api_key` / `endpoint` | — | Required (or set `LOGSETU = {"API_KEY": ..., "ENDPOINT": ...}` in settings) |
|
|
89
|
+
| `environment` | `"production"` | Tag on every log |
|
|
90
|
+
| `source` | `"django"` | Tag on every log; override per record with `extra={"source": "celery"}` |
|
|
91
|
+
| `get_tenant_id` | `None` | Dotted path or callable `(request) -> str` |
|
|
92
|
+
| `include_extra` | `True` | Forward `extra={...}` fields as metadata |
|
|
93
|
+
| `include_request_context` | `True` | Attach request id / path / method / tenant set by the middleware |
|
|
94
|
+
| `flush_interval` | `2.0` | Seconds between background flushes |
|
|
95
|
+
| `batch_size` | `10` | Flush as soon as this many logs are queued |
|
|
96
|
+
| `max_queue_size` | `1000` | Drop oldest logs beyond this |
|
|
97
|
+
| `max_retries` | `3` | Retries with exponential backoff on 5xx / network errors |
|
|
98
|
+
| `timeout` | `5.0` | HTTP timeout in seconds |
|
|
99
|
+
| `debug` | `False` | Print every transport failure to stderr (otherwise only the first) |
|
|
100
|
+
|
|
101
|
+
## Middleware options (`settings.LOGSETU`)
|
|
102
|
+
|
|
103
|
+
The middleware reuses the client and options of a configured `LogSetuHandler`. Without a handler it can run standalone:
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
LOGSETU = {
|
|
107
|
+
"API_KEY": env("LOGSETU_API_KEY"),
|
|
108
|
+
"ENDPOINT": env("LOGSETU_ENDPOINT"),
|
|
109
|
+
"ENVIRONMENT": "production",
|
|
110
|
+
"SOURCE": "django-backend",
|
|
111
|
+
"GET_TENANT_ID": "myapp.tenants.get_tenant_id",
|
|
112
|
+
"REDACT_HEADERS": ["authorization", "cookie", "x-api-key"], # defaults already cover these
|
|
113
|
+
"CAPTURE_5XX": True, # also report 5xx responses that didn't raise
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Every request gets a `request_id` (taken from `X-Request-ID` if present, otherwise generated) which is
|
|
118
|
+
attached to all logs emitted during that request and exposed as `request.logsetu_request_id`.
|
|
119
|
+
|
|
120
|
+
## Guarantees
|
|
121
|
+
|
|
122
|
+
- **Never blocks** the request cycle: `emit()` only appends to an in-memory queue.
|
|
123
|
+
- **Never raises** into your app: transport errors are printed once to stderr, then suppressed.
|
|
124
|
+
- **Never leaks secrets**: `Authorization`, `Cookie`, `X-Api-Key`, `X-CSRFToken` and `Proxy-Authorization` are redacted.
|
|
125
|
+
- **Never loses the last logs** on shutdown: the queue is flushed via `atexit`.
|
|
126
|
+
|
|
127
|
+
## Celery / management commands
|
|
128
|
+
|
|
129
|
+
Nothing special — the handler works anywhere Python's `logging` does. Set `extra={"source": "celery"}` to distinguish
|
|
130
|
+
workers, or configure a second handler with a different `source`.
|
|
131
|
+
|
|
132
|
+
## Flask
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
import os
|
|
136
|
+
from flask import Flask
|
|
137
|
+
from logsetu.flask import LogSetu
|
|
138
|
+
|
|
139
|
+
app = Flask(__name__)
|
|
140
|
+
LogSetu(app, api_key=os.environ["LOGSETU_API_KEY"], endpoint=os.environ["LOGSETU_ENDPOINT"], source="flask-api")
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
App factory? Create `logsetu = LogSetu()` at module level and call `logsetu.init_app(app)`; options you don't pass are
|
|
144
|
+
read from `app.config` (`LOGSETU_API_KEY`, `LOGSETU_ENDPOINT`, `LOGSETU_ENVIRONMENT`, `LOGSETU_SOURCE`).
|
|
145
|
+
|
|
146
|
+
The extension reports unhandled exceptions and 5xx responses (route, endpoint, redacted headers, traceback), and attaches
|
|
147
|
+
a `LogSetuHandler` to the root logger so `logging.warning(...)` and above is shipped with the request id, method and
|
|
148
|
+
path of the current request. Flask's own `"Exception on /path"` log line is de-duplicated.
|
|
149
|
+
|
|
150
|
+
| Option | Default | Description |
|
|
151
|
+
|---|---|---|
|
|
152
|
+
| `api_key` / `endpoint` | `app.config` | Project API key and server URL |
|
|
153
|
+
| `environment` / `source` | `"production"` / `"flask"` | Tags on every log |
|
|
154
|
+
| `get_tenant_id` | — | `callable(request) -> str \| None` or dotted path |
|
|
155
|
+
| `capture_logging` | `True` | Attach a handler to the root logger |
|
|
156
|
+
| `log_level` | `logging.WARNING` | Minimum level forwarded from `logging` |
|
|
157
|
+
| `capture_5xx` | `True` | Report 5xx responses that didn't raise |
|
|
158
|
+
| `log_requests` | `False` | One info/warn log per request |
|
|
159
|
+
| `redact_headers` | auth / cookie / API-key headers | Header names replaced by `[REDACTED]` |
|
|
160
|
+
| `client` | — | Use an existing `LogSetuClient` |
|
|
161
|
+
|
|
162
|
+
## FastAPI / Starlette / ASGI
|
|
163
|
+
|
|
164
|
+
```python
|
|
165
|
+
import os
|
|
166
|
+
from fastapi import FastAPI
|
|
167
|
+
from logsetu.fastapi import LogSetuMiddleware # alias of logsetu.asgi — works with any ASGI app
|
|
168
|
+
|
|
169
|
+
app = FastAPI()
|
|
170
|
+
app.add_middleware(
|
|
171
|
+
LogSetuMiddleware,
|
|
172
|
+
api_key=os.environ["LOGSETU_API_KEY"],
|
|
173
|
+
endpoint=os.environ["LOGSETU_ENDPOINT"],
|
|
174
|
+
source="fastapi",
|
|
175
|
+
ignore_paths=["/healthz"],
|
|
176
|
+
)
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
A pure ASGI middleware (streaming responses aren't buffered). Same options as Flask plus `ignore_paths`;
|
|
180
|
+
`get_tenant_id` receives the ASGI `scope`, and `api_key` / `endpoint` / `environment` / `source` fall back to the
|
|
181
|
+
`LOGSETU_*` environment variables. Request context reaches logs from both `async def` endpoints and sync endpoints
|
|
182
|
+
running in the threadpool. `HTTPException`s with 4xx status codes aren't reported as errors. The request id is
|
|
183
|
+
available as `request.state.logsetu_request_id`.
|
|
184
|
+
|
|
185
|
+
## License
|
|
186
|
+
|
|
187
|
+
MIT
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.25"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "logsetu"
|
|
7
|
+
version = "0.3.0"
|
|
8
|
+
description = "LogSetu SDK for Python — Django, DRF, Flask and FastAPI: ship logging records and request errors to your self-hosted LogSetu server"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
license-files = ["LICENSE"]
|
|
12
|
+
authors = [{ name = "Raj Verma" }]
|
|
13
|
+
requires-python = ">=3.9"
|
|
14
|
+
keywords = ["logging", "python", "django", "drf", "flask", "fastapi", "asgi", "error-tracking", "self-hosted", "sentry-alternative", "logsetu"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 4 - Beta",
|
|
17
|
+
"Framework :: Django",
|
|
18
|
+
"Framework :: Flask",
|
|
19
|
+
"Framework :: FastAPI",
|
|
20
|
+
"Intended Audience :: Developers",
|
|
21
|
+
"Programming Language :: Python :: 3",
|
|
22
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
23
|
+
"Topic :: System :: Logging",
|
|
24
|
+
"Typing :: Typed",
|
|
25
|
+
]
|
|
26
|
+
dependencies = []
|
|
27
|
+
|
|
28
|
+
[project.optional-dependencies]
|
|
29
|
+
django = ["Django>=3.2"]
|
|
30
|
+
flask = ["Flask>=2.2"]
|
|
31
|
+
fastapi = ["fastapi>=0.95"]
|
|
32
|
+
dev = ["Django>=3.2", "Flask>=2.2", "fastapi>=0.95", "httpx", "pytest>=8", "pytest-django>=4.8", "build", "twine"]
|
|
33
|
+
|
|
34
|
+
[project.urls]
|
|
35
|
+
Homepage = "https://github.com/itsrajverma/logsetu"
|
|
36
|
+
Documentation = "https://github.com/itsrajverma/logsetu/tree/main/docs"
|
|
37
|
+
Repository = "https://github.com/itsrajverma/logsetu"
|
|
38
|
+
Issues = "https://github.com/itsrajverma/logsetu/issues"
|
|
39
|
+
|
|
40
|
+
[tool.hatch.build.targets.wheel]
|
|
41
|
+
packages = ["src/logsetu"]
|
|
42
|
+
|
|
43
|
+
[tool.hatch.build.targets.sdist]
|
|
44
|
+
include = ["src/logsetu", "tests", "README.md", "LICENSE", "pyproject.toml"]
|
|
45
|
+
|
|
46
|
+
[tool.pytest.ini_options]
|
|
47
|
+
testpaths = ["tests"]
|
|
48
|
+
DJANGO_SETTINGS_MODULE = "tests.settings"
|
|
49
|
+
pythonpath = ["src", "."]
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""LogSetu SDK for Python: Django & DRF, Flask (``logsetu.flask``) and FastAPI / ASGI (``logsetu.fastapi``).
|
|
2
|
+
|
|
3
|
+
Quick start (settings.py)::
|
|
4
|
+
|
|
5
|
+
LOGGING = {
|
|
6
|
+
"version": 1,
|
|
7
|
+
"handlers": {
|
|
8
|
+
"logsetu": {
|
|
9
|
+
"class": "logsetu.handler.LogSetuHandler",
|
|
10
|
+
"api_key": env("LOGSETU_API_KEY"),
|
|
11
|
+
"endpoint": env("LOGSETU_ENDPOINT"),
|
|
12
|
+
"environment": "production",
|
|
13
|
+
"source": "django-backend",
|
|
14
|
+
"level": "WARNING",
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
"root": {"handlers": ["logsetu"], "level": "INFO"},
|
|
18
|
+
}
|
|
19
|
+
MIDDLEWARE = [..., "logsetu.middleware.LogSetuMiddleware"]
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from .client import LogSetuClient, get_client, __version__
|
|
23
|
+
from .context import get_request_context, update_request_context
|
|
24
|
+
|
|
25
|
+
__all__ = [
|
|
26
|
+
"LogSetuClient",
|
|
27
|
+
"get_client",
|
|
28
|
+
"get_request_context",
|
|
29
|
+
"update_request_context",
|
|
30
|
+
"capture_exception",
|
|
31
|
+
"__version__",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
default_app_config = "logsetu.apps.LogSetuConfig"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def capture_exception(exc: BaseException, message: str = "", **meta: object) -> None:
|
|
38
|
+
"""Report an exception through the configured ``LogSetuHandler`` (via the stdlib logger)."""
|
|
39
|
+
import logging
|
|
40
|
+
|
|
41
|
+
logging.getLogger("logsetu.capture").error(message or f"{type(exc).__name__}: {exc}", exc_info=exc, extra=meta)
|