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.
Files changed (52) hide show
  1. {analytics_platform-0.3.4 → analytics_platform-0.3.6}/PKG-INFO +110 -6
  2. {analytics_platform-0.3.4 → analytics_platform-0.3.6}/README.md +109 -5
  3. {analytics_platform-0.3.4 → analytics_platform-0.3.6}/analytics_platform/__init__.py +197 -16
  4. analytics_platform-0.3.6/analytics_platform/calls.py +253 -0
  5. analytics_platform-0.3.6/analytics_platform/celery.py +199 -0
  6. {analytics_platform-0.3.4 → analytics_platform-0.3.6}/analytics_platform/core.py +1 -1
  7. {analytics_platform-0.3.4 → analytics_platform-0.3.6}/analytics_platform/django.py +46 -2
  8. {analytics_platform-0.3.4 → analytics_platform-0.3.6}/analytics_platform/firewall.py +230 -23
  9. {analytics_platform-0.3.4 → analytics_platform-0.3.6}/analytics_platform/performance.py +54 -7
  10. analytics_platform-0.3.6/analytics_platform/queries.py +1476 -0
  11. analytics_platform-0.3.6/analytics_platform/queues.py +192 -0
  12. {analytics_platform-0.3.4 → analytics_platform-0.3.6}/analytics_platform/request_rules.py +102 -25
  13. {analytics_platform-0.3.4 → analytics_platform-0.3.6}/analytics_platform/resources.py +308 -1
  14. {analytics_platform-0.3.4 → analytics_platform-0.3.6}/analytics_platform/rq.py +96 -0
  15. {analytics_platform-0.3.4 → analytics_platform-0.3.6}/analytics_platform.egg-info/PKG-INFO +110 -6
  16. {analytics_platform-0.3.4 → analytics_platform-0.3.6}/analytics_platform.egg-info/SOURCES.txt +8 -1
  17. {analytics_platform-0.3.4 → analytics_platform-0.3.6}/pyproject.toml +1 -1
  18. analytics_platform-0.3.6/tests/test_calls_queues.py +539 -0
  19. {analytics_platform-0.3.4 → analytics_platform-0.3.6}/tests/test_hardening.py +23 -0
  20. analytics_platform-0.3.6/tests/test_limits.py +154 -0
  21. analytics_platform-0.3.6/tests/test_page_limits.py +187 -0
  22. {analytics_platform-0.3.4 → analytics_platform-0.3.6}/tests/test_queries.py +2 -1
  23. analytics_platform-0.3.6/tests/test_redis.py +428 -0
  24. analytics_platform-0.3.6/tests/test_static_late.py +285 -0
  25. analytics_platform-0.3.4/analytics_platform/celery.py +0 -91
  26. analytics_platform-0.3.4/analytics_platform/queries.py +0 -738
  27. {analytics_platform-0.3.4 → analytics_platform-0.3.6}/LICENSE +0 -0
  28. {analytics_platform-0.3.4 → analytics_platform-0.3.6}/analytics_platform/analytics.py +0 -0
  29. {analytics_platform-0.3.4 → analytics_platform-0.3.6}/analytics_platform/audit.py +0 -0
  30. {analytics_platform-0.3.4 → analytics_platform-0.3.6}/analytics_platform/challenge.py +0 -0
  31. {analytics_platform-0.3.4 → analytics_platform-0.3.6}/analytics_platform/crawlers.py +0 -0
  32. {analytics_platform-0.3.4 → analytics_platform-0.3.6}/analytics_platform/dramatiq.py +0 -0
  33. {analytics_platform-0.3.4 → analytics_platform-0.3.6}/analytics_platform/errors.py +0 -0
  34. {analytics_platform-0.3.4 → analytics_platform-0.3.6}/analytics_platform/flags.py +0 -0
  35. {analytics_platform-0.3.4 → analytics_platform-0.3.6}/analytics_platform/forms.py +0 -0
  36. {analytics_platform-0.3.4 → analytics_platform-0.3.6}/analytics_platform/inventory.py +0 -0
  37. {analytics_platform-0.3.4 → analytics_platform-0.3.6}/analytics_platform/maintenance.py +0 -0
  38. {analytics_platform-0.3.4 → analytics_platform-0.3.6}/analytics_platform/posture.py +0 -0
  39. {analytics_platform-0.3.4 → analytics_platform-0.3.6}/analytics_platform/proxy.py +0 -0
  40. {analytics_platform-0.3.4 → analytics_platform-0.3.6}/analytics_platform.egg-info/dependency_links.txt +0 -0
  41. {analytics_platform-0.3.4 → analytics_platform-0.3.6}/analytics_platform.egg-info/top_level.txt +0 -0
  42. {analytics_platform-0.3.4 → analytics_platform-0.3.6}/setup.cfg +0 -0
  43. {analytics_platform-0.3.4 → analytics_platform-0.3.6}/tests/test_celery.py +0 -0
  44. {analytics_platform-0.3.4 → analytics_platform-0.3.6}/tests/test_challenge.py +0 -0
  45. {analytics_platform-0.3.4 → analytics_platform-0.3.6}/tests/test_checkin.py +0 -0
  46. {analytics_platform-0.3.4 → analytics_platform-0.3.6}/tests/test_crawlers.py +0 -0
  47. {analytics_platform-0.3.4 → analytics_platform-0.3.6}/tests/test_errors.py +0 -0
  48. {analytics_platform-0.3.4 → analytics_platform-0.3.6}/tests/test_firewall.py +0 -0
  49. {analytics_platform-0.3.4 → analytics_platform-0.3.6}/tests/test_proxy.py +0 -0
  50. {analytics_platform-0.3.4 → analytics_platform-0.3.6}/tests/test_request_rules.py +0 -0
  51. {analytics_platform-0.3.4 → analytics_platform-0.3.6}/tests/test_resources.py +0 -0
  52. {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.4
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, and the database queries each one ran (below) | on |
125
- | `crawlers` | Which search engines and AI bots fetch your pages (Health → Crawlers). Django counts them for you; elsewhere call `platform.record_crawler(...)` | on |
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
- Security → Updates then tell the apps apart, and Health → All systems lists
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); on a machine shared
286
- by several apps, keep `resources` on in only one of them.
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, and the database queries each one ran (below) | on |
99
- | `crawlers` | Which search engines and AI bots fetch your pages (Health → Crawlers). Django counts them for you; elsewhere call `platform.record_crawler(...)` | on |
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
- Security → Updates then tell the apps apart, and Health → All systems lists
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); on a machine shared
260
- by several apps, keep `resources` on in only one of them.
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.