celery-liveops 0.1.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 (43) hide show
  1. celery_liveops-0.1.0/.devcontainer/devcontainer.json +28 -0
  2. celery_liveops-0.1.0/.github/workflows/ci.yml +46 -0
  3. celery_liveops-0.1.0/.github/workflows/demo.yml +73 -0
  4. celery_liveops-0.1.0/.github/workflows/release.yml +42 -0
  5. celery_liveops-0.1.0/.gitignore +14 -0
  6. celery_liveops-0.1.0/CHANGELOG.md +27 -0
  7. celery_liveops-0.1.0/LICENSE +21 -0
  8. celery_liveops-0.1.0/PKG-INFO +315 -0
  9. celery_liveops-0.1.0/README.md +277 -0
  10. celery_liveops-0.1.0/demo/Dockerfile +14 -0
  11. celery_liveops-0.1.0/demo/README.md +51 -0
  12. celery_liveops-0.1.0/demo/app/__init__.py +0 -0
  13. celery_liveops-0.1.0/demo/app/api.py +88 -0
  14. celery_liveops-0.1.0/demo/app/tasks.py +161 -0
  15. celery_liveops-0.1.0/demo/capture.py +168 -0
  16. celery_liveops-0.1.0/demo/docker-compose.yml +71 -0
  17. celery_liveops-0.1.0/demo/static/index.html +244 -0
  18. celery_liveops-0.1.0/docs/design-notes.md +232 -0
  19. celery_liveops-0.1.0/docs/screenshots/01-live.png +0 -0
  20. celery_liveops-0.1.0/docs/screenshots/02-no-signal.png +0 -0
  21. celery_liveops-0.1.0/docs/screenshots/03-orphan-lock.png +0 -0
  22. celery_liveops-0.1.0/pyproject.toml +65 -0
  23. celery_liveops-0.1.0/src/celery_liveops/__init__.py +205 -0
  24. celery_liveops-0.1.0/src/celery_liveops/config.py +166 -0
  25. celery_liveops-0.1.0/src/celery_liveops/contrib/__init__.py +1 -0
  26. celery_liveops-0.1.0/src/celery_liveops/contrib/fastapi.py +119 -0
  27. celery_liveops-0.1.0/src/celery_liveops/locks.py +258 -0
  28. celery_liveops-0.1.0/src/celery_liveops/logs.py +274 -0
  29. celery_liveops-0.1.0/src/celery_liveops/presence.py +341 -0
  30. celery_liveops-0.1.0/src/celery_liveops/py.typed +0 -0
  31. celery_liveops-0.1.0/src/celery_liveops/queues.py +156 -0
  32. celery_liveops-0.1.0/src/celery_liveops/scale.py +146 -0
  33. celery_liveops-0.1.0/src/celery_liveops/snapshots.py +190 -0
  34. celery_liveops-0.1.0/src/celery_liveops/store.py +86 -0
  35. celery_liveops-0.1.0/src/celery_liveops/watchdog.py +198 -0
  36. celery_liveops-0.1.0/tests/conftest.py +44 -0
  37. celery_liveops-0.1.0/tests/test_demo_smoke.py +98 -0
  38. celery_liveops-0.1.0/tests/test_locks.py +132 -0
  39. celery_liveops-0.1.0/tests/test_logs.py +159 -0
  40. celery_liveops-0.1.0/tests/test_presence.py +116 -0
  41. celery_liveops-0.1.0/tests/test_queues_and_scale.py +180 -0
  42. celery_liveops-0.1.0/tests/test_snapshots_and_api.py +219 -0
  43. celery_liveops-0.1.0/tests/test_watchdog.py +116 -0
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "celery-liveops",
3
+ "image": "mcr.microsoft.com/devcontainers/python:3.12",
4
+
5
+ // The demo is a compose stack, so the container needs its own Docker.
6
+ "features": {
7
+ "ghcr.io/devcontainers/features/docker-in-docker:2": {}
8
+ },
9
+
10
+ "forwardPorts": [8000, 15672],
11
+ "portsAttributes": {
12
+ "8000": { "label": "Demo panel", "onAutoForward": "openPreview" },
13
+ "15672": { "label": "RabbitMQ management" }
14
+ },
15
+
16
+ // Dev dependencies for the test suite; the demo itself runs in compose.
17
+ "postCreateCommand": "pip install -e '.[dev]'",
18
+
19
+ // Building the four images takes a couple of minutes on a cold Codespace;
20
+ // starting it here means the panel is ready by the time the editor is.
21
+ "postAttachCommand": "docker compose -f demo/docker-compose.yml up -d --build",
22
+
23
+ "customizations": {
24
+ "vscode": {
25
+ "extensions": ["ms-python.python", "charliermarsh.ruff"]
26
+ }
27
+ }
28
+ }
@@ -0,0 +1,46 @@
1
+ name: CI
2
+
3
+ on:
4
+ # Every commit on every branch: a push is exactly when you want to know.
5
+ push:
6
+ pull_request:
7
+ workflow_dispatch:
8
+
9
+ jobs:
10
+ test:
11
+ runs-on: ubuntu-latest
12
+ strategy:
13
+ fail-fast: false
14
+ matrix:
15
+ python: ["3.9", "3.10", "3.11", "3.12", "3.13"]
16
+
17
+ steps:
18
+ - uses: actions/checkout@v4
19
+
20
+ - uses: actions/setup-python@v5
21
+ with:
22
+ python-version: ${{ matrix.python }}
23
+
24
+ - name: Install
25
+ run: |
26
+ python -m pip install --upgrade pip
27
+ pip install -e ".[dev]"
28
+
29
+ - name: Lint
30
+ run: ruff check .
31
+
32
+ - name: Test
33
+ # fakeredis stands in for a server: the contract under test is what
34
+ # happens when Redis misbehaves, and a fake can stage that on demand.
35
+ run: pytest --cov=celery_liveops --cov-report=term-missing
36
+
37
+ build:
38
+ runs-on: ubuntu-latest
39
+ steps:
40
+ - uses: actions/checkout@v4
41
+ - uses: actions/setup-python@v5
42
+ with:
43
+ python-version: "3.12"
44
+ - run: pip install build twine
45
+ - run: python -m build
46
+ - run: twine check dist/*
@@ -0,0 +1,73 @@
1
+ # Runs the demo stack for real and photographs it.
2
+ #
3
+ # This is a test as much as a screenshot job: it builds the compose stack, starts
4
+ # actual work, stops the worker container mid-run and asserts the panel notices.
5
+ # If presence, the live log buffer or the lock catalogue break, this fails --
6
+ # which is more than the unit suite can claim, since it runs against fakeredis.
7
+ #
8
+ # Screenshots are always uploaded as a build artifact, and committed back to
9
+ # docs/screenshots (which is what the README shows) on main. The paths filter is
10
+ # what keeps that honest: the log timestamps baked into each image differ on
11
+ # every run, so without it every unrelated push would produce a commit.
12
+ name: Demo
13
+
14
+ on:
15
+ push:
16
+ branches: [main]
17
+ paths: ["demo/**", "src/**", ".github/workflows/demo.yml"]
18
+ pull_request:
19
+ paths: ["demo/**", "src/**", ".github/workflows/demo.yml"]
20
+ workflow_dispatch:
21
+
22
+ permissions:
23
+ contents: write
24
+
25
+ concurrency:
26
+ group: demo-${{ github.ref }}
27
+ cancel-in-progress: true
28
+
29
+ jobs:
30
+ capture:
31
+ runs-on: ubuntu-latest
32
+ timeout-minutes: 25
33
+
34
+ steps:
35
+ - uses: actions/checkout@v4
36
+
37
+ - uses: actions/setup-python@v5
38
+ with:
39
+ python-version: "3.12"
40
+
41
+ - name: Install the capture tooling
42
+ run: |
43
+ pip install playwright httpx
44
+ playwright install --with-deps chromium
45
+
46
+ - name: Start the stack
47
+ run: docker compose -f demo/docker-compose.yml up -d --build
48
+
49
+ - name: Drive it and capture
50
+ run: python demo/capture.py --out-dir docs/screenshots
51
+
52
+ - uses: actions/upload-artifact@v4
53
+ with:
54
+ name: demo-screenshots
55
+ path: docs/screenshots/*.png
56
+
57
+ - name: Commit the screenshots
58
+ if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request'
59
+ run: |
60
+ git config user.name "github-actions[bot]"
61
+ git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
62
+ git add docs/screenshots
63
+ git diff --staged --quiet && echo "nothing changed" && exit 0
64
+ git commit -m "Atualiza os prints do demo [skip ci]"
65
+ git push
66
+
67
+ - name: Container logs
68
+ if: failure()
69
+ run: docker compose -f demo/docker-compose.yml logs --tail=200
70
+
71
+ - name: Tear down
72
+ if: always()
73
+ run: docker compose -f demo/docker-compose.yml down -v
@@ -0,0 +1,42 @@
1
+ # Publishes to PyPI on a v* tag, using Trusted Publishing (OIDC) -- no API token
2
+ # stored in this repository.
3
+ #
4
+ # One-time setup on PyPI: Project -> Publishing -> add a GitHub publisher for
5
+ # this repo, workflow `release.yml`, environment `pypi`.
6
+ name: Release
7
+
8
+ on:
9
+ push:
10
+ tags: ["v*"]
11
+ # Manual escape hatch: a tag whose commit the runner skipped (a bot commit
12
+ # carrying the skip marker, say) never fires the push event, and the version
13
+ # number is already spent. This republishes the same ref without a new tag.
14
+ workflow_dispatch:
15
+
16
+ jobs:
17
+ build:
18
+ runs-on: ubuntu-latest
19
+ steps:
20
+ - uses: actions/checkout@v4
21
+ - uses: actions/setup-python@v5
22
+ with:
23
+ python-version: "3.12"
24
+ - run: pip install build
25
+ - run: python -m build
26
+ - uses: actions/upload-artifact@v4
27
+ with:
28
+ name: dist
29
+ path: dist/
30
+
31
+ publish:
32
+ needs: build
33
+ runs-on: ubuntu-latest
34
+ environment: pypi
35
+ permissions:
36
+ id-token: write # required for Trusted Publishing
37
+ steps:
38
+ - uses: actions/download-artifact@v4
39
+ with:
40
+ name: dist
41
+ path: dist/
42
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,14 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ build/
5
+ dist/
6
+ .venv/
7
+ venv/
8
+ .pytest_cache/
9
+ .ruff_cache/
10
+ .coverage
11
+ htmlcov/
12
+ .idea/
13
+ .vscode/
14
+ .DS_Store
@@ -0,0 +1,27 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here. The format follows
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project
5
+ adheres to [Semantic Versioning](https://semver.org/).
6
+
7
+ ## [0.1.0] - 2026-09-01
8
+
9
+ First public release.
10
+
11
+ ### Added
12
+
13
+ - **logs** - per-run live terminal over a capped Redis list, with the previous
14
+ attempt's buffer archived rather than deleted on retry (`read_orphan_logs`).
15
+ - **presence** - per-run and per-worker liveness written from Celery signals,
16
+ read with `EXISTS`; `alive_among` answers for a whole page in one round trip.
17
+ - **watchdog** - a hard deadline registered per task name, so one pool serves
18
+ both short and overnight jobs; `safe_stop_at` for cooperative stopping.
19
+ - **locks** - a named catalogue of the Redis locks a dead process leaves behind,
20
+ with an allowlist on release and automatic release limited to singleton locks.
21
+ - **queues** - `queue_depth`, `consumers_by_queue`, `has_consumer` and
22
+ `orphan_queues`, each failing in the direction its caller needs.
23
+ - **scale** - runtime pool resize, autoscaler ceiling, and boot-time
24
+ reapplication of a persisted target.
25
+ - **snapshots** - throttled, background-stored last frame of a browser task.
26
+ - `contrib.fastapi.liveops_router`, which refuses to mount without auth.
27
+ - A four-container demo stack under `demo/`.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Joao Pedro
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,315 @@
1
+ Metadata-Version: 2.5
2
+ Name: celery-liveops
3
+ Version: 0.1.0
4
+ Summary: See inside a long-running Celery task while it runs: live logs, presence, per-task deadlines, orphan locks.
5
+ Project-URL: Homepage, https://github.com/joaopalmeidao/celery-liveops
6
+ Project-URL: Issues, https://github.com/joaopalmeidao/celery-liveops/issues
7
+ Author: Joao Pedro
8
+ License: MIT
9
+ License-File: LICENSE
10
+ Keywords: celery,logs,observability,redis,selenium,watchdog,worker
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.9
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: System :: Distributed Computing
21
+ Classifier: Topic :: System :: Monitoring
22
+ Classifier: Typing :: Typed
23
+ Requires-Python: >=3.9
24
+ Requires-Dist: redis>=4.2
25
+ Provides-Extra: celery
26
+ Requires-Dist: celery>=5.2; extra == 'celery'
27
+ Provides-Extra: dev
28
+ Requires-Dist: celery>=5.2; extra == 'dev'
29
+ Requires-Dist: fakeredis>=2.20; extra == 'dev'
30
+ Requires-Dist: fastapi>=0.100; extra == 'dev'
31
+ Requires-Dist: httpx>=0.27; extra == 'dev'
32
+ Requires-Dist: pytest-cov>=4.1; extra == 'dev'
33
+ Requires-Dist: pytest>=7.4; extra == 'dev'
34
+ Requires-Dist: ruff>=0.6; extra == 'dev'
35
+ Provides-Extra: fastapi
36
+ Requires-Dist: fastapi>=0.100; extra == 'fastapi'
37
+ Description-Content-Type: text/markdown
38
+
39
+ # celery-liveops
40
+
41
+ [![CI](https://github.com/joaopalmeidao/celery-liveops/actions/workflows/ci.yml/badge.svg)](https://github.com/joaopalmeidao/celery-liveops/actions/workflows/ci.yml)
42
+ [![Demo](https://github.com/joaopalmeidao/celery-liveops/actions/workflows/demo.yml/badge.svg)](https://github.com/joaopalmeidao/celery-liveops/actions/workflows/demo.yml)
43
+ [![PyPI](https://img.shields.io/pypi/v/celery-liveops)](https://pypi.org/project/celery-liveops/)
44
+ [![Python](https://img.shields.io/pypi/pyversions/celery-liveops)](https://pypi.org/project/celery-liveops/)
45
+ [![License](https://img.shields.io/badge/license-MIT-blue)](LICENSE)
46
+
47
+ **See inside a long-running Celery task while it runs.**
48
+
49
+ A task that takes eight hours is a black box. The result backend tells you what
50
+ happened *after* it happened. Flower tells you a task is running — not what it is
51
+ doing, not whether the process behind it is still breathing, and not why the next
52
+ one exits instantly with "already running".
53
+
54
+ `celery-liveops` is the small set of pieces that answer those questions. They
55
+ come from a production system running headless-browser automations: hundreds of
56
+ runs a day, from 15-second jobs to overnight sweeps, on workers that must be
57
+ restarted without losing evidence.
58
+
59
+ ```python
60
+ from celery_liveops import install, capture_logs
61
+
62
+ install(watchdog={"crawl": 3600}, watchdog_enabled=True)
63
+
64
+ @app.task(bind=True)
65
+ def crawl(self, url):
66
+ with capture_logs(self.request.id):
67
+ log.info("fetching %s", url) # readable live, from your API process
68
+ ```
69
+
70
+ ```python
71
+ from celery_liveops import read_any_logs, is_alive
72
+
73
+ read_any_logs(task_id) # the terminal — live, or the archived previous attempt
74
+ is_alive(task_id) # ...or is this "running" row an orphan?
75
+ ```
76
+
77
+ ---
78
+
79
+ ## What is in the box
80
+
81
+ | Module | Answers |
82
+ |---|---|
83
+ | **logs** | What is this task printing *right now*? And what did the attempt that died print? |
84
+ | **presence** | Does this "running" row still have a living process behind it? Which workers are up? |
85
+ | **watchdog** | A hard deadline **per task**, not per container — so one pool serves a 15-minute job and an 8-hour one. |
86
+ | **locks** | Which Redis locks did a dead process leave behind, and who can safely release them? |
87
+ | **queues** | Queue depth, who consumes what, and the declared queue **nobody** is consuming. |
88
+ | **scale** | Resize a running worker; make that size survive a restart. |
89
+ | **snapshots** | A live frame of what a browser task is looking at. |
90
+
91
+ One rule runs through all of it: **observability must never break the job it is
92
+ observing.** Every Redis call is wrapped, every failure degrades the panel rather
93
+ than the work. A Redis outage greys out a badge; it does not fail a task.
94
+
95
+ ---
96
+
97
+ ## Install
98
+
99
+ ```bash
100
+ pip install celery-liveops # core (redis)
101
+ pip install "celery-liveops[celery]" # + signal wiring
102
+ pip install "celery-liveops[fastapi]" # + the ready-made router
103
+ ```
104
+
105
+ Python 3.9+. Redis 5+. Celery is optional — importing this in a web-only process
106
+ is harmless.
107
+
108
+ ---
109
+
110
+ ## The five things it does
111
+
112
+ ### 1. Live logs, across process boundaries
113
+
114
+ The worker writes, your API reads, and they are in different containers. The
115
+ transport is a capped Redis list (`RPUSH` + `LTRIM`) — exactly the shape a
116
+ scrolling terminal wants.
117
+
118
+ ```python
119
+ install_logging(logging.getLogger("myapp")) # one handler, once
120
+
121
+ with capture_logs(task_id):
122
+ ... # every log line in this block is captured
123
+ ```
124
+
125
+ The handler is a **cheap no-op outside a run**, so the same logging setup serves
126
+ your API, your beat process and your workers.
127
+
128
+ **The part worth stealing:** Celery keeps the task id across retries. When a
129
+ process dies without running its `finally` — OOM, a watchdog, a deploy mid-run —
130
+ that attempt's log exists nowhere but Redis, and the next attempt used to start
131
+ by deleting it. Here it is *renamed* instead:
132
+
133
+ ```python
134
+ read_orphan_logs(task_id) # what the attempt that died was doing
135
+ ```
136
+
137
+ Your reaper can archive that against the failed run before closing it. The only
138
+ evidence of the crash survives the retry.
139
+
140
+ ### 2. Presence — is anybody home?
141
+
142
+ ```python
143
+ is_alive(task_id) # one run
144
+ alive_among(page_of_task_ids) # a whole table, one round trip
145
+ workers() # every worker heartbeating right now
146
+ ```
147
+
148
+ Written from Celery's `task_prerun`/`task_postrun` signals plus a daemon thread
149
+ that refreshes while the task runs. Readers only ever do `EXISTS` — no
150
+ `celery inspect`, which is slow and times out precisely when you need it, in an
151
+ endpoint being polled every three seconds. Every key has a TTL, so a dying
152
+ process cleans up after itself.
153
+
154
+ ### 3. A deadline that belongs to the task
155
+
156
+ Celery enforces `task_time_limit` by killing the *child* process of a prefork
157
+ pool. With `--pool=solo` there is no child, so it never fires: a worker stuck on
158
+ one task holds its queue until somebody restarts it by hand.
159
+
160
+ ```python
161
+ install_watchdog(
162
+ deadlines={"quick.petition": 900, "overnight.sweep": 28800},
163
+ enabled=True,
164
+ )
165
+ ```
166
+
167
+ When the timeout is an environment variable it is a property of the *container*,
168
+ and every duration profile needs its own service — a pool of identical replicas
169
+ can never serve both, because the long run dies halfway through in the short
170
+ container. Registered per task name, one fungible pool serves every profile.
171
+
172
+ Cooperative stopping is better whenever the task can manage it:
173
+
174
+ ```python
175
+ stop_by = safe_stop_at(task_name="crawl") # 90% of the budget
176
+ for page in pages:
177
+ if datetime.now() >= stop_by:
178
+ save_checkpoint(page) # stop clean, resume later
179
+ break
180
+ ```
181
+
182
+ The watchdog is off unless you turn it on. A library that kills processes has to
183
+ be opted into, never switched on by the act of installing it.
184
+
185
+ ### 4. The locks a dead process left behind
186
+
187
+ A key taken to serialise work that is not reentrant — one login per account, one
188
+ scrape per catalogue — is released in a `finally`. When the process never reaches
189
+ that `finally`, the key stays and keeps blocking new work until its TTL expires.
190
+
191
+ The symptom is miserable: nothing shows an error. The trigger "works", the task
192
+ exits with "already running", the screen says nothing.
193
+
194
+ ```python
195
+ register_lock(
196
+ pattern="lock:login:*",
197
+ label="Login (one session per account)",
198
+ blocks="Other runs on the same account queue behind the login.",
199
+ )
200
+
201
+ list_locks() # what is held, and for how long
202
+ release_locks(["lock:login:42"]) # allowlist-checked
203
+ lock_state("lock:login:42") # why this click will not run anything
204
+ ```
205
+
206
+ Two guarantees: **only registered patterns can be released** (the raw key
207
+ arrives from a browser — without the allowlist, an arbitrary `DEL` against
208
+ production Redis is one POST away), and **no guessing** — automatic release is
209
+ limited to singleton locks that unambiguously belong to the run being killed,
210
+ because freeing a *live* run's lock is worse than the problem.
211
+
212
+ ### 5. The queue nobody is consuming
213
+
214
+ The quietest failure a Celery deployment has: the message is published, the
215
+ enqueue returns success, the screen says "queued" — and nothing ever runs it,
216
+ forever. It happens the day a service is commented out of the compose file.
217
+
218
+ ```python
219
+ orphan_queues() # declared, but nobody consuming
220
+ queue_depth("fast") # None means "could not ask" — NOT zero
221
+ has_consumer("fast") # True when it cannot tell — fail-fast callers
222
+ ```
223
+
224
+ Those two failure modes point in opposite directions on purpose. A reaper
225
+ deciding whether a pending item was abandoned must distinguish "the queue is
226
+ empty" from "I could not ask", or it invents failures that never happened. A
227
+ caller checking before it enqueues would rather wait than refuse real work
228
+ because the broker was slow.
229
+
230
+ ---
231
+
232
+ ## The panel, in one line
233
+
234
+ ```python
235
+ from fastapi import Depends, FastAPI
236
+ from celery_liveops.contrib.fastapi import liveops_router
237
+
238
+ app.include_router(liveops_router(dependencies=[Depends(require_operator)]))
239
+ ```
240
+
241
+ `GET /liveops/runs/{id}/logs`, `/snapshot`, `POST /runs/alive`, `GET /workers`,
242
+ `/locks`, `POST /locks/release`, `GET /queues`.
243
+
244
+ It exposes task output, screenshots and a lock release, so it **refuses to be
245
+ mounted** without either `dependencies=[...]` or an explicit `public=True`.
246
+
247
+ ---
248
+
249
+ ## Try it
250
+
251
+ A working stack — Redis, RabbitMQ, a worker, a FastAPI panel — is in
252
+ [`demo/`](demo/). In the browser, no install:
253
+
254
+ [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/joaopalmeidao/celery-liveops)
255
+
256
+ Or locally:
257
+
258
+ ```bash
259
+ docker compose -f demo/docker-compose.yml up --build
260
+ # open http://localhost:8000
261
+ ```
262
+
263
+ Start a task, watch its terminal stream, kill the worker mid-run and watch the
264
+ row go from *alive* to *no signal* while the orphan log survives.
265
+
266
+ ### These screenshots are generated, not staged
267
+
268
+ The [Demo workflow](.github/workflows/demo.yml) builds the stack on every change,
269
+ starts real work, **stops the worker container mid-run** and captures the panel.
270
+ So it is a test as much as a picture: if presence, the live buffer or the lock
271
+ catalogue break, it fails — which the unit suite cannot claim, running as it does
272
+ against `fakeredis`.
273
+
274
+ | A task streaming | Its worker stopped | The lock it left behind |
275
+ |---|---|---|
276
+ | ![live](docs/screenshots/01-live.png) | ![no signal](docs/screenshots/02-no-signal.png) | ![orphan lock](docs/screenshots/03-orphan-lock.png) |
277
+
278
+ Reproduce it yourself against a running stack:
279
+
280
+ ```bash
281
+ pip install playwright httpx && playwright install chromium
282
+ python demo/capture.py --out-dir docs/screenshots
283
+ ```
284
+
285
+ ---
286
+
287
+ ## Configuration
288
+
289
+ Everything has a working default; nothing needs configuring to try it.
290
+
291
+ ```python
292
+ configure(redis_url="redis://cache:6379/2", key_prefix="billing", max_lines=500)
293
+ configure(redis_client=my_existing_client) # bring your own connection
294
+ ```
295
+
296
+ Or by environment: `LIVEOPS_REDIS_URL`, `LIVEOPS_KEY_PREFIX`, `LIVEOPS_MAX_LINES`,
297
+ `LIVEOPS_LOG_TTL`, `LIVEOPS_PRESENCE_TTL`, `LIVEOPS_WATCHDOG_ENABLED`,
298
+ `LIVEOPS_DEFAULT_DEADLINE`, `LIVEOPS_SNAPSHOT_TTL`, `LIVEOPS_MAX_CONCURRENCY`.
299
+
300
+ See [`docs/design-notes.md`](docs/design-notes.md) for the reasoning behind the
301
+ less obvious choices.
302
+
303
+ ---
304
+
305
+ ## Development
306
+
307
+ ```bash
308
+ uv venv && uv pip install -e ".[dev]"
309
+ uv run pytest
310
+ uv run ruff check .
311
+ ```
312
+
313
+ ## License
314
+
315
+ MIT