Skip to content

Commit

Permalink
Make it possible to define a route tag for COAP metrics (#60)
Browse files Browse the repository at this point in the history
* Add route to the metrics filter builder

* Add way to wrap COAP routes with a filter

* Fix linting

* Fix linting
  • Loading branch information
akolosov-n authored Jul 18, 2023
1 parent 52b521f commit a98c39c
Show file tree
Hide file tree
Showing 4 changed files with 179 additions and 9 deletions.
47 changes: 42 additions & 5 deletions coap-core/src/main/java/com/mbed/coap/server/RouterService.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -34,14 +35,15 @@ public class RouterService implements Service<CoapRequest, CoapResponse> {

private final Map<RequestMatcher, Service<CoapRequest, CoapResponse>> handlers;
private final List<Entry<RequestMatcher, Service<CoapRequest, CoapResponse>>> prefixedHandlers;
public final Service<CoapRequest, CoapResponse> defaultHandler;

public final static Service<CoapRequest, CoapResponse> NOT_FOUND_SERVICE = request -> completedFuture(CoapResponse.notFound());

public static RouteBuilder builder() {
return new RouteBuilder();
}

private RouterService(Map<RequestMatcher, Service<CoapRequest, CoapResponse>> handlers) {
private RouterService(Map<RequestMatcher, Service<CoapRequest, CoapResponse>> handlers, Service<CoapRequest, CoapResponse> defaultHandler) {

this.handlers = unmodifiableMap(
handlers.entrySet().stream()
Expand All @@ -54,6 +56,8 @@ private RouterService(Map<RequestMatcher, Service<CoapRequest, CoapResponse>> ha
.filter(entry -> entry.getKey().isPrefixed())
.collect(Collectors.toList())
);

this.defaultHandler = defaultHandler;
}

@Override
Expand All @@ -78,11 +82,12 @@ private Service<CoapRequest, CoapResponse> findHandler(RequestMatcher requestMat
return e.getValue();
}
}
return NOT_FOUND_SERVICE;
return defaultHandler;
}

public static class RouteBuilder {
private final Map<RequestMatcher, Service<CoapRequest, CoapResponse>> handlers = new HashMap<>();
public Service<CoapRequest, CoapResponse> defaultHandler = NOT_FOUND_SERVICE;

public RouteBuilder get(String uriPath, Service<CoapRequest, CoapResponse> service) {
return add(Method.GET, uriPath, service);
Expand Down Expand Up @@ -121,15 +126,47 @@ private RouteBuilder add(Method method, String uriPath, Service<CoapRequest, Coa
return this;
}

public RouteBuilder defaultHandler(Service<CoapRequest, CoapResponse> defaultHandler) {
this.defaultHandler = defaultHandler;
return this;
}

public RouteBuilder mergeRoutes(RouteBuilder otherBuilder) {
this.handlers.putAll(otherBuilder.handlers);

return this;
}

public RouteBuilder wrapRoutes(WrapFilterProducer wrapperFilterProducer) {
Map<RequestMatcher, Service<CoapRequest, CoapResponse>> wrappedRouteHandlers = new HashMap<>();
for (Entry<RequestMatcher, Service<CoapRequest, CoapResponse>> e : handlers.entrySet()) {
RequestMatcher key = e.getKey();
Service<CoapRequest, CoapResponse> service = e.getValue();

wrappedRouteHandlers.put(key, wrapperFilterProducer.getFilter(key.method, key.uriPath).then(service));
}
handlers.putAll(wrappedRouteHandlers);

return this;
}

public RouteBuilder wrapRoutes(Filter<CoapRequest, CoapResponse, CoapRequest, CoapResponse> wrapperFilter) {
return wrapRoutes((WrapFilterProducer) (m, u) -> wrapperFilter);
}

public Service<CoapRequest, CoapResponse> build() {
return new RouterService(handlers);
return new RouterService(handlers, defaultHandler);
}

@FunctionalInterface
public interface WrapFilterProducer {
Filter<CoapRequest, CoapResponse, CoapRequest, CoapResponse> 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) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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<CoapRequest, CoapResponse> simpleHandler =
(CoapRequest r) -> CompletableFuture.completedFuture(ok(r.getMethod() + " " + r.options().getUriPath()));

@Test
public void shouldBuildSimpleService() throws ExecutionException, InterruptedException {
Service<CoapRequest, CoapResponse> 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<CoapRequest, CoapResponse> svc = RouterService.builder()
.get("/test1", simpleHandler)
.post("/test1", simpleHandler)
.get("/test2/*", simpleHandler)
.wrapRoutes((CoapRequest req, Service<CoapRequest, CoapResponse> 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<CoapRequest, CoapResponse> 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<CoapRequest, CoapResponse> 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());
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,16 @@
public class MicrometerMetricsFilter implements Filter.SimpleFilter<CoapRequest, CoapResponse> {
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
Expand Down Expand Up @@ -69,16 +71,25 @@ private List<Tag> 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() {
}
Expand All @@ -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();
Expand All @@ -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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<CoapRequest, CoapResponse> okService = filter.then(__ -> completedFuture(ok("OK")));
private final Service<CoapRequest, CoapResponse> failingService = filter.then(__ -> failedFuture(new Exception("error message")));

@BeforeEach
public void beforeEach() {
registry.clear();
}

@Test
public void shouldBuildFilter() {
MicrometerMetricsFilter.builder()
Expand Down Expand Up @@ -85,4 +92,27 @@ public void shouldRegisterTimerMetric() {
.timer()
);
}

@Test
public void shouldUseDefinedRouteForAllMeteredRequests() {
MicrometerMetricsFilter filterWithRoute = MicrometerMetricsFilter.builder().registry(registry).route("/test/DEVICE_ID").build();
Service<CoapRequest, CoapResponse> 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()
);
}
}

0 comments on commit a98c39c

Please sign in to comment.