package com.pinterest.secor.uploader; import com.google.api.client.googleapis.auth.oauth2.GoogleCredential; import com.google.api.client.googleapis.javanet.GoogleNetHttpTransport; import com.google.api.client.googleapis.media.MediaHttpUploader; import com.google.api.client.googleapis.media.MediaHttpUploaderProgressListener; import com.google.api.client.http.FileContent; import com.google.api.client.http.HttpRequest; import com.google.api.client.http.HttpRequestInitializer; import com.google.api.client.http.HttpTransport; import com.google.api.client.http.HttpUnsuccessfulResponseHandler; import com.google.api.client.http.HttpBackOffUnsuccessfulResponseHandler; import com.google.api.client.json.JsonFactory; import com.google.api.client.json.jackson2.JacksonFactory; import com.google.api.client.util.ExponentialBackOff; import com.google.api.services.storage.Storage; import com.google.api.services.storage.StorageScopes; import com.google.api.services.storage.model.StorageObject; import com.pinterest.secor.common.LogFilePath; import com.pinterest.secor.common.SecorConfig; import org.apache.commons.lang.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.nio.file.Files; import java.util.Collections; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.Date; import java.text.SimpleDateFormat; /** * Manages uploads to Google Cloud Storage using the Storage class from the Google API SDK. * <p> * It will use Service Account credential (json file) that can be generated from the Google Developers Console. * By default it will look up configured credential path in secor.gs.credentials.path or fallback to the default * credential in the environment variable GOOGLE_APPLICATION_CREDENTIALS. * <p> * Application credentials documentation * https://developers.google.com/identity/protocols/application-default-credentials * * @author Jerome Gagnon (jerome.gagnon.1@gmail.com) */ public class GsUploadManager extends UploadManager { private static final Logger LOG = LoggerFactory.getLogger(GsUploadManager.class); private static final JsonFactory JSON_FACTORY = JacksonFactory.getDefaultInstance(); private static final ExecutorService executor = Executors.newFixedThreadPool(256); /** * Global instance of the Storage. The best practice is to make it a single * globally shared instance across your application. */ private static Storage mStorageService; private Storage mClient; public GsUploadManager(SecorConfig config) throws Exception { super(config); mClient = getService(mConfig.getGsCredentialsPath(), mConfig.getGsConnectTimeoutInMs(), mConfig.getGsReadTimeoutInMs()); } @Override public Handle<?> upload(LogFilePath localPath) throws Exception { final String gsBucket = mConfig.getGsBucket(); final String gsKey = localPath.withPrefix(mConfig.getGsPath()).getLogFilePath(); final File localFile = new File(localPath.getLogFilePath()); final boolean directUpload = mConfig.getGsDirectUpload(); LOG.info("uploading file {} to gs://{}/{}", localFile, gsBucket, gsKey); final StorageObject storageObject = new StorageObject().setName(gsKey); final FileContent storageContent = new FileContent(Files.probeContentType(localFile.toPath()), localFile); final Future<?> f = executor.submit(new Runnable() { @Override public void run() { try { Storage.Objects.Insert request = mClient.objects().insert(gsBucket, storageObject, storageContent); if (directUpload) { request.getMediaHttpUploader().setDirectUploadEnabled(true); } request.getMediaHttpUploader().setProgressListener(new MediaHttpUploaderProgressListener() { @Override public void progressChanged(MediaHttpUploader uploader) throws IOException { LOG.debug("[{} %] upload file {} to gs://{}/{}", (int) uploader.getProgress() * 100, localFile, gsBucket, gsKey); } }); request.execute(); } catch (IOException e) { throw new RuntimeException(e); } } }); return new FutureHandle(f); } private static Storage getService(String credentialsPath, int connectTimeoutMs, int readTimeoutMs) throws Exception { if (mStorageService == null) { HttpTransport httpTransport = GoogleNetHttpTransport.newTrustedTransport(); GoogleCredential credential; try { // Lookup if configured path from the properties; otherwise fallback to Google Application default if (credentialsPath != null && !credentialsPath.isEmpty()) { credential = GoogleCredential .fromStream(new FileInputStream(credentialsPath), httpTransport, JSON_FACTORY) .createScoped(Collections.singleton(StorageScopes.CLOUD_PLATFORM)); } else { credential = GoogleCredential.getApplicationDefault(httpTransport, JSON_FACTORY); } } catch (IOException e) { throw new RuntimeException("Failed to load Google credentials : " + credentialsPath, e); } mStorageService = new Storage.Builder(httpTransport, JSON_FACTORY, setHttpBackoffTimeout(credential, connectTimeoutMs, readTimeoutMs)) .setApplicationName("com.pinterest.secor") .build(); } return mStorageService; } private static HttpRequestInitializer setHttpBackoffTimeout(final HttpRequestInitializer requestInitializer, final int connectTimeoutMs, final int readTimeoutMs) { return new HttpRequestInitializer() { @Override public void initialize(HttpRequest httpRequest) throws IOException { requestInitializer.initialize(httpRequest); // Configure exponential backoff on error // https://developers.google.com/api-client-library/java/google-http-java-client/backoff ExponentialBackOff backoff = new ExponentialBackOff(); HttpUnsuccessfulResponseHandler backoffHandler = new HttpBackOffUnsuccessfulResponseHandler(backoff) .setBackOffRequired(HttpBackOffUnsuccessfulResponseHandler.BackOffRequired.ALWAYS); httpRequest.setUnsuccessfulResponseHandler(backoffHandler); httpRequest.setConnectTimeout(connectTimeoutMs); httpRequest.setReadTimeout(readTimeoutMs); } }; } }