This is an automated email from the ASF dual-hosted git repository.

mobuchowski pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/main by this push:
     new d9ca6241562 Match file datasets in exclude_datasets without a doubled 
slash (#75921)
d9ca6241562 is described below

commit d9ca6241562a151730406b4c6015087e1f3a24ed
Author: Ramit Kataria <[email protected]>
AuthorDate: Thu Oct 29 01:59:48 2026 -0700

    Match file datasets in exclude_datasets without a doubled slash (#75921)
    
    File and other path-based OpenLineage datasets have names that start with 
"/",
    so joining namespace and name with another "/" made file://host1/x match 
only
    as file://host1//x, and the pattern a user would naturally write never 
matched.
    The feature is unreleased, so the format can still change without breaking
    existing policies.
    
    The docs also lacked the way to report a single declared asset instead of
    every object a hook collects, which was the workaround asked for in #63371,
    and did not say that the extras cap limits SQL hook lineage the same way.
---
 providers/openlineage/docs/emission_policy.rst              | 13 ++++++++++---
 .../airflow/providers/openlineage/api/emission_policy.py    |  3 ++-
 .../airflow/providers/openlineage/utils/emission_policy.py  |  8 ++++++--
 .../tests/unit/openlineage/utils/test_emission_policy.py    |  7 +++++++
 4 files changed, 25 insertions(+), 6 deletions(-)

diff --git a/providers/openlineage/docs/emission_policy.rst 
b/providers/openlineage/docs/emission_policy.rst
index 06488dfcafd..d8c331e3493 100644
--- a/providers/openlineage/docs/emission_policy.rst
+++ b/providers/openlineage/docs/emission_policy.rst
@@ -282,7 +282,10 @@ example the per-chunk objects of a multipart upload that 
hook lineage reports on
 
 - Patterns are always matched with ``re.fullmatch``, independent of 
``match_mode``, against
   ``<namespace>/<name>`` of the OpenLineage dataset: 
``s3://my-bucket/staging/part_1`` for a
-  dataset with namespace ``s3://my-bucket`` and name ``staging/part_1``. 
Airflow assets are
+  dataset with namespace ``s3://my-bucket`` and name ``staging/part_1``. Names 
that already start
+  with ``/``, such as file paths, get no extra ``/``: 
``file://host1/staging/part_1`` for
+  namespace ``file://host1`` and name ``/staging/part_1``, and 
``file/tmp/part_1`` for a local
+  file with namespace ``file``. Airflow assets are
   matched after translation to OpenLineage, so assets that cannot be 
translated are never
   reported, whatever the patterns.
 - They apply to every source of task lineage: extractor results, 
hook-collected assets,
@@ -294,7 +297,10 @@ example the per-chunk objects of a multipart upload that 
hook lineage reports on
   that case, since the event exists only to carry the datasets.
 - Excluding datasets never changes which source is used. If an extractor 
returns datasets that
   are all excluded, hook lineage is not consulted as a fallback; if hook 
lineage collected assets
-  that are all excluded, manually declared ``inlets`` / ``outlets`` are not 
used either.
+  that are all excluded, manually declared ``inlets`` / ``outlets`` are not 
used either. To
+  report your own assets instead of the ones a hook collects, for example one 
asset for an upload
+  prefix instead of one per uploaded part, set ``hook_lineage: false`` for the 
task and declare the
+  asset in ``outlets``.
 - A rule whose ``scope`` has a ``hook`` key only applies to datasets reported 
by that hook, and
   only to hook lineage and SQL hook lineage (the other sources have no 
reporting hook). Such a
   rule may only carry ``exclude_datasets`` or ``hook_lineage``; with a 
``hook`` scope,
@@ -343,7 +349,8 @@ In the example above:
     OpenLineage reads the hook lineage collector, after
     :ref:`[lineage] 
max_assets_per_collector<config:lineage__max_assets_per_collector>` has
     limited what the collector kept. Excluding assets reduces what gets 
reported, but it does not
-    make room for assets the cap already discarded.
+    make room for assets the cap already discarded. The same applies to SQL 
hook lineage queries
+    and :ref:`[lineage] 
max_extras_per_collector<config:lineage__max_extras_per_collector>`.
 
 .. note::
 
diff --git 
a/providers/openlineage/src/airflow/providers/openlineage/api/emission_policy.py
 
b/providers/openlineage/src/airflow/providers/openlineage/api/emission_policy.py
index 652b89d4e3f..50d3d9489e6 100644
--- 
a/providers/openlineage/src/airflow/providers/openlineage/api/emission_policy.py
+++ 
b/providers/openlineage/src/airflow/providers/openlineage/api/emission_policy.py
@@ -189,7 +189,8 @@ def extend_global_openlineage_emission_policy(
     :param hook_lineage: Whether to use ``HookLineageCollector`` as a fallback.
     :param include_full_task_info: Whether to include the full serialized 
operator state.
     :param exclude_datasets: Regex patterns matched with ``re.fullmatch`` 
against
-        ``"<namespace>/<name>"`` of each OpenLineage dataset of the task; 
matching datasets are
+        ``"<namespace>/<name>"`` of each OpenLineage dataset of the task 
(without a doubled ``/``
+        when the name starts with one); matching datasets are
         dropped. Replaces, instead of extending, any list from the 
``emission_policy`` config,
         including lists set for specific hooks.
     :raises ValueError: If ``exclude_datasets`` is not a list of valid regex 
patterns.
diff --git 
a/providers/openlineage/src/airflow/providers/openlineage/utils/emission_policy.py
 
b/providers/openlineage/src/airflow/providers/openlineage/utils/emission_policy.py
index 89a93b94035..39f007cac58 100644
--- 
a/providers/openlineage/src/airflow/providers/openlineage/utils/emission_policy.py
+++ 
b/providers/openlineage/src/airflow/providers/openlineage/utils/emission_policy.py
@@ -80,7 +80,8 @@ Control flag keys (all optional inside ``controls``; the dict 
must be non-empty)
   in the ``AirflowRunFacet``.  When ``false`` (default), only a curated subset 
of
   task attributes is sent.  Only meaningful for task events.  Default: 
``false``.
 - ``exclude_datasets``: list of ``re.fullmatch`` patterns (independent of 
``match_mode``)
-  matched against ``"<namespace>/<name>"`` of each OpenLineage dataset; 
matching datasets
+  matched against ``"<namespace>/<name>"`` of each OpenLineage dataset 
(without a doubled ``/``
+  when the name starts with one); matching datasets
   are dropped from task events and from the SQL hook lineage events of the 
task.  Applies
   to extractor, hook-collected and manually declared lineage.  Default: ``[]``.
 
@@ -326,7 +327,10 @@ def get_hook_class_name(hook: object | None) -> str | None:
 
 def _get_dataset_identity(dataset: Dataset) -> str:
     """Return the ``"<namespace>/<name>"`` string that ``exclude_datasets`` 
patterns match."""
-    return f"{dataset.namespace}/{dataset.name}"
+    # File and other path-based datasets have names starting with "/"; joining 
them with another
+    # "/" would make ``file://host/path`` match as ``file://host//path``.
+    separator = "" if dataset.name.startswith("/") else "/"
+    return f"{dataset.namespace}{separator}{dataset.name}"
 
 
 def _get_rule_exclude_patterns(rule: Rule) -> tuple[str, ...] | None:
diff --git 
a/providers/openlineage/tests/unit/openlineage/utils/test_emission_policy.py 
b/providers/openlineage/tests/unit/openlineage/utils/test_emission_policy.py
index 4784bcede1d..d64ceb3a9fc 100644
--- a/providers/openlineage/tests/unit/openlineage/utils/test_emission_policy.py
+++ b/providers/openlineage/tests/unit/openlineage/utils/test_emission_policy.py
@@ -2565,6 +2565,13 @@ class TestExcludeDatasetsResolution:
             pytest.param(
                 ["gs://bucket/staging/.*"], Dataset("gs://bucket", 
"final/out"), False, id="no-match"
             ),
+            pytest.param(
+                ["file://host1/staging/.*"],
+                Dataset("file://host1", "/staging/part_1"),
+                True,
+                id="absolute-name",
+            ),
+            pytest.param(["file/tmp/.*"], Dataset("file", "/tmp/part_1"), 
True, id="absolute-name-no-host"),
         ],
     )
     def test_is_excluded_matches_namespace_and_name(self, patterns, dataset, 
excluded):

Reply via email to