Skip to content

base_path is a bare os.getcwd snapshot, breaks OpenLineage file:// locations under absolute config paths #225

Description

@dk107dk

Summary

base_path, written into the Archive Run Manifest and consumed by the
OpenLineage integration to build file:// source locations, is nothing
more than os.getcwd() captured at whatever moment a Metadata object
first accessed the property. It has no defined relationship to the
archive, the run, or the paths it is meant to help resolve. When the
configured input paths are absolute, which is the normal case for a
long-running deployment such as FlightPath Server, using base_path to
build a lineage URL produces a broken, doubled-up path. The same code even
treats the same kind of path two different ways a few lines apart.

Where it comes from

csvpath/managers/metadata.py:51-55, the base Metadata class inherited
by every metadata type:

#
# find base dir so we can add file:// refs, if needed
#
@property
def base_path(self):
    if self._base_path is None:
        self._base_path = os.getcwd()
    return self._base_path

Lazily captured once per Metadata instance, the first time base_path
is read. Nothing ties it to archive_path, to the manifest own location,
or to any project root. It is simply wherever the process happened to be
running from at that moment.

Written to the filesystem manifest only in
csvpath/managers/run/run_registrar.py:48
(m["base_path"] = mdata.base_path, the Archive Run Manifest), though the
property is inherited and available on every metadata type, and is also
sent to the SQL integration tables for files and paths listeners
(csvpath/managers/integrations/sql/sql_file_listener.py:40,
csvpath/managers/integrations/sql/sql_paths_listener.py:35).

What it is actually used for

csvpath/managers/integrations/ol/job.py, the OpenLineage integration,
uses it to build sourceCodeLocation facets:

# build_file_job, line 56
location = f"file:////{mdata.base_path}{os.sep}{mdata.file_path}"

# build_paths_job, line 75
location = f"file:////{mdata.base_path}{os.sep}{mdata.group_file_path}"

This only produces a correct path when file_path / group_file_path are
relative to the process working directory at the exact moment base_path
was captured.

Why it is broken

file_path and group_file_path come from [inputs] files and
[inputs] csvpaths in config.ini (the same keys behind file_home and
named_paths_home). Those keys can be configured as either relative or
absolute paths, and are documented elsewhere in this review as being
absolute under a deployment such as FlightPath Server. Whenever they are
absolute, prepending base_path does not produce a valid path, it
produces an absolute path glued onto the front of another absolute path.

The same file is not even internally consistent about this assumption.
A few lines after building the file:// URL, build_paths_job opens
group_file_path directly, with no base_path prefix at all
(csvpath/managers/integrations/ol/job.py:84-86):

qp = f"{mdata.group_file_path}"
q = ""
with open(qp, "r", encoding="utf-8") as qf:
    q = qf.read()

That only works if group_file_path is directly openable from the current
process working directory, which contradicts the assumption behind the
file:// URL built two lines earlier for the same path.

Separately, since base_path is just os.getcwd() of whichever process
happens to be writing the manifest at the time, in a long-running,
multi-project server process it reflects wherever that process was
launched from, not anything about the specific run, archive, or project.
The value can be effectively arbitrary noise from the point of view of the
manifest reader.

Why this matters beyond OpenLineage

OpenLineage itself is a narrow integration, but lineage correctness is a
core concern: a wrong file:// location silently produces incorrect
provenance data for every file-stage and paths-load job reported to a
lineage consumer, with no error or warning anywhere in the pipeline. This
is worse than an unused or redundant manifest field, it can actively
misinform anything downstream that trusts the lineage record.

Suggested fix direction

Stop deriving OpenLineage source locations from a bare process
os.getcwd() snapshot. Likely directions:

  • Resolve file_path / group_file_path to an absolute path once, at the
    point they are first captured (using Nos, consistent with how the rest
    of the codebase normalizes paths across backends), so nothing downstream
    needs a separate base-path prefix to make sense of them, or
  • If a base directory concept is still wanted for relative paths, derive it
    from something stable and meaningful, for example the config-resolved
    project root, rather than the ambient process working directory.

Either way, job.py build_file_job and build_paths_job should stop
special-casing file_path/group_file_path two different ways (once with
a base_path prefix, once by opening the raw path directly) and treat
them consistently.

Found while reviewing manifest fields for the references v3 work.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions