stemtrace 0.3.7__tar.gz → 0.3.8__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.
- {stemtrace-0.3.7 → stemtrace-0.3.8}/.bumpversion.toml +10 -3
- {stemtrace-0.3.7 → stemtrace-0.3.8}/.github/workflows/release.yml +25 -25
- {stemtrace-0.3.7 → stemtrace-0.3.8}/.gitignore +1 -0
- {stemtrace-0.3.7 → stemtrace-0.3.8}/CHANGELOG.md +20 -1
- {stemtrace-0.3.7 → stemtrace-0.3.8}/CONTRIBUTING.md +12 -0
- {stemtrace-0.3.7 → stemtrace-0.3.8}/Makefile +47 -12
- {stemtrace-0.3.7 → stemtrace-0.3.8}/PKG-INFO +61 -7
- {stemtrace-0.3.7 → stemtrace-0.3.8}/README.md +60 -6
- {stemtrace-0.3.7 → stemtrace-0.3.8}/pyproject.toml +1 -1
- stemtrace-0.3.8/scripts/changelog_section.py +59 -0
- {stemtrace-0.3.7 → stemtrace-0.3.8}/src/stemtrace/__init__.py +45 -8
- {stemtrace-0.3.7 → stemtrace-0.3.8}/src/stemtrace/library/config.py +5 -4
- {stemtrace-0.3.7 → stemtrace-0.3.8}/src/stemtrace/library/signals.py +2 -2
- stemtrace-0.3.8/src/stemtrace/library/transports/breaker.py +426 -0
- stemtrace-0.3.8/src/stemtrace/library/transports/rabbitmq.py +649 -0
- {stemtrace-0.3.7 → stemtrace-0.3.8}/src/stemtrace/server/__main__.py +24 -0
- {stemtrace-0.3.7 → stemtrace-0.3.8}/src/stemtrace/server/ui/frontend/package-lock.json +2 -2
- {stemtrace-0.3.7 → stemtrace-0.3.8}/src/stemtrace/server/ui/frontend/package.json +1 -1
- stemtrace-0.3.8/tests/integration/test_rabbitmq_publish.py +349 -0
- stemtrace-0.3.8/tests/integration/test_rabbitmq_transport.py +342 -0
- stemtrace-0.3.8/tests/unit/test_changelog_section_script.py +102 -0
- stemtrace-0.3.8/tests/unit/test_cli.py +171 -0
- {stemtrace-0.3.7 → stemtrace-0.3.8}/tests/unit/test_init.py +169 -0
- stemtrace-0.3.8/tests/unit/test_publish_breaker.py +771 -0
- stemtrace-0.3.8/tests/unit/test_rabbitmq_publish.py +705 -0
- {stemtrace-0.3.7 → stemtrace-0.3.8}/tests/unit/test_signals.py +22 -0
- {stemtrace-0.3.7 → stemtrace-0.3.8}/tests/unit/test_transports.py +755 -35
- {stemtrace-0.3.7 → stemtrace-0.3.8}/uv.lock +1 -1
- stemtrace-0.3.7/src/stemtrace/library/transports/rabbitmq.py +0 -250
- stemtrace-0.3.7/tests/integration/test_rabbitmq_transport.py +0 -139
- {stemtrace-0.3.7 → stemtrace-0.3.8}/.dockerignore +0 -0
- {stemtrace-0.3.7 → stemtrace-0.3.8}/.github/dependabot.yml +0 -0
- {stemtrace-0.3.7 → stemtrace-0.3.8}/.github/workflows/ci.yml +0 -0
- {stemtrace-0.3.7 → stemtrace-0.3.8}/.github/workflows/e2e.yml +0 -0
- {stemtrace-0.3.7 → stemtrace-0.3.8}/.pre-commit-config.yaml +0 -0
- {stemtrace-0.3.7 → stemtrace-0.3.8}/.python-version +0 -0
- {stemtrace-0.3.7 → stemtrace-0.3.8}/Dockerfile +0 -0
- {stemtrace-0.3.7 → stemtrace-0.3.8}/Dockerfile.e2e +0 -0
- {stemtrace-0.3.7 → stemtrace-0.3.8}/LICENSE +0 -0
- {stemtrace-0.3.7 → stemtrace-0.3.8}/build_ui.py +0 -0
- {stemtrace-0.3.7 → stemtrace-0.3.8}/docker-compose.e2e.rabbitmq.yml +0 -0
- {stemtrace-0.3.7 → stemtrace-0.3.8}/docker-compose.e2e.yml +0 -0
- {stemtrace-0.3.7 → stemtrace-0.3.8}/docker-compose.rabbitmq.yml +0 -0
- {stemtrace-0.3.7 → stemtrace-0.3.8}/docker-compose.yml +0 -0
- {stemtrace-0.3.7 → stemtrace-0.3.8}/docs/brand/stemtrace-mark-light.svg +0 -0
- {stemtrace-0.3.7 → stemtrace-0.3.8}/docs/brand/stemtrace-mark.svg +0 -0
- {stemtrace-0.3.7 → stemtrace-0.3.8}/docs/screenshots/task_details.png +0 -0
- {stemtrace-0.3.7 → stemtrace-0.3.8}/docs/screenshots/unregistered.png +0 -0
- {stemtrace-0.3.7 → stemtrace-0.3.8}/docs/screenshots/workflow.png +0 -0
- {stemtrace-0.3.7 → stemtrace-0.3.8}/examples/celery_app.py +0 -0
- {stemtrace-0.3.7 → stemtrace-0.3.8}/examples/fastapi_integration.py +0 -0
- {stemtrace-0.3.7 → stemtrace-0.3.8}/examples/with_auth.py +0 -0
- {stemtrace-0.3.7 → stemtrace-0.3.8}/examples/with_login.py +0 -0
- {stemtrace-0.3.7 → stemtrace-0.3.8}/progress.md +0 -0
- {stemtrace-0.3.7 → stemtrace-0.3.8}/scripts/wait_for_http.py +0 -0
- {stemtrace-0.3.7 → stemtrace-0.3.8}/src/stemtrace/core/__init__.py +0 -0
- {stemtrace-0.3.7 → stemtrace-0.3.8}/src/stemtrace/core/events.py +0 -0
- {stemtrace-0.3.7 → stemtrace-0.3.8}/src/stemtrace/core/exceptions.py +0 -0
- {stemtrace-0.3.7 → stemtrace-0.3.8}/src/stemtrace/core/graph.py +0 -0
- {stemtrace-0.3.7 → stemtrace-0.3.8}/src/stemtrace/core/ports.py +0 -0
- {stemtrace-0.3.7 → stemtrace-0.3.8}/src/stemtrace/library/__init__.py +0 -0
- {stemtrace-0.3.7 → stemtrace-0.3.8}/src/stemtrace/library/bootsteps.py +0 -0
- {stemtrace-0.3.7 → stemtrace-0.3.8}/src/stemtrace/library/scrubbing.py +0 -0
- {stemtrace-0.3.7 → stemtrace-0.3.8}/src/stemtrace/library/transports/__init__.py +0 -0
- {stemtrace-0.3.7 → stemtrace-0.3.8}/src/stemtrace/library/transports/memory.py +0 -0
- {stemtrace-0.3.7 → stemtrace-0.3.8}/src/stemtrace/library/transports/redis.py +0 -0
- {stemtrace-0.3.7 → stemtrace-0.3.8}/src/stemtrace/py.typed +0 -0
- {stemtrace-0.3.7 → stemtrace-0.3.8}/src/stemtrace/server/__init__.py +0 -0
- {stemtrace-0.3.7 → stemtrace-0.3.8}/src/stemtrace/server/api/__init__.py +0 -0
- {stemtrace-0.3.7 → stemtrace-0.3.8}/src/stemtrace/server/api/routes.py +0 -0
- {stemtrace-0.3.7 → stemtrace-0.3.8}/src/stemtrace/server/api/schemas.py +0 -0
- {stemtrace-0.3.7 → stemtrace-0.3.8}/src/stemtrace/server/api/websocket.py +0 -0
- {stemtrace-0.3.7 → stemtrace-0.3.8}/src/stemtrace/server/consumer.py +0 -0
- {stemtrace-0.3.7 → stemtrace-0.3.8}/src/stemtrace/server/fastapi/__init__.py +0 -0
- {stemtrace-0.3.7 → stemtrace-0.3.8}/src/stemtrace/server/fastapi/auth.py +0 -0
- {stemtrace-0.3.7 → stemtrace-0.3.8}/src/stemtrace/server/fastapi/extension.py +0 -0
- {stemtrace-0.3.7 → stemtrace-0.3.8}/src/stemtrace/server/fastapi/form_auth.py +0 -0
- {stemtrace-0.3.7 → stemtrace-0.3.8}/src/stemtrace/server/fastapi/form_auth_middleware.py +0 -0
- {stemtrace-0.3.7 → stemtrace-0.3.8}/src/stemtrace/server/fastapi/login_routes.py +0 -0
- {stemtrace-0.3.7 → stemtrace-0.3.8}/src/stemtrace/server/fastapi/router.py +0 -0
- {stemtrace-0.3.7 → stemtrace-0.3.8}/src/stemtrace/server/store.py +0 -0
- {stemtrace-0.3.7 → stemtrace-0.3.8}/src/stemtrace/server/ui/__init__.py +0 -0
- {stemtrace-0.3.7 → stemtrace-0.3.8}/src/stemtrace/server/ui/frontend/biome.json +0 -0
- {stemtrace-0.3.7 → stemtrace-0.3.8}/src/stemtrace/server/ui/frontend/index.html +0 -0
- {stemtrace-0.3.7 → stemtrace-0.3.8}/src/stemtrace/server/ui/frontend/playwright.config.ts +0 -0
- {stemtrace-0.3.7 → stemtrace-0.3.8}/src/stemtrace/server/ui/frontend/postcss.config.js +0 -0
- {stemtrace-0.3.7 → stemtrace-0.3.8}/src/stemtrace/server/ui/frontend/src/api/client.ts +0 -0
- {stemtrace-0.3.7 → stemtrace-0.3.8}/src/stemtrace/server/ui/frontend/src/api/queries.ts +0 -0
- {stemtrace-0.3.7 → stemtrace-0.3.8}/src/stemtrace/server/ui/frontend/src/components/DateRangePicker.tsx +0 -0
- {stemtrace-0.3.7 → stemtrace-0.3.8}/src/stemtrace/server/ui/frontend/src/components/Filters.tsx +0 -0
- {stemtrace-0.3.7 → stemtrace-0.3.8}/src/stemtrace/server/ui/frontend/src/components/LogoutButton.tsx +0 -0
- {stemtrace-0.3.7 → stemtrace-0.3.8}/src/stemtrace/server/ui/frontend/src/components/SessionRejectedNotice.tsx +0 -0
- {stemtrace-0.3.7 → stemtrace-0.3.8}/src/stemtrace/server/ui/frontend/src/components/SproutMark.tsx +0 -0
- {stemtrace-0.3.7 → stemtrace-0.3.8}/src/stemtrace/server/ui/frontend/src/components/TaskGraph.tsx +0 -0
- {stemtrace-0.3.7 → stemtrace-0.3.8}/src/stemtrace/server/ui/frontend/src/components/TaskList.tsx +0 -0
- {stemtrace-0.3.7 → stemtrace-0.3.8}/src/stemtrace/server/ui/frontend/src/components/TaskStateBadge.tsx +0 -0
- {stemtrace-0.3.7 → stemtrace-0.3.8}/src/stemtrace/server/ui/frontend/src/components/TaskTimeline.tsx +0 -0
- {stemtrace-0.3.7 → stemtrace-0.3.8}/src/stemtrace/server/ui/frontend/src/components/TimelineBar.tsx +0 -0
- {stemtrace-0.3.7 → stemtrace-0.3.8}/src/stemtrace/server/ui/frontend/src/components/Tooltip.tsx +0 -0
- {stemtrace-0.3.7 → stemtrace-0.3.8}/src/stemtrace/server/ui/frontend/src/hooks/WebSocketContext.tsx +0 -0
- {stemtrace-0.3.7 → stemtrace-0.3.8}/src/stemtrace/server/ui/frontend/src/hooks/useInfiniteScroll.ts +0 -0
- {stemtrace-0.3.7 → stemtrace-0.3.8}/src/stemtrace/server/ui/frontend/src/index.css +0 -0
- {stemtrace-0.3.7 → stemtrace-0.3.8}/src/stemtrace/server/ui/frontend/src/main.tsx +0 -0
- {stemtrace-0.3.7 → stemtrace-0.3.8}/src/stemtrace/server/ui/frontend/src/routes/__root.tsx +0 -0
- {stemtrace-0.3.7 → stemtrace-0.3.8}/src/stemtrace/server/ui/frontend/src/routes/graph.$rootId.tsx +0 -0
- {stemtrace-0.3.7 → stemtrace-0.3.8}/src/stemtrace/server/ui/frontend/src/routes/graphs.tsx +0 -0
- {stemtrace-0.3.7 → stemtrace-0.3.8}/src/stemtrace/server/ui/frontend/src/routes/index.tsx +0 -0
- {stemtrace-0.3.7 → stemtrace-0.3.8}/src/stemtrace/server/ui/frontend/src/routes/registry.tsx +0 -0
- {stemtrace-0.3.7 → stemtrace-0.3.8}/src/stemtrace/server/ui/frontend/src/routes/tasks.$taskId.tsx +0 -0
- {stemtrace-0.3.7 → stemtrace-0.3.8}/src/stemtrace/server/ui/frontend/src/routes/workers.tsx +0 -0
- {stemtrace-0.3.7 → stemtrace-0.3.8}/src/stemtrace/server/ui/frontend/src/utils/format.ts +0 -0
- {stemtrace-0.3.7 → stemtrace-0.3.8}/src/stemtrace/server/ui/frontend/src/vite-env.d.ts +0 -0
- {stemtrace-0.3.7 → stemtrace-0.3.8}/src/stemtrace/server/ui/frontend/tests/auth.spec.ts +0 -0
- {stemtrace-0.3.7 → stemtrace-0.3.8}/src/stemtrace/server/ui/frontend/tests/fixtures/mock-api.ts +0 -0
- {stemtrace-0.3.7 → stemtrace-0.3.8}/src/stemtrace/server/ui/frontend/tests/fixtures/mock-data.ts +0 -0
- {stemtrace-0.3.7 → stemtrace-0.3.8}/src/stemtrace/server/ui/frontend/tests/graphs.spec.ts +0 -0
- {stemtrace-0.3.7 → stemtrace-0.3.8}/src/stemtrace/server/ui/frontend/tests/header.spec.ts +0 -0
- {stemtrace-0.3.7 → stemtrace-0.3.8}/src/stemtrace/server/ui/frontend/tests/registry.spec.ts +0 -0
- {stemtrace-0.3.7 → stemtrace-0.3.8}/src/stemtrace/server/ui/frontend/tests/task-detail.spec.ts +0 -0
- {stemtrace-0.3.7 → stemtrace-0.3.8}/src/stemtrace/server/ui/frontend/tests/tasks.spec.ts +0 -0
- {stemtrace-0.3.7 → stemtrace-0.3.8}/src/stemtrace/server/ui/frontend/tests/workers.spec.ts +0 -0
- {stemtrace-0.3.7 → stemtrace-0.3.8}/src/stemtrace/server/ui/frontend/tsconfig.json +0 -0
- {stemtrace-0.3.7 → stemtrace-0.3.8}/src/stemtrace/server/ui/frontend/tsconfig.tsbuildinfo +0 -0
- {stemtrace-0.3.7 → stemtrace-0.3.8}/src/stemtrace/server/ui/frontend/vite.config.ts +0 -0
- {stemtrace-0.3.7 → stemtrace-0.3.8}/src/stemtrace/server/ui/static.py +0 -0
- {stemtrace-0.3.7 → stemtrace-0.3.8}/src/stemtrace/server/ui/templates/login.html +0 -0
- {stemtrace-0.3.7 → stemtrace-0.3.8}/src/stemtrace/server/websocket.py +0 -0
- {stemtrace-0.3.7 → stemtrace-0.3.8}/tests/__init__.py +0 -0
- {stemtrace-0.3.7 → stemtrace-0.3.8}/tests/conftest.py +0 -0
- {stemtrace-0.3.7 → stemtrace-0.3.8}/tests/e2e/__init__.py +0 -0
- {stemtrace-0.3.7 → stemtrace-0.3.8}/tests/e2e/conftest.py +0 -0
- {stemtrace-0.3.7 → stemtrace-0.3.8}/tests/e2e/tasks.py +0 -0
- {stemtrace-0.3.7 → stemtrace-0.3.8}/tests/e2e/test_api_e2e.py +0 -0
- {stemtrace-0.3.7 → stemtrace-0.3.8}/tests/e2e/test_workers_e2e.py +0 -0
- {stemtrace-0.3.7 → stemtrace-0.3.8}/tests/integration/__init__.py +0 -0
- {stemtrace-0.3.7 → stemtrace-0.3.8}/tests/integration/test_embedded_static.py +0 -0
- {stemtrace-0.3.7 → stemtrace-0.3.8}/tests/integration/test_fastapi_integration.py +0 -0
- {stemtrace-0.3.7 → stemtrace-0.3.8}/tests/integration/test_redis_resilience_docker.py +0 -0
- {stemtrace-0.3.7 → stemtrace-0.3.8}/tests/integration/test_redis_transport.py +0 -0
- {stemtrace-0.3.7 → stemtrace-0.3.8}/tests/integration/test_static_ui.py +0 -0
- {stemtrace-0.3.7 → stemtrace-0.3.8}/tests/integration/test_wait_for_http_redirects.py +0 -0
- {stemtrace-0.3.7 → stemtrace-0.3.8}/tests/integration/test_workers_refresh.py +0 -0
- {stemtrace-0.3.7 → stemtrace-0.3.8}/tests/unit/__init__.py +0 -0
- {stemtrace-0.3.7 → stemtrace-0.3.8}/tests/unit/test_auth.py +0 -0
- {stemtrace-0.3.7 → stemtrace-0.3.8}/tests/unit/test_bootsteps.py +0 -0
- {stemtrace-0.3.7 → stemtrace-0.3.8}/tests/unit/test_build_ui_hook.py +0 -0
- {stemtrace-0.3.7 → stemtrace-0.3.8}/tests/unit/test_cli_form_login.py +0 -0
- {stemtrace-0.3.7 → stemtrace-0.3.8}/tests/unit/test_config.py +0 -0
- {stemtrace-0.3.7 → stemtrace-0.3.8}/tests/unit/test_consumer.py +0 -0
- {stemtrace-0.3.7 → stemtrace-0.3.8}/tests/unit/test_events.py +0 -0
- {stemtrace-0.3.7 → stemtrace-0.3.8}/tests/unit/test_examples.py +0 -0
- {stemtrace-0.3.7 → stemtrace-0.3.8}/tests/unit/test_exceptions.py +0 -0
- {stemtrace-0.3.7 → stemtrace-0.3.8}/tests/unit/test_favicon.py +0 -0
- {stemtrace-0.3.7 → stemtrace-0.3.8}/tests/unit/test_form_auth.py +0 -0
- {stemtrace-0.3.7 → stemtrace-0.3.8}/tests/unit/test_form_login.py +0 -0
- {stemtrace-0.3.7 → stemtrace-0.3.8}/tests/unit/test_graph.py +0 -0
- {stemtrace-0.3.7 → stemtrace-0.3.8}/tests/unit/test_redis_resilience.py +0 -0
- {stemtrace-0.3.7 → stemtrace-0.3.8}/tests/unit/test_routes.py +0 -0
- {stemtrace-0.3.7 → stemtrace-0.3.8}/tests/unit/test_scrubbing.py +0 -0
- {stemtrace-0.3.7 → stemtrace-0.3.8}/tests/unit/test_store.py +0 -0
- {stemtrace-0.3.7 → stemtrace-0.3.8}/tests/unit/test_ui_static_prefix.py +0 -0
- {stemtrace-0.3.7 → stemtrace-0.3.8}/tests/unit/test_wait_for_http_script.py +0 -0
- {stemtrace-0.3.7 → stemtrace-0.3.8}/tests/unit/test_websocket.py +0 -0
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
[tool.bumpversion]
|
|
2
|
-
current_version = "0.3.
|
|
2
|
+
current_version = "0.3.8"
|
|
3
3
|
parse = "(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)"
|
|
4
4
|
serialize = ["{major}.{minor}.{patch}"]
|
|
5
5
|
commit = true
|
|
6
|
-
tag = false #
|
|
7
|
-
|
|
6
|
+
tag = false # Tagged after the release PR merges (make release-tag)
|
|
7
|
+
message = "chore: release v{new_version}"
|
|
8
8
|
|
|
9
9
|
# Python package version in pyproject.toml
|
|
10
10
|
[[tool.bumpversion.files]]
|
|
@@ -60,3 +60,10 @@ replace = """## [Unreleased]
|
|
|
60
60
|
filename = "README.md"
|
|
61
61
|
search = "[](https://pypi.org/project/stemtrace)"
|
|
62
62
|
replace = "[](https://pypi.org/project/stemtrace)"
|
|
63
|
+
|
|
64
|
+
# CHANGELOG compare links: advance [unreleased], add the new version's link
|
|
65
|
+
[[tool.bumpversion.files]]
|
|
66
|
+
filename = "CHANGELOG.md"
|
|
67
|
+
search = "[unreleased]: https://github.com/iansokolskyi/stemtrace/compare/v{current_version}...HEAD"
|
|
68
|
+
replace = """[unreleased]: https://github.com/iansokolskyi/stemtrace/compare/v{new_version}...HEAD
|
|
69
|
+
[{new_version}]: https://github.com/iansokolskyi/stemtrace/compare/v{current_version}...v{new_version}"""
|
|
@@ -1,19 +1,9 @@
|
|
|
1
1
|
name: Release
|
|
2
2
|
|
|
3
3
|
on:
|
|
4
|
+
# Releases are tagged on main after the release PR merges (make release-tag).
|
|
4
5
|
push:
|
|
5
6
|
tags: ["v*"]
|
|
6
|
-
# Manual trigger from Actions tab
|
|
7
|
-
workflow_dispatch:
|
|
8
|
-
inputs:
|
|
9
|
-
version:
|
|
10
|
-
description: "Version to release (e.g., 0.1.0) - must match version in pyproject.toml"
|
|
11
|
-
required: true
|
|
12
|
-
type: string
|
|
13
|
-
|
|
14
|
-
env:
|
|
15
|
-
# Determine version from tag or manual input
|
|
16
|
-
VERSION: ${{ github.event.inputs.version || github.ref_name }}
|
|
17
7
|
|
|
18
8
|
jobs:
|
|
19
9
|
build:
|
|
@@ -25,17 +15,14 @@ jobs:
|
|
|
25
15
|
tag: ${{ steps.version.outputs.tag }}
|
|
26
16
|
steps:
|
|
27
17
|
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
|
|
18
|
+
with:
|
|
19
|
+
fetch-depth: 0 # full history for the "tag is on main" check
|
|
28
20
|
|
|
29
21
|
- name: Determine version
|
|
30
22
|
id: version
|
|
31
23
|
run: |
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
echo "tag=v${{ github.event.inputs.version }}" >> $GITHUB_OUTPUT
|
|
35
|
-
else
|
|
36
|
-
echo "version=${GITHUB_REF_NAME#v}" >> $GITHUB_OUTPUT
|
|
37
|
-
echo "tag=${GITHUB_REF_NAME}" >> $GITHUB_OUTPUT
|
|
38
|
-
fi
|
|
24
|
+
echo "version=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT"
|
|
25
|
+
echo "tag=${GITHUB_REF_NAME}" >> "$GITHUB_OUTPUT"
|
|
39
26
|
|
|
40
27
|
- name: Verify version matches pyproject.toml
|
|
41
28
|
run: |
|
|
@@ -50,6 +37,19 @@ jobs:
|
|
|
50
37
|
fi
|
|
51
38
|
echo "✅ Version $PYPROJECT_VERSION matches"
|
|
52
39
|
|
|
40
|
+
- name: Verify the tagged commit is on main
|
|
41
|
+
run: |
|
|
42
|
+
# Releases are tagged after the release PR merges; never from a branch.
|
|
43
|
+
git merge-base --is-ancestor "$GITHUB_SHA" origin/main || {
|
|
44
|
+
echo "❌ ${GITHUB_REF_NAME} points at a commit that isn't on main"; exit 1; }
|
|
45
|
+
|
|
46
|
+
- name: Check release notes exist in CHANGELOG.md
|
|
47
|
+
run: |
|
|
48
|
+
# Fail before anything is published: every release needs notes.
|
|
49
|
+
python3 scripts/changelog_section.py "$VERSION"
|
|
50
|
+
env:
|
|
51
|
+
VERSION: ${{ steps.version.outputs.version }}
|
|
52
|
+
|
|
53
53
|
- name: Set up Python
|
|
54
54
|
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
|
55
55
|
with:
|
|
@@ -158,17 +158,17 @@ jobs:
|
|
|
158
158
|
name: dist
|
|
159
159
|
path: dist/
|
|
160
160
|
|
|
161
|
-
- name:
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
git config user.email "github-actions[bot]@users.noreply.github.com"
|
|
166
|
-
git tag -a "${{ needs.build.outputs.tag }}" -m "Release ${{ needs.build.outputs.tag }}"
|
|
167
|
-
git push origin "${{ needs.build.outputs.tag }}"
|
|
161
|
+
- name: Extract release notes from CHANGELOG.md
|
|
162
|
+
run: python3 scripts/changelog_section.py "$VERSION" > "$RUNNER_TEMP/release_notes.md"
|
|
163
|
+
env:
|
|
164
|
+
VERSION: ${{ needs.build.outputs.version }}
|
|
168
165
|
|
|
169
166
|
- name: Create GitHub Release
|
|
170
167
|
uses: softprops/action-gh-release@efb35369e0ad2afab669f228072c1b0d510eae64 # v3.0.3
|
|
171
168
|
with:
|
|
172
169
|
tag_name: ${{ needs.build.outputs.tag }}
|
|
170
|
+
# CHANGELOG.md section first; GitHub appends contributors, PR list
|
|
171
|
+
# and the compare link.
|
|
172
|
+
body_path: ${{ runner.temp }}/release_notes.md
|
|
173
173
|
generate_release_notes: true
|
|
174
174
|
files: dist/*
|
|
@@ -7,6 +7,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.3.8] - 2026-10-10
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- `node_alias_from_arguments` can now be set on the standalone server and Docker image: `stemtrace server --node-alias-from-arguments <key>` or `STEMTRACE_NODE_ALIAS_FROM_ARGUMENTS=<key>` (a kwarg name or a positional index). `init_app()` reads the same env var when the argument isn't passed (#69).
|
|
14
|
+
- RabbitMQ: publishing from workers no longer blocks tasks while the broker is down, stalled or in a memory/disk alarm. Each publish tries every host of the URL once with a 2s timeout; repeated or slow failures open a circuit breaker that drops events (instead of queueing them) until the broker answers again, and the worker logs at most four warnings a minute with counts of dropped events. The README's broker section has the details and limits.
|
|
15
|
+
- RabbitMQ: the server's consumer now reconnects after the broker restarts or drops the connection; it used to retry forever on a dead channel.
|
|
16
|
+
- RabbitMQ: a broker outage no longer floods the server log with one ERROR traceback per second; reconnects back off from 1s to 30s and log one line per attempt, plus one when the connection is back (thanks @ohadmata — #70, #71).
|
|
17
|
+
- RabbitMQ: a message the consumer can't decode is dropped with a warning instead of blocking the queue forever.
|
|
18
|
+
- RabbitMQ: stopping the server no longer waits out the reconnect delay or hangs on a frozen broker.
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
- Health endpoint: `consumer_connected` is now reported for RabbitMQ, so `/api/health` shows `degraded` while the server can't read from the broker.
|
|
22
|
+
|
|
23
|
+
### Deprecated
|
|
24
|
+
- `init_worker(node_alias_from_arguments=...)` has no effect (node names are resolved by the server) and now logs a deprecation warning; it will be removed in a future major release.
|
|
25
|
+
|
|
10
26
|
## [0.3.7] - 2026-10-10
|
|
11
27
|
|
|
12
28
|
### Changed
|
|
@@ -165,7 +181,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
165
181
|
- **E2E test suite**: Docker API tests + Playwright browser tests
|
|
166
182
|
- **Comprehensive test suite**: 350+ Python tests, 90%+ coverage
|
|
167
183
|
|
|
168
|
-
[unreleased]: https://github.com/iansokolskyi/stemtrace/compare/v0.3.
|
|
184
|
+
[unreleased]: https://github.com/iansokolskyi/stemtrace/compare/v0.3.8...HEAD
|
|
185
|
+
[0.3.8]: https://github.com/iansokolskyi/stemtrace/compare/v0.3.7...v0.3.8
|
|
186
|
+
[0.3.7]: https://github.com/iansokolskyi/stemtrace/compare/v0.3.6...v0.3.7
|
|
187
|
+
[0.3.6]: https://github.com/iansokolskyi/stemtrace/compare/v0.3.5...v0.3.6
|
|
169
188
|
[0.3.5]: https://github.com/iansokolskyi/stemtrace/compare/v0.3.4...v0.3.5
|
|
170
189
|
[0.3.4]: https://github.com/iansokolskyi/stemtrace/compare/v0.3.3...v0.3.4
|
|
171
190
|
[0.3.3]: https://github.com/iansokolskyi/stemtrace/compare/v0.3.2...v0.3.3
|
|
@@ -136,6 +136,18 @@ uv lock --upgrade
|
|
|
136
136
|
- Fire-and-forget pattern for publishers (never block)
|
|
137
137
|
- Fakes over mocks in tests
|
|
138
138
|
|
|
139
|
+
## Releasing (maintainers)
|
|
140
|
+
|
|
141
|
+
Every release goes through a pull request into `main` and is published by CI from a tag on `main`.
|
|
142
|
+
|
|
143
|
+
1. Add user-facing entries under `## [Unreleased]` in `CHANGELOG.md`.
|
|
144
|
+
2. `make release-pr BUMP=patch|minor|major` runs the checks, creates `release/vX.Y.Z`, bumps the
|
|
145
|
+
version and opens a PR whose description is the changelog section.
|
|
146
|
+
3. When CI is green, merge the PR with a merge commit (not squash or rebase).
|
|
147
|
+
4. `make release-tag` tags the merge on `main`. The tag triggers the Release workflow, which
|
|
148
|
+
publishes to PyPI and GHCR and creates the GitHub Release from the changelog section. It
|
|
149
|
+
refuses to publish if the section is empty or the tag isn't on `main`.
|
|
150
|
+
|
|
139
151
|
## Questions?
|
|
140
152
|
|
|
141
153
|
- Open an issue for bugs or feature requests
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
.PHONY: install check types lint format test coverage clean ui-install ui-dev ui-build e2e e2e-api e2e-playwright build release-check version bump-dry bump-patch bump-minor bump-major release
|
|
1
|
+
.PHONY: install check types lint format test coverage clean ui-install ui-dev ui-build e2e e2e-api e2e-playwright build release-check version bump-dry bump-patch bump-minor bump-major release release-pr release-tag
|
|
2
2
|
|
|
3
3
|
# Install all dependencies
|
|
4
4
|
install:
|
|
@@ -99,6 +99,8 @@ build:
|
|
|
99
99
|
# Full pre-release checklist
|
|
100
100
|
release-check:
|
|
101
101
|
@echo "=== Pre-release Checklist ==="
|
|
102
|
+
@echo "0. Checking CHANGELOG.md has [Unreleased] entries..."
|
|
103
|
+
@uv run python scripts/changelog_section.py Unreleased > /dev/null
|
|
102
104
|
@echo "1. Running all checks..."
|
|
103
105
|
$(MAKE) check
|
|
104
106
|
@echo ""
|
|
@@ -107,7 +109,6 @@ release-check:
|
|
|
107
109
|
@echo ""
|
|
108
110
|
@echo "✅ Ready to release!"
|
|
109
111
|
@echo ""
|
|
110
|
-
@echo "Next: make release"
|
|
111
112
|
|
|
112
113
|
# =============================================================================
|
|
113
114
|
# Versioning (bump-my-version)
|
|
@@ -126,7 +127,7 @@ bump-patch:
|
|
|
126
127
|
@NEW_VER=$$(uv run bump-my-version show current_version); \
|
|
127
128
|
echo "✅ Version bumped to $$NEW_VER"; \
|
|
128
129
|
echo ""; \
|
|
129
|
-
echo "
|
|
130
|
+
echo "Note: releases use make release-pr, which runs the bump itself; this target is for manual use only"
|
|
130
131
|
|
|
131
132
|
# Bump minor version (0.1.0 -> 0.2.0)
|
|
132
133
|
bump-minor:
|
|
@@ -134,7 +135,7 @@ bump-minor:
|
|
|
134
135
|
@NEW_VER=$$(uv run bump-my-version show current_version); \
|
|
135
136
|
echo "✅ Version bumped to $$NEW_VER"; \
|
|
136
137
|
echo ""; \
|
|
137
|
-
echo "
|
|
138
|
+
echo "Note: releases use make release-pr, which runs the bump itself; this target is for manual use only"
|
|
138
139
|
|
|
139
140
|
# Bump major version (0.1.0 -> 1.0.0)
|
|
140
141
|
bump-major:
|
|
@@ -142,20 +143,54 @@ bump-major:
|
|
|
142
143
|
@NEW_VER=$$(uv run bump-my-version show current_version); \
|
|
143
144
|
echo "✅ Version bumped to $$NEW_VER"; \
|
|
144
145
|
echo ""; \
|
|
145
|
-
echo "
|
|
146
|
+
echo "Note: releases use make release-pr, which runs the bump itself; this target is for manual use only"
|
|
146
147
|
|
|
147
148
|
# Tag and push to trigger release workflow
|
|
148
149
|
release:
|
|
149
|
-
@
|
|
150
|
+
@echo "Releases go through a PR: make release-pr BUMP=patch|minor|major, then make release-tag."
|
|
151
|
+
@exit 1
|
|
152
|
+
|
|
153
|
+
# Open a release PR: preflight, checks, release/vX.Y.Z branch, version bump, PR with notes.
|
|
154
|
+
# Usage: make release-pr BUMP=patch|minor|major
|
|
155
|
+
release-pr:
|
|
156
|
+
@set -e; \
|
|
157
|
+
case "$(BUMP)" in patch|minor|major) ;; *) echo "Usage: make release-pr BUMP=patch|minor|major"; exit 1;; esac; \
|
|
158
|
+
test "$$(git rev-parse --abbrev-ref HEAD)" = main || { echo "❌ Run from main"; exit 1; }; \
|
|
159
|
+
test -z "$$(git status --porcelain)" || { echo "❌ Working tree not clean"; exit 1; }; \
|
|
160
|
+
gh auth status >/dev/null 2>&1 || { echo "❌ gh is not authenticated (gh auth login)"; exit 1; }; \
|
|
161
|
+
git fetch -q --tags origin main; \
|
|
162
|
+
test -z "$$(git log HEAD..origin/main --oneline)" || { echo "❌ main is behind origin/main"; exit 1; }; \
|
|
163
|
+
NEW=$$(uv run bump-my-version show new_version --increment $(BUMP)); \
|
|
164
|
+
! git rev-parse -q --verify "refs/heads/release/v$$NEW" >/dev/null || { echo "❌ Local branch release/v$$NEW exists"; exit 1; }; \
|
|
165
|
+
test -z "$$(git ls-remote --heads origin "release/v$$NEW")" || { echo "❌ Remote branch release/v$$NEW exists"; exit 1; }; \
|
|
166
|
+
! git rev-parse -q --verify "refs/tags/v$$NEW" >/dev/null || { echo "❌ Tag v$$NEW exists"; exit 1; }; \
|
|
167
|
+
$(MAKE) release-check; \
|
|
168
|
+
git switch -c "release/v$$NEW"; \
|
|
169
|
+
uv run bump-my-version bump $(BUMP); \
|
|
170
|
+
NOTES=$$(mktemp); trap 'rm -f "$$NOTES"' EXIT; \
|
|
171
|
+
uv run python scripts/changelog_section.py "$$NEW" > "$$NOTES"; \
|
|
172
|
+
git push -u origin "release/v$$NEW" || { echo "❌ Push failed; local branch release/v$$NEW holds the bump commit"; exit 1; }; \
|
|
173
|
+
gh pr create --base main --head "release/v$$NEW" --title "chore: release v$$NEW" --body-file "$$NOTES" \
|
|
174
|
+
|| { echo "❌ PR creation failed; release/v$$NEW is pushed, open the PR manually"; exit 1; }; \
|
|
175
|
+
echo ""; \
|
|
176
|
+
echo "✅ Release PR opened for v$$NEW"; \
|
|
177
|
+
echo "Next: gh pr checks --watch, merge with a merge commit, then make release-tag"
|
|
178
|
+
|
|
179
|
+
# Tag the merged release on main; the tag push triggers the Release workflow.
|
|
180
|
+
release-tag:
|
|
181
|
+
@set -e; \
|
|
182
|
+
git switch -q main; \
|
|
183
|
+
git pull -q --ff-only origin main; \
|
|
184
|
+
VERSION=$$(uv run bump-my-version show current_version); \
|
|
150
185
|
TAG="v$$VERSION"; \
|
|
151
|
-
|
|
186
|
+
SUBJECT=$$(git log -1 --format=%s); \
|
|
187
|
+
test "$$(git rev-list --parents -n 1 HEAD | wc -w)" -eq 3 || { echo "❌ HEAD of main is not a merge commit (merge the release PR with a merge commit)"; exit 1; }; \
|
|
188
|
+
case "$$SUBJECT" in *"/release/$$TAG"|*"release $$TAG"|*"release $$TAG "*) ;; *) echo "❌ HEAD of main is not the $$TAG release PR merge: $$SUBJECT"; exit 1;; esac; \
|
|
189
|
+
git fetch -q --tags origin; \
|
|
190
|
+
! git rev-parse -q --verify "refs/tags/$$TAG" >/dev/null || { echo "❌ $$TAG already exists"; exit 1; }; \
|
|
152
191
|
git tag -a "$$TAG" -m "Release $$TAG"; \
|
|
153
|
-
echo "Pushing to origin..."; \
|
|
154
|
-
git push origin main; \
|
|
155
192
|
git push origin "$$TAG"; \
|
|
156
|
-
echo ""
|
|
157
|
-
echo "✅ Released $$TAG"; \
|
|
158
|
-
echo " → GitHub Actions will publish to PyPI and Docker"
|
|
193
|
+
echo "✅ Tagged $$TAG → Release workflow publishes PyPI, GHCR and the GitHub Release"
|
|
159
194
|
|
|
160
195
|
# =============================================================================
|
|
161
196
|
# E2E Testing
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: stemtrace
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.8
|
|
4
4
|
Summary: A lightweight Celery task flow visualizer
|
|
5
5
|
Project-URL: Homepage, https://github.com/iansokolskyi/stemtrace
|
|
6
6
|
Project-URL: Documentation, https://github.com/iansokolskyi/stemtrace#readme
|
|
@@ -59,7 +59,7 @@ Description-Content-Type: text/markdown
|
|
|
59
59
|
|
|
60
60
|
**Zero-infrastructure Celery task flow visualizer**
|
|
61
61
|
|
|
62
|
-
[](https://pypi.org/project/stemtrace)
|
|
63
63
|
[](https://pypi.org/project/stemtrace/)
|
|
64
64
|
[](https://github.com/iansokolskyi/stemtrace/actions/workflows/ci.yml)
|
|
65
65
|
[](https://codecov.io/gh/iansokolskyi/stemtrace)
|
|
@@ -253,10 +253,6 @@ stemtrace.init_worker(
|
|
|
253
253
|
scrub_sensitive_data=True, # Scrub passwords, API keys, etc.
|
|
254
254
|
additional_sensitive_keys=frozenset({"my_secret"}), # Add custom keys
|
|
255
255
|
safe_keys=frozenset({"public_key"}), # Never scrub these keys
|
|
256
|
-
|
|
257
|
-
# UI display
|
|
258
|
-
node_alias_from_arguments="operator_type", # Use kwargs["operator_type"] as node name
|
|
259
|
-
# node_alias_from_arguments="0", # Or use args[0] (digit string = positional index)
|
|
260
256
|
)
|
|
261
257
|
|
|
262
258
|
# Introspection (after init)
|
|
@@ -336,6 +332,7 @@ stemtrace.init_app(
|
|
|
336
332
|
|----------|-------------|---------|
|
|
337
333
|
| `STEMTRACE_BROKER_URL` | Celery broker URL (used for on-demand worker/registry inspection). Also used as the default for `STEMTRACE_TRANSPORT_URL`. | `redis://localhost:6379/0` |
|
|
338
334
|
| `STEMTRACE_TRANSPORT_URL` | Event transport URL (where stemtrace publishes/consumes events). | Defaults to `STEMTRACE_BROKER_URL`. |
|
|
335
|
+
| `STEMTRACE_NODE_ALIAS_FROM_ARGUMENTS` | Server only: show a task argument as the graph node name (see [Node Names from Task Arguments](#node-names-from-task-arguments)). Same as `--node-alias-from-arguments` / `init_app(node_alias_from_arguments=...)`. | Unset (task names are shown). |
|
|
339
336
|
|
|
340
337
|
### Supported Brokers
|
|
341
338
|
|
|
@@ -356,6 +353,36 @@ again once events are re-consumed.
|
|
|
356
353
|
- Events already consumed/acked by the server are **gone**.
|
|
357
354
|
- Events published while the server is down are only visible after restart if the server’s
|
|
358
355
|
durable per-consumer queue still exists and the messages are still within TTL.
|
|
356
|
+
- Events that can't be published to RabbitMQ are dropped, not buffered on the worker, and a
|
|
357
|
+
broker outage doesn't slow tasks down. Each publish tries every host of the broker URL once
|
|
358
|
+
(2s connect timeout, 2s per read or write after that). A single quick failure, such as a
|
|
359
|
+
refused connection, drops only that event; a failure that took 0.5s or more (a timeout) or
|
|
360
|
+
three quick failures in a row make the worker skip publishing for a cooldown that grows from
|
|
361
|
+
1s to 30s and resets on the next successful publish.
|
|
362
|
+
- During a RabbitMQ memory or disk alarm, the broker accepts connections but stops reading
|
|
363
|
+
from publishers, so a publish is sent but never confirmed. The worker counts such events as
|
|
364
|
+
"sent but unconfirmed" (RabbitMQ may still deliver them once the alarm clears); after two
|
|
365
|
+
unconfirmed attempts in a row it retries only every 30s. Each attempt leaves one blocked
|
|
366
|
+
connection on the broker until the alarm clears, so expect 2 to 3 connections per worker
|
|
367
|
+
process in the first minute of an alarm and about 2 per minute after that, all released
|
|
368
|
+
when the alarm ends. Each retry also logs `Received method (10, 60) during closing channel 0`
|
|
369
|
+
from py-amqp's `amqp` logger (not stemtrace's): that's RabbitMQ's "connection blocked"
|
|
370
|
+
notice arriving while the connection closes.
|
|
371
|
+
- Logging is bounded: at most four warnings a minute however the broker behaves (failures,
|
|
372
|
+
an outage starting, an outage ending, and a summary). Each reports the number of events
|
|
373
|
+
dropped, and sent but unconfirmed, since the previous one, and losses are reported within
|
|
374
|
+
about 1.5 minutes even when the broker flaps.
|
|
375
|
+
- Limitation: a broker that answers but slowly (say, close to the 2s timeout for each step)
|
|
376
|
+
still costs every event several seconds, because publishes that eventually succeed don't
|
|
377
|
+
open the breaker.
|
|
378
|
+
- A failover broker URL (`amqp://h1//;amqp://h2//`) must list nodes of the same RabbitMQ
|
|
379
|
+
cluster: workers keep publishing to whichever host last worked, so the server must be able to
|
|
380
|
+
consume those events from any of them. Entries resolve as in kombu: a bare host (`;h2`)
|
|
381
|
+
takes user, password, port, virtual host and TLS options from the first entry, while a
|
|
382
|
+
URL-form entry (`;amqp://h2`) gets guest/guest unless it names credentials, and takes port
|
|
383
|
+
and virtual host from the previous entry only when it omits them (`//` means `/`). A host
|
|
384
|
+
that refuses the login ends that publish attempt without trying the later hosts, as in
|
|
385
|
+
kombu.
|
|
359
386
|
- **Workers + Registry tabs**: stemtrace uses **Celery inspect** on demand to populate workers
|
|
360
387
|
and registered tasks, so those pages work even if the server missed `worker_ready` events.
|
|
361
388
|
- **If you need durable history across restarts**: point stemtrace events at Redis even if your
|
|
@@ -387,6 +414,15 @@ docker run -p 8000:8000 \
|
|
|
387
414
|
ghcr.io/iansokolskyi/stemtrace
|
|
388
415
|
```
|
|
389
416
|
|
|
417
|
+
Show a task argument as the graph node name (here `kwargs["operator_type"]`):
|
|
418
|
+
|
|
419
|
+
```bash
|
|
420
|
+
docker run -p 8000:8000 \
|
|
421
|
+
-e STEMTRACE_BROKER_URL=redis://host.docker.internal:6379/0 \
|
|
422
|
+
-e STEMTRACE_NODE_ALIAS_FROM_ARGUMENTS=operator_type \
|
|
423
|
+
ghcr.io/iansokolskyi/stemtrace
|
|
424
|
+
```
|
|
425
|
+
|
|
390
426
|
Or with Docker Compose:
|
|
391
427
|
|
|
392
428
|
```yaml
|
|
@@ -430,9 +466,26 @@ stemtrace server \
|
|
|
430
466
|
--transport-url redis://myredis:6379/0 \
|
|
431
467
|
--host 0.0.0.0 \
|
|
432
468
|
--port 8000 \
|
|
469
|
+
--node-alias-from-arguments operator_type \
|
|
433
470
|
--reload # For development
|
|
434
471
|
```
|
|
435
472
|
|
|
473
|
+
#### Node Names from Task Arguments
|
|
474
|
+
|
|
475
|
+
By default, graph nodes are labeled with the task name. Generic tasks (e.g. one `run_operator`
|
|
476
|
+
task that does different things depending on its arguments) are easier to tell apart when the
|
|
477
|
+
graph shows an argument instead:
|
|
478
|
+
|
|
479
|
+
- `--node-alias-from-arguments operator_type` uses `kwargs["operator_type"]`
|
|
480
|
+
- `--node-alias-from-arguments 0` uses `args[0]` (a digit string is a positional index)
|
|
481
|
+
|
|
482
|
+
If the argument is missing, the task name is shown. This is a **server** setting: set it on
|
|
483
|
+
`stemtrace server` (flag or `STEMTRACE_NODE_ALIAS_FROM_ARGUMENTS`, e.g. in Docker) or pass
|
|
484
|
+
`node_alias_from_arguments` to `stemtrace.init_app(...)`. Surrounding whitespace is ignored, and
|
|
485
|
+
`stemtrace server` prints `Node alias: <key>` at startup when it is set. Workers need
|
|
486
|
+
`capture_args=True` (the default) so the arguments are available. Passing it to
|
|
487
|
+
`stemtrace.init_worker(...)` has no effect and is deprecated.
|
|
488
|
+
|
|
436
489
|
#### Protecting the Server (Built-in Login Page)
|
|
437
490
|
|
|
438
491
|
```bash
|
|
@@ -486,6 +539,7 @@ extension = stemtrace.init_app(
|
|
|
486
539
|
embedded_consumer=True, # Run consumer in FastAPI process
|
|
487
540
|
serve_ui=True, # Serve React dashboard
|
|
488
541
|
auth_dependency=None, # Optional auth (see below)
|
|
542
|
+
node_alias_from_arguments=None, # e.g. "operator_type" or "0"; see Node Names from Task Arguments
|
|
489
543
|
)
|
|
490
544
|
```
|
|
491
545
|
|
|
@@ -567,7 +621,7 @@ Because the status code stays 200, check `status` in the body if you want monito
|
|
|
567
621
|
What is detected depends on the transport:
|
|
568
622
|
|
|
569
623
|
- **Redis:** refused or reset connections, connect timeouts, repeated read timeouts, and consumer loop failures. On redis-py < 8 the default socket timeout is unbounded, so a half-open or hung broker connection may go unnoticed; add `?socket_timeout=…` or `?health_check_interval=…` to the broker URL if you need that.
|
|
570
|
-
- **RabbitMQ:**
|
|
624
|
+
- **RabbitMQ:** refused or dropped connections and channel errors while connecting or draining, and consumer loop failures. The consumer connection is read about once a second and a read that times out counts as connected, so a half-open connection, or a broker that is frozen but keeps the socket open, may go unnoticed. Messages the consumer can't decode (not JSON) are dropped with a warning and don't count as failures.
|
|
571
625
|
|
|
572
626
|
## 🗺️ Roadmap
|
|
573
627
|
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
**Zero-infrastructure Celery task flow visualizer**
|
|
6
6
|
|
|
7
|
-
[](https://pypi.org/project/stemtrace)
|
|
8
8
|
[](https://pypi.org/project/stemtrace/)
|
|
9
9
|
[](https://github.com/iansokolskyi/stemtrace/actions/workflows/ci.yml)
|
|
10
10
|
[](https://codecov.io/gh/iansokolskyi/stemtrace)
|
|
@@ -198,10 +198,6 @@ stemtrace.init_worker(
|
|
|
198
198
|
scrub_sensitive_data=True, # Scrub passwords, API keys, etc.
|
|
199
199
|
additional_sensitive_keys=frozenset({"my_secret"}), # Add custom keys
|
|
200
200
|
safe_keys=frozenset({"public_key"}), # Never scrub these keys
|
|
201
|
-
|
|
202
|
-
# UI display
|
|
203
|
-
node_alias_from_arguments="operator_type", # Use kwargs["operator_type"] as node name
|
|
204
|
-
# node_alias_from_arguments="0", # Or use args[0] (digit string = positional index)
|
|
205
201
|
)
|
|
206
202
|
|
|
207
203
|
# Introspection (after init)
|
|
@@ -281,6 +277,7 @@ stemtrace.init_app(
|
|
|
281
277
|
|----------|-------------|---------|
|
|
282
278
|
| `STEMTRACE_BROKER_URL` | Celery broker URL (used for on-demand worker/registry inspection). Also used as the default for `STEMTRACE_TRANSPORT_URL`. | `redis://localhost:6379/0` |
|
|
283
279
|
| `STEMTRACE_TRANSPORT_URL` | Event transport URL (where stemtrace publishes/consumes events). | Defaults to `STEMTRACE_BROKER_URL`. |
|
|
280
|
+
| `STEMTRACE_NODE_ALIAS_FROM_ARGUMENTS` | Server only: show a task argument as the graph node name (see [Node Names from Task Arguments](#node-names-from-task-arguments)). Same as `--node-alias-from-arguments` / `init_app(node_alias_from_arguments=...)`. | Unset (task names are shown). |
|
|
284
281
|
|
|
285
282
|
### Supported Brokers
|
|
286
283
|
|
|
@@ -301,6 +298,36 @@ again once events are re-consumed.
|
|
|
301
298
|
- Events already consumed/acked by the server are **gone**.
|
|
302
299
|
- Events published while the server is down are only visible after restart if the server’s
|
|
303
300
|
durable per-consumer queue still exists and the messages are still within TTL.
|
|
301
|
+
- Events that can't be published to RabbitMQ are dropped, not buffered on the worker, and a
|
|
302
|
+
broker outage doesn't slow tasks down. Each publish tries every host of the broker URL once
|
|
303
|
+
(2s connect timeout, 2s per read or write after that). A single quick failure, such as a
|
|
304
|
+
refused connection, drops only that event; a failure that took 0.5s or more (a timeout) or
|
|
305
|
+
three quick failures in a row make the worker skip publishing for a cooldown that grows from
|
|
306
|
+
1s to 30s and resets on the next successful publish.
|
|
307
|
+
- During a RabbitMQ memory or disk alarm, the broker accepts connections but stops reading
|
|
308
|
+
from publishers, so a publish is sent but never confirmed. The worker counts such events as
|
|
309
|
+
"sent but unconfirmed" (RabbitMQ may still deliver them once the alarm clears); after two
|
|
310
|
+
unconfirmed attempts in a row it retries only every 30s. Each attempt leaves one blocked
|
|
311
|
+
connection on the broker until the alarm clears, so expect 2 to 3 connections per worker
|
|
312
|
+
process in the first minute of an alarm and about 2 per minute after that, all released
|
|
313
|
+
when the alarm ends. Each retry also logs `Received method (10, 60) during closing channel 0`
|
|
314
|
+
from py-amqp's `amqp` logger (not stemtrace's): that's RabbitMQ's "connection blocked"
|
|
315
|
+
notice arriving while the connection closes.
|
|
316
|
+
- Logging is bounded: at most four warnings a minute however the broker behaves (failures,
|
|
317
|
+
an outage starting, an outage ending, and a summary). Each reports the number of events
|
|
318
|
+
dropped, and sent but unconfirmed, since the previous one, and losses are reported within
|
|
319
|
+
about 1.5 minutes even when the broker flaps.
|
|
320
|
+
- Limitation: a broker that answers but slowly (say, close to the 2s timeout for each step)
|
|
321
|
+
still costs every event several seconds, because publishes that eventually succeed don't
|
|
322
|
+
open the breaker.
|
|
323
|
+
- A failover broker URL (`amqp://h1//;amqp://h2//`) must list nodes of the same RabbitMQ
|
|
324
|
+
cluster: workers keep publishing to whichever host last worked, so the server must be able to
|
|
325
|
+
consume those events from any of them. Entries resolve as in kombu: a bare host (`;h2`)
|
|
326
|
+
takes user, password, port, virtual host and TLS options from the first entry, while a
|
|
327
|
+
URL-form entry (`;amqp://h2`) gets guest/guest unless it names credentials, and takes port
|
|
328
|
+
and virtual host from the previous entry only when it omits them (`//` means `/`). A host
|
|
329
|
+
that refuses the login ends that publish attempt without trying the later hosts, as in
|
|
330
|
+
kombu.
|
|
304
331
|
- **Workers + Registry tabs**: stemtrace uses **Celery inspect** on demand to populate workers
|
|
305
332
|
and registered tasks, so those pages work even if the server missed `worker_ready` events.
|
|
306
333
|
- **If you need durable history across restarts**: point stemtrace events at Redis even if your
|
|
@@ -332,6 +359,15 @@ docker run -p 8000:8000 \
|
|
|
332
359
|
ghcr.io/iansokolskyi/stemtrace
|
|
333
360
|
```
|
|
334
361
|
|
|
362
|
+
Show a task argument as the graph node name (here `kwargs["operator_type"]`):
|
|
363
|
+
|
|
364
|
+
```bash
|
|
365
|
+
docker run -p 8000:8000 \
|
|
366
|
+
-e STEMTRACE_BROKER_URL=redis://host.docker.internal:6379/0 \
|
|
367
|
+
-e STEMTRACE_NODE_ALIAS_FROM_ARGUMENTS=operator_type \
|
|
368
|
+
ghcr.io/iansokolskyi/stemtrace
|
|
369
|
+
```
|
|
370
|
+
|
|
335
371
|
Or with Docker Compose:
|
|
336
372
|
|
|
337
373
|
```yaml
|
|
@@ -375,9 +411,26 @@ stemtrace server \
|
|
|
375
411
|
--transport-url redis://myredis:6379/0 \
|
|
376
412
|
--host 0.0.0.0 \
|
|
377
413
|
--port 8000 \
|
|
414
|
+
--node-alias-from-arguments operator_type \
|
|
378
415
|
--reload # For development
|
|
379
416
|
```
|
|
380
417
|
|
|
418
|
+
#### Node Names from Task Arguments
|
|
419
|
+
|
|
420
|
+
By default, graph nodes are labeled with the task name. Generic tasks (e.g. one `run_operator`
|
|
421
|
+
task that does different things depending on its arguments) are easier to tell apart when the
|
|
422
|
+
graph shows an argument instead:
|
|
423
|
+
|
|
424
|
+
- `--node-alias-from-arguments operator_type` uses `kwargs["operator_type"]`
|
|
425
|
+
- `--node-alias-from-arguments 0` uses `args[0]` (a digit string is a positional index)
|
|
426
|
+
|
|
427
|
+
If the argument is missing, the task name is shown. This is a **server** setting: set it on
|
|
428
|
+
`stemtrace server` (flag or `STEMTRACE_NODE_ALIAS_FROM_ARGUMENTS`, e.g. in Docker) or pass
|
|
429
|
+
`node_alias_from_arguments` to `stemtrace.init_app(...)`. Surrounding whitespace is ignored, and
|
|
430
|
+
`stemtrace server` prints `Node alias: <key>` at startup when it is set. Workers need
|
|
431
|
+
`capture_args=True` (the default) so the arguments are available. Passing it to
|
|
432
|
+
`stemtrace.init_worker(...)` has no effect and is deprecated.
|
|
433
|
+
|
|
381
434
|
#### Protecting the Server (Built-in Login Page)
|
|
382
435
|
|
|
383
436
|
```bash
|
|
@@ -431,6 +484,7 @@ extension = stemtrace.init_app(
|
|
|
431
484
|
embedded_consumer=True, # Run consumer in FastAPI process
|
|
432
485
|
serve_ui=True, # Serve React dashboard
|
|
433
486
|
auth_dependency=None, # Optional auth (see below)
|
|
487
|
+
node_alias_from_arguments=None, # e.g. "operator_type" or "0"; see Node Names from Task Arguments
|
|
434
488
|
)
|
|
435
489
|
```
|
|
436
490
|
|
|
@@ -512,7 +566,7 @@ Because the status code stays 200, check `status` in the body if you want monito
|
|
|
512
566
|
What is detected depends on the transport:
|
|
513
567
|
|
|
514
568
|
- **Redis:** refused or reset connections, connect timeouts, repeated read timeouts, and consumer loop failures. On redis-py < 8 the default socket timeout is unbounded, so a half-open or hung broker connection may go unnoticed; add `?socket_timeout=…` or `?health_check_interval=…` to the broker URL if you need that.
|
|
515
|
-
- **RabbitMQ:**
|
|
569
|
+
- **RabbitMQ:** refused or dropped connections and channel errors while connecting or draining, and consumer loop failures. The consumer connection is read about once a second and a read that times out counts as connected, so a half-open connection, or a broker that is frozen but keeps the socket open, may go unnoticed. Messages the consumer can't decode (not JSON) are dropped with a warning and don't count as failures.
|
|
516
570
|
|
|
517
571
|
## 🗺️ Roadmap
|
|
518
572
|
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Print one version's section of CHANGELOG.md, for use as GitHub release notes.
|
|
2
|
+
|
|
3
|
+
Usage: python scripts/changelog_section.py <version|Unreleased>
|
|
4
|
+
|
|
5
|
+
Exits with status 1 when the section is missing or empty, so a release can't be
|
|
6
|
+
published without notes.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import re
|
|
10
|
+
import sys
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
CHANGELOG = Path(__file__).resolve().parents[1] / "CHANGELOG.md"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def changelog_section(text: str, version: str) -> str:
|
|
17
|
+
"""Return the body of the ``## [version]`` section, without its heading.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
text: Full CHANGELOG.md content (Keep a Changelog format).
|
|
21
|
+
version: Version such as ``0.3.8`` (a leading ``v`` is ignored) or
|
|
22
|
+
``Unreleased``.
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
The section body with surrounding blank lines stripped; empty if the
|
|
26
|
+
section is missing or holds only subheadings or HTML comments.
|
|
27
|
+
"""
|
|
28
|
+
version = version.removeprefix("v")
|
|
29
|
+
heading = re.compile(rf"^## \[{re.escape(version)}\][^\n]*\n", re.MULTILINE)
|
|
30
|
+
match = heading.search(text)
|
|
31
|
+
if match is None:
|
|
32
|
+
return ""
|
|
33
|
+
rest = text[match.end() :]
|
|
34
|
+
next_heading = re.search(r"^## \[", rest, re.MULTILINE)
|
|
35
|
+
body = rest[: next_heading.start()] if next_heading else rest
|
|
36
|
+
# Link reference definitions at the end of the file aren't release notes.
|
|
37
|
+
body = re.sub(r"^\[[^\]]+\]:\s+\S+\s*$", "", body, flags=re.MULTILINE)
|
|
38
|
+
body = body.strip()
|
|
39
|
+
# A section with only "### Added"-style headings or comments has no notes.
|
|
40
|
+
content = re.sub(r"<!--.*?-->", "", body, flags=re.DOTALL)
|
|
41
|
+
content = re.sub(r"^#{3,}.*$", "", content, flags=re.MULTILINE)
|
|
42
|
+
return body if content.strip() else ""
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def main(argv: list[str]) -> int:
|
|
46
|
+
"""Print the requested section of the repo's CHANGELOG.md; 1 if empty."""
|
|
47
|
+
if len(argv) != 2:
|
|
48
|
+
print(__doc__.strip().splitlines()[2], file=sys.stderr)
|
|
49
|
+
return 2
|
|
50
|
+
section = changelog_section(CHANGELOG.read_text(encoding="utf-8"), argv[1])
|
|
51
|
+
if not section:
|
|
52
|
+
print(f"No CHANGELOG.md entries for [{argv[1]}]", file=sys.stderr)
|
|
53
|
+
return 1
|
|
54
|
+
print(section)
|
|
55
|
+
return 0
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
if __name__ == "__main__":
|
|
59
|
+
sys.exit(main(sys.argv))
|