All Downloads are FREE. Search and download functionalities are using the official Maven repository.

zipkin2.elasticsearch.ElasticsearchStorage Maven / Gradle / Ivy

There is a newer version: 3.4.2
Show newest version
/*
 * Copyright 2015-2021 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 zipkin2.elasticsearch;

import com.fasterxml.jackson.core.JsonParser;
import com.google.auto.value.AutoValue;
import com.google.auto.value.extension.memoized.Memoized;
import com.linecorp.armeria.client.ResponseTimeoutException;
import com.linecorp.armeria.client.WebClient;
import com.linecorp.armeria.client.endpoint.EndpointGroup;
import com.linecorp.armeria.common.AggregatedHttpRequest;
import com.linecorp.armeria.common.HttpMethod;
import java.io.Closeable;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;
import zipkin2.Call;
import zipkin2.CheckResult;
import zipkin2.elasticsearch.internal.IndexNameFormatter;
import zipkin2.elasticsearch.internal.Internal;
import zipkin2.elasticsearch.internal.client.HttpCall;
import zipkin2.elasticsearch.internal.client.HttpCall.BodyConverter;
import zipkin2.internal.Nullable;
import zipkin2.storage.AutocompleteTags;
import zipkin2.storage.ServiceAndSpanNames;
import zipkin2.storage.SpanConsumer;
import zipkin2.storage.SpanStore;
import zipkin2.storage.StorageComponent;
import zipkin2.storage.Traces;

import static com.linecorp.armeria.common.HttpMethod.GET;
import static zipkin2.elasticsearch.ElasticsearchVersion.V7_0;
import static zipkin2.elasticsearch.ElasticsearchVersion.V6_7;
import static zipkin2.elasticsearch.ElasticsearchVersion.V7_8;
import static zipkin2.elasticsearch.EnsureIndexTemplate.ensureIndexTemplate;
import static zipkin2.elasticsearch.VersionSpecificTemplates.TYPE_AUTOCOMPLETE;
import static zipkin2.elasticsearch.VersionSpecificTemplates.TYPE_DEPENDENCY;
import static zipkin2.elasticsearch.VersionSpecificTemplates.TYPE_SPAN;
import static zipkin2.elasticsearch.internal.JsonReaders.enterPath;

@AutoValue
public abstract class ElasticsearchStorage extends zipkin2.storage.StorageComponent {
  /**
   * This defers creation of an {@link WebClient}. This is needed because routinely, I/O occurs in
   * constructors and this can delay or cause startup to crash. For example, an underlying {@link
   * EndpointGroup} could be delayed due to DNS, implicit api calls or health checks.
   */
  public interface LazyHttpClient extends Supplier, Closeable {
    /**
     * Lazily creates an instance of the http client configured to the correct elasticsearch host or
     * cluster. The same value should always be returned.
     */
    @Override WebClient get();

    @Override default void close() {
    }

    /** This should return the initial endpoints in a single-string without resolving them. */
    @Override String toString();
  }

  /** The lazy http client supplier will be closed on {@link #close()} */
  public static Builder newBuilder(LazyHttpClient lazyHttpClient) {
    return new $AutoValue_ElasticsearchStorage.Builder()
      .lazyHttpClient(lazyHttpClient)
      .strictTraceId(true)
      .searchEnabled(true)
      .index("zipkin")
      .dateSeparator('-')
      .indexShards(5)
      .indexReplicas(1)
      .ensureTemplates(true)
      .namesLookback(86400000)
      .flushOnWrites(false)
      .autocompleteKeys(Collections.emptyList())
      .autocompleteTtl((int) TimeUnit.HOURS.toMillis(1))
      .autocompleteCardinality(5 * 4000); // Ex. 5 site tags with cardinality 4000 each
  }

  abstract Builder toBuilder();

  @AutoValue.Builder
  public abstract static class Builder extends StorageComponent.Builder {

    /**
     * Only valid when the destination is Elasticsearch 5.x. Indicates the ingest pipeline used
     * before spans are indexed. No default.
     *
     * 

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 zipkin2.Span#timestamp()} are at or after * (now - lookback) in milliseconds. Defaults to 1 day (86400000). */ public abstract Builder namesLookback(int namesLookback); /** * Internal and visible only for testing. * *

See https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-refresh.html */ public 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 '-'. * *

By default, spans with a timestamp falling on 2016/03/19 end up in the index * 'zipkin-span-2016-03-19'. When the date separator is '.', the index would be * 'zipkin-span-2016.03.19'. If the date separator is 0, there is no delimiter. Ex the index * would be 'zipkin-span-20160319' */ 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. * *

Corresponds to index.number_of_shards */ 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. * *

Corresponds to index.number_of_replicas */ public abstract Builder indexReplicas(int indexReplicas); /** False disables automatic index template installation. */ public abstract Builder ensureTemplates(boolean ensureTemplates); /** * Only valid when the destination is Elasticsearch >= 7.8. Indicates the index template * priority in case of multiple matching templates. The template with highest priority is used. * Default to 0. * *

See https://www.elastic.co/guide/en/elasticsearch/reference/7.8/_index_template_and_settings_priority.html */ public abstract Builder templatePriority(@Nullable Integer templatePriority); /** {@inheritDoc} */ @Override public abstract Builder strictTraceId(boolean strictTraceId); /** {@inheritDoc} */ @Override public abstract Builder searchEnabled(boolean searchEnabled); /** {@inheritDoc} */ @Override public abstract Builder autocompleteKeys(List autocompleteKeys); /** {@inheritDoc} */ @Override public abstract Builder autocompleteTtl(int autocompleteTtl); /** {@inheritDoc} */ @Override public abstract Builder autocompleteCardinality(int autocompleteCardinality); @Override public abstract ElasticsearchStorage build(); abstract Builder lazyHttpClient(LazyHttpClient lazyHttpClient); abstract IndexNameFormatter.Builder indexNameFormatterBuilder(); Builder() { } } abstract LazyHttpClient lazyHttpClient(); @Nullable public abstract String pipeline(); public abstract boolean flushOnWrites(); public abstract boolean strictTraceId(); abstract boolean searchEnabled(); abstract List autocompleteKeys(); abstract int autocompleteTtl(); abstract int autocompleteCardinality(); abstract int indexShards(); abstract int indexReplicas(); public abstract IndexNameFormatter indexNameFormatter(); abstract boolean ensureTemplates(); public abstract int namesLookback(); @Nullable abstract Integer templatePriority(); @Override public SpanStore spanStore() { ensureIndexTemplates(); return new ElasticsearchSpanStore(this); } @Override public Traces traces() { return (Traces) spanStore(); } @Override public ServiceAndSpanNames serviceAndSpanNames() { return (ServiceAndSpanNames) spanStore(); } @Override public AutocompleteTags autocompleteTags() { ensureIndexTemplates(); return new ElasticsearchAutocompleteTags(this); } @Override public SpanConsumer spanConsumer() { ensureIndexTemplates(); return new ElasticsearchSpanConsumer(this); } /** Returns the Elasticsearch version of the connected cluster. Internal use only */ @Memoized public ElasticsearchVersion version() { try { return ElasticsearchVersion.get(http()); } catch (IOException e) { throw new UncheckedIOException(e); } } char indexTypeDelimiter() { return VersionSpecificTemplates.indexTypeDelimiter(version()); } /** This is an internal blocking call, only used in tests. */ public void clear() throws IOException { Set toClear = new LinkedHashSet<>(); toClear.add(indexNameFormatter().formatType(TYPE_SPAN)); toClear.add(indexNameFormatter().formatType(TYPE_DEPENDENCY)); for (String index : toClear) clear(index); } void clear(String index) throws IOException { String url = '/' + index; AggregatedHttpRequest delete = AggregatedHttpRequest.of(HttpMethod.DELETE, url); http().newCall(delete, BodyConverters.NULL, "delete-index").execute(); } /** * Internal code and api responses coerce to {@link RejectedExecutionException} when work is * rejected. We also classify {@link ResponseTimeoutException} as a capacity related exception * eventhough capacity is not the only reason (timeout could also result from a misconfiguration * or a network problem). */ @Override public boolean isOverCapacity(Throwable e) { return e instanceof RejectedExecutionException || e instanceof ResponseTimeoutException; } /** This is blocking so that we can determine if the cluster is healthy or not */ @Override public CheckResult check() { return ensureIndexTemplatesAndClusterReady(indexNameFormatter().formatType(TYPE_SPAN)); } /** * This allows the health check to display problems, such as access, installing the index * template. It also helps reduce traffic sent to nodes still initializing (when guarded on the * check result). Finally, this reads the cluster health of the index as it can go down after the * one-time initialization passes. */ CheckResult ensureIndexTemplatesAndClusterReady(String index) { try { version(); // ensure the version is available (even if we already cached it) ensureIndexTemplates(); // called only once, so we have to double-check health AggregatedHttpRequest request = AggregatedHttpRequest.of(GET, "/_cluster/health/" + index); CheckResult result = http().newCall(request, READ_STATUS, "get-cluster-health").execute(); if (result == null) throw new IllegalArgumentException("No content reading cluster health"); return result; } catch (Throwable e) { Call.propagateIfFatal(e); // Wrapping interferes with humans intended to read this message: // // Unwrap the marker exception as the health check is not relevant for the throttle component. // Unwrap any IOException from the first call to ensureIndexTemplates() if (e instanceof RejectedExecutionException || e instanceof UncheckedIOException) { e = e.getCause(); } return CheckResult.failed(e); } } volatile boolean ensuredTemplates; // synchronized since we don't want overlapping calls to apply the index templates void ensureIndexTemplates() { if (ensuredTemplates) return; if (!ensureTemplates()) ensuredTemplates = true; synchronized (this) { if (ensuredTemplates) return; doEnsureIndexTemplates(); ensuredTemplates = true; } } IndexTemplates doEnsureIndexTemplates() { try { HttpCall.Factory http = http(); IndexTemplates templates = versionSpecificTemplates(version()); ensureIndexTemplate(http, buildUrl(templates, TYPE_SPAN), templates.span()); ensureIndexTemplate(http, buildUrl(templates, TYPE_DEPENDENCY), templates.dependency()); ensureIndexTemplate(http, buildUrl(templates, TYPE_AUTOCOMPLETE), templates.autocomplete()); return templates; } catch (IOException e) { throw new UncheckedIOException(e); } } IndexTemplates versionSpecificTemplates(ElasticsearchVersion version) { return new VersionSpecificTemplates( indexNameFormatter().index(), indexReplicas(), indexShards(), searchEnabled(), strictTraceId(), templatePriority() ).get(version); } String buildUrl(IndexTemplates templates, String type) { String indexPrefix = indexNameFormatter().index() + templates.indexTypeDelimiter(); if (version().compareTo(V7_8) >= 0 && templatePriority() != null) { return "/_index_template/" + indexPrefix + type + "_template"; } if (version().compareTo(V6_7) >= 0 && version().compareTo(V7_0) < 0) { // because deprecation warning on 6 to prepare for 7: // // [types removal] The parameter include_type_name should be explicitly specified in get // template requests to prepare for 7.0. In 7.0 include_type_name will default to 'false', // which means responses will omit the type name in mapping definitions. // // The parameter include_type_name was added in 6.7. Using this with ES older than // 6.7 will result in unrecognized parameter: [include_type_name]. return "/_template/" + indexPrefix + type + "_template?include_type_name=true"; } return "/_template/" + indexPrefix + type + "_template"; } @Override public final String toString() { return "ElasticsearchStorage{initialEndpoints=" + lazyHttpClient() + ", index=" + indexNameFormatter().index() + "}"; } static { Internal.instance = new Internal() { @Override public HttpCall.Factory http(ElasticsearchStorage storage) { return storage.http(); } }; } @Memoized HttpCall.Factory http() { return new HttpCall.Factory(lazyHttpClient().get()); } @Override public void close() { lazyHttpClient().close(); } ElasticsearchStorage() { } static final BodyConverter READ_STATUS = new BodyConverter() { @Override public CheckResult convert(JsonParser parser, Supplier contentString) throws IOException { JsonParser status = enterPath(parser, "status"); if (status == null) { throw new IllegalArgumentException("Health status couldn't be read " + contentString.get()); } if ("RED".equalsIgnoreCase(status.getText())) { return CheckResult.failed(new IllegalStateException("Health status is RED")); } return CheckResult.OK; } @Override public String toString() { return "ReadStatus"; } }; }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy