/**
* 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.cassandra3;
import com.datastax.driver.core.BoundStatement;
import com.datastax.driver.core.PreparedStatement;
import com.datastax.driver.core.Session;
import com.datastax.driver.core.querybuilder.QueryBuilder;
import com.datastax.driver.core.utils.UUIDs;
import com.google.common.base.Function;
import com.google.common.base.Functions;
import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableSet;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import zipkin.Annotation;
import zipkin.BinaryAnnotation;
import zipkin.Span;
import zipkin.storage.cassandra3.Schema.AnnotationUDT;
import zipkin.storage.cassandra3.Schema.BinaryAnnotationUDT;
import zipkin.storage.cassandra3.Schema.TraceIdUDT;
import zipkin.storage.guava.GuavaSpanConsumer;
import static com.google.common.util.concurrent.Futures.transform;
import static zipkin.internal.ApplyTimestampAndDuration.guessTimestamp;
import static zipkin.storage.cassandra3.CassandraUtil.bindWithName;
import static zipkin.storage.cassandra3.CassandraUtil.durationIndexBucket;
final class CassandraSpanConsumer implements GuavaSpanConsumer {
private static final Logger LOG = LoggerFactory.getLogger(CassandraSpanConsumer.class);
private static final Function<Object, Void> TO_VOID = Functions.<Void>constant(null);
private final Session session;
private final boolean strictTraceId;
private final PreparedStatement insertSpan;
private final PreparedStatement insertTraceServiceSpanName;
private final PreparedStatement insertServiceSpanName;
private final Schema.Metadata metadata;
CassandraSpanConsumer(Session session, boolean strictTraceId) {
this.session = session;
this.strictTraceId = strictTraceId;
this.metadata = Schema.readMetadata(session);
insertSpan = session.prepare(
QueryBuilder
.insertInto(Schema.TABLE_TRACES)
.value("trace_id", QueryBuilder.bindMarker("trace_id"))
.value("ts_uuid", QueryBuilder.bindMarker("ts_uuid"))
.value("id", QueryBuilder.bindMarker("id"))
.value("ts", QueryBuilder.bindMarker("ts"))
.value("span_name", QueryBuilder.bindMarker("span_name"))
.value("parent_id", QueryBuilder.bindMarker("parent_id"))
.value("duration", QueryBuilder.bindMarker("duration"))
.value("annotations", QueryBuilder.bindMarker("annotations"))
.value("binary_annotations", QueryBuilder.bindMarker("binary_annotations"))
.value("all_annotations", QueryBuilder.bindMarker("all_annotations")));
insertTraceServiceSpanName = session.prepare(
QueryBuilder
.insertInto(Schema.TABLE_TRACE_BY_SERVICE_SPAN)
.value("service_name", QueryBuilder.bindMarker("service_name"))
.value("span_name", QueryBuilder.bindMarker("span_name"))
.value("bucket", QueryBuilder.bindMarker("bucket"))
.value("ts", QueryBuilder.bindMarker("ts"))
.value("trace_id", QueryBuilder.bindMarker("trace_id"))
.value("duration", QueryBuilder.bindMarker("duration")));
insertServiceSpanName = session.prepare(
QueryBuilder
.insertInto(Schema.TABLE_SERVICE_SPANS)
.value("service_name", QueryBuilder.bindMarker("service_name"))
.value("span_name", QueryBuilder.bindMarker("span_name")));
}
/**
* This fans out into many requests, last count was 2 * spans.size. If any of these fail, the
* returned future will fail. Most callers drop or log the result.
*/
@Override
public ListenableFuture<Void> accept(List<Span> rawSpans) {
ImmutableSet.Builder<ListenableFuture<?>> futures = ImmutableSet.builder();
for (Span span : rawSpans) {
// indexing occurs by timestamp, so derive one if not present.
Long timestamp = guessTimestamp(span);
TraceIdUDT traceId = new TraceIdUDT(span.traceIdHigh, span.traceId);
futures.add(storeSpan(span, traceId, timestamp));
for (String serviceName : span.serviceNames()) {
if (timestamp == null) continue; // QueryRequest.min/maxDuration needs timestamp
// Contract for Repository.storeTraceServiceSpanName is to store the span twice, once with
// the span name and another with empty string.
futures.add(storeTraceServiceSpanName(serviceName, span.name, timestamp, span.duration, traceId));
if (!span.name.isEmpty()) { // If span.name == "", this would be redundant
futures.add(storeTraceServiceSpanName(serviceName, "", timestamp, span.duration, traceId));
}
futures.add(storeServiceSpanName(serviceName, span.name));
}
}
return transform(Futures.allAsList(futures.build()), TO_VOID);
}
/**
* Store the span in the underlying storage for later retrieval.
*/
ListenableFuture<?> storeSpan(Span span, TraceIdUDT traceId, Long timestamp) {
try {
if ((null == timestamp || 0 == timestamp)
&& metadata.compactionClass.contains("TimeWindowCompactionStrategy")) {
LOG.warn("Span {} in trace {} had no timestamp. "
+ "If this happens a lot consider switching back to SizeTieredCompactionStrategy for "
+ "{}.traces", span.id, span.traceId, session.getLoggedKeyspace());
}
List<AnnotationUDT> annotations = new ArrayList<>(span.annotations.size());
for (Annotation annotation : span.annotations) {
annotations.add(new AnnotationUDT(annotation));
}
List<BinaryAnnotationUDT> binaryAnnotations = new ArrayList<>(span.binaryAnnotations.size());
for (BinaryAnnotation annotation : span.binaryAnnotations) {
binaryAnnotations.add(new BinaryAnnotationUDT(annotation));
}
Set<String> annotationKeys = CassandraUtil.annotationKeys(span);
if (!strictTraceId && traceId.getHigh() != 0L) {
storeSpan(span, new TraceIdUDT(0L, traceId.getLow()), timestamp);
}
BoundStatement bound = bindWithName(insertSpan, "insert-span")
.set("trace_id", traceId, TraceIdUDT.class)
.setUUID("ts_uuid", new UUID(
UUIDs.startOf(null != timestamp ? (timestamp / 1000) : 0).getMostSignificantBits(),
UUIDs.random().getLeastSignificantBits()))
.setLong("id", span.id)
.setString("span_name", span.name)
.setList("annotations", annotations)
.setList("binary_annotations", binaryAnnotations)
.setString("all_annotations", Joiner.on(',').join(annotationKeys));
if (null != span.timestamp) {
bound = bound.setLong("ts", span.timestamp);
}
if (null != span.duration) {
bound = bound.setLong("duration", span.duration);
}
if (null != span.parentId) {
bound = bound.setLong("parent_id", span.parentId);
}
return session.executeAsync(bound);
} catch (RuntimeException ex) {
return Futures.immediateFailedFuture(ex);
}
}
ListenableFuture<?> storeTraceServiceSpanName(
String serviceName,
String spanName,
long timestamp_micro,
Long duration,
TraceIdUDT traceId) {
int bucket = durationIndexBucket(timestamp_micro);
UUID ts = new UUID(
UUIDs.startOf(timestamp_micro / 1000).getMostSignificantBits(),
UUIDs.random().getLeastSignificantBits());
try {
BoundStatement bound =
bindWithName(insertTraceServiceSpanName, "insert-trace-service-span-name")
.setString("service_name", serviceName)
.setString("span_name", spanName)
.setInt("bucket", bucket)
.setUUID("ts", ts)
.set("trace_id", traceId, TraceIdUDT.class);
if (null != duration) {
bound = bound.setLong("duration", duration);
}
return session.executeAsync(bound);
} catch (RuntimeException ex) {
return Futures.immediateFailedFuture(ex);
}
}
ListenableFuture<?> storeServiceSpanName(
String serviceName,
String spanName
) {
try {
BoundStatement bound = bindWithName(insertServiceSpanName, "insert-service-span-name")
.setString("service_name", serviceName)
.setString("span_name", spanName);
return session.executeAsync(bound);
} catch (RuntimeException ex) {
return Futures.immediateFailedFuture(ex);
}
}
}