/*
* 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;
}
}