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}