/**
* 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.collector.kafka10;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
import org.apache.kafka.clients.consumer.Consumer;
import org.apache.kafka.clients.consumer.ConsumerRebalanceListener;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.apache.kafka.clients.consumer.ConsumerRecords;
import org.apache.kafka.clients.consumer.KafkaConsumer;
import org.apache.kafka.common.TopicPartition;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import zipkin.Codec;
import zipkin.collector.Collector;
import zipkin.collector.CollectorMetrics;
import static zipkin.storage.Callback.NOOP;
/** Consumes spans from Kafka messages, ignoring malformed input */
final class KafkaCollectorWorker implements Runnable {
private static final Logger LOG = LoggerFactory.getLogger(KafkaCollectorWorker.class);
final Consumer<byte[], byte[]> kafkaConsumer;
final Collector collector;
final CollectorMetrics metrics;
/** Kafka topic partitions currently assigned to this worker. List is not modifiable. */
final AtomicReference<List<TopicPartition>> assignedPartitions =
new AtomicReference<>(Collections.emptyList());
KafkaCollectorWorker(KafkaCollector.Builder builder) {
kafkaConsumer = new KafkaConsumer<>(builder.properties);
kafkaConsumer.subscribe(Collections.singleton(builder.topic), new ConsumerRebalanceListener() {
@Override public void onPartitionsRevoked(Collection<TopicPartition> partitions) {
assignedPartitions.set(Collections.emptyList());
}
@Override public void onPartitionsAssigned(Collection<TopicPartition> partitions) {
assignedPartitions.set(Collections.unmodifiableList(new ArrayList<>(partitions)));
}
});
this.collector = builder.delegate.build();
this.metrics = builder.metrics;
}
@Override
public void run() {
try {
LOG.info("Kafka consumer starting polling loop.");
while (true) {
final ConsumerRecords<byte[], byte[]> consumerRecords = kafkaConsumer.poll(1000);
LOG.debug("Kafka polling returned batch of {} messages.", consumerRecords.count());
for (ConsumerRecord<byte[], byte[]> record : consumerRecords) {
metrics.incrementMessages();
final byte[] bytes = record.value();
if (bytes.length == 0) {
metrics.incrementMessagesDropped();
} else {
// In TBinaryProtocol encoding, the first byte is the TType, in a range 0-16
// .. If the first byte isn't in that range, it isn't a thrift.
//
// When byte(0) == '[' (91), assume it is a list of json-encoded spans
//
// When byte(0) <= 16, assume it is a TBinaryProtocol-encoded thrift
// .. When serializing a Span (Struct), the first byte will be the type of a field
// .. When serializing a List[ThriftSpan], the first byte is the member type, TType.STRUCT(12)
// .. As ThriftSpan has no STRUCT fields: so, if the first byte is TType.STRUCT(12), it is a list.
if (bytes[0] == '[') {
collector.acceptSpans(bytes, Codec.JSON, NOOP);
} else {
if (bytes[0] == 12 /* TType.STRUCT */) {
collector.acceptSpans(bytes, Codec.THRIFT, NOOP);
} else {
collector.acceptSpans(Collections.singletonList(bytes), Codec.THRIFT, NOOP);
}
}
}
}
}
} finally {
LOG.info("Kafka consumer polling loop stopped.");
LOG.info("Closing Kafka consumer...");
kafkaConsumer.close();
LOG.info("Kafka consumer closed.");
}
}
}