diff --git a/core/che-core-metrics-core/pom.xml b/core/che-core-metrics-core/pom.xml index c9a3af6f19..6a8168d367 100644 --- a/core/che-core-metrics-core/pom.xml +++ b/core/che-core-metrics-core/pom.xml @@ -73,6 +73,31 @@ logback-classic test + + com.jayway.restassured + rest-assured + test + + + javax.ws.rs + javax.ws.rs-api + test + + + org.everrest + everrest-assured + test + + + org.mockito + mockito-core + test + + + org.mockito + mockito-testng + test + org.testng testng diff --git a/core/che-core-metrics-core/src/main/java/org/eclipse/che/core/metrics/ApiResponseCounter.java b/core/che-core-metrics-core/src/main/java/org/eclipse/che/core/metrics/ApiResponseCounter.java new file mode 100644 index 0000000000..33396d2948 --- /dev/null +++ b/core/che-core-metrics-core/src/main/java/org/eclipse/che/core/metrics/ApiResponseCounter.java @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2012-2018 Red Hat, Inc. + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ +package org.eclipse.che.core.metrics; + +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.binder.MeterBinder; +import javax.inject.Singleton; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Metric binding for Che API responses, that are grouped by http status codes. + * + * @author Mykhailo Kuznietsov + */ +@Singleton +public class ApiResponseCounter implements MeterBinder { + private static final Logger LOG = LoggerFactory.getLogger(ApiResponseCounter.class); + + // package private access for visibility in tests + Counter informationalResponseCounter; + Counter successResponseCounter; + Counter redirectResponseCounter; + Counter clientErrorResponseCounter; + Counter serverErrorResponseCounter; + + @Override + public void bindTo(MeterRegistry registry) { + informationalResponseCounter = + Counter.builder("che.server.api.response") + .description("Che Server Tomcat informational responses (1xx responses)") + .tag("code", "1xx") + .tag("area", "http") + .register(registry); + successResponseCounter = + Counter.builder("che.server.api.response") + .description("Che Server Tomcat success responses (2xx responses)") + .tag("code", "2xx") + .tag("area", "http") + .register(registry); + redirectResponseCounter = + Counter.builder("che.server.api.response") + .description("Che Server Tomcat redirect responses (3xx responses)") + .tag("code", "3xx") + .tag("area", "http") + .register(registry); + clientErrorResponseCounter = + Counter.builder("che.server.api.response") + .description("Che Server Tomcat client errors (4xx responses)") + .tag("code", "4xx") + .tag("area", "http") + .register(registry); + serverErrorResponseCounter = + Counter.builder("che.server.api.response") + .description("Che Server Tomcat server errors (5xx responses)") + .tag("code", "5xx") + .tag("area", "http") + .register(registry); + } + + public void handleStatus(int status) { + status = status / 100; + switch (status) { + case 1: + informationalResponseCounter.increment(); + break; + case 2: + successResponseCounter.increment(); + break; + case 3: + redirectResponseCounter.increment(); + break; + case 4: + clientErrorResponseCounter.increment(); + break; + case 5: + serverErrorResponseCounter.increment(); + break; + default: + // should not happen + LOG.warn("Unhandled HTTP status ", status); + } + } +} diff --git a/core/che-core-metrics-core/src/main/java/org/eclipse/che/core/metrics/ApiResponseMetricFilter.java b/core/che-core-metrics-core/src/main/java/org/eclipse/che/core/metrics/ApiResponseMetricFilter.java new file mode 100644 index 0000000000..94c173f195 --- /dev/null +++ b/core/che-core-metrics-core/src/main/java/org/eclipse/che/core/metrics/ApiResponseMetricFilter.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2012-2018 Red Hat, Inc. + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ +package org.eclipse.che.core.metrics; + +import java.io.IOException; +import javax.inject.Inject; +import javax.inject.Singleton; +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletResponse; + +/** + * Filter for tracking all HTTP requests through {@link ApiResponseCounter} + * + * @author Mykhailo Kuznietsov + */ +@Singleton +public class ApiResponseMetricFilter implements Filter { + + private ApiResponseCounter apiResponseCounter; + + @Inject + public void setApiResponseCounter(ApiResponseCounter counter) { + this.apiResponseCounter = counter; + } + + @Override + public void init(FilterConfig filterConfig) throws ServletException {} + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) + throws IOException, ServletException { + filterChain.doFilter(request, response); + if (response instanceof HttpServletResponse) { + apiResponseCounter.handleStatus(((HttpServletResponse) response).getStatus()); + } + } + + @Override + public void destroy() {} +} diff --git a/core/che-core-metrics-core/src/main/java/org/eclipse/che/core/metrics/MetricsModule.java b/core/che-core-metrics-core/src/main/java/org/eclipse/che/core/metrics/MetricsModule.java index 0241e4dc1a..f07b6286e9 100644 --- a/core/che-core-metrics-core/src/main/java/org/eclipse/che/core/metrics/MetricsModule.java +++ b/core/che-core-metrics-core/src/main/java/org/eclipse/che/core/metrics/MetricsModule.java @@ -48,5 +48,6 @@ public class MetricsModule extends AbstractModule { meterMultibinder.addBinding().to(ProcessorMetrics.class); meterMultibinder.addBinding().to(UptimeMetrics.class); meterMultibinder.addBinding().to(FileStoresMeterBinder.class); + meterMultibinder.addBinding().to(ApiResponseCounter.class); } } diff --git a/core/che-core-metrics-core/src/main/java/org/eclipse/che/core/metrics/MetricsServletModule.java b/core/che-core-metrics-core/src/main/java/org/eclipse/che/core/metrics/MetricsServletModule.java index 7d919ef3b8..ae242f5ed2 100644 --- a/core/che-core-metrics-core/src/main/java/org/eclipse/che/core/metrics/MetricsServletModule.java +++ b/core/che-core-metrics-core/src/main/java/org/eclipse/che/core/metrics/MetricsServletModule.java @@ -37,6 +37,7 @@ public class MetricsServletModule extends ServletModule { meterMultibinder.addBinding().toProvider(TomcatMetricsProvider.class); bind(Manager.class).toInstance(getManager(getServletContext())); + filter("/*").through(ApiResponseMetricFilter.class); } private Manager getManager(ServletContext servletContext) { diff --git a/core/che-core-metrics-core/src/test/java/org/eclipse/che/core/metrics/ApiResponseCounterTest.java b/core/che-core-metrics-core/src/test/java/org/eclipse/che/core/metrics/ApiResponseCounterTest.java new file mode 100644 index 0000000000..c8881c9f3c --- /dev/null +++ b/core/che-core-metrics-core/src/test/java/org/eclipse/che/core/metrics/ApiResponseCounterTest.java @@ -0,0 +1,118 @@ +/* + * Copyright (c) 2012-2018 Red Hat, Inc. + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ +package org.eclipse.che.core.metrics; + +import static org.testng.Assert.assertEquals; + +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +/** + * Test for {@link ApiResponseCounter} functionality + * + * @author Mykhailo Kuznietsov + */ +public class ApiResponseCounterTest { + private ApiResponseCounter apiResponseCounter; + private MeterRegistry registry; + + @BeforeMethod + public void setup() { + registry = new SimpleMeterRegistry(); + + apiResponseCounter = new ApiResponseCounter(); + apiResponseCounter.bindTo(registry); + } + + @Test(dataProvider = "information") + public void shouldCount1xxResponses(int status) { + apiResponseCounter.handleStatus(status); + + assertEquals(apiResponseCounter.informationalResponseCounter.count(), 1.0); + assertEquals(apiResponseCounter.successResponseCounter.count(), 0.0); + assertEquals(apiResponseCounter.redirectResponseCounter.count(), 0.0); + assertEquals(apiResponseCounter.clientErrorResponseCounter.count(), 0.0); + assertEquals(apiResponseCounter.serverErrorResponseCounter.count(), 0.0); + } + + @Test(dataProvider = "success") + public void shouldCount2xxResponses(int status) { + apiResponseCounter.handleStatus(status); + + assertEquals(apiResponseCounter.informationalResponseCounter.count(), 0.0); + assertEquals(apiResponseCounter.successResponseCounter.count(), 1.0); + assertEquals(apiResponseCounter.redirectResponseCounter.count(), 0.0); + assertEquals(apiResponseCounter.clientErrorResponseCounter.count(), 0.0); + assertEquals(apiResponseCounter.serverErrorResponseCounter.count(), 0.0); + } + + @Test(dataProvider = "redirect") + public void shouldCount3xxResponses(int status) { + apiResponseCounter.handleStatus(status); + + assertEquals(apiResponseCounter.informationalResponseCounter.count(), 0.0); + assertEquals(apiResponseCounter.successResponseCounter.count(), 0.0); + assertEquals(apiResponseCounter.redirectResponseCounter.count(), 1.0); + assertEquals(apiResponseCounter.clientErrorResponseCounter.count(), .0); + assertEquals(apiResponseCounter.serverErrorResponseCounter.count(), 0.0); + } + + @Test(dataProvider = "clientError") + public void shouldCount4xxResponses(int status) { + apiResponseCounter.handleStatus(status); + + assertEquals(apiResponseCounter.informationalResponseCounter.count(), 0.0); + assertEquals(apiResponseCounter.successResponseCounter.count(), 0.0); + assertEquals(apiResponseCounter.redirectResponseCounter.count(), 0.0); + assertEquals(apiResponseCounter.clientErrorResponseCounter.count(), 1.0); + assertEquals(apiResponseCounter.serverErrorResponseCounter.count(), 0.0); + } + + @Test(dataProvider = "serverError") + public void shouldCount5xxResponses(int status) { + apiResponseCounter.handleStatus(status); + + assertEquals(apiResponseCounter.informationalResponseCounter.count(), 0.0); + assertEquals(apiResponseCounter.successResponseCounter.count(), 0.0); + assertEquals(apiResponseCounter.redirectResponseCounter.count(), 0.0); + assertEquals(apiResponseCounter.clientErrorResponseCounter.count(), 0.0); + assertEquals(apiResponseCounter.serverErrorResponseCounter.count(), 1.0); + } + + @DataProvider(name = "information") + public Object[][] information() { + return new Object[][] {{100}, {101}}; + } + + @DataProvider(name = "success") + public Object[][] success() { + return new Object[][] {{200}, {201}, {202}, {203}, {204}}; + } + + @DataProvider(name = "redirect") + public Object[][] redirect() { + return new Object[][] {{300}, {301}, {302}, {303}, {304}}; + } + + @DataProvider(name = "clientError") + public Object[][] clientError() { + return new Object[][] {{400}, {401}, {402}, {403}, {404}, {405}}; + } + + @DataProvider(name = "serverError") + public Object[][] serverError() { + return new Object[][] {{500}, {501}, {502}, {503}, {504}}; + } +} diff --git a/core/che-core-metrics-core/src/test/java/org/eclipse/che/core/metrics/ApiResponseMetricFilterTest.java b/core/che-core-metrics-core/src/test/java/org/eclipse/che/core/metrics/ApiResponseMetricFilterTest.java new file mode 100644 index 0000000000..4469d3c857 --- /dev/null +++ b/core/che-core-metrics-core/src/test/java/org/eclipse/che/core/metrics/ApiResponseMetricFilterTest.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2012-2018 Red Hat, Inc. + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ +package org.eclipse.che.core.metrics; + +import static com.jayway.restassured.RestAssured.given; +import static org.everrest.assured.JettyHttpServer.ADMIN_USER_NAME; +import static org.everrest.assured.JettyHttpServer.ADMIN_USER_PASSWORD; +import static org.everrest.assured.JettyHttpServer.SECURE_PATH; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; + +import org.everrest.assured.EverrestJetty; +import org.mockito.Mock; +import org.mockito.testng.MockitoTestNGListener; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Listeners; +import org.testng.annotations.Test; + +/** + * Test for {@link ApiResponseMetricFilter} functionality + * + * @author Mykhailo Kuznietsov + */ +@Listeners({ + MockitoTestNGListener.class, + EverrestJetty.class, +}) +public class ApiResponseMetricFilterTest { + + @Mock private ApiResponseCounter apiResponseCounter; + + private ApiResponseMetricFilter filter; + + @BeforeMethod + public void setUp() { + filter = new ApiResponseMetricFilter(); + filter.setApiResponseCounter(apiResponseCounter); + } + + @Test + public void shouldHandleStatusOnHttpRequest() { + // requesting a non existing resource, so 404 is expected + int status = 404; + + given() + .auth() + .basic(ADMIN_USER_NAME, ADMIN_USER_PASSWORD) + .when() + .get(SECURE_PATH + "/service") + .then() + .statusCode(status); + + verify(apiResponseCounter).handleStatus(eq(status)); + } +}