Modelos de streaming do Dataflow fornecidos pelo Google
Mantenha tudo organizado com as coleções
Salve e categorize o conteúdo com base nas suas preferências.
O Google fornece um conjunto de modelos de código aberto
(em inglês) do Cloud Dataflow.
Esses modelos do Dataflow ajudam você a resolver tarefas grandes de dados, incluindo importação
e exportação de dados, backup e restauração de dados, além de operações em massa da API. Tudo isso sem o uso de um
ambiente de desenvolvimento dedicado. Os modelos são criados no Apache Beam e usam o Dataflow para
transformar os dados.
A inscrição Pub/Sub para o modelo do BigQuery é um pipeline de streaming que lê
mensagens formatadas em JSON de uma assinatura de Pub/Sub e as grava em uma
tabela do BigQuery. É possível usar o modelo como uma solução rápida para mover
dados do Pub/Sub para BigQuery. O modelo lê mensagens em formato JSON do Pub/Sub
e as converte em elementos do BigQuery.
Requisitos para este pipeline:
O campo data
das mensagens do Pub/Sub precisa usar o formato JSON, conforme descrito neste
guia JSON.
Por exemplo, mensagens com valores no campo data formatados como {"k1":"v1", "k2":"v2"} podem ser inseridos em uma tabela do BigQuery com duas colunas, k1 e k2, com um tipo de dados de string.
O diretório de saída precisa ser criado antes de executar o pipeline. O esquema da tabela precisa corresponder aos objetos JSON de entrada.
Parâmetros do modelo
Parâmetro
Descrição
inputSubscription
O tópico de entrada do Cloud Pub/Sub que será lido, no formato de projects/<project>/subscriptions/<subscription>.
outputTableSpec
O local da tabela de saída do BigQuery, no formato de <my-project>:<my-dataset>.<my-table>
outputDeadletterTable
A tabela do BigQuery para mensagens que não alcançaram a tabela de saída, no formato de <my-project>:<my-dataset>.<my-table>.
Se não existir, será criada durante a execução do pipeline.
Se não for especificada, será usada OUTPUT_TABLE_SPEC_error_records.
javascriptTextTransformGcsPath
(Opcional)
O URI do Cloud Storage do arquivo .js que define a função definida
pelo usuário (UDF, na sigla em inglês) do JavaScript que você quer usar. Por exemplo, gs://my-bucket/my-udfs/my_file.js.
javascriptTextTransformFunctionName
(Opcional)
O nome da função definida pelo usuário (UDF) do JavaScript que você quer usar.
Por exemplo, se o código de função do JavaScript for
myTransform(inJson) { /*...do stuff...*/ }, o nome da função será
myTransform. Para amostras de UDFs do JavaScript, consulte os
exemplos de UDF.
Como executar a inscrição Pub/Sub para o modelo do BigQuery
Console
Acesse a página Criar job usando um modelo do Dataflow.
Opcional: em Endpoint regional, selecione um valor no menu suspenso. O endpoint
regional padrão é us-central1.
Para ver uma lista de regiões em que é possível executar um job do Dataflow, consulte
Locais do Dataflow.
No menu suspenso Modelo do Dataflow, selecione
the Pub/Sub Subscription to BigQuery template.
Nos campos de parâmetro fornecidos, insira os valores de parâmetro.
Cliquem em Executar job.
gcloud
No shell ou no terminal, execute o modelo:
gcloud dataflow jobs run JOB_NAME \
--gcs-location gs://dataflow-templates/VERSION/PubSub_Subscription_to_BigQuery \
--region REGION_NAME \
--staging-location STAGING_LOCATION \
--parameters \
inputSubscription=projects/PROJECT_ID/subscriptions/SUBSCRIPTION_NAME,\
outputTableSpec=PROJECT_ID:DATASET.TABLE_NAME,\
outputDeadletterTable=PROJECT_ID:DATASET.TABLE_NAME
Substitua:
JOB_NAME:
um nome de job de sua escolha
REGION_NAME:
o endpoint regional em que você quer
implantar o job do Dataflow, por exemplo, us-central1
VERSION:
a versão do modelo que você quer usar
Use estes valores:
latest para usar a versão mais recente do modelo, disponível na pasta mãe
não datada no bucket:
gs://dataflow-templates/latest/
o nome da versão, como 2021-09-20-00_RC00, para usar uma versão específica do
modelo, que pode ser aninhada na respectiva pasta mãe datada no bucket:
gs://dataflow-templates
STAGING_LOCATION: o local para organizar arquivos locais (por exemplo, gs://your-bucket/staging).
TEMP_LOCATION: o local para gravar arquivos temporários (por exemplo, gs://your-bucket/temp)
SUBSCRIPTION_NAME: o nome da sua assinatura de Pub/Sub
DATASET: o conjunto de dados do BigQuery
TABLE_NAME: o nome da tabela do BigQuery
API
Para executar o modelo usando a API REST, envie uma solicitação HTTP POST. Para mais informações sobre a
API e os respectivos escopos de autorização, consulte
projects.templates.launch.
PROJECT_ID:
o ID do projeto do Cloud em que você quer executar o job do Dataflow
JOB_NAME:
um nome de job de sua escolha
LOCATION:
o endpoint regional em que você quer
implantar o job do Dataflow, por exemplo, us-central1
VERSION:
a versão do modelo que você quer usar
Use estes valores:
latest para usar a versão mais recente do modelo, disponível na pasta mãe
não datada no bucket:
gs://dataflow-templates/latest/
o nome da versão, como 2021-09-20-00_RC00, para usar uma versão específica do
modelo, que pode ser aninhada na respectiva pasta mãe datada no bucket:
gs://dataflow-templates
STAGING_LOCATION: o local para organizar arquivos locais (por exemplo, gs://your-bucket/staging).
TEMP_LOCATION: o local para gravar arquivos temporários (por exemplo, gs://your-bucket/temp)
SUBSCRIPTION_NAME: o nome da sua assinatura de Pub/Sub
/*
* Copyright (C) 2018 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.cloud.teleport.templates;
import static com.google.cloud.teleport.templates.TextToBigQueryStreaming.wrapBigQueryInsertError;
import com.google.api.services.bigquery.model.TableRow;
import com.google.cloud.teleport.coders.FailsafeElementCoder;
import com.google.cloud.teleport.metadata.Template;
import com.google.cloud.teleport.metadata.TemplateCategory;
import com.google.cloud.teleport.metadata.TemplateCreationParameter;
import com.google.cloud.teleport.metadata.TemplateParameter;
import com.google.cloud.teleport.templates.PubSubToBigQuery.Options;
import com.google.cloud.teleport.templates.common.BigQueryConverters.FailsafeJsonToTableRow;
import com.google.cloud.teleport.templates.common.ErrorConverters;
import com.google.cloud.teleport.templates.common.JavascriptTextTransformer.FailsafeJavascriptUdf;
import com.google.cloud.teleport.templates.common.JavascriptTextTransformer.JavascriptTextTransformerOptions;
import com.google.cloud.teleport.util.DualInputNestedValueProvider;
import com.google.cloud.teleport.util.DualInputNestedValueProvider.TranslatorInput;
import com.google.cloud.teleport.util.ResourceUtils;
import com.google.cloud.teleport.util.ValueProviderUtils;
import com.google.cloud.teleport.values.FailsafeElement;
import com.google.common.collect.ImmutableList;
import java.nio.charset.StandardCharsets;
import org.apache.beam.sdk.Pipeline;
import org.apache.beam.sdk.PipelineResult;
import org.apache.beam.sdk.coders.CoderRegistry;
import org.apache.beam.sdk.coders.StringUtf8Coder;
import org.apache.beam.sdk.io.gcp.bigquery.BigQueryIO;
import org.apache.beam.sdk.io.gcp.bigquery.BigQueryIO.Write.CreateDisposition;
import org.apache.beam.sdk.io.gcp.bigquery.BigQueryIO.Write.WriteDisposition;
import org.apache.beam.sdk.io.gcp.bigquery.BigQueryInsertError;
import org.apache.beam.sdk.io.gcp.bigquery.InsertRetryPolicy;
import org.apache.beam.sdk.io.gcp.bigquery.WriteResult;
import org.apache.beam.sdk.io.gcp.pubsub.PubsubIO;
import org.apache.beam.sdk.io.gcp.pubsub.PubsubMessage;
import org.apache.beam.sdk.io.gcp.pubsub.PubsubMessageWithAttributesCoder;
import org.apache.beam.sdk.options.Default;
import org.apache.beam.sdk.options.Description;
import org.apache.beam.sdk.options.PipelineOptions;
import org.apache.beam.sdk.options.PipelineOptionsFactory;
import org.apache.beam.sdk.options.ValueProvider;
import org.apache.beam.sdk.transforms.DoFn;
import org.apache.beam.sdk.transforms.Flatten;
import org.apache.beam.sdk.transforms.MapElements;
import org.apache.beam.sdk.transforms.PTransform;
import org.apache.beam.sdk.transforms.ParDo;
import org.apache.beam.sdk.transforms.SerializableFunction;
import org.apache.beam.sdk.values.PCollection;
import org.apache.beam.sdk.values.PCollectionList;
import org.apache.beam.sdk.values.PCollectionTuple;
import org.apache.beam.sdk.values.TupleTag;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link PubSubToBigQuery} pipeline is a streaming pipeline which ingests data in JSON format
* from Cloud Pub/Sub, executes a UDF, and outputs the resulting records to BigQuery. Any errors
* which occur in the transformation of the data or execution of the UDF will be output to a
* separate errors table in BigQuery. The errors table will be created if it does not exist prior to
* execution. Both output and error tables are specified by the user as template parameters.
*
* <p><b>Pipeline Requirements</b>
*
* <ul>
* <li>The Pub/Sub topic exists.
* <li>The BigQuery output table exists.
* </ul>
*
* <p><b>Example Usage</b>
*
* <pre>
* # Set the pipeline vars
* PROJECT_ID=PROJECT ID HERE
* BUCKET_NAME=BUCKET NAME HERE
* PIPELINE_FOLDER=gs://${BUCKET_NAME}/dataflow/pipelines/pubsub-to-bigquery
* USE_SUBSCRIPTION=true or false depending on whether the pipeline should read
* from a Pub/Sub Subscription or a Pub/Sub Topic.
*
* # Set the runner
* RUNNER=DataflowRunner
*
* # Build the template
* mvn compile exec:java \
* -Dexec.mainClass=com.google.cloud.teleport.templates.PubSubToBigQuery \
* -Dexec.cleanupDaemonThreads=false \
* -Dexec.args=" \
* --project=${PROJECT_ID} \
* --stagingLocation=${PIPELINE_FOLDER}/staging \
* --tempLocation=${PIPELINE_FOLDER}/temp \
* --templateLocation=${PIPELINE_FOLDER}/template \
* --runner=${RUNNER}
* --useSubscription=${USE_SUBSCRIPTION}
* "
*
* # Execute the template
* JOB_NAME=pubsub-to-bigquery-$USER-`date +"%Y%m%d-%H%M%S%z"`
*
* # Execute a pipeline to read from a Topic.
* gcloud dataflow jobs run ${JOB_NAME} \
* --gcs-location=${PIPELINE_FOLDER}/template \
* --zone=us-east1-d \
* --parameters \
* "inputTopic=projects/${PROJECT_ID}/topics/input-topic-name,\
* outputTableSpec=${PROJECT_ID}:dataset-id.output-table,\
* outputDeadletterTable=${PROJECT_ID}:dataset-id.deadletter-table"
*
* # Execute a pipeline to read from a Subscription.
* gcloud dataflow jobs run ${JOB_NAME} \
* --gcs-location=${PIPELINE_FOLDER}/template \
* --zone=us-east1-d \
* --parameters \
* "inputSubscription=projects/${PROJECT_ID}/subscriptions/input-subscription-name,\
* outputTableSpec=${PROJECT_ID}:dataset-id.output-table,\
* outputDeadletterTable=${PROJECT_ID}:dataset-id.deadletter-table"
* </pre>
*/
@Template(
name = "PubSub_Subscription_to_BigQuery",
category = TemplateCategory.STREAMING,
displayName = "Pub/Sub Subscription to BigQuery",
description =
"Streaming pipeline. Ingests JSON-encoded messages from a Pub/Sub subscription, transforms"
+ " them using a JavaScript user-defined function (UDF), and writes them to a"
+ " pre-existing BigQuery table as BigQuery elements.",
optionsClass = Options.class,
skipOptions = "inputTopic",
contactInformation = "https://cloud.google.com/support")
@Template(
name = "PubSub_to_BigQuery",
category = TemplateCategory.STREAMING,
displayName = "Pub/Sub Topic to BigQuery",
description =
"Streaming pipeline. Ingests JSON-encoded messages from a Pub/Sub topic, transforms them"
+ " using a JavaScript user-defined function (UDF), and writes them to a pre-existing"
+ " BigQuery table as BigQuery elements.",
optionsClass = Options.class,
skipOptions = "inputSubscription",
contactInformation = "https://cloud.google.com/support")
public class PubSubToBigQuery {
/** The log to output status messages to. */
private static final Logger LOG = LoggerFactory.getLogger(PubSubToBigQuery.class);
/** The tag for the main output for the UDF. */
public static final TupleTag<FailsafeElement<PubsubMessage, String>> UDF_OUT =
new TupleTag<FailsafeElement<PubsubMessage, String>>() {};
/** The tag for the main output of the json transformation. */
public static final TupleTag<TableRow> TRANSFORM_OUT = new TupleTag<TableRow>() {};
/** The tag for the dead-letter output of the udf. */
public static final TupleTag<FailsafeElement<PubsubMessage, String>> UDF_DEADLETTER_OUT =
new TupleTag<FailsafeElement<PubsubMessage, String>>() {};
/** The tag for the dead-letter output of the json to table row transform. */
public static final TupleTag<FailsafeElement<PubsubMessage, String>> TRANSFORM_DEADLETTER_OUT =
new TupleTag<FailsafeElement<PubsubMessage, String>>() {};
/** The default suffix for error tables if dead letter table is not specified. */
public static final String DEFAULT_DEADLETTER_TABLE_SUFFIX = "_error_records";
/** Pubsub message/string coder for pipeline. */
public static final FailsafeElementCoder<PubsubMessage, String> CODER =
FailsafeElementCoder.of(PubsubMessageWithAttributesCoder.of(), StringUtf8Coder.of());
/** String/String Coder for FailsafeElement. */
public static final FailsafeElementCoder<String, String> FAILSAFE_ELEMENT_CODER =
FailsafeElementCoder.of(StringUtf8Coder.of(), StringUtf8Coder.of());
/**
* The {@link Options} class provides the custom execution options passed by the executor at the
* command-line.
*/
public interface Options extends PipelineOptions, JavascriptTextTransformerOptions {
@TemplateParameter.BigQueryTable(
order = 1,
description = "BigQuery output table",
helpText =
"BigQuery table location to write the output to. The table’s schema must match the "
+ "input JSON objects.")
ValueProvider<String> getOutputTableSpec();
void setOutputTableSpec(ValueProvider<String> value);
@TemplateParameter.PubsubTopic(
order = 2,
description = "Input Pub/Sub topic",
helpText = "The Pub/Sub topic to read the input from.")
ValueProvider<String> getInputTopic();
void setInputTopic(ValueProvider<String> value);
@TemplateParameter.PubsubSubscription(
order = 3,
description = "Pub/Sub input subscription",
helpText =
"Pub/Sub subscription to read the input from, in the format of"
+ " 'projects/your-project-id/subscriptions/your-subscription-name'")
ValueProvider<String> getInputSubscription();
void setInputSubscription(ValueProvider<String> value);
@TemplateCreationParameter(template = "PubSub_to_BigQuery", value = "false")
@TemplateCreationParameter(template = "PubSub_Subscription_to_BigQuery", value = "true")
@Description(
"This determines whether the template reads from a Pub/sub subscription or a topic")
@Default.Boolean(false)
Boolean getUseSubscription();
void setUseSubscription(Boolean value);
@TemplateParameter.BigQueryTable(
order = 5,
optional = true,
description =
"Table for messages failed to reach the output table (i.e., Deadletter table)",
helpText =
"Messages failed to reach the output table for all kind of reasons (e.g., mismatched"
+ " schema, malformed json) are written to this table. It should be in the format"
+ " of \"your-project-id:your-dataset.your-table-name\". If it doesn't exist, it"
+ " will be created during pipeline execution. If not specified,"
+ " \"{outputTableSpec}_error_records\" is used instead.")
ValueProvider<String> getOutputDeadletterTable();
void setOutputDeadletterTable(ValueProvider<String> value);
}
/**
* The main entry-point for pipeline execution. This method will start the pipeline but will not
* wait for it's execution to finish. If blocking execution is required, use the {@link
* PubSubToBigQuery#run(Options)} method to start the pipeline and invoke {@code
* result.waitUntilFinish()} on the {@link PipelineResult}.
*
* @param args The command-line args passed by the executor.
*/
public static void main(String[] args) {
Options options = PipelineOptionsFactory.fromArgs(args).withValidation().as(Options.class);
run(options);
}
/**
* Runs the pipeline to completion with the specified options. This method does not wait until the
* pipeline is finished before returning. Invoke {@code result.waitUntilFinish()} on the result
* object to block until the pipeline is finished running if blocking programmatic execution is
* required.
*
* @param options The execution options.
* @return The pipeline result.
*/
public static PipelineResult run(Options options) {
Pipeline pipeline = Pipeline.create(options);
CoderRegistry coderRegistry = pipeline.getCoderRegistry();
coderRegistry.registerCoderForType(CODER.getEncodedTypeDescriptor(), CODER);
/*
* Steps:
* 1) Read messages in from Pub/Sub
* 2) Transform the PubsubMessages into TableRows
* - Transform message payload via UDF
* - Convert UDF result to TableRow objects
* 3) Write successful records out to BigQuery
* 4) Write failed records out to BigQuery
*/
/*
* Step #1: Read messages in from Pub/Sub
* Either from a Subscription or Topic
*/
PCollection<PubsubMessage> messages = null;
if (options.getUseSubscription()) {
messages =
pipeline.apply(
"ReadPubSubSubscription",
PubsubIO.readMessagesWithAttributes()
.fromSubscription(options.getInputSubscription()));
} else {
messages =
pipeline.apply(
"ReadPubSubTopic",
PubsubIO.readMessagesWithAttributes().fromTopic(options.getInputTopic()));
}
PCollectionTuple convertedTableRows =
messages
/*
* Step #2: Transform the PubsubMessages into TableRows
*/
.apply("ConvertMessageToTableRow", new PubsubMessageToTableRow(options));
/*
* Step #3: Write the successful records out to BigQuery
*/
WriteResult writeResult =
convertedTableRows
.get(TRANSFORM_OUT)
.apply(
"WriteSuccessfulRecords",
BigQueryIO.writeTableRows()
.withoutValidation()
.withCreateDisposition(CreateDisposition.CREATE_NEVER)
.withWriteDisposition(WriteDisposition.WRITE_APPEND)
.withExtendedErrorInfo()
.withMethod(BigQueryIO.Write.Method.STREAMING_INSERTS)
.withFailedInsertRetryPolicy(InsertRetryPolicy.retryTransientErrors())
.to(options.getOutputTableSpec()));
/*
* Step 3 Contd.
* Elements that failed inserts into BigQuery are extracted and converted to FailsafeElement
*/
PCollection<FailsafeElement<String, String>> failedInserts =
writeResult
.getFailedInsertsWithErr()
.apply(
"WrapInsertionErrors",
MapElements.into(FAILSAFE_ELEMENT_CODER.getEncodedTypeDescriptor())
.via((BigQueryInsertError e) -> wrapBigQueryInsertError(e)))
.setCoder(FAILSAFE_ELEMENT_CODER);
/*
* Step #4: Write records that failed table row transformation
* or conversion out to BigQuery deadletter table.
*/
PCollectionList.of(
ImmutableList.of(
convertedTableRows.get(UDF_DEADLETTER_OUT),
convertedTableRows.get(TRANSFORM_DEADLETTER_OUT)))
.apply("Flatten", Flatten.pCollections())
.apply(
"WriteFailedRecords",
ErrorConverters.WritePubsubMessageErrors.newBuilder()
.setErrorRecordsTable(
ValueProviderUtils.maybeUseDefaultDeadletterTable(
options.getOutputDeadletterTable(),
options.getOutputTableSpec(),
DEFAULT_DEADLETTER_TABLE_SUFFIX))
.setErrorRecordsTableSchema(ResourceUtils.getDeadletterTableSchemaJson())
.build());
// 5) Insert records that failed insert into deadletter table
failedInserts.apply(
"WriteFailedRecords",
ErrorConverters.WriteStringMessageErrors.newBuilder()
.setErrorRecordsTable(
ValueProviderUtils.maybeUseDefaultDeadletterTable(
options.getOutputDeadletterTable(),
options.getOutputTableSpec(),
DEFAULT_DEADLETTER_TABLE_SUFFIX))
.setErrorRecordsTableSchema(ResourceUtils.getDeadletterTableSchemaJson())
.build());
return pipeline.run();
}
/**
* If deadletterTable is available, it is returned as is, otherwise outputTableSpec +
* defaultDeadLetterTableSuffix is returned instead.
*/
private static ValueProvider<String> maybeUseDefaultDeadletterTable(
ValueProvider<String> deadletterTable,
ValueProvider<String> outputTableSpec,
String defaultDeadLetterTableSuffix) {
return DualInputNestedValueProvider.of(
deadletterTable,
outputTableSpec,
new SerializableFunction<TranslatorInput<String, String>, String>() {
@Override
public String apply(TranslatorInput<String, String> input) {
String userProvidedTable = input.getX();
String outputTableSpec = input.getY();
if (userProvidedTable == null) {
return outputTableSpec + defaultDeadLetterTableSuffix;
}
return userProvidedTable;
}
});
}
/**
* The {@link PubsubMessageToTableRow} class is a {@link PTransform} which transforms incoming
* {@link PubsubMessage} objects into {@link TableRow} objects for insertion into BigQuery while
* applying an optional UDF to the input. The executions of the UDF and transformation to {@link
* TableRow} objects is done in a fail-safe way by wrapping the element with it's original payload
* inside the {@link FailsafeElement} class. The {@link PubsubMessageToTableRow} transform will
* output a {@link PCollectionTuple} which contains all output and dead-letter {@link
* PCollection}.
*
* <p>The {@link PCollectionTuple} output will contain the following {@link PCollection}:
*
* <ul>
* <li>{@link PubSubToBigQuery#UDF_OUT} - Contains all {@link FailsafeElement} records
* successfully processed by the optional UDF.
* <li>{@link PubSubToBigQuery#UDF_DEADLETTER_OUT} - Contains all {@link FailsafeElement}
* records which failed processing during the UDF execution.
* <li>{@link PubSubToBigQuery#TRANSFORM_OUT} - Contains all records successfully converted from
* JSON to {@link TableRow} objects.
* <li>{@link PubSubToBigQuery#TRANSFORM_DEADLETTER_OUT} - Contains all {@link FailsafeElement}
* records which couldn't be converted to table rows.
* </ul>
*/
static class PubsubMessageToTableRow
extends PTransform<PCollection<PubsubMessage>, PCollectionTuple> {
private final Options options;
PubsubMessageToTableRow(Options options) {
this.options = options;
}
@Override
public PCollectionTuple expand(PCollection<PubsubMessage> input) {
PCollectionTuple udfOut =
input
// Map the incoming messages into FailsafeElements so we can recover from failures
// across multiple transforms.
.apply("MapToRecord", ParDo.of(new PubsubMessageToFailsafeElementFn()))
.apply(
"InvokeUDF",
FailsafeJavascriptUdf.<PubsubMessage>newBuilder()
.setFileSystemPath(options.getJavascriptTextTransformGcsPath())
.setFunctionName(options.getJavascriptTextTransformFunctionName())
.setSuccessTag(UDF_OUT)
.setFailureTag(UDF_DEADLETTER_OUT)
.build());
// Convert the records which were successfully processed by the UDF into TableRow objects.
PCollectionTuple jsonToTableRowOut =
udfOut
.get(UDF_OUT)
.apply(
"JsonToTableRow",
FailsafeJsonToTableRow.<PubsubMessage>newBuilder()
.setSuccessTag(TRANSFORM_OUT)
.setFailureTag(TRANSFORM_DEADLETTER_OUT)
.build());
// Re-wrap the PCollections so we can return a single PCollectionTuple
return PCollectionTuple.of(UDF_OUT, udfOut.get(UDF_OUT))
.and(UDF_DEADLETTER_OUT, udfOut.get(UDF_DEADLETTER_OUT))
.and(TRANSFORM_OUT, jsonToTableRowOut.get(TRANSFORM_OUT))
.and(TRANSFORM_DEADLETTER_OUT, jsonToTableRowOut.get(TRANSFORM_DEADLETTER_OUT));
}
}
/**
* The {@link PubsubMessageToFailsafeElementFn} wraps an incoming {@link PubsubMessage} with the
* {@link FailsafeElement} class so errors can be recovered from and the original message can be
* output to a error records table.
*/
static class PubsubMessageToFailsafeElementFn
extends DoFn<PubsubMessage, FailsafeElement<PubsubMessage, String>> {
@ProcessElement
public void processElement(ProcessContext context) {
PubsubMessage message = context.element();
context.output(
FailsafeElement.of(message, new String(message.getPayload(), StandardCharsets.UTF_8)));
}
}
}
Tópico do Pub/Sub para BigQuery
O tópico do Pub/Sub para o modelo do BigQuery é um pipeline de streaming que lê
mensagens formatadas em JSON de um tópico do Pub/Sub e as grava em uma
tabela do BigQuery. É possível usar o modelo como uma solução rápida para mover
dados do Pub/Sub para BigQuery. O modelo lê mensagens em formato JSON do Pub/Sub
e as converte em elementos do BigQuery.
Requisitos para este pipeline:
O campo data
das mensagens do Pub/Sub precisa usar o formato JSON, conforme descrito neste
guia JSON.
Por exemplo, mensagens com valores no campo data formatados como {"k1":"v1", "k2":"v2"} podem ser inseridos em uma tabela do BigQuery com duas colunas, k1 e k2, com um tipo de dados de string.
O diretório de saída precisa ser criado antes de executar o pipeline. O esquema da tabela precisa corresponder aos objetos JSON de entrada.
Parâmetros do modelo
Parâmetro
Descrição
inputTopic
O tópico de entrada do tópico do Pub/Sub que será lido, no formato de projects/<project>/topics/<topic>.
outputTableSpec
O local da tabela de saída do BigQuery, no formato de <my-project>:<my-dataset>.<my-table>
outputDeadletterTable
A tabela do BigQuery para mensagens que não chegaram à tabela de saída. É preciso usar o formato <my-project>:<my-dataset>.<my-table>.
Se não existir, será criada durante a execução do pipeline.
Se não for especificada, será usada <outputTableSpec>_error_records.
javascriptTextTransformGcsPath
(Opcional)
O URI do Cloud Storage do arquivo .js que define a função definida
pelo usuário (UDF, na sigla em inglês) do JavaScript que você quer usar. Por exemplo, gs://my-bucket/my-udfs/my_file.js.
javascriptTextTransformFunctionName
(Opcional)
O nome da função definida pelo usuário (UDF) do JavaScript que você quer usar.
Por exemplo, se o código de função do JavaScript for
myTransform(inJson) { /*...do stuff...*/ }, o nome da função será
myTransform. Para amostras de UDFs do JavaScript, consulte os
exemplos de UDF.
Como executar o tópico do Pub/Sub para o modelo do BigQuery
Console
Acesse a página Criar job usando um modelo do Dataflow.
Opcional: em Endpoint regional, selecione um valor no menu suspenso. O endpoint
regional padrão é us-central1.
Para ver uma lista de regiões em que é possível executar um job do Dataflow, consulte
Locais do Dataflow.
No menu suspenso Modelo do Dataflow, selecione
the Pub/Sub Topic to BigQuery template.
Nos campos de parâmetro fornecidos, insira os valores de parâmetro.
Cliquem em Executar job.
gcloud
No shell ou no terminal, execute o modelo:
gcloud dataflow jobs run JOB_NAME \
--gcs-location gs://dataflow-templates/VERSION/PubSub_to_BigQuery \
--region REGION_NAME \
--staging-location STAGING_LOCATION \
--parameters \
inputTopic=projects/PROJECT_ID/topics/TOPIC_NAME,\
outputTableSpec=PROJECT_ID:DATASET.TABLE_NAME,\
outputDeadletterTable=PROJECT_ID:DATASET.TABLE_NAME
Substitua:
JOB_NAME:
um nome de job de sua escolha
REGION_NAME:
o endpoint regional em que você quer
implantar o job do Dataflow, por exemplo, us-central1
VERSION:
a versão do modelo que você quer usar
Use estes valores:
latest para usar a versão mais recente do modelo, disponível na pasta mãe
não datada no bucket:
gs://dataflow-templates/latest/
o nome da versão, como 2021-09-20-00_RC00, para usar uma versão específica do
modelo, que pode ser aninhada na respectiva pasta mãe datada no bucket:
gs://dataflow-templates
STAGING_LOCATION: o local para organizar arquivos locais (por exemplo, gs://your-bucket/staging).
TEMP_LOCATION: o local para gravar arquivos temporários (por exemplo, gs://your-bucket/temp)
TOPIC_NAME: o nome do tópico do Pub/Sub
DATASET: o conjunto de dados do BigQuery
TABLE_NAME: o nome da tabela do BigQuery
API
Para executar o modelo usando a API REST, envie uma solicitação HTTP POST. Para mais informações sobre a
API e os respectivos escopos de autorização, consulte
projects.templates.launch.
PROJECT_ID:
o ID do projeto do Cloud em que você quer executar o job do Dataflow
JOB_NAME:
um nome de job de sua escolha
LOCATION:
o endpoint regional em que você quer
implantar o job do Dataflow, por exemplo, us-central1
VERSION:
a versão do modelo que você quer usar
Use estes valores:
latest para usar a versão mais recente do modelo, disponível na pasta mãe
não datada no bucket:
gs://dataflow-templates/latest/
o nome da versão, como 2021-09-20-00_RC00, para usar uma versão específica do
modelo, que pode ser aninhada na respectiva pasta mãe datada no bucket:
gs://dataflow-templates
STAGING_LOCATION: o local para organizar arquivos locais (por exemplo, gs://your-bucket/staging).
TEMP_LOCATION: o local para gravar arquivos temporários (por exemplo, gs://your-bucket/temp)
/*
* Copyright (C) 2018 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.cloud.teleport.templates;
import static com.google.cloud.teleport.templates.TextToBigQueryStreaming.wrapBigQueryInsertError;
import com.google.api.services.bigquery.model.TableRow;
import com.google.cloud.teleport.coders.FailsafeElementCoder;
import com.google.cloud.teleport.metadata.Template;
import com.google.cloud.teleport.metadata.TemplateCategory;
import com.google.cloud.teleport.metadata.TemplateCreationParameter;
import com.google.cloud.teleport.metadata.TemplateParameter;
import com.google.cloud.teleport.templates.PubSubToBigQuery.Options;
import com.google.cloud.teleport.templates.common.BigQueryConverters.FailsafeJsonToTableRow;
import com.google.cloud.teleport.templates.common.ErrorConverters;
import com.google.cloud.teleport.templates.common.JavascriptTextTransformer.FailsafeJavascriptUdf;
import com.google.cloud.teleport.templates.common.JavascriptTextTransformer.JavascriptTextTransformerOptions;
import com.google.cloud.teleport.util.DualInputNestedValueProvider;
import com.google.cloud.teleport.util.DualInputNestedValueProvider.TranslatorInput;
import com.google.cloud.teleport.util.ResourceUtils;
import com.google.cloud.teleport.util.ValueProviderUtils;
import com.google.cloud.teleport.values.FailsafeElement;
import com.google.common.collect.ImmutableList;
import java.nio.charset.StandardCharsets;
import org.apache.beam.sdk.Pipeline;
import org.apache.beam.sdk.PipelineResult;
import org.apache.beam.sdk.coders.CoderRegistry;
import org.apache.beam.sdk.coders.StringUtf8Coder;
import org.apache.beam.sdk.io.gcp.bigquery.BigQueryIO;
import org.apache.beam.sdk.io.gcp.bigquery.BigQueryIO.Write.CreateDisposition;
import org.apache.beam.sdk.io.gcp.bigquery.BigQueryIO.Write.WriteDisposition;
import org.apache.beam.sdk.io.gcp.bigquery.BigQueryInsertError;
import org.apache.beam.sdk.io.gcp.bigquery.InsertRetryPolicy;
import org.apache.beam.sdk.io.gcp.bigquery.WriteResult;
import org.apache.beam.sdk.io.gcp.pubsub.PubsubIO;
import org.apache.beam.sdk.io.gcp.pubsub.PubsubMessage;
import org.apache.beam.sdk.io.gcp.pubsub.PubsubMessageWithAttributesCoder;
import org.apache.beam.sdk.options.Default;
import org.apache.beam.sdk.options.Description;
import org.apache.beam.sdk.options.PipelineOptions;
import org.apache.beam.sdk.options.PipelineOptionsFactory;
import org.apache.beam.sdk.options.ValueProvider;
import org.apache.beam.sdk.transforms.DoFn;
import org.apache.beam.sdk.transforms.Flatten;
import org.apache.beam.sdk.transforms.MapElements;
import org.apache.beam.sdk.transforms.PTransform;
import org.apache.beam.sdk.transforms.ParDo;
import org.apache.beam.sdk.transforms.SerializableFunction;
import org.apache.beam.sdk.values.PCollection;
import org.apache.beam.sdk.values.PCollectionList;
import org.apache.beam.sdk.values.PCollectionTuple;
import org.apache.beam.sdk.values.TupleTag;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link PubSubToBigQuery} pipeline is a streaming pipeline which ingests data in JSON format
* from Cloud Pub/Sub, executes a UDF, and outputs the resulting records to BigQuery. Any errors
* which occur in the transformation of the data or execution of the UDF will be output to a
* separate errors table in BigQuery. The errors table will be created if it does not exist prior to
* execution. Both output and error tables are specified by the user as template parameters.
*
* <p><b>Pipeline Requirements</b>
*
* <ul>
* <li>The Pub/Sub topic exists.
* <li>The BigQuery output table exists.
* </ul>
*
* <p><b>Example Usage</b>
*
* <pre>
* # Set the pipeline vars
* PROJECT_ID=PROJECT ID HERE
* BUCKET_NAME=BUCKET NAME HERE
* PIPELINE_FOLDER=gs://${BUCKET_NAME}/dataflow/pipelines/pubsub-to-bigquery
* USE_SUBSCRIPTION=true or false depending on whether the pipeline should read
* from a Pub/Sub Subscription or a Pub/Sub Topic.
*
* # Set the runner
* RUNNER=DataflowRunner
*
* # Build the template
* mvn compile exec:java \
* -Dexec.mainClass=com.google.cloud.teleport.templates.PubSubToBigQuery \
* -Dexec.cleanupDaemonThreads=false \
* -Dexec.args=" \
* --project=${PROJECT_ID} \
* --stagingLocation=${PIPELINE_FOLDER}/staging \
* --tempLocation=${PIPELINE_FOLDER}/temp \
* --templateLocation=${PIPELINE_FOLDER}/template \
* --runner=${RUNNER}
* --useSubscription=${USE_SUBSCRIPTION}
* "
*
* # Execute the template
* JOB_NAME=pubsub-to-bigquery-$USER-`date +"%Y%m%d-%H%M%S%z"`
*
* # Execute a pipeline to read from a Topic.
* gcloud dataflow jobs run ${JOB_NAME} \
* --gcs-location=${PIPELINE_FOLDER}/template \
* --zone=us-east1-d \
* --parameters \
* "inputTopic=projects/${PROJECT_ID}/topics/input-topic-name,\
* outputTableSpec=${PROJECT_ID}:dataset-id.output-table,\
* outputDeadletterTable=${PROJECT_ID}:dataset-id.deadletter-table"
*
* # Execute a pipeline to read from a Subscription.
* gcloud dataflow jobs run ${JOB_NAME} \
* --gcs-location=${PIPELINE_FOLDER}/template \
* --zone=us-east1-d \
* --parameters \
* "inputSubscription=projects/${PROJECT_ID}/subscriptions/input-subscription-name,\
* outputTableSpec=${PROJECT_ID}:dataset-id.output-table,\
* outputDeadletterTable=${PROJECT_ID}:dataset-id.deadletter-table"
* </pre>
*/
@Template(
name = "PubSub_Subscription_to_BigQuery",
category = TemplateCategory.STREAMING,
displayName = "Pub/Sub Subscription to BigQuery",
description =
"Streaming pipeline. Ingests JSON-encoded messages from a Pub/Sub subscription, transforms"
+ " them using a JavaScript user-defined function (UDF), and writes them to a"
+ " pre-existing BigQuery table as BigQuery elements.",
optionsClass = Options.class,
skipOptions = "inputTopic",
contactInformation = "https://cloud.google.com/support")
@Template(
name = "PubSub_to_BigQuery",
category = TemplateCategory.STREAMING,
displayName = "Pub/Sub Topic to BigQuery",
description =
"Streaming pipeline. Ingests JSON-encoded messages from a Pub/Sub topic, transforms them"
+ " using a JavaScript user-defined function (UDF), and writes them to a pre-existing"
+ " BigQuery table as BigQuery elements.",
optionsClass = Options.class,
skipOptions = "inputSubscription",
contactInformation = "https://cloud.google.com/support")
public class PubSubToBigQuery {
/** The log to output status messages to. */
private static final Logger LOG = LoggerFactory.getLogger(PubSubToBigQuery.class);
/** The tag for the main output for the UDF. */
public static final TupleTag<FailsafeElement<PubsubMessage, String>> UDF_OUT =
new TupleTag<FailsafeElement<PubsubMessage, String>>() {};
/** The tag for the main output of the json transformation. */
public static final TupleTag<TableRow> TRANSFORM_OUT = new TupleTag<TableRow>() {};
/** The tag for the dead-letter output of the udf. */
public static final TupleTag<FailsafeElement<PubsubMessage, String>> UDF_DEADLETTER_OUT =
new TupleTag<FailsafeElement<PubsubMessage, String>>() {};
/** The tag for the dead-letter output of the json to table row transform. */
public static final TupleTag<FailsafeElement<PubsubMessage, String>> TRANSFORM_DEADLETTER_OUT =
new TupleTag<FailsafeElement<PubsubMessage, String>>() {};
/** The default suffix for error tables if dead letter table is not specified. */
public static final String DEFAULT_DEADLETTER_TABLE_SUFFIX = "_error_records";
/** Pubsub message/string coder for pipeline. */
public static final FailsafeElementCoder<PubsubMessage, String> CODER =
FailsafeElementCoder.of(PubsubMessageWithAttributesCoder.of(), StringUtf8Coder.of());
/** String/String Coder for FailsafeElement. */
public static final FailsafeElementCoder<String, String> FAILSAFE_ELEMENT_CODER =
FailsafeElementCoder.of(StringUtf8Coder.of(), StringUtf8Coder.of());
/**
* The {@link Options} class provides the custom execution options passed by the executor at the
* command-line.
*/
public interface Options extends PipelineOptions, JavascriptTextTransformerOptions {
@TemplateParameter.BigQueryTable(
order = 1,
description = "BigQuery output table",
helpText =
"BigQuery table location to write the output to. The table’s schema must match the "
+ "input JSON objects.")
ValueProvider<String> getOutputTableSpec();
void setOutputTableSpec(ValueProvider<String> value);
@TemplateParameter.PubsubTopic(
order = 2,
description = "Input Pub/Sub topic",
helpText = "The Pub/Sub topic to read the input from.")
ValueProvider<String> getInputTopic();
void setInputTopic(ValueProvider<String> value);
@TemplateParameter.PubsubSubscription(
order = 3,
description = "Pub/Sub input subscription",
helpText =
"Pub/Sub subscription to read the input from, in the format of"
+ " 'projects/your-project-id/subscriptions/your-subscription-name'")
ValueProvider<String> getInputSubscription();
void setInputSubscription(ValueProvider<String> value);
@TemplateCreationParameter(template = "PubSub_to_BigQuery", value = "false")
@TemplateCreationParameter(template = "PubSub_Subscription_to_BigQuery", value = "true")
@Description(
"This determines whether the template reads from a Pub/sub subscription or a topic")
@Default.Boolean(false)
Boolean getUseSubscription();
void setUseSubscription(Boolean value);
@TemplateParameter.BigQueryTable(
order = 5,
optional = true,
description =
"Table for messages failed to reach the output table (i.e., Deadletter table)",
helpText =
"Messages failed to reach the output table for all kind of reasons (e.g., mismatched"
+ " schema, malformed json) are written to this table. It should be in the format"
+ " of \"your-project-id:your-dataset.your-table-name\". If it doesn't exist, it"
+ " will be created during pipeline execution. If not specified,"
+ " \"{outputTableSpec}_error_records\" is used instead.")
ValueProvider<String> getOutputDeadletterTable();
void setOutputDeadletterTable(ValueProvider<String> value);
}
/**
* The main entry-point for pipeline execution. This method will start the pipeline but will not
* wait for it's execution to finish. If blocking execution is required, use the {@link
* PubSubToBigQuery#run(Options)} method to start the pipeline and invoke {@code
* result.waitUntilFinish()} on the {@link PipelineResult}.
*
* @param args The command-line args passed by the executor.
*/
public static void main(String[] args) {
Options options = PipelineOptionsFactory.fromArgs(args).withValidation().as(Options.class);
run(options);
}
/**
* Runs the pipeline to completion with the specified options. This method does not wait until the
* pipeline is finished before returning. Invoke {@code result.waitUntilFinish()} on the result
* object to block until the pipeline is finished running if blocking programmatic execution is
* required.
*
* @param options The execution options.
* @return The pipeline result.
*/
public static PipelineResult run(Options options) {
Pipeline pipeline = Pipeline.create(options);
CoderRegistry coderRegistry = pipeline.getCoderRegistry();
coderRegistry.registerCoderForType(CODER.getEncodedTypeDescriptor(), CODER);
/*
* Steps:
* 1) Read messages in from Pub/Sub
* 2) Transform the PubsubMessages into TableRows
* - Transform message payload via UDF
* - Convert UDF result to TableRow objects
* 3) Write successful records out to BigQuery
* 4) Write failed records out to BigQuery
*/
/*
* Step #1: Read messages in from Pub/Sub
* Either from a Subscription or Topic
*/
PCollection<PubsubMessage> messages = null;
if (options.getUseSubscription()) {
messages =
pipeline.apply(
"ReadPubSubSubscription",
PubsubIO.readMessagesWithAttributes()
.fromSubscription(options.getInputSubscription()));
} else {
messages =
pipeline.apply(
"ReadPubSubTopic",
PubsubIO.readMessagesWithAttributes().fromTopic(options.getInputTopic()));
}
PCollectionTuple convertedTableRows =
messages
/*
* Step #2: Transform the PubsubMessages into TableRows
*/
.apply("ConvertMessageToTableRow", new PubsubMessageToTableRow(options));
/*
* Step #3: Write the successful records out to BigQuery
*/
WriteResult writeResult =
convertedTableRows
.get(TRANSFORM_OUT)
.apply(
"WriteSuccessfulRecords",
BigQueryIO.writeTableRows()
.withoutValidation()
.withCreateDisposition(CreateDisposition.CREATE_NEVER)
.withWriteDisposition(WriteDisposition.WRITE_APPEND)
.withExtendedErrorInfo()
.withMethod(BigQueryIO.Write.Method.STREAMING_INSERTS)
.withFailedInsertRetryPolicy(InsertRetryPolicy.retryTransientErrors())
.to(options.getOutputTableSpec()));
/*
* Step 3 Contd.
* Elements that failed inserts into BigQuery are extracted and converted to FailsafeElement
*/
PCollection<FailsafeElement<String, String>> failedInserts =
writeResult
.getFailedInsertsWithErr()
.apply(
"WrapInsertionErrors",
MapElements.into(FAILSAFE_ELEMENT_CODER.getEncodedTypeDescriptor())
.via((BigQueryInsertError e) -> wrapBigQueryInsertError(e)))
.setCoder(FAILSAFE_ELEMENT_CODER);
/*
* Step #4: Write records that failed table row transformation
* or conversion out to BigQuery deadletter table.
*/
PCollectionList.of(
ImmutableList.of(
convertedTableRows.get(UDF_DEADLETTER_OUT),
convertedTableRows.get(TRANSFORM_DEADLETTER_OUT)))
.apply("Flatten", Flatten.pCollections())
.apply(
"WriteFailedRecords",
ErrorConverters.WritePubsubMessageErrors.newBuilder()
.setErrorRecordsTable(
ValueProviderUtils.maybeUseDefaultDeadletterTable(
options.getOutputDeadletterTable(),
options.getOutputTableSpec(),
DEFAULT_DEADLETTER_TABLE_SUFFIX))
.setErrorRecordsTableSchema(ResourceUtils.getDeadletterTableSchemaJson())
.build());
// 5) Insert records that failed insert into deadletter table
failedInserts.apply(
"WriteFailedRecords",
ErrorConverters.WriteStringMessageErrors.newBuilder()
.setErrorRecordsTable(
ValueProviderUtils.maybeUseDefaultDeadletterTable(
options.getOutputDeadletterTable(),
options.getOutputTableSpec(),
DEFAULT_DEADLETTER_TABLE_SUFFIX))
.setErrorRecordsTableSchema(ResourceUtils.getDeadletterTableSchemaJson())
.build());
return pipeline.run();
}
/**
* If deadletterTable is available, it is returned as is, otherwise outputTableSpec +
* defaultDeadLetterTableSuffix is returned instead.
*/
private static ValueProvider<String> maybeUseDefaultDeadletterTable(
ValueProvider<String> deadletterTable,
ValueProvider<String> outputTableSpec,
String defaultDeadLetterTableSuffix) {
return DualInputNestedValueProvider.of(
deadletterTable,
outputTableSpec,
new SerializableFunction<TranslatorInput<String, String>, String>() {
@Override
public String apply(TranslatorInput<String, String> input) {
String userProvidedTable = input.getX();
String outputTableSpec = input.getY();
if (userProvidedTable == null) {
return outputTableSpec + defaultDeadLetterTableSuffix;
}
return userProvidedTable;
}
});
}
/**
* The {@link PubsubMessageToTableRow} class is a {@link PTransform} which transforms incoming
* {@link PubsubMessage} objects into {@link TableRow} objects for insertion into BigQuery while
* applying an optional UDF to the input. The executions of the UDF and transformation to {@link
* TableRow} objects is done in a fail-safe way by wrapping the element with it's original payload
* inside the {@link FailsafeElement} class. The {@link PubsubMessageToTableRow} transform will
* output a {@link PCollectionTuple} which contains all output and dead-letter {@link
* PCollection}.
*
* <p>The {@link PCollectionTuple} output will contain the following {@link PCollection}:
*
* <ul>
* <li>{@link PubSubToBigQuery#UDF_OUT} - Contains all {@link FailsafeElement} records
* successfully processed by the optional UDF.
* <li>{@link PubSubToBigQuery#UDF_DEADLETTER_OUT} - Contains all {@link FailsafeElement}
* records which failed processing during the UDF execution.
* <li>{@link PubSubToBigQuery#TRANSFORM_OUT} - Contains all records successfully converted from
* JSON to {@link TableRow} objects.
* <li>{@link PubSubToBigQuery#TRANSFORM_DEADLETTER_OUT} - Contains all {@link FailsafeElement}
* records which couldn't be converted to table rows.
* </ul>
*/
static class PubsubMessageToTableRow
extends PTransform<PCollection<PubsubMessage>, PCollectionTuple> {
private final Options options;
PubsubMessageToTableRow(Options options) {
this.options = options;
}
@Override
public PCollectionTuple expand(PCollection<PubsubMessage> input) {
PCollectionTuple udfOut =
input
// Map the incoming messages into FailsafeElements so we can recover from failures
// across multiple transforms.
.apply("MapToRecord", ParDo.of(new PubsubMessageToFailsafeElementFn()))
.apply(
"InvokeUDF",
FailsafeJavascriptUdf.<PubsubMessage>newBuilder()
.setFileSystemPath(options.getJavascriptTextTransformGcsPath())
.setFunctionName(options.getJavascriptTextTransformFunctionName())
.setSuccessTag(UDF_OUT)
.setFailureTag(UDF_DEADLETTER_OUT)
.build());
// Convert the records which were successfully processed by the UDF into TableRow objects.
PCollectionTuple jsonToTableRowOut =
udfOut
.get(UDF_OUT)
.apply(
"JsonToTableRow",
FailsafeJsonToTableRow.<PubsubMessage>newBuilder()
.setSuccessTag(TRANSFORM_OUT)
.setFailureTag(TRANSFORM_DEADLETTER_OUT)
.build());
// Re-wrap the PCollections so we can return a single PCollectionTuple
return PCollectionTuple.of(UDF_OUT, udfOut.get(UDF_OUT))
.and(UDF_DEADLETTER_OUT, udfOut.get(UDF_DEADLETTER_OUT))
.and(TRANSFORM_OUT, jsonToTableRowOut.get(TRANSFORM_OUT))
.and(TRANSFORM_DEADLETTER_OUT, jsonToTableRowOut.get(TRANSFORM_DEADLETTER_OUT));
}
}
/**
* The {@link PubsubMessageToFailsafeElementFn} wraps an incoming {@link PubsubMessage} with the
* {@link FailsafeElement} class so errors can be recovered from and the original message can be
* output to a error records table.
*/
static class PubsubMessageToFailsafeElementFn
extends DoFn<PubsubMessage, FailsafeElement<PubsubMessage, String>> {
@ProcessElement
public void processElement(ProcessContext context) {
PubsubMessage message = context.element();
context.output(
FailsafeElement.of(message, new String(message.getPayload(), StandardCharsets.UTF_8)));
}
}
}
Avro do Pub/Sub para BigQuery
O modelo do Avro do Pub/Sub para BigQuery é um pipeline de streaming que
ingere dados do Avro de uma assinatura do Pub/Sub em uma tabela do BigQuery.
Qualquer erro que ocorre durante a gravação na tabela do BigQuery é transmitido para um tópico
não processado do Pub/Sub.
Requisitos para esse pipeline
A assinatura de entrada do Pub/Sub precisa existir.
O arquivo de esquema para os registros do Avro precisa existir no Cloud Storage.
O tópico do Pub/Sub não processado precisa existir.
O conjunto de dados de saída do BigQuery precisa existir.
Parâmetros do modelo
Parâmetro
Descrição
schemaPath
O local do Cloud Storage do arquivo de esquema do Avro. Por exemplo, gs://path/to/my/schema.avsc.
inputSubscription
A assinatura de entrada do Pub/Sub a ser lida. Por exemplo, projects/<project>/subscriptions/<subscription>.
outputTopic
O tópico do Pub/Sub a ser usado para registros não processados. Por exemplo, projects/<project-id>/topics/<topic-name>.
outputTableSpec
O local da tabela de saída do BigQuery. Por exemplo, <my-project>:<my-dataset>.<my-table>.
Dependendo do createDisposition especificado, a tabela de saída pode ser criada
automaticamente usando o esquema do Avro fornecido pelo usuário.
writeDisposition
(Opcional) O WriteDisposition do BigQuery.
Por exemplo, WRITE_APPEND, WRITE_EMPTY ou WRITE_TRUNCATE. Padrão: WRITE_APPEND
createDisposition
(Opcional) O CreateDisposition do BigQuery.
Por exemplo: CREATE_IF_NEEDED e CREATE_NEVER. Padrão: CREATE_IF_NEEDED
Como executar o Avro do Pub/Sub para o modelo do BigQuery
Console
Acesse a página Criar job usando um modelo do Dataflow.
REGION_NAME:
o endpoint regional em que você quer
implantar o job do Dataflow, por exemplo, us-central1
VERSION:
a versão do modelo que você quer usar
Use estes valores:
latest para usar a versão mais recente do modelo, disponível na pasta mãe
não datada no bucket:
gs://dataflow-templates/latest/
o nome da versão, como 2021-09-20-00_RC00, para usar uma versão específica do
modelo, que pode ser aninhada na respectiva pasta mãe datada no bucket:
gs://dataflow-templates
SCHEMA_PATH: o caminho do Cloud Storage para o arquivo de esquema do Avro (por exemplo, gs://MyBucket/file.avsc)
SUBSCRIPTION_NAME: o nome da assinatura de entrada do Pub/Sub
BIGQUERY_TABLE: o nome da tabela de saída do BigQuery.
DEADLETTER_TOPIC: o tópico do Pub/Sub a ser usado para a fila não processada
API
Para executar o modelo usando a API REST, envie uma solicitação HTTP POST. Para mais informações sobre a
API e os respectivos escopos de autorização, consulte
projects.templates.launch.
LOCATION:
o endpoint regional em que você quer
implantar o job do Dataflow, por exemplo, us-central1
VERSION:
a versão do modelo que você quer usar
Use estes valores:
latest para usar a versão mais recente do modelo, disponível na pasta mãe
não datada no bucket:
gs://dataflow-templates/latest/
o nome da versão, como 2021-09-20-00_RC00, para usar uma versão específica do
modelo, que pode ser aninhada na respectiva pasta mãe datada no bucket:
gs://dataflow-templates
SCHEMA_PATH: o caminho do Cloud Storage para o arquivo de esquema do Avro (por exemplo, gs://MyBucket/file.avsc)
SUBSCRIPTION_NAME: o nome da assinatura de entrada do Pub/Sub
BIGQUERY_TABLE: o nome da tabela de saída do BigQuery.
DEADLETTER_TOPIC: o tópico do Pub/Sub a ser usado para a fila não processada
/*
* Copyright (C) 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.cloud.teleport.v2.templates;
import com.google.cloud.teleport.metadata.Template;
import com.google.cloud.teleport.metadata.TemplateCategory;
import com.google.cloud.teleport.metadata.TemplateParameter;
import com.google.cloud.teleport.v2.common.UncaughtExceptionLogger;
import com.google.cloud.teleport.v2.options.BigQueryCommonOptions.WriteOptions;
import com.google.cloud.teleport.v2.options.BigQueryStorageApiStreamingOptions;
import com.google.cloud.teleport.v2.options.PubsubCommonOptions.ReadSubscriptionOptions;
import com.google.cloud.teleport.v2.options.PubsubCommonOptions.WriteTopicOptions;
import com.google.cloud.teleport.v2.templates.PubsubAvroToBigQuery.PubsubAvroToBigQueryOptions;
import com.google.cloud.teleport.v2.transforms.BigQueryConverters;
import com.google.cloud.teleport.v2.transforms.ErrorConverters;
import com.google.cloud.teleport.v2.utils.BigQueryIOUtils;
import com.google.cloud.teleport.v2.utils.SchemaUtils;
import org.apache.avro.Schema;
import org.apache.avro.generic.GenericRecord;
import org.apache.beam.sdk.Pipeline;
import org.apache.beam.sdk.PipelineResult;
import org.apache.beam.sdk.coders.AvroCoder;
import org.apache.beam.sdk.io.gcp.bigquery.WriteResult;
import org.apache.beam.sdk.io.gcp.pubsub.PubsubIO;
import org.apache.beam.sdk.options.PipelineOptionsFactory;
import org.apache.beam.sdk.options.Validation.Required;
import org.apache.beam.sdk.schemas.transforms.Convert;
import org.apache.beam.sdk.values.Row;
/**
* A Dataflow pipeline to stream <a href="https://avro.apache.org/">Apache Avro</a> records from
* Pub/Sub into a BigQuery table.
*
* <p>Any persistent failures while writing to BigQuery will be written to a Pub/Sub dead-letter
* topic.
*/
@Template(
name = "PubSub_Avro_to_BigQuery",
category = TemplateCategory.STREAMING,
displayName = "Pub/Sub Avro to BigQuery",
description =
"A streaming pipeline which inserts Avro records from a Pub/Sub subscription into a"
+ " BigQuery table.",
optionsClass = PubsubAvroToBigQueryOptions.class,
flexContainerName = "pubsub-avro-to-bigquery",
contactInformation = "https://cloud.google.com/support")
public final class PubsubAvroToBigQuery {
/**
* Validates input flags and executes the Dataflow pipeline.
*
* @param args command line arguments to the pipeline
*/
public static void main(String[] args) {
UncaughtExceptionLogger.register();
PubsubAvroToBigQueryOptions options =
PipelineOptionsFactory.fromArgs(args)
.withValidation()
.as(PubsubAvroToBigQueryOptions.class);
run(options);
}
/**
* Provides custom {@link org.apache.beam.sdk.options.PipelineOptions} required to execute the
* {@link PubsubAvroToBigQuery} pipeline.
*/
public interface PubsubAvroToBigQueryOptions
extends ReadSubscriptionOptions,
WriteOptions,
WriteTopicOptions,
BigQueryStorageApiStreamingOptions {
@TemplateParameter.GcsReadFile(
order = 1,
description = "Cloud Storage path to the Avro schema file",
helpText = "Cloud Storage path to Avro schema file. For example, gs://MyBucket/file.avsc.")
@Required
String getSchemaPath();
void setSchemaPath(String schemaPath);
}
/**
* Runs the pipeline with the supplied options.
*
* @param options execution parameters to the pipeline
* @return result of the pipeline execution as a {@link PipelineResult}
*/
private static PipelineResult run(PubsubAvroToBigQueryOptions options) {
BigQueryIOUtils.validateBQStorageApiOptionsStreaming(options);
// Create the pipeline.
Pipeline pipeline = Pipeline.create(options);
Schema schema = SchemaUtils.getAvroSchema(options.getSchemaPath());
WriteResult writeResults =
pipeline
.apply(
"Read Avro records",
PubsubIO.readAvroGenericRecords(schema)
.fromSubscription(options.getInputSubscription())
.withDeadLetterTopic(options.getOutputTopic()))
// Workaround for BEAM-12256. Eagerly convert to rows to avoid
// the RowToGenericRecord function that doesn't handle all data
// types.
// TODO: Remove this workaround when a fix for BEAM-12256 is
// released.
.apply(Convert.toRows())
.apply(
"Write to BigQuery",
BigQueryConverters.<Row>createWriteTransform(options).useBeamSchema());
BigQueryIOUtils.writeResultToBigQueryInsertErrors(writeResults, options)
.apply(
"Create error payload",
ErrorConverters.BigQueryInsertErrorToPubsubMessage.<GenericRecord>newBuilder()
.setPayloadCoder(AvroCoder.of(schema))
.setTranslateFunction(BigQueryConverters.TableRowToGenericRecordFn.of(schema))
.build())
.apply("Write failed records", PubsubIO.writeMessages().to(options.getOutputTopic()));
// Execute the pipeline and return the result.
return pipeline.run();
}
}
Buffer de protocolo do Pub/Sub para BigQuery
O modelo de buffer de protocolo do Pub/Sub para BigQuery é um pipeline de streaming
que ingere dados do buffer de protocolo de uma assinatura do Pub/Sub em uma tabela do BigQuery.
Qualquer erro que ocorre durante a gravação na tabela do BigQuery é transmitido para um tópico não
processado do Pub/Sub.
Uma função definida pelo usuário (UDF) do JavaScript pode ser fornecida para transformar dados. Erros ao executar
a UDF podem ser enviados para um tópico separado do Pub/Sub ou para o mesmo tópico não processado como
os erros do BigQuery.
Requisitos para este pipeline:
A assinatura de entrada do Pub/Sub precisa existir.
O arquivo de esquema dos registros do buffer de protocolo precisa existir no Cloud Storage.
O tópico do Pub/Sub não processado precisa existir.
O conjunto de dados de saída do BigQuery precisa existir.
Se a tabela do BigQuery existir, ela precisará ter um esquema que corresponda aos dados proto, independentemente do valor createDisposition.
Parâmetros do modelo
Parâmetro
Descrição
protoSchemaPath
O local do Cloud Storage do arquivo de esquema proto independente. Por exemplo, gs://path/to/my/file.pb
Esse arquivo pode ser gerado com a sinalização --descriptor_set_out do comando protoc.
A sinalização --include_imports garante que o arquivo seja independente.
fullMessageName
O nome completo da mensagem proto. Por exemplo, package.name.MessageName, em que package.name é o valor
fornecido para a instrução package, e não para a instrução java_package.
inputSubscription
A assinatura de entrada do Pub/Sub a ser lida. Por exemplo, projects/<project>/subscriptions/<subscription>.
outputTopic
O tópico do Pub/Sub a ser usado para registros não processados. Por exemplo, projects/<project-id>/topics/<topic-name>.
outputTableSpec
O local da tabela de saída do BigQuery. Por exemplo, my-project:my_dataset.my_table
Dependendo do createDisposition especificado, a tabela de saída pode ser criada
automaticamente usando o arquivo de esquema de entrada.
preserveProtoFieldNames
(Opcional) true para preservar o nome do campo do Proto original no JSON. false para usar mais nomes JSON padrão.
Por exemplo, false mudaria field_name para fieldName. (Padrão: false)
bigQueryTableSchemaPath
Opcional: caminho do Cloud Storage para o caminho do esquema do BigQuery. Por exemplo, gs://path/to/my/schema.json Se isso não for fornecido, o esquema será inferido do esquema Proto.
javascriptTextTransformGcsPath
(Opcional)
O URI do Cloud Storage do arquivo .js que define a função definida
pelo usuário (UDF, na sigla em inglês) do JavaScript que você quer usar. Por exemplo, gs://my-bucket/my-udfs/my_file.js.
javascriptTextTransformFunctionName
(Opcional)
O nome da função definida pelo usuário (UDF) do JavaScript que você quer usar.
Por exemplo, se o código de função do JavaScript for
myTransform(inJson) { /*...do stuff...*/ }, o nome da função será
myTransform. Para amostras de UDFs do JavaScript, consulte os
exemplos de UDF.
udfOutputTopic
(Opcional) O tópico do Pub/Sub que armazena os erros da UDF. Por exemplo,
projects/<project-id>/topics/<topic-name> Se isso não for fornecido, os erros de UDF
serão enviados para o mesmo tópico que outputTopic.
writeDisposition
(Opcional) O WriteDisposition do BigQuery.
Por exemplo, WRITE_APPEND, WRITE_EMPTY ou WRITE_TRUNCATE. Padrão: WRITE_APPEND.
createDisposition
(Opcional) O CreateDisposition do BigQuery.
Por exemplo: CREATE_IF_NEEDED e CREATE_NEVER. Padrão: CREATE_IF_NEEDED.
Como executar o tópico do Pub/Sub para o modelo do BigQuery
Console
Acesse a página Criar job usando um modelo do Dataflow.
REGION_NAME:
o endpoint regional em que você quer
implantar o job do Dataflow, por exemplo, us-central1
VERSION:
a versão do modelo que você quer usar
Use estes valores:
latest para usar a versão mais recente do modelo, disponível na pasta mãe
não datada no bucket:
gs://dataflow-templates/latest/
o nome da versão, como 2021-09-20-00_RC00, para usar uma versão específica do
modelo, que pode ser aninhada na respectiva pasta mãe datada no bucket:
gs://dataflow-templates
SCHEMA_PATH: o caminho do Cloud Storage para o arquivo de esquema do Proto (por exemplo, gs://MyBucket/file.pb)
PROTO_MESSAGE_NAME: o nome da mensagem do Proto (por exemplo, package.name.MessageName)
SUBSCRIPTION_NAME: o nome da assinatura de entrada do Pub/Sub
BIGQUERY_TABLE: o nome da tabela de saída do BigQuery.
UNPROCESSED_TOPIC: o tópico do Pub/Sub a ser usado para a fila não processada
API
Para executar o modelo usando a API REST, envie uma solicitação HTTP POST. Para mais informações sobre a
API e os respectivos escopos de autorização, consulte
projects.templates.launch.
PROJECT_ID:
o ID do projeto do Cloud em que você quer executar o job do Dataflow
JOB_NAME:
um nome de job de sua escolha
LOCATION:
o endpoint regional em que você quer
implantar o job do Dataflow, por exemplo, us-central1
VERSION:
a versão do modelo que você quer usar
Use estes valores:
latest para usar a versão mais recente do modelo, disponível na pasta mãe
não datada no bucket:
gs://dataflow-templates/latest/
o nome da versão, como 2021-09-20-00_RC00, para usar uma versão específica do
modelo, que pode ser aninhada na respectiva pasta mãe datada no bucket:
gs://dataflow-templates
SCHEMA_PATH: o caminho do Cloud Storage para o arquivo de esquema do Proto (por exemplo, gs://MyBucket/file.pb)
PROTO_MESSAGE_NAME: o nome da mensagem do Proto (por exemplo, package.name.MessageName)
SUBSCRIPTION_NAME: o nome da assinatura de entrada do Pub/Sub
BIGQUERY_TABLE: o nome da tabela de saída do BigQuery.
UNPROCESSED_TOPIC: o tópico do Pub/Sub a ser usado para a fila não processada
/*
* Copyright (C) 2021 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.cloud.teleport.v2.templates;
import static java.nio.charset.StandardCharsets.UTF_8;
import com.google.cloud.teleport.metadata.Template;
import com.google.cloud.teleport.metadata.TemplateCategory;
import com.google.cloud.teleport.metadata.TemplateParameter;
import com.google.cloud.teleport.v2.coders.FailsafeElementCoder;
import com.google.cloud.teleport.v2.common.UncaughtExceptionLogger;
import com.google.cloud.teleport.v2.options.BigQueryCommonOptions.WriteOptions;
import com.google.cloud.teleport.v2.options.BigQueryStorageApiStreamingOptions;
import com.google.cloud.teleport.v2.options.PubsubCommonOptions.ReadSubscriptionOptions;
import com.google.cloud.teleport.v2.options.PubsubCommonOptions.WriteTopicOptions;
import com.google.cloud.teleport.v2.templates.PubsubProtoToBigQuery.PubSubProtoToBigQueryOptions;
import com.google.cloud.teleport.v2.transforms.BigQueryConverters;
import com.google.cloud.teleport.v2.transforms.ErrorConverters;
import com.google.cloud.teleport.v2.transforms.FailsafeElementTransforms.ConvertFailsafeElementToPubsubMessage;
import com.google.cloud.teleport.v2.transforms.JavascriptTextTransformer.FailsafeJavascriptUdf;
import com.google.cloud.teleport.v2.transforms.JavascriptTextTransformer.JavascriptTextTransformerOptions;
import com.google.cloud.teleport.v2.utils.BigQueryIOUtils;
import com.google.cloud.teleport.v2.utils.GCSUtils;
import com.google.cloud.teleport.v2.utils.SchemaUtils;
import com.google.cloud.teleport.v2.values.FailsafeElement;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Strings;
import com.google.protobuf.Descriptors.Descriptor;
import com.google.protobuf.DynamicMessage;
import com.google.protobuf.InvalidProtocolBufferException;
import com.google.protobuf.util.JsonFormat;
import org.apache.beam.sdk.Pipeline;
import org.apache.beam.sdk.PipelineResult;
import org.apache.beam.sdk.coders.NullableCoder;
import org.apache.beam.sdk.coders.StringUtf8Coder;
import org.apache.beam.sdk.io.gcp.bigquery.BigQueryIO.Write;
import org.apache.beam.sdk.io.gcp.bigquery.WriteResult;
import org.apache.beam.sdk.io.gcp.pubsub.PubsubIO;
import org.apache.beam.sdk.io.gcp.pubsub.PubsubIO.Read;
import org.apache.beam.sdk.io.gcp.pubsub.PubsubMessage;
import org.apache.beam.sdk.options.Default;
import org.apache.beam.sdk.options.PipelineOptionsFactory;
import org.apache.beam.sdk.options.Validation.Required;
import org.apache.beam.sdk.transforms.MapElements;
import org.apache.beam.sdk.transforms.PTransform;
import org.apache.beam.sdk.values.PCollection;
import org.apache.beam.sdk.values.PCollectionTuple;
import org.apache.beam.sdk.values.TupleTag;
import org.apache.beam.sdk.values.TypeDescriptor;
import org.apache.beam.sdk.values.TypeDescriptors;
import org.apache.commons.lang3.ArrayUtils;
/**
* A template for writing <a href="https://developers.google.com/protocol-buffers">Protobuf</a>
* records from Pub/Sub to BigQuery.
*
* <p>Persistent failures are written to a Pub/Sub unprocessed topic.
*/
@Template(
name = "PubSub_Proto_to_BigQuery",
category = TemplateCategory.STREAMING,
displayName = "Pub/Sub Proto to BigQuery",
description =
"A streaming pipeline that reads Protobuf messages from a Pub/Sub subscription and writes"
+ " them to a BigQuery table.",
optionsClass = PubSubProtoToBigQueryOptions.class,
flexContainerName = "pubsub-proto-to-bigquery",
contactInformation = "https://cloud.google.com/support")
public final class PubsubProtoToBigQuery {
private static final TupleTag<FailsafeElement<String, String>> UDF_SUCCESS_TAG = new TupleTag<>();
private static final TupleTag<FailsafeElement<String, String>> UDF_FAILURE_TAG = new TupleTag<>();
private static final FailsafeElementCoder<String, String> FAILSAFE_CODER =
FailsafeElementCoder.of(StringUtf8Coder.of(), StringUtf8Coder.of());
public static void main(String[] args) {
UncaughtExceptionLogger.register();
run(PipelineOptionsFactory.fromArgs(args).as(PubSubProtoToBigQueryOptions.class));
}
/** {@link org.apache.beam.sdk.options.PipelineOptions} for {@link PubsubProtoToBigQuery}. */
public interface PubSubProtoToBigQueryOptions
extends ReadSubscriptionOptions,
WriteOptions,
WriteTopicOptions,
JavascriptTextTransformerOptions,
BigQueryStorageApiStreamingOptions {
@TemplateParameter.GcsReadFile(
order = 1,
description = "Cloud Storage Path to the Proto Schema File",
helpText =
"Cloud Storage path to a self-contained descriptor set file. Example:"
+ " gs://MyBucket/schema.pb. `schema.pb` can be generated by adding"
+ " `--descriptor_set_out=schema.pb` to the `protoc` command that compiles the"
+ " protos. The `--include_imports` flag can be used to guarantee that the file is"
+ " self-contained.")
@Required
String getProtoSchemaPath();
void setProtoSchemaPath(String value);
@TemplateParameter.Text(
order = 2,
regexes = {"^.+([a-zA-Z][a-zA-Z0-9_]+\\.?)+[a-zA-Z0-9_]$"},
description = "Full Proto Message Name",
helpText =
"The full message name (example: package.name.MessageName). If the message is nested"
+ " inside of another message, then include all messages with the '.' delimiter"
+ " (example: package.name.OuterMessage.InnerMessage). 'package.name' should be"
+ " from the `package` statement, not the `java_package` statement.")
@Required
String getFullMessageName();
void setFullMessageName(String value);
@TemplateParameter.Text(
order = 3,
optional = true,
description = "Preserve Proto Field Names",
helpText =
"Flag to control whether proto field names should be kept or converted to"
+ " lowerCamelCase. If the table already exists, this should be based on what"
+ " matches the table's schema. Otherwise, it will determine the column names of"
+ " the created table. True to preserve proto snake_case. False will convert fields"
+ " to lowerCamelCase. (Default: false)")
@Default.Boolean(false)
Boolean getPreserveProtoFieldNames();
void setPreserveProtoFieldNames(Boolean value);
@TemplateParameter.GcsReadFile(
order = 4,
optional = true,
description = "BigQuery Table Schema Path",
helpText =
"Cloud Storage path to the BigQuery schema JSON file. "
+ "If this is not set, then the schema is inferred "
+ "from the Proto schema.",
example = "gs://MyBucket/bq_schema.json")
String getBigQueryTableSchemaPath();
void setBigQueryTableSchemaPath(String value);
@TemplateParameter.PubsubTopic(
order = 5,
optional = true,
description = "Pub/Sub output topic for UDF failures",
helpText =
"An optional output topic to send UDF failures to. If this option is not set, then"
+ " failures will be written to the same topic as the BigQuery failures.",
example = "projects/your-project-id/topics/your-topic-name")
String getUdfOutputTopic();
void setUdfOutputTopic(String udfOutputTopic);
}
/** Runs the pipeline and returns the results. */
private static PipelineResult run(PubSubProtoToBigQueryOptions options) {
BigQueryIOUtils.validateBQStorageApiOptionsStreaming(options);
Pipeline pipeline = Pipeline.create(options);
Descriptor descriptor = getDescriptor(options);
PCollection<String> maybeForUdf =
pipeline
.apply("Read From Pubsub", readPubsubMessages(options, descriptor))
.apply("Dynamic Message to TableRow", new ConvertDynamicProtoMessageToJson(options));
WriteResult writeResult =
runUdf(maybeForUdf, options)
.apply("Write to BigQuery", writeToBigQuery(options, descriptor));
BigQueryIOUtils.writeResultToBigQueryInsertErrors(writeResult, options)
.apply(
"Create Error Payload",
ErrorConverters.BigQueryInsertErrorToPubsubMessage.<String>newBuilder()
.setPayloadCoder(StringUtf8Coder.of())
.setTranslateFunction(BigQueryConverters::tableRowToJson)
.build())
.apply("Write Failed BQ Records", PubsubIO.writeMessages().to(options.getOutputTopic()));
return pipeline.run();
}
/** Gets the {@link Descriptor} for the message type in the Pub/Sub topic. */
@VisibleForTesting
static Descriptor getDescriptor(PubSubProtoToBigQueryOptions options) {
String schemaPath = options.getProtoSchemaPath();
String messageName = options.getFullMessageName();
Descriptor descriptor = SchemaUtils.getProtoDomain(schemaPath).getDescriptor(messageName);
if (descriptor == null) {
throw new IllegalArgumentException(
messageName + " is not a recognized message in " + schemaPath);
}
return descriptor;
}
/** Returns the {@link PTransform} for reading Pub/Sub messages. */
private static Read<DynamicMessage> readPubsubMessages(
PubSubProtoToBigQueryOptions options, Descriptor descriptor) {
return PubsubIO.readProtoDynamicMessages(descriptor)
.fromSubscription(options.getInputSubscription())
.withDeadLetterTopic(options.getOutputTopic());
}
/**
* Writes messages to BigQuery, creating the table if necessary and allowed in {@code options}.
*
* <p>The BigQuery schema will be inferred from {@code descriptor} unless a JSON schema path is
* specified in {@code options}.
*/
@VisibleForTesting
static Write<String> writeToBigQuery(
PubSubProtoToBigQueryOptions options, Descriptor descriptor) {
Write<String> write =
BigQueryConverters.<String>createWriteTransform(options)
.withFormatFunction(BigQueryConverters::convertJsonToTableRow);
String schemaPath = options.getBigQueryTableSchemaPath();
if (Strings.isNullOrEmpty(schemaPath)) {
return write.withSchema(
SchemaUtils.createBigQuerySchema(descriptor, options.getPreserveProtoFieldNames()));
} else {
return write.withJsonSchema(GCSUtils.getGcsFileAsString(schemaPath));
}
}
/** {@link PTransform} that handles converting {@link PubsubMessage} values to JSON. */
private static class ConvertDynamicProtoMessageToJson
extends PTransform<PCollection<DynamicMessage>, PCollection<String>> {
private final boolean preserveProtoName;
private ConvertDynamicProtoMessageToJson(PubSubProtoToBigQueryOptions options) {
this.preserveProtoName = options.getPreserveProtoFieldNames();
}
@Override
public PCollection<String> expand(PCollection<DynamicMessage> input) {
return input.apply(
"Map to JSON",
MapElements.into(TypeDescriptors.strings())
.via(
message -> {
try {
JsonFormat.Printer printer = JsonFormat.printer();
return preserveProtoName
? printer.preservingProtoFieldNames().print(message)
: printer.print(message);
} catch (InvalidProtocolBufferException e) {
throw new RuntimeException(e);
}
}));
}
}
/**
* Handles running the UDF.
*
* <p>If {@code options} is configured so as not to run the UDF, then the UDF will not be called.
*
* <p>This may add a branch to the pipeline for outputting failed UDF records to an unprocessed
* topic.
*
* @param jsonCollection {@link PCollection} of JSON strings for use as input to the UDF
* @param options the options containing info on running the UDF
* @return the {@link PCollection} of UDF output as JSON or {@code jsonCollection} if UDF not
* called
*/
@VisibleForTesting
static PCollection<String> runUdf(
PCollection<String> jsonCollection, PubSubProtoToBigQueryOptions options) {
// In order to avoid generating a graph that makes it look like a UDF was called when none was
// intended, simply return the input as "success" output.
if (Strings.isNullOrEmpty(options.getJavascriptTextTransformGcsPath())) {
return jsonCollection;
}
// For testing purposes, we need to do this check before creating the PTransform rather than
// in `expand`. Otherwise, we get a NullPointerException due to the PTransform not returning
// a value.
if (Strings.isNullOrEmpty(options.getJavascriptTextTransformFunctionName())) {
throw new IllegalArgumentException(
"JavaScript function name cannot be null or empty if file is set");
}
PCollectionTuple maybeSuccess = jsonCollection.apply("Run UDF", new RunUdf(options));
maybeSuccess
.get(UDF_FAILURE_TAG)
.setCoder(FAILSAFE_CODER)
.apply(
"Get UDF Failures",
ConvertFailsafeElementToPubsubMessage.<String, String>builder()
.setOriginalPayloadSerializeFn(s -> ArrayUtils.toObject(s.getBytes(UTF_8)))
.setErrorMessageAttributeKey("udfErrorMessage")
.build())
.apply("Write Failed UDF", writeUdfFailures(options));
return maybeSuccess
.get(UDF_SUCCESS_TAG)
.setCoder(FAILSAFE_CODER)
.apply(
"Get UDF Output",
MapElements.into(TypeDescriptors.strings()).via(FailsafeElement::getPayload))
.setCoder(NullableCoder.of(StringUtf8Coder.of()));
}
/** {@link PTransform} that calls a UDF and returns both success and failure output. */
private static class RunUdf extends PTransform<PCollection<String>, PCollectionTuple> {
private final PubSubProtoToBigQueryOptions options;
RunUdf(PubSubProtoToBigQueryOptions options) {
this.options = options;
}
@Override
public PCollectionTuple expand(PCollection<String> input) {
return input
.apply("Prepare Failsafe UDF", makeFailsafe())
.setCoder(FAILSAFE_CODER)
.apply(
"Call UDF",
FailsafeJavascriptUdf.<String>newBuilder()
.setFileSystemPath(options.getJavascriptTextTransformGcsPath())
.setFunctionName(options.getJavascriptTextTransformFunctionName())
.setSuccessTag(UDF_SUCCESS_TAG)
.setFailureTag(UDF_FAILURE_TAG)
.build());
}
private static MapElements<String, FailsafeElement<String, String>> makeFailsafe() {
return MapElements.into(new TypeDescriptor<FailsafeElement<String, String>>() {})
.via((String json) -> FailsafeElement.of(json, json));
}
}
/**
* Returns a {@link PubsubIO.Write} configured to write UDF failures to the appropriate output
* topic.
*/
private static PubsubIO.Write<PubsubMessage> writeUdfFailures(
PubSubProtoToBigQueryOptions options) {
PubsubIO.Write<PubsubMessage> write = PubsubIO.writeMessages();
return Strings.isNullOrEmpty(options.getUdfOutputTopic())
? write.to(options.getOutputTopic())
: write.to(options.getUdfOutputTopic());
}
}
Pub/Sub para Pub/Sub
O modelo do Pub/Sub para Pub/Sub é um pipeline de streaming que lê
mensagens de uma assinatura do Pub/Sub e grava as mensagens em outro tópico
do Pub/Sub. O pipeline também aceita uma chave de atributo de mensagem opcional e um valor que pode ser usado
para filtrar as mensagens que precisam ser gravadas no tópico do Pub/Sub. Use esse modelo
para copiar mensagens de uma assinatura do Pub/Sub para outro tópico do Pub/Sub
com um filtro de mensagem opcional.
Requisitos para este pipeline:
A inscrição do Pub/Sub de origem precisa ser criada antes da execução.
REGION_NAME:
o endpoint regional em que você quer
implantar o job do Dataflow, por exemplo, us-central1
VERSION:
a versão do modelo que você quer usar
Use estes valores:
latest para usar a versão mais recente do modelo, disponível na pasta mãe
não datada no bucket:
gs://dataflow-templates/latest/
o nome da versão, como 2021-09-20-00_RC00, para usar uma versão específica do
modelo, que pode ser aninhada na respectiva pasta mãe datada no bucket:
gs://dataflow-templates
STAGING_LOCATION: o local para organizar arquivos locais (por exemplo, gs://your-bucket/staging).
TEMP_LOCATION: o local para gravar arquivos temporários (por exemplo, gs://your-bucket/temp)
SUBSCRIPTION_NAME: o nome da assinatura
do Pub/Sub
TOPIC_NAME: o nome do tópico do Pub/Sub
FILTER_KEY: a chave de atributo que servirá para filtrar os eventos. Nenhum filtro será aplicado se nenhuma chave for especificada
FILTER_VALUE: valor do atributo de filtro a ser usado se uma chave de filtro de eventos for fornecida.
Aceita uma string Java Regex válida como um valor de filtro de eventos. Se uma regex for fornecida,
a expressão completa deverá corresponder para que a mensagem seja filtrada. Correspondências parciais
(como substrings) não são filtradas. Um valor de filtro de evento nulo é usado por padrão.
API
Para executar o modelo usando a API REST, envie uma solicitação HTTP POST. Para mais informações sobre a
API e os respectivos escopos de autorização, consulte
projects.templates.launch.
PROJECT_ID:
o ID do projeto do Cloud em que você quer executar o job do Dataflow
JOB_NAME:
um nome de job de sua escolha
LOCATION:
o endpoint regional em que você quer
implantar o job do Dataflow, por exemplo, us-central1
VERSION:
a versão do modelo que você quer usar
Use estes valores:
latest para usar a versão mais recente do modelo, disponível na pasta mãe
não datada no bucket:
gs://dataflow-templates/latest/
o nome da versão, como 2021-09-20-00_RC00, para usar uma versão específica do
modelo, que pode ser aninhada na respectiva pasta mãe datada no bucket:
gs://dataflow-templates
STAGING_LOCATION: o local para organizar arquivos locais (por exemplo, gs://your-bucket/staging).
TEMP_LOCATION: o local para gravar arquivos temporários (por exemplo, gs://your-bucket/temp)
SUBSCRIPTION_NAME: o nome da assinatura
do Pub/Sub
TOPIC_NAME: o nome do tópico do Pub/Sub
FILTER_KEY: a chave de atributo que servirá para filtrar os eventos. Nenhum filtro será aplicado se nenhuma chave for especificada
FILTER_VALUE: valor do atributo de filtro a ser usado se uma chave de filtro de eventos for fornecida.
Aceita uma string Java Regex válida como um valor de filtro de eventos. Se uma regex for fornecida,
a expressão completa deverá corresponder para que a mensagem seja filtrada. Correspondências parciais
(como substrings) não são filtradas. Um valor de filtro de evento nulo é usado por padrão.
/*
* Copyright (C) 2018 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.cloud.teleport.templates;
import static com.google.common.base.Preconditions.checkNotNull;
import static org.apache.beam.vendor.guava.v26_0_jre.com.google.common.base.Preconditions.checkArgument;
import com.google.auto.value.AutoValue;
import com.google.cloud.teleport.metadata.Template;
import com.google.cloud.teleport.metadata.TemplateCategory;
import com.google.cloud.teleport.metadata.TemplateParameter;
import com.google.cloud.teleport.templates.PubsubToPubsub.Options;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
import javax.annotation.Nullable;
import org.apache.beam.sdk.Pipeline;
import org.apache.beam.sdk.PipelineResult;
import org.apache.beam.sdk.io.gcp.pubsub.PubsubIO;
import org.apache.beam.sdk.io.gcp.pubsub.PubsubMessage;
import org.apache.beam.sdk.metrics.Counter;
import org.apache.beam.sdk.metrics.Metrics;
import org.apache.beam.sdk.options.PipelineOptions;
import org.apache.beam.sdk.options.PipelineOptionsFactory;
import org.apache.beam.sdk.options.StreamingOptions;
import org.apache.beam.sdk.options.Validation;
import org.apache.beam.sdk.options.ValueProvider;
import org.apache.beam.sdk.transforms.DoFn;
import org.apache.beam.sdk.transforms.ParDo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/** A template that copies messages from one Pubsub subscription to another Pubsub topic. */
@Template(
name = "Cloud_PubSub_to_Cloud_PubSub",
category = TemplateCategory.STREAMING,
displayName = "Pub/Sub to Pub/Sub",
description =
"Streaming pipeline. Reads from a Pub/Sub subscription and writes to a Pub/Sub topic. ",
optionsClass = Options.class,
contactInformation = "https://cloud.google.com/support")
public class PubsubToPubsub {
/**
* Main entry point for executing the pipeline.
*
* @param args The command-line arguments to the pipeline.
*/
public static void main(String[] args) {
// Parse the user options passed from the command-line
Options options = PipelineOptionsFactory.fromArgs(args).withValidation().as(Options.class);
options.setStreaming(true);
run(options);
}
/**
* Runs the pipeline with the supplied options.
*
* @param options The execution parameters to the pipeline.
* @return The result of the pipeline execution.
*/
public static PipelineResult run(Options options) {
// Create the pipeline
Pipeline pipeline = Pipeline.create(options);
/**
* Steps: 1) Read PubSubMessage with attributes from input PubSub subscription. 2) Apply any
* filters if an attribute=value pair is provided. 3) Write each PubSubMessage to output PubSub
* topic.
*/
pipeline
.apply(
"Read PubSub Events",
PubsubIO.readMessagesWithAttributes().fromSubscription(options.getInputSubscription()))
.apply(
"Filter Events If Enabled",
ParDo.of(
ExtractAndFilterEventsFn.newBuilder()
.withFilterKey(options.getFilterKey())
.withFilterValue(options.getFilterValue())
.build()))
.apply("Write PubSub Events", PubsubIO.writeMessages().to(options.getOutputTopic()));
// Execute the pipeline and return the result.
return pipeline.run();
}
/**
* Options supported by {@link PubsubToPubsub}.
*
* <p>Inherits standard configuration options.
*/
public interface Options extends PipelineOptions, StreamingOptions {
@TemplateParameter.PubsubSubscription(
order = 1,
description = "Pub/Sub input subscription",
helpText =
"Pub/Sub subscription to read the input from, in the format of 'projects/your-project-id/subscriptions/your-subscription-name'",
example = "projects/your-project-id/subscriptions/your-subscription-name")
@Validation.Required
ValueProvider<String> getInputSubscription();
void setInputSubscription(ValueProvider<String> inputSubscription);
@TemplateParameter.PubsubTopic(
order = 2,
description = "Output Pub/Sub topic",
helpText =
"The name of the topic to which data should published, in the format of 'projects/your-project-id/topics/your-topic-name'",
example = "projects/your-project-id/topics/your-topic-name")
@Validation.Required
ValueProvider<String> getOutputTopic();
void setOutputTopic(ValueProvider<String> outputTopic);
@TemplateParameter.Text(
order = 3,
optional = true,
description = "Event filter key",
helpText =
"Attribute key by which events are filtered. No filters are applied if no key is specified.")
ValueProvider<String> getFilterKey();
void setFilterKey(ValueProvider<String> filterKey);
@TemplateParameter.Text(
order = 4,
optional = true,
description = "Event filter value",
helpText =
"Filter attribute value to use if an event filter key is provided. Accepts a valid "
+ "Java Regex string as an event filter value. In case a regex is provided, the complete "
+ "expression should match in order for the message to be filtered. Partial matches (e.g. "
+ "substring) will not be filtered. A null event filter value is used by default.")
ValueProvider<String> getFilterValue();
void setFilterValue(ValueProvider<String> filterValue);
}
/**
* DoFn that will determine if events are to be filtered. If filtering is enabled, it will only
* publish events that pass the filter else, it will publish all input events.
*/
@AutoValue
public abstract static class ExtractAndFilterEventsFn extends DoFn<PubsubMessage, PubsubMessage> {
private static final Logger LOG = LoggerFactory.getLogger(ExtractAndFilterEventsFn.class);
// Counter tracking the number of incoming Pub/Sub messages.
private static final Counter INPUT_COUNTER =
Metrics.counter(ExtractAndFilterEventsFn.class, "inbound-messages");
// Counter tracking the number of output Pub/Sub messages after the user provided filter
// is applied.
private static final Counter OUTPUT_COUNTER =
Metrics.counter(ExtractAndFilterEventsFn.class, "filtered-outbound-messages");
private Boolean doFilter;
private String inputFilterKey;
private Pattern inputFilterValueRegex;
private Boolean isNullFilterValue;
public static Builder newBuilder() {
return new AutoValue_PubsubToPubsub_ExtractAndFilterEventsFn.Builder();
}
@Nullable
abstract ValueProvider<String> filterKey();
@Nullable
abstract ValueProvider<String> filterValue();
@Setup
public void setup() {
if (this.doFilter != null) {
return; // Filter has been evaluated already
}
inputFilterKey = (filterKey() == null ? null : filterKey().get());
if (inputFilterKey == null) {
// Disable input message filtering.
this.doFilter = false;
} else {
this.doFilter = true; // Enable filtering.
String inputFilterValue = (filterValue() == null ? null : filterValue().get());
if (inputFilterValue == null) {
LOG.warn(
"User provided a NULL for filterValue. Only messages with a value of NULL for the"
+ " filterKey: {} will be filtered forward",
inputFilterKey);
// For backward compatibility, we are allowing filtering by null filterValue.
this.isNullFilterValue = true;
this.inputFilterValueRegex = null;
} else {
this.isNullFilterValue = false;
try {
inputFilterValueRegex = getFilterPattern(inputFilterValue);
} catch (PatternSyntaxException e) {
LOG.error("Invalid regex pattern for supplied filterValue: {}", inputFilterValue);
throw new RuntimeException(e);
}
}
LOG.info(
"Enabling event filter [key: " + inputFilterKey + "][value: " + inputFilterValue + "]");
}
}
@ProcessElement
public void processElement(ProcessContext context) {
INPUT_COUNTER.inc();
if (!this.doFilter) {
// Filter is not enabled
writeOutput(context, context.element());
} else {
PubsubMessage message = context.element();
String extractedValue = message.getAttribute(this.inputFilterKey);
if (this.isNullFilterValue) {
if (extractedValue == null) {
// If we are filtering for null and the extracted value is null, we forward
// the message.
writeOutput(context, message);
}
} else {
if (extractedValue != null
&& this.inputFilterValueRegex.matcher(extractedValue).matches()) {
// If the extracted value is not null and it matches the filter,
// we forward the message.
writeOutput(context, message);
}
}
}
}
/**
* Write a {@link PubsubMessage} and increment the output counter.
*
* @param context {@link ProcessContext} to write {@link PubsubMessage} to.
* @param message {@link PubsubMessage} output.
*/
private void writeOutput(ProcessContext context, PubsubMessage message) {
OUTPUT_COUNTER.inc();
context.output(message);
}
/**
* Return a {@link Pattern} based on a user provided regex string.
*
* @param regex Regex string to compile.
* @return {@link Pattern}
* @throws PatternSyntaxException If the string is an invalid regex.
*/
private Pattern getFilterPattern(String regex) throws PatternSyntaxException {
checkNotNull(regex, "Filter regex cannot be null.");
return Pattern.compile(regex);
}
/** Builder class for {@link ExtractAndFilterEventsFn}. */
@AutoValue.Builder
abstract static class Builder {
abstract Builder setFilterKey(ValueProvider<String> filterKey);
abstract Builder setFilterValue(ValueProvider<String> filterValue);
abstract ExtractAndFilterEventsFn build();
/**
* Method to set the filterKey used for filtering messages.
*
* @param filterKey Lookup key for the {@link PubsubMessage} attribute map.
* @return {@link Builder}
*/
public Builder withFilterKey(ValueProvider<String> filterKey) {
checkArgument(filterKey != null, "withFilterKey(filterKey) called with null input.");
return setFilterKey(filterKey);
}
/**
* Method to set the filterValue used for filtering messages.
*
* @param filterValue Lookup value for the {@link PubsubMessage} attribute map.
* @return {@link Builder}
*/
public Builder withFilterValue(ValueProvider<String> filterValue) {
checkArgument(filterValue != null, "withFilterValue(filterValue) called with null input.");
return setFilterValue(filterValue);
}
}
}
}
Pub/Sub para Splunk
O modelo do Pub/Sub para Splunk é um pipeline de streaming que lê
mensagens de uma assinatura do Pub/Sub e grava o payload da mensagem no Splunk pelo
Coletor de eventos HTTP (HEC, na sigla em inglês) do Splunk. O caso de uso mais comum desse modelo é exportar registros para o Splunk. Para ver um exemplo do fluxo de trabalho subjacente, consulte Como implantar exportações de registro prontas para produção no Splunk usando o Dataflow.
Antes de gravar no Splunk, também é possível aplicar uma função JavaScript
definida pelo usuário ao payload da mensagem. Todas as mensagens que apresentam falhas de processamento são encaminhadas
para um tópico não processado do Pub/Sub para posterior resolução de problemas e reprocessamento.
Como uma camada extra de proteção para o token HEC, é possível passar uma chave do Cloud KMS com o parâmetro de token HEC
codificado em base64 criptografado com a chave do Cloud KMS.
Consulte o endpoint de criptografia da API Cloud KMS
para saber mais detalhes sobre como criptografar o parâmetro de token do HEC.
Requisitos para este pipeline:
A inscrição do Pub/Sub de origem precisa existir antes da execução do pipeline.
O tópico do Pub/Sub precisa existir antes de o pipeline ser executado.
O endpoint HEC de Splunk precisa ser acessível pela rede dos workers do Dataflow.
O token Splunk de HEC precisa ser gerado e estar disponível.
Parâmetros do modelo
Parâmetro
Descrição
inputSubscription
A inscrição do Pub/Sub a partir da qual ler a entrada. Por exemplo, projects/<project-id>/subscriptions/<subscription-name>.
token
(Opcional) O token de autenticação HEC do Splunk. Precisará ser fornecido se tokenSource estiver definido como PLAINTEXT ou KMS.
url
O URL de HEC do Splunk. Precisa ser roteável a partir da VPC na qual o pipeline é executado. Por exemplo, https://splunk-hec-host:8088.
outputDeadletterTopic
O tópico do Pub/Sub para encaminhar mensagens não entregues. Por exemplo, projects/<project-id>/topics/<topic-name>.
javascriptTextTransformGcsPath
(Opcional)
O URI do Cloud Storage do arquivo .js que define a função definida
pelo usuário (UDF, na sigla em inglês) do JavaScript que você quer usar. Por exemplo, gs://my-bucket/my-udfs/my_file.js.
javascriptTextTransformFunctionName
(Opcional)
O nome da função definida pelo usuário (UDF) do JavaScript que você quer usar.
Por exemplo, se o código de função do JavaScript for
myTransform(inJson) { /*...do stuff...*/ }, o nome da função será
myTransform. Para amostras de UDFs do JavaScript, consulte os
exemplos de UDF.
batchCount
(Opcional) O tamanho do lote para enviar vários eventos para o Splunk. Padrão 1 (sem loteamento).
parallelism
(Opcional) O número máximo de solicitações paralelas. Padrão 1 (sem paralelismo).
disableCertificateValidation
(Opcional) Desativar a validação do certificado SSL. Falso padrão (validação ativada). Se for verdadeiro, os certificados não serão validados (todos os certificados são confiáveis) e o parâmetro `rootCaCertificatePath` será ignorado.
includePubsubMessage
(Opcional) Inclua a mensagem completa do Pub/Sub no payload. Falso padrão
(somente o elemento de dados está incluído no payload).
tokenSource
Origem do token. Um de PLAINTEXT, KMS ou SECRET_MANAGER. Esse parâmetro precisa ser fornecido se o Secret Manager for usado.
Se tokenSource estiver definido como KMS, tokenKMSEncryptionKey e a criptografia tokenprecisarão ser fornecidas.
Se tokenSource for definido como SECRET_MANAGER, tokenSecretIdprecisará ser fornecido.
Se tokenSource for definido como PLAINTEXT, tokenprecisará ser fornecido.
tokenKMSEncryptionKey
(Opcional) A chave do Cloud KMS para descriptografar a string do token HEC. Esse parâmetro precisará ser fornecido se o tokenSource estiver definido como KMS.
Se a chave do Cloud KMS for fornecida, a string do token HEC precisará ser transmitida de forma criptografada.
tokenSecretId
(Opcional) O ID do secret do Gerenciador de secrets do token. Esse parâmetro precisará ser fornecido se o tokenSource estiver definido como SECRET_MANAGER.
É necessário seguir este formato projects/<project-id>/secrets/<secret-name>/versions/<secret-version>.
rootCaCertificatePath
(Opcional) O URL completo do certificado de CA raiz no Cloud Storage. Por exemplo, gs://mybucket/mycerts/privateCA.crt O certificado fornecido no Cloud Storage precisa ser codificado em DER e pode ser fornecido em codificação binária ou imprimível (Base64).
Se o certificado for fornecido na codificação Base64, ele precisará ser delimitado no início por -----BEGIN CERTIFICATE----- e no final por -----END CERTIFICATE-----.
Se esse parâmetro for fornecido, esse arquivo de certificado de CA particular é buscado e adicionado ao armazenamento de confiança do worker do Dataflow para verificar o certificado SSL do endpoint do Splunk HEC.
Se esse parâmetro não for fornecido, o armazenamento de confiança padrão será usado.
enableBatchLogs
(Opcional) Especifica se os registros devem ser ativados para lotes gravados no Splunk. Padrão: true.
enableGzipHttpCompression
(Opcional) Especifica se as solicitações HTTP enviadas ao HEC do Splunk serão compactadas (conteúdo gzip codificado). Padrão: true.
Como executar o modelo do Pub/Sub para o Splunk
Console
Acesse a página Criar job usando um modelo do Dataflow.
REGION_NAME:
o endpoint regional em que você quer
implantar o job do Dataflow, por exemplo, us-central1
VERSION:
a versão do modelo que você quer usar
Use estes valores:
latest para usar a versão mais recente do modelo, disponível na pasta mãe
não datada no bucket:
gs://dataflow-templates/latest/
o nome da versão, como 2021-09-20-00_RC00, para usar uma versão específica do
modelo, que pode ser aninhada na respectiva pasta mãe datada no bucket:
gs://dataflow-templates
TEMP_LOCATION: o local para gravar arquivos temporários (por exemplo, gs://your-bucket/temp)
INPUT_SUBSCRIPTION_NAME: o nome da assinatura do Pub/Sub
TOKEN: token do coletor de eventos HTTP do Splunk
URL: o caminho do URL para o Coletor de eventos HTTP do Splunk
(por exemplo, https://splunk-hec-host:8088)
DEADLETTER_TOPIC_NAME: o nome do tópico do Pub/Sub
JAVASCRIPT_FUNCTION:
o nome da função definida pelo usuário (UDF) do JavaScript que você quer usar
Por exemplo, se o código de função do JavaScript for
myTransform(inJson) { /*...do stuff...*/ }, o nome da função será
myTransform. Para amostras de UDFs do JavaScript, consulte os
exemplos de UDF.
PATH_TO_JAVASCRIPT_UDF_FILE:
o URI do Cloud Storage do arquivo .js que define a função
definida pelo usuário (UDF, na sigla em inglês) do JavaScript que você quer usar, por exemplo,gs://my-bucket/my-udfs/my_file.js
BATCH_COUNT: o tamanho do lote a ser usado para enviar vários eventos para o Splunk
PARALLELISM: o número de solicitações paralelas a serem usadas para enviar eventos para o Splunk
DISABLE_VALIDATION: true se você quiser desativar a validação do certificado SSL
ROOT_CA_CERTIFICATE_PATH: o caminho para o certificado de CA raiz no Cloud Storage (por exemplo, gs://your-bucket/privateCA.crt)
API
Para executar o modelo usando a API REST, envie uma solicitação HTTP POST. Para mais informações sobre a
API e os respectivos escopos de autorização, consulte
projects.templates.launch.
PROJECT_ID:
o ID do projeto do Cloud em que você quer executar o job do Dataflow
JOB_NAME:
um nome de job de sua escolha
LOCATION:
o endpoint regional em que você quer
implantar o job do Dataflow, por exemplo, us-central1
VERSION:
a versão do modelo que você quer usar
Use estes valores:
latest para usar a versão mais recente do modelo, disponível na pasta mãe
não datada no bucket:
gs://dataflow-templates/latest/
o nome da versão, como 2021-09-20-00_RC00, para usar uma versão específica do
modelo, que pode ser aninhada na respectiva pasta mãe datada no bucket:
gs://dataflow-templates
TEMP_LOCATION: o local para gravar arquivos temporários (por exemplo, gs://your-bucket/temp)
INPUT_SUBSCRIPTION_NAME: o nome da assinatura do Pub/Sub
TOKEN: token do coletor de eventos HTTP do Splunk
URL: o caminho do URL para o Coletor de eventos HTTP do Splunk
(por exemplo, https://splunk-hec-host:8088)
DEADLETTER_TOPIC_NAME: o nome do tópico do Pub/Sub
JAVASCRIPT_FUNCTION:
o nome da função definida pelo usuário (UDF) do JavaScript que você quer usar
Por exemplo, se o código de função do JavaScript for
myTransform(inJson) { /*...do stuff...*/ }, o nome da função será
myTransform. Para amostras de UDFs do JavaScript, consulte os
exemplos de UDF.
PATH_TO_JAVASCRIPT_UDF_FILE:
o URI do Cloud Storage do arquivo .js que define a função
definida pelo usuário (UDF, na sigla em inglês) do JavaScript que você quer usar, por exemplo,gs://my-bucket/my-udfs/my_file.js
BATCH_COUNT: o tamanho do lote a ser usado para enviar vários eventos para o Splunk
PARALLELISM: o número de solicitações paralelas a serem usadas para enviar eventos para o Splunk
DISABLE_VALIDATION: true se você quiser desativar a validação do certificado SSL
ROOT_CA_CERTIFICATE_PATH: o caminho para o certificado de CA raiz no Cloud Storage (por exemplo, gs://your-bucket/privateCA.crt)
/*
* Copyright (C) 2019 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.cloud.teleport.templates;
import com.google.cloud.teleport.coders.FailsafeElementCoder;
import com.google.cloud.teleport.metadata.Template;
import com.google.cloud.teleport.metadata.TemplateCategory;
import com.google.cloud.teleport.splunk.SplunkEvent;
import com.google.cloud.teleport.splunk.SplunkEventCoder;
import com.google.cloud.teleport.splunk.SplunkIO;
import com.google.cloud.teleport.splunk.SplunkWriteError;
import com.google.cloud.teleport.templates.PubSubToSplunk.PubSubToSplunkOptions;
import com.google.cloud.teleport.templates.common.ErrorConverters;
import com.google.cloud.teleport.templates.common.JavascriptTextTransformer.FailsafeJavascriptUdf;
import com.google.cloud.teleport.templates.common.JavascriptTextTransformer.JavascriptTextTransformerOptions;
import com.google.cloud.teleport.templates.common.PubsubConverters.PubsubReadSubscriptionOptions;
import com.google.cloud.teleport.templates.common.PubsubConverters.PubsubWriteDeadletterTopicOptions;
import com.google.cloud.teleport.templates.common.SplunkConverters;
import com.google.cloud.teleport.templates.common.SplunkConverters.SplunkOptions;
import com.google.cloud.teleport.util.TokenNestedValueProvider;
import com.google.cloud.teleport.values.FailsafeElement;
import com.google.common.annotations.VisibleForTesting;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import com.google.gson.JsonSyntaxException;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import org.apache.beam.sdk.Pipeline;
import org.apache.beam.sdk.PipelineResult;
import org.apache.beam.sdk.coders.CoderRegistry;
import org.apache.beam.sdk.coders.StringUtf8Coder;
import org.apache.beam.sdk.io.gcp.pubsub.PubsubIO;
import org.apache.beam.sdk.io.gcp.pubsub.PubsubMessage;
import org.apache.beam.sdk.metrics.Counter;
import org.apache.beam.sdk.metrics.Metrics;
import org.apache.beam.sdk.options.PipelineOptionsFactory;
import org.apache.beam.sdk.options.ValueProvider;
import org.apache.beam.sdk.transforms.DoFn;
import org.apache.beam.sdk.transforms.Flatten;
import org.apache.beam.sdk.transforms.MapElements;
import org.apache.beam.sdk.transforms.PTransform;
import org.apache.beam.sdk.transforms.ParDo;
import org.apache.beam.sdk.values.PBegin;
import org.apache.beam.sdk.values.PCollection;
import org.apache.beam.sdk.values.PCollectionList;
import org.apache.beam.sdk.values.PCollectionTuple;
import org.apache.beam.sdk.values.TupleTag;
import org.apache.beam.vendor.guava.v26_0_jre.com.google.common.base.MoreObjects;
import org.apache.beam.vendor.guava.v26_0_jre.com.google.common.collect.ImmutableList;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link PubSubToSplunk} pipeline is a streaming pipeline which ingests data from Cloud
* Pub/Sub, executes a UDF, converts the output to {@link SplunkEvent}s and writes those records
* into Splunk's HEC endpoint. Any errors which occur in the execution of the UDF, conversion to
* {@link SplunkEvent} or writing to HEC will be streamed into a Pub/Sub topic.
*
* <p><b>Pipeline Requirements</b>
*
* <ul>
* <li>The source Pub/Sub subscription exists.
* <li>HEC end-point is routable from the VPC where the Dataflow job executes.
* <li>Deadletter topic exists.
* </ul>
*
* <p><b>Example Usage</b>
*
* <pre>
* # Set the pipeline vars
* PROJECT_ID=PROJECT ID HERE
* BUCKET_NAME=BUCKET NAME HERE
* PIPELINE_FOLDER=gs://${BUCKET_NAME}/dataflow/pipelines/pubsub-to-bigquery
* USE_SUBSCRIPTION=true or false depending on whether the pipeline should read
* from a Pub/Sub Subscription or a Pub/Sub Topic.
*
* # Set the runner
* RUNNER=DataflowRunner
*
* # Build the template
* mvn compile exec:java \
* -Dexec.mainClass=com.google.cloud.teleport.templates.PubSubToSplunk \
* -Dexec.cleanupDaemonThreads=false \
* -Dexec.args=" \
* --project=${PROJECT_ID} \
* --stagingLocation=${PIPELINE_FOLDER}/staging \
* --tempLocation=${PIPELINE_FOLDER}/temp \
* --templateLocation=${PIPELINE_FOLDER}/template/PubSubToSplunk \
* --runner=${RUNNER}
* "
*
* # Execute the template
* JOB_NAME=pubsub-to-splunk-$USER-`date +"%Y%m%d-%H%M%S%z"`
* BATCH_COUNT=1
* PARALLELISM=5
*
* # Execute the templated pipeline:
* gcloud dataflow jobs run ${JOB_NAME} \
* --gcs-location=${PIPELINE_FOLDER}/template/PubSubToSplunk \
* --zone=us-east1-d \
* --parameters \
* "inputSubscription=projects/${PROJECT_ID}/subscriptions/input-subscription-name,\
* token=my-splunk-hec-token,\
* url=http://splunk-hec-server-address:8088,\
* batchCount=${BATCH_COUNT},\
* parallelism=${PARALLELISM},\
* disableCertificateValidation=false,\
* outputDeadletterTopic=projects/${PROJECT_ID}/topics/deadletter-topic-name,\
* javascriptTextTransformGcsPath=gs://${BUCKET_NAME}/splunk/js/my-js-udf.js,\
* javascriptTextTransformFunctionName=myUdf"
* </pre>
*/
@Template(
name = "Cloud_PubSub_to_Splunk",
category = TemplateCategory.STREAMING,
displayName = "Pub/Sub to Splunk",
description =
"A pipeline that reads from a Pub/Sub subscription and writes to Splunk's HTTP Event Collector (HEC).",
optionsClass = PubSubToSplunkOptions.class,
optionsOrder = {
PubsubReadSubscriptionOptions.class,
SplunkOptions.class,
JavascriptTextTransformerOptions.class,
PubsubWriteDeadletterTopicOptions.class
},
contactInformation = "https://cloud.google.com/support")
public class PubSubToSplunk {
/** String/String Coder for FailsafeElement. */
public static final FailsafeElementCoder<String, String> FAILSAFE_ELEMENT_CODER =
FailsafeElementCoder.of(StringUtf8Coder.of(), StringUtf8Coder.of());
/** Counter to track inbound messages from source. */
private static final Counter INPUT_MESSAGES_COUNTER =
Metrics.counter(PubSubToSplunk.class, "inbound-pubsub-messages");
/** The tag for successful {@link SplunkEvent} conversion. */
private static final TupleTag<SplunkEvent> SPLUNK_EVENT_OUT = new TupleTag<SplunkEvent>() {};
/** The tag for failed {@link SplunkEvent} conversion. */
private static final TupleTag<FailsafeElement<String, String>> SPLUNK_EVENT_DEADLETTER_OUT =
new TupleTag<FailsafeElement<String, String>>() {};
/** The tag for the main output for the UDF. */
private static final TupleTag<FailsafeElement<String, String>> UDF_OUT =
new TupleTag<FailsafeElement<String, String>>() {};
/** The tag for the dead-letter output of the udf. */
private static final TupleTag<FailsafeElement<String, String>> UDF_DEADLETTER_OUT =
new TupleTag<FailsafeElement<String, String>>() {};
/** GSON to process a {@link PubsubMessage}. */
private static final Gson GSON = new Gson();
/** Logger for class. */
private static final Logger LOG = LoggerFactory.getLogger(PubSubToSplunk.class);
private static final Boolean DEFAULT_INCLUDE_PUBSUB_MESSAGE = false;
@VisibleForTesting protected static final String PUBSUB_MESSAGE_ATTRIBUTE_FIELD = "attributes";
@VisibleForTesting protected static final String PUBSUB_MESSAGE_DATA_FIELD = "data";
private static final String PUBSUB_MESSAGE_ID_FIELD = "messageId";
/**
* The main entry-point for pipeline execution. This method will start the pipeline but will not
* wait for it's execution to finish. If blocking execution is required, use the {@link
* PubSubToSplunk#run(PubSubToSplunkOptions)} method to start the pipeline and invoke {@code
* result.waitUntilFinish()} on the {@link PipelineResult}.
*
* @param args The command-line args passed by the executor.
*/
public static void main(String[] args) {
PubSubToSplunkOptions options =
PipelineOptionsFactory.fromArgs(args).withValidation().as(PubSubToSplunkOptions.class);
run(options);
}
/**
* Runs the pipeline to completion with the specified options. This method does not wait until the
* pipeline is finished before returning. Invoke {@code result.waitUntilFinish()} on the result
* object to block until the pipeline is finished running if blocking programmatic execution is
* required.
*
* @param options The execution options.
* @return The pipeline result.
*/
public static PipelineResult run(PubSubToSplunkOptions options) {
Pipeline pipeline = Pipeline.create(options);
// Register coders.
CoderRegistry registry = pipeline.getCoderRegistry();
registry.registerCoderForClass(SplunkEvent.class, SplunkEventCoder.of());
registry.registerCoderForType(
FAILSAFE_ELEMENT_CODER.getEncodedTypeDescriptor(), FAILSAFE_ELEMENT_CODER);
/*
* Steps:
* 1) Read messages in from Pub/Sub
* 2) Convert message to FailsafeElement for processing.
* 3) Apply user provided UDF (if any) on the input strings.
* 4) Convert successfully transformed messages into SplunkEvent objects
* 5) Write SplunkEvents to Splunk's HEC end point.
* 5a) Wrap write failures into a FailsafeElement.
* 6) Collect errors from UDF transform (#3), SplunkEvent transform (#4)
* and writing to Splunk HEC (#5) and stream into a Pub/Sub deadletter topic.
*/
// 1) Read messages in from Pub/Sub
PCollection<String> stringMessages =
pipeline.apply(
"ReadMessages",
new ReadMessages(options.getInputSubscription(), options.getIncludePubsubMessage()));
// 2) Convert message to FailsafeElement for processing.
PCollectionTuple transformedOutput =
stringMessages
.apply(
"ConvertToFailsafeElement",
MapElements.into(FAILSAFE_ELEMENT_CODER.getEncodedTypeDescriptor())
.via(input -> FailsafeElement.of(input, input)))
// 3) Apply user provided UDF (if any) on the input strings.
.apply(
"ApplyUDFTransformation",
FailsafeJavascriptUdf.<String>newBuilder()
.setFileSystemPath(options.getJavascriptTextTransformGcsPath())
.setFunctionName(options.getJavascriptTextTransformFunctionName())
.setLoggingEnabled(ValueProvider.StaticValueProvider.of(true))
.setSuccessTag(UDF_OUT)
.setFailureTag(UDF_DEADLETTER_OUT)
.build());
// 4) Convert successfully transformed messages into SplunkEvent objects
PCollectionTuple convertToEventTuple =
transformedOutput
.get(UDF_OUT)
.apply(
"ConvertToSplunkEvent",
SplunkConverters.failsafeStringToSplunkEvent(
SPLUNK_EVENT_OUT, SPLUNK_EVENT_DEADLETTER_OUT));
// 5) Write SplunkEvents to Splunk's HEC end point.
PCollection<SplunkWriteError> writeErrors =
convertToEventTuple
.get(SPLUNK_EVENT_OUT)
.apply(
"WriteToSplunk",
SplunkIO.writeBuilder()
.withToken(
new TokenNestedValueProvider(
options.getTokenSecretId(),
options.getTokenKMSEncryptionKey(),
options.getToken(),
options.getTokenSource()))
.withUrl(options.getUrl())
.withBatchCount(options.getBatchCount())
.withParallelism(options.getParallelism())
.withDisableCertificateValidation(options.getDisableCertificateValidation())
.withRootCaCertificatePath(options.getRootCaCertificatePath())
.withEnableBatchLogs(options.getEnableBatchLogs())
.withEnableGzipHttpCompression(options.getEnableGzipHttpCompression())
.build());
// 5a) Wrap write failures into a FailsafeElement.
PCollection<FailsafeElement<String, String>> wrappedSplunkWriteErrors =
writeErrors.apply(
"WrapSplunkWriteErrors",
ParDo.of(
new DoFn<SplunkWriteError, FailsafeElement<String, String>>() {
@ProcessElement
public void processElement(ProcessContext context) {
SplunkWriteError error = context.element();
FailsafeElement<String, String> failsafeElement =
FailsafeElement.of(error.payload(), error.payload());
if (error.statusMessage() != null) {
failsafeElement.setErrorMessage(error.statusMessage());
}
if (error.statusCode() != null) {
failsafeElement.setErrorMessage(
String.format("Splunk write status code: %d", error.statusCode()));
}
context.output(failsafeElement);
}
}));
// 6) Collect errors from UDF transform (#4), SplunkEvent transform (#5)
// and writing to Splunk HEC (#6) and stream into a Pub/Sub deadletter topic.
PCollectionList.of(
ImmutableList.of(
convertToEventTuple.get(SPLUNK_EVENT_DEADLETTER_OUT),
wrappedSplunkWriteErrors,
transformedOutput.get(UDF_DEADLETTER_OUT)))
.apply("FlattenErrors", Flatten.pCollections())
.apply(
"WriteFailedRecords",
ErrorConverters.WriteStringMessageErrorsToPubSub.newBuilder()
.setErrorRecordsTopic(options.getOutputDeadletterTopic())
.build());
return pipeline.run();
}
/**
* The {@link PubSubToSplunkOptions} class provides the custom options passed by the executor at
* the command line.
*/
public interface PubSubToSplunkOptions
extends SplunkOptions,
PubsubReadSubscriptionOptions,
PubsubWriteDeadletterTopicOptions,
JavascriptTextTransformerOptions {}
/**
* A {@link PTransform} that reads messages from a Pub/Sub subscription, increments a counter and
* returns a {@link PCollection} of {@link String} messages.
*/
private static class ReadMessages extends PTransform<PBegin, PCollection<String>> {
private final ValueProvider<String> subscriptionName;
private final ValueProvider<Boolean> inputIncludePubsubMessageFlag;
private Boolean includePubsubMessage;
ReadMessages(
ValueProvider<String> subscriptionName,
ValueProvider<Boolean> inputIncludePubsubMessageFlag) {
this.subscriptionName = subscriptionName;
this.inputIncludePubsubMessageFlag = inputIncludePubsubMessageFlag;
}
@Override
public PCollection<String> expand(PBegin input) {
return input
.apply(
"ReadPubsubMessage",
PubsubIO.readMessagesWithAttributes().fromSubscription(subscriptionName))
.apply(
"ExtractMessageIfRequired",
ParDo.of(
new DoFn<PubsubMessage, String>() {
@Setup
public void setup() {
if (inputIncludePubsubMessageFlag != null) {
includePubsubMessage = inputIncludePubsubMessageFlag.get();
}
includePubsubMessage =
MoreObjects.firstNonNull(
includePubsubMessage, DEFAULT_INCLUDE_PUBSUB_MESSAGE);
LOG.info("includePubsubMessage set to: {}", includePubsubMessage);
}
@ProcessElement
public void processElement(ProcessContext context) {
if (includePubsubMessage) {
context.output(formatPubsubMessage(context.element()));
} else {
context.output(
new String(context.element().getPayload(), StandardCharsets.UTF_8));
}
}
}))
.apply(
"CountMessages",
ParDo.of(
new DoFn<String, String>() {
@ProcessElement
public void processElement(ProcessContext context) {
INPUT_MESSAGES_COUNTER.inc();
context.output(context.element());
}
}));
}
}
/**
* Utility method that formats {@link org.apache.beam.sdk.io.gcp.pubsub.PubsubMessage} according
* to the model defined in {@link com.google.pubsub.v1.PubsubMessage}.
*
* @param pubsubMessage {@link org.apache.beam.sdk.io.gcp.pubsub.PubsubMessage}
* @return JSON String that adheres to the model defined in {@link
* com.google.pubsub.v1.PubsubMessage}
*/
@VisibleForTesting
protected static String formatPubsubMessage(PubsubMessage pubsubMessage) {
JsonObject messageJson = new JsonObject();
String payload = new String(pubsubMessage.getPayload(), StandardCharsets.UTF_8);
try {
JsonObject data = GSON.fromJson(payload, JsonObject.class);
messageJson.add(PUBSUB_MESSAGE_DATA_FIELD, data);
} catch (JsonSyntaxException e) {
messageJson.addProperty(PUBSUB_MESSAGE_DATA_FIELD, payload);
}
JsonObject attributes = getAttributesJson(pubsubMessage.getAttributeMap());
messageJson.add(PUBSUB_MESSAGE_ATTRIBUTE_FIELD, attributes);
if (pubsubMessage.getMessageId() != null) {
messageJson.addProperty(PUBSUB_MESSAGE_ID_FIELD, pubsubMessage.getMessageId());
}
return messageJson.toString();
}
/**
* Constructs a {@link JsonObject} from a {@link Map} of Pub/Sub attributes.
*
* @param attributesMap {@link Map} of Pub/Sub attributes
* @return {@link JsonObject} of Pub/Sub attributes
*/
private static JsonObject getAttributesJson(Map<String, String> attributesMap) {
JsonObject attributesJson = new JsonObject();
for (String key : attributesMap.keySet()) {
attributesJson.addProperty(key, attributesMap.get(key));
}
return attributesJson;
}
}
Pub/Sub para arquivos Avro no Cloud Storage
O modelo do Pub/Sub para arquivos do Avro no Cloud Storage é um pipeline de streaming que lê dados de um tópico
do Pub/Sub e grava arquivos Avro no bucket especificado do Cloud Storage.
Requisitos para este pipeline:
O tópico de entrada do Pub/Sub precisa existir antes da execução do pipeline.
Parâmetros do modelo
Parâmetro
Descrição
inputTopic
Tópico do Pub/Sub que será assinado para consumo de mensagens. O nome precisa estar no formato projects/<project-id>/topics/<topic-name>.
outputDirectory
Diretório em que os arquivos Avro de saída são arquivados. Precisa conter / no final.
Por exemplo, gs://example-bucket/example-directory/.
avroTempDirectory
Diretório dos arquivos Avro temporários. Precisa conter / no final. Por exemplo, gs://example-bucket/example-directory/.
outputFilenamePrefix
[Opcional] Prefixo do nome do arquivo de saída para os arquivos Avro.
outputFilenameSuffix
[Opcional] Sufixo do nome do arquivo de saída para os arquivos Avro.
outputShardTemplate
[Opcional] O modelo de fragmento do arquivo de saída. Ela é especificada como sequências repetidas das letras S ou N. Por exemplo, SSS-NNN. Eles são substituídos pelo número do fragmento ou pelo número total de fragmentos, respectivamente. Quando esse parâmetro não for especificado, o formato de modelo padrão será W-P-SS-of-NN.
Como executar o modelo Avro do Pub/Sub com o Cloud Storage
Console
Acesse a página Criar job usando um modelo do Dataflow.
REGION_NAME:
o endpoint regional em que você quer
implantar o job do Dataflow, por exemplo, us-central1
VERSION:
a versão do modelo que você quer usar
Use estes valores:
latest para usar a versão mais recente do modelo, disponível na pasta mãe
não datada no bucket:
gs://dataflow-templates/latest/
o nome da versão, como 2021-09-20-00_RC00, para usar uma versão específica do
modelo, que pode ser aninhada na respectiva pasta mãe datada no bucket:
gs://dataflow-templates
STAGING_LOCATION: o local para organizar arquivos locais (por exemplo, gs://your-bucket/staging).
TEMP_LOCATION: o local para gravar arquivos temporários (por exemplo, gs://your-bucket/temp)
TOPIC_NAME: o nome do tópico do Pub/Sub
BUCKET_NAME: o nome do bucket do
Cloud Storage
FILENAME_PREFIX: o prefixo de nome de arquivo de saída de sua preferência
FILENAME_SUFFIX: o sufixo de nome de arquivo de saída de sua preferência
SHARD_TEMPLATE: o modelo de fragmento de saída de sua preferência
API
Para executar o modelo usando a API REST, envie uma solicitação HTTP POST. Para mais informações sobre a
API e os respectivos escopos de autorização, consulte
projects.templates.launch.
PROJECT_ID:
o ID do projeto do Cloud em que você quer executar o job do Dataflow
JOB_NAME:
um nome de job de sua escolha
LOCATION:
o endpoint regional em que você quer
implantar o job do Dataflow, por exemplo, us-central1
VERSION:
a versão do modelo que você quer usar
Use estes valores:
latest para usar a versão mais recente do modelo, disponível na pasta mãe
não datada no bucket:
gs://dataflow-templates/latest/
o nome da versão, como 2021-09-20-00_RC00, para usar uma versão específica do
modelo, que pode ser aninhada na respectiva pasta mãe datada no bucket:
gs://dataflow-templates
STAGING_LOCATION: o local para organizar arquivos locais (por exemplo, gs://your-bucket/staging).
TEMP_LOCATION: o local para gravar arquivos temporários (por exemplo, gs://your-bucket/temp)
TOPIC_NAME: o nome do tópico do Pub/Sub
BUCKET_NAME: o nome do bucket do
Cloud Storage
FILENAME_PREFIX: o prefixo de nome de arquivo de saída de sua preferência
FILENAME_SUFFIX: o sufixo de nome de arquivo de saída de sua preferência
SHARD_TEMPLATE: o modelo de fragmento de saída de sua preferência
/*
* Copyright (C) 2018 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.cloud.teleport.templates;
import com.google.cloud.teleport.avro.AvroPubsubMessageRecord;
import com.google.cloud.teleport.io.WindowedFilenamePolicy;
import com.google.cloud.teleport.metadata.Template;
import com.google.cloud.teleport.metadata.TemplateCategory;
import com.google.cloud.teleport.metadata.TemplateCreationParameter;
import com.google.cloud.teleport.metadata.TemplateParameter;
import com.google.cloud.teleport.options.WindowedFilenamePolicyOptions;
import com.google.cloud.teleport.templates.PubsubToAvro.Options;
import com.google.cloud.teleport.util.DurationUtils;
import org.apache.beam.sdk.Pipeline;
import org.apache.beam.sdk.PipelineResult;
import org.apache.beam.sdk.io.AvroIO;
import org.apache.beam.sdk.io.FileBasedSink;
import org.apache.beam.sdk.io.fs.ResourceId;
import org.apache.beam.sdk.io.gcp.pubsub.PubsubIO;
import org.apache.beam.sdk.io.gcp.pubsub.PubsubMessage;
import org.apache.beam.sdk.options.Default;
import org.apache.beam.sdk.options.Description;
import org.apache.beam.sdk.options.PipelineOptions;
import org.apache.beam.sdk.options.PipelineOptionsFactory;
import org.apache.beam.sdk.options.StreamingOptions;
import org.apache.beam.sdk.options.Validation.Required;
import org.apache.beam.sdk.options.ValueProvider;
import org.apache.beam.sdk.options.ValueProvider.NestedValueProvider;
import org.apache.beam.sdk.transforms.DoFn;
import org.apache.beam.sdk.transforms.ParDo;
import org.apache.beam.sdk.transforms.SerializableFunction;
import org.apache.beam.sdk.transforms.windowing.FixedWindows;
import org.apache.beam.sdk.transforms.windowing.Window;
import org.apache.beam.sdk.values.PCollection;
/**
* This pipeline ingests incoming data from a Cloud Pub/Sub topic and outputs the raw data into
* windowed Avro files at the specified output directory.
*
* <p>Files output will have the following schema:
*
* <pre>
* {
* "type": "record",
* "name": "AvroPubsubMessageRecord",
* "namespace": "com.google.cloud.teleport.avro",
* "fields": [
* {"name": "message", "type": {"type": "array", "items": "bytes"}},
* {"name": "attributes", "type": {"type": "map", "values": "string"}},
* {"name": "timestamp", "type": "long"}
* ]
* }
* </pre>
*
* <p>Example Usage:
*
* <pre>
* # Set the pipeline vars
* PIPELINE_NAME=PubsubToAvro
* PROJECT_ID=PROJECT ID HERE
* PIPELINE_BUCKET=TEMPLATE STORAGE BUCKET NAME HERE
* OUTPUT_BUCKET=JOB OUTPUT BUCKET NAME HERE
* USE_SUBSCRIPTION=true or false depending on whether the pipeline should read
* from a Pub/Sub Subscription or a Pub/Sub Topic.
* PIPELINE_FOLDER=gs://${PIPELINE_BUCKET}/dataflow/pipelines/pubsub-to-gcs-avro
*
* # Set the runner
* RUNNER=DataflowRunner
*
* # Build the template
* mvn compile exec:java \
* -Dexec.mainClass=com.google.cloud.teleport.templates.${PIPELINE_NAME} \
* -Dexec.cleanupDaemonThreads=false \
* -Dexec.args=" \
* --project=${PROJECT_ID} \
* --stagingLocation=${PIPELINE_FOLDER}/staging \
* --tempLocation=${PIPELINE_FOLDER}/temp \
* --templateLocation=${PIPELINE_FOLDER}/template \
* --runner=${RUNNER} \
* --useSubscription=${USE_SUBSCRIPTION}"
*
* # Execute the template
* JOB_NAME=pubsub-to-bigquery-$USER-`date +"%Y%m%d-%H%M%S%z"`
*
* # Execute a pipeline to read from a Topic.
* gcloud dataflow jobs run ${JOB_NAME} \
* --gcs-location=${PIPELINE_FOLDER}/template \
* --zone=us-east1-d \
* --parameters \
* "inputTopic=projects/${PROJECT_ID}/topics/input-topic-name,\
* windowDuration=5m,\
* numShards=1,\
* userTempLocation=gs://${OUTPUT_BUCKET}/tmp/,\
* outputDirectory=gs://${OUTPUT_BUCKET}/output/,\
* outputFilenamePrefix=windowed-file,\
* outputFilenameSuffix=.txt"
*
* # Execute a pipeline to read from a Subscription.
* gcloud dataflow jobs run ${JOB_NAME} \
* --gcs-location=${PIPELINE_FOLDER}/template \
* --zone=us-east1-d \
* --parameters \
* "inputSubscription=projects/${PROJECT_ID}/subscriptions/input-subscription-name,\
* windowDuration=5m,\
* numShards=1,\
* userTempLocation=gs://${OUTPUT_BUCKET}/tmp/,\
* outputDirectory=gs://${OUTPUT_BUCKET}/output/,\
* outputFilenamePrefix=windowed-file,\
* outputFilenameSuffix=.avro"
* </pre>
*/
@Template(
name = "Cloud_PubSub_to_Avro",
category = TemplateCategory.STREAMING,
displayName = "Pub/Sub to Avro Files on Cloud Storage",
description =
"Streaming pipeline. Reads from a Pub/Sub subscription and outputs windowed Avro files to"
+ " the specified directory.",
optionsClass = Options.class,
skipOptions = "inputSubscription",
contactInformation = "https://cloud.google.com/support")
public class PubsubToAvro {
/**
* Options supported by the pipeline.
*
* <p>Inherits standard configuration options.
*/
public interface Options
extends PipelineOptions, StreamingOptions, WindowedFilenamePolicyOptions {
@TemplateParameter.PubsubSubscription(
order = 1,
description = "Pub/Sub input subscription",
helpText =
"Pub/Sub subscription to read the input from, in the format of"
+ " 'projects/your-project-id/subscriptions/your-subscription-name'",
example = "projects/your-project-id/subscriptions/your-subscription-name")
ValueProvider<String> getInputSubscription();
void setInputSubscription(ValueProvider<String> value);
@TemplateParameter.PubsubTopic(
order = 2,
description = "Pub/Sub input topic",
helpText =
"Pub/Sub topic to read the input from, in the format of "
+ "'projects/your-project-id/topics/your-topic-name'")
ValueProvider<String> getInputTopic();
void setInputTopic(ValueProvider<String> value);
@TemplateCreationParameter(value = "false")
@Description(
"This determines whether the template reads from " + "a pub/sub subscription or a topic")
@Default.Boolean(false)
Boolean getUseSubscription();
void setUseSubscription(Boolean value);
@TemplateParameter.GcsWriteFolder(
order = 4,
description = "Output file directory in Cloud Storage",
helpText =
"The path and filename prefix for writing output files. Must end with a slash. DateTime"
+ " formatting is used to parse directory path for date & time formatters.")
@Required
ValueProvider<String> getOutputDirectory();
void setOutputDirectory(ValueProvider<String> value);
@TemplateParameter.Text(
order = 5,
description = "Output filename prefix of the files to write",
helpText = "The prefix to place on each windowed file.")
@Default.String("output")
ValueProvider<String> getOutputFilenamePrefix();
void setOutputFilenamePrefix(ValueProvider<String> value);
@TemplateParameter.Text(
order = 6,
optional = true,
description = "Output filename suffix of the files to write",
helpText =
"The suffix to place on each windowed file. Typically a file extension such "
+ "as .txt or .csv.")
@Default.String("")
ValueProvider<String> getOutputFilenameSuffix();
void setOutputFilenameSuffix(ValueProvider<String> value);
@TemplateParameter.GcsWriteFolder(
order = 7,
description = "Temporary Avro write directory",
helpText = "Directory for temporary Avro files.")
@Required
ValueProvider<String> getAvroTempDirectory();
void setAvroTempDirectory(ValueProvider<String> value);
}
/**
* Main entry point for executing the pipeline.
*
* @param args The command-line arguments to the pipeline.
*/
public static void main(String[] args) {
Options options = PipelineOptionsFactory.fromArgs(args).withValidation().as(Options.class);
options.setStreaming(true);
run(options);
}
/**
* Runs the pipeline with the supplied options.
*
* @param options The execution parameters to the pipeline.
* @return The result of the pipeline execution.
*/
public static PipelineResult run(Options options) {
// Create the pipeline
Pipeline pipeline = Pipeline.create(options);
PCollection<PubsubMessage> messages = null;
/*
* Steps:
* 1) Read messages from PubSub
* 2) Window the messages into minute intervals specified by the executor.
* 3) Output the windowed data into Avro files, one per window by default.
*/
if (options.getUseSubscription()) {
messages =
pipeline.apply(
"Read PubSub Events",
PubsubIO.readMessagesWithAttributes()
.fromSubscription(options.getInputSubscription()));
} else {
messages =
pipeline.apply(
"Read PubSub Events",
PubsubIO.readMessagesWithAttributes().fromTopic(options.getInputTopic()));
}
messages
.apply("Map to Archive", ParDo.of(new PubsubMessageToArchiveDoFn()))
.apply(
options.getWindowDuration() + " Window",
Window.into(FixedWindows.of(DurationUtils.parseDuration(options.getWindowDuration()))))
// Apply windowed file writes. Use a NestedValueProvider because the filename
// policy requires a resourceId generated from the input value at runtime.
.apply(
"Write File(s)",
AvroIO.write(AvroPubsubMessageRecord.class)
.to(
WindowedFilenamePolicy.writeWindowedFiles()
.withOutputDirectory(options.getOutputDirectory())
.withOutputFilenamePrefix(options.getOutputFilenamePrefix())
.withShardTemplate(options.getOutputShardTemplate())
.withSuffix(options.getOutputFilenameSuffix())
.withYearPattern(options.getYearPattern())
.withMonthPattern(options.getMonthPattern())
.withDayPattern(options.getDayPattern())
.withHourPattern(options.getHourPattern())
.withMinutePattern(options.getMinutePattern()))
.withTempDirectory(
NestedValueProvider.of(
options.getAvroTempDirectory(),
(SerializableFunction<String, ResourceId>)
input -> FileBasedSink.convertToFileResourceIfPossible(input)))
/*.withTempDirectory(FileSystems.matchNewResource(
options.getAvroTempDirectory(),
Boolean.TRUE))
*/
.withWindowedWrites()
.withNumShards(options.getNumShards()));
// Execute the pipeline and return the result.
return pipeline.run();
}
/**
* Converts an incoming {@link PubsubMessage} to the {@link AvroPubsubMessageRecord} class by
* copying it's fields and the timestamp of the message.
*/
static class PubsubMessageToArchiveDoFn extends DoFn<PubsubMessage, AvroPubsubMessageRecord> {
@ProcessElement
public void processElement(ProcessContext context) {
PubsubMessage message = context.element();
context.output(
new AvroPubsubMessageRecord(
message.getPayload(), message.getAttributeMap(), context.timestamp().getMillis()));
}
}
}
Tópico do Pub/Sub para arquivos de texto no Cloud Storage
O modelo Cloud Pub/Sub para Cloud Storage Text é um pipeline de streaming que lê registros do Cloud Pub/Sub e os salva como uma série de arquivos do Cloud Storage em formato de texto. O modelo pode ser usado como uma maneira rápida de salvar dados em Pub/Sub para uso
futuro. Por padrão, o modelo gera um novo arquivo a cada cinco minutos.
Requisitos para este pipeline:
O tópico do Pub/Sub precisa existir antes da execução.
As mensagens publicadas no tópico precisam estar em formato de texto.
As mensagens publicadas no tópico não podem conter novas linhas. Observe que cada
mensagem do Pub/Sub é salva como uma linha única no arquivo de saída.
Parâmetros do modelo
Parâmetro
Descrição
inputTopic
O tópico do Pub/Sub em que a entrada será lida. O nome do tópico precisa estar no
formato projects/<project-id>/topics/<topic-name>.
outputDirectory
O caminho e o prefixo do nome do arquivo para gravar arquivos de saída. Por exemplo,
gs://bucket-name/path/. Esse valor precisa terminar com uma barra.
outputFilenamePrefix
O prefixo a ser colocado em cada arquivo em janela. Por exemplo, output-.
outputFilenameSuffix
O sufixo a ser colocado em cada arquivo em janela, normalmente uma extensão de arquivo, como
.txt ou .csv.
outputShardTemplate
O modelo de fragmento define a parte dinâmica de cada arquivo em janela. Por padrão, o pipeline usa um único fragmento para saída para o sistema de arquivos em cada janela. Isso significa
que todos os dados são gerados em um único arquivo por janela. O outputShardTemplate padrão é W-P-SS-of-NN, em que W é o intervalo de datas da janela, P são as informações do painel, S é o número do fragmento e N é a quantidade de fragmentos. No caso de um único arquivo, a parte SS-of-NN de
outputShardTemplate é 00-of-01.
Como executar o modelo do Pub/Sub para arquivos de texto no Cloud Storage
Console
Acesse a página Criar job usando um modelo do Dataflow.
REGION_NAME:
o endpoint regional em que você quer
implantar o job do Dataflow, por exemplo, us-central1
VERSION:
a versão do modelo que você quer usar
Use estes valores:
latest para usar a versão mais recente do modelo, disponível na pasta mãe
não datada no bucket:
gs://dataflow-templates/latest/
o nome da versão, como 2021-09-20-00_RC00, para usar uma versão específica do
modelo, que pode ser aninhada na respectiva pasta mãe datada no bucket:
gs://dataflow-templates
STAGING_LOCATION: o local para organizar arquivos locais (por exemplo, gs://your-bucket/staging).
TEMP_LOCATION: o local para gravar arquivos temporários (por exemplo, gs://your-bucket/temp)
TOPIC_NAME: o nome do tópico do Pub/Sub
BUCKET_NAME: o nome do bucket do
Cloud Storage
API
Para executar o modelo usando a API REST, envie uma solicitação HTTP POST. Para mais informações sobre a
API e os respectivos escopos de autorização, consulte
projects.templates.launch.
PROJECT_ID:
o ID do projeto do Cloud em que você quer executar o job do Dataflow
JOB_NAME:
um nome de job de sua escolha
LOCATION:
o endpoint regional em que você quer
implantar o job do Dataflow, por exemplo, us-central1
VERSION:
a versão do modelo que você quer usar
Use estes valores:
latest para usar a versão mais recente do modelo, disponível na pasta mãe
não datada no bucket:
gs://dataflow-templates/latest/
o nome da versão, como 2021-09-20-00_RC00, para usar uma versão específica do
modelo, que pode ser aninhada na respectiva pasta mãe datada no bucket:
gs://dataflow-templates
STAGING_LOCATION: o local para organizar arquivos locais (por exemplo, gs://your-bucket/staging).
TEMP_LOCATION: o local para gravar arquivos temporários (por exemplo, gs://your-bucket/temp)
/*
* Copyright (C) 2018 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.cloud.teleport.templates;
import com.google.cloud.teleport.io.WindowedFilenamePolicy;
import com.google.cloud.teleport.metadata.Template;
import com.google.cloud.teleport.metadata.TemplateCategory;
import com.google.cloud.teleport.metadata.TemplateCreationParameter;
import com.google.cloud.teleport.metadata.TemplateParameter;
import com.google.cloud.teleport.options.WindowedFilenamePolicyOptions;
import com.google.cloud.teleport.templates.PubsubToText.Options;
import com.google.cloud.teleport.util.DualInputNestedValueProvider;
import com.google.cloud.teleport.util.DualInputNestedValueProvider.TranslatorInput;
import com.google.cloud.teleport.util.DurationUtils;
import org.apache.beam.sdk.Pipeline;
import org.apache.beam.sdk.PipelineResult;
import org.apache.beam.sdk.io.FileBasedSink;
import org.apache.beam.sdk.io.TextIO;
import org.apache.beam.sdk.io.fs.ResourceId;
import org.apache.beam.sdk.io.gcp.pubsub.PubsubIO;
import org.apache.beam.sdk.options.Default;
import org.apache.beam.sdk.options.Description;
import org.apache.beam.sdk.options.PipelineOptions;
import org.apache.beam.sdk.options.PipelineOptionsFactory;
import org.apache.beam.sdk.options.StreamingOptions;
import org.apache.beam.sdk.options.Validation.Required;
import org.apache.beam.sdk.options.ValueProvider;
import org.apache.beam.sdk.options.ValueProvider.NestedValueProvider;
import org.apache.beam.sdk.transforms.SerializableFunction;
import org.apache.beam.sdk.transforms.windowing.FixedWindows;
import org.apache.beam.sdk.transforms.windowing.Window;
import org.apache.beam.sdk.values.PCollection;
/**
* This pipeline ingests incoming data from a Cloud Pub/Sub topic and outputs the raw data into
* windowed files at the specified output directory.
*
* <p>Example Usage:
*
* <pre>
* # Set the pipeline vars
* PIPELINE_NAME=PubsubToText
* PROJECT_ID=PROJECT ID HERE
* PIPELINE_BUCKET=TEMPLATE STORAGE BUCKET NAME HERE
* OUTPUT_BUCKET=JOB OUTPUT BUCKET NAME HERE
* PIPELINE_FOLDER=gs://${PIPELINE_BUCKET}/dataflow/pipelines/pubsub-to-gcs-text
* USE_SUBSCRIPTION=true or false depending on whether the pipeline should read
* from a Pub/Sub Subscription or a Pub/Sub Topic.
*
* # Set the runner
* RUNNER=DataflowRunner
*
* # Build the template
* mvn compile exec:java \
* -Dexec.mainClass=com.google.cloud.teleport.templates.${PIPELINE_NAME} \
* -Dexec.cleanupDaemonThreads=false \
* -Dexec.args=" \
* --project=${PROJECT_ID} \
* --stagingLocation=${PIPELINE_FOLDER}/staging \
* --tempLocation=${PIPELINE_FOLDER}/temp \
* --templateLocation=${PIPELINE_FOLDER}/template \
* --runner=${RUNNER} \
* --useSubscription=${USE_SUBSCRIPTION}"
*
* # Execute the template
* JOB_NAME=pubsub-to-bigquery-$USER-`date +"%Y%m%d-%H%M%S%z"`
*
* # Execute a pipeline to read from a Topic.
* gcloud dataflow jobs run ${JOB_NAME} \
* --gcs-location=${PIPELINE_FOLDER}/template \
* --zone=us-east1-d \
* --parameters \
* "inputTopic=projects/${PROJECT_ID}/topics/input-topic-name,\
* userTempLocation=gs://${OUTPUT_BUCKET}/tmp/,\
* windowDuration=5m,\
* numShards=1,\
* outputDirectory=gs://${OUTPUT_BUCKET}/output/,\
* outputFilenamePrefix=windowed-file,\
* outputFilenameSuffix=.txt"
*
* # Execute a pipeline to read from a Subscription.
* gcloud dataflow jobs run ${JOB_NAME} \
* --gcs-location=${PIPELINE_FOLDER}/template \
* --zone=us-east1-d \
* --parameters \
* "inputSubscription=projects/${PROJECT_ID}/subscriptions/input-subscription-name,\
* windowDuration=5m,\
* numShards=1,\
* userTempLocation=gs://${OUTPUT_BUCKET}/tmp/,\
* outputDirectory=gs://${OUTPUT_BUCKET}/output/,\
* outputFilenamePrefix=windowed-file,\
* outputFilenameSuffix=.txt"
* </pre>
*/
@Template(
name = "Cloud_PubSub_to_GCS_Text",
category = TemplateCategory.STREAMING,
displayName = "Pub/Sub to Text Files on Cloud Storage",
description =
"Streaming pipeline. Reads records from Pub/Sub and writes them to Cloud Storage, creating"
+ " a text file for each five minute window. Note that this pipeline assumes no"
+ " newlines in the body of the Pub/Sub message and thus each message becomes a single"
+ " line in the output file.",
optionsClass = Options.class,
contactInformation = "https://cloud.google.com/support")
public class PubsubToText {
/**
* Options supported by the pipeline.
*
* <p>Inherits standard configuration options.
*/
public interface Options
extends PipelineOptions, StreamingOptions, WindowedFilenamePolicyOptions {
@TemplateParameter.PubsubSubscription(
order = 1,
optional = true,
description = "Pub/Sub input subscription",
helpText =
"Pub/Sub subscription to read the input from, in the format of"
+ " 'projects/your-project-id/subscriptions/your-subscription-name'",
example = "projects/your-project-id/subscriptions/your-subscription-name")
ValueProvider<String> getInputSubscription();
void setInputSubscription(ValueProvider<String> value);
@TemplateParameter.PubsubTopic(
order = 2,
optional = true,
description = "Pub/Sub input topic",
helpText =
"Pub/Sub topic to read the input from, in the format of "
+ "'projects/your-project-id/topics/your-topic-name'")
ValueProvider<String> getInputTopic();
void setInputTopic(ValueProvider<String> value);
@TemplateCreationParameter(value = "false")
@Description(
"This determines whether the template reads from a Pub/Sub subscription or a topic")
@Default.Boolean(false)
Boolean getUseSubscription();
void setUseSubscription(Boolean value);
@TemplateParameter.GcsWriteFolder(
order = 3,
description = "Output file directory in Cloud Storage",
helpText =
"The path and filename prefix for writing output files. Must end with a slash. DateTime"
+ " formatting is used to parse directory path for date & time formatters.")
@Required
ValueProvider<String> getOutputDirectory();
void setOutputDirectory(ValueProvider<String> value);
@TemplateParameter.GcsWriteFolder(
order = 4,
optional = true,
description = "User provided temp location",
helpText =
"The user provided directory to output temporary files to. Must end with a slash.")
ValueProvider<String> getUserTempLocation();
void setUserTempLocation(ValueProvider<String> value);
@TemplateParameter.Text(
order = 5,
description = "Output filename prefix of the files to write",
helpText = "The prefix to place on each windowed file.")
@Default.String("output")
@Required
ValueProvider<String> getOutputFilenamePrefix();
void setOutputFilenamePrefix(ValueProvider<String> value);
@TemplateParameter.Text(
order = 6,
optional = true,
description = "Output filename suffix of the files to write",
helpText =
"The suffix to place on each windowed file. Typically a file extension such "
+ "as .txt or .csv.")
@Default.String("")
ValueProvider<String> getOutputFilenameSuffix();
void setOutputFilenameSuffix(ValueProvider<String> value);
}
/**
* Main entry point for executing the pipeline.
*
* @param args The command-line arguments to the pipeline.
*/
public static void main(String[] args) {
Options options = PipelineOptionsFactory.fromArgs(args).withValidation().as(Options.class);
options.setStreaming(true);
run(options);
}
/**
* Runs the pipeline with the supplied options.
*
* @param options The execution parameters to the pipeline.
* @return The result of the pipeline execution.
*/
public static PipelineResult run(Options options) {
// Create the pipeline
Pipeline pipeline = Pipeline.create(options);
PCollection<String> messages = null;
/*
* Steps:
* 1) Read string messages from PubSub
* 2) Window the messages into minute intervals specified by the executor.
* 3) Output the windowed files to GCS
*/
if (options.getUseSubscription()) {
messages =
pipeline.apply(
"Read PubSub Events",
PubsubIO.readStrings().fromSubscription(options.getInputSubscription()));
} else {
messages =
pipeline.apply(
"Read PubSub Events", PubsubIO.readStrings().fromTopic(options.getInputTopic()));
}
messages
.apply(
options.getWindowDuration() + " Window",
Window.into(FixedWindows.of(DurationUtils.parseDuration(options.getWindowDuration()))))
// Apply windowed file writes. Use a NestedValueProvider because the filename
// policy requires a resourceId generated from the input value at runtime.
.apply(
"Write File(s)",
TextIO.write()
.withWindowedWrites()
.withNumShards(options.getNumShards())
.to(
WindowedFilenamePolicy.writeWindowedFiles()
.withOutputDirectory(options.getOutputDirectory())
.withOutputFilenamePrefix(options.getOutputFilenamePrefix())
.withShardTemplate(options.getOutputShardTemplate())
.withSuffix(options.getOutputFilenameSuffix())
.withYearPattern(options.getYearPattern())
.withMonthPattern(options.getMonthPattern())
.withDayPattern(options.getDayPattern())
.withHourPattern(options.getHourPattern())
.withMinutePattern(options.getMinutePattern()))
.withTempDirectory(
NestedValueProvider.of(
maybeUseUserTempLocation(
options.getUserTempLocation(), options.getOutputDirectory()),
(SerializableFunction<String, ResourceId>)
input -> FileBasedSink.convertToFileResourceIfPossible(input))));
// Execute the pipeline and return the result.
return pipeline.run();
}
/**
* Utility method for using optional parameter userTempLocation as TempDirectory. This is useful
* when output bucket is locked and temporary data cannot be deleted.
*
* @param userTempLocation user provided temp location
* @param outputLocation user provided outputDirectory to be used as the default temp location
* @return userTempLocation if available, otherwise outputLocation is returned.
*/
private static ValueProvider<String> maybeUseUserTempLocation(
ValueProvider<String> userTempLocation, ValueProvider<String> outputLocation) {
return DualInputNestedValueProvider.of(
userTempLocation,
outputLocation,
new SerializableFunction<TranslatorInput<String, String>, String>() {
@Override
public String apply(TranslatorInput<String, String> input) {
return (input.getX() != null) ? input.getX() : input.getY();
}
});
}
}
Tópico do Pub/Sub ou assinatura de arquivos de texto no Cloud Storage
O modelo do Pub/Sub para o Cloud Storage Text é um pipeline de streaming que lê
registros do Pub/Sub e os salva como uma série de arquivos do Cloud Storage em formato
de texto. O modelo pode ser usado como uma maneira rápida de salvar dados em Pub/Sub para uso
futuro. Por padrão, o modelo gera um novo arquivo a cada cinco minutos.
Requisitos para este pipeline:
O tópico ou a assinatura do Pub/Sub precisa ter sido criado antes da execução.
As mensagens publicadas no tópico precisam estar em formato de texto.
As mensagens publicadas no tópico não podem conter novas linhas. Observe que cada
mensagem do Pub/Sub é salva como uma linha única no arquivo de saída.
Parâmetros do modelo
Parâmetro
Descrição
inputTopic
O tópico do Pub/Sub em que a entrada será lida. O nome do tópico precisa estar no
formato projects/<project-id>/topics/<topic-name>. Se esse parâmetro for fornecido, inputSubscription não poderá ser fornecido.
inputSubscription
O tópico do Pub/Sub em que a entrada será lida. O nome da assinatura precisa estar no formato projects/<project-id>/subscription/<subscription-name>. Se esse
parâmetro for fornecido, inputTopic não deverá ser fornecido.
outputDirectory
O caminho e o prefixo do nome do arquivo para gravar arquivos de saída. Por exemplo,
gs://bucket-name/path/. Esse valor precisa terminar com uma barra.
outputFilenamePrefix
O prefixo a ser colocado em cada arquivo em janela. Por exemplo, output-.
outputFilenameSuffix
O sufixo a ser colocado em cada arquivo em janela, normalmente uma extensão de arquivo, como
.txt ou .csv.
outputShardTemplate
O modelo de fragmento define a parte dinâmica de cada arquivo em janela. Por padrão, o pipeline usa um único fragmento para saída para o sistema de arquivos em cada janela. Isso significa
que todos os dados são gerados em um único arquivo por janela. O outputShardTemplate padrão é W-P-SS-of-NN, em que W é o intervalo de datas da janela, P são as informações do painel, S é o número do fragmento e N é a quantidade de fragmentos. No caso de um único arquivo, a parte SS-of-NN de
outputShardTemplate é 00-of-01.
windowDuration
(Opcional) A duração da janela é o intervalo em que os dados são gravados no diretório
de saída. Configure a duração com base na capacidade de processamento do pipeline. Por exemplo, uma capacidade de processamento
mais alta pode exigir tamanhos de janela menores para que os dados se encaixem na memória. O padrão é
5 min, com um mínimo de 1 s. Os formatos permitidos são: [int]s para segundos (5 s, por exemplo); [int]m para
minutos (12 min, por exemplo); [int]h para horas (2h, por exemplo).
Como executar o tópico do Pub/Sub ou a assinatura de arquivos de texto no modelo do Cloud Storage
Console
Acesse a página Criar job usando um modelo do Dataflow.
REGION_NAME:
o endpoint regional em que você quer
implantar o job do Dataflow, por exemplo, us-central1
VERSION:
a versão do modelo que você quer usar
Use estes valores:
latest para usar a versão mais recente do modelo, disponível na pasta mãe
não datada no bucket:
gs://dataflow-templates/latest/
o nome da versão, como 2021-09-20-00_RC00, para usar uma versão específica do
modelo, que pode ser aninhada na respectiva pasta mãe datada no bucket:
gs://dataflow-templates
SUBSCRIPTION_NAME: o nome da sua assinatura de Pub/Sub
BUCKET_NAME: o nome do bucket do Cloud Storage
API
Para executar o modelo usando a API REST, envie uma solicitação HTTP POST. Para mais informações sobre a
API e os respectivos escopos de autorização, consulte
projects.templates.launch.
PROJECT_ID:
o ID do projeto do Cloud em que você quer executar o job do Dataflow
JOB_NAME:
um nome de job de sua escolha
LOCATION:
o endpoint regional em que você quer
implantar o job do Dataflow, por exemplo, us-central1
VERSION:
a versão do modelo que você quer usar
Use estes valores:
latest para usar a versão mais recente do modelo, disponível na pasta mãe
não datada no bucket:
gs://dataflow-templates/latest/
o nome da versão, como 2021-09-20-00_RC00, para usar uma versão específica do
modelo, que pode ser aninhada na respectiva pasta mãe datada no bucket:
gs://dataflow-templates
SUBSCRIPTION_NAME: o nome da sua assinatura de Pub/Sub
/*
* Copyright (C) 2022 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.cloud.teleport.v2.templates.pubsubtotext;
import com.google.cloud.teleport.metadata.Template;
import com.google.cloud.teleport.metadata.TemplateCategory;
import com.google.cloud.teleport.metadata.TemplateParameter;
import com.google.cloud.teleport.v2.common.UncaughtExceptionLogger;
import com.google.cloud.teleport.v2.io.WindowedFilenamePolicy;
import com.google.cloud.teleport.v2.options.WindowedFilenamePolicyOptions;
import com.google.cloud.teleport.v2.templates.pubsubtotext.PubsubToText.Options;
import com.google.cloud.teleport.v2.utils.DurationUtils;
import com.google.common.base.Strings;
import org.apache.beam.sdk.Pipeline;
import org.apache.beam.sdk.PipelineResult;
import org.apache.beam.sdk.io.FileBasedSink;
import org.apache.beam.sdk.io.TextIO;
import org.apache.beam.sdk.io.gcp.pubsub.PubsubIO;
import org.apache.beam.sdk.options.Default;
import org.apache.beam.sdk.options.PipelineOptions;
import org.apache.beam.sdk.options.PipelineOptionsFactory;
import org.apache.beam.sdk.options.StreamingOptions;
import org.apache.beam.sdk.options.Validation.Required;
import org.apache.beam.sdk.transforms.windowing.FixedWindows;
import org.apache.beam.sdk.transforms.windowing.Window;
import org.apache.beam.sdk.values.PCollection;
/**
* This pipeline ingests incoming data from a Cloud Pub/Sub topic and outputs the raw data into
* windowed files at the specified output directory.
*
* <p>Example Usage:
*
* <pre>
* # Set the pipeline vars
* export PROJECT={project id}
* export TEMPLATE_MODULE=googlecloud-to-googlecloud
* export TEMPLATE_NAME=pubsub-to-text
* export BUCKET_NAME=gs://{bucket name}
* export TARGET_GCR_IMAGE=gcr.io/${PROJECT}/${TEMPLATE_NAME}-image
* export BASE_CONTAINER_IMAGE=gcr.io/dataflow-templates-base/java11-template-launcher-base
* export BASE_CONTAINER_IMAGE_VERSION=latest
* export APP_ROOT=/template/${TEMPLATE_NAME}
* export COMMAND_SPEC=${APP_ROOT}/resources/${TEMPLATE_NAME}-command-spec.json
* export TEMPLATE_IMAGE_SPEC=${BUCKET_NAME}/images/${TEMPLATE_NAME}-image-spec.json
*
* gcloud config set project ${PROJECT}
*
* # Build and push image to Google Container Repository
* mvn package \
* -Dimage=${TARGET_GCR_IMAGE} \
* -Dbase-container-image=${BASE_CONTAINER_IMAGE} \
* -Dbase-container-image.version=${BASE_CONTAINER_IMAGE_VERSION} \
* -Dapp-root=${APP_ROOT} \
* -Dcommand-spec=${COMMAND_SPEC} \
* -Djib.applicationCache=/tmp/jib-cache \
* -am -pl ${TEMPLATE_MODULE}
*
* # Create and upload image spec
* echo '{
* "image":"'${TARGET_GCR_IMAGE}'",
* "metadata":{
* "name":"Pub/Sub to text",
* "description":"Write Pub/Sub messages to GCS text files.",
* "parameters":[
* {
* "name":"inputSubscription",
* "label":"Pub/Sub subscription to read from",
* "paramType":"TEXT",
* "isOptional":true
* },
* {
* "name":"inputTopic",
* "label":"Pub/Sub topic to read from",
* "paramType":"TEXT",
* "isOptional":true
* },
* {
* "name":"outputDirectory",
* "label":"Directory to output files to",
* "paramType":"TEXT",
* "isOptional":false
* },
* {
* "name":"outputFilenamePrefix",
* "label":"The filename prefix of the files to write to",
* "paramType":"TEXT",
* "isOptional":false
* },
* {
* "name":"outputFilenameSuffix",
* "label":"The suffix of the files to write to",
* "paramType":"TEXT",
* "isOptional":true
* },
* {
* "name":"userTempLocation",
* "label":"The directory to output temporary files to",
* "paramType":"TEXT",
* "isOptional":true
* }
* ]
* },
* "sdk_info":{"language":"JAVA"}
* }' > image_spec.json
* gsutil cp image_spec.json ${TEMPLATE_IMAGE_SPEC}
* rm image_spec.json
*
* # Run template
* export JOB_NAME="${TEMPLATE_MODULE}-`date +%Y%m%d-%H%M%S-%N`"
* gcloud beta dataflow flex-template run ${JOB_NAME} \
* --project=${PROJECT} --region=us-central1 \
* --template-file-gcs-location=${TEMPLATE_IMAGE_SPEC} \
* --parameters inputTopic={topic},outputDirectory={directory},outputFilenamePrefix={prefix}
* </pre>
*/
@Template(
name = "Cloud_PubSub_to_GCS_Text_Flex",
category = TemplateCategory.STREAMING,
displayName = "Pub/Sub Subscription or Topic to Text Files on Cloud Storage",
description =
"Streaming pipeline. Reads records from Pub/Sub Subscription or Topic and writes them to"
+ " Cloud Storage, creating a text file for each five minute window. Note that this"
+ " pipeline assumes no newlines in the body of the Pub/Sub message and thus each"
+ " message becomes a single line in the output file.",
optionsClass = Options.class,
flexContainerName = "pubsub-to-text",
contactInformation = "https://cloud.google.com/support")
public class PubsubToText {
/**
* Options supported by the pipeline.
*
* <p>Inherits standard configuration options.
*/
public interface Options
extends PipelineOptions, StreamingOptions, WindowedFilenamePolicyOptions {
@TemplateParameter.PubsubTopic(
order = 1,
optional = true,
description = "Pub/Sub input topic",
helpText =
"Pub/Sub topic to read the input from, in the format of "
+ "'projects/your-project-id/topics/your-topic-name'",
example = "projects/your-project-id/topics/your-topic-name")
String getInputTopic();
void setInputTopic(String value);
@TemplateParameter.PubsubSubscription(
order = 2,
optional = true,
description = "Pub/Sub input subscription",
helpText =
"Pub/Sub subscription to read the input from, in the format of"
+ " 'projects/your-project-id/subscriptions/your-subscription-name'",
example = "projects/your-project-id/subscriptions/your-subscription-name")
String getInputSubscription();
void setInputSubscription(String value);
@TemplateParameter.GcsWriteFolder(
order = 3,
description = "Output file directory in Cloud Storage",
helpText =
"The path and filename prefix for writing output files. Must end with a slash. DateTime"
+ " formatting is used to parse directory path for date & time formatters.",
example = "gs://your-bucket/your-path")
@Required
String getOutputDirectory();
void setOutputDirectory(String value);
@TemplateParameter.GcsWriteFolder(
order = 4,
optional = true,
description = "User provided temp location",
helpText =
"The user provided directory to output temporary files to. Must end with a slash.")
String getUserTempLocation();
void setUserTempLocation(String value);
@TemplateParameter.Text(
order = 5,
optional = true,
description = "Output filename prefix of the files to write",
helpText = "The prefix to place on each windowed file.",
example = "output-")
@Default.String("output")
@Required
String getOutputFilenamePrefix();
void setOutputFilenamePrefix(String value);
@TemplateParameter.Text(
order = 6,
optional = true,
description = "Output filename suffix of the files to write",
helpText =
"The suffix to place on each windowed file. Typically a file extension such "
+ "as .txt or .csv.",
example = ".txt")
@Default.String("")
String getOutputFilenameSuffix();
void setOutputFilenameSuffix(String value);
}
/**
* Main entry point for executing the pipeline.
*
* @param args The command-line arguments to the pipeline.
*/
public static void main(String[] args) {
UncaughtExceptionLogger.register();
Options options = PipelineOptionsFactory.fromArgs(args).withValidation().as(Options.class);
options.setStreaming(true);
run(options);
}
/**
* Runs the pipeline with the supplied options.
*
* @param options The execution parameters to the pipeline.
* @return The result of the pipeline execution.
*/
public static PipelineResult run(Options options) {
boolean useInputSubscription = !Strings.isNullOrEmpty(options.getInputSubscription());
boolean useInputTopic = !Strings.isNullOrEmpty(options.getInputTopic());
if (useInputSubscription == useInputTopic) {
throw new IllegalArgumentException(
"Either input topic or input subscription must be provided, but not both.");
}
// Create the pipeline
Pipeline pipeline = Pipeline.create(options);
PCollection<String> messages = null;
/*
* Steps:
* 1) Read string messages from PubSub
* 2) Window the messages into minute intervals specified by the executor.
* 3) Output the windowed files to GCS
*/
if (useInputSubscription) {
messages =
pipeline.apply(
"Read PubSub Events",
PubsubIO.readStrings().fromSubscription(options.getInputSubscription()));
} else {
messages =
pipeline.apply(
"Read PubSub Events", PubsubIO.readStrings().fromTopic(options.getInputTopic()));
}
messages
.apply(
options.getWindowDuration() + " Window",
Window.into(FixedWindows.of(DurationUtils.parseDuration(options.getWindowDuration()))))
// Apply windowed file writes
.apply(
"Write File(s)",
TextIO.write()
.withWindowedWrites()
.withNumShards(options.getNumShards())
.to(
WindowedFilenamePolicy.writeWindowedFiles()
.withOutputDirectory(options.getOutputDirectory())
.withOutputFilenamePrefix(options.getOutputFilenamePrefix())
.withShardTemplate(options.getOutputShardTemplate())
.withSuffix(options.getOutputFilenameSuffix())
.withYearPattern(options.getYearPattern())
.withMonthPattern(options.getMonthPattern())
.withDayPattern(options.getDayPattern())
.withHourPattern(options.getHourPattern())
.withMinutePattern(options.getMinutePattern()))
.withTempDirectory(
FileBasedSink.convertToFileResourceIfPossible(
maybeUseUserTempLocation(
options.getUserTempLocation(), options.getOutputDirectory()))));
// Execute the pipeline and return the result.
return pipeline.run();
}
/**
* Utility method for using optional parameter userTempLocation as TempDirectory. This is useful
* when output bucket is locked and temporary data cannot be deleted.
*
* @param userTempLocation user provided temp location
* @param outputLocation user provided outputDirectory to be used as the default temp location
* @return userTempLocation if available, otherwise outputLocation is returned.
*/
private static String maybeUseUserTempLocation(String userTempLocation, String outputLocation) {
return !Strings.isNullOrEmpty(userTempLocation) ? userTempLocation : outputLocation;
}
}
Pub/Sub para MongoDB
O modelo do Pub/Sub para MongoDB é um pipeline de streaming que lê mensagens JSON codificadas de uma assinatura do Pub/Sub e grava no MongoDB como documentos.
Caso seja necessário, o pipeline é compatível com transformações extras que podem ser incluídas usando uma função definida pelo usuário (UDF) do JavaScript. Qualquer erro ocorrido devido à incompatibilidade
de esquema, JSON malformado ou ao executar transformações são gravados em uma tabela do BigQuery para mensagens não processadas, juntamente com mensagens de entrada. Se não existir uma tabela para registros não
processados antes da execução, o pipeline criará automaticamente essa tabela.
Requisitos para este pipeline:
A assinatura do Pub/Sub precisa existir e as mensagens precisam ser codificadas em um formato JSON válido.
O cluster do MongoDB precisa existir e poder ser acessado das máquinas de trabalho do Dataflow.
Parâmetros do modelo
Parâmetro
Descrição
inputSubscription
Nome da assinatura do Pub/Sub. Exemplo: projects/my-project-id/subscriptions/my-subscription-id
mongoDBUri
Lista separada por vírgulas dos servidores MongoDB. Exemplo: 192.285.234.12:27017,192.287.123.11:27017
database
Banco de dados no MongoDB para armazenar a coleção. Por exemplo, my-db.
collection
Nome da coleção dentro do banco de dados MongoDB. Por exemplo, my-collection.
deadletterTable
Tabela do BigQuery que armazena mensagens geradas por falhas (esquema incorreto, JSON incorreto etc.). Por exemplo, project-id:dataset-name.table-name.
javascriptTextTransformGcsPath
(Opcional)
O URI do Cloud Storage do arquivo .js que define a função definida
pelo usuário (UDF, na sigla em inglês) do JavaScript que você quer usar. Por exemplo, gs://my-bucket/my-udfs/my_file.js.
javascriptTextTransformFunctionName
(Opcional)
O nome da função definida pelo usuário (UDF) do JavaScript que você quer usar.
Por exemplo, se o código de função do JavaScript for
myTransform(inJson) { /*...do stuff...*/ }, o nome da função será
myTransform. Para amostras de UDFs do JavaScript, consulte os
exemplos de UDF.
batchSize
(Opcional) Tamanho do lote usado para a inserção de documentos em lote no MongoDB. Padrão: 1000.
batchSizeBytes
(Opcional) Tamanho do lote em bytes. Padrão: 5242880.
maxConnectionIdleTime
(Opcional) Tempo máximo de inatividade permitido em segundos antes que o tempo limite da conexão ocorra. Padrão: 60000.
sslEnabled
(Opcional) Valor booleano que indica se a conexão com o MongoDB está ativada para SSL. Padrão: true.
ignoreSSLCertificate
(Opcional) Valor booleano que indica se o certificado SSL deve ser ignorado. Padrão: true.
withOrdered
(Opcional) Valor booleano que permite inserções ordenadas em massa para o MongoDB. Padrão: true.
withSSLInvalidHostNameAllowed
(Opcional) Valor booleano que indica se o nome do host inválido é permitido para conexão SSL. Padrão: true.
Como executar o modelo do Pub/Sub para MongoDB
Console
Acesse a página Criar job usando um modelo do Dataflow.
PROJECT_ID:
o ID do projeto do Cloud em que você quer executar o job do Dataflow
REGION_NAME:
o endpoint regional em que você quer
implantar o job do Dataflow, por exemplo, us-central1
JOB_NAME:
um nome de job de sua escolha
VERSION:
a versão do modelo que você quer usar
Use estes valores:
latest para usar a versão mais recente do modelo, disponível na pasta mãe
não datada no bucket:
gs://dataflow-templates/latest/
o nome da versão, como 2021-09-20-00_RC00, para usar uma versão específica do
modelo, que pode ser aninhada na respectiva pasta mãe datada no bucket:
gs://dataflow-templates
INPUT_SUBSCRIPTION: a assinatura do Pub/Sub (por exemplo, projects/my-project-id/subscriptions/my-subscription-id)
MONGODB_URI: os endereços de servidor do MongoDB (por exemplo, 192.285.234.12:27017,192.287.123.11:27017)
DATABASE: o nome do banco de dados do MongoDB (por exemplo, users)
COLLECTION: o nome da coleção do MongoDB (por exemplo, profiles)
UNPROCESSED_TABLE: o nome da tabela do BigQuery (por exemplo, your-project:your-dataset.your-table-name)
API
Para executar o modelo usando a API REST, envie uma solicitação HTTP POST. Para mais informações sobre a
API e os respectivos escopos de autorização, consulte
projects.templates.launch.
PROJECT_ID:
o ID do projeto do Cloud em que você quer executar o job do Dataflow
LOCATION:
o endpoint regional em que você quer
implantar o job do Dataflow, por exemplo, us-central1
JOB_NAME:
um nome de job de sua escolha
VERSION:
a versão do modelo que você quer usar
Use estes valores:
latest para usar a versão mais recente do modelo, disponível na pasta mãe
não datada no bucket:
gs://dataflow-templates/latest/
o nome da versão, como 2021-09-20-00_RC00, para usar uma versão específica do
modelo, que pode ser aninhada na respectiva pasta mãe datada no bucket:
gs://dataflow-templates
INPUT_SUBSCRIPTION: a assinatura do Pub/Sub (por exemplo, projects/my-project-id/subscriptions/my-subscription-id)
MONGODB_URI: os endereços de servidor do MongoDB (por exemplo, 192.285.234.12:27017,192.287.123.11:27017)
DATABASE: o nome do banco de dados do MongoDB (por exemplo, users)
COLLECTION: o nome da coleção do MongoDB (por exemplo, profiles)
UNPROCESSED_TABLE: o nome da tabela do BigQuery (por exemplo, your-project:your-dataset.your-table-name)
/*
* Copyright (C) 2019 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.cloud.teleport.v2.templates;
import com.google.auto.value.AutoValue;
import com.google.cloud.teleport.metadata.Template;
import com.google.cloud.teleport.metadata.TemplateCategory;
import com.google.cloud.teleport.metadata.TemplateParameter;
import com.google.cloud.teleport.v2.coders.FailsafeElementCoder;
import com.google.cloud.teleport.v2.common.UncaughtExceptionLogger;
import com.google.cloud.teleport.v2.templates.PubSubToMongoDB.Options;
import com.google.cloud.teleport.v2.transforms.ErrorConverters;
import com.google.cloud.teleport.v2.transforms.JavascriptTextTransformer;
import com.google.cloud.teleport.v2.utils.SchemaUtils;
import com.google.cloud.teleport.v2.values.FailsafeElement;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import com.google.gson.JsonSyntaxException;
import java.nio.charset.StandardCharsets;
import javax.annotation.Nullable;
import org.apache.beam.sdk.Pipeline;
import org.apache.beam.sdk.PipelineResult;
import org.apache.beam.sdk.coders.CoderRegistry;
import org.apache.beam.sdk.coders.StringUtf8Coder;
import org.apache.beam.sdk.io.gcp.pubsub.PubsubIO;
import org.apache.beam.sdk.io.gcp.pubsub.PubsubMessage;
import org.apache.beam.sdk.io.gcp.pubsub.PubsubMessageWithAttributesCoder;
import org.apache.beam.sdk.io.mongodb.MongoDbIO;
import org.apache.beam.sdk.metrics.Counter;
import org.apache.beam.sdk.metrics.Metrics;
import org.apache.beam.sdk.options.Default;
import org.apache.beam.sdk.options.PipelineOptions;
import org.apache.beam.sdk.options.PipelineOptionsFactory;
import org.apache.beam.sdk.options.Validation;
import org.apache.beam.sdk.transforms.DoFn;
import org.apache.beam.sdk.transforms.MapElements;
import org.apache.beam.sdk.transforms.PTransform;
import org.apache.beam.sdk.transforms.ParDo;
import org.apache.beam.sdk.values.PCollection;
import org.apache.beam.sdk.values.PCollectionTuple;
import org.apache.beam.sdk.values.TupleTag;
import org.apache.beam.sdk.values.TupleTagList;
import org.apache.beam.sdk.values.TypeDescriptors;
import org.apache.beam.vendor.guava.v26_0_jre.com.google.common.base.Throwables;
import org.bson.Document;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link PubSubToMongoDB} pipeline is a streaming pipeline which ingests data in JSON format
* from PubSub, applies a Javascript UDF if provided and inserts resulting records as Bson Document
* in MongoDB. If the element fails to be processed then it is written to a deadletter table in
* BigQuery.
*
* <p><b>Pipeline Requirements</b>
*
* <ul>
* <li>The PubSub topic and subscriptions exist
* <li>The MongoDB is up and running
* </ul>
*
* <p><b>Example Usage</b>
*
* <pre>
* # Set the pipeline vars
* PROJECT_NAME=my-project
* BUCKET_NAME=my-bucket
* INPUT_SUBSCRIPTION=my-subscription
* MONGODB_DATABASE_NAME=testdb
* MONGODB_HOSTNAME=my-host:port
* MONGODB_COLLECTION_NAME=testCollection
* DEADLETTERTABLE=project:dataset.deadletter_table_name
*
* mvn compile exec:java \
* -Dexec.mainClass=com.google.cloud.teleport.v2.templates.PubSubToMongoDB \
* -Dexec.cleanupDaemonThreads=false \
* -Dexec.args=" \
* --project=${PROJECT_NAME} \
* --stagingLocation=gs://${BUCKET_NAME}/staging \
* --tempLocation=gs://${BUCKET_NAME}/temp \
* --runner=DataflowRunner \
* --inputSubscription=${INPUT_SUBSCRIPTION} \
* --mongoDBUri=${MONGODB_HOSTNAME} \
* --database=${MONGODB_DATABASE_NAME} \
* --collection=${MONGODB_COLLECTION_NAME} \
* --deadletterTable=${DEADLETTERTABLE}"
* </pre>
*/
@Template(
name = "Cloud_PubSub_to_MongoDB",
category = TemplateCategory.STREAMING,
displayName = "Pub/Sub to MongoDB",
description =
"Streaming pipeline that reads JSON encoded messages from a Pub/Sub subscription,"
+ " transforms them using a JavaScript user-defined function (UDF), and writes them to"
+ " a MongoDB as documents.",
optionsClass = Options.class,
flexContainerName = "pubsub-to-mongodb",
contactInformation = "https://cloud.google.com/support")
public class PubSubToMongoDB {
/**
* Options supported by {@link PubSubToMongoDB}
*
* <p>Inherits standard configuration options.
*/
/** The tag for the main output of the json transformation. */
public static final TupleTag<FailsafeElement<PubsubMessage, String>> TRANSFORM_OUT =
new TupleTag<FailsafeElement<PubsubMessage, String>>() {};
/** The tag for the dead-letter output of the json to table row transform. */
public static final TupleTag<FailsafeElement<PubsubMessage, String>> TRANSFORM_DEADLETTER_OUT =
new TupleTag<FailsafeElement<PubsubMessage, String>>() {};
/** Pubsub message/string coder for pipeline. */
public static final FailsafeElementCoder<PubsubMessage, String> CODER =
FailsafeElementCoder.of(PubsubMessageWithAttributesCoder.of(), StringUtf8Coder.of());
/** String/String Coder for FailsafeElement. */
public static final FailsafeElementCoder<String, String> FAILSAFE_ELEMENT_CODER =
FailsafeElementCoder.of(StringUtf8Coder.of(), StringUtf8Coder.of());
/** The log to output status messages to. */
private static final Logger LOG = LoggerFactory.getLogger(PubSubToMongoDB.class);
/**
* The {@link Options} class provides the custom execution options passed by the executor at the
* command-line.
*
* <p>Inherits standard configuration options, options from {@link
* JavascriptTextTransformer.JavascriptTextTransformerOptions}.
*/
public interface Options
extends JavascriptTextTransformer.JavascriptTextTransformerOptions, PipelineOptions {
@TemplateParameter.PubsubSubscription(
order = 1,
description = "Pub/Sub input subscription",
helpText =
"Pub/Sub subscription to read the input from, in the format of"
+ " 'projects/your-project-id/subscriptions/your-subscription-name'",
example = "projects/your-project-id/subscriptions/your-subscription-name")
@Validation.Required
String getInputSubscription();
void setInputSubscription(String inputSubscription);
@TemplateParameter.Text(
order = 2,
description = "MongoDB Connection URI",
helpText = "List of Mongo DB nodes separated by comma.",
example = "host1:port,host2:port,host3:port")
@Validation.Required
String getMongoDBUri();
void setMongoDBUri(String mongoDBUri);
@TemplateParameter.Text(
order = 3,
description = "MongoDB Database",
helpText = "Database in MongoDB to store the collection.",
example = "my-db")
@Validation.Required
String getDatabase();
void setDatabase(String database);
@TemplateParameter.Text(
order = 4,
description = "MongoDB collection",
helpText = "Name of the collection inside MongoDB database to put the documents to.",
example = "my-collection")
@Validation.Required
String getCollection();
void setCollection(String collection);
@TemplateParameter.BigQueryTable(
order = 5,
description = "The dead-letter table name to output failed messages to BigQuery",
helpText =
"Messages failed to reach the output table for all kind of reasons (e.g., mismatched"
+ " schema, malformed json) are written to this table. If it doesn't exist, it will"
+ " be created during pipeline execution. If not specified,"
+ " \"outputTableSpec_error_records\" is used instead.",
example = "your-project-id:your-dataset.your-table-name")
@Validation.Required
String getDeadletterTable();
void setDeadletterTable(String deadletterTable);
@TemplateParameter.Long(
order = 6,
optional = true,
description = "Batch Size",
helpText = "Batch Size used for batch insertion of documents into MongoDB.")
@Default.Long(1000)
Long getBatchSize();
void setBatchSize(Long batchSize);
@TemplateParameter.Long(
order = 7,
optional = true,
description = "Batch Size in Bytes",
helpText =
"Batch Size in bytes used for batch insertion of documents into MongoDB. Default:"
+ " 5242880 (5mb)")
@Default.Long(5242880)
Long getBatchSizeBytes();
void setBatchSizeBytes(Long batchSizeBytes);
@TemplateParameter.Integer(
order = 8,
optional = true,
description = "Max Connection idle time",
helpText = "Maximum idle time allowed in seconds before connection timeout occurs.")
@Default.Integer(60000)
int getMaxConnectionIdleTime();
void setMaxConnectionIdleTime(int maxConnectionIdleTime);
@TemplateParameter.Boolean(
order = 9,
optional = true,
description = "SSL Enabled",
helpText = "Indicates whether connection to MongoDB is ssl enabled.")
@Default.Boolean(true)
Boolean getSslEnabled();
void setSslEnabled(Boolean sslEnabled);
@TemplateParameter.Boolean(
order = 10,
optional = true,
description = "Ignore SSL Certificate",
helpText = "Indicates whether SSL certificate should be ignored.")
@Default.Boolean(true)
Boolean getIgnoreSSLCertificate();
void setIgnoreSSLCertificate(Boolean ignoreSSLCertificate);
@TemplateParameter.Boolean(
order = 11,
optional = true,
description = "withOrdered",
helpText = "Enables ordered bulk insertions into MongoDB.")
@Default.Boolean(true)
Boolean getWithOrdered();
void setWithOrdered(Boolean withOrdered);
@TemplateParameter.Boolean(
order = 12,
optional = true,
description = "withSSLInvalidHostNameAllowed",
helpText = "Indicates whether invalid host name is allowed for ssl connection.")
@Default.Boolean(true)
Boolean getWithSSLInvalidHostNameAllowed();
void setWithSSLInvalidHostNameAllowed(Boolean withSSLInvalidHostNameAllowed);
}
/** DoFn that will parse the given string elements as Bson Documents. */
private static class ParseAsDocumentsFn extends DoFn<String, Document> {
@ProcessElement
public void processElement(ProcessContext context) {
context.output(Document.parse(context.element()));
}
}
/**
* Main entry point for executing the pipeline.
*
* @param args The command-line arguments to the pipeline.
*/
public static void main(String[] args) {
UncaughtExceptionLogger.register();
// Parse the user options passed from the command-line.
Options options = PipelineOptionsFactory.fromArgs(args).withValidation().as(Options.class);
run(options);
}
/**
* Runs the pipeline with the supplied options.
*
* @param options The execution parameters to the pipeline.
* @return The result of the pipeline execution.
*/
public static PipelineResult run(Options options) {
// Create the pipeline
Pipeline pipeline = Pipeline.create(options);
// Register the coders for pipeline
CoderRegistry coderRegistry = pipeline.getCoderRegistry();
coderRegistry.registerCoderForType(
FAILSAFE_ELEMENT_CODER.getEncodedTypeDescriptor(), FAILSAFE_ELEMENT_CODER);
coderRegistry.registerCoderForType(CODER.getEncodedTypeDescriptor(), CODER);
/*
* Steps: 1) Read PubSubMessage with attributes from input PubSub subscription.
* 2) Apply Javascript UDF if provided.
* 3) Write to MongoDB
*
*/
LOG.info("Reading from subscription: " + options.getInputSubscription());
PCollectionTuple convertedPubsubMessages =
pipeline
/*
* Step #1: Read from a PubSub subscription.
*/
.apply(
"Read PubSub Subscription",
PubsubIO.readMessagesWithAttributes()
.fromSubscription(options.getInputSubscription()))
/*
* Step #2: Apply Javascript Transform and transform, if provided and transform
* the PubsubMessages into Json documents.
*/
.apply(
"Apply Javascript UDF",
PubSubMessageToJsonDocument.newBuilder()
.setJavascriptTextTransformFunctionName(
options.getJavascriptTextTransformFunctionName())
.setJavascriptTextTransformGcsPath(options.getJavascriptTextTransformGcsPath())
.build());
/*
* Step #3a: Write Json documents into MongoDB using {@link MongoDbIO.write}.
*/
convertedPubsubMessages
.get(TRANSFORM_OUT)
.apply(
"Get Json Documents",
MapElements.into(TypeDescriptors.strings()).via(FailsafeElement::getPayload))
.apply("Parse as BSON Document", ParDo.of(new ParseAsDocumentsFn()))
.apply(
"Put to MongoDB",
MongoDbIO.write()
.withBatchSize(options.getBatchSize())
.withUri(String.format("mongodb://%s", options.getMongoDBUri()))
.withDatabase(options.getDatabase())
.withCollection(options.getCollection())
.withIgnoreSSLCertificate(options.getIgnoreSSLCertificate())
.withMaxConnectionIdleTime(options.getMaxConnectionIdleTime())
.withOrdered(options.getWithOrdered())
.withSSLEnabled(options.getSslEnabled())
.withSSLInvalidHostNameAllowed(options.getWithSSLInvalidHostNameAllowed()));
/*
* Step 3b: Write elements that failed processing to deadletter table via {@link BigQueryIO}.
*/
convertedPubsubMessages
.get(TRANSFORM_DEADLETTER_OUT)
.apply(
"Write Transform Failures To BigQuery",
ErrorConverters.WritePubsubMessageErrors.newBuilder()
.setErrorRecordsTable(options.getDeadletterTable())
.setErrorRecordsTableSchema(SchemaUtils.DEADLETTER_SCHEMA)
.build());
// Execute the pipeline and return the result.
return pipeline.run();
}
/**
* The {@link PubSubMessageToJsonDocument} class is a {@link PTransform} which transforms incoming
* {@link PubsubMessage} objects into JSON objects for insertion into MongoDB while applying an
* optional UDF to the input. The executions of the UDF and transformation to Json objects is done
* in a fail-safe way by wrapping the element with it's original payload inside the {@link
* FailsafeElement} class. The {@link PubSubMessageToJsonDocument} transform will output a {@link
* PCollectionTuple} which contains all output and dead-letter {@link PCollection}.
*
* <p>The {@link PCollectionTuple} output will contain the following {@link PCollection}:
*
* <ul>
* <li>{@link PubSubToMongoDB#TRANSFORM_OUT} - Contains all records successfully converted to
* JSON objects.
* <li>{@link PubSubToMongoDB#TRANSFORM_DEADLETTER_OUT} - Contains all {@link FailsafeElement}
* records which couldn't be converted to table rows.
* </ul>
*/
@AutoValue
public abstract static class PubSubMessageToJsonDocument
extends PTransform<PCollection<PubsubMessage>, PCollectionTuple> {
public static Builder newBuilder() {
return new AutoValue_PubSubToMongoDB_PubSubMessageToJsonDocument.Builder();
}
@Nullable
public abstract String javascriptTextTransformGcsPath();
@Nullable
public abstract String javascriptTextTransformFunctionName();
@Override
public PCollectionTuple expand(PCollection<PubsubMessage> input) {
// Map the incoming messages into FailsafeElements so we can recover from failures
// across multiple transforms.
PCollection<FailsafeElement<PubsubMessage, String>> failsafeElements =
input.apply("MapToRecord", ParDo.of(new PubsubMessageToFailsafeElementFn()));
// If a Udf is supplied then use it to parse the PubSubMessages.
if (javascriptTextTransformGcsPath() != null) {
return failsafeElements.apply(
"InvokeUDF",
JavascriptTextTransformer.FailsafeJavascriptUdf.<PubsubMessage>newBuilder()
.setFileSystemPath(javascriptTextTransformGcsPath())
.setFunctionName(javascriptTextTransformFunctionName())
.setSuccessTag(TRANSFORM_OUT)
.setFailureTag(TRANSFORM_DEADLETTER_OUT)
.build());
} else {
return failsafeElements.apply(
"ProcessPubSubMessages",
ParDo.of(new ProcessFailsafePubSubFn())
.withOutputTags(TRANSFORM_OUT, TupleTagList.of(TRANSFORM_DEADLETTER_OUT)));
}
}
/** Builder for {@link PubSubMessageToJsonDocument}. */
@AutoValue.Builder
public abstract static class Builder {
public abstract Builder setJavascriptTextTransformGcsPath(
String javascriptTextTransformGcsPath);
public abstract Builder setJavascriptTextTransformFunctionName(
String javascriptTextTransformFunctionName);
public abstract PubSubMessageToJsonDocument build();
}
}
/**
* The {@link ProcessFailsafePubSubFn} class processes a {@link FailsafeElement} containing a
* {@link PubsubMessage} and a String of the message's payload {@link PubsubMessage#getPayload()}
* into a {@link FailsafeElement} of the original {@link PubsubMessage} and a JSON string that has
* been processed with {@link Gson}.
*
* <p>If {@link PubsubMessage#getAttributeMap()} is not empty then the message attributes will be
* serialized along with the message payload.
*/
static class ProcessFailsafePubSubFn
extends DoFn<FailsafeElement<PubsubMessage, String>, FailsafeElement<PubsubMessage, String>> {
private static final Counter successCounter =
Metrics.counter(PubSubMessageToJsonDocument.class, "successful-json-conversion");
private static Gson gson = new Gson();
private static final Counter failedCounter =
Metrics.counter(PubSubMessageToJsonDocument.class, "failed-json-conversion");
@ProcessElement
public void processElement(ProcessContext context) {
PubsubMessage pubsubMessage = context.element().getOriginalPayload();
JsonObject messageObject = new JsonObject();
try {
if (pubsubMessage.getPayload().length > 0) {
messageObject = gson.fromJson(new String(pubsubMessage.getPayload()), JsonObject.class);
}
// If message attributes are present they will be serialized along with the message payload
if (pubsubMessage.getAttributeMap() != null) {
pubsubMessage.getAttributeMap().forEach(messageObject::addProperty);
}
context.output(FailsafeElement.of(pubsubMessage, messageObject.toString()));
successCounter.inc();
} catch (JsonSyntaxException e) {
context.output(
TRANSFORM_DEADLETTER_OUT,
FailsafeElement.of(context.element())
.setErrorMessage(e.getMessage())
.setStacktrace(Throwables.getStackTraceAsString(e)));
failedCounter.inc();
}
}
}
/**
* The {@link PubsubMessageToFailsafeElementFn} wraps an incoming {@link PubsubMessage} with the
* {@link FailsafeElement} class so errors can be recovered from and the original message can be
* output to a error records table.
*/
static class PubsubMessageToFailsafeElementFn
extends DoFn<PubsubMessage, FailsafeElement<PubsubMessage, String>> {
@ProcessElement
public void processElement(ProcessContext context) {
PubsubMessage message = context.element();
context.output(
FailsafeElement.of(message, new String(message.getPayload(), StandardCharsets.UTF_8)));
}
}
}
Pub/Sub para Elasticsearch
O modelo do Pub/Sub para Elasticsearch é um pipeline de streaming que lê mensagens de uma assinatura do Pub/Sub, executa uma função definida pelo usuário (UDF, na sigla em inglês) e as grava no Elasticsearch como documentos. O modelo do Dataflow usa o recurso de fluxos de dados do Elasticsearch para armazenar dados de série temporal em vários índices, oferecendo um único recurso nomeado para solicitações. Esses fluxos são adequados para registros, métricas, rastros e outros dados gerados continuamente armazenados no Pub/Sub.
Requisitos para esse pipeline
A assinatura do Pub/Sub de origem precisa existir e as mensagens precisam ser codificadas em um formato JSON válido.
Um host Elasticsearch acessível publicamente em uma instância do GCP ou no Elastic Cloud com o Elasticsearch versão 7.0 ou mais recente. Consulte Integração do Google Cloud para Elastic para ver mais detalhes.
Um tópico do Pub/Sub para saída de erros.
Parâmetros do modelo
Parâmetro
Descrição
inputSubscription
A assinatura do Pub/Sub a ser consumida. O nome precisa estar no formato projects/<project-id>/subscriptions/<subscription-name>.
connectionUrl
URL do Elasticsearch no formato https://hostname:[port] ou especifique o CloudID se estiver usando o Elastic Cloud.
apiKey
Chave da API codificada em Base64 usada para autenticação.
errorOutputTopic
Tópico de saída do Pub/Sub para publicar registros com falha no formato de projects/<project-id>/topics/<topic-name>
dataset
(Opcional) O tipo de registros enviados via Pub/Sub, para os quais temos um painel pronto para uso. Os valores de tipos de registro conhecidos são audit, vpcflow e firewall. Padrão: pubsub.
namespace
(Opcional) Um agrupamento arbitrário, como um ambiente (dev, prod ou qa), uma equipe ou uma unidade de negócios estratégica. Padrão: default.
batchSize
(Opcional) Tamanho do lote em número de documentos. Padrão: 1000.
batchSizeBytes
(Opcional) Tamanho do lote em número de bytes. Padrão: 5242880 (5 mb).
maxRetryAttempts
(Opcional) Máximo de tentativas de repetição. Precisa ser > 0. Padrão: no retries.
maxRetryDuration
(Opcional) A duração máxima da nova tentativa em milissegundos precisa ser maior que 0. Padrão: no retries.
javascriptTextTransformGcsPath
(Opcional)
O URI do Cloud Storage do arquivo .js que define a função definida
pelo usuário (UDF, na sigla em inglês) do JavaScript que você quer usar. Por exemplo, gs://my-bucket/my-udfs/my_file.js.
javascriptTextTransformFunctionName
(Opcional)
O nome da função definida pelo usuário (UDF) do JavaScript que você quer usar.
Por exemplo, se o código de função do JavaScript for
myTransform(inJson) { /*...do stuff...*/ }, o nome da função será
myTransform. Para amostras de UDFs do JavaScript, consulte os
exemplos de UDF.
propertyAsIndex
(Opcional) Uma propriedade no documento que está sendo indexado com o valor que especificará os metadados de _index a serem incluídos com o documento na solicitação em massa (tem precedência sobre uma UDF _index). Padrão: none.
propertyAsId
(Opcional) Uma propriedade no documento que está sendo indexado com o valor que especificará os metadados de _id a serem incluídos com o documento na solicitação em massa (tem precedência sobre uma UDF _id). Padrão: none.
javaScriptIndexFnGcsPath
(Opcional) O caminho do Cloud Storage para a origem UDF em JavaScript de uma função que especificará os metadados de _index a serem incluídos com o documento na solicitação em massa. Padrão: none.
javaScriptIndexFnName
(Opcional) Nome da função UDF em JavaScript para a função que especificará os metadados de _index a serem incluídos com o documento na solicitação em massa. Padrão: none.
javaScriptIdFnGcsPath
(Opcional) O caminho do Cloud Storage para a origem UDF em JavaScript de uma função que especificará os metadados de _id a serem incluídos com o documento na solicitação em massa. Padrão: none.
javaScriptIdFnName
(Opcional) Nome da função UDF em JavaScript para a função que especificará os metadados de _id a serem incluídos com o documento na solicitação em massa. Padrão: none.
javaScriptTypeFnGcsPath
(Opcional) O caminho do Cloud Storage para a origem UDF em JavaScript de uma função que especificará os metadados de _type a serem incluídos com o documento na solicitação em massa. Padrão: none.
javaScriptTypeFnName
(Opcional) Nome da função UDF em JavaScript para a função que especificará os metadados de _type a serem incluídos com o documento na solicitação em massa. Padrão: none.
javaScriptIsDeleteFnGcsPath
(Opcional) O caminho do Cloud Storage para a origem UDF em JavaScript de uma função que determina se o documento deve ser excluído em vez de inserido ou atualizado. A função precisa retornar o valor da string "true" ou "false". Padrão: none.
javaScriptIsDeleteFnName
(Opcional) Nome da função UDF em JavaScript de uma função que vai determinar se o documento deve ser excluído em vez de inserido ou atualizado. A função precisa retornar o valor da string "true" ou "false". Padrão: none.
usePartialUpdate
(Opcional) Indica se as atualizações parciais vão ser usadas (atualizar em vez de criar ou indexar, permitindo documentos parciais) com solicitações Elasticsearch. Padrão: false.
bulkInsertMethod
(Opcional) Indica se é necessário usar INDEX (índice, permite ajustes) ou CREATE (criar, erros em _id duplicados) com solicitações em massa do Elasticsearch. Padrão: CREATE.
Como executar o Pub/Sub para o modelo do MongoDB
Console
Acesse a página Criar job usando um modelo do Dataflow.
PROJECT_ID:
o ID do projeto do Cloud em que você quer executar o job do Dataflow
JOB_NAME:
um nome de job de sua escolha
REGION_NAME:
o endpoint regional em que você quer
implantar o job do Dataflow, por exemplo, us-central1
VERSION:
a versão do modelo que você quer usar
Use estes valores:
latest para usar a versão mais recente do modelo, disponível na pasta mãe
não datada no bucket:
gs://dataflow-templates/latest/
o nome da versão, como 2021-09-20-00_RC00, para usar uma versão específica do
modelo, que pode ser aninhada na respectiva pasta mãe datada no bucket:
gs://dataflow-templates
ERROR_OUTPUT_TOPIC: o tópico do Pub/Sub para saída de erros
SUBSCRIPTION_NAME: o nome da sua assinatura de Pub/Sub
CONNECTION_URL: seu URL do Elasticsearch
DATASET: seu tipo de registro
NAMESPACE: seu namespace para conjunto de dados
APIKEY: sua chave de API codificada em base64 para autenticação
API
Para executar o modelo usando a API REST, envie uma solicitação HTTP POST. Para mais informações sobre a
API e os respectivos escopos de autorização, consulte
projects.templates.launch.
PROJECT_ID:
o ID do projeto do Cloud em que você quer executar o job do Dataflow
JOB_NAME:
um nome de job de sua escolha
LOCATION:
o endpoint regional em que você quer
implantar o job do Dataflow, por exemplo, us-central1
VERSION:
a versão do modelo que você quer usar
Use estes valores:
latest para usar a versão mais recente do modelo, disponível na pasta mãe
não datada no bucket:
gs://dataflow-templates/latest/
o nome da versão, como 2021-09-20-00_RC00, para usar uma versão específica do
modelo, que pode ser aninhada na respectiva pasta mãe datada no bucket:
gs://dataflow-templates
ERROR_OUTPUT_TOPIC: o tópico do Pub/Sub para saída de erros
SUBSCRIPTION_NAME: o nome da sua assinatura de Pub/Sub
CONNECTION_URL: seu URL do Elasticsearch
DATASET: seu tipo de registro
NAMESPACE: seu namespace para conjunto de dados
APIKEY: sua chave de API codificada em base64 para autenticação
/*
* Copyright (C) 2021 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.cloud.teleport.v2.elasticsearch.templates;
import com.google.cloud.teleport.metadata.Template;
import com.google.cloud.teleport.metadata.TemplateCategory;
import com.google.cloud.teleport.v2.coders.FailsafeElementCoder;
import com.google.cloud.teleport.v2.common.UncaughtExceptionLogger;
import com.google.cloud.teleport.v2.elasticsearch.options.PubSubToElasticsearchOptions;
import com.google.cloud.teleport.v2.elasticsearch.transforms.FailedPubsubMessageToPubsubTopicFn;
import com.google.cloud.teleport.v2.elasticsearch.transforms.ProcessEventMetadata;
import com.google.cloud.teleport.v2.elasticsearch.transforms.PubSubMessageToJsonDocument;
import com.google.cloud.teleport.v2.elasticsearch.transforms.WriteToElasticsearch;
import com.google.cloud.teleport.v2.elasticsearch.utils.ElasticsearchIndex;
import com.google.cloud.teleport.v2.values.FailsafeElement;
import org.apache.beam.sdk.Pipeline;
import org.apache.beam.sdk.PipelineResult;
import org.apache.beam.sdk.coders.CoderRegistry;
import org.apache.beam.sdk.coders.StringUtf8Coder;
import org.apache.beam.sdk.io.gcp.pubsub.PubsubIO;
import org.apache.beam.sdk.io.gcp.pubsub.PubsubMessage;
import org.apache.beam.sdk.io.gcp.pubsub.PubsubMessageWithAttributesCoder;
import org.apache.beam.sdk.options.PipelineOptionsFactory;
import org.apache.beam.sdk.transforms.MapElements;
import org.apache.beam.sdk.transforms.ParDo;
import org.apache.beam.sdk.values.PCollectionTuple;
import org.apache.beam.sdk.values.TupleTag;
import org.apache.beam.sdk.values.TypeDescriptors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link PubSubToElasticsearch} pipeline is a streaming pipeline which ingests data in JSON
* format from PubSub, applies a Javascript UDF if provided and writes the resulting records to
* Elasticsearch. If the element fails to be processed then it is written to an error output table
* in BigQuery.
*
* <p>Please refer to <b><a href=
* "https://github.com/GoogleCloudPlatform/DataflowTemplates/blob/master/v2/googlecloud-to-elasticsearch/docs/PubSubToElasticsearch/README.md">
* README.md</a></b> for further information.
*/
@Template(
name = "PubSub_to_Elasticsearch",
category = TemplateCategory.STREAMING,
displayName = "Pub/Sub to Elasticsearch",
description =
"A pipeline to read messages from Pub/Sub and writes into an Elasticsearch instance as json"
+ " documents with optional intermediate transformations using Javascript Udf.",
optionsClass = PubSubToElasticsearchOptions.class,
skipOptions = "index", // Template just ignores what is sent as "index"
flexContainerName = "pubsub-to-elasticsearch",
contactInformation = "https://cloud.google.com/support")
public class PubSubToElasticsearch {
/** The tag for the main output of the json transformation. */
public static final TupleTag<FailsafeElement<PubsubMessage, String>> TRANSFORM_OUT =
new TupleTag<FailsafeElement<PubsubMessage, String>>() {};
/** The tag for the error output table of the json to table row transform. */
public static final TupleTag<FailsafeElement<PubsubMessage, String>> TRANSFORM_ERROROUTPUT_OUT =
new TupleTag<FailsafeElement<PubsubMessage, String>>() {};
/** Pubsub message/string coder for pipeline. */
public static final FailsafeElementCoder<PubsubMessage, String> CODER =
FailsafeElementCoder.of(PubsubMessageWithAttributesCoder.of(), StringUtf8Coder.of());
/** String/String Coder for FailsafeElement. */
public static final FailsafeElementCoder<String, String> FAILSAFE_ELEMENT_CODER =
FailsafeElementCoder.of(StringUtf8Coder.of(), StringUtf8Coder.of());
/** The log to output status messages to. */
private static final Logger LOG = LoggerFactory.getLogger(PubSubToElasticsearch.class);
/**
* Main entry point for executing the pipeline.
*
* @param args The command-line arguments to the pipeline.
*/
public static void main(String[] args) {
UncaughtExceptionLogger.register();
// Parse the user options passed from the command-line.
PubSubToElasticsearchOptions pubSubToElasticsearchOptions =
PipelineOptionsFactory.fromArgs(args)
.withValidation()
.as(PubSubToElasticsearchOptions.class);
pubSubToElasticsearchOptions.setIndex(
new ElasticsearchIndex(
pubSubToElasticsearchOptions.getDataset(),
pubSubToElasticsearchOptions.getNamespace())
.getIndex());
run(pubSubToElasticsearchOptions);
}
/**
* Runs the pipeline with the supplied options.
*
* @param options The execution parameters to the pipeline.
* @return The result of the pipeline execution.
*/
public static PipelineResult run(PubSubToElasticsearchOptions options) {
// Create the pipeline
Pipeline pipeline = Pipeline.create(options);
// Register the coders for pipeline
CoderRegistry coderRegistry = pipeline.getCoderRegistry();
coderRegistry.registerCoderForType(
FAILSAFE_ELEMENT_CODER.getEncodedTypeDescriptor(), FAILSAFE_ELEMENT_CODER);
coderRegistry.registerCoderForType(CODER.getEncodedTypeDescriptor(), CODER);
/*
* Steps: 1) Read PubSubMessage with attributes from input PubSub subscription.
* 2) Apply Javascript UDF if provided.
* 3) Index Json string to output ES index.
*
*/
LOG.info("Reading from subscription: " + options.getInputSubscription());
PCollectionTuple convertedPubsubMessages =
pipeline
/*
* Step #1: Read from a PubSub subscription.
*/
.apply(
"ReadPubSubSubscription",
PubsubIO.readMessagesWithAttributes()
.fromSubscription(options.getInputSubscription()))
/*
* Step #2: Transform the PubsubMessages into Json documents.
*/
.apply(
"ConvertMessageToJsonDocument",
PubSubMessageToJsonDocument.newBuilder()
.setJavascriptTextTransformFunctionName(
options.getJavascriptTextTransformFunctionName())
.setJavascriptTextTransformGcsPath(options.getJavascriptTextTransformGcsPath())
.build());
/*
* Step #3a: Write Json documents into Elasticsearch using {@link ElasticsearchTransforms.WriteToElasticsearch}.
*/
convertedPubsubMessages
.get(TRANSFORM_OUT)
.apply(
"GetJsonDocuments",
MapElements.into(TypeDescriptors.strings()).via(FailsafeElement::getPayload))
.apply("Insert metadata", new ProcessEventMetadata())
.apply(
"WriteToElasticsearch",
WriteToElasticsearch.newBuilder()
.setOptions(options.as(PubSubToElasticsearchOptions.class))
.build());
/*
* Step 3b: Write elements that failed processing to error output PubSub topic via {@link PubSubIO}.
*/
convertedPubsubMessages
.get(TRANSFORM_ERROROUTPUT_OUT)
.apply(ParDo.of(new FailedPubsubMessageToPubsubTopicFn()))
.apply("writeFailureMessages", PubsubIO.writeMessages().to(options.getErrorOutputTopic()));
// Execute the pipeline and return the result.
return pipeline.run();
}
}
Datastream para o Cloud Spanner
O modelo do Datastream para Cloud Spanner é um pipeline de streaming que lê
eventos do Datastream
de um bucket do Cloud Storage e os grava em um banco de dados do Cloud Spanner.
Ela é destinada à
migração de dados de fontes do Datastream para o Cloud Spanner.
Todas as tabelas necessárias para migração precisam existir no banco de dados de destino do Cloud Spanner antes da
execução do modelo. Portanto, a migração de esquema de um banco de dados de origem para o Cloud Spanner
de destino precisa ser concluída antes da migração de dados. Os dados podem existir nas tabelas antes da migração. Esse
modelo não propaga alterações de esquema do Datastream no banco de dados do
Cloud Spanner.
A consistência de dados é garantida apenas no final da migração, quando todos os dados tiverem sido gravados no
Cloud Spanner. Para armazenar informações de pedidos de cada registro gravado no Cloud Spanner, esse
modelo cria uma tabela adicional (chamada de tabela de sombra) para cada tabela no
banco de dados do Cloud Spanner. Isso é usado para garantir consistência no final da migração. As tabelas de sombra
não são excluídas após a migração e podem ser usadas para fins de validação no final da
migração.
Todos os erros que ocorrem durante a operação, como incompatibilidades de esquema, arquivos JSON malformados ou erros
resultantes da execução de transformações, são registrados em uma fila de erros. A fila de erros é uma
pasta do Cloud Storage que armazena todos os eventos do Datastream que encontraram erros,
além do motivo do erro. em formato de texto. Os erros podem ser temporários ou permanentes e
são armazenados em pastas apropriadas do Cloud Storage na fila de erros. Os erros temporários são
repetidos automaticamente, ao contrário dos permanentes. No caso de erros permanentes, você tem
a opção de fazer correções nos eventos de mudança e movê-los para o bucket recuperável
enquanto o modelo estiver em execução.
Requisitos para este pipeline:
Um fluxo de Datastream no estado Em execução ou Não iniciado.
Um bucket do Cloud Storage em que os eventos do Datastream são replicados.
Um banco de dados do Cloud Spanner com tabelas existentes. Essas tabelas podem estar vazias ou conter dados.
Parâmetros do modelo
Parâmetro
Descrição
inputFilePattern
O local dos arquivos do Datastream que serão replicados no Cloud Storage. Normalmente, esse é o caminho raiz de um stream.
streamName
O nome ou modelo do stream para pesquisar informações de esquema e tipo de origem.
instanceId
A instância do Cloud Spanner em que as alterações são replicadas.
databaseId
O banco de dados do Cloud Spanner em que as alterações são replicadas.
projectId
O ID do projeto do Cloud Spanner.
deadLetterQueueDirectory
(Opcional) Esse é o caminho do arquivo para armazenar a saída da fila de erros. O padrão é um diretório no local temporário do job do Dataflow.
inputFileFormat
(Opcional) O formato do arquivo de saída produzido pelo Datastream. Por exemplo, avro,json. Padrão: avro.
shadowTablePrefix
(Opcional) O prefixo usado para nomear tabelas de sombra. Padrão: shadow_.
Como executar o modelo do Datastream para o Cloud Spanner
Console
Acesse a página Criar job usando um modelo do Dataflow.
PROJECT_ID:
o ID do projeto do Cloud em que você quer executar o job do Dataflow
JOB_NAME:
um nome de job de sua escolha
REGION_NAME:
o endpoint regional em que você quer
implantar o job do Dataflow, por exemplo, us-central1
VERSION:
a versão do modelo que você quer usar
Use estes valores:
latest para usar a versão mais recente do modelo, disponível na pasta mãe
não datada no bucket:
gs://dataflow-templates/latest/
o nome da versão, como 2021-09-20-00_RC00, para usar uma versão específica do
modelo, que pode ser aninhada na respectiva pasta mãe datada no bucket:
gs://dataflow-templates
GCS_FILE_PATH: o caminho do Cloud Storage usado para armazenar eventos do Datastream. Exemplo: gs://bucket/path/to/data/
CLOUDSPANNER_INSTANCE: a instância do Cloud Spanner.
CLOUDSPANNER_DATABASE: o banco de dados do Cloud Spanner.
DLQ: o caminho do Cloud Storage para o diretório da fila de erros.
API
Para executar o modelo usando a API REST, envie uma solicitação HTTP POST. Para mais informações sobre a
API e os respectivos escopos de autorização, consulte
projects.templates.launch.
PROJECT_ID:
o ID do projeto do Cloud em que você quer executar o job do Dataflow
JOB_NAME:
um nome de job de sua escolha
LOCATION:
o endpoint regional em que você quer
implantar o job do Dataflow, por exemplo, us-central1
VERSION:
a versão do modelo que você quer usar
Use estes valores:
latest para usar a versão mais recente do modelo, disponível na pasta mãe
não datada no bucket:
gs://dataflow-templates/latest/
o nome da versão, como 2021-09-20-00_RC00, para usar uma versão específica do
modelo, que pode ser aninhada na respectiva pasta mãe datada no bucket:
gs://dataflow-templates
GCS_FILE_PATH: o caminho do Cloud Storage usado para armazenar eventos do Datastream. Exemplo: gs://bucket/path/to/data/
CLOUDSPANNER_INSTANCE: a instância do Cloud Spanner.
CLOUDSPANNER_DATABASE: o banco de dados do Cloud Spanner.
DLQ: o caminho do Cloud Storage para o diretório da fila de erros.
Arquivos de texto no Cloud Storage para BigQuery (stream)
O pipeline de arquivos de texto no Cloud Storage para BigQuery
permite ler arquivos de texto armazenados no Cloud Storage, transformá-los usando uma
função definida pelo usuário (UDF) do JavaScript fornecida por você e anexar o resultado no BigQuery.
O pipeline é executado de modo indefinido e precisa ser encerrado manualmente pelo comando
cancelar e não
drenar, porque ele usa a
transformação
Watch, que é um DoFn divisível sem suporte para
drenagem.
Requisitos para este pipeline:
Crie um arquivo JSON que descreva o esquema da tabela de saída no BigQuery.
Verifique se há uma matriz JSON de nível superior intitulada fields e se o
conteúdo dela segue o padrão {"name": "COLUMN_NAME", "type": "DATA_TYPE"}.
Exemplo:
Crie um arquivo JavaScript (.js) com a função UDF que fornece a lógica para transformar as linhas de texto. A função precisa retornar uma string JSON.
Por exemplo, esta função divide cada linha de um arquivo CSV e retorna uma string JSON depois de transformar os valores.
function transform(line) {
var values = line.split(',');
var obj = new Object();
obj.location = values[0];
obj.name = values[1];
obj.age = values[2];
obj.color = values[3];
obj.coffee = values[4];
var jsonString = JSON.stringify(obj);
return jsonString;
}
Parâmetros do modelo
Parâmetro
Descrição
javascriptTextTransformGcsPath
O URI do Cloud Storage do arquivo .js que define a função definida
pelo usuário (UDF, na sigla em inglês) do JavaScript que você quer usar. Por exemplo, gs://my-bucket/my-udfs/my_file.js.
JSONPath
Local do Cloud Storage do arquivo de esquema do BigQuery, descrito como um JSON.
Por exemplo, gs://path/to/my/schema.json.
javascriptTextTransformFunctionName
o nome da função definida pelo usuário (UDF) do JavaScript que você quer usar.
Por exemplo, se o código de função do JavaScript for
myTransform(inJson) { /*...do stuff...*/ }, o nome da função será
myTransform. Para amostras de UDFs do JavaScript, consulte os
exemplos de UDF.
outputTable
A tabela do BigQuery totalmente qualificada.
Exemplo: my-project:dataset.table
inputFilePattern
Local do Cloud Storage do texto que você quer processar.
Por exemplo, gs://my-bucket/my-files/text.txt.
bigQueryLoadingTemporaryDirectory
Diretório temporário para o processo de carregamento do BigQuery.
Exemplo: gs://my-bucket/my-files/temp_dir
outputDeadletterTable
Tabela de mensagens que não alcançaram a tabela de saída.
Por exemplo, my-project:dataset.my-unprocessed-table. Se não existir, será criada durante a execução do pipeline.
Se não for especificada, será usada <outputTableSpec>_error_records.
Como executar o modelo Cloud Storage Text no BigQuery (Stream)
Console
Acesse a página Criar job usando um modelo do Dataflow.
REGION_NAME:
o endpoint regional em que você quer
implantar o job do Dataflow, por exemplo, us-central1
VERSION:
a versão do modelo que você quer usar
Use estes valores:
latest para usar a versão mais recente do modelo, disponível na pasta mãe
não datada no bucket:
gs://dataflow-templates/latest/
o nome da versão, como 2021-09-20-00_RC00, para usar uma versão específica do
modelo, que pode ser aninhada na respectiva pasta mãe datada no bucket:
gs://dataflow-templates
STAGING_LOCATION: o local para organizar arquivos locais (por exemplo, gs://your-bucket/staging).
TEMP_LOCATION: o local para gravar arquivos temporários (por exemplo, gs://your-bucket/temp)
JAVASCRIPT_FUNCTION:
o nome da função definida pelo usuário (UDF) do JavaScript que você quer usar
Por exemplo, se o código de função do JavaScript for
myTransform(inJson) { /*...do stuff...*/ }, o nome da função será
myTransform. Para amostras de UDFs do JavaScript, consulte os
exemplos de UDF.
PATH_TO_BIGQUERY_SCHEMA_JSON: o caminho do Cloud Storage para o
arquivo JSON que contém a definição do esquema
PATH_TO_JAVASCRIPT_UDF_FILE:
o URI do Cloud Storage do arquivo .js que define a função
definida pelo usuário (UDF, na sigla em inglês) do JavaScript que você quer usar, por exemplo,gs://my-bucket/my-udfs/my_file.js
PATH_TO_TEXT_DATA: o caminho do Cloud Storage para
o conjunto de dados de texto
BIGQUERY_TABLE: o nome da tabela do BigQuery
BIGQUERY_UNPROCESSED_TABLE: o nome da
tabela do BigQuery para mensagens não processadas
PATH_TO_TEMP_DIR_ON_GCS: o caminho do Cloud Storage para
o diretório temporário
API
Para executar o modelo usando a API REST, envie uma solicitação HTTP POST. Para mais informações sobre a
API e os respectivos escopos de autorização, consulte
projects.templates.launch.
PROJECT_ID:
o ID do projeto do Cloud em que você quer executar o job do Dataflow
JOB_NAME:
um nome de job de sua escolha
LOCATION:
o endpoint regional em que você quer
implantar o job do Dataflow, por exemplo, us-central1
VERSION:
a versão do modelo que você quer usar
Use estes valores:
latest para usar a versão mais recente do modelo, disponível na pasta mãe
não datada no bucket:
gs://dataflow-templates/latest/
o nome da versão, como 2021-09-20-00_RC00, para usar uma versão específica do
modelo, que pode ser aninhada na respectiva pasta mãe datada no bucket:
gs://dataflow-templates
STAGING_LOCATION: o local para organizar arquivos locais (por exemplo, gs://your-bucket/staging).
TEMP_LOCATION: o local para gravar arquivos temporários (por exemplo, gs://your-bucket/temp)
JAVASCRIPT_FUNCTION:
o nome da função definida pelo usuário (UDF) do JavaScript que você quer usar
Por exemplo, se o código de função do JavaScript for
myTransform(inJson) { /*...do stuff...*/ }, o nome da função será
myTransform. Para amostras de UDFs do JavaScript, consulte os
exemplos de UDF.
PATH_TO_BIGQUERY_SCHEMA_JSON: o caminho do Cloud Storage para o
arquivo JSON que contém a definição do esquema
PATH_TO_JAVASCRIPT_UDF_FILE:
o URI do Cloud Storage do arquivo .js que define a função
definida pelo usuário (UDF, na sigla em inglês) do JavaScript que você quer usar, por exemplo,gs://my-bucket/my-udfs/my_file.js
PATH_TO_TEXT_DATA: o caminho do Cloud Storage para
o conjunto de dados de texto
BIGQUERY_TABLE: o nome da tabela do BigQuery
BIGQUERY_UNPROCESSED_TABLE: o nome da
tabela do BigQuery para mensagens não processadas
PATH_TO_TEMP_DIR_ON_GCS: o caminho do Cloud Storage para
o diretório temporário
/*
* Copyright (C) 2018 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.cloud.teleport.templates;
import com.google.api.client.json.JsonFactory;
import com.google.api.services.bigquery.model.TableRow;
import com.google.cloud.teleport.coders.FailsafeElementCoder;
import com.google.cloud.teleport.metadata.Template;
import com.google.cloud.teleport.metadata.TemplateCategory;
import com.google.cloud.teleport.metadata.TemplateParameter;
import com.google.cloud.teleport.templates.TextToBigQueryStreaming.TextToBigQueryStreamingOptions;
import com.google.cloud.teleport.templates.common.BigQueryConverters.FailsafeJsonToTableRow;
import com.google.cloud.teleport.templates.common.ErrorConverters.WriteStringMessageErrors;
import com.google.cloud.teleport.templates.common.JavascriptTextTransformer.FailsafeJavascriptUdf;
import com.google.cloud.teleport.util.ResourceUtils;
import com.google.cloud.teleport.util.ValueProviderUtils;
import com.google.cloud.teleport.values.FailsafeElement;
import com.google.common.base.Charsets;
import com.google.common.collect.ImmutableList;
import com.google.common.io.ByteStreams;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.WritableByteChannel;
import org.apache.beam.sdk.Pipeline;
import org.apache.beam.sdk.PipelineResult;
import org.apache.beam.sdk.coders.CoderRegistry;
import org.apache.beam.sdk.coders.StringUtf8Coder;
import org.apache.beam.sdk.extensions.gcp.util.Transport;
import org.apache.beam.sdk.io.FileSystems;
import org.apache.beam.sdk.io.TextIO;
import org.apache.beam.sdk.io.fs.ResourceId;
import org.apache.beam.sdk.io.gcp.bigquery.BigQueryIO;
import org.apache.beam.sdk.io.gcp.bigquery.BigQueryIO.Write.CreateDisposition;
import org.apache.beam.sdk.io.gcp.bigquery.BigQueryIO.Write.Method;
import org.apache.beam.sdk.io.gcp.bigquery.BigQueryIO.Write.WriteDisposition;
import org.apache.beam.sdk.io.gcp.bigquery.BigQueryInsertError;
import org.apache.beam.sdk.io.gcp.bigquery.InsertRetryPolicy;
import org.apache.beam.sdk.io.gcp.bigquery.WriteResult;
import org.apache.beam.sdk.options.PipelineOptionsFactory;
import org.apache.beam.sdk.options.ValueProvider;
import org.apache.beam.sdk.options.ValueProvider.NestedValueProvider;
import org.apache.beam.sdk.transforms.Flatten;
import org.apache.beam.sdk.transforms.MapElements;
import org.apache.beam.sdk.transforms.SimpleFunction;
import org.apache.beam.sdk.transforms.Watch.Growth;
import org.apache.beam.sdk.values.PCollection;
import org.apache.beam.sdk.values.PCollectionList;
import org.apache.beam.sdk.values.PCollectionTuple;
import org.apache.beam.sdk.values.TupleTag;
import org.joda.time.Duration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link TextToBigQueryStreaming} is a streaming version of {@link TextIOToBigQuery} pipeline
* that reads text files, applies a JavaScript UDF and writes the output to BigQuery. The pipeline
* continuously polls for new files, reads them row-by-row and processes each record into BigQuery.
* The polling interval is set at 10 seconds.
*
* <p>Example Usage:
*
* <pre>
* {@code mvn compile exec:java \
* -Dexec.mainClass=com.google.cloud.teleport.templates.TextToBigQueryStreaming \
* -Dexec.args="\
* --project=${PROJECT_ID} \
* --stagingLocation=gs://${STAGING_BUCKET}/staging \
* --tempLocation=gs://${STAGING_BUCKET}/tmp \
* --runner=DataflowRunner \
* --inputFilePattern=gs://path/to/input* \
* --JSONPath=gs://path/to/json/schema.json \
* --outputTable={$PROJECT_ID}:${OUTPUT_DATASET}.${OUTPUT_TABLE} \
* --javascriptTextTransformGcsPath=gs://path/to/transform/udf.js \
* --javascriptTextTransformFunctionName=${TRANSFORM_NAME} \
* --bigQueryLoadingTemporaryDirectory=gs://${STAGING_BUCKET}/tmp \
* --outputDeadletterTable=${PROJECT_ID}:${ERROR_DATASET}.${ERROR_TABLE}"
* }
* </pre>
*/
@Template(
name = "Stream_GCS_Text_to_BigQuery",
category = TemplateCategory.STREAMING,
displayName = "Cloud Storage Text to BigQuery (Stream)",
description =
"A streaming pipeline that can read text files stored in Cloud Storage, perform a transform via a user defined JavaScript function, and stream the results into BigQuery. This pipeline requires a JavaScript function and a JSON representation of the BigQuery TableSchema.",
optionsClass = TextToBigQueryStreamingOptions.class,
contactInformation = "https://cloud.google.com/support")
public class TextToBigQueryStreaming {
private static final Logger LOG = LoggerFactory.getLogger(TextToBigQueryStreaming.class);
/** The tag for the main output for the UDF. */
private static final TupleTag<FailsafeElement<String, String>> UDF_OUT =
new TupleTag<FailsafeElement<String, String>>() {};
/** The tag for the dead-letter output of the udf. */
private static final TupleTag<FailsafeElement<String, String>> UDF_DEADLETTER_OUT =
new TupleTag<FailsafeElement<String, String>>() {};
/** The tag for the main output of the json transformation. */
private static final TupleTag<TableRow> TRANSFORM_OUT = new TupleTag<TableRow>() {};
/** The tag for the dead-letter output of the json to table row transform. */
private static final TupleTag<FailsafeElement<String, String>> TRANSFORM_DEADLETTER_OUT =
new TupleTag<FailsafeElement<String, String>>() {};
/** The default suffix for error tables if dead letter table is not specified. */
private static final String DEFAULT_DEADLETTER_TABLE_SUFFIX = "_error_records";
/** Default interval for polling files in GCS. */
private static final Duration DEFAULT_POLL_INTERVAL = Duration.standardSeconds(10);
/** Coder for FailsafeElement. */
private static final FailsafeElementCoder<String, String> FAILSAFE_ELEMENT_CODER =
FailsafeElementCoder.of(StringUtf8Coder.of(), StringUtf8Coder.of());
private static final JsonFactory JSON_FACTORY = Transport.getJsonFactory();
/**
* Main entry point for executing the pipeline. This will run the pipeline asynchronously. If
* blocking execution is required, use the {@link
* TextToBigQueryStreaming#run(TextToBigQueryStreamingOptions)} method to start the pipeline and
* invoke {@code result.waitUntilFinish()} on the {@link PipelineResult}
*
* @param args The command-line arguments to the pipeline.
*/
public static void main(String[] args) {
// Parse the user options passed from the command-line
TextToBigQueryStreamingOptions options =
PipelineOptionsFactory.fromArgs(args)
.withValidation()
.as(TextToBigQueryStreamingOptions.class);
run(options);
}
/**
* Runs the pipeline with the supplied options.
*
* @param options The execution parameters to the pipeline.
* @return The result of the pipeline execution.
*/
public static PipelineResult run(TextToBigQueryStreamingOptions options) {
// Create the pipeline
Pipeline pipeline = Pipeline.create(options);
// Register the coder for pipeline
FailsafeElementCoder<String, String> coder =
FailsafeElementCoder.of(StringUtf8Coder.of(), StringUtf8Coder.of());
CoderRegistry coderRegistry = pipeline.getCoderRegistry();
coderRegistry.registerCoderForType(coder.getEncodedTypeDescriptor(), coder);
/*
* Steps:
* 1) Read from the text source continuously.
* 2) Convert to FailsafeElement.
* 3) Apply Javascript udf transformation.
* - Tag records that were successfully transformed and those
* that failed transformation.
* 4) Convert records to TableRow.
* - Tag records that were successfully converted and those
* that failed conversion.
* 5) Insert successfully converted records into BigQuery.
* - Errors encountered while streaming will be sent to deadletter table.
* 6) Insert records that failed into deadletter table.
*/
PCollectionTuple transformedOutput =
pipeline
// 1) Read from the text source continuously.
.apply(
"ReadFromSource",
TextIO.read()
.from(options.getInputFilePattern())
.watchForNewFiles(DEFAULT_POLL_INTERVAL, Growth.never()))
// 2) Convert to FailsafeElement.
.apply(
"ConvertToFailsafeElement",
MapElements.into(FAILSAFE_ELEMENT_CODER.getEncodedTypeDescriptor())
.via(input -> FailsafeElement.of(input, input)))
// 3) Apply Javascript udf transformation.
.apply(
"ApplyUDFTransformation",
FailsafeJavascriptUdf.<String>newBuilder()
.setFileSystemPath(options.getJavascriptTextTransformGcsPath())
.setFunctionName(options.getJavascriptTextTransformFunctionName())
.setSuccessTag(UDF_OUT)
.setFailureTag(UDF_DEADLETTER_OUT)
.build());
PCollectionTuple convertedTableRows =
transformedOutput
// 4) Convert records to TableRow.
.get(UDF_OUT)
.apply(
"ConvertJSONToTableRow",
FailsafeJsonToTableRow.<String>newBuilder()
.setSuccessTag(TRANSFORM_OUT)
.setFailureTag(TRANSFORM_DEADLETTER_OUT)
.build());
WriteResult writeResult =
convertedTableRows
// 5) Insert successfully converted records into BigQuery.
.get(TRANSFORM_OUT)
.apply(
"InsertIntoBigQuery",
BigQueryIO.writeTableRows()
.withJsonSchema(getSchemaFromGCS(options.getJSONPath()))
.to(options.getOutputTable())
.withExtendedErrorInfo()
.withoutValidation()
.withCreateDisposition(CreateDisposition.CREATE_IF_NEEDED)
.withWriteDisposition(WriteDisposition.WRITE_APPEND)
.withMethod(Method.STREAMING_INSERTS)
.withFailedInsertRetryPolicy(InsertRetryPolicy.retryTransientErrors())
.withCustomGcsTempLocation(options.getBigQueryLoadingTemporaryDirectory()));
// Elements that failed inserts into BigQuery are extracted and converted to FailsafeElement
PCollection<FailsafeElement<String, String>> failedInserts =
writeResult
.getFailedInsertsWithErr()
.apply(
"WrapInsertionErrors",
MapElements.into(FAILSAFE_ELEMENT_CODER.getEncodedTypeDescriptor())
.via(TextToBigQueryStreaming::wrapBigQueryInsertError));
// 6) Insert records that failed transformation or conversion into deadletter table
PCollectionList.of(
ImmutableList.of(
transformedOutput.get(UDF_DEADLETTER_OUT),
convertedTableRows.get(TRANSFORM_DEADLETTER_OUT),
failedInserts))
.apply("Flatten", Flatten.pCollections())
.apply(
"WriteFailedRecords",
WriteStringMessageErrors.newBuilder()
.setErrorRecordsTable(
ValueProviderUtils.maybeUseDefaultDeadletterTable(
options.getOutputDeadletterTable(),
options.getOutputTable(),
DEFAULT_DEADLETTER_TABLE_SUFFIX))
.setErrorRecordsTableSchema(ResourceUtils.getDeadletterTableSchemaJson())
.build());
return pipeline.run();
}
/**
* Method to wrap a {@link BigQueryInsertError} into a {@link FailsafeElement}.
*
* @param insertError BigQueryInsert error.
* @return FailsafeElement object.
* @throws IOException
*/
static FailsafeElement<String, String> wrapBigQueryInsertError(BigQueryInsertError insertError) {
FailsafeElement<String, String> failsafeElement;
try {
String rowPayload = JSON_FACTORY.toString(insertError.getRow());
String errorMessage = JSON_FACTORY.toString(insertError.getError());
failsafeElement = FailsafeElement.of(rowPayload, rowPayload);
failsafeElement.setErrorMessage(errorMessage);
} catch (IOException e) {
throw new RuntimeException(e);
}
return failsafeElement;
}
/**
* Method to read a BigQuery schema file from GCS and return the file contents as a string.
*
* @param gcsPath Path string for the schema file in GCS.
* @return File contents as a string.
*/
private static ValueProvider<String> getSchemaFromGCS(ValueProvider<String> gcsPath) {
return NestedValueProvider.of(
gcsPath,
new SimpleFunction<String, String>() {
@Override
public String apply(String input) {
ResourceId sourceResourceId = FileSystems.matchNewResource(input, false);
String schema;
try (ReadableByteChannel rbc = FileSystems.open(sourceResourceId)) {
try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
try (WritableByteChannel wbc = Channels.newChannel(baos)) {
ByteStreams.copy(rbc, wbc);
schema = baos.toString(Charsets.UTF_8.name());
LOG.info("Extracted schema: " + schema);
}
}
} catch (IOException e) {
LOG.error("Error extracting schema: " + e.getMessage());
throw new RuntimeException(e);
}
return schema;
}
});
}
/**
* The {@link TextToBigQueryStreamingOptions} class provides the custom execution options passed
* by the executor at the command-line.
*/
public interface TextToBigQueryStreamingOptions extends TextIOToBigQuery.Options {
@TemplateParameter.BigQueryTable(
order = 1,
optional = true,
description = "The dead-letter table name to output failed messages to BigQuery",
helpText =
"Messages failed to reach the output table for all kind of reasons (e.g., mismatched "
+ "schema, malformed json) are written to this table. If it doesn't exist, it will be "
+ "created during pipeline execution. If not specified, \"outputTableSpec_error_records\" "
+ "is used instead.",
example = "your-project-id:your-dataset.your-table-name")
ValueProvider<String> getOutputDeadletterTable();
void setOutputDeadletterTable(ValueProvider<String> value);
}
}
Arquivos de texto no Cloud Storage para o Pub/Sub (Stream)
Esse modelo cria um pipeline de streaming que pesquisa continuamente novos arquivos de texto carregados
no Cloud Storage, lê cada arquivo linha por linha e publica strings em um tópico
do Pub/Sub. O modelo publica registros em um arquivo delimitado por uma nova linha contendo registros JSON
ou em um arquivo CSV em um tópico do Pub/Sub para processamento em tempo real. É possível usar esse modelo para reproduzir dados novamente no Pub/Sub.
O pipeline é executado indefinidamente e precisa ser encerrado manualmente com um "cancel", e não com um
"drain", porque ele usa uma transformação "Watch" que é um "SplittableDoFn" não
compatível com drenagem.
Atualmente, o intervalo de pesquisa é fixo e definido para 10 segundos. Esse modelo não configura carimbos de data/hora nos registros individuais, assim o horário do evento é igual ao da publicação durante a execução. O pipeline não deve ser usado caso dependa de um tempo exato
do evento para processamento.
Requisitos para este pipeline:
Os arquivos de entrada precisam estar no formato JSON ou CSV delimitado por nova linha. Registros que abrangem
várias linhas nos arquivos de origem podem causar problemas no downstream, porque cada linha nos arquivos
é publicada como uma mensagem para o Pub/Sub.
O tópico do Pub/Sub precisa existir antes da execução.
O pipeline é executado indefinidamente e precisa ser finalizado manualmente.
Parâmetros do modelo
Parâmetro
Descrição
inputFilePattern
O padrão do arquivo de entrada a ser lido. Por exemplo, gs://bucket-name/files/*.json
ou gs://bucket-name/path/*.csv.
outputTopic
O tópico de entrada do Pub/Sub a ser gravado. O nome precisa estar no formato projects/<project-id>/topics/<topic-name>.
Como executar os arquivos de texto no Cloud Storage para o modelo Pub/Sub (stream)
Console
Acesse a página Criar job usando um modelo do Dataflow.
Opcional: em Endpoint regional, selecione um valor no menu suspenso. O endpoint
regional padrão é us-central1.
Para ver uma lista de regiões em que é possível executar um job do Dataflow, consulte
Locais do Dataflow.
No menu suspenso Modelo do Dataflow, selecione
the Text Files on Cloud Storage to Pub/Sub (Stream) template.
Nos campos de parâmetro fornecidos, insira os valores de parâmetro.
Cliquem em Executar job.
gcloud
No shell ou no terminal, execute o modelo:
gcloud dataflow jobs run JOB_NAME \
--gcs-location gs://dataflow-templates/VERSION/Stream_GCS_Text_to_Cloud_PubSub \
--region REGION_NAME\
--staging-location STAGING_LOCATION\
--parameters \
inputFilePattern=gs://BUCKET_NAME/FILE_PATTERN,\
outputTopic=projects/PROJECT_ID/topics/TOPIC_NAME
Substitua:
JOB_NAME:
um nome de job de sua escolha
REGION_NAME:
o endpoint regional em que você quer
implantar o job do Dataflow, por exemplo, us-central1
STAGING_LOCATION: o local para organizar arquivos locais (por exemplo, gs://your-bucket/staging).
TEMP_LOCATION: o local para gravar arquivos temporários (por exemplo, gs://your-bucket/temp)
TOPIC_NAME: o nome do tópico do Pub/Sub
BUCKET_NAME: o nome do bucket do Cloud Storage
FILE_PATTERN: o padrão de arquivo glob para ler no bucket do Cloud Storage
(por exemplo, path/*.csv).
API
Para executar o modelo usando a API REST, envie uma solicitação HTTP POST. Para mais informações sobre a
API e os respectivos escopos de autorização, consulte
projects.templates.launch.
/*
* Copyright (C) 2018 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.cloud.teleport.templates;
import com.google.cloud.teleport.metadata.Template;
import com.google.cloud.teleport.metadata.TemplateCategory;
import com.google.cloud.teleport.templates.TextToPubsub.Options;
import org.apache.beam.sdk.Pipeline;
import org.apache.beam.sdk.PipelineResult;
import org.apache.beam.sdk.io.TextIO;
import org.apache.beam.sdk.io.gcp.pubsub.PubsubIO;
import org.apache.beam.sdk.options.PipelineOptionsFactory;
import org.apache.beam.sdk.transforms.Watch;
import org.joda.time.Duration;
/**
* The {@code TextToPubsubStream} is a streaming version of {@code TextToPubsub} pipeline that
* publishes records to Cloud Pub/Sub from a set of files. The pipeline continuously polls for new
* files, reads them row-by-row and publishes each record as a string message. The polling interval
* is fixed and equals to 10 seconds. At the moment, publishing messages with attributes is
* unsupported.
*
* <p>Example Usage:
*
* <pre>
* {@code mvn compile exec:java \
* -Dexec.mainClass=com.google.cloud.teleport.templates.TextToPubsubStream \
* -Dexec.args=" \
* --project=${PROJECT_ID} \
* --stagingLocation=gs://${STAGING_BUCKET}/dataflow/pipelines/${PIPELINE_FOLDER}/staging \
* --tempLocation=gs://${STAGING_BUCKET}/dataflow/pipelines/${PIPELINE_FOLDER}/temp \
* --runner=DataflowRunner \
* --inputFilePattern=gs://path/to/*.csv \
* --outputTopic=projects/${PROJECT_ID}/topics/${TOPIC_NAME}"
* }
* </pre>
*/
@Template(
name = "Stream_GCS_Text_to_Cloud_PubSub",
category = TemplateCategory.STREAMING,
displayName = "Text Files on Cloud Storage to Pub/Sub",
description =
"A pipeline that polls every 10 seconds for new text files stored in Cloud Storage and outputs each line to a Pub/Sub topic.",
optionsClass = Options.class,
contactInformation = "https://cloud.google.com/support")
public class TextToPubsubStream extends TextToPubsub {
private static final Duration DEFAULT_POLL_INTERVAL = Duration.standardSeconds(10);
/**
* Main entry-point for the pipeline. Reads in the command-line arguments, parses them, and
* executes the pipeline.
*
* @param args Arguments passed in from the command-line.
*/
public static void main(String[] args) {
// Parse the user options passed from the command-line
Options options = PipelineOptionsFactory.fromArgs(args).withValidation().as(Options.class);
run(options);
}
/**
* Executes the pipeline with the provided execution parameters.
*
* @param options The execution parameters.
*/
public static PipelineResult run(Options options) {
// Create the pipeline.
Pipeline pipeline = Pipeline.create(options);
/*
* Steps:
* 1) Read from the text source.
* 2) Write each text record to Pub/Sub
*/
pipeline
.apply(
"Read Text Data",
TextIO.read()
.from(options.getInputFilePattern())
.watchForNewFiles(DEFAULT_POLL_INTERVAL, Watch.Growth.never()))
.apply("Write to PubSub", PubsubIO.writeStrings().to(options.getOutputTopic()));
return pipeline.run();
}
}
Tokenização/mascaramento de dados do Cloud Storage para o BigQuery (usando o Cloud DLP)
O modelo de mascaramento de dados/tokenização do Cloud Storage para o BigQuery (usando o Cloud DLP) é um pipeline de streaming que lê arquivos csv
de um bucket do Cloud Storage, chama a API Cloud Data Loss Prevention (Cloud DLP) para
remoção identificação e grava os dados desidentificados na tabela especificada
do BigQuery. Este modelo é compatível com o uso de um modelo de inspeção e um modelo de desidentificação, ambos do Cloud DLP.
Isso permite que os usuários inspecionem informações potencialmente confidenciais e façam a desidentificação. Além disso, também é possível desidentificar os dados estruturados em que as colunas são especificadas para serem desidentificadas sem nenhuma inspeção necessária. Vale lembrar que este modelo não é compatível com um
caminho regional para o local do modelo de desidentificação. Somente um caminho global é compatível.
Requisitos para este pipeline:
Os dados de entrada para tokenizar precisam existir.
Os modelos do Cloud DLP precisam existir (por exemplo, DeidentifyTemplate e InspectTemplate). Veja os modelos do Cloud DLP para mais detalhes.
O conjunto de dados do BigQuery precisa existir.
Parâmetros do modelo
Parâmetro
Descrição
inputFilePattern
Arquivo(s) csv para ler registros de dados de entrada. O uso de caracteres curingas também é aceito. Por exemplo, gs://mybucket/my_csv_filename.csv ou gs://mybucket/file-*.csv.
dlpProjectId
ID do projeto do Cloud DLP que tem o recurso da API Cloud DLP.
Esse projeto do Cloud DLP pode ser o mesmo projeto que tem os modelos do Cloud DLP ou pode ser um projeto separado.
Por exemplo, my_dlp_api_project.
deidentifyTemplateName
Modelo de desidentificação do Cloud DLP a ser usado para solicitações de API, especificado com o padrão
projects/{template_project_id}/deidentifyTemplates/{deIdTemplateId}.
Por exemplo, projects/my_project/deidentifyTemplates/100.
datasetName
Conjunto de dados do BigQuery para enviar resultados tokenizados.
batchSize
Tamanho do lote/divisão para enviar dados para inspecionar e/ou retirar a tokenização. Se for um arquivo csv, batchSize é o número de linhas em um lote. Os usuários precisam determinar o tamanho do lote com base no tamanho dos registros e no tamanho do arquivo. Observe que a API Cloud DLP tem um limite de tamanho de payload de 524 KB por chamada de API.
inspectTemplateName
(Opcional) Modelo de inspeção do Cloud DLP a ser usado para solicitações de API, especificado com o padrão
projects/{template_project_id}/identifyTemplates/{idTemplateId}.
Por exemplo, projects/my_project/identifyTemplates/100.
Como executar o modelo de tokenização/mascaramento de dados do Cloud Storage para o BigQuery (usando o Cloud DLP)
Console
Acesse a página Criar job usando um modelo do Dataflow.
DLP_API_PROJECT_ID: o ID do projeto da API Cloud DLP
JOB_NAME:
um nome de job de sua escolha
REGION_NAME:
o endpoint regional em que você quer
implantar o job do Dataflow, por exemplo, us-central1
VERSION:
a versão do modelo que você quer usar
Use estes valores:
latest para usar a versão mais recente do modelo, disponível na pasta mãe
não datada no bucket:
gs://dataflow-templates/latest/
o nome da versão, como 2021-09-20-00_RC00, para usar uma versão específica do
modelo, que pode ser aninhada na respectiva pasta mãe datada no bucket:
gs://dataflow-templates
STAGING_LOCATION: o local para organizar arquivos locais (por exemplo, gs://your-bucket/staging).
TEMP_LOCATION: o local para gravar arquivos temporários (por exemplo, gs://your-bucket/temp)
INPUT_DATA: o caminho do arquivo de entrada
DEIDENTIFY_TEMPLATE: o número do modelo do Cloud DLPDeidentify
DATASET_NAME: o nome do conjunto de dados do BigQuery
INSPECT_TEMPLATE_NUMBER: o número do modelo do Cloud DLPInspect
BATCH_SIZE_VALUE: o tamanho do lote (número de linhas por API para csv).
API
Para executar o modelo usando a API REST, envie uma solicitação HTTP POST. Para mais informações sobre a
API e os respectivos escopos de autorização, consulte
projects.templates.launch.
PROJECT_ID:
o ID do projeto do Cloud em que você quer executar o job do Dataflow
DLP_API_PROJECT_ID: o ID do projeto da API Cloud DLP
JOB_NAME:
um nome de job de sua escolha
LOCATION:
o endpoint regional em que você quer
implantar o job do Dataflow, por exemplo, us-central1
VERSION:
a versão do modelo que você quer usar
Use estes valores:
latest para usar a versão mais recente do modelo, disponível na pasta mãe
não datada no bucket:
gs://dataflow-templates/latest/
o nome da versão, como 2021-09-20-00_RC00, para usar uma versão específica do
modelo, que pode ser aninhada na respectiva pasta mãe datada no bucket:
gs://dataflow-templates
STAGING_LOCATION: o local para organizar arquivos locais (por exemplo, gs://your-bucket/staging).
TEMP_LOCATION: o local para gravar arquivos temporários (por exemplo, gs://your-bucket/temp)
INPUT_DATA: o caminho do arquivo de entrada
DEIDENTIFY_TEMPLATE: o número do modelo do Cloud DLPDeidentify
DATASET_NAME: o nome do conjunto de dados do BigQuery
INSPECT_TEMPLATE_NUMBER: o número do modelo do Cloud DLPInspect
BATCH_SIZE_VALUE: o tamanho do lote (número de linhas por API para csv).
/*
* Copyright (C) 2018 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.cloud.teleport.templates;
import com.google.api.services.bigquery.model.TableCell;
import com.google.api.services.bigquery.model.TableFieldSchema;
import com.google.api.services.bigquery.model.TableRow;
import com.google.api.services.bigquery.model.TableSchema;
import com.google.cloud.dlp.v2.DlpServiceClient;
import com.google.cloud.teleport.metadata.Template;
import com.google.cloud.teleport.metadata.TemplateCategory;
import com.google.cloud.teleport.metadata.TemplateParameter;
import com.google.cloud.teleport.templates.DLPTextToBigQueryStreaming.TokenizePipelineOptions;
import com.google.common.base.Charsets;
import com.google.privacy.dlp.v2.ContentItem;
import com.google.privacy.dlp.v2.DeidentifyContentRequest;
import com.google.privacy.dlp.v2.DeidentifyContentRequest.Builder;
import com.google.privacy.dlp.v2.DeidentifyContentResponse;
import com.google.privacy.dlp.v2.FieldId;
import com.google.privacy.dlp.v2.ProjectName;
import com.google.privacy.dlp.v2.Table;
import com.google.privacy.dlp.v2.Value;
import java.io.BufferedReader;
import java.io.IOException;
import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import org.apache.beam.runners.dataflow.options.DataflowPipelineOptions;
import org.apache.beam.sdk.Pipeline;
import org.apache.beam.sdk.PipelineResult;
import org.apache.beam.sdk.coders.KvCoder;
import org.apache.beam.sdk.coders.StringUtf8Coder;
import org.apache.beam.sdk.io.Compression;
import org.apache.beam.sdk.io.FileIO;
import org.apache.beam.sdk.io.FileIO.ReadableFile;
import org.apache.beam.sdk.io.ReadableFileCoder;
import org.apache.beam.sdk.io.gcp.bigquery.BigQueryIO;
import org.apache.beam.sdk.io.gcp.bigquery.DynamicDestinations;
import org.apache.beam.sdk.io.gcp.bigquery.InsertRetryPolicy;
import org.apache.beam.sdk.io.gcp.bigquery.TableDestination;
import org.apache.beam.sdk.io.range.OffsetRange;
import org.apache.beam.sdk.metrics.Distribution;
import org.apache.beam.sdk.metrics.Metrics;
import org.apache.beam.sdk.options.PipelineOptionsFactory;
import org.apache.beam.sdk.options.Validation.Required;
import org.apache.beam.sdk.options.ValueProvider;
import org.apache.beam.sdk.options.ValueProvider.NestedValueProvider;
import org.apache.beam.sdk.transforms.DoFn;
import org.apache.beam.sdk.transforms.GroupByKey;
import org.apache.beam.sdk.transforms.ParDo;
import org.apache.beam.sdk.transforms.Watch;
import org.apache.beam.sdk.transforms.WithKeys;
import org.apache.beam.sdk.transforms.splittabledofn.OffsetRangeTracker;
import org.apache.beam.sdk.transforms.splittabledofn.RestrictionTracker;
import org.apache.beam.sdk.transforms.windowing.AfterProcessingTime;
import org.apache.beam.sdk.transforms.windowing.FixedWindows;
import org.apache.beam.sdk.transforms.windowing.Repeatedly;
import org.apache.beam.sdk.transforms.windowing.Window;
import org.apache.beam.sdk.values.KV;
import org.apache.beam.sdk.values.PCollection;
import org.apache.beam.sdk.values.PCollectionView;
import org.apache.beam.sdk.values.ValueInSingleWindow;
import org.apache.commons.csv.CSVFormat;
import org.apache.commons.csv.CSVRecord;
import org.joda.time.Duration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link DLPTextToBigQueryStreaming} is a streaming pipeline that reads CSV files from a
* storage location (e.g. Google Cloud Storage), uses Cloud DLP API to inspect, classify, and mask
* sensitive information (e.g. PII Data like passport or SIN number) and at the end stores
* obfuscated data in BigQuery (Dynamic Table Creation) to be used for various purposes. e.g. data
* analytics, ML model. Cloud DLP inspection and masking can be configured by the user and can make
* use of over 90 built in detectors and masking techniques like tokenization, secure hashing, date
* shifting, partial masking, and more.
*
* <p><b>Pipeline Requirements</b>
*
* <ul>
* <li>DLP Templates exist (e.g. deidentifyTemplate, InspectTemplate)
* <li>The BigQuery Dataset exists
* </ul>
*
* <p><b>Example Usage</b>
*
* <pre>
* # Set the pipeline vars
* PROJECT_ID=PROJECT ID HERE
* BUCKET_NAME=BUCKET NAME HERE
* PIPELINE_FOLDER=gs://${BUCKET_NAME}/dataflow/pipelines/dlp-text-to-bigquery
*
* # Set the runner
* RUNNER=DataflowRunner
*
* # Build the template
* mvn compile exec:java \
* -Dexec.mainClass=com.google.cloud.teleport.templates.DLPTextToBigQueryStreaming \
* -Dexec.cleanupDaemonThreads=false \
* -Dexec.args=" \
* --project=${PROJECT_ID} \
* --stagingLocation=${PIPELINE_FOLDER}/staging \
* --tempLocation=${PIPELINE_FOLDER}/temp \
* --templateLocation=${PIPELINE_FOLDER}/template \
* --runner=${RUNNER}"
*
* # Execute the template
* JOB_NAME=dlp-text-to-bigquery-$USER-`date +"%Y%m%d-%H%M%S%z"`
*
* gcloud dataflow jobs run ${JOB_NAME} \
* --gcs-location=${PIPELINE_FOLDER}/template \
* --zone=us-east1-d \
* --parameters \
* "inputFilePattern=gs://{bucketName}/{fileName}.csv, batchSize=15,datasetName={BQDatasetId},
* dlpProjectId={projectId},
* deidentifyTemplateName=projects/{projectId}/deidentifyTemplates/{deIdTemplateId}
* </pre>
*/
@Template(
name = "Stream_DLP_GCS_Text_to_BigQuery",
category = TemplateCategory.STREAMING,
displayName = "Data Masking/Tokenization from Cloud Storage to BigQuery (using Cloud DLP)",
description =
"An example pipeline that reads CSV files from Cloud Storage, uses Cloud DLP API to mask and tokenize data based on the DLP templates provided and stores output in BigQuery. Note, not all configuration settings are available in this default template. You may need to deploy a custom template to accommodate your specific environment and data needs. More details here: https://cloud.google.com/solutions/de-identification-re-identification-pii-using-cloud-dlp",
optionsClass = TokenizePipelineOptions.class,
contactInformation = "https://cloud.google.com/support")
public class DLPTextToBigQueryStreaming {
public static final Logger LOG = LoggerFactory.getLogger(DLPTextToBigQueryStreaming.class);
/** Default interval for polling files in GCS. */
private static final Duration DEFAULT_POLL_INTERVAL = Duration.standardSeconds(30);
/** Expected only CSV file in GCS bucket. */
private static final String ALLOWED_FILE_EXTENSION = String.valueOf("csv");
/** Regular expression that matches valid BQ table IDs. */
private static final Pattern TABLE_REGEXP = Pattern.compile("[-\\w$@]{1,1024}");
/** Default batch size if value not provided in execution. */
private static final Integer DEFAULT_BATCH_SIZE = 100;
/** Regular expression that matches valid BQ column name . */
private static final Pattern COLUMN_NAME_REGEXP = Pattern.compile("^[A-Za-z_]+[A-Za-z_0-9]*$");
/** Default window interval to create side inputs for header records. */
private static final Duration WINDOW_INTERVAL = Duration.standardSeconds(30);
/**
* Main entry point for executing the pipeline. This will run the pipeline asynchronously. If
* blocking execution is required, use the {@link
* DLPTextToBigQueryStreaming#run(TokenizePipelineOptions)} method to start the pipeline and
* invoke {@code result.waitUntilFinish()} on the {@link PipelineResult}
*
* @param args The command-line arguments to the pipeline.
*/
public static void main(String[] args) {
TokenizePipelineOptions options =
PipelineOptionsFactory.fromArgs(args).withValidation().as(TokenizePipelineOptions.class);
run(options);
}
/**
* Runs the pipeline with the supplied options.
*
* @param options The execution parameters to the pipeline.
* @return The result of the pipeline execution.
*/
public static PipelineResult run(TokenizePipelineOptions options) {
// Create the pipeline
Pipeline p = Pipeline.create(options);
/*
* Steps:
* 1) Read from the text source continuously based on default interval e.g. 30 seconds
* - Setup a window for 30 secs to capture the list of files emited.
* - Group by file name as key and ReadableFile as a value.
* 2) Output each readable file for content processing.
* 3) Split file contents based on batch size for parallel processing.
* 4) Process each split as a DLP table content request to invoke API.
* 5) Convert DLP Table Rows to BQ Table Row.
* 6) Create dynamic table and insert successfully converted records into BQ.
*/
PCollection<KV<String, Iterable<ReadableFile>>> csvFiles =
p
/*
* 1) Read from the text source continuously based on default interval e.g. 300 seconds
* - Setup a window for 30 secs to capture the list of files emited.
* - Group by file name as key and ReadableFile as a value.
*/
.apply(
"Poll Input Files",
FileIO.match()
.filepattern(options.getInputFilePattern())
.continuously(DEFAULT_POLL_INTERVAL, Watch.Growth.never()))
.apply("Find Pattern Match", FileIO.readMatches().withCompression(Compression.AUTO))
.apply("Add File Name as Key", WithKeys.of(file -> getFileName(file)))
.setCoder(KvCoder.of(StringUtf8Coder.of(), ReadableFileCoder.of()))
.apply(
"Fixed Window(30 Sec)",
Window.<KV<String, ReadableFile>>into(FixedWindows.of(WINDOW_INTERVAL))
.triggering(
Repeatedly.forever(
AfterProcessingTime.pastFirstElementInPane()
.plusDelayOf(Duration.ZERO)))
.discardingFiredPanes()
.withAllowedLateness(Duration.ZERO))
.apply(GroupByKey.create());
PCollection<KV<String, TableRow>> bqDataMap =
csvFiles
// 2) Output each readable file for content processing.
.apply(
"File Handler",
ParDo.of(
new DoFn<KV<String, Iterable<ReadableFile>>, KV<String, ReadableFile>>() {
@ProcessElement
public void processElement(ProcessContext c) {
String fileKey = c.element().getKey();
c.element()
.getValue()
.forEach(
file -> {
c.output(KV.of(fileKey, file));
});
}
}))
// 3) Split file contents based on batch size for parallel processing.
.apply(
"Process File Contents",
ParDo.of(
new CSVReader(
NestedValueProvider.of(
options.getBatchSize(),
batchSize -> {
if (batchSize != null) {
return batchSize;
} else {
return DEFAULT_BATCH_SIZE;
}
}))))
// 4) Create a DLP Table content request and invoke DLP API for each processsing
.apply(
"DLP-Tokenization",
ParDo.of(
new DLPTokenizationDoFn(
options.getDlpProjectId(),
options.getDeidentifyTemplateName(),
options.getInspectTemplateName())))
// 5) Convert DLP Table Rows to BQ Table Row
.apply("Process Tokenized Data", ParDo.of(new TableRowProcessorDoFn()));
// 6) Create dynamic table and insert successfully converted records into BQ.
bqDataMap.apply(
"Write To BQ",
BigQueryIO.<KV<String, TableRow>>write()
.to(new BQDestination(options.getDatasetName(), options.getDlpProjectId()))
.withFormatFunction(
element -> {
return element.getValue();
})
.withWriteDisposition(BigQueryIO.Write.WriteDisposition.WRITE_APPEND)
.withCreateDisposition(BigQueryIO.Write.CreateDisposition.CREATE_IF_NEEDED)
.withoutValidation()
.withFailedInsertRetryPolicy(InsertRetryPolicy.retryTransientErrors()));
return p.run();
}
/**
* The {@link TokenizePipelineOptions} interface provides the custom execution options passed by
* the executor at the command-line.
*/
public interface TokenizePipelineOptions extends DataflowPipelineOptions {
@TemplateParameter.GcsReadFile(
order = 1,
description = "Input Cloud Storage File(s)",
helpText = "The Cloud Storage location of the files you'd like to process.",
example = "gs://your-bucket/your-files/*.csv")
ValueProvider<String> getInputFilePattern();
void setInputFilePattern(ValueProvider<String> value);
@TemplateParameter.Text(
order = 2,
regexes = {
"^projects\\/[^\\n\\r\\/]+(\\/locations\\/[^\\n\\r\\/]+)?\\/deidentifyTemplates\\/[^\\n\\r\\/]+$"
},
description = "Cloud DLP deidentify template name",
helpText =
"Cloud DLP template to deidentify contents. Must be created here: https://console.cloud.google.com/security/dlp/create/template.",
example =
"projects/your-project-id/locations/global/deidentifyTemplates/generated_template_id")
@Required
ValueProvider<String> getDeidentifyTemplateName();
void setDeidentifyTemplateName(ValueProvider<String> value);
@TemplateParameter.Text(
order = 3,
optional = true,
regexes = {
"^projects\\/[^\\n\\r\\/]+(\\/locations\\/[^\\n\\r\\/]+)?\\/inspectTemplates\\/[^\\n\\r\\/]+$"
},
description = "Cloud DLP inspect template name",
helpText = "Cloud DLP template to inspect contents.",
example =
"projects/your-project-id/locations/global/inspectTemplates/generated_template_id")
ValueProvider<String> getInspectTemplateName();
void setInspectTemplateName(ValueProvider<String> value);
@TemplateParameter.Integer(
order = 4,
optional = true,
description = "Batch size",
helpText =
"Batch size contents (number of rows) to optimize DLP API call. Total size of the "
+ "rows must not exceed 512 KB and total cell count must not exceed 50,000. Default batch "
+ "size is set to 100. Ex. 1000")
@Required
ValueProvider<Integer> getBatchSize();
void setBatchSize(ValueProvider<Integer> value);
@TemplateParameter.Text(
order = 5,
regexes = {"^[^.]*$"},
description = "BigQuery Dataset",
helpText =
"BigQuery Dataset to be used. Dataset must exist prior to execution. Ex. pii_dataset")
ValueProvider<String> getDatasetName();
void setDatasetName(ValueProvider<String> value);
@TemplateParameter.ProjectId(
order = 6,
description = "Cloud DLP project ID",
helpText =
"Cloud DLP project ID to be used for data masking/tokenization. Ex. your-dlp-project")
ValueProvider<String> getDlpProjectId();
void setDlpProjectId(ValueProvider<String> value);
}
/**
* The {@link CSVReader} class uses experimental Split DoFn to split each csv file contents in
* chunks and process it in non-monolithic fashion. For example: if a CSV file has 100 rows and
* batch size is set to 15, then initial restrictions for the SDF will be 1 to 7 and split
* restriction will be {{1-2},{2-3}..{7-8}} for parallel executions.
*/
static class CSVReader extends DoFn<KV<String, ReadableFile>, KV<String, Table>> {
private ValueProvider<Integer> batchSize;
private PCollectionView<List<KV<String, List<String>>>> headerMap;
/** This counter is used to track number of lines processed against batch size. */
private Integer lineCount;
public CSVReader(ValueProvider<Integer> batchSize) {
lineCount = 1;
this.batchSize = batchSize;
}
@ProcessElement
public void processElement(ProcessContext c, RestrictionTracker<OffsetRange, Long> tracker)
throws IOException {
for (long i = tracker.currentRestriction().getFrom(); tracker.tryClaim(i); ++i) {
String fileKey = c.element().getKey();
try (BufferedReader br = getReader(c.element().getValue())) {
List<Table.Row> rows = new ArrayList<>();
Table dlpTable = null;
/** finding out EOL for this restriction so that we know the SOL */
int endOfLine = (int) (i * batchSize.get().intValue());
int startOfLine = (endOfLine - batchSize.get().intValue());
// getting the DLP table headers
Iterator<CSVRecord> csvRows = CSVFormat.DEFAULT.parse(br).iterator();
if (!csvRows.hasNext()) {
LOG.info("File `" + c.element().getKey() + "` is empty");
continue;
}
List<FieldId> dlpTableHeaders = toDlpTableHeaders(csvRows.next());
/** skipping all the rows that's not part of this restriction */
for (int line = 0; line < startOfLine; line++) {
if (csvRows.hasNext()) {
csvRows.next();
}
}
/** looping through buffered reader and creating DLP Table Rows equals to batch */
while (csvRows.hasNext() && lineCount <= batchSize.get()) {
CSVRecord csvRow = csvRows.next();
rows.add(convertCsvRowToTableRow(csvRow));
lineCount += 1;
}
/** creating DLP table and output for next transformation */
dlpTable = Table.newBuilder().addAllHeaders(dlpTableHeaders).addAllRows(rows).build();
c.output(KV.of(fileKey, dlpTable));
LOG.debug(
"Current Restriction From: {}, Current Restriction To: {},"
+ " StartofLine: {}, End Of Line {}, BatchData {}",
tracker.currentRestriction().getFrom(),
tracker.currentRestriction().getTo(),
startOfLine,
endOfLine,
dlpTable.getRowsCount());
}
}
}
private static List<FieldId> toDlpTableHeaders(CSVRecord headerRow) {
List<FieldId> result = new ArrayList<>();
for (String header : headerRow) {
result.add(FieldId.newBuilder().setName(header).build());
}
return result;
}
/**
* SDF needs to define a @GetInitialRestriction method that can create a restriction describing
* the complete work for a given element. For our case this would be the total number of rows
* for each CSV file. We will calculate the number of split required based on total number of
* rows and batch size provided.
*
* @throws IOException
*/
@GetInitialRestriction
public OffsetRange getInitialRestriction(@Element KV<String, ReadableFile> csvFile)
throws IOException {
int rowCount = 0;
int totalSplit = 0;
try (BufferedReader br = getReader(csvFile.getValue())) {
/** assume first row is header */
int checkRowCount = (int) br.lines().count() - 1;
rowCount = (checkRowCount < 1) ? 1 : checkRowCount;
totalSplit = rowCount / batchSize.get().intValue();
int remaining = rowCount % batchSize.get().intValue();
/**
* Adjusting the total number of split based on remaining rows. For example: batch size of
* 15 for 100 rows will have total 7 splits. As it's a range last split will have offset
* range {7,8}
*/
if (remaining > 0) {
totalSplit = totalSplit + 2;
} else {
totalSplit = totalSplit + 1;
}
}
LOG.debug("Initial Restriction range from 1 to: {}", totalSplit);
return new OffsetRange(1, totalSplit);
}
/**
* SDF needs to define a @SplitRestriction method that can split the intital restricton to a
* number of smaller restrictions. For example: a intital rewstriction of (x, N) as input and
* produces pairs (x, 0), (x, 1), …, (x, N-1) as output.
*/
@SplitRestriction
public void splitRestriction(
@Element KV<String, ReadableFile> csvFile,
@Restriction OffsetRange range,
OutputReceiver<OffsetRange> out) {
/** split the initial restriction by 1 */
for (final OffsetRange p : range.split(1, 1)) {
out.output(p);
}
}
@NewTracker
public OffsetRangeTracker newTracker(@Restriction OffsetRange range) {
return new OffsetRangeTracker(new OffsetRange(range.getFrom(), range.getTo()));
}
private Table.Row convertCsvRowToTableRow(CSVRecord csvRow) {
/** convert from CSV row to DLP Table Row */
Iterator<String> valueIterator = csvRow.iterator();
Table.Row.Builder tableRowBuilder = Table.Row.newBuilder();
while (valueIterator.hasNext()) {
String value = valueIterator.next();
if (value != null) {
tableRowBuilder.addValues(Value.newBuilder().setStringValue(value.toString()).build());
} else {
tableRowBuilder.addValues(Value.newBuilder().setStringValue("").build());
}
}
return tableRowBuilder.build();
}
private List<String> getHeaders(List<KV<String, List<String>>> headerMap, String fileKey) {
return headerMap.stream()
.filter(map -> map.getKey().equalsIgnoreCase(fileKey))
.findFirst()
.map(e -> e.getValue())
.orElse(null);
}
}
/**
* The {@link DLPTokenizationDoFn} class executes tokenization request by calling DLP api. It uses
* DLP table as a content item as CSV file contains fully structured data. DLP templates (e.g.
* de-identify, inspect) need to exist before this pipeline runs. As response from the API is
* received, this DoFn ouptputs KV of new table with table id as key.
*/
static class DLPTokenizationDoFn extends DoFn<KV<String, Table>, KV<String, Table>> {
private ValueProvider<String> dlpProjectId;
private DlpServiceClient dlpServiceClient;
private ValueProvider<String> deIdentifyTemplateName;
private ValueProvider<String> inspectTemplateName;
private boolean inspectTemplateExist;
private Builder requestBuilder;
private final Distribution numberOfRowsTokenized =
Metrics.distribution(DLPTokenizationDoFn.class, "numberOfRowsTokenizedDistro");
private final Distribution numberOfBytesTokenized =
Metrics.distribution(DLPTokenizationDoFn.class, "numberOfBytesTokenizedDistro");
public DLPTokenizationDoFn(
ValueProvider<String> dlpProjectId,
ValueProvider<String> deIdentifyTemplateName,
ValueProvider<String> inspectTemplateName) {
this.dlpProjectId = dlpProjectId;
this.dlpServiceClient = null;
this.deIdentifyTemplateName = deIdentifyTemplateName;
this.inspectTemplateName = inspectTemplateName;
this.inspectTemplateExist = false;
}
@Setup
public void setup() {
if (this.inspectTemplateName.isAccessible()) {
if (this.inspectTemplateName.get() != null) {
this.inspectTemplateExist = true;
}
}
if (this.deIdentifyTemplateName.isAccessible()) {
if (this.deIdentifyTemplateName.get() != null) {
this.requestBuilder =
DeidentifyContentRequest.newBuilder()
.setParent(ProjectName.of(this.dlpProjectId.get()).toString())
.setDeidentifyTemplateName(this.deIdentifyTemplateName.get());
if (this.inspectTemplateExist) {
this.requestBuilder.setInspectTemplateName(this.inspectTemplateName.get());
}
}
}
}
@StartBundle
public void startBundle() throws SQLException {
try {
this.dlpServiceClient = DlpServiceClient.create();
} catch (IOException e) {
LOG.error("Failed to create DLP Service Client", e.getMessage());
throw new RuntimeException(e);
}
}
@FinishBundle
public void finishBundle() throws Exception {
if (this.dlpServiceClient != null) {
this.dlpServiceClient.close();
}
}
@ProcessElement
public void processElement(ProcessContext c) {
String key = c.element().getKey();
Table nonEncryptedData = c.element().getValue();
ContentItem tableItem = ContentItem.newBuilder().setTable(nonEncryptedData).build();
this.requestBuilder.setItem(tableItem);
DeidentifyContentResponse response =
dlpServiceClient.deidentifyContent(this.requestBuilder.build());
Table tokenizedData = response.getItem().getTable();
numberOfRowsTokenized.update(tokenizedData.getRowsList().size());
numberOfBytesTokenized.update(tokenizedData.toByteArray().length);
c.output(KV.of(key, tokenizedData));
}
}
/**
* The {@link TableRowProcessorDoFn} class process tokenized DLP tables and convert them to
* BigQuery Table Row.
*/
public static class TableRowProcessorDoFn extends DoFn<KV<String, Table>, KV<String, TableRow>> {
@ProcessElement
public void processElement(ProcessContext c) {
Table tokenizedData = c.element().getValue();
List<String> headers =
tokenizedData.getHeadersList().stream()
.map(fid -> fid.getName())
.collect(Collectors.toList());
List<Table.Row> outputRows = tokenizedData.getRowsList();
if (outputRows.size() > 0) {
for (Table.Row outputRow : outputRows) {
if (outputRow.getValuesCount() != headers.size()) {
throw new IllegalArgumentException(
"CSV file's header count must exactly match with data element count");
}
c.output(
KV.of(
c.element().getKey(),
createBqRow(outputRow, headers.toArray(new String[headers.size()]))));
}
}
}
private static TableRow createBqRow(Table.Row tokenizedValue, String[] headers) {
TableRow bqRow = new TableRow();
AtomicInteger headerIndex = new AtomicInteger(0);
List<TableCell> cells = new ArrayList<>();
tokenizedValue
.getValuesList()
.forEach(
value -> {
String checkedHeaderName =
checkHeaderName(headers[headerIndex.getAndIncrement()].toString());
bqRow.set(checkedHeaderName, value.getStringValue());
cells.add(new TableCell().set(checkedHeaderName, value.getStringValue()));
});
bqRow.setF(cells);
return bqRow;
}
}
/**
* The {@link BQDestination} class creates BigQuery table destination and table schema based on
* the CSV file processed in earlier transformations. Table id is same as filename Table schema is
* same as file header columns.
*/
public static class BQDestination
extends DynamicDestinations<KV<String, TableRow>, KV<String, TableRow>> {
private ValueProvider<String> datasetName;
private ValueProvider<String> projectId;
public BQDestination(ValueProvider<String> datasetName, ValueProvider<String> projectId) {
this.datasetName = datasetName;
this.projectId = projectId;
}
@Override
public KV<String, TableRow> getDestination(ValueInSingleWindow<KV<String, TableRow>> element) {
String key = element.getValue().getKey();
String tableName = String.format("%s:%s.%s", projectId.get(), datasetName.get(), key);
LOG.debug("Table Name {}", tableName);
return KV.of(tableName, element.getValue().getValue());
}
@Override
public TableDestination getTable(KV<String, TableRow> destination) {
TableDestination dest =
new TableDestination(destination.getKey(), "pii-tokenized output data from dataflow");
LOG.debug("Table Destination {}", dest.getTableSpec());
return dest;
}
@Override
public TableSchema getSchema(KV<String, TableRow> destination) {
TableRow bqRow = destination.getValue();
TableSchema schema = new TableSchema();
List<TableFieldSchema> fields = new ArrayList<TableFieldSchema>();
List<TableCell> cells = bqRow.getF();
for (int i = 0; i < cells.size(); i++) {
Map<String, Object> object = cells.get(i);
String header = object.keySet().iterator().next();
/** currently all BQ data types are set to String */
fields.add(new TableFieldSchema().setName(checkHeaderName(header)).setType("STRING"));
}
schema.setFields(fields);
return schema;
}
}
private static String getFileName(ReadableFile file) {
String csvFileName = file.getMetadata().resourceId().getFilename().toString();
/** taking out .csv extension from file name e.g fileName.csv->fileName */
String[] fileKey = csvFileName.split("\\.", 2);
if (!fileKey[1].equals(ALLOWED_FILE_EXTENSION) || !TABLE_REGEXP.matcher(fileKey[0]).matches()) {
throw new RuntimeException(
"[Filename must contain a CSV extension "
+ " BQ table name must contain only letters, numbers, or underscores ["
+ fileKey[1]
+ "], ["
+ fileKey[0]
+ "]");
}
/** returning file name without extension */
return fileKey[0];
}
private static BufferedReader getReader(ReadableFile csvFile) {
BufferedReader br = null;
ReadableByteChannel channel = null;
/** read the file and create buffered reader */
try {
channel = csvFile.openSeekable();
} catch (IOException e) {
LOG.error("Failed to Read File {}", e.getMessage());
throw new RuntimeException(e);
}
if (channel != null) {
br = new BufferedReader(Channels.newReader(channel, Charsets.UTF_8.name()));
}
return br;
}
private static String checkHeaderName(String name) {
/** some checks to make sure BQ column names don't fail e.g. special characters */
String checkedHeader = name.replaceAll("\\s", "_");
checkedHeader = checkedHeader.replaceAll("'", "");
checkedHeader = checkedHeader.replaceAll("/", "");
if (!COLUMN_NAME_REGEXP.matcher(checkedHeader).matches()) {
throw new IllegalArgumentException("Column name can't be matched to a valid format " + name);
}
return checkedHeader;
}
}
Alterar a captura de dados do MySQL para o BigQuery usando o Debezium e o Pub/Sub (Stream)
O modelo da captura de dados de alteração do MySQL para o BigQuery usando o Debezium e o Pub/Sub
é um pipeline de streaming que lê mensagens Pub/Sub com dados de alteração de
um banco de dados MySQL e grava os registros no BigQuery. Um conector do Debezium captura
alterações no banco de dados MySQL e publica os dados alterados no Pub/Sub. O
modelo lê as mensagens do Pub/Sub e as grava no BigQuery.
É possível usar esse modelo para sincronizar bancos de dados MySQL e tabelas do BigQuery. O
pipeline grava os dados alterados em uma tabela de preparo do BigQuery e atualiza intermitentemente
uma tabela do BigQuery que replica o banco de dados MySQL.
PROJECT_ID:
o ID do projeto do Cloud em que você quer executar o job do Dataflow
SUBSCRIPTIONS: sua lista separada por vírgulas de nomes de assinatura do Pub/Sub
CHANGELOG_DATASET: seu conjunto de dados do BigQuery para dados do registro de alterações
REPLICA_DATASET: seu conjunto de dados do BigQuery para tabelas replicadas
Apache Kafka para BigQuery
O modelo Apache Kafka para BigQuery é um pipeline de streaming que ingere dados de texto do Apache Kafka, executa uma função definida pelo usuário (UDF) e gera os registros resultantes no BigQuery.
Qualquer erro que ocorrer na transformação dos dados, na execução da UDF ou na inserção na tabela de respostas será inserido em uma tabela de erros separada no BigQuery. Se a tabela de erros não existir antes da execução, ela será criada.
Requisitos para esse pipeline
A tabela de respostas do BigQuery precisa existir.
O servidor do agente do Apache Kafka precisa estar em execução e acessível em máquinas de trabalho do Dataflow.
Os tópicos do Apache Kafka precisam existir, e as mensagens precisam estar codificadas em um formato JSON válido.
Parâmetros do modelo
Parâmetro
Descrição
outputTableSpec
O local da tabela de respostas do BigQuery para gravar as mensagens do Apache Kafka, no formato de my-project:dataset.table
inputTopics
Os tópicos de entrada do Apache Kafka para leitura em uma lista separada por vírgulas. Exemplo: messages
bootstrapServers
O endereço do host dos servidores do agente do Apache Kafka em execução em uma lista separada por vírgulas, cada endereço de host no formato 35.70.252.199:9092
javascriptTextTransformGcsPath
(Opcional)
O URI do Cloud Storage do arquivo .js que define a função definida
pelo usuário (UDF, na sigla em inglês) do JavaScript que você quer usar. Por exemplo, gs://my-bucket/my-udfs/my_file.js.
javascriptTextTransformFunctionName
(Opcional)
O nome da função definida pelo usuário (UDF) do JavaScript que você quer usar.
Por exemplo, se o código de função do JavaScript for
myTransform(inJson) { /*...do stuff...*/ }, o nome da função será
myTransform. Para amostras de UDFs do JavaScript, consulte os
exemplos de UDF.
outputDeadletterTable
Opcional: a tabela do BigQuery para mensagens que não alcançaram a tabela de saída, no formato de my-project:dataset.my-deadletter-table. Se não existir, a tabela será criada durante a execução do pipeline.
Se não for especificada, será usada <outputTableSpec>_error_records.
Como executar o modelo do Apache Kafka para BigQuery
Console
Acesse a página Criar job usando um modelo do Dataflow.
PROJECT_ID:
o ID do projeto do Cloud em que você quer executar o job do Dataflow
JOB_NAME:
um nome de job de sua escolha
REGION_NAME:
o endpoint regional em que você quer
implantar o job do Dataflow, por exemplo, us-central1
VERSION:
a versão do modelo que você quer usar
Use estes valores:
latest para usar a versão mais recente do modelo, disponível na pasta mãe
não datada no bucket:
gs://dataflow-templates/latest/
o nome da versão, como 2021-09-20-00_RC00, para usar uma versão específica do
modelo, que pode ser aninhada na respectiva pasta mãe datada no bucket:
gs://dataflow-templates
BIGQUERY_TABLE: o nome da tabela do BigQuery
KAFKA_TOPICS: a lista de tópicos do Apache Kakfa. Se vários tópicos forem fornecidos, siga as instruções sobre como evitar vírgulas.
PATH_TO_JAVASCRIPT_UDF_FILE:
o URI do Cloud Storage do arquivo .js que define a função
definida pelo usuário (UDF, na sigla em inglês) do JavaScript que você quer usar, por exemplo,gs://my-bucket/my-udfs/my_file.js
JAVASCRIPT_FUNCTION:
o nome da função definida pelo usuário (UDF) do JavaScript que você quer usar
Por exemplo, se o código de função do JavaScript for
myTransform(inJson) { /*...do stuff...*/ }, o nome da função será
myTransform. Para amostras de UDFs do JavaScript, consulte os
exemplos de UDF.
KAFKA_SERVER_ADDRESSES: a lista de endereços IP do servidor de gerenciamento do Apache Kafka. Cada endereço IP precisa ter o número de portas que o servidor pode acessar. Por exemplo, 35.70.252.199:9092.
Se vários endereços forem fornecidos, siga instruções sobre como evitar vírgulas.
API
Para executar o modelo usando a API REST, envie uma solicitação HTTP POST. Para mais informações sobre a
API e os respectivos escopos de autorização, consulte
projects.templates.launch.
PROJECT_ID:
o ID do projeto do Cloud em que você quer executar o job do Dataflow
JOB_NAME:
um nome de job de sua escolha
LOCATION:
o endpoint regional em que você quer
implantar o job do Dataflow, por exemplo, us-central1
VERSION:
a versão do modelo que você quer usar
Use estes valores:
latest para usar a versão mais recente do modelo, disponível na pasta mãe
não datada no bucket:
gs://dataflow-templates/latest/
o nome da versão, como 2021-09-20-00_RC00, para usar uma versão específica do
modelo, que pode ser aninhada na respectiva pasta mãe datada no bucket:
gs://dataflow-templates
BIGQUERY_TABLE: o nome da tabela do BigQuery
KAFKA_TOPICS: a lista de tópicos do Apache Kakfa. Se vários tópicos forem fornecidos, siga as instruções sobre como evitar vírgulas.
PATH_TO_JAVASCRIPT_UDF_FILE:
o URI do Cloud Storage do arquivo .js que define a função
definida pelo usuário (UDF, na sigla em inglês) do JavaScript que você quer usar, por exemplo,gs://my-bucket/my-udfs/my_file.js
JAVASCRIPT_FUNCTION:
o nome da função definida pelo usuário (UDF) do JavaScript que você quer usar
Por exemplo, se o código de função do JavaScript for
myTransform(inJson) { /*...do stuff...*/ }, o nome da função será
myTransform. Para amostras de UDFs do JavaScript, consulte os
exemplos de UDF.
KAFKA_SERVER_ADDRESSES: a lista de endereços IP do servidor de gerenciamento do Apache Kafka. Cada endereço IP precisa ter o número de portas que o servidor pode acessar. Por exemplo, 35.70.252.199:9092.
Se vários endereços forem fornecidos, siga instruções sobre como evitar vírgulas.
/*
* Copyright (C) 2019 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.cloud.teleport.v2.templates;
import static com.google.cloud.teleport.v2.kafka.transforms.KafkaTransform.readFromKafka;
import com.google.api.services.bigquery.model.TableRow;
import com.google.cloud.teleport.metadata.Template;
import com.google.cloud.teleport.metadata.TemplateCategory;
import com.google.cloud.teleport.metadata.TemplateParameter;
import com.google.cloud.teleport.v2.coders.FailsafeElementCoder;
import com.google.cloud.teleport.v2.common.UncaughtExceptionLogger;
import com.google.cloud.teleport.v2.kafka.options.KafkaReadOptions;
import com.google.cloud.teleport.v2.options.BigQueryStorageApiStreamingOptions;
import com.google.cloud.teleport.v2.templates.KafkaToBigQuery.KafkaToBQOptions;
import com.google.cloud.teleport.v2.transforms.BigQueryConverters.FailsafeJsonToTableRow;
import com.google.cloud.teleport.v2.transforms.ErrorConverters;
import com.google.cloud.teleport.v2.transforms.ErrorConverters.WriteKafkaMessageErrors;
import com.google.cloud.teleport.v2.transforms.JavascriptTextTransformer.FailsafeJavascriptUdf;
import com.google.cloud.teleport.v2.transforms.JavascriptTextTransformer.JavascriptTextTransformerOptions;
import com.google.cloud.teleport.v2.utils.BigQueryIOUtils;
import com.google.cloud.teleport.v2.utils.MetadataValidator;
import com.google.cloud.teleport.v2.utils.SchemaUtils;
import com.google.cloud.teleport.v2.values.FailsafeElement;
import com.google.common.collect.ImmutableMap;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.apache.beam.sdk.Pipeline;
import org.apache.beam.sdk.PipelineResult;
import org.apache.beam.sdk.coders.CoderRegistry;
import org.apache.beam.sdk.coders.KvCoder;
import org.apache.beam.sdk.coders.NullableCoder;
import org.apache.beam.sdk.coders.StringUtf8Coder;
import org.apache.beam.sdk.io.gcp.bigquery.BigQueryIO;
import org.apache.beam.sdk.io.gcp.bigquery.BigQueryIO.Write.CreateDisposition;
import org.apache.beam.sdk.io.gcp.bigquery.BigQueryIO.Write.WriteDisposition;
import org.apache.beam.sdk.io.gcp.bigquery.BigQueryInsertError;
import org.apache.beam.sdk.io.gcp.bigquery.InsertRetryPolicy;
import org.apache.beam.sdk.io.gcp.bigquery.WriteResult;
import org.apache.beam.sdk.options.PipelineOptionsFactory;
import org.apache.beam.sdk.options.Validation.Required;
import org.apache.beam.sdk.transforms.DoFn;
import org.apache.beam.sdk.transforms.Flatten;
import org.apache.beam.sdk.transforms.MapElements;
import org.apache.beam.sdk.transforms.PTransform;
import org.apache.beam.sdk.transforms.ParDo;
import org.apache.beam.sdk.values.KV;
import org.apache.beam.sdk.values.PCollection;
import org.apache.beam.sdk.values.PCollectionList;
import org.apache.beam.sdk.values.PCollectionTuple;
import org.apache.beam.sdk.values.TupleTag;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.kafka.clients.consumer.ConsumerConfig;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link KafkaToBigQuery} pipeline is a streaming pipeline which ingests text data from Kafka,
* executes a UDF, and outputs the resulting records to BigQuery. Any errors which occur in the
* transformation of the data, execution of the UDF, or inserting into the output table will be
* inserted into a separate errors table in BigQuery. The errors table will be created if it does
* not exist prior to execution. Both output and error tables are specified by the user as
* parameters.
*
* <p><b>Pipeline Requirements</b>
*
* <ul>
* <li>The Kafka topic exists and the message is encoded in a valid JSON format.
* <li>The BigQuery output table exists.
* <li>The Kafka brokers are reachable from the Dataflow worker machines.
* </ul>
*
* <p><b>Example Usage</b>
*
* <pre>
*
* # Set some environment variables
* PROJECT=my-project
* TEMP_BUCKET=my-temp-bucket
* OUTPUT_TABLE=${PROJECT}:my_dataset.my_table
* TOPICS=my-topics
* JS_PATH=my-js-path-on-gcs
* JS_FUNC_NAME=my-js-func-name
* BOOTSTRAP=my-comma-separated-bootstrap-servers
*
* # Set containerization vars
* IMAGE_NAME=my-image-name
* TARGET_GCR_IMAGE=gcr.io/${PROJECT}/${IMAGE_NAME}
* BASE_CONTAINER_IMAGE=my-base-container-image
* BASE_CONTAINER_IMAGE_VERSION=my-base-container-image-version
* APP_ROOT=/path/to/app-root
* COMMAND_SPEC=/path/to/command-spec
*
* # Build and upload image
* mvn clean package \
* -Dimage=${TARGET_GCR_IMAGE} \
* -Dbase-container-image=${BASE_CONTAINER_IMAGE} \
* -Dbase-container-image.version=${BASE_CONTAINER_IMAGE_VERSION} \
* -Dapp-root=${APP_ROOT} \
* -Dcommand-spec=${COMMAND_SPEC}
*
* # Create an image spec in GCS that contains the path to the image
* {
* "docker_template_spec": {
* "docker_image": $TARGET_GCR_IMAGE
* }
* }
*
* # Execute template:
* API_ROOT_URL="https://dataflow.googleapis.com"
* TEMPLATES_LAUNCH_API="${API_ROOT_URL}/v1b3/projects/${PROJECT}/templates:launch"
* JOB_NAME="kafka-to-bigquery`date +%Y%m%d-%H%M%S-%N`"
*
* time curl -X POST -H "Content-Type: application/json" \
* -H "Authorization: Bearer $(gcloud auth print-access-token)" \
* "${TEMPLATES_LAUNCH_API}"`
* `"?validateOnly=false"`
* `"&dynamicTemplate.gcsPath=${TEMP_BUCKET}/path/to/image-spec"`
* `"&dynamicTemplate.stagingLocation=${TEMP_BUCKET}/staging" \
* -d '
* {
* "jobName":"'$JOB_NAME'",
* "parameters": {
* "outputTableSpec":"'$OUTPUT_TABLE'",
* "inputTopics":"'$TOPICS'",
* "javascriptTextTransformGcsPath":"'$JS_PATH'",
* "javascriptTextTransformFunctionName":"'$JS_FUNC_NAME'",
* "bootstrapServers":"'$BOOTSTRAP'"
* }
* }
* '
* </pre>
*/
@Template(
name = "Kafka_to_BigQuery",
category = TemplateCategory.STREAMING,
displayName = "Kafka to BigQuery",
description =
"A streaming pipeline which ingests data in JSON format from Kafka, performs a transform"
+ " via a user defined JavaScript function, and writes to a pre-existing BigQuery"
+ " table.",
optionsClass = KafkaToBQOptions.class,
flexContainerName = "kafka-to-bigquery",
contactInformation = "https://cloud.google.com/support")
public class KafkaToBigQuery {
/* Logger for class. */
private static final Logger LOG = LoggerFactory.getLogger(KafkaToBigQuery.class);
/** The tag for the main output for the UDF. */
private static final TupleTag<FailsafeElement<KV<String, String>, String>> UDF_OUT =
new TupleTag<FailsafeElement<KV<String, String>, String>>() {};
/** The tag for the main output of the json transformation. */
static final TupleTag<TableRow> TRANSFORM_OUT = new TupleTag<TableRow>() {};
/** The tag for the dead-letter output of the udf. */
static final TupleTag<FailsafeElement<KV<String, String>, String>> UDF_DEADLETTER_OUT =
new TupleTag<FailsafeElement<KV<String, String>, String>>() {};
/** The tag for the dead-letter output of the json to table row transform. */
static final TupleTag<FailsafeElement<KV<String, String>, String>> TRANSFORM_DEADLETTER_OUT =
new TupleTag<FailsafeElement<KV<String, String>, String>>() {};
/** The default suffix for error tables if dead letter table is not specified. */
private static final String DEFAULT_DEADLETTER_TABLE_SUFFIX = "_error_records";
/** String/String Coder for FailsafeElement. */
private static final FailsafeElementCoder<String, String> FAILSAFE_ELEMENT_CODER =
FailsafeElementCoder.of(
NullableCoder.of(StringUtf8Coder.of()), NullableCoder.of(StringUtf8Coder.of()));
/**
* The {@link KafkaToBQOptions} class provides the custom execution options passed by the executor
* at the command-line.
*/
public interface KafkaToBQOptions
extends KafkaReadOptions,
JavascriptTextTransformerOptions,
BigQueryStorageApiStreamingOptions {
@TemplateParameter.BigQueryTable(
order = 1,
description = "BigQuery output table",
helpText =
"BigQuery table location to write the output to. The name should be in the format "
+ "<project>:<dataset>.<table_name>. The table's schema must match input objects.")
@Required
String getOutputTableSpec();
void setOutputTableSpec(String outputTableSpec);
/**
* Get bootstrap server across releases.
*
* @deprecated This method is no longer acceptable to get bootstrap servers.
* <p>Use {@link KafkaToBQOptions#getReadBootstrapServers()} instead.
*/
@TemplateParameter.Text(
order = 2,
optional = true,
regexes = {"[,:a-zA-Z0-9._-]+"},
description = "Kafka Bootstrap Server list",
helpText = "Kafka Bootstrap Server list, separated by commas.",
example = "localhost:9092,127.0.0.1:9093")
@Deprecated
String getBootstrapServers();
/**
* Get bootstrap server across releases.
*
* @deprecated This method is no longer acceptable to set bootstrap servers.
* <p>Use {@link KafkaToBQOptions#setReadBootstrapServers()} instead.
*/
@Deprecated
void setBootstrapServers(String bootstrapServers);
/**
* Get bootstrap server across releases.
*
* @deprecated This method is no longer acceptable to get Input topics.
* <p>Use {@link KafkaToBQOptions#getKafkaReadTopics()} instead.
*/
@Deprecated
@TemplateParameter.Text(
order = 3,
regexes = {"[,a-zA-Z0-9._-]+"},
description = "Kafka topic(s) to read the input from",
helpText = "Kafka topic(s) to read the input from.",
example = "topic1,topic2")
String getInputTopics();
/**
* Get bootstrap server across releases.
*
* @deprecated This method is no longer acceptable to set Input topics.
* <p>Use {@link KafkaToBQOptions#getKafkaReadTopics()} instead.
*/
@Deprecated
void setInputTopics(String inputTopics);
@TemplateParameter.BigQueryTable(
order = 4,
optional = true,
description = "The dead-letter table name to output failed messages to BigQuery",
helpText =
"Messages failed to reach the output table for all kind of reasons (e.g., mismatched"
+ " schema, malformed json) are written to this table. If it doesn't exist, it will"
+ " be created during pipeline execution.",
example = "your-project-id:your-dataset.your-table-name")
String getOutputDeadletterTable();
void setOutputDeadletterTable(String outputDeadletterTable);
}
/**
* The main entry-point for pipeline execution. This method will start the pipeline but will not
* wait for it's execution to finish. If blocking execution is required, use the {@link
* KafkaToBigQuery#run(KafkaToBQOptions)} method to start the pipeline and invoke {@code
* result.waitUntilFinish()} on the {@link PipelineResult}.
*
* @param args The command-line args passed by the executor.
*/
public static void main(String[] args) {
UncaughtExceptionLogger.register();
KafkaToBQOptions options =
PipelineOptionsFactory.fromArgs(args).withValidation().as(KafkaToBQOptions.class);
run(options);
}
/**
* Runs the pipeline to completion with the specified options. This method does not wait until the
* pipeline is finished before returning. Invoke {@code result.waitUntilFinish()} on the result
* object to block until the pipeline is finished running if blocking programmatic execution is
* required.
*
* @param options The execution options.
* @return The pipeline result.
*/
public static PipelineResult run(KafkaToBQOptions options) {
// Validate BQ STORAGE_WRITE_API options
BigQueryIOUtils.validateBQStorageApiOptionsStreaming(options);
MetadataValidator.validate(options);
// Create the pipeline
Pipeline pipeline = Pipeline.create(options);
// Register the coder for pipeline
FailsafeElementCoder<KV<String, String>, String> coder =
FailsafeElementCoder.of(
KvCoder.of(
NullableCoder.of(StringUtf8Coder.of()), NullableCoder.of(StringUtf8Coder.of())),
NullableCoder.of(StringUtf8Coder.of()));
CoderRegistry coderRegistry = pipeline.getCoderRegistry();
coderRegistry.registerCoderForType(coder.getEncodedTypeDescriptor(), coder);
List<String> topicsList;
if (options.getKafkaReadTopics() != null) {
topicsList = new ArrayList<>(Arrays.asList(options.getKafkaReadTopics().split(",")));
} else if (options.getInputTopics() != null) {
topicsList = new ArrayList<>(Arrays.asList(options.getInputTopics().split(",")));
} else {
throw new IllegalArgumentException("Please Provide --kafkaReadTopic");
}
String bootstrapServers;
if (options.getReadBootstrapServers() != null) {
bootstrapServers = options.getReadBootstrapServers();
} else if (options.getBootstrapServers() != null) {
bootstrapServers = options.getBootstrapServers();
} else {
throw new IllegalArgumentException("Please Provide --bootstrapServers");
}
/*
* Steps:
* 1) Read messages in from Kafka
* 2) Transform the messages into TableRows
* - Transform message payload via UDF
* - Convert UDF result to TableRow objects
* 3) Write successful records out to BigQuery
* 4) Write failed records out to BigQuery
*/
PCollectionTuple convertedTableRows =
pipeline
/*
* Step #1: Read messages in from Kafka
*/
.apply(
"ReadFromKafka",
readFromKafka(
bootstrapServers,
topicsList,
ImmutableMap.of(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest"),
null))
/*
* Step #2: Transform the Kafka Messages into TableRows
*/
.apply("ConvertMessageToTableRow", new MessageToTableRow(options));
/*
* Step #3: Write the successful records out to BigQuery
*/
WriteResult writeResult =
convertedTableRows
.get(TRANSFORM_OUT)
.apply(
"WriteSuccessfulRecords",
BigQueryIO.writeTableRows()
.withoutValidation()
.withCreateDisposition(CreateDisposition.CREATE_NEVER)
.withWriteDisposition(WriteDisposition.WRITE_APPEND)
.withExtendedErrorInfo()
.withFailedInsertRetryPolicy(InsertRetryPolicy.retryTransientErrors())
.to(options.getOutputTableSpec()));
/*
* Step 3 Contd.
* Elements that failed inserts into BigQuery are extracted and converted to FailsafeElement
*/
PCollection<FailsafeElement<String, String>> failedInserts =
BigQueryIOUtils.writeResultToBigQueryInsertErrors(writeResult, options)
.apply(
"WrapInsertionErrors",
MapElements.into(FAILSAFE_ELEMENT_CODER.getEncodedTypeDescriptor())
.via(KafkaToBigQuery::wrapBigQueryInsertError))
.setCoder(FAILSAFE_ELEMENT_CODER);
/*
* Step #4: Write failed records out to BigQuery
*/
PCollectionList.of(convertedTableRows.get(UDF_DEADLETTER_OUT))
.and(convertedTableRows.get(TRANSFORM_DEADLETTER_OUT))
.apply("Flatten", Flatten.pCollections())
.apply(
"WriteTransformationFailedRecords",
WriteKafkaMessageErrors.newBuilder()
.setErrorRecordsTable(
ObjectUtils.firstNonNull(
options.getOutputDeadletterTable(),
options.getOutputTableSpec() + DEFAULT_DEADLETTER_TABLE_SUFFIX))
.setErrorRecordsTableSchema(SchemaUtils.DEADLETTER_SCHEMA)
.build());
/*
* Step #5: Insert records that failed BigQuery inserts into a deadletter table.
*/
failedInserts.apply(
"WriteInsertionFailedRecords",
ErrorConverters.WriteStringMessageErrors.newBuilder()
.setErrorRecordsTable(
ObjectUtils.firstNonNull(
options.getOutputDeadletterTable(),
options.getOutputTableSpec() + DEFAULT_DEADLETTER_TABLE_SUFFIX))
.setErrorRecordsTableSchema(SchemaUtils.DEADLETTER_SCHEMA)
.build());
return pipeline.run();
}
/**
* The {@link MessageToTableRow} class is a {@link PTransform} which transforms incoming Kafka
* Message objects into {@link TableRow} objects for insertion into BigQuery while applying a UDF
* to the input. The executions of the UDF and transformation to {@link TableRow} objects is done
* in a fail-safe way by wrapping the element with it's original payload inside the {@link
* FailsafeElement} class. The {@link MessageToTableRow} transform will output a {@link
* PCollectionTuple} which contains all output and dead-letter {@link PCollection}.
*
* <p>The {@link PCollectionTuple} output will contain the following {@link PCollection}:
*
* <ul>
* <li>{@link KafkaToBigQuery#UDF_OUT} - Contains all {@link FailsafeElement} records
* successfully processed by the UDF.
* <li>{@link KafkaToBigQuery#UDF_DEADLETTER_OUT} - Contains all {@link FailsafeElement} records
* which failed processing during the UDF execution.
* <li>{@link KafkaToBigQuery#TRANSFORM_OUT} - Contains all records successfully converted from
* JSON to {@link TableRow} objects.
* <li>{@link KafkaToBigQuery#TRANSFORM_DEADLETTER_OUT} - Contains all {@link FailsafeElement}
* records which couldn't be converted to table rows.
* </ul>
*/
static class MessageToTableRow
extends PTransform<PCollection<KV<String, String>>, PCollectionTuple> {
private final KafkaToBQOptions options;
MessageToTableRow(KafkaToBQOptions options) {
this.options = options;
}
@Override
public PCollectionTuple expand(PCollection<KV<String, String>> input) {
PCollectionTuple udfOut =
input
// Map the incoming messages into FailsafeElements so we can recover from failures
// across multiple transforms.
.apply("MapToRecord", ParDo.of(new MessageToFailsafeElementFn()))
.apply(
"InvokeUDF",
FailsafeJavascriptUdf.<KV<String, String>>newBuilder()
.setFileSystemPath(options.getJavascriptTextTransformGcsPath())
.setFunctionName(options.getJavascriptTextTransformFunctionName())
.setSuccessTag(UDF_OUT)
.setFailureTag(UDF_DEADLETTER_OUT)
.build());
// Convert the records which were successfully processed by the UDF into TableRow objects.
PCollectionTuple jsonToTableRowOut =
udfOut
.get(UDF_OUT)
.apply(
"JsonToTableRow",
FailsafeJsonToTableRow.<KV<String, String>>newBuilder()
.setSuccessTag(TRANSFORM_OUT)
.setFailureTag(TRANSFORM_DEADLETTER_OUT)
.build());
// Re-wrap the PCollections so we can return a single PCollectionTuple
return PCollectionTuple.of(UDF_OUT, udfOut.get(UDF_OUT))
.and(UDF_DEADLETTER_OUT, udfOut.get(UDF_DEADLETTER_OUT))
.and(TRANSFORM_OUT, jsonToTableRowOut.get(TRANSFORM_OUT))
.and(TRANSFORM_DEADLETTER_OUT, jsonToTableRowOut.get(TRANSFORM_DEADLETTER_OUT));
}
}
/**
* The {@link MessageToFailsafeElementFn} wraps an Kafka Message with the {@link FailsafeElement}
* class so errors can be recovered from and the original message can be output to a error records
* table.
*/
static class MessageToFailsafeElementFn
extends DoFn<KV<String, String>, FailsafeElement<KV<String, String>, String>> {
@ProcessElement
public void processElement(ProcessContext context) {
KV<String, String> message = context.element();
context.output(FailsafeElement.of(message, message.getValue()));
}
}
/**
* Method to wrap a {@link BigQueryInsertError} into a {@link FailsafeElement}.
*
* @param insertError BigQueryInsert error.
* @return FailsafeElement object.
*/
protected static FailsafeElement<String, String> wrapBigQueryInsertError(
BigQueryInsertError insertError) {
FailsafeElement<String, String> failsafeElement;
try {
failsafeElement =
FailsafeElement.of(
insertError.getRow().toPrettyString(), insertError.getRow().toPrettyString());
failsafeElement.setErrorMessage(insertError.getError().toPrettyString());
} catch (IOException e) {
LOG.error("Failed to wrap BigQuery insert error.");
throw new RuntimeException(e);
}
return failsafeElement;
}
}
Datastream para o BigQuery (stream)
O modelo do Datastream para o BigQuery é um pipeline de streaming que lê
dados do
Datastream e os replica no BigQuery. O modelo lê dados do Cloud Storage
usando notificações do Pub/Sub e os replica em uma tabela de preparo particionada
do BigQuery. Após a replicação, o modelo executa um MERGE no BigQuery
para mesclar todas as alterações de change data capture (CDC) em uma réplica da tabela de origem.
O modelo lida com a criação e a atualização das tabelas do BigQuery gerenciadas pela
replicação. Quando a linguagem de definição de dados (DDL) é obrigatória, um callback para o Datastream extrai o esquema da
tabela de origem e o converte em tipos de dados do BigQuery. As operações compatíveis incluem:
Novas tabelas são criadas à medida que os dados são inseridos.
Novas colunas são adicionadas às tabelas do BigQuery com valores iniciais nulos.
As colunas descartadas são ignoradas no BigQuery, e os valores futuros são nulos.
As colunas renomeadas são adicionadas ao BigQuery como novas colunas.
As alterações de tipo não são propagadas para o BigQuery.
Requisitos para este pipeline:
Um stream do Datastream que está pronto ou já está replicando dados.
Os conjuntos de dados de destino do BigQuery são criados, e a conta de serviço do Compute Engine recebe acesso de administrador a eles.
Uma chave primária é necessária na tabela de origem para a criação da tabela de réplica de destino.
Parâmetros do modelo
Parâmetro
Descrição
inputFilePattern
O local dos arquivos do Datastream que serão replicados no Cloud Storage. Normalmente, esse local de arquivo é o caminho raiz do stream.
gcsPubSubSubscription
A assinatura do Pub/Sub com notificações de arquivos do Datastream. Por exemplo, projects/my-project-id/subscriptions/my-subscription-id.
inputFileFormat
O formato do arquivo de saída produzido pelo Datastream. Por exemplo, avro,json. Padrão: avro.
outputStagingDatasetTemplate
O nome de um conjunto de dados existente para conter tabelas de preparo. É possível incluir o modelo {_metadata_dataset} como um marcador que é substituído pelo nome do seu conjunto de dados/esquema de origem (por exemplo, {_metadata_dataset}_log).
outputDatasetTemplate
O nome de um conjunto de dados existente para conter tabelas de réplica. É possível incluir o modelo {_metadata_dataset} como um marcador que é substituído pelo nome do conjunto de dados/esquema de origem (por exemplo, {_metadata_dataset}).
deadLetterQueueDirectory
O caminho do arquivo para armazenar todas as mensagens não processadas com o motivo de falha no processamento. O padrão é um diretório no local temporário do job do Dataflow. O valor padrão é suficiente na maioria das condições.
outputStagingTableNameTemplate
(Opcional) O modelo para o nome das tabelas de preparo. O padrão é {_metadata_table}_log. Se você estiver replicando vários esquemas, a sugestão é {_metadata_schema}_{_metadata_table}_log.
outputTableNameTemplate
(Opcional) O modelo para o nome das tabelas de réplica. Padrão: {_metadata_table}. Se você estiver replicando vários esquemas, a sugestão é {_metadata_schema}_{_metadata_table}.
outputProjectId
Opcional: projeto para conjuntos de dados do BigQuery em que os dados serão gerados. O padrão para esse parâmetro é o projeto em que o pipeline do Dataflow está sendo executado.
streamName
(Opcional) O nome ou modelo do stream para pesquisar informações de esquema. Padrão: {_metadata_stream}.
mergeFrequencyMinutes
(Opcional) O número de minutos entre as mesclagens de uma determinada tabela. Padrão: 5.
dlqRetryMinutes
(Opcional) O número de minutos entre novas tentativas de fila de mensagens inativas (DLQ) Padrão: 10.
javascriptTextTransformGcsPath
(Opcional)
O URI do Cloud Storage do arquivo .js que define a função definida
pelo usuário (UDF, na sigla em inglês) do JavaScript que você quer usar. Por exemplo, gs://my-bucket/my-udfs/my_file.js.
javascriptTextTransformFunctionName
(Opcional)
O nome da função definida pelo usuário (UDF) do JavaScript que você quer usar.
Por exemplo, se o código de função do JavaScript for
myTransform(inJson) { /*...do stuff...*/ }, o nome da função será
myTransform. Para amostras de UDFs do JavaScript, consulte os
exemplos de UDF.
Como executar o modelo do Datastream para o BigQuery
Console
Acesse a página Criar job usando um modelo do Dataflow.
PROJECT_ID:
o ID do projeto do Cloud em que você quer executar o job do Dataflow
JOB_NAME:
um nome de job de sua escolha
REGION_NAME:
o endpoint regional em que você quer
implantar o job do Dataflow, por exemplo, us-central1
VERSION:
the version of the template that you want to use
You can use the following values:
latest to use the latest version of the template, which is available in the
non-dated parent folder in the bucket—
gs://dataflow-templates/latest/
the version name, like 2021-09-20-00_RC00, to use a specific version of the
template, which can be found nested in the respective dated parent folder in the bucket—
gs://dataflow-templates/
GCS_FILE_PATH: o caminho do Cloud Storage para os dados do Datastream. Exemplo: gs://bucket/path/to/data/
GCS_SUBSCRIPTION_NAME: a assinatura do Pub/Sub para ler os arquivos alterados. Por exemplo, projects/my-project-id/subscriptions/my-subscription-id.
BIGQUERY_DATASET: nome do conjunto de dados do BigQuery.
BIGQUERY_TABLE: o modelo da tabela do BigQuery. Por exemplo, {_metadata_schema}_{_metadata_table}_log
API
Para executar o modelo usando a API REST, envie uma solicitação HTTP POST. Para mais informações sobre a
API e os respectivos escopos de autorização, consulte
projects.templates.launch.
PROJECT_ID:
o ID do projeto do Cloud em que você quer executar o job do Dataflow
JOB_NAME:
um nome de job de sua escolha
LOCATION:
o endpoint regional em que você quer
implantar o job do Dataflow, por exemplo, us-central1
VERSION:
the version of the template that you want to use
You can use the following values:
latest to use the latest version of the template, which is available in the
non-dated parent folder in the bucket—
gs://dataflow-templates/latest/
the version name, like 2021-09-20-00_RC00, to use a specific version of the
template, which can be found nested in the respective dated parent folder in the bucket—
gs://dataflow-templates/
GCS_FILE_PATH: o caminho do Cloud Storage para os dados do Datastream. Exemplo: gs://bucket/path/to/data/
GCS_SUBSCRIPTION_NAME: a assinatura do Pub/Sub para ler os arquivos alterados. Por exemplo, projects/my-project-id/subscriptions/my-subscription-id.
BIGQUERY_DATASET: nome do conjunto de dados do BigQuery.
BIGQUERY_TABLE: o modelo da tabela do BigQuery. Por exemplo, {_metadata_schema}_{_metadata_table}_log
Datastream para MySQL ou PostgreSQL (Stream)
O modelo Datastream para SQL é um pipeline de streaming que lê
dados do Datastream
e os replica em qualquer banco de dados MySQL ou PostgreSQL. O modelo lê dados do Cloud Storage
usando notificações do Pub/Sub e replica esses dados em tabelas de réplica do PostgreSQL.
O modelo não é compatível com a linguagem de definição de dados (DDL) e espera que todas as tabelas já existam no banco de dados.
A replicação usa transformações com estado do Dataflow para filtrar dados desatualizados e
garantir consistência nos dados fora de ordem. Por exemplo, se uma versão mais recente de uma linha
já tiver passado, uma versão que chega atrasada dessa linha será ignorada.
A linguagem de manipulação de dados (DML) executada é uma melhor tentativa de replicar perfeitamente
os dados de origem e de destino. As instruções DML executadas seguem as seguintes regras:
Se houver uma chave primária, as operações de inserção e atualização usarão sintaxe de mesclagem
(isto é, INSERT INTO table VALUES (...) ON CONFLICT (...) DO UPDATE.
Se houver chaves primárias, as exclusões serão replicadas como uma DML de exclusão.
Se não houver uma chave primária, as operações de inserção e atualização serão inseridas na tabela.
Se não houver chaves primárias, as exclusões serão ignoradas.
Se você estiver usando os utilitários Oracle para Postgres, adicione ROWID no
PostgreSQL como a chave primária quando não houver nenhuma.
Os requisitos para esse pipeline são:
Um stream do Datastream que está pronto ou já está replicando dados.
PROJECT_ID:
o ID do projeto do Cloud em que você quer executar o job do Dataflow
JOB_NAME:
um nome de job de sua escolha
REGION_NAME:
o endpoint regional em que você quer
implantar o job do Dataflow, por exemplo, us-central1
VERSION:
the version of the template that you want to use
You can use the following values:
latest to use the latest version of the template, which is available in the
non-dated parent folder in the bucket—
gs://dataflow-templates/latest/
the version name, like 2021-09-20-00_RC00, to use a specific version of the
template, which can be found nested in the respective dated parent folder in the bucket—
gs://dataflow-templates/
GCS_FILE_PATH: o caminho do Cloud Storage para os dados do Datastream. Exemplo: gs://bucket/path/to/data/
GCS_SUBSCRIPTION_NAME: a assinatura do Pub/Sub para ler os arquivos alterados. Por exemplo, projects/my-project-id/subscriptions/my-subscription-id.
DATABASE_HOST: o IP do host do SQL.
DATABASE_USER: seu usuário do SQL.
DATABASE_PASSWORD: sua senha do SQL.
API
Para executar o modelo usando a API REST, envie uma solicitação HTTP POST. Para mais informações sobre a
API e os respectivos escopos de autorização, consulte
projects.templates.launch.
PROJECT_ID:
o ID do projeto do Cloud em que você quer executar o job do Dataflow
JOB_NAME:
um nome de job de sua escolha
LOCATION:
o endpoint regional em que você quer
implantar o job do Dataflow, por exemplo, us-central1
VERSION:
the version of the template that you want to use
You can use the following values:
latest to use the latest version of the template, which is available in the
non-dated parent folder in the bucket—
gs://dataflow-templates/latest/
the version name, like 2021-09-20-00_RC00, to use a specific version of the
template, which can be found nested in the respective dated parent folder in the bucket—
gs://dataflow-templates/
GCS_FILE_PATH: o caminho do Cloud Storage para os dados do Datastream. Exemplo: gs://bucket/path/to/data/
GCS_SUBSCRIPTION_NAME: a assinatura do Pub/Sub para ler os arquivos alterados. Por exemplo, projects/my-project-id/subscriptions/my-subscription-id.
DATABASE_HOST: o IP do host do SQL.
DATABASE_USER: seu usuário do SQL.
DATABASE_PASSWORD: sua senha do SQL.
Pub/Sub para Java Database Connectivity (JDBC)
O modelo do Pub/Sub para Java Database Connectivity (JDBC) é um pipeline de streaming
que ingere dados de uma assinatura preexistente do Cloud Pub/Sub como strings JSON e grava os registros resultantes no
JDBC.
Requisitos para este pipeline:
A inscrição do Cloud Pub/Sub precisa existir antes da execução do pipeline.
A origem JDBC precisa existir antes da execução do pipeline.
O tópico de mensagens inativas de saída do Cloud Pub/Sub precisa existir antes de o pipeline ser executado.
Parâmetros do modelo
Parâmetro
Descrição
driverClassName
O nome da classe do driver do JDBC. Por exemplo, com.mysql.jdbc.Driver.
connectionUrl
A string do URL de conexão do JDBC. Por exemplo, jdbc:mysql://some-host:3306/sampledb Pode ser transmitida como uma string codificada em Base64 e depois criptografada com uma chave do Cloud KMS.
driverJars
Caminhos do Cloud Storage separados por vírgulas para drivers JDBC. Por exemplo, gs://your-bucket/driver_jar1.jar,gs://your-bucket/driver_jar2.jar
username
(Opcional) O nome do usuário a ser usado para a conexão JDBC. Pode ser transmitida como uma string criptografada em Base64 com uma chave do Cloud KMS.
password
(Opcional) Senha a ser usada para a conexão JDBC. Pode ser transmitida como uma string criptografada em Base64 com uma chave do Cloud KMS.
connectionProperties
(Opcional) String de propriedades a ser usada para a conexão JDBC. O formato da string precisa ser [propertyName=property;]*. Por exemplo, unicode=true;characterEncoding=UTF-8.
statement
Instrução a ser executada no banco de dados. A instrução precisa especificar os nomes das colunas da tabela em qualquer ordem. Somente os valores dos nomes das colunas especificadas são lidos no JSON e adicionados à instrução. Por exemplo, INSERT INTO tableName (column1, column2) VALUES (?,?).
inputSubscription
O tópico de entrada do Cloud Pub/Sub que será lido, no formato de projects/<project>/subscriptions/<subscription>.
outputDeadletterTopic
O tópico do Pub/Sub para encaminhar mensagens não entregues. Por exemplo, projects/<project-id>/topics/<topic-name>.
KMSEncryptionKey
(Opcional) A chave de criptografia do Cloud KMS para descriptografar o nome de usuário, senha e string de conexão. Se a chave do Cloud KMS for transmitida, o nome de usuário, senha e string de conexão precisarão ser transmitidos criptografados.
extraFilesToStage
Caminhos do Cloud Storage separados ou vírgulas do Secret Manager para que os arquivos sejam organizados no worker. Esses arquivos serão salvos no diretório /extra_files de cada worker. Por exemplo, gs://<my-bucket>/file.txt,projects/<project-id>/secrets/<secret-id>/versions/<version-id>.
Como executar o modelo do Pub/Sub para Java Database Connectivity (JDBC)
Console
Acesse a página Criar job usando um modelo do Dataflow.
latest para usar a versão mais recente do modelo, disponível na pasta mãe
não datada no bucket:
gs://dataflow-templates/latest/
o nome da versão, como 2021-09-20-00_RC00, para usar uma versão específica do
modelo, que pode ser aninhada na respectiva pasta mãe datada no bucket:
gs://dataflow-templates
REGION_NAME:
o endpoint regional em que você quer
implantar o job do Dataflow, por exemplo, us-central1
DRIVER_CLASS_NAME: o nome da classe do driver
JDBC_CONNECTION_URL: o URL de conexão de JDBC
DRIVER_PATHS: os caminho(s) do Cloud Storage separado(s) por vírgula do(s) driver(s) JDBC
CONNECTION_USERNAME: o nome de usuário da conexão JDBC.
CONNECTION_PASSWORD: a senha de conexão JDBC
CONNECTION_PROPERTIES: as propriedades da conexão JDBC, se necessário
SQL_STATEMENT: a instrução SQL a ser executada no banco de dados
INPUT_SUBSCRIPTION: a assinatura de entrada do Pub/Sub da qual ler
OUTPUT_DEADLETTER_TOPIC: o tópico do Pub/Sub para encaminhar mensagens não entregues
KMS_ENCRYPTION_KEY: a chave de criptografia do Cloud KMS
API
Para executar o modelo usando a API REST, envie uma solicitação HTTP POST. Para mais informações sobre a
API e os respectivos escopos de autorização, consulte
projects.templates.launch.
PROJECT_ID:
o ID do projeto do Cloud em que você quer executar o job do Dataflow
JOB_NAME:
um nome de job de sua escolha
VERSION:
a versão do modelo que você quer usar
Use estes valores:
latest para usar a versão mais recente do modelo, disponível na pasta mãe
não datada no bucket:
gs://dataflow-templates/latest/
o nome da versão, como 2021-09-20-00_RC00, para usar uma versão específica do
modelo, que pode ser aninhada na respectiva pasta mãe datada no bucket:
gs://dataflow-templates
LOCATION:
o endpoint regional em que você quer
implantar o job do Dataflow, por exemplo, us-central1
DRIVER_CLASS_NAME: o nome da classe do driver
JDBC_CONNECTION_URL: o URL de conexão de JDBC
DRIVER_PATHS: os caminho(s) do Cloud Storage separado(s) por vírgula do(s) driver(s) JDBC
CONNECTION_USERNAME: o nome de usuário da conexão JDBC.
CONNECTION_PASSWORD: a senha de conexão JDBC
CONNECTION_PROPERTIES: as propriedades da conexão JDBC, se necessário
SQL_STATEMENT: a instrução SQL a ser executada no banco de dados
INPUT_SUBSCRIPTION: a assinatura de entrada do Pub/Sub da qual ler
OUTPUT_DEADLETTER_TOPIC: o tópico do Pub/Sub para encaminhar mensagens não entregues
KMS_ENCRYPTION_KEY: a chave de criptografia do Cloud KMS
/*
* Copyright (C) 2021 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.cloud.teleport.v2.templates;
import static com.google.cloud.teleport.v2.utils.KMSUtils.maybeDecrypt;
import com.google.cloud.teleport.metadata.Template;
import com.google.cloud.teleport.metadata.TemplateCategory;
import com.google.cloud.teleport.v2.coders.FailsafeElementCoder;
import com.google.cloud.teleport.v2.common.UncaughtExceptionLogger;
import com.google.cloud.teleport.v2.io.DynamicJdbcIO;
import com.google.cloud.teleport.v2.options.PubsubToJdbcOptions;
import com.google.cloud.teleport.v2.transforms.ErrorConverters;
import com.google.cloud.teleport.v2.utils.JsonStringToQueryMapper;
import com.google.cloud.teleport.v2.values.FailsafeElement;
import com.google.common.base.Splitter;
import java.util.List;
import org.apache.beam.sdk.Pipeline;
import org.apache.beam.sdk.PipelineResult;
import org.apache.beam.sdk.coders.StringUtf8Coder;
import org.apache.beam.sdk.io.gcp.pubsub.PubsubIO;
import org.apache.beam.sdk.options.PipelineOptionsFactory;
import org.apache.beam.sdk.values.PCollection;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link PubsubToJdbc} streaming pipeline reads data from Google Cloud PubSub and publishes to
* JDBC. <br>
*/
@Template(
name = "Pubsub_to_Jdbc",
category = TemplateCategory.STREAMING,
displayName = "Pub/Sub to JDBC",
description =
"A streaming pipeline which ingests data in the form of json strings from Pub/Sub"
+ " subscription and writes to a JDBC table. JDBC connection string, user name and"
+ " password can be passed in directly as plaintext or encrypted using the Google Cloud"
+ " KMS API. If the parameter KMSEncryptionKey is specified, connectionUrl, username,"
+ " and password should be all in encrypted format. A sample curl command for the KMS"
+ " API encrypt endpoint: curl -s -X POST"
+ " \"https://cloudkms.googleapis.com/v1/projects/your-project/locations/your-path/keyRings/your-keyring/cryptoKeys/your-key:encrypt\""
+ " -d \"{\\\"plaintext\\\":\\\"PasteBase64EncodedString\\\"}\" -H \"Authorization:"
+ " Bearer $(gcloud auth application-default print-access-token)\" -H \"Content-Type:"
+ " application/json\"",
optionsClass = PubsubToJdbcOptions.class,
flexContainerName = "pubsub-to-jdbc",
contactInformation = "https://cloud.google.com/support")
public class PubsubToJdbc {
/* Logger for class.*/
private static final Logger LOG = LoggerFactory.getLogger(PubsubToJdbc.class);
/** String/String Coder for FailsafeElement. */
public static final FailsafeElementCoder<String, String> FAILSAFE_ELEMENT_CODER =
FailsafeElementCoder.of(StringUtf8Coder.of(), StringUtf8Coder.of());
/**
* Main entry point for pipeline execution.
*
* @param args Command line arguments to the pipeline.
*/
public static void main(String[] args) {
UncaughtExceptionLogger.register();
PubsubToJdbcOptions options =
PipelineOptionsFactory.fromArgs(args).withValidation().as(PubsubToJdbcOptions.class);
run(options);
}
/**
* Runs a pipeline which reads message from Pub/Sub and writes to JDBC.
*
* @param options The execution options.
* @return The pipeline result.
*/
public static PipelineResult run(PubsubToJdbcOptions options) {
// Create the pipeline
Pipeline pipeline = Pipeline.create(options);
LOG.info("Starting Pubsub-to-Jdbc Pipeline.");
/*
* Steps:
* 1) Read data from a Pub/Sub subscription
* 2) Write to Jdbc Table
* 3) Write errors to deadletter topic
*/
PCollection<String> pubsubData =
pipeline.apply(
"readFromPubSubSubscription",
PubsubIO.readStrings().fromSubscription(options.getInputSubscription()));
DynamicJdbcIO.DynamicDataSourceConfiguration dataSourceConfiguration =
DynamicJdbcIO.DynamicDataSourceConfiguration.create(
options.getDriverClassName(),
maybeDecrypt(options.getConnectionUrl(), options.getKMSEncryptionKey()))
.withDriverJars(options.getDriverJars());
if (options.getUsername() != null) {
dataSourceConfiguration =
dataSourceConfiguration.withUsername(
maybeDecrypt(options.getUsername(), options.getKMSEncryptionKey()));
}
if (options.getPassword() != null) {
dataSourceConfiguration =
dataSourceConfiguration.withPassword(
maybeDecrypt(options.getPassword(), options.getKMSEncryptionKey()));
}
if (options.getConnectionProperties() != null) {
dataSourceConfiguration =
dataSourceConfiguration.withConnectionProperties(options.getConnectionProperties());
}
PCollection<FailsafeElement<String, String>> errors =
pubsubData
.apply(
"writeToJdbc",
DynamicJdbcIO.<String>write()
.withDataSourceConfiguration(dataSourceConfiguration)
.withStatement(options.getStatement())
.withPreparedStatementSetter(
new JsonStringToQueryMapper(getKeyOrder(options.getStatement()))))
.setCoder(FAILSAFE_ELEMENT_CODER);
errors.apply(
"WriteFailedRecords",
ErrorConverters.WriteStringMessageErrorsToPubSub.newBuilder()
.setErrorRecordsTopic(options.getOutputDeadletterTopic())
.build());
return pipeline.run();
}
private static List<String> getKeyOrder(String statement) {
int startIndex = statement.indexOf("(");
int endIndex = statement.indexOf(")");
String data = statement.substring(startIndex + 1, endIndex);
return Splitter.on(',').splitToList(data);
}
}
Fluxos de alterações do Cloud Spanner para o Cloud Storage
Os fluxos de alterações do Cloud Spanner para o modelo do Cloud Storage são um pipeline de
streaming que transmite os registros de alteração de dados do Spanner e os grava em um bucket do Cloud Storage usando
o Dataflow Runner V2.
O pipeline agrupa os registros de stream de alterações do Spanner em janelas com base no carimbo de data/hora,
e cada janela representa uma duração de tempo cujo tamanho pode ser configurado com esse modelo.
Todos os registros com carimbos de data/hora pertencentes à janela têm a garantia de estarem na janela.
Não pode haver chegadas atrasadas. Também é possível definir vários fragmentos de saída. O pipeline cria um arquivo de saída do Cloud Storage por janela por fragmento. Em um arquivo de saída, os registros são desordenados. Os arquivos de saída podem ser gravados no formato JSON ou AVRO,
dependendo da configuração do usuário.
Observe que é possível minimizar a latência da rede e os custos de transporte dela executando o job do Dataflow na mesma região da sua instância do Cloud Spanner ou do bucket do Cloud Storage. Se você usar fontes, coletores, locais de arquivos de preparo ou de arquivos temporários localizados fora da região do job, seus dados poderão ser enviados entre regiões. Saiba mais sobre os endpoints regionais do Dataflow.
A instância do Cloud Spanner precisa existir antes da execução do pipeline.
O banco de dados do Cloud Spanner precisa existir antes da execução do pipeline.
A instância de metadados do Cloud Spanner precisa existir antes da execução do pipeline.
O banco de dados de metadados do Cloud Spanner precisa existir antes da execução do pipeline.
O fluxo de alterações do Cloud Spanner precisa ser criado antes da execução do pipeline.
O bucket de saída do Cloud Storage precisa existir antes da execução do pipeline.
Parâmetros do modelo
Parâmetro
Descrição
spannerInstanceId
O ID da instância do Cloud Spanner de onde os dados dos fluxos de alterações são lidos.
spannerDatabase
O banco de dados do Cloud Spanner de onde os dados dos fluxos de alterações são lidos.
spannerMetadataInstanceId
O ID da instância do Cloud Spanner a ser usado para a tabela de metadados do conector dos fluxos de alterações.
spannerMetadataDatabase
O banco de dados do Cloud Spanner a ser usado para a tabela de metadados do conector dos fluxos de alterações.
spannerChangeStreamName
O nome do fluxo de alterações a ser lido pelo Cloud Spanner.
gcsOutputDirectory
O local do arquivo dos fluxos de alterações na saída do Cloud Storage no formato: 'gs://${BUCKET}/${ROOT_PATH}/'.
outputFilenamePrefix
(Opcional) O prefixo do nome dos arquivos a serem gravados. O prefixo do arquivo padrão é definido como "saída".
spannerProjectId
(Opcional) Projeto do qual os fluxos de alterações serão lidos. Este é também o projeto em que a tabela de metadados do conector dos fluxos de alterações é criada. O padrão para esse parâmetro é o projeto em que o pipeline do Dataflow está sendo executado.
startTimestamp
(Opcional) O DateTime inicial, inclusive, para ler os fluxos de alterações. Ex-2021-10-12T07:20:50.52Z. O padrão é o carimbo de data/hora quando o pipeline é iniciado, ou seja, o horário atual.
endTimestamp
(Opcional) A terminação DateTime, inclusive, para usar em fluxos de alterações de leitura. Ex-2021-10-12T07:20:50.52Z. O padrão é um tempo infinito no futuro.
outputFileFormat
(Opcional) O formato do arquivo de saída do Cloud Storage. Os formatos permitidos são TEXT e AVRO. O padrão é AVRO.
windowDuration
(Opcional) A duração da janela é o intervalo em que os dados são gravados no diretório
de saída. Configure a duração com base na capacidade de processamento do pipeline. Por exemplo, uma capacidade de processamento
mais alta pode exigir tamanhos de janela menores para que os dados se encaixem na memória. O padrão é
5 min, com um mínimo de 1 s. Os formatos permitidos são: [int]s para segundos (5 s, por exemplo); [int]m para
minutos (12 min, por exemplo); [int]h para horas (2h, por exemplo).
rpcPriority
(Opcional) A prioridade da solicitação para chamadas do Cloud Spanner. O valor precisa ser um destes: [HIGH,MEDIUM,LOW]. (Padrão: HIGH)
numShards
(Opcional) O número máximo de fragmentos de saída produzidos durante a gravação. O número padrão é 20. Um número maior de fragmentos significa maior capacidade de gravação no Cloud Storage, mas um custo de agregação de dados potencialmente maior entre os fragmentos ao processar os arquivos de saída do Cloud Storage
spannerMetadataTableName
(Opcional) O nome da tabela de metadados do conector dos Fluxos de alterações do Cloud Spanner a ser usado. Se não for fornecido, uma tabela de metadados dos fluxos de alterações do Cloud Spanner será criada automaticamente durante o fluxo de pipeline. É preciso informar esse parâmetro ao atualizar um pipeline. Caso contrário, não o forneça.
Executar os fluxos de alterações do Cloud Spanner para o modelo do Cloud Storage
Console
Acesse a página Criar job usando um modelo do Dataflow.
latest para usar a versão mais recente do modelo, disponível na pasta mãe
não datada no bucket:
gs://dataflow-templates/latest/
o nome da versão, como 2021-09-20-00_RC00, para usar uma versão específica do
modelo, que pode ser aninhada na respectiva pasta mãe datada no bucket:
gs://dataflow-templates
REGION_NAME:
o endpoint regional em que você quer
implantar o job do Dataflow, por exemplo, us-central1
SPANNER_INSTANCE_ID: ID da instância do Cloud Spanner
SPANNER_DATABASE: banco de dados do Cloud Spanner
SPANNER_METADATA_INSTANCE_ID: ID da instância de metadados do Cloud Spanner
SPANNER_METADATA_DATABASE: banco de dados de metadados do Cloud Spanner
SPANNER_CHANGE_STREAM: fluxo de alterações do Cloud Spanner
GCS_OUTPUT_DIRECTORY: local do arquivo da saída dos fluxos de alterações
API
Para executar o modelo usando a API REST, envie uma solicitação HTTP POST. Para mais informações sobre a
API e os respectivos escopos de autorização, consulte
projects.templates.launch.
PROJECT_ID:
o ID do projeto do Cloud em que você quer executar o job do Dataflow
JOB_NAME:
um nome de job de sua escolha
VERSION:
a versão do modelo que você quer usar
Use estes valores:
latest para usar a versão mais recente do modelo, disponível na pasta mãe
não datada no bucket:
gs://dataflow-templates/latest/
o nome da versão, como 2021-09-20-00_RC00, para usar uma versão específica do
modelo, que pode ser aninhada na respectiva pasta mãe datada no bucket:
gs://dataflow-templates
LOCATION:
o endpoint regional em que você quer
implantar o job do Dataflow, por exemplo, us-central1
SPANNER_INSTANCE_ID: ID da instância do Cloud Spanner
SPANNER_DATABASE: banco de dados do Cloud Spanner
SPANNER_METADATA_INSTANCE_ID: ID da instância de metadados do Cloud Spanner
SPANNER_METADATA_DATABASE: banco de dados de metadados do Cloud Spanner
SPANNER_CHANGE_STREAM: fluxo de alterações do Cloud Spanner
GCS_OUTPUT_DIRECTORY: local do arquivo da saída dos fluxos de alterações
/*
* Copyright (C) 2022 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.cloud.teleport.v2.templates;
import com.google.cloud.Timestamp;
import com.google.cloud.spanner.Options.RpcPriority;
import com.google.cloud.teleport.metadata.Template;
import com.google.cloud.teleport.metadata.TemplateCategory;
import com.google.cloud.teleport.v2.common.UncaughtExceptionLogger;
import com.google.cloud.teleport.v2.options.SpannerChangeStreamsToGcsOptions;
import com.google.cloud.teleport.v2.transforms.FileFormatFactorySpannerChangeStreams;
import com.google.cloud.teleport.v2.utils.DurationUtils;
import java.util.ArrayList;
import java.util.List;
import org.apache.beam.sdk.Pipeline;
import org.apache.beam.sdk.PipelineResult;
import org.apache.beam.sdk.io.gcp.spanner.SpannerConfig;
import org.apache.beam.sdk.io.gcp.spanner.SpannerIO;
import org.apache.beam.sdk.options.PipelineOptionsFactory;
import org.apache.beam.sdk.options.ValueProvider;
import org.apache.beam.sdk.transforms.windowing.FixedWindows;
import org.apache.beam.sdk.transforms.windowing.Window;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link SpannerChangeStreamsToGcs} pipeline streams change stream record(s) and stores to
* Google Cloud Storage bucket in user specified format. The sink data can be stored in a Text or
* Avro file format.
*/
@Template(
name = "Spanner_Change_Streams_to_Google_Cloud_Storage",
category = TemplateCategory.STREAMING,
displayName = "Cloud Spanner change streams to Cloud Storage",
description =
"Streaming pipeline. Streams Spanner change stream data records and writes them into a"
+ " Cloud Storage bucket using Dataflow Runner V2.",
optionsClass = SpannerChangeStreamsToGcsOptions.class,
flexContainerName = "spanner-changestreams-to-gcs",
contactInformation = "https://cloud.google.com/support")
public class SpannerChangeStreamsToGcs {
private static final Logger LOG = LoggerFactory.getLogger(SpannerChangeStreamsToGcs.class);
private static final String USE_RUNNER_V2_EXPERIMENT = "use_runner_v2";
public static void main(String[] args) {
UncaughtExceptionLogger.register();
LOG.info("Starting Input Files to GCS");
SpannerChangeStreamsToGcsOptions options =
PipelineOptionsFactory.fromArgs(args).as(SpannerChangeStreamsToGcsOptions.class);
run(options);
}
private static String getProjectId(SpannerChangeStreamsToGcsOptions options) {
return options.getSpannerProjectId().isEmpty()
? options.getProject()
: options.getSpannerProjectId();
}
public static PipelineResult run(SpannerChangeStreamsToGcsOptions options) {
LOG.info("Requested File Format is " + options.getOutputFileFormat());
options.setStreaming(true);
options.setEnableStreamingEngine(true);
final Pipeline pipeline = Pipeline.create(options);
// Get the Spanner project, instance, database, and change stream parameters.
String projectId = getProjectId(options);
String instanceId = options.getSpannerInstanceId();
String databaseId = options.getSpannerDatabase();
String metadataInstanceId = options.getSpannerMetadataInstanceId();
String metadataDatabaseId = options.getSpannerMetadataDatabase();
String changeStreamName = options.getSpannerChangeStreamName();
// Retrieve and parse the start / end timestamps.
Timestamp startTimestamp =
options.getStartTimestamp().isEmpty()
? Timestamp.now()
: Timestamp.parseTimestamp(options.getStartTimestamp());
Timestamp endTimestamp =
options.getEndTimestamp().isEmpty()
? Timestamp.MAX_VALUE
: Timestamp.parseTimestamp(options.getEndTimestamp());
// Add use_runner_v2 to the experiments option, since Change Streams connector is only supported
// on Dataflow runner v2.
List<String> experiments = options.getExperiments();
if (experiments == null) {
experiments = new ArrayList<>();
}
if (!experiments.contains(USE_RUNNER_V2_EXPERIMENT)) {
experiments.add(USE_RUNNER_V2_EXPERIMENT);
}
options.setExperiments(experiments);
String metadataTableName =
options.getSpannerMetadataTableName() == null
? null
: options.getSpannerMetadataTableName();
final RpcPriority rpcPriority = options.getRpcPriority();
pipeline
.apply(
SpannerIO.readChangeStream()
.withSpannerConfig(
SpannerConfig.create()
.withHost(ValueProvider.StaticValueProvider.of(options.getSpannerHost()))
.withProjectId(projectId)
.withInstanceId(instanceId)
.withDatabaseId(databaseId))
.withMetadataInstance(metadataInstanceId)
.withMetadataDatabase(metadataDatabaseId)
.withChangeStreamName(changeStreamName)
.withInclusiveStartAt(startTimestamp)
.withInclusiveEndAt(endTimestamp)
.withRpcPriority(rpcPriority)
.withMetadataTable(metadataTableName))
.apply(
"Creating " + options.getWindowDuration() + " Window",
Window.into(FixedWindows.of(DurationUtils.parseDuration(options.getWindowDuration()))))
.apply(
"Write To GCS",
FileFormatFactorySpannerChangeStreams.newBuilder().setOptions(options).build());
return pipeline.run();
}
}
Fluxos de alterações do Cloud Spanner para o BigQuery
O modelo de fluxos de alterações do Cloud Spanner para o BigQuery é um pipeline de streaming que transmite os registros de alteração de dados do Cloud Spanner e os grava em tabelas do BigQuery usando o Dataflow Runner V2.
Se as tabelas do BigQuery necessárias não existirem, o pipeline as criará. Caso contrário, as tabelas atuais do BigQuery serão usadas. O esquema das tabelas atuais do BigQuery precisa
conter as colunas rastreadas correspondentes das tabelas do Cloud Spanner e as colunas de metadados
adicionais (consulte a descrição dos campos de metadados na lista a seguir) que não são
ignoradas explicitamente pela opção "ignoreFields". Cada nova linha do BigQuery inclui todas as colunas
monitoradas pelo fluxo de alterações da linha correspondente na tabela do Cloud Spanner no
carimbo de data/hora do registro de alterações.
Todas as colunas monitoradas do fluxo de alterações são incluídas em cada linha da tabela do BigQuery, independentemente de serem modificadas por uma transação do Cloud Spanner. As colunas não monitoradas não são incluídas na linha do BigQuery. Qualquer alteração do Cloud Spanner inferior à marca d'água do Dataflow é aplicada com sucesso às tabelas do BigQuery ou armazenada na fila de mensagens inativas para nova tentativa. As linhas do BigQuery são inseridas fora de ordem em comparação com a ordem original do carimbo de data/hora de confirmação do Cloud Spanner.
Os seguintes campos de metadados são adicionados às tabelas do BigQuery:
_metadata_spanner_mod_type: extraído do registro de alterações dos dados do fluxo de alterações.
_metadata_spanner_table_name: o nome da tabela do Cloud Spanner. Observe que este não é o nome da tabela de metadados do conector.
_metadata_spanner_commit_timestamp: extraído do registro de alterações dos dados do fluxo de alterações.
_metadata_spanner_server_transaction_id: extraído do registro de alterações dos dados do fluxo de alterações.
_metadata_spanner_record_sequence: extraído do registro de alterações dos dados do fluxo de alterações.
_metadata_spanner_is_last_record_in_transaction_in_partition: extraído do registro de alterações dos dados do fluxo de alterações.
_metadata_spanner_number_of_records_in_transaction: extraídos do registro de alterações dos dados do fluxo de alterações.
_metadata_spanner_number_of_partitions_in_transaction: extraído do registro de alterações dos dados do fluxo de alterações.
_metadata_big_query_commit_timestamp: o carimbo de data/hora de confirmação de quando a linha é inserida no BigQuery.
Observação:
Este modelo não propaga alterações de esquema do Cloud Spanner para o BigQuery.
Como uma alteração de esquema no Cloud Spanner provavelmente vai interromper o pipeline,
talvez ele precise ser recriado depois que o esquema for alterado.
Para os tipos de captura de valor OLD_AND_NEW_VALUES e NEW_VALUES, quando o registro de alteração de dados
tiver uma alteração UPDATE, o modelo precisará fazer uma leitura desatualizada para o Cloud Spanner no carimbo de data/hora de confirmação do registro de alteração de dados.
Isso serve para recuperar as colunas inalteradas, mas monitoradas. Configure
o banco de dados "version_retention_period" corretamente para a leitura desatualizada. Para
o tipo de captura de valor NEW_ROW, o modelo é mais eficiente, porque o registro de alteração de dados captura
a nova linha completa, incluindo colunas que não são atualizadas em UPDATEs e o modelo não
precisa fazer uma leitura desatualizada.
Você pode minimizar a latência da rede e os custos de transporte dela executando o job do Dataflow
na mesma região de sua instância do Cloud Spanner ou das tabelas do BigQuery. Se você usar fontes, coletores, locais de arquivos de preparo ou de arquivos temporários localizados fora da região do job, seus dados poderão ser enviados entre regiões. Saiba mais sobre os
endpoints regionais do Dataflow.
Esse modelo é compatível com todos os tipos de dados válidos do Cloud Spanner. No entanto, se o tipo
do BigQuery for mais preciso que o do Cloud Spanner, poderá ocorrer perda de precisão durante a
transformação. Especificamente:
Para o tipo JSON do Cloud Spanner, a ordem dos membros de um objeto é
ordenada lexicograficamente, mas não há essa garantia para o tipo JSON do BigQuery.
O Cloud Spanner é compatível com o tipo TIMESTAMP de nanossegundos, e o BigQuery é compatível apenas com o tipo TIMESTAMP de microssegundos.
A instância do Cloud Spanner precisa existir antes da execução do pipeline.
O banco de dados do Cloud Spanner precisa existir antes da execução do pipeline.
A instância de metadados do Cloud Spanner precisa existir antes da execução do pipeline.
O banco de dados de metadados do Cloud Spanner precisa existir antes da execução do pipeline.
O fluxo de alterações do Cloud Spanner precisa ser criado antes da execução do pipeline.
O conjunto de dados do BigQuery precisa existir antes da execução do pipeline.
Parâmetros do modelo
Parâmetro
Descrição
spannerInstanceId
A instância do Cloud Spanner em que os fluxos de alterações serão lidos.
spannerDatabase
O banco de dados do Cloud Spanner de onde os fluxos de alterações serão lidos.
spannerMetadataInstanceId
A instância do Cloud Spanner a ser usada para a tabela de metadados do conector dos fluxos de alterações.
spannerMetadataDatabase
O banco de dados do Cloud Spanner a ser usado para a tabela de metadados do conector dos fluxos de alterações.
spannerChangeStreamName
O nome do fluxo de alterações a ser lido pelo Cloud Spanner.
bigQueryDataSet
O conjunto de dados do BigQuery para a saída de fluxos de alterações.
spannerProjectId
(Opcional) Projeto do qual os fluxos de alterações serão lidos. Este é também o projeto em que a tabela de metadados do conector dos fluxos de alterações é criada. O padrão para esse parâmetro é o projeto em que o pipeline do Dataflow está sendo executado.
spannerMetadataTableName
(Opcional) O nome da tabela de metadados do conector dos Fluxos de alterações do Cloud Spanner a ser usado. Se não for informado, uma tabela de metadados do conector de streams de alteração do Cloud Spanner será criada automaticamente durante o fluxo do pipeline. É preciso informar esse parâmetro ao atualizar um pipeline. Caso contrário, não o forneça.
rpcPriority
(Opcional) A prioridade da solicitação para chamadas do Cloud Spanner. O valor precisa ser um destes: [HIGH,MEDIUM,LOW]. (Padrão: HIGH)
startTimestamp
(Opcional) O DateTime inicial, inclusive, para ler os fluxos de alterações. Ex-2021-10-12T07:20:50.52Z. O padrão é o carimbo de data/hora em que o pipeline é iniciado, ou seja, o horário atual.
endTimestamp
(Opcional) A terminação DateTime, inclusive, para usar em fluxos de alterações de leitura. Ex-2021-10-12T07:20:50.52Z. O padrão é um tempo infinito no futuro.
bigQueryProjectId
(Opcional) O projeto do BigQuery. O padrão é o projeto do job do Dataflow.
bigQueryChangelogTableNameTemplate
(Opcional) O modelo para o nome das tabelas de registro de alterações do BigQuery. Vai para o padrão {_metadata_spanner_table_name}_changelog.
deadLetterQueueDirectory
(Opcional) O caminho do arquivo para armazenar todos os registros não processados com o motivo da falha de processamento. O padrão é um diretório no local temporário do job do Dataflow. O valor padrão é suficiente na maioria das condições.
dlqRetryMinutes
(Opcional) O número de minutos entre novas tentativas de fila de mensagens inativas (DLQ) O valor padrão é 10.
ignoreFields
(Opcional) Lista separada por vírgulas de campos (diferencia maiúsculas de minúsculas) que devem ser ignorados. Podem ser campos de tabelas monitoradas ou campos de metadados adicionados pelo pipeline. Os campos ignorados não serão inseridos no BigQuery.
Executar os fluxos de alterações do Cloud Spanner para o modelo do BigQuery
Console
Acesse a página Criar job usando um modelo do Dataflow.
latest para usar a versão mais recente do modelo, disponível na pasta mãe
não datada no bucket:
gs://dataflow-templates/latest/
o nome da versão, como 2021-09-20-00_RC00, para usar uma versão específica do
modelo, que pode ser aninhada na respectiva pasta mãe datada no bucket:
gs://dataflow-templates
REGION_NAME:
o endpoint regional em que você quer
implantar o job do Dataflow, por exemplo, us-central1
SPANNER_INSTANCE_ID: ID da instância do Cloud Spanner
SPANNER_DATABASE: banco de dados do Cloud Spanner
SPANNER_METADATA_INSTANCE_ID: ID da instância de metadados do Cloud Spanner
SPANNER_METADATA_DATABASE: banco de dados de metadados do Cloud Spanner
SPANNER_CHANGE_STREAM: fluxo de alterações do Cloud Spanner
BIGQUERY_DATASET: o conjunto de dados do BigQuery usado para a saída de fluxos de alterações
API
Para executar o modelo usando a API REST, envie uma solicitação HTTP POST. Para mais informações sobre a
API e os respectivos escopos de autorização, consulte
projects.templates.launch.
PROJECT_ID:
o ID do projeto do Cloud em que você quer executar o job do Dataflow
JOB_NAME:
um nome de job de sua escolha
VERSION:
a versão do modelo que você quer usar
Use estes valores:
latest para usar a versão mais recente do modelo, disponível na pasta mãe
não datada no bucket:
gs://dataflow-templates/latest/
o nome da versão, como 2021-09-20-00_RC00, para usar uma versão específica do
modelo, que pode ser aninhada na respectiva pasta mãe datada no bucket:
gs://dataflow-templates
LOCATION:
o endpoint regional em que você quer
implantar o job do Dataflow, por exemplo, us-central1
SPANNER_INSTANCE_ID: ID da instância do Cloud Spanner
SPANNER_DATABASE: banco de dados do Cloud Spanner
SPANNER_METADATA_INSTANCE_ID: ID da instância de metadados do Cloud Spanner
SPANNER_METADATA_DATABASE: banco de dados de metadados do Cloud Spanner
SPANNER_CHANGE_STREAM: fluxo de alterações do Cloud Spanner
BIGQUERY_DATASET: o conjunto de dados do BigQuery usado para a saída de fluxos de alterações
/*
* Copyright (C) 2022 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.cloud.teleport.v2.templates.spannerchangestreamstobigquery;
import com.google.api.services.bigquery.model.TableRow;
import com.google.cloud.Timestamp;
import com.google.cloud.teleport.metadata.Template;
import com.google.cloud.teleport.metadata.TemplateCategory;
import com.google.cloud.teleport.v2.cdc.dlq.DeadLetterQueueManager;
import com.google.cloud.teleport.v2.cdc.dlq.StringDeadLetterQueueSanitizer;
import com.google.cloud.teleport.v2.coders.FailsafeElementCoder;
import com.google.cloud.teleport.v2.common.UncaughtExceptionLogger;
import com.google.cloud.teleport.v2.options.SpannerChangeStreamsToBigQueryOptions;
import com.google.cloud.teleport.v2.templates.spannerchangestreamstobigquery.model.Mod;
import com.google.cloud.teleport.v2.templates.spannerchangestreamstobigquery.schemautils.BigQueryUtils;
import com.google.cloud.teleport.v2.transforms.DLQWriteTransform;
import com.google.cloud.teleport.v2.utils.BigQueryIOUtils;
import com.google.cloud.teleport.v2.values.FailsafeElement;
import com.google.common.collect.ImmutableSet;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import org.apache.beam.runners.dataflow.options.DataflowPipelineOptions;
import org.apache.beam.sdk.Pipeline;
import org.apache.beam.sdk.PipelineResult;
import org.apache.beam.sdk.coders.StringUtf8Coder;
import org.apache.beam.sdk.io.gcp.bigquery.BigQueryIO;
import org.apache.beam.sdk.io.gcp.bigquery.BigQueryIO.Write;
import org.apache.beam.sdk.io.gcp.bigquery.BigQueryIO.Write.CreateDisposition;
import org.apache.beam.sdk.io.gcp.bigquery.InsertRetryPolicy;
import org.apache.beam.sdk.io.gcp.bigquery.WriteResult;
import org.apache.beam.sdk.io.gcp.spanner.SpannerConfig;
import org.apache.beam.sdk.io.gcp.spanner.SpannerIO;
import org.apache.beam.sdk.io.gcp.spanner.changestreams.model.DataChangeRecord;
import org.apache.beam.sdk.options.PipelineOptionsFactory;
import org.apache.beam.sdk.options.ValueProvider;
import org.apache.beam.sdk.transforms.DoFn;
import org.apache.beam.sdk.transforms.Flatten;
import org.apache.beam.sdk.transforms.MapElements;
import org.apache.beam.sdk.transforms.ParDo;
import org.apache.beam.sdk.transforms.Reshuffle;
import org.apache.beam.sdk.values.PCollection;
import org.apache.beam.sdk.values.PCollectionList;
import org.apache.beam.sdk.values.PCollectionTuple;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
// TODO(haikuo-google): Add integration test.
// TODO(haikuo-google): Add README.
// TODO(haikuo-google): Add stackdriver metrics.
// TODO(haikuo-google): Ideally side input should be used to store schema information and shared
// accrss DoFns, but since side input fix is not yet deployed at the moment, we read schema
// information in the beginning of the DoFn as a work around. We should use side input instead when
// it's available.
// TODO(haikuo-google): Test the case where tables or columns are added while the pipeline is
// running.
/**
* This pipeline ingests {@link DataChangeRecord} from Spanner change stream. The {@link
* DataChangeRecord} is then broken into {@link Mod}, which converted into {@link TableRow} and
* inserted into BigQuery table.
*/
@Template(
name = "Spanner_Change_Streams_to_BigQuery",
category = TemplateCategory.STREAMING,
displayName = "Cloud Spanner change streams to BigQuery",
description =
"Streaming pipeline. Streams Spanner data change records and writes them into BigQuery"
+ " using Dataflow Runner V2.",
optionsClass = SpannerChangeStreamsToBigQueryOptions.class,
flexContainerName = "spanner-changestreams-to-bigquery",
contactInformation = "https://cloud.google.com/support")
public final class SpannerChangeStreamsToBigQuery {
/** String/String Coder for {@link FailsafeElement}. */
public static final FailsafeElementCoder<String, String> FAILSAFE_ELEMENT_CODER =
FailsafeElementCoder.of(StringUtf8Coder.of(), StringUtf8Coder.of());
private static final Logger LOG = LoggerFactory.getLogger(SpannerChangeStreamsToBigQuery.class);
// Max number of deadletter queue retries.
private static final int DLQ_MAX_RETRIES = 5;
private static final String USE_RUNNER_V2_EXPERIMENT = "use_runner_v2";
/**
* Main entry point for executing the pipeline.
*
* @param args The command-line arguments to the pipeline.
*/
public static void main(String[] args) {
UncaughtExceptionLogger.register();
LOG.info("Starting to replicate change records from Spanner change streams to BigQuery");
SpannerChangeStreamsToBigQueryOptions options =
PipelineOptionsFactory.fromArgs(args)
.withValidation()
.as(SpannerChangeStreamsToBigQueryOptions.class);
run(options);
}
private static void validateOptions(SpannerChangeStreamsToBigQueryOptions options) {
if (options.getDlqRetryMinutes() <= 0) {
throw new IllegalArgumentException("dlqRetryMinutes must be positive.");
}
BigQueryIOUtils.validateBQStorageApiOptionsStreaming(options);
}
private static void setOptions(SpannerChangeStreamsToBigQueryOptions options) {
options.setStreaming(true);
options.setEnableStreamingEngine(true);
// Add use_runner_v2 to the experiments option, since change streams connector is only supported
// on Dataflow runner v2.
List<String> experiments = options.getExperiments();
if (experiments == null) {
experiments = new ArrayList<>();
}
if (!experiments.contains(USE_RUNNER_V2_EXPERIMENT)) {
experiments.add(USE_RUNNER_V2_EXPERIMENT);
}
options.setExperiments(experiments);
}
/**
* Runs the pipeline with the supplied options.
*
* @param options The execution parameters to the pipeline.
* @return The result of the pipeline execution.
*/
public static PipelineResult run(SpannerChangeStreamsToBigQueryOptions options) {
setOptions(options);
validateOptions(options);
/**
* Stages: 1) Read {@link DataChangeRecord} from change stream. 2) Create {@link
* FailsafeElement} of {@link Mod} JSON and merge from: - {@link DataChangeRecord}. - GCS Dead
* letter queue. 3) Convert {@link Mod} JSON into {@link TableRow} by reading from Spanner at
* commit timestamp. 4) Append {@link TableRow} to BigQuery. 5) Write Failures from 2), 3) and
* 4) to GCS dead letter queue.
*/
Pipeline pipeline = Pipeline.create(options);
DeadLetterQueueManager dlqManager = buildDlqManager(options);
String spannerProjectId = getSpannerProjectId(options);
String dlqDirectory = dlqManager.getRetryDlqDirectoryWithDateTime();
String tempDlqDirectory = dlqManager.getRetryDlqDirectory() + "tmp/";
// Retrieve and parse the startTimestamp and endTimestamp.
Timestamp startTimestamp =
options.getStartTimestamp().isEmpty()
? Timestamp.now()
: Timestamp.parseTimestamp(options.getStartTimestamp());
Timestamp endTimestamp =
options.getEndTimestamp().isEmpty()
? Timestamp.MAX_VALUE
: Timestamp.parseTimestamp(options.getEndTimestamp());
SpannerConfig spannerConfig =
SpannerConfig.create()
.withHost(ValueProvider.StaticValueProvider.of(options.getSpannerHost()))
.withProjectId(spannerProjectId)
.withInstanceId(options.getSpannerInstanceId())
.withDatabaseId(options.getSpannerDatabase())
.withRpcPriority(options.getRpcPriority());
SpannerIO.ReadChangeStream readChangeStream =
SpannerIO.readChangeStream()
.withSpannerConfig(spannerConfig)
.withMetadataInstance(options.getSpannerMetadataInstanceId())
.withMetadataDatabase(options.getSpannerMetadataDatabase())
.withChangeStreamName(options.getSpannerChangeStreamName())
.withInclusiveStartAt(startTimestamp)
.withInclusiveEndAt(endTimestamp)
.withRpcPriority(options.getRpcPriority());
String spannerMetadataTableName = options.getSpannerMetadataTableName();
if (spannerMetadataTableName != null) {
readChangeStream = readChangeStream.withMetadataTable(spannerMetadataTableName);
}
PCollection<DataChangeRecord> dataChangeRecord =
pipeline
.apply("Read from Spanner Change Streams", readChangeStream)
.apply("Reshuffle DataChangeRecord", Reshuffle.viaRandomKey());
PCollection<FailsafeElement<String, String>> sourceFailsafeModJson =
dataChangeRecord
.apply("DataChangeRecord To Mod JSON", ParDo.of(new DataChangeRecordToModJsonFn()))
.apply(
"Wrap Mod JSON In FailsafeElement",
ParDo.of(
new DoFn<String, FailsafeElement<String, String>>() {
@ProcessElement
public void process(
@Element String input,
OutputReceiver<FailsafeElement<String, String>> receiver) {
receiver.output(FailsafeElement.of(input, input));
}
}))
.setCoder(FAILSAFE_ELEMENT_CODER);
PCollectionTuple dlqModJson =
dlqManager.getReconsumerDataTransform(
pipeline.apply(dlqManager.dlqReconsumer(options.getDlqRetryMinutes())));
PCollection<FailsafeElement<String, String>> retryableDlqFailsafeModJson =
dlqModJson.get(DeadLetterQueueManager.RETRYABLE_ERRORS).setCoder(FAILSAFE_ELEMENT_CODER);
PCollection<FailsafeElement<String, String>> failsafeModJson =
PCollectionList.of(sourceFailsafeModJson)
.and(retryableDlqFailsafeModJson)
.apply("Merge Source And DLQ Mod JSON", Flatten.pCollections());
ImmutableSet.Builder<String> ignoreFieldsBuilder = ImmutableSet.builder();
for (String ignoreField : options.getIgnoreFields().split(",")) {
ignoreFieldsBuilder.add(ignoreField);
}
ImmutableSet<String> ignoreFields = ignoreFieldsBuilder.build();
FailsafeModJsonToTableRowTransformer.FailsafeModJsonToTableRowOptions
failsafeModJsonToTableRowOptions =
FailsafeModJsonToTableRowTransformer.FailsafeModJsonToTableRowOptions.builder()
.setSpannerConfig(spannerConfig)
.setSpannerChangeStream(options.getSpannerChangeStreamName())
.setIgnoreFields(ignoreFields)
.setCoder(FAILSAFE_ELEMENT_CODER)
.build();
FailsafeModJsonToTableRowTransformer.FailsafeModJsonToTableRow failsafeModJsonToTableRow =
new FailsafeModJsonToTableRowTransformer.FailsafeModJsonToTableRow(
failsafeModJsonToTableRowOptions);
PCollectionTuple tableRowTuple =
failsafeModJson.apply("Mod JSON To TableRow", failsafeModJsonToTableRow);
BigQueryDynamicDestinations.BigQueryDynamicDestinationsOptions
bigQueryDynamicDestinationsOptions =
BigQueryDynamicDestinations.BigQueryDynamicDestinationsOptions.builder()
.setSpannerConfig(spannerConfig)
.setChangeStreamName(options.getSpannerChangeStreamName())
.setIgnoreFields(ignoreFields)
.setBigQueryProject(getBigQueryProjectId(options))
.setBigQueryDataset(options.getBigQueryDataset())
.setBigQueryTableTemplate(options.getBigQueryChangelogTableNameTemplate())
.build();
WriteResult writeResult =
tableRowTuple
.get(failsafeModJsonToTableRow.transformOut)
.apply(
"Write To BigQuery",
BigQueryIO.<TableRow>write()
.to(BigQueryDynamicDestinations.of(bigQueryDynamicDestinationsOptions))
.withFormatFunction(element -> removeIntermediateMetadataFields(element))
.withFormatRecordOnFailureFunction(element -> element)
.withCreateDisposition(CreateDisposition.CREATE_IF_NEEDED)
.withWriteDisposition(Write.WriteDisposition.WRITE_APPEND)
.withExtendedErrorInfo()
.withFailedInsertRetryPolicy(InsertRetryPolicy.retryTransientErrors()));
PCollection<String> transformDlqJson =
tableRowTuple
.get(failsafeModJsonToTableRow.transformDeadLetterOut)
.apply(
"Failed Mod JSON During Table Row Transformation",
MapElements.via(new StringDeadLetterQueueSanitizer()));
PCollection<String> bqWriteDlqJson =
BigQueryIOUtils.writeResultToBigQueryInsertErrors(writeResult, options)
.apply(
"Failed Mod JSON During BigQuery Writes",
MapElements.via(new BigQueryDeadLetterQueueSanitizer()));
PCollectionList.of(transformDlqJson)
.and(bqWriteDlqJson)
.apply("Merge Failed Mod JSON From Transform And BigQuery", Flatten.pCollections())
.apply(
"Write Failed Mod JSON To DLQ",
DLQWriteTransform.WriteDLQ.newBuilder()
.withDlqDirectory(dlqDirectory)
.withTmpDirectory(tempDlqDirectory)
.setIncludePaneInfo(true)
.build());
PCollection<FailsafeElement<String, String>> nonRetryableDlqModJsonFailsafe =
dlqModJson.get(DeadLetterQueueManager.PERMANENT_ERRORS).setCoder(FAILSAFE_ELEMENT_CODER);
nonRetryableDlqModJsonFailsafe
.apply(
"Write Mod JSON With Non-retryable Error To DLQ",
MapElements.via(new StringDeadLetterQueueSanitizer()))
.setCoder(StringUtf8Coder.of())
.apply(
DLQWriteTransform.WriteDLQ.newBuilder()
.withDlqDirectory(dlqManager.getSevereDlqDirectoryWithDateTime())
.withTmpDirectory(dlqManager.getSevereDlqDirectory() + "tmp/")
.setIncludePaneInfo(true)
.build());
return pipeline.run();
}
private static DeadLetterQueueManager buildDlqManager(
SpannerChangeStreamsToBigQueryOptions options) {
String tempLocation =
options.as(DataflowPipelineOptions.class).getTempLocation().endsWith("/")
? options.as(DataflowPipelineOptions.class).getTempLocation()
: options.as(DataflowPipelineOptions.class).getTempLocation() + "/";
String dlqDirectory =
options.getDeadLetterQueueDirectory().isEmpty()
? tempLocation + "dlq/"
: options.getDeadLetterQueueDirectory();
LOG.info("Dead letter queue directory: {}", dlqDirectory);
return DeadLetterQueueManager.create(dlqDirectory, DLQ_MAX_RETRIES);
}
private static String getSpannerProjectId(SpannerChangeStreamsToBigQueryOptions options) {
return options.getSpannerProjectId().isEmpty()
? options.getProject()
: options.getSpannerProjectId();
}
private static String getBigQueryProjectId(SpannerChangeStreamsToBigQueryOptions options) {
return options.getBigQueryProjectId().isEmpty()
? options.getProject()
: options.getBigQueryProjectId();
}
/**
* Remove the following intermediate metadata fields that are not user data from {@link TableRow}:
* _metadata_error, _metadata_retry_count, _metadata_spanner_original_payload_json.
*/
private static TableRow removeIntermediateMetadataFields(TableRow tableRow) {
TableRow cleanTableRow = tableRow.clone();
Set<String> rowKeys = tableRow.keySet();
Set<String> metadataFields = BigQueryUtils.getBigQueryIntermediateMetadataFieldNames();
for (String rowKey : rowKeys) {
if (metadataFields.contains(rowKey)) {
cleanTableRow.remove(rowKey);
}
}
return cleanTableRow;
}
/**
* DoFn that converts a {@link DataChangeRecord} to multiple {@link Mod} in serialized JSON
* format.
*/
static class DataChangeRecordToModJsonFn extends DoFn<DataChangeRecord, String> {
@ProcessElement
public void process(@Element DataChangeRecord input, OutputReceiver<String> receiver) {
for (org.apache.beam.sdk.io.gcp.spanner.changestreams.model.Mod changeStreamsMod :
input.getMods()) {
Mod mod =
new Mod(
changeStreamsMod.getKeysJson(),
changeStreamsMod.getNewValuesJson(),
input.getCommitTimestamp(),
input.getServerTransactionId(),
input.isLastRecordInTransactionInPartition(),
input.getRecordSequence(),
input.getTableName(),
input.getModType(),
input.getValueCaptureType(),
input.getNumberOfRecordsInTransaction(),
input.getNumberOfPartitionsInTransaction());
String modJsonString;
try {
modJsonString = mod.toJson();
} catch (IOException e) {
// Ignore exception and print bad format.
modJsonString = String.format("\"%s\"", input);
}
receiver.output(modJsonString);
}
}
}
}
Fluxos de alterações do Cloud Spanner para o Pub/Sub
O modelo de fluxos de alterações do Cloud Spanner para o Pub/Sub é um pipeline de streaming
que transmite os registros de alteração de dados do Cloud Spanner e os grava em tópicos do Pub/Sub
usando o Dataflow Runner V2.
Você precisa criar o novo tópico do Pub/Sub antes de enviar seus dados.
Depois de criar, o Pub/Sub gera e anexa automaticamente uma assinatura ao novo
tópico. Se você tentar enviar dados para um tópico do Pub/Sub que não existe, o pipeline do Dataflow
vai gerar uma exceção e o pipeline vai travar enquanto tenta fazer uma
conexão.
Se o tópico do Pub/Sub necessário já existir, os dados podem ser gerados para ele.
A instância do Cloud Spanner precisa existir antes da execução do pipeline.
O banco de dados do Cloud Spanner precisa ser criado antes da execução do pipeline.
A instância de metadados do Cloud Spanner precisa existir antes da execução do pipeline.
O banco de dados de metadados do Cloud Spanner precisa existir antes da execução do pipeline.
O fluxo de alterações do Cloud Spanner precisa ser criado antes da execução do pipeline.
O tópico do Pub/Sub precisa ser criado antes da execução do pipeline.
Parâmetros do modelo
Parâmetro
Descrição
spannerInstanceId
A instância do Cloud Spanner em que os fluxos de alterações serão lidos.
spannerDatabase
O banco de dados do Cloud Spanner de onde os fluxos de alterações serão lidos.
spannerMetadataInstanceId
A instância do Cloud Spanner a ser usada para a tabela de metadados do conector dos fluxos de alterações.
spannerMetadataDatabase
O banco de dados do Cloud Spanner a ser usado para a tabela de metadados do conector dos fluxos de alterações.
spannerChangeStreamName
O nome do fluxo de alterações a ser lido pelo Cloud Spanner.
pubsubTopic
O tópico do Pub/Sub para saída dos fluxos de alteração.
spannerProjectId
(Opcional) Projeto do qual os fluxos de alterações serão lidos. Este é também o projeto em que a tabela de metadados
do conector dos fluxos de alterações é criada. O padrão para esse parâmetro é o projeto
em que o pipeline do Dataflow está sendo executado.
spannerMetadataTableName
(Opcional) O nome da tabela de metadados do conector dos Fluxos de alterações do Cloud Spanner a ser usado. Se não
for informado, o Cloud Spanner automaticamente criará a tabela de metadados do conector dos fluxos durante
a mudança do fluxo do pipeline. Você precisa fornecer esse parâmetro ao atualizar um pipeline atual.
Não use esse parâmetro para outros casos.
rpcPriority
(Opcional) A prioridade da solicitação para chamadas do Cloud Spanner. O valor precisa ser um destes:
[HIGH,MEDIUM,LOW]. (Padrão: HIGH)
startTimestamp
(Opcional) O início
DateTime, inclusive, para usar
em fluxos de alterações de leitura. Por exemplo, ex-2021-10-12T07:20:50.52Z. O padrão é o
carimbo de data/hora em que o pipeline é iniciado, ou seja, o horário atual.
endTimestamp
(Opcional) A terminação
DateTime, inclusive, para usar
em fluxos de alterações de leitura. Por exemplo, ex-2021-10-12T07:20:50.52Z. O padrão é um tempo infinito
no futuro.
outputFileFormat
(Opcional) O formato da saída. A saída é encapsulada em muitas PubsubMessages e enviada para
um tópico do Pub/Sub. Os formatos permitidos são JSON e AVRO. O padrão é JSON.
pubsubAPI
(Opcional) API Pub/Sub usada para implementar o pipeline. As APIs permitidas são
pubsubio
e native_client. Para um pequeno número de consultas por segundo (QPS),
native_client tem menos latência. Quando o QPS é alto, o pubsubio
tem um desempenho melhor e mais estável. O padrão é pubsubio.
Executar os fluxos de alterações do Cloud Spanner para o modelo do Pub/Sub
Console
Acesse a página Criar job usando um modelo do Dataflow.
latest para usar a versão mais recente do modelo, disponível na pasta mãe
não datada no bucket:
gs://dataflow-templates/latest/
o nome da versão, como 2021-09-20-00_RC00, para usar uma versão específica do
modelo, que pode ser aninhada na respectiva pasta mãe datada no bucket:
gs://dataflow-templates
REGION_NAME:
o endpoint regional em que você quer
implantar o job do Dataflow, por exemplo, us-central1
SPANNER_INSTANCE_ID: ID da instância do Cloud Spanner
SPANNER_DATABASE: banco de dados do Cloud Spanner
SPANNER_METADATA_INSTANCE_ID: ID da instância de metadados do Cloud Spanner
SPANNER_METADATA_DATABASE: banco de dados de metadados do Cloud Spanner
SPANNER_CHANGE_STREAM: fluxo de alterações do Cloud Spanner
PUBSUB_TOPIC: o tópico do Pub/Sub para saída dos fluxos de alteração
API
Para executar o modelo usando a API REST, envie uma solicitação HTTP POST. Para mais informações sobre a
API e os respectivos escopos de autorização, consulte
projects.templates.launch.
PROJECT_ID:
o ID do projeto do Cloud em que você quer executar o job do Dataflow
JOB_NAME:
um nome de job de sua escolha
VERSION:
a versão do modelo que você quer usar
Use estes valores:
latest para usar a versão mais recente do modelo, disponível na pasta mãe
não datada no bucket:
gs://dataflow-templates/latest/
o nome da versão, como 2021-09-20-00_RC00, para usar uma versão específica do
modelo, que pode ser aninhada na respectiva pasta mãe datada no bucket:
gs://dataflow-templates
LOCATION:
o endpoint regional em que você quer
implantar o job do Dataflow, por exemplo, us-central1
SPANNER_INSTANCE_ID: ID da instância do Cloud Spanner
SPANNER_DATABASE: banco de dados do Cloud Spanner
SPANNER_METADATA_INSTANCE_ID: ID da instância de metadados do Cloud Spanner
SPANNER_METADATA_DATABASE: banco de dados de metadados do Cloud Spanner
SPANNER_CHANGE_STREAM: fluxo de alterações do Cloud Spanner
PUBSUB_TOPIC: o tópico do Pub/Sub para saída dos fluxos de alteração
/*
* Copyright (C) 2022 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.cloud.teleport.v2.templates;
import com.google.cloud.Timestamp;
import com.google.cloud.spanner.Options.RpcPriority;
import com.google.cloud.teleport.metadata.Template;
import com.google.cloud.teleport.metadata.TemplateCategory;
import com.google.cloud.teleport.v2.common.UncaughtExceptionLogger;
import com.google.cloud.teleport.v2.options.SpannerChangeStreamsToPubSubOptions;
import com.google.cloud.teleport.v2.transforms.FileFormatFactorySpannerChangeStreamsToPubSub;
import java.util.ArrayList;
import java.util.List;
import org.apache.beam.sdk.Pipeline;
import org.apache.beam.sdk.PipelineResult;
import org.apache.beam.sdk.io.gcp.spanner.SpannerConfig;
import org.apache.beam.sdk.io.gcp.spanner.SpannerIO;
import org.apache.beam.sdk.options.PipelineOptionsFactory;
import org.apache.beam.sdk.options.ValueProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link SpannerChangeStreamsToPubSub} pipeline streams change stream record(s) and stores to
* pubsub topic in user specified format. The sink data can be stored in a JSON Text or Avro data
* format.
*/
@Template(
name = "Spanner_Change_Streams_to_PubSub",
category = TemplateCategory.STREAMING,
displayName = "Cloud Spanner change streams to Pub/Sub",
description =
"Streaming pipeline. Streams Spanner change stream data records and writes them into a"
+ " Pub/Sub topic using Dataflow Runner V2.",
optionsClass = SpannerChangeStreamsToPubSubOptions.class,
flexContainerName = "spanner-changestreams-to-pubsub",
contactInformation = "https://cloud.google.com/support")
public class SpannerChangeStreamsToPubSub {
private static final Logger LOG = LoggerFactory.getLogger(SpannerChangeStreamsToPubSub.class);
private static final String USE_RUNNER_V2_EXPERIMENT = "use_runner_v2";
public static void main(String[] args) {
UncaughtExceptionLogger.register();
LOG.info("Starting Input Messages to Pub/Sub");
SpannerChangeStreamsToPubSubOptions options =
PipelineOptionsFactory.fromArgs(args).as(SpannerChangeStreamsToPubSubOptions.class);
run(options);
}
private static String getProjectId(SpannerChangeStreamsToPubSubOptions options) {
return options.getSpannerProjectId().isEmpty()
? options.getProject()
: options.getSpannerProjectId();
}
public static PipelineResult run(SpannerChangeStreamsToPubSubOptions options) {
LOG.info("Requested Message Format is " + options.getOutputDataFormat());
options.setStreaming(true);
options.setEnableStreamingEngine(true);
final Pipeline pipeline = Pipeline.create(options);
// Get the Spanner project, instance, database, metadata instance, metadata database
// change stream, pubsub topic, and pubsub api parameters.
String projectId = getProjectId(options);
String instanceId = options.getSpannerInstanceId();
String databaseId = options.getSpannerDatabase();
String metadataInstanceId = options.getSpannerMetadataInstanceId();
String metadataDatabaseId = options.getSpannerMetadataDatabase();
String changeStreamName = options.getSpannerChangeStreamName();
String pubsubTopicName = options.getPubsubTopic();
String pubsubAPI = options.getPubsubAPI();
// Retrieve and parse the start / end timestamps.
Timestamp startTimestamp =
options.getStartTimestamp().isEmpty()
? Timestamp.now()
: Timestamp.parseTimestamp(options.getStartTimestamp());
Timestamp endTimestamp =
options.getEndTimestamp().isEmpty()
? Timestamp.MAX_VALUE
: Timestamp.parseTimestamp(options.getEndTimestamp());
// Add use_runner_v2 to the experiments option, since Change Streams connector is only supported
// on Dataflow runner v2.
List<String> experiments = options.getExperiments();
if (experiments == null) {
experiments = new ArrayList<>();
}
if (!experiments.contains(USE_RUNNER_V2_EXPERIMENT)) {
experiments.add(USE_RUNNER_V2_EXPERIMENT);
}
options.setExperiments(experiments);
String metadataTableName =
options.getSpannerMetadataTableName() == null
? null
: options.getSpannerMetadataTableName();
final RpcPriority rpcPriority = options.getRpcPriority();
final String errorMessage =
"Invalid api:" + pubsubAPI + ". Supported apis: pubsubio, native_client";
pipeline
.apply(
SpannerIO.readChangeStream()
.withSpannerConfig(
SpannerConfig.create()
.withHost(ValueProvider.StaticValueProvider.of(options.getSpannerHost()))
.withProjectId(projectId)
.withInstanceId(instanceId)
.withDatabaseId(databaseId))
.withMetadataInstance(metadataInstanceId)
.withMetadataDatabase(metadataDatabaseId)
.withChangeStreamName(changeStreamName)
.withInclusiveStartAt(startTimestamp)
.withInclusiveEndAt(endTimestamp)
.withRpcPriority(rpcPriority)
.withMetadataTable(metadataTableName))
.apply(
"Convert each record to a PubsubMessage",
FileFormatFactorySpannerChangeStreamsToPubSub.newBuilder()
.setOutputDataFormat(options.getOutputDataFormat())
.setProjectId(projectId)
.setPubsubAPI(pubsubAPI)
.setPubsubTopicName(pubsubTopicName)
.build());
return pipeline.run();
}
}
MongoDB para BigQuery (CDC)
O modelo MongoDB para BigQuery CDC (captura de dados alterados) é um pipeline de streaming que funciona em conjunto com o fluxo de alterações do MongoDB.
O pipeline lê os registros JSON enviados ao Pub/Sub por meio de um fluxo de alterações do MongoDB e os grava no BigQuery conforme especificado pelo parâmetro userOption.
Requisitos para esse pipeline
O conjunto de dados de destino do BigQuery precisa existir.
A instância de origem do MongoDB precisa ser acessível nas máquinas de trabalho do Dataflow.
O stream enviando as alterações do MongoDB para o Pub/Sub precisa estar em execução.
Parâmetros do modelo
Parâmetro
Descrição
mongoDbUri
URI de conexão do MongoDB no formato mongodb+srv://:@.
database
Banco de dados no MongoDB para leitura da coleção. Por exemplo, my-db.
collection
Nome da coleção dentro do banco de dados MongoDB. Por exemplo, my-collection.
outputTableSpec
Tabela do BigQuery a ser gravada. Por exemplo, bigquery-project:dataset.output_table.
userOption
FLATTEN ou NONE. FLATTEN nivela os documentos no primeiro nível. NONE armazena todo o documento como uma string JSON.
inputTopic
O tópico de entrada do tópico do Pub/Sub que será lido, no formato de projects/<project>/topics/<topic>.
Como executar o modelo do MongoDB para BigQuery (CDC)
Console
Acesse a página Criar job usando um modelo do Dataflow.
PROJECT_ID:
o ID do projeto do Cloud em que você quer executar o job do Dataflow
JOB_NAME:
um nome de job de sua escolha
REGION_NAME:
o endpoint regional em que você quer
implantar o job do Dataflow, por exemplo, us-central1
VERSION:
a versão do modelo que você quer usar
Use estes valores:
latest para usar a versão mais recente do modelo, disponível na pasta mãe
não datada no bucket:
gs://dataflow-templates/latest/
o nome da versão, como 2021-09-20-00_RC00, para usar uma versão específica do
modelo, que pode ser aninhada na respectiva pasta mãe datada no bucket:
gs://dataflow-templates
OUTPUT_TABLE_SPEC: o nome da tabela de destino do BigQuery.
MONGO_DB_URI: o URI do MongoDB.
DATABASE: o banco de dados do MongoDB.
COLLECTION: sua coleção do MongoDB.
USER_OPTION: FLATTEN ou NENHUM.
INPUT_TOPIC: o tópico de entrada do Pub/Sub.
API
Para executar o modelo usando a API REST, envie uma solicitação HTTP POST. Para mais informações sobre a
API e os respectivos escopos de autorização, consulte
projects.templates.launch.
PROJECT_ID:
o ID do projeto do Cloud em que você quer executar o job do Dataflow
JOB_NAME:
um nome de job de sua escolha
LOCATION:
o endpoint regional em que você quer
implantar o job do Dataflow, por exemplo, us-central1
VERSION:
a versão do modelo que você quer usar
Use estes valores:
latest para usar a versão mais recente do modelo, disponível na pasta mãe
não datada no bucket:
gs://dataflow-templates/latest/
o nome da versão, como 2021-09-20-00_RC00, para usar uma versão específica do
modelo, que pode ser aninhada na respectiva pasta mãe datada no bucket:
gs://dataflow-templates
OUTPUT_TABLE_SPEC: o nome da tabela de destino do BigQuery.
/*
* Copyright (C) 2019 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.cloud.teleport.v2.mongodb.templates;
import com.google.api.services.bigquery.model.TableRow;
import com.google.api.services.bigquery.model.TableSchema;
import com.google.cloud.teleport.metadata.Template;
import com.google.cloud.teleport.metadata.TemplateCategory;
import com.google.cloud.teleport.v2.common.UncaughtExceptionLogger;
import com.google.cloud.teleport.v2.mongodb.options.MongoDbToBigQueryOptions.BigQueryWriteOptions;
import com.google.cloud.teleport.v2.mongodb.options.MongoDbToBigQueryOptions.JavascriptDocumentTransformerOptions;
import com.google.cloud.teleport.v2.mongodb.options.MongoDbToBigQueryOptions.MongoDbOptions;
import com.google.cloud.teleport.v2.mongodb.options.MongoDbToBigQueryOptions.PubSubOptions;
import com.google.cloud.teleport.v2.mongodb.templates.MongoDbToBigQueryCdc.Options;
import com.google.cloud.teleport.v2.options.BigQueryStorageApiStreamingOptions;
import com.google.cloud.teleport.v2.transforms.JavascriptDocumentTransformer.TransformDocumentViaJavascript;
import com.google.cloud.teleport.v2.utils.BigQueryIOUtils;
import java.io.IOException;
import javax.script.ScriptException;
import org.apache.beam.sdk.Pipeline;
import org.apache.beam.sdk.io.gcp.bigquery.BigQueryIO;
import org.apache.beam.sdk.io.gcp.pubsub.PubsubIO;
import org.apache.beam.sdk.options.PipelineOptions;
import org.apache.beam.sdk.options.PipelineOptionsFactory;
import org.apache.beam.sdk.transforms.DoFn;
import org.apache.beam.sdk.transforms.ParDo;
import org.bson.Document;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link BigQueryToMongoDbCDC} pipeline is a streaming pipeline which reads data pushed to
* PubSub from MongoDB Changestream and outputs the resulting records to BigQuery.
*/
@Template(
name = "MongoDB_to_BigQuery_CDC",
category = TemplateCategory.STREAMING,
displayName = "MongoDB to BigQuery (CDC)",
description =
"A streaming pipeline which reads data pushed to Pub/Sub from MongoDB Changestream and"
+ " writes the resulting records to BigQuery.",
optionsClass = Options.class,
flexContainerName = "mongodb-to-bigquery-cdc",
contactInformation = "https://cloud.google.com/support")
public class MongoDbToBigQueryCdc {
private static final Logger LOG = LoggerFactory.getLogger(MongoDbToBigQuery.class);
/** Options interface. */
public interface Options
extends PipelineOptions,
MongoDbOptions,
PubSubOptions,
BigQueryWriteOptions,
JavascriptDocumentTransformerOptions,
BigQueryStorageApiStreamingOptions {}
/** class ParseAsDocumentsFn. */
private static class ParseAsDocumentsFn extends DoFn<String, Document> {
@ProcessElement
public void processElement(ProcessContext context) {
context.output(Document.parse(context.element()));
}
}
/**
* Main entry point for pipeline execution.
*
* @param args Command line arguments to the pipeline.
*/
public static void main(String[] args)
throws ScriptException, IOException, NoSuchMethodException {
UncaughtExceptionLogger.register();
Options options = PipelineOptionsFactory.fromArgs(args).withValidation().as(Options.class);
BigQueryIOUtils.validateBQStorageApiOptionsStreaming(options);
run(options);
}
/** Pipeline to read data from PubSub and write to MongoDB. */
public static boolean run(Options options)
throws ScriptException, IOException, NoSuchMethodException {
options.setStreaming(true);
Pipeline pipeline = Pipeline.create(options);
String userOption = options.getUserOption();
String inputOption = options.getInputTopic();
TableSchema bigquerySchema;
if (options.getJavascriptDocumentTransformFunctionName() != null
&& options.getJavascriptDocumentTransformGcsPath() != null) {
bigquerySchema =
MongoDbUtils.getTableFieldSchemaForUDF(
options.getMongoDbUri(),
options.getDatabase(),
options.getCollection(),
options.getJavascriptDocumentTransformGcsPath(),
options.getJavascriptDocumentTransformFunctionName(),
options.getUserOption());
} else {
bigquerySchema =
MongoDbUtils.getTableFieldSchema(
options.getMongoDbUri(),
options.getDatabase(),
options.getCollection(),
options.getUserOption());
}
pipeline
.apply("Read PubSub Messages", PubsubIO.readStrings().fromTopic(inputOption))
.apply(
"RTransform string to document",
ParDo.of(
new DoFn<String, Document>() {
@ProcessElement
public void process(ProcessContext c) {
Document document = Document.parse(c.element());
c.output(document);
}
}))
.apply(
"UDF",
TransformDocumentViaJavascript.newBuilder()
.setFileSystemPath(options.getJavascriptDocumentTransformGcsPath())
.setFunctionName(options.getJavascriptDocumentTransformFunctionName())
.build())
.apply(
"Read and transform data",
ParDo.of(
new DoFn<Document, TableRow>() {
@ProcessElement
public void process(ProcessContext c) {
Document document = c.element();
TableRow row = MongoDbUtils.getTableSchema(document, userOption);
c.output(row);
}
}))
.apply(
BigQueryIO.writeTableRows()
.to(options.getOutputTableSpec())
.withSchema(bigquerySchema)
.withCreateDisposition(BigQueryIO.Write.CreateDisposition.CREATE_IF_NEEDED)
.withWriteDisposition(BigQueryIO.Write.WriteDisposition.WRITE_APPEND));
pipeline.run();
return true;
}
}