/* * Copyright 2013-2016 the original author or authors. * * 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 org.springframework.cloud.netflix.turbine; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Map; import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.client.discovery.DiscoveryClient; import org.springframework.expression.Expression; import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.expression.spel.support.StandardEvaluationContext; import com.netflix.turbine.discovery.Instance; import com.netflix.turbine.discovery.InstanceDiscovery; import lombok.extern.apachecommons.CommonsLog; /** * Class that encapsulates an {@link InstanceDiscovery} * implementation that uses Spring Cloud Commons (see https://github.com/spring-cloud/spring-cloud-commons) * The plugin requires a list of applications configured. It then queries the set of * instances for * each application. Instance information retrieved from the {@link DiscoveryClient} * must be translated to * something that Turbine can understand i.e the * {@link Instance} class. * <p> * All the logic to perform this translation can be overriden here, so that you can * provide your own implementation if needed. * * @author Spencer Gibb */ @CommonsLog public class CommonsInstanceDiscovery implements InstanceDiscovery { private static final String DEFAULT_CLUSTER_NAME_EXPRESSION = "serviceId"; protected static final String PORT_KEY = "port"; protected static final String SECURE_PORT_KEY = "securePort"; protected static final String FUSED_HOST_PORT_KEY = "fusedHostPort"; private final Expression clusterNameExpression; private DiscoveryClient discoveryClient; private TurbineProperties turbineProperties; private final boolean combineHostPort; public CommonsInstanceDiscovery(TurbineProperties turbineProperties, DiscoveryClient discoveryClient) { this(turbineProperties, DEFAULT_CLUSTER_NAME_EXPRESSION); this.discoveryClient = discoveryClient; } protected CommonsInstanceDiscovery(TurbineProperties turbineProperties, String defaultExpression) { this.turbineProperties = turbineProperties; SpelExpressionParser parser = new SpelExpressionParser(); String clusterNameExpression = turbineProperties .getClusterNameExpression(); if (clusterNameExpression == null) { clusterNameExpression = defaultExpression; } this.clusterNameExpression = parser.parseExpression(clusterNameExpression); this.combineHostPort = turbineProperties.isCombineHostPort(); } protected Expression getClusterNameExpression() { return clusterNameExpression; } public TurbineProperties getTurbineProperties() { return turbineProperties; } protected boolean isCombineHostPort() { return combineHostPort; } /** * Method that queries DiscoveryClient for a list of configured application names * @return Collection<Instance> */ @Override public Collection<Instance> getInstanceList() throws Exception { List<Instance> instances = new ArrayList<>(); List<String> appNames = getTurbineProperties().getAppConfigList(); if (appNames == null || appNames.size() == 0) { log.info("No apps configured, returning an empty instance list"); return instances; } log.info("Fetching instance list for apps: " + appNames); for (String appName : appNames) { try { instances.addAll(getInstancesForApp(appName)); } catch (Exception ex) { log.error("Failed to fetch instances for app: " + appName + ", retrying once more", ex); try { instances.addAll(getInstancesForApp(appName)); } catch (Exception retryException) { log.error("Failed again to fetch instances for app: " + appName + ", giving up", ex); } } } return instances; } /** * helper that fetches the Instances for each application from DiscoveryClient. * @param serviceId * @return List<Instance> * @throws Exception */ protected List<Instance> getInstancesForApp(String serviceId) throws Exception { List<Instance> instances = new ArrayList<>(); log.info("Fetching instances for app: " + serviceId); List<ServiceInstance> serviceInstances = discoveryClient.getInstances(serviceId); if (serviceInstances == null || serviceInstances.isEmpty()) { log.warn("DiscoveryClient returned null or empty for service: " + serviceId); return instances; } try { log.info("Received instance list for service: " + serviceId + ", size=" + serviceInstances.size()); for (ServiceInstance serviceInstance : serviceInstances) { Instance instance = marshall(serviceInstance); if (instance != null) { instances.add(instance); } } } catch (Exception e) { log.warn("Failed to retrieve instances from DiscoveryClient", e); } return instances; } /** * Private helper that marshals the information from each instance into something that * Turbine can understand. Override this method for your own implementation. * @param serviceInstance * @return Instance */ Instance marshall(ServiceInstance serviceInstance) { String hostname = serviceInstance.getHost(); String managementPort = serviceInstance.getMetadata().get("management.port"); String port = managementPort == null ? String.valueOf(serviceInstance.getPort()) : managementPort; String cluster = getClusterName(serviceInstance); Boolean status = Boolean.TRUE; //TODO: where to get? if (hostname != null && cluster != null && status != null) { Instance instance = getInstance(hostname, port, cluster, status); Map<String, String> metadata = serviceInstance.getMetadata(); boolean securePortEnabled = serviceInstance.isSecure(); addMetadata(instance, hostname, port, securePortEnabled, port, metadata); return instance; } else { return null; } } protected void addMetadata(Instance instance, String hostname, String port, boolean securePortEnabled, String securePort, Map<String, String> metadata) { // add metadata if (metadata != null) { instance.getAttributes().putAll(metadata); } // add ports instance.getAttributes().put(PORT_KEY, port); if (securePortEnabled) { instance.getAttributes().put(SECURE_PORT_KEY, securePort); } if (this.isCombineHostPort()) { String fusedHostPort = securePortEnabled ? hostname+":"+securePort : instance.getHostname() ; instance.getAttributes().put(FUSED_HOST_PORT_KEY, fusedHostPort); } } protected Instance getInstance(String hostname, String port, String cluster, Boolean status) { String hostPart = this.isCombineHostPort() ? hostname+":"+port : hostname; return new Instance(hostPart, cluster, status); } /** * Helper that fetches the cluster name. Cluster is a Turbine concept and not a commons * concept. By default we choose the amazon serviceId as the cluster. A custom * implementation can be plugged in by overriding this method. */ protected String getClusterName(Object object) { StandardEvaluationContext context = new StandardEvaluationContext(object); Object value = this.clusterNameExpression.getValue(context); if (value != null) { return value.toString(); } return null; } }