/*
* Copyright © 2015-2016 Cask Data, Inc.
*
* 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 co.cask.cdap.logging.read;
import ch.qos.logback.classic.spi.ILoggingEvent;
import co.cask.cdap.common.conf.CConfiguration;
import co.cask.cdap.common.conf.Constants;
import co.cask.cdap.common.logging.LoggingContext;
import co.cask.cdap.logging.LoggingConfiguration;
import co.cask.cdap.logging.appender.kafka.LoggingEventSerializer;
import co.cask.cdap.logging.appender.kafka.StringPartitioner;
import co.cask.cdap.logging.context.LoggingContextHelper;
import co.cask.cdap.logging.filter.AndFilter;
import co.cask.cdap.logging.filter.Filter;
import co.cask.cdap.logging.kafka.KafkaConsumer;
import com.google.common.base.Preconditions;
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableList;
import com.google.inject.Inject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.List;
/**
* Reads log events stored in Kafka.
*/
public class KafkaLogReader implements LogReader {
private static final Logger LOG = LoggerFactory.getLogger(KafkaLogReader.class);
private static final int KAFKA_FETCH_TIMEOUT_MS = 30000;
private final List<LoggingConfiguration.KafkaHost> seedBrokers;
private final String topic;
private final LoggingEventSerializer serializer;
private final StringPartitioner partitioner;
/**
* Creates a Kafka log reader object.
* @param cConf configuration object containing Kafka seed brokers and number of Kafka partitions for log topic.
*/
@Inject
KafkaLogReader(CConfiguration cConf, StringPartitioner partitioner) {
try {
this.seedBrokers = LoggingConfiguration.getKafkaSeedBrokers(
cConf.get(LoggingConfiguration.KAFKA_SEED_BROKERS));
Preconditions.checkArgument(!this.seedBrokers.isEmpty(), "Kafka seed brokers list is empty!");
this.topic = cConf.get(Constants.Logging.KAFKA_TOPIC);
Preconditions.checkArgument(!this.topic.isEmpty(), "Kafka topic is emtpty!");
this.partitioner = partitioner;
this.serializer = new LoggingEventSerializer();
} catch (Exception e) {
throw Throwables.propagate(e);
}
}
@Override
public void getLogNext(LoggingContext loggingContext, ReadRange readRange, int maxEvents,
Filter filter, Callback callback) {
if (readRange.getKafkaOffset() == ReadRange.LATEST.getKafkaOffset()) {
getLogPrev(loggingContext, readRange, maxEvents, filter, callback);
return;
}
int partition = partitioner.partition(loggingContext.getLogPartition(), -1);
LOG.trace("Reading from kafka partiton {}", partition);
callback.init();
KafkaConsumer kafkaConsumer = new KafkaConsumer(seedBrokers, topic, partition, KAFKA_FETCH_TIMEOUT_MS);
try {
// If Kafka offset is not valid, then we might be rolling over from file while reading.
// Try to get the offset corresponding to fromOffset.getTime()
if (readRange.getKafkaOffset() == LogOffset.INVALID_KAFKA_OFFSET) {
readRange = new ReadRange(readRange.getFromMillis(), readRange.getToMillis(),
kafkaConsumer.fetchOffsetBefore(readRange.getFromMillis()));
}
Filter logFilter = new AndFilter(ImmutableList.of(LoggingContextHelper.createFilter(loggingContext),
filter));
long latestOffset = kafkaConsumer.fetchOffsetBefore(KafkaConsumer.LATEST_OFFSET);
long startOffset = readRange.getKafkaOffset() + 1;
LOG.trace("Using startOffset={}, latestOffset={}, readRange={}", startOffset, latestOffset, readRange);
if (startOffset >= latestOffset) {
// At end of events, nothing to return
return;
}
fetchLogEvents(kafkaConsumer, logFilter, startOffset, latestOffset, maxEvents, callback, readRange);
} catch (Throwable e) {
LOG.error("Got exception: ", e);
throw Throwables.propagate(e);
} finally {
try {
kafkaConsumer.close();
} catch (IOException e) {
LOG.error(String.format("Caught exception when closing KafkaConsumer for topic %s, partition %d",
topic, partition), e);
}
}
}
@Override
public void getLogPrev(LoggingContext loggingContext, ReadRange readRange, int maxEvents,
Filter filter, Callback callback) {
if (readRange.getKafkaOffset() == LogOffset.INVALID_KAFKA_OFFSET) {
readRange = new ReadRange(readRange.getFromMillis(), readRange.getToMillis(), ReadRange.LATEST.getKafkaOffset());
}
int partition = partitioner.partition(loggingContext.getLogPartition(), -1);
LOG.trace("Reading from kafka partiton {}", partition);
callback.init();
KafkaConsumer kafkaConsumer = new KafkaConsumer(seedBrokers, topic, partition, KAFKA_FETCH_TIMEOUT_MS);
try {
Filter logFilter = new AndFilter(ImmutableList.of(LoggingContextHelper.createFilter(loggingContext),
filter));
long latestOffset = kafkaConsumer.fetchOffsetBefore(KafkaConsumer.LATEST_OFFSET);
long earliestOffset = kafkaConsumer.fetchOffsetBefore(KafkaConsumer.EARLIEST_OFFSET);
long stopOffset;
long startOffset;
if (readRange.getKafkaOffset() < 0) {
stopOffset = latestOffset;
} else {
stopOffset = readRange.getKafkaOffset();
}
startOffset = stopOffset - maxEvents;
if (startOffset < earliestOffset) {
startOffset = earliestOffset;
}
LOG.trace("Using startOffset={}, latestOffset={}, readRange={}", startOffset, latestOffset, readRange);
if (startOffset >= stopOffset || startOffset >= latestOffset) {
// At end of kafka events, nothing to return
return;
}
// Events between startOffset and stopOffset may not have the required logs we are looking for,
// we'll need to return at least 1 log offset for next getLogPrev call to work.
int fetchCount = 0;
while (fetchCount == 0) {
fetchCount = fetchLogEvents(kafkaConsumer, logFilter, startOffset, stopOffset, maxEvents, callback, readRange);
stopOffset = startOffset;
if (stopOffset <= earliestOffset) {
// Truly no log messages found.
break;
}
startOffset = stopOffset - maxEvents;
if (startOffset < earliestOffset) {
startOffset = earliestOffset;
}
}
} catch (Throwable e) {
LOG.error("Got exception: ", e);
throw Throwables.propagate(e);
} finally {
try {
kafkaConsumer.close();
} catch (IOException e) {
LOG.error(String.format("Caught exception when closing KafkaConsumer for topic %s, partition %d",
topic, partition), e);
}
}
}
@Override
public void getLog(LoggingContext loggingContext, long fromTimeMs, long toTimeMs,
Filter filter, Callback callback) {
throw new UnsupportedOperationException("Getting logs by time is not supported by "
+ KafkaLogReader.class.getSimpleName());
}
private int fetchLogEvents(KafkaConsumer kafkaConsumer, Filter logFilter, long startOffset, long stopOffset,
int maxEvents, Callback callback, ReadRange readRange) {
KafkaCallback kafkaCallback = new KafkaCallback(logFilter, serializer, stopOffset, maxEvents, callback,
readRange.getFromMillis());
while (kafkaCallback.getCount() < maxEvents && startOffset < stopOffset) {
kafkaConsumer.fetchMessages(startOffset, kafkaCallback);
LogOffset lastOffset = kafkaCallback.getLastOffset();
LogOffset firstOffset = kafkaCallback.getFirstOffset();
// No more Kafka messages
if (lastOffset == null) {
break;
}
// If out of range, break
if (firstOffset.getTime() < readRange.getFromMillis() || lastOffset.getTime() > readRange.getToMillis()) {
break;
}
startOffset = kafkaCallback.getLastOffset().getKafkaOffset() + 1;
}
return kafkaCallback.getCount();
}
private static class KafkaCallback implements co.cask.cdap.logging.kafka.Callback {
private final Filter logFilter;
private final LoggingEventSerializer serializer;
private final long stopOffset;
private final int maxEvents;
private final Callback callback;
private final long fromTimeMs;
private LogOffset firstOffset;
private LogOffset lastOffset;
private int count = 0;
private KafkaCallback(Filter logFilter, LoggingEventSerializer serializer, long stopOffset, int maxEvents,
Callback callback, long fromTimeMs) {
this.logFilter = logFilter;
this.serializer = serializer;
this.stopOffset = stopOffset;
this.maxEvents = maxEvents;
this.callback = callback;
this.fromTimeMs = fromTimeMs;
}
@Override
public void handle(long offset, ByteBuffer msgBuffer) {
ILoggingEvent event = serializer.fromBytes(msgBuffer);
LogOffset logOffset = new LogOffset(offset, event.getTimeStamp());
if (offset < stopOffset && count < maxEvents && logFilter.match(event) && event.getTimeStamp() > fromTimeMs) {
++count;
callback.handle(new LogEvent(event, logOffset));
}
if (firstOffset == null) {
firstOffset = logOffset;
}
lastOffset = logOffset;
}
public LogOffset getFirstOffset() {
return firstOffset;
}
public LogOffset getLastOffset() {
return lastOffset;
}
public int getCount() {
return count;
}
}
}