sparkforensics-operator 0.2.0__tar.gz → 0.3.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 (102) hide show
  1. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.3.0}/AGENTS.md +6 -5
  2. sparkforensics_operator-0.2.0/docs/architecture.md → sparkforensics_operator-0.3.0/ARCHITECTURE.md +60 -26
  3. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.3.0}/CHANGELOG.md +36 -1
  4. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.3.0}/CLAUDE.md +4 -4
  5. sparkforensics_operator-0.3.0/CONTRIBUTING.md +77 -0
  6. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.3.0}/PKG-INFO +20 -11
  7. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.3.0}/README.md +19 -10
  8. sparkforensics_operator-0.3.0/docs/configuration.md +620 -0
  9. sparkforensics_operator-0.3.0/docs/getting-started.md +227 -0
  10. sparkforensics_operator-0.3.0/docs/index.md +28 -0
  11. sparkforensics_operator-0.3.0/docs/troubleshooting.md +406 -0
  12. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.3.0}/pyproject.toml +1 -1
  13. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.3.0}/src/sparkforensics_operator/callback.py +16 -1
  14. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.3.0}/src/sparkforensics_operator/hooks/analyze/_cli.py +14 -1
  15. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.3.0}/src/sparkforensics_operator/hooks/analyze/_remote_job.py +16 -3
  16. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.3.0}/src/sparkforensics_operator/hooks/analyze/base.py +69 -9
  17. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.3.0}/src/sparkforensics_operator/hooks/analyze/ssh.py +28 -6
  18. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.3.0}/src/sparkforensics_operator/hooks/analyze/subprocess.py +11 -3
  19. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.3.0}/src/sparkforensics_operator/operator.py +155 -24
  20. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.3.0}/src/sparkforensics_operator/report.py +43 -6
  21. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.3.0}/src/sparkforensics_operator/summary.py +44 -2
  22. sparkforensics_operator-0.3.0/tests/fixtures/sparkforensics_cli_0_4_0/README.md +17 -0
  23. sparkforensics_operator-0.3.0/tests/fixtures/sparkforensics_cli_0_4_0/comparison.json +1064 -0
  24. sparkforensics_operator-0.3.0/tests/fixtures/sparkforensics_cli_0_4_0/comparison.stderr +1 -0
  25. sparkforensics_operator-0.3.0/tests/fixtures/sparkforensics_cli_0_4_0/incomplete-run.json +691 -0
  26. sparkforensics_operator-0.3.0/tests/fixtures/sparkforensics_cli_0_4_0/incomplete-run.stderr +1 -0
  27. sparkforensics_operator-0.3.0/tests/fixtures/sparkforensics_cli_0_4_0/report.json +1209 -0
  28. sparkforensics_operator-0.3.0/tests/fixtures/sparkforensics_cli_0_4_0/report.stderr +2 -0
  29. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.3.0}/tests/hooks/analyze/_local_ssh.py +8 -1
  30. sparkforensics_operator-0.3.0/tests/hooks/analyze/test_base.py +108 -0
  31. sparkforensics_operator-0.3.0/tests/hooks/analyze/test_cli.py +49 -0
  32. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.3.0}/tests/hooks/analyze/test_deferrable_parity.py +64 -9
  33. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.3.0}/tests/hooks/analyze/test_ssh.py +40 -4
  34. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.3.0}/tests/hooks/analyze/test_ssh_deferrable.py +17 -1
  35. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.3.0}/tests/hooks/analyze/test_subprocess.py +41 -3
  36. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.3.0}/tests/test_callback.py +16 -0
  37. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.3.0}/tests/test_operator.py +228 -0
  38. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.3.0}/tests/test_operator_deferrable.py +92 -0
  39. sparkforensics_operator-0.3.0/tests/test_report.py +180 -0
  40. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.3.0}/tests/test_summary.py +81 -0
  41. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.3.0}/tests/test_templating.py +31 -0
  42. sparkforensics_operator-0.2.0/docs/api-reference.md +0 -396
  43. sparkforensics_operator-0.2.0/docs/glossary.md +0 -58
  44. sparkforensics_operator-0.2.0/docs/runbook.md +0 -374
  45. sparkforensics_operator-0.2.0/tests/hooks/analyze/test_base.py +0 -38
  46. sparkforensics_operator-0.2.0/tests/hooks/analyze/test_cli.py +0 -17
  47. sparkforensics_operator-0.2.0/tests/test_report.py +0 -128
  48. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.3.0}/.github/workflows/ci.yml +0 -0
  49. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.3.0}/.github/workflows/publish.yml +0 -0
  50. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.3.0}/.gitignore +0 -0
  51. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.3.0}/LICENSE +0 -0
  52. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.3.0}/constraints-airflow-2.6-floor.txt +0 -0
  53. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.3.0}/constraints-deferrable-floor.txt +0 -0
  54. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.3.0}/coverage-no-ssh.ini +0 -0
  55. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.3.0}/src/sparkforensics_operator/__init__.py +0 -0
  56. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.3.0}/src/sparkforensics_operator/_compat.py +0 -0
  57. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.3.0}/src/sparkforensics_operator/exceptions.py +0 -0
  58. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.3.0}/src/sparkforensics_operator/get_provider_info.py +0 -0
  59. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.3.0}/src/sparkforensics_operator/hooks/__init__.py +0 -0
  60. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.3.0}/src/sparkforensics_operator/hooks/_templating.py +0 -0
  61. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.3.0}/src/sparkforensics_operator/hooks/analyze/__init__.py +0 -0
  62. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.3.0}/src/sparkforensics_operator/hooks/log_source/__init__.py +0 -0
  63. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.3.0}/src/sparkforensics_operator/hooks/log_source/_dest_root.py +0 -0
  64. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.3.0}/src/sparkforensics_operator/hooks/log_source/_history_server_args.py +0 -0
  65. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.3.0}/src/sparkforensics_operator/hooks/log_source/_path_template.py +0 -0
  66. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.3.0}/src/sparkforensics_operator/hooks/log_source/_rolling_log.py +0 -0
  67. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.3.0}/src/sparkforensics_operator/hooks/log_source/base.py +0 -0
  68. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.3.0}/src/sparkforensics_operator/hooks/log_source/filesystem.py +0 -0
  69. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.3.0}/src/sparkforensics_operator/hooks/log_source/history_server.py +0 -0
  70. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.3.0}/src/sparkforensics_operator/hooks/log_source/history_server_app.py +0 -0
  71. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.3.0}/src/sparkforensics_operator/hooks/log_source/remote_path.py +0 -0
  72. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.3.0}/src/sparkforensics_operator/hooks/log_source/sftp.py +0 -0
  73. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.3.0}/src/sparkforensics_operator/hooks/log_source/tunnel.py +0 -0
  74. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.3.0}/src/sparkforensics_operator/hooks/log_source/xcom.py +0 -0
  75. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.3.0}/src/sparkforensics_operator/links.py +0 -0
  76. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.3.0}/src/sparkforensics_operator/log_ref.py +0 -0
  77. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.3.0}/src/sparkforensics_operator/notify.py +0 -0
  78. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.3.0}/src/sparkforensics_operator/sinks.py +0 -0
  79. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.3.0}/tests/__init__.py +0 -0
  80. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.3.0}/tests/_render.py +0 -0
  81. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.3.0}/tests/hooks/__init__.py +0 -0
  82. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.3.0}/tests/hooks/analyze/__init__.py +0 -0
  83. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.3.0}/tests/hooks/log_source/__init__.py +0 -0
  84. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.3.0}/tests/hooks/log_source/test_base.py +0 -0
  85. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.3.0}/tests/hooks/log_source/test_filesystem.py +0 -0
  86. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.3.0}/tests/hooks/log_source/test_history_server.py +0 -0
  87. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.3.0}/tests/hooks/log_source/test_history_server_app.py +0 -0
  88. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.3.0}/tests/hooks/log_source/test_remote_path.py +0 -0
  89. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.3.0}/tests/hooks/log_source/test_sftp.py +0 -0
  90. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.3.0}/tests/hooks/log_source/test_tunnel.py +0 -0
  91. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.3.0}/tests/hooks/log_source/test_xcom.py +0 -0
  92. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.3.0}/tests/test_compat.py +0 -0
  93. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.3.0}/tests/test_dagbag_integration.py +0 -0
  94. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.3.0}/tests/test_exceptions.py +0 -0
  95. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.3.0}/tests/test_links.py +0 -0
  96. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.3.0}/tests/test_log_ref.py +0 -0
  97. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.3.0}/tests/test_notify.py +0 -0
  98. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.3.0}/tests/test_package_exports.py +0 -0
  99. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.3.0}/tests/test_provider_info.py +0 -0
  100. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.3.0}/tests/test_serialization.py +0 -0
  101. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.3.0}/tests/test_sinks.py +0 -0
  102. {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.3.0}/tox.ini +0 -0
@@ -9,8 +9,8 @@ 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
@@ -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,22 @@ 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`. The CLI
38
+ reads a baseline only from a path, never from a History Server, so each
39
+ backend declares the kinds it can pass that way
40
+ (`AnalyzeHook.supported_baseline_refs`: `LocalEventLog` on the worker,
41
+ `RemoteEventLog` on the SSH host) and `analyze()` or the deferrable
42
+ submit step rejects any other before running anything. The comparison
43
+ itself, and the budgets on it, are the CLI's. Which run is the baseline
44
+ is the DAG author's choice, made through that log source: the operator
45
+ keeps no state across DAG runs.
46
+
47
+ Both trigger shapes (the standalone `SparkForensicsOperator` and the
48
+ `spark_forensics_callback` `on_success_callback` factory) converge on one function,
35
49
  `operator.run_spark_forensics(context, ...)`, so there is exactly one
36
50
  execute-analyze-persist-notify-threshold code path to maintain. Notify
37
51
  always runs before the threshold check's fail/warn/ignore branching, so a
@@ -39,19 +53,24 @@ breach that raises still gets a notification out first.
39
53
 
40
54
  ## Components
41
55
 
42
- - `_compat.py`, the single place this package imports Airflow's public
43
- API from. `BaseOperator`/`BaseOperatorLink` live in `airflow.sdk` on
56
+ - `_compat.py`, where this package imports Airflow's core API from (the
57
+ one exception is `links.py`'s lazy `XCom` import on the Airflow 2
58
+ branch; provider hooks are imported where they are used). `BaseOperator`/`BaseOperatorLink` live in `airflow.sdk` on
44
59
  3.x and `airflow.models.*` on 2.x. The Task SDK also re-homed `BaseHook`
45
60
  (3.1), and the exceptions, `TaskDeferred` and `conf` (3.2); their old
46
61
  paths still work there, as the same objects, behind deprecation shims.
47
62
  `_compat.py` tries the SDK path first and falls back to the old one, and
48
63
  `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
64
+ interpreter and fails if any import fails or emits a
65
+ `DeprecationWarning`, `PendingDeprecationWarning` or `FutureWarning`
66
+ from inside the package. The repo has
50
67
  no ruff configuration, so there are no ruff `AIR3` rules to enable; that
51
68
  test covers the same ground.
52
69
  - `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.
70
+ an empty `template_fields` and a `__repr__` that shows them. Also
71
+ `task_renderer()` and `render_with_task_env()`, which render a value
72
+ with a task's Jinja environment for the callback and the tunnel hook.
73
+ See "Templated hook arguments" below.
55
74
  - `log_ref.py`, the `EventLogRef` kinds (`LocalEventLog`,
56
75
  `RemoteEventLog`, `HistoryServerApp`): frozen dataclasses saying where a
57
76
  log is, the only thing a log source and a backend share.
@@ -68,8 +87,9 @@ breach that raises still gets a notification out first.
68
87
  cleaning up after itself in that case; currently `HistoryServerLogSourceHook`
69
88
  and `SFTPLogSourceHook` do this, via their own internal try/except
70
89
  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.
90
+ exception. See the "Disk fills up on a worker" entry in
91
+ `docs/troubleshooting.md` for which hook/config combinations actually
92
+ clean up.
73
93
  - `hooks/log_source/{sftp,tunnel}.py`, `SFTPLogSourceHook` and
74
94
  `SSHTunneledLogSourceHook`, for on-prem log sources reachable only over
75
95
  the SSH connection an `SSHOperator` already has configured (no cloud
@@ -89,22 +109,23 @@ breach that raises still gets a notification out first.
89
109
  `HistoryServerApp` and do no I/O. Per-run values come from Jinja, the
90
110
  same way as in the fetching hooks (see "Templated hook arguments").
91
111
  `hooks/log_source/_path_template.py` checks a rendered path (rendered,
92
- non-empty, no `..` segment) and `_history_server_args.py` checks a
112
+ non-empty, no `..` segment, no old `{run_id}`-style placeholder) and `_history_server_args.py` checks a
93
113
  rendered app id and attempt id; `_rolling_log.py` holds the
94
114
  rolling-log-entry-matching logic shared between `history_server.py`
95
115
  and `sftp.py`. `_dest_root.py` holds the `dest_root/<basename>` staging
96
116
  convention shared by `filesystem.py`/`sftp.py`, and the owned-temp-dir
97
117
  bookkeeping (create if `dest_dir` is unset, `rmtree` on cleanup/error if
98
118
  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
119
+ - `hooks/analyze/base.py`, `AnalyzeHook`: `analyze(log_ref, thresholds,
120
+ baseline_ref=None)` checks the reference against `supported_log_refs`
121
+ (`check_log_ref()`) and any baseline against `supported_baseline_refs`
122
+ (`check_baseline_ref()`), then calls the backend's `_analyze()`. `DeferrableAnalyzeHook` adds the
102
123
  detached-job interface the deferrable mode drives (`submit`,
103
124
  `trigger_for`, `defer_timeout`, `collect`, `abandon`); see "Deferrable
104
125
  execution" below.
105
126
  - `hooks/analyze/_cli.py`, the CLI contract every backend shares: argument
106
127
  building for each reference kind (a positional path, or
107
- `--shs-base-url`/`--app-id`/`--attempt-id`), threshold flags, exit-code
128
+ `--shs-base-url`/`--app-id`/`--attempt-id`), `--baseline`, threshold flags, exit-code
108
129
  handling (0/1/3 usable, 2 and anything else raise), and building the
109
130
  `Report` from the JSON report and stderr threshold lines. Backends only
110
131
  differ in how they run the command and collect its output.
@@ -139,6 +160,8 @@ breach that raises still gets a notification out first.
139
160
  threshold-evaluation *logic* lives here: sparkforensics's own CLI decides
140
161
  pass/violation/inconclusive, this module only parses what it printed, so
141
162
  it can never drift from what the CLI actually enforces.
163
+ - `exceptions.py`, `ThresholdBreached`, an `AirflowFailException` so a
164
+ breach fails the task without consuming its retries.
142
165
  - `sinks.py`, persists the report JSON (local path, `file://`, `s3://`).
143
166
  - `notify.py`, the `Notifier` base class: subclasses implement `_send()`
144
167
  for whatever messaging/paging system they target, and `notify()` makes
@@ -176,6 +199,7 @@ breach that raises still gets a notification out first.
176
199
  deferred run's `execute_complete()` calls.
177
200
  - `callback.py`, `spark_forensics_callback(**kwargs)`, an
178
201
  `on_success_callback` factory sharing the same core.
202
+ - `__init__.py`, the public exports and `__version__`.
179
203
 
180
204
  ## Deferrable execution
181
205
 
@@ -185,7 +209,7 @@ worker slot while the CLI runs on the SSH host:
185
209
  ```
186
210
  worker: execute() locate log_ref -> backend.submit() -> defer(trigger_for(job), kwargs)
187
211
  triggerer: SSHRemoteJobTrigger polls exit_code, streams stdout.log
188
- worker: execute_complete(event, job, log_ref, report_dest, thresholds)
212
+ worker: execute_complete(event, job, log_ref, report_dest, thresholds, baseline_log_ref)
189
213
  -> backend.collect() read stderr + report.json, remove the job dir
190
214
  -> handle_report() same persist/notify/threshold code as a sync run
191
215
  ```
@@ -207,8 +231,10 @@ from the DAG file, so nothing set on the instance before `defer()`
207
231
  survives. Everything `execute_complete()` needs travels in the resume
208
232
  kwargs, as JSON-native values that Airflow 2's `BaseSerialization` and
209
233
  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
234
+ binary, `timeout`, `poll_interval`, the job id, the submission time and
235
+ every remote path), the log reference as a dict (and the baseline's, when
236
+ there is one), the rendered `report_dest` and the thresholds the job was
237
+ submitted with. The
212
238
  job dict and the log reference are built from the rendered hooks, so
213
239
  templated hook arguments cross the deferral as the values the job ran
214
240
  with. The notifier, `on_threshold_breach` and `report_url_template` come
@@ -243,9 +269,10 @@ session ids in `/proc/<pid>/stat`. A pid is only signalled while its
243
269
  command line still names its job directory, so a pid reused after a
244
270
  reboot is left alone.
245
271
 
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
272
+ Failure paths. A trigger error event without an exit code makes
273
+ `collect()` stop and remove the job before raising. A deferral that times
274
+ out (the backend's `defer_timeout`, `timeout` plus 120 seconds counted
275
+ from submission, or the task's
249
276
  `execution_timeout`) resumes with `next_method="__fail__"`, which never
250
277
  reaches `execute_complete()`; `resume_execution()` is overridden to call
251
278
  `backend.abandon(context, job)` first, with the job from XCom. Airflow 2
@@ -256,7 +283,8 @@ anything on the resumed operator, then calls `on_kill()`, which is why
256
283
  between `submit()` and `defer()` (building the trigger, the timeout, the
257
284
  deferral itself) abandons the job before the error propagates, and a
258
285
  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.
286
+ "Deferred runs" section of `docs/troubleshooting.md` lists the behaviour
287
+ for each case.
260
288
 
261
289
  Killing a task. `SparkForensicsOperator.on_kill()` uses the context of the
262
290
  `execute()` or `execute_complete()` running in the same process, or,
@@ -306,7 +334,7 @@ synchronous.
306
334
  Hook arguments are Jinja templates, rendered by Airflow's own nested
307
335
  template-field mechanism, the same one used for any object assigned to a
308
336
  templated operator field. `SparkForensicsOperator.template_fields` is
309
- `("report_dest", "log_source", "backend")`, and each hook class declares
337
+ `("report_dest", "log_source", "backend", "baseline_log_source")`, and each hook class declares
310
338
  the attributes it templates in its own `template_fields`; Airflow renders
311
339
  those in place, on Airflow 2 and 3 alike. There is one mechanism, and no
312
340
  hook formats a string itself. The earlier `{run_id}`-style `str.format`
@@ -316,7 +344,7 @@ one is rejected with an error naming the Jinja replacement.
316
344
  Three details keep this safe.
317
345
 
318
346
  - `render_template_fields()` gives the task its own shallow copy of
319
- `log_source` and `backend` before rendering, because Airflow renders in
347
+ `log_source`, `baseline_log_source` and `backend` before rendering, because Airflow renders in
320
348
  place and the same hook instance is often shared by several tasks or by
321
349
  every mapped instance.
322
350
  - The log-source method is `locate()`, not `resolve()`. Airflow 3's
@@ -335,7 +363,8 @@ Three details keep this safe.
335
363
  when Airflow renders the task. `spark_forensics_callback` runs outside
336
364
  any template rendering, so it renders `report_dest`, `log_source` and
337
365
  `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
366
+ Both render through the task, or a copy of it with an empty
367
+ `template_ext` when it has one, so a
339
368
  value ending in `.json` is rendered as a string, not looked up as a
340
369
  template file on an EMR or Spark-on-Kubernetes upstream.
341
370
 
@@ -345,7 +374,12 @@ Before notifying and before any threshold breach raises,
345
374
  `handle_report()` pushes a summary under the `sparkforensics_summary`
346
375
  XCom key: the schema version, the destination, the report URL, the
347
376
  impact-band counts, whether the run violated a threshold, the breached
348
- and inconclusive threshold names, and the CLI exit code. Downstream tasks
377
+ and inconclusive threshold names, and the CLI exit code. A run compared
378
+ against a baseline adds a `comparison` entry: the direction the CLI
379
+ computed for one metric (`regression_metric`, wall-clock by default) as
380
+ the verdict, the comparison's confidence and reason, and that metric's
381
+ values and change in percent, taken from the CLI's comparison section.
382
+ The full section is persisted with the report. Downstream tasks
349
383
  can branch on it without reading the report. It is built from the same
350
384
  `Report` on both the synchronous and the deferred path, and
351
385
  `return_value` stays the destination.
@@ -358,7 +392,7 @@ presigns anything: the URL points at a place the viewer's own
358
392
  credentials open. `ReportLink` shows that URL when it is set and the raw
359
393
  destination when it is not.
360
394
 
361
- ## Deferred
395
+ ## Not implemented
362
396
 
363
397
  **http/MCP AnalyzeHook backend.** The spec originally described an "http"
364
398
  backend calling a deployed sparkforensics `server/` instance. `server/`
@@ -7,6 +7,40 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.3.0] - 2026-09-28
11
+
12
+ ### Added
13
+
14
+ - Run comparison: `baseline_log_source` on `SparkForensicsOperator` and
15
+ `spark_forensics_callback` resolves an earlier run's event log, the same
16
+ way `log_source` resolves the run under analysis, and passes it to
17
+ `sparkforensics-analyze --baseline`. It must resolve to a file path
18
+ readable where the analysis runs: a `LocalEventLog` for
19
+ `SubprocessAnalyzeHook`, a `RemoteEventLog` on the same SSH connection
20
+ for `SSHAnalyzeHook`. A History Server application is rejected, since
21
+ the CLI reads a baseline only from a path. A baseline that is, contains
22
+ or sits inside the log under analysis fails the task before analysis.
23
+ Works with `deferrable=True`.
24
+ - Comparison thresholds `max_regression_pct`, `regression_metric` and
25
+ `fail_on_introduced`, mirroring the CLI's flags and its rules: all need
26
+ `baseline_log_source`, and `regression_metric` needs
27
+ `max_regression_pct`. A breach raises `ThresholdBreached` like any other
28
+ threshold.
29
+ - The `sparkforensics_summary` XCom gains a `comparison` key when a
30
+ baseline ran: the checked metric's verdict, the comparison's confidence
31
+ and reason, and the metric's baseline and candidate values and change in
32
+ percent. The persisted report carries the CLI's full comparison section.
33
+
34
+ ### Changed
35
+
36
+ - The operator reads report schema 5, where a finding's text figure is in
37
+ `valueText` and its `value` is always a number or `null`. A report with
38
+ an older `schemaVersion` fails the task.
39
+ - The persisted report and `Report` carry the CLI's `verdict` and
40
+ `notRunChecks` sections (`Report.verdict`, `Report.not_run_checks`).
41
+ Before this, the operator persisted only the sections it listed, so
42
+ these two would have been dropped.
43
+
10
44
  ## [0.2.0] - 2026-09-25
11
45
 
12
46
  ### Added
@@ -154,7 +188,8 @@ Initial release.
154
188
  - Generic `Notifier` interface for threshold-breach alerting.
155
189
  - PyPI trusted-publishing GitHub Actions workflow (OIDC, no stored token).
156
190
 
157
- [Unreleased]: https://github.com/shuffle-works/sparkforensics-operator/compare/v0.2.0...HEAD
191
+ [Unreleased]: https://github.com/shuffle-works/sparkforensics-operator/compare/v0.3.0...HEAD
192
+ [0.3.0]: https://github.com/shuffle-works/sparkforensics-operator/releases/tag/v0.3.0
158
193
  [0.2.0]: https://github.com/shuffle-works/sparkforensics-operator/releases/tag/v0.2.0
159
194
  [0.1.1]: https://github.com/shuffle-works/sparkforensics-operator/releases/tag/v0.1.1
160
195
  [0.1.0]: https://github.com/shuffle-works/sparkforensics-operator/releases/tag/v0.1.0
@@ -9,7 +9,7 @@ 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.
@@ -18,6 +18,6 @@ that backends consume; see `docs/architecture.md`.
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,77 @@
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, Airflow or Node.js. The one real process is
19
+ 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
+ ## The tox Airflow matrix
26
+
27
+ `tox.ini` runs the suite against each supported Airflow major version:
28
+
29
+ ```bash
30
+ tox -e py311-airflow2
31
+ tox -e py311-airflow3
32
+ ```
33
+
34
+ The full env list is `py{39,310,311,312}-airflow2`,
35
+ `py{310,311,312}-airflow3`, and three floor envs:
36
+
37
+ - `py39-airflow2min` pins Airflow 2.6.3 without the `ssh` extra, and
38
+ omits the SSH-only modules from its coverage (`coverage-no-ssh.ini`).
39
+ - `py39-airflow2sshmin` runs the `ssh` extra's floor
40
+ (`apache-airflow-providers-ssh` 3.7.1) on Airflow 2.6.3. The deferrable
41
+ tests skip there, since that mode needs a newer provider.
42
+ - `py311-airflow2deferrablemin` pins the deferrable mode's floors
43
+ (`constraints-deferrable-floor.txt`).
44
+
45
+ The two Airflow 2.6.3 envs install through
46
+ `constraints-airflow-2.6-floor.txt` so the extras step can't upgrade
47
+ Airflow past the floor; `tox.ini` explains why.
48
+
49
+ CI (`.github/workflows/ci.yml`) runs a subset of these envs on every push
50
+ to `main` and every pull request. Each env writes coverage to
51
+ `coverage-<envname>.lcov`; CI uploads one file per env to Coveralls and
52
+ merges them in a `finish` job. `usedevelop = true` in `tox.ini` keeps the
53
+ lcov `SF:` paths pointing at `src/sparkforensics_operator/...` instead of
54
+ a `.tox` virtualenv, which Coveralls needs to attribute lines.
55
+
56
+ ## Releasing to PyPI
57
+
58
+ `.github/workflows/publish.yml` builds the sdist and wheel and publishes
59
+ them on every GitHub Release (`release: published`). It uses PyPI trusted
60
+ publishing (OIDC), so no API token is stored in the repository.
61
+
62
+ One-time setup before the first release:
63
+
64
+ - On PyPI, add a trusted publisher for this project: owner
65
+ `shuffle-works`, repo `sparkforensics-operator`, workflow `publish.yml`,
66
+ environment `pypi`.
67
+ - In the GitHub repository settings, create an environment named `pypi`
68
+ (optionally with required reviewers) to match.
69
+
70
+ To cut a release:
71
+
72
+ 1. Bump `version` in `pyproject.toml` and move the `[Unreleased]` entries
73
+ in `CHANGELOG.md` under the new version. The Airflow provider metadata
74
+ reads the version from the installed package, so nothing else needs
75
+ changing.
76
+ 2. Tag the release commit (for example `v0.2.0`).
77
+ 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.3.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,14 @@ 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),
77
+ and `max_regression_pct` fails the DAG when runtime, or the metric
78
+ `regression_metric` names, regressed past that budget.
79
+ `fail_on_introduced` fails it when the run gained new findings. The
80
+ comparison's verdict, confidence and measured regression land in the
81
+ summary XCom.
74
82
  - Reports persist to a local path, `file://`, or `s3://`, and a clickable
75
83
  "SparkForensics report" link shows up on the task in the Airflow UI. Set
76
84
  `report_url_template` (an S3 console URL, say) to make it open in a
@@ -85,8 +93,8 @@ runs as part of the DAG, right where the Spark task just ran.
85
93
  - An optional `Notifier` you implement (Slack, MS Teams, email, PagerDuty,
86
94
  ZenDuty, whatever you use) gets a best-effort pass/fail summary; a
87
95
  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:
96
+ - Tested against both Airflow 2.6+ and Airflow 3 (CI runs Python 3.9,
97
+ 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
98
  everything but the shell on the stand-in SSH host is mocked.
91
99
 
92
100
  ## Quick start
@@ -162,16 +170,17 @@ the analysis runs: the CLI runs as a detached job on the SSH host and the
162
170
  triggerer polls it, which needs a running triggerer with
163
171
  `sparkforensics-operator[ssh]` installed and
164
172
  `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).
173
+ [Deferrable execution](https://github.com/shuffle-works/sparkforensics-operator/blob/main/docs/configuration.md#deferrable-execution).
166
174
 
167
175
  Whichever host runs the analysis (the worker for `SubprocessAnalyzeHook`,
168
176
  the SSH host for `SSHAnalyzeHook`) needs Node.js 18+ and the
169
- `sparkforensics-cli` npm package (`npm install -g sparkforensics-cli`), so
177
+ `sparkforensics-cli` npm package, version 0.4.0 or newer
178
+ (`npm install -g "sparkforensics-cli@>=0.4.0"`), so
170
179
  `sparkforensics-analyze` is resolvable on `PATH`. Add the `ssh` extra for
171
180
  the SSH hooks (it works from Airflow 2.6, like the package; only
172
181
  `deferrable=True` needs Airflow 2.11+), and the `s3` extra if
173
182
  `report_dest` is `s3://...`.
174
- Full details in [`docs/runbook.md`](https://github.com/shuffle-works/sparkforensics-operator/blob/main/docs/runbook.md).
183
+ Full details in [Getting started](https://github.com/shuffle-works/sparkforensics-operator/blob/main/docs/getting-started.md).
175
184
 
176
185
  ## Development
177
186
 
@@ -189,11 +198,11 @@ tox -e py311-airflow3
189
198
 
190
199
  ## Learn more
191
200
 
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
201
+ - [Getting started](https://github.com/shuffle-works/sparkforensics-operator/blob/main/docs/getting-started.md), install and host prerequisites, and DAG wiring examples
202
+ - [Configuration](https://github.com/shuffle-works/sparkforensics-operator/blob/main/docs/configuration.md), the full operator/hook/threshold surface
203
+ - [Troubleshooting](https://github.com/shuffle-works/sparkforensics-operator/blob/main/docs/troubleshooting.md), error messages and deferred-run failure modes
204
+ - [Contributing](https://github.com/shuffle-works/sparkforensics-operator/blob/main/CONTRIBUTING.md), development setup and the release procedure
205
+ - [Architecture](https://github.com/shuffle-works/sparkforensics-operator/blob/main/ARCHITECTURE.md), components and design decisions
197
206
 
198
207
  ## License
199
208
 
@@ -36,6 +36,14 @@ runs as part of the DAG, right where the Spark task just ran.
36
36
  you configure are enforced, and a breach raises `ThresholdBreached`
37
37
  without consuming the task's retries, since re-running would just reach
38
38
  the same verdict.
39
+ - Compare a run against an earlier one. `baseline_log_source` takes any
40
+ log source that resolves to an event log path the analysis can read
41
+ (`path_template="/spark-logs/{{ prev_ds }}/eventlog"`, an XCom pull),
42
+ and `max_regression_pct` fails the DAG when runtime, or the metric
43
+ `regression_metric` names, regressed past that budget.
44
+ `fail_on_introduced` fails it when the run gained new findings. The
45
+ comparison's verdict, confidence and measured regression land in the
46
+ summary XCom.
39
47
  - Reports persist to a local path, `file://`, or `s3://`, and a clickable
40
48
  "SparkForensics report" link shows up on the task in the Airflow UI. Set
41
49
  `report_url_template` (an S3 console URL, say) to make it open in a
@@ -50,8 +58,8 @@ runs as part of the DAG, right where the Spark task just ran.
50
58
  - An optional `Notifier` you implement (Slack, MS Teams, email, PagerDuty,
51
59
  ZenDuty, whatever you use) gets a best-effort pass/fail summary; a
52
60
  delivery failure never fails the task itself.
53
- - Tested against both Airflow 2.6+ and Airflow 3.0+ (CI matrix, Python
54
- 3.9-3.12). No live Spark, Airflow or Node is needed to run the tests:
61
+ - Tested against both Airflow 2.6+ and Airflow 3 (CI runs Python 3.9,
62
+ 3.11 and 3.12; the tox matrix also covers 3.10). No live Spark, Airflow or Node is needed to run the tests:
55
63
  everything but the shell on the stand-in SSH host is mocked.
56
64
 
57
65
  ## Quick start
@@ -127,16 +135,17 @@ the analysis runs: the CLI runs as a detached job on the SSH host and the
127
135
  triggerer polls it, which needs a running triggerer with
128
136
  `sparkforensics-operator[ssh]` installed and
129
137
  `apache-airflow-providers-ssh>=6.0.1`, so Airflow 2.11 or newer. See
130
- [Deferrable execution](https://github.com/shuffle-works/sparkforensics-operator/blob/main/docs/api-reference.md#deferrable-execution).
138
+ [Deferrable execution](https://github.com/shuffle-works/sparkforensics-operator/blob/main/docs/configuration.md#deferrable-execution).
131
139
 
132
140
  Whichever host runs the analysis (the worker for `SubprocessAnalyzeHook`,
133
141
  the SSH host for `SSHAnalyzeHook`) needs Node.js 18+ and the
134
- `sparkforensics-cli` npm package (`npm install -g sparkforensics-cli`), so
142
+ `sparkforensics-cli` npm package, version 0.4.0 or newer
143
+ (`npm install -g "sparkforensics-cli@>=0.4.0"`), so
135
144
  `sparkforensics-analyze` is resolvable on `PATH`. Add the `ssh` extra for
136
145
  the SSH hooks (it works from Airflow 2.6, like the package; only
137
146
  `deferrable=True` needs Airflow 2.11+), and the `s3` extra if
138
147
  `report_dest` is `s3://...`.
139
- Full details in [`docs/runbook.md`](https://github.com/shuffle-works/sparkforensics-operator/blob/main/docs/runbook.md).
148
+ Full details in [Getting started](https://github.com/shuffle-works/sparkforensics-operator/blob/main/docs/getting-started.md).
140
149
 
141
150
  ## Development
142
151
 
@@ -154,11 +163,11 @@ tox -e py311-airflow3
154
163
 
155
164
  ## Learn more
156
165
 
157
- - [Architecture](https://github.com/shuffle-works/sparkforensics-operator/blob/main/docs/architecture.md), components and design decisions
158
- - [API reference](https://github.com/shuffle-works/sparkforensics-operator/blob/main/docs/api-reference.md), the full operator/hook/threshold
159
- surface
160
- - [Runbook](https://github.com/shuffle-works/sparkforensics-operator/blob/main/docs/runbook.md), worker and SSH host prerequisites, troubleshooting
161
- - [Glossary](https://github.com/shuffle-works/sparkforensics-operator/blob/main/docs/glossary.md), domain terms
166
+ - [Getting started](https://github.com/shuffle-works/sparkforensics-operator/blob/main/docs/getting-started.md), install and host prerequisites, and DAG wiring examples
167
+ - [Configuration](https://github.com/shuffle-works/sparkforensics-operator/blob/main/docs/configuration.md), the full operator/hook/threshold surface
168
+ - [Troubleshooting](https://github.com/shuffle-works/sparkforensics-operator/blob/main/docs/troubleshooting.md), error messages and deferred-run failure modes
169
+ - [Contributing](https://github.com/shuffle-works/sparkforensics-operator/blob/main/CONTRIBUTING.md), development setup and the release procedure
170
+ - [Architecture](https://github.com/shuffle-works/sparkforensics-operator/blob/main/ARCHITECTURE.md), components and design decisions
162
171
 
163
172
  ## License
164
173