Files
perfdb/pdb-ui/src/main/java/org/lucares/pdbui/PdbController.java
Andreas Huber bfcbd0a451 draw better dashboard images
Scaling big plots to small thumbnails results in bad images that barely
show any details.
We solve this by calling gnuplot a second time to generate the
thumbnails. They don't have any labels and are rendered in the required
size, so that not scaling is necessary.
Thumbnails have to be requested explicitly, because it can be expensive
to compute them.
2018-05-01 07:54:10 +02:00

276 lines
10 KiB
Java

package org.lucares.pdbui;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.text.Collator;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.SortedSet;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
import org.apache.commons.lang3.StringUtils;
import org.lucares.pdb.datastore.Proposal;
import org.lucares.pdb.plot.api.AxisScale;
import org.lucares.pdb.plot.api.Limit;
import org.lucares.pdb.plot.api.PlotSettings;
import org.lucares.pdb.plot.api.PlotType;
import org.lucares.pdbui.domain.Aggregate;
import org.lucares.pdbui.domain.AutocompleteProposal;
import org.lucares.pdbui.domain.AutocompleteProposalByValue;
import org.lucares.pdbui.domain.AutocompleteResponse;
import org.lucares.pdbui.domain.PlotRequest;
import org.lucares.pdbui.domain.PlotResponse;
import org.lucares.pdbui.domain.PlotResponseStats;
import org.lucares.performance.db.PerformanceDb;
import org.lucares.recommind.logs.InternalPlottingException;
import org.lucares.recommind.logs.NoDataPointsException;
import org.lucares.recommind.logs.PlotResult;
import org.lucares.recommind.logs.Plotter;
import org.lucares.utils.CollectionUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.http.CacheControl;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.util.StreamUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;
@Controller
@EnableAutoConfiguration
public class PdbController implements HardcodedValues, PropertyKeys {
private static final Logger LOGGER = LoggerFactory.getLogger(PdbController.class);
private final Plotter plotter;
private final PerformanceDb db;
private final ReentrantLock plotterLock = new ReentrantLock();
@Value("${" + PRODUCTION_MODE + ":true}")
private boolean modeProduction;
@Value("${" + CACHE_IMAGES_DURATION_SECONDS + ":" + CACHE_IMAGES_DURATION_SECONDS_DEFAULT + "}")
private int cacheDurationInSeconds;
public PdbController(final PerformanceDb db, final Plotter plotter) {
this.db = db;
this.plotter = plotter;
}
@GetMapping("/")
public ModelAndView index() {
final String view = "main";
final Map<String, Object> model = new HashMap<>();
// model.put("oldestValue",
// LocalDateTime.now().minusDays(7).format(DATE_FORMAT_BEGIN));
// model.put("latestValue", LocalDateTime.now().format(DATE_FORMAT_END));
model.put("isProduction", modeProduction);
return new ModelAndView(view, model);
}
@RequestMapping(path = "/plots", //
method = RequestMethod.GET, //
consumes = MediaType.APPLICATION_JSON_UTF8_VALUE, //
produces = MediaType.APPLICATION_JSON_UTF8_VALUE //
)
@ResponseBody
ResponseEntity<PlotResponse> createPlotGet(@RequestParam(name = "request") final String request)
throws InternalPlottingException, InterruptedException, JsonParseException, JsonMappingException,
IOException {
final ObjectMapper objectMapper = new ObjectMapper();
final PlotRequest plotRequest = objectMapper.readValue(request, PlotRequest.class);
return createPlot(plotRequest);
}
@RequestMapping(path = "/plots", //
method = RequestMethod.POST, //
consumes = MediaType.APPLICATION_JSON_UTF8_VALUE, //
produces = MediaType.APPLICATION_JSON_UTF8_VALUE //
)
@ResponseBody
ResponseEntity<PlotResponse> createPlot(@RequestBody final PlotRequest request)
throws InternalPlottingException, InterruptedException {
final PlotSettings plotSettings = PlotSettingsTransformer.toSettings(request);
if (StringUtils.isBlank(plotSettings.getQuery())) {
throw new BadRequest("The query must not be empty!");
}
// TODO the UI should cancel requests that are in flight before sending a plot
// request
if (plotterLock.tryLock(5, TimeUnit.SECONDS)) {
try {
final PlotResult result = plotter.plot(plotSettings);
final String imageUrl = WEB_IMAGE_OUTPUT_PATH + "/" + result.getImageName();
LOGGER.trace("image url: {}", imageUrl);
final String thumbnailUrl = result.getThumbnailPath() != null
? WEB_IMAGE_OUTPUT_PATH + "/" + result.getThumbnailName()
: "img/no-thumbnail.png";
final PlotResponseStats stats = PlotResponseStats.fromDataSeries(result.getDataSeries());
final PlotResponse plotResponse = new PlotResponse(stats, imageUrl, thumbnailUrl);
final CacheControl cacheControl = CacheControl.maxAge(cacheDurationInSeconds, TimeUnit.SECONDS);
return ResponseEntity.ok().cacheControl(cacheControl).body(plotResponse);
} catch (final NoDataPointsException e) {
throw new NotFoundException(e);
} finally {
plotterLock.unlock();
}
} else {
throw new ServiceUnavailableException("Too many parallel requests!");
}
}
@RequestMapping(path = "/plots", //
method = RequestMethod.GET, //
produces = MediaType.APPLICATION_OCTET_STREAM_VALUE //
)
StreamingResponseBody createPlotImage(@RequestParam(name = "query", defaultValue = "") final String query,
@RequestParam(name = "groupBy[]", defaultValue = "") final List<String> aGroupBy,
@RequestParam(name = "limitBy.number", defaultValue = "10") final int limit,
@RequestParam(name = "limitBy.selected", defaultValue = "NO_LIMIT") final Limit limitBy,
@RequestParam(name = "dateFrom", defaultValue = "") final String dateFrom,
@RequestParam(name = "dateRange", defaultValue = "1 week") final String dateRange,
@RequestParam(name = "axisScale", defaultValue = "LINEAR") final AxisScale axisScale,
@RequestParam(name = "plotType", defaultValue = "SCATTER") final PlotType plotType,
@RequestParam(name = "aggregate", defaultValue = "NONE") final Aggregate aggregate,
@RequestParam(name = "keyOutside", defaultValue = "false") final boolean keyOutside,
@RequestParam(name = "width", defaultValue = "1920") final int hidth,
@RequestParam(name = "height", defaultValue = "1080") final int height) {
return (final OutputStream outputStream) -> {
if (StringUtils.isBlank(query)) {
throw new BadRequest("The query must not be empty!");
}
final PlotSettings plotSettings = new PlotSettings();
plotSettings.setQuery(query);
plotSettings.setGroupBy(aGroupBy);
plotSettings.setHeight(height);
plotSettings.setWidth(hidth);
plotSettings.setLimit(limit);
plotSettings.setLimitBy(limitBy);
plotSettings.setDateFrom(dateFrom);
plotSettings.setDateRange(dateRange);
plotSettings.setYAxisScale(axisScale);
plotSettings.setPlotType(plotType);
plotSettings.setAggregate(PlotSettingsTransformer.toAggregateInternal(aggregate));
plotSettings.setKeyOutside(keyOutside);
plotSettings.setGenerateThumbnail(false);
if (plotterLock.tryLock()) {
try {
final PlotResult result = plotter.plot(plotSettings);
try (FileInputStream in = new FileInputStream(result.getImagePath().toFile())) {
StreamUtils.copy(in, outputStream);
}
} catch (final NoDataPointsException e) {
throw new NotFoundException(e);
} catch (final InternalPlottingException e) {
throw new InternalServerError(e);
} finally {
plotterLock.unlock();
}
} else {
throw new ServiceUnavailableException("Too many parallel requests!");
}
};
}
@RequestMapping(path = "/autocomplete", //
method = RequestMethod.GET, //
produces = MediaType.APPLICATION_JSON_UTF8_VALUE //
)
@ResponseBody
AutocompleteResponse autocomplete(@RequestParam(name = "query") final String query,
@RequestParam(name = "caretIndex") final int caretIndex) {
final AutocompleteResponse result = new AutocompleteResponse();
final int zeroBasedCaretIndex = caretIndex - 1;
final List<Proposal> proposals = db.autocomplete(query, zeroBasedCaretIndex);
final List<Proposal> nonEmptyProposals = CollectionUtils.filter(proposals, p -> p.hasResults());
final List<AutocompleteProposal> autocompleteProposals = toAutocompleteProposals(nonEmptyProposals);
Collections.sort(autocompleteProposals, new AutocompleteProposalByValue());
result.setProposals(autocompleteProposals);
return result;
}
@RequestMapping(path = "/fields", //
method = RequestMethod.GET, //
consumes = MediaType.APPLICATION_JSON_UTF8_VALUE, //
produces = MediaType.APPLICATION_JSON_UTF8_VALUE //
)
@ResponseBody
List<String> fields() {
final List<String> fields = db.getFields();
fields.sort(Collator.getInstance(Locale.ENGLISH));
return fields;
}
@RequestMapping(path = "/fields/{fieldName}/values", //
method = RequestMethod.GET, //
consumes = MediaType.APPLICATION_JSON_UTF8_VALUE, //
produces = MediaType.APPLICATION_JSON_UTF8_VALUE //
)
@ResponseBody
SortedSet<String> fields(@PathVariable(name = "fieldName") final String fieldName,
@RequestParam(name = "query") final String query) {
final SortedSet<String> fields = db.getFieldsValues(query, fieldName);
return fields;
}
private List<AutocompleteProposal> toAutocompleteProposals(final List<Proposal> proposals) {
final List<AutocompleteProposal> result = new ArrayList<>();
for (final Proposal proposal : proposals) {
final AutocompleteProposal e = new AutocompleteProposal();
e.setValue(proposal.getProposedTag());
e.setNewQuery(proposal.getNewQuery());
e.setNewCaretPosition(proposal.getNewCaretPosition());
result.add(e);
}
return result;
}
}