diff --git a/modules/enterprise/server/jar/src/main/java/org/rhq/enterprise/server/rest/MetricHandlerBean.java b/modules/enterprise/server/jar/src/main/java/org/rhq/enterprise/server/rest/MetricHandlerBean.java index c6b570dd8e0..f3a578a3a52 100644 --- a/modules/enterprise/server/jar/src/main/java/org/rhq/enterprise/server/rest/MetricHandlerBean.java +++ b/modules/enterprise/server/jar/src/main/java/org/rhq/enterprise/server/rest/MetricHandlerBean.java @@ -27,6 +27,7 @@ import java.util.Date; import java.util.HashMap; import java.util.HashSet; +import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; @@ -76,18 +77,25 @@ import org.rhq.core.domain.measurement.MeasurementDataTrait; import org.rhq.core.domain.measurement.MeasurementDefinition; import org.rhq.core.domain.measurement.MeasurementSchedule; +import org.rhq.core.domain.measurement.MeasurementScheduleRequest; +import org.rhq.core.domain.measurement.calltime.CallTimeData; +import org.rhq.core.domain.measurement.calltime.CallTimeDataComposite; import org.rhq.core.domain.measurement.composite.MeasurementDataNumericHighLowComposite; import org.rhq.core.domain.resource.Resource; import org.rhq.core.domain.resource.ResourceType; import org.rhq.core.domain.resource.group.ResourceGroup; +import org.rhq.core.domain.util.PageControl; +import org.rhq.core.domain.util.PageList; import org.rhq.enterprise.server.RHQConstants; import org.rhq.enterprise.server.storage.StorageClientManager; +import org.rhq.enterprise.server.measurement.CallTimeDataManagerLocal; import org.rhq.enterprise.server.measurement.MeasurementDataManagerLocal; import org.rhq.enterprise.server.measurement.MeasurementDefinitionManagerLocal; import org.rhq.enterprise.server.measurement.MeasurementScheduleManagerLocal; import org.rhq.enterprise.server.resource.ResourceManagerLocal; import org.rhq.enterprise.server.resource.group.ResourceGroupManagerLocal; import org.rhq.enterprise.server.rest.domain.Baseline; +import org.rhq.enterprise.server.rest.domain.CallTimeValueRest; import org.rhq.enterprise.server.rest.domain.Datapoint; import org.rhq.enterprise.server.rest.domain.DoubleValue; import org.rhq.enterprise.server.rest.domain.Link; @@ -114,6 +122,8 @@ public class MetricHandlerBean extends AbstractRestBean { static final String NO_RESOURCE_FOR_ID = "If no resource with the passed id exists"; static final String NO_SCHEDULE_FOR_ID = "No schedule with the passed id exists"; + @EJB + CallTimeDataManagerLocal calltimeDataManager; @EJB MeasurementDataManagerLocal dataManager; @EJB @@ -745,6 +755,101 @@ public StreamingOutput getMetricDataRaw(@ApiParam(required = true) @PathParam("s return so; } + @GET + @ApiOperation("Expose callTime data for given scheduleId") + @ApiErrors({ + @ApiError(code = 404, reason = NO_SCHEDULE_FOR_ID) + }) + @Produces({MediaType.APPLICATION_JSON,MediaType.TEXT_HTML,MediaType.APPLICATION_XML}) + @Path("data/{scheduleId}/callTime") + public StreamingOutput getCallTimesForResource( + @ApiParam(required = true) @PathParam("scheduleId") int scheduleId, + @ApiParam(value = "Start time since epoch", defaultValue = "Now - 8h") @QueryParam("startTime") long startTime, + @ApiParam(value = "End time since epoch", defaultValue = "Now") @QueryParam("endTime") long endTime, + @ApiParam(value = "True to return callTimes aggregated by callDestination", defaultValue = "True") @DefaultValue("true") @QueryParam("aggregate") boolean aggregate, + @Context HttpHeaders headers + ) { + MediaType mediaType = headers.getAcceptableMediaTypes().get(0); + MeasurementSchedule schedule = obtainSchedule(scheduleId, false, DataType.CALLTIME); + long now = System.currentTimeMillis(); + if (endTime==0) + endTime = now; + if (startTime==0) + startTime = endTime - EIGHT_HOURS; + + if (startTime == endTime) { + endTime++; // add 1ms, as otherwise the backend fails to find a value at the startTime + } + PageList callTimes; + if (aggregate) { + callTimes = calltimeDataManager.findCallTimeDataForResource(caller, schedule.getId(), startTime, endTime, PageControl.getUnlimitedInstance()); + } else { + callTimes = calltimeDataManager.findCallTimeDataRawForResource(caller, schedule.getId(), startTime, endTime, PageControl.getUnlimitedInstance()); + } + return new CallTimeDataStreamingOutput(mediaType,callTimes); + } + + @PUT + @Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML}) + @Consumes({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML}) + @ApiOperation("Submit a callTime metrics to the server") + @ApiErrors({ + @ApiError(code=404, reason = NO_SCHEDULE_FOR_ID), + @ApiError(code=406, reason = "beginTime is older than 7 days"), + @ApiError(code=406, reason = "callDestination is null"), + @ApiError(code=406, reason = "duration is negative number") + }) + @Path("data/{scheduleId}/callTime") + public Response putCallTimeValues( + @ApiParam("Id of the schedule") @PathParam("scheduleId") int scheduleId, + List callTimes, + @Context HttpHeaders headers, + @Context UriInfo uriInfo + ) { + if (callTimes.size() < 1) { + return Response.ok().build(); + } + MediaType mediaType = headers.getAcceptableMediaTypes().get(0); + MeasurementSchedule schedule = obtainSchedule(scheduleId, false, DataType.CALLTIME); + MeasurementScheduleRequest req = new MeasurementScheduleRequest(schedule); + CallTimeData ctd = new CallTimeData(req); + long now = System.currentTimeMillis(); + long startTime = System.currentTimeMillis(); + long endTime = startTime - SEVEN_DAYS; + int idx=0; + for (CallTimeValueRest v : callTimes) { + // validate + if (v.getCallDestination() == null) { + throw new IllegalArgumentException("Invalid item["+idx+"] "+v+" : callDestination must not be null"); + } + if (v.getDuration()<0) { + throw new IllegalArgumentException("Invalid item["+idx+"] "+v+" : duration must be a positive number"); + } + if (now - SEVEN_DAYS > v.getBeginTime()) { + throw new IllegalArgumentException("Invalid item["+idx+"] "+v+" : beginTime is older than 7 days"); + } + // measure interval, so we can return proper location header + if (v.getBeginTime() < startTime) { + startTime = v.getBeginTime(); + } + if (v.getBeginTime()+v.getDuration() > endTime) { + endTime = v.getBeginTime()+v.getDuration(); + } + ctd.addCallData(v.getCallDestination(), new Date(v.getBeginTime()), v.getDuration()); + idx++; + } + Set data = new HashSet(); + data.add(ctd); + calltimeDataManager.addCallTimeData(data); + UriBuilder uriBuilder = uriInfo.getBaseUriBuilder(); + uriBuilder.path("/metric/data/{scheduleId}/callTime"); + uriBuilder.queryParam("startTime",startTime); + uriBuilder.queryParam("endTime",endTime); + URI uri = uriBuilder.build(scheduleId); + + return Response.created(uri).type(mediaType).build(); + } + @PUT @Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML}) @Consumes({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML}) @@ -954,6 +1059,85 @@ public Response setBaseline(@ApiParam("Id of the schedule") @PathParam("schedule } + private class CallTimeDataStreamingOutput implements StreamingOutput { + private final PageList callTimes; + private final MediaType mediaType; + public CallTimeDataStreamingOutput(MediaType mType, PageList callTimes) { + this.callTimes = callTimes; + this.mediaType= mType; + } + + private void jsonOutput(CallTimeDataComposite c, PrintWriter pw) { + pw.print("{"); + pw.print("\"callDestination\":\""); pw.print(c.getCallDestination()); + pw.print("\",\"minimum\":"); pw.print(c.getMinimum()); + pw.print(",\"maximum\":"); pw.print(c.getMaximum()); + pw.print(",\"average\":"); pw.print(c.getAverage()); + pw.print(",\"total\":"); pw.print(c.getTotal()); + pw.print(",\"count\":"); pw.print(c.getCount()); + pw.print("}"); + pw.flush(); + } + + private void htmlOutput(CallTimeDataComposite c, PrintWriter pw) { + pw.print(""); + pw.print(""); pw.print(c.getCallDestination()); + pw.print(""); pw.print(c.getMinimum()); + pw.print(""); pw.print(c.getMaximum()); + pw.print(""); pw.print(c.getAverage()); + pw.print(""); pw.print(c.getTotal()); + pw.print(""); pw.print(c.getCount()); + pw.print("\n"); + pw.flush(); + } + + private void xmlOutput(CallTimeDataComposite c, PrintWriter pw) { + pw.print(" \n"); + pw.flush(); + } + + @Override + public void write(OutputStream os) throws IOException, WebApplicationException { + PrintWriter pw = new PrintWriter(os); + Iterator i = callTimes.iterator(); + if (mediaType.equals(MediaType.APPLICATION_JSON_TYPE)) { + pw.print("["); + if (i.hasNext()) { + jsonOutput(i.next(), pw); + } + while (i.hasNext()) { + pw.print(",\n"); + jsonOutput(i.next(), pw); + } + pw.println("]"); + } + else if (mediaType.equals(MediaType.TEXT_HTML_TYPE)) { + pw.println(""); + pw.print("\n"); + while (i.hasNext()) { + htmlOutput(i.next(), pw); + } + pw.println("
callDestinationminimummaximumaveragetotalcount
"); + } + else if (mediaType.equals(MediaType.APPLICATION_XML_TYPE)) { + pw.println("\n"); + while (i.hasNext()) { + xmlOutput(i.next(), pw); + } + pw.println(""); + } + pw.flush(); + pw.close(); + } + } + /** * Write the numeric data points to the output stream in the encoding * requested from the mediaType without creating tons of objects in the diff --git a/modules/enterprise/server/jar/src/main/java/org/rhq/enterprise/server/rest/domain/CallTimeValueRest.java b/modules/enterprise/server/jar/src/main/java/org/rhq/enterprise/server/rest/domain/CallTimeValueRest.java new file mode 100644 index 00000000000..d059c62e0a1 --- /dev/null +++ b/modules/enterprise/server/jar/src/main/java/org/rhq/enterprise/server/rest/domain/CallTimeValueRest.java @@ -0,0 +1,78 @@ +/* + * RHQ Management Platform + * Copyright (C) 2005-2014 Red Hat, Inc. + * All rights reserved. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation version 2 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + */ +package org.rhq.enterprise.server.rest.domain; + +import javax.xml.bind.annotation.XmlAttribute; +import javax.xml.bind.annotation.XmlRootElement; + +import com.wordnik.swagger.annotations.ApiClass; +import com.wordnik.swagger.annotations.ApiProperty; + +/** + * @author Libor Zoubek + */ +@ApiClass("One callTime data point of a metric (aka schedule)") +@XmlRootElement +public class CallTimeValueRest { + + String callDestination; + long beginTime; + long duration; + + public CallTimeValueRest() { + // Needed for JAXB + } + @ApiProperty("Time in millis since epoch when the request occurred") + @XmlAttribute + public long getBeginTime() { + return beginTime; + } + + @ApiProperty("Destination URI of request") + @XmlAttribute + public String getCallDestination() { + return callDestination; + } + + @ApiProperty("Time in millis - duration of request") + @XmlAttribute + public long getDuration() { + return duration; + } + + public void setBeginTime(long beginTime) { + this.beginTime = beginTime; + } + + public void setCallDestination(String callDestination) { + this.callDestination = callDestination; + } + + public void setDuration(long duration) { + this.duration = duration; + } + + @Override + public String toString() { + return new StringBuilder("[callDestination="+callDestination) + .append(", beginTime="+beginTime) + .append(", duration="+duration) + .append("]").toString(); + } +} diff --git a/modules/integration-tests/rest-api/src/test/java/org/rhq/modules/integrationTests/restApi/CallTimesTest.java b/modules/integration-tests/rest-api/src/test/java/org/rhq/modules/integrationTests/restApi/CallTimesTest.java new file mode 100644 index 00000000000..42fae091ddd --- /dev/null +++ b/modules/integration-tests/rest-api/src/test/java/org/rhq/modules/integrationTests/restApi/CallTimesTest.java @@ -0,0 +1,318 @@ +/* + * RHQ Management Platform + * Copyright (C) 2005-2014 Red Hat, Inc. + * All rights reserved. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation version 2 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + */ +package org.rhq.modules.integrationTests.restApi; + +import static com.jayway.restassured.RestAssured.given; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import com.jayway.restassured.http.ContentType; +import com.jayway.restassured.path.json.JsonPath; +import com.jayway.restassured.response.Response; + +import org.junit.Before; +import org.junit.Test; + +import org.rhq.modules.integrationTests.restApi.d.CallTimeValueRest; + +public class CallTimesTest extends AbstractBase { + + private int callTimeScheduleId; + private int webRuntimeResourceId; + + @Before + public void setUp() throws Exception { + super.setUp(); + + // Determine a calltime schedule id + + // first lookup coregui.war resource + Response r = + given() + .header(acceptJson) + .queryParam("q", "coregui.war") + .expect() + .statusCode(200) + .log().ifError() + .when() + .get("/resource"); + + JsonPath jp = r.jsonPath(); + + int coreGuiId = jp.getInt("[0].resourceId"); + // now list it's children + r = + given() + .header(acceptJson) + .pathParam("rid", coreGuiId) + .expect() + .statusCode(200) + .log().ifError() + .when() + .get("/resource/{rid}/children"); + + jp = r.jsonPath(); + + webRuntimeResourceId = jp.getInt("[0].resourceId"); + + // finally lookup calltime + r = + given() + .header(acceptJson) + .queryParam("type", "calltime") + .queryParam("enabledOnly", false) + .pathParam("rid", webRuntimeResourceId) + .expect() + .statusCode(200) + .log().ifError() + .when() + .get("/resource/{rid}/schedules"); + + jp = r.jsonPath(); + callTimeScheduleId = jp.getInt("[0].scheduleId"); + + } + + @Test + public void putAndGetCallTimes() throws Exception { + List list = new ArrayList(); + String destination1 = "/1/" + String.valueOf(System.currentTimeMillis()); + for (int i = 0; i < 100; i++) { + CallTimeValueRest c = CallTimeValueRest.defaultCallTimeValue(destination1); + list.add(c); + } + String destination2 = "/2/" + String.valueOf(System.currentTimeMillis()); + for (int i = 0; i < 99; i++) { + CallTimeValueRest c = CallTimeValueRest.defaultCallTimeValue(destination2); + list.add(c); + } + + Response r = + given() + .header(acceptJson) + .contentType(ContentType.JSON) + .pathParam("id", callTimeScheduleId) + .body(list) + .expect() + .statusCode(201) + .log().ifError() + .when() + .put("/metric/data/{id}/callTime"); + + String location = r.getHeader("Location"); + + assert location != null : "Location header was not found in server's response"; + + r = + given() + .header(acceptJson) + .expect() + .statusCode(200) + .log().body() + .when() + .get(location); + + List> result = r.as(List.class); + boolean found1 = false; + boolean found2 = false; + for (Map map : result) { + String dest = (String) map.get("callDestination"); + if (destination1.equals(dest)) { + found1 = true; + int count = (Integer) map.get("count"); + double total = (Double) map.get("total"); + assert count == 100 : "we've pushed 100 calltime values, but server retuns " + count; + assert total == 100 * 1000L : "expected total was 100000, but server returned " + total; + } + if (destination2.equals(dest)) { + found2 = true; + int count = (Integer) map.get("count"); + double total = (Double) map.get("total"); + assert count == 99 : "we've pushed 100 calltime values, but server retuns " + count; + assert total == 99 * 1000L : "expected total was 990000, but server returned " + total; + } + + } + assert found1 && found2 : "we just created callTime data, but server did not return it"; + } + + @Test + public void putAndGetRawCallTimes() throws Exception { + List list = new ArrayList(); + long now = System.currentTimeMillis(); + String destination = "/aggr/" + String.valueOf(now); + + // send 2 buckets of callTime values to the same destination + + for (int i = 0; i < 100; i++) { + CallTimeValueRest c = CallTimeValueRest.defaultCallTimeValue(destination); + list.add(c); + } + + given() + .header(acceptJson) + .contentType(ContentType.JSON) + .pathParam("id", callTimeScheduleId) + .body(list) + .expect() + .statusCode(201) + .log().ifError() + .when() + .put("/metric/data/{id}/callTime"); + + list.clear(); + + for (int i = 0; i < 100; i++) { + CallTimeValueRest c = CallTimeValueRest.defaultCallTimeValue(destination); + list.add(c); + } + + given() + .header(acceptJson) + .contentType(ContentType.JSON) + .pathParam("id", callTimeScheduleId) + .body(list) + .expect() + .statusCode(201) + .log().ifError() + .when() + .put("/metric/data/{id}/callTime"); + + // first get it aggregated + Response r = + given() + .header(acceptJson) + .pathParam("id", callTimeScheduleId) + .queryParam("endTime", now + 2000) + .queryParam("aggregate", true) + .expect() + .statusCode(200) + .log().body() + .when() + .get("/metric/data/{id}/callTime"); + + List> result = r.as(List.class); + boolean found1 = false; + for (Map map : result) { + String dest = (String) map.get("callDestination"); + if (destination.equals(dest)) { + found1 = true; + int count = (Integer) map.get("count"); + double total = (Double) map.get("total"); + assert count == 200 : "we've pushed 2 * 100 calltime values, but server retuns " + count; + assert total == 200 * 1000L : "expected total was 200000, but server returned " + total; + } + + } + assert found1 : "we just created callTime data, but server did not return it"; + + // lets get raw metrics now + + r = + given() + .header(acceptJson) + .pathParam("id", callTimeScheduleId) + .queryParam("endTime", now + 2000) + .queryParam("aggregate", false) + .expect() + .statusCode(200).log().body() + .when() + .get("/metric/data/{id}/callTime"); + + result = r.as(List.class); + found1 = false; + boolean found2 = false; + for (Map map : result) { + String dest = (String) map.get("callDestination"); + if (destination.equals(dest)) { + if (found1) { + found2 = true; + } + found1 = true; + int count = (Integer) map.get("count"); + double total = (Double) map.get("total"); + assert count == 100 : "we've pushed 100 calltime values per 1 raw report, but server retuns " + count; + assert total == 100 * 1000L : "expected total was 100000, but server returned " + total; + } + + } + assert found1 && found2 : "server should have returned 2 raw records for our callDestination"; + } + + @Test + public void putInvalidCallTimes() throws Exception { + List list = new ArrayList(); + CallTimeValueRest c = CallTimeValueRest.defaultCallTimeValue("test"); + list.add(c); + + c.setCallDestination(null); // set invalid + + given() + .header(acceptJson) + .contentType(ContentType.JSON) + .pathParam("id", callTimeScheduleId) + .body(list) + .expect() + .statusCode(406) + .log().ifError() + .when() + .put("/metric/data/{id}/callTime"); + + c.setCallDestination("test"); // set back to valid + c.setDuration(-1L); + + given() + .header(acceptJson) + .contentType(ContentType.JSON) + .pathParam("id", callTimeScheduleId) + .body(list) + .expect() + .statusCode(406) + .log().ifError() + .when() + .put("/metric/data/{id}/callTime"); + + c.setDuration(1); // set back to valid + c.setBeginTime(1); // set invalid + + given() + .header(acceptJson) + .contentType(ContentType.JSON) + .pathParam("id", callTimeScheduleId) + .body(list) + .expect() + .statusCode(406) + .log().ifError() + .when() + .put("/metric/data/{id}/callTime"); + + // send empty list + given() + .header(acceptJson) + .contentType(ContentType.JSON) + .pathParam("id", callTimeScheduleId) + .body(Collections.EMPTY_LIST) + .expect().statusCode(200).log().ifError() + .when() + .put("/metric/data/{id}/callTime"); + } +} diff --git a/modules/integration-tests/rest-api/src/test/java/org/rhq/modules/integrationTests/restApi/d/CallTimeValueRest.java b/modules/integration-tests/rest-api/src/test/java/org/rhq/modules/integrationTests/restApi/d/CallTimeValueRest.java new file mode 100644 index 00000000000..ae26d24be63 --- /dev/null +++ b/modules/integration-tests/rest-api/src/test/java/org/rhq/modules/integrationTests/restApi/d/CallTimeValueRest.java @@ -0,0 +1,65 @@ +/* + * RHQ Management Platform + * Copyright (C) 2005-2014 Red Hat, Inc. + * All rights reserved. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation version 2 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + */ +package org.rhq.modules.integrationTests.restApi.d; +/** + * CallTime value for testing the rest interface + * @author Libor Zoubek + */ + +public class CallTimeValueRest { + + public static CallTimeValueRest defaultCallTimeValue(String callDestination) { + CallTimeValueRest c = new CallTimeValueRest(); + c.callDestination = callDestination; + c.beginTime = System.currentTimeMillis(); + c.duration = 1000L; + return c; + } + + String callDestination; + long beginTime; + long duration; + + public CallTimeValueRest() { + } + + public long getBeginTime() { + return beginTime; + } + + public String getCallDestination() { + return callDestination; + } + + public long getDuration() { + return duration; + } + + public void setBeginTime(long beginTime) { + this.beginTime = beginTime; + } + + public void setCallDestination(String callDestination) { + this.callDestination = callDestination; + } + + public void setDuration(long duration) { + this.duration = duration; + } +}