/**
* Copyright 2005-2014 Restlet
*
* The contents of this file are subject to the terms of one of the following
* open source licenses: Apache 2.0 or or EPL 1.0 (the "Licenses"). You can
* select the license that you prefer but you may not use this file except in
* compliance with one of these Licenses.
*
* You can obtain a copy of the Apache 2.0 license at
* http://www.opensource.org/licenses/apache-2.0
*
* You can obtain a copy of the EPL 1.0 license at
* http://www.opensource.org/licenses/eclipse-1.0
*
* See the Licenses for the specific language governing permissions and
* limitations under the Licenses.
*
* Alternatively, you can obtain a royalty free commercial license with less
* limitations, transferable or non-transferable, directly at
* http://restlet.com/products/restlet-framework
*
* Restlet is a registered trademark of Restlet S.A.S.
*/
package org.restlet.ext.apispark.internal.agent.module;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.logging.Logger;
import org.restlet.Request;
import org.restlet.Response;
import org.restlet.ext.apispark.internal.ApiSparkConfig;
import org.restlet.ext.apispark.internal.agent.AgentUtils;
import org.restlet.ext.apispark.internal.agent.bean.CallLog;
import org.restlet.ext.apispark.internal.agent.bean.CallLogs;
import org.restlet.ext.apispark.internal.agent.bean.ModulesSettings;
import org.restlet.ext.apispark.internal.agent.resource.AnalyticsResource;
import com.google.common.collect.Lists;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
public class AnalyticsHandler {
/** Internal logger. */
protected static Logger LOGGER = Logger.getLogger(AnalyticsHandler.class
.getName());
/** Maximum number of concurrent call logs post threads */
private static final int THREAD_MAX_NUMBER = 3;
/**
* Number of buffered calls. Asynchronous post of analytics is triggered
* either every POST_PERIOD or when the buffer exceeds this number.
*/
private int bufferSize = 100;
/** Maximum time between two asynchronous call logs post. */
private long postPeriodInSecond = 60;
/**
* Initial time to wait between to attempts to reach the APISpark analytics
* service in milliseconds.
*
* This number is multiplied at each attempt. See
* {@link AsyncCallLogsPostTask#getRetryTime(int)} for more details.
*/
private static final long RETRY_AFTER = 500;
/**
* Maximum number of attempts to reach the APISpark analytics service if
* there are errors before the call logs are lost.
*/
private static final int MAX_ATTEMPTS = 5;
/**
* Maximum time between two attempts to reach the APISpark analytics
* service.
*/
private static final long MAX_TIME = TimeUnit.SECONDS.toMillis(10);
/** Timer trigerring call logs post to APISpark */
private Timer asyncPostTimer;
/** Client resourceused to post call logs to APISpark */
private AnalyticsResource analyticsClientResource;
/** Executor service used for async tasks */
private ExecutorService executorService;
/** List of call logs */
private final List<CallLog> callLogs;
/**
* Create a new analytics handler with the specified settings.
*
* @param apiSparkConfig
* The agent configuration.
* @param modulesSettings
* The modules settings.
*/
public AnalyticsHandler(ApiSparkConfig apiSparkConfig,
ModulesSettings modulesSettings) {
analyticsClientResource = AgentUtils.getClientResource(apiSparkConfig,
modulesSettings, AnalyticsResource.class,
AnalyticsModule.ANALYTICS_PATH);
callLogs = Collections.synchronizedList(Lists
.<CallLog> newArrayListWithExpectedSize(bufferSize));
bufferSize = apiSparkConfig.getAgentAnalyticsBufferSize();
executorService = new ThreadPoolExecutor(1, THREAD_MAX_NUMBER, 0L,
TimeUnit.SECONDS,
new LinkedBlockingQueue<Runnable>(bufferSize),
new ThreadFactoryBuilder().setNameFormat("analytics-poster-%d")
.build());
postPeriodInSecond = apiSparkConfig.getAgentAnalyticsPostPeriodInSecond();
long postPeriodInMs = TimeUnit.SECONDS.toMillis(postPeriodInSecond);
asyncPostTimer = new Timer();
asyncPostTimer.schedule(new TimerTask() {
@Override
public void run() {
flushLogs();
}
}, postPeriodInMs, postPeriodInMs);
}
/**
* Generates a CallLog for the request and adds it to the buffer.
*
* @param request
* The Request object associated with the request.
* @param response
* The Response object associated with the request.
* @param duration
* The duration of the request in milliseconds.
* @param startTime
* The time at which the request arrived to the agent as an
* epoch.
*/
public void addCallLogToBuffer(Request request, Response response,
int duration, long startTime) {
CallLog callLog = new CallLog();
callLog.setDate(new Date(startTime));
callLog.setDuration(duration);
callLog.setMethod(request.getMethod().getName());
callLog.setPath(request.getResourceRef().getPath());
callLog.setRemoteIp(request.getClientInfo().getUpstreamAddress());
callLog.setStatusCode(response.getStatus().getCode());
callLog.setUserAgent(request.getClientInfo().getAgent());
callLog.setUserToken((request.getClientInfo().getUser() == null) ? ""
: request.getClientInfo().getUser().getIdentifier());
callLogs.add(callLog);
if (callLogs.size() >= bufferSize) {
flushLogs();
}
}
/**
* Creates a new Thread that asynchronously posts call logs to APISpark
*/
public void flushLogs() {
if (callLogs.isEmpty()) {
return;
}
CallLogs logsToPost;
synchronized (callLogs) {
if (callLogs.isEmpty()) {
return;
}
logsToPost = new CallLogs(callLogs.size());
logsToPost.addAll(callLogs);
callLogs.clear();
}
postLogs(logsToPost);
}
/**
* Adds a task to the executor service to post call logs to the APISpark
* analytics service.
*
* If the executor service cannot satisfy the request, the call logs are
* lost and an error message is logged with the reason of the failure.
*
* @param logsToPost
* The call logs to post to the APISpark analytics service.
*/
private void postLogs(CallLogs logsToPost) {
try {
executorService.execute(new AsyncCallLogsPostTask(logsToPost));
} catch (RejectedExecutionException e) {
LOGGER.severe("Posting " + logsToPost.size()
+ " call logs failed permanently due to \""
+ e.getCause().getMessage() + "\".");
errorSendLog(logsToPost);
}
}
/**
* Called on permanent errors. Override to add your own behavior.
*
* @param logsToPost
* The list of logs that were not posted.
*/
protected void errorSendLog(CallLogs logsToPost) {
// do nothing
}
/**
* Asynchronous task posting the call logs to APISpark and implementing
* fall-back methods if attempts are not successful.
*
* @author Cyprien Quilici
*
*/
private class AsyncCallLogsPostTask implements Runnable {
private CallLogs logsToPost;
public AsyncCallLogsPostTask(CallLogs logsToPost) {
this.logsToPost = logsToPost;
}
@Override
public void run() {
for (int attemptNumber = 1; attemptNumber <= MAX_ATTEMPTS + 1; attemptNumber++) {
try {
analyticsClientResource.postLogs(logsToPost);
LOGGER.fine(logsToPost.size()
+ " call logs sent to the analytics service.");
break;
} catch (Exception e) {
if (attemptNumber == MAX_ATTEMPTS) {
LOGGER.severe("Posting " + logsToPost.size()
+ " call logs failed permanently after "
+ MAX_ATTEMPTS + " attempts.");
errorSendLog(logsToPost);
} else {
LOGGER.warning("Error sending "
+ logsToPost.size()
+ " call logs to the analytics service during attempt n°"
+ attemptNumber + " because \""
+ e.getMessage() + "\".");
try {
Thread.sleep(getRetryTime(attemptNumber));
} catch (InterruptedException e1) {
// ignore
}
}
}
}
}
/**
* Returns the time to wait between two attempts to reach the APISpark
* analytics service.
*
* It is multiplied by 2 each attempt with a maximum limit of
* {@link AnalyticsHandler#MAX_TIME}.
*
* @param attemptNumber
* The number of the attempt.
* @return The time to wait between two attempts to reach the APISpark
* analytics service.
*/
private long getRetryTime(int attemptNumber) {
long newTime = RETRY_AFTER
* ((int) Math.pow(2.0, attemptNumber - 1));
return Math.min(newTime, MAX_TIME);
}
}
public synchronized void stop() throws Exception {
asyncPostTimer.cancel();
executorService.shutdown();
}
}