package edu.stanford.nlp.util.logging;
import edu.stanford.nlp.util.logging.Redwood.Record;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Stack;
/**
* Filters repeated messages and replaces them with the number of times they were logged.
*
* @author David McClosky,
* @author Gabor Angeli (angeli at cs.stanford): approximate record equality, repeated tracks squashed
*/
public class RepeatedRecordHandler extends LogRecordHandler {
private final Stack<RepeatedRecordInfo> stack = new Stack<>();
RepeatedRecordInfo current = new RepeatedRecordInfo();
private final RepeatSemantics repeatSemantics;
/**
* Create a new repeated log message handler, using the given semantics for what
* constitutes a repeated record.
* @param repeatSemantics The semantics for what constitutes a repeated record
*/
public RepeatedRecordHandler(RepeatSemantics repeatSemantics){
this.repeatSemantics = repeatSemantics;
}
private void flush(RepeatedRecordInfo info, List<Record> willReturn) {
//(suppress all printing)
if(info.suppressRecord){ return; }
//(get time)
int repeatedRecordCount = info.timesSeen - info.timesPrinted;
if (repeatedRecordCount > 0) {
//(send message record)
//((add force tag))
Object[] newTags = new Object[info.lastRecord.channels().length+1];
System.arraycopy(info.lastRecord.channels(),0,newTags,1,info.lastRecord.channels().length);
newTags[0] = Redwood.FORCE;
//((create record))
Record newRecord = new Record(
repeatSemantics.message(repeatedRecordCount),
newTags,
info.lastRecord.depth,
info.lastRecord.timesstamp);
//((pass record))
willReturn.add(newRecord);
info.timesSeen = 0;
info.timesPrinted = 0;
}
}
private void flushParents(List<Record> willReturn){
Stack<RepeatedRecordInfo> reverseStack = new Stack<>();
while(!stack.isEmpty()){
reverseStack.push(stack.pop());
}
while(!reverseStack.isEmpty()){
RepeatedRecordInfo info = reverseStack.pop();
info.timesSeen -= 1;
flush(info, willReturn);
stack.push(info);
}
}
private boolean recordVerdict(Record r, boolean isRepeat, boolean shouldPrint, List<Record> willReturn){
if(r.force()){
flushParents(willReturn);
if(isRepeat){ flush(current,willReturn); } //if not repeat, will flush below
shouldPrint = true;
}
if(!isRepeat) {
flush(current,willReturn);
current.lastRecord = r;
}
if(shouldPrint){
current.timeOfLastPrintedRecord = r.timesstamp;
current.timesPrinted += 1;
}
current.timesSeen += 1;
current.somethingPrinted = true;
//(return)
return shouldPrint;
}
private boolean internalHandle(Record record, List<Record> willReturn){
// We are passing the record through a number of filters,
// ordered by priority, to determine whether or not
// to continue passing it on
//--Special Cases
//--Regular Cases
//(ckeck squashing)
if(this.current.suppressRecord){
return recordVerdict(record, false, false, willReturn); //arg 2 is irrelevant here
}
//(check first record printed)
if(this.current.lastRecord == null){
return recordVerdict(record,false, true, willReturn);
}
//(check equality)
if(this.repeatSemantics.equals(current.lastRecord,record)){
//(check time)
long currentTime = record.timesstamp;
if(currentTime - this.current.timeOfLastPrintedRecord > this.repeatSemantics.maxWaitTimeInMillis()){
return recordVerdict(record, true, true, willReturn);
}
//(check num printed)
if(this.current.timesSeen < this.repeatSemantics.numToForcePrint()){
return recordVerdict(record, true, true, willReturn);
} else {
return recordVerdict(record, true, false, willReturn);
}
} else {
//(different record)
return recordVerdict(record, false, true, willReturn);
}
}
/** {@inheritDoc} */
@Override
public List<Record> handle(Record record) {
List<Record> willReturn = new ArrayList<>();
if(internalHandle(record, willReturn)){
willReturn.add(record);
}
return willReturn;
}
/** {@inheritDoc} */
@Override
public List<Record> signalStartTrack(Record signal) {
//(handle record)
List<Record> willReturn = new ArrayList<>();
boolean isPrinting = internalHandle(signal, willReturn);
//(adjust state for track)
if(!signal.force()){
if(isPrinting){
current.trackCountPending = PendingType.PRINTING;
current.timesPrinted -= 1;
}else{
current.trackCountPending = PendingType.SEEN;
}
current.timesSeen -= 1;
}
//(push stack)
stack.push(current);
current = new RepeatedRecordInfo();
if(!isPrinting){ current.suppressRecord = true; }
return willReturn;
}
/** {@inheritDoc} */
@Override
public List<Record> signalEndTrack(int newDepth, long timeEnded) {
List<Record> willReturn = new ArrayList<>();
//(get state info)
boolean trackWasNonempty = current.somethingPrinted;
//(flush)
flush(current,willReturn);
current = stack.pop();
//(update seen counts)
if(trackWasNonempty){
if(current.trackCountPending == PendingType.PRINTING){
//((track was in fact printed))
current.timesPrinted += 1;
}
if(current.trackCountPending != PendingType.NONE){
//((track was in fact seen))
current.timesSeen += 1;
}
//((track is nonempty))
current.somethingPrinted = true;
}
//(update this track)
current.trackCountPending = PendingType.NONE;
return willReturn;
}
/** {@inheritDoc} */
@Override
public List<Record> signalShutdown(){
List<Record> willReturn = new ArrayList<>();
flush(current,willReturn);
return willReturn;
}
private enum PendingType { NONE, PRINTING, SEEN }
private static class RepeatedRecordInfo {
private Record lastRecord = null;
private int timesSeen = 0;
private int timesPrinted = 0;
private long timeOfLastPrintedRecord = 0L;
private boolean suppressRecord = false;
private boolean somethingPrinted = false;
private PendingType trackCountPending = PendingType.NONE;
}
/**
* Determines the semantics of what constitutes a repeated record
*/
public interface RepeatSemantics {
boolean equals(Record lastRecord, Record newRecord);
long maxWaitTimeInMillis();
int numToForcePrint();
String message(int linesOmitted);
}
/**
* Judges two records to be equal if they come from the same place,
* and begin with the same string, modulo numbers
*/
public static class ApproximateRepeatSemantics implements RepeatSemantics {
private static boolean sameMessage(String last, String current){
String lastNoNumbers = last.replaceAll("[0-9\\.\\-]+","#");
String currentNoNumbers = current.replaceAll("[0-9\\.\\-]+","#");
return lastNoNumbers.startsWith(currentNoNumbers.substring(0, Math.min(7, currentNoNumbers.length())));
}
@Override
public boolean equals(Record lastRecord, Record record) {
return Arrays.equals(record.channels(), lastRecord.channels()) &&
sameMessage(
lastRecord.content == null ? "null" : lastRecord.content.toString(),
record.content == null ? "null" : record.content.toString()
);
}
@Override
public long maxWaitTimeInMillis() {
return 1000;
}
@Override
public int numToForcePrint(){
return 3;
}
@Override
public String message(int linesOmitted){
return "... "+linesOmitted+" similar messages";
}
}
public static final ApproximateRepeatSemantics APPROXIMATE = new ApproximateRepeatSemantics();
/**
* Judges two records to be equal if they are from the same place,
* and have the same message
*/
public static class ExactRepeatSemantics implements RepeatSemantics {
@Override
public boolean equals(Record lastRecord, Record record) {
return Arrays.equals(record.channels(), lastRecord.channels()) &&
( (record.content == null && lastRecord.content == null) ||
(record.content != null && record.content.equals(lastRecord.content)) );
}
@Override
public long maxWaitTimeInMillis() {
return Long.MAX_VALUE;
}
@Override
public int numToForcePrint(){
return 1;
}
@Override
public String message(int linesOmitted){
return "(last message repeated " + linesOmitted + " times)";
}
}
public static final ExactRepeatSemantics EXACT = new ExactRepeatSemantics();
}