sparkforensics-operator 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sparkforensics_operator-0.1.0/.github/workflows/ci.yml +31 -0
- sparkforensics_operator-0.1.0/.github/workflows/publish.yml +33 -0
- sparkforensics_operator-0.1.0/.gitignore +10 -0
- sparkforensics_operator-0.1.0/CHANGELOG.md +23 -0
- sparkforensics_operator-0.1.0/CLAUDE.md +26 -0
- sparkforensics_operator-0.1.0/LICENSE +21 -0
- sparkforensics_operator-0.1.0/PKG-INFO +147 -0
- sparkforensics_operator-0.1.0/README.md +113 -0
- sparkforensics_operator-0.1.0/constraints-airflow-2.6-floor.txt +695 -0
- sparkforensics_operator-0.1.0/docs/api-reference.md +167 -0
- sparkforensics_operator-0.1.0/docs/architecture.md +87 -0
- sparkforensics_operator-0.1.0/docs/glossary.md +25 -0
- sparkforensics_operator-0.1.0/docs/runbook.md +163 -0
- sparkforensics_operator-0.1.0/pyproject.toml +45 -0
- sparkforensics_operator-0.1.0/src/sparkforensics_operator/__init__.py +29 -0
- sparkforensics_operator-0.1.0/src/sparkforensics_operator/_compat.py +23 -0
- sparkforensics_operator-0.1.0/src/sparkforensics_operator/callback.py +81 -0
- sparkforensics_operator-0.1.0/src/sparkforensics_operator/exceptions.py +10 -0
- sparkforensics_operator-0.1.0/src/sparkforensics_operator/hooks/__init__.py +0 -0
- sparkforensics_operator-0.1.0/src/sparkforensics_operator/hooks/analyze/__init__.py +0 -0
- sparkforensics_operator-0.1.0/src/sparkforensics_operator/hooks/analyze/base.py +18 -0
- sparkforensics_operator-0.1.0/src/sparkforensics_operator/hooks/analyze/subprocess.py +72 -0
- sparkforensics_operator-0.1.0/src/sparkforensics_operator/hooks/log_source/__init__.py +0 -0
- sparkforensics_operator-0.1.0/src/sparkforensics_operator/hooks/log_source/_dest_root.py +30 -0
- sparkforensics_operator-0.1.0/src/sparkforensics_operator/hooks/log_source/_path_template.py +31 -0
- sparkforensics_operator-0.1.0/src/sparkforensics_operator/hooks/log_source/_rolling_log.py +4 -0
- sparkforensics_operator-0.1.0/src/sparkforensics_operator/hooks/log_source/base.py +24 -0
- sparkforensics_operator-0.1.0/src/sparkforensics_operator/hooks/log_source/filesystem.py +49 -0
- sparkforensics_operator-0.1.0/src/sparkforensics_operator/hooks/log_source/history_server.py +99 -0
- sparkforensics_operator-0.1.0/src/sparkforensics_operator/hooks/log_source/sftp.py +107 -0
- sparkforensics_operator-0.1.0/src/sparkforensics_operator/hooks/log_source/tunnel.py +77 -0
- sparkforensics_operator-0.1.0/src/sparkforensics_operator/hooks/log_source/xcom.py +27 -0
- sparkforensics_operator-0.1.0/src/sparkforensics_operator/links.py +49 -0
- sparkforensics_operator-0.1.0/src/sparkforensics_operator/notify.py +37 -0
- sparkforensics_operator-0.1.0/src/sparkforensics_operator/operator.py +163 -0
- sparkforensics_operator-0.1.0/src/sparkforensics_operator/report.py +91 -0
- sparkforensics_operator-0.1.0/src/sparkforensics_operator/sinks.py +96 -0
- sparkforensics_operator-0.1.0/tests/__init__.py +0 -0
- sparkforensics_operator-0.1.0/tests/hooks/__init__.py +0 -0
- sparkforensics_operator-0.1.0/tests/hooks/analyze/__init__.py +0 -0
- sparkforensics_operator-0.1.0/tests/hooks/analyze/test_base.py +16 -0
- sparkforensics_operator-0.1.0/tests/hooks/analyze/test_subprocess.py +164 -0
- sparkforensics_operator-0.1.0/tests/hooks/log_source/__init__.py +0 -0
- sparkforensics_operator-0.1.0/tests/hooks/log_source/test_base.py +26 -0
- sparkforensics_operator-0.1.0/tests/hooks/log_source/test_filesystem.py +144 -0
- sparkforensics_operator-0.1.0/tests/hooks/log_source/test_history_server.py +208 -0
- sparkforensics_operator-0.1.0/tests/hooks/log_source/test_sftp.py +176 -0
- sparkforensics_operator-0.1.0/tests/hooks/log_source/test_tunnel.py +109 -0
- sparkforensics_operator-0.1.0/tests/hooks/log_source/test_xcom.py +48 -0
- sparkforensics_operator-0.1.0/tests/test_callback.py +107 -0
- sparkforensics_operator-0.1.0/tests/test_compat.py +19 -0
- sparkforensics_operator-0.1.0/tests/test_dagbag_integration.py +22 -0
- sparkforensics_operator-0.1.0/tests/test_exceptions.py +15 -0
- sparkforensics_operator-0.1.0/tests/test_links.py +54 -0
- sparkforensics_operator-0.1.0/tests/test_notify.py +42 -0
- sparkforensics_operator-0.1.0/tests/test_operator.py +302 -0
- sparkforensics_operator-0.1.0/tests/test_package_exports.py +30 -0
- sparkforensics_operator-0.1.0/tests/test_report.py +135 -0
- sparkforensics_operator-0.1.0/tests/test_sinks.py +156 -0
- sparkforensics_operator-0.1.0/tox.ini +30 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
test:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
strategy:
|
|
12
|
+
fail-fast: false
|
|
13
|
+
matrix:
|
|
14
|
+
include:
|
|
15
|
+
- python: "3.11"
|
|
16
|
+
tox-env: py311-airflow2
|
|
17
|
+
- python: "3.12"
|
|
18
|
+
tox-env: py312-airflow2
|
|
19
|
+
- python: "3.11"
|
|
20
|
+
tox-env: py311-airflow3
|
|
21
|
+
- python: "3.12"
|
|
22
|
+
tox-env: py312-airflow3
|
|
23
|
+
- python: "3.9"
|
|
24
|
+
tox-env: py39-airflow2min
|
|
25
|
+
steps:
|
|
26
|
+
- uses: actions/checkout@v4
|
|
27
|
+
- uses: actions/setup-python@v5
|
|
28
|
+
with:
|
|
29
|
+
python-version: ${{ matrix.python }}
|
|
30
|
+
- run: pip install tox
|
|
31
|
+
- run: tox -e ${{ matrix.tox-env }}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
build:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
steps:
|
|
11
|
+
- uses: actions/checkout@v4
|
|
12
|
+
- uses: actions/setup-python@v5
|
|
13
|
+
with:
|
|
14
|
+
python-version: "3.12"
|
|
15
|
+
- run: pip install build
|
|
16
|
+
- run: python -m build
|
|
17
|
+
- uses: actions/upload-artifact@v4
|
|
18
|
+
with:
|
|
19
|
+
name: dist
|
|
20
|
+
path: dist/
|
|
21
|
+
|
|
22
|
+
publish:
|
|
23
|
+
needs: build
|
|
24
|
+
runs-on: ubuntu-latest
|
|
25
|
+
environment: pypi
|
|
26
|
+
permissions:
|
|
27
|
+
id-token: write
|
|
28
|
+
steps:
|
|
29
|
+
- uses: actions/download-artifact@v4
|
|
30
|
+
with:
|
|
31
|
+
name: dist
|
|
32
|
+
path: dist/
|
|
33
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [0.1.0] - 2026-09-06
|
|
9
|
+
|
|
10
|
+
Initial release.
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- `SparkForensicsOperator`: after a Spark job runs, fetches its event log
|
|
15
|
+
and runs it through the `sparkforensics-analyze` CLI, then
|
|
16
|
+
persists/XComs/threshold-checks/notifies on the result.
|
|
17
|
+
- Log source hooks: `FilesystemLogSourceHook`, `HistoryServerLogSourceHook`,
|
|
18
|
+
`SFTPLogSourceHook`, `SSHTunneledLogSourceHook`, `XComLogSourceHook`.
|
|
19
|
+
- `SubprocessAnalyzeHook`, shelling out to the `sparkforensics-analyze` CLI.
|
|
20
|
+
- Generic `Notifier` interface for threshold-breach alerting.
|
|
21
|
+
- PyPI trusted-publishing GitHub Actions workflow (OIDC, no stored token).
|
|
22
|
+
|
|
23
|
+
[0.1.0]: https://github.com/shuffle-works/sparkforensics-operator/releases/tag/v0.1.0
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# sparkforensics-operator
|
|
2
|
+
|
|
3
|
+
Airflow package: after a Spark job runs, fetches its event log and runs it
|
|
4
|
+
through the sparkforensics-analyze CLI, then persists/XComs/threshold-checks/
|
|
5
|
+
notifies on the result.
|
|
6
|
+
|
|
7
|
+
## Stack
|
|
8
|
+
Python 3.9+, apache-airflow 2.6+ or 3.0+, pytest. Runtime dependency: Node.js
|
|
9
|
+
(engines `>=22.18.0 <23.0.0 || >=23.6.0`) + the `sparkforensics` npm package
|
|
10
|
+
on the worker, for the `sparkforensics-analyze` CLI the subprocess backend
|
|
11
|
+
shells out to.
|
|
12
|
+
|
|
13
|
+
## Commands
|
|
14
|
+
- `pip install -e ".[test,s3,ssh]"`, install for local dev.
|
|
15
|
+
- `pytest`, run the test suite (mocks everything: no live Spark/Airflow/Node
|
|
16
|
+
needed for any single test).
|
|
17
|
+
- `tox -e py311-airflow2` / `tox -e py311-airflow3`, run the suite against a
|
|
18
|
+
specific Airflow major version.
|
|
19
|
+
|
|
20
|
+
## Design docs
|
|
21
|
+
- `docs/superpowers/specs/2026-09-05-sparkforensics-operator-design.md`, the original design spec.
|
|
22
|
+
- `docs/superpowers/plans/2026-09-05-sparkforensics-operator.md`, the
|
|
23
|
+
implementation plan, including every upstream sparkforensics CLI/API
|
|
24
|
+
fact and every Airflow-2-vs-3 compat fact this codebase depends on.
|
|
25
|
+
- `docs/architecture.md`, `docs/api-reference.md`, `docs/runbook.md`,
|
|
26
|
+
`docs/glossary.md`, see below.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Agustin Recoba
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: sparkforensics-operator
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Airflow operator that runs sparkforensics analysis on a Spark job's event log and acts on the result.
|
|
5
|
+
Project-URL: Homepage, https://github.com/shuffle-works/sparkforensics-operator
|
|
6
|
+
Project-URL: Repository, https://github.com/shuffle-works/sparkforensics-operator
|
|
7
|
+
Project-URL: Issues, https://github.com/shuffle-works/sparkforensics-operator/issues
|
|
8
|
+
Author-email: Agustin Recoba <agustin.recoba@outlook.com>
|
|
9
|
+
License: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: airflow,data-engineering,observability,spark,sparkforensics
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Framework :: Apache Airflow
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
22
|
+
Requires-Python: >=3.9
|
|
23
|
+
Requires-Dist: apache-airflow>=2.6
|
|
24
|
+
Requires-Dist: requests>=2.28
|
|
25
|
+
Provides-Extra: s3
|
|
26
|
+
Requires-Dist: apache-airflow-providers-amazon>=8.0; extra == 's3'
|
|
27
|
+
Provides-Extra: ssh
|
|
28
|
+
Requires-Dist: apache-airflow-providers-sftp>=4.0; extra == 'ssh'
|
|
29
|
+
Requires-Dist: apache-airflow-providers-ssh>=5.0; extra == 'ssh'
|
|
30
|
+
Provides-Extra: test
|
|
31
|
+
Requires-Dist: pytest-cov>=4.0; extra == 'test'
|
|
32
|
+
Requires-Dist: pytest>=7.0; extra == 'test'
|
|
33
|
+
Description-Content-Type: text/markdown
|
|
34
|
+
|
|
35
|
+
# sparkforensics-operator
|
|
36
|
+
|
|
37
|
+
[](https://github.com/shuffle-works/sparkforensics-operator/actions/workflows/ci.yml)
|
|
38
|
+
[](LICENSE)
|
|
39
|
+
|
|
40
|
+
An Airflow operator that runs [sparkforensics](https://github.com/shuffle-works/sparkforensics)
|
|
41
|
+
analysis on a Spark job's event log right after it finishes, then acts on
|
|
42
|
+
the result: persist the report, push it to XCom, notify (via any
|
|
43
|
+
messaging/paging system you implement), and optionally fail the DAG if a
|
|
44
|
+
budget was breached.
|
|
45
|
+
|
|
46
|
+
Airflow tells you a Spark task succeeded. It doesn't tell you that stage 14
|
|
47
|
+
spilled 40GB from a skewed join, or that runtime crept 20% past last week's
|
|
48
|
+
run. This package closes that gap without a separate monitoring job: it
|
|
49
|
+
runs as part of the DAG, right where the Spark task just ran.
|
|
50
|
+
|
|
51
|
+
## Why this exists
|
|
52
|
+
|
|
53
|
+
- Two ways to wire it in: a standalone `SparkForensicsOperator` that fails
|
|
54
|
+
the DAG on a threshold breach, or a `spark_forensics_callback` factory to
|
|
55
|
+
attach as `on_success_callback` on the Spark task itself, log-only.
|
|
56
|
+
- Three ways to get the event log: fetch it from the Spark History Server,
|
|
57
|
+
read it from a filesystem/mounted-HDFS path template, or pull it straight
|
|
58
|
+
out of XCom if the upstream task already has it.
|
|
59
|
+
- Budget thresholds mirror `sparkforensics-analyze`'s own CLI flags (max
|
|
60
|
+
runtime, spill, skew, failed-task rate, min efficiency); only the ones
|
|
61
|
+
you configure are enforced, and a breach raises `ThresholdBreached`
|
|
62
|
+
without consuming the task's retries, since re-running would just reach
|
|
63
|
+
the same verdict.
|
|
64
|
+
- Reports persist to a local path, `file://`, or `s3://`, and a clickable
|
|
65
|
+
"SparkForensics report" link shows up on the task in the Airflow UI.
|
|
66
|
+
- An optional `Notifier` you implement (Slack, MS Teams, email, PagerDuty,
|
|
67
|
+
ZenDuty, whatever you use) gets a best-effort pass/fail summary; a
|
|
68
|
+
delivery failure never fails the task itself.
|
|
69
|
+
- Tested against both Airflow 2.6+ and Airflow 3.0+ (CI matrix, Python
|
|
70
|
+
3.9-3.12). 97 tests, 96% coverage, no live Spark/Airflow/Node needed to
|
|
71
|
+
run them: everything is mocked.
|
|
72
|
+
|
|
73
|
+
## Quick start
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
pip install sparkforensics-operator
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Standalone operator, chained after the Spark task, fails the DAG on breach:
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
from sparkforensics_operator import FilesystemLogSourceHook, SparkForensicsOperator, SubprocessAnalyzeHook
|
|
83
|
+
|
|
84
|
+
run_spark_job = SparkSubmitOperator(task_id="run_spark_job", ...)
|
|
85
|
+
|
|
86
|
+
check_spark_job = SparkForensicsOperator(
|
|
87
|
+
task_id="check_spark_job",
|
|
88
|
+
log_source=FilesystemLogSourceHook(path_template="/mnt/spark-logs/{run_id}/eventlog"),
|
|
89
|
+
backend=SubprocessAnalyzeHook(),
|
|
90
|
+
report_dest="s3://reports/{{ run_id }}/report.json",
|
|
91
|
+
max_runtime_ms=3_600_000,
|
|
92
|
+
max_skew_ratio=3,
|
|
93
|
+
on_threshold_breach="fail",
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
run_spark_job >> check_spark_job
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Or attach it as a callback instead. Airflow logs and swallows any exception
|
|
100
|
+
raised inside a callback, so a breach here never fails or retries the
|
|
101
|
+
upstream task, whatever `on_threshold_breach` is set to:
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
from sparkforensics_operator import SubprocessAnalyzeHook, XComLogSourceHook, spark_forensics_callback
|
|
105
|
+
|
|
106
|
+
run_spark_job = SparkSubmitOperator(
|
|
107
|
+
task_id="run_spark_job",
|
|
108
|
+
on_success_callback=spark_forensics_callback(
|
|
109
|
+
log_source=XComLogSourceHook(task_id="run_spark_job"),
|
|
110
|
+
backend=SubprocessAnalyzeHook(),
|
|
111
|
+
report_dest="s3://reports/run_spark_job/app.json",
|
|
112
|
+
max_runtime_ms=3_600_000,
|
|
113
|
+
),
|
|
114
|
+
...,
|
|
115
|
+
)
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
The worker needs Node.js (`>=22.18.0 <23.0.0 || >=23.6.0`) and the
|
|
119
|
+
`sparkforensics` npm package installed, so `sparkforensics-analyze` is
|
|
120
|
+
resolvable on `PATH`. Add the `s3` extra if `report_dest` is `s3://...`.
|
|
121
|
+
Full details in [`docs/runbook.md`](docs/runbook.md).
|
|
122
|
+
|
|
123
|
+
## Development
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
pip install -e ".[test,s3]"
|
|
127
|
+
pytest
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Test against a specific Airflow major version:
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
tox -e py311-airflow2
|
|
134
|
+
tox -e py311-airflow3
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## Learn more
|
|
138
|
+
|
|
139
|
+
- [Architecture](docs/architecture.md), components and design decisions
|
|
140
|
+
- [API reference](docs/api-reference.md), the full operator/hook/threshold
|
|
141
|
+
surface
|
|
142
|
+
- [Runbook](docs/runbook.md), worker prerequisites and troubleshooting
|
|
143
|
+
- [Glossary](docs/glossary.md), domain terms
|
|
144
|
+
|
|
145
|
+
## License
|
|
146
|
+
|
|
147
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# sparkforensics-operator
|
|
2
|
+
|
|
3
|
+
[](https://github.com/shuffle-works/sparkforensics-operator/actions/workflows/ci.yml)
|
|
4
|
+
[](LICENSE)
|
|
5
|
+
|
|
6
|
+
An Airflow operator that runs [sparkforensics](https://github.com/shuffle-works/sparkforensics)
|
|
7
|
+
analysis on a Spark job's event log right after it finishes, then acts on
|
|
8
|
+
the result: persist the report, push it to XCom, notify (via any
|
|
9
|
+
messaging/paging system you implement), and optionally fail the DAG if a
|
|
10
|
+
budget was breached.
|
|
11
|
+
|
|
12
|
+
Airflow tells you a Spark task succeeded. It doesn't tell you that stage 14
|
|
13
|
+
spilled 40GB from a skewed join, or that runtime crept 20% past last week's
|
|
14
|
+
run. This package closes that gap without a separate monitoring job: it
|
|
15
|
+
runs as part of the DAG, right where the Spark task just ran.
|
|
16
|
+
|
|
17
|
+
## Why this exists
|
|
18
|
+
|
|
19
|
+
- Two ways to wire it in: a standalone `SparkForensicsOperator` that fails
|
|
20
|
+
the DAG on a threshold breach, or a `spark_forensics_callback` factory to
|
|
21
|
+
attach as `on_success_callback` on the Spark task itself, log-only.
|
|
22
|
+
- Three ways to get the event log: fetch it from the Spark History Server,
|
|
23
|
+
read it from a filesystem/mounted-HDFS path template, or pull it straight
|
|
24
|
+
out of XCom if the upstream task already has it.
|
|
25
|
+
- Budget thresholds mirror `sparkforensics-analyze`'s own CLI flags (max
|
|
26
|
+
runtime, spill, skew, failed-task rate, min efficiency); only the ones
|
|
27
|
+
you configure are enforced, and a breach raises `ThresholdBreached`
|
|
28
|
+
without consuming the task's retries, since re-running would just reach
|
|
29
|
+
the same verdict.
|
|
30
|
+
- Reports persist to a local path, `file://`, or `s3://`, and a clickable
|
|
31
|
+
"SparkForensics report" link shows up on the task in the Airflow UI.
|
|
32
|
+
- An optional `Notifier` you implement (Slack, MS Teams, email, PagerDuty,
|
|
33
|
+
ZenDuty, whatever you use) gets a best-effort pass/fail summary; a
|
|
34
|
+
delivery failure never fails the task itself.
|
|
35
|
+
- Tested against both Airflow 2.6+ and Airflow 3.0+ (CI matrix, Python
|
|
36
|
+
3.9-3.12). 97 tests, 96% coverage, no live Spark/Airflow/Node needed to
|
|
37
|
+
run them: everything is mocked.
|
|
38
|
+
|
|
39
|
+
## Quick start
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
pip install sparkforensics-operator
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Standalone operator, chained after the Spark task, fails the DAG on breach:
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
from sparkforensics_operator import FilesystemLogSourceHook, SparkForensicsOperator, SubprocessAnalyzeHook
|
|
49
|
+
|
|
50
|
+
run_spark_job = SparkSubmitOperator(task_id="run_spark_job", ...)
|
|
51
|
+
|
|
52
|
+
check_spark_job = SparkForensicsOperator(
|
|
53
|
+
task_id="check_spark_job",
|
|
54
|
+
log_source=FilesystemLogSourceHook(path_template="/mnt/spark-logs/{run_id}/eventlog"),
|
|
55
|
+
backend=SubprocessAnalyzeHook(),
|
|
56
|
+
report_dest="s3://reports/{{ run_id }}/report.json",
|
|
57
|
+
max_runtime_ms=3_600_000,
|
|
58
|
+
max_skew_ratio=3,
|
|
59
|
+
on_threshold_breach="fail",
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
run_spark_job >> check_spark_job
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Or attach it as a callback instead. Airflow logs and swallows any exception
|
|
66
|
+
raised inside a callback, so a breach here never fails or retries the
|
|
67
|
+
upstream task, whatever `on_threshold_breach` is set to:
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
from sparkforensics_operator import SubprocessAnalyzeHook, XComLogSourceHook, spark_forensics_callback
|
|
71
|
+
|
|
72
|
+
run_spark_job = SparkSubmitOperator(
|
|
73
|
+
task_id="run_spark_job",
|
|
74
|
+
on_success_callback=spark_forensics_callback(
|
|
75
|
+
log_source=XComLogSourceHook(task_id="run_spark_job"),
|
|
76
|
+
backend=SubprocessAnalyzeHook(),
|
|
77
|
+
report_dest="s3://reports/run_spark_job/app.json",
|
|
78
|
+
max_runtime_ms=3_600_000,
|
|
79
|
+
),
|
|
80
|
+
...,
|
|
81
|
+
)
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
The worker needs Node.js (`>=22.18.0 <23.0.0 || >=23.6.0`) and the
|
|
85
|
+
`sparkforensics` npm package installed, so `sparkforensics-analyze` is
|
|
86
|
+
resolvable on `PATH`. Add the `s3` extra if `report_dest` is `s3://...`.
|
|
87
|
+
Full details in [`docs/runbook.md`](docs/runbook.md).
|
|
88
|
+
|
|
89
|
+
## Development
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
pip install -e ".[test,s3]"
|
|
93
|
+
pytest
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Test against a specific Airflow major version:
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
tox -e py311-airflow2
|
|
100
|
+
tox -e py311-airflow3
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Learn more
|
|
104
|
+
|
|
105
|
+
- [Architecture](docs/architecture.md), components and design decisions
|
|
106
|
+
- [API reference](docs/api-reference.md), the full operator/hook/threshold
|
|
107
|
+
surface
|
|
108
|
+
- [Runbook](docs/runbook.md), worker prerequisites and troubleshooting
|
|
109
|
+
- [Glossary](docs/glossary.md), domain terms
|
|
110
|
+
|
|
111
|
+
## License
|
|
112
|
+
|
|
113
|
+
[MIT](LICENSE)
|