diff --git a/gateleen-cache/pom.xml b/gateleen-cache/pom.xml index 566c6bc25..f729e0c4e 100644 --- a/gateleen-cache/pom.xml +++ b/gateleen-cache/pom.xml @@ -6,7 +6,7 @@ org.swisspush.gateleen gateleen - 2.1.12-SNAPSHOT + 2.1.13-SNAPSHOT gateleen-cache diff --git a/gateleen-core/pom.xml b/gateleen-core/pom.xml index a4542cdd0..a1374ed42 100644 --- a/gateleen-core/pom.xml +++ b/gateleen-core/pom.xml @@ -6,7 +6,7 @@ org.swisspush.gateleen gateleen - 2.1.12-SNAPSHOT + 2.1.13-SNAPSHOT gateleen-core diff --git a/gateleen-delegate/pom.xml b/gateleen-delegate/pom.xml index 6441dec37..1e5284186 100644 --- a/gateleen-delegate/pom.xml +++ b/gateleen-delegate/pom.xml @@ -6,7 +6,7 @@ org.swisspush.gateleen gateleen - 2.1.12-SNAPSHOT + 2.1.13-SNAPSHOT gateleen-delegate diff --git a/gateleen-delta/pom.xml b/gateleen-delta/pom.xml index ee032654d..fd1ef7530 100644 --- a/gateleen-delta/pom.xml +++ b/gateleen-delta/pom.xml @@ -6,7 +6,7 @@ org.swisspush.gateleen gateleen - 2.1.12-SNAPSHOT + 2.1.13-SNAPSHOT gateleen-delta diff --git a/gateleen-expansion/README_expansion.md b/gateleen-expansion/README_expansion.md index 2afb4053e..b057390c5 100644 --- a/gateleen-expansion/README_expansion.md +++ b/gateleen-expansion/README_expansion.md @@ -120,4 +120,28 @@ For more information about the StorageExpand feature see the [vertx-rest-storage This allows you to create one octet-stream containing each json resource in the given collection (see expand feature). Basically it works exactly the same way as the default expand feature works, except that it does not set an eTag for the request. -> Attention: No eTag header is created / returned when this feature is used! \ No newline at end of file +> Attention: No eTag header is created / returned when this feature is used! + +### Micrometer metrics +The expansion feature is monitored with micrometer. The following metrics are available: +* gateleen_expand_requests_total +* gateleen_storage_expand_requests_total + +For `expand_requests_total` additional tags are provided to specify the expand level. + +Example metrics: + +``` +# HELP gateleen_expand_requests_total +# TYPE gateleen_expand_requests_total counter +gateleen_expand_requests_total{level="1",} 23677.0 +gateleen_expand_requests_total{level="2",} 2350.0 +gateleen_expand_requests_total{level="3",} 77.0 +gateleen_expand_requests_total{level="4",} 0.0 +gateleen_expand_requests_total{level="0",} 0.0 +# HELP gateleen_storage_expand_requests_total +# TYPE gateleen_storage_expand_requests_total counter +gateleen_storage_expand_requests_total 37.0 +``` + +To enable the metrics, set a `MeterRegistry` instance by calling `setMeterRegistry(MeterRegistry meterRegistry)` method in `ExpansionHandler` class. \ No newline at end of file diff --git a/gateleen-expansion/pom.xml b/gateleen-expansion/pom.xml index 20d4c7f8d..565ac09d1 100644 --- a/gateleen-expansion/pom.xml +++ b/gateleen-expansion/pom.xml @@ -6,7 +6,7 @@ org.swisspush.gateleen gateleen - 2.1.12-SNAPSHOT + 2.1.13-SNAPSHOT gateleen-expansion @@ -22,6 +22,10 @@ org.swisspush rest-storage + + io.micrometer + micrometer-core + diff --git a/gateleen-expansion/src/main/java/org/swisspush/gateleen/expansion/ExpansionHandler.java b/gateleen-expansion/src/main/java/org/swisspush/gateleen/expansion/ExpansionHandler.java index 59f309d43..09408a85b 100755 --- a/gateleen-expansion/src/main/java/org/swisspush/gateleen/expansion/ExpansionHandler.java +++ b/gateleen-expansion/src/main/java/org/swisspush/gateleen/expansion/ExpansionHandler.java @@ -1,5 +1,7 @@ package org.swisspush.gateleen.expansion; +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.MeterRegistry; import io.vertx.core.AsyncResult; import io.vertx.core.Handler; import io.vertx.core.MultiMap; @@ -27,10 +29,7 @@ import org.swisspush.gateleen.routing.RuleFeaturesProvider; import org.swisspush.gateleen.routing.RuleProvider; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.Set; +import java.util.*; import java.util.concurrent.atomic.AtomicInteger; import static org.swisspush.gateleen.core.util.StatusCode.INTERNAL_SERVER_ERROR; @@ -89,6 +88,10 @@ public class ExpansionHandler implements RuleChangesObserver { private static final int DECREMENT_BY_ONE = 1; private static final int MAX_RECURSION_LEVEL = 0; + private static final String EXPAND_REQUEST_METRIC = "gateleen.expand.requests"; + private static final String STORAGE_EXPAND_REQUEST_METRIC = "gateleen.storage.expand.requests"; + private static final String LEVEL = "level"; + public static final String MAX_EXPANSION_LEVEL_SOFT_PROPERTY = "max.expansion.level.soft"; public static final String MAX_EXPANSION_LEVEL_HARD_PROPERTY = "max.expansion.level.hard"; public static final String MAX_SUBREQUEST_PROPERTY = "max.expansion.subrequests"; @@ -123,6 +126,9 @@ public class ExpansionHandler implements RuleChangesObserver { private RuleFeaturesProvider ruleFeaturesProvider = new RuleFeaturesProvider(new ArrayList<>()); + private final Map counterMap = new HashMap<>(); + private Counter storageExpandCounter; + /** * Creates a new instance of the ExpansionHandler. * @@ -176,6 +182,26 @@ public int getMaxSubRequestCount() { return maxSubRequestCount; } + public void setMeterRegistry(MeterRegistry meterRegistry) { + counterMap.clear(); + if(meterRegistry != null) { + counterMap.put(0, Counter.builder(EXPAND_REQUEST_METRIC).tag(LEVEL, "0").register(meterRegistry)); + counterMap.put(1, Counter.builder(EXPAND_REQUEST_METRIC).tag(LEVEL, "1").register(meterRegistry)); + counterMap.put(2, Counter.builder(EXPAND_REQUEST_METRIC).tag(LEVEL, "2").register(meterRegistry)); + counterMap.put(3, Counter.builder(EXPAND_REQUEST_METRIC).tag(LEVEL, "3").register(meterRegistry)); + counterMap.put(4, Counter.builder(EXPAND_REQUEST_METRIC).tag(LEVEL, "4").register(meterRegistry)); + + storageExpandCounter = Counter.builder(STORAGE_EXPAND_REQUEST_METRIC).register(meterRegistry); + } + } + + private void incrementExpandReqCount(int level) { + Counter counter = counterMap.get(level); + if(counter != null) { + counter.increment(); + } + } + /** * Initialize the lists which defines, when which parameter * is removed (if any). @@ -363,6 +389,8 @@ private void handleExpansionRequest(final HttpServerRequest req, final Recursive log.debug("constructed uri for request: {}", targetUri); Integer finalExpandLevel = expandLevel; + incrementExpandReqCount(finalExpandLevel); + httpClient.request(HttpMethod.GET, targetUri).onComplete(asyncReqResult -> { if (asyncReqResult.failed()) { log.warn("Failed request to {}: {}", targetUri, asyncReqResult.cause()); @@ -505,6 +533,11 @@ private void makeStorageExpandRequest(final String targetUri, final List subReso Logger log = RequestLoggerFactory.getLogger(ExpansionHandler.class, req); HttpMethod reqMethod = HttpMethod.POST; String reqUri = targetUri + "?storageExpand=true"; + + if(storageExpandCounter != null) { + storageExpandCounter.increment(); + } + httpClient.request(reqMethod, reqUri).onComplete(asyncResult -> { if (asyncResult.failed()) { log.warn("Failed request to {}", reqUri, asyncResult.cause()); diff --git a/gateleen-expansion/src/test/java/org/swisspush/gateleen/expansion/ExpansionHandlerTest.java b/gateleen-expansion/src/test/java/org/swisspush/gateleen/expansion/ExpansionHandlerTest.java index daef5a283..bb365d1ed 100644 --- a/gateleen-expansion/src/test/java/org/swisspush/gateleen/expansion/ExpansionHandlerTest.java +++ b/gateleen-expansion/src/test/java/org/swisspush/gateleen/expansion/ExpansionHandlerTest.java @@ -46,7 +46,6 @@ public class ExpansionHandlerTest { public void setUp() { vertx = Vertx.vertx(); httpClient = Mockito.mock(HttpClient.class); - // Mockito.when(httpClient.request(any(HttpMethod.class), anyString(), Matchers.>any())).thenReturn(Mockito.mock(HttpClientRequest.class)); storage = new MockResourceStorage(); } diff --git a/gateleen-hook-js/pom.xml b/gateleen-hook-js/pom.xml index 580d66312..d09e3486f 100644 --- a/gateleen-hook-js/pom.xml +++ b/gateleen-hook-js/pom.xml @@ -4,7 +4,7 @@ org.swisspush.gateleen gateleen - 2.1.12-SNAPSHOT + 2.1.13-SNAPSHOT gateleen-hook-js jar diff --git a/gateleen-hook/README_hook.md b/gateleen-hook/README_hook.md index 556c2a841..9ecc6849f 100644 --- a/gateleen-hook/README_hook.md +++ b/gateleen-hook/README_hook.md @@ -163,7 +163,7 @@ PUT http://myserver:7012/gateleen/everything/_hooks/listeners/http/myexample "destination": "/gateleen/example/thePosition", "filter": "/gateleen/everything/.*/position.*", "headers": [ - { "header":"X-Expire-After", "value":"3600", mode:"complete"} + { "header":"X-Expire-After", "value":"3600", "mode":"complete"} ], "headersFilter": "x-foo: (A|B)" } @@ -240,10 +240,76 @@ hookHandler.enableResourceLogging(true); ``` +## Query-Based Listener and Route Search +Gateleen allows searching for listeners and routes using the query parameter `q`. This simplifies filtering the registered hooks based on query parameters. +The search will be based on the value registered of the destination property +### Listener Search with `q` +Search for listeners based on a query parameter like this: +``` +GET http://myserver:7012/playground/server/hooks/v1/registrations/listeners?q=mylistener +``` + +The response will contain the matching listeners. If no match is found, an empty list is returned: + +**Example response with matches:** +```json +{ + "listeners": [ + "first+playground+server+test+nemo+origin+mylistener" + ] +} +``` +**Example response with no matches:** +```json +{ + "listeners": [] +} +``` + +### Route Search with `q` +Similarly, you can search for routes using a query parameter: + +``` +GET http://myserver:7012/playground/server/hooks/v1/registrations/routes/?q=myroute +``` +The response contains the matching routes, or an empty list if no match is found. + +**Example response with matches:** +```json +{ + "routes": [ + "first+playground+server+test+nemo+origin+myroute" + ] +} + +``` +**Example response with no matches:** +```json +{ + "routes": [] +} +``` + +## Micrometer metrics +The hook feature is monitored with micrometer. The following metrics are available: +* gateleen_listener_count +* gateleen_routes_count + +Example metrics: + +``` +# HELP gateleen_listener_count Amount of listener hooks currently registered +# TYPE gateleen_listener_count gauge +gateleen_listener_count 577.0 +# HELP gateleen_routes_count Amount of route hooks currently registered +# TYPE gateleen_routes_count gauge +gateleen_routes_count 15.0 +``` +To enable the metrics, set a `MeterRegistry` instance by calling `setMeterRegistry(MeterRegistry meterRegistry)` method in `HookHandler` class. \ No newline at end of file diff --git a/gateleen-hook/pom.xml b/gateleen-hook/pom.xml index 16d1fe266..5931d2bc6 100644 --- a/gateleen-hook/pom.xml +++ b/gateleen-hook/pom.xml @@ -6,7 +6,7 @@ org.swisspush.gateleen gateleen - 2.1.12-SNAPSHOT + 2.1.13-SNAPSHOT gateleen-hook @@ -17,6 +17,10 @@ gateleen-queue ${project.version} + + io.micrometer + micrometer-core + diff --git a/gateleen-hook/src/main/java/org/swisspush/gateleen/hook/HookHandler.java b/gateleen-hook/src/main/java/org/swisspush/gateleen/hook/HookHandler.java index 66b1a59e0..c0f42fff0 100755 --- a/gateleen-hook/src/main/java/org/swisspush/gateleen/hook/HookHandler.java +++ b/gateleen-hook/src/main/java/org/swisspush/gateleen/hook/HookHandler.java @@ -5,17 +5,14 @@ import com.networknt.schema.JsonSchema; import com.networknt.schema.JsonSchemaFactory; import com.networknt.schema.ValidationMessage; +import io.micrometer.core.instrument.Gauge; +import io.micrometer.core.instrument.MeterRegistry; import io.vertx.core.AsyncResult; import io.vertx.core.Handler; import io.vertx.core.Vertx; import io.vertx.core.buffer.Buffer; import io.vertx.core.eventbus.Message; -import io.vertx.core.http.HttpClient; -import io.vertx.core.http.HttpClientRequest; -import io.vertx.core.http.HttpClientResponse; -import io.vertx.core.http.HttpMethod; -import io.vertx.core.http.HttpServerRequest; -import io.vertx.core.http.HttpServerResponse; +import io.vertx.core.http.*; import io.vertx.core.http.impl.headers.HeadersMultiMap; import io.vertx.core.json.DecodeException; import io.vertx.core.json.JsonArray; @@ -31,12 +28,7 @@ import org.swisspush.gateleen.core.logging.LoggableResource; import org.swisspush.gateleen.core.logging.RequestLogger; import org.swisspush.gateleen.core.storage.ResourceStorage; -import org.swisspush.gateleen.core.util.CollectionContentComparator; -import org.swisspush.gateleen.core.util.HttpHeaderUtil; -import org.swisspush.gateleen.core.util.HttpRequestHeader; -import org.swisspush.gateleen.core.util.HttpServerRequestUtil; -import org.swisspush.gateleen.core.util.ResourcesUtils; -import org.swisspush.gateleen.core.util.StatusCode; +import org.swisspush.gateleen.core.util.*; import org.swisspush.gateleen.hook.queueingstrategy.DefaultQueueingStrategy; import org.swisspush.gateleen.hook.queueingstrategy.DiscardPayloadQueueingStrategy; import org.swisspush.gateleen.hook.queueingstrategy.QueueingStrategy; @@ -69,12 +61,13 @@ import java.util.Optional; import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; import java.util.function.Consumer; +import java.util.function.Function; import java.util.regex.Pattern; import java.util.stream.Collectors; -import static io.vertx.core.http.HttpMethod.DELETE; -import static io.vertx.core.http.HttpMethod.PUT; +import static io.vertx.core.http.HttpMethod.*; import static org.swisspush.gateleen.core.util.HttpRequestHeader.CONTENT_LENGTH; /** @@ -89,7 +82,6 @@ public class HookHandler implements LoggableResource { public static final String HOOKS_LISTENERS_URI_PART = "/_hooks/listeners/"; public static final String LISTENER_QUEUE_PREFIX = "listener-hook"; private static final String X_QUEUE = "x-queue"; - private static final String X_EXPIRE_AFTER = "X-Expire-After"; private static final String LISTENER_HOOK_TARGET_PATH = "listeners/"; public static final String HOOKS_ROUTE_URI_PART = "/_hooks/route"; @@ -123,6 +115,10 @@ public class HookHandler implements LoggableResource { public static final String LISTABLE = "listable"; public static final String COLLECTION = "collection"; + private static final String CONTENT_TYPE_JSON = "application/json"; + private static final String LISTENERS_KEY = "listeners"; + private static final String ROUTES_KEY = "routes"; + private final Comparator collectionContentComparator; private static final Logger log = LoggerFactory.getLogger(HookHandler.class); @@ -152,7 +148,14 @@ public class HookHandler implements LoggableResource { private int routeMultiplier; private final QueueSplitter queueSplitter; + private final String routeBase; + private final String listenerBase; + private final String normalizedRouteBase; + private final String normalizedListenerBase; + private final AtomicLong listenerCount = new AtomicLong(0); + private final AtomicLong routesCount = new AtomicLong(0); + private MeterRegistry meterRegistry; /** * Creates a new HookHandler. @@ -166,7 +169,7 @@ public class HookHandler implements LoggableResource { * @param hookRootUri hookRootUri */ public HookHandler(Vertx vertx, HttpClient selfClient, final ResourceStorage storage, - LoggingResourceManager loggingResourceManager, LogAppenderRepository logAppenderRepository, MonitoringHandler monitoringHandler, + LoggingResourceManager loggingResourceManager, LogAppenderRepository logAppenderRepository, @Nullable MonitoringHandler monitoringHandler, String userProfilePath, String hookRootUri) { this(vertx, selfClient, storage, loggingResourceManager, logAppenderRepository, monitoringHandler, userProfilePath, hookRootUri, new QueueClient(vertx, monitoringHandler)); @@ -186,14 +189,14 @@ public HookHandler(Vertx vertx, HttpClient selfClient, final ResourceStorage sto * @param requestQueue requestQueue */ public HookHandler(Vertx vertx, HttpClient selfClient, final ResourceStorage storage, - LoggingResourceManager loggingResourceManager, LogAppenderRepository logAppenderRepository, MonitoringHandler monitoringHandler, + LoggingResourceManager loggingResourceManager, LogAppenderRepository logAppenderRepository, @Nullable MonitoringHandler monitoringHandler, String userProfilePath, String hookRootUri, RequestQueue requestQueue) { this(vertx, selfClient, storage, loggingResourceManager, logAppenderRepository, monitoringHandler, userProfilePath, hookRootUri, requestQueue, false); } public HookHandler(Vertx vertx, HttpClient selfClient, final ResourceStorage storage, - LoggingResourceManager loggingResourceManager, LogAppenderRepository logAppenderRepository, MonitoringHandler monitoringHandler, + LoggingResourceManager loggingResourceManager, LogAppenderRepository logAppenderRepository, @Nullable MonitoringHandler monitoringHandler, String userProfilePath, String hookRootUri, RequestQueue requestQueue, boolean listableRoutes) { this(vertx, selfClient, storage, loggingResourceManager, logAppenderRepository, monitoringHandler, userProfilePath, hookRootUri, requestQueue, false, null); @@ -214,7 +217,7 @@ public HookHandler(Vertx vertx, HttpClient selfClient, final ResourceStorage sto * @param reducedPropagationManager reducedPropagationManager */ public HookHandler(Vertx vertx, HttpClient selfClient, final ResourceStorage storage, - LoggingResourceManager loggingResourceManager, LogAppenderRepository logAppenderRepository, MonitoringHandler monitoringHandler, + LoggingResourceManager loggingResourceManager, LogAppenderRepository logAppenderRepository, @Nullable MonitoringHandler monitoringHandler, String userProfilePath, String hookRootUri, RequestQueue requestQueue, boolean listableRoutes, @Nullable ReducedPropagationManager reducedPropagationManager) { this(vertx, selfClient, storage, loggingResourceManager, logAppenderRepository, monitoringHandler, userProfilePath, hookRootUri, @@ -222,7 +225,7 @@ public HookHandler(Vertx vertx, HttpClient selfClient, final ResourceStorage sto } public HookHandler(Vertx vertx, HttpClient selfClient, final ResourceStorage userProfileStorage, - LoggingResourceManager loggingResourceManager, LogAppenderRepository logAppenderRepository, MonitoringHandler monitoringHandler, + LoggingResourceManager loggingResourceManager, LogAppenderRepository logAppenderRepository, @Nullable MonitoringHandler monitoringHandler, String userProfilePath, String hookRootUri, RequestQueue requestQueue, boolean listableRoutes, ReducedPropagationManager reducedPropagationManager, @Nullable Handler doneHandler, ResourceStorage hookStorage) { this(vertx, selfClient, userProfileStorage, loggingResourceManager, logAppenderRepository, monitoringHandler, userProfilePath, hookRootUri, @@ -230,7 +233,7 @@ public HookHandler(Vertx vertx, HttpClient selfClient, final ResourceStorage use } public HookHandler(Vertx vertx, HttpClient selfClient, final ResourceStorage userProfileStorage, - LoggingResourceManager loggingResourceManager, LogAppenderRepository logAppenderRepository, MonitoringHandler monitoringHandler, + LoggingResourceManager loggingResourceManager, LogAppenderRepository logAppenderRepository, @Nullable MonitoringHandler monitoringHandler, String userProfilePath, String hookRootUri, RequestQueue requestQueue, boolean listableRoutes, ReducedPropagationManager reducedPropagationManager, @Nullable Handler doneHandler, ResourceStorage hookStorage, int routeMultiplier) { @@ -260,7 +263,7 @@ public HookHandler(Vertx vertx, HttpClient selfClient, final ResourceStorage use * parallel operation. */ public HookHandler(Vertx vertx, HttpClient selfClient, final ResourceStorage userProfileStorage, - LoggingResourceManager loggingResourceManager, LogAppenderRepository logAppenderRepository, MonitoringHandler monitoringHandler, + LoggingResourceManager loggingResourceManager, LogAppenderRepository logAppenderRepository, @Nullable MonitoringHandler monitoringHandler, String userProfilePath, String hookRootUri, RequestQueue requestQueue, boolean listableRoutes, ReducedPropagationManager reducedPropagationManager, @Nullable Handler doneHandler, ResourceStorage hookStorage, int routeMultiplier, @Nonnull QueueSplitter queueSplitter) { @@ -285,6 +288,11 @@ public HookHandler(Vertx vertx, HttpClient selfClient, final ResourceStorage use this.queueSplitter = queueSplitter; String hookSchema = ResourcesUtils.loadResource("gateleen_hooking_schema_hook", true); jsonSchemaHook = JsonSchemaFactory.getInstance().getSchema(hookSchema); + this.listenerBase = hookRootUri + HOOK_LISTENER_STORAGE_PATH; + this.routeBase = hookRootUri + HOOK_ROUTE_STORAGE_PATH; + this.normalizedListenerBase = this.listenerBase.replaceAll("/+$", ""); + this.normalizedRouteBase = this.routeBase.replaceAll("/+$", ""); + } public void init() { @@ -316,6 +324,16 @@ public void handle(Void aVoid) { initMethods.forEach(handlerConsumer -> handlerConsumer.accept(readyHandler)); } + public void setMeterRegistry(MeterRegistry meterRegistry) { + this.meterRegistry = meterRegistry; + if(meterRegistry != null) { + Gauge.builder("gateleen.listener.count", listenerCount, AtomicLong::get) + .description("Amount of listener hooks currently registered").register(meterRegistry); + Gauge.builder("gateleen.routes.count", routesCount, AtomicLong::get) + .description("Amount of route hooks currently registered").register(meterRegistry); + } + } + @Override public void enableResourceLogging(boolean resourceLoggingEnabled) { this.logHookConfigurationResourceChanges = resourceLoggingEnabled; @@ -360,8 +378,15 @@ private void registerCleanupHandler(Handler readyHandler) { routeRepository.removeRoute(key); } } - monitoringHandler.updateListenerCount(listenerRepository.size()); - monitoringHandler.updateRoutesCount(routeRepository.getRoutes().size()); + if(meterRegistry != null) { + listenerCount.set(listenerRepository.size()); + routesCount.set(routeRepository.getRoutes().size()); + } + + if(monitoringHandler != null) { + monitoringHandler.updateListenerCount(listenerRepository.size()); + monitoringHandler.updateRoutesCount(routeRepository.getRoutes().size()); + } log.trace("done"); }); @@ -379,9 +404,6 @@ private void registerCleanupHandler(Handler readyHandler) { private void loadStoredRoutes(Handler readyHandler) { log.debug("loadStoredRoutes"); - // load the names of the routes from the hookStorage - final String routeBase = hookRootUri + HOOK_ROUTE_STORAGE_PATH; - hookStorage.get(routeBase, buffer -> { if (buffer != null) { JsonObject listOfRoutes = new JsonObject(buffer.toString()); @@ -426,8 +448,6 @@ private void loadStoredRoutes(Handler readyHandler) { private void loadStoredListeners(final Handler readyHandler) { log.debug("loadStoredListeners"); - // load the names of the listener from the hookStorage - final String listenerBase = hookRootUri + HOOK_LISTENER_STORAGE_PATH; hookStorage.get(listenerBase, buffer -> { if (buffer != null) { JsonObject listOfListeners = new JsonObject(buffer.toString()); @@ -541,13 +561,12 @@ public void registerListenerRegistrationHandler(Handler readyHandler) { public boolean handle(final RoutingContext ctx) { HttpServerRequest request = ctx.request(); boolean consumed = false; - + var requestUri = request.uri(); /* * 1) Un- / Register Listener / Routes */ var requestMethod = request.method(); if (requestMethod == PUT) { - var requestUri = request.uri(); if (requestUri.contains(HOOKS_LISTENERS_URI_PART)) { handleListenerRegistration(request); return true; @@ -558,7 +577,6 @@ public boolean handle(final RoutingContext ctx) { } } if (requestMethod == DELETE) { - var requestUri = request.uri(); if (requestUri.contains(HOOKS_LISTENERS_URI_PART)) { handleListenerUnregistration(request); return true; @@ -569,6 +587,16 @@ public boolean handle(final RoutingContext ctx) { } } + if (requestMethod == GET && !request.params().isEmpty()) { + if (requestUri.contains(normalizedListenerBase) ) { + handleListenerSearch(request); + return true; + } else if (requestUri.contains(normalizedRouteBase) ) { + handleRouteSearch(request); + return true; + } + } + /* * 2) Check if we have to queue a request for listeners */ @@ -592,6 +620,60 @@ public boolean handle(final RoutingContext ctx) { } } + private void handleListenerSearch(HttpServerRequest request) { + handleSearch( + listenerRepository.getListeners().stream().collect(Collectors.toMap(Listener::getListenerId, listener -> listener)), + listener -> listener.getHook().getDestination(), + LISTENERS_KEY, + request + ); + } + + private void handleRouteSearch(HttpServerRequest request) { + handleSearch( + routeRepository.getRoutes().entrySet().stream().collect(Collectors.toMap(entry -> entry.getValue().getHookDisplayText(), Map.Entry::getValue)), + route -> route.getHook().getDestination(), + ROUTES_KEY, + request + ); + } + + /** + * Search the repository for items matching the query parameter. + * Output a JSON response with the matched results. + * If parameter queryParam is empty or null a 400 Bad Request is returned. + * All params cannot be null + * @param repository The items to search . + * @param getDestination Function to extract destinations. + * @param resultKey The key for the result in the response. + * @param request The HTTP request to make a specific validations and return the results. + */ + private void handleSearch(Map repository, Function getDestination, String resultKey, HttpServerRequest request) { + String queryParam = request.getParam("q"); + if (request.params().size() > 1 || StringUtils.isEmpty(queryParam)) { + request.response().setStatusCode(StatusCode.BAD_REQUEST.getStatusCode()); + request.response().setStatusMessage(StatusCode.BAD_REQUEST.getStatusMessage()); + request.response().end("Only the 'q' parameter is allowed and can't be empty or null"); + return ; + } + + JsonArray matchingResults = new JsonArray(); + repository.forEach((key, value) -> { + String destination = getDestination.apply(value); + if (destination != null && destination.contains(queryParam)) { + matchingResults.add(convertToStoragePattern(key)); + } + }); + + JsonObject result = new JsonObject(); + result.put(resultKey, matchingResults); + + String encodedResult = result.encode(); + + request.response().putHeader(HttpHeaders.CONTENT_TYPE, CONTENT_TYPE_JSON); + request.response().end(encodedResult); + } + /** * Create a listing of routes in the given parent. This happens * only if we have a GET request, the routes are listable and @@ -1294,7 +1376,12 @@ private void unregisterRoute(String requestUrl) { log.debug("Unregister route {}", routedUrl); routeRepository.removeRoute(routedUrl); - monitoringHandler.updateRoutesCount(routeRepository.getRoutes().size()); + if(meterRegistry != null) { + routesCount.set(routeRepository.getRoutes().size()); + } + if(monitoringHandler != null) { + monitoringHandler.updateRoutesCount(routeRepository.getRoutes().size()); + } } /** @@ -1309,7 +1396,12 @@ private void unregisterListener(String requestUrl) { routeRepository.removeRoute(hookRootUri + LISTENER_HOOK_TARGET_PATH + getListenerUrlSegment(requestUrl)); listenerRepository.removeListener(listenerId); - monitoringHandler.updateListenerCount(listenerRepository.size()); + if(meterRegistry != null) { + listenerCount.set(listenerRepository.size()); + } + if(monitoringHandler != null) { + monitoringHandler.updateListenerCount(listenerRepository.size()); + } } /** @@ -1420,7 +1512,7 @@ private void registerListener(Buffer buffer) { target = hook.getDestination(); } else { String urlPattern = hookRootUri + LISTENER_HOOK_TARGET_PATH + target; - routeRepository.addRoute(urlPattern, createRoute(urlPattern, hook)); + routeRepository.addRoute(urlPattern, createRoute(urlPattern, hook, requestUrl)); if (log.isTraceEnabled()) { log.trace("external target, add route for urlPattern: {}", urlPattern); @@ -1433,7 +1525,12 @@ private void registerListener(Buffer buffer) { // create and add a new listener (or update an already existing listener) listenerRepository.addListener(new Listener(listenerId, getMonitoredUrlSegment(requestUrl), target, hook)); - monitoringHandler.updateListenerCount(listenerRepository.size()); + if(meterRegistry != null) { + listenerCount.set(listenerRepository.size()); + } + if(monitoringHandler != null) { + monitoringHandler.updateListenerCount(listenerRepository.size()); + } } /** @@ -1602,18 +1699,24 @@ private void registerRoute(Buffer buffer) { } boolean mustCreateNewRoute = true; + Route existingRoute = routeRepository.getRoutes().get(routedUrl); if (existingRoute != null) { mustCreateNewRoute = mustCreateNewRouteForHook(existingRoute, hook); } if (mustCreateNewRoute) { - routeRepository.addRoute(routedUrl, createRoute(routedUrl, hook)); + routeRepository.addRoute(routedUrl, createRoute(routedUrl, hook, requestUrl)); } else { // see comment in #mustCreateNewRouteForHook() existingRoute.getRule().setHeaderFunction(hook.getHeaderFunction()); existingRoute.getHook().setExpirationTime(hook.getExpirationTime().orElse(null)); } - monitoringHandler.updateRoutesCount(routeRepository.getRoutes().size()); + if(meterRegistry != null) { + routesCount.set(routeRepository.getRoutes().size()); + } + if(monitoringHandler != null) { + monitoringHandler.updateRoutesCount(routeRepository.getRoutes().size()); + } } /** @@ -1658,11 +1761,14 @@ private boolean headersFilterPatternEquals(Pattern headersFilterPatternLeft, Pat * * @param urlPattern urlPattern * @param hook hook + * @param hookDisplayText text used for display only like in API * @return Route */ - private Route createRoute(String urlPattern, HttpHook hook) { - return new Route(vertx, userProfileStorage, loggingResourceManager, logAppenderRepository, monitoringHandler, - userProfilePath, hook, urlPattern, selfClient); + private Route createRoute(String urlPattern, HttpHook hook, String hookDisplayText) { + Route route = new Route(vertx, userProfileStorage, loggingResourceManager, logAppenderRepository, monitoringHandler, + userProfilePath, hook, urlPattern, selfClient, hookDisplayText); + route.setMeterRegistry(meterRegistry); + return route; } /** diff --git a/gateleen-hook/src/main/java/org/swisspush/gateleen/hook/Route.java b/gateleen-hook/src/main/java/org/swisspush/gateleen/hook/Route.java index 85080e44b..d7068c2a0 100755 --- a/gateleen-hook/src/main/java/org/swisspush/gateleen/hook/Route.java +++ b/gateleen-hook/src/main/java/org/swisspush/gateleen/hook/Route.java @@ -1,6 +1,8 @@ package org.swisspush.gateleen.hook; -import io.vertx.codegen.annotations.Nullable; +import javax.annotation.Nullable; + +import io.micrometer.core.instrument.MeterRegistry; import io.vertx.core.Handler; import io.vertx.core.Vertx; import io.vertx.core.buffer.Buffer; @@ -49,6 +51,7 @@ public class Route { private MonitoringHandler monitoringHandler; private String userProfilePath; private ResourceStorage storage; + private String hookDisplayText; private String urlPattern; private HttpHook httpHook; @@ -78,8 +81,9 @@ public class Route { * @param httpHook httpHook * @param urlPattern - this can be a listener or a normal urlPattern (eg. for a route) */ - public Route(Vertx vertx, ResourceStorage storage, LoggingResourceManager loggingResourceManager, LogAppenderRepository logAppenderRepository, - MonitoringHandler monitoringHandler, String userProfilePath, HttpHook httpHook, String urlPattern, HttpClient selfClient) { + public Route(Vertx vertx, ResourceStorage storage, LoggingResourceManager loggingResourceManager, + LogAppenderRepository logAppenderRepository, @Nullable MonitoringHandler monitoringHandler, String userProfilePath, + HttpHook httpHook, String urlPattern, HttpClient selfClient, String hookDisplayText) { this.vertx = vertx; this.storage = storage; this.loggingResourceManager = loggingResourceManager; @@ -89,6 +93,7 @@ public Route(Vertx vertx, ResourceStorage storage, LoggingResourceManager loggin this.httpHook = httpHook; this.urlPattern = urlPattern; this.selfClient = selfClient; + this.hookDisplayText = hookDisplayText; createRule(); @@ -97,6 +102,10 @@ public Route(Vertx vertx, ResourceStorage storage, LoggingResourceManager loggin createForwarder(); } + public void setMeterRegistry(MeterRegistry meterRegistry) { + forwarder.setMeterRegistry(meterRegistry); + } + /** * Creates the forwarder for this hook. */ @@ -273,4 +282,8 @@ public void cleanup() { public HttpHook getHook() { return httpHook; } + + public String getHookDisplayText() { + return hookDisplayText; + } } diff --git a/gateleen-hook/src/test/java/org/swisspush/gateleen/hook/HookHandlerTest.java b/gateleen-hook/src/test/java/org/swisspush/gateleen/hook/HookHandlerTest.java index 0dbef201d..2d148c3f0 100644 --- a/gateleen-hook/src/test/java/org/swisspush/gateleen/hook/HookHandlerTest.java +++ b/gateleen-hook/src/test/java/org/swisspush/gateleen/hook/HookHandlerTest.java @@ -15,14 +15,13 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; import org.mockito.Mockito; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.swisspush.gateleen.core.http.DummyHttpServerRequest; -import org.swisspush.gateleen.core.http.DummyHttpServerResponse; -import org.swisspush.gateleen.core.http.FastFailHttpServerRequest; -import org.swisspush.gateleen.core.http.FastFailHttpServerResponse; +import org.swisspush.gateleen.core.http.*; import org.swisspush.gateleen.core.storage.MockResourceStorage; +import org.swisspush.gateleen.core.util.StatusCode; import org.swisspush.gateleen.hook.reducedpropagation.ReducedPropagationManager; import org.swisspush.gateleen.logging.LogAppenderRepository; import org.swisspush.gateleen.logging.LoggingResourceManager; @@ -38,8 +37,9 @@ import java.util.concurrent.CountDownLatch; import static io.vertx.core.http.HttpMethod.PUT; -import static org.junit.Assert.assertEquals; +import static org.junit.Assert.*; import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; import static org.swisspush.gateleen.core.util.HttpRequestHeader.*; /** @@ -49,8 +49,9 @@ */ @RunWith(VertxUnitRunner.class) public class HookHandlerTest { - private static final String HOOK_ROOT_URI = "hookRootURI/"; + private static final String HOOK_LISTENER_URI = "/"+ HOOK_ROOT_URI + "registrations/listeners"; + String HOOK_ROUTE_URI = "/"+ HOOK_ROOT_URI + "registrations/routes"; private static final Logger logger = LoggerFactory.getLogger(HookHandlerTest.class); private Vertx vertx; private HttpClient httpClient; @@ -64,24 +65,23 @@ public class HookHandlerTest { private HookHandler hookHandler; private RoutingContext routingContext; - + private HttpServerResponse mockResponse; @Before public void setUp() { vertx = Vertx.vertx(); - routingContext = Mockito.mock(RoutingContext.class); - httpClient = Mockito.mock(HttpClient.class); - Mockito.when(httpClient.request(any(HttpMethod.class), anyString())).thenReturn(Mockito.mock(Future.class)); + routingContext = mock(RoutingContext.class); + httpClient = mock(HttpClient.class); + when(httpClient.request(any(HttpMethod.class), anyString())).thenReturn(mock(Future.class)); storage = new MockResourceStorage(); - loggingResourceManager = Mockito.mock(LoggingResourceManager.class); - logAppenderRepository = Mockito.mock(LogAppenderRepository.class); - monitoringHandler = Mockito.mock(MonitoringHandler.class); - requestQueue = Mockito.mock(RequestQueue.class); - reducedPropagationManager = Mockito.mock(ReducedPropagationManager.class); - - - hookHandler = new HookHandler(vertx, httpClient, storage, loggingResourceManager, logAppenderRepository, monitoringHandler, - "userProfilePath", HOOK_ROOT_URI, requestQueue, false, reducedPropagationManager); + loggingResourceManager = mock(LoggingResourceManager.class); + logAppenderRepository = mock(LogAppenderRepository.class); + monitoringHandler = mock(MonitoringHandler.class); + requestQueue = mock(RequestQueue.class); + reducedPropagationManager = mock(ReducedPropagationManager.class); + mockResponse = mock(HttpServerResponse.class); + hookHandler = new HookHandler(vertx, httpClient, storage, loggingResourceManager, logAppenderRepository, + monitoringHandler, "userProfilePath", HOOK_ROOT_URI, requestQueue, false, reducedPropagationManager); hookHandler.init(); } @@ -112,7 +112,29 @@ private JsonObject buildListenerConfig(JsonObject queueingStrategy, String devic return config; } - private JsonObject buildListenerConfigWithHeadersFilter(JsonObject queueingStrategy, String deviceId, String headersFilter){ + private JsonObject buildRouteConfig(String routeId) { + JsonObject config = new JsonObject(); + config.put("requesturl", "/playground/server/tests/"+ routeId+"/_hooks/routes/http/push/" ); + config.put("expirationTime", "2017-01-03T14:15:53.277"); + + JsonObject hook = new JsonObject(); + hook.put("destination", "/playground/server/push/v1/routes/" + routeId); + hook.put("methods", new JsonArray(Collections.singletonList("PUT"))); + hook.put("timeout", 42); + hook.put("connectionPoolSize", 10); + + JsonObject staticHeaders = new JsonObject(); + staticHeaders.put("x-custom-header", "route-header-value"); + hook.put("staticHeaders", staticHeaders); + config.put("hook", hook); + return config; + } + private void setRouteStorageEntryAndTriggerUpdate(JsonObject routeConfig) { + storage.putMockData("pathToRouteResource", routeConfig.encode()); + vertx.eventBus().request("gateleen.hook-route-insert", "pathToRouteResource"); + } + + private JsonObject buildListenerConfigWithHeadersFilter(JsonObject queueingStrategy, String deviceId, String headersFilter) { JsonObject config = buildListenerConfig(queueingStrategy, deviceId); config.getJsonObject("hook").put("headersFilter", headersFilter); return config; @@ -133,7 +155,7 @@ public void testListenerEnqueueWithDefaultQueueingStrategy(TestContext context) PUTRequest putRequest = new PUTRequest(uri, originalPayload); putRequest.addHeader(CONTENT_LENGTH.getName(), "99"); - Mockito.when(routingContext.request()).thenReturn(putRequest); + when(routingContext.request()).thenReturn(putRequest); hookHandler.handle(routingContext); @@ -160,7 +182,7 @@ public void testListenerEnqueueWithDefaultQueueingStrategyBecauseOfInvalidConfig PUTRequest putRequest = new PUTRequest(uri, originalPayload); putRequest.addHeader(CONTENT_LENGTH.getName(), "99"); - Mockito.when(routingContext.request()).thenReturn(putRequest); + when(routingContext.request()).thenReturn(putRequest); hookHandler.handle(routingContext); // verify that enqueue has been called WITH the payload @@ -185,7 +207,7 @@ public void testListenerEnqueueWithDiscardPayloadQueueingStrategy(TestContext co String originalPayload = "{\"key\":123}"; PUTRequest putRequest = new PUTRequest(uri, originalPayload); putRequest.addHeader(CONTENT_LENGTH.getName(), "99"); - Mockito.when(routingContext.request()).thenReturn(putRequest); + when(routingContext.request()).thenReturn(putRequest); hookHandler.handle(routingContext); // verify that enqueue has been called WITHOUT the payload but with 'Content-Length : 0' header @@ -197,7 +219,7 @@ public void testListenerEnqueueWithDiscardPayloadQueueingStrategy(TestContext co }), anyString(), any(Handler.class)); PUTRequest putRequestWithoutContentLengthHeader = new PUTRequest(uri, originalPayload); - Mockito.when(routingContext.request()).thenReturn(putRequestWithoutContentLengthHeader); + when(routingContext.request()).thenReturn(putRequestWithoutContentLengthHeader); hookHandler.handle(routingContext); // verify that enqueue has been called WITHOUT the payload and WITHOUT 'Content-Length' header @@ -226,7 +248,7 @@ public void testListenerEnqueueWithReducedPropagationQueueingStrategyButNoManage String originalPayload = "{\"key\":123}"; PUTRequest putRequest = new PUTRequest(uri, originalPayload); putRequest.addHeader(CONTENT_LENGTH.getName(), "99"); - Mockito.when(routingContext.request()).thenReturn(putRequest); + when(routingContext.request()).thenReturn(putRequest); hookHandler.handle(routingContext); // verify that no enqueue (or lockedEnqueue) has been called because no ReducedPropagationManager was configured @@ -252,7 +274,7 @@ public void testListenerEnqueueWithReducedPropagationQueueingStrategy(TestContex PUTRequest putRequest = new PUTRequest(uri, originalPayload); putRequest.addHeader(CONTENT_LENGTH.getName(), "99"); - Mockito.when(routingContext.request()).thenReturn(putRequest); + when(routingContext.request()).thenReturn(putRequest); hookHandler.handle(routingContext); String targetUri = "/playground/server/push/v1/devices/" + deviceId + "/playground/server/tests/hooktest/abc123"; @@ -274,7 +296,7 @@ public void testListenerEnqueueWithInvalidReducedPropagationQueueingStrategy(Tes PUTRequest putRequest = new PUTRequest(uri, originalPayload); putRequest.addHeader(CONTENT_LENGTH.getName(), "99"); - Mockito.when(routingContext.request()).thenReturn(putRequest); + when(routingContext.request()).thenReturn(putRequest); hookHandler.handle(routingContext); // verify that enqueue has been called WITH the payload @@ -301,7 +323,7 @@ public void testListenerEnqueueWithMatchingRequestsHeaderFilter(TestContext cont putRequest.addHeader(CONTENT_LENGTH.getName(), "99"); putRequest.addHeader("x-foo", "A"); - Mockito.when(routingContext.request()).thenReturn(putRequest); + when(routingContext.request()).thenReturn(putRequest); hookHandler.handle(routingContext); // verify that enqueue has been called WITH the payload @@ -326,7 +348,7 @@ public void testListenerNoEnqueueWithoutMatchingRequestsHeaderFilter(TestContext String originalPayload = "{\"key\":123}"; PUTRequest putRequest = new PUTRequest(uri, originalPayload); putRequest.addHeader(CONTENT_LENGTH.getName(), "99"); - Mockito.when(routingContext.request()).thenReturn(putRequest); + when(routingContext.request()).thenReturn(putRequest); hookHandler.handle(routingContext); // verify that no enqueue has been called since the header did not match @@ -369,7 +391,7 @@ public void hookRegistration_usesDefaultExpiryIfExpireAfterHeaderIsNegativeNumbe } // Trigger work - Mockito.when(routingContext.request()).thenReturn(request); + when(routingContext.request()).thenReturn(request); hookHandler.handle(routingContext); // Assert request was ok @@ -394,18 +416,18 @@ public void hookRegistration_RouteWithTimeout(TestContext testContext) { final MultiMap requestHeaders = MultiMap.caseInsensitiveMultiMap(); final Buffer requestBody = new BufferImpl(); requestBody.setBytes(0, ("{" + - " \"methods\": [ \"PUT\" , \"DELETE\" ]," + - " \"destination\": \"/an/example/destination/\"," + - " \"timeout\": 42" + - "}").getBytes()); + " \"methods\": [ \"PUT\" , \"DELETE\" ]," + + " \"destination\": \"/an/example/destination/\"," + + " \"timeout\": 42" + + "}").getBytes()); request = createSimpleRequest(HttpMethod.PUT, "/gateleen/example/_hooks/route/http/my-service/my-hook", - requestHeaders, requestBody, statusCodePtr, statusMessagePtr + requestHeaders, requestBody, statusCodePtr, statusMessagePtr ); } // Trigger work - Mockito.when(routingContext.request()).thenReturn(request); + when(routingContext.request()).thenReturn(request); hookHandler.handle(routingContext); // Assert request was ok @@ -440,7 +462,7 @@ public void hookRegistration_usesDefaultExpiryWhenHeaderContainsCorruptValue(Tes } // Trigger work - Mockito.when(routingContext.request()).thenReturn(request); + when(routingContext.request()).thenReturn(request); hookHandler.handle(routingContext); // Assert request was ok @@ -471,7 +493,7 @@ public void hookRegistration_usesDefaultExpiryIfHeaderIsMissing(TestContext test } // Trigger work - Mockito.when(routingContext.request()).thenReturn(request); + when(routingContext.request()).thenReturn(request); hookHandler.handle(routingContext); // Assert request was ok @@ -502,7 +524,7 @@ public void hookRegistration_usesMinusOneIfExpireAfterIsSetToMinusOne(TestContex } // Trigger work - Mockito.when(routingContext.request()).thenReturn(request); + when(routingContext.request()).thenReturn(request); hookHandler.handle(routingContext); // Assert request was ok @@ -536,7 +558,7 @@ public void listenerRegistration_acceptOnlyWhitelistedHttpMethods(TestContext te } // Trigger - Mockito.when(routingContext.request()).thenReturn(request); + when(routingContext.request()).thenReturn(request); hookHandler.handle(routingContext); { // Assert request got accepted. @@ -572,7 +594,7 @@ public void listenerRegistration_rejectNotWhitelistedHttpMethods(TestContext tes } // Trigger - Mockito.when(routingContext.request()).thenReturn(request); + when(routingContext.request()).thenReturn(request); hookHandler.handle(routingContext); { // Assert request got rejected. @@ -642,6 +664,283 @@ public void hookRegistration_RouteWithRouteMultiplier(TestContext testContext) t } + @Test + public void testHandleGETRequestWithEmptyParam(TestContext testContext) { + // Define URI and configures the request with an empty 'q' parameter + GETRequest request = new GETRequest(HOOK_LISTENER_URI, mockResponse); + request.addParameter("q", ""); // Empty parameter to simulate bad request + + // Mock RoutingContext + when(routingContext.request()).thenReturn(request); + + // Capture response content + ArgumentCaptor responseCaptor = ArgumentCaptor.forClass(String.class); + when(mockResponse.setStatusCode(anyInt())).thenReturn(mockResponse); + when(mockResponse.end(responseCaptor.capture())).thenReturn(Future.succeededFuture()); + + // Execute the Handler + boolean result = hookHandler.handle(routingContext); + + // Verify status 400 due to empty 'q' parameter + verify(mockResponse).setStatusCode(StatusCode.BAD_REQUEST.getStatusCode()); + testContext.assertTrue(result); + // Verify captured response content + String jsonResponse = responseCaptor.getValue(); + testContext.assertNotNull(jsonResponse); + // Confirm the response contains "Bad Request" + testContext.assertTrue(jsonResponse.contains("Only the 'q' parameter is allowed and can't be empty or null")); + } + + @Test + public void testHandleGETRequestWithNoParam(TestContext testContext) { + GETRequest request = new GETRequest(HOOK_LISTENER_URI, mockResponse); + when(routingContext.request()).thenReturn(request); + + boolean result = hookHandler.handle(routingContext); + + testContext.assertFalse(result); + } + + @Test + public void testHandleGETRequestWithWrongRoute(TestContext testContext) { + String wrongUri = "/hookRootURI/registrati/listeners"; + GETRequest request = new GETRequest(wrongUri, mockResponse); + request.addParameter("q", "value"); + when(routingContext.request()).thenReturn(request); + + boolean result = hookHandler.handle(routingContext); + + testContext.assertFalse(result); + } + + @Test + public void testHandleGETRequestWithListenersSearchSingleResult() throws InterruptedException { + // Define URI and configure GET request with specific search parameter + String singleListener= "mySingleListener"; + GETRequest request = new GETRequest(HOOK_LISTENER_URI, mockResponse); + request.addParameter("q", singleListener); + + setListenerStorageEntryAndTriggerUpdate(buildListenerConfigWithHeadersFilter(null, singleListener, "x-foo: (A|B)")); + // wait a moment to let the listener be registered + Thread.sleep(200); + // Mock RoutingContext and configure response capture + when(routingContext.request()).thenReturn(request); + + ArgumentCaptor responseCaptor = ArgumentCaptor.forClass(String.class); + when(mockResponse.setStatusCode(anyInt())).thenReturn(mockResponse); + when(mockResponse.end(responseCaptor.capture())).thenReturn(Future.succeededFuture()); + + // Execute the handler + boolean result = hookHandler.handle(routingContext); + assertTrue(result); + + // Validate JSON response content for matching listener + String jsonResponse = responseCaptor.getValue(); + assertNotNull(jsonResponse); + assertTrue(jsonResponse.contains(singleListener)); + } + + @Test + public void testHandleGETRequestWithListenersSearchMultipleResults() throws InterruptedException { + // Define the URI and set up the GET request with a broader search parameter for multiple listeners + GETRequest request = new GETRequest(HOOK_LISTENER_URI, mockResponse); + request.addParameter("q", "myListener"); // Search parameter that should match multiple listeners + + // Add multiple listeners to the MockResourceStorage using the expected configuration and register them + String listenerId1 = "myListener112222"; + String listenerId2 = "myListener222133"; + String notMatchListener = "notMatchListener"; + setListenerStorageEntryAndTriggerUpdate(buildListenerConfigWithHeadersFilter(null, listenerId1, "x-foo: (A|B)")); + Thread.sleep(200); + setListenerStorageEntryAndTriggerUpdate(buildListenerConfigWithHeadersFilter(null, listenerId2, "x-foo: (A|B)")); + Thread.sleep(200); + setListenerStorageEntryAndTriggerUpdate(buildListenerConfigWithHeadersFilter(null, notMatchListener, "x-foo: (A|B)")); + Thread.sleep(200); + + // Mock the RoutingContext and set up the response capture + when(routingContext.request()).thenReturn(request); + ArgumentCaptor responseCaptor = ArgumentCaptor.forClass(String.class); + when(mockResponse.setStatusCode(anyInt())).thenReturn(mockResponse); + when(mockResponse.end(responseCaptor.capture())).thenReturn(Future.succeededFuture()); + + // Execute the handler + boolean result = hookHandler.handle(routingContext); + assertTrue(result); + + // Validate the JSON response content for multiple matching listeners + String jsonResponse = responseCaptor.getValue(); + assertNotNull(jsonResponse); + assertTrue(jsonResponse.contains(listenerId1)); + assertTrue(jsonResponse.contains(listenerId2)); + assertFalse(jsonResponse.contains(notMatchListener)); + } + + @Test + public void testHandleGETRequestWithRoutesSearchEmptyResult() { + // Define URI and configure request with specific 'q' parameter for routes search + GETRequest request = new GETRequest(HOOK_ROUTE_URI, mockResponse); + request.addParameter("q", "routeNotFound"); + + // No routes are added to MockResourceStorage to simulate empty result + storage.putMockData(HOOK_ROUTE_URI, new JsonArray().encode()); + + // Mock RoutingContext and configure response capture + when(routingContext.request()).thenReturn(request); + + ArgumentCaptor responseCaptor = ArgumentCaptor.forClass(String.class); + when(mockResponse.setStatusCode(anyInt())).thenReturn(mockResponse); + when(mockResponse.end(responseCaptor.capture())).thenReturn(Future.succeededFuture()); + + // Execute the handler + boolean result = hookHandler.handle(routingContext); + + // Verifications + assertTrue(result); + + // Verify response content with empty result + String actualResponse = responseCaptor.getValue(); + assertNotNull(actualResponse); + JsonObject jsonResponse = new JsonObject(actualResponse); + assertTrue("Expected 'routes' to be an empty array", + jsonResponse.containsKey("routes") && jsonResponse.getJsonArray("routes").isEmpty()); + } + + @Test + public void testHandleGETRequestWithRoutesSearchMultipleResults() throws InterruptedException { + // Define the URI and set up the GET request with a broad search parameter for multiple routes + GETRequest request = new GETRequest(HOOK_ROUTE_URI, mockResponse); + request.addParameter("q", "valid"); // Search parameter that should match multiple routes + + // Add multiple routes to the MockResourceStorage using the expected configuration and register them + String routeId1 = "valid12345"; + String routeId2 = "valid67890"; + String notPreset = "notPreset"; + setRouteStorageEntryAndTriggerUpdate(buildRouteConfig(routeId1)); + Thread.sleep(200); + setRouteStorageEntryAndTriggerUpdate(buildRouteConfig( routeId2)); + Thread.sleep(200); + setRouteStorageEntryAndTriggerUpdate(buildRouteConfig( notPreset)); + Thread.sleep(200); + + // Mock the RoutingContext and set up the response capture + when(routingContext.request()).thenReturn(request); + ArgumentCaptor responseCaptor = ArgumentCaptor.forClass(String.class); + when(mockResponse.setStatusCode(anyInt())).thenReturn(mockResponse); + when(mockResponse.end(responseCaptor.capture())).thenReturn(Future.succeededFuture()); + + // Execute the handler + boolean result = hookHandler.handle(routingContext); + assertTrue(result); + + // Validate the JSON response content for multiple matching routes + String jsonResponse = responseCaptor.getValue(); + assertNotNull(jsonResponse); + assertTrue(jsonResponse.contains(routeId1)); + assertTrue(jsonResponse.contains(routeId2)); + assertFalse(jsonResponse.contains(notPreset)); + } + + @Test + public void testHandleListenerWithStorageAndEmptyList() { + // Set up the URI for listeners registration + GETRequest request = new GETRequest(HOOK_LISTENER_URI,mockResponse) ; + request.addParameter("q", "validQueryParam"); + + // Capture the response output + ArgumentCaptor responseCaptor = ArgumentCaptor.forClass(String.class); + when(routingContext.request()).thenReturn(request); + + // Execute the handler and validate the response + boolean result = hookHandler.handle(routingContext); + + assertTrue(result); + verify(mockResponse).end(responseCaptor.capture()); + + // Validate the response JSON for an empty listener list + String actualResponse = responseCaptor.getValue(); + assertEmptyResult(actualResponse); + } + + @Test + public void testHandleGETRequestWithExtraParam(TestContext testContext) { + // Define URI and configure the request with an extra parameter besides 'q' + GETRequest request = new GETRequest(HOOK_LISTENER_URI, mockResponse); + request.addParameter("q", "validQueryParam"); + request.addParameter("extra", "notAllowedParam"); // Extra parameter, not allowed + + // Mock the RoutingContext + when(routingContext.request()).thenReturn(request); + + // Capture the response content + ArgumentCaptor responseCaptor = ArgumentCaptor.forClass(String.class); + when(mockResponse.setStatusCode(anyInt())).thenReturn(mockResponse); + when(mockResponse.end(responseCaptor.capture())).thenReturn(Future.succeededFuture()); + + // Execute the Handler + boolean result = hookHandler.handle(routingContext); + + // Verify status 400 due to the extra parameter + verify(mockResponse).setStatusCode(StatusCode.BAD_REQUEST.getStatusCode()); + testContext.assertTrue(result); + + // Verify captured response content + String jsonResponse = responseCaptor.getValue(); + testContext.assertNotNull(jsonResponse); + // Confirm that the response contains "Bad Request" + testContext.assertTrue(jsonResponse.contains("Only the 'q' parameter is allowed and can't be empty or null")); + } + + @Test + public void testHandleGETRequestWithTrailingSlash(TestContext testContext) { + // Define URI with trailing slash and configure the request + GETRequest request = new GETRequest(HOOK_LISTENER_URI + "/", mockResponse); + request.addParameter("q", "validQueryParam"); + + // Mock the RoutingContext + when(routingContext.request()).thenReturn(request); + + // Capture the response content + ArgumentCaptor responseCaptor = ArgumentCaptor.forClass(String.class); + when(mockResponse.end(responseCaptor.capture())).thenReturn(Future.succeededFuture()); + + // Execute the Handler + boolean result = hookHandler.handle(routingContext); + + // Verify the result contains an empty listeners list + testContext.assertTrue(result); + String jsonResponse = responseCaptor.getValue(); + assertEmptyResult(jsonResponse); + } + + @Test + public void testHandleGETRequestWithInvalidParam(TestContext testContext) { + // Define URI with an invalid parameter different from 'q' + GETRequest request = new GETRequest(HOOK_LISTENER_URI, mockResponse); + request.addParameter("invalidParam", "someValue"); // Invalid parameter, not 'q' + + // Mock the RoutingContext + when(routingContext.request()).thenReturn(request); + + // Capture the response content + ArgumentCaptor responseCaptor = ArgumentCaptor.forClass(String.class); + when(mockResponse.setStatusCode(anyInt())).thenReturn(mockResponse); + when(mockResponse.end(responseCaptor.capture())).thenReturn(Future.succeededFuture()); + + // Execute the Handler + boolean result = hookHandler.handle(routingContext); + + // Verify status 400 due to invalid parameter + verify(mockResponse).setStatusCode(StatusCode.BAD_REQUEST.getStatusCode()); + testContext.assertTrue(result); + + // Verify captured response content + String jsonResponse = responseCaptor.getValue(); + testContext.assertNotNull(jsonResponse); + // Confirm that the response contains "Bad Request" + testContext.assertTrue(jsonResponse.contains("Only the 'q' parameter is allowed and can't be empty or null")); + } + + /////////////////////////////////////////////////////////////////////////////// // Helpers /////////////////////////////////////////////////////////////////////////////// @@ -664,7 +963,7 @@ public HttpServerResponse response() { }; putRequest.addHeader(CONTENT_LENGTH.getName(), "99"); - Mockito.when(routingContext.request()).thenReturn(putRequest); + when(routingContext.request()).thenReturn(putRequest); hookHandler.handle(routingContext); latch.await(); @@ -878,6 +1177,13 @@ private Buffer toBuffer(JsonObject jsonObject) { return buffer; } + private static void assertEmptyResult(String actualResponse) { + assertNotNull(actualResponse); + JsonObject jsonResponse = new JsonObject(actualResponse); + assertTrue("Expected 'listeners' to be an empty array", + jsonResponse.containsKey("listeners") && jsonResponse.getJsonArray("listeners").isEmpty()); + } + static class PUTRequest extends DummyHttpServerRequest { MultiMap headers = MultiMap.caseInsensitiveMultiMap(); @@ -914,4 +1220,50 @@ public void addHeader(String headerName, String headerValue) { headers.add(headerName, headerValue); } } + + static class GETRequest extends DummyHttpServerRequest { + MultiMap headers = MultiMap.caseInsensitiveMultiMap(); + MultiMap params = MultiMap.caseInsensitiveMultiMap(); + private final HttpServerResponse response; + private final String uri; + + public GETRequest(String uri, HttpServerResponse response) { + this.uri = uri; + this.response = response; + } + + @Override + public HttpMethod method() { + return HttpMethod.GET; + } + + @Override + public String uri() { + return uri; + } + + @Override + public MultiMap headers() { + return headers; + } + + @Override + public MultiMap params() { + return params; + } + + @Override + public HttpServerResponse response() { + return response; + } + + @Override + public String getParam(String paramName) { + return params.get(paramName); + } + + public void addParameter(String paramName, String paramValue) { + params.add(paramName, paramValue); + } + } } diff --git a/gateleen-kafka/pom.xml b/gateleen-kafka/pom.xml index 725625046..dd6bf50e2 100644 --- a/gateleen-kafka/pom.xml +++ b/gateleen-kafka/pom.xml @@ -6,7 +6,7 @@ org.swisspush.gateleen gateleen - 2.1.12-SNAPSHOT + 2.1.13-SNAPSHOT gateleen-kafka diff --git a/gateleen-logging/pom.xml b/gateleen-logging/pom.xml index 40ebe5a6a..74b30ea88 100644 --- a/gateleen-logging/pom.xml +++ b/gateleen-logging/pom.xml @@ -6,7 +6,7 @@ org.swisspush.gateleen gateleen - 2.1.12-SNAPSHOT + 2.1.13-SNAPSHOT gateleen-logging diff --git a/gateleen-merge/pom.xml b/gateleen-merge/pom.xml index 5c53639d7..6aae5e51f 100644 --- a/gateleen-merge/pom.xml +++ b/gateleen-merge/pom.xml @@ -6,7 +6,7 @@ org.swisspush.gateleen gateleen - 2.1.12-SNAPSHOT + 2.1.13-SNAPSHOT gateleen-merge diff --git a/gateleen-monitoring/pom.xml b/gateleen-monitoring/pom.xml index 748f87a04..a525247ee 100644 --- a/gateleen-monitoring/pom.xml +++ b/gateleen-monitoring/pom.xml @@ -6,7 +6,7 @@ org.swisspush.gateleen gateleen - 2.1.12-SNAPSHOT + 2.1.13-SNAPSHOT gateleen-monitoring diff --git a/gateleen-packing/pom.xml b/gateleen-packing/pom.xml index 8dcd4067d..e896dea69 100644 --- a/gateleen-packing/pom.xml +++ b/gateleen-packing/pom.xml @@ -6,7 +6,7 @@ org.swisspush.gateleen gateleen - 2.1.12-SNAPSHOT + 2.1.13-SNAPSHOT gateleen-packing diff --git a/gateleen-player/pom.xml b/gateleen-player/pom.xml index b7388fba6..ac3dc8e79 100644 --- a/gateleen-player/pom.xml +++ b/gateleen-player/pom.xml @@ -6,7 +6,7 @@ org.swisspush.gateleen gateleen - 2.1.12-SNAPSHOT + 2.1.13-SNAPSHOT gateleen-player diff --git a/gateleen-playground/pom.xml b/gateleen-playground/pom.xml index b021cb2e3..f47fd876b 100644 --- a/gateleen-playground/pom.xml +++ b/gateleen-playground/pom.xml @@ -6,7 +6,7 @@ org.swisspush.gateleen gateleen - 2.1.12-SNAPSHOT + 2.1.13-SNAPSHOT gateleen-playground diff --git a/gateleen-qos/pom.xml b/gateleen-qos/pom.xml index b7ee05fd2..8ca94b3e2 100644 --- a/gateleen-qos/pom.xml +++ b/gateleen-qos/pom.xml @@ -6,7 +6,7 @@ org.swisspush.gateleen gateleen - 2.1.12-SNAPSHOT + 2.1.13-SNAPSHOT gateleen-qos diff --git a/gateleen-queue/pom.xml b/gateleen-queue/pom.xml index 053115900..99204819e 100644 --- a/gateleen-queue/pom.xml +++ b/gateleen-queue/pom.xml @@ -6,7 +6,7 @@ org.swisspush.gateleen gateleen - 2.1.12-SNAPSHOT + 2.1.13-SNAPSHOT gateleen-queue diff --git a/gateleen-queue/src/main/java/org/swisspush/gateleen/queue/queuing/QueueClient.java b/gateleen-queue/src/main/java/org/swisspush/gateleen/queue/queuing/QueueClient.java index c85c94848..7251ffbde 100755 --- a/gateleen-queue/src/main/java/org/swisspush/gateleen/queue/queuing/QueueClient.java +++ b/gateleen-queue/src/main/java/org/swisspush/gateleen/queue/queuing/QueueClient.java @@ -13,6 +13,8 @@ import org.swisspush.gateleen.core.util.StatusCode; import org.swisspush.gateleen.monitoring.MonitoringHandler; +import javax.annotation.Nullable; + import static org.swisspush.redisques.util.RedisquesAPI.*; /** @@ -32,7 +34,7 @@ public class QueueClient implements RequestQueue { * @param vertx vertx * @param monitoringHandler monitoringHandler */ - public QueueClient(Vertx vertx, MonitoringHandler monitoringHandler) { + public QueueClient(Vertx vertx, @Nullable MonitoringHandler monitoringHandler) { this.vertx = vertx; this.monitoringHandler = monitoringHandler; } @@ -109,7 +111,7 @@ public void lockedEnqueue(HttpRequest queuedRequest, String queue, String lockRe vertx.eventBus().request(getRedisquesAddress(), buildLockedEnqueueOperation(queue, queuedRequest.toJsonObject().put(QUEUE_TIMESTAMP, System.currentTimeMillis()).encode(), lockRequestedBy), (Handler>>) event -> { - if (OK.equals(event.result().body().getString(STATUS))) { + if (OK.equals(event.result().body().getString(STATUS)) && monitoringHandler != null) { monitoringHandler.updateLastUsedQueueSizeInformation(queue); monitoringHandler.updateEnqueue(); } @@ -172,8 +174,10 @@ public Future enqueueFuture(HttpRequest queuedRequest, String queue) { queuedRequest.toJsonObject().put(QUEUE_TIMESTAMP, System.currentTimeMillis()).encode()), (Handler>>) event -> { if (OK.equals(event.result().body().getString(STATUS))) { - monitoringHandler.updateLastUsedQueueSizeInformation(queue); - monitoringHandler.updateEnqueue(); + if(monitoringHandler != null) { + monitoringHandler.updateLastUsedQueueSizeInformation(queue); + monitoringHandler.updateEnqueue(); + } promise.complete(); } else { promise.fail(event.result().body().getString(MESSAGE)); @@ -216,8 +220,10 @@ private void enqueue(final HttpServerRequest request, HttpRequest queuedRequest, vertx.eventBus().request(getRedisquesAddress(), buildEnqueueOperation(queue, queuedRequest.toJsonObject().put(QUEUE_TIMESTAMP, System.currentTimeMillis()).encode()), (Handler>>) event -> { if (OK.equals(event.result().body().getString(STATUS))) { - monitoringHandler.updateLastUsedQueueSizeInformation(queue); - monitoringHandler.updateEnqueue(); + if(monitoringHandler != null) { + monitoringHandler.updateLastUsedQueueSizeInformation(queue); + monitoringHandler.updateEnqueue(); + } if (request != null) { ResponseStatusCodeLogUtil.info(request, StatusCode.ACCEPTED, QueueClient.class); diff --git a/gateleen-queue/src/main/java/org/swisspush/gateleen/queue/queuing/QueueProcessor.java b/gateleen-queue/src/main/java/org/swisspush/gateleen/queue/queuing/QueueProcessor.java index 7e2a8214a..8c68d077b 100755 --- a/gateleen-queue/src/main/java/org/swisspush/gateleen/queue/queuing/QueueProcessor.java +++ b/gateleen-queue/src/main/java/org/swisspush/gateleen/queue/queuing/QueueProcessor.java @@ -25,6 +25,8 @@ import org.swisspush.gateleen.queue.queuing.circuitbreaker.util.QueueCircuitState; import org.swisspush.gateleen.queue.queuing.circuitbreaker.util.QueueResponseType; +import javax.annotation.Nullable; + import static io.vertx.core.Future.failedFuture; import static io.vertx.core.Future.succeededFuture; import static io.vertx.core.buffer.Buffer.buffer; @@ -49,18 +51,18 @@ public class QueueProcessor { private Logger log = LoggerFactory.getLogger(QueueProcessor.class); - public QueueProcessor(final Vertx vertx, final HttpClient httpClient, final MonitoringHandler monitoringHandler) { + public QueueProcessor(final Vertx vertx, final HttpClient httpClient, @Nullable final MonitoringHandler monitoringHandler) { this(vertx, httpClient, monitoringHandler, null); } - public QueueProcessor(final Vertx vertx, final HttpClient httpClient, final MonitoringHandler monitoringHandler, QueueCircuitBreaker queueCircuitBreaker) { + public QueueProcessor(final Vertx vertx, final HttpClient httpClient, @Nullable final MonitoringHandler monitoringHandler, QueueCircuitBreaker queueCircuitBreaker) { this(vertx, httpClient, monitoringHandler, queueCircuitBreaker, newGateleenThriftyExceptionFactory(), true); } public QueueProcessor( Vertx vertx, HttpClient httpClient, - MonitoringHandler monitoringHandler, + @Nullable MonitoringHandler monitoringHandler, QueueCircuitBreaker queueCircuitBreaker, GateleenExceptionFactory exceptionFactory, boolean immediatelyStartQueueProcessing @@ -275,7 +277,9 @@ private void executeQueuedRequest(Message message, Logger logger, Ht } message.reply(new JsonObject().put(STATUS, OK)); performCircuitBreakerActions(queueName, queuedRequest, SUCCESS, state); - monitoringHandler.updateDequeue(); + if(monitoringHandler != null) { + monitoringHandler.updateDequeue(); + } } else if (QueueRetryUtil.retryQueueItem(queuedRequest.getHeaders(), statusCode, logger)) { logger.info("Failed queued request to {}: {} {}", queuedRequest.getUri(), statusCode, response.statusMessage()); message.reply(new JsonObject().put(STATUS, ERROR).put(MESSAGE, statusCode + " " + response.statusMessage())); diff --git a/gateleen-queue/src/main/java/org/swisspush/gateleen/queue/queuing/QueuingHandler.java b/gateleen-queue/src/main/java/org/swisspush/gateleen/queue/queuing/QueuingHandler.java index e95c0face..00421e931 100755 --- a/gateleen-queue/src/main/java/org/swisspush/gateleen/queue/queuing/QueuingHandler.java +++ b/gateleen-queue/src/main/java/org/swisspush/gateleen/queue/queuing/QueuingHandler.java @@ -14,6 +14,8 @@ import org.swisspush.gateleen.queue.queuing.splitter.NoOpQueueSplitter; import org.swisspush.gateleen.queue.queuing.splitter.QueueSplitter; +import javax.annotation.Nullable; + import static org.swisspush.redisques.util.RedisquesAPI.buildCheckOperation; /** @@ -24,6 +26,7 @@ public class QueuingHandler implements Handler { public static final String QUEUE_HEADER = "x-queue"; + public static final String ORIGINALLY_QUEUED_HEADER = "x-originally-queued"; public static final String DUPLICATE_CHECK_HEADER = "x-duplicate-check"; private final RequestQueue requestQueue; @@ -42,7 +45,7 @@ public QueuingHandler( Vertx vertx, RedisProvider redisProvider, HttpServerRequest request, - MonitoringHandler monitoringHandler + @Nullable MonitoringHandler monitoringHandler ) { this(vertx, redisProvider, request, new QueueClient(vertx, monitoringHandler), new NoOpQueueSplitter()); } @@ -51,7 +54,7 @@ public QueuingHandler( Vertx vertx, RedisProvider redisProvider, HttpServerRequest request, - MonitoringHandler monitoringHandler, + @Nullable MonitoringHandler monitoringHandler, QueueSplitter queueSplitter ) { this( @@ -84,6 +87,9 @@ public void handle(final Buffer buffer) { // Remove the queue header to avoid feedback loop headers.remove(QUEUE_HEADER); + // Add a header to indicate that this request was initially queued + headers.add(ORIGINALLY_QUEUED_HEADER, "true"); + if (headers.names().contains(DUPLICATE_CHECK_HEADER)) { DuplicateCheckHandler.checkDuplicateRequest(redisProvider, request.uri(), buffer, headers.get(DUPLICATE_CHECK_HEADER), requestIsDuplicate -> { if (requestIsDuplicate) { diff --git a/gateleen-queue/src/test/java/org/swisspush/gateleen/queue/queuing/QueueClientTest.java b/gateleen-queue/src/test/java/org/swisspush/gateleen/queue/queuing/QueueClientTest.java index feb879e1b..de0675b47 100644 --- a/gateleen-queue/src/test/java/org/swisspush/gateleen/queue/queuing/QueueClientTest.java +++ b/gateleen-queue/src/test/java/org/swisspush/gateleen/queue/queuing/QueueClientTest.java @@ -69,6 +69,31 @@ public void testEnqueueFuture(TestContext context){ Mockito.verify(monitoringHandler, Mockito.timeout(1000).times(1)).updateEnqueue(); } + @Test + public void testEnqueueFutureNotUpdatingMonitoringHandlerWhenNotProvided(TestContext context){ + Async async = context.async(); + + // create a QueueClient without monitoringHandler + queueClient = new QueueClient(vertx, null); + + /* + * consume event bus messages directed to redisques and verify message content. + * reply with 'success' for enqueuing + */ + vertx.eventBus().localConsumer(Address.redisquesAddress(), (Handler>) message -> { + validateMessage(context, message, QueueOperation.enqueue, "myQueue"); + message.reply(new JsonObject().put(STATUS, OK)); + }); + + HttpRequest request = new HttpRequest(HttpMethod.PUT, "/targetUri", MultiMap.caseInsensitiveMultiMap(), Buffer.buffer("{\"key\":\"value\"}").getBytes()); + queueClient.enqueueFuture(request, "myQueue").onComplete(event -> { + context.assertTrue(event.succeeded()); + async.complete(); + }); + + Mockito.verifyNoInteractions(monitoringHandler); + } + @Test public void testEnqueueFutureNotUpdatingMonitoringHandlerOnRedisquesFail(TestContext context){ Async async = context.async(); diff --git a/gateleen-routing/README_routing.md b/gateleen-routing/README_routing.md index d89614afc..3a50831a8 100644 --- a/gateleen-routing/README_routing.md +++ b/gateleen-routing/README_routing.md @@ -134,4 +134,47 @@ Examples: } } ``` -Each request header entry is validated in the format `: `, so you are able to filter for request header names and values. \ No newline at end of file +Each request header entry is validated in the format `: `, so you are able to filter for request header names and values. + +## Micrometer metrics +The routing feature is monitored with micrometer. The following metrics are available: +* gateleen_forwarded_total +* gateleen_forwarded_seconds +* gateleen_forwarded_seconds_max +* gateleen_forwarded_seconds_count +* gateleen_forwarded_seconds_sum + +Additional tags are provided to split the forward count into sub counts. + +| tag | description | +|------------|-------------------------------------------------------------------------------------------------------------------| +| metricName | The `metricName` property from the corresponding routing rule. With this, you are able to count requests per rule | +| type | Describes where the request was forwarded to. Possible values are `local`, `external` and `null` | +| quantile | Values of `0.75` and `0.95` for percentile durations of requests | + + +Example metrics: + +``` +# HELP gateleen_forwarded_total Amount of forwarded requests +# TYPE gateleen_forwarded_total counter +gateleen_forwarded_total{metricName="storage-resources",type="storage",} 67565.0 +gateleen_forwarded_total{metricName="infotool_v1_informations",type="external",} 655.0 +gateleen_forwarded_total{metricName="infotool-v1",type="storage",} 4320.0 +# HELP gateleen_forwarded_seconds_max Durations of forwarded requests +# TYPE gateleen_forwarded_seconds_max gauge +gateleen_forwarded_seconds_max{metricName="storage-resources",type="storage",} 8.5515 +gateleen_forwarded_seconds_max{metricName="infotool_v1_informations",type="external",} 3.456 +# HELP gateleen_forwarded_seconds Durations of forwarded requests +# TYPE gateleen_forwarded_seconds summary +gateleen_forwarded_seconds{metricName="storage-resources",type="storage",quantile="0.75",} 6.2158 +gateleen_forwarded_seconds{metricName="storage-resources",type="storage",quantile="0.95",} 8.2123 +gateleen_forwarded_seconds_count{metricName="storage-resources",type="storage",} 67565.0 +gateleen_forwarded_seconds_sum{metricName="storage-resources",type="storage",} 656434.0 +gateleen_forwarded_seconds{metricName="infotool_v1_informations",type="external",quantile="0.75",} 4.2365 +gateleen_forwarded_seconds{metricName="infotool_v1_informations",type="external",quantile="0.95",} 4.8998 +gateleen_forwarded_seconds_count{metricName="infotool_v1_informations",type="external",} 7567.0 +gateleen_forwarded_seconds_sum{metricName="infotool_v1_informations",type="external",} 256324.0 +``` + +To enable the metrics, set a `MeterRegistry` instance by calling `withMeterRegistry(MeterRegistry meterRegistry)` method in `RouterBuilder` class. \ No newline at end of file diff --git a/gateleen-routing/pom.xml b/gateleen-routing/pom.xml index 78ac1902a..91cfa062c 100644 --- a/gateleen-routing/pom.xml +++ b/gateleen-routing/pom.xml @@ -6,7 +6,7 @@ org.swisspush.gateleen gateleen - 2.1.12-SNAPSHOT + 2.1.13-SNAPSHOT gateleen-routing @@ -27,6 +27,10 @@ gateleen-monitoring ${project.version} + + io.micrometer + micrometer-core + diff --git a/gateleen-routing/src/main/java/org/swisspush/gateleen/routing/AbstractForwarder.java b/gateleen-routing/src/main/java/org/swisspush/gateleen/routing/AbstractForwarder.java index 7cab72a0e..dccf549bb 100644 --- a/gateleen-routing/src/main/java/org/swisspush/gateleen/routing/AbstractForwarder.java +++ b/gateleen-routing/src/main/java/org/swisspush/gateleen/routing/AbstractForwarder.java @@ -1,5 +1,8 @@ package org.swisspush.gateleen.routing; +import javax.annotation.Nullable; + +import io.micrometer.core.instrument.MeterRegistry; import io.vertx.core.Handler; import io.vertx.core.http.HttpServerRequest; import io.vertx.ext.web.RoutingContext; @@ -18,21 +21,34 @@ public abstract class AbstractForwarder implements Handler { protected final LoggingResourceManager loggingResourceManager; protected final LogAppenderRepository logAppenderRepository; protected final MonitoringHandler monitoringHandler; + protected final String metricNameTag; + + public static final String FORWARDER_COUNT_METRIC_NAME = "gateleen.forwarded"; + public static final String FORWARDER_COUNT_METRIC_DESCRIPTION = "Amount of forwarded requests"; + public static final String FORWARDS_METRIC_NAME = "gateleen.forwarded.seconds"; + public static final String FORWARDS_METRIC_DESCRIPTION = "Durations of forwarded requests"; + public static final String FORWARDER_METRIC_TAG_TYPE = "type"; + public static final String FORWARDER_METRIC_TAG_METRICNAME = "metricName"; + public static final String FORWARDER_NO_METRICNAME = "no-metric-name"; - public AbstractForwarder(Rule rule, LoggingResourceManager loggingResourceManager, LogAppenderRepository logAppenderRepository, MonitoringHandler monitoringHandler) { + public AbstractForwarder(Rule rule, LoggingResourceManager loggingResourceManager, LogAppenderRepository logAppenderRepository, @Nullable MonitoringHandler monitoringHandler) { this.rule = rule; this.loggingResourceManager = loggingResourceManager; this.logAppenderRepository = logAppenderRepository; this.monitoringHandler = monitoringHandler; + + this.metricNameTag = rule.getMetricName() != null ? rule.getMetricName() : FORWARDER_NO_METRICNAME; } + protected abstract void setMeterRegistry(MeterRegistry meterRegistry); + protected boolean doHeadersFilterMatch(final HttpServerRequest request) { final Logger log = RequestLoggerFactory.getLogger(getClass(), request); - if(rule.getHeadersFilterPattern() != null){ + if (rule.getHeadersFilterPattern() != null) { log.debug("Looking for request headers with pattern {}", rule.getHeadersFilterPattern().pattern()); boolean matchFound = HttpHeaderUtil.hasMatchingHeader(request.headers(), rule.getHeadersFilterPattern()); - if(matchFound) { + if (matchFound) { log.debug("Matching request headers found"); } else { log.debug("No matching request headers found. Looking for the next routing rule"); @@ -64,4 +80,11 @@ protected void respondError(HttpServerRequest req, StatusCode statusCode) { log.warn("IllegalStateException while sending error response for {}", req.uri(), ex); } } + + protected String getRequestTarget(String target) { + if (target != null && (target.contains("localhost") || target.contains("127.0.0.1"))) { + return "local"; + } + return "external"; + } } diff --git a/gateleen-routing/src/main/java/org/swisspush/gateleen/routing/Forwarder.java b/gateleen-routing/src/main/java/org/swisspush/gateleen/routing/Forwarder.java index 3a049add7..9eb2a7189 100755 --- a/gateleen-routing/src/main/java/org/swisspush/gateleen/routing/Forwarder.java +++ b/gateleen-routing/src/main/java/org/swisspush/gateleen/routing/Forwarder.java @@ -1,5 +1,8 @@ package org.swisspush.gateleen.routing; +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Timer; import io.netty.channel.ConnectTimeoutException; import io.netty.handler.codec.http.HttpResponseStatus; import io.vertx.core.*; @@ -64,10 +67,13 @@ public class Forwarder extends AbstractForwarder { private static final int STATUS_CODE_2XX = 2; private static final Logger LOG = LoggerFactory.getLogger(Forwarder.class); + private Counter forwardCounter; + private Timer forwardTimer; + private MeterRegistry meterRegistry; public Forwarder(Vertx vertx, HttpClient client, Rule rule, final ResourceStorage storage, - LoggingResourceManager loggingResourceManager, LogAppenderRepository logAppenderRepository, MonitoringHandler monitoringHandler, - String userProfilePath, @Nullable AuthStrategy authStrategy) { + LoggingResourceManager loggingResourceManager, LogAppenderRepository logAppenderRepository, + @Nullable MonitoringHandler monitoringHandler, String userProfilePath, @Nullable AuthStrategy authStrategy) { super(rule, loggingResourceManager, logAppenderRepository, monitoringHandler); this.vertx = vertx; this.client = client; @@ -82,6 +88,31 @@ public Forwarder(Vertx vertx, HttpClient client, Rule rule, final ResourceStorag this.authStrategy = authStrategy; } + /** + * Sets the MeterRegistry for this Forwarder. + * If the provided MeterRegistry is not null, it initializes the forwardCounter + * with the appropriate metric name, description, and tags. + * + * @param meterRegistry the MeterRegistry to set + */ + @Override + public void setMeterRegistry(MeterRegistry meterRegistry) { + this.meterRegistry = meterRegistry; + if (meterRegistry != null) { + forwardCounter = Counter.builder(FORWARDER_COUNT_METRIC_NAME) + .description(FORWARDER_COUNT_METRIC_DESCRIPTION) + .tag(FORWARDER_METRIC_TAG_TYPE, getRequestTarget(target)) + .tag(FORWARDER_METRIC_TAG_METRICNAME, metricNameTag) + .register(meterRegistry); + forwardTimer = Timer.builder(FORWARDS_METRIC_NAME) + .description(FORWARDS_METRIC_DESCRIPTION) + .publishPercentiles(0.75, 0.95) + .tag(FORWARDER_METRIC_TAG_METRICNAME, metricNameTag) + .tag(FORWARDER_METRIC_TAG_TYPE, getRequestTarget(target)) + .register(meterRegistry); + } + } + private Map createProfileHeaderValues(JsonObject profile, Logger log) { Map profileValues = new HashMap<>(); if (rule.getProfile() != null) { @@ -148,15 +179,22 @@ public void handle(final RoutingContext ctx, final Buffer bodyData, @Nullable fi port = rule.getPort(); } target = rule.getHost() + ":" + port; - monitoringHandler.updateRequestsMeter(target, req.uri()); - monitoringHandler.updateRequestPerRuleMonitoring(req, rule.getMetricName()); + + if (forwardCounter != null) { + forwardCounter.increment(); + } + + if (monitoringHandler != null) { + monitoringHandler.updateRequestsMeter(target, req.uri()); + monitoringHandler.updateRequestPerRuleMonitoring(req, rule.getMetricName()); + } final String targetUri = urlPattern.matcher(req.uri()).replaceFirst(rule.getPath()).replaceAll("\\/\\/", "/"); log.debug("Forwarding request: {} to {}://{} with rule {}", req.uri(), rule.getScheme(), target + targetUri, rule.getRuleIdentifier()); final String userId = extractUserId(req, log); req.pause(); // pause the request to avoid problems with starting another async request (storage) maybeAuthenticate(rule).onComplete(event -> { - if(event.failed()) { + if (event.failed()) { req.resume(); log.error("Failed to authenticate request. Cause: {}", event.cause().getMessage()); respondError(req, StatusCode.UNAUTHORIZED); @@ -184,6 +222,12 @@ public void handle(final RoutingContext ctx, final Buffer bodyData, @Nullable fi }); } + private void handleForwardDurationMetrics(Timer.Sample timerSample) { + if (timerSample != null && forwardTimer != null) { + timerSample.stop(forwardTimer); + } + } + /** * Returns the userId defined in the on-behalf-of-header if provided, the userId from user-header otherwise. * @@ -240,7 +284,19 @@ private void handleRequest(final HttpServerRequest req, final Buffer bodyData, f final String uniqueId = req.headers().get("x-rp-unique_id"); final String timeout = req.headers().get("x-timeout"); - final long startTime = monitoringHandler.startRequestMetricTracking(rule.getMetricName(), req.uri()); + Long startTime = null; + + Timer.Sample timerSample = null; + if (meterRegistry != null) { + timerSample = Timer.start(meterRegistry); + } + + if (monitoringHandler != null) { + startTime = monitoringHandler.startRequestMetricTracking(rule.getMetricName(), req.uri()); + } + + Long finalStartTime = startTime; + Timer.Sample finalTimerSample = timerSample; client.request(req.method(), port, rule.getHost(), targetUri, new Handler<>() { @Override @@ -256,7 +312,7 @@ public void handle(AsyncResult event) { return; } HttpClientRequest cReq = event.result(); - final Handler> cResHandler = getAsyncHttpClientResponseHandler(req, targetUri, log, profileHeaderMap, loggingHandler, startTime, afterHandler); + final Handler> cResHandler = getAsyncHttpClientResponseHandler(req, targetUri, log, profileHeaderMap, loggingHandler, finalStartTime, finalTimerSample, afterHandler); cReq.response(cResHandler); if (timeout != null) { @@ -290,7 +346,7 @@ public void handle(AsyncResult event) { return; } - installExceptionHandler(req, targetUri, startTime, cReq); + installExceptionHandler(req, targetUri, finalStartTime, finalTimerSample, cReq); /* * If no bodyData is available @@ -421,9 +477,14 @@ private void setProfileHeaders(Logger log, Map profileHeaderMap, } } - private void installExceptionHandler(final HttpServerRequest req, final String targetUri, final long startTime, HttpClientRequest cReq) { + private void installExceptionHandler(final HttpServerRequest req, final String targetUri, final Long startTime, @Nullable Timer.Sample timerSample, HttpClientRequest cReq) { cReq.exceptionHandler(exception -> { - monitoringHandler.stopRequestMetricTracking(rule.getMetricName(), startTime, req.uri()); + if (monitoringHandler != null && startTime != null) { + monitoringHandler.stopRequestMetricTracking(rule.getMetricName(), startTime, req.uri()); + } + + handleForwardDurationMetrics(timerSample); + if (exception instanceof TimeoutException) { error("Timeout", req, targetUri); respondError(req, StatusCode.TIMEOUT); @@ -445,7 +506,7 @@ private void installExceptionHandler(final HttpServerRequest req, final String t }); } - private Handler> getAsyncHttpClientResponseHandler(final HttpServerRequest req, final String targetUri, final Logger log, final Map profileHeaderMap, final LoggingHandler loggingHandler, final long startTime, @Nullable final Handler afterHandler) { + private Handler> getAsyncHttpClientResponseHandler(final HttpServerRequest req, final String targetUri, final Logger log, final Map profileHeaderMap, final LoggingHandler loggingHandler, @Nullable final Long startTime, @Nullable Timer.Sample timerSample, @Nullable final Handler afterHandler) { return asyncResult -> { HttpClientResponse cRes = asyncResult.result(); if (asyncResult.failed()) { @@ -462,7 +523,12 @@ private Handler> getAsyncHttpClientResponseHandl } return; } - monitoringHandler.stopRequestMetricTracking(rule.getMetricName(), startTime, req.uri()); + if (monitoringHandler != null) { + monitoringHandler.stopRequestMetricTracking(rule.getMetricName(), startTime, req.uri()); + } + + handleForwardDurationMetrics(timerSample); + loggingHandler.setResponse(cRes); req.response().setStatusCode(cRes.statusCode()); req.response().setStatusMessage(cRes.statusMessage()); diff --git a/gateleen-routing/src/main/java/org/swisspush/gateleen/routing/NullForwarder.java b/gateleen-routing/src/main/java/org/swisspush/gateleen/routing/NullForwarder.java index 8d1b8fb48..bed0d498a 100755 --- a/gateleen-routing/src/main/java/org/swisspush/gateleen/routing/NullForwarder.java +++ b/gateleen-routing/src/main/java/org/swisspush/gateleen/routing/NullForwarder.java @@ -1,5 +1,7 @@ package org.swisspush.gateleen.routing; +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.MeterRegistry; import io.vertx.core.MultiMap; import io.vertx.core.buffer.Buffer; import io.vertx.core.eventbus.EventBus; @@ -17,6 +19,8 @@ import org.swisspush.gateleen.logging.LoggingResourceManager; import org.swisspush.gateleen.monitoring.MonitoringHandler; +import javax.annotation.Nullable; + /** * Consumes requests without forwarding them anywhere. * @@ -25,12 +29,31 @@ public class NullForwarder extends AbstractForwarder { private EventBus eventBus; + private Counter forwardCounter; - public NullForwarder(Rule rule, LoggingResourceManager loggingResourceManager, LogAppenderRepository logAppenderRepository, MonitoringHandler monitoringHandler, EventBus eventBus) { + public NullForwarder(Rule rule, LoggingResourceManager loggingResourceManager, LogAppenderRepository logAppenderRepository, @Nullable MonitoringHandler monitoringHandler, EventBus eventBus) { super(rule, loggingResourceManager, logAppenderRepository, monitoringHandler); this.eventBus = eventBus; } + /** + * Sets the MeterRegistry for this NullForwarder. + * If the provided MeterRegistry is not null, it initializes the forwardCounter + * with the appropriate metric name, description, and tags. + * + * @param meterRegistry the MeterRegistry to set + */ + @Override + public void setMeterRegistry(MeterRegistry meterRegistry) { + if(meterRegistry != null) { + forwardCounter = Counter.builder(FORWARDER_COUNT_METRIC_NAME) + .description(FORWARDER_COUNT_METRIC_DESCRIPTION) + .tag(FORWARDER_METRIC_TAG_METRICNAME, metricNameTag) + .tag(FORWARDER_METRIC_TAG_TYPE, "null") + .register(meterRegistry); + } + } + @Override public void handle(final RoutingContext ctx) { final Logger log = RequestLoggerFactory.getLogger(NullForwarder.class, ctx.request()); @@ -40,7 +63,13 @@ public void handle(final RoutingContext ctx) { return; } - monitoringHandler.updateRequestPerRuleMonitoring(ctx.request(), rule.getMetricName()); + if(forwardCounter != null) { + forwardCounter.increment(); + } + + if(monitoringHandler != null) { + monitoringHandler.updateRequestPerRuleMonitoring(ctx.request(), rule.getMetricName()); + } final LoggingHandler loggingHandler = new LoggingHandler(loggingResourceManager, logAppenderRepository, ctx.request(), eventBus); log.debug("Not forwarding request: {} with rule {}", ctx.request().uri(), rule.getRuleIdentifier()); final HeadersMultiMap requestHeaders = new HeadersMultiMap(); diff --git a/gateleen-routing/src/main/java/org/swisspush/gateleen/routing/Router.java b/gateleen-routing/src/main/java/org/swisspush/gateleen/routing/Router.java index 7c430f2e5..80add04a2 100755 --- a/gateleen-routing/src/main/java/org/swisspush/gateleen/routing/Router.java +++ b/gateleen-routing/src/main/java/org/swisspush/gateleen/routing/Router.java @@ -1,5 +1,6 @@ package org.swisspush.gateleen.routing; +import io.micrometer.core.instrument.MeterRegistry; import io.vertx.core.Handler; import io.vertx.core.Vertx; import io.vertx.core.buffer.Buffer; @@ -62,6 +63,7 @@ public class Router implements Refreshable, LoggableResource, ConfigurationResou private final LoggingResourceManager loggingResourceManager; private final LogAppenderRepository logAppenderRepository; private final MonitoringHandler monitoringHandler; + private final MeterRegistry meterRegistry; private final Logger log = LoggerFactory.getLogger(Router.class); private final Logger cleanupLogger = LoggerFactory.getLogger(Router.class.getName() + "Cleanup"); private final Vertx vertx; @@ -108,7 +110,8 @@ public static RouterBuilder builder() { final Map properties, LoggingResourceManager loggingResourceManager, LogAppenderRepository logAppenderRepository, - MonitoringHandler monitoringHandler, + @Nullable MonitoringHandler monitoringHandler, + MeterRegistry meterRegistry, HttpClient selfClient, String serverPath, String rulesPath, @@ -126,6 +129,7 @@ public static RouterBuilder builder() { this.loggingResourceManager = loggingResourceManager; this.logAppenderRepository = logAppenderRepository; this.monitoringHandler = monitoringHandler; + this.meterRegistry = meterRegistry; this.selfClient = selfClient; this.vertx = vertx; this.sharedData = vertx.sharedData().getLocalMap(ROUTER_STATE_MAP); @@ -322,7 +326,7 @@ private void createForwarders(List rules, io.vertx.ext.web.Router newRoute * is null. */ AuthStrategy authStrategy = selectAuthStrategy(rule); - Handler forwarder; + AbstractForwarder forwarder; if (rule.getPath() == null) { forwarder = new NullForwarder(rule, loggingResourceManager, logAppenderRepository, monitoringHandler, vertx.eventBus()); @@ -339,6 +343,8 @@ private void createForwarders(List rules, io.vertx.ext.web.Router newRoute newClients.add(client); } + forwarder.setMeterRegistry(meterRegistry); + if (rule.getMethods() == null) { log.info("Installing {} forwarder for all methods: {}", rule.getScheme().toUpperCase(), rule.getUrlPattern()); newRouter.routeWithRegex(rule.getUrlPattern()).handler(forwarder); diff --git a/gateleen-routing/src/main/java/org/swisspush/gateleen/routing/RouterBuilder.java b/gateleen-routing/src/main/java/org/swisspush/gateleen/routing/RouterBuilder.java index f7ef9685f..59e290ad8 100644 --- a/gateleen-routing/src/main/java/org/swisspush/gateleen/routing/RouterBuilder.java +++ b/gateleen-routing/src/main/java/org/swisspush/gateleen/routing/RouterBuilder.java @@ -1,5 +1,6 @@ package org.swisspush.gateleen.routing; +import io.micrometer.core.instrument.MeterRegistry; import io.vertx.core.Handler; import io.vertx.core.Vertx; import io.vertx.core.http.HttpClient; @@ -37,6 +38,7 @@ public class RouterBuilder { private LoggingResourceManager loggingResourceManager; private LogAppenderRepository logAppenderRepository; private MonitoringHandler monitoringHandler; + private MeterRegistry meterRegistry; private HttpClient selfClient; private String serverPath; private String rulesPath; @@ -98,6 +100,7 @@ public Router build() { loggingResourceManager, logAppenderRepository, monitoringHandler, + meterRegistry, selfClient, serverPath, rulesPath, @@ -183,6 +186,12 @@ public RouterBuilder withMonitoringHandler(MonitoringHandler monitoringHandler) return this; } + public RouterBuilder withMeterRegistry(MeterRegistry meterRegistry) { + ensureNotBuilt(); + this.meterRegistry = meterRegistry; + return this; + } + public RouterBuilder withSelfClient(HttpClient selfClient) { ensureNotBuilt(); this.selfClient = selfClient; diff --git a/gateleen-routing/src/main/java/org/swisspush/gateleen/routing/StorageForwarder.java b/gateleen-routing/src/main/java/org/swisspush/gateleen/routing/StorageForwarder.java index 1e48c2212..32a0f38a1 100755 --- a/gateleen-routing/src/main/java/org/swisspush/gateleen/routing/StorageForwarder.java +++ b/gateleen-routing/src/main/java/org/swisspush/gateleen/routing/StorageForwarder.java @@ -1,5 +1,8 @@ package org.swisspush.gateleen.routing; +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Timer; import io.netty.handler.codec.http.HttpResponseStatus; import io.vertx.core.AsyncResult; import io.vertx.core.Handler; @@ -28,6 +31,7 @@ import org.swisspush.gateleen.logging.LoggingResourceManager; import org.swisspush.gateleen.monitoring.MonitoringHandler; +import javax.annotation.Nullable; import java.util.regex.Pattern; /** @@ -43,8 +47,14 @@ public class StorageForwarder extends AbstractForwarder { private CORSHandler corsHandler; private GateleenExceptionFactory gateleenExceptionFactory; + private Counter forwardCounter; + private Timer forwardTimer; + private MeterRegistry meterRegistry; + + private static final String TYPE_STORAGE = "storage"; + public StorageForwarder(EventBus eventBus, Rule rule, LoggingResourceManager loggingResourceManager, - LogAppenderRepository logAppenderRepository, MonitoringHandler monitoringHandler, + LogAppenderRepository logAppenderRepository, @Nullable MonitoringHandler monitoringHandler, GateleenExceptionFactory gateleenExceptionFactory) { super(rule, loggingResourceManager, logAppenderRepository, monitoringHandler); this.eventBus = eventBus; @@ -54,6 +64,32 @@ public StorageForwarder(EventBus eventBus, Rule rule, LoggingResourceManager log this.gateleenExceptionFactory = gateleenExceptionFactory; } + /** + * Sets the MeterRegistry for this StorageForwarder. + * If the provided MeterRegistry is not null, it initializes the forwardCounter + * with the appropriate metric name, description, and tags. + * + * @param meterRegistry the MeterRegistry to set + */ + @Override + public void setMeterRegistry(MeterRegistry meterRegistry) { + this.meterRegistry = meterRegistry; + if (meterRegistry != null) { + forwardCounter = Counter.builder(FORWARDER_COUNT_METRIC_NAME) + .description(FORWARDER_COUNT_METRIC_DESCRIPTION) + .tag(FORWARDER_METRIC_TAG_METRICNAME, metricNameTag) + .tag(FORWARDER_METRIC_TAG_TYPE, TYPE_STORAGE) + .register(meterRegistry); + + forwardTimer = Timer.builder(FORWARDS_METRIC_NAME) + .description(FORWARDS_METRIC_DESCRIPTION) + .publishPercentiles(0.75, 0.95) + .tag(FORWARDER_METRIC_TAG_METRICNAME, metricNameTag) + .tag(FORWARDER_METRIC_TAG_TYPE, TYPE_STORAGE) + .register(meterRegistry); + } + } + @Override public void handle(final RoutingContext ctx) { final LoggingHandler loggingHandler = new LoggingHandler(loggingResourceManager, logAppenderRepository, ctx.request(), this.eventBus); @@ -65,9 +101,19 @@ public void handle(final RoutingContext ctx) { return; } - monitoringHandler.updateRequestsMeter("localhost", ctx.request().uri()); - monitoringHandler.updateRequestPerRuleMonitoring(ctx.request(), rule.getMetricName()); - final long startTime = monitoringHandler.startRequestMetricTracking(rule.getMetricName(), ctx.request().uri()); + Long startTime = null; + + Timer.Sample timerSample = null; + if(meterRegistry != null) { + timerSample = Timer.start(meterRegistry); + forwardCounter.increment(); + } + + if (monitoringHandler != null) { + monitoringHandler.updateRequestsMeter("localhost", ctx.request().uri()); + monitoringHandler.updateRequestPerRuleMonitoring(ctx.request(), rule.getMetricName()); + startTime = monitoringHandler.startRequestMetricTracking(rule.getMetricName(), ctx.request().uri()); + } log.debug("Forwarding {} request: {} to storage {} {} with rule {}", ctx.request().method(), ctx.request().uri(), rule.getStorage(), targetUri, rule.getRuleIdentifier()); final HeadersMultiMap requestHeaders = new HeadersMultiMap(); @@ -92,68 +138,79 @@ public void handle(final RoutingContext ctx) { loggingHandler.appendRequestPayload(buffer, requestHeaders); requestBuffer.appendBuffer(buffer); }); + Long finalStartTime = startTime; + + Timer.Sample finalTimerSample = timerSample; + ctx.request().endHandler(event -> eventBus.request(address, requestBuffer, new DeliveryOptions().setSendTimeout(10000), (Handler>>) result -> { - HttpServerResponse response = ctx.response(); - monitoringHandler.stopRequestMetricTracking(rule.getMetricName(), startTime, ctx.request().uri()); - if (result.failed()) { - String statusMessage = "Storage request for " + ctx.request().uri() + " failed with message: " + result.cause().getMessage(); - response.setStatusCode(StatusCode.INTERNAL_SERVER_ERROR.getStatusCode()); - response.setStatusMessage(statusMessage); - response.end(); - log.error("{}", statusMessage, gateleenExceptionFactory.newException(result.cause())); - } else { - Buffer buffer = result.result().body(); - int headerLength = buffer.getInt(0); - JsonObject responseJson = new JsonObject(buffer.getString(4, headerLength + 4)); - JsonArray headers = responseJson.getJsonArray("headers"); - MultiMap responseHeaders = null; - if (headers != null && !headers.isEmpty()) { - responseHeaders = JsonMultiMap.fromJson(headers); - - setUniqueIdHeader(responseHeaders); - - ctx.response().headers().setAll(responseHeaders); - } - corsHandler.handle(ctx.request()); - int statusCode = responseJson.getInteger("statusCode"); - - // translate with header info - int translatedStatus = Translator.translateStatusCode(statusCode, ctx.request().headers()); - - // nothing changed? - if (statusCode == translatedStatus) { - translatedStatus = Translator.translateStatusCode(statusCode, rule, log); - } - - boolean translated = statusCode != translatedStatus; - - // set the statusCode (if nothing hapend, it will remain the same) - statusCode = translatedStatus; - - response.setStatusCode(statusCode); - String statusMessage; - if (translated) { - statusMessage = HttpResponseStatus.valueOf(statusCode).reasonPhrase(); - response.setStatusMessage(statusMessage); - } else { - statusMessage = responseJson.getString("statusMessage"); - if (statusMessage != null) { - response.setStatusMessage(statusMessage); - } - } - Buffer data = buffer.getBuffer(4 + headerLength, buffer.length()); - response.headers().set("content-length", "" + data.length()); - response.write(data); - response.end(); - ResponseStatusCodeLogUtil.debug(ctx.request(), StatusCode.fromCode(statusCode), StorageForwarder.class); - if (responseHeaders != null) { - loggingHandler.appendResponsePayload(data, responseHeaders); - } - loggingHandler.log(ctx.request().uri(), ctx.request().method(), statusCode, statusMessage, - requestHeaders, responseHeaders != null ? responseHeaders : new HeadersMultiMap()); - } + HttpServerResponse response = ctx.response(); + if (monitoringHandler != null) { + monitoringHandler.stopRequestMetricTracking(rule.getMetricName(), finalStartTime, ctx.request().uri()); + } + + if(finalTimerSample != null) { + finalTimerSample.stop(forwardTimer); + } + + if (result.failed()) { + String statusMessage = "Storage request for " + ctx.request().uri() + " failed with message: " + result.cause().getMessage(); + response.setStatusCode(StatusCode.INTERNAL_SERVER_ERROR.getStatusCode()); + response.setStatusMessage(statusMessage); + response.end(); + log.error("{}", statusMessage, gateleenExceptionFactory.newException(result.cause())); + } else { + Buffer buffer = result.result().body(); + int headerLength = buffer.getInt(0); + JsonObject responseJson = new JsonObject(buffer.getString(4, headerLength + 4)); + JsonArray headers = responseJson.getJsonArray("headers"); + MultiMap responseHeaders = null; + if (headers != null && !headers.isEmpty()) { + responseHeaders = JsonMultiMap.fromJson(headers); + + setUniqueIdHeader(responseHeaders); + + ctx.response().headers().setAll(responseHeaders); + } + corsHandler.handle(ctx.request()); + int statusCode = responseJson.getInteger("statusCode"); + + // translate with header info + int translatedStatus = Translator.translateStatusCode(statusCode, ctx.request().headers()); + + // nothing changed? + if (statusCode == translatedStatus) { + translatedStatus = Translator.translateStatusCode(statusCode, rule, log); + } + + boolean translated = statusCode != translatedStatus; + + // set the statusCode (if nothing hapend, it will remain the same) + statusCode = translatedStatus; + + response.setStatusCode(statusCode); + String statusMessage; + if (translated) { + statusMessage = HttpResponseStatus.valueOf(statusCode).reasonPhrase(); + response.setStatusMessage(statusMessage); + } else { + statusMessage = responseJson.getString("statusMessage"); + if (statusMessage != null) { + response.setStatusMessage(statusMessage); + } + } + Buffer data = buffer.getBuffer(4 + headerLength, buffer.length()); + response.headers().set("content-length", "" + data.length()); + response.write(data); + response.end(); + ResponseStatusCodeLogUtil.debug(ctx.request(), StatusCode.fromCode(statusCode), StorageForwarder.class); + if (responseHeaders != null) { + loggingHandler.appendResponsePayload(data, responseHeaders); + } + loggingHandler.log(ctx.request().uri(), ctx.request().method(), statusCode, statusMessage, + requestHeaders, responseHeaders != null ? responseHeaders : new HeadersMultiMap()); + } })); } diff --git a/gateleen-routing/src/test/java/org/swisspush/gateleen/routing/RouterTest.java b/gateleen-routing/src/test/java/org/swisspush/gateleen/routing/RouterTest.java index b1184d662..2c90dfbc2 100755 --- a/gateleen-routing/src/test/java/org/swisspush/gateleen/routing/RouterTest.java +++ b/gateleen-routing/src/test/java/org/swisspush/gateleen/routing/RouterTest.java @@ -1,6 +1,9 @@ package org.swisspush.gateleen.routing; import com.google.common.collect.ImmutableMap; +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; import io.vertx.core.Handler; import io.vertx.core.MultiMap; import io.vertx.core.Vertx; @@ -28,10 +31,7 @@ import org.swisspush.gateleen.logging.LoggingResourceManager; import org.swisspush.gateleen.monitoring.MonitoringHandler; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; +import java.util.*; /** * Tests for the Router class @@ -45,6 +45,7 @@ public class RouterTest { private Map properties; private LoggingResourceManager loggingResourceManager; private MonitoringHandler monitoringHandler; + private MeterRegistry meterRegistry; private HttpClient httpClient; private String serverUrl; private String rulesPath; @@ -143,6 +144,7 @@ public void setUp() { Mockito.when(loggingResourceManager.getLoggingResource()).thenReturn(new LoggingResource()); monitoringHandler = Mockito.mock(MonitoringHandler.class); httpClient = Mockito.mock(HttpClient.class); + meterRegistry = new SimpleMeterRegistry(); serverUrl = "/gateleen/server"; rulesPath = serverUrl + "/admin/v1/routing/rules"; @@ -160,6 +162,7 @@ private RouterBuilder routerBuilder() { .withProperties(properties) .withLoggingResourceManager(loggingResourceManager) .withMonitoringHandler(monitoringHandler) + .withMeterRegistry(meterRegistry) .withSelfClient(httpClient) .withServerPath(serverUrl) .withRulesPath(rulesPath) @@ -168,6 +171,12 @@ private RouterBuilder routerBuilder() { .withStoragePort(storagePort); } + private void assertNoCountersIncremented(TestContext context) { + for (Counter counter : meterRegistry.get(AbstractForwarder.FORWARDER_COUNT_METRIC_NAME).counters()) { + context.assertEquals(0.0, counter.count(), "No counter should have been incremented"); + } + } + @Test public void testRequestHopValidationLimitNotYetReached(TestContext context) { storage = new MockResourceStorage(ImmutableMap.of(rulesPath, RULES_WITH_HOPS, serverUrl + "/loop/4/resource", RANDOM_RESOURCE)); @@ -240,6 +249,9 @@ public DummyHttpServerResponse response() { GETRandomResourceRequest request = new GETRandomResourceRequest(); router.route(request); + Counter counter = meterRegistry.get(AbstractForwarder.FORWARDER_COUNT_METRIC_NAME).tag(AbstractForwarder.FORWARDER_METRIC_TAG_METRICNAME, "loop_4").counter(); + context.assertEquals(1.0, counter.count(), "Counter for `loop_4` rule should have been incremented by 1"); + context.assertEquals("1", request.headers().get("x-hops"), "x-hops header should have value 1"); context.assertEquals(StatusCode.OK.getStatusCode(), request.response().getStatusCode(), "StatusCode should be 200"); context.assertEquals(StatusCode.OK.getStatusMessage(), request.response().getStatusMessage(), "StatusMessage should be OK"); @@ -317,6 +329,8 @@ public DummyHttpServerResponse response() { GETRandomResourceRequest request = new GETRandomResourceRequest(); router.route(request); + assertNoCountersIncremented(context); + context.assertEquals("1", request.headers().get("x-hops"), "x-hops header should have value 1"); context.assertEquals(StatusCode.INTERNAL_SERVER_ERROR.getStatusCode(), request.response().getStatusCode(), "StatusCode should be 500"); context.assertEquals("Request hops limit exceeded", request.response().getStatusMessage(), "StatusMessage should be 'Request hops limit exceeded'"); @@ -404,6 +418,9 @@ public DummyHttpServerResponse response() { context.assertEquals("6", request.headers().get("x-hops"), "x-hops header should have value 6"); context.assertEquals(StatusCode.INTERNAL_SERVER_ERROR.getStatusCode(), request.response().getStatusCode(), "StatusCode should be 500"); context.assertEquals("Request hops limit exceeded", request.response().getStatusMessage(), "StatusMessage should be 'Request hops limit exceeded'"); + + Counter counter = meterRegistry.get(AbstractForwarder.FORWARDER_COUNT_METRIC_NAME).tag(AbstractForwarder.FORWARDER_METRIC_TAG_METRICNAME, "loop_4").counter(); + context.assertEquals(5.0, counter.count(), "Counter for `loop_4` rule should have been incremented by 5"); } @Test @@ -478,6 +495,9 @@ public DummyHttpServerResponse response() { context.assertNull(request.headers().get("x-hops"), "No x-hops header should be present"); context.assertEquals(StatusCode.OK.getStatusCode(), request.response().getStatusCode(), "StatusCode should be 200"); context.assertEquals(StatusCode.OK.getStatusMessage(), request.response().getStatusMessage(), "StatusMessage should be OK"); + + Counter counter = meterRegistry.get(AbstractForwarder.FORWARDER_COUNT_METRIC_NAME).tag(AbstractForwarder.FORWARDER_METRIC_TAG_METRICNAME, "loop_4").counter(); + context.assertEquals(20.0, counter.count(), "Counter for `loop_4` rule should have been incremented by 20"); } @@ -488,6 +508,8 @@ public void testRouterConstructionValidConfiguration(TestContext context) { Router router = routerBuilder().withProperties(properties).build(); context.assertFalse(router.isRoutingBroken(), "Routing should not be broken"); context.assertNull(router.getRoutingBrokenMessage(), "RoutingBrokenMessage should be null"); + + assertNoCountersIncremented(context); } @Test @@ -496,6 +518,8 @@ public void testRouterConstructionWithMissingProperty(TestContext context) { context.assertTrue(router.isRoutingBroken(), "Routing should be broken because of missing properties entry"); context.assertNotNull(router.getRoutingBrokenMessage(), "RoutingBrokenMessage should contain 'gateleen.test.prop.1' property"); context.assertTrue(router.getRoutingBrokenMessage().contains("gateleen.test.prop.1"), "RoutingBrokenMessage should contain 'gateleen.test.prop.1' property"); + + assertNoCountersIncremented(context); } @Test @@ -538,6 +562,8 @@ public HttpServerResponse response() { router.route(new UpdateRulesWithValidResourceRequest()); context.assertFalse(router.isRoutingBroken(), "Routing should not be broken anymore"); context.assertNull(router.getRoutingBrokenMessage(), "RoutingBrokenMessage should be null"); + + assertNoCountersIncremented(context); } @Test @@ -578,6 +604,8 @@ public MultiMap headers() { context.assertEquals(StatusCode.OK.getStatusCode(), request.response().getStatusCode(), "StatusCode should be 200"); context.assertEquals(StatusCode.OK.getStatusMessage(), request.response().getStatusMessage(), "StatusMessage should be OK"); context.assertEquals(RULES_WITH_MISSING_PROPS, request.response().getResultBuffer(), "RoutingRules should be returned as result"); + + assertNoCountersIncremented(context); } @Test @@ -619,6 +647,8 @@ public MultiMap headers() { context.assertEquals(StatusCode.INTERNAL_SERVER_ERROR.getStatusMessage(), request.response().getStatusMessage(), "StatusMessage should be Internal Server Error"); context.assertTrue(request.response().getResultBuffer().contains("Routing is broken"), "Routing is broken message should be returned"); context.assertTrue(request.response().getResultBuffer().contains("gateleen.test.prop.1"), "The message should contain 'gateleen.test.prop.1' in the message"); + + assertNoCountersIncremented(context); } @Test @@ -742,6 +772,9 @@ public HttpServerRequest pause() { router.route(requestRandomResource); context.assertFalse(router.isRoutingBroken(), "Routing should not be broken anymore"); context.assertNull(router.getRoutingBrokenMessage(), "RoutingBrokenMessage should be null"); + + Counter counter = meterRegistry.get(AbstractForwarder.FORWARDER_COUNT_METRIC_NAME).tag(AbstractForwarder.FORWARDER_METRIC_TAG_TYPE, "local").counter(); + context.assertEquals(1.0, counter.count(), "Counter should have been incremented by 1"); } @Test @@ -793,6 +826,8 @@ public DummyHttpServerResponse response() { context.assertEquals(StatusCode.OK.getStatusCode(), request.response().getStatusCode(), "StatusCode should be 200"); context.assertEquals(StatusCode.OK.getStatusMessage(), request.response().getStatusMessage(), "StatusMessage should be OK"); context.assertEquals(ts, new JsonObject(request.response().getResultBuffer()).getLong("ts")); + + assertNoCountersIncremented(context); } @Test @@ -848,6 +883,7 @@ public DummyHttpServerResponse response() { router.route(request); context.assertEquals(StatusCode.NOT_FOUND.getStatusCode(), request.response().getStatusCode(), "StatusCode should be 404"); + assertNoCountersIncremented(context); } @Test @@ -871,6 +907,9 @@ public void testStorageRequestWithHeadersFilterPresent(TestContext context) { context.assertEquals(StatusCode.OK.getStatusCode(), request.response().getStatusCode(), "StatusCode should be 200"); context.assertEquals(StatusCode.OK.getStatusMessage(), request.response().getStatusMessage(), "StatusMessage should be OK"); + + Counter counter = meterRegistry.get(AbstractForwarder.FORWARDER_COUNT_METRIC_NAME).tag(AbstractForwarder.FORWARDER_METRIC_TAG_METRICNAME, "forward_storage").counter(); + context.assertEquals(1.0, counter.count(), "Counter for `forward_storage` rule should have been incremented by 1"); } @Test @@ -892,6 +931,8 @@ public void testStorageRequestWithHeadersFilterAbsent(TestContext context) { router.route(request); context.assertEquals(StatusCode.NOT_FOUND.getStatusCode(), request.response().getStatusCode(), "StatusCode should be 404"); + + assertNoCountersIncremented(context); } @Test @@ -914,6 +955,8 @@ public void testNullForwarderRequestWithHeadersFilterNotMatching(TestContext con router.route(request); context.assertEquals(StatusCode.NOT_FOUND.getStatusCode(), request.response().getStatusCode(), "StatusCode should be 404"); + + assertNoCountersIncremented(context); } @Test @@ -938,6 +981,9 @@ public void testNullForwarderRequestWithHeadersFilterPresent(TestContext context context.assertEquals(StatusCode.OK.getStatusCode(), request.response().getStatusCode(), "StatusCode should be 200"); context.assertEquals(StatusCode.OK.getStatusMessage(), request.response().getStatusMessage(), "StatusMessage should be OK"); + + Counter counter = meterRegistry.get(AbstractForwarder.FORWARDER_COUNT_METRIC_NAME).tag(AbstractForwarder.FORWARDER_METRIC_TAG_METRICNAME, "forward_null").counter(); + context.assertEquals(1.0, counter.count(), "Counter for `forward_null` rule should have been incremented by 1"); } @Test @@ -963,6 +1009,9 @@ public void testForwarderRequestWithHeadersFilterPresent(TestContext context) { // we expect a status code 500 because of a NullPointerException in the test setup // however, this means that the headersFilter evaluation did not return a 400 Bad Request context.assertEquals(StatusCode.INTERNAL_SERVER_ERROR.getStatusCode(), request.response().getStatusCode(), "StatusCode should be 500"); + + Counter counter = meterRegistry.get(AbstractForwarder.FORWARDER_COUNT_METRIC_NAME).tag(AbstractForwarder.FORWARDER_METRIC_TAG_METRICNAME, "forward_backend").counter(); + context.assertEquals(1.0, counter.count(), "Counter for `forward_backend` rule should have been incremented by 1"); } @Test @@ -986,6 +1035,8 @@ public void testForwarderRequestWithHeadersFilterNotMatching(TestContext context router.route(request); context.assertEquals(StatusCode.NOT_FOUND.getStatusCode(), request.response().getStatusCode(), "StatusCode should be 404"); + + assertNoCountersIncremented(context); } private DummyHttpServerRequest buildRequest(HttpMethod method, String uri, MultiMap headers, Buffer body, DummyHttpServerResponse response) { diff --git a/gateleen-runconfig/pom.xml b/gateleen-runconfig/pom.xml index 61fa79bf8..3b74e46d4 100644 --- a/gateleen-runconfig/pom.xml +++ b/gateleen-runconfig/pom.xml @@ -6,7 +6,7 @@ org.swisspush.gateleen gateleen - 2.1.12-SNAPSHOT + 2.1.13-SNAPSHOT gateleen-runconfig diff --git a/gateleen-scheduler/pom.xml b/gateleen-scheduler/pom.xml index 50cdad370..dae147c14 100644 --- a/gateleen-scheduler/pom.xml +++ b/gateleen-scheduler/pom.xml @@ -6,7 +6,7 @@ org.swisspush.gateleen gateleen - 2.1.12-SNAPSHOT + 2.1.13-SNAPSHOT gateleen-scheduler diff --git a/gateleen-scheduler/src/main/java/org/swisspush/gateleen/scheduler/Scheduler.java b/gateleen-scheduler/src/main/java/org/swisspush/gateleen/scheduler/Scheduler.java index 65ab5a309..6577b6946 100755 --- a/gateleen-scheduler/src/main/java/org/swisspush/gateleen/scheduler/Scheduler.java +++ b/gateleen-scheduler/src/main/java/org/swisspush/gateleen/scheduler/Scheduler.java @@ -15,6 +15,7 @@ import org.swisspush.gateleen.queue.expiry.ExpiryCheckHandler; import org.swisspush.gateleen.queue.queuing.QueueClient; +import javax.annotation.Nullable; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Collections; @@ -48,17 +49,17 @@ public class Scheduler { private Logger log; public Scheduler( - Vertx vertx, - String redisquesAddress, - RedisProvider redisProvider, - GateleenExceptionFactory exceptionFactory, - String name, - String cronExpression, - List requests, - MonitoringHandler monitoringHandler, - int maxRandomOffset, - boolean executeOnStartup, - boolean executeOnReload + Vertx vertx, + String redisquesAddress, + RedisProvider redisProvider, + GateleenExceptionFactory exceptionFactory, + String name, + String cronExpression, + List requests, + @Nullable MonitoringHandler monitoringHandler, + int maxRandomOffset, + boolean executeOnStartup, + boolean executeOnReload ) throws ParseException { this.vertx = vertx; this.redisquesAddress = redisquesAddress; @@ -138,7 +139,9 @@ public void stop() { private void trigger() { for (final HttpRequest request : requests) { - monitoringHandler.updateEnqueue(); + if (monitoringHandler != null) { + monitoringHandler.updateEnqueue(); + } if (log.isTraceEnabled()) { log.trace("Triggering request " + request.toJsonObject().encodePrettily()); @@ -156,7 +159,7 @@ private void trigger() { if (event.failed()) { if (log.isWarnEnabled()) { log.warn("Could not enqueue request '{}' '{}'", queueName, request.getUri(), - exceptionFactory.newException("eventBus.request('" + redisquesAddress + "', enqueOp) failed", event.cause())); + exceptionFactory.newException("eventBus.request('" + redisquesAddress + "', enqueOp) failed", event.cause())); } return; } diff --git a/gateleen-scheduler/src/main/java/org/swisspush/gateleen/scheduler/SchedulerFactory.java b/gateleen-scheduler/src/main/java/org/swisspush/gateleen/scheduler/SchedulerFactory.java index 1a5c34f39..7639bbf09 100755 --- a/gateleen-scheduler/src/main/java/org/swisspush/gateleen/scheduler/SchedulerFactory.java +++ b/gateleen-scheduler/src/main/java/org/swisspush/gateleen/scheduler/SchedulerFactory.java @@ -15,6 +15,7 @@ import org.swisspush.gateleen.validation.ValidationException; import org.swisspush.gateleen.validation.Validator; +import javax.annotation.Nullable; import java.nio.charset.Charset; import java.text.ParseException; import java.util.ArrayList; @@ -54,7 +55,7 @@ public SchedulerFactory( Vertx vertx, RedisProvider redisProvider, GateleenExceptionFactory exceptionFactory, - MonitoringHandler monitoringHandler, + @Nullable MonitoringHandler monitoringHandler, String schedulersSchema, String redisquesAddress ) { diff --git a/gateleen-scheduler/src/main/java/org/swisspush/gateleen/scheduler/SchedulerResourceManager.java b/gateleen-scheduler/src/main/java/org/swisspush/gateleen/scheduler/SchedulerResourceManager.java index da8bbbe18..a953151c2 100755 --- a/gateleen-scheduler/src/main/java/org/swisspush/gateleen/scheduler/SchedulerResourceManager.java +++ b/gateleen-scheduler/src/main/java/org/swisspush/gateleen/scheduler/SchedulerResourceManager.java @@ -21,6 +21,7 @@ import org.swisspush.gateleen.monitoring.MonitoringHandler; import org.swisspush.gateleen.validation.ValidationException; +import javax.annotation.Nullable; import java.util.Collections; import java.util.List; import java.util.Map; @@ -44,17 +45,17 @@ public class SchedulerResourceManager implements Refreshable, LoggableResource { private boolean logConfigurationResourceChanges = false; public SchedulerResourceManager(Vertx vertx, RedisProvider redisProvider, final ResourceStorage storage, - MonitoringHandler monitoringHandler, String schedulersUri) { + @Nullable MonitoringHandler monitoringHandler, String schedulersUri) { this(vertx, redisProvider, storage, monitoringHandler, schedulersUri, null); } public SchedulerResourceManager(Vertx vertx, RedisProvider redisProvider, final ResourceStorage storage, - MonitoringHandler monitoringHandler, String schedulersUri, Map props) { + @Nullable MonitoringHandler monitoringHandler, String schedulersUri, Map props) { this(vertx, redisProvider, storage, monitoringHandler, schedulersUri, props, Address.redisquesAddress()); } public SchedulerResourceManager(Vertx vertx, RedisProvider redisProvider, final ResourceStorage storage, - MonitoringHandler monitoringHandler, String schedulersUri, Map props, + @Nullable MonitoringHandler monitoringHandler, String schedulersUri, Map props, String redisquesAddress) { this(vertx, redisProvider, newGateleenThriftyExceptionFactory(), storage, monitoringHandler, schedulersUri, props, redisquesAddress, Collections.emptyMap()); } @@ -64,7 +65,7 @@ public SchedulerResourceManager( RedisProvider redisProvider, GateleenExceptionFactory exceptionFactory, ResourceStorage storage, - MonitoringHandler monitoringHandler, + @Nullable MonitoringHandler monitoringHandler, String schedulersUri, Map props, String redisquesAddress, diff --git a/gateleen-security/pom.xml b/gateleen-security/pom.xml index 7e4f6aaaf..1e53af8b3 100644 --- a/gateleen-security/pom.xml +++ b/gateleen-security/pom.xml @@ -6,7 +6,7 @@ org.swisspush.gateleen gateleen - 2.1.12-SNAPSHOT + 2.1.13-SNAPSHOT gateleen-security diff --git a/gateleen-test/pom.xml b/gateleen-test/pom.xml index 34a345e90..fabf66f4f 100644 --- a/gateleen-test/pom.xml +++ b/gateleen-test/pom.xml @@ -6,7 +6,7 @@ org.swisspush.gateleen gateleen - 2.1.12-SNAPSHOT + 2.1.13-SNAPSHOT gateleen-test jar diff --git a/gateleen-test/src/test/java/org/swisspush/gateleen/hook/ListenerTest.java b/gateleen-test/src/test/java/org/swisspush/gateleen/hook/ListenerTest.java index 4282fe13e..081483a25 100755 --- a/gateleen-test/src/test/java/org/swisspush/gateleen/hook/ListenerTest.java +++ b/gateleen-test/src/test/java/org/swisspush/gateleen/hook/ListenerTest.java @@ -6,11 +6,13 @@ import io.restassured.RestAssured; import io.restassured.http.Header; import io.restassured.http.Headers; +import io.restassured.response.Response; import io.vertx.core.json.JsonObject; import io.vertx.ext.unit.Async; import io.vertx.ext.unit.TestContext; import io.vertx.ext.unit.junit.VertxUnitRunner; import org.awaitility.Awaitility; +import org.junit.Assert; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; @@ -41,10 +43,16 @@ public class ListenerTest extends AbstractTest { private final static int WIREMOCK_PORT = 8881; private String requestUrlBase; private String targetUrlBase; + private String searchUrlBase; + private String defaultRegisterUrlListener; + private String defaultTargetListener; + private String[] defaultMethodsListener; + private final String defaultListenerName = "defaultListener"; @Rule public WireMockRule wireMockRule = new WireMockRule(WIREMOCK_PORT); + /** * Overwrite RestAssured configuration */ @@ -54,6 +62,12 @@ public void initRestAssured() { requestUrlBase = "/tests/gateleen/monitoredresource"; targetUrlBase = "http://localhost:" + MAIN_PORT + SERVER_ROOT + "/tests/gateleen/targetresource"; + searchUrlBase = "http://localhost:" + MAIN_PORT + SERVER_ROOT + "/hooks/v1/registrations/listeners"; + + String defaultQueryParam = "defaultQueryParam"; + defaultRegisterUrlListener = requestUrlBase + "/" + defaultQueryParam + TestUtils.getHookListenersUrlSuffix() + defaultListenerName; + defaultTargetListener = targetUrlBase + "/" + defaultListenerName; + defaultMethodsListener = new String[]{"PUT", "DELETE", "POST"}; } /** @@ -872,6 +886,216 @@ public void testRequestForwardingForTwoListenerAtSameResourceButDifferentHeaders async.complete(); } + @Test + public void testSearchListenerWithValidAndInvalidSearchParam(TestContext context) { + Async async = context.async(); + registerDefaultListener(); + + searchWithQueryParam("wq", defaultListenerName, 400); + searchWithQueryParam("q", "", 400); + + Response response = searchWithQueryParam("q",defaultListenerName,200); + + Assert.assertTrue(response.getBody().asString().contains(defaultListenerName)); + TestUtils.unregisterListener(defaultRegisterUrlListener); + + async.complete(); + } + + @Test + public void testSearchListenerWithNonMatchingQueryParam(TestContext context) { + Async async = context.async(); + + registerDefaultListener(); + // Verify that the listener was correctly registered + Response response = searchWithQueryParam("q",defaultListenerName,200); + Assert.assertTrue(response.getBody().asString().contains(defaultListenerName)); + + String localListenerName="nonMatchingQueryParam"; + // Verify that the listener search with a no registered listener + response = searchWithQueryParam("q",localListenerName,200); + Assert.assertFalse(response.getBody().asString().contains(localListenerName)); + + TestUtils.unregisterListener(defaultRegisterUrlListener); + + async.complete(); + } + + @Test + public void testHookHandleSearch_NoListenersRegistered(TestContext context) { + Async async = context.async(); + delete(); + initRoutingRules(); + + String queryParam = "someQuery"; + + // Verify that the listener search with a no registered listener + Response response = searchWithQueryParam("q",queryParam,200); + + // Parse response body as JSON + JsonObject jsonResponse = new JsonObject(response.getBody().asString()); + + Assert.assertTrue("Expected 'listeners' to be an empty array", + jsonResponse.containsKey("listeners") && jsonResponse.getJsonArray("listeners").isEmpty()); + async.complete(); + } + + @Test + public void testHookHandleSearch_ListenerPathInvalidParam(TestContext context) { + Async async = context.async(); + delete(); + initRoutingRules(); + + String queryParam = "testQuery"; + String requestUrl = searchUrlBase+ "?www=" + queryParam; + + // Validate the response + checkGETStatusCodeWithAwait(requestUrl, 400); + async.complete(); + } + + @Test + public void testHookHandleSearch_ListenerTwoParam(TestContext context) { + Async async = context.async(); + delete(); + initRoutingRules(); + + String queryParam = "testQuery"; + String requestUrl = searchUrlBase+ "?q=" + queryParam+"&www=" + queryParam; + + // Validate the response + checkGETStatusCodeWithAwait(requestUrl, 400); + async.complete(); + } + + @Test + public void testHookHandleSearch_UseSlashInSearchUrlBase(TestContext context) { + Async async = context.async(); + delete(); + initRoutingRules(); + + registerDefaultListener(); + + Response response = given() + .queryParam("q", defaultListenerName) + .when().get(searchUrlBase +"/") + .then().assertThat().statusCode(200) + .extract().response(); + + // Assert that the response contains the expected query param + Assert.assertTrue(response.getBody().asString().contains(defaultListenerName)); + + TestUtils.unregisterListener(defaultRegisterUrlListener); + + async.complete(); + } + + @Test + public void testHookHandleSearch_EmptyQueryParameter(TestContext context) { + Async async = context.async(); + delete(); + initRoutingRules(); + registerDefaultListener(); + + Response response = searchWithQueryParam("q","",400); + + Assert.assertTrue(response.getBody().asString().contains("Only the 'q' parameter is allowed and can't be empty or null")); + TestUtils.unregisterListener(defaultRegisterUrlListener); + + async.complete(); + } + + @Test + public void testHookHandleSearch_ReturnsTwoOutOfThreeListeners(TestContext context) { + Async async = context.async(); + delete(); + initRoutingRules(); + + // Settings for the three listeners + String subresource = "multiListenerTest"; + String listenerName1 = "listenerOne"; + String listenerName2 = "listenerTwo"; + String listenerName3 = "NoMatchThree"; + + String registerUrlListener1 = requestUrlBase + "/" + subresource + TestUtils.getHookListenersUrlSuffix() + listenerName1; + String registerUrlListener2 = requestUrlBase + "/" + subresource + TestUtils.getHookListenersUrlSuffix() + listenerName2; + String registerUrlListener3 = requestUrlBase + "/" + subresource + TestUtils.getHookListenersUrlSuffix() + listenerName3; + + String targetListener1 = targetUrlBase + "/" + listenerName1; + String targetListener2 = targetUrlBase + "/" + listenerName2; + String targetListener3 = targetUrlBase + "/" + listenerName3; + + String[] methodsListener = new String[]{"PUT", "DELETE", "POST"}; + + delete(registerUrlListener1); + delete(registerUrlListener2); + delete(registerUrlListener3); + + // Adding the three listeners + TestUtils.registerListener(registerUrlListener1, targetListener1, methodsListener, null, null, + null, null, "x-foo: (A|B)"); + TestUtils.registerListener(registerUrlListener2, targetListener2, methodsListener, null, null, + null, null, "x-foo: (A|B)"); + TestUtils.registerListener(registerUrlListener3, targetListener3, methodsListener, null, null, + null, null, "x-foo: (A|B)"); + // Perform a search for the listeners that should return only listenerOne and listenerTwo + // added in searchUrlBase a '/' to validate if also works + Response response = given() + .queryParam("q", "listener") + .when().get(searchUrlBase+"/") + .then().assertThat().statusCode(200) + .extract().response(); + + // Assert that the search contains listenerOne and listenerTwo but not listenerThree + Assert.assertTrue(response.getBody().asString().contains(listenerName1)); + Assert.assertTrue(response.getBody().asString().contains(listenerName2)); + Assert.assertFalse(response.getBody().asString().contains(listenerName3)); + + // Unregister the listeners + TestUtils.unregisterListener(registerUrlListener1); + TestUtils.unregisterListener(registerUrlListener2); + TestUtils.unregisterListener(registerUrlListener3); + + async.complete(); + } + + /** + * Test to verify that the search for listeners returns identical results + * when performed with and without a query parameter if only one matching listener exists. + * Registers a listener and performs searches, confirming that both responses match. + */ + @Test + public void testSearchListener_IdenticalResultsWithAndWithoutQueryParam(TestContext context) { + Async async = context.async(); + registerDefaultListener(); + + // Perform search with 'q' parameter matching the listener name + Response responseWithParam = searchWithQueryParam("q",defaultListenerName,200); + + // Perform search without 'q' parameter + Response responseWithoutParam = given().when().get(searchUrlBase) + .then().assertThat().statusCode(200) + .extract().response(); + + // Extract response bodies as strings for comparison + String responseBodyWithoutParam = responseWithoutParam.getBody().asString(); + String responseBodyWithParam = responseWithParam.getBody().asString(); + + // Verify that both responses are identical + Assert.assertEquals("Responses should be identical with and without 'q' when only one matching listener exists", + responseBodyWithoutParam, responseBodyWithParam); + + // Ensure the listener name is present in both responses + Assert.assertTrue("Listener name should be present in response without parameter", + responseBodyWithoutParam.contains(defaultListenerName)); + Assert.assertTrue("Listener name should be present in response with parameter", + responseBodyWithParam.contains(defaultListenerName)); + + // Clean up by removing the registered listener after the test + TestUtils.unregisterListener(defaultRegisterUrlListener); + async.complete(); + } + /** * Checks if the DELETE request gets a response * with the given status code. @@ -942,4 +1166,18 @@ private void checkGETStatusCodeWithAwait(final String request, final Integer sta private void checkGETBodyWithAwait(final String requestUrl, final String body) { await().atMost(TEN_SECONDS).until(() -> when().get(requestUrl).then().extract().body().asString(), equalTo(body)); } + + private Response searchWithQueryParam(String searchParam, String queryParam, int expectedStatusCode ) { + return given() + .queryParam(searchParam, queryParam) + .when().get(searchUrlBase) + .then().assertThat().statusCode(expectedStatusCode) + .extract().response(); + } + private void registerDefaultListener() { + delete(); + initRoutingRules(); + TestUtils.registerListener(defaultRegisterUrlListener, defaultTargetListener, defaultMethodsListener, null, null, + null, null, "x-foo: (A|B)"); + } } diff --git a/gateleen-test/src/test/java/org/swisspush/gateleen/hook/RouteListingTest.java b/gateleen-test/src/test/java/org/swisspush/gateleen/hook/RouteListingTest.java index 4a76ac64a..48256f05c 100644 --- a/gateleen-test/src/test/java/org/swisspush/gateleen/hook/RouteListingTest.java +++ b/gateleen-test/src/test/java/org/swisspush/gateleen/hook/RouteListingTest.java @@ -26,7 +26,7 @@ public class RouteListingTest extends AbstractTest { private String requestUrlBase; private String targetUrlBase; private String parentKey; - + private String searchUrlBase; /** * Overwrite RestAssured configuration @@ -38,6 +38,7 @@ public void initRestAssured() { parentKey = "routesource"; requestUrlBase = "/tests/gateleen/" + parentKey; targetUrlBase = "http://localhost:" + MAIN_PORT + SERVER_ROOT + "/tests/gateleen/routetarget"; + searchUrlBase = "http://localhost:" + MAIN_PORT + SERVER_ROOT + "/hooks/v1/registrations/routes"; } @@ -194,8 +195,112 @@ public void testListingWithoutStaticAndDynamicCollections(TestContext context) { async.complete(); } + @Test + public void testHookHandleSearch_WithValidAndInvalidSearchParam(TestContext context) { + Async async = context.async(); + delete(); // Remove any pre-existing data + initSettings(); // Initialize routing rules + + String queryParam = "routeTests"; + + addRoute(queryParam, true, true); + + // Verify that the route was correctly registered + searchWithQueryParam("w", queryParam, 400); + searchWithQueryParam("q", "", 400); + + // Verify that the route was correctly registered + Response response = searchWithQueryParam("q", queryParam, 200); + + // Assert that the response contains the expected query param + String responseBody = response.getBody().asString(); + Assert.assertTrue(responseBody.contains(queryParam)); + // Unregister the route + removeRoute(queryParam); + + async.complete(); + } + + @Test + public void testHookHandleSearch_RouteNonMatchingQueryParam(TestContext context) { + Async async = context.async(); + delete(); // Clean up before the test + initSettings(); // Initialize routing rules + + String nonMatchingQueryParam = "nonMatchingQuery"; + String queryParam = "other"; + + // Register a route using the addRoute method + addRoute(queryParam, true, true); + assertResponse(get(requestUrlBase), new String[]{queryParam+"/"}); + + Response response = searchWithQueryParam("q", queryParam, 200); + Assert.assertTrue("Query param should be found in response", + response.getBody().asString().contains(queryParam)); + response = searchWithQueryParam("q", nonMatchingQueryParam, 200); + JsonObject jsonResponse = new JsonObject(response.getBody().asString()); + Assert.assertTrue("Expected 'routes' to be an empty array", + jsonResponse.containsKey("routes") && jsonResponse.getJsonArray("routes").isEmpty()); + removeRoute(queryParam); + async.complete(); + } + + @Test + public void testHookHandleSearch_RouteWhenNoRoutesRegistered(TestContext context) { + Async async = context.async(); + delete(); // Ensure there's no previous data + initSettings(); // Initialize routing rules + + String queryParam = "someQuery"; + + // Send GET request with a query param when no routes are registered + Response response = searchWithQueryParam("q", queryParam, 200); + + // Parse response body as JSON + JsonObject jsonResponse = new JsonObject(response.getBody().asString()); + // Validate that "routes" exists and is an empty array + Assert.assertTrue("Expected 'routes' to be an empty array", + jsonResponse.containsKey("routes") && jsonResponse.getJsonArray("routes").isEmpty()); + + async.complete(); + } + + @Test + public void testHookHandleSearch_ReturnsIdenticalResultsWithAndWithoutSearchQueryParam(TestContext context) { + Async async = context.async(); + delete(); // Clear any existing data before starting the test + initSettings(); // Initialize routing rules + + String routeName = "GenericSingleRoute"; + addRoute(routeName, true, true); // Add a route that will be the only matching result + + // Perform search without 'q' parameter + Response responseWithoutParam = get(searchUrlBase) + .then().assertThat().statusCode(200) + .extract().response(); + + // Perform search with 'q' parameter matching the route name + Response responseWithParam = searchWithQueryParam("q", routeName, 200); + + // Extract response bodies as strings for comparison + String responseBodyWithoutParam = responseWithoutParam.getBody().asString(); + String responseBodyWithParam = responseWithParam.getBody().asString(); + + // Verify that both responses are identical + Assert.assertEquals("Responses should be identical with and without 'q' when only one matching route exists", + responseBodyWithoutParam, responseBodyWithParam); + + // Ensure the route name is present in both responses + Assert.assertTrue(responseBodyWithoutParam.contains(routeName)); + Assert.assertTrue(responseBodyWithParam.contains(routeName)); + + // Clean up by removing the registered route after the test + removeRoute(routeName); + + async.complete(); + } private void removeRoute(String name) { String route = requestUrlBase + "/" + name + TestUtils.getHookRouteUrlSuffix(); @@ -213,7 +318,6 @@ private void addRoute(String name, boolean collection, boolean listable) { TestUtils.registerRoute(route, target, methods, null, collection, listable); } - private void assertResponse(final Response response, final String[] expectedArray) { Assert.assertEquals(200, response.statusCode()); String bodyString = response.getBody().asString(); @@ -224,4 +328,11 @@ private void assertResponse(final Response response, final String[] expectedArra Assert.assertThat(array, Matchers.contains(expectedArray)); } + private Response searchWithQueryParam(String queryParamName, String queryParamValue, int expectedStatusCode ) { + return given() + .queryParam(queryParamName, queryParamValue) + .when().get(searchUrlBase) + .then().assertThat().statusCode(expectedStatusCode) + .extract().response(); + } } diff --git a/gateleen-testhelper/pom.xml b/gateleen-testhelper/pom.xml index 3e89f5510..6cc33c307 100644 --- a/gateleen-testhelper/pom.xml +++ b/gateleen-testhelper/pom.xml @@ -6,7 +6,7 @@ org.swisspush.gateleen gateleen - 2.1.12-SNAPSHOT + 2.1.13-SNAPSHOT gateleen-testhelper diff --git a/gateleen-user/pom.xml b/gateleen-user/pom.xml index 1e3c3c4c5..a19758134 100644 --- a/gateleen-user/pom.xml +++ b/gateleen-user/pom.xml @@ -6,7 +6,7 @@ org.swisspush.gateleen gateleen - 2.1.12-SNAPSHOT + 2.1.13-SNAPSHOT gateleen-user diff --git a/gateleen-validation/pom.xml b/gateleen-validation/pom.xml index 81e4c52e4..d0e624ccc 100644 --- a/gateleen-validation/pom.xml +++ b/gateleen-validation/pom.xml @@ -6,7 +6,7 @@ org.swisspush.gateleen gateleen - 2.1.12-SNAPSHOT + 2.1.13-SNAPSHOT gateleen-validation diff --git a/pom.xml b/pom.xml index 928b15bdd..cd834c448 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ org.swisspush.gateleen gateleen - 2.1.12-SNAPSHOT + 2.1.13-SNAPSHOT pom gateleen Middleware library based on Vert.x to build advanced JSON/REST communication servers @@ -74,6 +74,7 @@ 2.4.10 2.12.6 2.9.0 + 1.12.13 5.8.0 3.0.0 0.1.15 @@ -358,6 +359,11 @@ log4j-slf4j2-impl ${log4j-slf4j2.version} + + io.micrometer + micrometer-core + ${micrometer.version} + org.swisspush redisques