/** * 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.cassandra; import com.datastax.driver.core.BoundStatement; import com.datastax.driver.core.PreparedStatement; import com.datastax.driver.core.RegularStatement; import com.datastax.driver.core.Session; import com.datastax.driver.core.querybuilder.Insert; import com.datastax.driver.core.querybuilder.QueryBuilder; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Function; import com.google.common.base.Functions; import com.google.common.cache.CacheBuilderSpec; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import java.nio.ByteBuffer; import java.util.List; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import zipkin.Codec; import zipkin.Span; import zipkin.internal.Nullable; import zipkin.internal.Pair; import zipkin.storage.guava.GuavaSpanConsumer; import static com.google.common.util.concurrent.Futures.transform; import static zipkin.internal.ApplyTimestampAndDuration.guessTimestamp; import static zipkin.storage.cassandra.CassandraUtil.bindWithName; final class CassandraSpanConsumer implements GuavaSpanConsumer { private static final Logger LOG = LoggerFactory.getLogger(CassandraSpanConsumer.class); private static final long WRITTEN_NAMES_TTL = Long.getLong("zipkin.store.cassandra.internal.writtenNamesTtl", 60 * 60 * 1000); private static final Function<Object, Void> TO_VOID = Functions.<Void>constant(null); private final Session session; private final TimestampCodec timestampCodec; @Deprecated private final int spanTtl; @Deprecated private final Integer indexTtl; private final PreparedStatement insertSpan; private final PreparedStatement insertServiceName; private final PreparedStatement insertSpanName; private final Schema.Metadata metadata; private final DeduplicatingExecutor deduplicatingExecutor; private final CompositeIndexer indexer; CassandraSpanConsumer(Session session, int bucketCount, int spanTtl, int indexTtl, @Nullable CacheBuilderSpec indexCacheSpec) { this.session = session; this.timestampCodec = new TimestampCodec(session); this.spanTtl = spanTtl; this.metadata = Schema.readMetadata(session); this.indexTtl = metadata.hasDefaultTtl ? null : indexTtl; insertSpan = session.prepare( maybeUseTtl(QueryBuilder .insertInto("traces") .value("trace_id", QueryBuilder.bindMarker("trace_id")) .value("ts", QueryBuilder.bindMarker("ts")) .value("span_name", QueryBuilder.bindMarker("span_name")) .value("span", QueryBuilder.bindMarker("span")))); insertServiceName = session.prepare( maybeUseTtl(QueryBuilder .insertInto(Tables.SERVICE_NAMES) .value("service_name", QueryBuilder.bindMarker("service_name")))); insertSpanName = session.prepare( maybeUseTtl(QueryBuilder .insertInto(Tables.SPAN_NAMES) .value("service_name", QueryBuilder.bindMarker("service_name")) .value("bucket", 0) // bucket is deprecated on this index .value("span_name", QueryBuilder.bindMarker("span_name")))); deduplicatingExecutor = new DeduplicatingExecutor(session, WRITTEN_NAMES_TTL); indexer = new CompositeIndexer(session, indexCacheSpec, bucketCount, this.indexTtl); } private RegularStatement maybeUseTtl(Insert value) { return indexTtl == null ? value : value.using(QueryBuilder.ttl(QueryBuilder.bindMarker("ttl_"))); } /** * This fans out into many requests, last count was 8 * 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(); ImmutableList.Builder<Span> spans = ImmutableList.builder(); for (Span span : rawSpans) { // indexing occurs by timestamp, so derive one if not present. Long timestamp = guessTimestamp(span); spans.add(span); futures.add(storeSpan( span.traceId, timestamp != null ? timestamp : 0L, String.format("%s%d_%d_%d", span.traceIdHigh == 0 ? "" : span.traceIdHigh + "_", span.id, span.annotations.hashCode(), span.binaryAnnotations.hashCode()), // store the raw span without any adjustments ByteBuffer.wrap(Codec.THRIFT.writeSpan(span)))); for (String serviceName : span.serviceNames()) { // SpanStore.getServiceNames futures.add(storeServiceName(serviceName)); if (!span.name.isEmpty()) { // SpanStore.getSpanNames futures.add(storeSpanName(serviceName, span.name)); } } } futures.addAll(indexer.index(spans.build())); return transform(Futures.allAsList(futures.build()), TO_VOID); } /** * Store the span in the underlying storage for later retrieval. */ ListenableFuture<?> storeSpan(long traceId, long timestamp, String key, ByteBuffer span) { try { // If we couldn't guess the timestamp, that probably means that there was a missing timestamp. if (0 == timestamp && metadata.compactionClass.contains("DateTieredCompactionStrategy")) { LOG.warn("Span {} in trace {} had no timestamp. " + "If this happens a lot consider switching back to SizeTieredCompactionStrategy for " + "{}.traces", key, traceId, session.getLoggedKeyspace()); } BoundStatement bound = bindWithName(insertSpan, "insert-span") .setLong("trace_id", traceId) .setBytesUnsafe("ts", timestampCodec.serialize(timestamp)) .setString("span_name", key) .setBytes("span", span); if (!metadata.hasDefaultTtl) bound.setInt("ttl_", spanTtl); return session.executeAsync(bound); } catch (RuntimeException ex) { return Futures.immediateFailedFuture(ex); } } ListenableFuture<?> storeServiceName(final String serviceName) { BoundStatement bound = bindWithName(insertServiceName, "insert-service-name") .setString("service_name", serviceName); if (indexTtl != null) bound.setInt("ttl_", indexTtl); return deduplicatingExecutor.maybeExecuteAsync(bound, serviceName); } ListenableFuture<?> storeSpanName(String serviceName, String spanName) { BoundStatement bound = bindWithName(insertSpanName, "insert-span-name") .setString("service_name", serviceName) .setString("span_name", spanName); if (indexTtl != null) bound.setInt("ttl_", indexTtl); return deduplicatingExecutor.maybeExecuteAsync(bound, Pair.create(serviceName, spanName)); } /** Clears any caches */ @VisibleForTesting void clear() { indexer.clear(); deduplicatingExecutor.clear(); } }