analytics-platform 0.3.4__tar.gz → 0.3.6__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.
- {analytics_platform-0.3.4 → analytics_platform-0.3.6}/PKG-INFO +110 -6
- {analytics_platform-0.3.4 → analytics_platform-0.3.6}/README.md +109 -5
- {analytics_platform-0.3.4 → analytics_platform-0.3.6}/analytics_platform/__init__.py +197 -16
- analytics_platform-0.3.6/analytics_platform/calls.py +253 -0
- analytics_platform-0.3.6/analytics_platform/celery.py +199 -0
- {analytics_platform-0.3.4 → analytics_platform-0.3.6}/analytics_platform/core.py +1 -1
- {analytics_platform-0.3.4 → analytics_platform-0.3.6}/analytics_platform/django.py +46 -2
- {analytics_platform-0.3.4 → analytics_platform-0.3.6}/analytics_platform/firewall.py +230 -23
- {analytics_platform-0.3.4 → analytics_platform-0.3.6}/analytics_platform/performance.py +54 -7
- analytics_platform-0.3.6/analytics_platform/queries.py +1476 -0
- analytics_platform-0.3.6/analytics_platform/queues.py +192 -0
- {analytics_platform-0.3.4 → analytics_platform-0.3.6}/analytics_platform/request_rules.py +102 -25
- {analytics_platform-0.3.4 → analytics_platform-0.3.6}/analytics_platform/resources.py +308 -1
- {analytics_platform-0.3.4 → analytics_platform-0.3.6}/analytics_platform/rq.py +96 -0
- {analytics_platform-0.3.4 → analytics_platform-0.3.6}/analytics_platform.egg-info/PKG-INFO +110 -6
- {analytics_platform-0.3.4 → analytics_platform-0.3.6}/analytics_platform.egg-info/SOURCES.txt +8 -1
- {analytics_platform-0.3.4 → analytics_platform-0.3.6}/pyproject.toml +1 -1
- analytics_platform-0.3.6/tests/test_calls_queues.py +539 -0
- {analytics_platform-0.3.4 → analytics_platform-0.3.6}/tests/test_hardening.py +23 -0
- analytics_platform-0.3.6/tests/test_limits.py +154 -0
- analytics_platform-0.3.6/tests/test_page_limits.py +187 -0
- {analytics_platform-0.3.4 → analytics_platform-0.3.6}/tests/test_queries.py +2 -1
- analytics_platform-0.3.6/tests/test_redis.py +428 -0
- analytics_platform-0.3.6/tests/test_static_late.py +285 -0
- analytics_platform-0.3.4/analytics_platform/celery.py +0 -91
- analytics_platform-0.3.4/analytics_platform/queries.py +0 -738
- {analytics_platform-0.3.4 → analytics_platform-0.3.6}/LICENSE +0 -0
- {analytics_platform-0.3.4 → analytics_platform-0.3.6}/analytics_platform/analytics.py +0 -0
- {analytics_platform-0.3.4 → analytics_platform-0.3.6}/analytics_platform/audit.py +0 -0
- {analytics_platform-0.3.4 → analytics_platform-0.3.6}/analytics_platform/challenge.py +0 -0
- {analytics_platform-0.3.4 → analytics_platform-0.3.6}/analytics_platform/crawlers.py +0 -0
- {analytics_platform-0.3.4 → analytics_platform-0.3.6}/analytics_platform/dramatiq.py +0 -0
- {analytics_platform-0.3.4 → analytics_platform-0.3.6}/analytics_platform/errors.py +0 -0
- {analytics_platform-0.3.4 → analytics_platform-0.3.6}/analytics_platform/flags.py +0 -0
- {analytics_platform-0.3.4 → analytics_platform-0.3.6}/analytics_platform/forms.py +0 -0
- {analytics_platform-0.3.4 → analytics_platform-0.3.6}/analytics_platform/inventory.py +0 -0
- {analytics_platform-0.3.4 → analytics_platform-0.3.6}/analytics_platform/maintenance.py +0 -0
- {analytics_platform-0.3.4 → analytics_platform-0.3.6}/analytics_platform/posture.py +0 -0
- {analytics_platform-0.3.4 → analytics_platform-0.3.6}/analytics_platform/proxy.py +0 -0
- {analytics_platform-0.3.4 → analytics_platform-0.3.6}/analytics_platform.egg-info/dependency_links.txt +0 -0
- {analytics_platform-0.3.4 → analytics_platform-0.3.6}/analytics_platform.egg-info/top_level.txt +0 -0
- {analytics_platform-0.3.4 → analytics_platform-0.3.6}/setup.cfg +0 -0
- {analytics_platform-0.3.4 → analytics_platform-0.3.6}/tests/test_celery.py +0 -0
- {analytics_platform-0.3.4 → analytics_platform-0.3.6}/tests/test_challenge.py +0 -0
- {analytics_platform-0.3.4 → analytics_platform-0.3.6}/tests/test_checkin.py +0 -0
- {analytics_platform-0.3.4 → analytics_platform-0.3.6}/tests/test_crawlers.py +0 -0
- {analytics_platform-0.3.4 → analytics_platform-0.3.6}/tests/test_errors.py +0 -0
- {analytics_platform-0.3.4 → analytics_platform-0.3.6}/tests/test_firewall.py +0 -0
- {analytics_platform-0.3.4 → analytics_platform-0.3.6}/tests/test_proxy.py +0 -0
- {analytics_platform-0.3.4 → analytics_platform-0.3.6}/tests/test_request_rules.py +0 -0
- {analytics_platform-0.3.4 → analytics_platform-0.3.6}/tests/test_resources.py +0 -0
- {analytics_platform-0.3.4 → analytics_platform-0.3.6}/tests/test_rq_dramatiq.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: analytics-platform
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.6
|
|
4
4
|
Summary: Firewall, error monitoring and server analytics for your Python server, run from your analytics dashboard.
|
|
5
5
|
Author: steviec87
|
|
6
6
|
License-Expression: MIT
|
|
@@ -106,6 +106,7 @@ Celery's `task_always_eager`.
|
|
|
106
106
|
process of its own, so it sends after every job; only the worker checks
|
|
107
107
|
in (Health → All systems), not a process per job.
|
|
108
108
|
- Don't also wrap a task in `platform.job()`: it would be timed twice.
|
|
109
|
+
- Each queue's backlog: [`platform.watch_queue(...)`](#job-queues).
|
|
109
110
|
|
|
110
111
|
## Modules
|
|
111
112
|
|
|
@@ -121,8 +122,8 @@ Celery's `task_always_eager`.
|
|
|
121
122
|
| `maintenance` | A "back soon" page while maintenance mode is on in the dashboard | on |
|
|
122
123
|
| `headers` | Adds the security headers you pick on the Security page; your own headers win | on |
|
|
123
124
|
| `forms` | Checks forms before you accept them: `platform.check_form(request)` | on |
|
|
124
|
-
| `performance` | Response times and errors of every route (Health → Server speed): Django, Flask and FastAPI route patterns,
|
|
125
|
-
| `crawlers` | Which search engines and AI bots fetch your pages (
|
|
125
|
+
| `performance` | Response times and errors of every route (Health → Server speed): Django, Flask and FastAPI route patterns, the database queries each one ran, the other services it waited on, and your job queues' backlog (below) | on |
|
|
126
|
+
| `crawlers` | Which search engines and AI bots fetch your pages (Off-site → Crawlers). Django counts them for you; elsewhere call `platform.record_crawler(...)` | on |
|
|
126
127
|
| `inventory` | The installed packages and Python version, once a day, so the Security check can flag known security holes (`{"host": ...}`: the server's name) | on |
|
|
127
128
|
| `posture` | How the server is set up, once a day, for the Security check: Django's `DEBUG`, `SECRET_KEY`, `ALLOWED_HOSTS` and cookie settings (when it's a Django site), whether the app runs as root, code anyone on the machine can write, and files in the folders you serve to the web that shouldn't be public (a dump, a `.env`, a `.bak`). The dashboard then asks your site for each of those files and tells you whether it really is downloadable. Reads only, and sends no file's contents. `{"roots": ["public", {"dir": "uploads", "prefix": "files"}]}` (default: Django's `STATIC_ROOT` and `MEDIA_ROOT`, reported at their `STATIC_URL` / `MEDIA_URL`), `{"checks": fn}` for checks of your own (protocol: `sdk/protocols/posture.md`) | on |
|
|
128
129
|
|
|
@@ -275,15 +276,26 @@ Several services on one site: `Platform(..., server_name="worker")`.
|
|
|
275
276
|
**Several apps on one site** (microservices, a worker, a Node service next to
|
|
276
277
|
this one): install the package in each, with the same `site_id`, `secret` and
|
|
277
278
|
`endpoint`, and a `server_name` of its own. Errors, Server speed, Crawlers and
|
|
278
|
-
|
|
279
|
+
Health → Updates then tell the apps apart, and Health → All systems lists
|
|
279
280
|
each with a light: the package checks in about once a minute, so an app that
|
|
280
281
|
stops reads "Stopped" within ten minutes (`sdk/protocols/checkin.md`).
|
|
281
282
|
Without one, each app is named
|
|
282
283
|
after its machine: two apps on one machine are reported as one, and only the
|
|
283
284
|
last package list sent is kept. A service only your other apps call takes
|
|
284
285
|
`firewall=False, proxy=False, maintenance=False` (its requests all come from
|
|
285
|
-
your own apps, so the rate limit would see one visitor)
|
|
286
|
-
|
|
286
|
+
your own apps, so the rate limit would see one visitor).
|
|
287
|
+
|
|
288
|
+
**Several apps on one machine**: leave `resources` on in all of them. The
|
|
289
|
+
machine's CPU, memory and disks are shown once, and each app (each gunicorn
|
|
290
|
+
or Celery worker process too) adds what is its own: its container's memory
|
|
291
|
+
and CPU caps and how close it is to them, how often the cap throttled it,
|
|
292
|
+
processes the kernel killed for memory, open files against the limit, the
|
|
293
|
+
most requests in progress at once, and how long it has run. On Linux the
|
|
294
|
+
machine's waiting time (`/proc/pressure`), connections refused before the
|
|
295
|
+
app accepted them and the connection-tracking table come with them. Python
|
|
296
|
+
sends no heap or garbage-collection figures. A figure the machine won't give
|
|
297
|
+
(no cap, no PSI, not Linux) is left out, never sent as 0 (protocol:
|
|
298
|
+
`sdk/protocols/resources.md`, "Limits").
|
|
287
299
|
|
|
288
300
|
Options: `server_name`, `root` (traceback paths relative to it),
|
|
289
301
|
`capture_uncaught`, `capture_logging`, `spool_dir` (where unsent errors wait;
|
|
@@ -304,6 +316,9 @@ read, so no data leaves it.
|
|
|
304
316
|
| SQLAlchemy | nothing: every engine (Flask-SQLAlchemy too) |
|
|
305
317
|
| pymongo | nothing, but **create the platform before your `MongoClient`**: pymongo only reports to clients made after the package is listening |
|
|
306
318
|
| Any other DB-API driver (psycopg, psycopg2, mysqlclient, PyMySQL, sqlite3...) | `conn = platform.watch_database(psycopg.connect(...))`, and use `conn` as before |
|
|
319
|
+
| ClickHouse: clickhouse-connect, clickhouse-driver | `client = platform.watch_database(clickhouse_connect.get_client(...))` (or a `clickhouse_driver.Client`), and use `client` as before |
|
|
320
|
+
| Redis: redis-py (`Redis`, `RedisCluster`, sync or `redis.asyncio`) | `platform.watch_database(r)`: the client is watched in place, so keep using `r` |
|
|
321
|
+
| Django's cache on Redis (Django's own `RedisCache`, or django-redis) | `platform.watch_database(caches["default"])`, once at start-up (in `AppConfig.ready()`) |
|
|
307
322
|
|
|
308
323
|
A query outside any request or job is counted as `JOB (background)`. Each
|
|
309
324
|
query is counted once: when SQLAlchemy or Django runs it over a watched
|
|
@@ -311,6 +326,95 @@ connection, they count it and the connection doesn't. It is on with
|
|
|
311
326
|
`performance`, and switched off with it in the dashboard; `queries=False`
|
|
312
327
|
keeps Server speed and leaves queries out (protocol: `sdk/protocols/queries.md`).
|
|
313
328
|
|
|
329
|
+
ClickHouse: clickhouse-connect's `query()` (and `query_df()`, `query_np()`,
|
|
330
|
+
`query_arrow()`, `raw_query()`) reads the whole result before it returns, so
|
|
331
|
+
its time is the result's too; `command()` and the inserts are timed until
|
|
332
|
+
they answer. clickhouse-driver's `execute()` likewise. The streaming
|
|
333
|
+
queries (`query_rows_stream()`, `execute_iter()`) aren't timed. Only the
|
|
334
|
+
client you pass is wrapped; the library itself is left alone.
|
|
335
|
+
|
|
336
|
+
Redis: each command is counted by its name and the shape of its key (`GET
|
|
337
|
+
user:?:cart`); no other argument or value is read. A pipeline or transaction
|
|
338
|
+
is one call (`PIPELINE GET user:?`, `MULTI SET cart:?`), timed as a whole.
|
|
339
|
+
Commands that wait (`BLPOP`, `XREAD BLOCK`, subscribing) aren't timed, and
|
|
340
|
+
nor are this package's own reads of your RQ queues. An error reply counts
|
|
341
|
+
as failed; a nil reply doesn't.
|
|
342
|
+
|
|
343
|
+
```python
|
|
344
|
+
r = redis.Redis.from_url(REDIS_URL)
|
|
345
|
+
platform.watch_database(r)
|
|
346
|
+
|
|
347
|
+
# Django (settings: CACHES backend django.core.cache.backends.redis.RedisCache
|
|
348
|
+
# or django_redis.cache.RedisCache), in your AppConfig.ready():
|
|
349
|
+
from django.core.cache import caches
|
|
350
|
+
platform.watch_database(caches["default"])
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
## Outgoing calls
|
|
354
|
+
|
|
355
|
+
Health → Server speed → Calls shows how long each route waited on other
|
|
356
|
+
services (a payment provider, an email API, your own microservices) by the
|
|
357
|
+
host it called (`api.stripe.com`), with how many calls failed. One line for
|
|
358
|
+
each client, where you make it:
|
|
359
|
+
|
|
360
|
+
```python
|
|
361
|
+
session = platform.watch_http(requests.Session()) # requests
|
|
362
|
+
client = platform.watch_http(httpx.Client()) # httpx; AsyncClient too
|
|
363
|
+
|
|
364
|
+
# httpx, with calls that got no answer counted too:
|
|
365
|
+
client = httpx.Client(transport=platform.watch_http(httpx.HTTPTransport()))
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
- **Only the host is sent.** A call's path, query string, headers and body
|
|
369
|
+
never leave your server: paths carry IDs and keys.
|
|
370
|
+
- A call is timed from being sent to its response's headers arriving. A
|
|
371
|
+
call that got no answer (refused, timed out), or a 5xx, is a failed one; a
|
|
372
|
+
4xx is the other side's answer, not a failure. A call your code cancels
|
|
373
|
+
isn't counted.
|
|
374
|
+
- requests: each adapter the session has mounted is wrapped, keeping its
|
|
375
|
+
retries and pool settings; mount your own adapters before watching.
|
|
376
|
+
- httpx: `watch_http(client)` adds event hooks, which see every answered
|
|
377
|
+
call but not one that got no answer at all. A watched transport
|
|
378
|
+
(`AsyncHTTPTransport` for an `AsyncClient`) sees those too; with both on
|
|
379
|
+
one client a call is counted once.
|
|
380
|
+
- urllib3 and `urllib.request` have no public hook, so their calls aren't
|
|
381
|
+
counted (and nothing of theirs is patched).
|
|
382
|
+
- Calls are filed as queries are: under the request, the job, or
|
|
383
|
+
`JOB (background)`. The package's own calls to us are left out.
|
|
384
|
+
- They ride on Server speed's switch; `calls=False` leaves them out
|
|
385
|
+
(protocol: `sdk/protocols/calls.md`).
|
|
386
|
+
|
|
387
|
+
## Job queues
|
|
388
|
+
|
|
389
|
+
Health → Server speed → Jobs shows each queue's backlog once a minute: jobs
|
|
390
|
+
waiting, running, delayed and failed, how long the oldest has waited, and
|
|
391
|
+
jobs added against jobs finished, so a queue falling behind shows before the
|
|
392
|
+
emails go out an hour late.
|
|
393
|
+
|
|
394
|
+
```python
|
|
395
|
+
platform.watch_queue(celery_app) # Celery on a Redis broker; queues=["emails", ...] to name them
|
|
396
|
+
platform.watch_queue(Queue("emails", connection=redis)) # RQ, in web servers and workers alike
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
- **Celery** has no backlog API, so the broker's own lists are read from
|
|
400
|
+
Redis (one `LLEN` per queue and priority). Celery can't say what is
|
|
401
|
+
running, delayed or failed, nor how long a message has waited, so those
|
|
402
|
+
are left out; each process counts the tasks it published and, in a
|
|
403
|
+
worker, the ones it finished (a retry isn't finished). With a RabbitMQ
|
|
404
|
+
broker nothing is sent from here: the server agent reads RabbitMQ's own
|
|
405
|
+
numbers.
|
|
406
|
+
- **RQ**: the queue, its started, scheduled, deferred and failed job
|
|
407
|
+
registries, and when its first job was added. RQ has no hook for a job
|
|
408
|
+
being added; jobs finished are counted by `platform.rq_worker()`'s
|
|
409
|
+
worker, so watch the queue in the worker's code too.
|
|
410
|
+
- Watching one queue in several processes is fine: the dashboard takes the
|
|
411
|
+
largest reading, never the sum. Its Redis is named by host, port and
|
|
412
|
+
database; never a password.
|
|
413
|
+
- A reading that fails is a gap, not a zero. Readings run on a background
|
|
414
|
+
thread, never in a request.
|
|
415
|
+
- They ride on Server speed's switch; `queues=False` leaves them out
|
|
416
|
+
(protocol: `sdk/protocols/queues.md`).
|
|
417
|
+
|
|
314
418
|
## Server analytics
|
|
315
419
|
|
|
316
420
|
For what only your server knows for sure. Not pageviews: the tracker counts those.
|
|
@@ -80,6 +80,7 @@ Celery's `task_always_eager`.
|
|
|
80
80
|
process of its own, so it sends after every job; only the worker checks
|
|
81
81
|
in (Health → All systems), not a process per job.
|
|
82
82
|
- Don't also wrap a task in `platform.job()`: it would be timed twice.
|
|
83
|
+
- Each queue's backlog: [`platform.watch_queue(...)`](#job-queues).
|
|
83
84
|
|
|
84
85
|
## Modules
|
|
85
86
|
|
|
@@ -95,8 +96,8 @@ Celery's `task_always_eager`.
|
|
|
95
96
|
| `maintenance` | A "back soon" page while maintenance mode is on in the dashboard | on |
|
|
96
97
|
| `headers` | Adds the security headers you pick on the Security page; your own headers win | on |
|
|
97
98
|
| `forms` | Checks forms before you accept them: `platform.check_form(request)` | on |
|
|
98
|
-
| `performance` | Response times and errors of every route (Health → Server speed): Django, Flask and FastAPI route patterns,
|
|
99
|
-
| `crawlers` | Which search engines and AI bots fetch your pages (
|
|
99
|
+
| `performance` | Response times and errors of every route (Health → Server speed): Django, Flask and FastAPI route patterns, the database queries each one ran, the other services it waited on, and your job queues' backlog (below) | on |
|
|
100
|
+
| `crawlers` | Which search engines and AI bots fetch your pages (Off-site → Crawlers). Django counts them for you; elsewhere call `platform.record_crawler(...)` | on |
|
|
100
101
|
| `inventory` | The installed packages and Python version, once a day, so the Security check can flag known security holes (`{"host": ...}`: the server's name) | on |
|
|
101
102
|
| `posture` | How the server is set up, once a day, for the Security check: Django's `DEBUG`, `SECRET_KEY`, `ALLOWED_HOSTS` and cookie settings (when it's a Django site), whether the app runs as root, code anyone on the machine can write, and files in the folders you serve to the web that shouldn't be public (a dump, a `.env`, a `.bak`). The dashboard then asks your site for each of those files and tells you whether it really is downloadable. Reads only, and sends no file's contents. `{"roots": ["public", {"dir": "uploads", "prefix": "files"}]}` (default: Django's `STATIC_ROOT` and `MEDIA_ROOT`, reported at their `STATIC_URL` / `MEDIA_URL`), `{"checks": fn}` for checks of your own (protocol: `sdk/protocols/posture.md`) | on |
|
|
102
103
|
|
|
@@ -249,15 +250,26 @@ Several services on one site: `Platform(..., server_name="worker")`.
|
|
|
249
250
|
**Several apps on one site** (microservices, a worker, a Node service next to
|
|
250
251
|
this one): install the package in each, with the same `site_id`, `secret` and
|
|
251
252
|
`endpoint`, and a `server_name` of its own. Errors, Server speed, Crawlers and
|
|
252
|
-
|
|
253
|
+
Health → Updates then tell the apps apart, and Health → All systems lists
|
|
253
254
|
each with a light: the package checks in about once a minute, so an app that
|
|
254
255
|
stops reads "Stopped" within ten minutes (`sdk/protocols/checkin.md`).
|
|
255
256
|
Without one, each app is named
|
|
256
257
|
after its machine: two apps on one machine are reported as one, and only the
|
|
257
258
|
last package list sent is kept. A service only your other apps call takes
|
|
258
259
|
`firewall=False, proxy=False, maintenance=False` (its requests all come from
|
|
259
|
-
your own apps, so the rate limit would see one visitor)
|
|
260
|
-
|
|
260
|
+
your own apps, so the rate limit would see one visitor).
|
|
261
|
+
|
|
262
|
+
**Several apps on one machine**: leave `resources` on in all of them. The
|
|
263
|
+
machine's CPU, memory and disks are shown once, and each app (each gunicorn
|
|
264
|
+
or Celery worker process too) adds what is its own: its container's memory
|
|
265
|
+
and CPU caps and how close it is to them, how often the cap throttled it,
|
|
266
|
+
processes the kernel killed for memory, open files against the limit, the
|
|
267
|
+
most requests in progress at once, and how long it has run. On Linux the
|
|
268
|
+
machine's waiting time (`/proc/pressure`), connections refused before the
|
|
269
|
+
app accepted them and the connection-tracking table come with them. Python
|
|
270
|
+
sends no heap or garbage-collection figures. A figure the machine won't give
|
|
271
|
+
(no cap, no PSI, not Linux) is left out, never sent as 0 (protocol:
|
|
272
|
+
`sdk/protocols/resources.md`, "Limits").
|
|
261
273
|
|
|
262
274
|
Options: `server_name`, `root` (traceback paths relative to it),
|
|
263
275
|
`capture_uncaught`, `capture_logging`, `spool_dir` (where unsent errors wait;
|
|
@@ -278,6 +290,9 @@ read, so no data leaves it.
|
|
|
278
290
|
| SQLAlchemy | nothing: every engine (Flask-SQLAlchemy too) |
|
|
279
291
|
| pymongo | nothing, but **create the platform before your `MongoClient`**: pymongo only reports to clients made after the package is listening |
|
|
280
292
|
| Any other DB-API driver (psycopg, psycopg2, mysqlclient, PyMySQL, sqlite3...) | `conn = platform.watch_database(psycopg.connect(...))`, and use `conn` as before |
|
|
293
|
+
| ClickHouse: clickhouse-connect, clickhouse-driver | `client = platform.watch_database(clickhouse_connect.get_client(...))` (or a `clickhouse_driver.Client`), and use `client` as before |
|
|
294
|
+
| Redis: redis-py (`Redis`, `RedisCluster`, sync or `redis.asyncio`) | `platform.watch_database(r)`: the client is watched in place, so keep using `r` |
|
|
295
|
+
| Django's cache on Redis (Django's own `RedisCache`, or django-redis) | `platform.watch_database(caches["default"])`, once at start-up (in `AppConfig.ready()`) |
|
|
281
296
|
|
|
282
297
|
A query outside any request or job is counted as `JOB (background)`. Each
|
|
283
298
|
query is counted once: when SQLAlchemy or Django runs it over a watched
|
|
@@ -285,6 +300,95 @@ connection, they count it and the connection doesn't. It is on with
|
|
|
285
300
|
`performance`, and switched off with it in the dashboard; `queries=False`
|
|
286
301
|
keeps Server speed and leaves queries out (protocol: `sdk/protocols/queries.md`).
|
|
287
302
|
|
|
303
|
+
ClickHouse: clickhouse-connect's `query()` (and `query_df()`, `query_np()`,
|
|
304
|
+
`query_arrow()`, `raw_query()`) reads the whole result before it returns, so
|
|
305
|
+
its time is the result's too; `command()` and the inserts are timed until
|
|
306
|
+
they answer. clickhouse-driver's `execute()` likewise. The streaming
|
|
307
|
+
queries (`query_rows_stream()`, `execute_iter()`) aren't timed. Only the
|
|
308
|
+
client you pass is wrapped; the library itself is left alone.
|
|
309
|
+
|
|
310
|
+
Redis: each command is counted by its name and the shape of its key (`GET
|
|
311
|
+
user:?:cart`); no other argument or value is read. A pipeline or transaction
|
|
312
|
+
is one call (`PIPELINE GET user:?`, `MULTI SET cart:?`), timed as a whole.
|
|
313
|
+
Commands that wait (`BLPOP`, `XREAD BLOCK`, subscribing) aren't timed, and
|
|
314
|
+
nor are this package's own reads of your RQ queues. An error reply counts
|
|
315
|
+
as failed; a nil reply doesn't.
|
|
316
|
+
|
|
317
|
+
```python
|
|
318
|
+
r = redis.Redis.from_url(REDIS_URL)
|
|
319
|
+
platform.watch_database(r)
|
|
320
|
+
|
|
321
|
+
# Django (settings: CACHES backend django.core.cache.backends.redis.RedisCache
|
|
322
|
+
# or django_redis.cache.RedisCache), in your AppConfig.ready():
|
|
323
|
+
from django.core.cache import caches
|
|
324
|
+
platform.watch_database(caches["default"])
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
## Outgoing calls
|
|
328
|
+
|
|
329
|
+
Health → Server speed → Calls shows how long each route waited on other
|
|
330
|
+
services (a payment provider, an email API, your own microservices) by the
|
|
331
|
+
host it called (`api.stripe.com`), with how many calls failed. One line for
|
|
332
|
+
each client, where you make it:
|
|
333
|
+
|
|
334
|
+
```python
|
|
335
|
+
session = platform.watch_http(requests.Session()) # requests
|
|
336
|
+
client = platform.watch_http(httpx.Client()) # httpx; AsyncClient too
|
|
337
|
+
|
|
338
|
+
# httpx, with calls that got no answer counted too:
|
|
339
|
+
client = httpx.Client(transport=platform.watch_http(httpx.HTTPTransport()))
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
- **Only the host is sent.** A call's path, query string, headers and body
|
|
343
|
+
never leave your server: paths carry IDs and keys.
|
|
344
|
+
- A call is timed from being sent to its response's headers arriving. A
|
|
345
|
+
call that got no answer (refused, timed out), or a 5xx, is a failed one; a
|
|
346
|
+
4xx is the other side's answer, not a failure. A call your code cancels
|
|
347
|
+
isn't counted.
|
|
348
|
+
- requests: each adapter the session has mounted is wrapped, keeping its
|
|
349
|
+
retries and pool settings; mount your own adapters before watching.
|
|
350
|
+
- httpx: `watch_http(client)` adds event hooks, which see every answered
|
|
351
|
+
call but not one that got no answer at all. A watched transport
|
|
352
|
+
(`AsyncHTTPTransport` for an `AsyncClient`) sees those too; with both on
|
|
353
|
+
one client a call is counted once.
|
|
354
|
+
- urllib3 and `urllib.request` have no public hook, so their calls aren't
|
|
355
|
+
counted (and nothing of theirs is patched).
|
|
356
|
+
- Calls are filed as queries are: under the request, the job, or
|
|
357
|
+
`JOB (background)`. The package's own calls to us are left out.
|
|
358
|
+
- They ride on Server speed's switch; `calls=False` leaves them out
|
|
359
|
+
(protocol: `sdk/protocols/calls.md`).
|
|
360
|
+
|
|
361
|
+
## Job queues
|
|
362
|
+
|
|
363
|
+
Health → Server speed → Jobs shows each queue's backlog once a minute: jobs
|
|
364
|
+
waiting, running, delayed and failed, how long the oldest has waited, and
|
|
365
|
+
jobs added against jobs finished, so a queue falling behind shows before the
|
|
366
|
+
emails go out an hour late.
|
|
367
|
+
|
|
368
|
+
```python
|
|
369
|
+
platform.watch_queue(celery_app) # Celery on a Redis broker; queues=["emails", ...] to name them
|
|
370
|
+
platform.watch_queue(Queue("emails", connection=redis)) # RQ, in web servers and workers alike
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
- **Celery** has no backlog API, so the broker's own lists are read from
|
|
374
|
+
Redis (one `LLEN` per queue and priority). Celery can't say what is
|
|
375
|
+
running, delayed or failed, nor how long a message has waited, so those
|
|
376
|
+
are left out; each process counts the tasks it published and, in a
|
|
377
|
+
worker, the ones it finished (a retry isn't finished). With a RabbitMQ
|
|
378
|
+
broker nothing is sent from here: the server agent reads RabbitMQ's own
|
|
379
|
+
numbers.
|
|
380
|
+
- **RQ**: the queue, its started, scheduled, deferred and failed job
|
|
381
|
+
registries, and when its first job was added. RQ has no hook for a job
|
|
382
|
+
being added; jobs finished are counted by `platform.rq_worker()`'s
|
|
383
|
+
worker, so watch the queue in the worker's code too.
|
|
384
|
+
- Watching one queue in several processes is fine: the dashboard takes the
|
|
385
|
+
largest reading, never the sum. Its Redis is named by host, port and
|
|
386
|
+
database; never a password.
|
|
387
|
+
- A reading that fails is a gap, not a zero. Readings run on a background
|
|
388
|
+
thread, never in a request.
|
|
389
|
+
- They ride on Server speed's switch; `queues=False` leaves them out
|
|
390
|
+
(protocol: `sdk/protocols/queues.md`).
|
|
391
|
+
|
|
288
392
|
## Server analytics
|
|
289
393
|
|
|
290
394
|
For what only your server knows for sure. Not pageviews: the tracker counts those.
|