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

zipkin2.elasticsearch.ElasticsearchSpanConsumer Maven / Gradle / Ivy

/*
 * Copyright The OpenZipkin Authors
 * SPDX-License-Identifier: Apache-2.0
 */
package zipkin2.elasticsearch;

import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import zipkin2.Call;
import zipkin2.Span;
import zipkin2.elasticsearch.internal.BulkCallBuilder;
import zipkin2.elasticsearch.internal.BulkIndexWriter;
import zipkin2.elasticsearch.internal.IndexNameFormatter;
import zipkin2.internal.DelayLimiter;
import zipkin2.storage.SpanConsumer;

import static zipkin2.elasticsearch.VersionSpecificTemplates.TYPE_AUTOCOMPLETE;
import static zipkin2.elasticsearch.VersionSpecificTemplates.TYPE_SPAN;
import static zipkin2.internal.RecyclableBuffers.SHORT_STRING_LENGTH;

class ElasticsearchSpanConsumer implements SpanConsumer { // not final for testing
  final ElasticsearchStorage es;
  final Set autocompleteKeys;
  final IndexNameFormatter indexNameFormatter;
  final char indexTypeDelimiter;
  final boolean searchEnabled;
  final DelayLimiter delayLimiter;

  ElasticsearchSpanConsumer(ElasticsearchStorage es) {
    this.es = es;
    this.autocompleteKeys = new LinkedHashSet<>(es.autocompleteKeys());
    this.indexNameFormatter = es.indexNameFormatter();
    this.indexTypeDelimiter = es.indexTypeDelimiter();
    this.searchEnabled = es.searchEnabled();
    this.delayLimiter = DelayLimiter.newBuilder()
      .ttl(es.autocompleteTtl(), TimeUnit.MILLISECONDS)
      .cardinality(es.autocompleteCardinality()).build();
  }

  String formatTypeAndTimestampForInsert(String type, long timestampMillis) {
    return indexNameFormatter
      .formatTypeAndTimestampForInsert(type, indexTypeDelimiter, timestampMillis);
  }

  @Override public Call accept(List spans) {
    if (spans.isEmpty()) return Call.create(null);
    BulkSpanIndexer indexer = new BulkSpanIndexer(this);
    indexSpans(indexer, spans);
    return indexer.newCall();
  }

  void indexSpans(BulkSpanIndexer indexer, List spans) {
    for (Span span : spans) {
      final long indexTimestamp; // which index to store this span into
      if (span.timestampAsLong() != 0L) {
        indexTimestamp = span.timestampAsLong() / 1000L;
      } else if (!span.annotations().isEmpty()) {
        // guessTimestamp is made for determining the span's authoritative timestamp. When choosing
        // the index bucket, any annotation is better than using current time.
        indexTimestamp = span.annotations().get(0).timestamp() / 1000L;
      } else {
        indexTimestamp = System.currentTimeMillis();
      }
      indexer.add(indexTimestamp, span);
      if (searchEnabled && !span.tags().isEmpty()) {
        indexer.addAutocompleteValues(indexTimestamp, span);
      }
    }
  }

  /** Mutable type used for each call to store spans */
  static final class BulkSpanIndexer {
    final BulkCallBuilder bulkCallBuilder;
    final ElasticsearchSpanConsumer consumer;
    final List pendingAutocompleteContexts = new ArrayList<>();
    final BulkIndexWriter spanWriter;

    BulkSpanIndexer(ElasticsearchSpanConsumer consumer) {
      this.bulkCallBuilder = new BulkCallBuilder(consumer.es, consumer.es.version(), "index-span");
      this.consumer = consumer;
      this.spanWriter =
        consumer.searchEnabled ? BulkIndexWriter.SPAN : BulkIndexWriter.SPAN_SEARCH_DISABLED;
    }

    void add(long indexTimestamp, Span span) {
      String index = consumer.formatTypeAndTimestampForInsert(TYPE_SPAN, indexTimestamp);
      bulkCallBuilder.index(index, TYPE_SPAN, span, spanWriter);
    }

    void addAutocompleteValues(long indexTimestamp, Span span) {
      String idx = consumer.formatTypeAndTimestampForInsert(TYPE_AUTOCOMPLETE, indexTimestamp);
      for (Map.Entry tag : span.tags().entrySet()) {
        int length = tag.getKey().length() + tag.getValue().length() + 1;
        if (length > SHORT_STRING_LENGTH) continue;

        // If the autocomplete whitelist doesn't contain the key, skip storing its value
        if (!consumer.autocompleteKeys.contains(tag.getKey())) continue;

        AutocompleteContext context =
          new AutocompleteContext(indexTimestamp, tag.getKey(), tag.getValue());
        if (!consumer.delayLimiter.shouldInvoke(context)) continue;
        pendingAutocompleteContexts.add(context);

        bulkCallBuilder.index(idx, TYPE_AUTOCOMPLETE, tag, BulkIndexWriter.AUTOCOMPLETE);
      }
    }

    Call newCall() {
      Call storeCall = bulkCallBuilder.build();
      if (pendingAutocompleteContexts.isEmpty()) return storeCall;
      return storeCall.handleError((error, callback) -> {
        for (AutocompleteContext context : pendingAutocompleteContexts) {
          consumer.delayLimiter.invalidate(context);
        }
        callback.onError(error);
      });
    }
  }

  static final class AutocompleteContext {
    final long timestamp;
    final String key, value;

    AutocompleteContext(long timestamp, String key, String value) {
      this.timestamp = timestamp;
      this.key = key;
      this.value = value;
    }

    @Override public boolean equals(Object o) {
      if (o == this) return true;
      if (!(o instanceof AutocompleteContext)) return false;
      AutocompleteContext that = (AutocompleteContext) o;
      return timestamp == that.timestamp && key.equals(that.key) && value.equals(that.value);
    }

    @Override public int hashCode() {
      int h$ = 1;
      h$ *= 1000003;
      h$ ^= (int) (h$ ^ ((timestamp >>> 32) ^ timestamp));
      h$ *= 1000003;
      h$ ^= key.hashCode();
      h$ *= 1000003;
      h$ ^= value.hashCode();
      return h$;
    }
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy