/* * This program is free software; you can redistribute it and/or modify it under the * terms of the GNU Lesser General Public License, version 2.1 as published by the Free Software * Foundation. * * You should have received a copy of the GNU Lesser General Public License along with this * program; if not, you can obtain a copy at http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html * or from the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. * * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * See the GNU Lesser General Public License for more details. * * Copyright (c) 2015 Pentaho Corporation. All rights reserved. */ package org.pentaho.telemetry; import org.apache.commons.httpclient.HttpClient; import org.apache.commons.httpclient.URI; import org.apache.commons.httpclient.methods.PostMethod; import org.apache.commons.httpclient.methods.StringRequestEntity; import org.apache.commons.httpclient.methods.multipart.Part; import org.apache.commons.httpclient.methods.multipart.StringPart; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import java.io.EOFException; import java.io.File; import java.io.FileInputStream; import java.io.FilenameFilter; import java.io.IOException; import java.io.ObjectInputStream; import java.net.HttpURLConnection; import java.util.ArrayList; import java.util.Calendar; import java.util.HashMap; import java.util.Iterator; import java.util.List; /** * Used by {@link TelemetryHandler} to send telemetry events to a remote endpoint */ public class TelemetryEventSender implements Runnable { // region Constants protected static final String FILE_EXT = ".tel"; protected static final String LAST_SUBMISSION_DIR_NAME = "lastsubmission"; private static final int DAYS_TO_KEEP_FILES = 5; private static final int BLOCK_SIZE = 50; private static final int HTTP_CALL_TIMEOUT = 30000; // endregion // region Properties public Log getLogger() { return logger; } private static final Log logger = LogFactory.getLog( TelemetryEventSender.class ); protected static HttpClient getHttpClient() { return defaultHttpClient != null ? defaultHttpClient : new HttpClient(); } protected static PostMethod defaultHttpMethod; protected static PostMethod getHttpMethod() { return defaultHttpMethod != null ? defaultHttpMethod : new PostMethod(); } protected static HttpClient defaultHttpClient; public File getTelemetryDir() { return this.telemetryDir; } public void setTelemetryDir( File telemetryDir ) { this.telemetryDir = telemetryDir; } private File telemetryDir; public File getLastSubmissionDir() { return this.lastSubmissionDir; } public void setLastSubmissionDir( File lastSubmissionDir ) { if ( !lastSubmissionDir.exists() ) { lastSubmissionDir.mkdir(); } this.lastSubmissionDir = lastSubmissionDir; } private File lastSubmissionDir; // endregion // region Constructors public TelemetryEventSender( File telemetryDir ) { this.setTelemetryDir( telemetryDir ); this.setLastSubmissionDir( new File( telemetryDir.getAbsolutePath() + "/" + LAST_SUBMISSION_DIR_NAME ) ); } // endregion // region Methods @Override public void run() { //Delete everything in lastSubmission folder File[] submittedFiles = this.getLastSubmissionDir().listFiles(); for ( File f : submittedFiles ) { f.delete(); } //Get all requests in telemetryPath File[] unsubmittedRequests = this.getTelemetryDir().listFiles( new FilenameFilter() { @Override public boolean accept( File file, String name ) { return name.endsWith( FILE_EXT ); } } ); File[] block = new File[ BLOCK_SIZE ]; int blockIndex = 0; Calendar cld = Calendar.getInstance(); cld.add( Calendar.DAY_OF_YEAR, -DAYS_TO_KEEP_FILES ); for ( File f : unsubmittedRequests ) { //Check if file was created more than 5 days ago. If so, dismiss it if ( f.lastModified() < cld.getTime().getTime() ) { f.delete(); continue; } //Create blocks of BLOCK_SIZE if ( blockIndex > 0 && blockIndex % BLOCK_SIZE == 0 ) { sendRequest( block ); block = new File[ BLOCK_SIZE ]; blockIndex = 0; } else { block[ blockIndex ] = f; blockIndex++; } } if ( blockIndex > 0 ) { sendRequest( block ); } } /** * Given an array of telemetry event files, parses them, builds a JSON array with all the events and dispatches them * to one or more urls. Deletes files if request was successful. * * @param blockToSend Array of files with telemetry events to send to the server */ protected void sendRequest( File[] blockToSend ) { String baseUrl = null; HashMap<String, StringBuffer> urlsAndPostData = new HashMap<String, StringBuffer>(); HashMap<String, List<File>> urlsAndFiles = new HashMap<String, List<File>>(); for ( File f : blockToSend ) { if ( f == null ) { break; } try { FileInputStream fin = new FileInputStream( f ); ObjectInputStream ois = new ObjectInputStream( fin ); TelemetryEvent event = (TelemetryEvent) ois.readObject(); ois.close(); StringBuffer postData = urlsAndPostData.get( event.getUrlToCall() ); if ( postData == null ) { postData = new StringBuffer().append( "[" ); } else { postData.append( ", " ); } postData.append( event.encodeToJSON() ); urlsAndPostData.put( event.getUrlToCall(), postData ); List<File> filesForThisUrl = urlsAndFiles.get( event.getUrlToCall() ); if ( filesForThisUrl == null ) { filesForThisUrl = new ArrayList<File>(); } filesForThisUrl.add( f ); urlsAndFiles.put( event.getUrlToCall(), filesForThisUrl ); } catch ( EOFException eofe ) { this.getLogger().warn( "EOF caught while deserializing telemetry event. Probably a corrupt save. Deleting event.", eofe ); f.delete(); } catch ( IOException ioe ) { this.getLogger().error( "Error caught while deserializing telemetry event.", ioe ); f.delete(); } catch ( ClassNotFoundException cnfe ) { this.getLogger().error( "Class not found while deserializing telemetry event.", cnfe ); } } Iterator<String> urlIterator = urlsAndPostData.keySet().iterator(); while ( urlIterator.hasNext() ) { String url = urlIterator.next(); StringBuffer postData = urlsAndPostData.get( url ); postData.append( "]" ); postData.append( System.getProperty( "line.separator" ) ); boolean success = true; try { final HttpClient httpClient = getHttpClient(); final PostMethod httpMethod = getHttpMethod(); int timeout = HTTP_CALL_TIMEOUT; httpClient.getHttpConnectionManager().getParams().setSoTimeout( timeout ); httpMethod.setURI( new URI( url, true ) ); Part[] parts = new Part[] { new StringPart( "body", postData.toString() ) }; httpMethod.setRequestEntity( new StringRequestEntity( postData.toString(), "application/json", "UTF8" ) ); this.getLogger().info( "Calling " + url ); this.getLogger().info( "Data: " + postData.toString() ); // Execute the request final int resultCode = httpClient.executeMethod( httpMethod ); if ( resultCode != HttpURLConnection.HTTP_OK ) { this.getLogger().error( "Invalid Result Code Returned: " + resultCode ); success = false; } else { String resultXml = httpMethod.getResponseBodyAsString(); //TO DO: Improve error detection if ( resultXml.indexOf( "<result>OK</result>" ) < 0 ) { this.getLogger().warn( "Telemetry request had unexpected result: " + resultXml + "." ); success = false; } } // Clean up httpMethod.releaseConnection(); } catch ( Exception e ) { this.getLogger().warn( "Exception caught while making telemetry request.", e ); success = false; } //Clear files if ( success ) { for ( File f : blockToSend ) { if ( f != null ) { File newFile = new File( this.getLastSubmissionDir(), f.getName() ); f.renameTo( newFile ); f.delete(); } } } } } // endregion }