/**
* 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;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import zipkin.Annotation;
import zipkin.BinaryAnnotation;
import zipkin.Endpoint;
import zipkin.Span;
import zipkin.internal.ApplyTimestampAndDuration;
import zipkin.internal.Nullable;
import static zipkin.Constants.CORE_ANNOTATIONS;
import static zipkin.internal.Util.UTF_8;
import static zipkin.internal.Util.checkArgument;
/**
* Invoking this request retrieves traces matching the below filters.
*
* <p> Results should be filtered against {@link #endTs}, subject to {@link #limit} and {@link
* #lookback}. For example, if endTs is 10:20 today, limit is 10, and lookback is 7 days, traces
* returned should be those nearest to 10:20 today, not 10:20 a week ago.
*
* <p> Time units of {@link #endTs} and {@link #lookback} are milliseconds as opposed to
* microseconds, the grain of {@link Span#timestamp}. Milliseconds is a more familiar and supported
* granularity for query, index and windowing functions.
*/
public final class QueryRequest {
/**
* When present, corresponds to {@link zipkin.Endpoint#serviceName} and constrains all other
* parameters.
*/
@Nullable
public final String serviceName;
/** When present, only include traces with this {@link zipkin.Span#name} */
@Nullable
public final String spanName;
/**
* Include traces whose {@link zipkin.Span#annotations} include a value in this set, or where
* {@link zipkin.Span#binaryAnnotations} include a String whose key is in this set.
*
* <p> This is an AND condition against the set, as well against {@link #binaryAnnotations}
*/
public final List<String> annotations;
/**
* Include traces whose {@link zipkin.Span#binaryAnnotations} include a String whose key and
* value are an entry in this set.
*
* <p> This is an AND condition against the set, as well against {@link #annotations}
*/
public final Map<String, String> binaryAnnotations;
/**
* Only return traces whose {@link zipkin.Span#duration} is greater than or equal to
* minDuration microseconds.
*/
@Nullable
public final Long minDuration;
/**
* Only return traces whose {@link zipkin.Span#duration} is less than or equal to maxDuration
* microseconds. Only valid with {@link #minDuration}.
*/
@Nullable
public final Long maxDuration;
/**
* Only return traces where all {@link zipkin.Span#timestamp} are at or before this time in
* epoch milliseconds. Defaults to current time.
*/
public final long endTs;
/**
* Only return traces where all {@link zipkin.Span#timestamp} are at or after (endTs -
* lookback) in milliseconds. Defaults to endTs.
*/
public final long lookback;
/** Maximum number of traces to return. Defaults to 10 */
public final int limit;
/**
* Corresponds to query parameter "annotationQuery". Ex. "http.method=GET and error"
*
* @see QueryRequest.Builder#parseAnnotationQuery(String)
*/
@Nullable
public String toAnnotationQuery() {
StringBuilder annotationQuery = new StringBuilder();
for (Iterator<Map.Entry<String, String>> i = binaryAnnotations.entrySet().iterator();
i.hasNext(); ) {
Map.Entry<String, String> next = i.next();
annotationQuery.append(next.getKey()).append('=').append(next.getValue());
if (i.hasNext() || !annotations.isEmpty()) annotationQuery.append(" and ");
}
for (Iterator<String> i = annotations.iterator(); i.hasNext(); ) {
annotationQuery.append(i.next());
if (i.hasNext()) annotationQuery.append(" and ");
}
return annotationQuery.length() > 0 ? annotationQuery.toString() : null;
}
QueryRequest(
String serviceName,
String spanName,
List<String> annotations,
Map<String, String> binaryAnnotations,
Long minDuration,
Long maxDuration,
long endTs,
long lookback,
int limit) {
checkArgument(serviceName == null || !serviceName.isEmpty(), "serviceName was empty");
checkArgument(spanName == null || !spanName.isEmpty(), "spanName was empty");
checkArgument(endTs > 0, "endTs should be positive, in epoch microseconds: was %d", endTs);
checkArgument(limit > 0, "limit should be positive: was %d", limit);
this.serviceName = serviceName != null? serviceName.toLowerCase() : null;
this.spanName = spanName != null ? spanName.toLowerCase() : null;
this.annotations = annotations;
for (String annotation : annotations) {
checkArgument(!annotation.isEmpty(), "annotation was empty");
checkArgument(!CORE_ANNOTATIONS.contains(annotation),
"queries cannot be refined by core annotations: %s", annotation);
}
this.binaryAnnotations = binaryAnnotations;
for (Map.Entry<String, String> entry : binaryAnnotations.entrySet()) {
checkArgument(!entry.getKey().isEmpty(), "binary annotation key was empty");
checkArgument(!entry.getValue().isEmpty(),
"binary annotation value for %s was empty", entry.getKey());
}
if (minDuration != null) {
checkArgument(minDuration > 0, "minDuration must be a positive number of microseconds");
this.minDuration = minDuration;
if (maxDuration != null) {
checkArgument(maxDuration >= minDuration, "maxDuration should be >= minDuration");
this.maxDuration = maxDuration;
} else {
this.maxDuration = null;
}
} else {
checkArgument(maxDuration == null, "maxDuration is only valid with minDuration");
this.minDuration = this.maxDuration = null;
}
this.endTs = endTs;
this.lookback = lookback;
this.limit = limit;
}
public Builder toBuilder() {
return new Builder(this);
}
public static Builder builder() {
return new Builder();
}
public static final class Builder {
private String serviceName;
private String spanName;
private List<String> annotations = new LinkedList<>();
private Map<String, String> binaryAnnotations = new LinkedHashMap<>();
private Long minDuration;
private Long maxDuration;
private Long endTs;
private Long lookback;
private Integer limit;
Builder(){
}
Builder(QueryRequest source) {
this.serviceName = source.serviceName;
this.spanName = source.spanName;
this.annotations = source.annotations;
this.binaryAnnotations = source.binaryAnnotations;
this.minDuration = source.minDuration;
this.maxDuration = source.maxDuration;
this.endTs = source.endTs;
this.lookback = source.lookback;
this.limit = source.limit;
}
/** @see QueryRequest#serviceName */
public Builder serviceName(@Nullable String serviceName) {
this.serviceName = serviceName;
return this;
}
/**
* This ignores the reserved span name "all".
*
* @see QueryRequest#spanName
*/
public Builder spanName(@Nullable String spanName) {
this.spanName = "all".equals(spanName) ? null : spanName;
return this;
}
/**
* Corresponds to query parameter "annotationQuery". Ex. "http.method=GET and error"
*
* @see QueryRequest#toAnnotationQuery()
*/
public Builder parseAnnotationQuery(String annotationQuery) {
if (annotationQuery != null && !annotationQuery.isEmpty()) {
for (String ann : annotationQuery.split(" and ")) {
int idx = ann.indexOf('=');
if (idx == -1) {
addAnnotation(ann);
} else {
String[] keyValue = ann.split("=");
addBinaryAnnotation(ann.substring(0, idx),
keyValue.length < 2 ? "" : ann.substring(idx + 1));
}
}
}
return this;
}
/** @see QueryRequest#annotations */
public Builder addAnnotation(String annotation) {
this.annotations.add(annotation);
return this;
}
/** @see QueryRequest#binaryAnnotations */
public Builder addBinaryAnnotation(String key, String value) {
this.binaryAnnotations.put(key, value);
return this;
}
/** @see QueryRequest#minDuration */
public Builder minDuration(Long minDuration) {
this.minDuration = minDuration;
return this;
}
/** @see QueryRequest#maxDuration */
public Builder maxDuration(Long maxDuration) {
this.maxDuration = maxDuration;
return this;
}
/** @see QueryRequest#endTs */
public Builder endTs(Long endTs) {
this.endTs = endTs;
return this;
}
/** @see QueryRequest#lookback */
public Builder lookback(Long lookback) {
this.lookback = lookback;
return this;
}
/** @see QueryRequest#limit */
public Builder limit(Integer limit) {
this.limit = limit;
return this;
}
public QueryRequest build() {
long selectedEndTs = endTs == null ? System.currentTimeMillis() : endTs;
return new QueryRequest(
serviceName,
spanName,
annotations,
binaryAnnotations,
minDuration,
maxDuration,
selectedEndTs,
Math.min(lookback == null ? selectedEndTs : lookback, selectedEndTs),
limit == null ? 10 : limit);
}
}
@Override
public String toString() {
return "QueryRequest{"
+ "serviceName=" + serviceName + ", "
+ "spanName=" + spanName + ", "
+ "annotations=" + annotations + ", "
+ "binaryAnnotations=" + binaryAnnotations + ", "
+ "minDuration=" + minDuration + ", "
+ "maxDuration=" + maxDuration + ", "
+ "endTs=" + endTs + ", "
+ "lookback=" + lookback + ", "
+ "limit=" + limit
+ "}";
}
@Override
public boolean equals(Object o) {
if (o == this) {
return true;
}
if (o instanceof QueryRequest) {
QueryRequest that = (QueryRequest) o;
return ((this.serviceName == null) ? (that.serviceName == null) : this.serviceName.equals(that.serviceName))
&& ((this.spanName == null) ? (that.spanName == null) : this.spanName.equals(that.spanName))
&& ((this.annotations == null) ? (that.annotations == null) : this.annotations.equals(that.annotations))
&& ((this.binaryAnnotations == null) ? (that.binaryAnnotations == null) : this.binaryAnnotations.equals(that.binaryAnnotations))
&& ((this.minDuration == null) ? (that.minDuration == null) : this.minDuration.equals(that.minDuration))
&& ((this.maxDuration == null) ? (that.maxDuration == null) : this.maxDuration.equals(that.maxDuration))
&& (this.endTs == that.endTs)
&& (this.lookback == that.lookback)
&& (this.limit == that.limit);
}
return false;
}
@Override
public int hashCode() {
int h = 1;
h *= 1000003;
h ^= (serviceName == null) ? 0 : serviceName.hashCode();
h *= 1000003;
h ^= (spanName == null) ? 0 : spanName.hashCode();
h *= 1000003;
h ^= (annotations == null) ? 0 : annotations.hashCode();
h *= 1000003;
h ^= (binaryAnnotations == null) ? 0 : binaryAnnotations.hashCode();
h *= 1000003;
h ^= (minDuration == null) ? 0 : minDuration.hashCode();
h *= 1000003;
h ^= (maxDuration == null) ? 0 : maxDuration.hashCode();
h *= 1000003;
h ^= (endTs >>> 32) ^ endTs;
h *= 1000003;
h ^= (lookback >>> 32) ^ lookback;
h *= 1000003;
h ^= limit;
return h;
}
/** Tests the supplied trace against the current request */
public boolean test(List<Span> spans) {
Long timestamp = ApplyTimestampAndDuration.guessTimestamp(spans.get(0));
if (timestamp == null ||
timestamp < (endTs - lookback) * 1000 ||
timestamp > endTs * 1000) {
return false;
}
Set<String> serviceNames = new LinkedHashSet<>();
boolean testedDuration = minDuration == null && maxDuration == null;
String spanNameToMatch = spanName;
Set<String> annotationsToMatch = new LinkedHashSet<>(annotations);
Map<String, String> binaryAnnotationsToMatch = new LinkedHashMap<>(binaryAnnotations);
Set<String> currentServiceNames = new LinkedHashSet<>();
for (Span span : spans) {
currentServiceNames.clear();
for (Annotation a : span.annotations) {
if (appliesToServiceName(a.endpoint, serviceName)) {
annotationsToMatch.remove(a.value);
}
if (a.endpoint != null) {
serviceNames.add(a.endpoint.serviceName);
currentServiceNames.add(a.endpoint.serviceName);
}
}
for (BinaryAnnotation b : span.binaryAnnotations) {
if (appliesToServiceName(b.endpoint, serviceName) &&
b.type == BinaryAnnotation.Type.STRING &&
new String(b.value, UTF_8).equals(binaryAnnotationsToMatch.get(b.key))) {
binaryAnnotationsToMatch.remove(b.key);
}
if (appliesToServiceName(b.endpoint, serviceName)) {
annotationsToMatch.remove(b.key);
}
if (b.endpoint != null) {
serviceNames.add(b.endpoint.serviceName);
currentServiceNames.add(b.endpoint.serviceName);
}
}
if ((serviceName == null || currentServiceNames.contains(serviceName))
&& !testedDuration) {
if (minDuration != null && maxDuration != null) {
testedDuration =
span.duration >= minDuration && span.duration <= maxDuration;
} else if (minDuration != null) {
testedDuration = span.duration >= minDuration;
}
}
if (span.name.equals(spanNameToMatch)) {
spanNameToMatch = null;
}
}
return (serviceName == null || serviceNames.contains(serviceName))
&& spanNameToMatch == null
&& annotationsToMatch.isEmpty()
&& binaryAnnotationsToMatch.isEmpty()
&& testedDuration;
}
private static boolean appliesToServiceName(Endpoint endpoint, String serviceName) {
return serviceName == null || endpoint == null || endpoint.serviceName.equals(serviceName);
}
}