Skip to content

Commit d1f1752

Browse files
committed
Add OpenLineage extension for Java SDK
Adds sdks/java/extensions/openlineage, which emits OpenLineage events describing the datasets a Beam pipeline reads and writes, so lineage backends (Marquez, Atlan, and other OpenLineage consumers) can build a lineage graph for Beam jobs on any runner. The extension follows the architecture of the official OpenLineage Spark and Flink integrations: - Configuration extends io.openlineage.client.OpenLineageConfig and is resolved with the same precedence as the Spark integration: pipeline options over openlineage.yml over OPENLINEAGE__ env vars. Transport, facets, dataset and circuit-breaker settings use the standard openlineage-java config classes. - OpenLineageRunner wraps any runner, extracts datasets from the pipeline graph through classpath-guarded per-IO visitors (PubsubIO, IcebergIO; extensible via the public LineageProvider interface), mints a UUIDv7 run id at submission and propagates it via serialized options, and emits START and terminal COMPLETE/ABORT/FAIL events. - OpenLineageJobTracker emits periodic RUNNING events on the Flink integration's trackingIntervalInSeconds cadence (default 60s) and sweeps lineage reported through the Beam Lineage metrics API. - OpenLineageLineage implements the LineageBase plugin (--lineageType) to capture lineage live in worker JVMs for long-running streaming jobs, teeing FQNs back into the metrics store. - Dataset naming strictly follows the OpenLineage naming conventions; Iceberg datasets use the physical table location with the catalog table identity attached as a TABLE symlink, matching the Spark integration's IcebergHandler. Addresses #39427.
1 parent 477c747 commit d1f1752

27 files changed

Lines changed: 2687 additions & 0 deletions

CHANGES.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@
6969
## New Features / Improvements
7070

7171
* (Python) Removed the `envoy-data-plane` (and transitive `betterproto`) dependency; `EnvoyRateLimiter` now uses a small vendored protobuf definition instead, resolving dependency conflicts for downstream projects ([#37854](https://github.com/apache/beam/issues/37854)).
72+
* Added an OpenLineage extension (`sdks/java/extensions/openlineage`) that emits OpenLineage events describing the datasets a pipeline reads and writes, via a wrapper runner (`--runner=OpenLineageRunner`) and a pluggable lineage backend (`--lineageType`) (Java) ([#39427](https://github.com/apache/beam/issues/39427)).
7273
* X feature added (Java/Python) ([#X](https://github.com/apache/beam/issues/X)).
7374

7475
## Breaking Changes

build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,7 @@ tasks.register("javaPreCommit") {
299299
dependsOn(":sdks:java:extensions:join-library:build")
300300
dependsOn(":sdks:java:extensions:kryo:build")
301301
dependsOn(":sdks:java:extensions:ml:build")
302+
dependsOn(":sdks:java:extensions:openlineage:build")
302303
dependsOn(":sdks:java:extensions:protobuf:build")
303304
dependsOn(":sdks:java:extensions:python:build")
304305
dependsOn(":sdks:java:extensions:sbe:build")
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* License); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an AS IS BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
19+
plugins { id 'org.apache.beam.module' }
20+
applyJavaNature(
21+
automaticModuleName: 'org.apache.beam.sdk.extensions.openlineage',
22+
// matches sdks/java/io/iceberg, which this module inspects at test time
23+
requireJavaVersion: JavaVersion.VERSION_11,
24+
)
25+
26+
description = "Apache Beam :: SDKs :: Java :: Extensions :: OpenLineage"
27+
ext.summary = "Emit OpenLineage events for Beam pipelines."
28+
29+
def openlineage_version = "1.51.0"
30+
31+
dependencies {
32+
implementation project(path: ":sdks:java:core", configuration: "shadow")
33+
implementation "io.openlineage:openlineage-java:$openlineage_version"
34+
implementation library.java.jackson_annotations
35+
implementation library.java.jackson_core
36+
implementation library.java.slf4j_api
37+
implementation library.java.vendored_guava_32_1_2_jre
38+
// IO modules are compile-only: transform-specific lineage visitors activate at runtime only
39+
// when the corresponding IO is already on the user's classpath.
40+
compileOnly project(path: ":sdks:java:io:google-cloud-platform")
41+
compileOnly project(path: ":sdks:java:io:iceberg")
42+
compileOnly "org.apache.iceberg:iceberg-api:1.10.0"
43+
testImplementation library.java.junit
44+
testImplementation library.java.hamcrest
45+
testImplementation project(path: ":sdks:java:core", configuration: "shadowTest")
46+
testImplementation project(path: ":sdks:java:io:google-cloud-platform")
47+
testImplementation project(path: ":sdks:java:io:iceberg")
48+
testImplementation "org.apache.iceberg:iceberg-api:1.10.0"
49+
testImplementation "org.apache.iceberg:iceberg-core:1.10.0"
50+
testImplementation library.java.hadoop_common
51+
testRuntimeOnly project(path: ":runners:direct-java", configuration: "shadow")
52+
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
package org.apache.beam.sdk.extensions.openlineage;
19+
20+
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
21+
import com.fasterxml.jackson.annotation.JsonProperty;
22+
import io.openlineage.client.OpenLineageConfig;
23+
import io.openlineage.client.job.JobConfig;
24+
import org.checkerframework.checker.nullness.qual.Nullable;
25+
26+
/**
27+
* OpenLineage configuration for Beam pipelines, extending the generic {@link OpenLineageConfig}
28+
* from openlineage-java with Beam-specific entries — the same pattern as {@code
29+
* SparkOpenLineageConfig} and {@code FlinkOpenLineageConfig} in the official integrations.
30+
*
31+
* <p>Deserialized from {@code openlineage.yml} or {@code OPENLINEAGE__} environment variables and
32+
* merged with values from {@link OpenLineagePipelineOptions}.
33+
*/
34+
@JsonIgnoreProperties(ignoreUnknown = true)
35+
public class BeamOpenLineageConfig extends OpenLineageConfig<BeamOpenLineageConfig> {
36+
37+
/** Default interval between RUNNING events, matching the Flink integration. */
38+
static final int DEFAULT_TRACKING_INTERVAL_SECONDS = 60;
39+
40+
@JsonProperty("trackingIntervalInSeconds")
41+
private @Nullable Integer trackingIntervalInSeconds;
42+
43+
public @Nullable Integer getTrackingIntervalInSeconds() {
44+
return trackingIntervalInSeconds;
45+
}
46+
47+
public void setTrackingIntervalInSeconds(@Nullable Integer trackingIntervalInSeconds) {
48+
this.trackingIntervalInSeconds = trackingIntervalInSeconds;
49+
}
50+
51+
@Override
52+
public BeamOpenLineageConfig mergeWithNonNull(BeamOpenLineageConfig other) {
53+
BeamOpenLineageConfig merged = new BeamOpenLineageConfig();
54+
merged.setTransportConfig(mergePropertyWith(getTransportConfig(), other.getTransportConfig()));
55+
merged.setFacetsConfig(mergePropertyWith(getFacetsConfig(), other.getFacetsConfig()));
56+
merged.setDatasetConfig(mergePropertyWith(getDatasetConfig(), other.getDatasetConfig()));
57+
merged.setCircuitBreaker(mergePropertyWith(getCircuitBreaker(), other.getCircuitBreaker()));
58+
merged.setMetricsConfig(mergePropertyWith(getMetricsConfig(), other.getMetricsConfig()));
59+
merged.setRunConfig(mergePropertyWith(getRunConfig(), other.getRunConfig()));
60+
JobConfig mergedJobConfig = mergeJobConfig(getJobConfig(), other.getJobConfig());
61+
if (mergedJobConfig != null) {
62+
merged.setJobConfig(mergedJobConfig);
63+
}
64+
merged.setTrackingIntervalInSeconds(
65+
mergePropertyWith(trackingIntervalInSeconds, other.getTrackingIntervalInSeconds()));
66+
return merged;
67+
}
68+
69+
/**
70+
* Merges job configs field by field. {@link JobConfig#mergeWithNonNull} assumes a non-null owners
71+
* map on both sides and throws otherwise, so it cannot be used directly on configs assembled from
72+
* pipeline options.
73+
*/
74+
private static @Nullable JobConfig mergeJobConfig(
75+
@Nullable JobConfig base, @Nullable JobConfig overrides) {
76+
if (base == null) {
77+
return overrides;
78+
}
79+
if (overrides == null) {
80+
return base;
81+
}
82+
JobConfig merged = new JobConfig();
83+
merged.setNamespace(
84+
overrides.getNamespace() != null ? overrides.getNamespace() : base.getNamespace());
85+
merged.setName(overrides.getName() != null ? overrides.getName() : base.getName());
86+
merged.setOwners(overrides.getOwners() != null ? overrides.getOwners() : base.getOwners());
87+
merged.setTags(overrides.getTags() != null ? overrides.getTags() : base.getTags());
88+
return merged;
89+
}
90+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
package org.apache.beam.sdk.extensions.openlineage;
19+
20+
import com.fasterxml.jackson.core.type.TypeReference;
21+
import io.openlineage.client.DefaultConfigPathProvider;
22+
import io.openlineage.client.OpenLineageClientUtils;
23+
import io.openlineage.client.job.JobConfig;
24+
import org.slf4j.Logger;
25+
import org.slf4j.LoggerFactory;
26+
27+
/**
28+
* Resolves the effective {@link BeamOpenLineageConfig} for a pipeline, mirroring the Spark
29+
* integration's {@code ArgumentParser}: pipeline options take precedence over {@code
30+
* openlineage.yml}, which takes precedence over {@code OPENLINEAGE__} environment variables.
31+
*/
32+
class BeamOpenLineageConfigParser {
33+
34+
private static final Logger LOG = LoggerFactory.getLogger(BeamOpenLineageConfigParser.class);
35+
private static final TypeReference<BeamOpenLineageConfig> TYPE_REFERENCE =
36+
new TypeReference<BeamOpenLineageConfig>() {};
37+
38+
private BeamOpenLineageConfigParser() {}
39+
40+
static BeamOpenLineageConfig parse(OpenLineagePipelineOptions options) {
41+
BeamOpenLineageConfig fromEnv = loadFromEnvVars();
42+
BeamOpenLineageConfig fromFile = loadFromFile();
43+
BeamOpenLineageConfig fromOptions = fromOptions(options);
44+
return fromEnv.mergeWith(fromFile).mergeWith(fromOptions);
45+
}
46+
47+
private static BeamOpenLineageConfig loadFromFile() {
48+
try {
49+
return OpenLineageClientUtils.loadOpenLineageConfigYaml(
50+
new DefaultConfigPathProvider(), TYPE_REFERENCE);
51+
} catch (RuntimeException e) {
52+
LOG.debug("No openlineage.yml configuration found", e);
53+
return new BeamOpenLineageConfig();
54+
}
55+
}
56+
57+
private static BeamOpenLineageConfig loadFromEnvVars() {
58+
try {
59+
return OpenLineageClientUtils.loadOpenLineageConfigFromEnvVars(TYPE_REFERENCE);
60+
} catch (RuntimeException e) {
61+
LOG.debug("No OPENLINEAGE__ environment configuration found", e);
62+
return new BeamOpenLineageConfig();
63+
}
64+
}
65+
66+
private static BeamOpenLineageConfig fromOptions(OpenLineagePipelineOptions options) {
67+
BeamOpenLineageConfig config = new BeamOpenLineageConfig();
68+
String namespace = options.getOpenLineageNamespace();
69+
String jobName = options.getOpenLineageJobName();
70+
if (namespace != null || jobName != null) {
71+
JobConfig jobConfig = new JobConfig();
72+
if (namespace != null) {
73+
jobConfig.setNamespace(namespace);
74+
}
75+
if (jobName != null) {
76+
jobConfig.setName(jobName);
77+
}
78+
config.setJobConfig(jobConfig);
79+
}
80+
config.setTrackingIntervalInSeconds(options.getOpenLineageTrackingIntervalInSeconds());
81+
return config;
82+
}
83+
}

0 commit comments

Comments
 (0)