/** * Copyright 2015-2017 The OpenZipkin 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 zipkin.storage.elasticsearch.http; import com.google.auto.value.AutoValue; import com.google.auto.value.extension.memoized.Memoized; import com.squareup.moshi.JsonReader; import java.io.IOException; import java.util.Collections; import java.util.List; import okhttp3.HttpUrl; import okhttp3.MediaType; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.RequestBody; import okio.Buffer; import zipkin.internal.Nullable; import zipkin.storage.AsyncSpanConsumer; import zipkin.storage.AsyncSpanStore; import zipkin.storage.SpanStore; import zipkin.storage.StorageAdapters; import zipkin.storage.StorageComponent; import zipkin.storage.elasticsearch.http.internal.client.HttpCall; import static zipkin.internal.Util.checkNotNull; import static zipkin.moshi.JsonReaders.enterPath; @AutoValue public abstract class ElasticsearchHttpStorage implements StorageComponent { /** * A list of elasticsearch nodes to connect to, in http://host:port or https://host:port * format. Note this value is only read once. */ public interface HostsSupplier { List<String> get(); } static final MediaType APPLICATION_JSON = MediaType.parse("application/json"); public static Builder builder(OkHttpClient client) { return new $AutoValue_ElasticsearchHttpStorage.Builder() .client(client) .hosts(Collections.singletonList("http://localhost:9200")) .maxRequests(64) .strictTraceId(true) .index("zipkin") .dateSeparator('-') .indexShards(5) .indexReplicas(1) .namesLookback(86400000) .shutdownClientOnClose(false) .flushOnWrites(false); } public static Builder builder() { Builder result = builder(new OkHttpClient()); result.shutdownClientOnClose(true); return result; } @AutoValue.Builder public static abstract class Builder implements StorageComponent.Builder { abstract Builder client(OkHttpClient client); abstract Builder shutdownClientOnClose(boolean shutdownClientOnClose); /** * A list of elasticsearch nodes to connect to, in http://host:port or https://host:port * format. Defaults to "http://localhost:9200". */ public final Builder hosts(final List<String> hosts) { checkNotNull(hosts, "hosts"); return hostsSupplier(new HostsSupplier() { @Override public List<String> get() { return hosts; } @Override public String toString() { return hosts.toString(); } }); } /** * Like {@link #hosts(List)}, except the value is deferred. * * <p>This was added to support dynamic endpoint resolution for Amazon Elasticsearch. This value * is only read once. */ public abstract Builder hostsSupplier(HostsSupplier hosts); /** Sets maximum in-flight requests from this process to any Elasticsearch host. Defaults to 64 */ public abstract Builder maxRequests(int maxRequests); /** * Only valid when the destination is Elasticsearch 5.x. Indicates the ingest pipeline used * before spans are indexed. No default. * * <p>See https://www.elastic.co/guide/en/elasticsearch/reference/master/pipeline.html */ public abstract Builder pipeline(String pipeline); /** * Only return span and service names where all {@link zipkin.Span#timestamp} are at or after * (now - lookback) in milliseconds. Defaults to 1 day (86400000). */ public abstract Builder namesLookback(int namesLookback); /** Visible for testing */ abstract Builder flushOnWrites(boolean flushOnWrites); /** * The index prefix to use when generating daily index names. Defaults to zipkin. */ public final Builder index(String index) { indexNameFormatterBuilder().index(index); return this; } /** * The date separator to use when generating daily index names. Defaults to '-'. * * <p>By default, spans with a timestamp falling on 2016/03/19 end up in the index * 'zipkin-2016-03-19'. When the date separator is '.', the index would be 'zipkin-2016.03.19'. */ public final Builder dateSeparator(char dateSeparator) { indexNameFormatterBuilder().dateSeparator(dateSeparator); return this; } /** * The number of shards to split the index into. Each shard and its replicas are assigned to a * machine in the cluster. Increasing the number of shards and machines in the cluster will * improve read and write performance. Number of shards cannot be changed for existing indices, * but new daily indices will pick up changes to the setting. Defaults to 5. * * <p>Corresponds to <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/index-modules.html">index.number_of_shards</a> */ public abstract Builder indexShards(int indexShards); /** * The number of replica copies of each shard in the index. Each shard and its replicas are * assigned to a machine in the cluster. Increasing the number of replicas and machines in the * cluster will improve read performance, but not write performance. Number of replicas can be * changed for existing indices. Defaults to 1. It is highly discouraged to set this to 0 as it * would mean a machine failure results in data loss. * * <p>Corresponds to <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/index-modules.html">index.number_of_replicas</a> */ public abstract Builder indexReplicas(int indexReplicas); @Override public abstract Builder strictTraceId(boolean strictTraceId); @Override public abstract ElasticsearchHttpStorage build(); abstract IndexNameFormatter.Builder indexNameFormatterBuilder(); Builder() { } } abstract OkHttpClient client(); abstract boolean shutdownClientOnClose(); abstract HostsSupplier hostsSupplier(); @Nullable abstract String pipeline(); abstract boolean flushOnWrites(); abstract int maxRequests(); abstract boolean strictTraceId(); abstract int indexShards(); abstract int indexReplicas(); abstract IndexNameFormatter indexNameFormatter(); abstract int namesLookback(); @Override public SpanStore spanStore() { return StorageAdapters.asyncToBlocking(asyncSpanStore()); } @Override public AsyncSpanStore asyncSpanStore() { ensureIndexTemplate(); return new ElasticsearchHttpSpanStore(this); } @Override public AsyncSpanConsumer asyncSpanConsumer() { ensureIndexTemplate(); return new ElasticsearchHttpSpanConsumer(this); } /** This is a blocking call, only used in tests. */ void clear() throws IOException { clear(indexNameFormatter().allIndices()); } void clear(String index) throws IOException { Request deleteRequest = new Request.Builder() .url(http().baseUrl.newBuilder().addPathSegment(index).build()) .delete().tag("delete-index").build(); http().execute(deleteRequest, b -> null); flush(http(), index); } /** This is a blocking call, only used in tests. */ static void flush(HttpCall.Factory factory, String index) throws IOException { Request flushRequest = new Request.Builder() .url(factory.baseUrl.newBuilder().addPathSegment(index).addPathSegment("_flush").build()) .post(RequestBody.create(APPLICATION_JSON, "")) .tag("flush-index").build(); factory.execute(flushRequest, b -> null); } /** This is blocking so that we can determine if the cluster is healthy or not */ @Override public CheckResult check() { return ensureClusterReady(indexNameFormatter().allIndices()); } CheckResult ensureClusterReady(String index) { Request request = new Request.Builder().url(http().baseUrl.resolve("/_cluster/health/" + index)) .tag("get-cluster-health").build(); try { return http().execute(request, b -> { b.request(Long.MAX_VALUE); // Buffer the entire body. Buffer body = b.buffer(); JsonReader status = enterPath(JsonReader.of(body.clone()), "status"); if (status == null) { throw new IllegalStateException("Health status couldn't be read " + body.readUtf8()); } if ("RED".equalsIgnoreCase(status.nextString())) { throw new IllegalStateException("Health status is RED"); } return CheckResult.OK; }); } catch (RuntimeException e) { return CheckResult.failed(e); } } @Memoized // since there's a network call required to get the version String indexTemplate() { return new VersionSpecificTemplate(this).get(http()); } @Memoized // since we don't want overlapping calls to apply the index template boolean ensureIndexTemplate() { EnsureIndexTemplate.apply(http(), indexNameFormatter().index() + "_template", indexTemplate()); return true; // as Memoized cannot return void } @Memoized // hosts resolution might imply a network call, and we might make a new okhttp instance HttpCall.Factory http() { List<String> hosts = hostsSupplier().get(); if (hosts.isEmpty()) throw new IllegalArgumentException("no hosts configured"); OkHttpClient ok = hosts.size() == 1 ? client() : client().newBuilder() .dns(PseudoAddressRecordSet.create(hosts, client().dns())) .build(); ok.dispatcher().setMaxRequests(maxRequests()); ok.dispatcher().setMaxRequestsPerHost(maxRequests()); return new HttpCall.Factory(ok, HttpUrl.parse(hosts.get(0))); } @Override public void close() { if (!shutdownClientOnClose()) return; http().close(); } ElasticsearchHttpStorage() { } }