在某些情况下,业务上需要程序直接构造自定义的元数据事件【MetadataEvent】,然后将该元数据发送到DataHub。
DataHub分别针对python和java提供了emitter 库,如下:
本文基于上面的2个库,讲解如何构造元数据事件,然后将其发送到DataHub
基于Python的元数据摄取系统【acryl-datahub】提供了 REST 和 Kafka emitter API,很容易进行代码集成。
REST Emitter:基于requests 模块进行了一层薄包装,提供了一个通过HTTP发送元数据事件的阻塞接口,主要用于如下2种场景:
Kafka Emitter:基于confluent-kafka的SerializingProducer类之上的一个薄包装,提供了一个非阻塞接口,用于向DataHub发送元数据事件,主要用于如下场景:
注意:
Kafka Emitter使用Avro序列化metadata事件到Kafka
详细构建元数据事件的代码可以参考:acryl-datahub[mysql] 模块中的 datahub.ingestion.source.sql.sql_common 文件中的loop_tables方法
添加元数据后,在DataHub上查看的效果如下:
点击"View In Mysql",即跳转到 externalUrl
中定义的地址,即"https://blog.csdn.net/penriver/"
# -*- coding: utf-8 -*-
import datahub.emitter.mce_builder as builder
from datahub.emitter.rest_emitter import DatahubRestEmitter
from datahub.metadata.schema_classes import *
# 通过REST创建一个DataHub Emitter
emitter = DatahubRestEmitter(gms_server="http://172.25.21.188:8080", extra_headers={})
#测试连接
emitter.test_connection()
# 使用DatasetSnapshotClass构造元数据事件
def add_metadata_by_dataset_snapshot():
dataset_snapshot = DatasetSnapshotClass(
urn=builder.make_dataset_urn("mysql", "user_center.Person"),
aspects=[StatusClass(removed=False)],
)
dataset_properties = DatasetPropertiesClass(
description="用于存储人员记录表",
customProperties={
"governance": "ENABLED",
"归属系统": "用户中心"
},
externalUrl="https://blog.csdn.net/penriver/"
)
dataset_snapshot.aspects.append(dataset_properties)
schema_metadata = SchemaMetadataClass(
schemaName="PersonSchema",
platform=builder.make_data_platform_urn("mysql"),
version=1,
hash="",
platformSchema=MySqlDDLClass(tableSchema=""),
fields=[
SchemaFieldClass(
fieldPath="person_id",
type=SchemaFieldDataTypeClass(type=NumberTypeClass()),
nativeDataType="long",
description="人员标识"
),
SchemaFieldClass(
fieldPath="person_name",
type=SchemaFieldDataTypeClass(type=StringTypeClass()),
nativeDataType="varchar(100)",
description="人员姓名"
),
],
primaryKeys=["person_id"]
)
dataset_snapshot.aspects.append(schema_metadata)
mce = MetadataChangeEventClass(proposedSnapshot=dataset_snapshot)
emitter.emit(mce)
add_metadata_by_dataset_snapshot()
添加元数据后,在DataHub上查看的效果如下:
注意:通过MetadataChangeProposalWrapper构造元数据事件,一次只能更新元数据的一个Aspect的信息,如datasetProperties、schemaMetadata
# -*- coding: utf-8 -*-
import datahub.emitter.mce_builder as builder
from datahub.emitter.mcp import MetadataChangeProposalWrapper
from datahub.emitter.rest_emitter import DatahubRestEmitter
from datahub.metadata.schema_classes import *
# 通过REST创建一个DataHub Emitter
emitter = DatahubRestEmitter(gms_server="http://172.25.21.22:8080", extra_headers={})
#测试连接
emitter.test_connection()
# 使用MetadataChangeProposalWrapper构造元数据事件
def add_metadata_by_me1():
#构造一个数据集属性对象
dataset_properties = DatasetPropertiesClass(description="用于存储人员记录表",
customProperties={
"governance": "ENABLED",
"归属系统": "用户中心"
})
#构造一个MetadataChangeProposalWrapper对象
metadata_event = MetadataChangeProposalWrapper(
entityType="dataset",
changeType=ChangeTypeClass.UPSERT,
entityUrn=builder.make_dataset_urn("mysql", "user_center.Person2"),
aspectName="datasetProperties",
aspect=dataset_properties,
)
# emit 元数据,这是一个阻塞调用
emitter.emit(metadata_event)
def add_metadata_by_me2():
#构造一个数据集SchemaMetadataClass对象
schema_metadata = SchemaMetadataClass(
schemaName="Person2Schema",
platform=builder.make_data_platform_urn("mysql"),
version=1,
hash="",
platformSchema=MySqlDDLClass(tableSchema=""),
fields=[
SchemaFieldClass(
fieldPath="person_id",
type=SchemaFieldDataTypeClass(type=NumberTypeClass()),
nativeDataType="long",
description="人员标识"
),
SchemaFieldClass(
fieldPath="person_name",
type=SchemaFieldDataTypeClass(type=StringTypeClass()),
nativeDataType="varchar(100)",
description="人员姓名"
),
],
primaryKeys=["person_id"]
)
DatasetSnapshotClass
#构造一个MetadataChangeProposalWrapper对象
metadata_event = MetadataChangeProposalWrapper(
entityType="dataset",
changeType=ChangeTypeClass.UPSERT,
entityUrn=builder.make_dataset_urn("mysql", "user_center.Person2"),
aspectName="schemaMetadata",
aspect=schema_metadata,
)
# emit 元数据,这是一个阻塞调用
emitter.emit(metadata_event)
add_metadata_by_me1()
add_metadata_by_me2()
Java emitter 是基于Apache HttpClient库之上的一个轻度封装,它支持元数据的非阻塞emitter,并处理元数据 Aspect的JSON序列化细节。
构造REST Emitter 遵循基于lambda的流畅构建器模式【fluent builder pattern】。配置参数绝大部分与Python Emitter的配置一样。此外,您还可以通过将定制传递给HttpClient构建器来定制在底层构造的HttpClient。
它可以很容易地从基于JVM的系统发出元数据。例如,Spark的数据血缘集成使用Java emitter从Spark作业发出元数据事件。
Java包目前还不支持Kafka emitter,但官方已经计划在后续的版本中提供
Java emitter 中的创建一个maven项目,添加Datahub client依赖
<dependency>
<groupId>io.acrylgroupId>
<artifactId>datahub-clientartifactId>
<version>0.8.24version>
dependency>
<dependency>
<groupId>org.apache.httpcomponentsgroupId>
<artifactId>httpasyncclientartifactId>
<version>4.1.5version>
dependency>
<dependency>
<groupId>org.apache.httpcomponentsgroupId>
<artifactId>httpcoreartifactId>
<version>4.4.15version>
dependency>
示例代码如下: 可以使用阻塞或非阻塞两种方式
注意:目前java emitter存在一些BUG【如属性是中文时,可能导致写入失败】,不如python emitter稳定可靠。
public class SimpleDemo {
private static final Logger logger = LoggerFactory.getLogger(SimpleDemo.class);
public static void main(String[] args) throws IOException, ExecutionException, InterruptedException {
logger.info("创建RestEmitter");
RestEmitter emitter = RestEmitter.create(b ->
b.server("http://172.25.21.22:8080")
.timeoutSec(30)
.customizeHttpAsyncClient(c -> c.setConnectionTimeToLive(30, TimeUnit.SECONDS))
);
logger.info("创建mcpw");
MetadataChangeProposalWrapper mcpw = MetadataChangeProposalWrapper.builder()
.entityType("dataset")
.entityUrn("urn:li:dataset:(urn:li:dataPlatform:mysql,user-center.Person3,PROD)")
.upsert()
.aspect(new DatasetProperties().setDescription("person table"))
.build();
// Blocking call using future
// Future requestFuture = emitter.emit(mcpw, null);
// MetadataWriteResponse mwr = requestFuture.get();
// if (mwr.isSuccess()) {
// logger.info(String.format("成功发送元数据事件: %s", mcpw.getEntityUrn()));
// } else {
// HttpResponse httpResponse = (HttpResponse) mwr.getUnderlyingResponse();
// logger.info(String.format("失败发送元数据事件: %s, aspect: %s 状态码是: %d",
// mcpw.getEntityUrn(), mcpw.getAspectName(), httpResponse.getStatusLine().getStatusCode()));
// }
// Non-blocking using callback
emitter.emit(mcpw, new Callback() {
@Override
public void onCompletion(MetadataWriteResponse response) {
if (response.isSuccess()) {
logger.info(String.format("成功发送元数据事件: %s", mcpw.getEntityUrn()));
} else {
// Get the underlying http response
HttpResponse httpResponse = (HttpResponse) response.getUnderlyingResponse();
logger.info(String.format("失败发送元数据事件: %s, aspect: %s 状态码是: %d",
mcpw.getEntityUrn(), mcpw.getAspectName(), httpResponse.getStatusLine().getStatusCode()));
}
}
@Override
public void onFailure(Throwable exception) {
logger.info(
String.format("失败发送元数据事件: %s, aspect: %s 原因是: %s", mcpw.getEntityUrn(),
mcpw.getAspectName(), exception.getMessage()));
}
});
}
}
metadata-ingestion/as-a-library
metadata-ingestion/#using-as-a-library