/*
* Copyright 2013, The Sporting Exchange Limited
*
* 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 com.betfair.testing.utils.cougar.manager;
import org.apache.commons.io.input.Tailer;
import org.apache.commons.io.input.TailerListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.IOException;
import java.sql.Timestamp;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
public abstract class LogTailer<T extends LogTailer.LogRequirement> implements TailerListener {
private final Logger logger = LoggerFactory.getLogger(getClass());
public static final String DATE_FIELD = "_DATE_FIELD";
private final AtomicLong idSource = new AtomicLong();
private static final long DELAY = 100;
private static final long BLOCK_TIME = DELAY*10;
private Tailer tailer;
private BlockingQueue<LogLine> inputQueue = new LinkedBlockingDeque<LogLine>();
private CountDownLatch startupLatch = new CountDownLatch(1);
protected LogTailer(File toRead, long timeForFileToBeCreated) throws IOException {
long requiredTime = System.currentTimeMillis() + timeForFileToBeCreated;
while ((System.currentTimeMillis() < requiredTime) && !(toRead.exists() && toRead.canRead())) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
// ignore
}
}
if (!toRead.exists() || !toRead.canRead()) {
throw new IllegalStateException("Couldn't read "+toRead.getCanonicalPath()+" in the configured timeout");
}
logger.debug("Initialising Tailer for "+toRead.getCanonicalPath());
tailer = new Tailer(toRead, this, DELAY, false);
}
public void awaitStart() throws InterruptedException {
Thread t = new Thread(tailer, getClass().getSimpleName());
t.setDaemon(true);
t.start();
startupLatch.await();
}
@Override
public void init(Tailer tailer) {
startupLatch.countDown();
// logger.debug(System.currentTimeMillis()+": Started!");
}
@Override
public void fileNotFound() {
// should never happen
}
@Override
public void fileRotated() {
// logger.debug(getClass().getSimpleName()+": Following file rotation");
}
@Override
public void handle(String s) {
logger.debug(System.currentTimeMillis()+": Line received: "+s);
try {
Map<String, String> fields = getFieldsForLine(s);
if (fields == null) {
logger.error(System.currentTimeMillis()+": Parsing error on line: "+s);
}
else {
Timestamp datetime = toDate(fields.get(DATE_FIELD));
LogLine line = new LogLine(datetime, fields);
inputQueue.add(line);
}
} catch (ParseException e) {
logger.error("",e);
}
logger.debug(System.currentTimeMillis()+": End of handle");
}
@Override
public void handle(Exception e) {
// todo: are we interested??
e.printStackTrace();
}
public void lookForNoLogLines(Timestamp fromDate, long timeoutMs, T[] matchers) {
lookForLogLines(fromDate, timeoutMs, new ArrayList<T>(), true, matchers);
}
public void lookForLogLines(Timestamp fromDate, long timeoutMs, T... requirements) {
lookForLogLines(fromDate, timeoutMs, new ArrayList<T>(Arrays.asList(requirements)), requirements.length == 0, null);
}
private void lookForLogLines(Timestamp fromDate, long timeoutMs, List<T> remainingRequirements, boolean expectingNoLines, T[] matchers) {
// right, we look through the queue, testing each line to see if it matches the first requirement
// each time we match a requirement, we discard it (in order)
LogLine line;
// allow a little time for the first caller to catchup on the log..
while ((line = blockingPoll(inputQueue, BLOCK_TIME)) != null) {
// make sure we only consider lines after fromDate
if (line.getDatetime().before(fromDate)) {
continue;
}
if (matches(line, remainingRequirements.get(0))) {
remainingRequirements.remove(0);
}
// once we run out of requirements we exit cleanly
if (remainingRequirements.isEmpty() && !expectingNoLines) {
// logger.debug(System.currentTimeMillis()+": Found all lines we were looking for!");
return;
}
}
// if we run out of queued lines or we're not expecting any lines, then we start waiting for input to come into the queue
long endTime = System.currentTimeMillis() + timeoutMs;
long remainingTime = timeoutMs;
// as each line comes in we decrement the timeout remaining
do {
if (line != null) {
if (expectingNoLines) {
if (matchers == null || matchers.length == 0) {
throw new IllegalStateException(new Date()+"."+System.currentTimeMillis()%1000+" Found a log line when I was expecting none: "+line);
}
else {
boolean gotAMatch = false;
for (T matcher : matchers) {
if (matches(line, matcher)) {
gotAMatch = true;
}
}
if (gotAMatch) {
throw new IllegalStateException(new Date()+"."+System.currentTimeMillis()%1000+" Found a log line when I was expecting none: "+line);
}
}
}
// make sure we only consider lines after fromDate
if (line.getDatetime().before(fromDate)) {
continue;
}
if (matches(line, remainingRequirements.get(0))) {
remainingRequirements.remove(0);
}
// once we run out of requirements we exit cleanly
if (remainingRequirements.isEmpty()) {
// logger.debug(System.currentTimeMillis()+": Found all lines we were looking for!");
return;
}
}
line = blockingPoll(inputQueue, remainingTime);
} while ((remainingTime = endTime - System.currentTimeMillis()) > 0);
if (!expectingNoLines) {
// if we run out of time then we fail..
throw new IllegalStateException(new Date()+"."+System.currentTimeMillis()%1000+" Failed to find all log lines in time, remaining: "+remainingRequirements);
}
}
protected <T> T blockingPoll(BlockingQueue<T> queue, long ms) {
try {
return queue.poll(ms,TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
// ignore
}
return null;
}
protected abstract Map<String, String> getFieldsForLine(String s);
protected abstract Timestamp toDate(String dateFieldValue) throws ParseException;
protected abstract boolean matches(LogLine line, T requirement);
protected static interface LogRequirement {
}
protected class LogLine implements Comparable<LogLine> {
private long id = idSource.incrementAndGet();
private Timestamp datetime;
private Map<String, String> fields;
public LogLine(Timestamp datetime, Map<String, String> fields) {
this.datetime = datetime;
this.fields = fields;
}
public Timestamp getDatetime() {
return datetime;
}
public Map<String, String> getFields() {
return fields;
}
@Override
public int compareTo(LogLine o) {
long diff = id - o.id;
if (diff == 0) { return 0; };
return diff > 0 ? 1 : -1;
}
}
}