package rocks.inspectit.server.service.rest; import static org.springframework.web.bind.annotation.RequestMethod.GET; import java.text.DateFormat; import java.util.ArrayList; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; import javax.servlet.http.HttpServletResponse; import org.apache.commons.lang.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.PathVariable; 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 rocks.inspectit.server.service.rest.error.JsonError; import rocks.inspectit.shared.all.communication.DefaultData; import rocks.inspectit.shared.all.communication.data.ClassLoadingInformationData; import rocks.inspectit.shared.all.communication.data.CpuInformationData; import rocks.inspectit.shared.all.communication.data.ExceptionSensorData; import rocks.inspectit.shared.all.communication.data.HttpTimerData; import rocks.inspectit.shared.all.communication.data.InvocationSequenceData; import rocks.inspectit.shared.all.communication.data.MemoryInformationData; import rocks.inspectit.shared.all.communication.data.SqlStatementData; import rocks.inspectit.shared.all.communication.data.SystemInformationData; import rocks.inspectit.shared.all.communication.data.ThreadInformationData; import rocks.inspectit.shared.all.communication.data.TimerData; import rocks.inspectit.shared.all.exception.BusinessException; import rocks.inspectit.shared.all.exception.enumeration.StorageErrorCodeEnum; import rocks.inspectit.shared.cs.cmr.service.IStorageService; import rocks.inspectit.shared.cs.communication.data.cmr.RecordingData; import rocks.inspectit.shared.cs.indexing.aggregation.impl.SqlStatementDataAggregator; import rocks.inspectit.shared.cs.indexing.aggregation.impl.TimerDataAggregator; import rocks.inspectit.shared.cs.storage.StorageData; import rocks.inspectit.shared.cs.storage.processor.AbstractDataProcessor; import rocks.inspectit.shared.cs.storage.processor.impl.DataAggregatorProcessor; import rocks.inspectit.shared.cs.storage.processor.impl.DataSaverProcessor; import rocks.inspectit.shared.cs.storage.processor.impl.InvocationClonerDataProcessor; import rocks.inspectit.shared.cs.storage.processor.impl.InvocationExtractorDataProcessor; import rocks.inspectit.shared.cs.storage.recording.RecordingProperties; import rocks.inspectit.shared.cs.storage.recording.RecordingState; /** * Restful service provider for storages. * * @author Ivan Senic * */ @Controller @RequestMapping(value = "/storage") public class StorageRestfulService { /** * Reference to the existing {@link IStorageService}. */ @Autowired IStorageService storageService; /** * Handling of all the exceptions happening in this controller. * * @param exception * Exception being thrown * @return {@link ModelAndView} */ @ExceptionHandler(Exception.class) public ModelAndView handleAllException(Exception exception) { return new JsonError(exception).asModelAndView(); } /** * Returns all storages. * <p> * <i> Example URL: /storage</i> * * @return List of all storages. */ @RequestMapping(method = GET, value = "") @ResponseBody public List<StorageData> getAllStorages() { List<StorageData> storages = storageService.getExistingStorages(); return storages; } /** * Returns storage by ID. * <p> * <i> Example URL: /storage/{id}</i> * * @param id * ID bounded from path. * @return One storage or <code>null</code> if the storage with given ID does not exists. */ @RequestMapping(method = GET, value = "{id}") @ResponseBody public StorageData getStorageById(@PathVariable String id) { List<StorageData> storages = storageService.getExistingStorages(); for (StorageData storageData : storages) { if (Objects.equals(id, storageData.getId())) { return storageData; } } return null; } /** * Creates a new storage with given name. * <p> * <i> Example URL: /storage/{name}/create</i> * * @param name * Name of the storage. * @return Map containing message and created storage. * @throws BusinessException * If {@link BusinessException} occurs. */ @RequestMapping(method = GET, value = "{name}/create") @ResponseBody public Object createStorage(@PathVariable String name) throws BusinessException { if (StringUtils.isEmpty(name)) { throw new BusinessException("Create a new storage via storage REST service.", StorageErrorCodeEnum.STORAGE_NAME_IS_NOT_PROVIDED); } StorageData storageData = new StorageData(); storageData.setName(name); storageData = storageService.createAndOpenStorage(storageData); Map<String, Object> resultMap = new HashMap<>(); resultMap.put("message", "Storage successfully created."); resultMap.put("storage", storageData); return resultMap; } /** * Finalize storage by ID. * <p> * <i> Example URL: /storage/{id}/finalize</i> * * @param id * ID bounded from path. * @throws BusinessException * If {@link BusinessException} occurs. * @return Message for the user. */ @RequestMapping(method = GET, value = "{id}/finalize") @ResponseBody public Object finalizeStorage(@PathVariable String id) throws BusinessException { StorageData storageData = new StorageData(); storageData.setId(id); storageService.closeStorage(storageData); return Collections.singletonMap("message", "Storage id " + id + " successfully finalized."); } /** * Deletes storage by ID. * <p> * <i> Example URL: /storage/{id}/delete</i> * * @param id * ID bounded from path. * @throws BusinessException * If {@link BusinessException} occurs. * @return Message for the user. */ @RequestMapping(method = GET, value = "{id}/delete") @ResponseBody public Object deleteStorage(@PathVariable String id) throws BusinessException { StorageData storageData = new StorageData(); storageData.setId(id); storageService.deleteStorage(storageData); return Collections.singletonMap("message", "Storage id " + id + " successfully deleted."); } /** * Returns the current state of the recording. * <p> * <i> Example URL: /storage/state</i> * * @return {@link RecordingState}. */ @RequestMapping(method = GET, value = "state") @ResponseBody public Map<String, Object> getRecordingState() { RecordingState state = storageService.getRecordingState(); Map<String, Object> resultMap = new HashMap<>(); resultMap.put("recordingState", state); if (RecordingState.OFF != state) { // add recording storage RecordingData recordingData = storageService.getRecordingData(); resultMap.put("recordingStorage", recordingData.getRecordingStorage()); if (RecordingState.ON == state) { // add end date if we are having one Date recordingEndDate = recordingData.getRecordEndDate(); if (null != recordingEndDate) { resultMap.put("recordingStopDate", DateFormat.getDateTimeInstance().format(recordingEndDate)); } } // add start date if it's scheduled if (RecordingState.SCHEDULED == state) { resultMap.put("schduledStartDate", DateFormat.getDateTimeInstance().format(recordingData.getRecordStartDate())); } } return resultMap; } /** * Stops recording. * <p> * <i> Example URL: /storage/stop</i> * * @throws BusinessException * If {@link BusinessException} occurs. * @return Message for the user. */ @RequestMapping(method = GET, value = "stop") @ResponseBody public Object stopRecording() throws BusinessException { storageService.stopRecording(); return Collections.singletonMap("message", "Recording stopped."); } /** * Starts recording on the storage with the given ID with some advanced settings. This method * allows specification of the start delay and recording duration in milliseconds. Zero values * for that parameters are omitting them. * <p> * <i> Example URL: /storage/start?id=1&startDelay=30000&recordingDuration=60000 (makes a 30s * delay and records for 60s)</i> * * @param id * Storage ID. * @param startDelay * startDelay in milliseconds or <code>0</code> to ignore * @param recordingDuration * recording duration in milliseconds or <code>0</code> to ignore * @param extractInvocations * If invocations should be extracted. * @param autoFinalize * If storage should be auto-finalized when recording is stopped. * @return Map with informations for the user. * @throws BusinessException * If {@link BusinessException} occurs. */ @RequestMapping(method = RequestMethod.GET, value = "start") @ResponseBody public Object startOrScheduleRecording(@RequestParam(value = "id", required = true) String id, @RequestParam(value = "startDelay", required = false) Long startDelay, @RequestParam(value = "recordingDuration", required = false) Long recordingDuration, @RequestParam(value = "extractInvocations", required = false, defaultValue = "true") Boolean extractInvocations, @RequestParam(value = "autoFinalize", required = false, defaultValue = "true") Boolean autoFinalize) throws BusinessException { if (null == getStorageById(id)) { throw new BusinessException("Start or schedule recording on storage with ID=" + id + " via storage REST service.", StorageErrorCodeEnum.STORAGE_DOES_NOT_EXIST); } StorageData storageData = new StorageData(); storageData.setId(id); // extractInvocations and autoFinalize have default Values and should never be null RecordingProperties recordingProperties = getRecordingProperties(extractInvocations.booleanValue()); recordingProperties.setAutoFinalize(autoFinalize.booleanValue()); if ((null != startDelay) && (startDelay.longValue() > 0)) { recordingProperties.setStartDelay(startDelay.longValue()); } if ((null != recordingDuration) && (recordingDuration.longValue() > 0)) { recordingProperties.setRecordDuration(recordingDuration.longValue()); } StorageData recordingStorage = storageService.startOrScheduleRecording(storageData, recordingProperties); Map<String, Object> resultMap = new HashMap<>(); if (recordingProperties.getStartDelay() > 0) { resultMap.put("message", "Recording scheduled."); } else { resultMap.put("message", "Recording started."); } resultMap.put("recordingStorage", recordingStorage); return resultMap; } /** * Returns the recording properties with correctly set default set of * {@link AbstractDataProcessor}s. * * @param extractInvocations * If invocations should be extracted. * @return {@link RecordingProperties}. */ private RecordingProperties getRecordingProperties(boolean extractInvocations) { RecordingProperties recordingProperties = new RecordingProperties(); List<AbstractDataProcessor> normalProcessors = new ArrayList<>(); // data saver List<Class<? extends DefaultData>> classesToSave = new ArrayList<>(); Collections.addAll(classesToSave, InvocationSequenceData.class, HttpTimerData.class, ExceptionSensorData.class, MemoryInformationData.class, CpuInformationData.class, ClassLoadingInformationData.class, ThreadInformationData.class, SystemInformationData.class); DataSaverProcessor dataSaverProcessor = new DataSaverProcessor(classesToSave, true); normalProcessors.add(dataSaverProcessor); // data aggregators normalProcessors.add(new DataAggregatorProcessor<>(TimerData.class, 5000, new TimerDataAggregator(), true)); normalProcessors.add(new DataAggregatorProcessor<>(SqlStatementData.class, 5000, new SqlStatementDataAggregator(true), true)); // invocations support if (extractInvocations) { List<AbstractDataProcessor> chainedProcessorsForExtractor = new ArrayList<>(); chainedProcessorsForExtractor.addAll(normalProcessors); InvocationExtractorDataProcessor invocationExtractorDataProcessor = new InvocationExtractorDataProcessor(chainedProcessorsForExtractor); normalProcessors.add(invocationExtractorDataProcessor); } normalProcessors.add(new InvocationClonerDataProcessor()); recordingProperties.setRecordingDataProcessors(normalProcessors); return recordingProperties; } /** * Header information for swagger requests. * * @param response * Response information */ @ModelAttribute public void setVaryResponseHeader(HttpServletResponse response) { response.setHeader("Access-Control-Allow-Origin", "*"); response.setHeader("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept"); } }