com.github.triceo.splitlog.DefaultLogWatch Maven / Gradle / Ivy
package com.github.triceo.splitlog;
import java.io.File;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import org.apache.commons.collections4.BidiMap;
import org.apache.commons.collections4.bidimap.DualHashBidiMap;
import org.apache.commons.lang3.tuple.ImmutablePair;
import org.apache.commons.lang3.tuple.Pair;
import org.slf4j.Logger;
import com.github.triceo.splitlog.api.Follower;
import com.github.triceo.splitlog.api.LogWatch;
import com.github.triceo.splitlog.api.LogWatchBuilder;
import com.github.triceo.splitlog.api.Message;
import com.github.triceo.splitlog.api.MessageConsumer;
import com.github.triceo.splitlog.api.MessageDeliveryStatus;
import com.github.triceo.splitlog.api.MessageListener;
import com.github.triceo.splitlog.api.MessageMeasure;
import com.github.triceo.splitlog.api.MessageMetric;
import com.github.triceo.splitlog.api.MidDeliveryMessageCondition;
import com.github.triceo.splitlog.api.SimpleMessageCondition;
import com.github.triceo.splitlog.api.TailSplitter;
import com.github.triceo.splitlog.logging.SplitlogLoggerFactory;
/**
* Default log watch implementation which provides all the bells and whistles so
* that the rest of the tool can work together.
*
* The tailer thread will only be started after {@link #startFollowing()} or
* {@link #startConsuming(MessageListener)} is called. Subsequently, it will
* only be stopped after there no more running {@link Follower}s or
* {@link MessageConsumer}s.
*/
final class DefaultLogWatch implements LogWatch {
private static final AtomicLong ID_GENERATOR = new AtomicLong(0);
private static final Logger LOGGER = SplitlogLoggerFactory.getLogger(DefaultLogWatch.class);
private final ConsumerManager consumers = new ConsumerManager(this);
private MessageBuilder currentlyProcessedMessage;
private final SimpleMessageCondition gateCondition;
private final BidiMap> handingDown = new DualHashBidiMap>();
private boolean isStarted = false;
private boolean isStopped = false;
private WeakReference previousAcceptedMessage;
private final TailSplitter splitter;
private final LogWatchStorageManager storage;
private final LogWatchSweepingManager sweeping;
private final LogWatchTailingManager tailing;
private final long uniqueId = DefaultLogWatch.ID_GENERATOR.getAndIncrement();
private final File watchedFile;
protected DefaultLogWatch(final LogWatchBuilder builder, final TailSplitter splitter) {
this.splitter = splitter;
this.gateCondition = builder.getGateCondition();
this.storage = new LogWatchStorageManager(this, builder);
this.watchedFile = builder.getFileToWatch();
this.tailing = new LogWatchTailingManager(this, builder);
this.sweeping = new LogWatchSweepingManager(this.storage, builder.getDelayBetweenSweeps());
}
void addLine(final String line) {
if (this.isTerminated()) {
DefaultLogWatch.LOGGER.debug("Line '{}' ignored due to termination in {}.", line, this);
return;
}
final boolean isMessageBeingProcessed = this.currentlyProcessedMessage != null;
if (this.splitter.isStartingLine(line)) {
// new message begins
if (isMessageBeingProcessed) { // finish old message
DefaultLogWatch.LOGGER.debug("Existing message will be finished.");
final Message completeMessage = this.currentlyProcessedMessage.buildFinal(this.splitter);
final MessageDeliveryStatus accepted = this.handleCompleteMessage(completeMessage);
if (accepted == null) {
DefaultLogWatch.LOGGER.info("Message {} rejected at the gate to {}.", completeMessage, this);
} else if (accepted == MessageDeliveryStatus.ACCEPTED) {
this.previousAcceptedMessage = new WeakReference(completeMessage);
} else {
DefaultLogWatch.LOGGER.info("Message {} rejected from storage in {}.", completeMessage, this);
}
}
// prepare for new message
DefaultLogWatch.LOGGER.debug("New message is being prepared.");
this.currentlyProcessedMessage = new MessageBuilder(line);
if (this.previousAcceptedMessage != null) {
this.currentlyProcessedMessage.setPreviousMessage(this.previousAcceptedMessage.get());
}
} else {
// continue present message
if (!isMessageBeingProcessed) {
DefaultLogWatch.LOGGER.debug("Disregarding line as trash.");
// most likely just a garbage immediately after start
return;
}
DefaultLogWatch.LOGGER.debug("Existing message is being updated.");
this.currentlyProcessedMessage.add(line);
}
this.handleIncomingMessage(this.currentlyProcessedMessage.buildIntermediate(this.splitter));
DefaultLogWatch.LOGGER.debug("Line processing over.");
}
@Override
public int countConsumers() {
return this.consumers.countConsumers();
}
/**
* This is not part of the public API. Purely for purposes
* of testing the automated message sweep.
*
* @return How many messages there currently are in the internal message
* store.
*/
int countMessagesInStorage() {
return this.storage.getMessageStore().size();
}
@Override
public int countMetrics() {
return this.consumers.countMetrics();
}
/**
* Return all messages that have been sent to a given {@link Follower}, from
* its {@link #startFollowing()} until either its
* {@link #stopFollowing(Follower)} or to this moment, whichever is
* relevant.
*
* @param follower
* The follower in question.
* @return Unmodifiable list of all the received messages, in the order
* received.
*/
protected List getAllMessages(final Follower follower) {
return this.storage.getAllMessages(follower);
}
@Override
public MessageMetric extends Number, LogWatch> getMetric(final String id) {
return this.consumers.getMetric(id);
}
@Override
public String getMetricId(final MessageMetric extends Number, LogWatch> measure) {
return this.consumers.getMetricId(measure);
}
public long getUniqueId() {
return this.uniqueId;
}
@Override
public File getWatchedFile() {
return this.watchedFile;
}
/**
* Notify {@link MessageConsumer}s of a message that is either
* {@link MessageDeliveryStatus#ACCEPTED} or
* {@link MessageDeliveryStatus#REJECTED}.
*
* @param message
* The message in question.
* @return Null if stopped at the gate by
* {@link LogWatchBuilder#getGateCondition()},
* {@link MessageDeliveryStatus#ACCEPTED} if accepted in
* {@link LogWatchBuilder#getStorageCondition()},
* {@link MessageDeliveryStatus#REJECTED} otherwise.
*/
private MessageDeliveryStatus handleCompleteMessage(final Message message) {
if (!this.hasToLetMessageThroughTheGate(message)) {
return null;
}
final boolean messageAccepted = this.storage.registerMessage(message, this);
final MessageDeliveryStatus status = messageAccepted ? MessageDeliveryStatus.ACCEPTED
: MessageDeliveryStatus.REJECTED;
this.consumers.messageReceived(message, status, this);
this.currentlyProcessedMessage = null;
return status;
}
/**
* Notify {@link MessageConsumer}s of a message that is
* {@link MessageDeliveryStatus#INCOMING}.
*
* @param message
* The message in question.
* @return True if the message was passed to {@link MessageConsumer}s, false
* if stopped at the gate by
* {@link LogWatchBuilder#getGateCondition()}.
*/
private boolean handleIncomingMessage(final Message message) {
if (!this.hasToLetMessageThroughTheGate(message)) {
return false;
}
this.consumers.messageReceived(message, MessageDeliveryStatus.INCOMING, this);
return true;
}
/**
* Notify {@link Follower} of a message that could not be delivered fully as
* the Follower terminated. Will not notify local consumers.
*
* @param follower
* The follower that was terminated.
* @param message
* The message in question.
* @return True if the message was passed to the {@link Follower}, false if
* stopped at the gate by {@link LogWatchBuilder#getGateCondition()}
* .
*/
private boolean handleUndeliveredMessage(final Follower follower, final Message message) {
if (!this.hasToLetMessageThroughTheGate(message)) {
return false;
}
// FIXME should inform other consumers? or just metrics on LogWatch?
follower.messageReceived(message, MessageDeliveryStatus.INCOMPLETE, this);
return true;
}
private boolean hasToLetMessageThroughTheGate(final Message message) {
if (this.gateCondition.accept(message)) {
return true;
} else {
DefaultLogWatch.LOGGER.info("Message '{}' stopped at the gate in {}.", message, this);
return false;
}
}
@Override
public boolean isConsuming(final MessageConsumer consumer) {
return this.consumers.isConsuming(consumer);
}
@Override
public boolean isFollowedBy(final Follower follower) {
return this.isConsuming(follower);
}
@Override
public boolean isHandingDown(final MessageMeasure extends Number, Follower> measure) {
return this.handingDown.containsValue(measure);
}
@Override
public boolean isHandingDown(final String id) {
return this.handingDown.containsKey(id);
}
@Override
public boolean isMeasuring(final MessageMetric extends Number, LogWatch> metric) {
return this.consumers.isMeasuring(metric);
}
@Override
public boolean isMeasuring(final String id) {
return this.consumers.isMeasuring(id);
}
@Override
public boolean isStarted() {
return this.isStarted;
}
@Override
public boolean isStopped() {
return this.isStopped;
}
@Override
public boolean isTerminated() {
return this.isStopped();
}
@Override
public synchronized boolean start() {
if (this.isStarted()) {
return false;
}
this.isStarted = true;
this.tailing.start();
this.sweeping.start();
return true;
}
@Override
public synchronized MessageConsumer startConsuming(final MessageListener consumer) {
return this.consumers.startConsuming(consumer);
}
@Override
public Follower startFollowing() {
return this.startFollowingActually(null).getKey();
}
@Override
public Pair startFollowing(final MidDeliveryMessageCondition waitFor) {
final Pair> pair = this.startFollowingActually(waitFor);
final Follower f = pair.getKey();
try {
return ImmutablePair.of(f, pair.getValue().get());
} catch (final Exception e) {
return ImmutablePair.of(f, null);
}
}
@Override
public Pair startFollowing(final MidDeliveryMessageCondition waitFor,
final long howLong, final TimeUnit unit) {
final Pair> pair = this.startFollowingActually(waitFor);
final Follower f = pair.getKey();
try {
return ImmutablePair.of(f, pair.getValue().get(howLong, unit));
} catch (final Exception e) {
return ImmutablePair.of(f, null);
}
}
private synchronized Pair> startFollowingActually(
final MidDeliveryMessageCondition condition) {
if (this.isStopped()) {
throw new IllegalStateException("Cannot start following on an already terminated LogWatch.");
}
// assemble list of consumers to be handing down and then the follower
final List>> pairs = new ArrayList>>();
for (final BidiMap.Entry> entry : this.handingDown
.entrySet()) {
pairs.add(ImmutablePair.> of(entry.getKey(),
entry.getValue()));
}
// register the follower
final Follower follower = new DefaultFollower(this, pairs);
final Future expectation = condition == null ? null : follower.expect(condition);
this.consumers.registerConsumer(follower);
this.storage.followerStarted(follower);
DefaultLogWatch.LOGGER.info("Registered {} for {}.", follower, this);
return ImmutablePair.of(follower, expectation);
}
@Override
public Pair> startFollowingWithExpectation(
final MidDeliveryMessageCondition waitFor) {
final Pair> pair = this.startFollowingActually(waitFor);
return ImmutablePair.of(pair.getKey(), pair.getValue());
}
@Override
public synchronized boolean startHandingDown(final MessageMeasure extends Number, Follower> measure,
final String id) {
if (this.isStopped()) {
throw new IllegalStateException("Log watch already terminated.");
} else if (measure == null) {
throw new IllegalArgumentException("Measure may not be null.");
} else if (id == null) {
throw new IllegalArgumentException("ID may not be null.");
} else if (this.handingDown.containsKey(id) || this.handingDown.containsValue(measure)) {
return false;
}
this.handingDown.put(id, measure);
return true;
}
@Override
public MessageMetric startMeasuring(final MessageMeasure measure,
final String id) {
return this.consumers.startMeasuring(measure, id);
}
/**
* Invoking this method will cause the running
* {@link LogWatchStorageSweeper} to be de-scheduled. Any currently present
* {@link Message}s will only be removed from memory when this watch
* instance is removed from memory.
*/
@Override
public synchronized boolean stop() {
if (!this.isStarted()) {
throw new IllegalStateException("Cannot terminate what was not started.");
} else if (this.isStopped()) {
return false;
}
DefaultLogWatch.LOGGER.info("Terminating {}.", this);
this.isStopped = true;
this.consumers.stop();
this.tailing.stop();
this.handingDown.clear();
this.previousAcceptedMessage = null;
this.sweeping.stop();
DefaultLogWatch.LOGGER.info("Terminated {}.", this);
return true;
}
@Override
public synchronized boolean stopConsuming(final MessageConsumer consumer) {
final boolean result = this.consumers.stopConsuming(consumer);
if (!result) {
return false;
}
if (consumer instanceof Follower) {
this.storage.followerTerminated((Follower) consumer);
}
if (this.countConsumers() < 1) {
this.currentlyProcessedMessage = null;
}
return true;
}
@Override
public synchronized boolean stopFollowing(final Follower follower) {
if (!this.isFollowedBy(follower)) {
return false;
}
if (this.currentlyProcessedMessage != null) {
this.handleUndeliveredMessage(follower, this.currentlyProcessedMessage.buildIntermediate(this.splitter));
}
this.stopConsuming(follower);
DefaultLogWatch.LOGGER.info("Unregistered {} for {}.", follower, this);
return true;
}
@Override
public synchronized boolean stopHandingDown(final MessageMeasure extends Number, Follower> measure) {
return (this.handingDown.removeValue(measure) != null);
}
@Override
public synchronized boolean stopHandingDown(final String id) {
return (this.handingDown.remove(id) != null);
}
@Override
public boolean stopMeasuring(final MessageMetric extends Number, LogWatch> metric) {
return this.consumers.stopMeasuring(metric);
}
@Override
public boolean stopMeasuring(final String id) {
return this.consumers.stopMeasuring(id);
}
@Override
public boolean terminate() {
return this.stop();
}
@Override
public String toString() {
final StringBuilder builder = new StringBuilder();
builder.append("DefaultLogWatch [getUniqueId()=").append(this.getUniqueId()).append(", ");
if (this.getWatchedFile() != null) {
builder.append("getWatchedFile()=").append(this.getWatchedFile()).append(", ");
}
builder.append("isStopped()=").append(this.isStopped()).append("]");
return builder.toString();
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy