Skip to content

Commit 567ce58

Browse files
committed
add superset_openlineage module to superset image
1 parent 2dc59e1 commit 567ce58

28 files changed

Lines changed: 1749 additions & 0 deletions

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,16 @@ All notable changes to this project will be documented in this file.
44

55
## [Unreleased]
66

7+
### Added
8+
9+
- superset: Add the `superset-openlineage` integration that emits OpenLineage events for SQL Lab queries and chart/dashboard data fetches; config-only, inert unless enabled in `superset_config.py` ([#XXXX]).
10+
711
### Removed
812

913
- omid: remove 1.1.2 ([#1593]).
1014

1115
[#1593]: https://github.com/stackabletech/docker-images/pull/1593
16+
[#XXXX]: https://github.com/stackabletech/docker-images/pull/XXXX
1217

1318
## [26.7.0] - 2026-07-21
1419

superset/Dockerfile

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ RUN microdnf update \
7777
COPY superset/stackable/constraints/${PRODUCT_VERSION}/constraints.txt /tmp/constraints.txt
7878
COPY superset/stackable/constraints/${PRODUCT_VERSION}/build-constraints.txt /tmp/build-constraints.txt
7979
COPY --from=opa-authorizer-builder /tmp/opa-authorizer/dist/opa_authorizer-0.1.0-py3-none-any.whl /tmp/
80+
COPY superset/stackable/superset-openlineage /tmp/superset-openlineage
8081

8182
COPY --chown=${STACKABLE_USER_UID}:0 superset/stackable/patches/patchable.toml /stackable/src/superset/stackable/patches/patchable.toml
8283
COPY --chown=${STACKABLE_USER_UID}:0 superset/stackable/patches/${PRODUCT_VERSION} /stackable/src/superset/stackable/patches/${PRODUCT_VERSION}
@@ -169,6 +170,13 @@ fi
169170

170171
uv pip install --no-cache-dir /tmp/opa_authorizer-0.1.0-py3-none-any.whl
171172

173+
# Install the Stackable OpenLineage integration for Superset. It emits OpenLineage
174+
# events for SQL Lab queries and chart/dashboard data fetches. It is config-only and
175+
# stays inert unless enabled in superset_config.py (QUERY_LOGGER, EVENT_LOGGER and
176+
# init_app via FLASK_APP_MUTATOR); see superset/stackable/superset-openlineage/README.md.
177+
# sqlglot and SQLAlchemy are provided by Superset itself; only openlineage-python is pulled in.
178+
uv pip install --no-cache-dir /tmp/superset-openlineage
179+
172180
# Setuptools 82+ removed pkg_resources, which is still needed by Superset 4.x
173181
# dependencies. Re-pin after all other installs in case newer versions
174182
# have been pulled in by other dependencies.
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
# superset-openlineage
2+
3+
OpenLineage integration for Apache Superset, shipped in the Stackable Superset
4+
image. It emits OpenLineage events when SQL Lab queries run and when dashboard
5+
charts fetch data.
6+
7+
It is **config-only**: no changes are made to the Superset source tree, and the
8+
plugin stays completely inert unless it is explicitly enabled in
9+
`superset_config.py`. The package is installed into the image (see the
10+
`superset/Dockerfile` build stage), but activation is left to the operator/admin.
11+
12+
## Enable it
13+
14+
Add the following to `superset_config.py`:
15+
16+
```python
17+
from superset_openlineage import (
18+
build_query_logger,
19+
OpenLineageEventLogger,
20+
init_app,
21+
)
22+
23+
QUERY_LOGGER = build_query_logger()
24+
EVENT_LOGGER = OpenLineageEventLogger()
25+
26+
# Registers a teardown_request that clears the request-scoped stash
27+
# (required to avoid cross-request lineage mis-attribution on reused workers).
28+
def FLASK_APP_MUTATOR(app):
29+
init_app(app)
30+
```
31+
32+
## Configure the transport
33+
34+
Point the client at a lineage backend with standard OpenLineage configuration
35+
(environment variables or `openlineage.yml`):
36+
37+
```bash
38+
export OPENLINEAGE_URL=http://marquez:5000
39+
export OPENLINEAGE_API_KEY=<token> # optional
40+
# or set OPENLINEAGE_DISABLED=true to turn emission off entirely
41+
```
42+
43+
## Plugin settings
44+
45+
| Env var | Default | Meaning |
46+
|---|---|---|
47+
| `SUPERSET_OPENLINEAGE_ENABLED` | `true` | Master switch for the plugin |
48+
| `SUPERSET_OPENLINEAGE_NAMESPACE` | `superset` | OpenLineage job namespace |
49+
| `SUPERSET_OPENLINEAGE_PRODUCER` | Superset repo URL | `producer` URI on events |
50+
| `SUPERSET_OPENLINEAGE_JOB_NAME` | `$source.$identity` | Job-name template (`string.Template`) |
51+
52+
### Job-name template
53+
54+
A Python `string.Template`. Placeholders: `$source`, `$identity`,
55+
`$sql_editor_id`, `$client_id`, `$slice_id`, `$dashboard_id`, `$sql_hash`,
56+
`$schema`, `$catalog`, `$database`, `$username`. Placeholders that do not apply
57+
in a given context render empty and surrounding separators collapse; an empty
58+
result falls back to `$source.$sql_hash`.
59+
60+
## Known limitations
61+
62+
- Failed queries and cache hits emit no events (the config-only hooks provide no
63+
failure or cache signal).
64+
- Column-level lineage is emitted only for `CTAS`/`CVAS` statements (which have a
65+
real output table); plain `SELECT` queries (charts/dashboards and most SQL Lab)
66+
carry table-level inputs only.
67+
- Queries executed via Superset's newer SQL execution engine
68+
(`superset/sql/execution/*`) are not covered yet — they invoke `QUERY_LOGGER`
69+
but emit no completion event, so they produce no lineage (their pending stash
70+
entries are reclaimed by the teardown hook).
71+
72+
## Tests
73+
74+
Unit tests live under `superset_openlineage/tests/` and require
75+
`openlineage-python`, `sqlglot`, `SQLAlchemy` and `pytest`. They are not
76+
installed into the image.
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
[build-system]
2+
requires = ["setuptools>=64"]
3+
build-backend = "setuptools.build_meta"
4+
5+
[project]
6+
name = "superset-openlineage"
7+
version = "0.1.0"
8+
description = "OpenLineage integration for Apache Superset"
9+
# Keep in sync with the python-version build arguments in superset/boil-config.toml.
10+
requires-python = ">=3.10"
11+
# Only openlineage-python is pulled in here. sqlglot and SQLAlchemy are runtime
12+
# dependencies of Apache Superset itself, so they are intentionally NOT declared
13+
# to avoid conflicting with Superset's own pins.
14+
dependencies = [
15+
"openlineage-python>=1.30,<2",
16+
]
17+
18+
[tool.setuptools]
19+
# Only the package is installed; the tests/ subpackage is intentionally excluded
20+
# from the wheel.
21+
packages = ["superset_openlineage"]
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# Licensed to the Apache Software Foundation (ASF) under one
2+
# or more contributor license agreements. See the NOTICE file
3+
# distributed with this work for additional information
4+
# regarding copyright ownership. The ASF licenses this file
5+
# to you under the Apache License, Version 2.0 (the
6+
# "License"); you may not use this file except in compliance
7+
# with the License. You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing,
12+
# software distributed under the License is distributed on an
13+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
# KIND, either express or implied. See the License for the
15+
# specific language governing permissions and limitations
16+
# under the License.
17+
"""OpenLineage integration for Apache Superset (config-only plugin)."""
18+
19+
__all__ = ["build_query_logger", "OpenLineageEventLogger", "init_app"]
20+
21+
22+
# lazy re-export to avoid import cost at config load
23+
def __getattr__(name: str) -> object:
24+
if name == "build_query_logger":
25+
from superset_openlineage.query_logger import build_query_logger
26+
27+
return build_query_logger
28+
if name == "OpenLineageEventLogger":
29+
from superset_openlineage.event_logger import OpenLineageEventLogger
30+
31+
return OpenLineageEventLogger
32+
if name == "init_app":
33+
from superset_openlineage.lifecycle import init_app
34+
35+
return init_app
36+
raise AttributeError(name)
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# Licensed to the Apache Software Foundation (ASF) under one
2+
# or more contributor license agreements. See the NOTICE file
3+
# distributed with this work for additional information
4+
# regarding copyright ownership. The ASF licenses this file
5+
# to you under the Apache License, Version 2.0 (the
6+
# "License"); you may not use this file except in compliance
7+
# with the License. You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing,
12+
# software distributed under the License is distributed on an
13+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
# KIND, either express or implied. See the License for the
15+
# specific language governing permissions and limitations
16+
# under the License.
17+
from __future__ import annotations
18+
19+
import logging
20+
from typing import Any
21+
22+
from superset_openlineage import settings
23+
24+
logger = logging.getLogger(__name__)
25+
26+
_client: Any = None
27+
_initialized: bool = False
28+
29+
30+
def reset_client() -> None:
31+
global _client, _initialized
32+
_client = None
33+
_initialized = False
34+
35+
36+
def get_client() -> Any:
37+
global _client, _initialized
38+
if _initialized:
39+
return _client
40+
_initialized = True
41+
if not settings.is_enabled():
42+
_client = None
43+
return None
44+
try:
45+
from openlineage.client import OpenLineageClient
46+
47+
_client = OpenLineageClient()
48+
except Exception as ex: # noqa: BLE001 - never break Superset on OL init
49+
logger.warning("OpenLineage: client init failed, disabling: %s", ex)
50+
_client = None
51+
return _client
52+
53+
54+
def emit(event: Any) -> None:
55+
client = get_client()
56+
if client is None:
57+
return
58+
try:
59+
client.emit(event)
60+
except Exception as ex: # noqa: BLE001 - emission must never break a query
61+
logger.warning("OpenLineage: emit failed, dropping event: %s", ex)
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
# Licensed to the Apache Software Foundation (ASF) under one
2+
# or more contributor license agreements. See the NOTICE file
3+
# distributed with this work for additional information
4+
# regarding copyright ownership. The ASF licenses this file
5+
# to you under the Apache License, Version 2.0 (the
6+
# "License"); you may not use this file except in compliance
7+
# with the License. You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing,
12+
# software distributed under the License is distributed on an
13+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
# KIND, either express or implied. See the License for the
15+
# specific language governing permissions and limitations
16+
# under the License.
17+
from __future__ import annotations
18+
19+
from contextvars import ContextVar
20+
from dataclasses import dataclass, field
21+
from typing import Any
22+
23+
_PENDING: ContextVar[list["PendingRun"] | None] = ContextVar("ol_pending", default=None)
24+
25+
26+
@dataclass
27+
class PendingRun:
28+
run_id: str
29+
sql: str
30+
dialect: str | None
31+
source: str
32+
sql_hash: str
33+
schema: str | None
34+
catalog: str | None
35+
database: str | None
36+
username: str | None
37+
sql_editor_id: str | None
38+
client_id: str | None
39+
start_time: float
40+
inputs: list[Any] = field(default_factory=list)
41+
outputs: list[Any] = field(default_factory=list)
42+
column_lineage: Any = None
43+
44+
45+
def _stack() -> list[PendingRun]:
46+
stack = _PENDING.get()
47+
if stack is None:
48+
stack = []
49+
_PENDING.set(stack)
50+
return stack
51+
52+
53+
def push_pending(run: PendingRun) -> None:
54+
stack = list(_stack())
55+
stack.append(run)
56+
_PENDING.set(stack)
57+
58+
59+
def pop_pending() -> PendingRun | None:
60+
stack = list(_stack())
61+
if not stack:
62+
return None
63+
run = stack.pop()
64+
_PENDING.set(stack)
65+
return run
66+
67+
68+
def clear_pending() -> None:
69+
_PENDING.set([])

0 commit comments

Comments
 (0)