sparkforensics-operator 0.2.0__tar.gz → 0.4.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 (112) hide show
  1. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/.github/workflows/ci.yml +30 -0
  2. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/AGENTS.md +8 -7
  3. sparkforensics_operator-0.2.0/docs/architecture.md → sparkforensics_operator-0.4.0/ARCHITECTURE.md +111 -35
  4. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/CHANGELOG.md +70 -1
  5. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/CLAUDE.md +6 -6
  6. sparkforensics_operator-0.4.0/CONTRIBUTING.md +100 -0
  7. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/PKG-INFO +23 -11
  8. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/README.md +22 -10
  9. sparkforensics_operator-0.4.0/docs/configuration.md +755 -0
  10. sparkforensics_operator-0.4.0/docs/getting-started.md +227 -0
  11. sparkforensics_operator-0.4.0/docs/index.md +28 -0
  12. sparkforensics_operator-0.4.0/docs/troubleshooting.md +467 -0
  13. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/pyproject.toml +1 -1
  14. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/src/sparkforensics_operator/callback.py +43 -7
  15. sparkforensics_operator-0.4.0/src/sparkforensics_operator/exceptions.py +29 -0
  16. sparkforensics_operator-0.4.0/src/sparkforensics_operator/hooks/analyze/_cli.py +220 -0
  17. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/src/sparkforensics_operator/hooks/analyze/_remote_job.py +53 -6
  18. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/src/sparkforensics_operator/hooks/analyze/base.py +48 -8
  19. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/src/sparkforensics_operator/hooks/analyze/ssh.py +116 -14
  20. sparkforensics_operator-0.4.0/src/sparkforensics_operator/hooks/analyze/subprocess.py +89 -0
  21. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/src/sparkforensics_operator/notify.py +25 -4
  22. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/src/sparkforensics_operator/operator.py +311 -41
  23. sparkforensics_operator-0.4.0/src/sparkforensics_operator/report.py +207 -0
  24. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/src/sparkforensics_operator/sinks.py +5 -1
  25. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/src/sparkforensics_operator/summary.py +56 -4
  26. sparkforensics_operator-0.4.0/tests/fixtures/sparkforensics_cli_0_4_0/README.md +17 -0
  27. sparkforensics_operator-0.4.0/tests/fixtures/sparkforensics_cli_0_4_0/comparison.json +1064 -0
  28. sparkforensics_operator-0.4.0/tests/fixtures/sparkforensics_cli_0_4_0/comparison.stderr +1 -0
  29. sparkforensics_operator-0.4.0/tests/fixtures/sparkforensics_cli_0_4_0/incomplete-run.json +691 -0
  30. sparkforensics_operator-0.4.0/tests/fixtures/sparkforensics_cli_0_4_0/incomplete-run.stderr +1 -0
  31. sparkforensics_operator-0.4.0/tests/fixtures/sparkforensics_cli_0_4_0/report.json +1209 -0
  32. sparkforensics_operator-0.4.0/tests/fixtures/sparkforensics_cli_0_4_0/report.stderr +2 -0
  33. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/tests/hooks/analyze/_local_ssh.py +8 -1
  34. sparkforensics_operator-0.4.0/tests/hooks/analyze/test_base.py +100 -0
  35. sparkforensics_operator-0.4.0/tests/hooks/analyze/test_cli.py +394 -0
  36. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/tests/hooks/analyze/test_deferrable_parity.py +64 -9
  37. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/tests/hooks/analyze/test_ssh.py +105 -6
  38. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/tests/hooks/analyze/test_ssh_deferrable.py +168 -1
  39. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/tests/hooks/analyze/test_subprocess.py +166 -3
  40. sparkforensics_operator-0.4.0/tests/integration/__init__.py +0 -0
  41. sparkforensics_operator-0.4.0/tests/integration/conftest.py +70 -0
  42. sparkforensics_operator-0.4.0/tests/integration/test_real_cli.py +101 -0
  43. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/tests/test_callback.py +19 -0
  44. sparkforensics_operator-0.4.0/tests/test_notify.py +92 -0
  45. sparkforensics_operator-0.4.0/tests/test_operator.py +895 -0
  46. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/tests/test_operator_deferrable.py +150 -0
  47. sparkforensics_operator-0.4.0/tests/test_report.py +392 -0
  48. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/tests/test_sinks.py +25 -0
  49. sparkforensics_operator-0.4.0/tests/test_summary.py +303 -0
  50. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/tests/test_templating.py +78 -0
  51. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/tox.ini +2 -0
  52. sparkforensics_operator-0.2.0/docs/api-reference.md +0 -396
  53. sparkforensics_operator-0.2.0/docs/glossary.md +0 -58
  54. sparkforensics_operator-0.2.0/docs/runbook.md +0 -374
  55. sparkforensics_operator-0.2.0/src/sparkforensics_operator/exceptions.py +0 -10
  56. sparkforensics_operator-0.2.0/src/sparkforensics_operator/hooks/analyze/_cli.py +0 -88
  57. sparkforensics_operator-0.2.0/src/sparkforensics_operator/hooks/analyze/subprocess.py +0 -55
  58. sparkforensics_operator-0.2.0/src/sparkforensics_operator/report.py +0 -92
  59. sparkforensics_operator-0.2.0/tests/hooks/analyze/test_base.py +0 -38
  60. sparkforensics_operator-0.2.0/tests/hooks/analyze/test_cli.py +0 -17
  61. sparkforensics_operator-0.2.0/tests/test_notify.py +0 -42
  62. sparkforensics_operator-0.2.0/tests/test_operator.py +0 -321
  63. sparkforensics_operator-0.2.0/tests/test_report.py +0 -128
  64. sparkforensics_operator-0.2.0/tests/test_summary.py +0 -143
  65. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/.github/workflows/publish.yml +0 -0
  66. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/.gitignore +0 -0
  67. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/LICENSE +0 -0
  68. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/constraints-airflow-2.6-floor.txt +0 -0
  69. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/constraints-deferrable-floor.txt +0 -0
  70. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/coverage-no-ssh.ini +0 -0
  71. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/src/sparkforensics_operator/__init__.py +0 -0
  72. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/src/sparkforensics_operator/_compat.py +0 -0
  73. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/src/sparkforensics_operator/get_provider_info.py +0 -0
  74. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/src/sparkforensics_operator/hooks/__init__.py +0 -0
  75. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/src/sparkforensics_operator/hooks/_templating.py +0 -0
  76. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/src/sparkforensics_operator/hooks/analyze/__init__.py +0 -0
  77. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/src/sparkforensics_operator/hooks/log_source/__init__.py +0 -0
  78. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/src/sparkforensics_operator/hooks/log_source/_dest_root.py +0 -0
  79. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/src/sparkforensics_operator/hooks/log_source/_history_server_args.py +0 -0
  80. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/src/sparkforensics_operator/hooks/log_source/_path_template.py +0 -0
  81. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/src/sparkforensics_operator/hooks/log_source/_rolling_log.py +0 -0
  82. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/src/sparkforensics_operator/hooks/log_source/base.py +0 -0
  83. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/src/sparkforensics_operator/hooks/log_source/filesystem.py +0 -0
  84. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/src/sparkforensics_operator/hooks/log_source/history_server.py +0 -0
  85. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/src/sparkforensics_operator/hooks/log_source/history_server_app.py +0 -0
  86. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/src/sparkforensics_operator/hooks/log_source/remote_path.py +0 -0
  87. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/src/sparkforensics_operator/hooks/log_source/sftp.py +0 -0
  88. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/src/sparkforensics_operator/hooks/log_source/tunnel.py +0 -0
  89. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/src/sparkforensics_operator/hooks/log_source/xcom.py +0 -0
  90. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/src/sparkforensics_operator/links.py +0 -0
  91. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/src/sparkforensics_operator/log_ref.py +0 -0
  92. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/tests/__init__.py +0 -0
  93. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/tests/_render.py +0 -0
  94. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/tests/hooks/__init__.py +0 -0
  95. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/tests/hooks/analyze/__init__.py +0 -0
  96. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/tests/hooks/log_source/__init__.py +0 -0
  97. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/tests/hooks/log_source/test_base.py +0 -0
  98. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/tests/hooks/log_source/test_filesystem.py +0 -0
  99. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/tests/hooks/log_source/test_history_server.py +0 -0
  100. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/tests/hooks/log_source/test_history_server_app.py +0 -0
  101. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/tests/hooks/log_source/test_remote_path.py +0 -0
  102. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/tests/hooks/log_source/test_sftp.py +0 -0
  103. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/tests/hooks/log_source/test_tunnel.py +0 -0
  104. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/tests/hooks/log_source/test_xcom.py +0 -0
  105. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/tests/test_compat.py +0 -0
  106. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/tests/test_dagbag_integration.py +0 -0
  107. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/tests/test_exceptions.py +0 -0
  108. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/tests/test_links.py +0 -0
  109. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/tests/test_log_ref.py +0 -0
  110. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/tests/test_package_exports.py +0 -0
  111. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/tests/test_provider_info.py +0 -0
  112. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/tests/test_serialization.py +0 -0
@@ -5,6 +5,10 @@ on:
5
5
  branches: [main]
6
6
  pull_request:
7
7
 
8
+ concurrency:
9
+ group: ${{ github.workflow }}-${{ github.ref }}
10
+ cancel-in-progress: true
11
+
8
12
  jobs:
9
13
  test:
10
14
  runs-on: ubuntu-latest
@@ -40,6 +44,32 @@ jobs:
40
44
  path-to-lcov: coverage-${{ matrix.tox-env }}.lcov
41
45
  parallel: true
42
46
 
47
+ # The rest of the suite simulates the CLI. This job runs the real one on
48
+ # public corpus logs, so a change in its exit codes or flag rules fails
49
+ # here. SPARKFORENSICS_REQUIRE_CLI turns a missing CLI or an unreachable
50
+ # corpus into a failure instead of a skip.
51
+ cli-integration:
52
+ runs-on: ubuntu-latest
53
+ strategy:
54
+ fail-fast: false
55
+ matrix:
56
+ # The release the operator is tested against (TESTED_CLI_VERSION in
57
+ # report.py), and the newest one.
58
+ cli: ["0.12.0", "latest"]
59
+ env:
60
+ SPARKFORENSICS_REQUIRE_CLI: "1"
61
+ steps:
62
+ - uses: actions/checkout@v4
63
+ - uses: actions/setup-python@v5
64
+ with:
65
+ python-version: "3.12"
66
+ - uses: actions/setup-node@v4
67
+ with:
68
+ node-version: "22"
69
+ - run: npm install -g sparkforensics-cli@${{ matrix.cli }}
70
+ - run: pip install tox
71
+ - run: tox -e py312-airflow3 -- tests/integration -v
72
+
43
73
  finish:
44
74
  needs: test
45
75
  runs-on: ubuntu-latest
@@ -9,14 +9,14 @@ Python 3.9+, apache-airflow 2.6+ or 3.0+, pytest. Runtime dependency: Node.js
9
9
  18+ + the `sparkforensics-cli` npm package (`npm install -g sparkforensics-cli`)
10
10
  wherever the analysis runs: the worker for `SubprocessAnalyzeHook`, the SSH
11
11
  host for `SSHAnalyzeHook`. Log sources resolve an `EventLogRef` (`log_ref.py`)
12
- that backends consume; see `docs/architecture.md`. `deferrable=True` runs the
13
- SSH analysis as the SSH provider's detached remote job (architecture.md's
12
+ that backends consume; see `ARCHITECTURE.md`. `deferrable=True` runs the
13
+ SSH analysis as the SSH provider's detached remote job (`ARCHITECTURE.md`'s
14
14
  "Deferrable execution").
15
15
 
16
16
  ## Commands
17
17
  - `pip install -e ".[test,s3,ssh]"`, install for local dev.
18
- - `pytest`, run the test suite (mocks everything: no live Spark/Airflow/Node
19
- needed for any single test).
18
+ - `pytest`, run the test suite (mocks everything except `tests/integration`,
19
+ which runs the real CLI on public logs and skips without it).
20
20
  - `tox -e py311-airflow2` / `tox -e py311-airflow3`, run the suite against a
21
21
  specific Airflow major version.
22
22
 
@@ -44,9 +44,10 @@ of going public. The original multi-PR history (42 commits, 4 PRs) lives in
44
44
  indefinitely). `git blame`/`git log -p` on any line predating that date stop
45
45
  at the squash commit; check the archive repo instead.
46
46
 
47
- ## Design docs
48
- - `docs/architecture.md`, `docs/api-reference.md`, `docs/runbook.md`,
49
- `docs/glossary.md`, see below.
47
+ ## Docs
48
+ - User docs: `docs/index.md` (getting started, configuration,
49
+ troubleshooting). Contributor docs: `CONTRIBUTING.md` (dev, tox matrix,
50
+ release), `ARCHITECTURE.md` (design notes).
50
51
 
51
52
  ## Maintaining this file
52
53
 
@@ -30,8 +30,24 @@ Each backend declares the reference kinds it can read from where it runs
30
30
  (`AnalyzeHook.supported_log_refs`); `AnalyzeHook.analyze()` rejects any
31
31
  other kind with an error naming what it reads, before running anything, so
32
32
  a mismatched pairing (say, `SSHAnalyzeHook` with a log the worker
33
- downloaded) fails fast instead of analyzing the wrong file. Both trigger shapes (the standalone `SparkForensicsOperator` and the `spark_forensics_callback`
34
- `on_success_callback` factory) converge on one function,
33
+ downloaded) fails fast instead of analyzing the wrong file.
34
+
35
+ A run can also be compared against an earlier one: `baseline_log_source`
36
+ is a second `LogSourceHook`, located and cleaned up next to `log_source`,
37
+ and the backend passes its reference to the CLI as `--baseline` (an event
38
+ log path) or `--baseline-shs-base-url`/`--baseline-app-id`/
39
+ `--baseline-attempt-id` (a History Server application, which the CLI
40
+ fetches itself, like the run under analysis). Each backend declares the
41
+ kinds it can pass (`AnalyzeHook.supported_baseline_refs`: `LocalEventLog`
42
+ and `HistoryServerApp` on the worker, `RemoteEventLog` on its own host and
43
+ `HistoryServerApp` on the SSH host), and `analyze()` or the deferrable
44
+ submit step rejects any other before running anything. The comparison
45
+ itself, and the budgets on it, are the CLI's. Which run is the baseline
46
+ is the DAG author's choice, made through that log source: the operator
47
+ keeps no state across DAG runs.
48
+
49
+ Both trigger shapes (the standalone `SparkForensicsOperator` and the
50
+ `spark_forensics_callback` `on_success_callback` factory) converge on one function,
35
51
  `operator.run_spark_forensics(context, ...)`, so there is exactly one
36
52
  execute-analyze-persist-notify-threshold code path to maintain. Notify
37
53
  always runs before the threshold check's fail/warn/ignore branching, so a
@@ -39,19 +55,24 @@ breach that raises still gets a notification out first.
39
55
 
40
56
  ## Components
41
57
 
42
- - `_compat.py`, the single place this package imports Airflow's public
43
- API from. `BaseOperator`/`BaseOperatorLink` live in `airflow.sdk` on
58
+ - `_compat.py`, where this package imports Airflow's core API from (the
59
+ one exception is `links.py`'s lazy `XCom` import on the Airflow 2
60
+ branch; provider hooks are imported where they are used). `BaseOperator`/`BaseOperatorLink` live in `airflow.sdk` on
44
61
  3.x and `airflow.models.*` on 2.x. The Task SDK also re-homed `BaseHook`
45
62
  (3.1), and the exceptions, `TaskDeferred` and `conf` (3.2); their old
46
63
  paths still work there, as the same objects, behind deprecation shims.
47
64
  `_compat.py` tries the SDK path first and falls back to the old one, and
48
65
  `tests/test_compat.py` imports every module of the package in a fresh
49
- interpreter and fails on any deprecation warning they emit. The repo has
66
+ interpreter and fails if any import fails or emits a
67
+ `DeprecationWarning`, `PendingDeprecationWarning` or `FutureWarning`
68
+ from inside the package. The repo has
50
69
  no ruff configuration, so there are no ruff `AIR3` rules to enable; that
51
70
  test covers the same ground.
52
71
  - `hooks/_templating.py`, `TemplatedHookMixin`, the base of every hook:
53
- an empty `template_fields` and a `__repr__` that shows them. See
54
- "Templated hook arguments" below.
72
+ an empty `template_fields` and a `__repr__` that shows them. Also
73
+ `task_renderer()` and `render_with_task_env()`, which render a value
74
+ with a task's Jinja environment for the callback and the tunnel hook.
75
+ See "Templated hook arguments" below.
55
76
  - `log_ref.py`, the `EventLogRef` kinds (`LocalEventLog`,
56
77
  `RemoteEventLog`, `HistoryServerApp`): frozen dataclasses saying where a
57
78
  log is, the only thing a log source and a backend share.
@@ -68,8 +89,9 @@ breach that raises still gets a notification out first.
68
89
  cleaning up after itself in that case; currently `HistoryServerLogSourceHook`
69
90
  and `SFTPLogSourceHook` do this, via their own internal try/except
70
91
  around the download/transfer that removes their owned temp dir on any
71
- exception. See `docs/runbook.md`'s "Disk fills up" entry for which
72
- hook/config combinations actually clean up.
92
+ exception. See the "Disk fills up on a worker" entry in
93
+ `docs/troubleshooting.md` for which hook/config combinations actually
94
+ clean up.
73
95
  - `hooks/log_source/{sftp,tunnel}.py`, `SFTPLogSourceHook` and
74
96
  `SSHTunneledLogSourceHook`, for on-prem log sources reachable only over
75
97
  the SSH connection an `SSHOperator` already has configured (no cloud
@@ -89,27 +111,47 @@ breach that raises still gets a notification out first.
89
111
  `HistoryServerApp` and do no I/O. Per-run values come from Jinja, the
90
112
  same way as in the fetching hooks (see "Templated hook arguments").
91
113
  `hooks/log_source/_path_template.py` checks a rendered path (rendered,
92
- non-empty, no `..` segment) and `_history_server_args.py` checks a
114
+ non-empty, no `..` segment, no old `{run_id}`-style placeholder) and `_history_server_args.py` checks a
93
115
  rendered app id and attempt id; `_rolling_log.py` holds the
94
116
  rolling-log-entry-matching logic shared between `history_server.py`
95
117
  and `sftp.py`. `_dest_root.py` holds the `dest_root/<basename>` staging
96
118
  convention shared by `filesystem.py`/`sftp.py`, and the owned-temp-dir
97
119
  bookkeeping (create if `dest_dir` is unset, `rmtree` on cleanup/error if
98
120
  owned) shared by `sftp.py`/`history_server.py`.
99
- - `hooks/analyze/base.py`, `AnalyzeHook`: `analyze(log_ref, thresholds)`
100
- checks the reference against `supported_log_refs` (`check_log_ref()`),
101
- then calls the backend's `_analyze()`. `DeferrableAnalyzeHook` adds the
121
+ - `hooks/analyze/base.py`, `AnalyzeHook`: `analyze(log_ref, thresholds,
122
+ baseline_ref=None)` checks the reference against `supported_log_refs`
123
+ (`check_log_ref()`) and any baseline against `supported_baseline_refs`
124
+ (`check_baseline_ref()`), then calls the backend's `_analyze()`. `DeferrableAnalyzeHook` adds the
102
125
  detached-job interface the deferrable mode drives (`submit`,
103
126
  `trigger_for`, `defer_timeout`, `collect`, `abandon`); see "Deferrable
104
127
  execution" below.
105
128
  - `hooks/analyze/_cli.py`, the CLI contract every backend shares: argument
106
129
  building for each reference kind (a positional path, or
107
- `--shs-base-url`/`--app-id`/`--attempt-id`), threshold flags, exit-code
108
- handling (0/1/3 usable, 2 and anything else raise), and building the
109
- `Report` from the JSON report and stderr threshold lines. Backends only
110
- differ in how they run the command and collect its output.
130
+ `--shs-base-url`/`--app-id`/`--attempt-id`), `--baseline`, threshold flags
131
+ (a dict of per-metric budgets becomes one `--regression-budget` or
132
+ `--stage-regression-budget <metric>:<pct>` each), the comparison tuning
133
+ flags (`--normalize-path` and `--ignore-conf` once per pattern,
134
+ `--noise-band-pct`), the `--redact` switch, the Markdown run (below),
135
+ exit-code handling, and building the `Report` from the JSON report and
136
+ stderr threshold lines. Exit codes 0, 1 and 3 carry a report. Every other
137
+ code raises a `CliError` with its own message: 2 (usage error), 4
138
+ (unreadable event log or failed History Server fetch), 5 (unreadable
139
+ baseline), 6 (internal error), anything else "unexpected". The CLI's
140
+ stderr separates a History Server failure a retry can clear
141
+ (`HistoryServerFetchError`) from one it cannot (`CliConfigurationError`,
142
+ an `AirflowFailException`, so Airflow does not retry it). Backends differ
143
+ only in how they run the command and collect its output.
111
144
  - `hooks/analyze/subprocess.py`, `SubprocessAnalyzeHook`, runs the CLI on
112
- the worker with `--out <tmpfile>`.
145
+ the worker with `--out <tmpfile>`. The CLI writes one report format per
146
+ run, so for the Markdown report (`thresholds["markdown"]`, set by
147
+ `markdown_dest`) the backend runs it a second time with `--format md` and
148
+ no budget flags, and sets `Report.markdown`
149
+ (`AnalyzeHook.supports_markdown`). The first run's report and thresholds
150
+ decide the outcome; the second run only adds the text. `SSHAnalyzeHook`
151
+ does the same on the host: a second synchronous run or, in a deferred
152
+ job, a second command in the same remote job. Its outputs (report, stderr,
153
+ exit code) go to their own files, and the job's exit code stays the
154
+ first run's.
113
155
  - `hooks/analyze/ssh.py`, `SSHAnalyzeHook`, runs the CLI on the SSH host
114
156
  over `SSHHook` (imported lazily, `ssh` extra) and reads the report from
115
157
  stdout, so nothing is written on that host (see "Killing a task" below
@@ -135,15 +177,35 @@ breach that raises still gets a notification out first.
135
177
  which reports the CLI's pid, and the command that stops it.
136
178
  - `report.py`, `Report`/`ThresholdResult` dataclasses, plus parsing of the
137
179
  CLI's JSON report (`--out` file or stdout) and its stderr threshold
138
- lines. No
180
+ lines. Several regression budgets print as `max-regression` lines
181
+ (`max-stage-regression` for the stage budgets), and
182
+ `parse_threshold_results()` keeps every line it reads, so a name can have
183
+ several results. It refuses a `schemaVersion` outside
184
+ `MIN_SCHEMA_VERSION` to `MAX_SCHEMA_VERSION` (an
185
+ `UnsupportedReportSchema` that `build_report()` turns into a
186
+ `CliConfigurationError`), reads the CLI's version from the report's
187
+ `generator`, and checks it against the release the operator is tested
188
+ against (`TESTED_CLI_VERSION`). No
139
189
  threshold-evaluation *logic* lives here: sparkforensics's own CLI decides
140
190
  pass/violation/inconclusive, this module only parses what it printed, so
141
191
  it can never drift from what the CLI actually enforces.
142
- - `sinks.py`, persists the report JSON (local path, `file://`, `s3://`).
192
+ - `exceptions.py`, `ThresholdBreached`, an `AirflowFailException` so a
193
+ breach fails the task without consuming its retries, and the CLI
194
+ errors: `CliError` (a CLI exit code with no report, carrying
195
+ `exit_code`), its `HistoryServerFetchError` (retried) and
196
+ `CliConfigurationError` (an `AirflowFailException`, not retried).
197
+ - `sinks.py`, persists the report JSON (local path, `file://`, `s3://`),
198
+ and, through `persist_text()`, the Markdown report. The JSON is the
199
+ CLI's whole report (`Report.raw`) plus the operator's `thresholdResults`
200
+ and the comparison section, so a field the CLI adds reaches the file
201
+ without a change here. `operator._report_json()` builds it; a `Report`
202
+ built by hand (no `raw`) persists the sections the operator reads.
143
203
  - `notify.py`, the `Notifier` base class: subclasses implement `_send()`
144
204
  for whatever messaging/paging system they target, and `notify()` makes
145
- that best-effort, never raising. No concrete notifier ships in this
146
- package.
205
+ that best-effort, never raising. `_default_message()` writes the
206
+ one-line summary (application, verdict, run time, finding counts,
207
+ skipped lines, destination), leaving out what the report lacks. No
208
+ concrete notifier ships in this package.
147
209
  - `summary.py`, the summary XCom (`SUMMARY_XCOM_KEY`,
148
210
  `build_summary()`) and the report URL (`render_report_url()`,
149
211
  `validate_report_url_template()`). See "Summary XCom and report URL"
@@ -176,6 +238,7 @@ breach that raises still gets a notification out first.
176
238
  deferred run's `execute_complete()` calls.
177
239
  - `callback.py`, `spark_forensics_callback(**kwargs)`, an
178
240
  `on_success_callback` factory sharing the same core.
241
+ - `__init__.py`, the public exports and `__version__`.
179
242
 
180
243
  ## Deferrable execution
181
244
 
@@ -185,7 +248,7 @@ worker slot while the CLI runs on the SSH host:
185
248
  ```
186
249
  worker: execute() locate log_ref -> backend.submit() -> defer(trigger_for(job), kwargs)
187
250
  triggerer: SSHRemoteJobTrigger polls exit_code, streams stdout.log
188
- worker: execute_complete(event, job, log_ref, report_dest, thresholds)
251
+ worker: execute_complete(event, job, log_ref, report_dest, thresholds, baseline_log_ref)
189
252
  -> backend.collect() read stderr + report.json, remove the job dir
190
253
  -> handle_report() same persist/notify/threshold code as a sync run
191
254
  ```
@@ -207,8 +270,10 @@ from the DAG file, so nothing set on the instance before `defer()`
207
270
  survives. Everything `execute_complete()` needs travels in the resume
208
271
  kwargs, as JSON-native values that Airflow 2's `BaseSerialization` and
209
272
  Airflow 3's serde both round-trip: the job dict (connection id, CLI
210
- binary, `timeout`, every remote path), the log reference as a dict, the
211
- rendered `report_dest` and the thresholds the job was submitted with. The
273
+ binary, `timeout`, `poll_interval`, the job id, the submission time and
274
+ every remote path), the log reference as a dict (and the baseline's, when
275
+ there is one), the rendered `report_dest` and the thresholds the job was
276
+ submitted with. The
212
277
  job dict and the log reference are built from the rendered hooks, so
213
278
  templated hook arguments cross the deferral as the values the job ran
214
279
  with. The notifier, `on_threshold_breach` and `report_url_template` come
@@ -243,10 +308,11 @@ session ids in `/proc/<pid>/stat`. A pid is only signalled while its
243
308
  command line still names its job directory, so a pid reused after a
244
309
  reboot is left alone.
245
310
 
246
- Failure paths. A trigger error event makes `collect()` stop and remove
247
- the job before raising. A deferral that times out (the backend's
248
- `defer_timeout`, `timeout` plus 120 seconds, or the task's
249
- `execution_timeout`) resumes with `next_method="__fail__"`, which never
311
+ Failure paths. A trigger error event without an exit code makes
312
+ `collect()` stop and remove the job before raising. A deferral that times
313
+ out (the backend's `defer_timeout`, `timeout` per CLI run in the job, so
314
+ twice with `markdown_dest`, plus 120 seconds counted from submission, or
315
+ the task's `execution_timeout`) resumes with `next_method="__fail__"`, which never
250
316
  reaches `execute_complete()`; `resume_execution()` is overridden to call
251
317
  `backend.abandon(context, job)` first, with the job from XCom. Airflow 2
252
318
  takes a different path when `execution_timeout` ran out during the
@@ -256,7 +322,8 @@ anything on the resumed operator, then calls `on_kill()`, which is why
256
322
  between `submit()` and `defer()` (building the trigger, the timeout, the
257
323
  deferral itself) abandons the job before the error propagates, and a
258
324
  task timeout or kill there reaches `on_kill()`, which does the same. The
259
- runbook's "Deferred runs" section lists the behaviour for each case.
325
+ "Deferred runs" section of `docs/troubleshooting.md` lists the behaviour
326
+ for each case.
260
327
 
261
328
  Killing a task. `SparkForensicsOperator.on_kill()` uses the context of the
262
329
  `execute()` or `execute_complete()` running in the same process, or,
@@ -306,7 +373,7 @@ synchronous.
306
373
  Hook arguments are Jinja templates, rendered by Airflow's own nested
307
374
  template-field mechanism, the same one used for any object assigned to a
308
375
  templated operator field. `SparkForensicsOperator.template_fields` is
309
- `("report_dest", "log_source", "backend")`, and each hook class declares
376
+ `("report_dest", "log_source", "backend", "baseline_log_source")`, and each hook class declares
310
377
  the attributes it templates in its own `template_fields`; Airflow renders
311
378
  those in place, on Airflow 2 and 3 alike. There is one mechanism, and no
312
379
  hook formats a string itself. The earlier `{run_id}`-style `str.format`
@@ -316,7 +383,7 @@ one is rejected with an error naming the Jinja replacement.
316
383
  Three details keep this safe.
317
384
 
318
385
  - `render_template_fields()` gives the task its own shallow copy of
319
- `log_source` and `backend` before rendering, because Airflow renders in
386
+ `log_source`, `baseline_log_source` and `backend` before rendering, because Airflow renders in
320
387
  place and the same hook instance is often shared by several tasks or by
321
388
  every mapped instance.
322
389
  - The log-source method is `locate()`, not `resolve()`. Airflow 3's
@@ -335,7 +402,8 @@ Three details keep this safe.
335
402
  when Airflow renders the task. `spark_forensics_callback` runs outside
336
403
  any template rendering, so it renders `report_dest`, `log_source` and
337
404
  `backend` itself, on copies, with the upstream task's Jinja environment.
338
- Both render through a copy of the task with an empty `template_ext`, so a
405
+ Both render through the task, or a copy of it with an empty
406
+ `template_ext` when it has one, so a
339
407
  value ending in `.json` is rendered as a string, not looked up as a
340
408
  template file on an EMR or Spark-on-Kubernetes upstream.
341
409
 
@@ -345,7 +413,15 @@ Before notifying and before any threshold breach raises,
345
413
  `handle_report()` pushes a summary under the `sparkforensics_summary`
346
414
  XCom key: the schema version, the destination, the report URL, the
347
415
  impact-band counts, whether the run violated a threshold, the breached
348
- and inconclusive threshold names, and the CLI exit code. Downstream tasks
416
+ and inconclusive threshold names, the CLI exit code, the CLI version
417
+ that wrote the report, and the run's verdict title, application name,
418
+ wall-clock time and skipped line count (`Report.verdict_title`,
419
+ `app_name`, `run_time_ms` and `skipped_lines`, read from the CLI's
420
+ summary). A run compared against a baseline adds a `comparison` entry: the direction the CLI
421
+ computed for one metric (`regression_metric`, wall-clock by default) as
422
+ the verdict, the comparison's confidence and reason, and that metric's
423
+ values and change in percent, taken from the CLI's comparison section.
424
+ The full section is persisted with the report. Downstream tasks
349
425
  can branch on it without reading the report. It is built from the same
350
426
  `Report` on both the synchronous and the deferred path, and
351
427
  `return_value` stays the destination.
@@ -358,7 +434,7 @@ presigns anything: the URL points at a place the viewer's own
358
434
  credentials open. `ReportLink` shows that URL when it is set and the raw
359
435
  destination when it is not.
360
436
 
361
- ## Deferred
437
+ ## Not implemented
362
438
 
363
439
  **http/MCP AnalyzeHook backend.** The spec originally described an "http"
364
440
  backend calling a deployed sparkforensics `server/` instance. `server/`
@@ -7,6 +7,73 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.4.0] - 2026-11-04
11
+
12
+ ### Upgrade notes
13
+
14
+ - `sparkforensics-cli` 0.10.2 counts every failed task attempt in `max_failed_task_rate_pct`, so a job with retried tasks can fail where it passed.
15
+ - With 0.10.2, a log with skipped lines gives exit 3 and an inconclusive `parse-incomplete` result. The task does not fail; see `skipped_lines` in the summary XCom.
16
+ - The operator is tested against `sparkforensics-cli` 0.12.0.
17
+ - With CLI 0.12.0 and newer, `findings` lists critical and warning findings only. The persisted report, `report.findings` and the `findings` XCom omit info findings; `notesHidden` in the report counts them. The summary's `impact_band_counts` is unchanged.
18
+
19
+ ### Added
20
+
21
+ - `markdown_dest` saves the CLI's Markdown report (`--format md`) next to the JSON report. The analysis runs twice.
22
+ - `redact=True` passes `--redact`.
23
+ - `normalize_path`, `ignore_conf` and `noise_band_pct` tune the baseline comparison.
24
+ - `regression_budgets`, `stage_regression_budgets` and `stage_qualities` set several regression budgets at once.
25
+ - A Spark History Server application can be the baseline on `SubprocessAnalyzeHook` and `SSHAnalyzeHook`.
26
+ - The summary XCom has `verdict`, `app_name`, `run_time_ms`, `skipped_lines` and `cli_version`. The default notification names the application, the verdict and the run time.
27
+ - A warning is logged when the CLI is older than the tested version.
28
+ - `CliError`, `CliConfigurationError` and `HistoryServerFetchError` in `sparkforensics_operator.exceptions`.
29
+
30
+ ### Changed
31
+
32
+ - The operator reads the JSON output shape of `sparkforensics-cli` 0.12.0 (`{schemaVersion, run, comparison, budgets}`) and still reads the older shapes.
33
+ - Each CLI exit code without a report (2, 4, 5, 6) has its own error message. The task still fails.
34
+ - Airflow does not retry usage errors, and retries History Server failures that can clear.
35
+ - A report with a newer `schemaVersion` than the operator reads fails the task.
36
+ - The saved report is the CLI's whole JSON report. `Report.raw` holds it. It includes `effectiveConf`, where the CLI masks secret-looking keys and values; see [Report destinations](docs/configuration.md#report-destinations).
37
+ - `Report.threshold_results` keeps every result of a threshold the CLI reports on several lines.
38
+
39
+ ### Fixed
40
+
41
+ - Thresholds such as `0.00001` or `1e16` reach the CLI as plain decimals, not exponent form, which the CLI rejects.
42
+
43
+ ## [0.3.0] - 2026-09-28
44
+
45
+ ### Added
46
+
47
+ - Run comparison: `baseline_log_source` on `SparkForensicsOperator` and
48
+ `spark_forensics_callback` resolves an earlier run's event log, the same
49
+ way `log_source` resolves the run under analysis, and passes it to
50
+ `sparkforensics-analyze --baseline`. It must resolve to a file path
51
+ readable where the analysis runs: a `LocalEventLog` for
52
+ `SubprocessAnalyzeHook`, a `RemoteEventLog` on the same SSH connection
53
+ for `SSHAnalyzeHook`. A History Server application is rejected, since
54
+ the CLI reads a baseline only from a path. A baseline that is, contains
55
+ or sits inside the log under analysis fails the task before analysis.
56
+ Works with `deferrable=True`.
57
+ - Comparison thresholds `max_regression_pct`, `regression_metric` and
58
+ `fail_on_introduced`, mirroring the CLI's flags and its rules: all need
59
+ `baseline_log_source`, and `regression_metric` needs
60
+ `max_regression_pct`. A breach raises `ThresholdBreached` like any other
61
+ threshold.
62
+ - The `sparkforensics_summary` XCom gains a `comparison` key when a
63
+ baseline ran: the checked metric's verdict, the comparison's confidence
64
+ and reason, and the metric's baseline and candidate values and change in
65
+ percent. The persisted report carries the CLI's full comparison section.
66
+
67
+ ### Changed
68
+
69
+ - The operator reads report schema 5, where a finding's text figure is in
70
+ `valueText` and its `value` is always a number or `null`. A report with
71
+ an older `schemaVersion` fails the task.
72
+ - The persisted report and `Report` carry the CLI's `verdict` and
73
+ `notRunChecks` sections (`Report.verdict`, `Report.not_run_checks`).
74
+ Before this, the operator persisted only the sections it listed, so
75
+ these two would have been dropped.
76
+
10
77
  ## [0.2.0] - 2026-09-25
11
78
 
12
79
  ### Added
@@ -154,7 +221,9 @@ Initial release.
154
221
  - Generic `Notifier` interface for threshold-breach alerting.
155
222
  - PyPI trusted-publishing GitHub Actions workflow (OIDC, no stored token).
156
223
 
157
- [Unreleased]: https://github.com/shuffle-works/sparkforensics-operator/compare/v0.2.0...HEAD
224
+ [Unreleased]: https://github.com/shuffle-works/sparkforensics-operator/compare/v0.4.0...HEAD
225
+ [0.4.0]: https://github.com/shuffle-works/sparkforensics-operator/releases/tag/v0.4.0
226
+ [0.3.0]: https://github.com/shuffle-works/sparkforensics-operator/releases/tag/v0.3.0
158
227
  [0.2.0]: https://github.com/shuffle-works/sparkforensics-operator/releases/tag/v0.2.0
159
228
  [0.1.1]: https://github.com/shuffle-works/sparkforensics-operator/releases/tag/v0.1.1
160
229
  [0.1.0]: https://github.com/shuffle-works/sparkforensics-operator/releases/tag/v0.1.0
@@ -9,15 +9,15 @@ Python 3.9+, apache-airflow 2.6+ or 3.0+, pytest. Runtime dependency: Node.js
9
9
  18+ + the `sparkforensics-cli` npm package (`npm install -g sparkforensics-cli`)
10
10
  wherever the analysis runs: the worker for `SubprocessAnalyzeHook`, the SSH
11
11
  host for `SSHAnalyzeHook`. Log sources resolve an `EventLogRef` (`log_ref.py`)
12
- that backends consume; see `docs/architecture.md`.
12
+ that backends consume; see `ARCHITECTURE.md`.
13
13
 
14
14
  ## Commands
15
15
  - `pip install -e ".[test,s3,ssh]"`, install for local dev.
16
- - `pytest`, run the test suite (mocks everything: no live Spark/Airflow/Node
17
- needed for any single test).
16
+ - `pytest`, run the test suite (mocks everything except `tests/integration`,
17
+ which runs the real CLI on public logs and skips without it).
18
18
  - `tox -e py311-airflow2` / `tox -e py311-airflow3`, run the suite against a
19
19
  specific Airflow major version.
20
20
 
21
- ## Design docs
22
- - `docs/architecture.md`, `docs/api-reference.md`, `docs/runbook.md`,
23
- `docs/glossary.md`, see below.
21
+ ## Docs
22
+ - User docs: `docs/index.md` (getting started, configuration,
23
+ troubleshooting). Contributor docs: `CONTRIBUTING.md`, `ARCHITECTURE.md`.
@@ -0,0 +1,100 @@
1
+ # Contributing
2
+
3
+ Design notes and the component map are in [ARCHITECTURE.md](ARCHITECTURE.md).
4
+ User documentation lives under [docs/](docs/index.md).
5
+
6
+ ## Development install
7
+
8
+ ```bash
9
+ pip install -e ".[test,s3,ssh]"
10
+ ```
11
+
12
+ ## Running the tests
13
+
14
+ ```bash
15
+ pytest
16
+ ```
17
+
18
+ The suite needs no live Spark or Airflow, and no Node.js apart from
19
+ `tests/integration` (below). The one real process is a shell: the deferred-mode tests run the SSH provider's real shell wrapper against a local `sh` standing in
20
+ for the SSH host (`tests/hooks/analyze/_local_ssh.py`), so they need
21
+ `bash`, `setsid` and coreutils `timeout` on the machine running the tests.
22
+ The kill and sweep tests also run with procps hidden
23
+ (`LocalHost.hide_procps()`), as on slim images.
24
+
25
+ ### Tests against the real CLI
26
+
27
+ The other tests simulate `sparkforensics-analyze`, so they cannot catch the
28
+ CLI changing an exit code or a flag's rules. `tests/integration` runs the
29
+ real CLI on public logs from the
30
+ [spark-event-corpus-data](https://github.com/shuffle-works/spark-event-corpus-data)
31
+ repository (fetched at a pinned tag), through `SubprocessAnalyzeHook`. It
32
+ covers a clean log, a violation, an unreadable log, an unreadable baseline
33
+ and budgets that print in exponent form.
34
+
35
+ ```bash
36
+ npm install -g sparkforensics-cli
37
+ pytest tests/integration
38
+ ```
39
+
40
+ They need Node.js 18+, the `sparkforensics-cli` package on `PATH` (or
41
+ its binary at `SPARKFORENSICS_ANALYZE_BIN`) and network access to GitHub, and
42
+ skip when the CLI or the network is missing. With `SPARKFORENSICS_REQUIRE_CLI=1`
43
+ a missing CLI or an unreachable corpus fails the run instead, as in CI. The
44
+ `cli-integration` job in `.github/workflows/ci.yml` runs them against the
45
+ release the operator is tested against (`TESTED_CLI_VERSION` in
46
+ `report.py`) and the newest one.
47
+
48
+ ## The tox Airflow matrix
49
+
50
+ `tox.ini` runs the suite against each supported Airflow major version:
51
+
52
+ ```bash
53
+ tox -e py311-airflow2
54
+ tox -e py311-airflow3
55
+ ```
56
+
57
+ The full env list is `py{39,310,311,312}-airflow2`,
58
+ `py{310,311,312}-airflow3`, and three floor envs:
59
+
60
+ - `py39-airflow2min` pins Airflow 2.6.3 without the `ssh` extra, and
61
+ omits the SSH-only modules from its coverage (`coverage-no-ssh.ini`).
62
+ - `py39-airflow2sshmin` runs the `ssh` extra's floor
63
+ (`apache-airflow-providers-ssh` 3.7.1) on Airflow 2.6.3. The deferrable
64
+ tests skip there, since that mode needs a newer provider.
65
+ - `py311-airflow2deferrablemin` pins the deferrable mode's floors
66
+ (`constraints-deferrable-floor.txt`).
67
+
68
+ The two Airflow 2.6.3 envs install through
69
+ `constraints-airflow-2.6-floor.txt` so the extras step can't upgrade
70
+ Airflow past the floor; `tox.ini` explains why.
71
+
72
+ CI (`.github/workflows/ci.yml`) runs a subset of these envs on every push
73
+ to `main` and every pull request. Each env writes coverage to
74
+ `coverage-<envname>.lcov`; CI uploads one file per env to Coveralls and
75
+ merges them in a `finish` job. `usedevelop = true` in `tox.ini` keeps the
76
+ lcov `SF:` paths pointing at `src/sparkforensics_operator/...` instead of
77
+ a `.tox` virtualenv, which Coveralls needs to attribute lines.
78
+
79
+ ## Releasing to PyPI
80
+
81
+ `.github/workflows/publish.yml` builds the sdist and wheel and publishes
82
+ them on every GitHub Release (`release: published`). It uses PyPI trusted
83
+ publishing (OIDC), so no API token is stored in the repository.
84
+
85
+ One-time setup before the first release:
86
+
87
+ - On PyPI, add a trusted publisher for this project: owner
88
+ `shuffle-works`, repo `sparkforensics-operator`, workflow `publish.yml`,
89
+ environment `pypi`.
90
+ - In the GitHub repository settings, create an environment named `pypi`
91
+ (optionally with required reviewers) to match.
92
+
93
+ To cut a release:
94
+
95
+ 1. Bump `version` in `pyproject.toml` and move the `[Unreleased]` entries
96
+ in `CHANGELOG.md` under the new version. The Airflow provider metadata
97
+ reads the version from the installed package, so nothing else needs
98
+ changing.
99
+ 2. Tag the release commit (for example `v0.2.0`).
100
+ 3. Publish a GitHub Release from that tag. The workflow does the rest.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: sparkforensics-operator
3
- Version: 0.2.0
3
+ Version: 0.4.0
4
4
  Summary: Airflow operator that runs sparkforensics analysis on a Spark job's event log and acts on the result.
5
5
  Project-URL: Homepage, https://github.com/shuffle-works/sparkforensics-operator
6
6
  Project-URL: Repository, https://github.com/shuffle-works/sparkforensics-operator
@@ -71,6 +71,17 @@ runs as part of the DAG, right where the Spark task just ran.
71
71
  you configure are enforced, and a breach raises `ThresholdBreached`
72
72
  without consuming the task's retries, since re-running would just reach
73
73
  the same verdict.
74
+ - Compare a run against an earlier one. `baseline_log_source` takes any
75
+ log source that resolves to an event log path the analysis can read
76
+ (`path_template="/spark-logs/{{ prev_ds }}/eventlog"`, an XCom pull) or
77
+ to a Spark History Server application. `max_regression_pct` fails the
78
+ DAG when runtime, or the metric `regression_metric` names, regressed past
79
+ that budget. `regression_budgets` budgets several metrics at once, and
80
+ `stage_regression_budgets` the paired stages. `fail_on_introduced` fails
81
+ it when the run gained new findings. The comparison's verdict,
82
+ confidence and measured regression land in the summary XCom.
83
+ - `markdown_dest` also persists the CLI's Markdown report, for a notifier
84
+ to post or a person to read.
74
85
  - Reports persist to a local path, `file://`, or `s3://`, and a clickable
75
86
  "SparkForensics report" link shows up on the task in the Airflow UI. Set
76
87
  `report_url_template` (an S3 console URL, say) to make it open in a
@@ -85,8 +96,8 @@ runs as part of the DAG, right where the Spark task just ran.
85
96
  - An optional `Notifier` you implement (Slack, MS Teams, email, PagerDuty,
86
97
  ZenDuty, whatever you use) gets a best-effort pass/fail summary; a
87
98
  delivery failure never fails the task itself.
88
- - Tested against both Airflow 2.6+ and Airflow 3.0+ (CI matrix, Python
89
- 3.9-3.12). No live Spark, Airflow or Node is needed to run the tests:
99
+ - Tested against both Airflow 2.6+ and Airflow 3 (CI runs Python 3.9,
100
+ 3.11 and 3.12; the tox matrix also covers 3.10). No live Spark, Airflow or Node is needed to run the tests:
90
101
  everything but the shell on the stand-in SSH host is mocked.
91
102
 
92
103
  ## Quick start
@@ -162,16 +173,17 @@ the analysis runs: the CLI runs as a detached job on the SSH host and the
162
173
  triggerer polls it, which needs a running triggerer with
163
174
  `sparkforensics-operator[ssh]` installed and
164
175
  `apache-airflow-providers-ssh>=6.0.1`, so Airflow 2.11 or newer. See
165
- [Deferrable execution](https://github.com/shuffle-works/sparkforensics-operator/blob/main/docs/api-reference.md#deferrable-execution).
176
+ [Deferrable execution](https://github.com/shuffle-works/sparkforensics-operator/blob/main/docs/configuration.md#deferrable-execution).
166
177
 
167
178
  Whichever host runs the analysis (the worker for `SubprocessAnalyzeHook`,
168
179
  the SSH host for `SSHAnalyzeHook`) needs Node.js 18+ and the
169
- `sparkforensics-cli` npm package (`npm install -g sparkforensics-cli`), so
180
+ `sparkforensics-cli` npm package, version 0.4.0 or newer
181
+ (`npm install -g "sparkforensics-cli@>=0.4.0"`), so
170
182
  `sparkforensics-analyze` is resolvable on `PATH`. Add the `ssh` extra for
171
183
  the SSH hooks (it works from Airflow 2.6, like the package; only
172
184
  `deferrable=True` needs Airflow 2.11+), and the `s3` extra if
173
185
  `report_dest` is `s3://...`.
174
- Full details in [`docs/runbook.md`](https://github.com/shuffle-works/sparkforensics-operator/blob/main/docs/runbook.md).
186
+ Full details in [Getting started](https://github.com/shuffle-works/sparkforensics-operator/blob/main/docs/getting-started.md).
175
187
 
176
188
  ## Development
177
189
 
@@ -189,11 +201,11 @@ tox -e py311-airflow3
189
201
 
190
202
  ## Learn more
191
203
 
192
- - [Architecture](https://github.com/shuffle-works/sparkforensics-operator/blob/main/docs/architecture.md), components and design decisions
193
- - [API reference](https://github.com/shuffle-works/sparkforensics-operator/blob/main/docs/api-reference.md), the full operator/hook/threshold
194
- surface
195
- - [Runbook](https://github.com/shuffle-works/sparkforensics-operator/blob/main/docs/runbook.md), worker and SSH host prerequisites, troubleshooting
196
- - [Glossary](https://github.com/shuffle-works/sparkforensics-operator/blob/main/docs/glossary.md), domain terms
204
+ - [Getting started](https://github.com/shuffle-works/sparkforensics-operator/blob/main/docs/getting-started.md), install and host prerequisites, and DAG wiring examples
205
+ - [Configuration](https://github.com/shuffle-works/sparkforensics-operator/blob/main/docs/configuration.md), the full operator/hook/threshold surface
206
+ - [Troubleshooting](https://github.com/shuffle-works/sparkforensics-operator/blob/main/docs/troubleshooting.md), error messages and deferred-run failure modes
207
+ - [Contributing](https://github.com/shuffle-works/sparkforensics-operator/blob/main/CONTRIBUTING.md), development setup and the release procedure
208
+ - [Architecture](https://github.com/shuffle-works/sparkforensics-operator/blob/main/ARCHITECTURE.md), components and design decisions
197
209
 
198
210
  ## License
199
211