// Copyright 2017 JanusGraph 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.janusgraph.diskstorage.es; import com.google.common.base.Joiner; import com.google.common.base.Preconditions; import org.apache.commons.lang.StringUtils; import org.apache.http.HttpHost; import org.elasticsearch.client.RestClient; import org.elasticsearch.client.transport.TransportClient; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.transport.InetSocketTransportAddress; import org.janusgraph.diskstorage.configuration.ConfigNamespace; import org.janusgraph.diskstorage.configuration.ConfigOption; import org.janusgraph.diskstorage.configuration.Configuration; import org.janusgraph.diskstorage.es.rest.RestElasticSearchClient; import org.janusgraph.util.system.IOUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.lang.reflect.Array; import java.net.InetAddress; import java.util.ArrayList; import java.util.List; import java.util.Map; import static org.janusgraph.graphdb.configuration.GraphDatabaseConfiguration.INDEX_CONF_FILE; import static org.janusgraph.graphdb.configuration.GraphDatabaseConfiguration.INDEX_HOSTS; import static org.janusgraph.graphdb.configuration.GraphDatabaseConfiguration.INDEX_PORT; /** * Create an ES {@link org.elasticsearch.client.transport.TransportClient} or * {@link org.elasticsearch.client.RestClient} from a JanusGraph * {@link org.janusgraph.diskstorage.configuration.Configuration}. * <p> * Assumes that an ES cluster is already running. It does not attempt to start an * embedded ES instance. It just connects to whatever hosts are given in * {@link org.janusgraph.graphdb.configuration.GraphDatabaseConfiguration#INDEX_HOSTS}. * <p> * Setting arbitrary ES options is supported with the TransportClient * via {@link org.janusgraph.graphdb.configuration.GraphDatabaseConfiguration#INDEX_CONF_FILE}. * When this is set, it will be opened as an ordinary file and the contents will be * parsed as Elasticsearch settings. These settings override JanusGraph's defaults but * options explicitly provided in JanusGraph's config file in JanusGraph's properties will * override any value that might be in the ES settings file. * <p> * After loading the index conf file (when provided), any key-value pairs under the * {@link org.janusgraph.diskstorage.es.ElasticSearchIndex#ES_EXTRAS_NS} namespace * are copied into the Elasticsearch settings builder. This allows overridding arbitrary * ES settings from within the JanusGraph properties file. Settings in the ext namespace take * precedence over those in the index conf file. * <p> * After loading the index conf file and any key-value pairs under the ext namespace, * JanusGraph checks for ConfigOptions defined in * {@link org.janusgraph.diskstorage.es.ElasticSearchIndex} * that correspond directly to ES settings and copies them into the ES settings builder. */ public enum ElasticSearchSetup { /** * Start an ES TransportClient connected to * {@link org.janusgraph.graphdb.configuration.GraphDatabaseConfiguration#INDEX_HOSTS}. */ TRANSPORT_CLIENT { @Override public Connection connect(Configuration config) throws IOException { log.debug("Configuring TransportClient"); Settings.Builder settingsBuilder = settingsBuilder(config); if (config.has(ElasticSearchIndex.CLIENT_SNIFF)) { String k = "client.transport.sniff"; settingsBuilder.put(k, config.get(ElasticSearchIndex.CLIENT_SNIFF)); log.debug("Set {}: {}", k, config.get(ElasticSearchIndex.CLIENT_SNIFF)); } settingsBuilder.put("index.max_result_window", Integer.MAX_VALUE); TransportClient tc = TransportClient.builder().settings(settingsBuilder.build()).build(); int defaultPort = config.has(INDEX_PORT) ? config.get(INDEX_PORT) : ElasticSearchIndex.HOST_PORT_DEFAULT; for (String host : config.get(INDEX_HOSTS)) { String[] hostparts = host.split(":"); String hostname = hostparts[0]; int hostport = defaultPort; if (hostparts.length == 2) hostport = Integer.parseInt(hostparts[1]); log.info("Configured remote host: {} : {}", hostname, hostport); tc.addTransportAddress(new InetSocketTransportAddress(InetAddress.getByName(hostname), hostport)); } TransportElasticSearchClient client = new TransportElasticSearchClient(tc); if (config.has(ElasticSearchIndex.BULK_REFRESH)) { client.setBulkRefresh(!"false".equals(config.get(ElasticSearchIndex.BULK_REFRESH))); } return new Connection(client); } }, /** * Create an ES RestClient connected to * {@link org.janusgraph.graphdb.configuration.GraphDatabaseConfiguration#INDEX_HOSTS}. */ REST_CLIENT { @Override public Connection connect(Configuration config) throws IOException { log.debug("Configuring RestClient"); final List<HttpHost> hosts = new ArrayList<>(); int defaultPort = config.has(INDEX_PORT) ? config.get(INDEX_PORT) : ElasticSearchIndex.HOST_PORT_DEFAULT; for (String host : config.get(INDEX_HOSTS)) { String[] hostparts = host.split(":"); String hostname = hostparts[0]; int hostport = defaultPort; if (hostparts.length == 2) hostport = Integer.parseInt(hostparts[1]); log.info("Configured remote host: {} : {}", hostname, hostport); hosts.add(new HttpHost(hostname, hostport, "http")); } RestClient rc = RestClient.builder(hosts.toArray(new HttpHost[hosts.size()])).build(); RestElasticSearchClient client = new RestElasticSearchClient(rc); if (config.has(ElasticSearchIndex.BULK_REFRESH)) { client.setBulkRefresh(config.get(ElasticSearchIndex.BULK_REFRESH)); } return new Connection(client); } }; /** * Build and setup a new ES settings builder by consulting all JanusGraph config options * relevant to TransportClient or Node. Options may be specific to a single client, * but in this case they have no effect/are ignored on the other client. * <p> * This method creates a new ES ImmutableSettings.Builder, then carries out the following * operations on that settings builder in the listed order: * * <ol> * <li>Enable client.transport.ignore_cluster_name in the settings builder</li> * <li>If conf-file is set, open it using a FileInputStream and load its contents into the settings builder</li> * <li>Apply any settings in the ext.* meta namespace</li> * <li>If cluster-name is set, copy that value to cluster.name in the settings builder</li> * <li>If ignore-cluster-name is set, copy that value to client.transport.ignore_cluster_name in the settings builder</li> * <li>If client-sniff is set, copy that value to client.transport.sniff in the settings builder</li> * <li>If ttl-interval is set, copy that volue to indices.ttl.interval in the settings builder</li> * <li>Unconditionally set script.inline to true (i.e. enable inline scripting)</li> * </ol> * * This method then returns the builder. * * @param config a JanusGraph configuration possibly containing Elasticsearch index settings * @return ES settings builder configured according to the {@code config} parameter * @throws java.io.IOException if conf-file was set but could not be read */ private static Settings.Builder settingsBuilder(Configuration config) throws IOException { Settings.Builder settings = Settings.settingsBuilder(); // Set JanusGraph defaults settings.put("client.transport.ignore_cluster_name", true); settings.put("path.home", System.getProperty("java.io.tmpdir")); // Apply overrides from ES conf file applySettingsFromFile(settings, config, INDEX_CONF_FILE); // Apply ext.* overrides from JanusGraph conf file applySettingsFromJanusGraphConf(settings, config, ElasticSearchIndex.ES_EXTRAS_NS); // Apply individual JanusGraph ConfigOptions that map to ES settings if (config.has(ElasticSearchIndex.CLUSTER_NAME)) { String clustername = config.get(ElasticSearchIndex.CLUSTER_NAME); Preconditions.checkArgument(StringUtils.isNotBlank(clustername), "Invalid cluster name: %s", clustername); String k = "cluster.name"; settings.put(k, clustername); log.debug("Set {}: {}", k, clustername); } if (config.has(ElasticSearchIndex.IGNORE_CLUSTER_NAME)) { boolean ignoreClusterName = config.get(ElasticSearchIndex.IGNORE_CLUSTER_NAME); String k = "client.transport.ignore_cluster_name"; settings.put(k, ignoreClusterName); log.debug("Set {}: {}", k, ignoreClusterName); } // Force-enable inline scripting. This is probably only useful in Node mode. String inlineScriptsKey = "script.inline"; String inlineScriptsVal = settings.get(inlineScriptsKey); if (null != inlineScriptsVal && !"true".equals(inlineScriptsVal)) { log.error("JanusGraph requires Elasticsearch inline scripting but found {} set to false", inlineScriptsKey); throw new IOException("JanusGraph requires Elasticsearch inline scripting"); } settings.put(inlineScriptsKey, true); log.debug("Set {}: {}", inlineScriptsKey, false); return settings; } static void applySettingsFromFile(Settings.Builder settings, Configuration config, ConfigOption<String> confFileOption) throws FileNotFoundException { if (config.has(confFileOption)) { String confFile = config.get(confFileOption); log.debug("Loading Elasticsearch settings from file {}", confFile); InputStream confStream = null; try { confStream = new FileInputStream(confFile); settings.loadFromStream(confFile, confStream); } finally { IOUtils.closeQuietly(confStream); } } else { log.debug("Option {} is not set; not attempting to load Elasticsearch settings from file", confFileOption); } } static void applySettingsFromJanusGraphConf(Settings.Builder settings, Configuration config, ConfigNamespace rootNS) { int keysLoaded = 0; Map<String,Object> configSub = config.getSubset(rootNS); for (Map.Entry<String,Object> entry : configSub.entrySet()) { String key = entry.getKey(); Object val = entry.getValue(); if (null == val) continue; if (List.class.isAssignableFrom(val.getClass())) { // Pretty print lists using comma-separated values and no surrounding square braces for ES List l = (List) val; settings.put(key, Joiner.on(",").join(l)); } else if (val.getClass().isArray()) { // As with Lists, but now for arrays // The Object copy[] business lets us avoid repetitive primitive array type checking and casting Object copy[] = new Object[Array.getLength(val)]; for (int i= 0; i < copy.length; i++) { copy[i] = Array.get(val, i); } settings.put(key, Joiner.on(",").join(copy)); } else { // Copy anything else unmodified settings.put(key, val.toString()); } log.debug("[ES ext.* cfg] Set {}: {}", key, val); keysLoaded++; } log.debug("Loaded {} settings from the {} JanusGraph config namespace", keysLoaded, rootNS); } private static final Logger log = LoggerFactory.getLogger(ElasticSearchSetup.class); public abstract Connection connect(Configuration config) throws IOException; public static class Connection { private final ElasticSearchClient client; public Connection(ElasticSearchClient client) { this.client = client; Preconditions.checkNotNull(this.client, "Unable to instantiate Elasticsearch Client object"); } public ElasticSearchClient getClient() { return client; } } }