/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you 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 gobblin.eventhub.writer; import java.io.IOException; import gobblin.configuration.State; import gobblin.eventhub.EventhubMetricNames; import gobblin.instrumented.Instrumented; import gobblin.metrics.MetricContext; import org.apache.http.HttpEntity; import org.apache.http.HttpResponse; import org.apache.http.HttpStatus; import org.apache.http.StatusLine; import org.apache.http.client.HttpClient; import org.apache.http.client.methods.HttpPost; import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClients; import org.apache.http.util.EntityUtils; import org.codehaus.jackson.map.ObjectMapper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.codahale.metrics.Meter; import com.google.common.util.concurrent.Futures; import com.microsoft.azure.servicebus.SharedAccessSignatureTokenProvider; import lombok.extern.slf4j.Slf4j; import gobblin.password.PasswordManager; import gobblin.writer.Batch; import gobblin.writer.BatchAsyncDataWriter; import gobblin.writer.SyncDataWriter; import gobblin.writer.WriteCallback; import gobblin.writer.WriteResponse; import gobblin.writer.WriteResponseFuture; import gobblin.writer.WriteResponseMapper; import java.time.Duration; import java.util.ArrayList; import java.util.List; import java.util.Properties; import java.util.concurrent.Future; import com.codahale.metrics.Timer; /** * Data Writer for Eventhub. * This Data Writer use HttpClient internally and publish data to Eventhub via Post REST API * Synchronous model is used here that after each data is sent through httpClient, a response is consumed * immediately. Also this class supports sending multiple records in a batch manner. * * The String input needs to be Unicode based because it will convert to JSON format when using REST API * * For batch sending, please refer to https://docs.microsoft.com/en-us/rest/api/eventhub/send-batch-events for sending batch records * For unicode based json string, please refer to http://rfc7159.net/ */ @Slf4j public class EventhubDataWriter implements SyncDataWriter<String>, BatchAsyncDataWriter<String> { private static final Logger LOG = LoggerFactory.getLogger(EventhubDataWriter.class); private HttpClient httpclient; private final String namespaceName; private final String eventHubName; private final String sasKeyName; private final String sasKey; private final String targetURI; private final Meter bytesWritten; private final Meter recordsAttempted; private final Meter recordsSuccess; private final Meter recordsFailed; private final Timer writeTimer; private long postStartTimestamp = 0; private long sigExpireInMinute = 1; private String signature = ""; private MetricContext metricContext; private static final ObjectMapper mapper = new ObjectMapper(); private static final WriteResponseMapper<Integer> WRITE_RESPONSE_WRAPPER = new WriteResponseMapper<Integer>() { @Override public WriteResponse wrap(final Integer returnCode) { return new WriteResponse<Integer>() { @Override public Integer getRawResponse() { return returnCode; } @Override public String getStringResponse() { return returnCode.toString(); } @Override public long bytesWritten() { // Don't know how many bytes were written return -1; } }; } }; /** User needs to provide eventhub properties */ public EventhubDataWriter(Properties properties) { PasswordManager manager = PasswordManager.getInstance(properties); namespaceName = properties.getProperty(BatchedEventhubDataWriter.EVH_NAMESPACE); eventHubName = properties.getProperty(BatchedEventhubDataWriter.EVH_HUBNAME); sasKeyName = properties.getProperty(BatchedEventhubDataWriter.EVH_SAS_KEYNAME); String encodedSasKey = properties.getProperty(BatchedEventhubDataWriter.EVH_SAS_KEYVALUE); sasKey = manager.readPassword(encodedSasKey); targetURI = "https://" + namespaceName + ".servicebus.windows.net/" + eventHubName + "/messages"; httpclient = HttpClients.createDefault(); metricContext = Instrumented.getMetricContext(new State(properties),EventhubDataWriter.class); recordsAttempted = this.metricContext.meter(EventhubMetricNames.EventhubDataWriterMetrics.RECORDS_ATTEMPTED_METER); recordsSuccess = this.metricContext.meter(EventhubMetricNames.EventhubDataWriterMetrics.RECORDS_SUCCESS_METER); recordsFailed = this.metricContext.meter(EventhubMetricNames.EventhubDataWriterMetrics.RECORDS_FAILED_METER); bytesWritten = this.metricContext.meter(EventhubMetricNames.EventhubDataWriterMetrics.BYTES_WRITTEN_METER); writeTimer = this.metricContext.timer(EventhubMetricNames.EventhubDataWriterMetrics.WRITE_TIMER); } /** User needs to provide eventhub properties and an httpClient */ public EventhubDataWriter(Properties properties, HttpClient httpclient) { this (properties); this.httpclient = httpclient; } /** * Write a whole batch to eventhub */ public Future<WriteResponse> write (Batch<String> batch, WriteCallback callback) { Timer.Context context = writeTimer.time(); int returnCode = 0; LOG.info ("Dispatching batch " + batch.getId()); recordsAttempted.mark(batch.getRecords().size()); try { String encoded = encodeBatch(batch); returnCode = request (encoded); WriteResponse<Integer> response = WRITE_RESPONSE_WRAPPER.wrap(returnCode); callback.onSuccess(response); bytesWritten.mark(encoded.length()); recordsSuccess.mark(batch.getRecords().size()); } catch (Exception e) { LOG.error("Dispatching batch " + batch.getId() + " failed :" + e.toString()); callback.onFailure(e); recordsFailed.mark(batch.getRecords().size()); } context.close(); Future<Integer> future = Futures.immediateFuture(returnCode); return new WriteResponseFuture<>(future, WRITE_RESPONSE_WRAPPER); } /** * Write a single record to eventhub */ public WriteResponse write (String record) throws IOException { recordsAttempted.mark(); String encoded = encodeRecord(record); int returnCode = request (encoded); recordsSuccess.mark(); bytesWritten.mark(encoded.length()); return WRITE_RESPONSE_WRAPPER.wrap(returnCode); } /** * A signature which contains the duration. * After the duration is expired, the signature becomes invalid */ public void refreshSignature () { if (postStartTimestamp == 0 || (System.nanoTime() - postStartTimestamp) > Duration.ofMinutes(sigExpireInMinute).toNanos()) { // generate signature try { signature = SharedAccessSignatureTokenProvider .generateSharedAccessSignature(sasKeyName, sasKey, namespaceName, Duration.ofMinutes(sigExpireInMinute)); postStartTimestamp = System.nanoTime(); LOG.info ("Signature is refreshing: " + signature); } catch (Exception e) { throw new RuntimeException(e); } } } /** * Send an encoded string to the Eventhub using post method */ private int request (String encoded) throws IOException { refreshSignature(); HttpPost httpPost = new HttpPost(targetURI); httpPost.setHeader("Content-type", "application/vnd.microsoft.servicebus.json"); httpPost.setHeader("Authorization", signature); httpPost.setHeader("Host", namespaceName + ".servicebus.windows.net "); StringEntity entity = new StringEntity(encoded); httpPost.setEntity(entity); HttpResponse response = httpclient.execute(httpPost); StatusLine status = response.getStatusLine(); HttpEntity entity2 = response.getEntity(); // do something useful with the response body // and ensure it is fully consumed EntityUtils.consume(entity2); int returnCode = status.getStatusCode(); if (returnCode != HttpStatus.SC_CREATED) { LOG.error (new IOException(status.getReasonPhrase()).toString()); throw new IOException(status.getReasonPhrase()); } return returnCode; } /** * Each record of batch is wrapped by a 'Body' json object * put this new object into an array, encode the whole array */ private String encodeBatch (Batch<String> batch) throws IOException { // Convert original json object to a new json object with format {"Body": "originalJson"} // Add new json object to an array and send the whole array to eventhub using REST api // Refer to https://docs.microsoft.com/en-us/rest/api/eventhub/send-batch-events List<String> records = batch.getRecords(); ArrayList<EventhubRequest> arrayList = new ArrayList<>(); for (String record: records) { arrayList.add(new EventhubRequest(record)); } return mapper.writeValueAsString (arrayList); } /** * A single record is wrapped by a 'Body' json object * encode this json object */ private String encodeRecord (String record)throws IOException { // Convert original json object to a new json object with format {"Body": "originalJson"} // Add new json object to an array and send the whole array to eventhub using REST api // Refer to https://docs.microsoft.com/en-us/rest/api/eventhub/send-batch-events ArrayList<EventhubRequest> arrayList = new ArrayList<>(); arrayList.add(new EventhubRequest(record)); return mapper.writeValueAsString (arrayList); } /** * Close the HttpClient */ public void close() throws IOException { if (httpclient instanceof CloseableHttpClient) { ((CloseableHttpClient)httpclient).close(); } } public void cleanup() { // do nothing } public void flush() { // do nothing } }