Skip to main content

openlineage_client/
facets.rs

1//! OpenLineage facet types.
2//!
3//! Every facet embeds a [`BaseFacet`] carrying the spec-mandated `_producer` and
4//! `_schemaURL` fields (underscore-prefixed to avoid collisions with the facet
5//! payload). Shapes follow the OpenLineage spec; see
6//! <https://openlineage.io/docs/spec/facets/>.
7
8use serde::{Deserialize, Serialize};
9use serde_json::{Map, Value};
10
11/// Common fields present on every OpenLineage facet.
12///
13/// Flattened into each concrete facet so the serialized JSON carries the
14/// required `_producer` and `_schemaURL` keys.
15#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct BaseFacet {
17    /// URI identifying the software that produced this facet. Maps to the
18    /// `_producer` spec field.
19    #[serde(rename = "_producer")]
20    pub producer: String,
21    /// URI of the JSON schema this facet conforms to. Maps to the `_schemaURL`
22    /// spec field.
23    #[serde(rename = "_schemaURL")]
24    pub schema_url: String,
25}
26
27impl BaseFacet {
28    /// Build a [`BaseFacet`] for `producer` pointing at the facet schema named
29    /// `schema` (e.g. `1-2-0/SchemaDatasetFacet.json`).
30    pub fn new(producer: &str, schema: &str) -> Self {
31        Self {
32            producer: producer.to_string(),
33            schema_url: format!("https://openlineage.io/spec/facets/{schema}"),
34        }
35    }
36}
37
38// ---------------------------------------------------------------------------
39// Run facets
40// ---------------------------------------------------------------------------
41
42/// Bag of run facets. Known facets are typed; anything supplied by a
43/// [`crate::context::LineageContext`] flows through `extra`.
44#[derive(Debug, Clone, Default, Serialize, Deserialize)]
45pub struct RunFacets {
46    /// Identifies the engine executing the run.
47    #[serde(skip_serializing_if = "Option::is_none")]
48    pub processing_engine: Option<ProcessingEngineRunFacet>,
49    /// Links the run to its parent run and job.
50    #[serde(skip_serializing_if = "Option::is_none")]
51    pub parent: Option<ParentRunFacet>,
52    /// The nominal (scheduled) time window for the run. Maps to the
53    /// `nominalTime` spec field.
54    #[serde(rename = "nominalTime", skip_serializing_if = "Option::is_none")]
55    pub nominal_time: Option<NominalTimeRunFacet>,
56    /// Failure details for a failed run. Maps to the `errorMessage` spec field.
57    #[serde(rename = "errorMessage", skip_serializing_if = "Option::is_none")]
58    pub error_message: Option<ErrorMessageRunFacet>,
59    /// Additional custom run facets passed through verbatim.
60    #[serde(flatten)]
61    pub extra: Map<String, Value>,
62}
63
64/// The facet a query engine is expected to populate to identify itself.
65#[derive(Debug, Clone, Serialize, Deserialize)]
66pub struct ProcessingEngineRunFacet {
67    /// Common facet fields (`_producer`, `_schemaURL`).
68    #[serde(flatten)]
69    pub base: BaseFacet,
70    /// Version of the processing engine.
71    pub version: String,
72    /// Name of the processing engine (e.g. `spark`, `airflow`).
73    pub name: String,
74    /// Version of the OpenLineage adapter or integration in use. Maps to the
75    /// `openlineageAdapterVersion` spec field.
76    #[serde(rename = "openlineageAdapterVersion")]
77    pub openlineage_adapter_version: String,
78}
79
80/// Links this run to a parent (and optionally root) run/job — set by orchestrators.
81#[derive(Debug, Clone, Serialize, Deserialize)]
82pub struct ParentRunFacet {
83    /// Common facet fields (`_producer`, `_schemaURL`).
84    #[serde(flatten)]
85    pub base: BaseFacet,
86    /// The immediate parent run.
87    pub run: ParentRun,
88    /// The immediate parent job.
89    pub job: ParentJob,
90    /// The root run/job at the top of the parent chain, when distinct from the
91    /// immediate parent.
92    #[serde(skip_serializing_if = "Option::is_none")]
93    pub root: Option<RootParent>,
94}
95
96/// Reference to a parent run by its run identifier.
97#[derive(Debug, Clone, Serialize, Deserialize)]
98pub struct ParentRun {
99    /// Unique identifier of the parent run. Maps to the `runId` spec field.
100    #[serde(rename = "runId")]
101    pub run_id: String,
102}
103
104/// Reference to a parent job by namespace and name.
105#[derive(Debug, Clone, Serialize, Deserialize)]
106pub struct ParentJob {
107    /// Namespace the parent job belongs to.
108    pub namespace: String,
109    /// Name of the parent job.
110    pub name: String,
111}
112
113/// The root run and job at the top of a multi-level parent chain.
114#[derive(Debug, Clone, Serialize, Deserialize)]
115pub struct RootParent {
116    /// The root run.
117    pub run: ParentRun,
118    /// The root job.
119    pub job: ParentJob,
120}
121
122/// The nominal (scheduled) start and end times of a run.
123#[derive(Debug, Clone, Serialize, Deserialize)]
124pub struct NominalTimeRunFacet {
125    /// Common facet fields (`_producer`, `_schemaURL`).
126    #[serde(flatten)]
127    pub base: BaseFacet,
128    /// Scheduled start time as an ISO-8601 timestamp. Maps to the
129    /// `nominalStartTime` spec field.
130    #[serde(rename = "nominalStartTime")]
131    pub nominal_start_time: String,
132    /// Scheduled end time as an ISO-8601 timestamp, if known. Maps to the
133    /// `nominalEndTime` spec field.
134    #[serde(rename = "nominalEndTime", skip_serializing_if = "Option::is_none")]
135    pub nominal_end_time: Option<String>,
136}
137
138/// Failure details attached to a failed run (the `errorMessage` run facet).
139#[derive(Debug, Clone, Serialize, Deserialize)]
140pub struct ErrorMessageRunFacet {
141    /// Common facet fields (`_producer`, `_schemaURL`).
142    #[serde(flatten)]
143    pub base: BaseFacet,
144    /// Human-readable error message.
145    pub message: String,
146    /// Programming language the error originated from. Maps to the
147    /// `programmingLanguage` spec field.
148    #[serde(rename = "programmingLanguage")]
149    pub programming_language: String,
150    /// Full stack trace, if available. Maps to the `stackTrace` spec field.
151    #[serde(rename = "stackTrace", skip_serializing_if = "Option::is_none")]
152    pub stack_trace: Option<String>,
153}
154
155// ---------------------------------------------------------------------------
156// Job facets
157// ---------------------------------------------------------------------------
158
159/// Bag of job facets. Known facets are typed; anything else flows through
160/// `extra`.
161#[derive(Debug, Clone, Default, Serialize, Deserialize)]
162pub struct JobFacets {
163    /// The SQL query backing the job, if any.
164    #[serde(skip_serializing_if = "Option::is_none")]
165    pub sql: Option<SqlJobFacet>,
166    /// Classification of the job (batch/stream, integration, type). Maps to the
167    /// `jobType` spec field.
168    #[serde(rename = "jobType", skip_serializing_if = "Option::is_none")]
169    pub job_type: Option<JobTypeJobFacet>,
170    /// Additional custom job facets passed through verbatim.
171    #[serde(flatten)]
172    pub extra: Map<String, Value>,
173}
174
175/// The SQL query executed by a job (the `sql` job facet).
176#[derive(Debug, Clone, Serialize, Deserialize)]
177pub struct SqlJobFacet {
178    /// Common facet fields (`_producer`, `_schemaURL`).
179    #[serde(flatten)]
180    pub base: BaseFacet,
181    /// The SQL query text.
182    pub query: String,
183}
184
185/// Classifies a job by processing mode, integration, and type (the `jobType`
186/// job facet).
187#[derive(Debug, Clone, Serialize, Deserialize)]
188pub struct JobTypeJobFacet {
189    /// Common facet fields (`_producer`, `_schemaURL`).
190    #[serde(flatten)]
191    pub base: BaseFacet,
192    /// Processing mode, typically `BATCH` or `STREAMING`. Maps to the
193    /// `processingType` spec field.
194    #[serde(rename = "processingType")]
195    pub processing_type: String,
196    /// The integration that produced the job (e.g. `SPARK`, `AIRFLOW`).
197    pub integration: String,
198    /// Integration-specific job type (e.g. `QUERY`, `JOB`). Maps to the
199    /// `jobType` spec field.
200    #[serde(rename = "jobType")]
201    pub job_type: String,
202}
203
204/// Free-text description of a job (the `documentation` job facet).
205#[derive(Debug, Clone, Serialize, Deserialize)]
206pub struct DocumentationJobFacet {
207    /// Common facet fields (`_producer`, `_schemaURL`).
208    #[serde(flatten)]
209    pub base: BaseFacet,
210    /// Free-text description of the job.
211    pub description: String,
212}
213
214/// Who owns a job (the `ownership` job facet).
215#[derive(Debug, Clone, Serialize, Deserialize)]
216pub struct OwnershipJobFacet {
217    /// Common facet fields (`_producer`, `_schemaURL`).
218    #[serde(flatten)]
219    pub base: BaseFacet,
220    /// The owners of the job.
221    pub owners: Vec<Owner>,
222}
223
224/// One owner entry of an [`OwnershipJobFacet`]: a name plus an optional kind
225/// (e.g. `MAINTAINER`, or a custom value like `team` / `user`).
226#[derive(Debug, Clone, Serialize, Deserialize)]
227pub struct Owner {
228    /// Identifier of the owner (e.g. a user or team name).
229    pub name: String,
230    /// Kind of owner (e.g. `MAINTAINER`, `team`, `user`). Maps to the `type`
231    /// spec field.
232    #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
233    pub type_: Option<String>,
234}
235
236/// Business tags on a job (the `tags` job facet).
237#[derive(Debug, Clone, Serialize, Deserialize)]
238pub struct TagsJobFacet {
239    /// Common facet fields (`_producer`, `_schemaURL`).
240    #[serde(flatten)]
241    pub base: BaseFacet,
242    /// The tags applied to the job.
243    pub tags: Vec<TagsJobFacetFields>,
244}
245
246/// One tag of a [`TagsJobFacet`]: a key with an optional value and an optional
247/// source naming the system that assigned the tag.
248#[derive(Debug, Clone, Serialize, Deserialize)]
249pub struct TagsJobFacetFields {
250    /// The tag key.
251    pub key: String,
252    /// The tag value, if any.
253    #[serde(skip_serializing_if = "Option::is_none")]
254    pub value: Option<String>,
255    /// The system that assigned the tag, if known.
256    #[serde(skip_serializing_if = "Option::is_none")]
257    pub source: Option<String>,
258}
259
260// ---------------------------------------------------------------------------
261// Dataset facets
262// ---------------------------------------------------------------------------
263
264/// Bag of dataset facets. Known facets are typed; anything else flows through
265/// `extra`.
266#[derive(Debug, Clone, Default, Serialize, Deserialize)]
267pub struct DatasetFacets {
268    /// The dataset's field-level schema.
269    #[serde(skip_serializing_if = "Option::is_none")]
270    pub schema: Option<SchemaDatasetFacet>,
271    /// The physical data source backing the dataset. Maps to the `dataSource`
272    /// spec field.
273    #[serde(rename = "dataSource", skip_serializing_if = "Option::is_none")]
274    pub data_source: Option<DataSourceDatasetFacet>,
275    /// Column-level lineage. Per spec this facet describes how an **output**
276    /// dataset's fields are derived, so it is only ever attached to outputs.
277    #[serde(rename = "columnLineage", skip_serializing_if = "Option::is_none")]
278    pub column_lineage: Option<ColumnLineageDatasetFacet>,
279    /// Alternate identifiers (symlinks) for the dataset.
280    #[serde(skip_serializing_if = "Option::is_none")]
281    pub symlinks: Option<SymlinksDatasetFacet>,
282    /// The lifecycle state change this run applied to the dataset (e.g. it was
283    /// created or overwritten). Maps to the `lifecycleStateChange` spec field;
284    /// only meaningful on outputs.
285    #[serde(
286        rename = "lifecycleStateChange",
287        skip_serializing_if = "Option::is_none"
288    )]
289    pub lifecycle_state_change: Option<LifecycleStateChangeDatasetFacet>,
290    /// Additional custom dataset facets passed through verbatim.
291    #[serde(flatten)]
292    pub extra: Map<String, Value>,
293}
294
295/// Output-only dataset facets (serialized under `outputFacets`).
296#[derive(Debug, Clone, Default, Serialize, Deserialize)]
297pub struct OutputDatasetFacets {
298    /// Statistics about the data written to the output. Maps to the
299    /// `outputStatistics` spec field.
300    #[serde(rename = "outputStatistics", skip_serializing_if = "Option::is_none")]
301    pub output_statistics: Option<OutputStatisticsOutputDatasetFacet>,
302    /// Additional custom output dataset facets passed through verbatim.
303    #[serde(flatten)]
304    pub extra: Map<String, Value>,
305}
306
307impl OutputDatasetFacets {
308    /// Returns `true` when no output facets are present.
309    pub fn is_empty(&self) -> bool {
310        self.output_statistics.is_none() && self.extra.is_empty()
311    }
312}
313
314/// Runtime statistics about the data written to an output dataset.
315#[derive(Debug, Clone, Serialize, Deserialize)]
316pub struct OutputStatisticsOutputDatasetFacet {
317    /// Common facet fields (`_producer`, `_schemaURL`).
318    #[serde(flatten)]
319    pub base: BaseFacet,
320    /// Number of rows written. Maps to the `rowCount` spec field.
321    #[serde(rename = "rowCount", skip_serializing_if = "Option::is_none")]
322    pub row_count: Option<i64>,
323    /// Size in bytes of the data written.
324    #[serde(skip_serializing_if = "Option::is_none")]
325    pub size: Option<i64>,
326    /// Number of files written. Maps to the `fileCount` spec field.
327    #[serde(rename = "fileCount", skip_serializing_if = "Option::is_none")]
328    pub file_count: Option<i64>,
329}
330
331/// Input-only dataset facets (serialized under `inputFacets`).
332#[derive(Debug, Clone, Default, Serialize, Deserialize)]
333pub struct InputDatasetFacets {
334    /// Statistics about the data read from the input. Maps to the
335    /// `inputStatistics` spec field.
336    #[serde(rename = "inputStatistics", skip_serializing_if = "Option::is_none")]
337    pub input_statistics: Option<InputStatisticsInputDatasetFacet>,
338    /// Additional custom input dataset facets passed through verbatim.
339    #[serde(flatten)]
340    pub extra: Map<String, Value>,
341}
342
343impl InputDatasetFacets {
344    /// Returns `true` when no input facets are present.
345    pub fn is_empty(&self) -> bool {
346        self.input_statistics.is_none() && self.extra.is_empty()
347    }
348}
349
350/// Runtime statistics about the data read from an input dataset.
351#[derive(Debug, Clone, Serialize, Deserialize)]
352pub struct InputStatisticsInputDatasetFacet {
353    /// Common facet fields (`_producer`, `_schemaURL`).
354    #[serde(flatten)]
355    pub base: BaseFacet,
356    /// Number of rows read. Maps to the `rowCount` spec field.
357    #[serde(rename = "rowCount", skip_serializing_if = "Option::is_none")]
358    pub row_count: Option<i64>,
359    /// Size in bytes of the data read.
360    #[serde(skip_serializing_if = "Option::is_none")]
361    pub size: Option<i64>,
362    /// Number of files read. Maps to the `fileCount` spec field.
363    #[serde(rename = "fileCount", skip_serializing_if = "Option::is_none")]
364    pub file_count: Option<i64>,
365}
366
367/// The field-level schema of a dataset (the `schema` dataset facet).
368#[derive(Debug, Clone, Serialize, Deserialize)]
369pub struct SchemaDatasetFacet {
370    /// Common facet fields (`_producer`, `_schemaURL`).
371    #[serde(flatten)]
372    pub base: BaseFacet,
373    /// The dataset's fields, in order.
374    pub fields: Vec<SchemaField>,
375}
376
377/// One field of a [`SchemaDatasetFacet`].
378#[derive(Debug, Clone, Serialize, Deserialize)]
379pub struct SchemaField {
380    /// Name of the field.
381    pub name: String,
382    /// Data type of the field. Maps to the `type` spec field.
383    #[serde(rename = "type")]
384    pub type_: String,
385    /// Optional human-readable description of the field.
386    #[serde(skip_serializing_if = "Option::is_none")]
387    pub description: Option<String>,
388}
389
390/// The physical data source backing a dataset (the `dataSource` dataset facet).
391#[derive(Debug, Clone, Serialize, Deserialize)]
392pub struct DataSourceDatasetFacet {
393    /// Common facet fields (`_producer`, `_schemaURL`).
394    #[serde(flatten)]
395    pub base: BaseFacet,
396    /// Name of the data source.
397    pub name: String,
398    /// URI of the data source.
399    pub uri: String,
400}
401
402/// Alternate identifiers for a dataset (the `symlinks` dataset facet).
403#[derive(Debug, Clone, Serialize, Deserialize)]
404pub struct SymlinksDatasetFacet {
405    /// Common facet fields (`_producer`, `_schemaURL`).
406    #[serde(flatten)]
407    pub base: BaseFacet,
408    /// The alternate identifiers for the dataset.
409    pub identifiers: Vec<SymlinkIdentifier>,
410}
411
412/// One alternate identifier of a [`SymlinksDatasetFacet`].
413#[derive(Debug, Clone, Serialize, Deserialize)]
414pub struct SymlinkIdentifier {
415    /// Namespace of the alternate identifier.
416    pub namespace: String,
417    /// Name of the alternate identifier.
418    pub name: String,
419    /// Kind of identifier (e.g. `TABLE`). Maps to the `type` spec field.
420    #[serde(rename = "type")]
421    pub type_: String,
422}
423
424/// The lifecycle state change a run applied to a dataset (the
425/// `lifecycleStateChange` dataset facet).
426#[derive(Debug, Clone, Serialize, Deserialize)]
427pub struct LifecycleStateChangeDatasetFacet {
428    /// Common facet fields (`_producer`, `_schemaURL`).
429    #[serde(flatten)]
430    pub base: BaseFacet,
431    /// The state change applied. One of the spec's enum values: `ALTER`,
432    /// `CREATE`, `DROP`, `OVERWRITE`, `RENAME`, `TRUNCATE`.
433    #[serde(rename = "lifecycleStateChange")]
434    pub lifecycle_state_change: String,
435}
436
437/// How an output dataset's fields derive from input dataset fields.
438///
439/// Keyed by output field name; the spec defines this facet on output datasets
440/// (consumers ignore it on inputs). Fed by the positional plan resolution in
441/// `crate::column`.
442#[derive(Debug, Clone, Serialize, Deserialize)]
443pub struct ColumnLineageDatasetFacet {
444    /// Common facet fields (`_producer`, `_schemaURL`).
445    #[serde(flatten)]
446    pub base: BaseFacet,
447    /// Output field name -> the input fields it derives from.
448    pub fields: std::collections::BTreeMap<String, FieldLineage>,
449    /// Dataset-level (whole-row) influences not tied to a single output column.
450    #[serde(default, skip_serializing_if = "Vec::is_empty")]
451    pub dataset: Vec<InputField>,
452}
453
454/// Provenance of a single output column.
455#[derive(Debug, Clone, Serialize, Deserialize)]
456pub struct FieldLineage {
457    /// The input fields the output column derives from. Maps to the
458    /// `inputFields` spec field.
459    #[serde(rename = "inputFields")]
460    pub input_fields: Vec<InputField>,
461}
462
463/// A source `(dataset, field)` an output column derives from, with how.
464#[derive(Debug, Clone, Serialize, Deserialize)]
465pub struct InputField {
466    /// Namespace of the source dataset.
467    pub namespace: String,
468    /// Name of the source dataset.
469    pub name: String,
470    /// Name of the source field within the dataset, if column-specific.
471    #[serde(skip_serializing_if = "Option::is_none")]
472    pub field: Option<String>,
473    /// How the source influences the output.
474    #[serde(default, skip_serializing_if = "Vec::is_empty")]
475    pub transformations: Vec<Transformation>,
476}
477
478/// How an input influences an output: `DIRECT` (the source value flows into the
479/// output) or `INDIRECT` (influence via filter/group/join/sort). `subtype` is
480/// conventionally one of `IDENTITY`, `TRANSFORMATION`, `AGGREGATION`, `FILTER`,
481/// `JOIN`, `GROUP_BY`, `SORT`, `WINDOW`.
482#[derive(Debug, Clone, Serialize, Deserialize)]
483pub struct Transformation {
484    /// Whether the influence is `DIRECT` or `INDIRECT`. Maps to the `type` spec
485    /// field.
486    #[serde(rename = "type")]
487    pub type_: TransformationType,
488    /// More specific classification (e.g. `IDENTITY`, `AGGREGATION`, `FILTER`).
489    #[serde(skip_serializing_if = "Option::is_none")]
490    pub subtype: Option<String>,
491    /// Human-readable description of the transformation.
492    #[serde(default, skip_serializing_if = "String::is_empty")]
493    pub description: String,
494    /// Whether the transformation masks or obfuscates the source value.
495    #[serde(default)]
496    pub masking: bool,
497}
498
499/// Kind of influence an input field has on an output field.
500#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
501#[serde(rename_all = "UPPERCASE")]
502pub enum TransformationType {
503    /// The source value flows directly into the output.
504    Direct,
505    /// The source influences the output indirectly (e.g. via filter, group,
506    /// join, or sort).
507    Indirect,
508}