/* * ApplicationInsights-Java * Copyright (c) Microsoft Corporation * All rights reserved. * * MIT License * Permission is hereby granted, free of charge, to any person obtaining a copy of this * software and associated documentation files (the ""Software""), to deal in the Software * without restriction, including without limitation the rights to use, copy, modify, merge, * publish, distribute, sublicense, and/or sell copies of the Software, and to permit * persons to whom the Software is furnished to do so, subject to the following conditions: * The above copyright notice and this permission notice shall be included in all copies or * substantial portions of the Software. * THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR * PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE * FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. */ package com.microsoft.applicationinsights.internal.channel.common; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.net.SocketException; import java.net.UnknownHostException; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.Calendar; import java.util.Date; import java.util.TimeZone; import java.util.concurrent.TimeUnit; import com.microsoft.applicationinsights.internal.channel.TransmissionDispatcher; import com.microsoft.applicationinsights.internal.channel.TransmissionOutput; import com.microsoft.applicationinsights.internal.logger.InternalLogger; import org.apache.http.Header; import org.apache.http.HttpEntity; import org.apache.http.HttpResponse; import org.apache.http.HttpStatus; import org.apache.http.client.methods.HttpPost; import org.apache.http.conn.ConnectionPoolTimeoutException; import org.apache.http.entity.ByteArrayEntity; import com.google.common.base.Preconditions; import com.google.common.base.Strings; /** * The class is responsible for the actual sending of {@link com.microsoft.applicationinsights.internal.channel.common.Transmission} * * The class uses Apache's HttpClient framework for that. * * Created by gupele on 12/18/2014. */ public final class TransmissionNetworkOutput implements TransmissionOutput { private final static String CONTENT_TYPE_HEADER = "Content-Type"; private final static String CONTENT_ENCODING_HEADER = "Content-Encoding"; private final static String RESPONSE_THROTTLING_HEADER = "Retry-After"; private final static String RESPONSE_RETRY_AFTER_DATE_FORMAT = "E, dd MMM yyyy HH:mm:ss"; private final static String DEFAULT_SERVER_URI = "https://dc.services.visualstudio.com/v2/track"; // For future use: re-send a failed transmission back to the dispatcher private TransmissionDispatcher transmissionDispatcher; private final String serverUri; private volatile boolean stopped; // Use one instance for optimization private final ApacheSender httpClient; private TransmissionPolicyManager transmissionPolicyManager; public static TransmissionNetworkOutput create(TransmissionPolicyManager transmissionPolicyManager) { return create(DEFAULT_SERVER_URI, transmissionPolicyManager); } public static TransmissionNetworkOutput create(String endpoint, TransmissionPolicyManager transmissionPolicyManager) { String realEndpoint = Strings.isNullOrEmpty(endpoint) ? DEFAULT_SERVER_URI : endpoint; return new TransmissionNetworkOutput(realEndpoint, transmissionPolicyManager); } private TransmissionNetworkOutput(String serverUri, TransmissionPolicyManager transmissionPolicyManager) { Preconditions.checkNotNull(serverUri, "serverUri should be a valid non-null value"); Preconditions.checkArgument(!Strings.isNullOrEmpty(serverUri), "serverUri should be a valid non-null value"); Preconditions.checkNotNull(transmissionPolicyManager, "transmissionPolicyManager should be a valid non-null value"); this.serverUri = serverUri; httpClient = ApacheSenderFactory.INSTANCE.create(); this.transmissionPolicyManager = transmissionPolicyManager; stopped = false; } public void setTransmissionDispatcher(TransmissionDispatcher transmissionDispatcher) { this.transmissionDispatcher = transmissionDispatcher; } /** * Stops all threads from sending data. * @param timeout The timeout to wait, which is not relevant here. * @param timeUnit The time unit, which is not relevant in this method. */ @Override public synchronized void stop(long timeout, TimeUnit timeUnit) { if (stopped) { return; } httpClient.close(); stopped = true; } /** * Tries to send a {@link com.microsoft.applicationinsights.internal.channel.common.Transmission} * The thread that calls that method might be suspended if there is a throttling issues, in any case * the thread that enters this method is responsive for 'stop' request that might be issued by the application. * @param transmission The data to send * @return True when done. */ @Override public boolean send(Transmission transmission) { while (!stopped) { if (transmissionPolicyManager.getTransmissionPolicyState().getCurrentState() != TransmissionPolicy.UNBLOCKED) { return false; } HttpResponse response = null; HttpPost request = null; try { request = createTransmissionPostRequest(transmission); httpClient.enhanceRequest(request); response = httpClient.sendPostRequest(request); HttpEntity respEntity = response.getEntity(); int code = response.getStatusLine().getStatusCode(); TransmissionSendResult sendResult = translateResponse(code, respEntity); switch (sendResult) { case PAYMENT_REQUIRED: case THROTTLED: suspendTransmissions(TransmissionPolicy.BLOCKED_BUT_CAN_BE_PERSISTED, response); break; case THROTTLED_OVER_EXTENDED_TIME: suspendTransmissions(TransmissionPolicy.BLOCKED_AND_CANNOT_BE_PERSISTED, response); break; default: return true; } } catch (ConnectionPoolTimeoutException e) { InternalLogger.INSTANCE.error("Failed to send, connection pool timeout exception"); } catch (SocketException e) { InternalLogger.INSTANCE.error("Failed to send, socket timeout exception"); } catch (UnknownHostException e) { InternalLogger.INSTANCE.error("Failed to send, wrong host address or cannot reach address due to network issues, exception: %s", e.getMessage()); } catch (IOException ioe) { InternalLogger.INSTANCE.error("Failed to send, exception: %s", ioe.getMessage()); } catch (Exception e) { InternalLogger.INSTANCE.error("Failed to send, unexpected exception: %s", e.getMessage()); } catch (Throwable t) { InternalLogger.INSTANCE.error("Failed to send, unexpected error: %s", t.getMessage()); } finally { if (request != null) { request.releaseConnection(); } httpClient.dispose(response); } } return true; } private void suspendTransmissions(TransmissionPolicy suspensionPolicy, HttpResponse response) { Header retryAfterHeader = response.getFirstHeader(RESPONSE_THROTTLING_HEADER); if (retryAfterHeader == null) { return; } String retryAfterAsString = retryAfterHeader.getValue(); if (Strings.isNullOrEmpty(retryAfterAsString)) { return; } try { DateFormat formatter = new SimpleDateFormat(RESPONSE_RETRY_AFTER_DATE_FORMAT); Date date = formatter.parse(retryAfterAsString); Date now = Calendar.getInstance().getTime(); long retryAfterAsSeconds = (date.getTime() - convertToDateToGmt(now).getTime())/1000; transmissionPolicyManager.suspendInSeconds(suspensionPolicy, retryAfterAsSeconds); } catch (Throwable e) { InternalLogger.INSTANCE.logAlways(InternalLogger.LoggingLevel.ERROR, "Throttled but failed to block transmission, exception: %s", e.getMessage()); } } private static Date convertToDateToGmt(Date date){ TimeZone tz = TimeZone.getDefault(); Date ret = new Date(date.getTime() - tz.getRawOffset()); // If we are now in DST, back off by the delta. Note that we are checking the GMT date, this is the KEY. if (tz.inDaylightTime(ret)) { Date dstDate = new Date(ret.getTime() - tz.getDSTSavings()); // Check to make sure we have not crossed back into standard time if (tz.inDaylightTime(dstDate)) { ret = dstDate; } } return ret; } private TransmissionSendResult translateResponse(int code, HttpEntity respEntity) { if (code == HttpStatus.SC_OK) { return TransmissionSendResult.SENT_SUCCESSFULLY; } TransmissionSendResult result; String errorMessage; if (code < HttpStatus.SC_OK || (code >= HttpStatus.SC_MULTIPLE_CHOICES && code < HttpStatus.SC_BAD_REQUEST) || code > HttpStatus.SC_INTERNAL_SERVER_ERROR) { errorMessage = String.format("Unexpected response code: %d", code); result = TransmissionSendResult.REJECTED_BY_SERVER; } else { switch (code) { case HttpStatus.SC_BAD_REQUEST: errorMessage = "Bad request "; result = TransmissionSendResult.BAD_REQUEST; break; case 429: result = TransmissionSendResult.THROTTLED; errorMessage = "Throttling (All messages of the transmission were rejected) "; break; case 439: result = TransmissionSendResult.THROTTLED_OVER_EXTENDED_TIME; errorMessage = "Throttling extended"; break; case 402: result = TransmissionSendResult.PAYMENT_REQUIRED; errorMessage = "Throttling: payment required"; break; case HttpStatus.SC_PARTIAL_CONTENT: result = TransmissionSendResult.PARTIALLY_THROTTLED; errorMessage = "Throttling (Partial messages of the transmission were rejected) "; break; case HttpStatus.SC_INTERNAL_SERVER_ERROR: errorMessage = "Internal server error "; result = TransmissionSendResult.INTERNAL_SERVER_ERROR; break; default: result = TransmissionSendResult.REJECTED_BY_SERVER; errorMessage = String.format("Error, response code: %d", code); break; } } logError(errorMessage, respEntity); return result; } private void logError(String baseErrorMessage, HttpEntity respEntity) { if (respEntity == null || !InternalLogger.INSTANCE.isErrorEnabled()) { InternalLogger.INSTANCE.error(baseErrorMessage); return; } InputStream inputStream = null; try { inputStream = respEntity.getContent(); InputStreamReader streamReader = new InputStreamReader(inputStream, "UTF-8"); BufferedReader reader = new BufferedReader(streamReader); String responseLine = reader.readLine(); respEntity.getContent().close(); InternalLogger.INSTANCE.error("Failed to send, %s : %s", baseErrorMessage, responseLine); } catch (IOException e) { InternalLogger.INSTANCE.error("Failed to send, %s, failed to log the error", baseErrorMessage); } finally { if (inputStream != null) { try { inputStream.close(); } catch (IOException e) { } } } } private HttpPost createTransmissionPostRequest(Transmission transmission) { HttpPost request = new HttpPost(serverUri); request.addHeader(CONTENT_TYPE_HEADER, transmission.getWebContentType()); request.addHeader(CONTENT_ENCODING_HEADER, transmission.getWebContentEncodingType()); ByteArrayEntity bae = new ByteArrayEntity(transmission.getContent()); request.setEntity(bae); return request; } }