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.
- {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/.github/workflows/ci.yml +30 -0
- {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/AGENTS.md +8 -7
- sparkforensics_operator-0.2.0/docs/architecture.md → sparkforensics_operator-0.4.0/ARCHITECTURE.md +111 -35
- {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/CHANGELOG.md +70 -1
- {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/CLAUDE.md +6 -6
- sparkforensics_operator-0.4.0/CONTRIBUTING.md +100 -0
- {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/PKG-INFO +23 -11
- {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/README.md +22 -10
- sparkforensics_operator-0.4.0/docs/configuration.md +755 -0
- sparkforensics_operator-0.4.0/docs/getting-started.md +227 -0
- sparkforensics_operator-0.4.0/docs/index.md +28 -0
- sparkforensics_operator-0.4.0/docs/troubleshooting.md +467 -0
- {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/pyproject.toml +1 -1
- {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/src/sparkforensics_operator/callback.py +43 -7
- sparkforensics_operator-0.4.0/src/sparkforensics_operator/exceptions.py +29 -0
- sparkforensics_operator-0.4.0/src/sparkforensics_operator/hooks/analyze/_cli.py +220 -0
- {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/src/sparkforensics_operator/hooks/analyze/_remote_job.py +53 -6
- {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/src/sparkforensics_operator/hooks/analyze/base.py +48 -8
- {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/src/sparkforensics_operator/hooks/analyze/ssh.py +116 -14
- sparkforensics_operator-0.4.0/src/sparkforensics_operator/hooks/analyze/subprocess.py +89 -0
- {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/src/sparkforensics_operator/notify.py +25 -4
- {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/src/sparkforensics_operator/operator.py +311 -41
- sparkforensics_operator-0.4.0/src/sparkforensics_operator/report.py +207 -0
- {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/src/sparkforensics_operator/sinks.py +5 -1
- {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/src/sparkforensics_operator/summary.py +56 -4
- sparkforensics_operator-0.4.0/tests/fixtures/sparkforensics_cli_0_4_0/README.md +17 -0
- sparkforensics_operator-0.4.0/tests/fixtures/sparkforensics_cli_0_4_0/comparison.json +1064 -0
- sparkforensics_operator-0.4.0/tests/fixtures/sparkforensics_cli_0_4_0/comparison.stderr +1 -0
- sparkforensics_operator-0.4.0/tests/fixtures/sparkforensics_cli_0_4_0/incomplete-run.json +691 -0
- sparkforensics_operator-0.4.0/tests/fixtures/sparkforensics_cli_0_4_0/incomplete-run.stderr +1 -0
- sparkforensics_operator-0.4.0/tests/fixtures/sparkforensics_cli_0_4_0/report.json +1209 -0
- sparkforensics_operator-0.4.0/tests/fixtures/sparkforensics_cli_0_4_0/report.stderr +2 -0
- {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/tests/hooks/analyze/_local_ssh.py +8 -1
- sparkforensics_operator-0.4.0/tests/hooks/analyze/test_base.py +100 -0
- sparkforensics_operator-0.4.0/tests/hooks/analyze/test_cli.py +394 -0
- {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/tests/hooks/analyze/test_deferrable_parity.py +64 -9
- {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/tests/hooks/analyze/test_ssh.py +105 -6
- {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/tests/hooks/analyze/test_ssh_deferrable.py +168 -1
- {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/tests/hooks/analyze/test_subprocess.py +166 -3
- sparkforensics_operator-0.4.0/tests/integration/__init__.py +0 -0
- sparkforensics_operator-0.4.0/tests/integration/conftest.py +70 -0
- sparkforensics_operator-0.4.0/tests/integration/test_real_cli.py +101 -0
- {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/tests/test_callback.py +19 -0
- sparkforensics_operator-0.4.0/tests/test_notify.py +92 -0
- sparkforensics_operator-0.4.0/tests/test_operator.py +895 -0
- {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/tests/test_operator_deferrable.py +150 -0
- sparkforensics_operator-0.4.0/tests/test_report.py +392 -0
- {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/tests/test_sinks.py +25 -0
- sparkforensics_operator-0.4.0/tests/test_summary.py +303 -0
- {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/tests/test_templating.py +78 -0
- {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/tox.ini +2 -0
- sparkforensics_operator-0.2.0/docs/api-reference.md +0 -396
- sparkforensics_operator-0.2.0/docs/glossary.md +0 -58
- sparkforensics_operator-0.2.0/docs/runbook.md +0 -374
- sparkforensics_operator-0.2.0/src/sparkforensics_operator/exceptions.py +0 -10
- sparkforensics_operator-0.2.0/src/sparkforensics_operator/hooks/analyze/_cli.py +0 -88
- sparkforensics_operator-0.2.0/src/sparkforensics_operator/hooks/analyze/subprocess.py +0 -55
- sparkforensics_operator-0.2.0/src/sparkforensics_operator/report.py +0 -92
- sparkforensics_operator-0.2.0/tests/hooks/analyze/test_base.py +0 -38
- sparkforensics_operator-0.2.0/tests/hooks/analyze/test_cli.py +0 -17
- sparkforensics_operator-0.2.0/tests/test_notify.py +0 -42
- sparkforensics_operator-0.2.0/tests/test_operator.py +0 -321
- sparkforensics_operator-0.2.0/tests/test_report.py +0 -128
- sparkforensics_operator-0.2.0/tests/test_summary.py +0 -143
- {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/.github/workflows/publish.yml +0 -0
- {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/.gitignore +0 -0
- {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/LICENSE +0 -0
- {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/constraints-airflow-2.6-floor.txt +0 -0
- {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/constraints-deferrable-floor.txt +0 -0
- {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/coverage-no-ssh.ini +0 -0
- {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/src/sparkforensics_operator/__init__.py +0 -0
- {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/src/sparkforensics_operator/_compat.py +0 -0
- {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/src/sparkforensics_operator/get_provider_info.py +0 -0
- {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/src/sparkforensics_operator/hooks/__init__.py +0 -0
- {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/src/sparkforensics_operator/hooks/_templating.py +0 -0
- {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/src/sparkforensics_operator/hooks/analyze/__init__.py +0 -0
- {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/src/sparkforensics_operator/hooks/log_source/__init__.py +0 -0
- {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/src/sparkforensics_operator/hooks/log_source/_dest_root.py +0 -0
- {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/src/sparkforensics_operator/hooks/log_source/_history_server_args.py +0 -0
- {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/src/sparkforensics_operator/hooks/log_source/_path_template.py +0 -0
- {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/src/sparkforensics_operator/hooks/log_source/_rolling_log.py +0 -0
- {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/src/sparkforensics_operator/hooks/log_source/base.py +0 -0
- {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/src/sparkforensics_operator/hooks/log_source/filesystem.py +0 -0
- {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/src/sparkforensics_operator/hooks/log_source/history_server.py +0 -0
- {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/src/sparkforensics_operator/hooks/log_source/history_server_app.py +0 -0
- {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/src/sparkforensics_operator/hooks/log_source/remote_path.py +0 -0
- {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/src/sparkforensics_operator/hooks/log_source/sftp.py +0 -0
- {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/src/sparkforensics_operator/hooks/log_source/tunnel.py +0 -0
- {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/src/sparkforensics_operator/hooks/log_source/xcom.py +0 -0
- {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/src/sparkforensics_operator/links.py +0 -0
- {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/src/sparkforensics_operator/log_ref.py +0 -0
- {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/tests/__init__.py +0 -0
- {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/tests/_render.py +0 -0
- {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/tests/hooks/__init__.py +0 -0
- {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/tests/hooks/analyze/__init__.py +0 -0
- {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/tests/hooks/log_source/__init__.py +0 -0
- {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/tests/hooks/log_source/test_base.py +0 -0
- {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/tests/hooks/log_source/test_filesystem.py +0 -0
- {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/tests/hooks/log_source/test_history_server.py +0 -0
- {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/tests/hooks/log_source/test_history_server_app.py +0 -0
- {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/tests/hooks/log_source/test_remote_path.py +0 -0
- {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/tests/hooks/log_source/test_sftp.py +0 -0
- {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/tests/hooks/log_source/test_tunnel.py +0 -0
- {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/tests/hooks/log_source/test_xcom.py +0 -0
- {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/tests/test_compat.py +0 -0
- {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/tests/test_dagbag_integration.py +0 -0
- {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/tests/test_exceptions.py +0 -0
- {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/tests/test_links.py +0 -0
- {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/tests/test_log_ref.py +0 -0
- {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/tests/test_package_exports.py +0 -0
- {sparkforensics_operator-0.2.0 → sparkforensics_operator-0.4.0}/tests/test_provider_info.py +0 -0
- {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 `
|
|
13
|
-
SSH analysis as the SSH provider's detached remote job (
|
|
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
|
|
19
|
-
|
|
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
|
-
##
|
|
48
|
-
-
|
|
49
|
-
`
|
|
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
|
|
sparkforensics_operator-0.2.0/docs/architecture.md → sparkforensics_operator-0.4.0/ARCHITECTURE.md
RENAMED
|
@@ -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.
|
|
34
|
-
|
|
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`,
|
|
43
|
-
|
|
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
|
|
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.
|
|
54
|
-
|
|
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
|
|
72
|
-
hook/config combinations actually
|
|
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`
|
|
101
|
-
|
|
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
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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.
|
|
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
|
-
- `
|
|
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.
|
|
146
|
-
|
|
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`,
|
|
211
|
-
|
|
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
|
|
247
|
-
the job before raising. A deferral that times
|
|
248
|
-
`defer_timeout`, `timeout`
|
|
249
|
-
|
|
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
|
-
|
|
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
|
|
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,
|
|
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
|
-
##
|
|
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.
|
|
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 `
|
|
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
|
|
17
|
-
|
|
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
|
-
##
|
|
22
|
-
-
|
|
23
|
-
`
|
|
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.
|
|
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
|
|
89
|
-
3.
|
|
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/
|
|
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
|
|
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 [
|
|
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
|
-
- [
|
|
193
|
-
- [
|
|
194
|
-
|
|
195
|
-
- [
|
|
196
|
-
- [
|
|
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
|
|