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}