diff --git a/coap-core/src/main/java/com/mbed/coap/server/RouterService.java b/coap-core/src/main/java/com/mbed/coap/server/RouterService.java index 4e727622..e36bfa54 100644 --- a/coap-core/src/main/java/com/mbed/coap/server/RouterService.java +++ b/coap-core/src/main/java/com/mbed/coap/server/RouterService.java @@ -21,6 +21,7 @@ import com.mbed.coap.packet.CoapRequest; import com.mbed.coap.packet.CoapResponse; import com.mbed.coap.packet.Method; +import com.mbed.coap.utils.Filter; import com.mbed.coap.utils.Service; import java.util.HashMap; import java.util.List; @@ -34,6 +35,7 @@ public class RouterService implements Service { private final Map> handlers; private final List>> prefixedHandlers; + public final Service defaultHandler; public final static Service NOT_FOUND_SERVICE = request -> completedFuture(CoapResponse.notFound()); @@ -41,7 +43,7 @@ public static RouteBuilder builder() { return new RouteBuilder(); } - private RouterService(Map> handlers) { + private RouterService(Map> handlers, Service defaultHandler) { this.handlers = unmodifiableMap( handlers.entrySet().stream() @@ -54,6 +56,8 @@ private RouterService(Map> ha .filter(entry -> entry.getKey().isPrefixed()) .collect(Collectors.toList()) ); + + this.defaultHandler = defaultHandler; } @Override @@ -78,11 +82,12 @@ private Service findHandler(RequestMatcher requestMat return e.getValue(); } } - return NOT_FOUND_SERVICE; + return defaultHandler; } public static class RouteBuilder { private final Map> handlers = new HashMap<>(); + public Service defaultHandler = NOT_FOUND_SERVICE; public RouteBuilder get(String uriPath, Service service) { return add(Method.GET, uriPath, service); @@ -121,15 +126,47 @@ private RouteBuilder add(Method method, String uriPath, Service defaultHandler) { + this.defaultHandler = defaultHandler; + return this; + } + + public RouteBuilder mergeRoutes(RouteBuilder otherBuilder) { + this.handlers.putAll(otherBuilder.handlers); + + return this; + } + + public RouteBuilder wrapRoutes(WrapFilterProducer wrapperFilterProducer) { + Map> wrappedRouteHandlers = new HashMap<>(); + for (Entry> e : handlers.entrySet()) { + RequestMatcher key = e.getKey(); + Service service = e.getValue(); + + wrappedRouteHandlers.put(key, wrapperFilterProducer.getFilter(key.method, key.uriPath).then(service)); + } + handlers.putAll(wrappedRouteHandlers); + + return this; + } + + public RouteBuilder wrapRoutes(Filter wrapperFilter) { + return wrapRoutes((WrapFilterProducer) (m, u) -> wrapperFilter); + } public Service build() { - return new RouterService(handlers); + return new RouterService(handlers, defaultHandler); + } + + @FunctionalInterface + public interface WrapFilterProducer { + Filter getFilter(Method method, String uriPath); } } static final class RequestMatcher { - private final Method method; - private final String uriPath; + final Method method; + final String uriPath; private transient final boolean isPrefixed; RequestMatcher(Method method, String uriPath) { diff --git a/coap-core/src/test/java/com/mbed/coap/server/RouterServiceTest.java b/coap-core/src/test/java/com/mbed/coap/server/RouterServiceTest.java new file mode 100644 index 00000000..bb7cbc97 --- /dev/null +++ b/coap-core/src/test/java/com/mbed/coap/server/RouterServiceTest.java @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2022-2023 java-coap contributors (https://github.com/open-coap/java-coap) + * SPDX-License-Identifier: Apache-2.0 + * 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 com.mbed.coap.server; + +import static com.mbed.coap.packet.CoapResponse.ok; +import static org.junit.jupiter.api.Assertions.assertEquals; +import com.mbed.coap.packet.CoapRequest; +import com.mbed.coap.packet.CoapResponse; +import com.mbed.coap.packet.Code; +import com.mbed.coap.utils.Service; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import org.junit.jupiter.api.Test; + +class RouterServiceTest { + Service simpleHandler = + (CoapRequest r) -> CompletableFuture.completedFuture(ok(r.getMethod() + " " + r.options().getUriPath())); + + @Test + public void shouldBuildSimpleService() throws ExecutionException, InterruptedException { + Service svc = RouterService.builder() + .get("/test1", simpleHandler) + .post("/test1", simpleHandler) + .get("/test2/*", simpleHandler) + .get("/test3", simpleHandler) + .build(); + + assertEquals("GET /test1", svc.apply(CoapRequest.get("/test1")).get().getPayloadString()); + assertEquals("POST /test1", svc.apply(CoapRequest.post("/test1")).get().getPayloadString()); + assertEquals("GET /test2/prefixed-route", svc.apply(CoapRequest.get("/test2/prefixed-route")).get().getPayloadString()); + assertEquals(Code.C404_NOT_FOUND, svc.apply(CoapRequest.get("/test3/not-prefixed-route")).get().getCode()); + } + + @Test + public void shouldWrapRoutes() throws ExecutionException, InterruptedException { + Service svc = RouterService.builder() + .get("/test1", simpleHandler) + .post("/test1", simpleHandler) + .get("/test2/*", simpleHandler) + .wrapRoutes((CoapRequest req, Service nextSvc) -> CompletableFuture.completedFuture(ok("42"))) + .get("/test3", simpleHandler) + .build(); + + assertEquals("42", svc.apply(CoapRequest.get("/test1")).get().getPayloadString()); + assertEquals("42", svc.apply(CoapRequest.post("/test1")).get().getPayloadString()); + assertEquals("42", svc.apply(CoapRequest.get("/test2/prefixed-route")).get().getPayloadString()); + assertEquals("GET /test3", svc.apply(CoapRequest.get("/test3")).get().getPayloadString()); + assertEquals(Code.C404_NOT_FOUND, svc.apply(CoapRequest.get("/test4")).get().getCode()); + } + + @Test + public void shouldChangeDefaultHandler() throws ExecutionException, InterruptedException { + Service svc = RouterService.builder() + .defaultHandler((CoapRequest r) -> CompletableFuture.completedFuture(ok("OK"))) + .build(); + + assertEquals(Code.C205_CONTENT, svc.apply(CoapRequest.get("/test3")).get().getCode()); + } + + @Test + public void shouldMergeRoutes() throws ExecutionException, InterruptedException { + RouterService.RouteBuilder builder1 = RouterService.builder() + .get("/test1", simpleHandler); + RouterService.RouteBuilder builder2 = RouterService.builder() + .get("/test2", simpleHandler) + .mergeRoutes(builder1); + + Service svc = builder2.build(); + + assertEquals(Code.C205_CONTENT, svc.apply(CoapRequest.get("/test1")).get().getCode()); + assertEquals(Code.C205_CONTENT, svc.apply(CoapRequest.get("/test2")).get().getCode()); + } + +} diff --git a/coap-metrics/src/main/java/org/opencoap/coap/metrics/micrometer/MicrometerMetricsFilter.java b/coap-metrics/src/main/java/org/opencoap/coap/metrics/micrometer/MicrometerMetricsFilter.java index 9fc4a9b0..74307a00 100644 --- a/coap-metrics/src/main/java/org/opencoap/coap/metrics/micrometer/MicrometerMetricsFilter.java +++ b/coap-metrics/src/main/java/org/opencoap/coap/metrics/micrometer/MicrometerMetricsFilter.java @@ -33,14 +33,16 @@ public class MicrometerMetricsFilter implements Filter.SimpleFilter { private final MeterRegistry registry; private final String metricName; + private final String route; public static MicrometerMetricsFilterBuilder builder() { return new MicrometerMetricsFilterBuilder(); } - MicrometerMetricsFilter(MeterRegistry registry, String metricName, DistributionStatisticConfig distributionStatisticConfig) { + MicrometerMetricsFilter(MeterRegistry registry, String metricName, DistributionStatisticConfig distributionStatisticConfig, String route) { this.registry = registry; this.metricName = metricName; + this.route = route; registry.config().meterFilter(new MeterFilter() { @Override @@ -69,16 +71,25 @@ private List requestTags(CoapRequest req, CoapResponse resp, Throwable err) return Arrays.asList( Tag.of("method", req.getMethod().name()), Tag.of("status", resp != null ? resp.getCode().codeToString() : "n/a"), - Tag.of("route", uriPath != null ? uriPath : "/"), + Tag.of("route", getRoute(uriPath)), Tag.of("throwable", err != null ? err.getClass().getCanonicalName() : "n/a") ); } + private String getRoute(String uriPath) { + if (route != null) { + return route; + } + + return uriPath != null ? uriPath : "/"; + } + public static class MicrometerMetricsFilterBuilder { public String DEFAULT_METRIC_NAME = "coap.server.requests"; private MeterRegistry registry; private String metricName; private DistributionStatisticConfig distributionStatisticConfig; + private String route; MicrometerMetricsFilterBuilder() { } @@ -98,6 +109,11 @@ public MicrometerMetricsFilterBuilder distributionStatisticConfig(DistributionSt return this; } + public MicrometerMetricsFilterBuilder route(String route) { + this.route = route; + return this; + } + public MicrometerMetricsFilter build() { if (this.registry == null) { this.registry = new LoggingMeterRegistry(); @@ -111,7 +127,7 @@ public MicrometerMetricsFilter build() { this.distributionStatisticConfig = DistributionStatisticConfig.builder().percentiles(0.5, 0.9, 0.95, 0.99).build(); } - return new MicrometerMetricsFilter(this.registry, this.metricName, this.distributionStatisticConfig); + return new MicrometerMetricsFilter(this.registry, this.metricName, this.distributionStatisticConfig, this.route); } } } diff --git a/coap-metrics/src/test/java/org/opencoap/coap/metrics/micrometer/MicrometerMetricsFilterTest.java b/coap-metrics/src/test/java/org/opencoap/coap/metrics/micrometer/MicrometerMetricsFilterTest.java index ea110326..dcdad986 100644 --- a/coap-metrics/src/test/java/org/opencoap/coap/metrics/micrometer/MicrometerMetricsFilterTest.java +++ b/coap-metrics/src/test/java/org/opencoap/coap/metrics/micrometer/MicrometerMetricsFilterTest.java @@ -26,15 +26,22 @@ import io.micrometer.core.instrument.MeterRegistry; import io.micrometer.core.instrument.distribution.DistributionStatisticConfig; import io.micrometer.core.instrument.logging.LoggingMeterRegistry; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; import java.util.concurrent.ExecutionException; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.BeforeEach; class MicrometerMetricsFilterTest { - private final MeterRegistry registry = new LoggingMeterRegistry(); + private final MeterRegistry registry = new SimpleMeterRegistry(); private final MicrometerMetricsFilter filter = MicrometerMetricsFilter.builder().registry(registry).build(); private final Service okService = filter.then(__ -> completedFuture(ok("OK"))); private final Service failingService = filter.then(__ -> failedFuture(new Exception("error message"))); + @BeforeEach + public void beforeEach() { + registry.clear(); + } + @Test public void shouldBuildFilter() { MicrometerMetricsFilter.builder() @@ -85,4 +92,27 @@ public void shouldRegisterTimerMetric() { .timer() ); } + + @Test + public void shouldUseDefinedRouteForAllMeteredRequests() { + MicrometerMetricsFilter filterWithRoute = MicrometerMetricsFilter.builder().registry(registry).route("/test/DEVICE_ID").build(); + Service svc = filterWithRoute.then(__ -> completedFuture(ok("OK"))); + svc.apply(get("/test/1")).join(); + svc.apply(get("/test/2")).join(); + + assertNull( + registry.find("coap.server.requests") + .tag("route", "/test/1") + .timer() + ); + assertNull( + registry.find("coap.server.requests") + .tag("route", "/test/2") + .timer() + ); + assertEquals(2, registry.find("coap.server.requests") + .tag("route", "/test/DEVICE_ID") + .timer().count() + ); + } }