diff --git a/ranger-client/src/main/java/io/appform/ranger/client/AbstractRangerHubClient.java b/ranger-client/src/main/java/io/appform/ranger/client/AbstractRangerHubClient.java index 8eac12a6..66d838a3 100644 --- a/ranger-client/src/main/java/io/appform/ranger/client/AbstractRangerHubClient.java +++ b/ranger-client/src/main/java/io/appform/ranger/client/AbstractRangerHubClient.java @@ -37,6 +37,7 @@ @SuperBuilder public abstract class AbstractRangerHubClient, D extends Deserializer> implements RangerHubClient { + private final String upstreamId; private final String namespace; private final ObjectMapper mapper; private final D deserializer; @@ -60,6 +61,7 @@ public abstract class AbstractRangerHubClient, D @Override public void start() { + requireNonNull(upstreamId, "upstreamId can't be null"); requireNonNull(mapper, "Mapper can't be null"); requireNonNull(namespace, "namespace can't be null"); requireNonNull(deserializer, "deserializer can't be null"); @@ -88,7 +90,7 @@ public void start() { this.excludedServices = Objects.requireNonNullElseGet(this.excludedServices, Set::of); if(null == this.serviceDataSource){ - this.serviceDataSource = getDefaultDataSource(); + this.serviceDataSource = getDefaultDataSource(upstreamId); } this.hub = buildHub(); @@ -193,7 +195,7 @@ public CompletableFuture addService(Service service) { } - protected abstract ServiceDataSource getDefaultDataSource(); + protected abstract ServiceDataSource getDefaultDataSource(String upstreamId); protected abstract ServiceFinderFactory getFinderFactory(); diff --git a/ranger-client/src/test/java/io/appform/ranger/client/stubs/RangerTestHub.java b/ranger-client/src/test/java/io/appform/ranger/client/stubs/RangerTestHub.java index 34b4b4c8..01b0c8b1 100644 --- a/ranger-client/src/test/java/io/appform/ranger/client/stubs/RangerTestHub.java +++ b/ranger-client/src/test/java/io/appform/ranger/client/stubs/RangerTestHub.java @@ -52,7 +52,7 @@ protected void postBuild(ServiceFinderHub> buildFinder(Service service) { val finder = new TestSimpleUnshardedServiceFinder() + .withUpstreamId("test-metric") .withNamespace(service.getNamespace()) .withServiceName(service.getServiceName()) .withDeserializer(new Deserializer<>() { diff --git a/ranger-client/src/test/java/io/appform/ranger/client/stubs/TestSimpleUnshardedServiceFinder.java b/ranger-client/src/test/java/io/appform/ranger/client/stubs/TestSimpleUnshardedServiceFinder.java index 3cbb0258..872dd535 100644 --- a/ranger-client/src/test/java/io/appform/ranger/client/stubs/TestSimpleUnshardedServiceFinder.java +++ b/ranger-client/src/test/java/io/appform/ranger/client/stubs/TestSimpleUnshardedServiceFinder.java @@ -17,10 +17,7 @@ import io.appform.ranger.core.finder.SimpleUnshardedServiceFinder; import io.appform.ranger.core.finder.SimpleUnshardedServiceFinderBuilder; -import io.appform.ranger.core.model.Deserializer; -import io.appform.ranger.core.model.NodeDataSource; -import io.appform.ranger.core.model.Service; -import io.appform.ranger.core.model.ServiceNode; +import io.appform.ranger.core.model.*; import io.appform.ranger.core.units.TestNodeData; import lombok.Builder; @@ -38,12 +35,22 @@ public SimpleUnshardedServiceFinder build() { } @Override - protected NodeDataSource> dataSource(Service service) { + protected NodeDataSource> dataSource(String upstreamId, Service service) { return new TestDataSource(); } static class TestDataSource implements NodeDataSource>{ + @Override + public String getUpstreamId() { + return "testDataSource"; + } + + @Override + public DataStoreType getDataStoreType() { + return DataStoreType.HTTP; + } + @Override public Optional>> refresh(Deserializer deserializer) { return Optional.of(Collections.singletonList( diff --git a/ranger-client/src/test/java/io/appform/ranger/client/utils/RangerHubTestUtils.java b/ranger-client/src/test/java/io/appform/ranger/client/utils/RangerHubTestUtils.java index 6c6dcb0f..923729dd 100644 --- a/ranger-client/src/test/java/io/appform/ranger/client/utils/RangerHubTestUtils.java +++ b/ranger-client/src/test/java/io/appform/ranger/client/utils/RangerHubTestUtils.java @@ -39,6 +39,7 @@ public static RangerTestHub getTestHub(){ .nodeRefreshTimeMs(1000) .initialCriteria(new TestCriteria()) .deserializer(new TestDeserializer<>()) + .upstreamId("test-metric") .build(); } @@ -51,6 +52,7 @@ public static RangerTestHub getTestHubWithDataSource(){ .useDefaultDataSource(false) .serviceDataSource(new StaticDataSource(Set.of(RangerHubTestUtils.service))) .deserializer(new TestDeserializer<>()) + .upstreamId("test-metric") .build(); } diff --git a/ranger-core/pom.xml b/ranger-core/pom.xml index 3f64787d..ff4d8162 100644 --- a/ranger-core/pom.xml +++ b/ranger-core/pom.xml @@ -53,6 +53,11 @@ jakarta.validation-api 2.0.2 + + io.dropwizard + dropwizard-metrics + ${dropwizard.version} + org.mockito mockito-core diff --git a/ranger-core/src/main/java/io/appform/ranger/core/finder/BaseServiceFinderBuilder.java b/ranger-core/src/main/java/io/appform/ranger/core/finder/BaseServiceFinderBuilder.java index a4bc7ba5..90176edc 100644 --- a/ranger-core/src/main/java/io/appform/ranger/core/finder/BaseServiceFinderBuilder.java +++ b/ranger-core/src/main/java/io/appform/ranger/core/finder/BaseServiceFinderBuilder.java @@ -47,6 +47,7 @@ public abstract class BaseServiceFinderBuilder B extends BaseServiceFinderBuilder, D extends Deserializer> { + protected String upstreamId; protected String namespace; protected String serviceName; protected int nodeRefreshIntervalMs; @@ -58,6 +59,11 @@ public abstract class BaseServiceFinderBuilder protected final List> startSignalHandlers = new ArrayList<>(); protected final List> stopSignalHandlers = new ArrayList<>(); + public B withUpstreamId(final String upstreamId) { + this.upstreamId = upstreamId; + return (B)this; + } + public B withNamespace(final String namespace) { this.namespace = namespace; return (B)this; @@ -136,6 +142,7 @@ public B withStopSignalHandlers(List> stopSignalHandlers) { public abstract F build(); protected F buildFinder() { + requireNonNull(upstreamId); requireNonNull(namespace); requireNonNull(serviceName); requireNonNull(deserializer); @@ -149,7 +156,7 @@ protected F buildFinder() { val finder = buildFinder(service, shardSelector, nodeSelector); val registry = finder.getServiceRegistry(); val signalGenerators = new ArrayList>(); - val nodeDataSource = dataSource(service); + val nodeDataSource = dataSource(upstreamId, service); signalGenerators.add(new ScheduledRegistryUpdateSignal<>(service, nodeRefreshIntervalMs)); additionalRefreshSignals.addAll(implementationSpecificRefreshSignals(service, nodeDataSource)); @@ -178,7 +185,7 @@ protected List> implementationSpecificRefreshSignals(Service service, return Collections.emptyList(); } - protected abstract NodeDataSource dataSource(Service service); + protected abstract NodeDataSource dataSource(String upstreamId, Service service); protected abstract F buildFinder( Service service, diff --git a/ranger-core/src/main/java/io/appform/ranger/core/finder/serviceregistry/ServiceRegistryUpdater.java b/ranger-core/src/main/java/io/appform/ranger/core/finder/serviceregistry/ServiceRegistryUpdater.java index cf7963ee..4dff74f4 100644 --- a/ranger-core/src/main/java/io/appform/ranger/core/finder/serviceregistry/ServiceRegistryUpdater.java +++ b/ranger-core/src/main/java/io/appform/ranger/core/finder/serviceregistry/ServiceRegistryUpdater.java @@ -23,10 +23,12 @@ import io.appform.ranger.core.healthcheck.HealthcheckStatus; import io.appform.ranger.core.model.Deserializer; import io.appform.ranger.core.model.NodeDataSource; +import io.appform.ranger.core.model.ServiceNode; import io.appform.ranger.core.model.ServiceRegistry; import io.appform.ranger.core.signals.Signal; import io.appform.ranger.core.util.Exceptions; import io.appform.ranger.core.util.FinderUtils; +import io.appform.ranger.core.util.MetricRecorder; import java.util.concurrent.atomic.AtomicBoolean; import lombok.extern.slf4j.Slf4j; import lombok.val; @@ -139,17 +141,24 @@ private void updateRegistry() throws InterruptedException { log.debug("Checking for updates on data source for service: {}", serviceRegistry.getService().getServiceName()); var callFailed = false; - if (nodeDataSource.isActive()) { //Source should implement circuit breaker to fail fast and reopen after some - // time + if (nodeDataSource.isActive()) { //Source should implement circuit breaker to fail fast and reopen after some time + val stopwatch = Stopwatch.createStarted(); try { val nodeList = nodeDataSource.refresh(deserializer).orElse(null); if (null != nodeList) { + MetricRecorder.recordNodesFetchedCount(serviceRegistry.getService().getServiceName(), + nodeDataSource.getDataStoreType(), nodeDataSource.getUpstreamId(), nodeList.size()); log.debug("Updating nodeList of size: {} for [{}]", nodeList.size(), serviceRegistry.getService().getServiceName()); val livenessCheckMaxAge = nodeDataSource.healthcheckZombieCheckThresholdTime(serviceRegistry.getService()); //Remove all stale nodes before updating. This is done centrally to ensure some data sources //don't skip this check. Some control is still provided so that they can overload. - serviceRegistry.updateNodes(FinderUtils.filterValidNodes(serviceRegistry.getService(), nodeList, livenessCheckMaxAge)); + List> validNodes = FinderUtils.filterValidNodes(serviceRegistry.getService(), nodeList, livenessCheckMaxAge); + MetricRecorder.recordServiceRegistryUpdateNodeCount(serviceRegistry.getService().getServiceName(), + nodeDataSource.getDataStoreType(), nodeDataSource.getUpstreamId(), validNodes.size()); + serviceRegistry.updateNodes(validNodes); + MetricRecorder.recordNodeDataRefreshSuccess(nodeDataSource.getDataStoreType(), nodeDataSource.getUpstreamId(), + stopwatch.elapsed(TimeUnit.MILLISECONDS)); } else { log.warn("Empty list returned from node data source. We are in a weird state. Keeping old list for {}", @@ -161,6 +170,11 @@ private void updateRegistry() throws InterruptedException { e.getClass().getSimpleName(), e.getMessage()); callFailed = true; + MetricRecorder.recordNodeDataRefreshFailure(serviceRegistry.getService().getServiceName(), + nodeDataSource.getDataStoreType(), nodeDataSource.getUpstreamId(), + stopwatch.elapsed(TimeUnit.MILLISECONDS)); + } finally { + stopwatch.stop(); } } if (!nodeDataSource.isActive() || callFailed) { @@ -168,11 +182,14 @@ private void updateRegistry() throws InterruptedException { log.warn("Node data source seems to be down. Keeping old list for {}." + " Will update timestamp to keep stale date relevant.", serviceRegistry.getService().getServiceName()); - serviceRegistry.updateNodes(serviceRegistry.nodeList() - .stream() - .filter(node -> HealthcheckStatus.healthy == node.getHealthcheckStatus()) - .map(node -> node.setLastUpdatedTimeStamp(currTime)) - .toList()); + val retainedNodes = serviceRegistry.nodeList() + .stream() + .filter(node -> HealthcheckStatus.healthy == node.getHealthcheckStatus()) + .map(node -> node.setLastUpdatedTimeStamp(currTime)) + .toList(); + serviceRegistry.updateNodes(retainedNodes); + MetricRecorder.recordStaleDataRetained(serviceRegistry.getService().getServiceName(), + nodeDataSource.getDataStoreType(), nodeDataSource.getUpstreamId(), retainedNodes.size()); } } diff --git a/ranger-core/src/main/java/io/appform/ranger/core/healthcheck/HealthChecker.java b/ranger-core/src/main/java/io/appform/ranger/core/healthcheck/HealthChecker.java index be9a1f79..a52e65ad 100644 --- a/ranger-core/src/main/java/io/appform/ranger/core/healthcheck/HealthChecker.java +++ b/ranger-core/src/main/java/io/appform/ranger/core/healthcheck/HealthChecker.java @@ -15,6 +15,7 @@ */ package io.appform.ranger.core.healthcheck; +import io.appform.ranger.core.util.MetricRecorder; import lombok.extern.slf4j.Slf4j; import lombok.val; @@ -57,11 +58,13 @@ private boolean refreshHealth() { catch (Exception e) { log.error("Error running healthcheck. Setting node to unhealthy", e); healthcheckStatus = HealthcheckStatus.unhealthy; + MetricRecorder.recordHealthcheckFailure(); } if (HealthcheckStatus.unhealthy == healthcheckStatus) { break; } } + MetricRecorder.recordHealthcheckStatus(HealthcheckStatus.healthy == healthcheckStatus); //Trigger update only if state change has happened //Conditions on which update will be triggered //1. First time diff --git a/ranger-core/src/main/java/io/appform/ranger/core/model/DataStoreType.java b/ranger-core/src/main/java/io/appform/ranger/core/model/DataStoreType.java new file mode 100644 index 00000000..4ff1b84f --- /dev/null +++ b/ranger-core/src/main/java/io/appform/ranger/core/model/DataStoreType.java @@ -0,0 +1,22 @@ +/* + * Copyright 2024 Authors, Flipkart Internet Pvt. Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.appform.ranger.core.model; + +public enum DataStoreType { + ZK, + HTTP, + DROVE, +} diff --git a/ranger-core/src/main/java/io/appform/ranger/core/model/NodeDataSink.java b/ranger-core/src/main/java/io/appform/ranger/core/model/NodeDataSink.java index faa7c4ad..1fb367c2 100644 --- a/ranger-core/src/main/java/io/appform/ranger/core/model/NodeDataSink.java +++ b/ranger-core/src/main/java/io/appform/ranger/core/model/NodeDataSink.java @@ -19,5 +19,10 @@ * */ public interface NodeDataSink> extends NodeDataStoreConnector { + + DataStoreType getDataStoreType(); + + String getUpstreamId(); + void updateState(S serializer, ServiceNode serviceNode); } diff --git a/ranger-core/src/main/java/io/appform/ranger/core/model/NodeDataSource.java b/ranger-core/src/main/java/io/appform/ranger/core/model/NodeDataSource.java index 86276b2e..3aedad4c 100644 --- a/ranger-core/src/main/java/io/appform/ranger/core/model/NodeDataSource.java +++ b/ranger-core/src/main/java/io/appform/ranger/core/model/NodeDataSource.java @@ -26,6 +26,10 @@ @SuppressWarnings("unused") public interface NodeDataSource> extends NodeDataStoreConnector { + String getUpstreamId(); + + DataStoreType getDataStoreType(); + Optional>> refresh(D deserializer) throws CommunicationException; default long healthcheckZombieCheckThresholdTime(Service service) { diff --git a/ranger-core/src/main/java/io/appform/ranger/core/serviceprovider/BaseServiceProviderBuilder.java b/ranger-core/src/main/java/io/appform/ranger/core/serviceprovider/BaseServiceProviderBuilder.java index 4a0c493e..046de4a3 100644 --- a/ranger-core/src/main/java/io/appform/ranger/core/serviceprovider/BaseServiceProviderBuilder.java +++ b/ranger-core/src/main/java/io/appform/ranger/core/serviceprovider/BaseServiceProviderBuilder.java @@ -40,9 +40,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; -import java.util.Objects; import java.util.function.Consumer; -import java.util.function.Supplier; import static java.util.Objects.requireNonNull; @@ -51,6 +49,7 @@ @SuppressWarnings({"unchecked", "unused", "UnusedReturnValue"}) public abstract class BaseServiceProviderBuilder, S extends Serializer> { + protected String upstreamId; protected String namespace; protected String serviceName; protected S serializer; @@ -70,6 +69,11 @@ public abstract class BaseServiceProviderBuilder> isolatedMonitors = new ArrayList<>(); + public B withUpstreamId(final String upstreamId) { + this.upstreamId = upstreamId; + return (B)this; + } + public BaseServiceProviderBuilder withNamespace(final String namespace) { this.namespace = namespace; return this; @@ -175,10 +179,11 @@ public B healthUpdateHandler(final HealthUpdateHandler healthUpdateHandler) { } protected final ServiceProvider buildProvider() { - Preconditions.checkNotNull(namespace); - Preconditions.checkNotNull(serviceName); - Preconditions.checkNotNull(serializer); - Preconditions.checkNotNull(hostname); + requireNonNull(upstreamId); + requireNonNull(namespace); + requireNonNull(serviceName); + requireNonNull(serializer); + requireNonNull(hostname); Preconditions.checkNotNull(healthUpdateHandler); Preconditions.checkArgument(port > 0); Preconditions.checkArgument(!healthchecks.isEmpty() || !isolatedMonitors.isEmpty()); @@ -200,7 +205,7 @@ protected final ServiceProvider buildProvider() { healthchecks.add(serviceHealthAggregator); val service = Service.builder().namespace(namespace).serviceName(serviceName).build(); - val usableNodeDataSource = dataSink(service); + val usableNodeDataSource = dataSink(upstreamId, service); val healthcheckUpdateSignalGenerator = new ScheduledSignal<>( @@ -246,5 +251,5 @@ protected final ServiceProvider buildProvider() { public abstract ServiceProvider build(); - protected abstract NodeDataSink dataSink(final Service service); + protected abstract NodeDataSink dataSink(String upstreamId, final Service service); } diff --git a/ranger-core/src/main/java/io/appform/ranger/core/util/FinderUtils.java b/ranger-core/src/main/java/io/appform/ranger/core/util/FinderUtils.java index 412486b4..e1ba1c76 100644 --- a/ranger-core/src/main/java/io/appform/ranger/core/util/FinderUtils.java +++ b/ranger-core/src/main/java/io/appform/ranger/core/util/FinderUtils.java @@ -62,6 +62,7 @@ public static boolean isValidNode( return false; } if(serviceNode.getLastUpdatedTimeStamp() < healthcheckZombieCheckThresholdTime) { + MetricRecorder.recordZombieNodeFound(service.getServiceName()); log.warn("Zombie node [{}:{}] found for [{}]", serviceNode.getHost(), serviceNode.getPort(), service.getServiceName()); return false; diff --git a/ranger-core/src/main/java/io/appform/ranger/core/util/MetricRecorder.java b/ranger-core/src/main/java/io/appform/ranger/core/util/MetricRecorder.java new file mode 100644 index 00000000..e434b461 --- /dev/null +++ b/ranger-core/src/main/java/io/appform/ranger/core/util/MetricRecorder.java @@ -0,0 +1,273 @@ +package io.appform.ranger.core.util; + +import com.codahale.metrics.MetricRegistry; +import io.appform.ranger.core.model.DataStoreType; +import lombok.experimental.UtilityClass; + +import static java.util.concurrent.TimeUnit.*; + +@UtilityClass +public class MetricRecorder { + + private static final String PACKAGE_PREFIX = "io.appform.ranger"; + public static final String ACTIVE = "active"; + private static final String INACTIVE = "inactive"; + private static final String NULL_OR_EMPTY_RESPONSE = "nullOrEmptyResponse"; + private static final String DATA_STORE_TYPE = "dataStoreType"; + private static final String DATA_SOURCE = "dataSource"; + private static final String SERVICE_NAME = "serviceName"; + private static final String ZOMBIE_NODES = "zombieNodes"; + private static final String HTTP_CALL = "httpCall"; + private static final String UNKNOWN_FAILURE = "unknownFailure"; + private static final String RESPONSE_PARSE_FAILURE = "responseParseFailure"; + private static final String NODE_DATA_REFRESH = "nodeDataRefresh"; + private static final String HEALTHY = "healthy"; + private static final String UNHEALTHY = "unhealthy"; + private static final String UPDATE = "update"; + + public static final String SERVICES_LIST = "services"; + public static final String LIST_NODES = "listNodes"; + public static final String SUCCESS = "success"; + public static final String FAILURE = "failure"; + public static final String NODE_DATA_SINK = "nodeDataSink"; + public static final String REGISTER_SERVICE = "registerService"; + public static final String SERIALIZATION = "serialization"; + public static final String DESERIALIZATION = "deserialization"; + public static final String STALE_DATA_RETAINED = "staleDataRetained"; + public static final String ZK_READ = "zkRead"; + public static final String HEALTH_CHECKER = "healthChecker"; + public static final String NODE_COUNT = "nodeCount"; + + private static MetricRegistry metricRegistry; + + public static void initialize(MetricRegistry registry) { + metricRegistry = registry; + } + + public static void recordZombieNodeFound(String serviceName) { + if (metricRegistry != null) { + metricRegistry.meter(MetricRegistry.name(PACKAGE_PREFIX, ZOMBIE_NODES)).mark(); + metricRegistry.meter(MetricRegistry.name(PACKAGE_PREFIX, ZOMBIE_NODES, SERVICE_NAME, serviceName)).mark(); + } + } + + public static void recordNoteDataSourceStatus(DataStoreType dataStoreType, String upstreamId, boolean active) { + if (metricRegistry != null) { + metricRegistry.meter(MetricRegistry.name(PACKAGE_PREFIX, DATA_STORE_TYPE, dataStoreType.name(), + DATA_SOURCE, upstreamId, active ? ACTIVE : INACTIVE)).mark(); + } + } + + public static void recordNodeDataRefreshSuccess(DataStoreType dataStoreType, String upstreamId, long elapsed) { + if (metricRegistry != null) { + metricRegistry.timer(MetricRegistry.name(PACKAGE_PREFIX, DATA_STORE_TYPE, dataStoreType.name(), + DATA_SOURCE, upstreamId, NODE_DATA_REFRESH, SUCCESS)).update(elapsed, MILLISECONDS); + } + } + + public static void recordNodeDataRefreshFailure(String serviceName, DataStoreType dataStoreType, + String upstreamId, long elapsed) { + if (metricRegistry != null) { + metricRegistry.timer(MetricRegistry.name(PACKAGE_PREFIX, DATA_STORE_TYPE, dataStoreType.name(), + DATA_SOURCE, upstreamId, NODE_DATA_REFRESH, FAILURE)).update(elapsed, MILLISECONDS); + metricRegistry.timer(MetricRegistry.name(PACKAGE_PREFIX, DATA_STORE_TYPE, dataStoreType.name(), + DATA_SOURCE, upstreamId, SERVICE_NAME, serviceName, NODE_DATA_REFRESH, FAILURE)) + .update(elapsed, MILLISECONDS); + } + } + + public static void recordStaleDataRetained(String serviceName, DataStoreType dataStoreType, String upstreamId, int size) { + if (metricRegistry != null) { + metricRegistry.meter(MetricRegistry.name(PACKAGE_PREFIX, DATA_STORE_TYPE, dataStoreType.name(), + DATA_SOURCE, upstreamId, STALE_DATA_RETAINED)).mark(); + metricRegistry.meter(MetricRegistry.name(PACKAGE_PREFIX, DATA_STORE_TYPE, dataStoreType.name(), + DATA_SOURCE, upstreamId, SERVICE_NAME, serviceName, STALE_DATA_RETAINED)).mark(); + metricRegistry.histogram(MetricRegistry.name(PACKAGE_PREFIX, DATA_STORE_TYPE, dataStoreType.name(), + DATA_SOURCE, upstreamId, SERVICE_NAME, serviceName, STALE_DATA_RETAINED, NODE_COUNT)).update(size); + } + } + + public static void recordNodeDataSinkUpdateStatus(DataStoreType dataStoreType, String upstreamId, String status) { + if (metricRegistry != null) { + metricRegistry.meter(MetricRegistry.name(PACKAGE_PREFIX, DATA_STORE_TYPE, dataStoreType.name(), + DATA_SOURCE, upstreamId, NODE_DATA_SINK, UPDATE, status)).mark(); + } + } + + public static void recordHealthcheckFailure() { + if (metricRegistry != null) { + metricRegistry.meter(MetricRegistry.name(PACKAGE_PREFIX, HEALTH_CHECKER, FAILURE)).mark(); + } + } + + public static void recordHealthcheckStatus(boolean healthy) { + if (metricRegistry != null) { + metricRegistry.meter(MetricRegistry.name(PACKAGE_PREFIX, HEALTH_CHECKER,"status", healthy ? HEALTHY : UNHEALTHY)).mark(); + } + } + + public static void recordServicesFetchStatus(DataStoreType dataStoreType, String upstreamId, String success) { + if (metricRegistry != null) { + metricRegistry.meter(MetricRegistry.name(PACKAGE_PREFIX, DATA_STORE_TYPE, dataStoreType.name(), + DATA_SOURCE, upstreamId, SERVICES_LIST, "fetch", success)).mark(); + } + } + + public static void recordRemoteCallStatusCode(DataStoreType dataStoreType, String upstreamId, + String remoteCall, int statusCode) { + if (metricRegistry != null) { + metricRegistry.meter(MetricRegistry.name(PACKAGE_PREFIX, DATA_STORE_TYPE, dataStoreType.name(), + DATA_SOURCE, upstreamId, HTTP_CALL, remoteCall, "responseStatus", Integer.toString(statusCode))).mark(); + } + } + + public static void recordCacheUpdateOnDroveEvent(DataStoreType dataStoreType, String upstreamId, + String eventName, String serviceName) { + if (metricRegistry != null) { + metricRegistry.meter(MetricRegistry.name(PACKAGE_PREFIX, DATA_STORE_TYPE, dataStoreType.name(), + DATA_SOURCE, upstreamId, "cacheUpdateOnDroveEvent", eventName, SERVICE_NAME, serviceName)).mark(); + } + } + + public static void recordServicesParseFailure(DataStoreType dataStoreType, String upstreamId) { + if (metricRegistry != null) { + metricRegistry.meter(MetricRegistry.name(PACKAGE_PREFIX, DATA_STORE_TYPE, dataStoreType.name(), + DATA_SOURCE, upstreamId, HTTP_CALL, SERVICES_LIST, RESPONSE_PARSE_FAILURE)).mark(); + } + } + + public static void recordListNodesParseFailure(DataStoreType dataStoreType, String upstreamId) { + if (metricRegistry != null) { + metricRegistry.meter(MetricRegistry.name(PACKAGE_PREFIX, DATA_STORE_TYPE, dataStoreType.name(), + DATA_SOURCE, upstreamId, HTTP_CALL, LIST_NODES, RESPONSE_PARSE_FAILURE)).mark(); + } + } + + public static void recordListNodesParseFailure(DataStoreType dataStoreType, String upstreamId, String serviceName) { + if (metricRegistry != null) { + metricRegistry.meter(MetricRegistry.name(PACKAGE_PREFIX, DATA_STORE_TYPE, dataStoreType.name(), + DATA_SOURCE, upstreamId, HTTP_CALL, LIST_NODES, SERVICE_NAME, serviceName, RESPONSE_PARSE_FAILURE)).mark(); + } + } + + + public static void recordZookeeperReadUnknownFailure(DataStoreType dataStoreType, String upstreamId, + String operation, String exceptionName) { + if (metricRegistry != null) { + metricRegistry.meter(MetricRegistry.name(PACKAGE_PREFIX, DATA_STORE_TYPE, dataStoreType.name(), + DATA_SOURCE, upstreamId, ZK_READ, operation, UNKNOWN_FAILURE)).mark(); + metricRegistry.meter(MetricRegistry.name(PACKAGE_PREFIX, DATA_STORE_TYPE, dataStoreType.name(), + DATA_SOURCE, upstreamId, ZK_READ, operation, UNKNOWN_FAILURE, exceptionName)).mark(); + } + } + + public static void recordRemoteCallUnknownFailure(DataStoreType dataStoreType, String upstreamId, + String httpMethodName, String exceptionName) { + if (metricRegistry != null) { + metricRegistry.meter(MetricRegistry.name(PACKAGE_PREFIX, DATA_STORE_TYPE, dataStoreType.name(), + DATA_SOURCE, upstreamId, HTTP_CALL, httpMethodName, UNKNOWN_FAILURE)).mark(); + metricRegistry.meter(MetricRegistry.name(PACKAGE_PREFIX, DATA_STORE_TYPE, dataStoreType.name(), + DATA_SOURCE, upstreamId, HTTP_CALL, httpMethodName, UNKNOWN_FAILURE, exceptionName)).mark(); + } + } + + public static void recordRemoteCallUnknownFailure(DataStoreType dataStoreType, String upstreamId, + String httpMethodName, String exceptionName, String serviceName) { + recordRemoteCallUnknownFailure(dataStoreType, upstreamId, httpMethodName, exceptionName); + if (metricRegistry != null) { + metricRegistry.meter(MetricRegistry.name(PACKAGE_PREFIX, DATA_STORE_TYPE, dataStoreType.name(), + DATA_SOURCE, upstreamId, HTTP_CALL, httpMethodName, SERVICE_NAME, serviceName, + UNKNOWN_FAILURE)).mark(); + metricRegistry.meter(MetricRegistry.name(PACKAGE_PREFIX, DATA_STORE_TYPE, dataStoreType.name(), + DATA_SOURCE, upstreamId, HTTP_CALL, httpMethodName, SERVICE_NAME, serviceName, + UNKNOWN_FAILURE, exceptionName)).mark(); + } + } + + public static void recordNullOrEmptyServicesListResponse(DataStoreType dataStoreType, String upstreamId) { + if (metricRegistry != null) { + metricRegistry.meter(MetricRegistry.name(PACKAGE_PREFIX, DATA_STORE_TYPE, dataStoreType.name(), + DATA_SOURCE, upstreamId, HTTP_CALL, SERVICES_LIST, NULL_OR_EMPTY_RESPONSE)).mark(); + } + } + + public static void recordNullOrEmptyListNodeResponse(DataStoreType dataStoreType, String upstreamId) { + if (metricRegistry != null) { + metricRegistry.meter(MetricRegistry.name(PACKAGE_PREFIX, DATA_STORE_TYPE, dataStoreType.name(), + DATA_SOURCE, upstreamId, HTTP_CALL, LIST_NODES, NULL_OR_EMPTY_RESPONSE)).mark(); + } + } + + public static void recordNullOrEmptyListNodeResponse(DataStoreType dataStoreType, String upstreamId, + String serviceName) { + if (metricRegistry != null) { + metricRegistry.meter(MetricRegistry.name(PACKAGE_PREFIX, DATA_STORE_TYPE, dataStoreType.name(), + DATA_SOURCE, upstreamId, HTTP_CALL, LIST_NODES, SERVICE_NAME, serviceName, NULL_OR_EMPTY_RESPONSE)).mark(); + } + } + + public static void recordNodeDataSinkUnknownFailure(DataStoreType dataStoreType, String upstreamId, + String serviceName, String exceptionName) { + if (metricRegistry != null) { + metricRegistry.meter(MetricRegistry.name(PACKAGE_PREFIX, DATA_STORE_TYPE, dataStoreType.name(), + DATA_SOURCE, upstreamId, NODE_DATA_SINK, UNKNOWN_FAILURE)).mark(); + metricRegistry.meter(MetricRegistry.name(PACKAGE_PREFIX, DATA_STORE_TYPE, dataStoreType.name(), + DATA_SOURCE, upstreamId, NODE_DATA_SINK, SERVICE_NAME, serviceName, UNKNOWN_FAILURE)).mark(); + metricRegistry.meter(MetricRegistry.name(PACKAGE_PREFIX, DATA_STORE_TYPE, dataStoreType.name(), + DATA_SOURCE, upstreamId, NODE_DATA_SINK, SERVICE_NAME, serviceName, UNKNOWN_FAILURE, exceptionName)).mark(); + } + } + + public static void recordNodeDataSinkSerDeFailure(DataStoreType dataStoreType, String upstreamId, + String serDe, String serviceName, String exceptionName) { + if (metricRegistry != null) { + metricRegistry.meter(MetricRegistry.name(PACKAGE_PREFIX, DATA_STORE_TYPE, dataStoreType.name(), + DATA_SOURCE, upstreamId, NODE_DATA_SINK, serDe, FAILURE)).mark(); + metricRegistry.meter(MetricRegistry.name(PACKAGE_PREFIX, DATA_STORE_TYPE, dataStoreType.name(), + DATA_SOURCE, upstreamId, NODE_DATA_SINK, serDe, SERVICE_NAME, serviceName, FAILURE)).mark(); + metricRegistry.meter(MetricRegistry.name(PACKAGE_PREFIX, DATA_STORE_TYPE, dataStoreType.name(), + DATA_SOURCE, upstreamId, NODE_DATA_SINK, serDe, SERVICE_NAME, serviceName, FAILURE, exceptionName)).mark(); + } + } + + public static void recordNullOrEmptyRegisterServiceResponse(DataStoreType dataStoreType, String upstreamId, + String serviceName) { + if (metricRegistry != null) { + metricRegistry.meter(MetricRegistry.name(PACKAGE_PREFIX, DATA_STORE_TYPE, dataStoreType.name(), + DATA_SOURCE, upstreamId, HTTP_CALL, REGISTER_SERVICE, NULL_OR_EMPTY_RESPONSE)).mark(); + metricRegistry.meter(MetricRegistry.name(PACKAGE_PREFIX, DATA_STORE_TYPE, dataStoreType.name(), + DATA_SOURCE, upstreamId, HTTP_CALL, REGISTER_SERVICE, SERVICE_NAME, serviceName, NULL_OR_EMPTY_RESPONSE)) + .mark(); + } + } + + public static void recordServiceNodesReturned(String serviceName, int serviceNodes) { + if (metricRegistry != null) { + metricRegistry.histogram(MetricRegistry.name(PACKAGE_PREFIX, SERVICE_NAME, + serviceName, "nodesReturned")).update(serviceNodes); + } + } + + public static void recordServicesReturned(int services) { + if (metricRegistry != null) { + metricRegistry.histogram(MetricRegistry.name(PACKAGE_PREFIX,"servicesReturned")) + .update(services); + } + } + + public static void recordServiceRegistryUpdateNodeCount(String serviceName, DataStoreType dataStoreType, String upstreamId, int size) { + if (metricRegistry != null) { + metricRegistry.histogram(MetricRegistry.name(PACKAGE_PREFIX, DATA_STORE_TYPE, dataStoreType.name(), + DATA_SOURCE, upstreamId, "serviceRegistryUpdate", SERVICE_NAME, serviceName, NODE_COUNT)) + .update(size); + } + } + + public static void recordNodesFetchedCount(String serviceName, DataStoreType dataStoreType, String upstreamId, int size) { + if (metricRegistry != null) { + metricRegistry.histogram(MetricRegistry.name(PACKAGE_PREFIX, DATA_STORE_TYPE, dataStoreType.name(), + DATA_SOURCE, upstreamId, LIST_NODES, SERVICE_NAME, serviceName, NODE_COUNT)) + .update(size); + } + } +} diff --git a/ranger-core/src/test/java/io/appform/ranger/core/finder/serviceregistry/ServiceRegistryUpdaterMetricsIntegrationTest.java b/ranger-core/src/test/java/io/appform/ranger/core/finder/serviceregistry/ServiceRegistryUpdaterMetricsIntegrationTest.java new file mode 100644 index 00000000..2ad52c62 --- /dev/null +++ b/ranger-core/src/test/java/io/appform/ranger/core/finder/serviceregistry/ServiceRegistryUpdaterMetricsIntegrationTest.java @@ -0,0 +1,579 @@ +/* + * Copyright 2024 Authors, Flipkart Internet Pvt. Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.appform.ranger.core.finder.serviceregistry; + +import com.codahale.metrics.MetricRegistry; +import io.appform.ranger.core.healthcheck.HealthcheckStatus; +import io.appform.ranger.core.model.*; +import io.appform.ranger.core.signals.Signal; +import io.appform.ranger.core.units.TestNodeData; +import io.appform.ranger.core.util.MetricRecorder; +import lombok.val; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.time.Duration; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicBoolean; + +import static org.awaitility.Awaitility.await; +import static org.junit.jupiter.api.Assertions.*; + +class ServiceRegistryUpdaterMetricsIntegrationTest { + + private MetricRegistry metricRegistry; + private static final String METRIC_ID = "test-updater-metric"; + private static final Service TEST_SERVICE = new Service("test-ns", "test-svc"); + + private ServiceRegistryUpdater updater; + + @BeforeEach + void setUp() { + metricRegistry = new MetricRegistry(); + MetricRecorder.initialize(metricRegistry); + } + + @AfterEach + void tearDown() { + if (updater != null) { + updater.stop(); + } + } + + @Test + void testSuccessfulRefresh_recordsSuccessTimer() { + val registry = new MapBasedServiceRegistry(TEST_SERVICE); + val nodes = List.of( + ServiceNode.builder() + .host("host1") + .port(8080) + .nodeData(TestNodeData.builder().shardId(1).build()) + .healthcheckStatus(HealthcheckStatus.healthy) + .lastUpdatedTimeStamp(System.currentTimeMillis()) + .build() + ); + val dataSource = new TestNodeDataSource(METRIC_ID, DataStoreType.ZK, true, nodes, false); + val signal = new TestSignal(); + + updater = new ServiceRegistryUpdater<>(registry, dataSource, List.of(signal), new TestDeserializer()); + updater.start(); + + // Wait for initial update and a brief period for metric recording to complete + awaitRefresh(registry); + + // Wait for success timer to be recorded using Awaitility + val timerName = "io.appform.ranger.dataStoreType.ZK.dataSource." + METRIC_ID + ".nodeDataRefresh.success"; + await() + .atMost(Duration.ofSeconds(5)) + .pollInterval(Duration.ofMillis(50)) + .untilAsserted(() -> { + val timer = metricRegistry.getTimers().get(timerName); + assertNotNull(timer, "Node data refresh success timer should exist"); + assertTrue(timer.getCount() >= 1, "Timer should have at least 1 update"); + }); + + // No failure timer + val failureTimerName = "io.appform.ranger.dataStoreType.ZK.dataSource." + METRIC_ID + ".nodeDataRefresh.failure"; + val failureTimer = metricRegistry.getTimers().get(failureTimerName); + assertTrue(failureTimer == null || failureTimer.getCount() == 0, + "Failure timer should not be recorded on success"); + } + + @Test + void testRefreshThrowsException_recordsFailureTimer() { + val registry = new MapBasedServiceRegistry(TEST_SERVICE); + val dataSource = new TestNodeDataSource(METRIC_ID, DataStoreType.HTTP, true, null, true); + val signal = new TestSignal(); + + updater = new ServiceRegistryUpdater<>(registry, dataSource, List.of(signal), new TestDeserializer()); + // Use try-catch since initial update will fail + try { + updater.start(); + } catch (Exception e) { + // Expected: initial update fails + } + + // Wait for failure timer to be recorded using Awaitility + val failureTimerName = "io.appform.ranger.dataStoreType.HTTP.dataSource." + METRIC_ID + ".nodeDataRefresh.failure"; + await() + .atMost(Duration.ofSeconds(5)) + .pollInterval(Duration.ofMillis(50)) + .untilAsserted(() -> { + val failureTimer = metricRegistry.getTimers().get(failureTimerName); + assertNotNull(failureTimer, "Node data refresh failure timer should exist"); + assertTrue(failureTimer.getCount() >= 1, "Failure timer should have at least 1 update"); + }); + } + + @Test + void testInactiveDataSource_recordsStaleDataRetained() { + val registry = new MapBasedServiceRegistry(TEST_SERVICE); + // Start with an active source so initial update succeeds + val nodes = List.of( + ServiceNode.builder() + .host("host1") + .port(8080) + .nodeData(TestNodeData.builder().shardId(1).build()) + .healthcheckStatus(HealthcheckStatus.healthy) + .lastUpdatedTimeStamp(System.currentTimeMillis()) + .build() + ); + val dataSource = new TestNodeDataSource(METRIC_ID, DataStoreType.DROVE, true, nodes, false); + val signal = new TestSignal(); + + updater = new ServiceRegistryUpdater<>(registry, dataSource, List.of(signal), new TestDeserializer()); + updater.start(); + awaitRefresh(registry); + + // Now deactivate the data source and trigger another update + dataSource.setActive(false); + signal.fire(); + + // Wait for stale data retained meter to be recorded using Awaitility + val meterName = "io.appform.ranger.dataStoreType.DROVE.dataSource." + METRIC_ID + ".staleDataRetained"; + await() + .atMost(Duration.ofSeconds(5)) + .pollInterval(Duration.ofMillis(50)) + .untilAsserted(() -> { + val meter = metricRegistry.getMeters().get(meterName); + assertNotNull(meter, "Stale data retained meter should exist"); + assertTrue(meter.getCount() >= 1, "Stale data retained should be recorded at least once"); + }); + } + + @Test + void testSuccessfulRefresh_zombieNodesFiltered_recordsZombieMetric() { + val registry = new MapBasedServiceRegistry(TEST_SERVICE); + val nodes = List.of( + ServiceNode.builder() + .host("healthy-host") + .port(8080) + .nodeData(TestNodeData.builder().shardId(1).build()) + .healthcheckStatus(HealthcheckStatus.healthy) + .lastUpdatedTimeStamp(System.currentTimeMillis()) + .build(), + ServiceNode.builder() + .host("zombie-host") + .port(8081) + .nodeData(TestNodeData.builder().shardId(2).build()) + .healthcheckStatus(HealthcheckStatus.healthy) + .lastUpdatedTimeStamp(0L) // Very old => zombie + .build() + ); + val dataSource = new TestNodeDataSource(METRIC_ID, DataStoreType.ZK, true, nodes, false); + val signal = new TestSignal(); + + updater = new ServiceRegistryUpdater<>(registry, dataSource, List.of(signal), new TestDeserializer()); + updater.start(); + awaitRefresh(registry); + + // Verify zombie metric recorded (from FinderUtils.filterValidNodes called inside updateRegistry) + val zombieMeter = metricRegistry.getMeters().get("io.appform.ranger.zombieNodes"); + assertNotNull(zombieMeter, "Zombie nodes meter should exist"); + assertTrue(zombieMeter.getCount() >= 1); + + val svcZombieMeter = metricRegistry.getMeters().get("io.appform.ranger.zombieNodes.serviceName.test-svc"); + assertNotNull(svcZombieMeter); + assertTrue(svcZombieMeter.getCount() >= 1); + + // Also verify success timer still recorded + val timerName = "io.appform.ranger.dataStoreType.ZK.dataSource." + METRIC_ID + ".nodeDataRefresh.success"; + val timer = metricRegistry.getTimers().get(timerName); + assertNotNull(timer); + assertTrue(timer.getCount() >= 1); + } + + @Test + void testSuccessfulRefresh_recordsNodesFetchedCount() { + val registry = new MapBasedServiceRegistry(TEST_SERVICE); + val nodes = List.of( + ServiceNode.builder() + .host("host1").port(8080) + .nodeData(TestNodeData.builder().shardId(1).build()) + .healthcheckStatus(HealthcheckStatus.healthy) + .lastUpdatedTimeStamp(System.currentTimeMillis()) + .build(), + ServiceNode.builder() + .host("host2").port(8081) + .nodeData(TestNodeData.builder().shardId(2).build()) + .healthcheckStatus(HealthcheckStatus.healthy) + .lastUpdatedTimeStamp(System.currentTimeMillis()) + .build() + ); + val dataSource = new TestNodeDataSource(METRIC_ID, DataStoreType.ZK, true, nodes, false); + val signal = new TestSignal(); + + updater = new ServiceRegistryUpdater<>(registry, dataSource, List.of(signal), new TestDeserializer()); + updater.start(); + awaitRefresh(registry); + + val histName = "io.appform.ranger.dataStoreType.ZK.dataSource." + METRIC_ID + + ".listNodes.serviceName." + TEST_SERVICE.getServiceName() + ".nodeCount"; + await() + .atMost(Duration.ofSeconds(5)) + .pollInterval(Duration.ofMillis(50)) + .untilAsserted(() -> { + val histogram = metricRegistry.getHistograms().get(histName); + assertNotNull(histogram, "listNodes nodeCount histogram should be recorded"); + assertTrue(histogram.getCount() >= 1, "Histogram should have at least one update"); + assertEquals(2, histogram.getSnapshot().getMax(), + "Fetched count should equal total nodes returned by data source (2)"); + }); + } + + @Test + void testSuccessfulRefresh_recordsServiceRegistryUpdateNodeCount() { + val registry = new MapBasedServiceRegistry(TEST_SERVICE); + val nodes = List.of( + ServiceNode.builder() + .host("host1").port(8080) + .nodeData(TestNodeData.builder().shardId(1).build()) + .healthcheckStatus(HealthcheckStatus.healthy) + .lastUpdatedTimeStamp(System.currentTimeMillis()) + .build(), + ServiceNode.builder() + .host("host2").port(8081) + .nodeData(TestNodeData.builder().shardId(2).build()) + .healthcheckStatus(HealthcheckStatus.healthy) + .lastUpdatedTimeStamp(System.currentTimeMillis()) + .build() + ); + val dataSource = new TestNodeDataSource(METRIC_ID, DataStoreType.ZK, true, nodes, false); + val signal = new TestSignal(); + + updater = new ServiceRegistryUpdater<>(registry, dataSource, List.of(signal), new TestDeserializer()); + updater.start(); + awaitRefresh(registry); + + val histName = "io.appform.ranger.dataStoreType.ZK.dataSource." + METRIC_ID + + ".serviceRegistryUpdate.serviceName." + TEST_SERVICE.getServiceName() + ".nodeCount"; + await() + .atMost(Duration.ofSeconds(5)) + .pollInterval(Duration.ofMillis(50)) + .untilAsserted(() -> { + val histogram = metricRegistry.getHistograms().get(histName); + assertNotNull(histogram, "serviceRegistryUpdate nodeCount histogram should be recorded"); + assertTrue(histogram.getCount() >= 1, "Histogram should have at least one update"); + assertEquals(2, histogram.getSnapshot().getMax(), + "Valid node count should equal 2 healthy, non-zombie nodes"); + }); + } + + @Test + void testFetchedCountExceedsValidCount_whenZombiesPresent() { + val registry = new MapBasedServiceRegistry(TEST_SERVICE); + // 1 healthy + 1 zombie (very old timestamp). Fetched = 2, valid = 1. + val nodes = List.of( + ServiceNode.builder() + .host("healthy-host").port(8080) + .nodeData(TestNodeData.builder().shardId(1).build()) + .healthcheckStatus(HealthcheckStatus.healthy) + .lastUpdatedTimeStamp(System.currentTimeMillis()) + .build(), + ServiceNode.builder() + .host("zombie-host").port(8081) + .nodeData(TestNodeData.builder().shardId(2).build()) + .healthcheckStatus(HealthcheckStatus.healthy) + .lastUpdatedTimeStamp(0L) // zombie + .build() + ); + val dataSource = new TestNodeDataSource(METRIC_ID, DataStoreType.HTTP, true, nodes, false); + val signal = new TestSignal(); + + updater = new ServiceRegistryUpdater<>(registry, dataSource, List.of(signal), new TestDeserializer()); + updater.start(); + awaitRefresh(registry); + + val fetchedHistName = "io.appform.ranger.dataStoreType.HTTP.dataSource." + METRIC_ID + + ".listNodes.serviceName." + TEST_SERVICE.getServiceName() + ".nodeCount"; + val validHistName = "io.appform.ranger.dataStoreType.HTTP.dataSource." + METRIC_ID + + ".serviceRegistryUpdate.serviceName." + TEST_SERVICE.getServiceName() + ".nodeCount"; + + await() + .atMost(Duration.ofSeconds(5)) + .pollInterval(Duration.ofMillis(50)) + .untilAsserted(() -> { + val fetchedHist = metricRegistry.getHistograms().get(fetchedHistName); + val validHist = metricRegistry.getHistograms().get(validHistName); + assertNotNull(fetchedHist, "Fetched count histogram should exist"); + assertNotNull(validHist, "Valid count histogram should exist"); + assertEquals(2, fetchedHist.getSnapshot().getMax(), + "Fetched count should be 2 (all nodes including zombie)"); + assertEquals(1, validHist.getSnapshot().getMax(), + "Valid count should be 1 (zombie filtered out)"); + }); + } + + @Test + void testInactiveDataSource_recordsStaleDataRetainedWithNodeCountHistogram() { + val registry = new MapBasedServiceRegistry(TEST_SERVICE); + val nodes = List.of( + ServiceNode.builder() + .host("host1").port(8080) + .nodeData(TestNodeData.builder().shardId(1).build()) + .healthcheckStatus(HealthcheckStatus.healthy) + .lastUpdatedTimeStamp(System.currentTimeMillis()) + .build(), + ServiceNode.builder() + .host("host2").port(8081) + .nodeData(TestNodeData.builder().shardId(2).build()) + .healthcheckStatus(HealthcheckStatus.healthy) + .lastUpdatedTimeStamp(System.currentTimeMillis()) + .build() + ); + val dataSource = new TestNodeDataSource(METRIC_ID, DataStoreType.ZK, true, nodes, false); + val signal = new TestSignal(); + + updater = new ServiceRegistryUpdater<>(registry, dataSource, List.of(signal), new TestDeserializer()); + updater.start(); + awaitRefresh(registry); + + // Deactivate and trigger stale path + dataSource.setActive(false); + signal.fire(); + + val staleNodeCountHistName = "io.appform.ranger.dataStoreType.ZK.dataSource." + METRIC_ID + + ".serviceName." + TEST_SERVICE.getServiceName() + ".staleDataRetained.nodeCount"; + await() + .atMost(Duration.ofSeconds(5)) + .pollInterval(Duration.ofMillis(50)) + .untilAsserted(() -> { + val histogram = metricRegistry.getHistograms().get(staleNodeCountHistName); + assertNotNull(histogram, "staleDataRetained nodeCount histogram should be recorded"); + assertTrue(histogram.getCount() >= 1, "Histogram should have at least one update"); + // The stale path retains healthy nodes only; 2 healthy nodes were present + assertTrue(histogram.getSnapshot().getMax() >= 0, + "Histogram should record a non-negative node count"); + }); + } + + @Test + void testCallFailure_recordsStaleDataRetainedWithNodeCountHistogram() { + val registry = new MapBasedServiceRegistry(TEST_SERVICE); + val signal = new TestSignal(); + + // First start with a healthy node so the registry has something to retain + val initialNodes = List.of( + ServiceNode.builder() + .host("host1").port(8080) + .nodeData(TestNodeData.builder().shardId(1).build()) + .healthcheckStatus(HealthcheckStatus.healthy) + .lastUpdatedTimeStamp(System.currentTimeMillis()) + .build() + ); + val dataSource = new TestNodeDataSource(METRIC_ID, DataStoreType.HTTP, true, initialNodes, false); + + updater = new ServiceRegistryUpdater<>(registry, dataSource, List.of(signal), new TestDeserializer()); + updater.start(); + awaitRefresh(registry); + + // Now trigger a call failure (while still active, but throws on refresh) + val failingDataSource = new TestNodeDataSource(METRIC_ID, DataStoreType.HTTP, true, null, true); + // Create a new updater with the failing data source and trigger + updater.stop(); + val signal2 = new TestSignal(); + val updater2 = new ServiceRegistryUpdater<>(registry, failingDataSource, List.of(signal2), new TestDeserializer()); + try { + updater2.start(); + } catch (Exception ignored) { + // May throw on initial update failure + } + + val staleNodeCountHistName = "io.appform.ranger.dataStoreType.HTTP.dataSource." + METRIC_ID + + ".serviceName." + TEST_SERVICE.getServiceName() + ".staleDataRetained.nodeCount"; + await() + .atMost(Duration.ofSeconds(5)) + .pollInterval(Duration.ofMillis(50)) + .untilAsserted(() -> { + val histogram = metricRegistry.getHistograms().get(staleNodeCountHistName); + assertNotNull(histogram, "staleDataRetained nodeCount histogram should be recorded on call failure"); + assertTrue(histogram.getCount() >= 1, "Histogram should have at least one update"); + }); + + updater2.stop(); + } + + @Test + void testRefreshReturnsNull_noSuccessOrFailureTimer_butStaleRetained() { + val registry = new MapBasedServiceRegistry(TEST_SERVICE); + // Return null from refresh (empty Optional) + val dataSource = new TestNodeDataSource(METRIC_ID, DataStoreType.ZK, true, + null, false); // null nodes => refresh returns empty + // Pre-populate registry so initial wait doesn't block forever + registry.updateNodes(List.of( + ServiceNode.builder() + .host("old-host") + .port(8080) + .nodeData(TestNodeData.builder().shardId(1).build()) + .healthcheckStatus(HealthcheckStatus.healthy) + .lastUpdatedTimeStamp(System.currentTimeMillis()) + .build() + )); + + val signal = new TestSignal(); + updater = new ServiceRegistryUpdater<>(registry, dataSource, List.of(signal), new TestDeserializer()); + + // Cannot call start() because initial refresh returns null which won't set refreshed=true + // Instead, test the scenario after start by triggering signal + // Use alternate approach: make first call return valid, then switch to null + dataSource.setNodeList(List.of( + ServiceNode.builder() + .host("valid-host") + .port(8080) + .nodeData(TestNodeData.builder().shardId(1).build()) + .healthcheckStatus(HealthcheckStatus.healthy) + .lastUpdatedTimeStamp(System.currentTimeMillis()) + .build() + )); + + updater = new ServiceRegistryUpdater<>(registry, dataSource, List.of(signal), new TestDeserializer()); + updater.start(); + awaitRefresh(registry); + + // Get the initial count after first successful refresh + val timerName = "io.appform.ranger.dataStoreType.ZK.dataSource." + METRIC_ID + ".nodeDataRefresh.success"; + + // Wait for the first success timer to be recorded + await() + .atMost(Duration.ofSeconds(5)) + .pollInterval(Duration.ofMillis(50)) + .untilAsserted(() -> { + val timer = metricRegistry.getTimers().get(timerName); + assertNotNull(timer, "Success timer should exist after first refresh"); + assertTrue(timer.getCount() >= 1, "Should have at least one success"); + }); + + val beforeTimer = metricRegistry.getTimers().get(timerName); + final long beforeCount = beforeTimer.getCount(); + + // Now set to null and trigger update + dataSource.setNodeList(null); + signal.fire(); + + // Wait a bit to ensure the signal processing would have completed + // Use a shorter wait since we're just ensuring the async operation completes + await() + .atMost(Duration.ofSeconds(2)) + .pollDelay(Duration.ofMillis(100)) + .until(() -> true); + + val afterTimer = metricRegistry.getTimers().get(timerName); + final long afterCount = afterTimer == null ? 0L : afterTimer.getCount(); + + assertEquals(beforeCount, afterCount, + "Success timer count should not increase when refresh returns null"); + } + + // ==================== Test helpers ==================== + + private void awaitRefresh(ServiceRegistry registry) { + await() + .atMost(Duration.ofSeconds(5)) + .pollInterval(Duration.ofMillis(50)) + .untilAsserted(() -> assertTrue(registry.isRefreshed(), "Registry should be refreshed")); + } + + // ==================== Test implementations ==================== + + static class TestDeserializer implements Deserializer { + } + + static class TestNodeDataSource implements NodeDataSource { + private final String upstreamId; + private final DataStoreType dataStoreType; + private final AtomicBoolean active; + private volatile List> nodeList; + private final boolean throwOnRefresh; + + TestNodeDataSource(String upstreamId, DataStoreType dataStoreType, boolean active, + List> nodeList, boolean throwOnRefresh) { + this.upstreamId = upstreamId; + this.dataStoreType = dataStoreType; + this.active = new AtomicBoolean(active); + this.nodeList = nodeList; + this.throwOnRefresh = throwOnRefresh; + } + + void setActive(boolean active) { + this.active.set(active); + } + + void setNodeList(List> nodeList) { + this.nodeList = nodeList; + } + + @Override + public String getUpstreamId() { + return upstreamId; + } + + @Override + public DataStoreType getDataStoreType() { + return dataStoreType; + } + + @Override + public Optional>> refresh(TestDeserializer deserializer) { + if (throwOnRefresh) { + throw new RuntimeException("Simulated refresh failure"); + } + return Optional.ofNullable(nodeList); + } + + @Override + public void start() { + // No-op for test + } + + @Override + public void ensureConnected() { + // No-op for test + } + + @Override + public void stop() { + // No-op for test + } + + @Override + public boolean isActive() { + return active.get(); + } + } + + static class TestSignal extends Signal { + protected TestSignal() { + super(() -> null, Collections.emptyList()); + } + + public void fire() { + onSignalReceived(); + } + + @Override + public void start() { + // No-op for test + } + + @Override + public void stop() { + // No-op for test + } + } +} diff --git a/ranger-core/src/test/java/io/appform/ranger/core/finderhub/ServiceFinderHubTest.java b/ranger-core/src/test/java/io/appform/ranger/core/finderhub/ServiceFinderHubTest.java index 2e97cbb8..bce345ae 100644 --- a/ranger-core/src/test/java/io/appform/ranger/core/finderhub/ServiceFinderHubTest.java +++ b/ranger-core/src/test/java/io/appform/ranger/core/finderhub/ServiceFinderHubTest.java @@ -17,7 +17,6 @@ package io.appform.ranger.core.finderhub; -import com.google.common.collect.Lists; import io.appform.ranger.core.exceptions.CommunicationException; import io.appform.ranger.core.finder.BaseServiceFinderBuilder; import io.appform.ranger.core.finder.ServiceFinder; @@ -47,6 +46,7 @@ class ServiceFinderHubTest { new DynamicDataSource(List.of(new Service("NS", "PRE_REGISTERED_SERVICE"))), service -> new TestServiceFinderBuilder() + .withUpstreamId("test-metric") .withNamespace(service.getNamespace()) .withServiceName(service.getServiceName()) .withDeserializer(new Deserializer() { @@ -72,15 +72,11 @@ void testDynamicServiceAddition() { @Test void testTimeoutOnHubStartup() { - /* - This is intentionally set to 5 seconds to allow for failsafe's default attempt count (2 + 1 = 3) to be over - (Assuming hardcoded delay of 1 second per attempt). - */ var testServiceFinderHub = new TestServiceFinderHubBuilder() .withServiceDataSource(new DynamicDataSource(List.of(new Service("NS", "SERVICE")))) .withServiceFinderFactory(new TestServiceFinderFactory()) - .withRefreshFrequencyMs(10_000) - .withHubStartTimeout(5_000) + .withRefreshFrequencyMs(5_000) + .withHubStartTimeout(1_000) .withServiceRefreshTimeout(10_000) .build(); @@ -97,6 +93,7 @@ void testTimeoutOnHubStartup() { void testDelayedServiceAddition() { val delayedHub = new ServiceFinderHub<>(new DynamicDataSource(List.of(new Service("NS", "SERVICE"))), service -> new TestServiceFinderBuilder() + .withUpstreamId("test-metric") .withNamespace(service.getNamespace()) .withServiceName(service.getServiceName()) .withDeserializer(new Deserializer() {}) @@ -105,6 +102,7 @@ void testDelayedServiceAddition() { Assertions.assertThrows(IllegalStateException.class, delayedHub::start); val serviceFinderHub = new ServiceFinderHub<>(new DynamicDataSource(List.of(new Service("NS", "SERVICE"))), service -> new TestServiceFinderBuilder() + .withUpstreamId("test-metric") .withNamespace(service.getNamespace()) .withServiceName(service.getServiceName()) .withDeserializer(new Deserializer() {}) @@ -118,6 +116,7 @@ void testDelayedServiceAddition() { @Test void testDynamicServiceAdditionWithNonDynamicDataSource() { val serviceFinderHub = new ServiceFinderHub<>(new StaticDataSource(new HashSet<>()), service -> new TestServiceFinderBuilder() + .withUpstreamId("test-metric") .withNamespace(service.getNamespace()) .withServiceName(service.getServiceName()) .withDeserializer(new Deserializer() { @@ -136,9 +135,10 @@ void testDynamicServiceAdditionWithNonDynamicDataSource() { void testWeightedNodeSelectionWithVaryingWeights() { final ServiceFinderHub> serviceFinderHubVaryingWeights = new ServiceFinderHub<>( - new DynamicDataSource(Lists.newArrayList(new Service("NS", "PRE_REGISTERED_SERVICE"))), + new DynamicDataSource(List.of(new Service("NS", "PRE_REGISTERED_SERVICE"))), service -> new TestServiceFinderBuilder() + .withUpstreamId("test-metric") .withNamespace(service.getNamespace()) .withServiceName(service.getServiceName()) .withNodeSelector(new WeightedRandomServiceNodeSelector<>( @@ -150,6 +150,16 @@ void testWeightedNodeSelectionWithVaryingWeights() { .withDeserializer(new Deserializer() { }) .withDataSource(new NodeDataSource<>() { + @Override + public String getUpstreamId() { + return "testVaryingWeights"; + } + + @Override + public DataStoreType getDataStoreType() { + return DataStoreType.HTTP; + } + @Override public Optional>> refresh( final Deserializer deserializer) @@ -230,9 +240,10 @@ public boolean isActive() { void testWeightedNodeSelectionWithVaryingNodeAge() { final ServiceFinderHub> serviceFinderHubVaryingNodeAge = new ServiceFinderHub<>( - new DynamicDataSource(Lists.newArrayList(new Service("NS", "PRE_REGISTERED_SERVICE"))), + new DynamicDataSource(List.of(new Service("NS", "PRE_REGISTERED_SERVICE"))), service -> new TestServiceFinderBuilder() + .withUpstreamId("test-metric") .withNamespace(service.getNamespace()) .withServiceName(service.getServiceName()) .withNodeSelector(new WeightedRandomServiceNodeSelector<>( @@ -244,6 +255,16 @@ void testWeightedNodeSelectionWithVaryingNodeAge() { .withDeserializer(new Deserializer() { }) .withDataSource(new NodeDataSource<>() { + @Override + public String getUpstreamId() { + return "testVaryingNodeAge"; + } + + @Override + public DataStoreType getDataStoreType() { + return DataStoreType.HTTP; + } + @Override public Optional>> refresh( final Deserializer deserializer) @@ -327,6 +348,7 @@ public class TestServiceFinderFactory implements ServiceFinderFactory> buildFinder(Service service) { val finder = new TestServiceFinderBuilder() + .withUpstreamId("test-metric") .withNamespace(service.getNamespace()) .withServiceName(service.getServiceName()) .withDeserializer(new Deserializer() {}) @@ -363,7 +385,7 @@ public ServiceFinder> build( } @Override - protected NodeDataSource> dataSource(Service service) { + protected NodeDataSource> dataSource(String upstreamId, Service service) { return testNodeDataSource; } @@ -388,6 +410,16 @@ public TestServiceFinderBuilder withDataSource( } private static class TestNodeDataSource implements NodeDataSource> { + @Override + public String getUpstreamId() { + return "testNodeDataSource"; + } + + @Override + public DataStoreType getDataStoreType() { + return DataStoreType.HTTP; + } + @Override public Optional>> refresh(Deserializer deserializer) { val list = new ArrayList>(); diff --git a/ranger-core/src/test/java/io/appform/ranger/core/healthcheck/HealthCheckerMetricsIntegrationTest.java b/ranger-core/src/test/java/io/appform/ranger/core/healthcheck/HealthCheckerMetricsIntegrationTest.java new file mode 100644 index 00000000..0639f38e --- /dev/null +++ b/ranger-core/src/test/java/io/appform/ranger/core/healthcheck/HealthCheckerMetricsIntegrationTest.java @@ -0,0 +1,205 @@ +/* + * Copyright 2024 Authors, Flipkart Internet Pvt. Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.appform.ranger.core.healthcheck; + +import com.codahale.metrics.MetricRegistry; +import io.appform.ranger.core.util.MetricRecorder; +import lombok.val; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +class HealthCheckerMetricsIntegrationTest { + + private MetricRegistry metricRegistry; + + // Metric keys produced by MetricRecorder (no DataStoreType/upstreamId context): + // recordHealthcheckStatus -> io.appform.ranger.healthChecker.status. + // recordHealthcheckFailure -> io.appform.ranger.healthChecker.failure + private static final String HC_PREFIX = "io.appform.ranger.healthChecker"; + private static final String HEALTHY_KEY = HC_PREFIX + ".status.healthy"; + private static final String UNHEALTHY_KEY = HC_PREFIX + ".status.unhealthy"; + private static final String FAILURE_KEY = HC_PREFIX + ".failure"; + + @BeforeEach + void setUp() { + metricRegistry = new MetricRegistry(); + MetricRecorder.initialize(metricRegistry); + } + + @Test + void testHealthyCheck_recordsHealthyMetric() { + val healthChecker = new HealthChecker( + List.of(() -> HealthcheckStatus.healthy), 10000); + + val result = healthChecker.get(); + + assertNotNull(result, "First call should always return a result"); + assertEquals(HealthcheckStatus.healthy, result.getStatus()); + + // Verify healthy metric recorded + val healthyMeter = metricRegistry.getMeters().get(HEALTHY_KEY); + assertNotNull(healthyMeter, "Healthy meter should exist"); + assertEquals(1, healthyMeter.getCount()); + + // Verify no failure metric + assertNull(metricRegistry.getMeters().get(FAILURE_KEY)); + // Verify no unhealthy metric + assertNull(metricRegistry.getMeters().get(UNHEALTHY_KEY)); + } + + @Test + void testUnhealthyCheck_recordsUnhealthyMetric() { + val healthChecker = new HealthChecker( + List.of(() -> HealthcheckStatus.unhealthy), 10000); + + val result = healthChecker.get(); + + assertNotNull(result); + assertEquals(HealthcheckStatus.unhealthy, result.getStatus()); + + // Verify unhealthy metric recorded + val unhealthyMeter = metricRegistry.getMeters().get(UNHEALTHY_KEY); + assertNotNull(unhealthyMeter, "Unhealthy meter should exist"); + assertEquals(1, unhealthyMeter.getCount()); + + // No healthy metric + assertNull(metricRegistry.getMeters().get(HEALTHY_KEY)); + } + + @Test + void testExceptionInHealthcheck_recordsFailureAndUnhealthyMetrics() { + val healthChecker = new HealthChecker(List.of(() -> { + throw new RuntimeException("Healthcheck error"); + }), 10000); + + val result = healthChecker.get(); + + assertNotNull(result); + assertEquals(HealthcheckStatus.unhealthy, result.getStatus()); + + // Verify failure metric recorded (exception path) + val failureMeter = metricRegistry.getMeters().get(FAILURE_KEY); + assertNotNull(failureMeter, "Failure meter should exist on exception"); + assertEquals(1, failureMeter.getCount()); + + // Verify unhealthy status metric also recorded + val unhealthyMeter = metricRegistry.getMeters().get(UNHEALTHY_KEY); + assertNotNull(unhealthyMeter, "Unhealthy meter should be recorded after exception"); + assertEquals(1, unhealthyMeter.getCount()); + } + + @Test + void testMultipleHealthchecks_firstUnhealthy_shortCircuits() { + val healthChecker = new HealthChecker(List.of( + () -> HealthcheckStatus.unhealthy, + () -> HealthcheckStatus.healthy // Should not be reached + ), 10000); + + val result = healthChecker.get(); + + assertNotNull(result); + assertEquals(HealthcheckStatus.unhealthy, result.getStatus()); + + val unhealthyMeter = metricRegistry.getMeters().get(UNHEALTHY_KEY); + assertNotNull(unhealthyMeter); + assertEquals(1, unhealthyMeter.getCount()); + } + + @Test + void testMultipleHealthchecks_allHealthy() { + val healthChecker = new HealthChecker(List.of( + () -> HealthcheckStatus.healthy, + () -> HealthcheckStatus.healthy, + () -> HealthcheckStatus.healthy + ), 10000); + + val result = healthChecker.get(); + + assertNotNull(result); + assertEquals(HealthcheckStatus.healthy, result.getStatus()); + + val healthyMeter = metricRegistry.getMeters().get(HEALTHY_KEY); + assertNotNull(healthyMeter); + assertEquals(1, healthyMeter.getCount()); + } + + @Test + void testRepeatedCalls_metricsAccumulate() { + val healthChecker = new HealthChecker( + List.of(() -> HealthcheckStatus.healthy), 0); // staleUpdateThreshold=0 => always returns result + + healthChecker.get(); + healthChecker.get(); + healthChecker.get(); + + val healthyMeter = metricRegistry.getMeters().get(HEALTHY_KEY); + assertNotNull(healthyMeter); + assertEquals(3, healthyMeter.getCount(), "Healthy metric count should accumulate"); + } + + @Test + void testHealthStatusTransition_bothMetricsRecorded() { + // Start healthy, then transition to unhealthy + val statusHolder = new HealthcheckStatus[]{HealthcheckStatus.healthy}; + val healthChecker = new HealthChecker(List.of(() -> statusHolder[0]), 0); + + // First call: healthy + val result1 = healthChecker.get(); + assertNotNull(result1); + assertEquals(HealthcheckStatus.healthy, result1.getStatus()); + + // Switch to unhealthy + statusHolder[0] = HealthcheckStatus.unhealthy; + val result2 = healthChecker.get(); + assertNotNull(result2); + assertEquals(HealthcheckStatus.unhealthy, result2.getStatus()); + + val healthyMeter = metricRegistry.getMeters().get(HEALTHY_KEY); + assertNotNull(healthyMeter); + assertEquals(1, healthyMeter.getCount()); + + val unhealthyMeter = metricRegistry.getMeters().get(UNHEALTHY_KEY); + assertNotNull(unhealthyMeter); + assertEquals(1, unhealthyMeter.getCount()); + } + + @Test + void testExceptionInSecondHealthcheck_recordsFailure() { + val healthChecker = new HealthChecker(List.of( + () -> HealthcheckStatus.healthy, // First passes + () -> { throw new RuntimeException("Second fails"); } // Second throws + ), 10000); + + val result = healthChecker.get(); + + assertNotNull(result); + assertEquals(HealthcheckStatus.unhealthy, result.getStatus()); + + // Failure meter for the exception + val failureMeter = metricRegistry.getMeters().get(FAILURE_KEY); + assertNotNull(failureMeter); + assertEquals(1, failureMeter.getCount()); + + // Unhealthy status because the second check failed + val unhealthyMeter = metricRegistry.getMeters().get(UNHEALTHY_KEY); + assertNotNull(unhealthyMeter); + assertEquals(1, unhealthyMeter.getCount()); + } +} diff --git a/ranger-core/src/test/java/io/appform/ranger/core/serviceprovider/ServiceProviderTest.java b/ranger-core/src/test/java/io/appform/ranger/core/serviceprovider/ServiceProviderTest.java index 932599c3..0b1bfab8 100644 --- a/ranger-core/src/test/java/io/appform/ranger/core/serviceprovider/ServiceProviderTest.java +++ b/ranger-core/src/test/java/io/appform/ranger/core/serviceprovider/ServiceProviderTest.java @@ -21,10 +21,7 @@ import io.appform.ranger.core.healthcheck.updater.HealthStatusHandler; import io.appform.ranger.core.healthcheck.updater.HealthUpdateHandler; import io.appform.ranger.core.healthcheck.updater.LastUpdatedHandler; -import io.appform.ranger.core.model.NodeDataSink; -import io.appform.ranger.core.model.Serializer; -import io.appform.ranger.core.model.Service; -import io.appform.ranger.core.model.ServiceNode; +import io.appform.ranger.core.model.*; import io.appform.ranger.core.units.TestNodeData; import lombok.val; import org.junit.jupiter.api.Assertions; @@ -61,6 +58,16 @@ static class TestNodeDataSink serviceNode) { testNodeData = serviceNode.getNodeData(); @@ -96,7 +103,7 @@ public ServiceProvider> build() { } @Override - protected NodeDataSink> dataSink(Service service) { + protected NodeDataSink> dataSink(String upstreamId, Service service) { return new TestNodeDataSink<>(); } } @@ -116,6 +123,7 @@ void testInvalidServiceProviderNoHealthCheck() { final HealthUpdateHandler healthUpdateHandler = new LastUpdatedHandler() .setNext(new HealthStatusHandler()); Assertions.assertThrowsExactly(IllegalArgumentException.class, () -> new TestServiceProviderBuilder<>() + .withUpstreamId("test-metric") .withServiceName("test-service") .withNamespace("test") .withHostname("localhost-1") @@ -142,6 +150,7 @@ void testBuildServiceProvider() { final HealthUpdateHandler healthUpdateHandler = new LastUpdatedHandler() .setNext(new HealthStatusHandler()); val testProvider = new TestServiceProviderBuilder<>() + .withUpstreamId("test-metric") .withServiceName("test-service") .withNamespace("test") .withHostname("localhost-1") diff --git a/ranger-core/src/test/java/io/appform/ranger/core/util/FinderUtilsMetricsIntegrationTest.java b/ranger-core/src/test/java/io/appform/ranger/core/util/FinderUtilsMetricsIntegrationTest.java new file mode 100644 index 00000000..438efdaa --- /dev/null +++ b/ranger-core/src/test/java/io/appform/ranger/core/util/FinderUtilsMetricsIntegrationTest.java @@ -0,0 +1,179 @@ +/* + * Copyright 2024 Authors, Flipkart Internet Pvt. Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.appform.ranger.core.util; + +import com.codahale.metrics.MetricRegistry; +import io.appform.ranger.core.healthcheck.HealthcheckStatus; +import io.appform.ranger.core.model.Service; +import io.appform.ranger.core.model.ServiceNode; +import lombok.val; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +class FinderUtilsMetricsIntegrationTest { + + private MetricRegistry metricRegistry; + + @BeforeEach + void setUp() { + metricRegistry = new MetricRegistry(); + MetricRecorder.initialize(metricRegistry); + } + + @Test + void testZombieNodeDetection_recordsMetric() { + val service = new Service("test-ns", "test-svc"); + val zombieNode = ServiceNode.builder() + .host("localhost") + .port(8080) + .nodeData(1) + .healthcheckStatus(HealthcheckStatus.healthy) + .lastUpdatedTimeStamp(0L) // Very old timestamp => zombie + .build(); + + val thresholdTime = System.currentTimeMillis() - 60000; // 1 minute ago + + val result = FinderUtils.isValidNode(service, thresholdTime, zombieNode); + + assertFalse(result, "Zombie node should be invalid"); + + // Verify global zombie nodes meter + val globalMeter = metricRegistry.getMeters().get("io.appform.ranger.zombieNodes"); + assertNotNull(globalMeter, "Global zombie nodes meter should exist"); + assertEquals(1, globalMeter.getCount()); + + // Verify per-service zombie nodes meter + val svcMeter = metricRegistry.getMeters().get("io.appform.ranger.zombieNodes.serviceName.test-svc"); + assertNotNull(svcMeter, "Per-service zombie nodes meter should exist"); + assertEquals(1, svcMeter.getCount()); + } + + @Test + void testZombieNodeDetection_multipleZombies() { + val service = new Service("ns", "my-service"); + val thresholdTime = System.currentTimeMillis() - 60000; + + // Create 3 zombie nodes + for (int i = 0; i < 3; i++) { + val zombieNode = ServiceNode.builder() + .host("host-" + i) + .port(8080 + i) + .nodeData(i) + .healthcheckStatus(HealthcheckStatus.healthy) + .lastUpdatedTimeStamp(0L) + .build(); + FinderUtils.isValidNode(service, thresholdTime, zombieNode); + } + + val globalMeter = metricRegistry.getMeters().get("io.appform.ranger.zombieNodes"); + assertEquals(3, globalMeter.getCount()); + + val svcMeter = metricRegistry.getMeters().get("io.appform.ranger.zombieNodes.serviceName.my-service"); + assertEquals(3, svcMeter.getCount()); + } + + @Test + void testHealthyNode_noMetricRecorded() { + val service = new Service("ns", "svc"); + val healthyNode = ServiceNode.builder() + .host("localhost") + .port(8080) + .nodeData(1) + .healthcheckStatus(HealthcheckStatus.healthy) + .lastUpdatedTimeStamp(System.currentTimeMillis()) // Fresh timestamp + .build(); + + val thresholdTime = System.currentTimeMillis() - 60000; + val result = FinderUtils.isValidNode(service, thresholdTime, healthyNode); + + assertTrue(result, "Healthy node should be valid"); + assertTrue(metricRegistry.getMeters().isEmpty(), "No meters should be recorded for healthy nodes"); + } + + @Test + void testUnhealthyNode_noZombieMetricRecorded() { + val service = new Service("ns", "svc"); + val unhealthyNode = ServiceNode.builder() + .host("localhost") + .port(8080) + .nodeData(1) + .healthcheckStatus(HealthcheckStatus.unhealthy) + .lastUpdatedTimeStamp(System.currentTimeMillis()) + .build(); + + val thresholdTime = System.currentTimeMillis() - 60000; + val result = FinderUtils.isValidNode(service, thresholdTime, unhealthyNode); + + assertFalse(result, "Unhealthy node should be invalid"); + // Zombie metric should NOT be recorded for unhealthy nodes (only for stale healthy nodes) + assertTrue(metricRegistry.getMeters().isEmpty(), + "No zombie meters should be recorded for unhealthy nodes"); + } + + @Test + void testFilterValidNodes_zombiesFiltered_metricsRecorded() { + val service = new Service("ns", "filter-svc"); + val thresholdTime = System.currentTimeMillis() - 60000; + + val nodes = List.of( + ServiceNode.builder() + .host("good-host") + .port(8080) + .nodeData(1) + .healthcheckStatus(HealthcheckStatus.healthy) + .lastUpdatedTimeStamp(System.currentTimeMillis()) + .build(), + ServiceNode.builder() + .host("zombie-host") + .port(8081) + .nodeData(2) + .healthcheckStatus(HealthcheckStatus.healthy) + .lastUpdatedTimeStamp(0L) // zombie + .build(), + ServiceNode.builder() + .host("zombie-host-2") + .port(8082) + .nodeData(3) + .healthcheckStatus(HealthcheckStatus.healthy) + .lastUpdatedTimeStamp(0L) // zombie + .build() + ); + + val filtered = FinderUtils.filterValidNodes(service, nodes, thresholdTime); + + assertEquals(1, filtered.size(), "Only 1 valid node should remain"); + assertEquals("good-host", filtered.get(0).getHost()); + + val globalMeter = metricRegistry.getMeters().get("io.appform.ranger.zombieNodes"); + assertEquals(2, globalMeter.getCount(), "2 zombie detections should be recorded"); + + val svcMeter = metricRegistry.getMeters().get("io.appform.ranger.zombieNodes.serviceName.filter-svc"); + assertEquals(2, svcMeter.getCount()); + } + + @Test + void testNullNode_noMetricRecorded() { + val service = new Service("ns", "svc"); + val result = FinderUtils.isValidNode(service, System.currentTimeMillis() - 60000, null); + + assertFalse(result, "Null node should be invalid"); + assertTrue(metricRegistry.getMeters().isEmpty(), "No meters should be recorded for null nodes"); + } +} diff --git a/ranger-core/src/test/java/io/appform/ranger/core/util/MetricRecorderTest.java b/ranger-core/src/test/java/io/appform/ranger/core/util/MetricRecorderTest.java new file mode 100644 index 00000000..ebc0f1e1 --- /dev/null +++ b/ranger-core/src/test/java/io/appform/ranger/core/util/MetricRecorderTest.java @@ -0,0 +1,276 @@ +/* + * Copyright 2024 Authors, Flipkart Internet Pvt. Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.appform.ranger.core.util; + +import com.codahale.metrics.MetricRegistry; +import io.appform.ranger.core.model.DataStoreType; +import lombok.val; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for the new and modified methods introduced in MetricRecorder: + *
    + *
  • {@link MetricRecorder#recordStaleDataRetained} – now also records a nodeCount histogram
  • + *
  • {@link MetricRecorder#recordServiceRegistryUpdateNodeCount} – new method
  • + *
  • {@link MetricRecorder#recordNodesFetchedCount} – new method
  • + *
+ */ +class MetricRecorderTest { + + private static final String SERVICE_NAME = "test-service"; + private static final String UPSTREAM_ID = "test-upstream"; + private static final String PACKAGE_PREFIX = "io.appform.ranger"; + + private MetricRegistry metricRegistry; + + @BeforeEach + void setUp() { + metricRegistry = new MetricRegistry(); + MetricRecorder.initialize(metricRegistry); + } + + @AfterEach + void tearDown() { + // Reset the static registry so other test classes are not affected + MetricRecorder.initialize(null); + } + + // ───────────────────────────────────────────────────────────────────────── + // recordStaleDataRetained (modified: size parameter + histogram) + // ───────────────────────────────────────────────────────────────────────── + + @Test + void recordStaleDataRetained_marksGlobalMeter() { + MetricRecorder.recordStaleDataRetained(SERVICE_NAME, DataStoreType.ZK, UPSTREAM_ID, 3); + + val meterName = PACKAGE_PREFIX + ".dataStoreType.ZK.dataSource." + UPSTREAM_ID + ".staleDataRetained"; + val meter = metricRegistry.getMeters().get(meterName); + assertNotNull(meter, "Global staleDataRetained meter should be created"); + assertEquals(1, meter.getCount(), "Global meter should be marked once"); + } + + @Test + void recordStaleDataRetained_marksServiceNameMeter() { + MetricRecorder.recordStaleDataRetained(SERVICE_NAME, DataStoreType.HTTP, UPSTREAM_ID, 5); + + val meterName = PACKAGE_PREFIX + ".dataStoreType.HTTP.dataSource." + UPSTREAM_ID + + ".serviceName." + SERVICE_NAME + ".staleDataRetained"; + val meter = metricRegistry.getMeters().get(meterName); + assertNotNull(meter, "Per-service staleDataRetained meter should be created"); + assertEquals(1, meter.getCount(), "Per-service meter should be marked once"); + } + + @Test + void recordStaleDataRetained_updatesNodeCountHistogram() { + MetricRecorder.recordStaleDataRetained(SERVICE_NAME, DataStoreType.DROVE, UPSTREAM_ID, 7); + + val histName = PACKAGE_PREFIX + ".dataStoreType.DROVE.dataSource." + UPSTREAM_ID + + ".serviceName." + SERVICE_NAME + ".staleDataRetained.nodeCount"; + val histogram = metricRegistry.getHistograms().get(histName); + assertNotNull(histogram, "staleDataRetained nodeCount histogram should be created"); + assertEquals(1, histogram.getCount(), "Histogram should have one update"); + assertEquals(7, histogram.getSnapshot().getMax(), "Histogram max should equal size passed (7)"); + } + + @Test + void recordStaleDataRetained_zeroSize_histogramUpdated() { + MetricRecorder.recordStaleDataRetained(SERVICE_NAME, DataStoreType.ZK, UPSTREAM_ID, 0); + + val histName = PACKAGE_PREFIX + ".dataStoreType.ZK.dataSource." + UPSTREAM_ID + + ".serviceName." + SERVICE_NAME + ".staleDataRetained.nodeCount"; + val histogram = metricRegistry.getHistograms().get(histName); + assertNotNull(histogram); + assertEquals(0, histogram.getSnapshot().getMax(), "Histogram max should be 0 when size is 0"); + } + + @Test + void recordStaleDataRetained_noRegistry_doesNotThrow() { + MetricRecorder.initialize(null); + // Should be a safe no-op + assertDoesNotThrow( + () -> MetricRecorder.recordStaleDataRetained(SERVICE_NAME, DataStoreType.ZK, UPSTREAM_ID, 3), + "recordStaleDataRetained should not throw when registry is not initialized" + ); + } + + @Test + void recordStaleDataRetained_multipleCalls_metersAccumulate() { + MetricRecorder.recordStaleDataRetained(SERVICE_NAME, DataStoreType.ZK, UPSTREAM_ID, 2); + MetricRecorder.recordStaleDataRetained(SERVICE_NAME, DataStoreType.ZK, UPSTREAM_ID, 4); + + val globalMeterName = PACKAGE_PREFIX + ".dataStoreType.ZK.dataSource." + UPSTREAM_ID + ".staleDataRetained"; + val globalMeter = metricRegistry.getMeters().get(globalMeterName); + assertEquals(2, globalMeter.getCount(), "Global meter should accumulate 2 marks"); + + val histName = PACKAGE_PREFIX + ".dataStoreType.ZK.dataSource." + UPSTREAM_ID + + ".serviceName." + SERVICE_NAME + ".staleDataRetained.nodeCount"; + val histogram = metricRegistry.getHistograms().get(histName); + assertEquals(2, histogram.getCount(), "Histogram should have 2 updates"); + assertEquals(4, histogram.getSnapshot().getMax(), "Histogram max should be 4 (latest max)"); + } + + // ───────────────────────────────────────────────────────────────────────── + // recordServiceRegistryUpdateNodeCount (new method) + // ───────────────────────────────────────────────────────────────────────── + + @Test + void recordServiceRegistryUpdateNodeCount_createsHistogramWithCorrectName() { + MetricRecorder.recordServiceRegistryUpdateNodeCount(SERVICE_NAME, DataStoreType.ZK, UPSTREAM_ID, 10); + + val histName = PACKAGE_PREFIX + ".dataStoreType.ZK.dataSource." + UPSTREAM_ID + + ".serviceRegistryUpdate.serviceName." + SERVICE_NAME + ".nodeCount"; + val histogram = metricRegistry.getHistograms().get(histName); + assertNotNull(histogram, "serviceRegistryUpdate nodeCount histogram should be created"); + } + + @Test + void recordServiceRegistryUpdateNodeCount_updatesHistogramWithSize() { + MetricRecorder.recordServiceRegistryUpdateNodeCount(SERVICE_NAME, DataStoreType.HTTP, UPSTREAM_ID, 15); + + val histName = PACKAGE_PREFIX + ".dataStoreType.HTTP.dataSource." + UPSTREAM_ID + + ".serviceRegistryUpdate.serviceName." + SERVICE_NAME + ".nodeCount"; + val histogram = metricRegistry.getHistograms().get(histName); + assertNotNull(histogram); + assertEquals(1, histogram.getCount(), "Histogram should have one update"); + assertEquals(15, histogram.getSnapshot().getMax(), "Histogram max should equal size passed (15)"); + } + + @Test + void recordServiceRegistryUpdateNodeCount_zeroNodes_histogramUpdated() { + MetricRecorder.recordServiceRegistryUpdateNodeCount(SERVICE_NAME, DataStoreType.ZK, UPSTREAM_ID, 0); + + val histName = PACKAGE_PREFIX + ".dataStoreType.ZK.dataSource." + UPSTREAM_ID + + ".serviceRegistryUpdate.serviceName." + SERVICE_NAME + ".nodeCount"; + val histogram = metricRegistry.getHistograms().get(histName); + assertNotNull(histogram); + assertEquals(0, histogram.getSnapshot().getMax(), "Histogram max should be 0 when no valid nodes"); + } + + @Test + void recordServiceRegistryUpdateNodeCount_noRegistry_doesNotThrow() { + MetricRecorder.initialize(null); + assertDoesNotThrow( + () -> MetricRecorder.recordServiceRegistryUpdateNodeCount(SERVICE_NAME, DataStoreType.ZK, UPSTREAM_ID, 5), + "recordServiceRegistryUpdateNodeCount should not throw when registry is not initialized" + ); + } + + @Test + void recordServiceRegistryUpdateNodeCount_multipleCalls_histogramAccumulates() { + MetricRecorder.recordServiceRegistryUpdateNodeCount(SERVICE_NAME, DataStoreType.DROVE, UPSTREAM_ID, 3); + MetricRecorder.recordServiceRegistryUpdateNodeCount(SERVICE_NAME, DataStoreType.DROVE, UPSTREAM_ID, 8); + + val histName = PACKAGE_PREFIX + ".dataStoreType.DROVE.dataSource." + UPSTREAM_ID + + ".serviceRegistryUpdate.serviceName." + SERVICE_NAME + ".nodeCount"; + val histogram = metricRegistry.getHistograms().get(histName); + assertEquals(2, histogram.getCount(), "Histogram should have 2 updates"); + assertEquals(8, histogram.getSnapshot().getMax(), "Histogram max should be 8"); + } + + @Test + void recordServiceRegistryUpdateNodeCount_differentDataStoreTypes_separateHistograms() { + MetricRecorder.recordServiceRegistryUpdateNodeCount(SERVICE_NAME, DataStoreType.ZK, UPSTREAM_ID, 5); + MetricRecorder.recordServiceRegistryUpdateNodeCount(SERVICE_NAME, DataStoreType.HTTP, UPSTREAM_ID, 10); + + val zkHistName = PACKAGE_PREFIX + ".dataStoreType.ZK.dataSource." + UPSTREAM_ID + + ".serviceRegistryUpdate.serviceName." + SERVICE_NAME + ".nodeCount"; + val httpHistName = PACKAGE_PREFIX + ".dataStoreType.HTTP.dataSource." + UPSTREAM_ID + + ".serviceRegistryUpdate.serviceName." + SERVICE_NAME + ".nodeCount"; + + assertNotNull(metricRegistry.getHistograms().get(zkHistName), "ZK histogram should exist"); + assertNotNull(metricRegistry.getHistograms().get(httpHistName), "HTTP histogram should exist"); + assertEquals(5, metricRegistry.getHistograms().get(zkHistName).getSnapshot().getMax()); + assertEquals(10, metricRegistry.getHistograms().get(httpHistName).getSnapshot().getMax()); + } + + // ───────────────────────────────────────────────────────────────────────── + // recordNodesFetchedCount (new method) + // ───────────────────────────────────────────────────────────────────────── + + @Test + void recordNodesFetchedCount_createsHistogramWithCorrectName() { + MetricRecorder.recordNodesFetchedCount(SERVICE_NAME, DataStoreType.ZK, UPSTREAM_ID, 6); + + val histName = PACKAGE_PREFIX + ".dataStoreType.ZK.dataSource." + UPSTREAM_ID + + ".listNodes.serviceName." + SERVICE_NAME + ".nodeCount"; + val histogram = metricRegistry.getHistograms().get(histName); + assertNotNull(histogram, "listNodes nodeCount histogram should be created"); + } + + @Test + void recordNodesFetchedCount_updatesHistogramWithSize() { + MetricRecorder.recordNodesFetchedCount(SERVICE_NAME, DataStoreType.HTTP, UPSTREAM_ID, 20); + + val histName = PACKAGE_PREFIX + ".dataStoreType.HTTP.dataSource." + UPSTREAM_ID + + ".listNodes.serviceName." + SERVICE_NAME + ".nodeCount"; + val histogram = metricRegistry.getHistograms().get(histName); + assertNotNull(histogram); + assertEquals(1, histogram.getCount(), "Histogram should have one update"); + assertEquals(20, histogram.getSnapshot().getMax(), "Histogram max should equal size passed (20)"); + } + + @Test + void recordNodesFetchedCount_zeroNodes_histogramUpdated() { + MetricRecorder.recordNodesFetchedCount(SERVICE_NAME, DataStoreType.ZK, UPSTREAM_ID, 0); + + val histName = PACKAGE_PREFIX + ".dataStoreType.ZK.dataSource." + UPSTREAM_ID + + ".listNodes.serviceName." + SERVICE_NAME + ".nodeCount"; + val histogram = metricRegistry.getHistograms().get(histName); + assertNotNull(histogram); + assertEquals(0, histogram.getSnapshot().getMax(), "Histogram max should be 0 when no nodes fetched"); + } + + @Test + void recordNodesFetchedCount_noRegistry_doesNotThrow() { + MetricRecorder.initialize(null); + assertDoesNotThrow( + () -> MetricRecorder.recordNodesFetchedCount(SERVICE_NAME, DataStoreType.ZK, UPSTREAM_ID, 5), + "recordNodesFetchedCount should not throw when registry is not initialized" + ); + } + + @Test + void recordNodesFetchedCount_multipleCalls_histogramAccumulates() { + MetricRecorder.recordNodesFetchedCount(SERVICE_NAME, DataStoreType.ZK, UPSTREAM_ID, 4); + MetricRecorder.recordNodesFetchedCount(SERVICE_NAME, DataStoreType.ZK, UPSTREAM_ID, 9); + + val histName = PACKAGE_PREFIX + ".dataStoreType.ZK.dataSource." + UPSTREAM_ID + + ".listNodes.serviceName." + SERVICE_NAME + ".nodeCount"; + val histogram = metricRegistry.getHistograms().get(histName); + assertEquals(2, histogram.getCount(), "Histogram should have 2 updates"); + assertEquals(9, histogram.getSnapshot().getMax(), "Histogram max should be 9"); + } + + @Test + void recordNodesFetchedCount_differentUpstreamIds_separateHistograms() { + MetricRecorder.recordNodesFetchedCount(SERVICE_NAME, DataStoreType.ZK, "upstream-a", 3); + MetricRecorder.recordNodesFetchedCount(SERVICE_NAME, DataStoreType.ZK, "upstream-b", 7); + + val histNameA = PACKAGE_PREFIX + ".dataStoreType.ZK.dataSource.upstream-a" + + ".listNodes.serviceName." + SERVICE_NAME + ".nodeCount"; + val histNameB = PACKAGE_PREFIX + ".dataStoreType.ZK.dataSource.upstream-b" + + ".listNodes.serviceName." + SERVICE_NAME + ".nodeCount"; + + assertNotNull(metricRegistry.getHistograms().get(histNameA), "Histogram for upstream-a should exist"); + assertNotNull(metricRegistry.getHistograms().get(histNameB), "Histogram for upstream-b should exist"); + assertEquals(3, metricRegistry.getHistograms().get(histNameA).getSnapshot().getMax()); + assertEquals(7, metricRegistry.getHistograms().get(histNameB).getSnapshot().getMax()); + } +} diff --git a/ranger-discovery-bundle/src/main/java/io/appform/ranger/discovery/bundle/Constants.java b/ranger-discovery-bundle/src/main/java/io/appform/ranger/discovery/bundle/Constants.java index 018d15b8..1097eba2 100644 --- a/ranger-discovery-bundle/src/main/java/io/appform/ranger/discovery/bundle/Constants.java +++ b/ranger-discovery-bundle/src/main/java/io/appform/ranger/discovery/bundle/Constants.java @@ -25,8 +25,6 @@ */ @UtilityClass public class Constants { - public static final String DEFAULT_NAMESPACE = "default"; - public static final String DEFAULT_HOST = "__DEFAULT_SERVICE_HOST"; public static final int DEFAULT_PORT = -1; public static final int DEFAULT_DW_CHECK_INTERVAL = 15; diff --git a/ranger-discovery-bundle/src/main/java/io/appform/ranger/discovery/bundle/ServiceDiscoveryBundle.java b/ranger-discovery-bundle/src/main/java/io/appform/ranger/discovery/bundle/ServiceDiscoveryBundle.java index f0c220d8..9570da63 100644 --- a/ranger-discovery-bundle/src/main/java/io/appform/ranger/discovery/bundle/ServiceDiscoveryBundle.java +++ b/ranger-discovery-bundle/src/main/java/io/appform/ranger/discovery/bundle/ServiceDiscoveryBundle.java @@ -38,10 +38,14 @@ import io.appform.ranger.core.model.ServiceNodeSelector; import io.appform.ranger.core.model.ShardSelector; import io.appform.ranger.core.serviceprovider.ServiceProvider; +import io.appform.ranger.core.util.MetricRecorder; import io.appform.ranger.discovery.core.ServiceDiscoveryConfiguration; import io.appform.ranger.discovery.core.healthchecks.InitialDelayChecker; import io.appform.ranger.discovery.core.healthchecks.InternalHealthChecker; import io.appform.ranger.discovery.core.healthchecks.RotationCheck; +import io.appform.ranger.id.IdGenerator; +import io.appform.ranger.id.NodeIdManager; +import io.appform.ranger.id.constraints.IdValidationConstraint; import io.appform.ranger.discovery.core.monitors.DropwizardHealthMonitor; import io.appform.ranger.discovery.bundle.monitors.DropwizardServerStartupCheck; import io.appform.ranger.discovery.core.resolvers.DefaultNodeInfoResolver; @@ -54,9 +58,6 @@ import io.appform.ranger.discovery.core.rotationstatus.RotationStatus; import io.appform.ranger.discovery.core.selectors.HierarchicalEnvironmentAwareShardSelector; import io.appform.ranger.discovery.core.util.ConfigurationUtils; -import io.appform.ranger.id.IdGenerator; -import io.appform.ranger.id.NodeIdManager; -import io.appform.ranger.id.constraints.IdValidationConstraint; import io.appform.ranger.zookeeper.ServiceProviderBuilders; import io.appform.ranger.zookeeper.serde.ZkNodeDataSerializer; import io.dropwizard.Configuration; @@ -84,6 +85,7 @@ import java.util.stream.Collectors; import static io.appform.ranger.discovery.bundle.Constants.LOCAL_ADDRESSES; +import static io.appform.ranger.discovery.core.Constants.DEFAULT_DATA_SINK_ID; import static java.util.Objects.requireNonNull; @@ -129,7 +131,7 @@ public void initialize(Bootstrap bootstrap) { public void run(T configuration, Environment environment) throws Exception { val portSchemeResolver = createPortSchemeResolver(); - requireNonNull(portSchemeResolver, "Port scheme resolver can't be null"); + Preconditions.checkNotNull(portSchemeResolver, "Port scheme resolver can't be null"); val portScheme = portSchemeResolver.resolve(configuration); serviceDiscoveryConfiguration = getRangerConfiguration(configuration); val objectMapper = environment.getObjectMapper(); @@ -152,6 +154,9 @@ public void run(T configuration, portScheme); serviceDiscoveryClient = buildDiscoveryClient(environment, namespace, serviceName, initialCriteria, useInitialCriteria, shardSelector, nodeSelector); + if (serviceDiscoveryConfiguration.isMetricsEnabled()){ + MetricRecorder.initialize(environment.metrics()); + } environment.lifecycle() .manage(new ServiceDiscoveryManager(serviceName)); environment.jersey() @@ -254,6 +259,7 @@ private RangerClient> buildDiscove ShardSelector> shardSelector, final ServiceNodeSelector nodeSelector) { return SimpleRangerZKClient.builder() + .upstreamId(DEFAULT_DATA_SINK_ID) .curatorFramework(curator) .namespace(namespace) .serviceName(serviceName) @@ -299,6 +305,7 @@ private ServiceProvider> buildService .setNext(new RoutingWeightHandler<>(getWeightSupplier().get())) .setNext(new StartupTimeHandler<>()); val serviceProviderBuilder = ServiceProviderBuilders.shardedServiceProviderBuilder() + .withUpstreamId(DEFAULT_DATA_SINK_ID) .withCuratorFramework(curator) .withNamespace(namespace) .withServiceName(serviceName) diff --git a/ranger-discovery-bundle/src/test/java/io/appform/ranger/discovery/bundle/ServiceDiscoveryBundleCustomHostPortTest.java b/ranger-discovery-bundle/src/test/java/io/appform/ranger/discovery/bundle/ServiceDiscoveryBundleCustomHostPortTest.java index 754cc7ef..c942313f 100644 --- a/ranger-discovery-bundle/src/test/java/io/appform/ranger/discovery/bundle/ServiceDiscoveryBundleCustomHostPortTest.java +++ b/ranger-discovery-bundle/src/test/java/io/appform/ranger/discovery/bundle/ServiceDiscoveryBundleCustomHostPortTest.java @@ -20,6 +20,7 @@ import com.codahale.metrics.health.HealthCheckRegistry; import com.fasterxml.jackson.databind.ObjectMapper; import io.appform.ranger.core.healthcheck.HealthcheckStatus; +import io.appform.ranger.discovery.core.Constants; import io.appform.ranger.discovery.core.ServiceDiscoveryConfiguration; import io.dropwizard.Configuration; import io.dropwizard.jersey.DropwizardResourceConfig; @@ -97,6 +98,7 @@ void setup() throws Exception { when(environment.lifecycle()).thenReturn(lifecycleEnvironment); when(environment.healthChecks()).thenReturn(healthChecks); when(environment.getObjectMapper()).thenReturn(new ObjectMapper()); + when(environment.metrics()).thenReturn(metricRegistry); AdminEnvironment adminEnvironment = mock(AdminEnvironment.class); doNothing().when(adminEnvironment) .addTask(any()); diff --git a/ranger-discovery-bundle/src/test/java/io/appform/ranger/discovery/bundle/ServiceDiscoveryBundleDwMonitorTest.java b/ranger-discovery-bundle/src/test/java/io/appform/ranger/discovery/bundle/ServiceDiscoveryBundleDwMonitorTest.java index 4c21f3be..7e377b97 100644 --- a/ranger-discovery-bundle/src/test/java/io/appform/ranger/discovery/bundle/ServiceDiscoveryBundleDwMonitorTest.java +++ b/ranger-discovery-bundle/src/test/java/io/appform/ranger/discovery/bundle/ServiceDiscoveryBundleDwMonitorTest.java @@ -21,6 +21,7 @@ import com.codahale.metrics.health.HealthCheckRegistry; import com.fasterxml.jackson.databind.ObjectMapper; import io.appform.ranger.core.healthcheck.HealthcheckStatus; +import io.appform.ranger.discovery.core.Constants; import io.appform.ranger.discovery.core.ServiceDiscoveryConfiguration; import io.dropwizard.Configuration; import io.dropwizard.jersey.DropwizardResourceConfig; @@ -112,6 +113,7 @@ protected Result check() { when(environment.lifecycle()).thenReturn(lifecycleEnvironment); when(environment.healthChecks()).thenReturn(healthChecks); when(environment.getObjectMapper()).thenReturn(new ObjectMapper()); + when(environment.metrics()).thenReturn(metricRegistry); AdminEnvironment adminEnvironment = mock(AdminEnvironment.class); doNothing().when(adminEnvironment) .addTask(any()); diff --git a/ranger-discovery-bundle/src/test/java/io/appform/ranger/discovery/bundle/ServiceDiscoveryBundleDwStalenessMonitorTest.java b/ranger-discovery-bundle/src/test/java/io/appform/ranger/discovery/bundle/ServiceDiscoveryBundleDwStalenessMonitorTest.java index beb7afda..4ce15359 100644 --- a/ranger-discovery-bundle/src/test/java/io/appform/ranger/discovery/bundle/ServiceDiscoveryBundleDwStalenessMonitorTest.java +++ b/ranger-discovery-bundle/src/test/java/io/appform/ranger/discovery/bundle/ServiceDiscoveryBundleDwStalenessMonitorTest.java @@ -21,6 +21,7 @@ import com.codahale.metrics.health.HealthCheckRegistry; import com.fasterxml.jackson.databind.ObjectMapper; import io.appform.ranger.core.healthcheck.HealthcheckStatus; +import io.appform.ranger.discovery.core.Constants; import io.appform.ranger.discovery.core.ServiceDiscoveryConfiguration; import io.dropwizard.Configuration; import io.dropwizard.jersey.DropwizardResourceConfig; @@ -111,6 +112,7 @@ protected Result check() { when(environment.lifecycle()).thenReturn(lifecycleEnvironment); when(environment.healthChecks()).thenReturn(healthChecks); when(environment.getObjectMapper()).thenReturn(new ObjectMapper()); + when(environment.metrics()).thenReturn(metricRegistry); AdminEnvironment adminEnvironment = mock(AdminEnvironment.class); doNothing().when(adminEnvironment) .addTask(any()); diff --git a/ranger-discovery-bundle/src/test/java/io/appform/ranger/discovery/bundle/ServiceDiscoveryBundleHierarchicalSelectorTest.java b/ranger-discovery-bundle/src/test/java/io/appform/ranger/discovery/bundle/ServiceDiscoveryBundleHierarchicalSelectorTest.java index 14c58c56..c5fb7a4e 100644 --- a/ranger-discovery-bundle/src/test/java/io/appform/ranger/discovery/bundle/ServiceDiscoveryBundleHierarchicalSelectorTest.java +++ b/ranger-discovery-bundle/src/test/java/io/appform/ranger/discovery/bundle/ServiceDiscoveryBundleHierarchicalSelectorTest.java @@ -21,6 +21,7 @@ import com.codahale.metrics.health.HealthCheckRegistry; import com.fasterxml.jackson.databind.ObjectMapper; import io.appform.ranger.core.healthcheck.HealthcheckStatus; +import io.appform.ranger.discovery.core.Constants; import io.appform.ranger.discovery.core.ServiceDiscoveryConfiguration; import io.appform.ranger.discovery.core.util.ConfigurationUtils; import io.dropwizard.Configuration; @@ -89,6 +90,7 @@ void setup() throws Exception { when(environment.lifecycle()).thenReturn(lifecycleEnvironment); when(environment.healthChecks()).thenReturn(healthChecks); when(environment.getObjectMapper()).thenReturn(new ObjectMapper()); + when(environment.metrics()).thenReturn(metricRegistry); AdminEnvironment adminEnvironment = mock(AdminEnvironment.class); doNothing().when(adminEnvironment) .addTask(any()); diff --git a/ranger-discovery-bundle/src/test/java/io/appform/ranger/discovery/bundle/ServiceDiscoveryBundleLocalHostPortTest.java b/ranger-discovery-bundle/src/test/java/io/appform/ranger/discovery/bundle/ServiceDiscoveryBundleLocalHostPortTest.java index 3ecdfec2..b2ad909f 100644 --- a/ranger-discovery-bundle/src/test/java/io/appform/ranger/discovery/bundle/ServiceDiscoveryBundleLocalHostPortTest.java +++ b/ranger-discovery-bundle/src/test/java/io/appform/ranger/discovery/bundle/ServiceDiscoveryBundleLocalHostPortTest.java @@ -92,6 +92,7 @@ void shouldFailLocalhostPublish() { when(environment.lifecycle()).thenReturn(lifecycleEnvironment); when(environment.healthChecks()).thenReturn(healthChecks); when(environment.getObjectMapper()).thenReturn(new ObjectMapper()); + when(environment.metrics()).thenReturn(metricRegistry); AdminEnvironment adminEnvironment = mock(AdminEnvironment.class); doNothing().when(adminEnvironment) .addTask(any()); @@ -120,6 +121,7 @@ void shouldThrowExceptionForInvalidZkHost() { when(environment.lifecycle()).thenReturn(lifecycleEnvironment); when(environment.healthChecks()).thenReturn(healthChecks); when(environment.getObjectMapper()).thenReturn(new ObjectMapper()); + when(environment.metrics()).thenReturn(metricRegistry); AdminEnvironment adminEnvironment = mock(AdminEnvironment.class); doNothing().when(adminEnvironment) .addTask(any()); @@ -156,6 +158,7 @@ void testPublishWithEmptyZkHost() throws UnknownHostException { when(environment.lifecycle()).thenReturn(lifecycleEnvironment); when(environment.healthChecks()).thenReturn(healthChecks); when(environment.getObjectMapper()).thenReturn(new ObjectMapper()); + when(environment.metrics()).thenReturn(metricRegistry); AdminEnvironment adminEnvironment = mock(AdminEnvironment.class); doNothing().when(adminEnvironment) .addTask(any()); @@ -192,6 +195,7 @@ void testPublishWithNullZkHost() throws UnknownHostException { when(environment.lifecycle()).thenReturn(lifecycleEnvironment); when(environment.healthChecks()).thenReturn(healthChecks); when(environment.getObjectMapper()).thenReturn(new ObjectMapper()); + when(environment.metrics()).thenReturn(metricRegistry); AdminEnvironment adminEnvironment = mock(AdminEnvironment.class); doNothing().when(adminEnvironment) .addTask(any()); @@ -227,6 +231,7 @@ void shouldPublishingToLocalZk() { when(environment.lifecycle()).thenReturn(lifecycleEnvironment); when(environment.healthChecks()).thenReturn(healthChecks); when(environment.getObjectMapper()).thenReturn(new ObjectMapper()); + when(environment.metrics()).thenReturn(metricRegistry); AdminEnvironment adminEnvironment = mock(AdminEnvironment.class); doNothing().when(adminEnvironment) .addTask(any()); @@ -257,6 +262,7 @@ void shouldPublishToRemoteZk() { when(environment.lifecycle()).thenReturn(lifecycleEnvironment); when(environment.healthChecks()).thenReturn(healthChecks); when(environment.getObjectMapper()).thenReturn(new ObjectMapper()); + when(environment.metrics()).thenReturn(metricRegistry); AdminEnvironment adminEnvironment = mock(AdminEnvironment.class); doNothing().when(adminEnvironment) .addTask(any()); diff --git a/ranger-discovery-bundle/src/test/java/io/appform/ranger/discovery/bundle/ServiceDiscoveryBundleRotationTest.java b/ranger-discovery-bundle/src/test/java/io/appform/ranger/discovery/bundle/ServiceDiscoveryBundleRotationTest.java index 1d3a8663..eb34c944 100644 --- a/ranger-discovery-bundle/src/test/java/io/appform/ranger/discovery/bundle/ServiceDiscoveryBundleRotationTest.java +++ b/ranger-discovery-bundle/src/test/java/io/appform/ranger/discovery/bundle/ServiceDiscoveryBundleRotationTest.java @@ -22,6 +22,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import io.appform.ranger.discovery.bundle.rotationstatus.BIRTask; import io.appform.ranger.discovery.bundle.rotationstatus.OORTask; +import io.appform.ranger.discovery.core.Constants; import io.appform.ranger.discovery.core.ServiceDiscoveryConfiguration; import io.appform.ranger.discovery.core.rotationstatus.RotationStatus; import io.appform.ranger.discovery.core.util.ConfigurationUtils; @@ -92,6 +93,7 @@ void setup() throws Exception { when(environment.lifecycle()).thenReturn(lifecycleEnvironment); when(environment.healthChecks()).thenReturn(healthChecks); when(environment.getObjectMapper()).thenReturn(new ObjectMapper()); + when(environment.metrics()).thenReturn(metricRegistry); AdminEnvironment adminEnvironment = mock(AdminEnvironment.class); doNothing().when(adminEnvironment) .addTask(any()); diff --git a/ranger-discovery-bundle/src/test/java/io/appform/ranger/discovery/bundle/ServiceDiscoveryBundleTest.java b/ranger-discovery-bundle/src/test/java/io/appform/ranger/discovery/bundle/ServiceDiscoveryBundleTest.java index 474e47a6..edabd5c6 100644 --- a/ranger-discovery-bundle/src/test/java/io/appform/ranger/discovery/bundle/ServiceDiscoveryBundleTest.java +++ b/ranger-discovery-bundle/src/test/java/io/appform/ranger/discovery/bundle/ServiceDiscoveryBundleTest.java @@ -21,6 +21,7 @@ import com.codahale.metrics.health.HealthCheckRegistry; import com.fasterxml.jackson.databind.ObjectMapper; import io.appform.ranger.core.healthcheck.HealthcheckStatus; +import io.appform.ranger.discovery.core.Constants; import io.appform.ranger.discovery.core.ServiceDiscoveryConfiguration; import io.appform.ranger.discovery.core.resolvers.DefaultNodeInfoResolver; import io.appform.ranger.discovery.core.resolvers.NodeInfoResolver; @@ -95,6 +96,7 @@ void setup() throws Exception { when(environment.lifecycle()).thenReturn(lifecycleEnvironment); when(environment.healthChecks()).thenReturn(healthChecks); when(environment.getObjectMapper()).thenReturn(new ObjectMapper()); + when(environment.metrics()).thenReturn(metricRegistry); AdminEnvironment adminEnvironment = mock(AdminEnvironment.class); doNothing().when(adminEnvironment) .addTask(any()); diff --git a/ranger-discovery-bundle/src/test/java/io/appform/ranger/discovery/bundle/resolvers/DefaultNodeInfoResolverTest.java b/ranger-discovery-bundle/src/test/java/io/appform/ranger/discovery/bundle/resolvers/DefaultNodeInfoResolverTest.java index cac313c4..fdf07928 100644 --- a/ranger-discovery-bundle/src/test/java/io/appform/ranger/discovery/bundle/resolvers/DefaultNodeInfoResolverTest.java +++ b/ranger-discovery-bundle/src/test/java/io/appform/ranger/discovery/bundle/resolvers/DefaultNodeInfoResolverTest.java @@ -15,6 +15,7 @@ */ package io.appform.ranger.discovery.bundle.resolvers; +import io.appform.ranger.discovery.core.Constants; import io.appform.ranger.discovery.core.ServiceDiscoveryConfiguration; import io.appform.ranger.discovery.core.resolvers.DefaultNodeInfoResolver; import lombok.val; diff --git a/ranger-discovery-bundle/src/test/java/io/appform/ranger/discovery/bundle/resolvers/DefaultPortSchemeResolverTest.java b/ranger-discovery-bundle/src/test/java/io/appform/ranger/discovery/bundle/resolvers/DefaultPortSchemeResolverTest.java index 8e79e40c..1a678867 100644 --- a/ranger-discovery-bundle/src/test/java/io/appform/ranger/discovery/bundle/resolvers/DefaultPortSchemeResolverTest.java +++ b/ranger-discovery-bundle/src/test/java/io/appform/ranger/discovery/bundle/resolvers/DefaultPortSchemeResolverTest.java @@ -25,6 +25,8 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +import java.util.List; + import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -34,7 +36,7 @@ class DefaultPortSchemeResolverTest { void testPortSchemeDefaultServerFactory() { val server = mock(DefaultServerFactory.class); val connectorFactory = mock(HttpConnectorFactory.class); - when(server.getApplicationConnectors()).thenReturn(Lists.newArrayList(connectorFactory)); + when(server.getApplicationConnectors()).thenReturn(List.of(connectorFactory)); val resolver = new DefaultPortSchemeResolver<>(); val configuration = mock(Configuration.class); when(configuration.getServerFactory()).thenReturn(server); diff --git a/ranger-discovery-core/src/main/java/io/appform/ranger/discovery/core/Constants.java b/ranger-discovery-core/src/main/java/io/appform/ranger/discovery/core/Constants.java index 0ea6e15a..2a57a6b6 100644 --- a/ranger-discovery-core/src/main/java/io/appform/ranger/discovery/core/Constants.java +++ b/ranger-discovery-core/src/main/java/io/appform/ranger/discovery/core/Constants.java @@ -24,6 +24,8 @@ @UtilityClass public class Constants { public static final String DEFAULT_NAMESPACE = "default"; + public static final String DEFAULT_DATA_SINK_ID = "zkServiceDiscovery"; + public static final String DEFAULT_HOST = "__DEFAULT_SERVICE_HOST"; public static final int DEFAULT_PORT = -1; public static final int DEFAULT_DW_CHECK_INTERVAL = 15; diff --git a/ranger-discovery-core/src/main/java/io/appform/ranger/discovery/core/ServiceDiscoveryConfiguration.java b/ranger-discovery-core/src/main/java/io/appform/ranger/discovery/core/ServiceDiscoveryConfiguration.java index 2abf66cd..bd25c8fe 100644 --- a/ranger-discovery-core/src/main/java/io/appform/ranger/discovery/core/ServiceDiscoveryConfiguration.java +++ b/ranger-discovery-core/src/main/java/io/appform/ranger/discovery/core/ServiceDiscoveryConfiguration.java @@ -17,6 +17,7 @@ package io.appform.ranger.discovery.core; import com.google.common.base.Strings; +import javax.validation.constraints.NotBlank; import lombok.Builder; import lombok.Data; import lombok.EqualsAndHashCode; @@ -38,7 +39,6 @@ @NoArgsConstructor public class ServiceDiscoveryConfiguration { - @NotNull @NotEmpty private String namespace = Constants.DEFAULT_NAMESPACE; @@ -80,6 +80,8 @@ public class ServiceDiscoveryConfiguration { private Set tags; + private boolean metricsEnabled = true; + @Builder public ServiceDiscoveryConfiguration(String namespace, String environment, @@ -93,7 +95,8 @@ public ServiceDiscoveryConfiguration(String namespace, boolean initialRotationStatus, int dropwizardCheckInterval, int dropwizardCheckStaleness, - Set tags) { + Set tags, + boolean metricsEnabled) { this.namespace = Strings.isNullOrEmpty(namespace) ? Constants.DEFAULT_NAMESPACE : namespace; @@ -117,5 +120,6 @@ public ServiceDiscoveryConfiguration(String namespace, : dropwizardCheckInterval; this.dropwizardCheckStaleness = dropwizardCheckStaleness; this.tags = tags; + this.metricsEnabled = metricsEnabled; } } diff --git a/ranger-discovery-dw5-bundle/src/main/java/io/appform/ranger/discovery/bundle/ServiceDiscoveryBundle.java b/ranger-discovery-dw5-bundle/src/main/java/io/appform/ranger/discovery/bundle/ServiceDiscoveryBundle.java index 06d505d5..9861e407 100644 --- a/ranger-discovery-dw5-bundle/src/main/java/io/appform/ranger/discovery/bundle/ServiceDiscoveryBundle.java +++ b/ranger-discovery-dw5-bundle/src/main/java/io/appform/ranger/discovery/bundle/ServiceDiscoveryBundle.java @@ -38,6 +38,7 @@ import io.appform.ranger.core.model.ServiceNodeSelector; import io.appform.ranger.core.model.ShardSelector; import io.appform.ranger.core.serviceprovider.ServiceProvider; +import io.appform.ranger.core.util.MetricRecorder; import io.appform.ranger.discovery.core.ServiceDiscoveryConfiguration; import io.appform.ranger.discovery.core.healthchecks.InitialDelayChecker; import io.appform.ranger.discovery.core.healthchecks.InternalHealthChecker; @@ -84,6 +85,7 @@ import java.util.stream.Collectors; import static io.appform.ranger.discovery.bundle.Constants.LOCAL_ADDRESSES; +import static io.appform.ranger.discovery.core.Constants.DEFAULT_DATA_SINK_ID; /** @@ -151,6 +153,9 @@ public void run(T configuration, portScheme); serviceDiscoveryClient = buildDiscoveryClient(environment, namespace, serviceName, initialCriteria, useInitialCriteria, shardSelector, nodeSelector); + if (serviceDiscoveryConfiguration.isMetricsEnabled()){ + MetricRecorder.initialize(environment.metrics()); + } environment.lifecycle() .manage(new ServiceDiscoveryManager(serviceName)); environment.jersey() @@ -253,6 +258,7 @@ private RangerClient> buildDiscove ShardSelector> shardSelector, final ServiceNodeSelector nodeSelector) { return SimpleRangerZKClient.builder() + .upstreamId(DEFAULT_DATA_SINK_ID) .curatorFramework(curator) .namespace(namespace) .serviceName(serviceName) @@ -298,6 +304,7 @@ private ServiceProvider> buildService .setNext(new RoutingWeightHandler<>(getWeightSupplier().get())) .setNext(new StartupTimeHandler<>()); val serviceProviderBuilder = ServiceProviderBuilders.shardedServiceProviderBuilder() + .withUpstreamId(DEFAULT_DATA_SINK_ID) .withCuratorFramework(curator) .withNamespace(namespace) .withServiceName(serviceName) diff --git a/ranger-discovery-dw5-bundle/src/test/java/io/appform/ranger/discovery/bundle/ServiceDiscoveryBundleCustomHostPortTest.java b/ranger-discovery-dw5-bundle/src/test/java/io/appform/ranger/discovery/bundle/ServiceDiscoveryBundleCustomHostPortTest.java index 72d2ae4a..35c48310 100644 --- a/ranger-discovery-dw5-bundle/src/test/java/io/appform/ranger/discovery/bundle/ServiceDiscoveryBundleCustomHostPortTest.java +++ b/ranger-discovery-dw5-bundle/src/test/java/io/appform/ranger/discovery/bundle/ServiceDiscoveryBundleCustomHostPortTest.java @@ -20,6 +20,7 @@ import com.codahale.metrics.health.HealthCheckRegistry; import com.fasterxml.jackson.databind.ObjectMapper; import io.appform.ranger.core.healthcheck.HealthcheckStatus; +import io.appform.ranger.discovery.core.Constants; import io.appform.ranger.discovery.core.ServiceDiscoveryConfiguration; import io.dropwizard.core.Configuration; import io.dropwizard.core.server.DefaultServerFactory; @@ -97,6 +98,7 @@ void setup() throws Exception { when(environment.lifecycle()).thenReturn(lifecycleEnvironment); when(environment.healthChecks()).thenReturn(healthChecks); when(environment.getObjectMapper()).thenReturn(new ObjectMapper()); + when(environment.metrics()).thenReturn(metricRegistry); AdminEnvironment adminEnvironment = mock(AdminEnvironment.class); doNothing().when(adminEnvironment) .addTask(any()); diff --git a/ranger-discovery-dw5-bundle/src/test/java/io/appform/ranger/discovery/bundle/ServiceDiscoveryBundleDwMonitorTest.java b/ranger-discovery-dw5-bundle/src/test/java/io/appform/ranger/discovery/bundle/ServiceDiscoveryBundleDwMonitorTest.java index 71b7ace8..2cb13f4a 100644 --- a/ranger-discovery-dw5-bundle/src/test/java/io/appform/ranger/discovery/bundle/ServiceDiscoveryBundleDwMonitorTest.java +++ b/ranger-discovery-dw5-bundle/src/test/java/io/appform/ranger/discovery/bundle/ServiceDiscoveryBundleDwMonitorTest.java @@ -21,6 +21,7 @@ import com.codahale.metrics.health.HealthCheckRegistry; import com.fasterxml.jackson.databind.ObjectMapper; import io.appform.ranger.core.healthcheck.HealthcheckStatus; +import io.appform.ranger.discovery.core.Constants; import io.appform.ranger.discovery.core.ServiceDiscoveryConfiguration; import io.dropwizard.core.Configuration; import io.dropwizard.core.server.DefaultServerFactory; @@ -112,6 +113,7 @@ protected Result check() { when(environment.lifecycle()).thenReturn(lifecycleEnvironment); when(environment.healthChecks()).thenReturn(healthChecks); when(environment.getObjectMapper()).thenReturn(new ObjectMapper()); + when(environment.metrics()).thenReturn(metricRegistry); AdminEnvironment adminEnvironment = mock(AdminEnvironment.class); doNothing().when(adminEnvironment) .addTask(any()); diff --git a/ranger-discovery-dw5-bundle/src/test/java/io/appform/ranger/discovery/bundle/ServiceDiscoveryBundleDwStalenessMonitorTest.java b/ranger-discovery-dw5-bundle/src/test/java/io/appform/ranger/discovery/bundle/ServiceDiscoveryBundleDwStalenessMonitorTest.java index 23c289ac..823455e8 100644 --- a/ranger-discovery-dw5-bundle/src/test/java/io/appform/ranger/discovery/bundle/ServiceDiscoveryBundleDwStalenessMonitorTest.java +++ b/ranger-discovery-dw5-bundle/src/test/java/io/appform/ranger/discovery/bundle/ServiceDiscoveryBundleDwStalenessMonitorTest.java @@ -21,6 +21,7 @@ import com.codahale.metrics.health.HealthCheckRegistry; import com.fasterxml.jackson.databind.ObjectMapper; import io.appform.ranger.core.healthcheck.HealthcheckStatus; +import io.appform.ranger.discovery.core.Constants; import io.appform.ranger.discovery.core.ServiceDiscoveryConfiguration; import io.dropwizard.jersey.DropwizardResourceConfig; import io.dropwizard.jersey.setup.JerseyEnvironment; @@ -111,6 +112,7 @@ protected Result check() { when(environment.lifecycle()).thenReturn(lifecycleEnvironment); when(environment.healthChecks()).thenReturn(healthChecks); when(environment.getObjectMapper()).thenReturn(new ObjectMapper()); + when(environment.metrics()).thenReturn(metricRegistry); AdminEnvironment adminEnvironment = mock(AdminEnvironment.class); doNothing().when(adminEnvironment) .addTask(any()); diff --git a/ranger-discovery-dw5-bundle/src/test/java/io/appform/ranger/discovery/bundle/ServiceDiscoveryBundleHierarchicalSelectorTest.java b/ranger-discovery-dw5-bundle/src/test/java/io/appform/ranger/discovery/bundle/ServiceDiscoveryBundleHierarchicalSelectorTest.java index 0ef3ced6..4f0f1cb2 100644 --- a/ranger-discovery-dw5-bundle/src/test/java/io/appform/ranger/discovery/bundle/ServiceDiscoveryBundleHierarchicalSelectorTest.java +++ b/ranger-discovery-dw5-bundle/src/test/java/io/appform/ranger/discovery/bundle/ServiceDiscoveryBundleHierarchicalSelectorTest.java @@ -21,6 +21,7 @@ import com.codahale.metrics.health.HealthCheckRegistry; import com.fasterxml.jackson.databind.ObjectMapper; import io.appform.ranger.core.healthcheck.HealthcheckStatus; +import io.appform.ranger.discovery.core.Constants; import io.appform.ranger.discovery.core.ServiceDiscoveryConfiguration; import io.appform.ranger.discovery.core.util.ConfigurationUtils; import io.dropwizard.jersey.DropwizardResourceConfig; @@ -89,6 +90,7 @@ void setup() throws Exception { when(environment.lifecycle()).thenReturn(lifecycleEnvironment); when(environment.healthChecks()).thenReturn(healthChecks); when(environment.getObjectMapper()).thenReturn(new ObjectMapper()); + when(environment.metrics()).thenReturn(metricRegistry); AdminEnvironment adminEnvironment = mock(AdminEnvironment.class); doNothing().when(adminEnvironment) .addTask(any()); diff --git a/ranger-discovery-dw5-bundle/src/test/java/io/appform/ranger/discovery/bundle/ServiceDiscoveryBundleLocalHostPortTest.java b/ranger-discovery-dw5-bundle/src/test/java/io/appform/ranger/discovery/bundle/ServiceDiscoveryBundleLocalHostPortTest.java index 8e036177..d8838aeb 100644 --- a/ranger-discovery-dw5-bundle/src/test/java/io/appform/ranger/discovery/bundle/ServiceDiscoveryBundleLocalHostPortTest.java +++ b/ranger-discovery-dw5-bundle/src/test/java/io/appform/ranger/discovery/bundle/ServiceDiscoveryBundleLocalHostPortTest.java @@ -40,6 +40,7 @@ import java.util.UUID; import static io.appform.ranger.discovery.bundle.Constants.LOCAL_ADDRESSES; +import static io.appform.ranger.discovery.core.Constants.*; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; @@ -92,6 +93,7 @@ void shouldFailLocalhostPublish() { when(environment.lifecycle()).thenReturn(lifecycleEnvironment); when(environment.healthChecks()).thenReturn(healthChecks); when(environment.getObjectMapper()).thenReturn(new ObjectMapper()); + when(environment.metrics()).thenReturn(metricRegistry); AdminEnvironment adminEnvironment = mock(AdminEnvironment.class); doNothing().when(adminEnvironment) .addTask(any()); @@ -120,6 +122,7 @@ void shouldThrowExceptionForInvalidZkHost() { when(environment.lifecycle()).thenReturn(lifecycleEnvironment); when(environment.healthChecks()).thenReturn(healthChecks); when(environment.getObjectMapper()).thenReturn(new ObjectMapper()); + when(environment.metrics()).thenReturn(metricRegistry); AdminEnvironment adminEnvironment = mock(AdminEnvironment.class); doNothing().when(adminEnvironment) .addTask(any()); @@ -156,6 +159,7 @@ void testPublishWithEmptyZkHost() throws UnknownHostException { when(environment.lifecycle()).thenReturn(lifecycleEnvironment); when(environment.healthChecks()).thenReturn(healthChecks); when(environment.getObjectMapper()).thenReturn(new ObjectMapper()); + when(environment.metrics()).thenReturn(metricRegistry); AdminEnvironment adminEnvironment = mock(AdminEnvironment.class); doNothing().when(adminEnvironment) .addTask(any()); @@ -192,6 +196,7 @@ void testPublishWithNullZkHost() throws UnknownHostException { when(environment.lifecycle()).thenReturn(lifecycleEnvironment); when(environment.healthChecks()).thenReturn(healthChecks); when(environment.getObjectMapper()).thenReturn(new ObjectMapper()); + when(environment.metrics()).thenReturn(metricRegistry); AdminEnvironment adminEnvironment = mock(AdminEnvironment.class); doNothing().when(adminEnvironment) .addTask(any()); @@ -227,6 +232,7 @@ void shouldPublishingToLocalZk() { when(environment.lifecycle()).thenReturn(lifecycleEnvironment); when(environment.healthChecks()).thenReturn(healthChecks); when(environment.getObjectMapper()).thenReturn(new ObjectMapper()); + when(environment.metrics()).thenReturn(metricRegistry); AdminEnvironment adminEnvironment = mock(AdminEnvironment.class); doNothing().when(adminEnvironment) .addTask(any()); @@ -257,6 +263,7 @@ void shouldPublishToRemoteZk() { when(environment.lifecycle()).thenReturn(lifecycleEnvironment); when(environment.healthChecks()).thenReturn(healthChecks); when(environment.getObjectMapper()).thenReturn(new ObjectMapper()); + when(environment.metrics()).thenReturn(metricRegistry); AdminEnvironment adminEnvironment = mock(AdminEnvironment.class); doNothing().when(adminEnvironment) .addTask(any()); diff --git a/ranger-discovery-dw5-bundle/src/test/java/io/appform/ranger/discovery/bundle/ServiceDiscoveryBundleRotationTest.java b/ranger-discovery-dw5-bundle/src/test/java/io/appform/ranger/discovery/bundle/ServiceDiscoveryBundleRotationTest.java index 3f9e961f..88c7bec7 100644 --- a/ranger-discovery-dw5-bundle/src/test/java/io/appform/ranger/discovery/bundle/ServiceDiscoveryBundleRotationTest.java +++ b/ranger-discovery-dw5-bundle/src/test/java/io/appform/ranger/discovery/bundle/ServiceDiscoveryBundleRotationTest.java @@ -22,6 +22,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import io.appform.ranger.discovery.bundle.rotationstatus.BIRTask; import io.appform.ranger.discovery.bundle.rotationstatus.OORTask; +import io.appform.ranger.discovery.core.Constants; import io.appform.ranger.discovery.core.ServiceDiscoveryConfiguration; import io.appform.ranger.discovery.core.rotationstatus.RotationStatus; import io.appform.ranger.discovery.core.util.ConfigurationUtils; @@ -92,6 +93,7 @@ void setup() throws Exception { when(environment.lifecycle()).thenReturn(lifecycleEnvironment); when(environment.healthChecks()).thenReturn(healthChecks); when(environment.getObjectMapper()).thenReturn(new ObjectMapper()); + when(environment.metrics()).thenReturn(metricRegistry); AdminEnvironment adminEnvironment = mock(AdminEnvironment.class); doNothing().when(adminEnvironment) .addTask(any()); diff --git a/ranger-discovery-dw5-bundle/src/test/java/io/appform/ranger/discovery/bundle/ServiceDiscoveryBundleTest.java b/ranger-discovery-dw5-bundle/src/test/java/io/appform/ranger/discovery/bundle/ServiceDiscoveryBundleTest.java index d0a2c857..17efe152 100644 --- a/ranger-discovery-dw5-bundle/src/test/java/io/appform/ranger/discovery/bundle/ServiceDiscoveryBundleTest.java +++ b/ranger-discovery-dw5-bundle/src/test/java/io/appform/ranger/discovery/bundle/ServiceDiscoveryBundleTest.java @@ -21,6 +21,7 @@ import com.codahale.metrics.health.HealthCheckRegistry; import com.fasterxml.jackson.databind.ObjectMapper; import io.appform.ranger.core.healthcheck.HealthcheckStatus; +import io.appform.ranger.discovery.core.Constants; import io.appform.ranger.discovery.core.ServiceDiscoveryConfiguration; import io.appform.ranger.discovery.core.resolvers.DefaultNodeInfoResolver; import io.appform.ranger.discovery.core.resolvers.NodeInfoResolver; @@ -95,6 +96,7 @@ void setup() throws Exception { when(environment.lifecycle()).thenReturn(lifecycleEnvironment); when(environment.healthChecks()).thenReturn(healthChecks); when(environment.getObjectMapper()).thenReturn(new ObjectMapper()); + when(environment.metrics()).thenReturn(metricRegistry); AdminEnvironment adminEnvironment = mock(AdminEnvironment.class); doNothing().when(adminEnvironment) .addTask(any()); diff --git a/ranger-discovery-dw5-bundle/src/test/java/io/appform/ranger/discovery/bundle/resolvers/DefaultNodeInfoResolverTest.java b/ranger-discovery-dw5-bundle/src/test/java/io/appform/ranger/discovery/bundle/resolvers/DefaultNodeInfoResolverTest.java index cac313c4..fdf07928 100644 --- a/ranger-discovery-dw5-bundle/src/test/java/io/appform/ranger/discovery/bundle/resolvers/DefaultNodeInfoResolverTest.java +++ b/ranger-discovery-dw5-bundle/src/test/java/io/appform/ranger/discovery/bundle/resolvers/DefaultNodeInfoResolverTest.java @@ -15,6 +15,7 @@ */ package io.appform.ranger.discovery.bundle.resolvers; +import io.appform.ranger.discovery.core.Constants; import io.appform.ranger.discovery.core.ServiceDiscoveryConfiguration; import io.appform.ranger.discovery.core.resolvers.DefaultNodeInfoResolver; import lombok.val; diff --git a/ranger-discovery-dw5-bundle/src/test/java/io/appform/ranger/discovery/bundle/resolvers/DefaultPortSchemeResolverTest.java b/ranger-discovery-dw5-bundle/src/test/java/io/appform/ranger/discovery/bundle/resolvers/DefaultPortSchemeResolverTest.java index a2efd010..14503b3a 100644 --- a/ranger-discovery-dw5-bundle/src/test/java/io/appform/ranger/discovery/bundle/resolvers/DefaultPortSchemeResolverTest.java +++ b/ranger-discovery-dw5-bundle/src/test/java/io/appform/ranger/discovery/bundle/resolvers/DefaultPortSchemeResolverTest.java @@ -25,6 +25,8 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +import java.util.List; + import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -34,7 +36,7 @@ class DefaultPortSchemeResolverTest { void testPortSchemeDefaultServerFactory() { val server = mock(DefaultServerFactory.class); val connectorFactory = mock(HttpConnectorFactory.class); - when(server.getApplicationConnectors()).thenReturn(Lists.newArrayList(connectorFactory)); + when(server.getApplicationConnectors()).thenReturn(List.of(connectorFactory)); val resolver = new DefaultPortSchemeResolver<>(); val configuration = mock(Configuration.class); when(configuration.getServerFactory()).thenReturn(server); diff --git a/ranger-drove-client/src/main/java/io/appform/ranger/client/drove/AbstractRangerDroveHubClient.java b/ranger-drove-client/src/main/java/io/appform/ranger/client/drove/AbstractRangerDroveHubClient.java index b205f9d8..fd350094 100644 --- a/ranger-drove-client/src/main/java/io/appform/ranger/client/drove/AbstractRangerDroveHubClient.java +++ b/ranger-drove-client/src/main/java/io/appform/ranger/client/drove/AbstractRangerDroveHubClient.java @@ -45,7 +45,7 @@ public abstract class AbstractRangerDroveHubClient nodeSelector = new RandomServiceNodeSelector<>(); @Override - protected ServiceDataSource getDefaultDataSource() { + protected ServiceDataSource getDefaultDataSource(String upstreamId) { return new DroveServiceDataSource<>(clientConfig, getMapper(), getNamespace(), droveCommunicator); } diff --git a/ranger-drove-client/src/main/java/io/appform/ranger/client/drove/SimpleRangerDroveClient.java b/ranger-drove-client/src/main/java/io/appform/ranger/client/drove/SimpleRangerDroveClient.java index ee692fb7..8b1ace0c 100644 --- a/ranger-drove-client/src/main/java/io/appform/ranger/client/drove/SimpleRangerDroveClient.java +++ b/ranger-drove-client/src/main/java/io/appform/ranger/client/drove/SimpleRangerDroveClient.java @@ -58,6 +58,7 @@ public void start() { requireNonNull(deserializer, "deserializer can't be null"); this.serviceFinder = DroveServiceFinderBuilders.droveUnshardedServiceFinderBuilider() + .withUpstreamId(clientConfig.getId()) .withClientConfig(clientConfig) .withServiceName(serviceName) .withNamespace(namespace) diff --git a/ranger-drove-client/src/test/java/io/appform/ranger/client/drove/BaseRangerDroveClientTest.java b/ranger-drove-client/src/test/java/io/appform/ranger/client/drove/BaseRangerDroveClientTest.java index 1b655966..42701615 100644 --- a/ranger-drove-client/src/test/java/io/appform/ranger/client/drove/BaseRangerDroveClientTest.java +++ b/ranger-drove-client/src/test/java/io/appform/ranger/client/drove/BaseRangerDroveClientTest.java @@ -114,6 +114,7 @@ public void prepareHttpMocks() throws Exception { response)))); clientConfig = DroveUpstreamConfig.builder() + .id("test-metric") .endpoints(List.of("http://localhost:" + wireMockExtension.getPort())) .build(); log.debug("Started http subsystem. Wiremock port: {}", wireMockExtension.getPort()); diff --git a/ranger-drove-client/src/test/java/io/appform/ranger/client/drove/ShardedRangerDroveClientTest.java b/ranger-drove-client/src/test/java/io/appform/ranger/client/drove/ShardedRangerDroveClientTest.java index f458c268..fc0a5fab 100644 --- a/ranger-drove-client/src/test/java/io/appform/ranger/client/drove/ShardedRangerDroveClientTest.java +++ b/ranger-drove-client/src/test/java/io/appform/ranger/client/drove/ShardedRangerDroveClientTest.java @@ -43,6 +43,7 @@ public TestNodeData translate(ExposedAppInfo appInfo, ExposedAppInfo.ExposedHost .namespace(namespace) .mapper(getObjectMapper()) .nodeRefreshTimeMs(1000) + .upstreamId("test-metric") .build(); client.start(); val service = RangerTestUtils.getService(namespace, "TEST_APP"); diff --git a/ranger-drove-client/src/test/java/io/appform/ranger/client/drove/UnshardedRangerDroveClientTest.java b/ranger-drove-client/src/test/java/io/appform/ranger/client/drove/UnshardedRangerDroveClientTest.java index 64019c98..952f2a29 100644 --- a/ranger-drove-client/src/test/java/io/appform/ranger/client/drove/UnshardedRangerDroveClientTest.java +++ b/ranger-drove-client/src/test/java/io/appform/ranger/client/drove/UnshardedRangerDroveClientTest.java @@ -42,6 +42,7 @@ public TestNodeData translate(ExposedAppInfo appInfo, ExposedAppInfo.ExposedHost }) .mapper(getObjectMapper()) .nodeRefreshTimeMs(1000) + .upstreamId("test-metric") .build(); client.start(); val service = RangerTestUtils.getService(namespace, "TEST_APP"); diff --git a/ranger-drove/src/main/java/io/appform/ranger/drove/common/DroveApiCommunicator.java b/ranger-drove/src/main/java/io/appform/ranger/drove/common/DroveApiCommunicator.java index c1da48b0..728c7e6a 100644 --- a/ranger-drove/src/main/java/io/appform/ranger/drove/common/DroveApiCommunicator.java +++ b/ranger-drove/src/main/java/io/appform/ranger/drove/common/DroveApiCommunicator.java @@ -16,6 +16,7 @@ package io.appform.ranger.drove.common; +import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.base.Strings; @@ -25,16 +26,15 @@ import com.phonepe.drove.models.api.AppSummary; import com.phonepe.drove.models.api.ExposedAppInfo; import com.phonepe.drove.models.application.ApplicationState; +import io.appform.ranger.core.model.DataStoreType; import io.appform.ranger.core.model.Service; +import io.appform.ranger.core.util.MetricRecorder; import io.appform.ranger.drove.config.DroveUpstreamConfig; import lombok.extern.slf4j.Slf4j; import lombok.val; import org.apache.http.HttpStatus; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; +import java.util.*; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; @@ -43,11 +43,15 @@ import java.util.stream.Collectors; import java.util.stream.StreamSupport; +import static io.appform.ranger.core.util.MetricRecorder.LIST_NODES; +import static io.appform.ranger.core.util.MetricRecorder.SERVICES_LIST; + /** * */ @Slf4j public class DroveApiCommunicator implements DroveCommunicator { + private final String upstreamId; private final String namespace; private final DroveUpstreamConfig config; private final DroveClient droveClient; @@ -63,6 +67,7 @@ public DroveApiCommunicator( this.config = config; this.droveClient = droveClient; this.mapper = mapper; + this.upstreamId = config.getId(); resetter.scheduleWithFixedDelay(() -> upstreamAvailable.set(true), 0, 60, TimeUnit.SECONDS); } @@ -98,13 +103,11 @@ public List defaultValue() { @Override public List handle(DroveClient.Response response) throws Exception { + MetricRecorder.recordRemoteCallStatusCode(DataStoreType.DROVE, upstreamId, SERVICES_LIST, response.statusCode()); if (response.statusCode() != HttpStatus.SC_OK) { throw new DroveCommunicationException("Error communicating to drove: " + response); } - val apiResponse = mapper.readValue( - response.body(), - new TypeReference>>() { - }); + final var apiResponse = parseServicesResponse(response); if (!apiResponse.getStatus().equals(ApiErrorCode.SUCCESS)) { log.error("Error calling drove: " + apiResponse.getMessage()); throwDroveCommError(response); @@ -125,6 +128,27 @@ public List handle(DroveClient.Response response) throws Exception { })); } + private ApiResponse> parseServicesResponse(DroveClient.Response response) { + try { + val apiResponse = mapper.readValue( + response.body(), + new TypeReference>>() { + }); + + if(apiResponse == null || apiResponse.getData() == null || apiResponse.getData().isEmpty()) { + MetricRecorder.recordNullOrEmptyServicesListResponse(DataStoreType.DROVE, upstreamId); + log.warn("Received empty services list from drove. Response body: {}", response.body()); + } + + return apiResponse; + } catch (JsonProcessingException e) { + MetricRecorder.recordServicesParseFailure(DataStoreType.DROVE, upstreamId); + log.error("Error parsing response from drove: {}. Response body: {}", e.getMessage(), response.body(), e); + throw new DroveCommunicationException("Error parsing response from drove: " + e.getMessage()); + } + } + + @Override @SuppressWarnings("java:S1168") public Map> listNodes(Iterable services) { @@ -144,13 +168,12 @@ public Map> defaultValue() { } @Override - public Map> handle(DroveClient.Response response) throws Exception { + public Map> handle(DroveClient.Response response) { + MetricRecorder.recordRemoteCallStatusCode(DataStoreType.DROVE, upstreamId, LIST_NODES, response.statusCode()); if (response.statusCode() != HttpStatus.SC_OK) { throwDroveCommError(response); } - val apiResponse = mapper.readValue(response.body(), - new TypeReference>>() { - }); + final var apiResponse = parseListNodesResponse(services, response); if (!apiResponse.getStatus().equals(ApiErrorCode.SUCCESS)) { throwDroveCommError(response); } @@ -165,6 +188,27 @@ public Map> handle(DroveClient.Response response) })); } + private ApiResponse> parseListNodesResponse(Iterable services, DroveClient.Response response) { + try { + val apiResponse = mapper.readValue(response.body(), + new TypeReference>>() { + }); + + if(apiResponse == null || apiResponse.getData() == null || apiResponse.getData().isEmpty()) { + MetricRecorder.recordNullOrEmptyListNodeResponse(DataStoreType.DROVE, upstreamId); + services.forEach(service -> MetricRecorder.recordNullOrEmptyListNodeResponse(DataStoreType.DROVE, upstreamId, service.getServiceName())); + log.warn("Received empty node list from drove. Response body: {}", response.body()); + } + + return apiResponse; + } catch (JsonProcessingException e) { + MetricRecorder.recordListNodesParseFailure(DataStoreType.DROVE, upstreamId); + services.forEach(service -> MetricRecorder.recordListNodesParseFailure(DataStoreType.DROVE, upstreamId, service.getServiceName())); + log.error("Error parsing response from drove: {}. Response body: {}", e.getMessage(), response.body(), e); + throw new DroveCommunicationException("Error parsing response from drove: " + e.getMessage()); + } + } + private T executeRemoteCall(Supplier executor) { upstreamAvailable.set(true); try { diff --git a/ranger-drove/src/main/java/io/appform/ranger/drove/common/DroveCachingCommunicator.java b/ranger-drove/src/main/java/io/appform/ranger/drove/common/DroveCachingCommunicator.java index 892a39ec..7c7e8378 100644 --- a/ranger-drove/src/main/java/io/appform/ranger/drove/common/DroveCachingCommunicator.java +++ b/ranger-drove/src/main/java/io/appform/ranger/drove/common/DroveCachingCommunicator.java @@ -32,7 +32,9 @@ import com.phonepe.drove.models.events.events.DroveInstanceStateChangeEvent; import com.phonepe.drove.models.events.events.datatags.AppEventDataTag; import com.phonepe.drove.models.events.events.datatags.AppInstanceEventDataTag; +import io.appform.ranger.core.model.DataStoreType; import io.appform.ranger.core.model.Service; +import io.appform.ranger.core.util.MetricRecorder; import io.appform.ranger.drove.config.DroveUpstreamConfig; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; @@ -48,6 +50,8 @@ */ @Slf4j public class DroveCachingCommunicator implements DroveCommunicator { + + private final String upstreamId; private final DroveCommunicator root; private final DroveRemoteEventListener listener; //Zombie check is 60 secs .. so this provides about 10 secs @@ -60,6 +64,7 @@ public DroveCachingCommunicator( DroveUpstreamConfig config, DroveClient droveClient, ObjectMapper mapper) { + this.upstreamId = config.getId(); this.root = root; val offsetStore = new DroveEventPollingOffsetInMemoryStore(); offsetStore.setLastOffset(System.currentTimeMillis()); //Only interested in new events @@ -136,14 +141,18 @@ private void handleEvents(String namespace, List events, EnumSet implements NodeDataStoreConnector { + protected final String upstreamId; protected final DroveUpstreamConfig config; protected final ObjectMapper mapper; protected final DroveCommunicator droveClient; @@ -35,6 +38,7 @@ public DroveNodeDataStoreConnector( final DroveUpstreamConfig config, final ObjectMapper mapper, final DroveCommunicator droveClient) { + this.upstreamId = config.getId(); this.config = config; this.mapper = mapper; this.droveClient = droveClient; @@ -51,7 +55,6 @@ public void start() { public void ensureConnected() { do { Thread.sleep(1_000); - } while (droveClient.leader().orElse(null) == null); } @@ -62,7 +65,9 @@ public void stop() { @Override public boolean isActive() { - return droveClient.healthy(); + var healthy = droveClient.healthy(); + MetricRecorder.recordNoteDataSourceStatus(DataStoreType.DROVE, upstreamId, healthy); + return healthy; } } diff --git a/ranger-drove/src/main/java/io/appform/ranger/drove/common/DroveOkHttpTransport.java b/ranger-drove/src/main/java/io/appform/ranger/drove/common/DroveOkHttpTransport.java index 928a91a1..03a02141 100644 --- a/ranger-drove/src/main/java/io/appform/ranger/drove/common/DroveOkHttpTransport.java +++ b/ranger-drove/src/main/java/io/appform/ranger/drove/common/DroveOkHttpTransport.java @@ -18,12 +18,15 @@ import com.phonepe.drove.client.DroveClient; import com.phonepe.drove.client.DroveHttpTransport; +import io.appform.ranger.core.model.DataStoreType; +import io.appform.ranger.core.util.MetricRecorder; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import lombok.val; import okhttp3.Headers; import okhttp3.OkHttpClient; import okhttp3.Request; +import org.apache.http.client.methods.HttpGet; import java.net.URI; import java.util.List; @@ -34,9 +37,12 @@ */ @Slf4j public class DroveOkHttpTransport implements DroveHttpTransport { + + private final String upstreamId; private final OkHttpClient httpClient; - public DroveOkHttpTransport(final OkHttpClient httpClient) { + public DroveOkHttpTransport(String upstreamId, final OkHttpClient httpClient) { + this.upstreamId = upstreamId; this.httpClient = httpClient; log.info("Okhttp based transport initialized"); } @@ -68,6 +74,7 @@ public T get( return responseHandler.handle(droveResponse); } catch (Exception e) { + MetricRecorder.recordRemoteCallUnknownFailure(DataStoreType.DROVE, upstreamId, HttpGet.METHOD_NAME, e.getClass().getSimpleName()); log.error("Error calling drove: {}. Error: {}", e.getMessage(), e.getClass().getSimpleName()); throw new DroveCommunicationException(e.getMessage()); } diff --git a/ranger-drove/src/main/java/io/appform/ranger/drove/config/DroveUpstreamConfig.java b/ranger-drove/src/main/java/io/appform/ranger/drove/config/DroveUpstreamConfig.java index 60882748..b866f635 100644 --- a/ranger-drove/src/main/java/io/appform/ranger/drove/config/DroveUpstreamConfig.java +++ b/ranger-drove/src/main/java/io/appform/ranger/drove/config/DroveUpstreamConfig.java @@ -16,6 +16,7 @@ package io.appform.ranger.drove.config; import io.dropwizard.util.Duration; +import javax.validation.constraints.NotBlank; import lombok.Builder; import lombok.Value; import lombok.extern.jackson.Jacksonized; @@ -40,6 +41,9 @@ public class DroveUpstreamConfig { public static final Duration DEFAULT_OPERATION_TIMEOUT = Duration.seconds(5); public static final Duration DEFAULT_EVENT_POLLING_INTERVAL = Duration.seconds(5); + @NotBlank + String id; + @Valid @NotEmpty List endpoints; diff --git a/ranger-drove/src/main/java/io/appform/ranger/drove/servicefinder/DroveNodeDataSource.java b/ranger-drove/src/main/java/io/appform/ranger/drove/servicefinder/DroveNodeDataSource.java index bc3e448c..58cc585b 100644 --- a/ranger-drove/src/main/java/io/appform/ranger/drove/servicefinder/DroveNodeDataSource.java +++ b/ranger-drove/src/main/java/io/appform/ranger/drove/servicefinder/DroveNodeDataSource.java @@ -16,9 +16,11 @@ package io.appform.ranger.drove.servicefinder; import com.fasterxml.jackson.databind.ObjectMapper; +import io.appform.ranger.core.model.DataStoreType; import io.appform.ranger.core.model.NodeDataSource; import io.appform.ranger.core.model.Service; import io.appform.ranger.core.model.ServiceNode; +import io.appform.ranger.core.util.MetricRecorder; import io.appform.ranger.drove.common.DroveCommunicationException; import io.appform.ranger.drove.common.DroveCommunicator; import io.appform.ranger.drove.common.DroveNodeDataStoreConnector; @@ -50,6 +52,16 @@ public DroveNodeDataSource( this.service = service; } + @Override + public String getUpstreamId() { + return upstreamId; + } + + @Override + public DataStoreType getDataStoreType() { + return DataStoreType.DROVE; + } + @Override public Optional>> refresh(D deserializer) { requireNonNull(config, "client config has not been set for node data"); @@ -59,16 +71,17 @@ public Optional>> refresh(D deserializer) { val nodes = deserializer.deserialize( Objects.requireNonNull(exposedAppInfos, "Unexpected empty response from server")); return Optional.of(nodes); - } - catch (DroveCommunicationException e) { - log.error("Drove communication error", e); + } catch (DroveCommunicationException e) { + log.error("Drove communication error while refreshing data for service : {}", service.getServiceName(), e); return Optional.empty(); //In case of refresh failure, maintain old list } } @Override public boolean isActive() { - return droveClient.healthy(); + var healthy = droveClient.healthy(); + MetricRecorder.recordNoteDataSourceStatus(DataStoreType.DROVE, upstreamId, healthy); + return healthy; } } diff --git a/ranger-drove/src/main/java/io/appform/ranger/drove/servicefinder/DroveShardedServiceFinderBuilder.java b/ranger-drove/src/main/java/io/appform/ranger/drove/servicefinder/DroveShardedServiceFinderBuilder.java index fb6071fb..6aa3c605 100644 --- a/ranger-drove/src/main/java/io/appform/ranger/drove/servicefinder/DroveShardedServiceFinderBuilder.java +++ b/ranger-drove/src/main/java/io/appform/ranger/drove/servicefinder/DroveShardedServiceFinderBuilder.java @@ -59,11 +59,11 @@ public SimpleShardedServiceFinder build() { } @Override - protected NodeDataSource> dataSource(Service service) { - return new DroveNodeDataSource<>(service, clientConfig, mapper, - Objects.requireNonNullElseGet(droveClient, - () -> RangerDroveUtils.buildDroveClient( - namespace, clientConfig, mapper))); + protected NodeDataSource> dataSource(String upstreamId, Service service) { + return new DroveNodeDataSource<>(service, clientConfig, + mapper, Objects.requireNonNullElseGet(droveClient, + () -> RangerDroveUtils.buildDroveClient( + namespace, clientConfig, mapper))); } } diff --git a/ranger-drove/src/main/java/io/appform/ranger/drove/servicefinder/DroveUnshardedServiceFinderBuilider.java b/ranger-drove/src/main/java/io/appform/ranger/drove/servicefinder/DroveUnshardedServiceFinderBuilider.java index 87cff3c4..b324cd1c 100644 --- a/ranger-drove/src/main/java/io/appform/ranger/drove/servicefinder/DroveUnshardedServiceFinderBuilider.java +++ b/ranger-drove/src/main/java/io/appform/ranger/drove/servicefinder/DroveUnshardedServiceFinderBuilider.java @@ -56,12 +56,11 @@ public SimpleUnshardedServiceFinder build() { } @Override - protected NodeDataSource> dataSource(Service service) { + protected NodeDataSource> dataSource(String upstreamId, Service service) { return new DroveNodeDataSource<>( service, clientConfig, - mapper, - Objects.requireNonNullElseGet(droveCommunicator, + mapper, Objects.requireNonNullElseGet(droveCommunicator, () -> RangerDroveUtils.buildDroveClient(namespace, clientConfig, mapper))); } diff --git a/ranger-drove/src/main/java/io/appform/ranger/drove/servicefinderhub/DroveServiceDataSource.java b/ranger-drove/src/main/java/io/appform/ranger/drove/servicefinderhub/DroveServiceDataSource.java index 5d839305..ec87c8e9 100644 --- a/ranger-drove/src/main/java/io/appform/ranger/drove/servicefinderhub/DroveServiceDataSource.java +++ b/ranger-drove/src/main/java/io/appform/ranger/drove/servicefinderhub/DroveServiceDataSource.java @@ -16,8 +16,11 @@ package io.appform.ranger.drove.servicefinderhub; import com.fasterxml.jackson.databind.ObjectMapper; + import io.appform.ranger.core.finderhub.ServiceDataSource; import io.appform.ranger.core.model.Service; +import io.appform.ranger.core.model.DataStoreType; +import io.appform.ranger.core.util.MetricRecorder; import io.appform.ranger.drove.common.DroveNodeDataStoreConnector; import io.appform.ranger.drove.config.DroveUpstreamConfig; import io.appform.ranger.drove.common.DroveCommunicator; @@ -25,6 +28,8 @@ import java.util.Collection; +import static io.appform.ranger.core.util.MetricRecorder.FAILURE; +import static io.appform.ranger.core.util.MetricRecorder.SUCCESS; import static java.util.Objects.requireNonNull; @Slf4j @@ -44,9 +49,17 @@ public DroveServiceDataSource( public Collection services() { requireNonNull(config, "client config has not been set for node data"); requireNonNull(mapper, "mapper has not been set for node data"); - return droveClient.services() - .stream() - .map(serviceName -> new Service(namespace, serviceName)) - .toList(); + try { + var result = droveClient.services() + .stream() + .map(serviceName -> new Service(namespace, serviceName)) + .toList(); + MetricRecorder.recordServicesFetchStatus(DataStoreType.DROVE, upstreamId, SUCCESS); + return result; + } catch (Exception e) { + log.error("Error fetching services from drove data source id: {}, namespace: {}", upstreamId, namespace, e); + MetricRecorder.recordServicesFetchStatus(DataStoreType.DROVE, upstreamId, FAILURE); + throw e; + } } } diff --git a/ranger-drove/src/main/java/io/appform/ranger/drove/servicefinderhub/DroveShardedServiceFinderFactory.java b/ranger-drove/src/main/java/io/appform/ranger/drove/servicefinderhub/DroveShardedServiceFinderFactory.java index cb83841e..487d4569 100644 --- a/ranger-drove/src/main/java/io/appform/ranger/drove/servicefinderhub/DroveShardedServiceFinderFactory.java +++ b/ranger-drove/src/main/java/io/appform/ranger/drove/servicefinderhub/DroveShardedServiceFinderFactory.java @@ -61,6 +61,7 @@ public DroveShardedServiceFinderFactory( @Override public ServiceFinder> buildFinder(Service service) { val serviceFinder = new DroveShardedServiceFinderBuilder() + .withUpstreamId(clientConfig.getId()) .withClientConfig(clientConfig) .withDroveClient(droveClient) .withObjectMapper(mapper) diff --git a/ranger-drove/src/main/java/io/appform/ranger/drove/servicefinderhub/DroveUnshardedServiceFinderFactory.java b/ranger-drove/src/main/java/io/appform/ranger/drove/servicefinderhub/DroveUnshardedServiceFinderFactory.java index 3bf4c505..3b73913b 100644 --- a/ranger-drove/src/main/java/io/appform/ranger/drove/servicefinderhub/DroveUnshardedServiceFinderFactory.java +++ b/ranger-drove/src/main/java/io/appform/ranger/drove/servicefinderhub/DroveUnshardedServiceFinderFactory.java @@ -61,6 +61,7 @@ public DroveUnshardedServiceFinderFactory( @Override public ServiceFinder> buildFinder(Service service) { val serviceFinder = new DroveUnshardedServiceFinderBuilider() + .withUpstreamId(clientConfig.getId()) .withClientConfig(clientConfig) .withDroveCommunicator(droveCommunicator) .withObjectMapper(mapper) diff --git a/ranger-drove/src/main/java/io/appform/ranger/drove/utils/RangerDroveUtils.java b/ranger-drove/src/main/java/io/appform/ranger/drove/utils/RangerDroveUtils.java index 96f6143f..67c313d1 100644 --- a/ranger-drove/src/main/java/io/appform/ranger/drove/utils/RangerDroveUtils.java +++ b/ranger-drove/src/main/java/io/appform/ranger/drove/utils/RangerDroveUtils.java @@ -99,9 +99,7 @@ public static DroveCommunicator buildDroveClient( List.of(new BasicAuthDecorator(config.getUsername(), config.getPassword()), new AuthHeaderDecorator(config.getAuthHeader())), - new DroveOkHttpTransport(createOkHttpClient(config))); -// new DroveHttpComponentsTransport(droveConfig, -// createHttpClient(config))); + new DroveOkHttpTransport(config.getId(), createOkHttpClient(config))); val apiCommunicator = new DroveApiCommunicator(namespace, config, droveClient, mapper); return config.isSkipCaching() ? apiCommunicator diff --git a/ranger-drove/src/test/java/io/appform/ranger/drove/common/DroveApiCommunicatorMetricsIntegrationTest.java b/ranger-drove/src/test/java/io/appform/ranger/drove/common/DroveApiCommunicatorMetricsIntegrationTest.java new file mode 100644 index 00000000..8fc6e05d --- /dev/null +++ b/ranger-drove/src/test/java/io/appform/ranger/drove/common/DroveApiCommunicatorMetricsIntegrationTest.java @@ -0,0 +1,370 @@ +/* + * Copyright 2024 Authors, Flipkart Internet Pvt. Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.appform.ranger.drove.common; + +import com.codahale.metrics.MetricRegistry; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.github.tomakehurst.wiremock.http.Fault; +import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo; +import com.github.tomakehurst.wiremock.junit5.WireMockTest; +import com.phonepe.drove.models.api.ApiResponse; +import com.phonepe.drove.models.api.AppSummary; +import com.phonepe.drove.models.api.ExposedAppInfo; +import com.phonepe.drove.models.application.ApplicationState; +import com.phonepe.drove.models.application.PortType; +import io.appform.ranger.core.model.Service; +import io.appform.ranger.core.util.MetricRecorder; +import io.appform.ranger.drove.config.DroveUpstreamConfig; +import io.appform.ranger.drove.utils.RangerDroveUtils; +import lombok.SneakyThrows; +import lombok.val; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.Date; +import java.util.List; +import java.util.Map; + +import static com.github.tomakehurst.wiremock.client.WireMock.*; +import static org.junit.jupiter.api.Assertions.*; + +/** + * Integration tests for DroveApiCommunicator metrics recording. + * Tests verify that metrics are pushed through actual Drove API calls via WireMock. + */ +@WireMockTest +class DroveApiCommunicatorMetricsIntegrationTest { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + private static final String METRIC_PREFIX = "io.appform.ranger"; + private MetricRegistry metricRegistry; + + @BeforeEach + void setUp() { + metricRegistry = new MetricRegistry(); + MetricRecorder.initialize(metricRegistry); + } + + // ==================== services() - Success with 200 ==================== + + @Test + @SneakyThrows + void testServices_success_recordsStatusCodeAndNoFailure(WireMockRuntimeInfo wm) { + val response = ApiResponse.success(Map.of( + "APP_1", new AppSummary("APP_1", "APP_1", 4, 4, 4, 1024, Map.of(), + ApplicationState.RUNNING, new Date(), new Date()), + "APP_2", new AppSummary("APP_2", "APP_2", 4, 4, 4, 1024, Map.of(), + ApplicationState.RUNNING, new Date(), new Date()))); + + stubFor(get("/apis/v1/applications") + .withBasicAuth("guest", "guest") + .willReturn(okJson(MAPPER.writeValueAsString(response)))); + + try (val client = buildClient(wm, "drove-api-1")) { + val services = client.services(); + assertNotNull(services); + assertEquals(2, services.size()); + + // Verify 200 status code meter + val statusMeter = metricRegistry.getMeters().get( + METRIC_PREFIX + ".dataStoreType.DROVE.dataSource.drove-api-1.httpCall.services.responseStatus.200"); + assertNotNull(statusMeter, "200 status meter should be recorded for services"); + assertEquals(1, statusMeter.getCount()); + } + } + + // ==================== services() - Empty data (null/empty services response) ==================== + + @Test + @SneakyThrows + void testServices_emptyData_recordsNullOrEmptyServicesResponse(WireMockRuntimeInfo wm) { + val response = ApiResponse.success(Map.of()); // empty map + + stubFor(get("/apis/v1/applications") + .withBasicAuth("guest", "guest") + .willReturn(okJson(MAPPER.writeValueAsString(response)))); + + try (val client = buildClient(wm, "drove-api-2")) { + val services = client.services(); + assertNotNull(services); + assertTrue(services.isEmpty()); + + // Verify null/empty services meter + val emptyMeter = metricRegistry.getMeters().get( + METRIC_PREFIX + ".dataStoreType.DROVE.dataSource.drove-api-2.httpCall.services.nullOrEmptyResponse"); + assertNotNull(emptyMeter, "Null/empty services response meter should be recorded"); + assertEquals(1, emptyMeter.getCount()); + } + } + + // ==================== services() - Non-200 (error) ==================== + + @Test + @SneakyThrows + void testServices_serverError_recordsStatusCode(WireMockRuntimeInfo wm) { + stubFor(get("/apis/v1/applications") + .withBasicAuth("guest", "guest") + .willReturn(aResponse().withStatus(500).withBody("Internal Server Error"))); + + try (val client = buildClient(wm, "drove-api-3")) { + assertThrows(DroveCommunicationException.class, client::services); + + // Verify 500 status code meter + val statusMeter = metricRegistry.getMeters().get( + METRIC_PREFIX + ".dataStoreType.DROVE.dataSource.drove-api-3.httpCall.services.responseStatus.500"); + assertNotNull(statusMeter, "500 status meter should be recorded"); + assertEquals(1, statusMeter.getCount()); + } + } + + // ==================== services() - Invalid JSON (parse failure) ==================== + + @Test + @SneakyThrows + void testServices_invalidJson_recordsParseFailure(WireMockRuntimeInfo wm) { + stubFor(get("/apis/v1/applications") + .withBasicAuth("guest", "guest") + .willReturn(aResponse().withStatus(200).withBody("not-valid-json{{{"))); + + try (val client = buildClient(wm, "drove-api-4")) { + assertThrows(DroveCommunicationException.class, client::services); + + // Verify parse failure meter + val parseMeter = metricRegistry.getMeters().get( + METRIC_PREFIX + ".dataStoreType.DROVE.dataSource.drove-api-4.httpCall.services.responseParseFailure"); + assertNotNull(parseMeter, "Services parse failure meter should be recorded"); + assertEquals(1, parseMeter.getCount()); + } + } + + // ==================== services() - Network error (unknown failure from OkHttp transport) ==================== + + @Test + @SneakyThrows + void testServices_networkError_recordsRemoteCallUnknownFailure(WireMockRuntimeInfo wm) { + stubFor(get("/apis/v1/applications") + .withBasicAuth("guest", "guest") + .willReturn(aResponse().withFault(Fault.MALFORMED_RESPONSE_CHUNK))); + + try (val client = buildClient(wm, "drove-api-5")) { + assertThrows(DroveCommunicationException.class, client::services); + + // Verify remote call unknown failure meter (from DroveOkHttpTransport.get()) + val failureMeter = metricRegistry.getMeters().get( + METRIC_PREFIX + ".dataStoreType.DROVE.dataSource.drove-api-5.httpCall.GET.unknownFailure"); + assertNotNull(failureMeter, "Remote call unknown failure meter should be recorded"); + assertTrue(failureMeter.getCount() >= 1); + } + } + + // ==================== listNodes() - Success with 200 ==================== + + @Test + @SneakyThrows + void testListNodes_success_recordsStatusCode(WireMockRuntimeInfo wm) { + val nodeResponse = ApiResponse.success(List.of( + new ExposedAppInfo("TEST_APP", "v1", "host.internal", Map.of(), + List.of(new ExposedAppInfo.ExposedHost("host1.internal", 32000, PortType.HTTP))))); + + stubFor(get(urlPathEqualTo("/apis/v1/endpoints")) + .withBasicAuth("guest", "guest") + .willReturn(aResponse() + .withBody(MAPPER.writeValueAsBytes(nodeResponse)) + .withStatus(200))); + + try (val client = buildClient(wm, "drove-api-6")) { + val service = new Service("testns", "TEST_APP"); + val nodes = client.listNodes(service); + + assertNotNull(nodes); + assertFalse(nodes.isEmpty()); + + // Verify 200 status code for listNodes + val statusMeter = metricRegistry.getMeters().get( + METRIC_PREFIX + ".dataStoreType.DROVE.dataSource.drove-api-6.httpCall.listNodes.responseStatus.200"); + assertNotNull(statusMeter, "200 status meter for listNodes should exist"); + assertEquals(1, statusMeter.getCount()); + } + } + + // ==================== listNodes() - Empty response ==================== + + @Test + @SneakyThrows + void testListNodes_emptyData_recordsNullOrEmptyListNodeResponse(WireMockRuntimeInfo wm) { + val nodeResponse = ApiResponse.success(List.of()); // empty list + + stubFor(get(urlPathEqualTo("/apis/v1/endpoints")) + .withBasicAuth("guest", "guest") + .willReturn(aResponse() + .withBody(MAPPER.writeValueAsBytes(nodeResponse)) + .withStatus(200))); + + try (val client = buildClient(wm, "drove-api-7")) { + val service = new Service("testns", "TEST_APP"); + val nodes = client.listNodes(service); + + assertNotNull(nodes); + assertTrue(nodes.isEmpty()); + + // Verify null/empty list node response + val aggregateMeter = metricRegistry.getMeters().get( + METRIC_PREFIX + ".dataStoreType.DROVE.dataSource.drove-api-7.httpCall.listNodes.nullOrEmptyResponse"); + assertNotNull(aggregateMeter, "Aggregate null/empty list node response meter should be recorded"); + assertEquals(1, aggregateMeter.getCount()); + + val emptyMeter = metricRegistry.getMeters().get( + METRIC_PREFIX + ".dataStoreType.DROVE.dataSource.drove-api-7.httpCall.listNodes.serviceName.TEST_APP.nullOrEmptyResponse"); + assertNotNull(emptyMeter, "Null/empty list node response meter should be recorded"); + assertEquals(1, emptyMeter.getCount()); + } + } + + // ==================== listNodes() - Invalid JSON (parse failure) ==================== + + @Test + @SneakyThrows + void testListNodes_invalidJson_recordsParseFailure(WireMockRuntimeInfo wm) { + stubFor(get(urlPathEqualTo("/apis/v1/endpoints")) + .withBasicAuth("guest", "guest") + .willReturn(aResponse().withStatus(200).withBody("invalid-json{{{{"))); + + try (val client = buildClient(wm, "drove-api-8")) { + val service = new Service("testns", "PARSE_FAIL_APP"); + assertThrows(DroveCommunicationException.class, () -> client.listNodes(service)); + + // Verify list nodes parse failure + val aggregateParseMeter = metricRegistry.getMeters().get( + METRIC_PREFIX + ".dataStoreType.DROVE.dataSource.drove-api-8.httpCall.listNodes.responseParseFailure"); + assertNotNull(aggregateParseMeter, "Aggregate list nodes parse failure meter should be recorded"); + assertEquals(1, aggregateParseMeter.getCount()); + + val parseMeter = metricRegistry.getMeters().get( + METRIC_PREFIX + ".dataStoreType.DROVE.dataSource.drove-api-8.httpCall.listNodes.serviceName.PARSE_FAIL_APP.responseParseFailure"); + assertNotNull(parseMeter, "List nodes parse failure meter should be recorded"); + assertEquals(1, parseMeter.getCount()); + } + } + + // ==================== listNodes() - Non-200 (error) ==================== + + @Test + @SneakyThrows + void testListNodes_serverError_recordsStatusCode(WireMockRuntimeInfo wm) { + stubFor(get(urlPathEqualTo("/apis/v1/endpoints")) + .withBasicAuth("guest", "guest") + .willReturn(aResponse().withStatus(503).withBody("Service Unavailable"))); + + try (val client = buildClient(wm, "drove-api-9")) { + val service = new Service("testns", "ERROR_APP"); + assertThrows(DroveCommunicationException.class, () -> client.listNodes(service)); + + // Verify 503 status code + val statusMeter = metricRegistry.getMeters().get( + METRIC_PREFIX + ".dataStoreType.DROVE.dataSource.drove-api-9.httpCall.listNodes.responseStatus.503"); + assertNotNull(statusMeter, "503 status meter for listNodes should be recorded"); + assertEquals(1, statusMeter.getCount()); + } + } + + // ==================== listNodes() - Multi-service: aggregate pushed only once ==================== + + @Test + @SneakyThrows + void testListNodes_multiServiceEmptyResponse_aggregateMetricPushedOnce(WireMockRuntimeInfo wm) { + val nodeResponse = ApiResponse.success(List.of()); // empty list + + stubFor(get(urlPathEqualTo("/apis/v1/endpoints")) + .withBasicAuth("guest", "guest") + .willReturn(aResponse() + .withBody(MAPPER.writeValueAsBytes(nodeResponse)) + .withStatus(200))); + + try (val client = buildClient(wm, "drove-api-10")) { + val service1 = new Service("testns", "APP_ONE"); + val service2 = new Service("testns", "APP_TWO"); + val result = client.listNodes(List.of(service1, service2)); + + assertNotNull(result); + assertTrue(result.isEmpty()); + + // Aggregate metric must be pushed exactly once regardless of how many services were in the batch + val aggregateMeter = metricRegistry.getMeters().get( + METRIC_PREFIX + ".dataStoreType.DROVE.dataSource.drove-api-10.httpCall.listNodes.nullOrEmptyResponse"); + assertNotNull(aggregateMeter, "Aggregate null/empty metric should be recorded"); + assertEquals(1, aggregateMeter.getCount(), + "Aggregate metric must be pushed exactly once even for multi-service batches"); + + // Service-level metrics pushed once per service + val svc1Meter = metricRegistry.getMeters().get( + METRIC_PREFIX + ".dataStoreType.DROVE.dataSource.drove-api-10.httpCall.listNodes.serviceName.APP_ONE.nullOrEmptyResponse"); + assertNotNull(svc1Meter, "Service-level metric for APP_ONE should be recorded"); + assertEquals(1, svc1Meter.getCount()); + + val svc2Meter = metricRegistry.getMeters().get( + METRIC_PREFIX + ".dataStoreType.DROVE.dataSource.drove-api-10.httpCall.listNodes.serviceName.APP_TWO.nullOrEmptyResponse"); + assertNotNull(svc2Meter, "Service-level metric for APP_TWO should be recorded"); + assertEquals(1, svc2Meter.getCount()); + } + } + + @Test + @SneakyThrows + void testListNodes_multiServiceParseFailure_aggregateMetricPushedOnce(WireMockRuntimeInfo wm) { + stubFor(get(urlPathEqualTo("/apis/v1/endpoints")) + .withBasicAuth("guest", "guest") + .willReturn(aResponse().withStatus(200).withBody("invalid-json{{{{"))); + + try (val client = buildClient(wm, "drove-api-11")) { + val service1 = new Service("testns", "APP_ONE"); + val service2 = new Service("testns", "APP_TWO"); + assertThrows(DroveCommunicationException.class, () -> client.listNodes(List.of(service1, service2))); + + // Aggregate metric must be pushed exactly once + val aggregateParseMeter = metricRegistry.getMeters().get( + METRIC_PREFIX + ".dataStoreType.DROVE.dataSource.drove-api-11.httpCall.listNodes.responseParseFailure"); + assertNotNull(aggregateParseMeter, "Aggregate parse failure metric should be recorded"); + assertEquals(1, aggregateParseMeter.getCount(), + "Aggregate metric must be pushed exactly once even for multi-service batches"); + + // Service-level metrics pushed once per service + val svc1Meter = metricRegistry.getMeters().get( + METRIC_PREFIX + ".dataStoreType.DROVE.dataSource.drove-api-11.httpCall.listNodes.serviceName.APP_ONE.responseParseFailure"); + assertNotNull(svc1Meter, "Service-level parse failure metric for APP_ONE should be recorded"); + assertEquals(1, svc1Meter.getCount()); + + val svc2Meter = metricRegistry.getMeters().get( + METRIC_PREFIX + ".dataStoreType.DROVE.dataSource.drove-api-11.httpCall.listNodes.serviceName.APP_TWO.responseParseFailure"); + assertNotNull(svc2Meter, "Service-level parse failure metric for APP_TWO should be recorded"); + assertEquals(1, svc2Meter.getCount()); + } + } + + // ==================== Helper ==================== + + private DroveCommunicator buildClient(WireMockRuntimeInfo wm, String upstreamId) { + return RangerDroveUtils.buildDroveClient( + "testns", + DroveUpstreamConfig.builder() + .id(upstreamId) + .endpoints(List.of("http://localhost:" + wm.getHttpPort())) + .username("guest") + .password("guest") + .skipCaching(true) + .build(), + MAPPER); + } +} diff --git a/ranger-drove/src/test/java/io/appform/ranger/drove/servicefinder/DroveNodeDataSourceMetricsIntegrationTest.java b/ranger-drove/src/test/java/io/appform/ranger/drove/servicefinder/DroveNodeDataSourceMetricsIntegrationTest.java new file mode 100644 index 00000000..d4ea9c30 --- /dev/null +++ b/ranger-drove/src/test/java/io/appform/ranger/drove/servicefinder/DroveNodeDataSourceMetricsIntegrationTest.java @@ -0,0 +1,201 @@ +/* + * Copyright 2024 Authors, Flipkart Internet Pvt. Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.appform.ranger.drove.servicefinder; + +import com.codahale.metrics.MetricRegistry; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo; +import com.github.tomakehurst.wiremock.junit5.WireMockTest; +import com.phonepe.drove.models.api.ApiResponse; +import com.phonepe.drove.models.api.ExposedAppInfo; +import com.phonepe.drove.models.application.PortType; +import io.appform.ranger.core.model.Service; +import io.appform.ranger.core.util.MetricRecorder; +import io.appform.ranger.drove.common.DroveCommunicator; +import io.appform.ranger.drove.config.DroveUpstreamConfig; +import io.appform.ranger.drove.serde.DroveResponseDataDeserializer; +import io.appform.ranger.drove.utils.RangerDroveUtils; +import lombok.SneakyThrows; +import lombok.val; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.Map; + +import static com.github.tomakehurst.wiremock.client.WireMock.*; +import static org.junit.jupiter.api.Assertions.*; + +/** + * Integration tests for DroveNodeDataSource metrics recording. + * Tests verify metrics from actual Drove data source flows. + */ +@WireMockTest +class DroveNodeDataSourceMetricsIntegrationTest { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + private static final String METRIC_PREFIX = "io.appform.ranger"; + private MetricRegistry metricRegistry; + + private static final DroveResponseDataDeserializer STRING_DESERIALIZER = + new DroveResponseDataDeserializer<>() { + @Override + protected String translate(ExposedAppInfo appInfo, ExposedAppInfo.ExposedHost host) { + return host.getHost() + ":" + host.getPort(); + } + }; + + @BeforeEach + void setUp() { + metricRegistry = new MetricRegistry(); + MetricRecorder.initialize(metricRegistry); + } + + // ==================== isActive() - Healthy upstream ==================== + + @Test + @SneakyThrows + void testIsActive_healthyUpstream_recordsActiveStatus(WireMockRuntimeInfo wm) { + // Drove client starts with upstreamAvailable = true + val config = buildConfig(wm, "drove-node-src-1"); + try (val droveClient = buildClient(config)) { + val service = new Service("testns", "TEST_APP"); + val dataSource = new DroveNodeDataSource>( + service, config, MAPPER, droveClient); + + val active = dataSource.isActive(); + + assertTrue(active); + + val activeMeter = metricRegistry.getMeters().get( + METRIC_PREFIX + ".dataStoreType.DROVE.dataSource.drove-node-src-1.active"); + assertNotNull(activeMeter, "Active status meter should be recorded"); + assertEquals(1, activeMeter.getCount()); + } + } + + // ==================== isActive() - Unhealthy upstream ==================== + + @Test + @SneakyThrows + void testIsActive_unhealthyUpstream_recordsInactiveStatus(WireMockRuntimeInfo wm) { + stubFor(get("/apis/v1/applications") + .withBasicAuth("guest", "guest") + .willReturn(aResponse().withStatus(500).withBody("Error"))); + + val config = buildConfig(wm, "drove-node-src-2"); + try (val droveClient = buildClient(config)) { + val service = new Service("testns", "TEST_APP"); + val dataSource = new DroveNodeDataSource>( + service, config, MAPPER, droveClient); + + // First call to services fails, setting upstreamAvailable to false + try { + droveClient.services(); + } catch (Exception ignored) { + // Ignored + } + + val active = dataSource.isActive(); + + assertFalse(active); + + val inactiveMeter = metricRegistry.getMeters().get( + METRIC_PREFIX + ".dataStoreType.DROVE.dataSource.drove-node-src-2.inactive"); + assertNotNull(inactiveMeter, "Inactive status meter should be recorded"); + assertEquals(1, inactiveMeter.getCount()); + } + } + + // ==================== refresh() - Success ==================== + + @Test + @SneakyThrows + void testRefresh_success_returnsNodes(WireMockRuntimeInfo wm) { + val nodeResponse = ApiResponse.success(List.of( + new ExposedAppInfo("TEST_APP", "v1", "host.internal", Map.of(), + List.of(new ExposedAppInfo.ExposedHost("host1.internal", 32000, PortType.HTTP), + new ExposedAppInfo.ExposedHost("host2.internal", 32001, PortType.HTTP))))); + + stubFor(get(urlPathEqualTo("/apis/v1/endpoints")) + .withBasicAuth("guest", "guest") + .willReturn(aResponse() + .withBody(MAPPER.writeValueAsBytes(nodeResponse)) + .withStatus(200))); + + val config = buildConfig(wm, "drove-node-src-3"); + try (val droveClient = buildClient(config)) { + val service = new Service("testns", "TEST_APP"); + val dataSource = new DroveNodeDataSource>( + service, config, MAPPER, droveClient); + + val result = dataSource.refresh(STRING_DESERIALIZER); + + assertTrue(result.isPresent()); + assertEquals(2, result.get().size()); + + // Verify 200 status code was recorded (from DroveApiCommunicator) + val statusMeter = metricRegistry.getMeters().get( + METRIC_PREFIX + ".dataStoreType.DROVE.dataSource.drove-node-src-3.httpCall.listNodes.responseStatus.200"); + assertNotNull(statusMeter, "200 status meter should be recorded for listNodes"); + assertEquals(1, statusMeter.getCount()); + } + } + + // ==================== refresh() - Communication failure ==================== + + @Test + @SneakyThrows + void testRefresh_communicationFailure_returnsEmpty(WireMockRuntimeInfo wm) { + stubFor(get(urlPathEqualTo("/apis/v1/endpoints")) + .withBasicAuth("guest", "guest") + .willReturn(aResponse().withStatus(500).withBody("Error"))); + + val config = buildConfig(wm, "drove-node-src-4"); + try (val droveClient = buildClient(config)) { + val service = new Service("testns", "FAIL_APP"); + val dataSource = new DroveNodeDataSource>( + service, config, MAPPER, droveClient); + + val result = dataSource.refresh(STRING_DESERIALIZER); + + // On DroveCommunicationException, returns Optional.empty() to maintain old list + assertFalse(result.isPresent()); + + // Verify 500 status code was still recorded + val statusMeter = metricRegistry.getMeters().get( + METRIC_PREFIX + ".dataStoreType.DROVE.dataSource.drove-node-src-4.httpCall.listNodes.responseStatus.500"); + assertNotNull(statusMeter, "500 status meter should be recorded"); + assertEquals(1, statusMeter.getCount()); + } + } + + // ==================== Helpers ==================== + + private DroveUpstreamConfig buildConfig(WireMockRuntimeInfo wm, String upstreamId) { + return DroveUpstreamConfig.builder() + .id(upstreamId) + .endpoints(List.of("http://localhost:" + wm.getHttpPort())) + .username("guest") + .password("guest") + .skipCaching(true) + .build(); + } + + private DroveCommunicator buildClient(DroveUpstreamConfig config) { + return RangerDroveUtils.buildDroveClient("testns", config, MAPPER); + } +} diff --git a/ranger-drove/src/test/java/io/appform/ranger/drove/servicefinder/DroveNodeDataSourceTest.java b/ranger-drove/src/test/java/io/appform/ranger/drove/servicefinder/DroveNodeDataSourceTest.java index 3a7b90b7..7559d376 100644 --- a/ranger-drove/src/test/java/io/appform/ranger/drove/servicefinder/DroveNodeDataSourceTest.java +++ b/ranger-drove/src/test/java/io/appform/ranger/drove/servicefinder/DroveNodeDataSourceTest.java @@ -78,8 +78,7 @@ void testSuccess() { val ds = new DroveNodeDataSource( service, config, - MAPPER, - droveClient); + MAPPER, droveClient); ds.start(); val res = ds.refresh(new DNodeDataDeserializer()).orElse(null); assertNotNull(res); @@ -104,8 +103,7 @@ void testUpstreamFailure() { val ds = new DroveNodeDataSource( service, config, - MAPPER, - droveClient); + MAPPER, droveClient); ds.start(); val res = ds.refresh(new DNodeDataDeserializer()).orElse(null); assertNull(res); diff --git a/ranger-drove/src/test/java/io/appform/ranger/drove/servicefinder/DroveShardedServiceFinderBuilderTest.java b/ranger-drove/src/test/java/io/appform/ranger/drove/servicefinder/DroveShardedServiceFinderBuilderTest.java index 05d7f579..babc9857 100644 --- a/ranger-drove/src/test/java/io/appform/ranger/drove/servicefinder/DroveShardedServiceFinderBuilderTest.java +++ b/ranger-drove/src/test/java/io/appform/ranger/drove/servicefinder/DroveShardedServiceFinderBuilderTest.java @@ -89,6 +89,7 @@ public NodeData translate(ExposedAppInfo appInfo, ExposedAppInfo.ExposedHost hos .withShardSelector((criteria, registry) -> registry.nodeList()) .withNodeRefreshIntervalMs(1000) .withDroveClient(droveClient) + .withUpstreamId("test-metric") .build(); finder.start(); await().atMost(Duration.ofSeconds(30)) diff --git a/ranger-drove/src/test/java/io/appform/ranger/drove/servicefinderhub/DroveServiceDataSourceMetricsIntegrationTest.java b/ranger-drove/src/test/java/io/appform/ranger/drove/servicefinderhub/DroveServiceDataSourceMetricsIntegrationTest.java new file mode 100644 index 00000000..982e167a --- /dev/null +++ b/ranger-drove/src/test/java/io/appform/ranger/drove/servicefinderhub/DroveServiceDataSourceMetricsIntegrationTest.java @@ -0,0 +1,192 @@ +/* + * Copyright 2024 Authors, Flipkart Internet Pvt. Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.appform.ranger.drove.servicefinderhub; + +import com.codahale.metrics.MetricRegistry; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo; +import com.github.tomakehurst.wiremock.junit5.WireMockTest; +import com.phonepe.drove.models.api.ApiResponse; +import com.phonepe.drove.models.api.AppSummary; +import com.phonepe.drove.models.application.ApplicationState; +import io.appform.ranger.core.util.MetricRecorder; +import io.appform.ranger.drove.common.DroveCommunicationException; +import io.appform.ranger.drove.common.DroveCommunicator; +import io.appform.ranger.drove.config.DroveUpstreamConfig; +import io.appform.ranger.drove.utils.RangerDroveUtils; +import lombok.SneakyThrows; +import lombok.val; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.Date; +import java.util.List; +import java.util.Map; + +import static com.github.tomakehurst.wiremock.client.WireMock.*; +import static org.junit.jupiter.api.Assertions.*; + +/** + * Integration tests for DroveServiceDataSource metrics recording. + * Tests verify that metrics are pushed through actual Drove code flows. + */ +@WireMockTest +class DroveServiceDataSourceMetricsIntegrationTest { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + private static final String METRIC_PREFIX = "io.appform.ranger"; + private MetricRegistry metricRegistry; + + @BeforeEach + void setUp() { + metricRegistry = new MetricRegistry(); + MetricRecorder.initialize(metricRegistry); + } + + // ==================== services() - Success ==================== + + @Test + @SneakyThrows + void testServices_success_recordsFetchSuccess(WireMockRuntimeInfo wm) { + val response = ApiResponse.success(Map.of( + "SVC_A", new AppSummary("SVC_A", "SVC_A", 4, 4, 4, 1024, Map.of(), + ApplicationState.RUNNING, new Date(), new Date()), + "SVC_B", new AppSummary("SVC_B", "SVC_B", 4, 4, 4, 1024, Map.of(), + ApplicationState.RUNNING, new Date(), new Date()))); + + stubFor(get("/apis/v1/applications") + .withBasicAuth("guest", "guest") + .willReturn(okJson(MAPPER.writeValueAsString(response)))); + + val config = buildConfig(wm, "drove-svc-src-1"); + try (val droveClient = buildClient(config)) { + val dataSource = new DroveServiceDataSource<>( + config, MAPPER, "testns", droveClient); + + val services = dataSource.services(); + + assertNotNull(services); + assertEquals(2, services.size()); + + // Verify services fetch success + val successMeter = metricRegistry.getMeters().get( + METRIC_PREFIX + ".dataStoreType.DROVE.dataSource.drove-svc-src-1.services.fetch.success"); + assertNotNull(successMeter, "Services fetch success meter should be recorded"); + assertEquals(1, successMeter.getCount()); + + // No failure + assertNull(metricRegistry.getMeters().get( + METRIC_PREFIX + ".dataStoreType.DROVE.dataSource.drove-svc-src-1.services.fetch.failure")); + } + } + + // ==================== services() - Failure ==================== + + @Test + @SneakyThrows + void testServices_failure_recordsFetchFailure(WireMockRuntimeInfo wm) { + stubFor(get("/apis/v1/applications") + .withBasicAuth("guest", "guest") + .willReturn(aResponse().withStatus(500).withBody("Error"))); + + val config = buildConfig(wm, "drove-svc-src-2"); + try (val droveClient = buildClient(config)) { + val dataSource = new DroveServiceDataSource<>( + config, MAPPER, "testns", droveClient); + + assertThrows(DroveCommunicationException.class, dataSource::services); + + // Verify services fetch failure + val failureMeter = metricRegistry.getMeters().get( + METRIC_PREFIX + ".dataStoreType.DROVE.dataSource.drove-svc-src-2.services.fetch.failure"); + assertNotNull(failureMeter, "Services fetch failure meter should be recorded"); + assertEquals(1, failureMeter.getCount()); + } + } + + // ==================== isActive() - Healthy ==================== + + @Test + @SneakyThrows + void testIsActive_healthy_recordsActiveStatus(WireMockRuntimeInfo wm) { + // Stub services endpoint to make client healthy (upstreamAvailable defaults true) + val response = ApiResponse.success(Map.of()); + stubFor(get("/apis/v1/applications") + .withBasicAuth("guest", "guest") + .willReturn(okJson(MAPPER.writeValueAsString(response)))); + + val config = buildConfig(wm, "drove-svc-src-3"); + try (val droveClient = buildClient(config)) { + val dataSource = new DroveServiceDataSource<>( + config, MAPPER, "testns", droveClient); + + val active = dataSource.isActive(); + + assertTrue(active); + + // Verify active status + val activeMeter = metricRegistry.getMeters().get( + METRIC_PREFIX + ".dataStoreType.DROVE.dataSource.drove-svc-src-3.active"); + assertNotNull(activeMeter, "Active status meter should be recorded"); + assertEquals(1, activeMeter.getCount()); + } + } + + // ==================== isActive() - Unhealthy (after failed call) ==================== + + @Test + @SneakyThrows + void testIsActive_afterFailedCall_recordsInactiveStatus(WireMockRuntimeInfo wm) { + stubFor(get("/apis/v1/applications") + .withBasicAuth("guest", "guest") + .willReturn(aResponse().withStatus(500).withBody("Error"))); + + val config = buildConfig(wm, "drove-svc-src-4"); + try (val droveClient = buildClient(config)) { + val dataSource = new DroveServiceDataSource<>( + config, MAPPER, "testns", droveClient); + + // First call fails and sets upstreamAvailable to false + assertThrows(DroveCommunicationException.class, dataSource::services); + + val active = dataSource.isActive(); + + assertFalse(active); + + // Verify inactive status + val inactiveMeter = metricRegistry.getMeters().get( + METRIC_PREFIX + ".dataStoreType.DROVE.dataSource.drove-svc-src-4.inactive"); + assertNotNull(inactiveMeter, "Inactive status meter should be recorded"); + assertEquals(1, inactiveMeter.getCount()); + } + } + + // ==================== Helpers ==================== + + private DroveUpstreamConfig buildConfig(WireMockRuntimeInfo wm, String upstreamId) { + return DroveUpstreamConfig.builder() + .id(upstreamId) + .endpoints(List.of("http://localhost:" + wm.getHttpPort())) + .username("guest") + .password("guest") + .skipCaching(true) + .build(); + } + + private DroveCommunicator buildClient(DroveUpstreamConfig config) { + return RangerDroveUtils.buildDroveClient("testns", config, MAPPER); + } +} diff --git a/ranger-http-client/src/main/java/io/appform/ranger/client/http/AbstractRangerHttpHubClient.java b/ranger-http-client/src/main/java/io/appform/ranger/client/http/AbstractRangerHttpHubClient.java index 5dd6910b..aff83c23 100644 --- a/ranger-http-client/src/main/java/io/appform/ranger/client/http/AbstractRangerHttpHubClient.java +++ b/ranger-http-client/src/main/java/io/appform/ranger/client/http/AbstractRangerHttpHubClient.java @@ -49,8 +49,8 @@ public abstract class AbstractRangerHttpHubClient nodeSelector = new RandomServiceNodeSelector<>(); @Override - protected ServiceDataSource getDefaultDataSource() { - return new HttpServiceDataSource<>(clientConfig, + protected ServiceDataSource getDefaultDataSource(String upstreamId) { + return new HttpServiceDataSource<>(upstreamId, clientConfig, Objects.requireNonNullElseGet(getHttpClient(), () -> RangerHttpUtils.httpClient( clientConfig, diff --git a/ranger-http-client/src/main/java/io/appform/ranger/client/http/SimpleRangerHttpClient.java b/ranger-http-client/src/main/java/io/appform/ranger/client/http/SimpleRangerHttpClient.java index 6eebe5c5..440f4f23 100644 --- a/ranger-http-client/src/main/java/io/appform/ranger/client/http/SimpleRangerHttpClient.java +++ b/ranger-http-client/src/main/java/io/appform/ranger/client/http/SimpleRangerHttpClient.java @@ -57,6 +57,7 @@ public void start() { requireNonNull(deserializer, "deserializer can't be null"); this.serviceFinder = HttpServiceFinderBuilders.httpUnshardedServiceFinderBuilider() + .withUpstreamId(clientConfig.getId()) .withClientConfig(clientConfig) .withServiceName(serviceName) .withNamespace(namespace) diff --git a/ranger-http-client/src/test/java/io/appform/ranger/client/http/BaseRangerHttpClientTest.java b/ranger-http-client/src/test/java/io/appform/ranger/client/http/BaseRangerHttpClientTest.java index cd748f2f..f086c00f 100644 --- a/ranger-http-client/src/test/java/io/appform/ranger/client/http/BaseRangerHttpClientTest.java +++ b/ranger-http-client/src/test/java/io/appform/ranger/client/http/BaseRangerHttpClientTest.java @@ -76,6 +76,7 @@ public void prepareHttpMocks() throws Exception { .withStatus(200))); httpClientConfig = HttpClientConfig.builder() + .id("test-metric") .host("127.0.0.1") .port(wireMockExtension.getPort()) .connectionTimeoutMs(30_000) diff --git a/ranger-http-client/src/test/java/io/appform/ranger/client/http/ShardedRangerHttpClientTest.java b/ranger-http-client/src/test/java/io/appform/ranger/client/http/ShardedRangerHttpClientTest.java index 956c5ff6..0c420c95 100644 --- a/ranger-http-client/src/test/java/io/appform/ranger/client/http/ShardedRangerHttpClientTest.java +++ b/ranger-http-client/src/test/java/io/appform/ranger/client/http/ShardedRangerHttpClientTest.java @@ -34,6 +34,7 @@ void testShardedHttpHubClient(){ .deserializer(this::read) .mapper(getObjectMapper()) .nodeRefreshTimeMs(1000) + .upstreamId("test-metric") .build(); client.start(); val service = RangerTestUtils.getService("test-n", "test-s"); diff --git a/ranger-http-client/src/test/java/io/appform/ranger/client/http/UnshardedRangerHttpClientTest.java b/ranger-http-client/src/test/java/io/appform/ranger/client/http/UnshardedRangerHttpClientTest.java index b4b64325..03749275 100644 --- a/ranger-http-client/src/test/java/io/appform/ranger/client/http/UnshardedRangerHttpClientTest.java +++ b/ranger-http-client/src/test/java/io/appform/ranger/client/http/UnshardedRangerHttpClientTest.java @@ -34,6 +34,7 @@ void testUnshardedRangerHubClient(){ .deserializer(this::read) .mapper(getObjectMapper()) .nodeRefreshTimeMs(1000) + .upstreamId("test-metric") .build(); client.start(); val service = RangerTestUtils.getService("test-n", "test-s"); diff --git a/ranger-http/pom.xml b/ranger-http/pom.xml index 890c9cc5..8802d0d3 100644 --- a/ranger-http/pom.xml +++ b/ranger-http/pom.xml @@ -29,6 +29,10 @@ ranger-http HTTP and Hub based Discovery For Ranger + + 2.0.1.Final + + io.appform.ranger @@ -54,6 +58,11 @@ ${wiremock.version} test + + javax.validation + validation-api + ${javax.validation.version} + io.appform.ranger ranger-core diff --git a/ranger-http/src/main/java/io/appform/ranger/http/common/HttpNodeDataStoreConnector.java b/ranger-http/src/main/java/io/appform/ranger/http/common/HttpNodeDataStoreConnector.java index 210a6e02..4244ca05 100644 --- a/ranger-http/src/main/java/io/appform/ranger/http/common/HttpNodeDataStoreConnector.java +++ b/ranger-http/src/main/java/io/appform/ranger/http/common/HttpNodeDataStoreConnector.java @@ -15,7 +15,9 @@ */ package io.appform.ranger.http.common; +import io.appform.ranger.core.model.DataStoreType; import io.appform.ranger.core.model.NodeDataStoreConnector; +import io.appform.ranger.core.util.MetricRecorder; import io.appform.ranger.http.config.HttpClientConfig; import io.appform.ranger.http.servicefinder.HttpCommunicator; import lombok.extern.slf4j.Slf4j; @@ -60,6 +62,7 @@ protected int defaultPort() { @Override public boolean isActive() { + MetricRecorder.recordNoteDataSourceStatus(DataStoreType.HTTP, config.getId(),true); return true; } } diff --git a/ranger-http/src/main/java/io/appform/ranger/http/config/HttpClientConfig.java b/ranger-http/src/main/java/io/appform/ranger/http/config/HttpClientConfig.java index dbcac3a8..65d41c87 100644 --- a/ranger-http/src/main/java/io/appform/ranger/http/config/HttpClientConfig.java +++ b/ranger-http/src/main/java/io/appform/ranger/http/config/HttpClientConfig.java @@ -20,6 +20,8 @@ import lombok.Value; import lombok.extern.jackson.Jacksonized; +import javax.validation.constraints.NotBlank; + /** * */ @@ -28,6 +30,9 @@ @Jacksonized @AllArgsConstructor public class HttpClientConfig { + + @NotBlank + String id; String host; int port; boolean secure; diff --git a/ranger-http/src/main/java/io/appform/ranger/http/servicefinder/HttpApiCommunicator.java b/ranger-http/src/main/java/io/appform/ranger/http/servicefinder/HttpApiCommunicator.java index b5ee80f9..efaabbd2 100644 --- a/ranger-http/src/main/java/io/appform/ranger/http/servicefinder/HttpApiCommunicator.java +++ b/ranger-http/src/main/java/io/appform/ranger/http/servicefinder/HttpApiCommunicator.java @@ -17,19 +17,13 @@ package io.appform.ranger.http.servicefinder; import com.fasterxml.jackson.databind.ObjectMapper; +import io.appform.ranger.core.model.DataStoreType; import io.appform.ranger.core.model.Service; import io.appform.ranger.core.model.ServiceNode; +import io.appform.ranger.core.util.MetricRecorder; import io.appform.ranger.http.config.HttpClientConfig; import io.appform.ranger.http.model.ServiceDataSourceResponse; import io.appform.ranger.http.serde.HTTPResponseDataDeserializer; -import lombok.Getter; -import lombok.extern.slf4j.Slf4j; -import lombok.val; -import okhttp3.HttpUrl; -import okhttp3.OkHttpClient; -import okhttp3.Request; -import okhttp3.Response; - import java.util.List; import java.util.Objects; import java.util.Set; @@ -38,6 +32,15 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Supplier; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import lombok.val; +import okhttp3.HttpUrl; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; + +import static io.appform.ranger.core.util.MetricRecorder.*; /** * Direct api based communication @@ -47,6 +50,7 @@ public class HttpApiCommunicator implements HttpCommunicator { private final AtomicBoolean upstreamAvailable = new AtomicBoolean(true); private final ScheduledExecutorService resetter = Executors.newSingleThreadScheduledExecutor(); + private final String upstreamId; @Getter private final OkHttpClient httpClient; private final HttpClientConfig config; @@ -54,6 +58,7 @@ public class HttpApiCommunicator implements HttpCommunicator { public HttpApiCommunicator(OkHttpClient httpClient, HttpClientConfig config, ObjectMapper mapper) { Objects.requireNonNull(mapper, "mapper has not been set for node data"); + this.upstreamId = config.getId(); this.httpClient = httpClient; this.config = config; this.mapper = mapper; @@ -81,6 +86,7 @@ public Set services() { .build(); try (val response = httpClient.newCall(request).execute()) { + MetricRecorder.recordRemoteCallStatusCode(DataStoreType.HTTP, upstreamId, SERVICES_LIST, response.code()); if (response.isSuccessful()) { return parseServices(response, httpUrl); } @@ -90,6 +96,7 @@ public Set services() { } } catch (Exception e) { + MetricRecorder.recordRemoteCallUnknownFailure(DataStoreType.HTTP, upstreamId, SERVICES_LIST, e.getClass().getSimpleName()); throw new HttpCommunicationException( "Error parsing the response from server for url: " + httpUrl + " with exception " + e.getClass().getSimpleName() + ": " + e.getMessage()); @@ -118,14 +125,16 @@ public List> listNodes( .build(); try (val response = httpClient.newCall(request).execute()) { + MetricRecorder.recordRemoteCallStatusCode(DataStoreType.HTTP, upstreamId, LIST_NODES, response.code()); if (response.isSuccessful()) { - return parseNodeList(deserializer, response, httpUrl); + return parseNodeList(service, deserializer, response, httpUrl); } else { throw new HttpCommunicationException("HTTP call failed. url: " + httpUrl + " status: " + response.code()); } } catch (Exception e) { + MetricRecorder.recordRemoteCallUnknownFailure(DataStoreType.HTTP, upstreamId, LIST_NODES, e.getClass().getSimpleName(), service.getServiceName()); throw new HttpCommunicationException("Error getting node data from the http endpoint: " + httpUrl + ". Error: " + e.getMessage()); } @@ -162,6 +171,12 @@ private Set parseServices(Response response, HttpUrl httpUrl) { else { val bytes = body.bytes(); val serviceDataSourceResponse = mapper.readValue(bytes, ServiceDataSourceResponse.class); + + if(serviceDataSourceResponse == null || serviceDataSourceResponse.getData() == null || serviceDataSourceResponse.getData().isEmpty()) { + MetricRecorder.recordNullOrEmptyServicesListResponse(DataStoreType.HTTP, upstreamId); + log.warn("Received empty services list from http. Response body: {}", response.body()); + } + if (serviceDataSourceResponse.valid()) { return serviceDataSourceResponse.getData(); } @@ -170,25 +185,36 @@ private Set parseServices(Response response, HttpUrl httpUrl) { "Http call to returned a failure response. Url:" + httpUrl + " data: " + serviceDataSourceResponse); } } - } - catch (Exception e) { + } catch (HttpCommunicationException httpCommunicationException){ + throw httpCommunicationException; + } catch (Exception e) { + MetricRecorder.recordServicesParseFailure(DataStoreType.HTTP, upstreamId); throw new HttpCommunicationException( "Error reading data from server. Url: " + httpUrl + "Error: " + e.getMessage()); } } - private static List> parseNodeList( + private List> parseNodeList( + Service service, HTTPResponseDataDeserializer deserializer, Response response, HttpUrl httpUrl) { try (val body = response.body()) { if (null == body) { log.warn("HTTP call to {} returned empty body", httpUrl); + MetricRecorder.recordNullOrEmptyListNodeResponse(DataStoreType.HTTP, upstreamId); + MetricRecorder.recordNullOrEmptyListNodeResponse(DataStoreType.HTTP, upstreamId, service.getServiceName()); throw new HttpCommunicationException("Empty response received for call to " + httpUrl); } else { val bytes = body.bytes(); val serviceNodesResponse = deserializer.deserialize(bytes); + + if(serviceNodesResponse == null || serviceNodesResponse.getData() == null || serviceNodesResponse.getData().isEmpty()) { + MetricRecorder.recordNullOrEmptyListNodeResponse(DataStoreType.HTTP, upstreamId); + MetricRecorder.recordNullOrEmptyListNodeResponse(DataStoreType.HTTP, upstreamId, service.getServiceName()); + } + if (serviceNodesResponse.valid()) { return serviceNodesResponse.getData(); } @@ -197,8 +223,11 @@ private static List> parseNodeList( "Http call returned null nodes for url: " + httpUrl + " response: " + serviceNodesResponse); } } - } - catch (Exception e) { + } catch (HttpCommunicationException httpCommunicationException){ + throw httpCommunicationException; + } catch (Exception e) { + MetricRecorder.recordListNodesParseFailure(DataStoreType.HTTP, upstreamId); + MetricRecorder.recordListNodesParseFailure(DataStoreType.HTTP, upstreamId, service.getServiceName()); throw new HttpCommunicationException( "Error parsing node data from server. Url: " + httpUrl + "Error: " + e.getMessage()); } diff --git a/ranger-http/src/main/java/io/appform/ranger/http/servicefinder/HttpNodeDataSource.java b/ranger-http/src/main/java/io/appform/ranger/http/servicefinder/HttpNodeDataSource.java index d88f94a6..fbf30844 100644 --- a/ranger-http/src/main/java/io/appform/ranger/http/servicefinder/HttpNodeDataSource.java +++ b/ranger-http/src/main/java/io/appform/ranger/http/servicefinder/HttpNodeDataSource.java @@ -15,9 +15,12 @@ */ package io.appform.ranger.http.servicefinder; + +import io.appform.ranger.core.model.DataStoreType; import io.appform.ranger.core.model.NodeDataSource; import io.appform.ranger.core.model.Service; import io.appform.ranger.core.model.ServiceNode; +import io.appform.ranger.core.util.MetricRecorder; import io.appform.ranger.http.common.HttpNodeDataStoreConnector; import io.appform.ranger.http.config.HttpClientConfig; import io.appform.ranger.http.serde.HTTPResponseDataDeserializer; @@ -37,21 +40,33 @@ @Slf4j public class HttpNodeDataSource> extends HttpNodeDataStoreConnector implements NodeDataSource { + private final String upstreamId; private final Service service; private final AtomicBoolean upstreamAvailable = new AtomicBoolean(true); private final ScheduledExecutorService resetter = Executors.newSingleThreadScheduledExecutor(); public HttpNodeDataSource( + final String upstreamId, final Service service, final HttpClientConfig config, final HttpCommunicator httpCommunicator) { super(config, httpCommunicator); Objects.requireNonNull(config, "client config has not been set for node data"); Objects.requireNonNull(httpCommunicator, "http communicator has not been set for node data"); + this.upstreamId = upstreamId; this.service = service; resetter.scheduleWithFixedDelay(() -> upstreamAvailable.set(true), 0, 60, TimeUnit.SECONDS); } + public String getUpstreamId() { + return upstreamId; + } + + @Override + public DataStoreType getDataStoreType() { + return DataStoreType.HTTP; + } + @Override public Optional>> refresh(D deserializer) { return Optional.of(httpCommunicator.listNodes(service, deserializer)); @@ -59,6 +74,8 @@ public Optional>> refresh(D deserializer) { @Override public boolean isActive() { - return upstreamAvailable.get(); + var httpUpstreamAvailable = upstreamAvailable.get(); + MetricRecorder.recordNoteDataSourceStatus(DataStoreType.HTTP, upstreamId, httpUpstreamAvailable); + return httpUpstreamAvailable; } } diff --git a/ranger-http/src/main/java/io/appform/ranger/http/servicefinder/HttpShardedServiceFinderBuilder.java b/ranger-http/src/main/java/io/appform/ranger/http/servicefinder/HttpShardedServiceFinderBuilder.java index 44d3cf6e..d269dfa7 100644 --- a/ranger-http/src/main/java/io/appform/ranger/http/servicefinder/HttpShardedServiceFinderBuilder.java +++ b/ranger-http/src/main/java/io/appform/ranger/http/servicefinder/HttpShardedServiceFinderBuilder.java @@ -56,8 +56,8 @@ public SimpleShardedServiceFinder build() { } @Override - protected NodeDataSource> dataSource(Service service) { - return new HttpNodeDataSource<>(service, clientConfig, + protected NodeDataSource> dataSource(String upstreamId, Service service) { + return new HttpNodeDataSource<>(upstreamId, service, clientConfig, Objects.requireNonNullElseGet(httpCommunicator, () -> RangerHttpUtils.httpClient(clientConfig, mapper))); } diff --git a/ranger-http/src/main/java/io/appform/ranger/http/servicefinder/HttpUnshardedServiceFinderBuilider.java b/ranger-http/src/main/java/io/appform/ranger/http/servicefinder/HttpUnshardedServiceFinderBuilider.java index fae76ea2..625c7cc5 100644 --- a/ranger-http/src/main/java/io/appform/ranger/http/servicefinder/HttpUnshardedServiceFinderBuilider.java +++ b/ranger-http/src/main/java/io/appform/ranger/http/servicefinder/HttpUnshardedServiceFinderBuilider.java @@ -54,8 +54,8 @@ public SimpleUnshardedServiceFinder build() { } @Override - protected NodeDataSource> dataSource(Service service) { - return new HttpNodeDataSource<>(service, clientConfig, + protected NodeDataSource> dataSource(String upstreamId, Service service) { + return new HttpNodeDataSource<>(upstreamId, service, clientConfig, Objects.requireNonNullElseGet(httpClient, () -> RangerHttpUtils.httpClient(clientConfig, mapper))); } diff --git a/ranger-http/src/main/java/io/appform/ranger/http/servicefinderhub/HttpServiceDataSource.java b/ranger-http/src/main/java/io/appform/ranger/http/servicefinderhub/HttpServiceDataSource.java index a52cc214..71eec43f 100644 --- a/ranger-http/src/main/java/io/appform/ranger/http/servicefinderhub/HttpServiceDataSource.java +++ b/ranger-http/src/main/java/io/appform/ranger/http/servicefinderhub/HttpServiceDataSource.java @@ -15,26 +15,44 @@ */ package io.appform.ranger.http.servicefinderhub; + import io.appform.ranger.core.finderhub.ServiceDataSource; import io.appform.ranger.core.model.Service; +import io.appform.ranger.core.model.DataStoreType; +import io.appform.ranger.core.util.MetricRecorder; import io.appform.ranger.http.common.HttpNodeDataStoreConnector; import io.appform.ranger.http.config.HttpClientConfig; import io.appform.ranger.http.servicefinder.HttpCommunicator; import lombok.extern.slf4j.Slf4j; +import lombok.val; import java.util.Collection; import java.util.Objects; +import static io.appform.ranger.core.util.MetricRecorder.FAILURE; +import static io.appform.ranger.core.util.MetricRecorder.SUCCESS; + @Slf4j public class HttpServiceDataSource extends HttpNodeDataStoreConnector implements ServiceDataSource { - public HttpServiceDataSource(HttpClientConfig config, HttpCommunicator httpClient) { + private final String upstreamId; + + public HttpServiceDataSource(String upstreamId, HttpClientConfig config, HttpCommunicator httpClient) { super(config, httpClient); + this.upstreamId = upstreamId; } @Override public Collection services() { Objects.requireNonNull(config, "client config has not been set for node data"); - return httpCommunicator.services(); + try { + val result = httpCommunicator.services(); + MetricRecorder.recordServicesFetchStatus(DataStoreType.HTTP, upstreamId, SUCCESS); + return result; + } + catch (Exception e) { + MetricRecorder.recordServicesFetchStatus(DataStoreType.HTTP, upstreamId, FAILURE); + throw e; + } } } diff --git a/ranger-http/src/main/java/io/appform/ranger/http/servicefinderhub/HttpShardedServiceFinderFactory.java b/ranger-http/src/main/java/io/appform/ranger/http/servicefinderhub/HttpShardedServiceFinderFactory.java index b1b1f508..88ea5e4a 100644 --- a/ranger-http/src/main/java/io/appform/ranger/http/servicefinderhub/HttpShardedServiceFinderFactory.java +++ b/ranger-http/src/main/java/io/appform/ranger/http/servicefinderhub/HttpShardedServiceFinderFactory.java @@ -61,6 +61,7 @@ public HttpShardedServiceFinderFactory( @Override public ServiceFinder> buildFinder(Service service) { val serviceFinder = new HttpShardedServiceFinderBuilder() + .withUpstreamId(clientConfig.getId()) .withClientConfig(clientConfig) .withObjectMapper(mapper) .withHttpCommunicator(httpClient) diff --git a/ranger-http/src/main/java/io/appform/ranger/http/servicefinderhub/HttpUnshardedServiceFinderFactory.java b/ranger-http/src/main/java/io/appform/ranger/http/servicefinderhub/HttpUnshardedServiceFinderFactory.java index 51c8cb02..5cd5a400 100644 --- a/ranger-http/src/main/java/io/appform/ranger/http/servicefinderhub/HttpUnshardedServiceFinderFactory.java +++ b/ranger-http/src/main/java/io/appform/ranger/http/servicefinderhub/HttpUnshardedServiceFinderFactory.java @@ -60,6 +60,7 @@ public HttpUnshardedServiceFinderFactory( @Override public ServiceFinder> buildFinder(Service service) { val serviceFinder = new HttpUnshardedServiceFinderBuilider() + .withUpstreamId(clientConfig.getId()) .withClientConfig(clientConfig) .withObjectMapper(mapper) .withHttpClient(httpClient) diff --git a/ranger-http/src/main/java/io/appform/ranger/http/serviceprovider/HttpNodeDataSink.java b/ranger-http/src/main/java/io/appform/ranger/http/serviceprovider/HttpNodeDataSink.java index f8506aa3..2830358d 100644 --- a/ranger-http/src/main/java/io/appform/ranger/http/serviceprovider/HttpNodeDataSink.java +++ b/ranger-http/src/main/java/io/appform/ranger/http/serviceprovider/HttpNodeDataSink.java @@ -17,10 +17,13 @@ import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; + +import io.appform.ranger.core.model.DataStoreType; import io.appform.ranger.core.model.NodeDataSink; import io.appform.ranger.core.model.Service; import io.appform.ranger.core.model.ServiceNode; import io.appform.ranger.core.util.Exceptions; +import io.appform.ranger.core.util.MetricRecorder; import io.appform.ranger.http.common.HttpNodeDataStoreConnector; import io.appform.ranger.http.config.HttpClientConfig; import io.appform.ranger.http.model.ServiceRegistrationResponse; @@ -31,24 +34,38 @@ import okhttp3.HttpUrl; import okhttp3.Request; import okhttp3.RequestBody; +import okhttp3.ResponseBody; import java.io.IOException; import java.util.Optional; +import static io.appform.ranger.core.util.MetricRecorder.*; import static java.util.Objects.requireNonNull; @Slf4j public class HttpNodeDataSink> extends HttpNodeDataStoreConnector implements NodeDataSink { + private final String upstreamId; private final Service service; private final ObjectMapper mapper; - public HttpNodeDataSink(Service service, HttpClientConfig config, ObjectMapper mapper, HttpCommunicator httpClient) { + public HttpNodeDataSink(String upstreamId, Service service, HttpClientConfig config, ObjectMapper mapper, HttpCommunicator httpClient) { super(config, httpClient); + this.upstreamId = upstreamId; this.service = service; this.mapper = mapper; } + @Override + public DataStoreType getDataStoreType() { + return DataStoreType.HTTP; + } + + @Override + public String getUpstreamId() { + return upstreamId; + } + @Override public void updateState(S serializer, ServiceNode serviceNode) { requireNonNull(config, "client config has not been set for node data"); @@ -67,28 +84,46 @@ public void updateState(S serializer, ServiceNode serviceNode) { : config.getPort()) .encodedPath(url) .build(); - val requestBody = RequestBody.create(serializer.serialize(serviceNode)); - val serviceRegistrationResponse = registerService(httpUrl, requestBody).orElse(null); + val serializedData = getSerializedData(service.getServiceName(), serializer, serviceNode); + val requestBody = RequestBody.create(serializedData); + val serviceRegistrationResponse = registerService(service.getServiceName(), httpUrl, requestBody).orElse(null); if(null == serviceRegistrationResponse || !serviceRegistrationResponse.valid()){ log.warn("Http call to {} returned a failure response {}", httpUrl, serviceRegistrationResponse); + recordNullOrEmptyRegisterServiceResponse(); Exceptions.illegalState("Error updating state on the server for node data: " + httpUrl); } + MetricRecorder.recordNodeDataSinkUpdateStatus(DataStoreType.HTTP, upstreamId, SUCCESS); + } + + private void recordNullOrEmptyRegisterServiceResponse() { + MetricRecorder.recordNullOrEmptyRegisterServiceResponse(DataStoreType.HTTP, upstreamId, service.getServiceName()); + MetricRecorder.recordNodeDataSinkUpdateStatus(DataStoreType.HTTP, upstreamId, FAILURE); } - private Optional> registerService(HttpUrl httpUrl, RequestBody requestBody){ + private > byte[] getSerializedData(String serviceName, S serializer, ServiceNode serviceNode) { + try { + return serializer.serialize(serviceNode); + } catch (Exception e) { + MetricRecorder.recordNodeDataSinkSerDeFailure(DataStoreType.HTTP, upstreamId, MetricRecorder.SERIALIZATION, serviceName, e.getClass().getSimpleName()); + log.error("Error serializing data for service {} with node {} with exception", serviceName, serviceNode, e); + throw e; + } + } + + private Optional> registerService(String serviceName, HttpUrl httpUrl, RequestBody requestBody){ val request = new Request.Builder() .url(httpUrl) .post(requestBody) .build(); try (val response = httpCommunicator.getHttpClient().newCall(request).execute()) { + MetricRecorder.recordRemoteCallStatusCode(DataStoreType.HTTP, upstreamId, REGISTER_SERVICE, response.code()); if (response.isSuccessful()) { try (val body = response.body()) { if (null == body) { log.warn("HTTP call to {} returned empty body", httpUrl); } else { - return Optional.of(mapper.readValue(body.bytes(), - new TypeReference>() {})); + return Optional.of(parseRegisterServiceResponse(serviceName, body)); } } } @@ -97,8 +132,21 @@ private Optional> registerService(HttpUrl httpUrl } } catch (IOException e) { + MetricRecorder.recordNodeDataSinkUnknownFailure(DataStoreType.HTTP, upstreamId, serviceName, e.getClass().getSimpleName()); log.error("Error updating state on the server with httpUrl {} with exception {} ", httpUrl, e); } return Optional.empty(); } + + private ServiceRegistrationResponse parseRegisterServiceResponse(String serviceName, ResponseBody body) throws IOException { + try { + return mapper.readValue(body.bytes(), + new TypeReference>() { + }); + } + catch (IOException e) { + MetricRecorder.recordNodeDataSinkSerDeFailure(DataStoreType.HTTP, upstreamId, DESERIALIZATION, serviceName, e.getClass().getSimpleName()); + throw e; + } + } } diff --git a/ranger-http/src/main/java/io/appform/ranger/http/serviceprovider/HttpShardedServiceProviderBuilder.java b/ranger-http/src/main/java/io/appform/ranger/http/serviceprovider/HttpShardedServiceProviderBuilder.java index 4caec43d..6b6868ad 100644 --- a/ranger-http/src/main/java/io/appform/ranger/http/serviceprovider/HttpShardedServiceProviderBuilder.java +++ b/ranger-http/src/main/java/io/appform/ranger/http/serviceprovider/HttpShardedServiceProviderBuilder.java @@ -56,8 +56,8 @@ public ServiceProvider> build() { } @Override - protected NodeDataSink> dataSink(Service service) { - return new HttpNodeDataSink<>(service, clientConfig, mapper, + protected NodeDataSink> dataSink(String upstreamId, Service service) { + return new HttpNodeDataSink<>(upstreamId, service, clientConfig, mapper, Objects.requireNonNullElseGet(httpClient, () -> RangerHttpUtils.httpClient(clientConfig, mapper))); } diff --git a/ranger-http/src/test/java/io/appform/ranger/http/servicefinder/HttpApiCommunicatorMetricsIntegrationTest.java b/ranger-http/src/test/java/io/appform/ranger/http/servicefinder/HttpApiCommunicatorMetricsIntegrationTest.java new file mode 100644 index 00000000..e7275279 --- /dev/null +++ b/ranger-http/src/test/java/io/appform/ranger/http/servicefinder/HttpApiCommunicatorMetricsIntegrationTest.java @@ -0,0 +1,369 @@ +/* + * Copyright 2024 Authors, Flipkart Internet Pvt. Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.appform.ranger.http.servicefinder; + +import com.codahale.metrics.MetricRegistry; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo; +import com.github.tomakehurst.wiremock.junit5.WireMockTest; +import io.appform.ranger.core.model.Service; +import io.appform.ranger.core.model.ServiceNode; +import io.appform.ranger.core.util.MetricRecorder; +import io.appform.ranger.http.config.HttpClientConfig; +import io.appform.ranger.http.model.ServiceDataSourceResponse; +import io.appform.ranger.http.model.ServiceNodesResponse; +import io.appform.ranger.http.serde.HTTPResponseDataDeserializer; +import io.appform.ranger.http.utils.RangerHttpUtils; +import lombok.val; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.util.List; +import java.util.Set; + +import static com.github.tomakehurst.wiremock.client.WireMock.*; +import static org.junit.jupiter.api.Assertions.*; + +@WireMockTest +class HttpApiCommunicatorMetricsIntegrationTest { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + private static final String METRIC_PREFIX = "io.appform.ranger"; + private MetricRegistry metricRegistry; + + @BeforeEach + void setUp() { + metricRegistry = new MetricRegistry(); + MetricRecorder.initialize(metricRegistry); + } + + // ==================== services() - Success ==================== + + @Test + void testServices_success_recordsStatusCodeAndNoFailure(WireMockRuntimeInfo wmInfo) throws Exception { + val responseObj = ServiceDataSourceResponse.builder() + .data(Set.of(new Service("ns", "svc1"), new Service("ns", "svc2"))) + .build(); + stubFor(get(urlPathEqualTo("/ranger/services/v1")) + .willReturn(aResponse() + .withBody(MAPPER.writeValueAsBytes(responseObj)) + .withStatus(200))); + + val config = buildConfig(wmInfo, "http-metric-1"); + val communicator = RangerHttpUtils.httpClient(config, MAPPER); + val services = communicator.services(); + + assertNotNull(services); + assertEquals(2, services.size()); + + // Verify status code 200 meter + val statusMeter = metricRegistry.getMeters().get( + METRIC_PREFIX + ".dataStoreType.HTTP.dataSource.http-metric-1.httpCall.services.responseStatus.200"); + assertNotNull(statusMeter, "200 status meter should be recorded"); + assertEquals(1, statusMeter.getCount()); + + // No unknown failure + assertNull(metricRegistry.getMeters().get( + METRIC_PREFIX + ".dataStoreType.HTTP.dataSource.http-metric-1.httpCall.services.unknownFailure")); + + // No parse failure + assertNull(metricRegistry.getMeters().get( + METRIC_PREFIX + ".dataStoreType.HTTP.dataSource.http-metric-1.httpCall.services.responseParseFailure")); + } + + // ==================== services() - Empty response ==================== + + @Test + void testServices_emptyData_recordsNullOrEmptyServicesResponse(WireMockRuntimeInfo wmInfo) throws Exception { + val responseObj = ServiceDataSourceResponse.builder() + .data(Set.of()) // empty set + .build(); + stubFor(get(urlPathEqualTo("/ranger/services/v1")) + .willReturn(aResponse() + .withBody(MAPPER.writeValueAsBytes(responseObj)) + .withStatus(200))); + + val config = buildConfig(wmInfo, "http-metric-2"); + val communicator = RangerHttpUtils.httpClient(config, MAPPER); + + // Empty set is valid (data != null), so services() returns successfully with empty set + val services = communicator.services(); + assertNotNull(services); + assertTrue(services.isEmpty()); + + // Verify null/empty services meter is still recorded + val emptyMeter = metricRegistry.getMeters().get( + METRIC_PREFIX + ".dataStoreType.HTTP.dataSource.http-metric-2.httpCall.services.nullOrEmptyResponse"); + assertNotNull(emptyMeter, "Null/empty services meter should be recorded"); + assertEquals(1, emptyMeter.getCount()); + } + + // ==================== services() - Non-200 response ==================== + + @Test + void testServices_serverError_recordsStatusCodeAndUnknownFailure(WireMockRuntimeInfo wmInfo) { + stubFor(get(urlPathEqualTo("/ranger/services/v1")) + .willReturn(aResponse() + .withStatus(500) + .withBody("Internal Server Error"))); + + val config = buildConfig(wmInfo, "http-metric-3"); + val communicator = RangerHttpUtils.httpClient(config, MAPPER); + + assertThrows(Exception.class, communicator::services); + + // Verify 500 status code meter + val statusMeter = metricRegistry.getMeters().get( + METRIC_PREFIX + ".dataStoreType.HTTP.dataSource.http-metric-3.httpCall.services.responseStatus.500"); + assertNotNull(statusMeter, "500 status meter should be recorded"); + assertEquals(1, statusMeter.getCount()); + + // Verify unknown failure meter (HttpCommunicationException re-thrown) + val failureMeter = metricRegistry.getMeters().get( + METRIC_PREFIX + ".dataStoreType.HTTP.dataSource.http-metric-3.httpCall.services.unknownFailure"); + assertNotNull(failureMeter, "Unknown failure meter should be recorded"); + assertTrue(failureMeter.getCount() >= 1); + } + + // ==================== services() - Invalid JSON (parse failure) ==================== + + @Test + void testServices_invalidJson_recordsParseFailure(WireMockRuntimeInfo wmInfo) { + stubFor(get(urlPathEqualTo("/ranger/services/v1")) + .willReturn(aResponse() + .withBody("not-valid-json{{{") + .withStatus(200))); + + val config = buildConfig(wmInfo, "http-metric-4"); + val communicator = RangerHttpUtils.httpClient(config, MAPPER); + + assertThrows(Exception.class, communicator::services); + + // Verify parse failure meter + val parseMeter = metricRegistry.getMeters().get( + METRIC_PREFIX + ".dataStoreType.HTTP.dataSource.http-metric-4.httpCall.services.responseParseFailure"); + assertNotNull(parseMeter, "Services parse failure meter should be recorded"); + assertEquals(1, parseMeter.getCount()); + } + + // ==================== services() - Connection refused (unknown failure) ==================== + + @Test + void testServices_connectionRefused_recordsUnknownFailure() { + // Use a port that is not listening + val config = HttpClientConfig.builder() + .id("http-metric-5") + .host("127.0.0.1") + .port(19999) // unlikely to have something listening + .connectionTimeoutMs(500) + .operationTimeoutMs(500) + .build(); + val communicator = RangerHttpUtils.httpClient(config, MAPPER); + + assertThrows(Exception.class, communicator::services); + + // Verify unknown failure meter + val failureMeter = metricRegistry.getMeters().get( + METRIC_PREFIX + ".dataStoreType.HTTP.dataSource.http-metric-5.httpCall.services.unknownFailure"); + assertNotNull(failureMeter, "Unknown failure meter should be recorded on connection error"); + assertEquals(1, failureMeter.getCount()); + } + + // ==================== listNodes() - Success ==================== + + @Test + void testListNodes_success_recordsStatusCode(WireMockRuntimeInfo wmInfo) throws Exception { + val nodeResponse = ServiceNodesResponse.builder() + .data(List.of( + ServiceNode.builder().host("h1").port(8080).nodeData("data1").build(), + ServiceNode.builder().host("h2").port(8081).nodeData("data2").build() + )) + .build(); + stubFor(get(urlPathEqualTo("/ranger/nodes/v1/test-ns/test-svc")) + .willReturn(aResponse() + .withBody(MAPPER.writeValueAsBytes(nodeResponse)) + .withStatus(200))); + + val config = buildConfig(wmInfo, "http-metric-6"); + val communicator = RangerHttpUtils.httpClient(config, MAPPER); + val service = new Service("test-ns", "test-svc"); + + HTTPResponseDataDeserializer deserializer = data -> { + try { + return MAPPER.readValue(data, MAPPER.getTypeFactory() + .constructParametricType(ServiceNodesResponse.class, String.class)); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + }; + + val nodes = communicator.listNodes(service, deserializer); + + assertNotNull(nodes); + assertEquals(2, nodes.size()); + + // Verify 200 status code for listNodes + val statusMeter = metricRegistry.getMeters().get( + METRIC_PREFIX + ".dataStoreType.HTTP.dataSource.http-metric-6.httpCall.listNodes.responseStatus.200"); + assertNotNull(statusMeter, "200 status meter for listNodes should exist"); + assertEquals(1, statusMeter.getCount()); + } + + // ==================== listNodes() - Server error ==================== + + @Test + void testListNodes_serverError_recordsStatusCodeAndUnknownFailure(WireMockRuntimeInfo wmInfo) { + stubFor(get(urlPathEqualTo("/ranger/nodes/v1/test-ns/error-svc")) + .willReturn(aResponse() + .withStatus(503) + .withBody("Service Unavailable"))); + + val config = buildConfig(wmInfo, "http-metric-7"); + val communicator = RangerHttpUtils.httpClient(config, MAPPER); + val service = new Service("test-ns", "error-svc"); + + HTTPResponseDataDeserializer deserializer = data -> { + try { + return MAPPER.readValue(data, MAPPER.getTypeFactory() + .constructParametricType(ServiceNodesResponse.class, String.class)); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + }; + + assertThrows(Exception.class, () -> communicator.listNodes(service, deserializer)); + + // Verify 503 status code + val statusMeter = metricRegistry.getMeters().get( + METRIC_PREFIX + ".dataStoreType.HTTP.dataSource.http-metric-7.httpCall.listNodes.responseStatus.503"); + assertNotNull(statusMeter, "503 status meter for listNodes should exist"); + assertEquals(1, statusMeter.getCount()); + + // Verify unknown failure (with service name) + val failureMeter = metricRegistry.getMeters().get( + METRIC_PREFIX + ".dataStoreType.HTTP.dataSource.http-metric-7.httpCall.listNodes.unknownFailure"); + assertNotNull(failureMeter, "Unknown failure meter for listNodes should exist"); + assertTrue(failureMeter.getCount() >= 1); + } + + // ==================== listNodes() - Empty body ==================== + + @Test + void testListNodes_emptyBody_recordsNullOrEmptyListNodeResponse(WireMockRuntimeInfo wmInfo) { + stubFor(get(urlPathEqualTo("/ranger/nodes/v1/test-ns/empty-svc")) + .willReturn(aResponse() + .withStatus(200) + .withBody(""))); // empty body + + val config = buildConfig(wmInfo, "http-metric-8"); + val communicator = RangerHttpUtils.httpClient(config, MAPPER); + val service = new Service("test-ns", "empty-svc"); + + HTTPResponseDataDeserializer deserializer = data -> { + try { + return MAPPER.readValue(data, MAPPER.getTypeFactory() + .constructParametricType(ServiceNodesResponse.class, String.class)); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + }; + + assertThrows(Exception.class, () -> communicator.listNodes(service, deserializer)); + + // The 200 status code should be recorded + val statusMeter = metricRegistry.getMeters().get( + METRIC_PREFIX + ".dataStoreType.HTTP.dataSource.http-metric-8.httpCall.listNodes.responseStatus.200"); + assertNotNull(statusMeter, "200 status meter should exist even for empty body"); + assertEquals(1, statusMeter.getCount()); + } + + // ==================== listNodes() - Invalid JSON (parse failure) ==================== + + @Test + void testListNodes_invalidJson_recordsParseFailure(WireMockRuntimeInfo wmInfo) { + stubFor(get(urlPathEqualTo("/ranger/nodes/v1/test-ns/parse-fail-svc")) + .willReturn(aResponse() + .withStatus(200) + .withBody("{{invalid json}}"))); + + val config = buildConfig(wmInfo, "http-metric-9"); + val communicator = RangerHttpUtils.httpClient(config, MAPPER); + val service = new Service("test-ns", "parse-fail-svc"); + + HTTPResponseDataDeserializer deserializer = data -> { + throw new RuntimeException("Parse error simulation"); + }; + + assertThrows(Exception.class, () -> communicator.listNodes(service, deserializer)); + + // The listNodes parse failure meter should exist + val aggregateParseMeter = metricRegistry.getMeters().get( + METRIC_PREFIX + ".dataStoreType.HTTP.dataSource.http-metric-9.httpCall.listNodes.responseParseFailure"); + assertNotNull(aggregateParseMeter, "Aggregate list nodes parse failure meter should be recorded"); + assertEquals(1, aggregateParseMeter.getCount()); + + val parseMeter = metricRegistry.getMeters().get( + METRIC_PREFIX + ".dataStoreType.HTTP.dataSource.http-metric-9.httpCall.listNodes.serviceName.parse-fail-svc.responseParseFailure"); + assertNotNull(parseMeter, "List nodes parse failure meter should be recorded"); + assertEquals(1, parseMeter.getCount()); + } + + // ==================== listNodes() - Connection refused ==================== + + @Test + void testListNodes_connectionRefused_recordsUnknownFailureWithServiceName() { + val config = HttpClientConfig.builder() + .id("http-metric-10") + .host("127.0.0.1") + .port(19998) + .connectionTimeoutMs(500) + .operationTimeoutMs(500) + .build(); + val communicator = RangerHttpUtils.httpClient(config, MAPPER); + val service = new Service("test-ns", "conn-fail-svc"); + + HTTPResponseDataDeserializer deserializer = data -> + ServiceNodesResponse.builder().data(List.of()).build(); + + assertThrows(Exception.class, () -> communicator.listNodes(service, deserializer)); + + // Verify unknown failure meter (generic) + val genericFailureMeter = metricRegistry.getMeters().get( + METRIC_PREFIX + ".dataStoreType.HTTP.dataSource.http-metric-10.httpCall.listNodes.unknownFailure"); + assertNotNull(genericFailureMeter, "Generic unknown failure meter should exist"); + assertEquals(1, genericFailureMeter.getCount()); + + // Verify service-specific unknown failure meter + val svcFailureMeter = metricRegistry.getMeters().get( + METRIC_PREFIX + ".dataStoreType.HTTP.dataSource.http-metric-10.httpCall.listNodes.serviceName.conn-fail-svc.unknownFailure"); + assertNotNull(svcFailureMeter, "Service-specific unknown failure meter should exist"); + assertEquals(1, svcFailureMeter.getCount()); + } + + // ==================== Helper ==================== + + private HttpClientConfig buildConfig(WireMockRuntimeInfo wmInfo, String upstreamId) { + return HttpClientConfig.builder() + .id(upstreamId) + .host("127.0.0.1") + .port(wmInfo.getHttpPort()) + .connectionTimeoutMs(30_000) + .operationTimeoutMs(30_000) + .build(); + } +} diff --git a/ranger-http/src/test/java/io/appform/ranger/http/servicefinder/HttpShardedServiceFinderBuilderTest.java b/ranger-http/src/test/java/io/appform/ranger/http/servicefinder/HttpShardedServiceFinderBuilderTest.java index 88c1b6cb..9fc42a80 100644 --- a/ranger-http/src/test/java/io/appform/ranger/http/servicefinder/HttpShardedServiceFinderBuilderTest.java +++ b/ranger-http/src/test/java/io/appform/ranger/http/servicefinder/HttpShardedServiceFinderBuilderTest.java @@ -88,6 +88,7 @@ void testFinder(WireMockRuntimeInfo wireMockRuntimeInfo) throws Exception { }) .withShardSelector((criteria, registry) -> registry.nodeList()) .withNodeRefreshIntervalMs(1000) + .withUpstreamId("test-metric") .build(); finder.start(); RangerTestUtils.sleepUntilFinderStarts(finder); diff --git a/ranger-http/src/test/java/io/appform/ranger/http/servicefinderhub/HttpServiceDataSourceMetricsIntegrationTest.java b/ranger-http/src/test/java/io/appform/ranger/http/servicefinderhub/HttpServiceDataSourceMetricsIntegrationTest.java new file mode 100644 index 00000000..724daebc --- /dev/null +++ b/ranger-http/src/test/java/io/appform/ranger/http/servicefinderhub/HttpServiceDataSourceMetricsIntegrationTest.java @@ -0,0 +1,147 @@ +/* + * Copyright 2024 Authors, Flipkart Internet Pvt. Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.appform.ranger.http.servicefinderhub; + +import com.codahale.metrics.MetricRegistry; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo; +import com.github.tomakehurst.wiremock.junit5.WireMockTest; +import io.appform.ranger.core.model.Service; +import io.appform.ranger.core.util.MetricRecorder; +import io.appform.ranger.http.config.HttpClientConfig; +import io.appform.ranger.http.model.ServiceDataSourceResponse; +import io.appform.ranger.http.utils.RangerHttpUtils; +import lombok.val; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.Set; + +import static com.github.tomakehurst.wiremock.client.WireMock.*; +import static org.junit.jupiter.api.Assertions.*; + +@WireMockTest +class HttpServiceDataSourceMetricsIntegrationTest { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + private static final String METRIC_PREFIX = "io.appform.ranger"; + private MetricRegistry metricRegistry; + + @BeforeEach + void setUp() { + metricRegistry = new MetricRegistry(); + MetricRecorder.initialize(metricRegistry); + } + + @Test + void testServices_success_recordsServicesFetchSuccess(WireMockRuntimeInfo wmInfo) throws Exception { + val responseObj = ServiceDataSourceResponse.builder() + .data(Set.of(new Service("ns", "svc1"))) + .build(); + stubFor(get(urlPathEqualTo("/ranger/services/v1")) + .willReturn(aResponse() + .withBody(MAPPER.writeValueAsBytes(responseObj)) + .withStatus(200))); + + val config = buildConfig(wmInfo, "sds-metric-1"); + val httpServiceDataSource = new HttpServiceDataSource<>( + "sds-metric-1", config, RangerHttpUtils.httpClient(config, MAPPER)); + val services = httpServiceDataSource.services(); + + assertNotNull(services); + assertFalse(services.isEmpty()); + + // Verify services fetch success meter + val successMeter = metricRegistry.getMeters().get( + METRIC_PREFIX + ".dataStoreType.HTTP.dataSource.sds-metric-1.services.fetch.success"); + assertNotNull(successMeter, "Services fetch success meter should exist"); + assertEquals(1, successMeter.getCount()); + + // No failure + assertNull(metricRegistry.getMeters().get( + METRIC_PREFIX + ".dataStoreType.HTTP.dataSource.sds-metric-1.services.fetch.failure")); + } + + @Test + void testServices_failure_recordsServicesFetchFailure(WireMockRuntimeInfo wmInfo) { + stubFor(get(urlPathEqualTo("/ranger/services/v1")) + .willReturn(aResponse() + .withStatus(500) + .withBody("error"))); + + val config = buildConfig(wmInfo, "sds-metric-2"); + val httpServiceDataSource = new HttpServiceDataSource<>( + "sds-metric-2", config, RangerHttpUtils.httpClient(config, MAPPER)); + + assertThrows(Exception.class, httpServiceDataSource::services); + + // Verify services fetch failure meter + val failureMeter = metricRegistry.getMeters().get( + METRIC_PREFIX + ".dataStoreType.HTTP.dataSource.sds-metric-2.services.fetch.failure"); + assertNotNull(failureMeter, "Services fetch failure meter should exist"); + assertEquals(1, failureMeter.getCount()); + + // No success + assertNull(metricRegistry.getMeters().get( + METRIC_PREFIX + ".dataStoreType.HTTP.dataSource.sds-metric-2.services.fetch.success")); + } + + @Test + void testServices_connectionRefused_recordsServicesFetchFailure() { + val config = HttpClientConfig.builder() + .id("sds-metric-3") + .host("127.0.0.1") + .port(19997) // not listening + .connectionTimeoutMs(500) + .operationTimeoutMs(500) + .build(); + val httpServiceDataSource = new HttpServiceDataSource<>( + "sds-metric-3", config, RangerHttpUtils.httpClient(config, MAPPER)); + + assertThrows(Exception.class, httpServiceDataSource::services); + + val failureMeter = metricRegistry.getMeters().get( + METRIC_PREFIX + ".dataStoreType.HTTP.dataSource.sds-metric-3.services.fetch.failure"); + assertNotNull(failureMeter, "Services fetch failure meter should exist on connection error"); + assertEquals(1, failureMeter.getCount()); + } + + @Test + void testIsActive_recordsDataSourceStatus(WireMockRuntimeInfo wmInfo) { + val config = buildConfig(wmInfo, "sds-metric-4"); + val httpServiceDataSource = new HttpServiceDataSource<>( + "sds-metric-4", config, RangerHttpUtils.httpClient(config, MAPPER)); + + val active = httpServiceDataSource.isActive(); + assertTrue(active, "HTTP data source should always be active"); + + // HttpNodeDataStoreConnector.isActive() records status with config.getId() + val statusMeter = metricRegistry.getMeters().get( + METRIC_PREFIX + ".dataStoreType.HTTP.dataSource.sds-metric-4.active"); + assertNotNull(statusMeter, "Active status meter should be recorded"); + assertEquals(1, statusMeter.getCount()); + } + + private HttpClientConfig buildConfig(WireMockRuntimeInfo wmInfo, String upstreamId) { + return HttpClientConfig.builder() + .id(upstreamId) + .host("127.0.0.1") + .port(wmInfo.getHttpPort()) + .connectionTimeoutMs(30_000) + .operationTimeoutMs(30_000) + .build(); + } +} diff --git a/ranger-http/src/test/java/io/appform/ranger/http/servicefinderhub/HttpServiceDataSourceTest.java b/ranger-http/src/test/java/io/appform/ranger/http/servicefinderhub/HttpServiceDataSourceTest.java index 9a967926..362f2944 100644 --- a/ranger-http/src/test/java/io/appform/ranger/http/servicefinderhub/HttpServiceDataSourceTest.java +++ b/ranger-http/src/test/java/io/appform/ranger/http/servicefinderhub/HttpServiceDataSourceTest.java @@ -65,7 +65,7 @@ void testServiceDataSource(WireMockRuntimeInfo wireMockRuntimeInfo) throws IOExc .connectionTimeoutMs(30_000) .operationTimeoutMs(30_000) .build(); - val httpServiceDataSource = new HttpServiceDataSource<>(clientConfig, + val httpServiceDataSource = new HttpServiceDataSource<>(null, clientConfig, RangerHttpUtils.httpClient(clientConfig, MAPPER)); val services = httpServiceDataSource.services(); Assertions.assertNotNull(services); @@ -86,7 +86,7 @@ void testServiceDataSource(WireMockRuntimeInfo wireMockRuntimeInfo) throws IOExc .operationTimeoutMs(30_000) .replicationSource(true) .build(); - val httpServiceDataSource = new HttpServiceDataSource<>(clientConfig, + val httpServiceDataSource = new HttpServiceDataSource<>(null, clientConfig, RangerHttpUtils.httpClient(clientConfig, MAPPER)); val services = httpServiceDataSource.services(); Assertions.assertNotNull(services); diff --git a/ranger-http/src/test/java/io/appform/ranger/http/serviceprovider/HttpShardedServiceProviderBuilderTest.java b/ranger-http/src/test/java/io/appform/ranger/http/serviceprovider/HttpShardedServiceProviderBuilderTest.java index d60e3794..a0ec57c7 100644 --- a/ranger-http/src/test/java/io/appform/ranger/http/serviceprovider/HttpShardedServiceProviderBuilderTest.java +++ b/ranger-http/src/test/java/io/appform/ranger/http/serviceprovider/HttpShardedServiceProviderBuilderTest.java @@ -89,6 +89,7 @@ void testProvider(WireMockRuntimeInfo wireMockRuntimeInfo) throws Exception { .withNodeData(farmNodeData) .withSerializer(node -> requestBytes) .healthUpdateHandler(healthUpdateHandler) + .withUpstreamId("test-metric") .build(); serviceProvider.start(); Assertions.assertNotNull(serviceProvider); diff --git a/ranger-hub-server-bundle-core/ranger-hub-server-bundle/src/main/java/io/appform/ranger/hub/server/bundle/RangerHubServerBundle.java b/ranger-hub-server-bundle-core/ranger-hub-server-bundle/src/main/java/io/appform/ranger/hub/server/bundle/RangerHubServerBundle.java index 473f7b53..b10d71f5 100644 --- a/ranger-hub-server-bundle-core/ranger-hub-server-bundle/src/main/java/io/appform/ranger/hub/server/bundle/RangerHubServerBundle.java +++ b/ranger-hub-server-bundle-core/ranger-hub-server-bundle/src/main/java/io/appform/ranger/hub/server/bundle/RangerHubServerBundle.java @@ -100,6 +100,7 @@ private RangerHubClient> addCurat .build(); curatorFrameworks.add(curatorFramework); return UnshardedRangerZKHubClient.builder() + .upstreamId(zkConfiguration.getId()) .namespace(namespace) .connectionString(zookeeper) .curatorFramework(curatorFramework) @@ -125,6 +126,7 @@ private RangerHubClient> addCurat private RangerHubClient> getHttpHubClient( HttpClientConfig httpClientConfig, RangerHttpUpstreamConfiguration httpConfiguration) { return UnshardedRangerHttpHubClient.builder() + .upstreamId(httpClientConfig.getId()) .namespace(namespace) .mapper(getMapper()) .clientConfig(httpClientConfig) @@ -153,6 +155,7 @@ private RangerHubClient> getDrove DroveUpstreamConfig.DEFAULT_REGION_TAG_NAME); val droveCommunicator = RangerDroveUtils.buildDroveClient(namespace, droveConfig, getMapper()); return UnshardedRangerDroveHubClient.builder() + .upstreamId(droveConfig.getId()) .namespace(namespace) .mapper(getMapper()) .clientConfig(droveConfig) @@ -197,9 +200,7 @@ public List>> vis @Override public List>> visit(RangerZkUpstreamConfiguration rangerZkConfiguration) { - return rangerZkConfiguration.getZookeepers().stream() - .map(zk -> addCuratorAndGetZkHubClient(zk, rangerZkConfiguration)) - .toList(); + return List.of(addCuratorAndGetZkHubClient(rangerZkConfiguration.getZookeeper(), rangerZkConfiguration)); } @Override diff --git a/ranger-hub-server-bundle-core/src/main/java/io/appform/ranger/hub/server/bundle/configuration/RangerZkUpstreamConfiguration.java b/ranger-hub-server-bundle-core/src/main/java/io/appform/ranger/hub/server/bundle/configuration/RangerZkUpstreamConfiguration.java index e1a3a650..1071b828 100644 --- a/ranger-hub-server-bundle-core/src/main/java/io/appform/ranger/hub/server/bundle/configuration/RangerZkUpstreamConfiguration.java +++ b/ranger-hub-server-bundle-core/src/main/java/io/appform/ranger/hub/server/bundle/configuration/RangerZkUpstreamConfiguration.java @@ -18,15 +18,13 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import io.appform.ranger.core.model.HubConstants; import io.appform.ranger.hub.server.bundle.models.BackendType; +import javax.validation.constraints.NotBlank; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.ToString; -import javax.validation.Valid; import javax.validation.constraints.Max; import javax.validation.constraints.Min; -import javax.validation.constraints.NotEmpty; -import java.util.List; @Data @EqualsAndHashCode(callSuper = true) @@ -34,9 +32,11 @@ @JsonIgnoreProperties(ignoreUnknown = true) public class RangerZkUpstreamConfiguration extends RangerUpstreamConfiguration { - @NotEmpty - @Valid - private List zookeepers; + @NotBlank + private String id; + + @NotBlank + private String zookeeper; private boolean disablePushUpdaters; diff --git a/ranger-hub-server-bundle/src/main/java/io/appform/ranger/hub/server/bundle/RangerHubServerBundle.java b/ranger-hub-server-bundle/src/main/java/io/appform/ranger/hub/server/bundle/RangerHubServerBundle.java index a02d7200..c1b3bcae 100644 --- a/ranger-hub-server-bundle/src/main/java/io/appform/ranger/hub/server/bundle/RangerHubServerBundle.java +++ b/ranger-hub-server-bundle/src/main/java/io/appform/ranger/hub/server/bundle/RangerHubServerBundle.java @@ -111,6 +111,7 @@ private RangerHubClient> addCurat .build(); curatorFrameworks.add(curatorFramework); return UnshardedRangerZKHubClient.builder() + .upstreamId(zkConfiguration.getId()) .namespace(namespace) .connectionString(zookeeper) .curatorFramework(curatorFramework) @@ -137,6 +138,7 @@ private RangerHubClient> addCurat private RangerHubClient> getHttpHubClient( HttpClientConfig httpClientConfig, RangerHttpUpstreamConfiguration httpConfiguration) { return UnshardedRangerHttpHubClient.builder() + .upstreamId(httpClientConfig.getId()) .namespace(namespace) .mapper(getMapper()) .clientConfig(httpClientConfig) @@ -166,6 +168,7 @@ private RangerHubClient> getDrove DroveUpstreamConfig.DEFAULT_REGION_TAG_NAME); val droveCommunicator = RangerDroveUtils.buildDroveClient(namespace, droveConfig, getMapper()); return UnshardedRangerDroveHubClient.builder() + .upstreamId(droveConfig.getId()) .namespace(namespace) .mapper(getMapper()) .clientConfig(droveConfig) @@ -211,9 +214,7 @@ public List>> vis @Override public List>> visit(RangerZkUpstreamConfiguration rangerZkConfiguration) { - return rangerZkConfiguration.getZookeepers().stream() - .map(zk -> addCuratorAndGetZkHubClient(zk, rangerZkConfiguration)) - .toList(); + return List.of(addCuratorAndGetZkHubClient(rangerZkConfiguration.getZookeeper(), rangerZkConfiguration)); } @Override diff --git a/ranger-hub-server-bundle/src/test/java/io/appform/ranger/hub/server/bundle/RangerHubServerBundleTest.java b/ranger-hub-server-bundle/src/test/java/io/appform/ranger/hub/server/bundle/RangerHubServerBundleTest.java index 677f394c..a228cf66 100644 --- a/ranger-hub-server-bundle/src/test/java/io/appform/ranger/hub/server/bundle/RangerHubServerBundleTest.java +++ b/ranger-hub-server-bundle/src/test/java/io/appform/ranger/hub/server/bundle/RangerHubServerBundleTest.java @@ -16,6 +16,7 @@ package io.appform.ranger.hub.server.bundle; +import com.codahale.metrics.MetricRegistry; import com.codahale.metrics.health.HealthCheckRegistry; import com.fasterxml.jackson.databind.ObjectMapper; import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo; @@ -67,6 +68,7 @@ class TestConfig extends Configuration { .upstreams(List.of( new RangerHttpUpstreamConfiguration() .setHttpClientConfigs(List.of(HttpClientConfig.builder() + .id("test-metric") .host("localhost") .port(wm.getHttpPort()) .build())) @@ -90,6 +92,7 @@ class TestConfig extends Configuration { when(environment.healthChecks()).thenReturn(healthChecks); when(environment.admin()).thenReturn(adminEnvironment); when(environment.getObjectMapper()).thenReturn(mapper); + when(environment.metrics()).thenReturn(new MetricRegistry()); when(bootstrap.getHealthCheckRegistry()).thenReturn(mock(HealthCheckRegistry.class)); val bundle = new RangerHubServerBundle() { diff --git a/ranger-hub-server-dw5-bundle/src/main/java/io/appform/ranger/hub/server/bundle/RangerHubServerBundle.java b/ranger-hub-server-dw5-bundle/src/main/java/io/appform/ranger/hub/server/bundle/RangerHubServerBundle.java index a2a02a0b..7f445899 100644 --- a/ranger-hub-server-dw5-bundle/src/main/java/io/appform/ranger/hub/server/bundle/RangerHubServerBundle.java +++ b/ranger-hub-server-dw5-bundle/src/main/java/io/appform/ranger/hub/server/bundle/RangerHubServerBundle.java @@ -111,6 +111,7 @@ private RangerHubClient> addCurat .build(); curatorFrameworks.add(curatorFramework); return UnshardedRangerZKHubClient.builder() + .upstreamId(zkConfiguration.getId()) .namespace(namespace) .connectionString(zookeeper) .curatorFramework(curatorFramework) @@ -137,6 +138,7 @@ private RangerHubClient> addCurat private RangerHubClient> getHttpHubClient( HttpClientConfig httpClientConfig, RangerHttpUpstreamConfiguration httpConfiguration) { return UnshardedRangerHttpHubClient.builder() + .upstreamId(httpClientConfig.getId()) .namespace(namespace) .mapper(getMapper()) .clientConfig(httpClientConfig) @@ -166,6 +168,7 @@ private RangerHubClient> getDrove DroveUpstreamConfig.DEFAULT_REGION_TAG_NAME); val droveCommunicator = RangerDroveUtils.buildDroveClient(namespace, droveConfig, getMapper()); return UnshardedRangerDroveHubClient.builder() + .upstreamId(droveConfig.getId()) .namespace(namespace) .mapper(getMapper()) .clientConfig(droveConfig) @@ -211,9 +214,7 @@ public List>> vis @Override public List>> visit(RangerZkUpstreamConfiguration rangerZkConfiguration) { - return rangerZkConfiguration.getZookeepers().stream() - .map(zk -> addCuratorAndGetZkHubClient(zk, rangerZkConfiguration)) - .toList(); + return List.of(addCuratorAndGetZkHubClient(rangerZkConfiguration.getZookeeper(), rangerZkConfiguration)); } @Override diff --git a/ranger-hub-server-dw5-bundle/src/test/java/io/appform/ranger/hub/server/bundle/RangerHubServerBundleTest.java b/ranger-hub-server-dw5-bundle/src/test/java/io/appform/ranger/hub/server/bundle/RangerHubServerBundleTest.java index a5d8e7c3..6b44167b 100644 --- a/ranger-hub-server-dw5-bundle/src/test/java/io/appform/ranger/hub/server/bundle/RangerHubServerBundleTest.java +++ b/ranger-hub-server-dw5-bundle/src/test/java/io/appform/ranger/hub/server/bundle/RangerHubServerBundleTest.java @@ -16,6 +16,7 @@ package io.appform.ranger.hub.server.bundle; +import com.codahale.metrics.MetricRegistry; import com.codahale.metrics.health.HealthCheckRegistry; import com.fasterxml.jackson.databind.ObjectMapper; import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo; @@ -68,11 +69,12 @@ class TestConfig extends Configuration { private final RangerServerConfiguration upstreams = RangerServerConfiguration.builder() .namespace("test") .upstreams(ImmutableList.builder() - .add(new RangerHttpUpstreamConfiguration() - .setHttpClientConfigs(List.of(HttpClientConfig.builder() - .host("localhost") - .port(wm.getHttpPort()) - .build()))) + .add(new RangerHttpUpstreamConfiguration() + .setHttpClientConfigs(List.of(HttpClientConfig.builder() + .id("test-metric") + .host("localhost") + .port(wm.getHttpPort()) + .build()))) .build()) .build(); @@ -92,6 +94,7 @@ class TestConfig extends Configuration { when(environment.healthChecks()).thenReturn(healthChecks); when(environment.admin()).thenReturn(adminEnvironment); when(environment.getObjectMapper()).thenReturn(mapper); + when(environment.metrics()).thenReturn(new MetricRegistry()); when(bootstrap.getHealthCheckRegistry()).thenReturn(mock(HealthCheckRegistry.class)); val bundle = new RangerHubServerBundle() { diff --git a/ranger-id/perf/results/io.appform.ranger.id.IdGeneratorPerfTest.testGenerate.json b/ranger-id/perf/results/io.appform.ranger.id.IdGeneratorPerfTest.testGenerate.json index f514d3a7..5f29a5c5 100644 --- a/ranger-id/perf/results/io.appform.ranger.id.IdGeneratorPerfTest.testGenerate.json +++ b/ranger-id/perf/results/io.appform.ranger.id.IdGeneratorPerfTest.testGenerate.json @@ -4,5 +4,5 @@ "iterations" : 4, "threads" : 1, "forks" : 3, - "mean_ops" : 983476.854812921 + "mean_ops" : 650814.0256598704 } \ No newline at end of file diff --git a/ranger-id/perf/results/io.appform.ranger.id.IdGeneratorPerfTest.testGenerateBase36.json b/ranger-id/perf/results/io.appform.ranger.id.IdGeneratorPerfTest.testGenerateBase36.json index 2c9dfd65..75ec8538 100644 --- a/ranger-id/perf/results/io.appform.ranger.id.IdGeneratorPerfTest.testGenerateBase36.json +++ b/ranger-id/perf/results/io.appform.ranger.id.IdGeneratorPerfTest.testGenerateBase36.json @@ -4,5 +4,5 @@ "iterations" : 4, "threads" : 1, "forks" : 3, - "mean_ops" : 920553.6244546492 + "mean_ops" : 516175.3456572488 } \ No newline at end of file diff --git a/ranger-id/src/main/java/io/appform/ranger/id/CollisionChecker.java b/ranger-id/src/main/java/io/appform/ranger/id/CollisionChecker.java index 7e897a27..cb630caf 100644 --- a/ranger-id/src/main/java/io/appform/ranger/id/CollisionChecker.java +++ b/ranger-id/src/main/java/io/appform/ranger/id/CollisionChecker.java @@ -31,8 +31,18 @@ public class CollisionChecker { private final BitSet bitSet = new BitSet(1000); private long currentInstant = 0; - private final Lock dataLock = new ReentrantLock(); + private static final long NANO_TIME_MS; + private static final long CURRENT_TIME_MS; + private static final long NANO_TO_EPOCH_OFFSET_MS; + + static { + NANO_TIME_MS = TimeUnit.NANOSECONDS.toMillis(System.nanoTime()); + CURRENT_TIME_MS = System.currentTimeMillis(); + NANO_TO_EPOCH_OFFSET_MS = NANO_TIME_MS - CURRENT_TIME_MS; + log.info("CollisionChecker init: NANO_TIME_MS={}, CURRENT_TIME_MS={}, NANO_TO_EPOCH_OFFSET_MS={}", + NANO_TIME_MS, CURRENT_TIME_MS, NANO_TO_EPOCH_OFFSET_MS); + } private final TimeUnit resolution; @@ -44,22 +54,22 @@ public CollisionChecker(@NonNull TimeUnit resolution) { this.resolution = resolution; } - public boolean check(long timeInMillis, int location) { + public long checkAndGetTime(int location) { dataLock.lock(); try { - long resolvedTime = resolution.convert(timeInMillis, TimeUnit.MILLISECONDS); + long resolvedTime = resolution.convert( + System.nanoTime(), TimeUnit.NANOSECONDS + ) - resolution.convert(NANO_TO_EPOCH_OFFSET_MS, TimeUnit.MILLISECONDS); if (currentInstant != resolvedTime) { currentInstant = resolvedTime; bitSet.clear(); } - if (bitSet.get(location)) { - return false; + return -1; } bitSet.set(location); - return true; - } - finally { + return resolvedTime; + } finally { dataLock.unlock(); } } diff --git a/ranger-id/src/main/java/io/appform/ranger/id/nonce/RandomNonceGenerator.java b/ranger-id/src/main/java/io/appform/ranger/id/nonce/RandomNonceGenerator.java index 76c14210..69a99729 100644 --- a/ranger-id/src/main/java/io/appform/ranger/id/nonce/RandomNonceGenerator.java +++ b/ranger-id/src/main/java/io/appform/ranger/id/nonce/RandomNonceGenerator.java @@ -56,12 +56,11 @@ public void retryEventListener(final ExecutionAttemptedEvent e private NonceInfo random(final CollisionChecker collisionChecker) { int randomGen; - long curTimeMs; + long resolvedTime; do { - curTimeMs = System.currentTimeMillis(); randomGen = secureRandom.nextInt(Constants.MAX_ID_PER_MS); - } while (!collisionChecker.check(curTimeMs, randomGen)); - return new NonceInfo(randomGen, curTimeMs); + resolvedTime = collisionChecker.checkAndGetTime(randomGen); + } while (resolvedTime == -1); + return new NonceInfo(randomGen, resolvedTime); } - } diff --git a/ranger-id/src/test/java/io/appform/ranger/id/CollisionCheckerTest.java b/ranger-id/src/test/java/io/appform/ranger/id/CollisionCheckerTest.java index 7f0775ad..600bf8b6 100644 --- a/ranger-id/src/test/java/io/appform/ranger/id/CollisionCheckerTest.java +++ b/ranger-id/src/test/java/io/appform/ranger/id/CollisionCheckerTest.java @@ -17,24 +17,60 @@ package io.appform.ranger.id; +import java.util.concurrent.TimeUnit; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; - import java.util.stream.IntStream; +import static org.awaitility.Awaitility.await; + + /** * Test on {@link CollisionChecker} */ class CollisionCheckerTest { @Test - void testCheck() { + void testCheckAndGetTimeAndCollision() { + CollisionChecker collisionChecker = new CollisionChecker(); + + long firstAttempt = collisionChecker.checkAndGetTime(1); + long secondAttempt = collisionChecker.checkAndGetTime(1); + + Assertions.assertTrue(firstAttempt > 0, "Should successfully return a valid timestamp"); + + // the second attempt MUST be a collision if still we are on same millisecond (-1) + if (System.currentTimeMillis() == firstAttempt) { + Assertions.assertEquals(-1L, secondAttempt, "Should return -1 on collision"); + } + } + + @Test + void testTimeRolloverClearsBitSet() { CollisionChecker collisionChecker = new CollisionChecker(); - Assertions.assertTrue(collisionChecker.check(100, 1)); - Assertions.assertFalse(collisionChecker.check(100, 1)); + + long firstTime = collisionChecker.checkAndGetTime(10); + Assertions.assertTrue(firstTime > 0); + + await().atMost(101, TimeUnit.MILLISECONDS) + .until(() -> System.currentTimeMillis() > firstTime); + + long secondTime = collisionChecker.checkAndGetTime(10); + + Assertions.assertTrue(secondTime > firstTime, "Time should have moved forward"); + Assertions.assertNotEquals(-1L, secondTime, "Should successfully acquire location 10 in the new millisecond"); + } + + @Test + void testHighVolumeCheckAndGetTimeInSingleMillisecond() { + CollisionChecker collisionChecker = new CollisionChecker(); + + // Generate 1000 IDs as fast as possible. + // We assert that any duplicate location requested in the exact same ms returns -1 IntStream.range(0, 1000).forEach(i -> { - Assertions.assertTrue(collisionChecker.check(101, i)); - Assertions.assertFalse(collisionChecker.check(101, i)); + long time1 = collisionChecker.checkAndGetTime(i); + long time2 = collisionChecker.checkAndGetTime(i); + Assertions.assertNotEquals(time2, time1); }); } } \ No newline at end of file diff --git a/ranger-server-bundle/README.md b/ranger-server-bundle/README.md index eae598f5..24a9736b 100644 --- a/ranger-server-bundle/README.md +++ b/ranger-server-bundle/README.md @@ -15,7 +15,7 @@ Ranger server bundle is a common dropwizard bundle atop which we could implement @Override protected List>> withHubs(AppConfiguration configuration) { - return Lists.newArrayList( + return List.of( RangerServerUtils.buildRangerHub(curatorFramework, rangerConfiguration, environment.getObjectMapper()) ); } diff --git a/ranger-server-bundle/src/main/java/io/appform/ranger/server/bundle/RangerServerBundle.java b/ranger-server-bundle/src/main/java/io/appform/ranger/server/bundle/RangerServerBundle.java index 59b2e451..db4fcfa0 100644 --- a/ranger-server-bundle/src/main/java/io/appform/ranger/server/bundle/RangerServerBundle.java +++ b/ranger-server-bundle/src/main/java/io/appform/ranger/server/bundle/RangerServerBundle.java @@ -20,6 +20,7 @@ import io.appform.ranger.client.RangerHubClient; import io.appform.ranger.core.model.ServiceRegistry; import io.appform.ranger.core.signals.Signal; +import io.appform.ranger.core.util.MetricRecorder; import io.appform.ranger.server.bundle.resources.RangerResource; import io.appform.ranger.server.bundle.rotation.BirTask; import io.appform.ranger.server.bundle.rotation.OorTask; @@ -82,11 +83,14 @@ protected List> withLifecycleSignals(U configuration){ protected abstract List withHealthChecks(U configuration); + protected boolean withMetricsEnabled(U configuration){ + return true; + } + + @Override public void initialize(Bootstrap bootstrap) { - /* - Nothing to init here! - */ + } @@ -101,6 +105,10 @@ public void run(U configuration, Environment environment) { val lifecycleSignals = withLifecycleSignals(configuration); val healthChecks = withHealthChecks(configuration); + if(withMetricsEnabled(configuration)){ + MetricRecorder.initialize(environment.metrics()); + } + environment.admin() .addTask(new OorTask(rotationStatus)); environment.admin() diff --git a/ranger-server-bundle/src/main/java/io/appform/ranger/server/bundle/resources/RangerResource.java b/ranger-server-bundle/src/main/java/io/appform/ranger/server/bundle/resources/RangerResource.java index d30e72da..fbc1ebb2 100644 --- a/ranger-server-bundle/src/main/java/io/appform/ranger/server/bundle/resources/RangerResource.java +++ b/ranger-server-bundle/src/main/java/io/appform/ranger/server/bundle/resources/RangerResource.java @@ -20,6 +20,7 @@ import io.appform.ranger.core.model.Service; import io.appform.ranger.core.model.ServiceNode; import io.appform.ranger.core.model.ServiceRegistry; +import io.appform.ranger.core.util.MetricRecorder; import io.appform.ranger.http.response.model.GenericResponse; import lombok.extern.slf4j.Slf4j; import lombok.val; @@ -53,12 +54,14 @@ public RangerResource(List> rangerHubs) { @Timed public GenericResponse> getServices( @QueryParam("skipDataFromReplicationSources") @DefaultValue("false") boolean skipDataFromReplicationSources) { + val services = rangerHubs.stream() + .filter(hub -> !skipDataFromReplicationSources || !hub.isReplicationSource()) + .map(RangerHubClient::getRegisteredServices) + .flatMap(Collection::stream) + .collect(Collectors.toSet()); + MetricRecorder.recordServicesReturned(services.size()); return GenericResponse.>builder() - .data(rangerHubs.stream() - .filter(hub -> !skipDataFromReplicationSources || !hub.isReplicationSource()) - .map(RangerHubClient::getRegisteredServices) - .flatMap(Collection::stream) - .collect(Collectors.toSet())) + .data(services) .build(); } @@ -70,18 +73,20 @@ public GenericResponse>> getNodes( @NotNull @NotEmpty @PathParam("serviceName") final String serviceName, @QueryParam("skipDataFromReplicationSources") @DefaultValue("false") boolean skipDataFromReplicationSources) { val service = Service.builder().namespace(namespace).serviceName(serviceName).build(); + val serviceNodes = rangerHubs.stream() + .filter(hub -> !(skipDataFromReplicationSources && hub.isReplicationSource())) + .map(hub -> hub.getAllNodes(service)) + .flatMap(List::stream) + .collect(Collectors.toMap(node -> node.getHost() + ":" + node.getPort(), + Function.identity(), + (oldV, newV) -> + oldV.getLastUpdatedTimeStamp() > newV.getLastUpdatedTimeStamp() + ? oldV + : newV)) + .values(); + MetricRecorder.recordServiceNodesReturned(service.getServiceName(), serviceNodes.size()); return GenericResponse.>>builder() - .data(rangerHubs.stream() - .filter(hub -> !(skipDataFromReplicationSources && hub.isReplicationSource())) - .map(hub -> hub.getAllNodes(service)) - .flatMap(List::stream) - .collect(Collectors.toMap(node -> node.getHost() + ":" + node.getPort(), - Function.identity(), - (oldV, newV) -> - oldV.getLastUpdatedTimeStamp() > newV.getLastUpdatedTimeStamp() - ? oldV - : newV)) - .values()) + .data(serviceNodes) .build(); } } diff --git a/ranger-server-dw5-bundle/README.md b/ranger-server-dw5-bundle/README.md index eae598f5..24a9736b 100644 --- a/ranger-server-dw5-bundle/README.md +++ b/ranger-server-dw5-bundle/README.md @@ -15,7 +15,7 @@ Ranger server bundle is a common dropwizard bundle atop which we could implement @Override protected List>> withHubs(AppConfiguration configuration) { - return Lists.newArrayList( + return List.of( RangerServerUtils.buildRangerHub(curatorFramework, rangerConfiguration, environment.getObjectMapper()) ); } diff --git a/ranger-server-dw5-bundle/src/main/java/io/appform/ranger/server/bundle/RangerServerBundle.java b/ranger-server-dw5-bundle/src/main/java/io/appform/ranger/server/bundle/RangerServerBundle.java index 92d17285..c6cbb026 100644 --- a/ranger-server-dw5-bundle/src/main/java/io/appform/ranger/server/bundle/RangerServerBundle.java +++ b/ranger-server-dw5-bundle/src/main/java/io/appform/ranger/server/bundle/RangerServerBundle.java @@ -20,6 +20,7 @@ import io.appform.ranger.client.RangerHubClient; import io.appform.ranger.core.model.ServiceRegistry; import io.appform.ranger.core.signals.Signal; +import io.appform.ranger.core.util.MetricRecorder; import io.appform.ranger.server.bundle.resources.RangerResource; import io.appform.ranger.server.bundle.rotation.BirTask; import io.appform.ranger.server.bundle.rotation.OorTask; @@ -81,15 +82,17 @@ protected List> withLifecycleSignals(U configuration) { return List.of(); } + protected boolean withMetricsEnabled(U configuration) { + return true; + } + protected abstract List> withHubs(U configuration); protected abstract List withHealthChecks(U configuration); @Override public void initialize(Bootstrap bootstrap) { - /* - Nothing to init here! - */ + } @@ -118,6 +121,10 @@ protected void configure() { val lifecycleSignals = withLifecycleSignals(configuration); val healthChecks = withHealthChecks(configuration); + if(withMetricsEnabled(configuration)){ + MetricRecorder.initialize(environment.metrics()); + } + environment.admin() .addTask(new OorTask(rotationStatus)); environment.admin() diff --git a/ranger-server/src/main/java/io/appform/ranger/server/App.java b/ranger-server/src/main/java/io/appform/ranger/server/App.java index 061f02ad..a7a42092 100644 --- a/ranger-server/src/main/java/io/appform/ranger/server/App.java +++ b/ranger-server/src/main/java/io/appform/ranger/server/App.java @@ -21,6 +21,7 @@ import com.fasterxml.jackson.databind.MapperFeature; import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.module.paramnames.ParameterNamesModule; +import io.appform.ranger.core.util.MetricRecorder; import io.appform.ranger.hub.server.bundle.RangerHubServerBundle; import io.appform.ranger.hub.server.bundle.configuration.RangerServerConfiguration; import io.dropwizard.core.Application; @@ -36,6 +37,7 @@ public class App extends Application { @Override public void initialize(Bootstrap bootstrap) { + MetricRecorder.initialize(bootstrap.getMetricRegistry()); bootstrap.addBundle(new RangerHubServerBundle<>() { @Override protected RangerServerConfiguration getRangerConfiguration(AppConfig configuration) { diff --git a/ranger-server/src/main/resources/local.yml b/ranger-server/src/main/resources/local.yml index 8e0c6012..181ebbd3 100644 --- a/ranger-server/src/main/resources/local.yml +++ b/ranger-server/src/main/resources/local.yml @@ -12,7 +12,7 @@ ranger: nodeRefreshTimeMs: 5000 serviceRefreshTimeoutMs: 3000 hubStartTimeoutMs: 5000 - zookeepers: [ "localhost:2181" ] + zookeeper: "localhost:2181" disablePushUpdaters: true diff --git a/ranger-zk-client/src/main/java/io/appform/ranger/client/zk/AbstractRangerZKHubClient.java b/ranger-zk-client/src/main/java/io/appform/ranger/client/zk/AbstractRangerZKHubClient.java index 41872e14..311aa31c 100644 --- a/ranger-zk-client/src/main/java/io/appform/ranger/client/zk/AbstractRangerZKHubClient.java +++ b/ranger-zk-client/src/main/java/io/appform/ranger/client/zk/AbstractRangerZKHubClient.java @@ -53,8 +53,8 @@ protected ServiceFinderHub buildHub() { } @Override - protected ServiceDataSource getDefaultDataSource() { - return new ZkServiceDataSource(getNamespace(), connectionString, curatorFramework); + protected ServiceDataSource getDefaultDataSource(String upstreamId) { + return new ZkServiceDataSource(upstreamId, getNamespace(), connectionString, curatorFramework); } } diff --git a/ranger-zk-client/src/main/java/io/appform/ranger/client/zk/ShardedRangerZKHubClient.java b/ranger-zk-client/src/main/java/io/appform/ranger/client/zk/ShardedRangerZKHubClient.java index 55af8445..e32053ff 100644 --- a/ranger-zk-client/src/main/java/io/appform/ranger/client/zk/ShardedRangerZKHubClient.java +++ b/ranger-zk-client/src/main/java/io/appform/ranger/client/zk/ShardedRangerZKHubClient.java @@ -41,6 +41,7 @@ public class ShardedRangerZKHubClient @Override protected ServiceFinderFactory> getFinderFactory() { return ZkShardedServiceFinderFactory.builder() + .upstreamId(getUpstreamId()) .curatorFramework(getCuratorFramework()) .connectionString(getConnectionString()) .nodeRefreshIntervalMs(getNodeRefreshTimeMs()) diff --git a/ranger-zk-client/src/main/java/io/appform/ranger/client/zk/SimpleRangerZKClient.java b/ranger-zk-client/src/main/java/io/appform/ranger/client/zk/SimpleRangerZKClient.java index 10e00a1c..e4cb2904 100644 --- a/ranger-zk-client/src/main/java/io/appform/ranger/client/zk/SimpleRangerZKClient.java +++ b/ranger-zk-client/src/main/java/io/appform/ranger/client/zk/SimpleRangerZKClient.java @@ -18,7 +18,6 @@ import com.fasterxml.jackson.databind.ObjectMapper; import io.appform.ranger.client.AbstractRangerClient; import io.appform.ranger.core.finder.SimpleShardedServiceFinder; -import io.appform.ranger.core.finder.nodeselector.RandomServiceNodeSelector; import io.appform.ranger.core.finder.serviceregistry.MapBasedServiceRegistry; import io.appform.ranger.core.finder.shardselector.MatchingShardSelector; import io.appform.ranger.core.model.HubConstants; @@ -41,6 +40,7 @@ @SuperBuilder public class SimpleRangerZKClient extends AbstractRangerClient> { + private String upstreamId; private final String serviceName; private final String namespace; private final ObjectMapper mapper; @@ -58,6 +58,7 @@ public class SimpleRangerZKClient extends AbstractRangerClientshardedFinderBuilder() + .withUpstreamId(upstreamId) .withCuratorFramework(curatorFramework) .withNamespace(namespace) .withServiceName(serviceName) diff --git a/ranger-zk-client/src/main/java/io/appform/ranger/client/zk/UnshardedRangerZKHubClient.java b/ranger-zk-client/src/main/java/io/appform/ranger/client/zk/UnshardedRangerZKHubClient.java index 2b3eea83..4367c3ab 100644 --- a/ranger-zk-client/src/main/java/io/appform/ranger/client/zk/UnshardedRangerZKHubClient.java +++ b/ranger-zk-client/src/main/java/io/appform/ranger/client/zk/UnshardedRangerZKHubClient.java @@ -41,6 +41,7 @@ public class UnshardedRangerZKHubClient @Override protected ServiceFinderFactory> getFinderFactory() { return ZKUnshardedServiceFinderFactory.builder() + .upstreamId(getUpstreamId()) .curatorFramework(getCuratorFramework()) .connectionString(getConnectionString()) .nodeRefreshIntervalMs(getNodeRefreshTimeMs()) diff --git a/ranger-zk-client/src/test/java/io/appform/ranger/client/zk/BaseRangerZKClientTest.java b/ranger-zk-client/src/test/java/io/appform/ranger/client/zk/BaseRangerZKClientTest.java index 67459647..f22252cd 100644 --- a/ranger-zk-client/src/test/java/io/appform/ranger/client/zk/BaseRangerZKClientTest.java +++ b/ranger-zk-client/src/test/java/io/appform/ranger/client/zk/BaseRangerZKClientTest.java @@ -110,6 +110,7 @@ protected void initilizeProvider(){ final HealthUpdateHandler healthUpdateHandler = new LastUpdatedHandler() .setNext(new HealthStatusHandler()); provider = ServiceProviderBuilders.shardedServiceProviderBuilder() + .withUpstreamId("test-metric") .withHostname("localhost") .withPort(1080) .withNamespace("test-n") diff --git a/ranger-zk-client/src/test/java/io/appform/ranger/client/zk/ShardedZKRangerClientTest.java b/ranger-zk-client/src/test/java/io/appform/ranger/client/zk/ShardedZKRangerClientTest.java index 48278988..85b6bd99 100644 --- a/ranger-zk-client/src/test/java/io/appform/ranger/client/zk/ShardedZKRangerClientTest.java +++ b/ranger-zk-client/src/test/java/io/appform/ranger/client/zk/ShardedZKRangerClientTest.java @@ -28,6 +28,7 @@ class ShardedZKRangerClientTest extends BaseRangerZKClientTest { @Test void testShardedHub(){ val zkHubClient = ShardedRangerZKHubClient.builder() + .upstreamId("testzk") .namespace("test-n") .connectionString(getTestingCluster().getConnectString()) .curatorFramework(getCuratorFramework()) diff --git a/ranger-zk-client/src/test/java/io/appform/ranger/client/zk/SimpleRangerZKClientTest.java b/ranger-zk-client/src/test/java/io/appform/ranger/client/zk/SimpleRangerZKClientTest.java index c0603c50..d1e096cb 100644 --- a/ranger-zk-client/src/test/java/io/appform/ranger/client/zk/SimpleRangerZKClientTest.java +++ b/ranger-zk-client/src/test/java/io/appform/ranger/client/zk/SimpleRangerZKClientTest.java @@ -31,6 +31,7 @@ void testBaseClient(){ .serviceName("s1") .disableWatchers(true) .mapper(getObjectMapper()) + .upstreamId("test-metric") .build(); client.start(); Assertions.assertNotNull( client.getNode().orElse(null)); diff --git a/ranger-zk-client/src/test/java/io/appform/ranger/client/zk/UnshardedZKRangerClientTest.java b/ranger-zk-client/src/test/java/io/appform/ranger/client/zk/UnshardedZKRangerClientTest.java index a7a479c0..e9aeefb4 100644 --- a/ranger-zk-client/src/test/java/io/appform/ranger/client/zk/UnshardedZKRangerClientTest.java +++ b/ranger-zk-client/src/test/java/io/appform/ranger/client/zk/UnshardedZKRangerClientTest.java @@ -35,6 +35,7 @@ void testShardedHub(){ .mapper(getObjectMapper()) .deserializer(this::read) .nodeRefreshTimeMs(1000) + .upstreamId("test-metric") .build(); zkHubClient.start(); val service = RangerTestUtils.getService("test-n", "s1"); diff --git a/ranger-zookeeper/src/main/java/io/appform/ranger/zookeeper/common/ZkNodeDataStoreConnector.java b/ranger-zookeeper/src/main/java/io/appform/ranger/zookeeper/common/ZkNodeDataStoreConnector.java index 3f3f7a5e..41eb55fb 100644 --- a/ranger-zookeeper/src/main/java/io/appform/ranger/zookeeper/common/ZkNodeDataStoreConnector.java +++ b/ranger-zookeeper/src/main/java/io/appform/ranger/zookeeper/common/ZkNodeDataStoreConnector.java @@ -18,9 +18,11 @@ import dev.failsafe.Failsafe; import dev.failsafe.Fallback; import dev.failsafe.RetryPolicy; +import io.appform.ranger.core.model.DataStoreType; import io.appform.ranger.core.model.NodeDataStoreConnector; import io.appform.ranger.core.model.Service; import io.appform.ranger.core.util.Exceptions; +import io.appform.ranger.core.util.MetricRecorder; import io.appform.ranger.zookeeper.util.PathBuilder; import lombok.AccessLevel; import lombok.Getter; @@ -37,6 +39,7 @@ @Slf4j public class ZkNodeDataStoreConnector implements NodeDataStoreConnector { + protected final String upstreamId; @Getter(AccessLevel.PROTECTED) protected final Service service; @Getter(AccessLevel.PROTECTED) @@ -61,9 +64,10 @@ public class ZkNodeDataStoreConnector implements NodeDataStoreConnector { .build(); protected ZkNodeDataStoreConnector( - final Service service, + String upstreamId, final Service service, final CuratorFramework curatorFramework, final ZkStoreType storeType) { + this.upstreamId = upstreamId; this.service = service; this.curatorFramework = curatorFramework; this.storeType = storeType; @@ -162,8 +166,10 @@ public void stop() { @Override public boolean isActive() { - return curatorFramework != null && curatorFramework.getZookeeperClient() != null + var zkConnectionActive = curatorFramework != null && curatorFramework.getZookeeperClient() != null && curatorFramework.getZookeeperClient().isConnected(); + MetricRecorder.recordNoteDataSourceStatus(DataStoreType.ZK, upstreamId, zkConnectionActive); + return zkConnectionActive; } protected boolean isStarted() { diff --git a/ranger-zookeeper/src/main/java/io/appform/ranger/zookeeper/servicefinder/ZkNodeDataSource.java b/ranger-zookeeper/src/main/java/io/appform/ranger/zookeeper/servicefinder/ZkNodeDataSource.java index 28cce6a0..f349d863 100644 --- a/ranger-zookeeper/src/main/java/io/appform/ranger/zookeeper/servicefinder/ZkNodeDataSource.java +++ b/ranger-zookeeper/src/main/java/io/appform/ranger/zookeeper/servicefinder/ZkNodeDataSource.java @@ -15,9 +15,11 @@ */ package io.appform.ranger.zookeeper.servicefinder; +import io.appform.ranger.core.model.DataStoreType; import io.appform.ranger.core.model.NodeDataSource; import io.appform.ranger.core.model.Service; import io.appform.ranger.core.model.ServiceNode; +import io.appform.ranger.core.util.MetricRecorder; import io.appform.ranger.zookeeper.common.ZkNodeDataStoreConnector; import io.appform.ranger.zookeeper.common.ZkStoreType; import io.appform.ranger.zookeeper.serde.ZkNodeDataDeserializer; @@ -33,6 +35,7 @@ import java.util.List; import java.util.Optional; +import static io.appform.ranger.core.util.MetricRecorder.LIST_NODES; import static java.util.Objects.requireNonNull; /** @@ -42,9 +45,19 @@ public class ZkNodeDataSource> extends ZkNodeDataStoreConnector implements NodeDataSource { public ZkNodeDataSource( - Service service, + String upstreamId, Service service, CuratorFramework curatorFramework) { - super(service, curatorFramework, ZkStoreType.SOURCE); + super(upstreamId, service, curatorFramework, ZkStoreType.SOURCE); + } + + @Override + public String getUpstreamId() { + return upstreamId; + } + + @Override + public DataStoreType getDataStoreType() { + return DataStoreType.ZK; } @Override @@ -64,8 +77,8 @@ private Optional>> checkForUpdateOnZookeeper(D deserializer) return Optional.empty(); } requireNonNull(deserializer, "Deserializer has not been set for node data"); + val serviceName = service.getServiceName(); try { - val serviceName = service.getServiceName(); if (!isActive()) { log.warn("ZK connection is not active. Ignoring refresh request for service: {}", service.getServiceName()); @@ -76,45 +89,69 @@ private Optional>> checkForUpdateOnZookeeper(D deserializer) val children = curatorFramework.getChildren().forPath(parentPath); List> nodes = new ArrayList<>(children.size()); log.debug("Found {} nodes for [{}]", children.size(), serviceName); + if(children.isEmpty()){ + recordNullOrEmptyResponse(serviceName); + } for (val child : children) { - byte[] data = readChild(parentPath, child).orElse(null); + byte[] data = readChild(serviceName, parentPath, child).orElse(null); if (data == null || data.length == 0) { continue; } - val node = deserializer.deserialize(data); + final var node = parseServiceNodeData(serviceName, deserializer, data); nodes.add(node); } return Optional.of(nodes); } catch (NoNodeException e) { + recordNullOrEmptyResponse(serviceName); log.error( "No ZK container node found for service: {}. Will return empty list for now. Please doublecheck service name", service.getServiceName()); return Optional.of(Collections.emptyList()); } catch (Exception e) { + MetricRecorder.recordZookeeperReadUnknownFailure(DataStoreType.ZK, upstreamId, LIST_NODES, e.getClass().getSimpleName()); log.error("Error getting node data from zookeeper: ", e); throw new ZkCommunicationException("Error getting node data from zookeeper: exception %s , message: %s" .formatted(e.getClass().getSimpleName(), e.getMessage())); } } - private Optional readChild(String parentPath, String child) throws Exception { + private > ServiceNode parseServiceNodeData(String serviceName, D deserializer, byte[] data) { + try { + return deserializer.deserialize(data); + } catch (Exception e) { + MetricRecorder.recordListNodesParseFailure(DataStoreType.ZK, upstreamId); + MetricRecorder.recordListNodesParseFailure(DataStoreType.ZK, upstreamId, serviceName); + log.error("Error deserializing node data : {} for service name: {} ", new String(data), serviceName, e); + throw e; + } + } + + private Optional readChild(String serviceName, String parentPath, String child) throws Exception { final String path = String.format("%s/%s", parentPath, child); try { return Optional.ofNullable(curatorFramework.getData().forPath(path)); } catch (KeeperException.NoNodeException e) { + recordNullOrEmptyResponse(serviceName); log.warn("Node not found for path {}", path); return Optional.empty(); } catch (KeeperException e) { + recordNullOrEmptyResponse(serviceName); log.error("Could not get data for node: {}", path, e); return Optional.empty(); } catch (Exception e){ + MetricRecorder.recordZookeeperReadUnknownFailure(DataStoreType.ZK, upstreamId, LIST_NODES, e.getClass().getSimpleName()); log.error("Could not read child for node: {}", path, e); throw e; } } + private void recordNullOrEmptyResponse(String serviceName) { + MetricRecorder.recordNullOrEmptyListNodeResponse(DataStoreType.ZK, upstreamId); + MetricRecorder.recordNullOrEmptyListNodeResponse(DataStoreType.ZK, upstreamId, serviceName); + } + } diff --git a/ranger-zookeeper/src/main/java/io/appform/ranger/zookeeper/servicefinder/ZkSimpleShardedServiceFinderBuilder.java b/ranger-zookeeper/src/main/java/io/appform/ranger/zookeeper/servicefinder/ZkSimpleShardedServiceFinderBuilder.java index cac3c738..5fd83ded 100644 --- a/ranger-zookeeper/src/main/java/io/appform/ranger/zookeeper/servicefinder/ZkSimpleShardedServiceFinderBuilder.java +++ b/ranger-zookeeper/src/main/java/io/appform/ranger/zookeeper/servicefinder/ZkSimpleShardedServiceFinderBuilder.java @@ -38,9 +38,11 @@ */ @Slf4j public class ZkSimpleShardedServiceFinderBuilder extends SimpleShardedServiceFinderBuilder, ZkNodeDataDeserializer> { + protected CuratorFramework curatorFramework; protected String connectionString; + public ZkSimpleShardedServiceFinderBuilder withCuratorFramework(CuratorFramework curatorFramework) { this.curatorFramework = curatorFramework; return this; @@ -67,8 +69,8 @@ public SimpleShardedServiceFinder build() { } @Override - protected NodeDataSource> dataSource(Service service) { - return new ZkNodeDataSource<>(service, curatorFramework); + protected NodeDataSource> dataSource(String upstreamId, Service service) { + return new ZkNodeDataSource<>(upstreamId, service, curatorFramework); } @Override diff --git a/ranger-zookeeper/src/main/java/io/appform/ranger/zookeeper/servicefinder/ZkSimpleUnshardedServiceFinderBuilder.java b/ranger-zookeeper/src/main/java/io/appform/ranger/zookeeper/servicefinder/ZkSimpleUnshardedServiceFinderBuilder.java index cd3cf6b5..b2ca6d6e 100644 --- a/ranger-zookeeper/src/main/java/io/appform/ranger/zookeeper/servicefinder/ZkSimpleUnshardedServiceFinderBuilder.java +++ b/ranger-zookeeper/src/main/java/io/appform/ranger/zookeeper/servicefinder/ZkSimpleUnshardedServiceFinderBuilder.java @@ -69,8 +69,8 @@ public SimpleUnshardedServiceFinder build() { @Override protected NodeDataSource> dataSource( - Service service) { - return new ZkNodeDataSource<>(service, curatorFramework); + String upstreamId, Service service) { + return new ZkNodeDataSource<>(upstreamId, service, curatorFramework); } diff --git a/ranger-zookeeper/src/main/java/io/appform/ranger/zookeeper/servicefinderhub/ZKUnshardedServiceFinderFactory.java b/ranger-zookeeper/src/main/java/io/appform/ranger/zookeeper/servicefinderhub/ZKUnshardedServiceFinderFactory.java index 4a470522..6e27a355 100644 --- a/ranger-zookeeper/src/main/java/io/appform/ranger/zookeeper/servicefinderhub/ZKUnshardedServiceFinderFactory.java +++ b/ranger-zookeeper/src/main/java/io/appform/ranger/zookeeper/servicefinderhub/ZKUnshardedServiceFinderFactory.java @@ -37,6 +37,7 @@ public class ZKUnshardedServiceFinderFactory implements ServiceFinderFactory< private final ZkNodeDataDeserializer deserializer; private final ShardSelector> shardSelector; private final ServiceNodeSelector nodeSelector; + private final String upstreamId; @Builder public ZKUnshardedServiceFinderFactory( @@ -46,7 +47,8 @@ public ZKUnshardedServiceFinderFactory( boolean disablePushUpdaters, ZkNodeDataDeserializer deserializer, ShardSelector> shardSelector, - ServiceNodeSelector nodeSelector) { + ServiceNodeSelector nodeSelector, + String upstreamId) { this.curatorFramework = curatorFramework; this.connectionString = connectionString; this.nodeRefreshIntervalMs = nodeRefreshIntervalMs; @@ -54,11 +56,13 @@ public ZKUnshardedServiceFinderFactory( this.deserializer = deserializer; this.shardSelector = shardSelector; this.nodeSelector = nodeSelector; + this.upstreamId = upstreamId; } @Override public SimpleUnshardedServiceFinder buildFinder(Service service) { val finder = new ZkSimpleUnshardedServiceFinderBuilder() + .withUpstreamId(upstreamId) .withDeserializer(deserializer) .withNamespace(service.getNamespace()) .withServiceName(service.getServiceName()) diff --git a/ranger-zookeeper/src/main/java/io/appform/ranger/zookeeper/servicefinderhub/ZkServiceDataSource.java b/ranger-zookeeper/src/main/java/io/appform/ranger/zookeeper/servicefinderhub/ZkServiceDataSource.java index 37d043a1..8243e827 100644 --- a/ranger-zookeeper/src/main/java/io/appform/ranger/zookeeper/servicefinderhub/ZkServiceDataSource.java +++ b/ranger-zookeeper/src/main/java/io/appform/ranger/zookeeper/servicefinderhub/ZkServiceDataSource.java @@ -15,8 +15,11 @@ */ package io.appform.ranger.zookeeper.servicefinderhub; + import io.appform.ranger.core.finderhub.ServiceDataSource; import io.appform.ranger.core.model.Service; +import io.appform.ranger.core.model.DataStoreType; +import io.appform.ranger.core.util.MetricRecorder; import io.appform.ranger.zookeeper.util.PathBuilder; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; @@ -27,8 +30,11 @@ import java.util.Collection; import java.util.Collections; +import java.util.List; +import java.util.Set; import java.util.stream.Collectors; +import static io.appform.ranger.core.util.MetricRecorder.*; import static java.util.Objects.requireNonNull; /** @@ -37,14 +43,17 @@ @Slf4j public class ZkServiceDataSource implements ServiceDataSource { + private final String upstreamId; private final String namespace; private final String connectionString; private CuratorFramework curatorFramework; private boolean curatorProvided; - public ZkServiceDataSource(String namespace, + public ZkServiceDataSource(String upstreamId, + String namespace, String connectionString, CuratorFramework curatorFramework){ + this.upstreamId = upstreamId; this.namespace = namespace; this.connectionString = connectionString; this.curatorFramework = curatorFramework; @@ -53,12 +62,30 @@ public ZkServiceDataSource(String namespace, @Override @SneakyThrows public Collection services() { - val children = curatorFramework.getChildren() - .forPath(PathBuilder.REGISTERED_SERVICES_PATH); - return null == children ? Collections.emptySet() : - children.stream() - .map(child -> Service.builder().namespace(namespace).serviceName(child).build()) - .collect(Collectors.toSet()); + try { + val children = curatorFramework.getChildren() + .forPath(PathBuilder.REGISTERED_SERVICES_PATH); + val result = getServices(children); + MetricRecorder.recordServicesFetchStatus(DataStoreType.ZK, upstreamId, SUCCESS); + return result; + } + catch (Exception e) { + MetricRecorder.recordZookeeperReadUnknownFailure(DataStoreType.ZK, upstreamId, SERVICES_LIST, e.getClass().getSimpleName()); + MetricRecorder.recordServicesFetchStatus(DataStoreType.ZK, upstreamId, FAILURE); + throw e; + } + } + + private Set getServices(List children) { + if(children == null || children.isEmpty()) { + MetricRecorder.recordNullOrEmptyServicesListResponse(DataStoreType.ZK, upstreamId); + log.warn("No services found for namespace: {} in zk data source with metric id: {}", namespace, upstreamId); + return Collections.emptySet(); + } else { + return children.stream() + .map(child -> Service.builder().namespace(namespace).serviceName(child).build()) + .collect(Collectors.toSet()); + } } @Override diff --git a/ranger-zookeeper/src/main/java/io/appform/ranger/zookeeper/servicefinderhub/ZkShardedServiceFinderFactory.java b/ranger-zookeeper/src/main/java/io/appform/ranger/zookeeper/servicefinderhub/ZkShardedServiceFinderFactory.java index 5689cab3..6ed8020d 100644 --- a/ranger-zookeeper/src/main/java/io/appform/ranger/zookeeper/servicefinderhub/ZkShardedServiceFinderFactory.java +++ b/ranger-zookeeper/src/main/java/io/appform/ranger/zookeeper/servicefinderhub/ZkShardedServiceFinderFactory.java @@ -38,6 +38,7 @@ public class ZkShardedServiceFinderFactory implements ServiceFinderFactory deserializer; private final ShardSelector> shardSelector; private final ServiceNodeSelector nodeSelector; + private final String upstreamId; @Builder public ZkShardedServiceFinderFactory( @@ -47,7 +48,8 @@ public ZkShardedServiceFinderFactory( boolean disablePushUpdaters, ZkNodeDataDeserializer deserializer, ShardSelector> shardSelector, - ServiceNodeSelector nodeSelector) { + ServiceNodeSelector nodeSelector, + String upstreamId) { this.curatorFramework = curatorFramework; this.connectionString = connectionString; this.nodeRefreshIntervalMs = nodeRefreshIntervalMs; @@ -55,11 +57,13 @@ public ZkShardedServiceFinderFactory( this.deserializer = deserializer; this.shardSelector = shardSelector; this.nodeSelector = nodeSelector; + this.upstreamId = upstreamId; } @Override public SimpleShardedServiceFinder buildFinder(Service service) { val finder = new ZkSimpleShardedServiceFinderBuilder() + .withUpstreamId(upstreamId) .withDeserializer(deserializer) .withNamespace(service.getNamespace()) .withServiceName(service.getServiceName()) diff --git a/ranger-zookeeper/src/main/java/io/appform/ranger/zookeeper/serviceprovider/ZkNodeDataSink.java b/ranger-zookeeper/src/main/java/io/appform/ranger/zookeeper/serviceprovider/ZkNodeDataSink.java index cac71fee..320dba76 100644 --- a/ranger-zookeeper/src/main/java/io/appform/ranger/zookeeper/serviceprovider/ZkNodeDataSink.java +++ b/ranger-zookeeper/src/main/java/io/appform/ranger/zookeeper/serviceprovider/ZkNodeDataSink.java @@ -15,10 +15,13 @@ */ package io.appform.ranger.zookeeper.serviceprovider; + +import io.appform.ranger.core.model.DataStoreType; import io.appform.ranger.core.model.NodeDataSink; import io.appform.ranger.core.model.Service; import io.appform.ranger.core.model.ServiceNode; import io.appform.ranger.core.util.Exceptions; +import io.appform.ranger.core.util.MetricRecorder; import io.appform.ranger.zookeeper.common.ZkNodeDataStoreConnector; import io.appform.ranger.zookeeper.common.ZkStoreType; import io.appform.ranger.zookeeper.serde.ZkNodeDataSerializer; @@ -29,6 +32,8 @@ import org.apache.zookeeper.CreateMode; import org.apache.zookeeper.KeeperException; +import static io.appform.ranger.core.util.MetricRecorder.FAILURE; +import static io.appform.ranger.core.util.MetricRecorder.SUCCESS; import static java.util.Objects.requireNonNull; /** @@ -37,9 +42,19 @@ @Slf4j public class ZkNodeDataSink> extends ZkNodeDataStoreConnector implements NodeDataSink { public ZkNodeDataSink( - Service service, + String upstreamId, Service service, CuratorFramework curatorFramework) { - super(service, curatorFramework, ZkStoreType.SINK); + super(upstreamId, service, curatorFramework, ZkStoreType.SINK); + } + + @Override + public DataStoreType getDataStoreType() { + return DataStoreType.ZK; + } + + @Override + public String getUpstreamId() { + return upstreamId; } @Override @@ -54,20 +69,32 @@ public void updateState(S serializer, ServiceNode serviceNode) { try { if (null == curatorFramework.checkExists().forPath(path)) { log.info("No node exists for path: {}. Will create now.", path); - createPath(serviceNode, serializer); + createPath(service.getServiceName(), serviceNode, serializer); } else { - curatorFramework.setData().forPath(path, serializer.serialize(serviceNode)); + val serviceData = getSerializedData(service.getServiceName(), serializer, serviceNode); + curatorFramework.setData().forPath(path, serviceData); } + MetricRecorder.recordNodeDataSinkUpdateStatus(getDataStoreType(), upstreamId, SUCCESS); } catch (Exception e) { log.error("Error updating node data at path " + path, e); + MetricRecorder.recordNodeDataSinkUpdateStatus(getDataStoreType(), upstreamId, FAILURE); Exceptions.illegalState(e); } } + private > byte[] getSerializedData(String serviceName, S serializer, ServiceNode serviceNode) { + try { + return serializer.serialize(serviceNode); + } catch (Exception e) { + MetricRecorder.recordNodeDataSinkSerDeFailure(getDataStoreType(), upstreamId, MetricRecorder.SERIALIZATION, serviceName, e.getClass().getSimpleName()); + throw e; + } + } + private synchronized void createPath( - ServiceNode serviceNode, + String serviceName, ServiceNode serviceNode, S serializer) { val instancePath = PathBuilder.instancePath(service, serviceNode); try { @@ -75,7 +102,7 @@ private synchronized void createPath( curatorFramework.create() .creatingParentContainersIfNeeded() .withMode(CreateMode.EPHEMERAL) - .forPath(instancePath, serializer.serialize(serviceNode)); + .forPath(instancePath, getSerializedData(serviceName, serializer, serviceNode)); log.info("Created instance path: {}", instancePath); } } @@ -83,6 +110,7 @@ private synchronized void createPath( log.warn("Node already exists.. Race condition?", e); } catch (Exception e) { + MetricRecorder.recordNodeDataSinkUnknownFailure(getDataStoreType(), upstreamId, service.getServiceName(), e.getClass().getSimpleName()); val message = String.format( "Could not create node for %s after 60 retries (1 min). " + "This service will not be discoverable. Retry after some time.", service.getServiceName()); diff --git a/ranger-zookeeper/src/main/java/io/appform/ranger/zookeeper/serviceprovider/ZkServiceProviderBuilder.java b/ranger-zookeeper/src/main/java/io/appform/ranger/zookeeper/serviceprovider/ZkServiceProviderBuilder.java index 16824194..157c7364 100644 --- a/ranger-zookeeper/src/main/java/io/appform/ranger/zookeeper/serviceprovider/ZkServiceProviderBuilder.java +++ b/ranger-zookeeper/src/main/java/io/appform/ranger/zookeeper/serviceprovider/ZkServiceProviderBuilder.java @@ -61,7 +61,7 @@ public ServiceProvider> build() { } @Override - protected NodeDataSink> dataSink(Service service) { - return new ZkNodeDataSink<>(service, curatorFramework); + protected NodeDataSink> dataSink(String upstreamId, Service service) { + return new ZkNodeDataSink<>(upstreamId, service, curatorFramework); } } diff --git a/ranger-zookeeper/src/test/java/io/appform/ranger/zookeeper/healthservice/ServiceProviderIntegrationTest.java b/ranger-zookeeper/src/test/java/io/appform/ranger/zookeeper/healthservice/ServiceProviderIntegrationTest.java index 7c83dbf9..741d2ddf 100644 --- a/ranger-zookeeper/src/test/java/io/appform/ranger/zookeeper/healthservice/ServiceProviderIntegrationTest.java +++ b/ranger-zookeeper/src/test/java/io/appform/ranger/zookeeper/healthservice/ServiceProviderIntegrationTest.java @@ -24,7 +24,6 @@ import io.appform.ranger.core.healthcheck.updater.HealthStatusHandler; import io.appform.ranger.core.healthcheck.updater.HealthUpdateHandler; import io.appform.ranger.core.healthcheck.updater.LastUpdatedHandler; -import io.appform.ranger.core.healthcheck.updater.StartupTimeHandler; import io.appform.ranger.core.healthservice.TimeEntity; import io.appform.ranger.core.healthservice.monitor.sample.RotationStatusMonitor; import io.appform.ranger.core.model.ServiceNode; @@ -70,6 +69,7 @@ public void startTestCluster() throws Exception { registerService("localhost-4", 9003, 2, anotherFile); serviceFinder = ServiceFinderBuilders.shardedFinderBuilder() + .withUpstreamId("test-metric") .withConnectionString(testingCluster.getConnectString()) .withNamespace("test") .withServiceName("test-service") @@ -135,6 +135,7 @@ private void registerService(String host, int port, int shardId, File file) { final HealthUpdateHandler healthUpdateHandler = new LastUpdatedHandler() .setNext(new HealthStatusHandler()); val serviceProvider = ServiceProviderBuilders.shardedServiceProviderBuilder() + .withUpstreamId("test-metric") .withConnectionString(testingCluster.getConnectString()) .withNamespace("test") .withServiceName("test-service") diff --git a/ranger-zookeeper/src/test/java/io/appform/ranger/zookeeper/model/CustomShardSelectorTest.java b/ranger-zookeeper/src/test/java/io/appform/ranger/zookeeper/model/CustomShardSelectorTest.java index 3fff1b67..eaf3712e 100644 --- a/ranger-zookeeper/src/test/java/io/appform/ranger/zookeeper/model/CustomShardSelectorTest.java +++ b/ranger-zookeeper/src/test/java/io/appform/ranger/zookeeper/model/CustomShardSelectorTest.java @@ -100,6 +100,7 @@ public List> nodes(Predicate criteria, @Test void testBasicDiscovery() { val serviceFinder = ServiceFinderBuilders.shardedFinderBuilder() + .withUpstreamId("test-metric") .withConnectionString(testingCluster.getConnectString()) .withNamespace("test") .withServiceName("test-service") @@ -135,6 +136,7 @@ private void registerService(String host, int port, int a, int b) { final HealthUpdateHandler healthUpdateHandler = new LastUpdatedHandler() .setNext(new HealthStatusHandler()); val serviceProvider = ServiceProviderBuilders.shardedServiceProviderBuilder() + .withUpstreamId("test-metric") .withConnectionString(testingCluster.getConnectString()) .withNamespace("test") .withServiceName("test-service") diff --git a/ranger-zookeeper/src/test/java/io/appform/ranger/zookeeper/model/ServiceNoProviderTest.java b/ranger-zookeeper/src/test/java/io/appform/ranger/zookeeper/model/ServiceNoProviderTest.java index d1947e9c..d585a701 100644 --- a/ranger-zookeeper/src/test/java/io/appform/ranger/zookeeper/model/ServiceNoProviderTest.java +++ b/ranger-zookeeper/src/test/java/io/appform/ranger/zookeeper/model/ServiceNoProviderTest.java @@ -54,6 +54,7 @@ public void stopTestCluster() throws Exception { @Test void testBasicDiscovery() { val serviceFinder = ServiceFinderBuilders.shardedFinderBuilder() + .withUpstreamId("test-metric") .withConnectionString(testingCluster.getConnectString()) .withNamespace("test") .withServiceName("test-service") @@ -79,6 +80,7 @@ void testBasicDiscovery() { @Test void testBasicDiscoveryRR() { val serviceFinder = ServiceFinderBuilders.shardedFinderBuilder() + .withUpstreamId("test-metric") .withConnectionString(testingCluster.getConnectString()) .withNamespace("test") .withServiceName("test-service") diff --git a/ranger-zookeeper/src/test/java/io/appform/ranger/zookeeper/model/ServiceProviderExtCuratorTest.java b/ranger-zookeeper/src/test/java/io/appform/ranger/zookeeper/model/ServiceProviderExtCuratorTest.java index 1dd16e2b..876b8f3d 100644 --- a/ranger-zookeeper/src/test/java/io/appform/ranger/zookeeper/model/ServiceProviderExtCuratorTest.java +++ b/ranger-zookeeper/src/test/java/io/appform/ranger/zookeeper/model/ServiceProviderExtCuratorTest.java @@ -80,6 +80,7 @@ public void stopTestCluster() throws Exception { @Test void testBasicDiscovery() { val serviceFinder = ServiceFinderBuilders.shardedFinderBuilder() + .withUpstreamId("test-metric") .withCuratorFramework(curatorFramework) .withNamespace("test") .withServiceName("test-service") @@ -122,6 +123,7 @@ private void registerService(String host, int port, int shardId) { final HealthUpdateHandler healthUpdateHandler = new LastUpdatedHandler() .setNext(new HealthStatusHandler()); val serviceProvider = ServiceProviderBuilders.shardedServiceProviderBuilder() + .withUpstreamId("test-metric") .withCuratorFramework(curatorFramework) .withNamespace("test") .withServiceName("test-service") diff --git a/ranger-zookeeper/src/test/java/io/appform/ranger/zookeeper/model/ServiceProviderHealthcheckTest.java b/ranger-zookeeper/src/test/java/io/appform/ranger/zookeeper/model/ServiceProviderHealthcheckTest.java index 506b76ba..3e1a6348 100644 --- a/ranger-zookeeper/src/test/java/io/appform/ranger/zookeeper/model/ServiceProviderHealthcheckTest.java +++ b/ranger-zookeeper/src/test/java/io/appform/ranger/zookeeper/model/ServiceProviderHealthcheckTest.java @@ -66,6 +66,7 @@ public void stopTestCluster() throws Exception { @Test void testBasicDiscovery() { val serviceFinder = ServiceFinderBuilders.shardedFinderBuilder() + .withUpstreamId("test-metric") .withConnectionString(testingCluster.getConnectString()) .withNamespace("test") .withServiceName("test-service") @@ -141,6 +142,7 @@ public void start() { final HealthUpdateHandler healthUpdateHandler = new LastUpdatedHandler() .setNext(new HealthStatusHandler()); val serviceProvider = ServiceProviderBuilders.shardedServiceProviderBuilder() + .withUpstreamId("test-metric") .withConnectionString(connectionString) .withNamespace("test") .withServiceName("test-service") diff --git a/ranger-zookeeper/src/test/java/io/appform/ranger/zookeeper/model/ServiceProviderTest.java b/ranger-zookeeper/src/test/java/io/appform/ranger/zookeeper/model/ServiceProviderTest.java index f8105a49..2fb893ce 100644 --- a/ranger-zookeeper/src/test/java/io/appform/ranger/zookeeper/model/ServiceProviderTest.java +++ b/ranger-zookeeper/src/test/java/io/appform/ranger/zookeeper/model/ServiceProviderTest.java @@ -73,6 +73,7 @@ public void stopTestCluster() throws Exception { @Test void testBasicDiscovery() { val serviceFinder = ServiceFinderBuilders.shardedFinderBuilder() + .withUpstreamId("test-metric") .withConnectionString(testingCluster.getConnectString()) .withNamespace("test") .withServiceName("test-service") @@ -114,6 +115,7 @@ void testBasicDiscovery() { void testBasicDiscoveryRR() { val serviceFinder = ServiceFinderBuilders.shardedFinderBuilder() + .withUpstreamId("test-metric") .withConnectionString(testingCluster.getConnectString()) .withNamespace("test") .withServiceName("test-service") @@ -158,6 +160,7 @@ void testBasicDiscoveryRR() { void testVisibility() { val serviceFinder = ServiceFinderBuilders. shardedFinderBuilder() + .withUpstreamId("test-metric") .withConnectionString(testingCluster.getConnectString()) .withNamespace("test") .withServiceName("test-service") @@ -186,6 +189,7 @@ private void registerService(String host, int port, int shardId) { final HealthUpdateHandler healthUpdateHandler = new LastUpdatedHandler() .setNext(new HealthStatusHandler<>()); val serviceProvider = ServiceProviderBuilders.shardedServiceProviderBuilder() + .withUpstreamId("test-metric") .withConnectionString(testingCluster.getConnectString()) .withNamespace("test") .withServiceName("test-service") diff --git a/ranger-zookeeper/src/test/java/io/appform/ranger/zookeeper/model/SimpleServiceProviderTest.java b/ranger-zookeeper/src/test/java/io/appform/ranger/zookeeper/model/SimpleServiceProviderTest.java index 7c9c7ce5..7a564c15 100644 --- a/ranger-zookeeper/src/test/java/io/appform/ranger/zookeeper/model/SimpleServiceProviderTest.java +++ b/ranger-zookeeper/src/test/java/io/appform/ranger/zookeeper/model/SimpleServiceProviderTest.java @@ -75,6 +75,7 @@ public boolean equals(Object obj) { @Test void testBasicDiscovery() { val serviceFinder = ServiceFinderBuilders.unshardedFinderBuilder() + .withUpstreamId("test-metric") .withConnectionString(testingCluster.getConnectString()) .withNamespace("test") .withServiceName("test-service") @@ -110,6 +111,7 @@ private void registerService(String host, int port) { final HealthUpdateHandler healthUpdateHandler = new LastUpdatedHandler() .setNext(new HealthStatusHandler()); val serviceProvider = ServiceProviderBuilders.unshardedServiceProviderBuilder() + .withUpstreamId("test-metric") .withConnectionString(testingCluster.getConnectString()) .withNamespace("test") .withServiceName("test-service") diff --git a/ranger-zookeeper/src/test/java/io/appform/ranger/zookeeper/servicefinder/ZkNodeDataSourceMetricsIntegrationTest.java b/ranger-zookeeper/src/test/java/io/appform/ranger/zookeeper/servicefinder/ZkNodeDataSourceMetricsIntegrationTest.java new file mode 100644 index 00000000..fa0defd4 --- /dev/null +++ b/ranger-zookeeper/src/test/java/io/appform/ranger/zookeeper/servicefinder/ZkNodeDataSourceMetricsIntegrationTest.java @@ -0,0 +1,309 @@ +/* + * Copyright 2024 Authors, Flipkart Internet Pvt. Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.appform.ranger.zookeeper.servicefinder; + +import com.codahale.metrics.MetricRegistry; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.appform.ranger.core.model.Service; +import io.appform.ranger.core.model.ServiceNode; +import io.appform.ranger.core.units.TestNodeData; +import io.appform.ranger.core.util.MetricRecorder; +import io.appform.ranger.zookeeper.serde.ZkNodeDataDeserializer; +import io.appform.ranger.zookeeper.util.PathBuilder; +import lombok.SneakyThrows; +import lombok.val; +import org.apache.curator.framework.CuratorFramework; +import org.apache.curator.framework.CuratorFrameworkFactory; +import org.apache.curator.retry.ExponentialBackoffRetry; +import org.apache.curator.test.TestingCluster; +import org.apache.zookeeper.CreateMode; +import org.junit.jupiter.api.*; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Integration tests for ZkNodeDataSource metrics recording. + * Tests verify that metrics are pushed through actual ZK operations. + */ +class ZkNodeDataSourceMetricsIntegrationTest { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + private static final String METRIC_PREFIX = "io.appform.ranger"; + private static final String NAMESPACE = "test"; + private static final String SERVICE_NAME = "node-source-svc"; + + private TestingCluster testingCluster; + private CuratorFramework curatorFramework; + private MetricRegistry metricRegistry; + + @BeforeEach + void setUp() throws Exception { + metricRegistry = new MetricRegistry(); + MetricRecorder.initialize(metricRegistry); + + testingCluster = new TestingCluster(3); + testingCluster.start(); + + curatorFramework = CuratorFrameworkFactory.builder() + .namespace(NAMESPACE) + .connectString(testingCluster.getConnectString()) + .retryPolicy(new ExponentialBackoffRetry(1000, 3)) + .build(); + curatorFramework.start(); + curatorFramework.blockUntilConnected(); + } + + @AfterEach + void tearDown() throws Exception { + if (curatorFramework != null) { + curatorFramework.close(); + } + if (testingCluster != null) { + testingCluster.close(); + } + } + + // ==================== isActive() - Active connection ==================== + + @Test + void testIsActive_connectedZk_recordsActiveStatus() { + val service = new Service(NAMESPACE, SERVICE_NAME); + val dataSource = new ZkNodeDataSource>( + "zk-node-src-1", service, curatorFramework); + dataSource.start(); + + val active = dataSource.isActive(); + + assertTrue(active); + + // Verify active status metric + val activeMeter = metricRegistry.getMeters().get( + METRIC_PREFIX + ".dataStoreType.ZK.dataSource.zk-node-src-1.active"); + assertNotNull(activeMeter, "Active status meter should be recorded"); + assertEquals(1, activeMeter.getCount()); + + dataSource.stop(); + } + + // ==================== isActive() - Disconnected ==================== + + @Test + void testIsActive_disconnectedZk_recordsInactiveStatus() throws Exception { + val service = new Service(NAMESPACE, SERVICE_NAME); + val disconnectedCurator = CuratorFrameworkFactory.builder() + .namespace(NAMESPACE) + .connectString("127.0.0.1:19999") // non-existent ZK + .retryPolicy(new ExponentialBackoffRetry(100, 1)) + .sessionTimeoutMs(500) + .connectionTimeoutMs(500) + .build(); + disconnectedCurator.start(); + // Don't wait for connection — it should fail + + val dataSource = new ZkNodeDataSource>( + "zk-node-src-2", service, disconnectedCurator); + // Don't call start() — it would block waiting for connection + + val active = dataSource.isActive(); + + assertFalse(active); + + // Verify inactive status metric + val inactiveMeter = metricRegistry.getMeters().get( + METRIC_PREFIX + ".dataStoreType.ZK.dataSource.zk-node-src-2.inactive"); + assertNotNull(inactiveMeter, "Inactive status meter should be recorded"); + assertEquals(1, inactiveMeter.getCount()); + + disconnectedCurator.close(); + } + + // ==================== refresh() - Success with nodes ==================== + + @Test + void testRefresh_withNodes_noEmptyMetric() throws Exception { + val service = new Service(NAMESPACE, SERVICE_NAME); + val dataSource = new ZkNodeDataSource>( + "zk-node-src-3", service, curatorFramework); + dataSource.start(); + + // Create service path and child nodes in ZK + val servicePath = PathBuilder.servicePath(service); + curatorFramework.create().creatingParentContainersIfNeeded().forPath(servicePath); + + val node1 = ServiceNode.builder() + .host("host1").port(8080) + .nodeData(TestNodeData.builder().shardId(1).build()) + .build(); + val nodeData1 = MAPPER.writeValueAsBytes(node1); + curatorFramework.create().withMode(CreateMode.EPHEMERAL) + .forPath(servicePath + "/" + node1.representation(), nodeData1); + + val result = dataSource.refresh(validDeserializer()); + + assertTrue(result.isPresent()); + assertEquals(1, result.get().size()); + assertEquals("host1", result.get().get(0).getHost()); + + // No null/empty list node response metric should be recorded + val emptyMeter = metricRegistry.getMeters().get( + METRIC_PREFIX + ".dataStoreType.ZK.dataSource.zk-node-src-3.httpCall.listNodes.serviceName." + + SERVICE_NAME + ".nullOrEmptyResponse"); + assertNull(emptyMeter, "No empty list node metric should be recorded when nodes exist"); + + dataSource.stop(); + } + + // ==================== refresh() - Empty children ==================== + + @Test + void testRefresh_emptyChildren_recordsNullOrEmptyListNodeResponse() throws Exception { + val service = new Service(NAMESPACE, SERVICE_NAME); + val dataSource = new ZkNodeDataSource>( + "zk-node-src-4", service, curatorFramework); + dataSource.start(); + + // Create service path but no child nodes + val servicePath = PathBuilder.servicePath(service); + curatorFramework.create().creatingParentContainersIfNeeded().forPath(servicePath); + + val result = dataSource.refresh(validDeserializer()); + + assertTrue(result.isPresent()); + assertTrue(result.get().isEmpty()); + + // Verify null/empty list node response metric + val aggregateMeter = metricRegistry.getMeters().get( + METRIC_PREFIX + ".dataStoreType.ZK.dataSource.zk-node-src-4.httpCall.listNodes.nullOrEmptyResponse"); + assertNotNull(aggregateMeter, "Aggregate empty list node meter should be recorded"); + assertEquals(1, aggregateMeter.getCount()); + + val emptyMeter = metricRegistry.getMeters().get( + METRIC_PREFIX + ".dataStoreType.ZK.dataSource.zk-node-src-4.httpCall.listNodes.serviceName." + + SERVICE_NAME + ".nullOrEmptyResponse"); + assertNotNull(emptyMeter, "Empty list node meter should be recorded"); + assertEquals(1, emptyMeter.getCount()); + + dataSource.stop(); + } + + // ==================== refresh() - NoNodeException (path doesn't exist) ==================== + + @Test + void testRefresh_noServicePath_recordsNullOrEmptyListNodeResponse() throws Exception { + val service = new Service(NAMESPACE, "nonexistent-svc"); + val dataSource = new ZkNodeDataSource>( + "zk-node-src-5", service, curatorFramework); + dataSource.start(); + + // Don't create the service path — triggers NoNodeException + + val result = dataSource.refresh(validDeserializer()); + + assertTrue(result.isPresent()); + assertTrue(result.get().isEmpty()); + + // Verify null/empty list node response metric (NoNodeException path) + val aggregateMeter = metricRegistry.getMeters().get( + METRIC_PREFIX + ".dataStoreType.ZK.dataSource.zk-node-src-5.httpCall.listNodes.nullOrEmptyResponse"); + assertNotNull(aggregateMeter, "Aggregate empty list node meter should be recorded for NoNodeException"); + assertTrue(aggregateMeter.getCount() >= 1); + + val emptyMeter = metricRegistry.getMeters().get( + METRIC_PREFIX + ".dataStoreType.ZK.dataSource.zk-node-src-5.httpCall.listNodes.serviceName.nonexistent-svc.nullOrEmptyResponse"); + assertNotNull(emptyMeter, "Empty list node meter should be recorded for NoNodeException"); + assertTrue(emptyMeter.getCount() >= 1); + + dataSource.stop(); + } + + // ==================== refresh() - Deserializer failure (parse failure) ==================== + + @Test + void testRefresh_deserializerThrows_recordsListNodesParseFailure() throws Exception { + val service = new Service(NAMESPACE, SERVICE_NAME); + val dataSource = new ZkNodeDataSource>( + "zk-node-src-6", service, curatorFramework); + dataSource.start(); + + // Create service path with a child node containing bad data + val servicePath = PathBuilder.servicePath(service); + curatorFramework.create().creatingParentContainersIfNeeded().forPath(servicePath); + curatorFramework.create().withMode(CreateMode.EPHEMERAL) + .forPath(servicePath + "/bad-node", "invalid-json{{{".getBytes()); + + // Deserializer that always throws + ZkNodeDataDeserializer badDeserializer = data -> { + throw new RuntimeException("Parse failure simulation"); + }; + + assertThrows(RuntimeException.class, () -> dataSource.refresh(badDeserializer)); + + // Verify list nodes parse failure metric + val aggregateParseMeter = metricRegistry.getMeters().get( + METRIC_PREFIX + ".dataStoreType.ZK.dataSource.zk-node-src-6.httpCall.listNodes.responseParseFailure"); + assertNotNull(aggregateParseMeter, "Aggregate list nodes parse failure meter should be recorded"); + assertEquals(1, aggregateParseMeter.getCount()); + + val parseMeter = metricRegistry.getMeters().get( + METRIC_PREFIX + ".dataStoreType.ZK.dataSource.zk-node-src-6.httpCall.listNodes.serviceName." + + SERVICE_NAME + ".responseParseFailure"); + assertNotNull(parseMeter, "List nodes parse failure meter should be recorded"); + assertEquals(1, parseMeter.getCount()); + + dataSource.stop(); + } + + // ==================== refresh() - Not started ==================== + + @Test + void testRefresh_notStarted_returnsEmpty() { + val service = new Service(NAMESPACE, SERVICE_NAME); + val dataSource = new ZkNodeDataSource>( + "zk-node-src-7", service, curatorFramework); + // Intentionally do NOT call start() + + val result = dataSource.refresh(validDeserializer()); + + assertFalse(result.isPresent()); + } + + // ==================== refresh() - Stopped ==================== + + @Test + void testRefresh_stopped_returnsEmpty() { + val service = new Service(NAMESPACE, SERVICE_NAME); + val dataSource = new ZkNodeDataSource>( + "zk-node-src-8", service, curatorFramework); + dataSource.start(); + dataSource.stop(); + + val result = dataSource.refresh(validDeserializer()); + + assertFalse(result.isPresent()); + } + + // ==================== Helper ==================== + + @SneakyThrows + private static ServiceNode deserializeNode(byte[] data) { + return MAPPER.readValue(data, new TypeReference>() {}); + } + + private ZkNodeDataDeserializer validDeserializer() { + return ZkNodeDataSourceMetricsIntegrationTest::deserializeNode; + } +} diff --git a/ranger-zookeeper/src/test/java/io/appform/ranger/zookeeper/servicefinderhub/ZkServiceDataSourceMetricsIntegrationTest.java b/ranger-zookeeper/src/test/java/io/appform/ranger/zookeeper/servicefinderhub/ZkServiceDataSourceMetricsIntegrationTest.java new file mode 100644 index 00000000..04b04920 --- /dev/null +++ b/ranger-zookeeper/src/test/java/io/appform/ranger/zookeeper/servicefinderhub/ZkServiceDataSourceMetricsIntegrationTest.java @@ -0,0 +1,195 @@ +/* + * Copyright 2024 Authors, Flipkart Internet Pvt. Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.appform.ranger.zookeeper.servicefinderhub; + +import com.codahale.metrics.MetricRegistry; +import io.appform.ranger.core.util.MetricRecorder; +import lombok.val; +import org.apache.curator.framework.CuratorFramework; +import org.apache.curator.framework.CuratorFrameworkFactory; +import org.apache.curator.retry.ExponentialBackoffRetry; +import org.apache.curator.test.TestingCluster; +import org.junit.jupiter.api.*; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Integration tests for ZkServiceDataSource metrics recording. + * Tests verify that metrics are pushed through actual ZK operations. + */ +class ZkServiceDataSourceMetricsIntegrationTest { + + private static final String METRIC_PREFIX = "io.appform.ranger"; + private static final String NAMESPACE = "test"; + + private TestingCluster testingCluster; + private CuratorFramework curatorFramework; + private MetricRegistry metricRegistry; + + @BeforeEach + void setUp() throws Exception { + metricRegistry = new MetricRegistry(); + MetricRecorder.initialize(metricRegistry); + + testingCluster = new TestingCluster(3); + testingCluster.start(); + + curatorFramework = CuratorFrameworkFactory.builder() + .namespace(NAMESPACE) + .connectString(testingCluster.getConnectString()) + .retryPolicy(new ExponentialBackoffRetry(1000, 3)) + .build(); + curatorFramework.start(); + curatorFramework.blockUntilConnected(); + } + + @AfterEach + void tearDown() throws Exception { + if (curatorFramework != null) { + curatorFramework.close(); + } + if (testingCluster != null) { + testingCluster.close(); + } + } + + // ==================== services() - Success with children ==================== + + @Test + void testServices_withChildren_recordsSuccessStatus() throws Exception { + // Create some child nodes under "/" (the registered services path) + curatorFramework.create().creatingParentContainersIfNeeded().forPath("/service-a"); + curatorFramework.create().creatingParentContainersIfNeeded().forPath("/service-b"); + + val dataSource = new ZkServiceDataSource( + "zk-svc-src-1", NAMESPACE, testingCluster.getConnectString(), curatorFramework); + dataSource.start(); + + val services = dataSource.services(); + + assertNotNull(services); + assertEquals(2, services.size()); + + // Verify success fetch status + val successMeter = metricRegistry.getMeters().get( + METRIC_PREFIX + ".dataStoreType.ZK.dataSource.zk-svc-src-1.services.fetch.success"); + assertNotNull(successMeter, "Services fetch success meter should be recorded"); + assertEquals(1, successMeter.getCount()); + + // No empty response metric + val emptyMeter = metricRegistry.getMeters().get( + METRIC_PREFIX + ".dataStoreType.ZK.dataSource.zk-svc-src-1.httpCall.services.nullOrEmptyResponse"); + assertNull(emptyMeter, "No null/empty services response meter should be recorded when children exist"); + + dataSource.stop(); + } + + // ==================== services() - Empty children ==================== + + @Test + void testServices_noChildren_recordsNullOrEmptyServicesAndSuccess() throws Exception { + // Use a fresh curator with a different namespace that has no children + val emptyCurator = CuratorFrameworkFactory.builder() + .namespace("empty-ns") + .connectString(testingCluster.getConnectString()) + .retryPolicy(new ExponentialBackoffRetry(1000, 3)) + .build(); + emptyCurator.start(); + emptyCurator.blockUntilConnected(); + + // Ensure root path exists but is empty + if (emptyCurator.checkExists().forPath("/") == null) { + emptyCurator.create().forPath("/"); + } + + val dataSource = new ZkServiceDataSource( + "zk-svc-src-2", "empty-ns", testingCluster.getConnectString(), emptyCurator); + dataSource.start(); + + val services = dataSource.services(); + + assertNotNull(services); + assertTrue(services.isEmpty()); + + // Verify null/empty services response metric + val emptyMeter = metricRegistry.getMeters().get( + METRIC_PREFIX + ".dataStoreType.ZK.dataSource.zk-svc-src-2.httpCall.services.nullOrEmptyResponse"); + assertNotNull(emptyMeter, "Null/empty services response meter should be recorded"); + assertEquals(1, emptyMeter.getCount()); + + // Verify success fetch status (still marked success even if empty) + val successMeter = metricRegistry.getMeters().get( + METRIC_PREFIX + ".dataStoreType.ZK.dataSource.zk-svc-src-2.services.fetch.success"); + assertNotNull(successMeter, "Services fetch success meter should still be recorded"); + assertEquals(1, successMeter.getCount()); + + emptyCurator.close(); + } + + // ==================== services() - ZK failure ==================== + + @Test + void testServices_zkFailure_recordsUnknownFailureAndFailureStatus() throws Exception { + val dataSource = new ZkServiceDataSource( + "zk-svc-src-3", NAMESPACE, testingCluster.getConnectString(), curatorFramework); + dataSource.start(); + + // Close the curator to simulate connection failure + curatorFramework.close(); + + assertThrows(Exception.class, dataSource::services); + + // Verify failure fetch status + val failureMeter = metricRegistry.getMeters().get( + METRIC_PREFIX + ".dataStoreType.ZK.dataSource.zk-svc-src-3.services.fetch.failure"); + assertNotNull(failureMeter, "Services fetch failure meter should be recorded"); + assertEquals(1, failureMeter.getCount()); + + // Verify ZK read unknown failure + val unknownFailureMeter = metricRegistry.getMeters().get( + METRIC_PREFIX + ".dataStoreType.ZK.dataSource.zk-svc-src-3.zkRead.services.unknownFailure"); + assertNotNull(unknownFailureMeter, "ZK read unknown failure meter should be recorded"); + assertEquals(1, unknownFailureMeter.getCount()); + + // Null out curatorFramework to prevent double-close in tearDown + curatorFramework = null; + } + + // ==================== services() - Success with provided curator ==================== + + @Test + void testServices_providedCurator_recordsSuccess() throws Exception { + // Create a service node + curatorFramework.create().creatingParentContainersIfNeeded().forPath("/my-service"); + + val dataSource = new ZkServiceDataSource( + "zk-svc-src-4", NAMESPACE, testingCluster.getConnectString(), curatorFramework); + dataSource.start(); + + val services = dataSource.services(); + + assertNotNull(services); + assertTrue(services.stream().anyMatch(s -> "my-service".equals(s.getServiceName()))); + + // Verify success + val successMeter = metricRegistry.getMeters().get( + METRIC_PREFIX + ".dataStoreType.ZK.dataSource.zk-svc-src-4.services.fetch.success"); + assertNotNull(successMeter, "Success meter should be recorded"); + assertEquals(1, successMeter.getCount()); + + dataSource.stop(); + } +} diff --git a/ranger-zookeeper/src/test/java/io/appform/ranger/zookeeper/servicehub/ServiceHubTest.java b/ranger-zookeeper/src/test/java/io/appform/ranger/zookeeper/servicehub/ServiceHubTest.java index ebfbb73b..7b3d9289 100644 --- a/ranger-zookeeper/src/test/java/io/appform/ranger/zookeeper/servicehub/ServiceHubTest.java +++ b/ranger-zookeeper/src/test/java/io/appform/ranger/zookeeper/servicehub/ServiceHubTest.java @@ -93,6 +93,7 @@ void testHub() { final HealthUpdateHandler healthUpdateHandler = new LastUpdatedHandler() .setNext(new HealthStatusHandler<>()); val provider1 = ServiceProviderBuilders.shardedServiceProviderBuilder() + .withUpstreamId("test-metric") .withHostname("localhost") .withPort(1080) .withNamespace(NAMESPACE) @@ -112,8 +113,9 @@ void testHub() { .withCuratorFramework(curatorFramework) .withNamespace("test") .withRefreshFrequencyMs(1000) - .withServiceDataSource(new ZkServiceDataSource("test", testingCluster.getConnectString(), curatorFramework)) + .withServiceDataSource(new ZkServiceDataSource(null, "test", testingCluster.getConnectString(), curatorFramework)) .withServiceFinderFactory(ZkShardedServiceFinderFactory.builder() + .upstreamId("test-metric") .curatorFramework(curatorFramework) .deserializer(this::read) .build()) diff --git a/ranger-zookeeper/src/test/java/io/appform/ranger/zookeeper/serviceprovider/BaseServiceProviderBuilderTest.java b/ranger-zookeeper/src/test/java/io/appform/ranger/zookeeper/serviceprovider/BaseServiceProviderBuilderTest.java index e120afc0..d0db7bc1 100644 --- a/ranger-zookeeper/src/test/java/io/appform/ranger/zookeeper/serviceprovider/BaseServiceProviderBuilderTest.java +++ b/ranger-zookeeper/src/test/java/io/appform/ranger/zookeeper/serviceprovider/BaseServiceProviderBuilderTest.java @@ -63,6 +63,7 @@ void testServiceProviderBuilder() { .setNext(new HealthStatusHandler<>()); try { val serviceProvider = ServiceProviderBuilders.unshardedServiceProviderBuilder() + .withUpstreamId("test-metric") .withConnectionString(testingCluster.getConnectString()) .withNamespace("test") .withServiceName("test-service") @@ -86,6 +87,7 @@ void testServiceProviderBuilder() { Assertions.assertTrue(exception instanceof IllegalArgumentException); val serviceProvider = ServiceProviderBuilders.unshardedServiceProviderBuilder() + .withUpstreamId("test-metric") .withConnectionString(testingCluster.getConnectString()) .withNamespace("test") .withServiceName("test-service") diff --git a/ranger-zookeeper/src/test/java/io/appform/ranger/zookeeper/serviceprovider/ZkNodeDataSinkMetricsIntegrationTest.java b/ranger-zookeeper/src/test/java/io/appform/ranger/zookeeper/serviceprovider/ZkNodeDataSinkMetricsIntegrationTest.java new file mode 100644 index 00000000..4593d5c7 --- /dev/null +++ b/ranger-zookeeper/src/test/java/io/appform/ranger/zookeeper/serviceprovider/ZkNodeDataSinkMetricsIntegrationTest.java @@ -0,0 +1,240 @@ +/* + * Copyright 2024 Authors, Flipkart Internet Pvt. Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.appform.ranger.zookeeper.serviceprovider; + +import com.codahale.metrics.MetricRegistry; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.appform.ranger.core.model.Service; +import io.appform.ranger.core.model.ServiceNode; +import io.appform.ranger.core.units.TestNodeData; +import io.appform.ranger.core.util.MetricRecorder; +import io.appform.ranger.zookeeper.serde.ZkNodeDataSerializer; +import lombok.val; +import org.apache.curator.framework.CuratorFramework; +import org.apache.curator.framework.CuratorFrameworkFactory; +import org.apache.curator.retry.ExponentialBackoffRetry; +import org.apache.curator.test.TestingCluster; +import org.junit.jupiter.api.*; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Integration tests for ZkNodeDataSink metrics recording. + * Tests verify that metrics are pushed through actual ZK write operations. + */ +class ZkNodeDataSinkMetricsIntegrationTest { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + private static final String METRIC_PREFIX = "io.appform.ranger"; + private static final String NAMESPACE = "test"; + private static final String SERVICE_NAME = "sink-test-svc"; + + private TestingCluster testingCluster; + private CuratorFramework curatorFramework; + private MetricRegistry metricRegistry; + + @BeforeEach + void setUp() throws Exception { + metricRegistry = new MetricRegistry(); + MetricRecorder.initialize(metricRegistry); + + testingCluster = new TestingCluster(3); + testingCluster.start(); + + curatorFramework = CuratorFrameworkFactory.builder() + .namespace(NAMESPACE) + .connectString(testingCluster.getConnectString()) + .retryPolicy(new ExponentialBackoffRetry(1000, 3)) + .build(); + curatorFramework.start(); + curatorFramework.blockUntilConnected(); + } + + @AfterEach + void tearDown() throws Exception { + if (curatorFramework != null) { + curatorFramework.close(); + } + if (testingCluster != null) { + testingCluster.close(); + } + } + + // ==================== updateState() - Success (create new node) ==================== + + @Test + void testUpdateState_createNewNode_recordsSuccess() { + val service = new Service(NAMESPACE, SERVICE_NAME); + val sink = new ZkNodeDataSink>( + "zk-sink-1", service, curatorFramework); + sink.start(); + + val serviceNode = ServiceNode.builder() + .host("host1").port(8080) + .nodeData(TestNodeData.builder().shardId(1).build()) + .build(); + + ZkNodeDataSerializer serializer = node -> { + try { + return MAPPER.writeValueAsBytes(node); + } catch (Exception e) { + throw new RuntimeException(e); + } + }; + + sink.updateState(serializer, serviceNode); + + // Verify success meter + val successMeter = metricRegistry.getMeters().get( + METRIC_PREFIX + ".dataStoreType.ZK.dataSource.zk-sink-1.nodeDataSink.update.success"); + assertNotNull(successMeter, "Node data sink update success meter should be recorded"); + assertEquals(1, successMeter.getCount()); + + // No failure + val failureMeter = metricRegistry.getMeters().get( + METRIC_PREFIX + ".dataStoreType.ZK.dataSource.zk-sink-1.nodeDataSink.update.failure"); + assertNull(failureMeter, "No failure meter should be recorded on success"); + + sink.stop(); + } + + // ==================== updateState() - Success (update existing node) ==================== + + @Test + void testUpdateState_updateExistingNode_recordsSuccess() { + val service = new Service(NAMESPACE, SERVICE_NAME); + val sink = new ZkNodeDataSink>( + "zk-sink-2", service, curatorFramework); + sink.start(); + + val serviceNode = ServiceNode.builder() + .host("host2").port(8081) + .nodeData(TestNodeData.builder().shardId(2).build()) + .build(); + + ZkNodeDataSerializer serializer = node -> { + try { + return MAPPER.writeValueAsBytes(node); + } catch (Exception e) { + throw new RuntimeException(e); + } + }; + + // First call creates + sink.updateState(serializer, serviceNode); + // Second call updates + sink.updateState(serializer, serviceNode); + + // Verify 2 success markers + val successMeter = metricRegistry.getMeters().get( + METRIC_PREFIX + ".dataStoreType.ZK.dataSource.zk-sink-2.nodeDataSink.update.success"); + assertNotNull(successMeter, "Success meter should exist"); + assertEquals(2, successMeter.getCount()); + + sink.stop(); + } + + // ==================== updateState() - Serialization failure ==================== + + @Test + void testUpdateState_serializerThrows_recordsSerDeFailure() { + val service = new Service(NAMESPACE, SERVICE_NAME); + val sink = new ZkNodeDataSink>( + "zk-sink-3", service, curatorFramework); + sink.start(); + + val serviceNode = ServiceNode.builder() + .host("host3").port(8082) + .nodeData(TestNodeData.builder().shardId(3).build()) + .build(); + + // Serializer that throws + ZkNodeDataSerializer badSerializer = node -> { + throw new RuntimeException("Serialization failed"); + }; + + // updateState should propagate the exception (wrapped in IllegalStateException) + assertThrows(Exception.class, () -> sink.updateState(badSerializer, serviceNode)); + + // Verify serialization failure metric + val serdeMeter = metricRegistry.getMeters().get( + METRIC_PREFIX + ".dataStoreType.ZK.dataSource.zk-sink-3.nodeDataSink.serialization.failure"); + assertNotNull(serdeMeter, "Serialization failure meter should be recorded"); + assertTrue(serdeMeter.getCount() >= 1); + + // Verify service-specific serialization failure + val svcSerdeMeter = metricRegistry.getMeters().get( + METRIC_PREFIX + ".dataStoreType.ZK.dataSource.zk-sink-3.nodeDataSink.serialization.serviceName." + + SERVICE_NAME + ".failure"); + assertNotNull(svcSerdeMeter, "Service-specific serialization failure meter should be recorded"); + assertTrue(svcSerdeMeter.getCount() >= 1); + + sink.stop(); + } + + // ==================== updateState() - Stopped state (no-op) ==================== + + @Test + void testUpdateState_stoppedSink_noMetricsRecorded() { + val service = new Service(NAMESPACE, SERVICE_NAME); + val sink = new ZkNodeDataSink>( + "zk-sink-4", service, curatorFramework); + sink.start(); + sink.stop(); + + val serviceNode = ServiceNode.builder() + .host("host4").port(8083) + .nodeData(TestNodeData.builder().shardId(4).build()) + .build(); + + ZkNodeDataSerializer serializer = node -> { + try { + return MAPPER.writeValueAsBytes(node); + } catch (Exception e) { + throw new RuntimeException(e); + } + }; + + // Should not throw, just return early + sink.updateState(serializer, serviceNode); + + // No success or failure metrics + assertNull(metricRegistry.getMeters().get( + METRIC_PREFIX + ".dataStoreType.ZK.dataSource.zk-sink-4.nodeDataSink.update.success")); + assertNull(metricRegistry.getMeters().get( + METRIC_PREFIX + ".dataStoreType.ZK.dataSource.zk-sink-4.nodeDataSink.update.failure")); + } + + // ==================== isActive() via ZkNodeDataStoreConnector base class ==================== + + @Test + void testIsActive_connectedCurator_recordsActiveStatus() { + val service = new Service(NAMESPACE, SERVICE_NAME); + val sink = new ZkNodeDataSink>( + "zk-sink-5", service, curatorFramework); + sink.start(); + + val active = sink.isActive(); + + assertTrue(active); + val activeMeter = metricRegistry.getMeters().get( + METRIC_PREFIX + ".dataStoreType.ZK.dataSource.zk-sink-5.active"); + assertNotNull(activeMeter, "Active meter should be recorded"); + assertEquals(1, activeMeter.getCount()); + + sink.stop(); + } +}