/**
* 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 java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import zipkin.DependencyLink;
import zipkin.Span;
import zipkin.internal.CorrectForClockSkew;
import zipkin.internal.GroupByTraceId;
import zipkin.internal.MergeById;
import zipkin.internal.Nullable;
import zipkin.internal.Util;
import zipkin.storage.AsyncSpanStore;
import zipkin.storage.Callback;
import zipkin.storage.QueryRequest;
import zipkin.storage.elasticsearch.http.internal.client.Aggregation;
import zipkin.storage.elasticsearch.http.internal.client.HttpCall;
import zipkin.storage.elasticsearch.http.internal.client.SearchCallFactory;
import zipkin.storage.elasticsearch.http.internal.client.SearchRequest;
import static java.util.Arrays.asList;
final class ElasticsearchHttpSpanStore implements AsyncSpanStore {
static final String SPAN = "span";
static final String DEPENDENCY_LINK = "dependencylink";
static final String SERVICE_SPAN = "servicespan";
final SearchCallFactory search;
final String[] allIndices;
final IndexNameFormatter indexNameFormatter;
final boolean strictTraceId;
final int namesLookback;
ElasticsearchHttpSpanStore(ElasticsearchHttpStorage es) {
this.search = new SearchCallFactory(es.http());
this.allIndices = new String[] {es.indexNameFormatter().allIndices()};
this.indexNameFormatter = es.indexNameFormatter();
this.strictTraceId = es.strictTraceId();
this.namesLookback = es.namesLookback();
}
@Override public void getTraces(QueryRequest request, Callback<List<List<Span>>> callback) {
long beginMillis = request.endTs - request.lookback;
long endMillis = request.endTs;
SearchRequest.Filters filters = new SearchRequest.Filters();
filters.addRange("timestamp_millis", beginMillis, endMillis);
if (request.serviceName != null) {
filters.addNestedTerms(asList(
"annotations.endpoint.serviceName",
"binaryAnnotations.endpoint.serviceName"
), request.serviceName);
}
if (request.spanName != null) {
filters.addTerm("name", request.spanName);
}
for (String annotation : request.annotations) {
Map<String, String> annotationValues = new LinkedHashMap<>();
annotationValues.put("annotations.value", annotation);
Map<String, String> binaryAnnotationKeys = new LinkedHashMap<>();
binaryAnnotationKeys.put("binaryAnnotations.key", annotation);
if (request.serviceName != null) {
annotationValues.put("annotations.endpoint.serviceName", request.serviceName);
binaryAnnotationKeys.put("binaryAnnotations.endpoint.serviceName", request.serviceName);
}
filters.addNestedTerms(annotationValues, binaryAnnotationKeys);
}
for (Map.Entry<String, String> kv : request.binaryAnnotations.entrySet()) {
// In our index template, we make sure the binaryAnnotation value is indexed as string,
// meaning non-string values won't even be indexed at all. This means that we can only
// match string values here, which happens to be exactly what we want.
Map<String, String> nestedTerms = new LinkedHashMap<>();
nestedTerms.put("binaryAnnotations.key", kv.getKey());
nestedTerms.put("binaryAnnotations.value", kv.getValue());
if (request.serviceName != null) {
nestedTerms.put("binaryAnnotations.endpoint.serviceName", request.serviceName);
}
filters.addNestedTerms(nestedTerms);
}
if (request.minDuration != null) {
filters.addRange("duration", request.minDuration, request.maxDuration);
}
// We need to filter to traces that contain at least one span that matches the request,
// but the zipkin API is supposed to order traces by first span, regardless of if it was
// filtered or not. This is not possible without either multiple, heavyweight queries
// or complex multiple indexing, defeating much of the elegance of using elasticsearch for this.
// So we fudge and order on the first span among the filtered spans - in practice, there should
// be no significant difference in user experience since span start times are usually very
// close to each other in human time.
Aggregation traceIdTimestamp = Aggregation.terms("traceId", request.limit)
.addSubAggregation(Aggregation.min("timestamp_millis"))
.orderBy("timestamp_millis", "desc");
List<String> indices = indexNameFormatter.indexNamePatternsForRange(beginMillis, endMillis);
SearchRequest esRequest = SearchRequest.forIndicesAndType(indices, SPAN)
.filters(filters).addAggregation(traceIdTimestamp);
HttpCall<List<String>> traceIdsCall = search.newCall(esRequest, BodyConverters.SORTED_KEYS);
// When we receive span results, we need to group them by trace ID
Callback<List<Span>> successCallback = new Callback<List<Span>>() {
@Override public void onSuccess(List<Span> input) {
List<List<Span>> traces = GroupByTraceId.apply(input, strictTraceId, true);
// Due to tokenization of the trace ID, our matches are imprecise on Span.traceIdHigh
for (Iterator<List<Span>> trace = traces.iterator(); trace.hasNext(); ) {
List<Span> next = trace.next();
if (next.get(0).traceIdHigh != 0 && !request.test(next)) {
trace.remove();
}
}
callback.onSuccess(traces);
}
@Override public void onError(Throwable t) {
callback.onError(t);
}
};
// Fire off the query to get spans once we have trace ids
traceIdsCall.submit(new Callback<List<String>>() {
@Override public void onSuccess(@Nullable List<String> traceIds) {
if (traceIds == null || traceIds.isEmpty()) {
callback.onSuccess(Collections.emptyList());
return;
}
SearchRequest request = SearchRequest.forIndicesAndType(indices, SPAN)
.terms("traceId", traceIds);
search.newCall(request, BodyConverters.SPANS).submit(successCallback);
}
@Override public void onError(Throwable t) {
callback.onError(t);
}
});
}
@Override public void getTrace(long id, Callback<List<Span>> callback) {
getTrace(0L, id, callback);
}
@Override public void getTrace(long traceIdHigh, long traceIdLow, Callback<List<Span>> callback) {
getRawTrace(traceIdHigh, traceIdLow, new Callback<List<Span>>() {
@Override public void onSuccess(@Nullable List<Span> value) {
List<Span> result = CorrectForClockSkew.apply(MergeById.apply(value));
callback.onSuccess(result.isEmpty() ? null : result);
}
@Override public void onError(Throwable t) {
callback.onError(t);
}
});
}
@Override public void getRawTrace(long traceId, Callback<List<Span>> callback) {
getRawTrace(0L, traceId, callback);
}
@Override
public void getRawTrace(long traceIdHigh, long traceIdLow, Callback<List<Span>> callback) {
String traceIdHex = Util.toLowerHex(strictTraceId ? traceIdHigh : 0L, traceIdLow);
SearchRequest request = SearchRequest.forIndicesAndType(asList(allIndices), SPAN)
.term("traceId", traceIdHex);
search.newCall(request, BodyConverters.NULLABLE_SPANS).submit(callback);
}
@Override public void getServiceNames(Callback<List<String>> callback) {
long endMillis = System.currentTimeMillis();
long beginMillis = endMillis - namesLookback;
List<String> indices = indexNameFormatter.indexNamePatternsForRange(beginMillis, endMillis);
SearchRequest request = SearchRequest.forIndicesAndType(indices, SERVICE_SPAN)
.addAggregation(Aggregation.terms("serviceName", Integer.MAX_VALUE));
search.newCall(request, BodyConverters.SORTED_KEYS).submit(new Callback<List<String>>() {
@Override public void onSuccess(List<String> value) {
if (!value.isEmpty()) callback.onSuccess(value);
// Special cased code until sites update their collectors. What this does is do a more
// expensive nested query to get service names when the servicespan type returns nothing.
SearchRequest.Filters filters = new SearchRequest.Filters();
filters.addRange("timestamp_millis", beginMillis, endMillis);
SearchRequest request = SearchRequest.forIndicesAndType(indices, SPAN)
.filters(filters)
.addAggregation(Aggregation.nestedTerms("annotations.endpoint.serviceName"))
.addAggregation(Aggregation.nestedTerms("binaryAnnotations.endpoint.serviceName"));
search.newCall(request, BodyConverters.SORTED_KEYS).submit(callback);
}
@Override public void onError(Throwable t) {
callback.onError(t);
}
});
}
@Override public void getSpanNames(String serviceName, Callback<List<String>> callback) {
if (serviceName == null || "".equals(serviceName)) {
callback.onSuccess(Collections.emptyList());
return;
}
long endMillis = System.currentTimeMillis();
long beginMillis = endMillis - namesLookback;
List<String> indices = indexNameFormatter.indexNamePatternsForRange(beginMillis, endMillis);
SearchRequest request = SearchRequest.forIndicesAndType(indices, SERVICE_SPAN)
.term("serviceName", serviceName.toLowerCase(Locale.ROOT))
.addAggregation(Aggregation.terms("spanName", Integer.MAX_VALUE));
search.newCall(request, BodyConverters.SORTED_KEYS).submit(new Callback<List<String>>() {
@Override public void onSuccess(List<String> value) {
if (!value.isEmpty()) callback.onSuccess(value);
// Special cased code until sites update their collectors. What this does is do a more
// expensive nested query to get span names when the servicespan type returns nothing.
SearchRequest.Filters filters = new SearchRequest.Filters();
filters.addRange("timestamp_millis", beginMillis, endMillis);
filters.addNestedTerms(asList(
"annotations.endpoint.serviceName",
"binaryAnnotations.endpoint.serviceName"
), serviceName.toLowerCase(Locale.ROOT));
SearchRequest request = SearchRequest.forIndicesAndType(indices, SPAN)
.filters(filters)
.addAggregation(Aggregation.terms("name", Integer.MAX_VALUE));
search.newCall(request, BodyConverters.SORTED_KEYS).submit(callback);
}
@Override public void onError(Throwable t) {
callback.onError(t);
}
});
}
@Override public void getDependencies(long endTs, @Nullable Long lookback,
Callback<List<DependencyLink>> callback) {
long beginMillis = lookback != null ? endTs - lookback : 0;
// We just return all dependencies in the days that fall within endTs and lookback as
// dependency links themselves don't have timestamps.
List<String> indices = indexNameFormatter.indexNamePatternsForRange(beginMillis, endTs);
getDependencies(indices, callback);
}
void getDependencies(List<String> indices, Callback<List<DependencyLink>> callback) {
SearchRequest request = SearchRequest.forIndicesAndType(indices, DEPENDENCY_LINK);
search.newCall(request, BodyConverters.DEPENDENCY_LINKS).submit(callback);
}
}