package com.anjlab.ping.pages.job;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.TimeZone;
import org.apache.tapestry5.StreamResponse;
import org.apache.tapestry5.annotations.AfterRender;
import org.apache.tapestry5.annotations.InjectPage;
import org.apache.tapestry5.annotations.Persist;
import org.apache.tapestry5.annotations.Property;
import org.apache.tapestry5.ioc.annotations.Inject;
import org.apache.tapestry5.services.Request;
import org.apache.tapestry5.services.Response;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.anjlab.cubics.BeanValueProvider;
import com.anjlab.cubics.Cube;
import com.anjlab.cubics.FactModel;
import com.anjlab.cubics.aggregate.histogram.Histogram.HistogramMergeStrategy;
import com.anjlab.cubics.aggregate.histogram.HistogramAggregateFactory;
import com.anjlab.cubics.aggregate.histogram.Range;
import com.anjlab.cubics.aggregate.pie.PieAggregateFactory;
import com.anjlab.cubics.coerce.IntegerCoercer;
import com.anjlab.cubics.renders.html.HtmlRender;
import com.anjlab.ping.entities.Job;
import com.anjlab.ping.entities.JobResult;
import com.anjlab.ping.filters.BackupJobResultsFilter;
import com.anjlab.ping.pages.Index;
import com.anjlab.ping.services.Application;
import com.anjlab.ping.services.JobResultCSVExporter;
import com.anjlab.ping.services.JobResultsAnalyzer;
import com.anjlab.ping.services.Mailer;
import com.anjlab.ping.services.Utils;
import com.anjlab.ping.services.location.IPResolver;
import com.anjlab.ping.services.location.Location;
import com.anjlab.ping.services.location.LocationResolver;
import com.google.appengine.api.users.UserService;
import com.google.appengine.api.users.UserServiceFactory;
public class Analytics {
private static final Logger logger = LoggerFactory.getLogger(Analytics.class);
private static final String CSV = "csv";
@Property
private Job job;
private Date dateFrom;
private Date dateTo;
public String getTimeDiff() {
return Utils.formatMillisecondsToWordsUpToMinutes(dateTo.getTime() - dateFrom.getTime());
}
public String getDateFrom() {
return application.formatDate(dateFrom);
}
public String getDateTo() {
return application.formatDate(dateTo);
}
public String getClientTime() {
return application.formatDate(new Date());
}
@InjectPage
private Index index;
@SuppressWarnings("unused")
@Property
@Persist
private String message;
@AfterRender
public void cleanup() {
message = null;
job = null;
view = null;
results = null;
}
@Inject
private Application application;
@Inject
private Request request;
public boolean isAdmin() {
UserService userService = UserServiceFactory.getUserService();
return userService.isUserLoggedIn()
&& userService.isUserAdmin();
}
public Index onActivate(Long jobId) {
try {
int defaultEnd = 20000;
try {
this.end = request.getParameterNames().contains("end")
? Long.parseLong(request.getParameter("end"))
: defaultEnd;
} catch(Exception e) {
this.end = defaultEnd;
}
job = application.findJob(jobId);
if (job == null) {
index.operationFailed("Job not found");
return index;
}
// Set default view
if (view == null) {
view = DEFAULT_VIEW;
}
initResults();
} catch (Exception e) {
index.operationFailed(e.getMessage(), e);
return index;
}
return null;
}
public Long[] onPassivate() {
if (job != null) {
return Utils.createJobContext(job);
}
return null;
}
public StreamResponse onActionFromExportCSV() {
return getExport(CSV);
}
private static final String DEFAULT_VIEW = "month > day";
@SuppressWarnings("unused")
@Property
private final String viewModel = DEFAULT_VIEW + ",dayTime > hour,dayOfWeek > month";
@Property
@Persist
private String view;
private double end;
private List<JobResult> results;
public String getCubeHTML() {
TimeZone timeZone = application.getTimeZone();
for (JobResult result : results) {
result.setTimeZone(timeZone);
}
FactModel<JobResult> model = new FactModel<JobResult>(new BeanValueProvider<JobResult>(JobResult.class));
model.setDimensions(view.split(" > "));
model.setMeasures("responseTime", "pingResult");
model.declareCustomAggregate(new PieAggregateFactory<JobResult>(new IntegerCoercer()), "pingResult");
Range[] ranges;
if (end > 1000) {
List<Range> rangeList = new ArrayList<Range>();
ranges = Range.createRanges(0, 100, 1000);
ranges[ranges.length - 1].setRightInclusive(false);
rangeList.addAll(Arrays.asList(ranges));
ranges = Range.createRanges(1000, (end - 1000) / 10, end);
rangeList.addAll(Arrays.asList(ranges));
ranges = rangeList.toArray(ranges);
} else {
ranges = Range.createRanges(0, end / 10, end);
}
model.declareCustomAggregate(
new HistogramAggregateFactory<JobResult>(HistogramMergeStrategy.SameRanges, ranges),
"responseTime");
Cube<JobResult> cube = Cube.createCube(model, results);
HtmlRender<JobResult> render = new HtmlRender<JobResult>(cube);
render.getAggregatesOptions("pingResult").
reorder("pie-" + Job.PING_RESULT_OK + "-%", "count", "pie").
exclude("min", "max", "sum", "avg").
setFormat("pie-" + Job.PING_RESULT_OK + "-%", "%.5f").
setLabel("pie-" + Job.PING_RESULT_OK + "-%", "%").
setLabel("count", "# of pings").
setLabel("pie", "chart");
render.getAggregatesOptions("responseTime").
reorder("avg").
exclude("count").
setLabel("histogram", "chart");
render.getMeasuresOptions().
setLabel("responseTime", "Response Time, ms").
setLabel("pingResult", "Availability");
render.getDimensionsOptions().
setLabel("all", "All").
setLabel("month", "Month").
setLabel("weekOfMonth", "Week Of Month").
setLabel("day", "Day").
setLabel("hour", "Hour").
setLabel("dayTime", "Day Time").
setLabel("dayOfWeek", "Day Of Week");
return render.render().toString();
}
private void initResults() {
results = job.getRecentJobResults2(Application.DEFAULT_NUMBER_OF_JOB_RESULTS);
if (results.size() > 0) {
dateFrom = results.get(0).getTimestamp();
dateTo = results.get(results.size() - 1).getTimestamp();
} else {
dateTo = dateFrom = new Date();
}
}
private StreamResponse getExport(final String format) {
return new StreamResponse() {
@Override
public void prepareResponse(Response response) {
response.setHeader(
"Content-Disposition",
"attachment; filename=" + getFilename());
}
private String getFilename() {
return Utils.getCSVExportFilename(job);
}
@Override
public InputStream getStream() throws IOException {
InputStream csvExport = new ByteArrayInputStream(
JobResultCSVExporter.export(application.getTimeZone(), results));
return csvExport;
}
@Override
public String getContentType() {
return "text/csv";
}
};
}
public void onActionFromRunJob() {
application.runJob(job);
application.updateJob(job, false, false);
message = job.getLastPingSummary();
}
public void onActionFromSendResultsByMail() throws Exception {
BackupJobResultsFilter filter = new BackupJobResultsFilter();
filter.setApplication(application);
filter.sendResultsByMail(
job, job.getRecentJobResults(Integer.MAX_VALUE), Mailer.DMITRY_GUSEV_GMAIL_COM);
}
public String getLocationMetrics() {
Location pingServiceLocation = getPingServiceLocation();
Location pingURLLocation = getJobLocation();
long distance = pingServiceLocation.distanceInMeters(pingURLLocation);
return "It is " + formatDistanceInKilometers(distance)
+ " kilometers from <span class='hoverable' title='"
+ pingServiceLocation + "'>Ping Service</span> to <span class='hoverable' title='"
+ pingURLLocation + "'>"
+ job.getTitleFriendly() + "</span>.";
}
private String formatDistanceInKilometers(long distanceInMeters) {
return String.format("%,d", distanceInMeters / 1000);
}
private Location pingServiceLocation;
@Inject
private LocationResolver locationResolver;
@Inject
private IPResolver ipResolver;
public Location getPingServiceLocation() {
if (pingServiceLocation == null) {
pingServiceLocation = locationResolver.resolveLocation(
ipResolver.resolveIp(Application.PING_SERVICE_PING_URL));
}
return pingServiceLocation;
}
private Location jobLocation;
public Location getJobLocation() {
if (jobLocation == null) {
jobLocation = locationResolver.resolveLocation(ipResolver.resolveIp(job.getPingURL()));
}
return jobLocation;
}
public String getDetailedReport() {
JobResultsAnalyzer analyzer = new JobResultsAnalyzer(results, true);
TimeZone timeZone = application.getTimeZone();
StringBuilder report = analyzer.buildHtmlReport(timeZone);
report.insert(0, "<p>Time Zone: " + timeZone.getDisplayName() + " (" + timeZone.getID() + ")</p>");
return report.toString();
}
public Long[] getJobContext() {
return Utils.createJobContext(job);
}
public void onActionFromDeleteJob() {
try {
// This is admin action
if (isAdmin()) {
application.deleteJob(job.getKey().getId(), false);
}
} catch (Exception e) {
logger.error("Error deleting job", e);
message = "Error deleting job";
}
}
}