Skip to main content

openlineage_client/
context.rs

1//! Top-level orchestration context contributed per emitted run.
2//!
3//! Different orchestration systems (Airflow, Dagster, Databricks Jobs) model
4//! run/job identity differently. Rather than hardcode a field set, an emitting
5//! integration contributes a [`LineageContext`] per query. The mechanism by
6//! which a context is produced is engine-specific (e.g. DataFusion derives it
7//! from a `SessionState`), so the *provider* trait lives with the engine
8//! integration — this crate owns only the context data and its env conventions.
9
10use serde_json::{Map, Value};
11use uuid::Uuid;
12
13use crate::config::OpenLineageConfig;
14use crate::facets::{BaseFacet, ParentJob, ParentRun, ParentRunFacet};
15
16/// Top-level context an integration contributes to each emitted run.
17///
18/// All fields are optional; unset values fall back to [`OpenLineageConfig`]
19/// defaults and plan-derived identity.
20#[derive(Debug, Default, Clone)]
21pub struct LineageContext {
22    /// Correlate with an orchestrator-owned run id (else a fresh UUIDv7 is used).
23    pub run_id: Option<Uuid>,
24    /// Namespace of the emitting job (else the configured default namespace).
25    pub job_namespace: Option<String>,
26    /// Name of the emitting job (else a plan-derived job name).
27    pub job_name: Option<String>,
28    /// Standard OpenLineage `parent` run facet.
29    pub parent_run: Option<ParentRunFacet>,
30    /// Arbitrary extra run facets merged into the emitted event.
31    pub run_facets: Map<String, Value>,
32    /// Arbitrary extra job facets merged into the emitted event.
33    pub job_facets: Map<String, Value>,
34    /// The SQL text of the query, if the host has it. Populates the `sql` job
35    /// facet. A plan walk cannot recover this, so the integration supplies it
36    /// from the request boundary.
37    pub sql: Option<String>,
38}
39
40impl LineageContext {
41    /// Build a context from the established OpenLineage parent-run environment
42    /// conventions, returning `None`-filled fields when nothing is set.
43    ///
44    /// Reads `OPENLINEAGE_PARENT_ID` (slash form `{namespace}/{name}/{runId}`),
45    /// falling back to the discrete `OPENLINEAGE_PARENT_JOB_NAMESPACE` /
46    /// `OPENLINEAGE_PARENT_JOB_NAME` / `OPENLINEAGE_PARENT_RUN_ID` variables.
47    pub fn from_env(config: &OpenLineageConfig) -> Self {
48        let parent_run = parent_from_env(config);
49        LineageContext {
50            parent_run,
51            ..Default::default()
52        }
53    }
54}
55
56fn parent_from_env(config: &OpenLineageConfig) -> Option<ParentRunFacet> {
57    let (namespace, name, run_id) = if let Ok(parent_id) = std::env::var("OPENLINEAGE_PARENT_ID") {
58        // Format: {namespace}/{name}/{runId}
59        let parts: Vec<&str> = parent_id.splitn(3, '/').collect();
60        match parts.as_slice() {
61            [ns, n, rid] => (ns.to_string(), n.to_string(), rid.to_string()),
62            _ => return None,
63        }
64    } else {
65        let ns = std::env::var("OPENLINEAGE_PARENT_JOB_NAMESPACE").ok()?;
66        let n = std::env::var("OPENLINEAGE_PARENT_JOB_NAME").ok()?;
67        let rid = std::env::var("OPENLINEAGE_PARENT_RUN_ID").ok()?;
68        (ns, n, rid)
69    };
70
71    Some(ParentRunFacet {
72        base: BaseFacet::new(&config.producer, "1-1-0/ParentRunFacet.json"),
73        run: ParentRun { run_id },
74        job: ParentJob { namespace, name },
75        root: None,
76    })
77}
78
79#[cfg(test)]
80mod tests {
81    use super::*;
82
83    const PARENT_VARS: [&str; 4] = [
84        "OPENLINEAGE_PARENT_ID",
85        "OPENLINEAGE_PARENT_JOB_NAMESPACE",
86        "OPENLINEAGE_PARENT_JOB_NAME",
87        "OPENLINEAGE_PARENT_RUN_ID",
88    ];
89
90    fn clear_parent_vars() {
91        for v in PARENT_VARS {
92            unsafe { std::env::remove_var(v) };
93        }
94    }
95
96    // Process env is global, so the parent-from-env cases run in one serialized
97    // test rather than racing as separate `#[test]`s. Each step clears the vars
98    // first, then sets only what it exercises.
99    #[test]
100    fn parent_from_env_covers_all_forms() {
101        let config = OpenLineageConfig::default();
102
103        // No vars set → no parent facet.
104        clear_parent_vars();
105        assert!(parent_from_env(&config).is_none(), "unset env yields None");
106
107        // Slash form `{namespace}/{name}/{runId}`.
108        clear_parent_vars();
109        unsafe { std::env::set_var("OPENLINEAGE_PARENT_ID", "airflow/dag.task/run-7") };
110        let p = parent_from_env(&config).expect("slash form parses");
111        assert_eq!(p.job.namespace, "airflow");
112        assert_eq!(p.job.name, "dag.task");
113        assert_eq!(p.run.run_id, "run-7");
114
115        // Malformed slash form (too few segments) → None.
116        clear_parent_vars();
117        unsafe { std::env::set_var("OPENLINEAGE_PARENT_ID", "only-one-part") };
118        assert!(
119            parent_from_env(&config).is_none(),
120            "a non-triple PARENT_ID is rejected"
121        );
122
123        // Discrete fallback variables.
124        clear_parent_vars();
125        unsafe {
126            std::env::set_var("OPENLINEAGE_PARENT_JOB_NAMESPACE", "dagster");
127            std::env::set_var("OPENLINEAGE_PARENT_JOB_NAME", "asset.build");
128            std::env::set_var("OPENLINEAGE_PARENT_RUN_ID", "run-9");
129        }
130        let p = parent_from_env(&config).expect("discrete vars parse");
131        assert_eq!(p.job.namespace, "dagster");
132        assert_eq!(p.job.name, "asset.build");
133        assert_eq!(p.run.run_id, "run-9");
134
135        // A partial discrete set (missing run id) → None.
136        clear_parent_vars();
137        unsafe {
138            std::env::set_var("OPENLINEAGE_PARENT_JOB_NAMESPACE", "dagster");
139            std::env::set_var("OPENLINEAGE_PARENT_JOB_NAME", "asset.build");
140        }
141        assert!(
142            parent_from_env(&config).is_none(),
143            "missing run id falls through to None"
144        );
145
146        // from_env wires parent_run through and leaves the rest defaulted.
147        clear_parent_vars();
148        unsafe { std::env::set_var("OPENLINEAGE_PARENT_ID", "ns/job/run") };
149        let ctx = LineageContext::from_env(&config);
150        assert!(ctx.parent_run.is_some());
151        assert!(ctx.run_id.is_none() && ctx.sql.is_none());
152
153        clear_parent_vars();
154    }
155}