Java tutorial
/* * Copyright 2013 Robert Gacki <robert.gacki@cgi.com> * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package de.otto.mongodb.profiler.web; import com.google.gson.JsonArray; import com.google.gson.JsonObject; import com.google.gson.JsonPrimitive; import de.otto.mongodb.profiler.AverageMeasure; import de.otto.mongodb.profiler.ChronoSampler; import de.otto.mongodb.profiler.ProfiledDatabase; import de.otto.mongodb.profiler.ProfilerService; import de.otto.mongodb.profiler.op.DocumentMoveMeasure; import de.otto.mongodb.profiler.op.OpProfile; import de.otto.mongodb.profiler.op.OpProfiler; import de.otto.mongodb.profiler.op.UpdateProfile; import org.joda.time.DateTime; import org.springframework.http.HttpEntity; import org.springframework.http.MediaType; import org.springframework.stereotype.Controller; import org.springframework.validation.BindingResult; import org.springframework.web.bind.annotation.*; import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.View; import org.springframework.web.servlet.view.RedirectView; import org.springframework.web.util.UriComponentsBuilder; import javax.validation.Valid; import java.math.BigDecimal; import java.util.*; import java.util.concurrent.TimeUnit; @Controller @RequestMapping(OpProfileController.CONTROLLER_RESOURCE) public class OpProfileController extends AbstractController { public static final String CONTROLLER_RESOURCE = "/connections/{connectionId:.+}/databases/{databaseName:.+}/ops"; private final int maximumGraphValues = 3_000; public OpProfileController(ProfilerService profilerService) { super(profilerService); } @RequestMapping(method = RequestMethod.GET, produces = MediaType.TEXT_HTML_VALUE) public View showOpProfiles(@PathVariable("connectionId") final String connectionId, @PathVariable("databaseName") final String databaseName, final UriComponentsBuilder uriComponentsBuilder) throws ResourceNotFoundException { final String uri = uriComponentsBuilder.path("/connections/{connectionId}/databases/{databaseName}") .fragment("OpProfiles").buildAndExpand(connectionId, databaseName).toUriString(); return new RedirectView(uri); } @RequestMapping(method = RequestMethod.GET, produces = MediaType.TEXT_HTML_VALUE, params = "render=panel") public ModelAndView showOpProfilesPanelFragment(@PathVariable("connectionId") final String connectionId, @PathVariable("databaseName") final String databaseName, @RequestParam(value = "renderType", required = false) final String renderTypeValue, Locale locale) throws ResourceNotFoundException { final OpProfilesPanelViewModel.RenderType renderType; if (renderTypeValue == null || renderTypeValue.isEmpty()) { renderType = OpProfilesPanelViewModel.RenderType.PANEL; } else { renderType = OpProfilesPanelViewModel.RenderType.valueOf(renderTypeValue.toUpperCase(Locale.ENGLISH)); } final ProfiledDatabase database = requireDatabase(connectionId, databaseName); final OpProfiler opProfiler = getProfilerService().getOpProfiler(database); final OpProfilesPanelViewModel viewModel = new OpProfilesPanelViewModel(connectionId, databaseName, opProfiler, renderType, locale); return new ModelAndView("fragment.op-profiles-panel").addObject("model", viewModel); } @Page(mainNavigation = MainNavigation.DATABASES) @RequestMapping(value = "/{id:.+}", method = RequestMethod.GET, produces = MediaType.TEXT_HTML_VALUE) public ModelAndView showOpProfile(@PathVariable("connectionId") final String connectionId, @PathVariable("databaseName") final String databaseName, @PathVariable("id") final String id, Locale locale) throws ResourceNotFoundException { final ProfiledDatabase database = requireDatabase(connectionId, databaseName); final OpProfile profile = requireProfile(database, id); final OpProfilePageViewModel viewModel = new OpProfilePageViewModel(profile, database, locale); return new ModelAndView("page.op-profile").addObject("model", viewModel); } private static final class MovedDocumentsSample { public final long time; public final long normal; public final long moved; public final BigDecimal ratio; private MovedDocumentsSample(long time, long normal, long moved, BigDecimal ratio) { this.time = time; this.normal = normal; this.moved = moved; this.ratio = ratio; } } private static final ChronoSampler.Reduction<DocumentMoveMeasure.Mark, MovedDocumentsSample> MOVED_DOCUMENTS_REDUCTION = new ChronoSampler.Reduction<DocumentMoveMeasure.Mark, MovedDocumentsSample>() { @Override public MovedDocumentsSample reduce(long time, Deque<DocumentMoveMeasure.Mark> marks) { if (marks.isEmpty()) { return null; } long moved = 0; long normal = 0; for (DocumentMoveMeasure.Mark mark : marks) { if (mark.moved) { moved += 1; } else { normal += 1; } } return new MovedDocumentsSample(time, normal, moved, DocumentMoveMeasure.ratio(normal, moved)); } }; @RequestMapping(value = "/{id:.+}/chart-data/moved-documents", method = RequestMethod.GET, produces = { JSON_TYPE_1, JSON_TYPE_2 }) public HttpEntity<String> getMovedDocumentChartData(@PathVariable("connectionId") final String connectionId, @PathVariable("databaseName") final String databaseName, @PathVariable("id") final String id, @RequestParam(value = "sampleRate", required = false) Long sampleRate) throws ResourceNotFoundException { final ProfiledDatabase database = requireDatabase(connectionId, databaseName); final OpProfile profile = requireProfile(database, id); if (!(profile instanceof UpdateProfile)) { throw new ResourceNotFoundException("No update profile found!"); } if (sampleRate == null) { sampleRate = 15L; } final DocumentMoveMeasure measure = ((UpdateProfile) profile).getMoveMeasure(); final ChronoSampler<DocumentMoveMeasure.Mark, MovedDocumentsSample> sampler = new ChronoSampler<>( sampleRate, TimeUnit.MINUTES, MOVED_DOCUMENTS_REDUCTION, lowerBoundary(-24)); for (DocumentMoveMeasure.Mark mark : measure.getMarks()) { sampler.add(mark.time, mark); } final List<MovedDocumentsSample> samples = sampler.finish(); final JsonArray ratioValues = new JsonArray(); final JsonArray totalValues = new JsonArray(); final JsonArray movedValues = new JsonArray(); for (MovedDocumentsSample sample : samples) { final JsonArray ratioValue = new JsonArray(); ratioValue.add(new JsonPrimitive(Long.valueOf(sample.time))); ratioValue.add(new JsonPrimitive( Integer.valueOf(sample.ratio.movePointRight(2).setScale(0, BigDecimal.ROUND_UP).intValue()))); ratioValues.add(ratioValue); final JsonArray totalValue = new JsonArray(); totalValue.add(new JsonPrimitive(Long.valueOf(sample.time))); totalValue.add(new JsonPrimitive(Long.valueOf(sample.normal + sample.moved))); totalValues.add(totalValue); final JsonArray movedValue = new JsonArray(); movedValue.add(new JsonPrimitive(Long.valueOf(sample.time))); movedValue.add(new JsonPrimitive(Long.valueOf(sample.moved))); movedValues.add(movedValue); } final JsonObject ratioJson = new JsonObject(); ratioJson.add("key", new JsonPrimitive("Ratio")); ratioJson.add("values", ratioValues); final JsonObject totalJson = new JsonObject(); totalJson.add("key", new JsonPrimitive("Total")); totalJson.add("values", totalValues); final JsonObject movedJson = new JsonObject(); movedJson.add("key", new JsonPrimitive("Moved")); movedJson.add("values", movedValues); final JsonObject json = new JsonObject(); json.add("ratio", ratioJson); json.add("total", totalJson); json.add("moved", movedJson); return new HttpEntity<>(json.toString()); } @RequestMapping(value = "/control", method = RequestMethod.POST, params = "action=startProfiling", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE) public View startProfiling(@PathVariable("connectionId") final String connectionId, @PathVariable("databaseName") final String databaseName, final UriComponentsBuilder uriComponentsBuilder) throws ResourceNotFoundException { getProfilerService().getOpProfiler(requireDatabase(connectionId, databaseName)).continueProfiling(); return new RedirectView(uriComponentsBuilder.path("/connections/{connectionId}/databases/{databaseName}") .fragment("OpProfiles").buildAndExpand(connectionId, databaseName).toUriString()); } @RequestMapping(value = "/control", method = RequestMethod.POST, params = "action=stopProfiling", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE) public View stopProfiling(@PathVariable("connectionId") final String connectionId, @PathVariable("databaseName") final String databaseName, final UriComponentsBuilder uriComponentsBuilder) throws ResourceNotFoundException { getProfilerService().getOpProfiler(requireDatabase(connectionId, databaseName)).stopProfiling(); return new RedirectView(uriComponentsBuilder.path("/connections/{connectionId}/databases/{databaseName}") .fragment("OpProfiles").buildAndExpand(connectionId, databaseName).toUriString()); } @RequestMapping(value = "/control", method = RequestMethod.POST, params = "action=reset", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE) public View resetProfiles(@PathVariable("connectionId") final String connectionId, @PathVariable("databaseName") final String databaseName, final UriComponentsBuilder uriComponentsBuilder) throws ResourceNotFoundException { getProfilerService().getOpProfiler(requireDatabase(connectionId, databaseName)).reset(); return new RedirectView(uriComponentsBuilder.path("/connections/{connectionId}/databases/{databaseName}") .fragment("OpProfiles").buildAndExpand(connectionId, databaseName).toUriString()); } @RequestMapping(value = "/control", method = RequestMethod.POST, params = "action=changeProfilingLevel", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE) public View changeProfilingLevel(@PathVariable("connectionId") final String connectionId, @PathVariable("databaseName") final String databaseName, @RequestParam(value = "level") final Integer levelValue, final UriComponentsBuilder uriComponentsBuilder) throws ResourceNotFoundException { final OpProfiler.ProfilingLevel level = OpProfiler.ProfilingLevel.forValue(levelValue); if (level != null) { getProfilerService().getOpProfiler(requireDatabase(connectionId, databaseName)) .setProfilingLevel(level); } return new RedirectView(uriComponentsBuilder.path("/connections/{connectionId}/databases/{databaseName}") .fragment("OpProfiles").buildAndExpand(connectionId, databaseName).toUriString()); } @RequestMapping(value = "/control", method = RequestMethod.POST, params = "action=explainNextQueries", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE) public View explainNextQueries(@PathVariable("connectionId") final String connectionId, @PathVariable("databaseName") final String databaseName, final UriComponentsBuilder uriComponentsBuilder) throws Exception { getProfilerService().getOpProfiler(requireDatabase(connectionId, databaseName)).explainNextQueries(); return new RedirectView(uriComponentsBuilder.path("/connections/{connectionId}/databases/{databaseName}") .fragment("OpProfiles").buildAndExpand(connectionId, databaseName).toUriString()); } @Page @RequestMapping(value = "/control", method = RequestMethod.GET, params = "action=changeOutlierConfiguration", produces = MediaType.TEXT_HTML_VALUE) public ModelAndView showOutlierConfiguration(@PathVariable("connectionId") final String connectionId, @PathVariable("databaseName") final String databaseName) throws ResourceNotFoundException { final ProfiledDatabase database = requireDatabase(connectionId, databaseName); final OpProfile.OutlierConfiguration config = getProfilerService().getOpProfiler(database) .getOutlierConfiguration(); final OpOutlierConfigurationFormModel model = new OpOutlierConfigurationFormModel(); if (config != null) { model.setEnabled(true); model.setIgnoreOutliers(config.ignoreOutliers()); model.setCaptureOutliers(config.captureOutliers()); } return new ModelAndView("page.op-outlierconfig") .addObject("model", new OpOutlierConfigurationPageViewModel(database, config != null)) .addObject("configuration", model); } @Page @RequestMapping(value = "/control", method = RequestMethod.POST, params = "action=changeOutlierConfiguration", produces = MediaType.TEXT_HTML_VALUE, consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE) public ModelAndView changeOutlierConfiguration(@PathVariable("connectionId") final String connectionId, @PathVariable("databaseName") final String databaseName, @Valid @ModelAttribute("configuration") final OpOutlierConfigurationFormModel model, final BindingResult bindingResult, final UriComponentsBuilder uriComponentsBuilder) throws ResourceNotFoundException { final ProfiledDatabase database = requireDatabase(connectionId, databaseName); final OpProfiler opProfiler = getProfilerService().getOpProfiler(database); if (bindingResult.hasErrors()) { final boolean outlierConfiEnabled = model.isEnabled() || getProfilerService().getOpProfiler(database).getOutlierConfiguration() != null; return new ModelAndView("page.op-outlierconfig") .addObject("model", new OpOutlierConfigurationPageViewModel(database, outlierConfiEnabled)) .addObject("configuration", model); } final OpProfile.OutlierConfiguration outlierConfig; if (model.isEnabled()) { outlierConfig = new OpProfile.OutlierConfiguration() { @Override public boolean ignoreOutliers() { return Boolean.TRUE.equals(model.getIgnoreOutliers()); } @Override public boolean captureOutliers() { return Boolean.TRUE.equals(model.getCaptureOutliers()); } @Override public AverageMeasure.OutlierStrategy createStrategy() { return model.getStrategy().createStrategy(model); } }; } else { outlierConfig = null; } opProfiler.setOutlierConfiguration(outlierConfig); opProfiler.reset(); return new ModelAndView(new RedirectView( uriComponentsBuilder.path("/connections/{connectionId}/databases/{databaseName}/ops") .queryParam("action", "changeOutlierConfiguration") .buildAndExpand(connectionId, databaseName).toUriString())); } @RequestMapping(value = "/{id:.+}/issues", method = RequestMethod.POST, consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE) public View dismissIssue(@PathVariable("connectionId") final String connectionId, @PathVariable("databaseName") final String databaseName, @PathVariable("id") final String id, @RequestParam("dismiss") final String issueCode, final UriComponentsBuilder uriComponentsBuilder) throws Exception { requireProfile(requireDatabase(connectionId, databaseName), id).dismissIssue(issueCode); return new RedirectView( uriComponentsBuilder.path("/connections/{connectionId}/databases/{databaseName}/ops/{id}") .buildAndExpand(connectionId, databaseName, id).toUriString()); } @RequestMapping(value = "/{id:.+}/stats", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE) public HttpEntity<String> getStatsData(@PathVariable("connectionId") final String connectionId, @PathVariable("databaseName") final String databaseName, @PathVariable("id") final String id, @RequestParam(value = "datum", required = false) final List<String> datum, @RequestParam(value = "from", required = false) final Long from, @RequestParam(value = "to", required = false) final Long to, @RequestParam(value = "min", required = false) final Double min, @RequestParam(value = "max", required = false) final Double max, @RequestParam(value = "limit", required = false) final Integer limit) throws Exception { final ProfiledDatabase database = requireDatabase(connectionId, databaseName); final OpProfile profile = requireProfile(database, id); final JsonObject json = new JsonObject(); final JsonObject outliersJson = new JsonObject(); final JsonObject params = new JsonObject(); outliersJson.add("params", params); final JsonArray datumParamValues = new JsonArray(); params.add("datum", datumParamValues); if (datum != null) { for (String datumValue : datum) { datumParamValues.add(new JsonPrimitive(datumValue)); } } final DateTime lowerTimeBound = from != null ? new DateTime(from.longValue()) : null; final DateTime upperTimeBound = to != null ? new DateTime(to.longValue()) : null; if (lowerTimeBound != null && upperTimeBound != null && !upperTimeBound.isAfter(lowerTimeBound)) { throw new IllegalArgumentException("'to' must not be lower than or equal to 'from'"); } params.add("from", from != null ? new JsonPrimitive(from.longValue()) : null); params.add("to", to != null ? new JsonPrimitive(to.longValue()) : null); if (min != null && max != null && !(max.longValue() > min.longValue())) { throw new IllegalArgumentException("'max' must not be lower than or equal to 'min'"); } params.add("min", min != null ? new JsonPrimitive(min.intValue()) : null); params.add("max", max != null ? new JsonPrimitive(max.intValue()) : null); if (limit != null && limit.intValue() < 1) { throw new IllegalArgumentException("'limit' must not be lower than 1"); } final int maximumValues = limit != null && limit.intValue() > 0 ? limit.intValue() : maximumGraphValues; params.add("limit", new JsonPrimitive(maximumValues)); if (profile.getOutlierConfiguration() != null && profile.getOutlierConfiguration().captureOutliers()) { final OutlierGraphDataBuilder builder = new OutlierGraphDataBuilder(datum, maximumValues, lowerTimeBound, upperTimeBound, min, max); builder.add("executionTimeOutliers", "Execution time outliers", profile.getAverageMillisOutliers()); builder.add("readLockAcquisitionTimeOutliers", "Read lock acquisition time outliers", profile.getAverageAcquireReadLockMicrosOutliers()); builder.add("writeLockAcquisitionTimeOutliers", "Write lock acquisition time outliers", profile.getAverageAcquireWriteLockMicrosOutliers()); builder.add("readLockTimeOutliers", "Read lock time outliers", profile.getAverageLockedReadMicrosOutliers()); builder.add("writeLockTimeOutliers", "Write lock time outliers", profile.getAverageLockedWriteMicrosOutliers()); outliersJson.add("data", builder.getGroups()); outliersJson.add("lowestTime", new JsonPrimitive(builder.getLowestTime())); outliersJson.add("highestTime", new JsonPrimitive(builder.getHighestTime())); final JsonArray warningJson = new JsonArray(); if (!builder.getKeysWithTooMuchData().isEmpty()) { final StringBuilder warning = new StringBuilder(); final JsonArray tooMuchDataFor = new JsonArray(); for (String key : builder.getKeysWithTooMuchData()) { tooMuchDataFor.add(new JsonPrimitive(key)); if (warning.length() > 0) { warning.append(", "); } warning.append(key); } warningJson.add(new JsonPrimitive(String.format( "The maximum amount of %d values has been exceeded. " + "The following data could not be (completely) returned: %s. " + "Please narrow your filter to get applicable results.", maximumValues, warning.toString()))); } if (warningJson.size() > 0) { outliersJson.add("warnings", warningJson); } } json.add("outliers", outliersJson); return new HttpEntity<>(json.toString()); } protected OpProfile requireProfile(final ProfiledDatabase database, final String id) throws ResourceNotFoundException { final OpProfiler profiler = getProfilerService().getOpProfiler(database); if (profiler == null) { throw new IllegalStateException( String.format("The CollectionProfiler is required to exist for database [%s]!", database)); } final OpProfile profile = profiler.getProfile(id); if (profile == null) { throw new ResourceNotFoundException( String.format("No query profile found for ID [%s] of database [%s]!", id, database)); } return profile; } private static class OutlierGraphDataBuilder { private final List<String> requestedData; private final JsonArray groups; private final Long lowerTimeBound; private final Long upperTimeBound; private final Double lowerValueBound; private final Double upperValueBound; private final int limit; private int valuesLeft; private List<String> keysWithTooMuchData; private long lowestTime = Long.MAX_VALUE; private long highestTime = Long.MIN_VALUE; private OutlierGraphDataBuilder(final List<String> requestedData, final int limit, final DateTime lowerTimeBound, final DateTime upperTimeBound, final Double lowerValueBound, final Double upperValueBound) { if (lowerTimeBound != null && upperTimeBound != null && lowerTimeBound.isAfter(upperTimeBound)) { throw new IllegalArgumentException("'lowerTimeBound' must be lower than 'upperTimeBound'"); } this.requestedData = requestedData; this.groups = new JsonArray(); this.limit = this.valuesLeft = limit; this.keysWithTooMuchData = new ArrayList<>(); this.lowerTimeBound = lowerTimeBound != null ? lowerTimeBound.getMillis() : null; this.upperTimeBound = upperTimeBound != null ? upperTimeBound.getMillis() : null; this.lowerValueBound = lowerValueBound; this.upperValueBound = upperValueBound; } public void add(String dataType, String key, Collection<AverageMeasure.Outlier> outliers) { // No data available if (outliers == null || outliers.isEmpty()) { return; } // Data was requested by the user? if (requestedData != null && !requestedData.contains(dataType)) { return; } // Too much data for the limit, then don't display any other group? if (groups.size() > 0 && (valuesLeft <= 0 || valuesLeft < outliers.size())) { keysWithTooMuchData.add(key); return; } final JsonArray jsonValues = new JsonArray(); for (AverageMeasure.Outlier outlier : outliers) { if (valuesLeft == 0) { keysWithTooMuchData.add(key); break; } final long time = outlier.getTime(); if ((lowerTimeBound != null && lowerTimeBound.longValue() > time) || (upperTimeBound != null && upperTimeBound.longValue() < time)) { continue; } final double value = outlier.getValue(); if ((lowerValueBound != null && lowerValueBound.longValue() > value) || (upperValueBound != null && upperValueBound.longValue() < value)) { continue; } final JsonObject json = new JsonObject(); json.add("x", new JsonPrimitive(time)); json.add("y", new JsonPrimitive(outlier.getValue())); jsonValues.add(json); valuesLeft -= 1; if (time < lowestTime) { lowestTime = outlier.getTime(); } if (time > highestTime) { highestTime = outlier.getTime(); } } if (jsonValues.size() > 0) { final JsonObject jsonGroup = new JsonObject(); jsonGroup.add("key", new JsonPrimitive(key)); jsonGroup.add("values", jsonValues); groups.add(jsonGroup); } } private int getLimit() { return limit; } public JsonArray getGroups() { return groups; } public long getLowestTime() { return lowestTime; } public long getHighestTime() { return highestTime; } public List<String> getKeysWithTooMuchData() { return keysWithTooMuchData; } } }