reqly 0.1.4__tar.gz → 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.
Files changed (36) hide show
  1. reqly-0.3.0/PKG-INFO +238 -0
  2. reqly-0.3.0/README.md +191 -0
  3. {reqly-0.1.4 → reqly-0.3.0}/pyproject.toml +18 -8
  4. {reqly-0.1.4 → reqly-0.3.0}/reqly/__init__.py +42 -21
  5. {reqly-0.1.4 → reqly-0.3.0}/reqly/core/buffer.py +22 -1
  6. {reqly-0.1.4 → reqly-0.3.0}/reqly/core/capture.py +7 -7
  7. {reqly-0.1.4 → reqly-0.3.0}/reqly/core/client.py +6 -0
  8. {reqly-0.1.4 → reqly-0.3.0}/reqly/core/config.py +45 -2
  9. {reqly-0.1.4 → reqly-0.3.0}/reqly/core/sampling.py +7 -4
  10. {reqly-0.1.4 → reqly-0.3.0}/reqly/core/shipper.py +31 -5
  11. reqly-0.3.0/reqly/integrations/django.py +142 -0
  12. reqly-0.3.0/reqly/integrations/fastapi.py +113 -0
  13. {reqly-0.1.4 → reqly-0.3.0}/reqly/integrations/flask.py +5 -0
  14. reqly-0.3.0/reqly/integrations/litestar.py +13 -0
  15. reqly-0.3.0/reqly/integrations/starlette.py +37 -0
  16. reqly-0.3.0/reqly.egg-info/PKG-INFO +238 -0
  17. {reqly-0.1.4 → reqly-0.3.0}/reqly.egg-info/SOURCES.txt +8 -1
  18. {reqly-0.1.4 → reqly-0.3.0}/reqly.egg-info/requires.txt +11 -0
  19. {reqly-0.1.4 → reqly-0.3.0}/setup.cfg +4 -4
  20. {reqly-0.1.4 → reqly-0.3.0}/tests/test_buffer.py +20 -0
  21. reqly-0.3.0/tests/test_instrument.py +32 -0
  22. reqly-0.3.0/tests/test_more_frameworks.py +250 -0
  23. reqly-0.3.0/tests/test_shipper.py +47 -0
  24. reqly-0.3.0/tests/test_v2_fields.py +111 -0
  25. reqly-0.1.4/PKG-INFO +0 -137
  26. reqly-0.1.4/README.md +0 -101
  27. reqly-0.1.4/reqly/integrations/fastapi.py +0 -64
  28. reqly-0.1.4/reqly.egg-info/PKG-INFO +0 -137
  29. {reqly-0.1.4 → reqly-0.3.0}/reqly/core/__init__.py +0 -0
  30. {reqly-0.1.4 → reqly-0.3.0}/reqly/integrations/__init__.py +0 -0
  31. {reqly-0.1.4 → reqly-0.3.0}/reqly.egg-info/dependency_links.txt +0 -0
  32. {reqly-0.1.4 → reqly-0.3.0}/reqly.egg-info/top_level.txt +0 -0
  33. {reqly-0.1.4 → reqly-0.3.0}/tests/test_capture.py +0 -0
  34. {reqly-0.1.4 → reqly-0.3.0}/tests/test_fastapi_integration.py +0 -0
  35. {reqly-0.1.4 → reqly-0.3.0}/tests/test_flask_integration.py +0 -0
  36. {reqly-0.1.4 → reqly-0.3.0}/tests/test_sampling.py +0 -0
reqly-0.3.0/PKG-INFO ADDED
@@ -0,0 +1,238 @@
1
+ Metadata-Version: 2.4
2
+ Name: reqly
3
+ Version: 0.3.0
4
+ Summary: Self-hosted API monitoring for FastAPI, Flask, Django, Starlette and Litestar in two lines: latency percentiles, error rates and release tracking per route, with deploy-aware alerts and weekly AI anomaly reports.
5
+ Author: Tanish Poddar
6
+ License-Expression: GPL-3.0-or-later
7
+ Project-URL: Homepage, https://reqly.tanisheesh.in
8
+ Project-URL: Repository, https://github.com/tanisheesh/reqly
9
+ Project-URL: Issues, https://github.com/tanisheesh/reqly/issues
10
+ Project-URL: Documentation, https://github.com/tanisheesh/reqly/blob/main/docs/SETUP.md
11
+ Project-URL: Changelog, https://github.com/tanisheesh/reqly/blob/main/sdk/CHANGELOG.md
12
+ Project-URL: Live Demo, https://reqly-eventflow-dashboard.onrender.com
13
+ Keywords: observability,apm,monitoring,fastapi,flask,django,starlette,litestar,telemetry,metrics,latency,percentiles,alerting,anomaly-detection,release-tracking,self-hosted,timescaledb
14
+ Classifier: Development Status :: 4 - Beta
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.9
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
23
+ Classifier: Topic :: System :: Monitoring
24
+ Classifier: Framework :: Django
25
+ Classifier: Framework :: FastAPI
26
+ Classifier: Framework :: Flask
27
+ Requires-Python: >=3.9
28
+ Description-Content-Type: text/markdown
29
+ Requires-Dist: httpx<1.0,>=0.27
30
+ Provides-Extra: fastapi
31
+ Requires-Dist: fastapi>=0.100; extra == "fastapi"
32
+ Provides-Extra: starlette
33
+ Requires-Dist: starlette>=0.27; extra == "starlette"
34
+ Provides-Extra: litestar
35
+ Requires-Dist: litestar>=2.0; extra == "litestar"
36
+ Provides-Extra: flask
37
+ Requires-Dist: flask>=2.3; extra == "flask"
38
+ Provides-Extra: django
39
+ Requires-Dist: django>=4.2; extra == "django"
40
+ Provides-Extra: dev
41
+ Requires-Dist: pytest>=8.0; extra == "dev"
42
+ Requires-Dist: fastapi>=0.100; extra == "dev"
43
+ Requires-Dist: litestar>=2.0; extra == "dev"
44
+ Requires-Dist: flask>=2.3; extra == "dev"
45
+ Requires-Dist: django>=4.2; extra == "dev"
46
+ Requires-Dist: httpx>=0.27; extra == "dev"
47
+
48
+ # reqly
49
+
50
+ **Self-hosted API monitoring for FastAPI, Flask, Django, Starlette and Litestar — two lines of code.**
51
+ Latency percentiles, error rates and release tracking for every route, shipped to your own
52
+ [Reqly](https://github.com/tanisheesh/reqly) collector — which turns them into deploy-aware
53
+ hourly alerts and weekly AI anomaly reports.
54
+
55
+ [![PyPI](https://img.shields.io/pypi/v/reqly?color=06b6d4&label=reqly)](https://pypi.org/project/reqly/)
56
+ [![Python](https://img.shields.io/pypi/pyversions/reqly?color=06b6d4)](https://pypi.org/project/reqly/)
57
+ [![License: GPL v3](https://img.shields.io/badge/license-GPL--3.0-06b6d4)](https://github.com/tanisheesh/reqly/blob/main/LICENSE)
58
+
59
+ ---
60
+
61
+ ## Install
62
+
63
+ ```bash
64
+ pip install reqly
65
+ ```
66
+
67
+ ## Usage
68
+
69
+ **FastAPI**
70
+
71
+ ```python
72
+ import reqly
73
+ from fastapi import FastAPI
74
+
75
+ app = FastAPI()
76
+ reqly.instrument(
77
+ app,
78
+ service_name="checkout-api",
79
+ collector_url="https://reqly.example.com",
80
+ api_key="your-ingest-key",
81
+ )
82
+ # Every route is now tracked: latency, errors, status codes, release
83
+ ```
84
+
85
+ **Flask**
86
+
87
+ ```python
88
+ import reqly
89
+ from flask import Flask
90
+
91
+ app = Flask(__name__)
92
+ reqly.instrument(app, service_name="checkout-api") # settings from REQLY_* env vars
93
+ ```
94
+
95
+ **Starlette / Litestar** — same call:
96
+
97
+ ```python
98
+ reqly.instrument(app, service_name="checkout-api")
99
+ ```
100
+
101
+ **Django** (also Django REST Framework and Django Ninja) — Django has no app object, so add
102
+ the middleware first in `MIDDLEWARE`:
103
+
104
+ ```python
105
+ # settings.py
106
+ MIDDLEWARE = [
107
+ "reqly.integrations.django.ReqlyMiddleware",
108
+ # ...
109
+ ]
110
+ REQLY = {"service_name": "checkout-api", "api_key": "your-ingest-key"} # optional
111
+ ```
112
+
113
+ `instrument()` detects the framework by itself — no decorators, no middleware to wire up.
114
+ Routes are recorded as templates in one style across frameworks: Django's
115
+ `users/<int:pk>/` and DRF's `^users/(?P<pk>[^/.]+)/$` both become `/users/{pk}/`.
116
+ The release you're running is picked up automatically from your CI or host
117
+ (`GITHUB_SHA`, `RENDER_GIT_COMMIT`, `VERCEL_GIT_COMMIT_SHA`, …), so deploys show up in
118
+ Reqly with no extra code.
119
+
120
+ ## What you get
121
+
122
+ From the SDK, per request: method, **route template** (`/orders/{id}`, never the raw
123
+ path), status code, duration, error type, host, **release**, environment and
124
+ request/response **body size**.
125
+
126
+ In the Reqly dashboard and collector:
127
+
128
+ - **p50 / p95 / p99 latency** per route and per service — real percentiles from
129
+ mergeable sketches, not the max of per-route numbers
130
+ - **Error rates, status codes and top routes** over 1h / 6h / 24h / 7d
131
+ - **Deploy markers and per-release health** — each release's error rate and p95
132
+ - **Hourly alerts** to Slack, Discord or a webhook when a route breaks from its usual
133
+ weekday-hour pattern, with **root-cause hints**
134
+ - **Weekly AI report** — statistics find the anomalies, Groq (Llama 3.3-70b) writes the
135
+ summary; plain-text fallback without an API key
136
+
137
+ An alert from the demo data looks like this:
138
+
139
+ ```
140
+ 🔴 Anomaly — flask-demo /orders (Friday 15:00-16:00 UTC, z=5.37)
141
+ • error rate 30.0% vs 2.2% usual · p95 6588ms vs 1576ms usual
142
+ • running release v2 — vs v1: errors 2.6% → 33.1%, p95 2072ms → 4501ms
143
+ • 100% of errors came from host pod-3, which served 23% of requests
144
+ ```
145
+
146
+ **Not on Python?** Node, Java, Go and .NET apps can report to the same collector through
147
+ OpenTelemetry — no Reqly SDK needed. See the
148
+ [OpenTelemetry guide](https://github.com/tanisheesh/reqly/blob/main/docs/OTEL.md).
149
+
150
+ ## Configuration
151
+
152
+ Every option can be passed to `instrument()` or set as an environment variable.
153
+ Resolution order: **argument → environment variable → default**.
154
+
155
+ | argument | environment variable | default |
156
+ |---|---|---|
157
+ | `service_name` | `REQLY_SERVICE_NAME` | `sys.argv[0]` basename |
158
+ | `collector_url` | `REQLY_COLLECTOR_URL` | `http://localhost:8000` |
159
+ | `api_key` | `REQLY_API_KEY` | `None` |
160
+ | `release` | `REQLY_RELEASE`, then CI variables (`GITHUB_SHA`, `CI_COMMIT_SHA`, `RENDER_GIT_COMMIT`, `VERCEL_GIT_COMMIT_SHA`, `RAILWAY_GIT_COMMIT_SHA`, `HEROKU_SLUG_COMMIT`, `K_REVISION`, …) | auto-detected, else `None` |
161
+ | `environment` | `REQLY_ENVIRONMENT` | `None` |
162
+ | `sample_rate` | `REQLY_SAMPLE_RATE` | `1.0` |
163
+ | `flush_interval_seconds` | `REQLY_FLUSH_INTERVAL_SECONDS` | `5.0` |
164
+ | `max_batch_size` | `REQLY_MAX_BATCH_SIZE` | `200` |
165
+ | `max_queue_size` | `REQLY_MAX_QUEUE_SIZE` | `2000` |
166
+ | `ignore_routes` | `REQLY_IGNORE_ROUTES` (comma-separated) | `/health,/metrics` |
167
+ | `capture_request_body` | `REQLY_CAPTURE_REQUEST_BODY` | `False` (not implemented yet) |
168
+
169
+ With `sample_rate` below 1.0, request counts in the dashboard are the sampled volume;
170
+ latency percentiles and error rates stay unbiased.
171
+
172
+ ## Design guarantees
173
+
174
+ **Fail-open** — any internal SDK error is caught and logged once; instrumentation disables
175
+ itself rather than raise into your app. A slow or unreachable collector never blocks
176
+ request threads — shipping happens on a background thread with strict HTTP timeouts.
177
+
178
+ **Bounded cardinality** — routes are recorded as the framework's matched template
179
+ (`/users/{id}`), never the raw path (`/users/123`). Unmatched paths (404s, scanners)
180
+ collapse into a single `__unmatched__` bucket.
181
+
182
+ **Bounded memory** — events wait in a fixed-size in-memory queue; under backpressure the
183
+ oldest events are dropped and counted instead of growing without limit.
184
+
185
+ **Safe retries** — batches are retried with exponential backoff on `408`, `429` and any
186
+ `5xx` (for example a collector restart behind a proxy); other `4xx` responses are dropped
187
+ immediately. Every event carries a unique `event_id` the collector deduplicates on, so a
188
+ retry never double-counts.
189
+
190
+ **Pre-fork servers** — under gunicorn `--preload` (or uWSGI without lazy-apps) each
191
+ forked worker restarts its own flush thread and HTTP connection pool, so workers' events
192
+ are shipped instead of silently queuing forever.
193
+
194
+ ## Compatibility
195
+
196
+ | | Supported |
197
+ |---|---|
198
+ | Python | 3.9 – 3.13 |
199
+ | FastAPI | 0.100+ (including routes in `app.mount()`ed sub-apps) |
200
+ | Starlette | 0.27+ (including `Mount`) |
201
+ | Litestar | 2.0+ |
202
+ | Flask | 2.3+ |
203
+ | Django | 4.2+, sync and async views; DRF and Django Ninja |
204
+ | Collector | any version; `release`, `environment` and body sizes are stored by collector 0.3.0+ and ignored by older ones |
205
+
206
+ ## Self-hosting the collector
207
+
208
+ The SDK sends data to a Reqly collector you run. The full stack — collector,
209
+ TimescaleDB and dashboard — starts with Docker Compose:
210
+
211
+ ```bash
212
+ git clone https://github.com/tanisheesh/reqly.git
213
+ cd reqly
214
+ docker compose up -d
215
+ ```
216
+
217
+ Setup, configuration and AWS deployment:
218
+ [docs/SETUP.md](https://github.com/tanisheesh/reqly/blob/main/docs/SETUP.md) ·
219
+ [infra/DEPLOY.md](https://github.com/tanisheesh/reqly/blob/main/infra/DEPLOY.md) ·
220
+ [ingest API spec](https://github.com/tanisheesh/reqly/blob/main/docs/INGEST_SPEC.md)
221
+
222
+ ## Live demo
223
+
224
+ Reqly monitors [EventFlow](https://eventflow-g2h5.onrender.com), a Flask event management
225
+ app, in production:
226
+
227
+ - **Demo app** → [eventflow-g2h5.onrender.com](https://eventflow-g2h5.onrender.com)
228
+ - **Metrics dashboard** → [reqly-eventflow-dashboard.onrender.com](https://reqly-eventflow-dashboard.onrender.com)
229
+
230
+ > Log in as **Administrator** (`admin@eventhub.com` / `Admin@123`) → click **Metrics** in the nav.
231
+
232
+ ## Changelog
233
+
234
+ See [CHANGELOG.md](https://github.com/tanisheesh/reqly/blob/main/sdk/CHANGELOG.md).
235
+
236
+ ## License
237
+
238
+ GPL-3.0-or-later — see [LICENSE](https://github.com/tanisheesh/reqly/blob/main/LICENSE).
reqly-0.3.0/README.md ADDED
@@ -0,0 +1,191 @@
1
+ # reqly
2
+
3
+ **Self-hosted API monitoring for FastAPI, Flask, Django, Starlette and Litestar — two lines of code.**
4
+ Latency percentiles, error rates and release tracking for every route, shipped to your own
5
+ [Reqly](https://github.com/tanisheesh/reqly) collector — which turns them into deploy-aware
6
+ hourly alerts and weekly AI anomaly reports.
7
+
8
+ [![PyPI](https://img.shields.io/pypi/v/reqly?color=06b6d4&label=reqly)](https://pypi.org/project/reqly/)
9
+ [![Python](https://img.shields.io/pypi/pyversions/reqly?color=06b6d4)](https://pypi.org/project/reqly/)
10
+ [![License: GPL v3](https://img.shields.io/badge/license-GPL--3.0-06b6d4)](https://github.com/tanisheesh/reqly/blob/main/LICENSE)
11
+
12
+ ---
13
+
14
+ ## Install
15
+
16
+ ```bash
17
+ pip install reqly
18
+ ```
19
+
20
+ ## Usage
21
+
22
+ **FastAPI**
23
+
24
+ ```python
25
+ import reqly
26
+ from fastapi import FastAPI
27
+
28
+ app = FastAPI()
29
+ reqly.instrument(
30
+ app,
31
+ service_name="checkout-api",
32
+ collector_url="https://reqly.example.com",
33
+ api_key="your-ingest-key",
34
+ )
35
+ # Every route is now tracked: latency, errors, status codes, release
36
+ ```
37
+
38
+ **Flask**
39
+
40
+ ```python
41
+ import reqly
42
+ from flask import Flask
43
+
44
+ app = Flask(__name__)
45
+ reqly.instrument(app, service_name="checkout-api") # settings from REQLY_* env vars
46
+ ```
47
+
48
+ **Starlette / Litestar** — same call:
49
+
50
+ ```python
51
+ reqly.instrument(app, service_name="checkout-api")
52
+ ```
53
+
54
+ **Django** (also Django REST Framework and Django Ninja) — Django has no app object, so add
55
+ the middleware first in `MIDDLEWARE`:
56
+
57
+ ```python
58
+ # settings.py
59
+ MIDDLEWARE = [
60
+ "reqly.integrations.django.ReqlyMiddleware",
61
+ # ...
62
+ ]
63
+ REQLY = {"service_name": "checkout-api", "api_key": "your-ingest-key"} # optional
64
+ ```
65
+
66
+ `instrument()` detects the framework by itself — no decorators, no middleware to wire up.
67
+ Routes are recorded as templates in one style across frameworks: Django's
68
+ `users/<int:pk>/` and DRF's `^users/(?P<pk>[^/.]+)/$` both become `/users/{pk}/`.
69
+ The release you're running is picked up automatically from your CI or host
70
+ (`GITHUB_SHA`, `RENDER_GIT_COMMIT`, `VERCEL_GIT_COMMIT_SHA`, …), so deploys show up in
71
+ Reqly with no extra code.
72
+
73
+ ## What you get
74
+
75
+ From the SDK, per request: method, **route template** (`/orders/{id}`, never the raw
76
+ path), status code, duration, error type, host, **release**, environment and
77
+ request/response **body size**.
78
+
79
+ In the Reqly dashboard and collector:
80
+
81
+ - **p50 / p95 / p99 latency** per route and per service — real percentiles from
82
+ mergeable sketches, not the max of per-route numbers
83
+ - **Error rates, status codes and top routes** over 1h / 6h / 24h / 7d
84
+ - **Deploy markers and per-release health** — each release's error rate and p95
85
+ - **Hourly alerts** to Slack, Discord or a webhook when a route breaks from its usual
86
+ weekday-hour pattern, with **root-cause hints**
87
+ - **Weekly AI report** — statistics find the anomalies, Groq (Llama 3.3-70b) writes the
88
+ summary; plain-text fallback without an API key
89
+
90
+ An alert from the demo data looks like this:
91
+
92
+ ```
93
+ 🔴 Anomaly — flask-demo /orders (Friday 15:00-16:00 UTC, z=5.37)
94
+ • error rate 30.0% vs 2.2% usual · p95 6588ms vs 1576ms usual
95
+ • running release v2 — vs v1: errors 2.6% → 33.1%, p95 2072ms → 4501ms
96
+ • 100% of errors came from host pod-3, which served 23% of requests
97
+ ```
98
+
99
+ **Not on Python?** Node, Java, Go and .NET apps can report to the same collector through
100
+ OpenTelemetry — no Reqly SDK needed. See the
101
+ [OpenTelemetry guide](https://github.com/tanisheesh/reqly/blob/main/docs/OTEL.md).
102
+
103
+ ## Configuration
104
+
105
+ Every option can be passed to `instrument()` or set as an environment variable.
106
+ Resolution order: **argument → environment variable → default**.
107
+
108
+ | argument | environment variable | default |
109
+ |---|---|---|
110
+ | `service_name` | `REQLY_SERVICE_NAME` | `sys.argv[0]` basename |
111
+ | `collector_url` | `REQLY_COLLECTOR_URL` | `http://localhost:8000` |
112
+ | `api_key` | `REQLY_API_KEY` | `None` |
113
+ | `release` | `REQLY_RELEASE`, then CI variables (`GITHUB_SHA`, `CI_COMMIT_SHA`, `RENDER_GIT_COMMIT`, `VERCEL_GIT_COMMIT_SHA`, `RAILWAY_GIT_COMMIT_SHA`, `HEROKU_SLUG_COMMIT`, `K_REVISION`, …) | auto-detected, else `None` |
114
+ | `environment` | `REQLY_ENVIRONMENT` | `None` |
115
+ | `sample_rate` | `REQLY_SAMPLE_RATE` | `1.0` |
116
+ | `flush_interval_seconds` | `REQLY_FLUSH_INTERVAL_SECONDS` | `5.0` |
117
+ | `max_batch_size` | `REQLY_MAX_BATCH_SIZE` | `200` |
118
+ | `max_queue_size` | `REQLY_MAX_QUEUE_SIZE` | `2000` |
119
+ | `ignore_routes` | `REQLY_IGNORE_ROUTES` (comma-separated) | `/health,/metrics` |
120
+ | `capture_request_body` | `REQLY_CAPTURE_REQUEST_BODY` | `False` (not implemented yet) |
121
+
122
+ With `sample_rate` below 1.0, request counts in the dashboard are the sampled volume;
123
+ latency percentiles and error rates stay unbiased.
124
+
125
+ ## Design guarantees
126
+
127
+ **Fail-open** — any internal SDK error is caught and logged once; instrumentation disables
128
+ itself rather than raise into your app. A slow or unreachable collector never blocks
129
+ request threads — shipping happens on a background thread with strict HTTP timeouts.
130
+
131
+ **Bounded cardinality** — routes are recorded as the framework's matched template
132
+ (`/users/{id}`), never the raw path (`/users/123`). Unmatched paths (404s, scanners)
133
+ collapse into a single `__unmatched__` bucket.
134
+
135
+ **Bounded memory** — events wait in a fixed-size in-memory queue; under backpressure the
136
+ oldest events are dropped and counted instead of growing without limit.
137
+
138
+ **Safe retries** — batches are retried with exponential backoff on `408`, `429` and any
139
+ `5xx` (for example a collector restart behind a proxy); other `4xx` responses are dropped
140
+ immediately. Every event carries a unique `event_id` the collector deduplicates on, so a
141
+ retry never double-counts.
142
+
143
+ **Pre-fork servers** — under gunicorn `--preload` (or uWSGI without lazy-apps) each
144
+ forked worker restarts its own flush thread and HTTP connection pool, so workers' events
145
+ are shipped instead of silently queuing forever.
146
+
147
+ ## Compatibility
148
+
149
+ | | Supported |
150
+ |---|---|
151
+ | Python | 3.9 – 3.13 |
152
+ | FastAPI | 0.100+ (including routes in `app.mount()`ed sub-apps) |
153
+ | Starlette | 0.27+ (including `Mount`) |
154
+ | Litestar | 2.0+ |
155
+ | Flask | 2.3+ |
156
+ | Django | 4.2+, sync and async views; DRF and Django Ninja |
157
+ | Collector | any version; `release`, `environment` and body sizes are stored by collector 0.3.0+ and ignored by older ones |
158
+
159
+ ## Self-hosting the collector
160
+
161
+ The SDK sends data to a Reqly collector you run. The full stack — collector,
162
+ TimescaleDB and dashboard — starts with Docker Compose:
163
+
164
+ ```bash
165
+ git clone https://github.com/tanisheesh/reqly.git
166
+ cd reqly
167
+ docker compose up -d
168
+ ```
169
+
170
+ Setup, configuration and AWS deployment:
171
+ [docs/SETUP.md](https://github.com/tanisheesh/reqly/blob/main/docs/SETUP.md) ·
172
+ [infra/DEPLOY.md](https://github.com/tanisheesh/reqly/blob/main/infra/DEPLOY.md) ·
173
+ [ingest API spec](https://github.com/tanisheesh/reqly/blob/main/docs/INGEST_SPEC.md)
174
+
175
+ ## Live demo
176
+
177
+ Reqly monitors [EventFlow](https://eventflow-g2h5.onrender.com), a Flask event management
178
+ app, in production:
179
+
180
+ - **Demo app** → [eventflow-g2h5.onrender.com](https://eventflow-g2h5.onrender.com)
181
+ - **Metrics dashboard** → [reqly-eventflow-dashboard.onrender.com](https://reqly-eventflow-dashboard.onrender.com)
182
+
183
+ > Log in as **Administrator** (`admin@eventhub.com` / `Admin@123`) → click **Metrics** in the nav.
184
+
185
+ ## Changelog
186
+
187
+ See [CHANGELOG.md](https://github.com/tanisheesh/reqly/blob/main/sdk/CHANGELOG.md).
188
+
189
+ ## License
190
+
191
+ GPL-3.0-or-later — see [LICENSE](https://github.com/tanisheesh/reqly/blob/main/LICENSE).
@@ -4,15 +4,17 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "reqly"
7
- version = "0.1.4"
8
- description = "Auto-instrumentation SDK for FastAPI and Flask: zero-code latency, error rates, and status codes shipped to a Reqly collector."
7
+ version = "0.3.0"
8
+ description = "Self-hosted API monitoring for FastAPI, Flask, Django, Starlette and Litestar in two lines: latency percentiles, error rates and release tracking per route, with deploy-aware alerts and weekly AI anomaly reports."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
11
11
  license = "GPL-3.0-or-later"
12
- authors = [{ name = "reqly" }]
12
+ authors = [{ name = "Tanish Poddar" }]
13
13
  keywords = [
14
- "observability", "apm", "monitoring", "fastapi", "flask",
15
- "telemetry", "metrics", "latency", "tracing"
14
+ "observability", "apm", "monitoring", "fastapi", "flask", "django",
15
+ "starlette", "litestar", "telemetry",
16
+ "metrics", "latency", "percentiles", "alerting", "anomaly-detection",
17
+ "release-tracking", "self-hosted", "timescaledb"
16
18
  ]
17
19
  classifiers = [
18
20
  "Development Status :: 4 - Beta",
@@ -22,8 +24,10 @@ classifiers = [
22
24
  "Programming Language :: Python :: 3.10",
23
25
  "Programming Language :: Python :: 3.11",
24
26
  "Programming Language :: Python :: 3.12",
27
+ "Programming Language :: Python :: 3.13",
25
28
  "Topic :: Software Development :: Libraries :: Python Modules",
26
29
  "Topic :: System :: Monitoring",
30
+ "Framework :: Django",
27
31
  "Framework :: FastAPI",
28
32
  "Framework :: Flask",
29
33
  ]
@@ -32,19 +36,25 @@ dependencies = [
32
36
  ]
33
37
 
34
38
  [project.urls]
35
- Homepage = "https://reqly-sdk.vercel.app/"
39
+ Homepage = "https://reqly.tanisheesh.in"
36
40
  Repository = "https://github.com/tanisheesh/reqly"
37
41
  Issues = "https://github.com/tanisheesh/reqly/issues"
38
- Documentation = "https://github.com/tanisheesh/reqly/blob/main/CONTRIBUTING.md"
39
- "Live Demo" = "https://eventflow-g2h5.onrender.com"
42
+ Documentation = "https://github.com/tanisheesh/reqly/blob/main/docs/SETUP.md"
43
+ Changelog = "https://github.com/tanisheesh/reqly/blob/main/sdk/CHANGELOG.md"
44
+ "Live Demo" = "https://reqly-eventflow-dashboard.onrender.com"
40
45
 
41
46
  [project.optional-dependencies]
42
47
  fastapi = ["fastapi>=0.100"]
48
+ starlette = ["starlette>=0.27"]
49
+ litestar = ["litestar>=2.0"]
43
50
  flask = ["flask>=2.3"]
51
+ django = ["django>=4.2"]
44
52
  dev = [
45
53
  "pytest>=8.0",
46
54
  "fastapi>=0.100",
55
+ "litestar>=2.0",
47
56
  "flask>=2.3",
57
+ "django>=4.2",
48
58
  "httpx>=0.27",
49
59
  ]
50
60
 
@@ -1,40 +1,40 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import logging
4
- from importlib.metadata import PackageNotFoundError
5
- from importlib.metadata import version as _pkg_version
6
4
 
7
5
  from .core.client import ReqlyClient
8
- from .core.config import Config
6
+ from .core.config import Config, _get_sdk_version
9
7
 
10
- try:
11
- __version__ = _pkg_version("reqly")
12
- except PackageNotFoundError:
13
- __version__ = "0.1.3"
8
+ __version__ = _get_sdk_version()
14
9
 
15
10
  __all__ = ["instrument"]
16
11
 
17
12
  logger = logging.getLogger("reqly")
18
13
 
19
- # Keep a reference on each instrumented app so repeated calls / shutdown
20
- # hooks can find the client without the caller having to hold onto it.
21
- _CLIENTS: dict[int, ReqlyClient] = {}
14
+ # id(app) -> (app, client) for every instrumented app. Holding the app
15
+ # itself keeps its id from being reused by a new object, so a repeat
16
+ # instrument() call on the same app is recognized reliably -- without
17
+ # setting attributes on the app (Litestar apps use __slots__ and refuse).
18
+ _CLIENTS: dict[int, tuple[object, ReqlyClient]] = {}
22
19
 
23
20
 
24
21
  def _detect_framework(app) -> str:
25
- module = type(app).__module__ or ""
26
- if "flask" in module:
27
- return "flask"
28
- if "fastapi" in module or "starlette" in module:
29
- return "fastapi"
30
- # Fallback to duck typing if the module name isn't conclusive.
31
- if hasattr(app, "add_middleware"):
32
- return "fastapi"
22
+ # Walk the class hierarchy so subclasses of an app class (a project's
23
+ # own `class App(FastAPI)`) are recognized too. FastAPI subclasses
24
+ # Starlette, so it must be checked first.
25
+ modules = [cls.__module__ or "" for cls in type(app).__mro__]
26
+ for framework in ("fastapi", "litestar", "starlette", "flask"):
27
+ if any(m == framework or m.startswith(framework + ".") for m in modules):
28
+ return framework
29
+ # Fallback to duck typing if the module names aren't conclusive.
30
+ if hasattr(app, "add_middleware") and hasattr(app, "router"):
31
+ return "starlette"
33
32
  if hasattr(app, "before_request") and hasattr(app, "wsgi_app"):
34
33
  return "flask"
35
34
  raise TypeError(
36
35
  "reqly.instrument(): could not detect framework for app of type "
37
- f"{type(app)!r}. Supported: FastAPI, Flask."
36
+ f"{type(app)!r}. Supported: FastAPI, Starlette, Litestar, Flask "
37
+ "(Django: add reqly.integrations.django.ReqlyMiddleware to MIDDLEWARE)."
38
38
  )
39
39
 
40
40
 
@@ -50,8 +50,11 @@ def instrument(
50
50
  max_queue_size: int | None = None,
51
51
  ignore_routes: list[str] | None = None,
52
52
  capture_request_body: bool | None = None,
53
+ release: str | None = None,
54
+ environment: str | None = None,
53
55
  ) -> ReqlyClient | None:
54
- """Instrument a FastAPI or Flask app with one line.
56
+ """Instrument a FastAPI, Starlette, Litestar or Flask app with one line.
57
+ (Django: add ``reqly.integrations.django.ReqlyMiddleware`` to MIDDLEWARE.)
55
58
 
56
59
  Config resolution order for any omitted argument: explicit kwarg >
57
60
  environment variable (REQLY_*) > default. See core.config.Config
@@ -62,6 +65,14 @@ def instrument(
62
65
  rather than raising, so adding Reqly can never be the reason an
63
66
  app fails to start.
64
67
  """
68
+ entry = _CLIENTS.get(id(app))
69
+ existing = entry[1] if entry is not None and entry[0] is app else None
70
+ if existing is not None:
71
+ # A second call would add a second middleware and a second flush
72
+ # thread, double-counting every request.
73
+ logger.warning("reqly: app is already instrumented, ignoring repeat instrument() call")
74
+ return existing
75
+
65
76
  try:
66
77
  framework = _detect_framework(app)
67
78
  config = Config.resolve(
@@ -74,6 +85,8 @@ def instrument(
74
85
  max_queue_size=max_queue_size,
75
86
  ignore_routes=ignore_routes,
76
87
  capture_request_body=capture_request_body,
88
+ release=release,
89
+ environment=environment,
77
90
  )
78
91
  client = ReqlyClient(config)
79
92
 
@@ -87,12 +100,20 @@ def instrument(
87
100
  from .integrations.fastapi import instrument_fastapi
88
101
 
89
102
  instrument_fastapi(app, client)
103
+ elif framework == "starlette":
104
+ from .integrations.starlette import instrument_starlette
105
+
106
+ instrument_starlette(app, client)
107
+ elif framework == "litestar":
108
+ from .integrations.litestar import instrument_litestar
109
+
110
+ instrument_litestar(app, client)
90
111
  else:
91
112
  from .integrations.flask import instrument_flask
92
113
 
93
114
  instrument_flask(app, client)
94
115
 
95
- _CLIENTS[id(app)] = client
116
+ _CLIENTS[id(app)] = (app, client)
96
117
  return client
97
118
  except Exception:
98
119
  logger.warning(
@@ -2,6 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import atexit
4
4
  import logging
5
+ import os
5
6
  import threading
6
7
  import time
7
8
  from collections import deque
@@ -40,11 +41,31 @@ class EventBuffer:
40
41
  self._dropped_events = 0
41
42
 
42
43
  self._stop_event = threading.Event()
44
+ self._start_thread()
45
+ atexit.register(self.shutdown)
46
+ # Pre-fork servers (gunicorn --preload, uWSGI without lazy-apps)
47
+ # import the app -- and start this thread -- in the master, then
48
+ # fork workers. Threads don't survive fork, so without this hook every
49
+ # worker would queue events forever and never ship them.
50
+ if hasattr(os, "register_at_fork"):
51
+ os.register_at_fork(after_in_child=self._reinit_after_fork)
52
+
53
+ def _start_thread(self) -> None:
43
54
  self._thread = threading.Thread(
44
55
  target=self._run, name="reqly-flush", daemon=True
45
56
  )
46
57
  self._thread.start()
47
- atexit.register(self.shutdown)
58
+
59
+ def _reinit_after_fork(self) -> None:
60
+ if self._stop_event.is_set():
61
+ return
62
+ # The parent may have held the lock mid-fork; a fresh one is safe
63
+ # because the child is single-threaded at this point. Events queued
64
+ # before the fork belong to the parent, which ships them itself.
65
+ self._lock = threading.Lock()
66
+ self._queue.clear()
67
+ self._shipper.reset_after_fork()
68
+ self._start_thread()
48
69
 
49
70
  def add(self, event: RequestEvent) -> None:
50
71
  with self._lock: