Skip to main content

openlineage_client/
config.rs

1//! Static configuration shared across emitted events.
2
3use std::time::Duration;
4
5/// Identifies the emitting integration on every emitted event/facet, and tunes
6/// the emit transport.
7#[derive(Debug, Clone)]
8pub struct OpenLineageConfig {
9    /// `producer` URI stamped on events and facets (the emitting code). Engine
10    /// integrations should override this with their own identity.
11    pub producer: String,
12    /// Default job namespace when the context provides none
13    /// (from `OPENLINEAGE_NAMESPACE`, falling back to `"default"`).
14    pub job_namespace: String,
15    /// Engine name for the `processing_engine` run facet. Empty unless an engine
16    /// integration sets it.
17    pub engine_name: String,
18    /// Engine version for the `processing_engine` run facet. Empty unless an
19    /// engine integration sets it.
20    pub engine_version: String,
21    /// This crate's version, for `openlineageAdapterVersion`.
22    pub adapter_version: String,
23    /// Per-request timeout applied by HTTP transports. One slow/hung upstream
24    /// request can otherwise stall the shared background drain; bounding it lets
25    /// the drain keep making progress. See `OPENLINEAGE_TIMEOUT_MS`.
26    pub request_timeout: Duration,
27}
28
29/// Default `producer` URI for this crate. Engine integrations override it with a
30/// producer that names the engine (e.g. `datafusion-openlineage`).
31pub const DEFAULT_PRODUCER: &str =
32    "https://github.com/open-lakehouse/headwaters/openlineage-client";
33
34/// Default job namespace when none is configured or supplied per query.
35pub const DEFAULT_NAMESPACE: &str = "default";
36
37/// Default per-request transport timeout. A balance between tolerating a briefly
38/// slow upstream and not letting one hung request starve every session's events.
39pub const DEFAULT_REQUEST_TIMEOUT: Duration = Duration::from_secs(30);
40
41impl OpenLineageConfig {
42    /// Build a config from the standard OpenLineage environment conventions.
43    ///
44    /// Reads `OPENLINEAGE_NAMESPACE` for the default job namespace (falling back
45    /// to [`DEFAULT_NAMESPACE`]) and `OPENLINEAGE_TIMEOUT_MS` for the request
46    /// timeout (falling back to [`DEFAULT_REQUEST_TIMEOUT`]); the producer/adapter
47    /// identity is fixed for this crate and the engine identity is left empty (an
48    /// engine integration such as `datafusion-openlineage` stamps it). This is
49    /// the documented entry point for env-driven configuration — pair it with
50    /// [`OpenLineageClient::from_env`](crate::OpenLineageClient::from_env), which
51    /// reads `OPENLINEAGE_URL` / `OPENLINEAGE_ENDPOINT` / `OPENLINEAGE_API_KEY`
52    /// for the transport, so an integration can wire itself up entirely from the
53    /// environment the rest of the OpenLineage ecosystem already uses.
54    ///
55    /// [`Default`] is equivalent and reads the same environment; `from_env`
56    /// exists to make the env dependency explicit and discoverable at call sites.
57    pub fn from_env() -> Self {
58        Self::with_env(
59            std::env::var("OPENLINEAGE_NAMESPACE").ok(),
60            std::env::var("OPENLINEAGE_TIMEOUT_MS").ok(),
61        )
62    }
63
64    /// [`Self::from_env`] with the `OPENLINEAGE_NAMESPACE` / `OPENLINEAGE_TIMEOUT_MS`
65    /// values injected, so the fallback logic is unit-testable without mutating
66    /// process-global env. An absent or empty namespace falls back to
67    /// [`DEFAULT_NAMESPACE`]; an absent/empty/unparsable timeout falls back to
68    /// [`DEFAULT_REQUEST_TIMEOUT`].
69    pub fn with_env(namespace: Option<String>, timeout_ms: Option<String>) -> Self {
70        let request_timeout = timeout_ms
71            .as_deref()
72            .filter(|s| !s.is_empty())
73            .and_then(|s| s.parse::<u64>().ok())
74            .map(Duration::from_millis)
75            .unwrap_or(DEFAULT_REQUEST_TIMEOUT);
76        Self {
77            job_namespace: namespace
78                .filter(|ns| !ns.is_empty())
79                .unwrap_or_else(|| DEFAULT_NAMESPACE.to_string()),
80            request_timeout,
81            ..Self::fixed()
82        }
83    }
84
85    /// The fixed (non-environment) fields: producer and adapter identity, with the
86    /// fallback namespace and default request timeout. Shared by [`Self::from_env`]
87    /// and [`Default`].
88    ///
89    /// The engine identity (`engine_name` / `engine_version`) is left empty here —
90    /// this crate is engine-agnostic. An engine integration fills it in (see e.g.
91    /// `datafusion_openlineage::OpenLineageConfig::for_datafusion`).
92    fn fixed() -> Self {
93        Self {
94            producer: DEFAULT_PRODUCER.to_string(),
95            job_namespace: DEFAULT_NAMESPACE.to_string(),
96            engine_name: String::new(),
97            engine_version: String::new(),
98            adapter_version: env!("CARGO_PKG_VERSION").to_string(),
99            request_timeout: DEFAULT_REQUEST_TIMEOUT,
100        }
101    }
102}
103
104impl Default for OpenLineageConfig {
105    fn default() -> Self {
106        Self::from_env()
107    }
108}
109
110#[cfg(test)]
111mod tests {
112    use super::*;
113
114    #[test]
115    fn namespace_falls_back_when_unset() {
116        let cfg = OpenLineageConfig::with_env(None, None);
117        assert_eq!(cfg.job_namespace, DEFAULT_NAMESPACE);
118    }
119
120    #[test]
121    fn empty_namespace_falls_back() {
122        let cfg = OpenLineageConfig::with_env(Some(String::new()), None);
123        assert_eq!(cfg.job_namespace, DEFAULT_NAMESPACE);
124    }
125
126    #[test]
127    fn namespace_is_taken_from_env() {
128        let cfg = OpenLineageConfig::with_env(Some("analytics".to_string()), None);
129        assert_eq!(cfg.job_namespace, "analytics");
130    }
131
132    #[test]
133    fn timeout_parses_from_env() {
134        let cfg = OpenLineageConfig::with_env(None, Some("5000".to_string()));
135        assert_eq!(cfg.request_timeout, Duration::from_millis(5000));
136    }
137
138    #[test]
139    fn timeout_falls_back_when_unparsable() {
140        let cfg = OpenLineageConfig::with_env(None, Some("not-a-number".to_string()));
141        assert_eq!(cfg.request_timeout, DEFAULT_REQUEST_TIMEOUT);
142    }
143
144    #[test]
145    fn fixed_identity_is_engine_neutral() {
146        let cfg = OpenLineageConfig::with_env(Some("x".to_string()), None);
147        assert_eq!(cfg.producer, DEFAULT_PRODUCER);
148        assert_eq!(cfg.adapter_version, env!("CARGO_PKG_VERSION"));
149        // Engine identity is filled in by an engine integration, not this crate.
150        assert!(cfg.engine_name.is_empty());
151        assert!(cfg.engine_version.is_empty());
152    }
153}